@poodle64/librarian 2026.9.20 → 2026.9.22

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 (28) hide show
  1. package/README.md +133 -7
  2. package/dist/chat.svelte.d.ts +189 -0
  3. package/dist/chat.svelte.js +554 -0
  4. package/dist/client.d.ts +29 -6
  5. package/dist/client.js +11 -3
  6. package/dist/components/agent-transcript/agent-transcript.svelte +42 -6
  7. package/dist/components/agent-transcript/agent-transcript.svelte.d.ts +9 -0
  8. package/dist/components/answer-mark/answer-mark.svelte +223 -0
  9. package/dist/components/answer-mark/answer-mark.svelte.d.ts +11 -0
  10. package/dist/components/answer-mark/index.d.ts +2 -0
  11. package/dist/components/answer-mark/index.js +2 -0
  12. package/dist/components/conversation/conversation.svelte +21 -3
  13. package/dist/components/conversation/conversation.svelte.d.ts +9 -0
  14. package/dist/components/conversation-list/conversation-list.svelte +496 -0
  15. package/dist/components/conversation-list/conversation-list.svelte.d.ts +26 -0
  16. package/dist/components/conversation-list/index.d.ts +2 -0
  17. package/dist/components/conversation-list/index.js +2 -0
  18. package/dist/components/fair-use-notice/fair-use-notice.svelte +97 -0
  19. package/dist/components/fair-use-notice/fair-use-notice.svelte.d.ts +11 -0
  20. package/dist/components/fair-use-notice/index.d.ts +2 -0
  21. package/dist/components/fair-use-notice/index.js +2 -0
  22. package/dist/components/working/working.svelte +8 -3
  23. package/dist/components/working/working.svelte.d.ts +3 -0
  24. package/dist/copy.d.ts +40 -0
  25. package/dist/copy.js +36 -1
  26. package/dist/session.svelte.js +7 -1
  27. package/dist/transcript.svelte.js +17 -3
  28. package/package.json +5 -1
