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
2.8 KiB72 linesraw
1import type { View, ViewContext } from "@berry/api/web.ts";
2import { guardNavigation } from "./navigation.ts";
3
4/** An extension's view as a conversation's route opens it: `create` is `null` when it has none to show. */
5export type ViewItem = { name: string; create: (() => View) | null };
6
7/** The address of a place in a conversation's view: each segment of the view's route escaped on its own. */
8export const viewHref = (tid: string, extension: string, route = "") =>
9  `#/${tid}/${extension}${route ? `/${route.split("/").map(encodeURIComponent).join("/")}` : ""}`;
10
11/** A view's route back from its address, after `<tid>/<extension>/`. */
12export function viewRoute(escaped: string) {
13  return escaped.split("/").map((segment) => {
14    try {
15      return decodeURIComponent(segment);
16    } catch {
17      return segment;
18    }
19  }).join("/");
20}
21
22/** The pane of the view a conversation has open, if any; a view is made when it opens and disposed when it closes. */
23export function extensionView() {
24  const element = document.createElement("div");
25  element.className = "pane extension-view";
26  element.hidden = true;
27  let shown: { item: ViewItem; view: View | null; context: ViewContext | null } | null = null;
28  let unguard: (() => void) | null = null;
29  const close = () => {
30    unguard?.();
31    unguard = null;
32    shown?.view?.dispose?.();
33    shown = null;
34  };
35  return {
36    element,
37    update(item: ViewItem | null, context: ViewContext) {
38      if (shown?.item.name !== item?.name || shown?.item.create !== item?.create) {
39        close();
40        element.replaceChildren();
41        if (item !== null) {
42          shown = { item, view: item.create?.() ?? null, context: null };
43          const current = shown;
44          unguard = guardNavigation((destination) =>
45            current.view?.beforeLeave?.(destination) ?? true
46          );
47          if (shown.view === null) {
48            const missing = document.createElement("p");
49            missing.className = "view-missing";
50            missing.textContent = `${item.name} has no view in this conversation`;
51            element.append(missing);
52          }
53        }
54      }
55      element.hidden = shown === null;
56      if (shown?.view && !sameContext(shown.context, context)) {
57        shown.context = context;
58        shown.view.update(context);
59        if (shown.view.element.parentNode !== element) element.replaceChildren(shown.view.element);
60      }
61    },
62    flush() {
63      return shown?.view?.flush?.();
64    },
65    dispose: close,
66  };
67}
68
69// a conversation draws every frame while it streams; its views hear only what changes
70const sameContext = (a: ViewContext | null, b: ViewContext) =>
71  a !== null && a.tid === b.tid && a.route === b.route && a.latest === b.latest &&
72  a.model?.provider === b.model?.provider && a.model?.modelId === b.model?.modelId;