char-slop/berry

multipurpose self-modifiable agentic harness

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

Charlotte SomConfigure code editing as naive, hashline, or both per conversation8744a75

main
5.5 KiB148 linesraw
1import { code, defineRenderers, type ToolCall, type ToolView } from "@berry/api/web.ts";
2import * as j from "@char/justin";
3import type { Event } from "./mod.ts";
4import { bashRenderer, powershellRenderer } from "./bash-view.tsx";
5import "./web.css";
6
7const pathOf = j.validation.compile(j.obj({ path: j.string }));
8const contentOf = j.validation.compile(j.obj({ content: j.string }));
9const offsetOf = j.validation.compile(j.obj({ offset: j.number }));
10const editsOf = j.validation.compile(j.obj({ edits: j.unknown }));
11const modeOf = j.validation.compile(j.obj({ editMode: j.string }));
12
13function diff(): ToolView {
14  const element = <pre class="diff" />;
15  let shown: Pick<ToolCall, "result" | "error" | "details"> | null = null;
16  return {
17    element,
18    update({ result, error, details }) {
19      if (shown?.result === result && shown.error === error && shown.details === details) return;
20      shown = { result, error, details };
21      if (error !== null) return element.replaceChildren(<span class="error">{error}</span>);
22      const naive = modeOf(details).value?.editMode === "naive";
23      // the first line repeats the card's header; hashes are for the model, and people read the change
24      const lines = (typeof result === "string" ? result : "").split("\n").filter((line) =>
25        !/^Applied \d+ edit/.test(line)
26      );
27      element.replaceChildren(
28        ...lines.filter((line, i) => line !== "" || i > 0).map((line) => {
29          if (line.startsWith("@@")) return <span class="hunk">{line}</span>;
30          const marker = line[0] === "+" || line[0] === "-" ? line[0] : " ";
31          const text = naive ? line.slice(1) : line.slice(1).replace(/^[A-Za-z]{4}│/, "");
32          return (
33            <span class={marker === "+" ? "add" : marker === "-" ? "del" : ""}>
34              <i>{marker}</i>
35              {text}
36            </span>
37          );
38        }),
39      );
40    },
41  };
42}
43
44/** A read as the model saw it: the file's lines highlighted, with their anchors as the gutter. */
45function read(): ToolView {
46  const element = <div class="tool-body" />;
47  let shown: Pick<ToolCall, "args" | "result" | "error" | "images" | "details"> | null = null;
48  return {
49    element,
50    update({ args, result, error, images, details }) {
51      if (
52        shown?.args === args && shown.result === result && shown.error === error &&
53        shown.images === images && shown.details === details
54      ) return;
55      shown = { args, result, error, images, details };
56      if (images?.length && error === null && (result === "" || typeof result !== "string")) {
57        return element.replaceChildren();
58      }
59      if (error !== null || typeof result !== "string") {
60        return element.replaceChildren(<pre class={error ? "error" : ""}>{error ?? ""}</pre>);
61      }
62      if (modeOf(details).value?.editMode === "naive") {
63        return element.replaceChildren(<pre>{result}</pre>);
64      }
65      const [body, note] = result.split(/\n\n(?=\[Showing lines)/);
66      const rows = body.split("\n").map((row) => row.match(/^([A-Za-z]{4})│(.*)$/));
67      if (rows.some((row) => row === null)) return element.replaceChildren(<pre>{result}</pre>);
68      const start = offsetOf(args).value?.offset;
69      element.replaceChildren(
70        code(rows.map((row) => row![2]).join("\n"), {
71          path: pathOf(args).value?.path ?? "",
72          gutter: (i) => `${start === undefined ? i + 1 : start + i} ${rows[i]![1]}`,
73        }),
74        ...(note ? [<p class="note">{note}</p>] : []),
75      );
76    },
77  };
78}
79
80function written(): ToolView {
81  const element = <div class="tool-body" />;
82  let shown: { args: unknown; error: string | null; streaming: boolean } | null = null;
83  return {
84    element,
85    update({ args, error, status }) {
86      const streaming = status === "streaming";
87      if (shown?.args === args && shown.error === error && shown.streaming === streaming) return;
88      shown = { args, error, streaming };
89      // highlighted once the model has written all of it
90      const path = streaming ? {} : { path: pathOf(args).value?.path ?? "" };
91      element.replaceChildren(
92        error !== null
93          ? <pre class="error">{error}</pre>
94          : code(contentOf(args).value?.content ?? "", path),
95      );
96    },
97  };
98}
99
100function listing(): ToolView {
101  const text = <pre /> as HTMLPreElement;
102  return {
103    element: <div class="tool-body">{text}</div>,
104    update({ result, error }) {
105      text.className = error ? "error" : "";
106      text.textContent = error ?? (typeof result === "string" ? result : "");
107    },
108  };
109}
110
111export default defineRenderers<Event>({
112  tools: {
113    read: {
114      initial: "closed",
115      summary: (args) => {
116        const offset = offsetOf(args).value?.offset;
117        return `${pathOf(args).value?.path ?? ""}${offset === undefined ? "" : `:${offset}`}`;
118      },
119      body: read,
120    },
121    edit: {
122      initial: "open",
123      summary: (args) => {
124        const edits = editsOf(args).value?.edits;
125        return `${pathOf(args).value?.path ?? ""}${
126          Array.isArray(edits) ? ` · ${edits.length} edit(s)` : ""
127        }`;
128      },
129      body: diff,
130    },
131    write: { summary: (args) => pathOf(args).value?.path ?? null, body: written },
132    list: { summary: (args) => pathOf(args).value?.path ?? ".", body: listing },
133    bash: bashRenderer,
134    powershell: powershellRenderer,
135  },
136  event: (event) =>
137    event.type === "cd"
138      ? (
139        <span>
140          cd → <code>{event.cwd}</code>
141        </span>
142      )
143      : (
144        <span>
145          editing mode → <code>{event.editMode}</code>
146        </span>
147      ),
148});