@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.
- package/README.md +128 -43
- package/dist/attachments.d.ts +30 -0
- package/dist/attachments.js +80 -0
- package/dist/citations.d.ts +63 -0
- package/dist/citations.js +89 -0
- package/dist/client.d.ts +5 -0
- package/dist/client.js +38 -12
- package/dist/components/activity-group/activity-group.svelte +36 -44
- package/dist/components/activity-group/activity-group.svelte.d.ts +1 -1
- package/dist/components/agent-transcript/agent-transcript.svelte +166 -26
- package/dist/components/agent-transcript/agent-transcript.svelte.d.ts +8 -0
- package/dist/components/composer/composer.svelte +206 -53
- package/dist/components/composer/composer.svelte.d.ts +5 -1
- package/dist/components/conversation/conversation.svelte +179 -0
- package/dist/components/conversation/conversation.svelte.d.ts +26 -0
- package/dist/components/conversation/index.d.ts +2 -0
- package/dist/components/conversation/index.js +2 -0
- package/dist/components/document-pane/document-pane.svelte +177 -0
- package/dist/components/document-pane/document-pane.svelte.d.ts +9 -0
- package/dist/components/document-pane/index.d.ts +2 -0
- package/dist/components/document-pane/index.js +2 -0
- package/dist/components/markdown/index.d.ts +1 -1
- package/dist/components/markdown/index.js +1 -1
- package/dist/components/markdown/markdown.d.ts +13 -0
- package/dist/components/markdown/markdown.js +49 -1
- package/dist/components/markdown/markdown.svelte +85 -3
- package/dist/components/markdown/markdown.svelte.d.ts +6 -0
- package/dist/follow-scroll.svelte.d.ts +34 -0
- package/dist/follow-scroll.svelte.js +46 -0
- package/dist/transcript.svelte.d.ts +27 -0
- package/dist/transcript.svelte.js +27 -0
- 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.
|
|
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
|
}
|