@poodle64/librarian 2026.10.3 → 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 (54) hide show
  1. package/README.md +12 -4
  2. package/dist/chat.svelte.d.ts +40 -0
  3. package/dist/chat.svelte.js +57 -3
  4. package/dist/client.d.ts +3 -0
  5. package/dist/client.js +4 -1
  6. package/dist/components/activity-group/activity-group.svelte +72 -77
  7. package/dist/components/activity-group/activity-group.svelte.d.ts +6 -1
  8. package/dist/components/agent-transcript/agent-transcript.svelte +225 -9
  9. package/dist/components/agent-transcript/agent-transcript.svelte.d.ts +19 -0
  10. package/dist/components/artefact-pane/artefact-pane.svelte +387 -43
  11. package/dist/components/artefact-pane/artefact-pane.svelte.d.ts +10 -1
  12. package/dist/components/conversation/conversation.svelte +74 -4
  13. package/dist/components/conversation/conversation.svelte.d.ts +15 -0
  14. package/dist/components/markdown/markdown.d.ts +30 -2
  15. package/dist/components/markdown/markdown.js +142 -5
  16. package/dist/components/markdown/markdown.svelte +130 -16
  17. package/dist/components/markdown/markdown.svelte.d.ts +12 -0
  18. package/dist/components/source-fold/index.d.ts +2 -0
  19. package/dist/components/source-fold/index.js +2 -0
  20. package/dist/components/source-fold/source-fold.svelte +225 -0
  21. package/dist/components/source-fold/source-fold.svelte.d.ts +17 -0
  22. package/dist/components/source-list/source-list.svelte +5 -3
  23. package/dist/components/source-list/source-list.svelte.d.ts +2 -0
  24. package/dist/components/thinking-row/thinking-row.svelte +38 -16
  25. package/dist/components/thinking-row/thinking-row.svelte.d.ts +3 -0
  26. package/dist/components/tool-row/tool-row.svelte +161 -49
  27. package/dist/components/tool-row/tool-row.svelte.d.ts +2 -3
  28. package/dist/components/visual-block/entity-card.svelte +94 -0
  29. package/dist/components/visual-block/entity-card.svelte.d.ts +7 -0
  30. package/dist/components/visual-block/timeline.svelte +99 -0
  31. package/dist/components/visual-block/timeline.svelte.d.ts +7 -0
  32. package/dist/components/visual-block/vega-chart.svelte +110 -0
  33. package/dist/components/visual-block/vega-chart.svelte.d.ts +7 -0
  34. package/dist/components/visual-block/visual-block.svelte +53 -0
  35. package/dist/components/visual-block/visual-block.svelte.d.ts +13 -0
  36. package/dist/components/written-card/index.d.ts +2 -0
  37. package/dist/components/written-card/index.js +2 -0
  38. package/dist/components/written-card/written-card.svelte +178 -0
  39. package/dist/components/written-card/written-card.svelte.d.ts +15 -0
  40. package/dist/copy.d.ts +73 -0
  41. package/dist/copy.js +56 -0
  42. package/dist/diff.d.ts +8 -0
  43. package/dist/diff.js +37 -0
  44. package/dist/steps.d.ts +28 -0
  45. package/dist/steps.js +168 -0
  46. package/dist/transcript.svelte.d.ts +19 -34
  47. package/dist/transcript.svelte.js +64 -56
  48. package/dist/visuals.d.ts +38 -0
  49. package/dist/visuals.js +82 -0
  50. package/dist/web-sources.d.ts +34 -0
  51. package/dist/web-sources.js +90 -0
  52. package/dist/written.d.ts +71 -0
  53. package/dist/written.js +83 -0
  54. package/package.json +14 -1
@@ -15,6 +15,8 @@
15
15
  <script lang="ts">
16
16
  import CheckIcon from '@lucide/svelte/icons/check';
17
17
  import CopyIcon from '@lucide/svelte/icons/copy';
