@poodle64/librarian 2026.10.4 → 2026.10.5

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 (50) hide show
  1. package/README.md +7 -5
  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/entity-card.svelte +94 -0
  23. package/dist/components/visual-block/entity-card.svelte.d.ts +7 -0
  24. package/dist/components/visual-block/timeline.svelte +99 -0
  25. package/dist/components/visual-block/timeline.svelte.d.ts +7 -0
  26. package/dist/components/visual-block/vega-chart.svelte +110 -0
  27. package/dist/components/visual-block/vega-chart.svelte.d.ts +7 -0
  28. package/dist/components/visual-block/visual-block.svelte +53 -0
  29. package/dist/components/visual-block/visual-block.svelte.d.ts +13 -0
  30. package/dist/components/written-card/written-card.svelte +97 -49
  31. package/dist/components/written-card/written-card.svelte.d.ts +1 -1
  32. package/dist/copy.d.ts +54 -0
  33. package/dist/copy.js +44 -1
  34. package/dist/diff.d.ts +8 -0
  35. package/dist/diff.js +37 -0
  36. package/dist/steps.d.ts +28 -0
  37. package/dist/steps.js +168 -0
  38. package/dist/transcript.svelte.d.ts +4 -0
  39. package/dist/transcript.svelte.js +13 -2
  40. package/dist/visuals.d.ts +38 -0
  41. package/dist/visuals.js +82 -0
  42. package/dist/web-sources.d.ts +19 -3
  43. package/dist/web-sources.js +39 -7
  44. package/dist/written.d.ts +26 -1
  45. package/dist/written.js +42 -0
  46. package/package.json +10 -1
  47. package/dist/components/web-list/index.d.ts +0 -2
  48. package/dist/components/web-list/index.js +0 -2
  49. package/dist/components/web-list/web-list.svelte +0 -84
  50. package/dist/components/web-list/web-list.svelte.d.ts +0 -9
package/README.md CHANGED
@@ -35,8 +35,9 @@ src/lib/
35
35
  document-pane/ the cited document, open at the cited passage
36
36
  artefact-card/ an artefact's card, in the host's words
37
37
  artefact-pane/ an artefact that is its answer's prose, or one version of a written document, in the column
38
- written-card/ a document the persona wrote: version, Open, downloads
39
- web-list/ the web pages an answer drew on, apart from its numbered sources
38
+ written-card/ a document the persona wrote: kind, pages, version, Open, and a download chip
39
+ source-fold/ what an answer drew on, folded: the library's numbered sources, then the web pages numbered on from them
40
+ visual-block/ a fenced block of a registered language: its renderer, Drawing… while it arrives, its code when invalid
40
41
  composer/ the input box: attachments, one collection control, a note, send/stop
41
42
  markdown/ sanitised, streaming-safe markdown + highlighting
42
43
  activity-group/ what the persona did, one collapsed row per step
@@ -401,9 +402,10 @@ end that way.
401
402
  | An artefact | `turn.artefact = { title, summary }` cards under the prose once the turn settles; `isAnswer: true` is an artefact that IS the prose, and replaces it |
402
403
  | Opening it | `onopenartefact(turn)` and `showing` (the turn id the host shows). Without a handler, an `isAnswer` artefact opens in `ArtefactPane` and any other offers no action |
403
404
  | Editing a question | `Conversation`'s `onedit(position, text)` (`Chat.edit`) offers an edit control on each plain question while nothing is being answered; the turns from there on are dropped and the new question streams in at that position (`ask({ edit })`). Files are not sent again |