package/dist/copy.d.ts CHANGED
@@ -31,6 +31,8 @@ export interface LibrarianCopy {
31
31
  answerFailed: string;
32
32
  /** Shown when the stream never opened, or died mid-answer. */
33
33
  unreachable: string;
34
+ /** Shown on a question stopped before a word of its answer came. */
35
+ noAnswer: string;
34
36
  /** Heading over the list of what an answer cited. */
35
37
  sources: string;
36
38
  /** Heading over the follow-up questions the persona offered. */
@@ -55,6 +57,41 @@ export interface LibrarianCopy {
55
57
  /** An artefact card's action, and the same action while it is open. */
56
58
  openArtefact: string;
57
59
  showingArtefact: string;
60
+ /** Beside the clock while the question waits behind someone else's. */
61
+ waiting: string;
62
+ /** Beside the clock on an answer this page did not start. */
63
+ stillAnswering: string;
64
+ /** The answer mark: the two verdicts, the optional note, and after. */
65
+ markHelpful: string;
66
+ markNotHelpful: string;
67
+ markNote: string;
68
+ markSend: string;
69
+ markNoted: string;
70
+ markFailed: string;
71
+ /** The fair-use notice, while there are questions left and once there are
72
+ * none, filled by `fill()`: `{resets}` is a time already, "midnight" or
73
+ * "2:00 pm". */
74
+ questionsLeft: string;
75
+ limitReached: string;
76
+ /** `{resets}` when the allowance comes back at the turn of the day. */
77
+ midnight: string;
78
+ support: string;
79
+ /** The conversation list. */
80
+ pastQuestions: string;
81
+ newQuestion: string;
82
+ readingPastQuestions: string;
83
+ noPastQuestions: string;
84
+ pastQuestionsFailed: string;
85
+ conversationActions: string;
86
+ rename: string;
87
+ download: string;
88
+ delete: string;
89
+ save: string;
90
+ cancel: string;
91
+ deletePrompt: string;
92
+ renameFailed: string;
93
+ downloadFailed: string;
94
+ deleteFailed: string;
58
95
  }
59
96
  /**
60
97
  * The display name for a persona a host identified by its slug.
@@ -71,6 +108,9 @@ export declare function personaName(name: string): string;
71
108
  * says it is, and a sentence that guesses is wrong for half of them.
72
109
  */
73
110
  export declare function copyFor(name?: string): LibrarianCopy;
111
+ /** A sentence with `{name}` places, filled. A place with no value stays as
112
+ * written, so a host's override that drops one loses nothing else. */
113
+ export declare function fill(template: string, values: Record<string, string | number>): string;
74
114
  /** The library's own words, for a host that names no persona. */
75
115
  export declare const DEFAULT_COPY: LibrarianCopy;
76
116
  /**
package/dist/copy.js CHANGED
@@ -59,6 +59,7 @@ export function copyFor(name = DEFAULT_PERSONA) {
59
59
  // simply stops is the one thing a reader must not have to guess about.
60
60
  answerFailed: `${who} stopped before finishing this one.`,
61
61
  unreachable: `${who} can't be reached right now.`,
62
+ noAnswer: 'Stopped before an answer came.',
62
63
  sources: 'Sources',
63
64
  suggestions: 'Ask next',
64
65
  scope: `What ${who} answers from`,
@@ -74,9 +75,43 @@ export function copyFor(name = DEFAULT_PERSONA) {
74
75
  closeSource: 'Close source',
75
76
  documentUnavailable: "That document can't be opened right now.",
76
77
  openArtefact: 'Open',
77
- showingArtefact: 'Showing'
78
+ showingArtefact: 'Showing',
79
+ waiting: `${who} is answering another question first`,
80
+ stillAnswering: `${who} is still answering`,
81
+ markHelpful: 'Helpful',
82
+ markNotHelpful: 'Not helpful',
83
+ markNote: 'Anything to add? (optional)',
84
+ markSend: 'Send',
85
+ markNoted: 'Thanks, noted.',
86
+ markFailed: "That couldn't be sent. Nothing was recorded.",
87
+ // The industry wording, whole: the number, that it is a fair-use limit
88
+ // rather than a fault, and when it comes back. No apology.
89
+ questionsLeft: '{remaining} of {limit} questions left today. Resets at {resets}.',
90
+ limitReached: "You've reached today's fair-use limit of {limit} questions. It resets at {resets}.",
91
+ midnight: 'midnight',
92
+ support: 'Support',
93
+ pastQuestions: 'Past questions',
94
+ newQuestion: 'New question',
95
+ readingPastQuestions: 'Reading your past questions…',
96
+ noPastQuestions: 'No past questions yet.',
97
+ pastQuestionsFailed: "Your past questions couldn't be read just now.",
98
+ conversationActions: 'More',
99
+ rename: 'Rename',
100
+ download: 'Download',
101
+ delete: 'Delete',
102
+ save: 'Save',
103
+ cancel: 'Cancel',
104
+ deletePrompt: 'Delete this conversation? It goes from every device.',
105
+ renameFailed: "That conversation couldn't be renamed.",
106
+ downloadFailed: "That conversation couldn't be downloaded.",
107
+ deleteFailed: "That conversation couldn't be deleted."
78
108
  };
79
109
  }
110
+ /** A sentence with `{name}` places, filled. A place with no value stays as
111
+ * written, so a host's override that drops one loses nothing else. */
112
+ export function fill(template, values) {
113
+ return template.replace(/\{(\w+)\}/g, (place, key) => key in values ? String(values[key]) : place);
114
+ }
80
115
  /** The library's own words, for a host that names no persona. */
81
116
  export const DEFAULT_COPY = copyFor();
82
117
  /**
@@ -55,6 +55,12 @@ export class Session {
55
55
  }
56
56
  if (!FOLDED.has(event.type))
57
57
  return;
58
+ // A user frame that is neither a replayed message nor a tool's result is
59
+ // the CLI nudging its own model (`isSynthetic`: "[structured-output-
60
+ // enforce] You MUST call the StructuredOutput tool…"): neither side of
61
+ // the conversation.
62
+ if (event.type === 'user' && !contentOf(event).some((b) => b.type === 'tool_result'))
63
+ return;
58
64
  // Settled by its own `result`. A stream that broke mid-run is not: the
59
65
  // run goes on, and a watch opened again folds the rest onto it.
60
66
  const last = this.turns.at(-1);
@@ -90,7 +96,7 @@ export class Session {
90
96
  }
91
97
  /** The words of a replayed frame: a bare string, or its text blocks. */
92
98
  function said(event) {
93
- const content = event.message?.content;
99
+ const content = typeof event.message === 'object' ? event.message.content : undefined;
94
100
  if (typeof content === 'string')
95
101
  return content;
96
102
  return contentOf(event)
@@ -299,7 +299,7 @@ export function fold(into, state, event) {
299
299
  }
300
300
  /** A message's content blocks; a user frame may carry a bare string instead. */
301
301
  export function contentOf(event) {
302
- const content = event.message?.content;
302
+ const content = typeof event.message === 'object' ? event.message.content : undefined;
303
303
  return Array.isArray(content) ? content : [];
304
304
  }
305
305
  function wholeBlock(block, index) {
@@ -366,9 +366,11 @@ export function segment(blocks, describeTool) {
366
366
  let current = null;
367
367
  let lastTool = -1;
368
368
  for (let i = 0; i < blocks.length; i += 1)
369
- if (blocks[i].kind === 'tool')
369
+ if (isStep(blocks[i]))
370
370
  lastTool = i;
371
371
  for (const [i, block] of blocks.entries()) {
372
+ if (block.kind === 'tool' && !isStep(block))
373
+ continue;
372
374
  if (block.kind === 'text' && i > lastTool) {
373
375
  current = null;
374
376
  out.push(block);
@@ -397,6 +399,18 @@ export function segment(blocks, describeTool) {
397
399
  }
398
400
  return out;
399
401
  }
402
+ /**
403
+ * The tool the CLI adds under `--json-schema`. Calling it hands in the run's
404
+ * answer, which arrives again whole on the `result` (`structured_output`): it
405
+ * is not a step of the work, and the words before it are the answer, not
406
+ * narration ahead of more work. Measured on Claude Code 2.1.283, where a run
407
+ * with a schema always ends in this call.
408
+ */
409
+ const HANDS_IN = 'StructuredOutput';
410
+ /** A call that is part of the work, rather than the handing-in of its answer. */
411
+ function isStep(block) {
412
+ return block.kind === 'tool' && block.name !== HANDS_IN;
413
+ }
400
414
  function sameStep(last, block, words) {
401
415
  if (last.block.kind !== block.kind)
402
416
  return false;
@@ -436,7 +450,7 @@ export function summariseActivity(group) {
436
450
  export function readFrom(blocks, describeTool) {
437
451
  const out = [];
438
452
  for (const block of blocks) {
439
- if (block.kind !== 'tool' || block.result === undefined || block.isError)
453
+ if (block.kind !== 'tool' || !isStep(block) || block.result === undefined || block.isError)
440
454
  continue;
441
455
  const source = wordsFor(block, describeTool).source;
442
456
  if (!source || out.some((c) => c.document_id === source.id))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poodle64/librarian",
3
- "version": "2026.9.20",
3
+ "version": "2026.9.22",
4
4
  "description": "An agent's conversation surface as a consumable Svelte 5 package: the stream client, transcript state and self-styling chat components (transcript, composer, markdown) every household app renders instead of rebuilding, speaking as whichever persona the app names.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -30,6 +30,10 @@
30
30
  "types": "./dist/session.svelte.d.ts",
31
31
  "svelte": "./dist/session.svelte.js"
32
32
  },
33
+ "./chat": {
34
+ "types": "./dist/chat.svelte.d.ts",
35
+ "svelte": "./dist/chat.svelte.js"
36
+ },
33
37
  "./history": {
34
38
  "types": "./dist/history.svelte.d.ts",
35
39
  "svelte": "./dist/history.svelte.js"