18
+ import PencilIcon from '@lucide/svelte/icons/pencil';
19
+ import FileTextIcon from '@lucide/svelte/icons/file-text';
18
20
  import RefreshCwIcon from '@lucide/svelte/icons/refresh-cw';
19
21
  import {
20
22
  outcomeLine,
@@ -33,7 +35,10 @@
33
35
  splitSources,
34
36
  type Citation
35
37
  } from '../../citations';
36
- import { DEFAULT_PERSONA, personaName, resolveCopy, type ChatCopy } from '../../copy';
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';
37
42
  import type { Verdict } from '../../chat.svelte';
38
43
  import AnswerMark from '../answer-mark/answer-mark.svelte';
39
44
  import AnswerShare from '../answer-share/answer-share.svelte';
@@ -41,7 +46,8 @@
41
46
  import ActivityGroup from '../activity-group/activity-group.svelte';
42
47
  import ArtefactCard from '../artefact-card/artefact-card.svelte';
43
48
  import Markdown from '../markdown/markdown.svelte';
44
- import SourceList from '../source-list/source-list.svelte';
49
+ import SourceFold from '../source-fold/source-fold.svelte';
50
+ import WrittenCard from '../written-card/written-card.svelte';
45
51
 
46
52
  /** Everything one turn renders from: what `Conversation` hands a host's
47
53
  * own `turn` snippet, so it can render this component or its own. */
@@ -76,6 +82,23 @@
76
82
  onopenartefact?: () => void;
77
83
  /** The persona's own words for its tools, and what they read. */
78
84
  describeTool?: DescribeTool;
85
+ /** The names of the files that went with the question, shown as chips. */
86
+ files?: string[];
87
+ /** Replaces this question with `text` and asks it again from here
88
+ * (`Chat.edit`), resolving false when that did not go through. Omit
89
+ * and no edit control renders: not while an answer is being written. */
90
+ onedit?: (text: string) => Promise<boolean>;
91
+ /** The documents the persona wrote answering it, carded under the answer. */
92
+ written?: Written[];
93
+ /** The version the reading column shows now, by `writtenKey`. */
94
+ showingWritten?: string;
95
+ /** Opens a version in the reading column. Omit and the cards only link. */
96
+ onopenwritten?: (written: Written) => void;
97
+ /** Where a version downloads. Omit and no download links render. */
98
+ writtenUrl?: WrittenUrl;
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>;
79
102
  /** When an answer this page did not start began, epoch ms: it is still
80
103
  * being written, and the clock counts from then. */
81
104
  answering?: number;
@@ -111,6 +134,13 @@
111
134
  artefactOpen = false,
112
135
  onopenartefact,
113
136
  describeTool,
137
+ files = [],
138
+ onedit,
139
+ written = [],
140
+ showingWritten,
141
+ onopenwritten,
142
+ writtenUrl,
143
+ visuals,
114
144
  answering,
115
145
  onmark,
116
146
  shared,
@@ -183,6 +213,51 @@
183
213
 
184
214
  const hasAnswer = $derived(texts.length > 0);
185
215
 
