@poodle64/librarian 2026.9.9 → 2026.9.11
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 +23 -5
- package/dist/citations.js +4 -1
- package/dist/components/activity-group/activity-group.svelte +5 -0
- package/dist/components/agent-transcript/agent-transcript.svelte +24 -5
- package/dist/components/thinking-row/thinking-row.svelte +13 -2
- package/dist/components/thinking-row/thinking-row.svelte.d.ts +2 -2
- package/dist/copy.d.ts +2 -0
- package/dist/copy.js +3 -0
- package/dist/transcript.svelte.d.ts +25 -3
- package/dist/transcript.svelte.js +33 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ src/lib/
|
|
|
28
28
|
markdown/ sanitised, streaming-safe markdown + highlighting
|
|
29
29
|
activity-group/ a whole investigation, as one quiet line
|
|
30
30
|
tool-row/ one tool call
|
|
31
|
-
thinking-row/ one
|
|
31
|
+
thinking-row/ one thought, or one line of between-tool narration
|
|
32
32
|
working/ the pre-first-token "something is happening" indicator
|
|
33
33
|
```
|
|
34
34
|
|
|
@@ -231,6 +231,23 @@ one asks it through `onsuggest` and takes the whole row with it — the moment
|
|
|
231
231
|
between the click and the new turn arriving is otherwise long enough to ask a
|
|
232
232
|
second question by mistake.
|
|
233
233
|
|
|
234
|
+
### Where the answer starts
|
|
235
|
+
|
|
236
|
+
Milton narrates between tool calls — "Let me also check whether…" — and the
|
|
237
|
+
caller stream gives that nowhere to arrive: it carries no `thinking` blocks
|
|
238
|
+
at all, so narration is an ordinary `text` block, identical to the answer
|
|
239
|
+
except in POSITION. `segment()` reads that position: a text block with any
|
|
240
|
+
tool call still to come in the turn is narration and folds into the activity
|
|
241
|
+
group as a thinking-shaped row; the run of text after the LAST tool call is
|
|
242
|
+
the answer. While a turn streams the judgement is provisional — a block that
|
|
243
|
+
is currently last renders as prose, and a tool call arriving after it
|
|
244
|
+
re-homes it — which is why `segment()` is pure and re-derived per event
|
|
245
|
+
rather than deciding once.
|
|
246
|
+
|
|
247
|
+
The cost is an answer Milton interrupts to go back to the shelf: its first
|
|
248
|
+
half folds away. Position is the only signal the stream gives, and a rule
|
|
249
|
+
read off the prose itself would be unexplainable the first time it misfired.
|
|
250
|
+
|
|
234
251
|
### Changing the words
|
|
235
252
|
|
|
236
253
|
Every user-visible string this package renders lives in
|
|
@@ -271,15 +288,16 @@ pnpm run test # build + vitest
|
|
|
271
288
|
pnpm run screenshots # the state grid, real engine (see below)
|
|
272
289
|
```
|
|
273
290
|
|
|
274
|
-
`docs/screenshots/` is
|
|
291
|
+
`docs/screenshots/` is eleven states x three widths x both themes, taken by
|
|
275
292
|
`scripts/screenshots.mjs` against the console's `/librarian` lab route
|
|
276
293
|
running from its own static build. The same script asserts what a screenshot
|
|
277
294
|
cannot: that nothing scrolls sideways at any width, that the source pane
|
|
278
295
|
opens and closes from the keyboard with focus returning to the chip and is
|
|
279
296
|
really draggable, that the scope statement folds once there is a
|
|
280
|
-
conversation over it and reopens from that line,
|
|
281
|
-
|
|
282
|
-
|
|
297
|
+
conversation over it and reopens from that line, that a follow-up chip asks
|
|
298
|
+
its question and takes the rest of the row with it, and that Milton's
|
|
299
|
+
between-tool narration is nowhere in the answer prose before the activity
|
|
300
|
+
line is opened. It exits non-zero on any of them.
|
|
283
301
|
|
|
284
302
|
```bash
|
|
285
303
|
pnpm --filter @poodle64/console run build
|
package/dist/citations.js
CHANGED
|
@@ -119,7 +119,10 @@ export function formatVerified(iso) {
|
|
|
119
119
|
const [, year, month, day] = match;
|
|
120
120
|
const index = Number(month) - 1;
|
|
121
121
|
const date = Number(day);
|
|
122
|
-
|
|
122
|
+
// Round-tripped rather than bounds-checked: 1-31 admits "31 Apr 2026",
|
|
123
|
+
// and a mark that shows a day that never happened is worse than no mark.
|
|
124
|
+
const made = new Date(Date.UTC(Number(year), index, date));
|
|
125
|
+
if (made.getUTCMonth() !== index || made.getUTCDate() !== date)
|
|
123
126
|
return null;
|
|
124
127
|
return `${date} ${MONTHS[index]} ${year}`;
|
|
125
128
|
}
|
|
@@ -7,6 +7,11 @@
|
|
|
7
7
|
to mention. So there is exactly one row in every state — "Working…" while it
|
|
8
8
|
runs, a count of what was done once it settles — and the steps are behind a
|
|
9
9
|
disclosure for the reader who wants them.
|
|
10
|
+
|
|
11
|
+
That commentary is a step in here too. It reaches the caller as an ordinary
|
|
12
|
+
`text` block, not a `thinking` one, so `segment()` re-homes any text with a
|
|
13
|
+
tool call still to come into this group; it renders as a ThinkingRow beside
|
|
14
|
+
the tools, folded like the rest.
|
|
10
15
|
-->
|
|
11
16
|
<script lang="ts">
|
|
12
17
|
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
|
|
@@ -105,9 +105,28 @@
|
|
|
105
105
|
);
|
|
106
106
|
|
|
107
107
|
const hasAnswer = $derived(texts.length > 0);
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* What went wrong, if anything did — in words, whether or not the run gave
|
|
111
|
+
* any.
|
|
112
|
+
*
|
|
113
|
+
* Two different failures reach a turn and only one of them carries a
|
|
114
|
+
* message. `library_error` (the stream never opened) sets `error`; a run
|
|
115
|
+
* that opened and then failed sets `is_error` on its terminal frame and
|
|
116
|
+
* says nothing at all — Claude Code's own `error_max_turns` and
|
|
117
|
+
* `error_during_execution` are exactly that shape. Reading only `error`
|
|
118
|
+
* left the second kind rendering as a clean, complete answer: no banner,
|
|
119
|
+
* a duration badge, and — worse — the "he holds nothing on this" line,
|
|
120
|
+
* which is a claim about the shelf made off a run that never finished
|
|
121
|
+
* looking.
|
|
122
|
+
*/
|
|
123
|
+
const failure = $derived(
|
|
124
|
+
outcome?.error ?? (outcome?.isError ? words.answerFailed : null)
|
|
125
|
+
);
|
|
126
|
+
|
|
108
127
|
// A failed turn settles too, and "Ask again" is the one thing a reader wants
|
|
109
128
|
// from it — there is just nothing to copy.
|
|
110
|
-
const settled = $derived(!running && (hasAnswer || Boolean(
|
|
129
|
+
const settled = $derived(!running && (hasAnswer || Boolean(failure)));
|
|
111
130
|
|
|
112
131
|
/**
|
|
113
132
|
* Milton answered and cited nothing, and we WATCHED him do it.
|
|
@@ -120,7 +139,7 @@
|
|
|
120
139
|
* here knows whether there were any.
|
|
121
140
|
*/
|
|
122
141
|
const notHeld = $derived(
|
|
123
|
-
settled && hasAnswer && sources.length === 0 && Boolean(outcome) && !
|
|
142
|
+
settled && hasAnswer && sources.length === 0 && Boolean(outcome) && !failure
|
|
124
143
|
);
|
|
125
144
|
|
|
126
145
|
// A follow-up is offered once. Clicking it asks the question, which puts a
|
|
@@ -248,12 +267,12 @@
|
|
|
248
267
|
</section>
|
|
249
268
|
{/if}
|
|
250
269
|
|
|
251
|
-
{#if
|
|
270
|
+
{#if failure}
|
|
252
271
|
<p
|
|
253
272
|
class="border-status-error/40 bg-status-error/10 text-foreground max-w-[72ch] rounded-lg border px-3 py-2 text-sm"
|
|
254
273
|
role="alert"
|
|
255
274
|
>
|
|
256
|
-
{
|
|
275
|
+
{failure}
|
|
257
276
|
</p>
|
|
258
277
|
{/if}
|
|
259
278
|
|
|
@@ -281,7 +300,7 @@
|
|
|
281
300
|
<span>{words.askAgain}</span>
|
|
282
301
|
</button>
|
|
283
302
|
{/if}
|
|
284
|
-
{#if outcome && !
|
|
303
|
+
{#if outcome && !failure}
|
|
285
304
|
<span class="pl-1 font-mono text-xs tabular-nums opacity-70">
|
|
286
305
|
{((outcome.durationMs ?? 0) / 1000).toFixed(1)}s
|
|
287
306
|
</span>
|
|
@@ -1,9 +1,20 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
One line of Milton's working-out, behind a chevron.
|
|
3
|
+
|
|
4
|
+
Two different blocks render as this row and deliberately read the same. A
|
|
5
|
+
real `thinking` block is one; the other is a `text` block Milton wrote
|
|
6
|
+
BETWEEN two tool calls — "Let me also check whether…" — which the caller
|
|
7
|
+
stream gives no way to tell from the answer except by position, and which
|
|
8
|
+
read as answer prose on production until `segment()` started re-homing it
|
|
9
|
+
here. To a reader both are the same thing: what he was working through, not
|
|
10
|
+
what he concluded. So both fold, and both fold by default.
|
|
11
|
+
-->
|
|
1
12
|
<script lang="ts">
|
|
2
13
|
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
|
|
3
|
-
import type { ThinkingBlock } from '../../transcript.svelte';
|
|
14
|
+
import type { TextBlock, ThinkingBlock } from '../../transcript.svelte';
|
|
4
15
|
|
|
5
16
|
interface Props {
|
|
6
|
-
block: ThinkingBlock;
|
|
17
|
+
block: ThinkingBlock | TextBlock;
|
|
7
18
|
active: boolean;
|
|
8
19
|
}
|
|
9
20
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { ThinkingBlock } from '../../transcript.svelte';
|
|
1
|
+
import type { TextBlock, ThinkingBlock } from '../../transcript.svelte';
|
|
2
2
|
interface Props {
|
|
3
|
-
block: ThinkingBlock;
|
|
3
|
+
block: ThinkingBlock | TextBlock;
|
|
4
4
|
active: boolean;
|
|
5
5
|
}
|
|
6
6
|
declare const ThinkingRow: import("svelte").Component<Props, {}, "">;
|
package/dist/copy.d.ts
CHANGED
|
@@ -19,6 +19,8 @@ export interface LibrarianCopy {
|
|
|
19
19
|
notVerified: string;
|
|
20
20
|
/** Shown under an answer that finished without citing anything. */
|
|
21
21
|
notHeld: string;
|
|
22
|
+
/** Shown when the run itself ended in error and said nothing about why. */
|
|
23
|
+
answerFailed: string;
|
|
22
24
|
/** Heading over the list of what an answer cited. */
|
|
23
25
|
sources: string;
|
|
24
26
|
/** Heading over the follow-up questions the librarian offered. */
|
package/dist/copy.js
CHANGED
|
@@ -20,6 +20,9 @@ export const DEFAULT_COPY = {
|
|
|
20
20
|
// reading it has no model of any of those, and the room's own instruction
|
|
21
21
|
// to Milton forbids him naming them either.
|
|
22
22
|
notHeld: 'Milton answered this one without a source. He may not hold a document that covers it.',
|
|
23
|
+
// A run can end `is_error` carrying no message at all, and a turn that
|
|
24
|
+
// simply stops is the one thing a reader must not have to guess about.
|
|
25
|
+
answerFailed: 'Milton stopped before he finished this one.',
|
|
23
26
|
sources: 'Sources',
|
|
24
27
|
suggestions: 'Ask next',
|
|
25
28
|
scope: 'What Milton answers from',
|
|
@@ -91,8 +91,11 @@ export declare class Transcript {
|
|
|
91
91
|
apply(event: AgentEvent): void;
|
|
92
92
|
}
|
|
93
93
|
export interface ActivityStep {
|
|
94
|
-
/** The block this step stands for; a repeated step keeps the FIRST.
|
|
95
|
-
|
|
94
|
+
/** The block this step stands for; a repeated step keeps the FIRST.
|
|
95
|
+
*
|
|
96
|
+
* A `text` block here is interstitial narration, not the answer — see
|
|
97
|
+
* `segment()`. */
|
|
98
|
+
block: Block;
|
|
96
99
|
/** How many identical consecutive steps collapsed into this one. */
|
|
97
100
|
repeats: number;
|
|
98
101
|
}
|
|
@@ -124,7 +127,7 @@ export interface Turn {
|
|
|
124
127
|
/**
|
|
125
128
|
* Fold a turn's flat block list into what a reader should actually see.
|
|
126
129
|
*
|
|
127
|
-
*
|
|
130
|
+
* Three problems this solves, all reported off a real transcript:
|
|
128
131
|
*
|
|
129
132
|
* 1. Fifteen tool rows stood between the question and the first word of the
|
|
130
133
|
* answer, so the answer had to be scrolled to. Contiguous activity becomes
|
|
@@ -132,6 +135,25 @@ export interface Turn {
|
|
|
132
135
|
* 2. "Reading pspf guidelines 2026" appeared five times in a row — five pages
|
|
133
136
|
* of one document, which is one act of reading to a human. Consecutive
|
|
134
137
|
* steps with the same label collapse to one row carrying a count.
|
|
138
|
+
* 3. Milton's between-tool narration was rendering unfolded, as answer prose.
|
|
139
|
+
* The caller stream carries no `thinking` blocks at all — measured on
|
|
140
|
+
* production 11/09/2026 — so "Let me also check whether…" arrives as an
|
|
141
|
+
* ordinary `text` block, indistinguishable from the answer except by
|
|
142
|
+
* POSITION. A text block with any tool call still to come in this turn is
|
|
143
|
+
* narration; only the run of text after the LAST tool call is the answer.
|
|
144
|
+
* Narration folds into the activity group as a thinking-shaped step.
|
|
145
|
+
*
|
|
146
|
+
* That third rule is provisional while a turn streams, and deliberately so: a
|
|
147
|
+
* text block that is currently last IS the answer as far as anything can know,
|
|
148
|
+
* and renders as prose. The tool call that arrives after it re-homes it into
|
|
149
|
+
* the group. Nothing here holds state to make that work — `segment` is pure
|
|
150
|
+
* and re-derived on every event, so re-homing is just the next call returning
|
|
151
|
+
* a different shape.
|
|
152
|
+
*
|
|
153
|
+
* The cost of the rule is a real answer that Milton interrupts to go back to
|
|
154
|
+
* the shelf: its first half folds away. That is the trade taken knowingly —
|
|
155
|
+
* the position of a block is the only signal the stream gives, and a rule read
|
|
156
|
+
* off the prose itself would be unexplainable the first time it misfired.
|
|
135
157
|
*/
|
|
136
158
|
export declare function segment(blocks: Block[]): Segment[];
|
|
137
159
|
/** One line describing a whole investigation, for the collapsed state.
|
|
@@ -267,7 +267,7 @@ function renderResult(content) {
|
|
|
267
267
|
/**
|
|
268
268
|
* Fold a turn's flat block list into what a reader should actually see.
|
|
269
269
|
*
|
|
270
|
-
*
|
|
270
|
+
* Three problems this solves, all reported off a real transcript:
|
|
271
271
|
*
|
|
272
272
|
* 1. Fifteen tool rows stood between the question and the first word of the
|
|
273
273
|
* answer, so the answer had to be scrolled to. Contiguous activity becomes
|
|
@@ -275,20 +275,44 @@ function renderResult(content) {
|
|
|
275
275
|
* 2. "Reading pspf guidelines 2026" appeared five times in a row — five pages
|
|
276
276
|
* of one document, which is one act of reading to a human. Consecutive
|
|
277
277
|
* steps with the same label collapse to one row carrying a count.
|
|
278
|
+
* 3. Milton's between-tool narration was rendering unfolded, as answer prose.
|
|
279
|
+
* The caller stream carries no `thinking` blocks at all — measured on
|
|
280
|
+
* production 11/09/2026 — so "Let me also check whether…" arrives as an
|
|
281
|
+
* ordinary `text` block, indistinguishable from the answer except by
|
|
282
|
+
* POSITION. A text block with any tool call still to come in this turn is
|
|
283
|
+
* narration; only the run of text after the LAST tool call is the answer.
|
|
284
|
+
* Narration folds into the activity group as a thinking-shaped step.
|
|
285
|
+
*
|
|
286
|
+
* That third rule is provisional while a turn streams, and deliberately so: a
|
|
287
|
+
* text block that is currently last IS the answer as far as anything can know,
|
|
288
|
+
* and renders as prose. The tool call that arrives after it re-homes it into
|
|
289
|
+
* the group. Nothing here holds state to make that work — `segment` is pure
|
|
290
|
+
* and re-derived on every event, so re-homing is just the next call returning
|
|
291
|
+
* a different shape.
|
|
292
|
+
*
|
|
293
|
+
* The cost of the rule is a real answer that Milton interrupts to go back to
|
|
294
|
+
* the shelf: its first half folds away. That is the trade taken knowingly —
|
|
295
|
+
* the position of a block is the only signal the stream gives, and a rule read
|
|
296
|
+
* off the prose itself would be unexplainable the first time it misfired.
|
|
278
297
|
*/
|
|
279
298
|
export function segment(blocks) {
|
|
280
299
|
const out = [];
|
|
281
300
|
let current = null;
|
|
282
|
-
|
|
283
|
-
|
|
301
|
+
let lastTool = -1;
|
|
302
|
+
for (let i = 0; i < blocks.length; i += 1)
|
|
303
|
+
if (blocks[i].kind === 'tool')
|
|
304
|
+
lastTool = i;
|
|
305
|
+
for (const [i, block] of blocks.entries()) {
|
|
306
|
+
if (block.kind === 'text' && i > lastTool) {
|
|
284
307
|
current = null;
|
|
285
308
|
out.push(block);
|
|
286
309
|
continue;
|
|
287
310
|
}
|
|
288
311
|
// A thinking block with no text is a row whose chevron opens on nothing.
|
|
289
312
|
// Claude Code's thinking display defaults to "omitted", so most arrive
|
|
290
|
-
// empty — rendering them is worse than dropping them.
|
|
291
|
-
|
|
313
|
+
// empty — rendering them is worse than dropping them. An empty narration
|
|
314
|
+
// block is the same row, for the same reason.
|
|
315
|
+
if (block.kind !== 'tool' && !block.text.trim())
|
|
292
316
|
continue;
|
|
293
317
|
if (!current) {
|
|
294
318
|
current = {
|
|
@@ -318,6 +342,10 @@ function sameStep(a, b) {
|
|
|
318
342
|
return false;
|
|
319
343
|
if (a.kind === 'thinking')
|
|
320
344
|
return true;
|
|
345
|
+
// Two narration sentences are two things Milton said; collapsing them to
|
|
346
|
+
// one row with a count would lose the second one entirely.
|
|
347
|
+
if (a.kind === 'text')
|
|
348
|
+
return false;
|
|
321
349
|
const left = describe(a);
|
|
322
350
|
const right = describe(b);
|
|
323
351
|
return left.verb === right.verb && left.object === right.object;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poodle64/librarian",
|
|
3
|
-
"version": "2026.9.
|
|
3
|
+
"version": "2026.9.11",
|
|
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",
|