char-slop/berry

multipurpose self-modifiable agentic harness

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

Charlotte Somdeslop: prose pass over ai-generated ui text10b0cd2

main
7.3 KiB224 linesraw
1import type { ToolView, View, ViewContext } from "@berry/api/web.ts";
2import * as j from "@char/justin";
3
4const snapshot = j.obj({
5  tab: j.string,
6  url: j.string,
7  title: j.string,
8  text: j.string,
9  elements: j.array(j.obj({
10    ref: j.string,
11    role: j.string,
12    name: j.string,
13    value: j.optional(j.string),
14    disabled: j.optional(j.boolean),
15    checked: j.optional(j.boolean),
16    options: j.optional(j.array(j.obj({ label: j.string, value: j.string, selected: j.boolean }))),
17  })),
18  truncated: j.boolean,
19  frames: j.array(j.obj({ url: j.string, title: j.string })),
20  diagnostics: j.array(j.string),
21});
22const resultOf = j.validation.compile(j.obj({
23  snapshot: j.optional(j.union(snapshot, j.null)),
24  tabs: j.optional(j.array(j.obj({ tab: j.string, url: j.string, title: j.string }))),
25}));
26const dataOf = j.validation.compile(j.obj({
27  active: j.boolean,
28  tabs: j.array(snapshot),
29  history: j.array(j.obj({ action: j.string, tab: j.string, url: j.string, at: j.number })),
30  screenshots: j.array(
31    j.obj({ tab: j.string, image: j.obj({ type: j.string, data: j.string, mimeType: j.string }) }),
32  ),
33}));
34const argsOf = j.validation.compile(
35  j.obj({ action: j.string, tab: j.optional(j.string), url: j.optional(j.string) }),
36);
37
38export function summary(args: unknown): string | null {
39  const value = argsOf(args).value;
40  return value ? [value.action, value.tab, value.url].filter(Boolean).join(" · ") : null;
41}
42
43function node<K extends keyof HTMLElementTagNameMap>(
44  tag: K,
45  text?: string,
46): HTMLElementTagNameMap[K] {
47  const element = document.createElement(tag);
48  if (text !== undefined) element.textContent = text;
49  return element;
50}
51
52function renderSnapshot(value: j.Infer<typeof snapshot>): HTMLElement {
53  const element = node("section");
54  const link = node("a", value.url);
55  if (/^https?:\/\//.test(value.url)) {
56    link.href = value.url;
57    link.target = "_blank";
58    link.rel = "noopener noreferrer";
59  }
60  const list = node("ul");
61  for (const item of value.elements) {
62    list.append(
63      node(
64        "li",
65        `${item.ref} · ${item.role} ${JSON.stringify(item.name)}${
66          item.value !== undefined ? ` = ${JSON.stringify(item.value)}` : ""
67        }${item.disabled ? " (disabled)" : ""}${
68          item.checked !== undefined ? ` (${item.checked ? "checked" : "unchecked"})` : ""
69        }${
70          item.options
71            ? ` — ${
72              item.options.map((option) =>
73                `${option.label}: ${option.value}${option.selected ? " ✓" : ""}`
74              ).join(", ")
75            }`
76            : ""
77        }`,
78      ),
79    );
80  }
81  element.append(
82    node("h3", `${value.tab} · ${value.title || "Untitled"}`),
83    link,
84    node("h4", "Page text"),
85    node("pre", value.text),
86    node("h4", "Elements"),
87    list,
88  );
89  if (value.truncated) element.append(node("p", "Snapshot truncated."));
90  if (value.frames.length) {
91    element.append(node(
92      "p",
93      `Embedded frames (not inspected): ${
94        value.frames.map((frame) => frame.title || frame.url).join(", ")
95      }`,
96    ));
97  }
98  if (value.diagnostics.length) {
99    element.append(node("h4", "Console & network"), node("pre", value.diagnostics.join("\n")));
100  }
101  return element;
102}
103
104export function browserToolView(): ToolView {
105  const element = node("div");
106  element.className = "tool-body browser-pane";
107  return {
108    element,
109    update(call) {
110      const result = resultOf(call.result).value;
111      element.replaceChildren(
112        ...(result?.snapshot ? [renderSnapshot(result.snapshot)] : [node(
113          "pre",
114          call.error || call.output ||
115            (result
116              ? result.tabs?.map((tab) => `${tab.tab} · ${tab.title || tab.url}`).join("\n") ||
117                "No open tabs."
118              : ""),
119        )]),
120      );
121    },
122  };
123}
124
125export function browserView(): View {
126  const element = node("div");
127  element.className = "browser-pane";
128  const toolbar = node("div");
129  const refresh = node("button", "Refresh");
130  refresh.type = "button";
131  const close = node("button", "Close browser");
132  close.type = "button";
133  close.disabled = true;
134  const status = node("p");
135  status.setAttribute("role", "status");
136  const tabs = node("div");
137  tabs.setAttribute("role", "group");
138  tabs.setAttribute("aria-label", "Browser tabs");
139  const content = node("div");
140  const history = node("ol");
141  toolbar.append(refresh, close, status);
142  element.append(toolbar, tabs, content, node("h3", "Recent actions"), history);
143  let context: ViewContext | undefined;
144  let selected: string | undefined;
145  let controller: AbortController | undefined;
146  let previous: string | undefined;
147  let disposed = false;
148  const load = async () => {
149    if (!context || disposed) return;
150    controller?.abort();
151    const request = controller = new AbortController();
152    try {
153      const data = dataOf(await context.request("view", {}, { signal: request.signal })).value;
154      if (request.signal.aborted || disposed) return;
155      if (!data) throw new Error("Invalid browser view response.");
156      close.disabled = !data.active;
157      status.textContent = data.active
158        ? "[TODO: active browser session help]"
159        : "[TODO: historical browser snapshots help]";
160      if (!data.tabs.some((tab) => tab.tab === selected)) selected = data.tabs.at(-1)?.tab;
161      const draw = () => {
162        const value = data.tabs.find((tab) => tab.tab === selected);
163        const screenshot = data.screenshots.find((entry) => entry.tab === selected)?.image;
164        const children: Node[] = [];
165        if (screenshot?.mimeType === "image/png") {
166          const image = node("img");
167          image.src = `data:image/png;base64,${screenshot.data}`;
168          image.alt = `Latest screenshot of ${value?.title || selected}`;
169          children.push(image);
170        }
171        if (value) children.push(renderSnapshot(value));
172        else children.push(node("p", "open a page to get started"));
173        content.replaceChildren(...children);
174        tabs.replaceChildren(...data.tabs.map((tab) => {
175          const button = node("button", `${tab.tab} · ${tab.title || tab.url}`);
176          button.type = "button";
177          button.setAttribute("aria-pressed", String(tab.tab === selected));
178          button.addEventListener("click", () => {
179            selected = tab.tab;
180            draw();
181          });
182          return button;
183        }));
184      };
185      draw();
186      history.replaceChildren(
187        ...data.history.toReversed().map((item) =>
188          node("li", `${item.tab} ${item.action}${item.url ? ` · ${item.url}` : ""}`)
189        ),
190      );
191    } catch (error) {
192      if (!request.signal.aborted && !disposed) status.textContent = String(error);
193    }
194  };
195  refresh.addEventListener("click", () => void load());
196  close.addEventListener("click", async () => {
197    if (!context) return;
198    close.disabled = true;
199    try {
200      await context.request("close", {});
201    } catch (error) {
202      if (!disposed) status.textContent = String(error);
203      close.disabled = false;
204      return;
205    }
206    await load();
207  });
208  return {
209    element,
210    update(next) {
211      if (context?.tid !== next.tid) selected = undefined;
212      context = next;
213      const key = `${next.tid}:${next.latest}`;
214      if (key !== previous) {
215        previous = key;
216        void load();
217      }
218    },
219    dispose() {
220      disposed = true;
221      controller?.abort();
222    },
223  };
224}