216
+ // Pages the answer drew on, once it has settled: a link still being typed
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
+ };
242
+
243
+ let editing = $state(false);
244
+ let edited = $state('');
245
+ let sending = $state(false);
246
+
247
+ function startEdit() {
248
+ edited = asked;
249
+ editing = true;
250
+ }
251
+
252
+ async function sendEdit() {
253
+ const text = edited.trim();
254
+ if (!text || !onedit || sending) return;
255
+ sending = true;
256
+ const sent = await onedit(text);
257
+ sending = false;
258
+ if (sent) editing = false;
259
+ }
260
+
186
261
  /**
187
262
  * What went wrong, if anything did — in words, whether or not the run gave
188
263
  * any.
@@ -251,12 +326,57 @@
251
326
  VS Code-style panel: this surface is read by people who arrive with
252
327
  those two as their model of what a chat looks like. A session whose
253
328
  stream never carried its prompt has no words to put here. -->
254
- {#if asked}
329
+ {#if asked && editing}
330
+ <form
331
+ class="ds-lib-ask ds-lib-edit"
332
+ onsubmit={(event) => {
333
+ event.preventDefault();
334
+ void sendEdit();
335
+ }}
336
+ >
337
+ <textarea
338
+ class="ds-lib-edit-text"
339
+ rows="3"
340
+ aria-label={words.editQuestion}
341
+ bind:value={edited}
342
+ ></textarea>
343
+ {#if files.length > 0}<p class="ds-lib-edit-note">{words.editFilesNote}</p>{/if}
344
+ <div class="ds-lib-edit-actions">
345
+ <button type="button" class="ds-lib-action" onclick={() => (editing = false)}>
346
+ {words.cancel}
347
+ </button>
348
+ <button type="submit" class="ds-lib-action" disabled={!edited.trim() || sending}>
349
+ {words.editSend}
350
+ </button>
351
+ </div>
352
+ </form>
353
+ {:else if asked}
255
354
  <div class="ds-lib-ask">
256
- <div class="ds-lib-bubble">{asked}</div>
257
- {#if clock}
258
- <time class="ds-lib-ask-time" datetime={clock.iso}>{clock.text}</time>
355
+ {#if files.length > 0}
356
+ <ul class="ds-lib-ask-files">
357
+ {#each files as file, i (i)}
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>
365
+ {/each}
366
+ </ul>
259
367
  {/if}
368
+ <div class="ds-lib-bubble">{asked}</div>
369
+ <div class="ds-lib-ask-meta">
370
+ {#if onedit && !running}
371
+ <button type="button" class="ds-lib-action" onclick={startEdit}>
372
+ <PencilIcon size={14} />
373
+ <span>{words.editQuestion}</span>
374
+ </button>
375
+ {/if}
376
+ {#if clock}
377
+ <time class="ds-lib-ask-time" datetime={clock.iso}>{clock.text}</time>
378
+ {/if}
379
+ </div>
260
380
  </div>
261
381
  {/if}
262
382
 
@@ -279,7 +399,12 @@
279
399
 
280
400
  {#each segments as seg (seg.index)}
281
401
  {#if seg.kind === 'activity'}
282
- <ActivityGroup group={seg} live={running && seg.index === lastIndex} />
402
+ <ActivityGroup
403
+ group={seg}
404
+ live={running && seg.index === lastIndex}
405
+ {written}
406
+ copy={words}
407
+ />
283
408
  {:else if !isArtefact}
284
409
  <Markdown
285
410
  content={seg.index === lastTextIndex && split.citations.length > 0
@@ -287,7 +412,10 @@
287
412
  : seg.text}
288
413
  streaming={running && seg.index === lastIndex}
289
414
  {collectionNames}
415
+ {visuals}
416
+ copy={words}
290
417
  citationNumbers={numbers}
418
+ {webNumbers}
291
419
  oncite={(n) => {
292
420
  const found = sources.find((c) => c.n === n);
293
421
  if (found) oncite?.(found);
@@ -300,8 +428,18 @@
300
428
  <ArtefactCard {artefact} open={artefactOpen} onopen={onopenartefact} copy={words} />
301
429
  {/if}
302
430
 
303
- {#if !isArtefact && sources.length > 0}
304
- <SourceList {sources} {words} {oncite} />
431
+ {#each cards as doc (writtenKey(doc))}
432
+ <WrittenCard
433
+ written={doc}
434
+ open={showingWritten === writtenKey(doc)}
435
+ onopen={onopenwritten ? () => onopenwritten(doc) : undefined}
436
+ url={writtenUrl}
437
+ copy={words}
438
+ />
439
+ {/each}
440
+
441
+ {#if !isArtefact && (sources.length > 0 || web.length > 0)}
442
+ <SourceFold library={sources} {web} {words} {oncite} {readWhen} />
305
443
  {/if}
306
444
 
307
445
  <!-- An empty `notHeld` is a host whose persona answers from no sources,
@@ -395,6 +533,84 @@
395
533
  white-space: pre-wrap;
396
534
  }
397
535
 
536
+ .ds-lib-ask-meta {
537
+ display: flex;
538
+ align-items: center;
539
+ gap: 0.5rem;
540
+ }
541
+
542
+ .ds-lib-ask-files {
543
+ display: flex;
544
+ flex-wrap: wrap;
545
+ justify-content: flex-end;
546
+ gap: 0.25rem;
547
+ margin: 0;
548
+ padding: 0;
549
+ list-style: none;
550
+ }
551
+
552
+ .ds-lib-ask-file {
553
+ display: flex;
554
+ max-width: 100%;
555
+ align-items: center;
556
+ gap: 0.625rem;
557
+ border: 1px solid var(--ds-color-border);
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;
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;
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);
585
+ }
586
+
587
+ .ds-lib-edit {
588
+ align-items: stretch;
589
+ }
590
+
591
+ .ds-lib-edit-text {
592
+ width: 100%;
593
+ resize: vertical;
594
+ border: 1px solid var(--ds-color-border);
595
+ border-radius: var(--ds-radius-lg);
596
+ background: var(--ds-color-surface-1);
597
+ padding: 0.5rem 0.75rem;
598
+ color: var(--ds-color-foreground);
599
+ font: inherit;
600
+ }
601
+
602
+ .ds-lib-edit-note {
603
+ margin: 0;
604
+ color: var(--ds-color-muted-foreground);
605
+ font-size: var(--ds-text-2xs);
606
+ }
607
+
608
+ .ds-lib-edit-actions {
609
+ display: flex;
610
+ justify-content: flex-end;
611
+ gap: 0.25rem;
612
+ }
613
+
398
614
  .ds-lib-ask-time,
399
615
  .ds-lib-answer-time {
400
616
  color: var(--ds-color-muted-foreground);
@@ -1,6 +1,8 @@
1
1
  import { type Artefact, type Block, type DescribeTool, type Outcome } from '../../transcript.svelte';
2
2
  import { type Citation } from '../../citations';
3
3
  import { type ChatCopy } from '../../copy';
4
+ import { type Written, type WrittenUrl } from '../../written';
5
+ import type { Visual } from '../../visuals';
4
6
  import type { Verdict } from '../../chat.svelte';
5
7
  /** Everything one turn renders from: what `Conversation` hands a host's
6
8
  * own `turn` snippet, so it can render this component or its own. */
@@ -35,6 +37,23 @@ export interface AgentTranscriptProps {
35
37
  onopenartefact?: () => void;
36
38
  /** The persona's own words for its tools, and what they read. */
37
39
  describeTool?: DescribeTool;
40
+ /** The names of the files that went with the question, shown as chips. */
41
+ files?: string[];
42
+ /** Replaces this question with `text` and asks it again from here
43
+ * (`Chat.edit`), resolving false when that did not go through. Omit
44
+ * and no edit control renders: not while an answer is being written. */
45
+ onedit?: (text: string) => Promise<boolean>;
46
+ /** The documents the persona wrote answering it, carded under the answer. */
47
+ written?: Written[];
48
+ /** The version the reading column shows now, by `writtenKey`. */
49
+ showingWritten?: string;
50
+ /** Opens a version in the reading column. Omit and the cards only link. */
51
+ onopenwritten?: (written: Written) => void;
52
+ /** Where a version downloads. Omit and no download links render. */
53
+ writtenUrl?: WrittenUrl;
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>;
38
57
  /** When an answer this page did not start began, epoch ms: it is still
39
58
  * being written, and the clock counts from then. */
40
59
  answering?: number;