@poodle64/librarian 2026.10.4 → 2026.10.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +8 -6
  2. package/dist/components/activity-group/activity-group.svelte +99 -17
  3. package/dist/components/activity-group/activity-group.svelte.d.ts +4 -1
  4. package/dist/components/agent-transcript/agent-transcript.svelte +82 -26
  5. package/dist/components/agent-transcript/agent-transcript.svelte.d.ts +4 -3
  6. package/dist/components/artefact-pane/artefact-pane.svelte +278 -78
  7. package/dist/components/conversation/conversation.svelte +6 -6
  8. package/dist/components/conversation/conversation.svelte.d.ts +4 -4
  9. package/dist/components/markdown/markdown.d.ts +27 -10
  10. package/dist/components/markdown/markdown.js +99 -22
  11. package/dist/components/markdown/markdown.svelte +122 -28
  12. package/dist/components/markdown/markdown.svelte.d.ts +12 -4
  13. package/dist/components/source-fold/index.d.ts +2 -0
  14. package/dist/components/source-fold/index.js +2 -0
  15. package/dist/components/source-fold/source-fold.svelte +225 -0
  16. package/dist/components/source-fold/source-fold.svelte.d.ts +17 -0
  17. package/dist/components/source-list/source-list.svelte +5 -3
  18. package/dist/components/source-list/source-list.svelte.d.ts +2 -0
  19. package/dist/components/thinking-row/thinking-row.svelte +37 -16
  20. package/dist/components/thinking-row/thinking-row.svelte.d.ts +2 -0
  21. package/dist/components/tool-row/tool-row.svelte +130 -38
  22. package/dist/components/visual-block/chart-draw.svelte +26 -0
  23. package/dist/components/visual-block/chart-draw.svelte.d.ts +7 -0
  24. package/dist/components/visual-block/chart.svelte +73 -0
  25. package/dist/components/visual-block/chart.svelte.d.ts +8 -0
  26. package/dist/components/visual-block/entity-card.svelte +94 -0
  27. package/dist/components/visual-block/entity-card.svelte.d.ts +7 -0
  28. package/dist/components/visual-block/timeline.svelte +99 -0
  29. package/dist/components/visual-block/timeline.svelte.d.ts +7 -0
  30. package/dist/components/visual-block/visual-block.svelte +53 -0
  31. package/dist/components/visual-block/visual-block.svelte.d.ts +13 -0
  32. package/dist/components/written-card/written-card.svelte +97 -49
  33. package/dist/components/written-card/written-card.svelte.d.ts +1 -1
  34. package/dist/copy.d.ts +54 -0
  35. package/dist/copy.js +44 -1
  36. package/dist/diff.d.ts +8 -0
  37. package/dist/diff.js +37 -0
  38. package/dist/steps.d.ts +28 -0
  39. package/dist/steps.js +168 -0
  40. package/dist/transcript.svelte.d.ts +4 -0
  41. package/dist/transcript.svelte.js +13 -2
  42. package/dist/visuals.d.ts +46 -0
  43. package/dist/visuals.js +84 -0
  44. package/dist/web-sources.d.ts +19 -3
  45. package/dist/web-sources.js +39 -7
  46. package/dist/written.d.ts +26 -1
  47. package/dist/written.js +42 -0
  48. package/package.json +8 -1
  49. package/dist/components/web-list/index.d.ts +0 -2
  50. package/dist/components/web-list/index.js +0 -2
  51. package/dist/components/web-list/web-list.svelte +0 -84
  52. package/dist/components/web-list/web-list.svelte.d.ts +0 -9
@@ -12,6 +12,10 @@
12
12
  only ever one, in the same place, closing whichever was there.
13
13
  -->
14
14
  <script lang="ts">
15
+ import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
16
+ import CopyIcon from '@lucide/svelte/icons/copy';
17
+ import CheckIcon from '@lucide/svelte/icons/check';
18
+ import DownloadIcon from '@lucide/svelte/icons/download';
15
19
  import XIcon from '@lucide/svelte/icons/x';
16
20
  import { untrack } from 'svelte';
17
21
  import { segment, type TextBlock, type Turn } from '../../transcript.svelte';
@@ -21,6 +25,8 @@
21
25
  downloads,
22
26
  framed,
23
27
  isHtml,
