@poodle64/librarian 2026.9.11 → 2026.9.13

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.
package/README.md CHANGED
@@ -24,6 +24,8 @@ src/lib/
24
24
  scope-statement/ what this room answers from, and what it does not hold
25
25
  agent-transcript/ one question and everything Milton did answering it
26
26
  document-pane/ the cited document, open at the cited passage
27
+ artefact-card/ a study artefact's card in the transcript
28
+ artefact-pane/ a study artefact, open in the reading column
27
29
  composer/ the input box: attachments, scope chips, send/stop
28
30
  markdown/ sanitised, streaming-safe markdown + highlighting
29
31
  activity-group/ a whole investigation, as one quiet line
@@ -153,14 +155,14 @@ on it fires once and never again.
153
155
 
154
156
  | Concern | How |
155
157
  | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
156
- | The ask route | `ask({ endpoint })`: a room's `/api/rooms/{id}/ask`, a caller's `/api/caller/ask` |
158
+ | The ask route | `ask({ endpoint })`: a room's `/api/rooms/{id}/ask`, a caller's `/api/caller/ask` |
157
159
  | Attachments | nothing: `ask()` posts multipart (`question`, `resume`, `collections[]`, `files[]`) whenever `files` is non-empty, and JSON when it is not. The route must accept both |
158
160
  | Reading a cited document | `loadDocument(document_id) => Promise<{title, sections: [{anchor, heading, text}]}>`, proxied through the app's own authenticated route (cadmus: `GET /api/sources/documents/{id}/content`) |
159
- | Asking again | `onregenerate`: re-send the last question as a NEW turn; the package exposes the action and never re-asks by itself |
161
+ | Asking again | `onregenerate`: re-send the last question as a NEW turn; the package exposes the action and never re-asks by itself |
160
162
  | Asking a follow-up | `onsuggest(question)`: ask it as a NEW turn. Without the handler the chips do not render at all — a chip that does nothing is worse than no chip |
161
163
  | The empty state | `welcome` and up to three `examples` |
162
164
  | What this surface covers | `scope`: one statement per room, in the host's own words. Rendered above the first turn and folded to a line once the conversation starts |
163
- | The words themselves | `copy`: a partial of `LibrarianCopy`. Every component resolves it itself, so overriding one line does not mean restating the rest |
165
+ | The words themselves | `copy`: a partial of `LibrarianCopy`. Every component resolves it itself, so overriding one line does not mean restating the rest |
164
166
 
165
167
  Nothing here fetches on its own behalf. The library's document read is
166
168
  authenticated, and a package that called it directly would be reaching past
package/dist/client.d.ts CHANGED
@@ -57,6 +57,11 @@ export interface AskOptions {
57
57
  collections?: string[];
58
58
  /** Files Milton reads for this question. Switches the request to multipart. */
59
59
  files?: File[];
60
+ /** What kind of turn this ask should become. Absent means an ordinary
61
+ * question; a host that offers a study artefact sends its own kind
62
+ * (`'briefing'`) and interprets what comes back accordingly — the wire
63
+ * vocabulary is the host's own, never a fixed set here. */
64
+ kind?: string;
60
65
  signal?: AbortSignal;
61
66
  /** Where the ask lands. Each app mounts its own ask route. */
62
67
  endpoint?: string;
package/dist/client.js CHANGED
@@ -29,7 +29,8 @@ function requestInit(options, signal) {
29
29
  question: options.question,
30
30
  resume: options.resume ?? null,
31
31
  subtree: options.subtree ?? '',
32
- collections: options.collections ?? []
32
+ collections: options.collections ?? [],
33
+ kind: options.kind
33
34
  }),
34
35
  signal
35
36
  };
@@ -42,6 +43,8 @@ function requestInit(options, signal) {
42
43
  form.append('collections[]', collection);
43
44
  for (const file of options.files)
44
45
  form.append('files[]', file, file.name);
46
+ if (options.kind)
47
+ form.append('kind', options.kind);
45
48
  return { method: 'POST', credentials: 'include', body: form, signal };
46
49
  }
47
50
  /** Async-iterate the events of one question. */
@@ -28,6 +28,7 @@
28
28
  import { resolveCopy, type LibrarianCopy } from '../../copy';
29
29
  import Working from '../working/working.svelte';
