// What an extension's `web.tsx` exports: how the ui shows its tools and its events. import type { Channel, JsonValue } from "./mod.ts"; export type { Channel, ChannelMessage } from "./mod.ts"; import type { ImageContent } from "@earendil-works/pi-ai"; /** Highlighted source, as the rest of the ui shows it, or its lines alone. */ export { code, type CodeOptions, highlight } from "../web/code.tsx"; export type CallStatus = "streaming" | "pending" | "running" | "ok" | "error" | "cancelled"; /** One tool call as the ui knows it, whether the model made it or a script did. Absent parts are `null`. */ export type ToolCall = { readonly name: string; /** Partial while the model is still writing it. */ readonly args: JsonValue | null; readonly status: CallStatus; /** Running output so far. */ readonly output: string | null; readonly details: JsonValue | null; /** What the tool returned: the value for a script's call, the result text for the model's. */ readonly result: JsonValue | null; /** Image output, separate from the result text or value. */ readonly images?: readonly ImageContent[]; readonly error: string | null; /** When it started running, in wall-clock milliseconds, as far as the ui knows. */ readonly startedAt: number | null; /** How long it ran, once it has ended and that is known. */ readonly durationMs: number | null; }; /** A card's body: `update` draws each new state of the call, the first before the body is shown. */ export type ToolView = { readonly element: Element; update(call: ToolCall): void; /** Once the card is gone. */ dispose?(): void; }; export type ToolRenderer = { /** How the card first shows its body: whole, clipped at a height (the default), or not at all. */ readonly initial?: "open" | "preview" | "closed"; /** A line for the card's header, such as the path an edit touches. */ readonly summary?: (args: JsonValue | null) => string | null; /** The timeout in seconds, shown alongside the call's elapsed time. */ readonly timeout?: (args: JsonValue | null) => number | null; /** What a call that succeeded came to, when that is a failure all the same, such as `exit 3`; shown in place of `ok`. */ readonly outcome?: (call: ToolCall) => string | null; /** The card's expanded body. */ readonly body?: () => ToolView; }; /** A header widget's current conversation, its extension's handlers, and user-command access. */ export type HeaderContext = { readonly tid: string; readonly model: { readonly provider: string; readonly modelId: string } | null; /** Ask one of this extension's handlers, in this conversation. */ request(name: string, args?: JsonValue, options?: { signal?: AbortSignal }): Promise; /** Open a channel to this extension, in this conversation. */ connect(name: string, args?: JsonValue): Channel; /** Run a command as the user would. */ command(name: string, args?: string): Promise; }; /** A header widget, updated with the selected model and disposed with its conversation view. */ export type HeaderView = { readonly element: Element; update(context: HeaderContext): void; dispose?(): void; }; export type FieldSchema = { type?: string; title?: string; description?: string; default?: JsonValue; enum?: JsonValue[]; anyOf?: { const?: JsonValue }[]; items?: FieldSchema; }; export type ConfigurationSchema = { properties: Record }; /** The configuration being edited, either in a conversation or before one is created. */ export type ConfigurationContext = { readonly tid: string | null; readonly extension: string; readonly model: HeaderContext["model"]; readonly schema: ConfigurationSchema; /** Null until the conversation's configuration has loaded. */ readonly values: Readonly> | null; readonly saveStatus: "idle" | "saving" | "error"; readonly error: string | null; save(patch: Record): Promise; /** The same generated form on every call, with its saving and validation behavior intact. */ renderDefault(): HTMLElement; /** These capabilities are absent before conversation creation. */ readonly request?: HeaderContext["request"]; readonly command?: HeaderContext["command"]; }; /** Configuration controls, updated with their context and disposed when replaced or unmounted. */ export type ConfigurationView = { readonly element: HTMLElement; update(context: ConfigurationContext): void; dispose?(): void; }; /** What a view is shown with: its conversation, its own place in the address, and user-command access. */ export type ViewContext = HeaderContext & { /** The view's place, after `#///` in the address: empty when it opens. */ readonly route: string; /** The address of another place in the view, for links to it. */ href(route: string): string; /** Move to another place in the view, as a link would, so the browser's back button returns. */ go(route: string): void; /** The conversation's latest entry: a new one, such as a tool's result, may change what the view shows. */ readonly latest: number | null; }; export type ViewDestination = { tid: string | null; extension: string | null; route: string }; /** A view that takes the conversation's main pane while it is open, the transcript moving aside. */ export type View = { readonly element: Element; /** Each new context, the first before the view is shown. */ update(context: ViewContext): void; /** Resolve drafts and report changes before navigation; false keeps the view open. */ beforeLeave?(destination: ViewDestination): boolean | Promise; /** Report held context before an idle message sends, so the agent sees it first. */ flush?(): Promise; dispose?(): void; }; export type Renderers = { /** Mounted in the top right while this extension is selected or always on. */ readonly header?: () => HeaderView; /** Replaces the generated configuration controls in both new and existing conversations. */ readonly configuration?: () => ConfigurationView; /** * Opened from the conversation's bar, which links it by `label`, at `#//`, while this extension is * selected or always on. */ readonly view?: { readonly label: string; readonly create: () => View }; readonly tools?: Readonly>; /** One of the extension's events in the transcript: `null` hides it, `undefined` falls back to the default. */ readonly event?: (event: E) => Element | null | undefined; }; export function defineRenderers( renderers: Renderers, ): Renderers { return renderers; }