404
- | Documents it wrote | `artefact` frames and a stored turn's `artefacts` become `turn.written`, carded with Open and downloads. `loadWritten(name, version)` and `writtenUrl(name, version, format)` (`Chat.loadArtefact` / `Chat.artefactUrl` over the transport) let the pane show a version: Markdown sanitised, HTML only in a `sandbox=""` frame |
405
- | Web sources | Pages from the turn's supacrawl calls and the answer's Markdown links list under `copy.fromTheWeb`, unnumbered, apart from the library's `[n]` sources |
406
- | Drawing a fence | `fences={{ chart: Chart }}` on `Conversation` draws a fenced block of that language with the host's component (`source` is its text), once the answer has settled |
405
+ | Documents it wrote | `artefact` frames and a stored turn's `artefacts` (each with `title`, `kind`, `changes`, `pages`) become `turn.written`: the newest version of each is a card with Open and a download chip (Word where `formats` has it, else the source). The pane marks what a version changed since the one before: a "Changed since vN" line from `changes`, and a bar beside each changed block. `loadWritten(name, version)` and `writtenUrl(name, version, format)` (`Chat.loadArtefact` / `Chat.artefactUrl` over the transport) let the pane show a version: Markdown sanitised, HTML only in a `sandbox=""` frame |
406
+ | The turn's steps | One fold, "Read your attachment, searched the library and the web, and wrote a minute · 6 steps", composed from the steps (`stepsSummary`); it says what is happening now while the turn runs, and opens on one row per step (a search row counts what it found, a web search opens on the pages it found) |
407
+ | Web sources | Pages from the turn's supacrawl calls and the answer's Markdown links fold with the library's sources ("6 sources · 4 library · 2 web"), numbered on from the library's, and a link to one in the answer reads as that number's chip |
408
+ | Drawing a fence | `visuals` on `Conversation` is a registry keyed on a fence's language, `{ component, parse }` each; `defaultVisuals` (from `@poodle64/librarian/visuals`) draws `vega-lite` (one Vega-Lite v5 spec, in the app's tokens), `timeline` and `card`. An app passes `{ ...defaultVisuals, openui }`. A block whose closing fence has not arrived is "Drawing…", one that does not parse is its code |
407
409
  | "Answered without a source" | `copy.notHeld: ''` for a persona that answers from no sources |
408
410
  | Saying something mid-run | `Composer`'s `sendWhileRunning`: while the session works the box stays open and Send sits beside Stop. For a route that reads a message while the run works; a chat leaves it off and waits for its answer |
409
411
 
@@ -1,10 +1,11 @@
1
1
  <!--
2
- What the persona did before it answered, one collapsed row per step, in
3
- order — the way Claude Code's own panel lists them.
2
+ What the persona did before it answered, folded behind one line.
4
3
 
5
- Each row says what the step did in a reader's words and opens on what went
6
- in and what came back. A thinking block is a folded row too: "Thinking…"
7
- while it streams, "Thought" once done.
4
+ The line is a sentence composed from the steps ("Read your attachment,
5
+ searched the library and the web, and wrote a minute · 6 steps"), and while
6
+ the turn runs it says what is happening now. It opens on one row per step,
7
+ in order, the way Claude Code's own panel lists them: each says what the step
8
+ did in a reader's words and opens on what went in and what came back.
8
9
 
9
10
  The persona's between-tool commentary is a step in here as well. It reaches
10
11
  the caller as an ordinary `text` block, not a `thinking` one, so `segment()`
@@ -12,8 +13,12 @@
12
13
  as a ThinkingRow beside the tools, folded like the rest.
13
14
  -->
14
15
  <script lang="ts">
16
+ import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
17
+ import ChevronUpIcon from '@lucide/svelte/icons/chevron-up';
15
18
  import type { ActivityGroup } from '../../transcript.svelte';
16
- import type { ChatCopy } from '../../copy';
19
+ import { resolveCopy, type ChatCopy } from '../../copy';
20
+ import { stepsNow, stepsSummary } from '../../steps';
21
+ import type { Written } from '../../written';
17
22
  import ToolRow from '../tool-row/tool-row.svelte';
18
23
  import ThinkingRow from '../thinking-row/thinking-row.svelte';
19
24
 
@@ -21,26 +26,103 @@
21
26
  group: ActivityGroup;
22
27
  /** True while this group is the live one. */
23
28
  live: boolean;
29
+ /** The documents the turn wrote: a step that wrote one says its kind. */
30
+ written?: Written[];
24
31
  copy?: Partial<ChatCopy>;
25
32
  }