30
30
  import ActivityGroup from '../activity-group/activity-group.svelte';
31
+ import ArtefactCard from '../artefact-card/artefact-card.svelte';
31
32
  import Markdown from '../markdown/markdown.svelte';
32
33
 
33
34
  interface Props {
@@ -51,6 +52,14 @@
51
52
  * no chip. */
52
53
  onsuggest?: (question: string) => void;
53
54
  copy?: Partial<LibrarianCopy>;
55
+ /** A study artefact rather than an ordinary answer: once settled, this
56
+ * renders as a card instead of prose. */
57
+ kind?: 'answer' | 'artefact';
58
+ /** The artefact's own name, for the card. */
59
+ title?: string;
60
+ /** Opens the artefact in the reading column. Required wherever `kind`
61
+ * is `'artefact'`. */
62
+ onopenartefact?: () => void;
54
63
  }
55
64
 
56
65
  let {
@@ -64,9 +73,14 @@
64
73
  oncite,
65
74
  onregenerate,
66
75
  onsuggest,
67
- copy
76
+ copy,
77
+ kind = 'answer',
78
+ title,
79
+ onopenartefact
68
80
  }: Props = $props();
69
81
 
82
+ const isArtefact = $derived(kind === 'artefact');
83
+
70
84
  const words = $derived(resolveCopy(copy));
71
85
 
72
86
  const asked = $derived(readerQuestion(question));
