@poodle64/librarian 2026.9.6 → 2026.9.7

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 (32) hide show
  1. package/README.md +128 -43
  2. package/dist/attachments.d.ts +30 -0
  3. package/dist/attachments.js +80 -0
  4. package/dist/citations.d.ts +63 -0
  5. package/dist/citations.js +89 -0
  6. package/dist/client.d.ts +5 -0
  7. package/dist/client.js +38 -12
  8. package/dist/components/activity-group/activity-group.svelte +36 -44
  9. package/dist/components/activity-group/activity-group.svelte.d.ts +1 -1
  10. package/dist/components/agent-transcript/agent-transcript.svelte +166 -26
  11. package/dist/components/agent-transcript/agent-transcript.svelte.d.ts +8 -0
  12. package/dist/components/composer/composer.svelte +206 -53
  13. package/dist/components/composer/composer.svelte.d.ts +5 -1
  14. package/dist/components/conversation/conversation.svelte +179 -0
  15. package/dist/components/conversation/conversation.svelte.d.ts +26 -0
  16. package/dist/components/conversation/index.d.ts +2 -0
  17. package/dist/components/conversation/index.js +2 -0
  18. package/dist/components/document-pane/document-pane.svelte +177 -0
  19. package/dist/components/document-pane/document-pane.svelte.d.ts +9 -0
  20. package/dist/components/document-pane/index.d.ts +2 -0
  21. package/dist/components/document-pane/index.js +2 -0
  22. package/dist/components/markdown/index.d.ts +1 -1
  23. package/dist/components/markdown/index.js +1 -1
  24. package/dist/components/markdown/markdown.d.ts +13 -0
  25. package/dist/components/markdown/markdown.js +49 -1
  26. package/dist/components/markdown/markdown.svelte +85 -3
  27. package/dist/components/markdown/markdown.svelte.d.ts +6 -0
  28. package/dist/follow-scroll.svelte.d.ts +34 -0
  29. package/dist/follow-scroll.svelte.js +46 -0
  30. package/dist/transcript.svelte.d.ts +27 -0
  31. package/dist/transcript.svelte.js +27 -0
  32. package/package.json +16 -2
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Follow the stream until the reader disagrees.
3
+ *
4
+ * The whole behaviour is one rule stated three ways: a viewport already at the
5
+ * bottom is following; a reader who scrolls UP has stopped following; a reader
6
+ * who comes back to the bottom is following again. Nothing else — no timers,
7
+ * no "was that scroll ours or theirs" bookkeeping — because the auto-scroll
8
+ * this drives always lands AT the bottom, which the first clause re-affirms
9
+ * rather than fights.
10
+ *
11
+ * Growing content moves the bottom away from a parked reader without moving
12
+ * their scrollTop, so distance alone cannot say who moved: the direction of
13
+ * scrollTop can, and that is the only thing `measure` remembers between calls.
14
+ */
15
+ /** Distance from the bottom, in px, still counted as being at the bottom.
16
+ * A line of prose is ~28px; two lines of slack survives sub-pixel layout
17
+ * rounding and a caret-height change without unpinning the reader. */
18
+ export declare const FOLLOW_THRESHOLD = 64;
19
+ export interface ScrollMetrics {
20
+ scrollTop: number;
21
+ clientHeight: number;
22
+ scrollHeight: number;
23
+ }
24
+ export declare function distanceFromBottom(m: ScrollMetrics): number;
25
+ export declare function atBottom(m: ScrollMetrics, threshold?: number): boolean;
26
+ export declare class FollowScroll {
27
+ #private;
28
+ /** True while the viewport should be dragged along with new content. */
29
+ following: boolean;
30
+ /** Feed every scroll event, and the metrics after every content change. */
31
+ measure(m: ScrollMetrics, threshold?: number): void;
32
+ /** A new question re-pins: the reader asked for the thing about to arrive. */
33
+ pin(): void;
34
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Follow the stream until the reader disagrees.
3
+ *
4
+ * The whole behaviour is one rule stated three ways: a viewport already at the
5
+ * bottom is following; a reader who scrolls UP has stopped following; a reader
6
+ * who comes back to the bottom is following again. Nothing else — no timers,
7
+ * no "was that scroll ours or theirs" bookkeeping — because the auto-scroll
8
+ * this drives always lands AT the bottom, which the first clause re-affirms
9
+ * rather than fights.
10
+ *
11
+ * Growing content moves the bottom away from a parked reader without moving
12
+ * their scrollTop, so distance alone cannot say who moved: the direction of
13
+ * scrollTop can, and that is the only thing `measure` remembers between calls.
14
+ */
15
+ /** Distance from the bottom, in px, still counted as being at the bottom.
16
+ * A line of prose is ~28px; two lines of slack survives sub-pixel layout
17
+ * rounding and a caret-height change without unpinning the reader. */
18
+ export const FOLLOW_THRESHOLD = 64;
19
+ export function distanceFromBottom(m) {
20
+ return m.scrollHeight - m.scrollTop - m.clientHeight;
21
+ }
22
+ export function atBottom(m, threshold = FOLLOW_THRESHOLD) {
23
+ return distanceFromBottom(m) <= threshold;
24
+ }
25
+ export class FollowScroll {
26
+ /** True while the viewport should be dragged along with new content. */
27
+ following = $state(true);
28
+ #lastTop = 0;
29
+ /** Feed every scroll event, and the metrics after every content change. */
30
+ measure(m, threshold = FOLLOW_THRESHOLD) {
31
+ // 1px of tolerance: a trackpad's fractional scrollTop otherwise reads as
32
+ // an upward flick on a viewport that has not actually moved.
33
+ const wentUp = m.scrollTop < this.#lastTop - 1;
34
+ this.#lastTop = m.scrollTop;
35
+ if (atBottom(m, threshold)) {
36
+ this.following = true;
37
+ return;
38
+ }
39
+ if (wentUp)
40
+ this.following = false;
41
+ }
42
+ /** A new question re-pins: the reader asked for the thing about to arrive. */
43
+ pin() {
44
+ this.following = true;
45
+ }
46
+ }
@@ -7,6 +7,7 @@
7
7
  * verbatim under `other`, so nothing is silently dropped.
8
8
  */
9
9
  import type { AgentEvent } from './client';
10
+ import type { Citation } from './citations';
10
11
  export interface TextBlock {
11
12
  kind: 'text';
12
13
  index: number;
@@ -34,6 +35,18 @@ export interface Outcome {
34
35
  isError?: boolean;
35
36
  error?: string;
36
37
  }
38
+ /**
39
+ * The question as the READER asked it.
40
+ *
41
+ * A host prepends a system preamble to every question before it goes to the
42
+ * library — cadmus sends `{room.preamble}\n\n{question}` — and echoing that
43
+ * back into the transcript shows a colleague the machine room. Leading
44
+ * blank-line-separated paragraphs addressed to the model are dropped.
45
+ *
46
+ * The LAST paragraph is never dropped, whatever it starts with: someone whose
47
+ * entire question is "You are wrong about the leave rule" must still see it.
48
+ */
49
+ export declare function readerQuestion(question: string): string;
37
50
  /** What Milton DID, in a reader's own words — never the tool's name or the
38
51
  * raw command it ran.
39
52
  *
@@ -69,6 +82,8 @@ export declare class Transcript {
69
82
  sessionId: string | null;
70
83
  model: string | null;
71
84
  outcome: Outcome | null;
85
+ /** Sources for the answer, from the library's own `citations` frame. */
86
+ citations: Citation[];
72
87
  other: AgentEvent[];
73
88
  reset(): void;
74
89
  apply(event: AgentEvent): void;
@@ -89,6 +104,18 @@ export interface ActivityGroup {
89
104
  searches: number;
90
105
  }
91
106
  export type Segment = ActivityGroup | TextBlock;
107
+ /** One question and the answer to it, as the transcript renders it.
108
+ *
109
+ * A finished turn is a plain object the host keeps in a list; the LIVE turn is
110
+ * a `Transcript` spread into the same shape. Both render identically, which is
111
+ * what stops a conversation flickering as the last turn settles. */
112
+ export interface Turn {
113
+ id: string;
114
+ question: string;
115
+ blocks: Block[];
116
+ outcome: Outcome | null;
117
+ citations?: Citation[];
118
+ }
92
119
  /**
93
120
  * Fold a turn's flat block list into what a reader should actually see.
94
121
  *
@@ -6,6 +6,24 @@
6
6
  * deltas that belong to each. An event type this does not know about is kept
7
7
  * verbatim under `other`, so nothing is silently dropped.
8
8
  */
9
+ /**
10
+ * The question as the READER asked it.
11
+ *
12
+ * A host prepends a system preamble to every question before it goes to the
13
+ * library — cadmus sends `{room.preamble}\n\n{question}` — and echoing that
14
+ * back into the transcript shows a colleague the machine room. Leading
15
+ * blank-line-separated paragraphs addressed to the model are dropped.
16
+ *
17
+ * The LAST paragraph is never dropped, whatever it starts with: someone whose
18
+ * entire question is "You are wrong about the leave rule" must still see it.
19
+ */
20
+ export function readerQuestion(question) {
21
+ const parts = question.split(/\n{2,}/);
22
+ let start = 0;
23
+ while (start < parts.length - 1 && /^(you are|you're|your role|act as|system:)\b/i.test(parts[start].trim()))
24
+ start += 1;
25
+ return parts.slice(start).join('\n\n').trim();
26
+ }
9
27
  /** What Milton DID, in a reader's own words — never the tool's name or the
10
28
  * raw command it ran.
11
29
  *
@@ -118,6 +136,8 @@ export class Transcript {
118
136
  sessionId = $state(null);
119
137
  model = $state(null);
120
138
  outcome = $state(null);
139
+ /** Sources for the answer, from the library's own `citations` frame. */
140
+ citations = $state([]);
121
141
  other = $state([]);
122
142
  /** Content-block index is per MESSAGE, so it repeats across turns; this
123
143
  * maps the live index onto a position in the flat list. Cleared whenever a
@@ -130,6 +150,7 @@ export class Transcript {
130
150
  reset() {
131
151
  this.blocks = [];
132
152
  this.outcome = null;
153
+ this.citations = [];
133
154
  this.other = [];
134
155
  this.#open.clear();
135
156
  }
@@ -149,6 +170,12 @@ export class Transcript {
149
170
  };
150
171
  return;
151
172
  }
173
+ // Emitted after the final assistant text, so it lands on a turn that is
174
+ // otherwise complete.
175
+ if (event.type === 'citations') {
176
+ this.citations = event.items ?? [];
177
+ return;
178
+ }
152
179
  if (event.type === 'library_error') {
153
180
  this.outcome = { isError: true, error: event.error ?? "Milton can't be reached right now." };
154
181
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poodle64/librarian",
3
- "version": "2026.9.6",
3
+ "version": "2026.9.7",
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",
@@ -30,6 +30,18 @@
30
30
  "types": "./dist/history.svelte.d.ts",
31
31
  "svelte": "./dist/history.svelte.js"
32
32
  },
33
+ "./citations": {
34
+ "types": "./dist/citations.d.ts",
35
+ "svelte": "./dist/citations.js"
36
+ },
37
+ "./attachments": {
38
+ "types": "./dist/attachments.d.ts",
39
+ "svelte": "./dist/attachments.js"
40
+ },
41
+ "./follow-scroll": {
42
+ "types": "./dist/follow-scroll.svelte.d.ts",
43
+ "svelte": "./dist/follow-scroll.svelte.js"
44
+ },
33
45
  "./*": {
34
46
  "types": "./dist/components/*/index.d.ts",
35
47
  "svelte": "./dist/components/*/index.js"
@@ -55,6 +67,7 @@
55
67
  "isomorphic-dompurify": "^3.23.0",
56
68
  "jsdom": "^29.1.1",
57
69
  "marked": "^18.0.11",
70
+ "playwright": "^1.62.0",
58
71
  "publint": "^0.3.15",
59
72
  "shiki": "^4.4.3",
60
73
  "svelte": "^5.56.2",
@@ -67,7 +80,8 @@
67
80
  "build": "svelte-kit sync && svelte-package && publint",
68
81
  "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
69
82
  "test": "pnpm run build && vitest run",
70
- "prepublishOnly": "pnpm run build"
83
+ "prepublishOnly": "pnpm run build",
84
+ "screenshots": "node scripts/screenshots.mjs"
71
85
  },
72
86
  "packageManager": "pnpm@10.28.0"
73
87
  }