28
+ kindLabel,
29
+ stripFrontMatter,
24
30
  writtenKey,
25
31
  type LoadWritten,
26
32
  type Written,
@@ -65,7 +71,11 @@
65
71
  // The newest, until the reader picks another.
66
72
  const current = $derived(versions.find((v) => v.version === version) ?? versions.at(-1));
67
73
  const currentKey = $derived(current ? writtenKey(current) : '');
74
+ // The one before it, whose blocks this one is marked against.
75
+ const before = $derived(current ? versions.filter((v) => v.version < current.version).at(-1) : undefined);
76
+ const beforeKey = $derived(before ? writtenKey(before) : '');
68
77
  let source = $state<{ text: string; mediaType: string } | null>(null);
78
+ let earlier = $state<string | null>(null);
69
79
  let failed = $state(false);
70
80
 
71
81
  // Keyed on the version alone: a new version of the same name arriving in
@@ -85,6 +95,21 @@
85
95
  return () => (live = false);
86
96
  });
87
97
 
98
+ $effect(() => {
99
+ earlier = null;
100
+ if (!beforeKey) return;
101
+ const prior = untrack(() => before);
102
+ const load = untrack(() => loadWritten);
103
+ if (!prior || !load || isHtml(prior.media_type)) return;
104
+ let live = true;
105
+ // A version that cannot be read is no reason to mark nothing else.
106
+ load(prior.name, prior.version).then(
107
+ (read) => live && (earlier = stripFrontMatter(read.text)),
108
+ () => {}
109
+ );
110
+ return () => (live = false);
111
+ });
112
+
88
113
  const texts = $derived(
89
114
  segment(turn?.blocks ?? []).filter((s): s is TextBlock => s.kind === 'text')
90
115
  );
@@ -92,6 +117,37 @@
92
117
  const split = $derived(splitSources(rawAnswer));
93
118
  const sources = $derived(resolveCitations(turn?.citations ?? [], split.citations));
94
119
 
120
+ const links = $derived(current && writtenUrl ? downloads(current, words) : []);
121
+ // The one download the bar offers directly: Word where there is one, else the source.
122
+ const primary = $derived(links.find((link) => link.format === 'docx') ?? links[0]);
123
+ const sub = $derived.by(() => {
124
+ if (!current) return '';
125
+ const fresh = current.at !== undefined && Date.now() - current.at < 120_000;
126
+ const when = fresh
127
+ ? fill(current.version > 1 ? words.paneEdited : words.paneWritten, { when: words.justNow })
128
+ : '';
129
+ return [kindLabel(current, words), when].filter(Boolean).join(' · ');
130
+ });
131
+ const changed = $derived(
132
+ current?.changes && before
133
+ ? fill(words.changedSince, { n: before.version, changes: current.changes })
134
+ : ''
135
+ );
136
+
137
+ let menu = $state(false);
138
+ let copied = $state(false);
139
+
140
+ async function copyDocument() {
141
+ if (!source) return;
142
+ try {
143
+ await navigator.clipboard.writeText(stripFrontMatter(source.text));
144
+ copied = true;
145
+ setTimeout(() => (copied = false), 1600);
146
+ } catch {
147
+ // A denied clipboard is not worth an error state on text the reader can select.
148
+ }
149
+ }
150
+
95
151
  let closeButton = $state<HTMLButtonElement | null>(null);
96
152
 
97
153
  // Opening a pane a reader reached with the keyboard must move focus into
@@ -103,74 +159,113 @@
103
159
  });
104
160
 
105
161
  function keydown(event: KeyboardEvent) {
106
- if (event.key === 'Escape') {
107
- event.stopPropagation();
108
- onclose();
109
- }
162
+ if (event.key !== 'Escape') return;
163
+ event.stopPropagation();
164
+ if (menu) menu = false;
165
+ else onclose();
110
166
  }
111
167
  </script>
112
168
 
113
169
  <svelte:window onkeydown={keydown} />
114
170
 
115
- <aside aria-label={current?.name ?? 'Study artefact'} class="ds-lib-pane">
171
+ <aside aria-label={current?.title ?? 'Study artefact'} class="ds-lib-pane">
116
172
  <!-- The sheet's grabber. On a phone this is an overlay a reader has to be
117
173
  able to see the top edge of; on a desktop it is a column, so there is
118
174
  nothing here to grab. -->