@@ -181,7 +195,7 @@
181
195
  {#each segments as seg (seg.index)}
182
196
  {#if seg.kind === 'activity'}
183
197
  <ActivityGroup group={seg} live={running && seg.index === lastIndex} />
184
- {:else}
198
+ {:else if !isArtefact}
185
199
  <div class="max-w-[72ch] min-w-0">
186
200
  <Markdown
187
201
  content={seg.index === lastTextIndex && split.citations.length > 0
@@ -199,7 +213,11 @@
199
213
  {/if}
200
214
  {/each}
201
215
 
202
- {#if sources.length > 0}
216
+ {#if isArtefact && settled}
217
+ <ArtefactCard title={title ?? 'Briefing'} citationCount={sources.length} onopen={() => onopenartefact?.()} />
218
+ {/if}
219
+
220
+ {#if !isArtefact && sources.length > 0}
203
221
  <section class="max-w-[72ch]">
204
222
  <h2 class="text-muted-foreground mb-1.5 text-xs font-medium tracking-wide uppercase">
205
223
  {words.sources}
@@ -237,11 +255,11 @@
237
255
  </section>
238
256
  {/if}
239
257
 
240
- {#if notHeld}
258
+ {#if !isArtefact && notHeld}
241
259
  <p class="text-muted-foreground max-w-[72ch] text-sm">{words.notHeld}</p>
242
260
  {/if}
243
261
 
244
- {#if followUps.length > 0}
262
+ {#if !isArtefact && followUps.length > 0}
245
263
  <section class="max-w-[72ch]">
246
264
  <h2 class="text-muted-foreground mb-1.5 text-xs font-medium tracking-wide uppercase">
247
265
  {words.suggestions}
@@ -276,7 +294,7 @@
276
294
  </p>
277
295
  {/if}
278
296
 
279
- {#if settled}
297
+ {#if !isArtefact && settled}
280
298
  <div class="text-muted-foreground -ml-1.5 flex items-center gap-1">
281
299
  {#if hasAnswer}
282
300
  <button
@@ -22,6 +22,14 @@ interface Props {
22
22
  * no chip. */
23
23
  onsuggest?: (question: string) => void;
24
24
  copy?: Partial<LibrarianCopy>;
25
+ /** A study artefact rather than an ordinary answer: once settled, this
26
+ * renders as a card instead of prose. */
27
+ kind?: 'answer' | 'artefact';
28
+ /** The artefact's own name, for the card. */
29
+ title?: string;
30
+ /** Opens the artefact in the reading column. Required wherever `kind`
31
+ * is `'artefact'`. */
32
+ onopenartefact?: () => void;
25
33
  }
26
34
  declare const AgentTranscript: import("svelte").Component<Props, {}, "">;
27
35
  type AgentTranscript = ReturnType<typeof AgentTranscript>;
@@ -0,0 +1,40 @@
1
+ <!--
2
+ A study artefact's card in the transcript: what a colleague taps to read it.
3
+ It is not the artefact itself — that lives in the reading column
4
+ (`ArtefactPane`), never stacked beside this card and never inline as prose,
5
+ which is the whole of the one-surface decision this package exists to keep
6
+ every consumer honest about.
7
+ -->
8
+ <script lang="ts">
9
+ import FileTextIcon from '@lucide/svelte/icons/file-text';
10
+ import Panel from '@poodle64/ui/panel';
11
+
12
+ interface Props {
13
+ title: string;
14
+ citationCount: number;
15
+ onopen: () => void;
16
+ }
17
+
18
+ let { title, citationCount, onopen }: Props = $props();
19
+ </script>
20
+
21
+ <Panel
22
+ icon={FileTextIcon}
23
+ {title}
24
+ subtitle="Briefing doc"
25
+ role="button"
26
+ tabindex={0}
27
+ onclick={onopen}
28
+ onkeydown={(event: KeyboardEvent) => {
29
+ if (event.key !== 'Enter' && event.key !== ' ') return;
30
+ event.preventDefault();
31
+ onopen();
32
+ }}
33
+ class="hover:border-border-strong focus-visible:ring-ring w-full max-w-[28rem] cursor-pointer text-left transition-colors focus-visible:ring-2 focus-visible:outline-none"
34
+ >
35
+ {#snippet children()}
36
+ <p class="text-muted-foreground text-xs">
37
+ {citationCount} source{citationCount === 1 ? '' : 's'}
38
+ </p>
39
+ {/snippet}
40
+ </Panel>
@@ -0,0 +1,8 @@
1
+ interface Props {
2
+ title: string;
3
+ citationCount: number;
4
+ onopen: () => void;
5
+ }
6
+ declare const ArtefactCard: import("svelte").Component<Props, {}, "">;
7
+ type ArtefactCard = ReturnType<typeof ArtefactCard>;
8
+ export default ArtefactCard;
@@ -0,0 +1 @@
1
+ export { default } from './artefact-card.svelte';
@@ -0,0 +1 @@
1
+ export { default } from './artefact-card.svelte';
@@ -0,0 +1,120 @@
1
+ <!--
2
+ A study artefact, open in the reading column.
3
+
4
+ The same shell shape `DocumentPane` uses (a column at `lg`, a bottom sheet
5
+ below it) because decision 2 of the ask-milton surface is that a colleague
6
+ never has to learn which of the two things is in the column before they
7
+ open it — only ever one, in the same place, closing whichever was there.
8
+ -->
9
+ <script lang="ts">
10
+ import XIcon from '@lucide/svelte/icons/x';
11
+ import { segment, type TextBlock, type Turn } from '../../transcript.svelte';
12
+ import { resolveCitations, splitSources, trustMark, type Citation } from '../../citations';
13
+ import { resolveCopy, type LibrarianCopy } from '../../copy';
14
+ import Markdown from '../markdown/markdown.svelte';
15
+
16
+ interface Props {
17
+ turn: Turn;
18
+ onclose: () => void;
19
+ /** Opens the cited document instead — swapping the column back, never
20
+ * stacking it beside the artefact. Omit and the sources still list, but
21
+ * do not open. */
22
+ oncite?: (citation: Citation) => void;
23
+ copy?: Partial<LibrarianCopy>;
24
+ }
25
+
26
+ let { turn, onclose, oncite, copy }: Props = $props();
27
+
28
+ const words = $derived(resolveCopy(copy));
29
+
30
+ const texts = $derived(segment(turn.blocks).filter((s): s is TextBlock => s.kind === 'text'));
31
+ const rawAnswer = $derived(texts.map((t) => t.text).join('\n\n'));
32
+ const split = $derived(splitSources(rawAnswer));
33
+ const sources = $derived(resolveCitations(turn.citations ?? [], split.citations));
34
+
35
+ let closeButton = $state<HTMLButtonElement | null>(null);
36
+
37
+ // Opening a pane a reader reached with the keyboard must move focus into
38
+ // it, or Escape and Tab both act on the transcript behind it.
39
+ $effect(() => {
40
+ const returnTo = globalThis.document?.activeElement as HTMLElement | null;
41
+ closeButton?.focus();
42
+ return () => returnTo?.focus?.();
43
+ });
44
+
45
+ function keydown(event: KeyboardEvent) {
46
+ if (event.key === 'Escape') {
47
+ event.stopPropagation();
48
+ onclose();
49
+ }
50
+ }
51
+ </script>
52
+
53
+ <svelte:window onkeydown={keydown} />
54
+
55
+ <aside
56
+ aria-label="Study artefact"
57
+ class="bg-surface-1 border-border fixed inset-x-0 bottom-0 z-40 flex h-[80svh] flex-col rounded-t-2xl border-t shadow-2xl lg:relative lg:h-auto lg:w-[560px] lg:shrink-0 lg:rounded-none lg:border-t-0 lg:border-l lg:shadow-none"
58
+ >
59
+ <!-- The sheet's grabber. Below `lg` this is an overlay a reader has to be
60
+ able to see the top edge of; on a desktop it is a column, so there is
61
+ nothing here to grab. -->
62
+ <div class="flex justify-center pt-2 pb-1 lg:hidden" aria-hidden="true">
63
+ <span class="bg-border h-1 w-9 rounded-full"></span>
64
+ </div>
65
+
66
+ <header class="border-border flex items-start gap-2 border-b px-4 py-3 lg:pt-3">
67
+ <div class="min-w-0 flex-1">
68
+ <h2 class="text-foreground truncate text-sm font-semibold">{turn.title ?? 'Briefing'}</h2>
69
+ <p class="text-muted-foreground truncate text-xs">
70
+ Briefing doc · {sources.length} source{sources.length === 1 ? '' : 's'}
71
+ </p>
72
+ </div>
73
+ <button
74
+ bind:this={closeButton}
75
+ type="button"
76
+ onclick={onclose}
77
+ aria-label={words.closeSource}
78
+ class="text-muted-foreground hover:text-foreground hover:bg-surface-2 focus-visible:ring-ring -mt-1 flex size-8 shrink-0 items-center justify-center rounded-lg transition-colors focus-visible:ring-2 focus-visible:outline-none"
79
+ >
80
+ <XIcon class="size-4" />
81
+ </button>
82
+ </header>
83
+
84
+ <div class="min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 py-3">
85
+ <Markdown content={split.citations.length > 0 ? split.body : rawAnswer} />
86
+
87
+ {#if sources.length > 0}
88
+ <section class="mt-6">
89
+ <h3 class="text-muted-foreground mb-1.5 text-xs font-medium tracking-wide uppercase">
90
+ {words.sources}
91
+ </h3>
92
+ <ol class="flex flex-col gap-1">
93
+ {#each sources as source (source.n)}
94
+ {@const mark = trustMark(source, words)}
95
+ <li>
96
+ <button
97
+ type="button"
98
+ onclick={() => oncite?.(source)}
99
+ disabled={!source.document_id || !oncite}
100
+ class="border-border hover:border-border-strong hover:bg-surface-2 focus-visible:ring-ring flex w-full items-baseline gap-2 rounded-lg border px-2.5 py-1.5 text-left text-sm transition-colors focus-visible:ring-2 focus-visible:outline-none disabled:cursor-default disabled:hover:bg-transparent"
101
+ >
102
+ <span
103
+ class="bg-primary/15 text-foreground shrink-0 rounded px-1.5 font-mono text-xs tabular-nums"
104
+ >{source.n}</span
105
+ >
106
+ <span class="min-w-0">
107
+ <span class="text-foreground">{source.title}</span>
108
+ {#if source.section}<span class="text-muted-foreground">
109
+ · {source.section}</span
110
+ >{/if}
111
+ {#if mark}<span class="text-muted-foreground/80"> · {mark}</span>{/if}
112
+ </span>
113
+ </button>
114
+ </li>
115
+ {/each}
116
+ </ol>
117
+ </section>
118
+ {/if}
119
+ </div>
120
+ </aside>
@@ -0,0 +1,15 @@
1
+ import { type Turn } from '../../transcript.svelte';
2
+ import { type Citation } from '../../citations';
3
+ import { type LibrarianCopy } from '../../copy';
4
+ interface Props {
5
+ turn: Turn;
6
+ onclose: () => void;
7
+ /** Opens the cited document instead — swapping the column back, never
8
+ * stacking it beside the artefact. Omit and the sources still list, but
9
+ * do not open. */
10
+ oncite?: (citation: Citation) => void;
11
+ copy?: Partial<LibrarianCopy>;
12
+ }
13
+ declare const ArtefactPane: import("svelte").Component<Props, {}, "">;
14
+ type ArtefactPane = ReturnType<typeof ArtefactPane>;
15
+ export default ArtefactPane;
@@ -0,0 +1 @@
1
+ export { default } from './artefact-pane.svelte';
@@ -0,0 +1 @@
1
+ export { default } from './artefact-pane.svelte';
@@ -12,6 +12,7 @@
12
12
  -->
13
13
  <script lang="ts">
14
14
  import ArrowUpIcon from '@lucide/svelte/icons/arrow-up';
15
+ import FileTextIcon from '@lucide/svelte/icons/file-text';
15
16
  import PaperclipIcon from '@lucide/svelte/icons/paperclip';
16
17
  import SquareIcon from '@lucide/svelte/icons/square';
17
18
  import XIcon from '@lucide/svelte/icons/x';
@@ -39,6 +40,10 @@
39
40
  onscope: (scope: Scope) => void;
40
41
  onsubmit: () => void;
41
42
  onstop: () => void;
43
+ /** Asks for a study artefact instead of an ordinary answer. Omit and no
44
+ * control renders — studying is a mode of asking, never a tab, so a
45
+ * host with nothing to build offers none rather than a disabled one. */
46
+ onbriefing?: () => void;
42
47
  }
43
48
 
44
49
  let {
@@ -51,7 +56,8 @@
51
56
  attachments = true,
52
57
  onscope,
53
58
  onsubmit,
54
- onstop
59
+ onstop,
60
+ onbriefing
55
61
  }: Props = $props();
56
62
 
57
63
  // What Milton is being asked about — this document, this collection, or
@@ -235,6 +241,19 @@
235
241
  </button>
236
242
  {/if}
237
243
 
244
+ {#if onbriefing}
245
+ <button
246
+ type="button"
247
+ onclick={onbriefing}
248
+ disabled={running}
249
+ aria-label="Ask for a briefing"
250
+ title="Ask for a briefing"
251
+ class="text-muted-foreground hover:text-foreground hover:bg-surface-2 focus-visible:ring-ring flex size-8 shrink-0 items-center justify-center rounded-full transition-colors focus-visible:ring-2 focus-visible:outline-none disabled:opacity-30"
252
+ >
253
+ <FileTextIcon class="size-4" />
254
+ </button>
255
+ {/if}
256
+
238
257
  {#if hasChoice}
239
258
  <div class="flex min-w-0 flex-wrap items-center gap-1">
240
259
  {#each choices as choice (choice.id)}
@@ -13,6 +13,10 @@ interface Props {
13
13
  onscope: (scope: Scope) => void;
14
14
  onsubmit: () => void;
15
15
  onstop: () => void;
16
+ /** Asks for a study artefact instead of an ordinary answer. Omit and no
17
+ * control renders — studying is a mode of asking, never a tab, so a
18
+ * host with nothing to build offers none rather than a disabled one. */
19
+ onbriefing?: () => void;
16
20
  }
17
21
  declare const Composer: import("svelte").Component<Props, {}, "value" | "files">;
18
22
  type Composer = ReturnType<typeof Composer>;
@@ -16,9 +16,15 @@
16
16
  import { resolveCopy, type LibrarianCopy } from '../../copy';
17
17
  import { FollowScroll } from '../../follow-scroll.svelte';
18
18
  import AgentTranscript from '../agent-transcript/agent-transcript.svelte';
19
+ import ArtefactPane from '../artefact-pane/artefact-pane.svelte';
19
20
  import DocumentPane from '../document-pane/document-pane.svelte';
20
21
  import ScopeStatement from '../scope-statement/scope-statement.svelte';
21
22
 
23
+ /** What the reading column holds — a cited document or a study artefact,
24
+ * never both and never two panes: whichever the reader last opened
25
+ * replaces whatever was there. */
26
+ type Column = { kind: 'document'; citation: Citation } | { kind: 'artefact'; turn: Turn } | null;
27
+
22
28
  interface Props {
23
29
  turns: Turn[];
24
30
  running: boolean;
@@ -68,7 +74,7 @@
68
74
  const words = $derived(resolveCopy(copy));
69
75
 
70
76
  let viewport = $state<HTMLElement | null>(null);
71
- let open = $state<Citation | null>(null);
77
+ let column = $state<Column>(null);
72
78
  const follow = new FollowScroll();
73
79
 
74
80
  function metrics(el: HTMLElement) {
@@ -101,6 +107,25 @@
101
107
  viewport.scrollTop = viewport.scrollHeight;
102
108
  });
103
109
 
110
+ // Asking for a briefing puts its card in the transcript AND opens the
111
+ // artefact in the column, closing whatever document was there — a
112
+ // colleague who just asked for one is asking to read it, not merely to
113
+ // see that it exists. Fires on the RUNNING → settled transition only, so
114
+ // reopening a past conversation whose last turn happens to be an
115
+ // artefact does not reopen a pane the reader may have since closed.
116
+ //
117
+ // Plain, not `$state`: read and written in the same effect, which would
118
+ // otherwise re-trigger itself forever (the pattern this file's `seen`
119
+ // already uses for the same reason).
120
+ let wasRunning = false;
121
+ $effect(() => {
122
+ const last = turns.at(-1);
123
+ if (wasRunning && !running && last?.kind === 'artefact' && !last.outcome?.isError) {
124
+ column = { kind: 'artefact', turn: last };
125
+ }
126
+ wasRunning = running;
127
+ });
128
+
104
129
  function scrolled() {
105
130
  if (viewport) follow.measure(metrics(viewport));
106
131
  }
@@ -113,8 +138,13 @@
113
138
 
114
139
  function cite(citation: Citation) {
115
140
  // A citation derived from the prose has no id to read, so its chip is
116
- // legible but inert rather than opening an empty pane.
117
- if (loadDocument && citation.document_id) open = citation;
141
+ // legible but inert rather than opening an empty pane. Replaces
142
+ // whatever the column held, artefact included — never a second pane.
143
+ if (loadDocument && citation.document_id) column = { kind: 'document', citation };
144
+ }
145
+
146
+ function openArtefact(turn: Turn) {
147
+ column = { kind: 'artefact', turn };
118
148
  }
119
149
  </script>
120
150
 
@@ -173,6 +203,9 @@
173
203
  suggestions={turn.suggestions ?? []}
174
204
  {collectionNames}
175
205
  {copy}
206
+ kind={turn.kind}
207
+ title={turn.title}
208
+ onopenartefact={() => openArtefact(turn)}
176
209
  oncite={cite}
177
210
  onregenerate={index === turns.length - 1 && !running ? onregenerate : undefined}
178
211
  onsuggest={index === turns.length - 1 && !running ? onsuggest : undefined}
@@ -201,7 +234,14 @@
201
234
 
202
235
  </div>
203
236
 
204
- {#if open && loadDocument}
205
- <DocumentPane citation={open} {loadDocument} {copy} onclose={() => (open = null)} />
237
+ {#if column?.kind === 'document' && loadDocument}
238
+ <DocumentPane citation={column.citation} {loadDocument} {copy} onclose={() => (column = null)} />
239
+ {:else if column?.kind === 'artefact'}
240
+ <ArtefactPane
241
+ turn={column.turn}
242
+ {copy}
243
+ onclose={() => (column = null)}
244
+ oncite={(citation) => cite(citation)}
245
+ />
206
246
  {/if}
207
247
  </div>
@@ -123,6 +123,12 @@ export interface Turn {
123
123
  /** Follow-ups offered after this answer. Absent on a turn read back from
124
124
  * history: they belonged to the moment it was asked. */
125
125
  suggestions?: string[];
126
+ /** A study artefact rather than an ordinary answer: renders as a card once
127
+ * settled, never as prose, and opens the reading column instead of a
128
+ * citation. Absent (or `'answer'`) is every ordinary turn. */
129
+ kind?: 'answer' | 'artefact';
130
+ /** The artefact's own name. Only meaningful when `kind` is `'artefact'`. */
131
+ title?: string;
126
132
  }
127
133
  /**
128
134
  * Fold a turn's flat block list into what a reader should actually see.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poodle64/librarian",
3
- "version": "2026.9.11",
3
+ "version": "2026.9.13",
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",