char-slop/berry

multipurpose self-modifiable agentic harness

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

Charlotte Somvirtualize transcript contentsba4e2f4

main
12.3 KiB436 linesraw
1import { assertEquals } from "@std/assert";
2import type { LiveState } from "@earendil-works/pi-durable";
3import { withDom } from "@berry/util/dom.ts";
4import { scroller } from "./scroller.tsx";
5import { Scope } from "./ui.ts";
6
7Deno.test("only new bottom content follows a reader near the previous bottom", async () => {
8  let resize = () => {};
9  await withDom(async (window) => {
10    const scope = new Scope();
11    try {
12      const content = document.createElement("div");
13      const older = document.createElement("div");
14      const newest = document.createElement("div");
15      content.append(older, newest);
16      const element = scroller(scope, content);
17      const height = 400;
18      let total = 1000;
19      let top = 0;
20      Object.defineProperties(element, {
21        clientHeight: { get: () => height },
22        scrollHeight: { get: () => total },
23        scrollTop: {
24          get: () => top,
25          set: (value: number) => top = Math.max(0, Math.min(value, total - height)),
26        },
27      });
28      resize();
29      assertEquals(top, 600);
30      const flush = () => window.happyDOM.waitUntilComplete();
31
32      total += 200;
33      newest.textContent = "streaming";
34      await flush();
35      assertEquals(top, 800);
36
37      // scrolling can precede the browser's scroll event
38      element.dispatchEvent(new Event("pointerdown"));
39      top = 700;
40      total += 200;
41      newest.firstChild!.textContent = "still streaming";
42      await flush();
43      assertEquals(top, 700);
44      element.dispatchEvent(new Event("scroll"));
45      element.dispatchEvent(new Event("pointerup"));
46
47      top = total - height - 65;
48      element.dispatchEvent(new Event("scroll"));
49      total += 200;
50      newest.append(" more");
51      await flush();
52      assertEquals(top, 935);
53
54      top = total - height - 64;
55      element.dispatchEvent(new Event("scroll"));
56      total += 200;
57      newest.append(" more");
58      await flush();
59      assertEquals(top, 1400);
60
61      // a layout change is not new output, but it moves the bottom a pinned reader follows
62      total += 100;
63      resize();
64      newest.append(" more");
65      await flush();
66      assertEquals(top, 1500);
67
68      top = total - height;
69      total += 200;
70      older.textContent = "an older tool updated";
71      await flush();
72      assertEquals(top, 1500);
73
74      top = total - height;
75      total += 200;
76      content.prepend(document.createElement("div"));
77      await flush();
78      assertEquals(top, 1700);
79
80      top = total - height;
81      total += 200;
82      content.append(document.createElement("div"));
83      await flush();
84      assertEquals(top, 2100);
85
86      scope.dispose();
87      total += 200;
88      content.append(document.createElement("div"));
89      await flush();
90      assertEquals(top, 2100);
91    } finally {
92      scope.dispose();
93    }
94  }, {
95    globals: {
96      ResizeObserver: class {
97        constructor(callback: () => void) {
98          resize = callback;
99        }
100        observe() {}
101        disconnect() {}
102      },
103    },
104  });
105});
106
107Deno.test("a resize keeps a reader at the bottom there, and leaves a reader above in place", async () => {
108  let resize = () => {};
109  await withDom(async (window) => {
110    const scope = new Scope();
111    try {
112      const content = document.createElement("div");
113      content.append(document.createElement("div"));
114      const element = scroller(scope, content);
115      let width = 800;
116      let height = 400;
117      let total = 1000;
118      let top = 0;
119      const clamp = (value: number) => Math.max(0, Math.min(value, total - height));
120      Object.defineProperties(element, {
121        clientWidth: { get: () => width },
122        clientHeight: { get: () => height },
123        scrollHeight: { get: () => total },
124        scrollTop: { get: () => top, set: (value: number) => top = clamp(value) },
125      });
126      const output = async () => {
127        total += 200;
128        content.lastElementChild!.append(" more");
129        await window.happyDOM.waitUntilComplete();
130      };
131      resize();
132      assertEquals(top, 600);
133
134      // the composer grows
135      height = 300;
136      resize();
137      assertEquals(top, 700);
138
139      // narrowing rewraps the transcript taller
140      width = 600;
141      total = 1300;
142      resize();
143      assertEquals(top, 1000);
144      await output();
145      assertEquals(top, 1200);
146
147      // widening clamps the reader, whose scroll event can arrive first
148      width = 800;
149      total = 1100;
150      top = clamp(top);
151      element.dispatchEvent(new Event("scroll"));
152      resize();
153      await output();
154      assertEquals(top, 1000);
155
156      top = 500;
157      element.dispatchEvent(new Event("scroll"));
158      height = 200;
159      resize();
160      assertEquals(top, 500);
161      await output();
162      assertEquals(top, 500);
163    } finally {
164      scope.dispose();
165    }
166  }, {
167    globals: {
168      ResizeObserver: class {
169        constructor(callback: () => void) {
170          resize = callback;
171        }
172        observe() {}
173        disconnect() {}
174      },
175    },
176  });
177});
178
179Deno.test("a pinned reader stays at the bottom through layout changes larger than the follow margin", async () => {
180  let resize = () => {};
181  await withDom(async (window) => {
182    const scope = new Scope();
183    try {
184      const content = document.createElement("div");
185      content.append(document.createElement("div"));
186      const element = scroller(scope, content);
187      let height = 400;
188      let total = 1000;
189      let top = 0;
190      Object.defineProperties(element, {
191        clientHeight: { get: () => height },
192        scrollHeight: { get: () => total },
193        scrollTop: {
194          get: () => top,
195          set: (value: number) => top = Math.max(0, Math.min(value, total - height)),
196        },
197      });
198      resize();
199      assertEquals(top, 600);
200
201      // the existing content grows, as an image or font loading makes it
202      total += 300;
203      content.prepend(document.createElement("div"));
204      await window.happyDOM.waitUntilComplete();
205      element.dispatchEvent(new Event("scroll"));
206      resize();
207      assertEquals(top, 900);
208
209      // a scroll event from the pane shrinking arrives before the resize settles it
210      height = 300;
211      element.dispatchEvent(new Event("scroll"));
212      resize();
213      assertEquals(top, 1000);
214
215      // the composer grows in successive steps, as its textarea gains a row and then its controls stack
216      height = 260;
217      resize();
218      assertEquals(top, 1040);
219      height = 220;
220      resize();
221      assertEquals(top, 1080);
222
223      top = 500;
224      element.dispatchEvent(new Event("scroll"));
225      height = 180;
226      resize();
227      assertEquals(top, 500);
228    } finally {
229      scope.dispose();
230    }
231  }, {
232    globals: {
233      ResizeObserver: class {
234        constructor(callback: () => void) {
235          resize = callback;
236        }
237        observe() {}
238        disconnect() {}
239      },
240    },
241  });
242});
243
244Deno.test("a reader scrolled up can jump back to the newest output, and follows it again", async () => {
245  let resize = () => {};
246  await withDom(async (window) => {
247    const scope = new Scope();
248    try {
249      const content = document.createElement("div");
250      content.append(document.createElement("div"));
251      const element = scroller(scope, content);
252      let total = 1000;
253      let top = 0;
254      Object.defineProperties(element, {
255        clientHeight: { value: 400 },
256        scrollHeight: { get: () => total },
257        scrollTop: {
258          get: () => top,
259          set: (value: number) => top = Math.max(0, Math.min(value, total - 400)),
260        },
261      });
262      const jump = element.querySelector<HTMLButtonElement>(
263        'button[aria-label="Jump to the newest output"]',
264      )!;
265      const shown = () => jump.closest("[hidden]") === null;
266      resize();
267      assertEquals(shown(), false);
268
269      top = 100;
270      element.dispatchEvent(new Event("scroll"));
271      assertEquals(shown(), true);
272
273      jump.click();
274      assertEquals(top, 600);
275      assertEquals(shown(), false);
276      total += 200;
277      content.lastElementChild!.append("more");
278      await window.happyDOM.waitUntilComplete();
279      assertEquals(top, 800);
280    } finally {
281      scope.dispose();
282    }
283  }, {
284    globals: {
285      ResizeObserver: class {
286        constructor(callback: () => void) {
287          resize = callback;
288        }
289        observe() {}
290        disconnect() {}
291      },
292    },
293  });
294});
295
296Deno.test("a transcript waits for visible, scrollable content before its initial scroll", async () => {
297  let resize = () => {};
298  await withDom(async (window) => {
299    const scope = new Scope();
300    try {
301      const content = document.createElement("div");
302      const element = scroller(scope, content);
303      let visible = true;
304      let total = 400;
305      let top = 0;
306      Object.defineProperties(element, {
307        clientHeight: { get: () => visible ? 400 : 0 },
308        scrollHeight: { get: () => visible ? total : 0 },
309        scrollTop: {
310          get: () => top,
311          set: (value: number) => top = visible ? Math.max(0, Math.min(value, total - 400)) : 0,
312        },
313      });
314      resize();
315      content.append(document.createElement("div"));
316      await window.happyDOM.waitUntilComplete();
317      assertEquals(top, 0);
318      visible = false;
319      resize();
320      content.append(document.createElement("div"));
321      total = 1000;
322      await window.happyDOM.waitUntilComplete();
323      resize();
324      assertEquals(top, 0);
325
326      visible = true;
327      resize();
328      assertEquals(top, 600);
329
330      top = 200;
331      element.dispatchEvent(new Event("scroll"));
332      total += 200;
333      resize();
334      assertEquals(top, 200);
335      visible = false;
336      resize();
337      visible = true;
338      resize();
339      assertEquals(top, 200);
340    } finally {
341      scope.dispose();
342    }
343  }, {
344    globals: {
345      ResizeObserver: class {
346        constructor(callback: () => void) {
347          resize = callback;
348        }
349        observe() {}
350        disconnect() {}
351      },
352    },
353  });
354});
355
356Deno.test("a scroller has its content draw more as the reader scrolls, and as it resizes", async () => {
357  let resize = () => {};
358  await withDom(() => {
359    const scope = new Scope();
360    try {
361      const content = document.createElement("div");
362      content.append(document.createElement("div"));
363      const drawn: number[] = [];
364      const element = scroller(scope, content, undefined, (pane) => drawn.push(pane.clientHeight));
365      Object.defineProperties(element, {
366        clientHeight: { value: 400 },
367        scrollHeight: { value: 1000 },
368        scrollTop: { value: 600, writable: true },
369      });
370      resize();
371      assertEquals(drawn, [400]);
372      element.dispatchEvent(new Event("scroll"));
373      assertEquals(drawn, [400, 400]);
374      resize();
375      assertEquals(drawn, [400, 400, 400]);
376    } finally {
377      scope.dispose();
378    }
379  }, {
380    globals: {
381      ResizeObserver: class {
382        constructor(callback: () => void) {
383          resize = callback;
384        }
385        observe() {}
386        disconnect() {}
387      },
388    },
389  });
390});
391
392Deno.test("a transcript follows its last block, not updates to earlier blocks in the same response", async () => {
393  await withDom(async (window) => {
394    const { transcript } = await import("./transcript.tsx");
395    const { fauxAssistantMessage, fauxText } = await import(
396      "@earendil-works/pi-ai/providers/faux"
397    );
398    const scope = new Scope();
399    const history = transcript();
400    scope.add(history.dispose);
401    const element = scroller(scope, history.element, history.last);
402    let total = 1000;
403    let top = 0;
404    Object.defineProperties(element, {
405      clientHeight: { value: 400 },
406      scrollHeight: { get: () => total },
407      scrollTop: {
408        get: () => top,
409        set: (value: number) => top = Math.max(0, Math.min(value, total - 400)),
410      },
411    });
412    const entries: never[] = [];
413    const update = async (first: string, last: string) => {
414      history.update(entries, {
415        generation: {
416          attempt: 0,
417          message: fauxAssistantMessage([fauxText(first), fauxText(last)]),
418        },
419      } as unknown as LiveState);
420      await window.happyDOM.waitUntilComplete();
421    };
422    try {
423      await update("first", "last");
424      assertEquals(top, 600);
425      total += 200;
426      await update("first updated", "last");
427      assertEquals(top, 600);
428      top = 800;
429      total += 200;
430      await update("first updated", "last updated");
431      assertEquals(top, 1000);
432    } finally {
433      scope.dispose();
434    }
435  });
436});