@poodle64/librarian 2026.9.28 → 2026.10.0

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 (46) hide show
  1. package/README.md +66 -116
  2. package/dist/attachments.d.ts +2 -2
  3. package/dist/attachments.js +3 -3
  4. package/dist/chat.svelte.d.ts +22 -23
  5. package/dist/chat.svelte.js +41 -64
  6. package/dist/citations.d.ts +7 -7
  7. package/dist/citations.js +6 -6
  8. package/dist/client.d.ts +16 -45
  9. package/dist/client.js +15 -23
  10. package/dist/components/activity-group/activity-group.svelte +1 -1
  11. package/dist/components/agent-transcript/agent-transcript.svelte +9 -110
  12. package/dist/components/agent-transcript/agent-transcript.svelte.d.ts +4 -16
  13. package/dist/components/answer-mark/answer-mark.svelte +2 -2
  14. package/dist/components/answer-mark/answer-mark.svelte.d.ts +2 -2
  15. package/dist/components/answer-share/answer-share.svelte +2 -2
  16. package/dist/components/answer-share/answer-share.svelte.d.ts +2 -2
  17. package/dist/components/artefact-card/artefact-card.svelte +2 -2
  18. package/dist/components/artefact-card/artefact-card.svelte.d.ts +2 -2
  19. package/dist/components/artefact-pane/artefact-pane.svelte +2 -2
  20. package/dist/components/artefact-pane/artefact-pane.svelte.d.ts +2 -2
  21. package/dist/components/composer/composer.svelte +39 -112
  22. package/dist/components/composer/composer.svelte.d.ts +18 -18
  23. package/dist/components/composer/index.d.ts +1 -2
  24. package/dist/components/conversation/conversation.svelte +7 -18
  25. package/dist/components/conversation/conversation.svelte.d.ts +7 -12
  26. package/dist/components/conversation-list/conversation-list.svelte +2 -2
  27. package/dist/components/conversation-list/conversation-list.svelte.d.ts +2 -2
  28. package/dist/components/document-pane/document-pane.svelte +2 -2
  29. package/dist/components/document-pane/document-pane.svelte.d.ts +2 -2
  30. package/dist/components/fair-use-notice/fair-use-notice.svelte +2 -2
  31. package/dist/components/fair-use-notice/fair-use-notice.svelte.d.ts +2 -2
  32. package/dist/components/scope-statement/scope-statement.svelte +5 -5
  33. package/dist/components/scope-statement/scope-statement.svelte.d.ts +2 -2
  34. package/dist/components/source-list/source-list.svelte +2 -2
  35. package/dist/components/source-list/source-list.svelte.d.ts +2 -2
  36. package/dist/components/working/working.svelte +2 -2
  37. package/dist/components/working/working.svelte.d.ts +2 -2
  38. package/dist/copy.d.ts +16 -24
  39. package/dist/copy.js +10 -16
  40. package/dist/session.svelte.d.ts +1 -1
  41. package/dist/session.svelte.js +6 -7
  42. package/dist/transcript.svelte.d.ts +18 -32
  43. package/dist/transcript.svelte.js +19 -40
  44. package/package.json +1 -5
  45. package/dist/history.svelte.d.ts +0 -37
  46. package/dist/history.svelte.js +0 -59
@@ -24,7 +24,6 @@
24
24
  type Artefact,
25
25
  type Block,
26
26
  type DescribeTool,
27
- type Depth,
28
27
  type Outcome,
29
28
  type TextBlock
30
29
  } from '../../transcript.svelte';
@@ -34,7 +33,7 @@
34
33
  splitSources,
35
34
  type Citation
36
35
  } from '../../citations';
37
- import { DEFAULT_PERSONA, personaName, resolveCopy, type LibrarianCopy } from '../../copy';
36
+ import { DEFAULT_PERSONA, personaName, resolveCopy, type ChatCopy } from '../../copy';
38
37
  import type { Verdict } from '../../chat.svelte';
39
38
  import AnswerMark from '../answer-mark/answer-mark.svelte';
40
39
  import AnswerShare from '../answer-share/answer-share.svelte';
@@ -51,22 +50,16 @@
51
50
  blocks: Block[];
52
51
  outcome: Outcome | null;
