142 lines
3.9 KiB
TypeScript
142 lines
3.9 KiB
TypeScript
import type { UIEnvelope, UIEnvelopePayload, UIRegistry, UISurface } from "./types";
|
|
|
|
const surfaces = new Set<UISurface>(["chat_inline", "side_panel", "canvas", "map_overlay"]);
|
|
|
|
export function parseUiEnvelopePayload(value: unknown): UIEnvelopePayload | null {
|
|
if (!isRecord(value)) {
|
|
return null;
|
|
}
|
|
|
|
const envelope = parseUiEnvelope(value.envelope);
|
|
if (!envelope) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
session_id: typeof value.session_id === "string" ? value.session_id : "",
|
|
envelope_id: typeof value.envelope_id === "string" ? value.envelope_id : "",
|
|
created_at: typeof value.created_at === "number" ? value.created_at : Date.now(),
|
|
envelope
|
|
};
|
|
}
|
|
|
|
export function parseUiEnvelope(value: unknown): UIEnvelope | null {
|
|
if (!isRecord(value) || value.schemaVersion !== "agent-ui@1") {
|
|
return null;
|
|
}
|
|
|
|
if (value.kind === "registered_component") {
|
|
if (typeof value.component !== "string" || !isSurface(value.surface)) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
kind: "registered_component",
|
|
schemaVersion: "agent-ui@1",
|
|
component: value.component,
|
|
surface: value.surface,
|
|
props: value.props,
|
|
data: value.data,
|
|
fallbackText: optionalString(value.fallbackText)
|
|
};
|
|
}
|
|
|
|
if (value.kind === "chart") {
|
|
if (value.grammar !== "echarts-safe-subset" || !isChartSurface(value.surface)) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
kind: "chart",
|
|
schemaVersion: "agent-ui@1",
|
|
grammar: "echarts-safe-subset",
|
|
surface: value.surface,
|
|
spec: value.spec,
|
|
data: value.data,
|
|
fallbackText: optionalString(value.fallbackText)
|
|
};
|
|
}
|
|
|
|
if (value.kind === "map_action") {
|
|
if (typeof value.action !== "string" || value.surface !== "map_overlay") {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
kind: "map_action",
|
|
schemaVersion: "agent-ui@1",
|
|
action: value.action,
|
|
surface: "map_overlay",
|
|
params: value.params,
|
|
fallbackText: optionalString(value.fallbackText)
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
export function isUiEnvelopeAllowed(envelope: UIEnvelope, registry: UIRegistry | null) {
|
|
if (!registry) {
|
|
return true;
|
|
}
|
|
|
|
if (envelope.kind === "registered_component") {
|
|
const manifest = registry.components.find((item) => item.id === envelope.component);
|
|
return Boolean(manifest?.supportedSurfaces.includes(envelope.surface));
|
|
}
|
|
|
|
if (envelope.kind === "chart") {
|
|
return registry.chart_grammars.includes(envelope.grammar);
|
|
}
|
|
|
|
const manifest = registry.actions.find((item) => item.id === envelope.action);
|
|
return Boolean(manifest?.supportedSurfaces.includes(envelope.surface));
|
|
}
|
|
|
|
export function parseUiRegistry(value: unknown): UIRegistry | null {
|
|
if (!isRecord(value) || value.schema_version !== "agent-ui-registry@1") {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
schema_version: "agent-ui-registry@1",
|
|
chart_grammars: stringArray(value.chart_grammars),
|
|
components: manifestArray(value.components),
|
|
actions: manifestArray(value.actions)
|
|
};
|
|
}
|
|
|
|
function manifestArray(value: unknown) {
|
|
if (!Array.isArray(value)) {
|
|
return [];
|
|
}
|
|
|
|
return value.flatMap((item) => {
|
|
if (!isRecord(item) || typeof item.id !== "string") {
|
|
return [];
|
|
}
|
|
|
|
return [{ id: item.id, supportedSurfaces: stringArray(item.supportedSurfaces).filter(isSurface) }];
|
|
});
|
|
}
|
|
|
|
function stringArray(value: unknown) {
|
|
return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : [];
|
|
}
|
|
|
|
function isChartSurface(value: unknown): value is "chat_inline" | "side_panel" | "canvas" {
|
|
return value === "chat_inline" || value === "side_panel" || value === "canvas";
|
|
}
|
|
|
|
function isSurface(value: unknown): value is UISurface {
|
|
return typeof value === "string" && surfaces.has(value as UISurface);
|
|
}
|
|
|
|
function optionalString(value: unknown) {
|
|
return typeof value === "string" ? value : undefined;
|
|
}
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
return typeof value === "object" && value !== null;
|
|
}
|