@poodle64/librarian 2026.9.13 → 2026.9.14

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.
@@ -25,7 +25,7 @@
25
25
  trustMark,
26
26
  type Citation
27
27
  } from '../../citations';
28
- import { resolveCopy, type LibrarianCopy } from '../../copy';
28
+ import { DEFAULT_COPY, resolveCopy, type LibrarianCopy } from '../../copy';
29
29
  import Working from '../working/working.svelte';
30
30
  import ActivityGroup from '../activity-group/activity-group.svelte';
31
31
  import ArtefactCard from '../artefact-card/artefact-card.svelte';
@@ -133,9 +133,15 @@
133
133
  * a duration badge, and — worse — the "he holds nothing on this" line,
134
134
  * which is a claim about the shelf made off a run that never finished
135
135
  * looking.
136
+ *
137
+ * `error` still carries `DEFAULT_COPY.unreachable` verbatim from `client.ts`
138
+ * — the stream layer has no `name` to compose with — so that exact default
139
+ * is swapped for `words.unreachable`, which `Conversation`'s `name` prop
140
+ * (or a host's own `copy` override) already resolved.
136
141
  */
137
142
  const failure = $derived(
138
- outcome?.error ?? (outcome?.isError ? words.answerFailed : null)
143
+ (outcome?.error === DEFAULT_COPY.unreachable ? words.unreachable : outcome?.error) ??
144
+ (outcome?.isError ? words.answerFailed : null)
139
145
  );
140
146
 
141
147
  // A failed turn settles too, and "Ask again" is the one thing a reader wants
@@ -44,6 +44,8 @@
44
44
  * control renders — studying is a mode of asking, never a tab, so a
45
45
  * host with nothing to build offers none rather than a disabled one. */
46
46
  onbriefing?: () => void;
47
+ /** The persona's display name; the placeholder in both states takes it. */
48
+ name?: string;
47
49
  }
48
50
 
49
51
  let {
@@ -57,7 +59,8 @@
57
59
  onscope,
58
60
  onsubmit,
59
61
  onstop,
60
- onbriefing
62
+ onbriefing,
63
+ name = 'Milton'
61
64
  }: Props = $props();
62
65
 
63
66
  // What Milton is being asked about — this document, this collection, or
@@ -214,7 +217,7 @@
214
217
  onkeydown={keydown}
215
218
  rows="1"
216
219
  disabled={running}
217
- placeholder={running ? 'Milton is answering…' : 'Ask Milton…'}
220
+ placeholder={running ? `${name} is answering…` : `Ask ${name}…`}
218
221
  class="text-foreground placeholder:text-muted-foreground w-full resize-none bg-transparent px-4 pt-3 pb-2 text-base outline-none disabled:opacity-60"
219
222
  ></textarea>
220
223
 
@@ -17,6 +17,8 @@ interface Props {
17
17
  * control renders — studying is a mode of asking, never a tab, so a
18
18
  * host with nothing to build offers none rather than a disabled one. */
19
19
  onbriefing?: () => void;
20
+ /** The persona's display name; the placeholder in both states takes it. */
21
+ name?: string;
20
22
  }
21
23
  declare const Composer: import("svelte").Component<Props, {}, "value" | "files">;
22
24
  type Composer = ReturnType<typeof Composer>;
@@ -31,6 +31,9 @@
31
31
  /** `Transcript.version` — bumped per event. Text grows in place, so a
32
32
  * count of turns is not enough to keep the scroll following. */
33
33
  version?: number;
34
+ /** The persona's display name. The aria-label, the `welcome` default and
35
+ * the unreachable alert all take it. */
36
+ name?: string;
34
37
  welcome?: string;
35
38
  /** Up to three, shown in the empty state as tappable pills. */
36
39
  examples?: string[];
@@ -59,7 +62,8 @@
59
62
  turns,
60
63
  running,
61
64
  version = 0,
62
- welcome = 'Ask Milton a question about the library.',
65
+ name = 'Milton',
66
+ welcome = `Ask ${name} a question about the library.`,
63
67
  examples = [],
64
68
  onexample,
65
69
  scope,
@@ -73,6 +77,12 @@
73
77
 
74
78
  const words = $derived(resolveCopy(copy));
75
79
 
