char-slop/berry

multipurpose self-modifiable agentic harness

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

Charlotte Somdiff: use a request instead of a command for saving diffse9d146d

main
4.2 KiB120 linesraw
1import type { JsonValue } from "@berry/api";
2import type { HeaderContext } from "@berry/api/web.ts";
3import type { OpenFile, Save, Saved } from "./edit-types.ts";
4
5export type Draft = { opened: OpenFile; content: string; pending?: Save };
6export type Stored = {
7  id: string;
8  saved: boolean;
9  closed: boolean;
10  drafts: Record<string, Draft>;
11  /** Receipts of this session's saves, carried to the report when the session ends. */
12  saves: Saved[];
13};
14
15const PREFIX = "berry.diff.edits.";
16const recovering = new Map<string, Promise<void>>();
17const reporting = new Map<string, Promise<void>>();
18
19export function editId() {
20  return Array.from(
21    crypto.getRandomValues(new Uint8Array(16)),
22    (byte) => byte.toString(16).padStart(2, "0"),
23  ).join("");
24}
25
26export function draftKey(tid: string) {
27  const storage = document.defaultView!.sessionStorage;
28  let tab = storage.getItem("berry.diff.tab");
29  if (!tab) storage.setItem("berry.diff.tab", tab = editId());
30  return `${PREFIX}${tid}.${tab}`;
31}
32
33export function loadDrafts(key: string): Stored {
34  try {
35    const value = JSON.parse(localStorage.getItem(key) ?? "null");
36    if (value?.id && value.drafts) {
37      return {
38        ...value,
39        drafts: Object.assign(Object.create(null), value.drafts),
40        saves: Array.isArray(value.saves) ? value.saves : [],
41      };
42    }
43  } catch { /* a broken recovery record must not stop reviewing */ }
44  return { id: editId(), saved: false, closed: false, drafts: Object.create(null), saves: [] };
45}
46
47export function storeDrafts(key: string, stored: Stored) {
48  localStorage.setItem(key, JSON.stringify(stored));
49}
50
51/** Report a closed session's saves and retire it; concurrent callers share the one report. */
52function reportStored(key: string, context: HeaderContext): Promise<void> {
53  const running = reporting.get(key);
54  if (running) return running;
55  const result = (async () => {
56    const stored = loadDrafts(key);
57    if (!stored.saved) return;
58    await context.command(
59      "diff-finish",
60      JSON.stringify({ session: stored.id, saves: stored.saves }),
61    );
62    const current = loadDrafts(key);
63    if (Object.keys(current.drafts).length) {
64      storeDrafts(key, { ...current, id: editId(), saved: false, closed: false, saves: [] });
65    } else localStorage.removeItem(key);
66  })();
67  reporting.set(key, result);
68  void result.finally(() => reporting.delete(key)).catch(() => {});
69  return result;
70}
71
72/**
73 * Land a closed tab's unacknowledged saves. Reporting them is only kicked off: it may wait for the agent's
74 * turn, which must not hold the recovering view open; `recoverClosed` awaits it instead.
75 */
76export function recover(key: string, context: HeaderContext): Promise<void> {
77  const running = recovering.get(key);
78  if (running) return running;
79  const result = (async () => {
80    const stored = loadDrafts(key);
81    if (!stored.closed) return;
82    let failed: unknown;
83    for (const draft of Object.values(stored.drafts)) {
84      if (!draft.pending) continue;
85      try {
86        const saved = await context.request(
87          "save",
88          draft.pending as unknown as JsonValue,
89        ) as unknown as Saved;
90        draft.opened.content = draft.pending.content;
91        draft.opened.version = saved.version;
92        draft.opened.working = saved.working;
93        delete draft.pending;
94        if (!stored.saves.some((receipt) => receipt.id === saved.id)) stored.saves.push(saved);
95        stored.saved = true;
96        storeDrafts(key, stored);
97      } catch (error) {
98        failed ??= error;
99      }
100    }
101    // a failed report stays saved in storage, to retry on a later visit
102    if (stored.saved) void reportStored(key, context).catch(() => {});
103    if (failed) {
104      throw new Error("Some saves could not be recovered. Their drafts are kept in the diff view.");
105    }
106  })();
107  recovering.set(key, result);
108  void result.finally(() => recovering.delete(key)).catch(() => {});
109  return result;
110}
111
112export async function recoverClosed(context: HeaderContext) {
113  const prefix = `${PREFIX}${context.tid}.`;
114  const keys = Array.from({ length: localStorage.length }, (_, i) => localStorage.key(i)!)
115    .filter((key) => key.startsWith(prefix));
116  for (const key of keys) {
117    await recover(key, context);
118    if (loadDrafts(key).closed) await reportStored(key, context);
119  }
120}