char-slop/berry

multipurpose self-modifiable agentic harness

git clone https://git.t4t.associates/char-slop/berry

Charlotte Somadd flush() to views and use for diff edits481a657

main
6.6 KiB154 linesraw
1// What an extension's `web.tsx` exports: how the ui shows its tools and its events.
2
3import type { Channel, JsonValue } from "./mod.ts";
4
5export type { Channel, ChannelMessage } from "./mod.ts";
6import type { ImageContent } from "@earendil-works/pi-ai";
7
8/** Highlighted source, as the rest of the ui shows it, or its lines alone. */
9export { code, type CodeOptions, highlight } from "../web/code.tsx";
10
11export type CallStatus = "streaming" | "pending" | "running" | "ok" | "error" | "cancelled";
12
13/** One tool call as the ui knows it, whether the model made it or a script did. Absent parts are `null`. */
14export type ToolCall = {
15  readonly name: string;
16  /** Partial while the model is still writing it. */
17  readonly args: JsonValue | null;
18  readonly status: CallStatus;
19  /** Running output so far. */
20  readonly output: string | null;
21  readonly details: JsonValue | null;
22  /** What the tool returned: the value for a script's call, the result text for the model's. */
23  readonly result: JsonValue | null;
24  /** Image output, separate from the result text or value. */
25  readonly images?: readonly ImageContent[];
26  readonly error: string | null;
27  /** When it started running, in wall-clock milliseconds, as far as the ui knows. */
28  readonly startedAt: number | null;
29  /** How long it ran, once it has ended and that is known. */
30  readonly durationMs: number | null;
31};
32
33/** A card's body: `update` draws each new state of the call, the first before the body is shown. */
34export type ToolView = {
35  readonly element: Element;
36  update(call: ToolCall): void;
37  /** Once the card is gone. */
38  dispose?(): void;
39};
40
41export type ToolRenderer = {
42  /** How the card first shows its body: whole, clipped at a height (the default), or not at all. */
43  readonly initial?: "open" | "preview" | "closed";
44  /** A line for the card's header, such as the path an edit touches. */
45  readonly summary?: (args: JsonValue | null) => string | null;
46  /** The timeout in seconds, shown alongside the call's elapsed time. */
47  readonly timeout?: (args: JsonValue | null) => number | null;
48  /** What a call that succeeded came to, when that is a failure all the same, such as `exit 3`; shown in place of `ok`. */
49  readonly outcome?: (call: ToolCall) => string | null;
50  /** The card's expanded body. */
51  readonly body?: () => ToolView;
52};
53
54/** A header widget's current conversation, its extension's handlers, and user-command access. */
55export type HeaderContext = {
56  readonly tid: string;
57  readonly model: { readonly provider: string; readonly modelId: string } | null;
58  /** Ask one of this extension's handlers, in this conversation. */
59  request(name: string, args?: JsonValue, options?: { signal?: AbortSignal }): Promise<JsonValue>;
60  /** Open a channel to this extension, in this conversation. */
61  connect(name: string, args?: JsonValue): Channel;
62  /** Run a command as the user would. */
63  command(name: string, args?: string): Promise<JsonValue | null>;
64};
65
66/** A header widget, updated with the selected model and disposed with its conversation view. */
67export type HeaderView = {
68  readonly element: Element;
69  update(context: HeaderContext): void;
70  dispose?(): void;
71};
72
73export type FieldSchema = {
74  type?: string;
75  title?: string;
76  description?: string;
77  default?: JsonValue;
78  enum?: JsonValue[];
79  anyOf?: { const?: JsonValue }[];
80  items?: FieldSchema;
81};
82export type ConfigurationSchema = { properties: Record<string, FieldSchema> };
83
84/** The configuration being edited, either in a conversation or before one is created. */
85export type ConfigurationContext = {
86  readonly tid: string | null;
87  readonly extension: string;
88  readonly model: HeaderContext["model"];
89  readonly schema: ConfigurationSchema;
90  /** Null until the conversation's configuration has loaded. */
91  readonly values: Readonly<Record<string, JsonValue>> | null;
92  readonly saveStatus: "idle" | "saving" | "error";
93  readonly error: string | null;
94  save(patch: Record<string, JsonValue>): Promise<void>;
95  /** The same generated form on every call, with its saving and validation behavior intact. */
96  renderDefault(): HTMLElement;
97  /** These capabilities are absent before conversation creation. */
98  readonly request?: HeaderContext["request"];
99  readonly command?: HeaderContext["command"];
100};
101
102/** Configuration controls, updated with their context and disposed when replaced or unmounted. */
103export type ConfigurationView = {
104  readonly element: HTMLElement;
105  update(context: ConfigurationContext): void;
106  dispose?(): void;
107};
108
109/** What a view is shown with: its conversation, its own place in the address, and user-command access. */
110export type ViewContext = HeaderContext & {
111  /** The view's place, after `#/<tid>/<extension>/` in the address: empty when it opens. */
112  readonly route: string;
113  /** The address of another place in the view, for links to it. */
114  href(route: string): string;
115  /** Move to another place in the view, as a link would, so the browser's back button returns. */
116  go(route: string): void;
117  /** The conversation's latest entry: a new one, such as a tool's result, may change what the view shows. */
118  readonly latest: number | null;
119};
120
121export type ViewDestination = { tid: string | null; extension: string | null; route: string };
122
123/** A view that takes the conversation's main pane while it is open, the transcript moving aside. */
124export type View = {
125  readonly element: Element;
126  /** Each new context, the first before the view is shown. */
127  update(context: ViewContext): void;
128  /** Resolve drafts and report changes before navigation; false keeps the view open. */
129  beforeLeave?(destination: ViewDestination): boolean | Promise<boolean>;
130  /** Report held context before an idle message sends, so the agent sees it first. */
131  flush?(): Promise<void>;
132  dispose?(): void;
133};
134
135export type Renderers<E extends JsonValue = JsonValue> = {
136  /** Mounted in the top right while this extension is selected or always on. */
137  readonly header?: () => HeaderView;
138  /** Replaces the generated configuration controls in both new and existing conversations. */
139  readonly configuration?: () => ConfigurationView;
140  /**
141   * Opened from the conversation's bar, which links it by `label`, at `#/<tid>/<extension>`, while this extension is
142   * selected or always on.
143   */
144  readonly view?: { readonly label: string; readonly create: () => View };
145  readonly tools?: Readonly<Record<string, ToolRenderer>>;
146  /** One of the extension's events in the transcript: `null` hides it, `undefined` falls back to the default. */
147  readonly event?: (event: E) => Element | null | undefined;
148};
149
150export function defineRenderers<E extends JsonValue = JsonValue>(
151  renderers: Renderers<E>,
152): Renderers<E> {
153  return renderers;
154}