53
52
  running: boolean;
54
- /** Sources from the library's `citations` frame. Absent falls back to
53
+ /** Sources from the app's `citations` frame. Absent falls back to
55
54
  * whatever a "## Sources" block in the prose says. */
56
55
  citations?: Citation[];
57
56
  /** Forwarded to Markdown; omit if the caller has no collections to chip. */
58
57
  collectionNames?: Set<string>;
59
- /** Follow-ups from the library's `suggestions` frame. */
60
- suggestions?: string[];
61
58
  /** Opens the document pane. Omit and chips render but do not open. */
62
59
  oncite?: (citation: Citation) => void;
63
60
  /** Offered on the LAST answer only — re-asks the same question. */
64
61
  onregenerate?: () => void;
65
- /** Asks a follow-up. Offered on the LAST answer only; omit and the
66
- * chips do not render, because a chip that does nothing is worse than
67
- * no chip. */
68
- onsuggest?: (question: string) => void;
69
- copy?: Partial<LibrarianCopy>;
62
+ copy?: Partial<ChatCopy>;
70
63
  /** The persona's display name, signed on the answer card. */
71
64
  name?: string;
72
65
  /** When the question was asked, epoch ms. Rendered as a clock time —
@@ -83,8 +76,6 @@
83
76
  onopenartefact?: () => void;
84
77
  /** The persona's own words for its tools, and what they read. */
85
78
  describeTool?: DescribeTool;
86
- /** The question waits behind somebody else's. */
87
- waiting?: boolean;
88
79
  /** When an answer this page did not start began, epoch ms: it is still
89
80
  * being written, and the clock counts from then. */
90
81
  answering?: number;
@@ -98,10 +89,6 @@
98
89
  * true once recorded. Omit and nothing about sharing renders: nothing
99
90
  * is shared unless the reader asks. */
100
91
  onshare?: (share: boolean) => Promise<boolean>;
101
- /** How hard the persona worked on this one, for the footer's cost line
102
- * (`Quick · 56 s · about A$0.34`). Absent renders the footer exactly
103
- * as it always has: the duration alone. */
104
- depth?: Depth;
105
92
  /** Prices `outcome.costUsd` in the host's own currency and words
106
93
  * ("about A$0.34"). The package never converts currency itself,
107
94
  * and a zero-cost run (a local model) shows no cost regardless. */
@@ -115,10 +102,8 @@
115
102
  running,
116
103
  citations = [],
117
104
  collectionNames = new Set(),
118
- suggestions = [],
119
105
  oncite,
120
106
  onregenerate,
121
- onsuggest,
122
107
  copy,
123
108
  name = DEFAULT_PERSONA,
124
109
  at,
@@ -126,12 +111,10 @@
126
111
  artefactOpen = false,
127
112
  onopenartefact,
128
113
  describeTool,
129
- waiting = false,
130
114
  answering,
131
115
  onmark,
132
116
  shared,
133
117
  onshare,
134
- depth,
135
118
  formatCost
136
119
  }: AgentTranscriptProps = $props();
137
120
 
@@ -214,7 +197,7 @@
214
197
  *
215
198
  * Reading none of this left a failed turn rendering as a clean, complete
216
199
  * answer: no banner, a duration badge, and — worse — the "answered without
217
- * a source" line, which is a claim about the shelf made off a run that
200
+ * a source" line, which is a claim about the sources made off a run that
218
201
  * never finished looking.
219
202
  */
220
203
  const failure = $derived(
@@ -248,13 +231,6 @@
248
231
  settled && hasAnswer && sources.length === 0 && Boolean(outcome) && !failure
249
232
  );
250
233
 
251
- // A follow-up is offered once. Clicking it asks the question, which puts a
252
- // new turn below this one — and leaving the row up for the moment before
253
- // that arrives invites a second click on a third suggestion, asking two
254
- // questions the reader only meant to ask one of.
255
- let used = $state(false);
256
- const followUps = $derived(!used && onsuggest ? suggestions : []);
257
-
258
234
  let copied = $state(false);
259
235
  // Not `copy`: the prop of that name is the package's own words, and a
260
236
  // function shadowing it here would be a redeclaration, not a shadow.
