@poodle64/librarian 2026.9.10 → 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 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 thinking block
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 ten states x three widths x both themes, taken by
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, and that a follow-up chip
281
- asks its question and takes the rest of the row with it. It exits non-zero
282
- on any of them.
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
@@ -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';
@@ -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, {}, "">;
@@ -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
- block: ToolBlock | ThinkingBlock;
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
- * Two problems this solves, both reported off a real transcript:
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
- * Two problems this solves, both reported off a real transcript:
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
- for (const block of blocks) {
283
- if (block.kind === 'text') {
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
- if (block.kind === 'thinking' && !block.text.trim())
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.10",
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",