119
175
  <div class="ds-lib-pane-grabber" aria-hidden="true"><span></span></div>
120
176
 
121
177
  <header class="ds-lib-pane-header">
178
+ <button
179
+ bind:this={closeButton}
180
+ type="button"
181
+ class="ds-lib-pane-close"
182
+ onclick={onclose}
183
+ aria-label={words.closeSource}
184
+ >
185
+ <XIcon size={16} />
186
+ </button>
122
187
  <div class="ds-lib-pane-heading">
123
188
  {#if current}
124
- <h2 class="ds-lib-pane-title">{current.name}</h2>
125
- <div class="ds-lib-pane-tools">
126
- <select
127
- class="ds-lib-pane-versions"
128
- aria-label={words.writtenVersions}
129
- value={current.version}
130
- onchange={(event) => onversion?.(Number(event.currentTarget.value))}
189
+ <h2 class="ds-lib-pane-title">{current.title}</h2>
190
+ {#if sub}<p class="ds-lib-pane-subtitle">{sub}</p>{/if}
191
+ {:else}
192
+ <h2 class="ds-lib-pane-title">{turn?.artefact?.title}</h2>
193
+ {#if turn?.artefact?.summary}
194
+ <p class="ds-lib-pane-subtitle">{turn.artefact.summary}</p>
195
+ {/if}
196
+ {/if}
197
+ </div>
198
+ {#if current}
199
+ <select
200
+ class="ds-lib-pane-versions"
201
+ aria-label={words.writtenVersions}
202
+ value={current.version}
203
+ onchange={(event) => onversion?.(Number(event.currentTarget.value))}
204
+ >
205
+ {#each versions as option (option.version)}
206
+ <option value={option.version}>{fill(words.writtenVersion, { n: option.version })}</option>
207
+ {/each}
208
+ </select>
209
+ {#if source && !failed}
210
+ <button
211
+ type="button"
212
+ class="ds-lib-pane-tool ds-lib-pane-copy"
213
+ onclick={copyDocument}
214
+ aria-label={copied ? words.copiedDocument : words.copyDocument}
215
+ >
216
+ {#if copied}<CheckIcon size={15} />{:else}<CopyIcon size={15} />{/if}
217
+ </button>
218
+ {/if}
219
+ {#if writtenUrl && links.length > 0}
220
+ <div class="ds-lib-pane-menu">
221
+ <button
222
+ type="button"
223
+ class="ds-lib-pane-tool"
224
+ aria-haspopup="menu"
225
+ aria-expanded={menu}
226
+ onclick={() => (menu = !menu)}
131
227
  >
132
- {#each versions as option (option.version)}
133
- <option value={option.version}>{fill(words.writtenVersion, { n: option.version })}</option>
134
- {/each}
135
- </select>
136
- {#if writtenUrl}
137
- <ul class="ds-lib-pane-links" aria-label={words.download}>
138
- {#each downloads(current, words) as link (link.format)}
139
- <li>
140
- <a href={writtenUrl(current.name, current.version, link.format)}>{link.label}</a>
228
+ <DownloadIcon size={15} />{words.download}<ChevronDownIcon size={14} />
229
+ </button>
230
+ {#if menu}
231
+ <ul class="ds-lib-pane-links" role="menu" aria-label={words.download}>
232
+ {#each links as link (link.format)}
233
+ <li role="none">
234
+ <a
235
+ role="menuitem"
236
+ href={writtenUrl(current.name, current.version, link.format)}
237
+ onclick={() => (menu = false)}>{link.label}</a
238
+ >
141
239
  </li>
142
240
  {/each}
143
241
  </ul>
144
242
  {/if}
145
243
  </div>
146
- {:else}
147
- <h2 class="ds-lib-pane-title">{turn?.artefact?.title}</h2>
148
- {#if turn?.artefact?.summary}
149
- <p class="ds-lib-pane-subtitle">{turn.artefact.summary}</p>
244
+ {#if primary}
245
+ <a
246
+ class="ds-lib-pane-tool ds-lib-pane-quick"
247
+ href={writtenUrl(current.name, current.version, primary.format)}
248
+ >
249
+ <DownloadIcon size={15} />{primary.label}
250
+ </a>
150
251
  {/if}
151
252
  {/if}
152
- </div>
153
- <button
154
- bind:this={closeButton}
155
- type="button"
156
- class="ds-lib-pane-close"
157
- onclick={onclose}
158
- aria-label={words.closeSource}
159
- >
160
- <XIcon size={16} />
161
- </button>
253
+ {/if}
162
254
  </header>
163
255
 
164
256
  {#if current}
257
+ {#if changed}<p class="ds-lib-pane-changed">{changed}</p>{/if}
165
258
  {@const html = source !== null && isHtml(source.mediaType || current.media_type)}
166
259
  <div class="ds-lib-pane-body" class:is-frame={html}>
167
260
  {#if failed}
168
- <p class="ds-lib-pane-subtitle">{words.documentUnavailable}</p>
261
+ <p class="ds-lib-pane-note">{words.documentUnavailable}</p>
169
262
  {:else if source && html}
170
- <iframe class="ds-lib-pane-frame" sandbox="" srcdoc={framed(source.text)} title={current.name}
263
+ <iframe class="ds-lib-pane-frame" sandbox="" srcdoc={framed(source.text)} title={current.title}
171
264
  ></iframe>
172
265
  {:else if source}
173
- <Markdown content={source.text} />
266
+ <div class="ds-lib-pane-paper">
267
+ <Markdown content={stripFrontMatter(source.text)} previous={earlier ?? undefined} />
268
+ </div>
174
269
  {/if}
175
270
  </div>
176
271
  {:else}
@@ -189,17 +284,12 @@
189
284
  <style>
190
285
  .ds-lib-pane {
191
286
  position: fixed;
192
- inset-inline: 0;
193
- bottom: 0;
287
+ inset: 0;
194
288
  z-index: 40;
195
289
  display: flex;
196
- height: 80svh;
290
+ height: 100dvh;
197
291
  flex-direction: column;
198
- border-top: 1px solid var(--ds-color-border);
199
- border-start-start-radius: var(--ds-radius-xl);
200
- border-start-end-radius: var(--ds-radius-xl);
201
292
  background: var(--ds-color-surface-1);
202
- box-shadow: 0 -12px 40px rgb(0 0 0 / 0.25);
203
293
  }
204
294
 
205
295
  .ds-lib-pane-grabber {
@@ -216,17 +306,19 @@
216
306
  background: var(--ds-color-border);
217
307
  }
218
308
 
309
+ /* A phone's sheet leads with Close; the column ends with it. */
219
310
  .ds-lib-pane-header {
220
311
  display: flex;
221
- align-items: flex-start;
312
+ align-items: center;
222
313
  gap: 0.5rem;
223
314
  border-bottom: 1px solid var(--ds-color-border);
224
- padding: 0.75rem 1rem;
315
+ padding: 0.5rem 1rem 0.75rem;
225
316
  }
226
317
 
227
318
  .ds-lib-pane-heading {
228
319
  min-width: 0;
229
320
  flex: 1;
321
+ order: 1;
230
322
  }
231
323
 
232
324
  .ds-lib-pane-title,
@@ -240,23 +332,24 @@
240
332
  .ds-lib-pane-title {
241
333
  color: var(--ds-color-foreground);
242
334
  font-family: inherit;
243
- font-size: 0.875rem;
335
+ font-size: 0.9375rem;
244
336
  font-weight: 600;
245
337
  }
246
338
 
247
339
  .ds-lib-pane-subtitle {
340
+ display: none;
248
341
  color: var(--ds-color-muted-foreground);
249
342
  font-size: var(--ds-text-2xs);
250
343
  }
251
344
 
252
345
  .ds-lib-pane-close {
253
346
  display: flex;
254
- width: 2rem;
255
- height: 2rem;
347
+ width: 2.5rem;
348
+ height: 2.5rem;
256
349
  flex: none;
257
350
  align-items: center;
258
351
  justify-content: center;
259
- margin-top: -0.25rem;
352
+ order: 0;
260
353
  border: 0;
261
354
  border-radius: var(--ds-radius-lg);
262
355
  background: none;
@@ -272,50 +365,135 @@
272
365
  color: var(--ds-color-foreground);
273
366
  }
274
367
 
275
- .ds-lib-pane-close:focus-visible {
368
+ .ds-lib-pane-close:focus-visible,
369
+ .ds-lib-pane-tool:focus-visible,
370
+ .ds-lib-pane-versions:focus-visible {
276
371
  outline: 2px solid var(--ds-color-ring);
277
372
  outline-offset: 2px;
278
373
  }
279
374
 
280
- .ds-lib-pane-body {
281
- min-height: 0;
282
- flex: 1;
283
- overflow-y: auto;
284
- overscroll-behavior: contain;
285
- padding: 0.75rem 1rem;
375
+ .ds-lib-pane-versions {
376
+ order: 2;
377
+ border: 1px solid var(--ds-color-border);
378
+ border-radius: var(--ds-radius-md);
379
+ background: var(--ds-color-surface-1);
380
+ padding: 0.375rem 0.5rem;
381
+ color: var(--ds-color-foreground);
382
+ font: inherit;
383
+ font-size: 0.8125rem;
286
384
  }
287
385
 
288
- .ds-lib-pane-tools {
289
- display: flex;
290
- flex-wrap: wrap;
386
+ .ds-lib-pane-tool {
387
+ display: inline-flex;
388
+ min-height: 2.25rem;
291
389
  align-items: center;
292
- gap: 0.25rem 0.75rem;
293
- margin-top: 0.25rem;
294
- }
295
-
296
- .ds-lib-pane-versions {
390
+ gap: 0.375rem;
297
391
  border: 1px solid var(--ds-color-border);
298
392
  border-radius: var(--ds-radius-md);
299
- background: var(--ds-color-surface-1);
300
- padding: 0.125rem 0.25rem;
393
+ background: none;
394
+ padding: 0.25rem 0.625rem;
301
395
  color: var(--ds-color-foreground);
302
396
  font: inherit;
303
- font-size: var(--ds-text-2xs);
397
+ font-size: 0.8125rem;
398
+ text-decoration: none;
399
+ cursor: pointer;
400
+ }
401
+
402
+ .ds-lib-pane-tool:hover {
403
+ background: var(--ds-color-surface-2);
404
+ }
405
+
406
+ .ds-lib-pane-copy {
407
+ display: none;
408
+ order: 3;
409
+ }
410
+
411
+ .ds-lib-pane-menu {
412
+ position: relative;
413
+ display: none;
414
+ order: 4;
415
+ }
416
+
417
+ .ds-lib-pane-quick {
418
+ order: 3;
304
419
  }
305
420
 
306
421
  .ds-lib-pane-links {
422
+ position: absolute;
423
+ inset-block-start: calc(100% + 0.25rem);
424
+ inset-inline-end: 0;
425
+ z-index: 2;
307
426
  display: flex;
308
- flex-wrap: wrap;
309
- gap: 0.75rem;
427
+ min-width: 9rem;
428
+ flex-direction: column;
310
429
  margin: 0;
311
- padding: 0;
312
- font-size: var(--ds-text-2xs);
430
+ border: 1px solid var(--ds-color-border);
431
+ border-radius: var(--ds-radius-md);
432
+ background: var(--ds-color-surface-1);
433
+ padding: 0.25rem;
434
+ box-shadow: 0 8px 24px rgb(0 0 0 / 0.25);
313
435
  list-style: none;
314
436
  }
315
437
 
316
438
  .ds-lib-pane-links a {
439
+ display: block;
440
+ border-radius: var(--ds-radius-sm);
441
+ padding: 0.5rem 0.625rem;
442
+ color: var(--ds-color-foreground);
443
+ font-size: 0.8125rem;
444
+ text-decoration: none;
445
+ }
446
+
447
+ .ds-lib-pane-links a:hover,
448
+ .ds-lib-pane-links a:focus-visible {
449
+ background: var(--ds-color-surface-2);
450
+ }
451
+
452
+ .ds-lib-pane-changed {
453
+ display: flex;
454
+ align-items: center;
455
+ gap: 0.5rem;
456
+ margin: 0;
457
+ border-bottom: 1px solid var(--ds-color-border);
458
+ padding: 0.5rem 1rem;
317
459
  color: var(--ds-color-muted-foreground);
318
- text-decoration: underline;
460
+ font-size: 0.8125rem;
461
+ }
462
+
463
+ .ds-lib-pane-changed::before {
464
+ content: '';
465
+ width: 2px;
466
+ height: 0.875rem;
467
+ flex: none;
468
+ background: var(--ds-color-foreground);
469
+ }
470
+
471
+ .ds-lib-pane-body {
472
+ min-height: 0;
473
+ flex: 1;
474
+ overflow-y: auto;
475
+ overscroll-behavior: contain;
476
+ padding: 0.75rem 1rem;
477
+ }
478
+
479
+ .ds-lib-pane-note {
480
+ margin: 0;
481
+ color: var(--ds-color-muted-foreground);
482
+ font-size: var(--ds-text-2xs);
483
+ }
484
+
485
+ /* The document is paper, light in either theme: its own ink and rules
486
+ replace the app's, so a dark page does not paint it grey on grey. */
487
+ .ds-lib-pane-paper {
488
+ --ds-color-foreground: var(--ds-paper-ink, #1b1c1e);
489
+ --ds-color-muted-foreground: var(--ds-paper-muted, #5f6368);
490
+ --ds-color-border: var(--ds-paper-rule, #c4c7cc);
491
+ --ds-color-surface-1: var(--ds-paper-shade, #dcdee2);
492
+ --ds-color-primary-ink: var(--ds-paper-ink, #1b1c1e);
493
+ border-radius: var(--ds-radius-sm);
494
+ background: var(--ds-paper, #e9eaec);
495
+ padding: 1.25rem 1.5rem;
496
+ color: var(--ds-color-foreground);
319
497
  }
320
498
 
321
499
  .ds-lib-pane-body.is-frame {
@@ -340,16 +518,38 @@
340
518
  position: relative;
341
519
  inset: auto;
342
520
  height: auto;
343
- width: 35rem;
521
+ width: 42rem;
344
522
  flex: none;
345
- border-top: 0;
346
523
  border-inline-start: 1px solid var(--ds-color-border);
347
- border-radius: 0;
348
- box-shadow: none;
349
524
  }
350
525
 
351
526
  .ds-lib-pane-grabber {
352
527
  display: none;
353
528
  }
529
+
530
+ .ds-lib-pane-header {
531
+ padding-top: 0.75rem;
532
+ }
533
+
534
+ .ds-lib-pane-heading {
535
+ order: 0;
536
+ }
537
+
538
+ .ds-lib-pane-subtitle {
539
+ display: block;
540
+ }
541
+
542
+ .ds-lib-pane-copy,
543
+ .ds-lib-pane-menu {
544
+ display: inline-flex;
545
+ }
546
+
547
+ .ds-lib-pane-quick {
548
+ display: none;
549
+ }
550
+
551
+ .ds-lib-pane-close {
552
+ order: 5;
553
+ }
354
554
  }
355
555
  </style>
@@ -33,7 +33,7 @@
33
33
  type AgentTranscriptProps
34
34
  } from '../agent-transcript/agent-transcript.svelte';
35
35
  import ArtefactPane from '../artefact-pane/artefact-pane.svelte';
36
- import type { Fences } from '../markdown/markdown';
36
+ import type { Visual } from '../../visuals';
37
37
  import DocumentPane from '../document-pane/document-pane.svelte';
38
38
  import ScopeStatement from '../scope-statement/scope-statement.svelte';
39
39
 
@@ -109,9 +109,9 @@
109
109
  loadWritten?: LoadWritten;
110
110
  /** Where a version downloads. Without it no download links render. */
111
111
  writtenUrl?: WrittenUrl;
112
- /** Languages of fenced code the host draws with its own component
113
- * rather than as code. */
114
- fences?: Fences;
112
+ /** Renderers by a fence's language (`defaultVisuals`, and the app's own):
113
+ * a block of one is drawn by its component rather than as code. */
114
+ visuals?: Record<string, Visual>;
115
115
  /** Prices a turn's `outcome.costUsd` in the host's own currency and
116
116
  * words ("about A$0.34"), for the footer's cost line beside a turn's
117
117
  * duration. The package never converts currency itself; omitted, the
@@ -162,7 +162,7 @@
162
162
  onedit,
163
163
  loadWritten,
164
164
  writtenUrl,
165
- fences,
165
+ visuals,
166
166
  collectionNames = new Set(),
167
167
  composer,
168
168
  turn: presentTurn,
@@ -386,7 +386,7 @@
386
386
  showingWritten: shownWritten?.key,
387
387
  onopenwritten: loadWritten ? openWritten : undefined,
388
388
  writtenUrl,
389
- fences,
389
+ visuals,
390
390
  onedit:
391
391
  onedit && !running && !turn.kind && turn.question
392
392
  ? (text: string) => onedit(index, text)
@@ -5,7 +5,7 @@ import type { Citation, LoadDocument } from '../../citations';
5
5
  import { type LoadWritten, type WrittenUrl } from '../../written';
6
6
  import { type ChatCopy } from '../../copy';
7
7
  import { type AgentTranscriptProps } from '../agent-transcript/agent-transcript.svelte';
8
- import type { Fences } from '../markdown/markdown';
8
+ import type { Visual } from '../../visuals';
9
9
  interface Props {
10
10
  turns: Turn[];
11
11
  running: boolean;
@@ -68,9 +68,9 @@ interface Props {
68
68
  loadWritten?: LoadWritten;
69
69
  /** Where a version downloads. Without it no download links render. */
70
70
  writtenUrl?: WrittenUrl;
71
- /** Languages of fenced code the host draws with its own component
72
- * rather than as code. */
73
- fences?: Fences;
71
+ /** Renderers by a fence's language (`defaultVisuals`, and the app's own):
72
+ * a block of one is drawn by its component rather than as code. */
73
+ visuals?: Record<string, Visual>;
74
74
  /** Prices a turn's `outcome.costUsd` in the host's own currency and
75
75
  * words ("about A$0.34"), for the footer's cost line beside a turn's
76
76
  * duration. The package never converts currency itself; omitted, the
@@ -15,11 +15,15 @@
15
15
  * Sanitised on the way out, always. The content is model output rendered as
16
16
  * HTML, which is exactly the case DOMPurify exists for.
17
17
  */
18
- import { type Component } from 'svelte';
19
18
  /** Close anything the stream has opened but not yet finished. */
20
19
  export declare function balance(markdown: string): string;
21
- export declare function render(markdown: string, { streaming }?: {
22
- streaming?: boolean | undefined;
20
+ /**
21
+ * `markdown` as sanitised HTML. With `previous`, the version before it, each
22
+ * block it changed is marked on the sanitised DOM, before its one serialisation.
23
+ */
24
+ export declare function render(markdown: string, { streaming, previous }?: {
25
+ streaming?: boolean;
26
+ previous?: string;
23
27
  }): string;
24
28
  /**
25
29
  * Mark every inline `code` span that names a known collection.
@@ -43,16 +47,29 @@ export declare function markCollections(root: HTMLElement, collections: Set<stri
43
47
  * button ROLE and a tab stop, and the component delegates the events.
44
48
  */
45
49
  export declare function markCitations(root: HTMLElement, valid: Set<number>): void;
46
- /** Fenced code blocks of a language a host names, drawn by its own component. */
47
- export type Fences = Record<string, Component<{
50
+ /** A turn's prose and the fenced blocks of a registered language, in order. */
51
+ export type Part = {
52
+ kind: 'md';
53
+ text: string;
54
+ } | {
55
+ kind: 'visual';
56
+ lang: string;
48
57
  source: string;
49
- }>>;
58
+ closed: boolean;
59
+ };
60
+ /**
61
+ * Cut `markdown` at each fenced block whose language `has` a renderer for.
62
+ * A fence of another language (or none) is ordinary code and stays in the
63
+ * prose, and so does anything inside one: an example of a visual's fence in a
64
+ * code block is text. A block still open at the end is `closed: false`.
65
+ */
66
+ export declare function splitVisuals(markdown: string, has: (lang: string) => boolean): Part[];
50
67
  /**
51
- * Draw every fenced block whose language the host registered with the host's
52
- * component, in place of the code block. The source is the block's text, never
53
- * HTML. Returns what unmounts them.
68
+ * Turn each link to a numbered web page into its citation chip: the link's
69
+ * text stays, unlinked, and the chip after it carries the page's number and
70
+ * opens the page. Against the rendered DOM, for the reason `markCitations` is.
54
71
  */
55
- export declare function mountFences(root: HTMLElement, fences: Fences): () => void;
72
+ export declare function markWebLinks(root: HTMLElement, numbers: Map<string, number>): void;
56
73
  /**
57
74
  * Replace every `<pre><code>` in a rendered fragment with a highlighted one.
58
75
  * Runs against the DOM node rather than the HTML string so it can be applied