26
33
 
27
- let { group, live, copy }: Props = $props();
34
+ let { group, live, written = [], copy }: Props = $props();
35
+ let open = $state(false);
36
+
37
+ const words = $derived(resolveCopy(copy));
38
+ const summary = $derived(stepsSummary(group.steps, written, words));
39
+ const now = $derived(stepsNow(group.steps, written, words));
28
40
  </script>
29
41
 
30
- <div class="ds-lib-activity-steps">
31
- {#each group.steps as step, i (step.block.index)}
32
- {#if step.block.kind === 'tool'}
33
- <ToolRow block={step.block} words={step.words} running={live} {copy} />
34
- {:else}
35
- <ThinkingRow block={step.block} active={live && i === group.steps.length - 1} />
36
- {/if}
37
- {/each}
42
+ <div class="ds-lib-steps">
43
+ <button
44
+ type="button"
45
+ class="ds-lib-steps-head"
46
+ onclick={() => (open = !open)}
47
+ aria-expanded={open}
48
+ >
49
+ <span class="ds-lib-steps-line">
50
+ {#if live}
51
+ <span class="ds-lib-steps-now">{now}</span>
52
+ {:else}
53
+ {summary.sentence}<span class="ds-lib-steps-trail">{` · ${summary.trail}`}</span>
54
+ {/if}
55
+ </span>
56
+ <span class="ds-lib-steps-chevron" aria-hidden="true">
57
+ {#if open}<ChevronUpIcon size={15} />{:else}<ChevronDownIcon size={15} />{/if}
58
+ </span>
59
+ </button>
60
+
61
+ {#if open}
62
+ <div class="ds-lib-steps-rows">
63
+ {#each group.steps as step, i (step.block.index)}
64
+ {#if step.block.kind === 'tool'}
65
+ <ToolRow block={step.block} words={step.words} running={live} {copy} />
66
+ {:else}
67
+ <ThinkingRow block={step.block} active={live && i === group.steps.length - 1} {copy} />
68
+ {/if}
69
+ {/each}
70
+ </div>
71
+ {/if}
38
72
  </div>
39
73
 
40
74
  <style>
41
- .ds-lib-activity-steps {
75
+ .ds-lib-steps-head {
76
+ display: flex;
77
+ width: 100%;
78
+ align-items: flex-start;
79
+ gap: 0.5rem;
80
+ border: 0;
81
+ background: none;
82
+ padding: 0.25rem 0;
83
+ color: var(--ds-color-muted-foreground);
84
+ font: inherit;
85
+ font-size: 0.875rem;
86
+ line-height: 1.375rem;
87
+ text-align: start;
88
+ cursor: pointer;
89
+ }
90
+
91
+ .ds-lib-steps-head:hover {
92
+ color: var(--ds-color-foreground);
93
+ }
94
+
95
+ .ds-lib-steps-head:focus-visible {
96
+ outline: 2px solid var(--ds-color-ring);
97
+ outline-offset: 2px;
98
+ }
99
+
100
+ .ds-lib-steps-line {
101
+ flex: 1;
102
+ }
103
+
104
+ .ds-lib-steps-now {
105
+ color: var(--ds-color-foreground);
106
+ font-weight: 500;
107
+ }
108
+
109
+ .ds-lib-steps-trail {
110
+ font-size: var(--ds-text-2xs);
111
+ opacity: 0.8;
112
+ }
113
+
114
+ .ds-lib-steps-chevron {
115
+ display: flex;
116
+ flex: none;
117
+ padding-top: 0.25rem;
118
+ }
119
+
120
+ .ds-lib-steps-rows {
42
121
  display: flex;
43
122
  flex-direction: column;
44
- gap: 0.25rem;
123
+ gap: 0.125rem;
124
+ margin-top: 0.25rem;
125
+ border-inline-start: 1px solid var(--ds-color-border);
126
+ padding-inline-start: 0.75rem;
45
127
  }
46
128
  </style>
@@ -1,9 +1,12 @@
1
1
  import type { ActivityGroup } from '../../transcript.svelte';
2
- import type { ChatCopy } from '../../copy';
2
+ import { type ChatCopy } from '../../copy';
3
+ import type { Written } from '../../written';
3
4
  interface Props {
4
5
  group: ActivityGroup;
5
6
  /** True while this group is the live one. */
6
7
  live: boolean;
8
+ /** The documents the turn wrote: a step that wrote one says its kind. */
9
+ written?: Written[];
7
10
  copy?: Partial<ChatCopy>;
8
11
  }
9
12
  declare const ActivityGroup: import("svelte").Component<Props, {}, "">;
@@ -16,7 +16,7 @@
16
16
  import CheckIcon from '@lucide/svelte/icons/check';
17
17
  import CopyIcon from '@lucide/svelte/icons/copy';
18
18
  import PencilIcon from '@lucide/svelte/icons/pencil';
19
- import PaperclipIcon from '@lucide/svelte/icons/paperclip';
19
+ import FileTextIcon from '@lucide/svelte/icons/file-text';
20
20
  import RefreshCwIcon from '@lucide/svelte/icons/refresh-cw';
21
21
  import {
22
22
  outcomeLine,
@@ -35,9 +35,10 @@
35
35
  splitSources,
36
36
  type Citation
37
37
  } from '../../citations';
38
- import { DEFAULT_PERSONA, personaName, resolveCopy, type ChatCopy } from '../../copy';
39
- import { webSources } from '../../web-sources';
40
- import { writtenKey, type Written, type WrittenUrl } from '../../written';
38
+ import { DEFAULT_PERSONA, fill, personaName, resolveCopy, type ChatCopy } from '../../copy';
39
+ import { webKey, webSources } from '../../web-sources';
40
+ import { newestPerName, writtenKey, type Written, type WrittenUrl } from '../../written';
41
+ import type { Visual } from '../../visuals';
41
42
  import type { Verdict } from '../../chat.svelte';
42
43
  import AnswerMark from '../answer-mark/answer-mark.svelte';
43
44
  import AnswerShare from '../answer-share/answer-share.svelte';
@@ -45,9 +46,7 @@
45
46
  import ActivityGroup from '../activity-group/activity-group.svelte';
46
47
  import ArtefactCard from '../artefact-card/artefact-card.svelte';
47
48
  import Markdown from '../markdown/markdown.svelte';
48
- import type { Fences } from '../markdown/markdown';
49
- import SourceList from '../source-list/source-list.svelte';
50
- import WebList from '../web-list/web-list.svelte';
49
+ import SourceFold from '../source-fold/source-fold.svelte';
51
50
  import WrittenCard from '../written-card/written-card.svelte';
52
51
 
53
52
  /** Everything one turn renders from: what `Conversation` hands a host's
@@ -97,8 +96,9 @@
97
96
  onopenwritten?: (written: Written) => void;
98
97
  /** Where a version downloads. Omit and no download links render. */
99
98
  writtenUrl?: WrittenUrl;
100
- /** Languages of fenced code the host draws with its own component. */
101
- fences?: Fences;
99
+ /** Renderers by a fence's language: a block of one is drawn by its
100
+ * component in the answer. Absent, every fence is code. */
101
+ visuals?: Record<string, Visual>;
102
102
  /** When an answer this page did not start began, epoch ms: it is still
103
103
  * being written, and the clock counts from then. */
104
104
  answering?: number;
@@ -140,7 +140,7 @@
140
140
  showingWritten,
141
141
  onopenwritten,
142
142
  writtenUrl,
143
- fences,
143
+ visuals,
144
144
  answering,
145
145
  onmark,
146
146
  shared,
@@ -214,8 +214,31 @@
214
214
  const hasAnswer = $derived(texts.length > 0);
215
215
 
216
216
  // Pages the answer drew on, once it has settled: a link still being typed
217
- // is not a source.
218
- const web = $derived(running ? [] : webSources(blocks, answer));
217
+ // is not a source. They are numbered on from the library's, and a link to
218
+ // one reads as the same chip.
219
+ const web = $derived.by(() => {
220
+ if (running) return [];
221
+ const from = Math.max(0, ...sources.map((c) => c.n));
222
+ return webSources(blocks, answer).map((page, i) => ({ ...page, n: from + i + 1 }));
223
+ });
224
+ const webNumbers = $derived(new Map(web.map((page) => [webKey(page.url), page.n])));
225
+
226
+ // Which day the pages were read: today only for a turn asked today.
227
+ const readWhen = $derived.by(() => {
228
+ if (at === undefined) return '';
229
+ const when = new Date(at);
230
+ if (new Date().toDateString() === when.toDateString()) return words.readToday;
231
+ return fill(words.readOn, {
232
+ date: when.toLocaleDateString(undefined, { day: 'numeric', month: 'short', year: 'numeric' })
233
+ });
234
+ });
235
+
236
+ const cards = $derived(newestPerName(written));
237
+
238
+ const fileType = (file: string): string => {
239
+ const ext = /\.([A-Za-z0-9]{1,5})$/.exec(file)?.[1];
240
+ return ext ? ext.toUpperCase() : '';
241
+ };
219
242
 
220
243
  let editing = $state(false);
221
244
  let edited = $state('');
@@ -329,14 +352,20 @@
329
352
  </form>
330
353
  {:else if asked}
331
354
  <div class="ds-lib-ask">
332
- <div class="ds-lib-bubble">{asked}</div>
333
355
  {#if files.length > 0}
334
356
  <ul class="ds-lib-ask-files">
335
357
  {#each files as file, i (i)}
336
- <li class="ds-lib-ask-file"><PaperclipIcon size={12} />{file}</li>
358
+ <li class="ds-lib-ask-file">
359
+ <span class="ds-lib-ask-file-icon" aria-hidden="true"><FileTextIcon size={16} /></span>
360
+ <span class="ds-lib-ask-file-text">
361
+ <span class="ds-lib-ask-file-name">{file}</span>
362
+ {#if fileType(file)}<span class="ds-lib-ask-file-type">{fileType(file)}</span>{/if}
363
+ </span>
364
+ </li>
337
365
  {/each}
338
366
  </ul>
339
367
  {/if}
368
+ <div class="ds-lib-bubble">{asked}</div>
340
369
  <div class="ds-lib-ask-meta">
341
370
  {#if onedit && !running}
342
371
  <button type="button" class="ds-lib-action" onclick={startEdit}>
@@ -370,7 +399,12 @@
370
399
 
371
400
  {#each segments as seg (seg.index)}
372
401
  {#if seg.kind === 'activity'}
373
- <ActivityGroup group={seg} live={running && seg.index === lastIndex} copy={words} />
402
+ <ActivityGroup
403
+ group={seg}
404
+ live={running && seg.index === lastIndex}
405
+ {written}
406
+ copy={words}
407
+ />
374
408
  {:else if !isArtefact}
375
409
  <Markdown
376
410
  content={seg.index === lastTextIndex && split.citations.length > 0
@@ -378,8 +412,10 @@
378
412
  : seg.text}
379
413
  streaming={running && seg.index === lastIndex}
380
414
  {collectionNames}
381
- {fences}
415
+ {visuals}
416
+ copy={words}
382
417
  citationNumbers={numbers}
418
+ {webNumbers}
383
419
  oncite={(n) => {
384
420
  const found = sources.find((c) => c.n === n);
385
421
  if (found) oncite?.(found);
@@ -392,7 +428,7 @@
392
428
  <ArtefactCard {artefact} open={artefactOpen} onopen={onopenartefact} copy={words} />
393
429
  {/if}
394
430
 
395
- {#each written as doc (writtenKey(doc))}
431
+ {#each cards as doc (writtenKey(doc))}
396
432
  <WrittenCard
397
433
  written={doc}
398
434
  open={showingWritten === writtenKey(doc)}
@@ -402,12 +438,8 @@
402
438
  />
403
439
  {/each}
404
440
 
405
- {#if !isArtefact && sources.length > 0}
406
- <SourceList {sources} {words} {oncite} />
407
- {/if}
408
-
409
- {#if !isArtefact && web.length > 0}
410
- <WebList sources={web} {words} />
441
+ {#if !isArtefact && (sources.length > 0 || web.length > 0)}
442
+ <SourceFold library={sources} {web} {words} {oncite} {readWhen} />
411
443
  {/if}
412
444
 
413
445
  <!-- An empty `notHeld` is a host whose persona answers from no sources,
@@ -519,13 +551,37 @@
519
551
 
520
552
  .ds-lib-ask-file {
521
553
  display: flex;
554
+ max-width: 100%;
522
555
  align-items: center;
523
- gap: 0.25rem;
556
+ gap: 0.625rem;
524
557
  border: 1px solid var(--ds-color-border);
525
- border-radius: var(--ds-radius-md);
526
- padding: 0.125rem 0.5rem;
558
+ border-radius: var(--ds-radius-full);
559
+ padding: 0.375rem 0.875rem 0.375rem 0.5rem;
560
+ }
561
+
562
+ .ds-lib-ask-file-icon {
563
+ display: flex;
564
+ flex: none;
527
565
  color: var(--ds-color-muted-foreground);
566
+ }
567
+
568
+ .ds-lib-ask-file-text {
569
+ display: flex;
570
+ min-width: 0;
571
+ flex-direction: column;
528
572
  font-size: var(--ds-text-2xs);
573
+ line-height: 1.25;
574
+ }
575
+
576
+ .ds-lib-ask-file-name {
577
+ color: var(--ds-color-foreground);
578
+ font-size: 0.8125rem;
579
+ font-weight: 600;
580
+ overflow-wrap: anywhere;
581
+ }
582
+
583
+ .ds-lib-ask-file-type {
584
+ color: var(--ds-color-muted-foreground);
529
585
  }
530
586
 
531
587
  .ds-lib-edit {
@@ -2,8 +2,8 @@ import { type Artefact, type Block, type DescribeTool, type Outcome } from '../.
2
2
  import { type Citation } from '../../citations';
3
3
  import { type ChatCopy } from '../../copy';
4
4
  import { type Written, type WrittenUrl } from '../../written';
5
+ import type { Visual } from '../../visuals';
5
6
  import type { Verdict } from '../../chat.svelte';
6
- import type { Fences } from '../markdown/markdown';
7
7
  /** Everything one turn renders from: what `Conversation` hands a host's
8
8
  * own `turn` snippet, so it can render this component or its own. */
9
9
  export interface AgentTranscriptProps {
@@ -51,8 +51,9 @@ export interface AgentTranscriptProps {
51
51
  onopenwritten?: (written: Written) => void;
52
52
  /** Where a version downloads. Omit and no download links render. */
53
53
  writtenUrl?: WrittenUrl;
54
- /** Languages of fenced code the host draws with its own component. */
55
- fences?: Fences;
54
+ /** Renderers by a fence's language: a block of one is drawn by its
55
+ * component in the answer. Absent, every fence is code. */
56
+ visuals?: Record<string, Visual>;
56
57
  /** When an answer this page did not start began, epoch ms: it is still
57
58
  * being written, and the clock counts from then. */
58
59
  answering?: number;