80
+ // A host's own `copy.unreachable` still wins: it comes last in the spread.
81
+ const effectiveCopy = $derived<Partial<LibrarianCopy>>({
82
+ unreachable: `${name} can't be reached right now.`,
83
+ ...copy
84
+ });
85
+
76
86
  let viewport = $state<HTMLElement | null>(null);
77
87
  let column = $state<Column>(null);
78
88
  const follow = new FollowScroll();
@@ -160,7 +170,7 @@
160
170
  role="log"
161
171
  aria-live="polite"
162
172
  aria-busy={running}
163
- aria-label="Conversation with Milton"
173
+ aria-label="Conversation with {name}"
164
174
  class="min-h-0 flex-1 overflow-y-auto overscroll-contain"
165
175
  >
166
176
  <div class="mx-auto flex w-full max-w-[46rem] flex-col gap-8 px-4 py-6">
@@ -190,7 +200,7 @@
190
200
  sit at the top of the same column so a reader meets them in
191
201
  the same order whether or not they have asked anything yet. -->
192
202
  {#if scope}
193
- <ScopeStatement statement={scope} expanded={turns.length === 0} {copy} />
203
+ <ScopeStatement statement={scope} expanded={turns.length === 0} copy={effectiveCopy} />
194
204
  {/if}
195
205
 
196
206
  {#each turns as turn, index (turn.id)}
@@ -202,7 +212,7 @@
202
212
  citations={turn.citations ?? []}
203
213
  suggestions={turn.suggestions ?? []}
204
214
  {collectionNames}
205
- {copy}
215
+ copy={effectiveCopy}
206
216
  kind={turn.kind}
207
217
  title={turn.title}
208
218
  onopenartefact={() => openArtefact(turn)}
@@ -235,11 +245,16 @@
235
245
  </div>
236
246
 
237
247
  {#if column?.kind === 'document' && loadDocument}
238
- <DocumentPane citation={column.citation} {loadDocument} {copy} onclose={() => (column = null)} />
248
+ <DocumentPane
249
+ citation={column.citation}
250
+ {loadDocument}
251
+ copy={effectiveCopy}
252
+ onclose={() => (column = null)}
253
+ />
239
254
  {:else if column?.kind === 'artefact'}
240
255
  <ArtefactPane
241
256
  turn={column.turn}
242
- {copy}
257
+ copy={effectiveCopy}
243
258
  onclose={() => (column = null)}
244
259
  oncite={(citation) => cite(citation)}
245
260
  />
@@ -8,6 +8,9 @@ interface Props {
8
8
  /** `Transcript.version` — bumped per event. Text grows in place, so a
9
9
  * count of turns is not enough to keep the scroll following. */
10
10
  version?: number;
11
+ /** The persona's display name. The aria-label, the `welcome` default and
12
+ * the unreachable alert all take it. */
13
+ name?: string;
11
14
  welcome?: string;
12
15
  /** Up to three, shown in the empty state as tappable pills. */
13
16
  examples?: string[];
package/dist/copy.d.ts CHANGED
@@ -21,6 +21,9 @@ export interface LibrarianCopy {
21
21
  notHeld: string;
22
22
  /** Shown when the run itself ended in error and said nothing about why. */
23
23
  answerFailed: string;
24
+ /** Shown when the stream never opened at all. `Conversation`'s `name`
25
+ * prop composes this default; a host overriding `copy` directly wins. */
26
+ unreachable: string;
24
27
  /** Heading over the list of what an answer cited. */
25
28
  sources: string;
26
29
  /** Heading over the follow-up questions the librarian offered. */
package/dist/copy.js CHANGED
@@ -23,6 +23,7 @@ export const DEFAULT_COPY = {
23
23
  // A run can end `is_error` carrying no message at all, and a turn that
24
24
  // simply stops is the one thing a reader must not have to guess about.
25
25
  answerFailed: 'Milton stopped before he finished this one.',
26
+ unreachable: "Milton can't be reached right now.",
26
27
  sources: 'Sources',
27
28
  suggestions: 'Ask next',
28
29
  scope: 'What Milton answers from',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poodle64/librarian",
3
- "version": "2026.9.13",
3
+ "version": "2026.9.14",
4
4
  "description": "Milton's conversation surface as a consumable Svelte 5 package: the stream client, transcript state and chat components (transcript, composer, markdown) every household app renders instead of rebuilding.",
5
5
  "type": "module",
6
6
  "license": "MIT",