@@ -296,11 +272,7 @@
296
272
  {#if running && segments.length === 0}
297
273
  <Working
298
274
  copy={words}
299
- label={waiting
300
- ? words.waiting
301
- : answering !== undefined
302
- ? words.stillAnswering
303
- : undefined}
275
+ label={answering !== undefined ? words.stillAnswering : undefined}
304
276
  since={answering}
305
277
  />
306
278
  {/if}
@@ -332,7 +304,7 @@
332
304
  <SourceList {sources} {words} {oncite} />
333
305
  {/if}
334
306
 
335
- <!-- An empty `notHeld` is a host whose persona answers from no shelf,
307
+ <!-- An empty `notHeld` is a host whose persona answers from no sources,
336
308
  where "without a source" would be a claim about nothing. -->
337
309
  {#if unanswered && asked}
338
310
  <p class="ds-lib-not-held">{words.noAnswer}</p>
@@ -342,30 +314,6 @@
342
314
  <p class="ds-lib-not-held">{words.notHeld}</p>
343
315
  {/if}
344
316
 
345
- {#if !isArtefact && followUps.length > 0}
346
- <section>
347
- <h2 class="ds-lib-follow-heading">{words.suggestions}</h2>
348
- <!-- Wrapping, not scrolling: at 390 a question is most of a line, so
349
- a row of three would clip two of them to fragments. -->
350
- <ul class="ds-lib-chips">
351
- {#each followUps as followUp (followUp)}
352
- <li>
353
- <button
354
- type="button"
355
- class="ds-lib-chip"
356
- onclick={() => {
357
- used = true;
358
- onsuggest?.(followUp);
359
- }}
360
- >
361
- {followUp}
362
- </button>
363
- </li>
364
- {/each}
365
- </ul>
366
- </section>
367
- {/if}
368
-
369
317
  {#if failure}
370
318
  <p class="ds-lib-failure" role="alert">{failure}</p>
371
319
  {/if}
@@ -402,10 +350,10 @@
402
350
  {/if}
403
351
  <!-- What it took, which is not the same fact as when it was
404
352
  asked; both are on the card and neither stands in for the
405
- other. Depth and cost fold into the SAME line, never a
406
- second one, once the host offers either. -->
353
+ other. Cost folds into the SAME line, never a
354
+ second one, once the host offers it. -->
407
355
  {#if outcome && !failure}
408
- <span class="ds-lib-duration">{outcomeLine(outcome, depth, words, formatCost)}</span>
356
+ <span class="ds-lib-duration">{outcomeLine(outcome, formatCost)}</span>
409
357
  {/if}
410
358
  </div>
411
359
  {/if}
@@ -515,55 +463,6 @@
515
463
  font-size: 0.875rem;
516
464
  }
517
465
 
518
- .ds-lib-follow-heading {
519
- margin: 0 0 0.375rem;
520
- color: var(--ds-color-muted-foreground);
521
- font-family: inherit;
522
- font-size: var(--ds-text-2xs);
523
- font-weight: 500;
524
- letter-spacing: 0.05em;
525
- text-transform: uppercase;
526
- }
527
-
528
- .ds-lib-chips {
529
- display: flex;
530
- flex-wrap: wrap;
531
- gap: 0.5rem;
532
- margin: 0;
533
- padding: 0;
534
- list-style: none;
535
- }
536
-
537
- .ds-lib-chips li {
538
- max-width: 100%;
539
- }
540
-
541
- .ds-lib-chip {
542
- max-width: 100%;
543
- border: 1px solid var(--ds-color-border);
544
- border-radius: var(--ds-radius-full);
545
- background: none;
546
- padding: 0.5rem 0.875rem;
547
- color: var(--ds-color-foreground);
548
- font: inherit;
549
- font-size: 0.875rem;
550
- text-align: start;
551
- cursor: pointer;
552
- transition:
553
- background-color 150ms ease,
554
- border-color 150ms ease;
555
- }
556
-
557
- .ds-lib-chip:hover {
558
- border-color: var(--ds-color-border-strong);
559
- background: var(--ds-color-surface-1);
560
- }
561
-
562
- .ds-lib-chip:focus-visible {
563
- outline: 2px solid var(--ds-color-ring);
564
- outline-offset: 2px;
565
- }
566
-
567
466
  .ds-lib-failure {
568
467
  margin: 0;
569
468
  border: 1px solid color-mix(in oklab, var(--ds-color-status-error) 40%, transparent);
@@ -1,6 +1,6 @@
1
- import { type Artefact, type Block, type DescribeTool, type Depth, type Outcome } from '../../transcript.svelte';
1
+ import { type Artefact, type Block, type DescribeTool, type Outcome } from '../../transcript.svelte';
2
2
  import { type Citation } from '../../citations';
3
- import { type LibrarianCopy } from '../../copy';
3
+ import { type ChatCopy } from '../../copy';
4
4
  import type { Verdict } from '../../chat.svelte';
5
5
  /** Everything one turn renders from: what `Conversation` hands a host's
6
6
  * own `turn` snippet, so it can render this component or its own. */
@@ -9,22 +9,16 @@ export interface AgentTranscriptProps {
9
9
  blocks: Block[];
10
10
  outcome: Outcome | null;
11
11
  running: boolean;
12
- /** Sources from the library's `citations` frame. Absent falls back to
12
+ /** Sources from the app's `citations` frame. Absent falls back to
13
13
  * whatever a "## Sources" block in the prose says. */
14
14
  citations?: Citation[];
15
15
  /** Forwarded to Markdown; omit if the caller has no collections to chip. */
16
16
  collectionNames?: Set<string>;
17
- /** Follow-ups from the library's `suggestions` frame. */
18
- suggestions?: string[];
19
17
  /** Opens the document pane. Omit and chips render but do not open. */
20
18
  oncite?: (citation: Citation) => void;
21
19
  /** Offered on the LAST answer only — re-asks the same question. */
22
20
  onregenerate?: () => void;
23
- /** Asks a follow-up. Offered on the LAST answer only; omit and the
24
- * chips do not render, because a chip that does nothing is worse than
25
- * no chip. */
26
- onsuggest?: (question: string) => void;
27
- copy?: Partial<LibrarianCopy>;
21
+ copy?: Partial<ChatCopy>;
28
22
  /** The persona's display name, signed on the answer card. */
29
23
  name?: string;
30
24
  /** When the question was asked, epoch ms. Rendered as a clock time —
@@ -41,8 +35,6 @@ export interface AgentTranscriptProps {
41
35
  onopenartefact?: () => void;
42
36
  /** The persona's own words for its tools, and what they read. */
43
37
  describeTool?: DescribeTool;
44
- /** The question waits behind somebody else's. */
45
- waiting?: boolean;
46
38
  /** When an answer this page did not start began, epoch ms: it is still
47
39
  * being written, and the clock counts from then. */
48
40
  answering?: number;
@@ -56,10 +48,6 @@ export interface AgentTranscriptProps {
56
48
  * true once recorded. Omit and nothing about sharing renders: nothing
57
49
  * is shared unless the reader asks. */
58
50
  onshare?: (share: boolean) => Promise<boolean>;
59
- /** How hard the persona worked on this one, for the footer's cost line
60
- * (`Quick · 56 s · about A$0.34`). Absent renders the footer exactly
61
- * as it always has: the duration alone. */
62
- depth?: Depth;
63
51
  /** Prices `outcome.costUsd` in the host's own currency and words
64
52
  * ("about A$0.34"). The package never converts currency itself,
65
53
  * and a zero-cost run (a local model) shows no cost regardless. */
@@ -18,13 +18,13 @@
18
18
  import ThumbsDownIcon from '@lucide/svelte/icons/thumbs-down';
19
19
  import ThumbsUpIcon from '@lucide/svelte/icons/thumbs-up';
20
20
  import type { Verdict } from '../../chat.svelte';
21
- import { resolveCopy, type LibrarianCopy } from '../../copy';
21
+ import { resolveCopy, type ChatCopy } from '../../copy';
22
22
 
23
23
  interface Props {
24
24
  /** Records a verdict, replacing any before it. Resolves true once it is
25
25
  * recorded. */
26
26
  onmark: (verdict: Verdict) => Promise<boolean>;
27
- copy?: Partial<LibrarianCopy>;
27
+ copy?: Partial<ChatCopy>;
28
28
  }
29
29
 
30
30
  let { onmark, copy }: Props = $props();
@@ -1,10 +1,10 @@
1
1
  import type { Verdict } from '../../chat.svelte';
2
- import { type LibrarianCopy } from '../../copy';
2
+ import { type ChatCopy } from '../../copy';
3
3
  interface Props {
4
4
  /** Records a verdict, replacing any before it. Resolves true once it is
5
5
  * recorded. */
6
6
  onmark: (verdict: Verdict) => Promise<boolean>;
7
- copy?: Partial<LibrarianCopy>;
7
+ copy?: Partial<ChatCopy>;
8
8
  }
9
9
  declare const AnswerMark: import("svelte").Component<Props, {}, "">;
10
10
  type AnswerMark = ReturnType<typeof AnswerMark>;
@@ -13,7 +13,7 @@
13
13
  import CheckIcon from '@lucide/svelte/icons/check';
14
14
  import LinkIcon from '@lucide/svelte/icons/link';
15
15
  import Share2Icon from '@lucide/svelte/icons/share-2';
16
- import { resolveCopy, type LibrarianCopy } from '../../copy';
16
+ import { resolveCopy, type ChatCopy } from '../../copy';
17
17
 
18
18
  interface Props {
19
19
  /** Shares the answer (true) or stops sharing it (false), resolving true
@@ -22,7 +22,7 @@
22
22
  /** Where the read-only view lives while the answer is shared, as the
23
23
  * host's own address. */
24
24
  shared?: string;
25
- copy?: Partial<LibrarianCopy>;
25
+ copy?: Partial<ChatCopy>;
26
26
  }
27
27
 
28
28
  let { onshare, shared, copy }: Props = $props();
@@ -1,4 +1,4 @@
1
- import { type LibrarianCopy } from '../../copy';
1
+ import { type ChatCopy } from '../../copy';
2
2
  interface Props {
3
3
  /** Shares the answer (true) or stops sharing it (false), resolving true
4
4
  * once recorded. */
@@ -6,7 +6,7 @@ interface Props {
6
6
  /** Where the read-only view lives while the answer is shared, as the
7
7
  * host's own address. */
8
8
  shared?: string;
9
- copy?: Partial<LibrarianCopy>;
9
+ copy?: Partial<ChatCopy>;
10
10
  }
11
11
  declare const AnswerShare: import("svelte").Component<Props, {}, "">;
12
12
  type AnswerShare = ReturnType<typeof AnswerShare>;
@@ -12,7 +12,7 @@
12
12
  <script lang="ts">
13
13
  import FileTextIcon from '@lucide/svelte/icons/file-text';
14
14
  import type { Artefact } from '../../transcript.svelte';
15
- import { resolveCopy, type LibrarianCopy } from '../../copy';
15
+ import { resolveCopy, type ChatCopy } from '../../copy';
16
16
 
17
17
  interface Props {
18
18
  artefact: Artefact;
@@ -21,7 +21,7 @@
21
21
  /** Omit and the card informs but does not open: a host with nowhere
22
22
  * to show the artefact offers no action rather than a dead one. */
23
23
  onopen?: () => void;
24
- copy?: Partial<LibrarianCopy>;
24
+ copy?: Partial<ChatCopy>;
25
25
  }
26
26
 
27
27
  let { artefact, open = false, onopen, copy }: Props = $props();
@@ -1,5 +1,5 @@
1
1
  import type { Artefact } from '../../transcript.svelte';
2
- import { type LibrarianCopy } from '../../copy';
2
+ import { type ChatCopy } from '../../copy';
3
3
  interface Props {
4
4
  artefact: Artefact;
5
5
  /** The host is showing it now. */
@@ -7,7 +7,7 @@ interface Props {
7
7
  /** Omit and the card informs but does not open: a host with nowhere
8
8
  * to show the artefact offers no action rather than a dead one. */
9
9
  onopen?: () => void;
10
- copy?: Partial<LibrarianCopy>;
10
+ copy?: Partial<ChatCopy>;
11
11
  }
12
12
  declare const ArtefactCard: import("svelte").Component<Props, {}, "">;
13
13
  type ArtefactCard = ReturnType<typeof ArtefactCard>;
@@ -10,7 +10,7 @@
10
10
  import XIcon from '@lucide/svelte/icons/x';
11
11
  import { segment, type TextBlock, type Turn } from '../../transcript.svelte';
12
12
  import { resolveCitations, splitSources, type Citation } from '../../citations';
13
- import { resolveCopy, type LibrarianCopy } from '../../copy';
13
+ import { resolveCopy, type ChatCopy } from '../../copy';
14
14
  import Markdown from '../markdown/markdown.svelte';
15
15
  import SourceList from '../source-list/source-list.svelte';
16
16
 
@@ -21,7 +21,7 @@
21
21
  * stacking it beside the artefact. Omit and the sources still list, but
22
22
  * do not open. */
23
23
  oncite?: (citation: Citation) => void;
24
- copy?: Partial<LibrarianCopy>;
24
+ copy?: Partial<ChatCopy>;
25
25
  }
26
26
 
27
27
  let { turn, onclose, oncite, copy }: Props = $props();
@@ -1,6 +1,6 @@
1
1
  import { type Turn } from '../../transcript.svelte';
2
2
  import { type Citation } from '../../citations';
3
- import { type LibrarianCopy } from '../../copy';
3
+ import { type ChatCopy } from '../../copy';
4
4
  interface Props {
5
5
  turn: Turn;
6
6
  onclose: () => void;
@@ -8,7 +8,7 @@ interface Props {
8
8
  * stacking it beside the artefact. Omit and the sources still list, but
9
9
  * do not open. */
10
10
  oncite?: (citation: Citation) => void;
11
- copy?: Partial<LibrarianCopy>;
11
+ copy?: Partial<ChatCopy>;
12
12
  }
13
13
  declare const ArtefactPane: import("svelte").Component<Props, {}, "">;
14
14
  type ArtefactPane = ReturnType<typeof ArtefactPane>;
@@ -6,7 +6,7 @@
6
6
  The empty→typing transition lives almost entirely in that control — the box
7
7
  chrome grows with the text and nothing else moves.
8
8
 
9
- While Milton is answering the box is disabled and the send button becomes a
9
+ While the persona is answering the box is disabled and the send button becomes a
10
10
  stop button: an ask route takes one question per request, and a box that
11
11
  takes typing it will then discard is worse than one that plainly waits. A
12
12
  host whose session reads messages while it works (`sendWhileRunning`) keeps
@@ -26,30 +26,35 @@
26
26
  MAX_FILES,
27
27
  rejectionMessage
28
28
  } from '../../attachments';
29
- import type { Depth } from '../../client';
30
- import { DEFAULT_PERSONA, resolveCopy, type LibrarianCopy } from '../../copy';
29
+ import { DEFAULT_PERSONA, resolveCopy, type ChatCopy } from '../../copy';
31
30
 
32
31
  /** Past this many turns, the long-conversation banner offers to start
33
32
  * over: a follow-up re-reads the whole conversation, so each one costs
34
33
  * more than the last (measured — the longest ran 2.05M cached tokens). */
35
34
  const DEFAULT_TURN_LIMIT = 8;
36
35
 
37
- export type Scope = 'document' | 'collection' | 'library';
36
+ /** A collection a person may read, as the app lists it: `name` is what a
37
+ * question is narrowed to, `summary` is what the picker shows. */
38
+ export interface CollectionChoice {
39
+ name: string;
40
+ summary: string;
41
+ }
38
42
 
39
43
  interface Props {
40
44
  value: string;
41
45
  running: boolean;
42
- /** What the next message is about. A surface with one scope — a
43
- * session over one document — passes none and offers no choice. */
44
- scope?: Scope;
46
+ /** The collections this person may read. The picker renders when there
47
+ * are two or more: with one, or none, there is nothing to choose. */
48
+ collections?: CollectionChoice[];
49
+ /** The collection the next question is narrowed to, by name; undefined
50
+ * asks across all of them. */
51
+ collection?: string;
45
52
  /** Files to send with the next question. Cleared by the caller on send. */
46
53
  files?: File[];
47
- /** Names for the two narrower scopes; absent means that scope is unavailable. */
48
- documentName?: string;
49
- collectionName?: string;
50
54
  /** Hides the paperclip for a host whose ask route takes no files. */
51
55
  attachments?: boolean;
52
- onscope?: (scope: Scope) => void;
56
+ /** The person picked a collection by name, or all of them (undefined). */
57
+ oncollection?: (collection: string | undefined) => void;
53
58
  onsubmit: () => void;
54
59
  onstop: () => void;
55
60
  /** Asks for a study artefact instead of an ordinary answer. Omit and no
@@ -60,21 +65,16 @@
60
65
  * both placeholders are composed from it. */
61
66
  name?: string;
62
67
  /** Overrides for the package's own words. */
63
- copy?: Partial<LibrarianCopy>;
68
+ copy?: Partial<ChatCopy>;
64
69
  /** The session reads a message sent while it works — a job whose
65
70
  * route writes it onto the running process — so the box stays open
66
71
  * and Send sits beside Stop. Words only: the paperclip still waits for
67
72
  * the run. Absent, a chat waits for its answer. */
68
73
  sendWhileRunning?: boolean;
69
- /** One quiet line in the box's footer, where a scope choice would
74
+ /** One quiet line in the box's footer, where the collection picker would
70
75
  * sit, in the host's words: what sending does here ("It carries on
71
76
  * from where it stopped."). */
72
77
  note?: string;
73
- /** Quick or Thorough for the next question, as a two-way switch with a
74
- * one-line hint. Bind it and pass it to `ask()`; omit it and nothing
75
- * renders. Once shown it is never disabled or hidden, and never names
76
- * the model behind it. */
77
- depth?: Depth;
78
78
  /** How many turns are in the conversation so far. Past `turnLimit`
79
79
  * (8 by default), a quiet banner appears above the box offering to
80
80
  * start over. Omit and no banner ever renders — a host that does not
@@ -90,12 +90,11 @@
90
90
  let {
91
91
  value = $bindable(),
92
92
  running,
93
- scope,
93
+ collections = [],
94
+ collection,
94
95
  files = $bindable([]),
95
- documentName,
96
- collectionName,
97
96
  attachments = true,
98
- onscope,
97
+ oncollection,
99
98
  onsubmit,
100
99
  onstop,
101
100
  onbriefing,
@@ -103,7 +102,6 @@
103
102
  copy,
104
103
  note,
105
104
  sendWhileRunning = false,
106
- depth = $bindable(),
107
105
  turnCount,
108
106
  turnLimit = DEFAULT_TURN_LIMIT,
109
107
  onnewquestion
@@ -120,24 +118,11 @@
120
118
  turnCount !== undefined && turnCount > turnLimit && Boolean(onnewquestion)
121
119
  );
122
120
 
123
- // What Milton is being asked about — this document, this collection, or
124
- // the whole library. Callers that have no narrower scope pass neither
125
- // name, leaving "The whole library" the sole entry — and a sole entry is
126
- // not a choice, so `hasChoice` below hides it rather than rendering a
127
- // chip with nothing to switch to.
128
- const choices = $derived(
129
- [
130
- documentName ? { id: 'document' as const, label: documentName } : null,
131
- collectionName ? { id: 'collection' as const, label: collectionName } : null,
132
- { id: 'library' as const, label: 'The whole library' }
133
- ].filter((c) => c !== null)
134
- );
135
-
136
- // A room with one shelf set has one scope, fixed by the caller never
137
- // passing a name — no pick to make, so the row renders nothing rather
138
- // than a chip that only ever reselects itself (design-system, the
139
- // fixed-scope Composer defect).
140
- const hasChoice = $derived(scope !== undefined && choices.length > 1);
121
+ // Who the question is put to: every collection the person may read, or one
122
+ // of them. A person with one collection (or none) has nothing to pick, so
123
+ // the row renders nothing rather than a chip that only reselects itself.
124
+ // A collection is shown by its summary, the words a reader knows it by.
125
+ const hasChoice = $derived(collections.length > 1);
141
126
 
142
127
  // Three chips in a narrow column clipped all three to fragments
143
128
  // ("defence-s…", "All colle…"). Wrapping beats truncating: a chip a reader
@@ -166,7 +151,7 @@
166
151
  grow();
167
152
  });
168
153
 
169
- // Focus comes back once Milton is done, so a reader can keep going without
154
+ // Focus comes back once the persona is done, so a reader can keep going without
170
155
  // reaching for the mouse. It has to be the transition OUT of `waiting`: a
171
156
  // disabled element cannot hold focus, so the focus call in `submit()` below
172
157
  // survives only for a box that never disables — measured, focus lands on
@@ -321,41 +306,26 @@
321
306
  </button>
322
307
  {/if}
323
308
 
324
- {#if depth !== undefined}
325
- <div role="group" aria-label={words.depthGroupLabel} class="ds-lib-depth">
326
- <button
327
- type="button"
328
- aria-pressed={depth === 'quick'}
329
- class="ds-lib-depth-side"
330
- class:is-chosen={depth === 'quick'}
331
- onclick={() => (depth = 'quick')}
332
- >
333
- {words.depthQuick}
334
- </button>
309
+ {#if hasChoice}
310
+ <div role="group" aria-label={words.collectionsLabel} class="ds-lib-scopes">
335
311
  <button
336
312
  type="button"
337
- aria-pressed={depth === 'thorough'}
338
- class="ds-lib-depth-side"
339
- class:is-chosen={depth === 'thorough'}
340
- onclick={() => (depth = 'thorough')}
313
+ class="ds-lib-scope-chip"
314
+ aria-pressed={collection === undefined}
315
+ class:is-chosen={collection === undefined}
316
+ onclick={() => oncollection?.(undefined)}
341
317
  >
342
- {words.depthThorough}
318
+ {words.allCollections}
343
319
  </button>
344
- </div>
345
- <span class="ds-lib-note">
346
- {depth === 'quick' ? words.depthQuickHint : words.depthThoroughHint}
347
- </span>
348
- {/if}
349
- {#if hasChoice}
350
- <div class="ds-lib-scopes">
351
- {#each choices as choice (choice.id)}
320
+ {#each collections as choice (choice.name)}
352
321
  <button
353
322
  type="button"
354
323
  class="ds-lib-scope-chip"
355
- class:is-chosen={scope === choice.id}
356
- onclick={() => onscope?.(choice.id)}
324
+ aria-pressed={collection === choice.name}
325
+ class:is-chosen={collection === choice.name}
326
+ onclick={() => oncollection?.(choice.name)}
357
327
  >
358
- {choice.label}
328
+ {choice.summary || choice.name}
359
329
  </button>
360
330
  {/each}
361
331
  </div>
@@ -607,49 +577,6 @@
607
577
  font-size: var(--ds-text-2xs);
608
578
  }
609
579
 
610
- /* A labelled button group, never a select: two states a reader compares
611
- at a glance, not a list to open. 44px touch targets throughout — the
612
- one row here a colleague reaches for on every single question. */
613
- .ds-lib-depth {
614
- display: flex;
615
- flex: none;
616
- gap: 0.125rem;
617
- border-radius: var(--ds-radius-lg);
618
- background: var(--ds-color-surface-3);
619
- padding: 0.1875rem;
620
- }
621
-
622
- .ds-lib-depth-side {
623
- min-width: 2.75rem;
624
- min-height: 2.75rem;
625
- border: 0;
626
- border-radius: var(--ds-radius-md);
627
- background: none;
628
- padding: 0 0.75rem;
629
- color: var(--ds-color-muted-foreground);
630
- font: inherit;
631
- font-size: var(--ds-text-2xs);
632
- cursor: pointer;
633
- transition:
634
- color 150ms ease,
635
- background-color 150ms ease;
636
- }
637
-
638
- .ds-lib-depth-side:hover {
639
- color: var(--ds-color-foreground);
640
- }
641
-
642
- .ds-lib-depth-side.is-chosen {
643
- background: var(--ds-color-primary);
644
- color: var(--ds-color-primary-foreground);
645
- font-weight: 600;
646
- }
647
-
648
- .ds-lib-depth-side:focus-visible {
649
- outline: 2px solid var(--ds-color-ring);
650
- outline-offset: 2px;
651
- }
652
-
653
580
  /* Above the box, never inside it: it is about the conversation so far,
654
581
  not this one question. Warning-tinted, never error — nothing has gone
655
582
  wrong, a follow-up just costs more than the last one did. */