@poodle64/librarian 2026.9.1
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 +115 -0
- package/dist/client.d.ts +57 -0
- package/dist/client.js +60 -0
- package/dist/components/activity-group/activity-group.svelte +63 -0
- package/dist/components/activity-group/activity-group.svelte.d.ts +9 -0
- package/dist/components/activity-group/index.d.ts +2 -0
- package/dist/components/activity-group/index.js +2 -0
- package/dist/components/agent-transcript/agent-transcript.svelte +71 -0
- package/dist/components/agent-transcript/agent-transcript.svelte.d.ts +12 -0
- package/dist/components/agent-transcript/index.d.ts +2 -0
- package/dist/components/agent-transcript/index.js +2 -0
- package/dist/components/composer/composer.svelte +115 -0
- package/dist/components/composer/composer.svelte.d.ts +15 -0
- package/dist/components/composer/index.d.ts +3 -0
- package/dist/components/composer/index.js +2 -0
- package/dist/components/markdown/index.d.ts +3 -0
- package/dist/components/markdown/index.js +3 -0
- package/dist/components/markdown/markdown.d.ts +37 -0
- package/dist/components/markdown/markdown.js +132 -0
- package/dist/components/markdown/markdown.svelte +221 -0
- package/dist/components/markdown/markdown.svelte.d.ts +9 -0
- package/dist/components/thinking-row/index.d.ts +2 -0
- package/dist/components/thinking-row/index.js +2 -0
- package/dist/components/thinking-row/thinking-row.svelte +35 -0
- package/dist/components/thinking-row/thinking-row.svelte.d.ts +8 -0
- package/dist/components/tool-row/index.d.ts +2 -0
- package/dist/components/tool-row/index.js +2 -0
- package/dist/components/tool-row/tool-row.svelte +77 -0
- package/dist/components/tool-row/tool-row.svelte.d.ts +11 -0
- package/dist/components/working/index.d.ts +2 -0
- package/dist/components/working/index.js +2 -0
- package/dist/components/working/working.svelte +50 -0
- package/dist/components/working/working.svelte.d.ts +7 -0
- package/dist/history.svelte.d.ts +37 -0
- package/dist/history.svelte.js +59 -0
- package/dist/transcript.svelte.d.ts +105 -0
- package/dist/transcript.svelte.js +316 -0
- package/package.json +70 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fold Claude Code's event stream into what a reader needs to see.
|
|
3
|
+
*
|
|
4
|
+
* It folds, it does not translate: every block here is a real content block
|
|
5
|
+
* from the stream (text, thinking, tool_use) and the fold only assembles the
|
|
6
|
+
* deltas that belong to each. An event type this does not know about is kept
|
|
7
|
+
* verbatim under `other`, so nothing is silently dropped.
|
|
8
|
+
*/
|
|
9
|
+
import type { AgentEvent } from './client';
|
|
10
|
+
export interface TextBlock {
|
|
11
|
+
kind: 'text';
|
|
12
|
+
index: number;
|
|
13
|
+
text: string;
|
|
14
|
+
}
|
|
15
|
+
export interface ThinkingBlock {
|
|
16
|
+
kind: 'thinking';
|
|
17
|
+
index: number;
|
|
18
|
+
text: string;
|
|
19
|
+
}
|
|
20
|
+
export interface ToolBlock {
|
|
21
|
+
kind: 'tool';
|
|
22
|
+
index: number;
|
|
23
|
+
name: string;
|
|
24
|
+
/** Accumulated `input_json_delta`. Parsed lazily — it is invalid JSON mid-stream. */
|
|
25
|
+
rawInput: string;
|
|
26
|
+
result?: string;
|
|
27
|
+
isError?: boolean;
|
|
28
|
+
}
|
|
29
|
+
export type Block = TextBlock | ThinkingBlock | ToolBlock;
|
|
30
|
+
export interface Outcome {
|
|
31
|
+
turns?: number;
|
|
32
|
+
costUsd?: number;
|
|
33
|
+
durationMs?: number;
|
|
34
|
+
isError?: boolean;
|
|
35
|
+
error?: string;
|
|
36
|
+
}
|
|
37
|
+
/** What the agent is DOING, in words a reader who has never seen a shell knows.
|
|
38
|
+
*
|
|
39
|
+
* The collapsed row is read by someone asking a question about documents, not
|
|
40
|
+
* by an engineer: `Bash ls -1 .` tells them nothing and looks like a leak from
|
|
41
|
+
* the machine room. The raw command is still one click away on expand, so this
|
|
42
|
+
* hides nothing — it just stops the transcript opening with jargon.
|
|
43
|
+
*/
|
|
44
|
+
export declare function describe(block: ToolBlock): {
|
|
45
|
+
verb: string;
|
|
46
|
+
object: string;
|
|
47
|
+
};
|
|
48
|
+
/** Best-effort one-line summary of a tool call, for the collapsed row.
|
|
49
|
+
*
|
|
50
|
+
* A staged path leads with a 64-character content hash and ends with the
|
|
51
|
+
* document's title — so truncating from the LEFT, as a path naturally does,
|
|
52
|
+
* shows the reader the noise and cuts the signal. Paths are elided from the
|
|
53
|
+
* front instead, which is why this is not just `slice(0, n)`.
|
|
54
|
+
*/
|
|
55
|
+
export declare function summarise(block: ToolBlock): string;
|
|
56
|
+
export declare class Transcript {
|
|
57
|
+
#private;
|
|
58
|
+
blocks: Block[];
|
|
59
|
+
/** Bumped on every applied event.
|
|
60
|
+
*
|
|
61
|
+
* Streaming grows an EXISTING block's `.text` in place, and Svelte's
|
|
62
|
+
* fine-grained tracking never sees that unless something reads it. An
|
|
63
|
+
* effect keyed on `blocks.length` therefore fires when a new block is
|
|
64
|
+
* pushed and never again — which is why a single long prose answer used to
|
|
65
|
+
* stream 870px past the fold with auto-scroll still "pinned" at the top.
|
|
66
|
+
* Consumers key on this instead. */
|
|
67
|
+
version: number;
|
|
68
|
+
sessionId: string | null;
|
|
69
|
+
model: string | null;
|
|
70
|
+
outcome: Outcome | null;
|
|
71
|
+
other: AgentEvent[];
|
|
72
|
+
reset(): void;
|
|
73
|
+
apply(event: AgentEvent): void;
|
|
74
|
+
}
|
|
75
|
+
export interface ActivityStep {
|
|
76
|
+
/** The block this step stands for; a repeated step keeps the FIRST. */
|
|
77
|
+
block: ToolBlock | ThinkingBlock;
|
|
78
|
+
/** How many identical consecutive steps collapsed into this one. */
|
|
79
|
+
repeats: number;
|
|
80
|
+
}
|
|
81
|
+
export interface ActivityGroup {
|
|
82
|
+
kind: 'activity';
|
|
83
|
+
index: number;
|
|
84
|
+
steps: ActivityStep[];
|
|
85
|
+
/** Distinct collections touched, for the one-line summary. */
|
|
86
|
+
collections: string[];
|
|
87
|
+
documents: number;
|
|
88
|
+
searches: number;
|
|
89
|
+
}
|
|
90
|
+
export type Segment = ActivityGroup | TextBlock;
|
|
91
|
+
/**
|
|
92
|
+
* Fold a turn's flat block list into what a reader should actually see.
|
|
93
|
+
*
|
|
94
|
+
* Two problems this solves, both reported off a real transcript:
|
|
95
|
+
*
|
|
96
|
+
* 1. Fifteen tool rows stood between the question and the first word of the
|
|
97
|
+
* answer, so the answer had to be scrolled to. Contiguous activity becomes
|
|
98
|
+
* ONE group the caller can collapse.
|
|
99
|
+
* 2. "Reading pspf guidelines 2026" appeared five times in a row — five pages
|
|
100
|
+
* of one document, which is one act of reading to a human. Consecutive
|
|
101
|
+
* steps with the same label collapse to one row carrying a count.
|
|
102
|
+
*/
|
|
103
|
+
export declare function segment(blocks: Block[]): Segment[];
|
|
104
|
+
/** One line describing a whole investigation, for the collapsed state. */
|
|
105
|
+
export declare function summariseActivity(group: ActivityGroup): string;
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fold Claude Code's event stream into what a reader needs to see.
|
|
3
|
+
*
|
|
4
|
+
* It folds, it does not translate: every block here is a real content block
|
|
5
|
+
* from the stream (text, thinking, tool_use) and the fold only assembles the
|
|
6
|
+
* deltas that belong to each. An event type this does not know about is kept
|
|
7
|
+
* verbatim under `other`, so nothing is silently dropped.
|
|
8
|
+
*/
|
|
9
|
+
/** What the agent is DOING, in words a reader who has never seen a shell knows.
|
|
10
|
+
*
|
|
11
|
+
* The collapsed row is read by someone asking a question about documents, not
|
|
12
|
+
* by an engineer: `Bash ls -1 .` tells them nothing and looks like a leak from
|
|
13
|
+
* the machine room. The raw command is still one click away on expand, so this
|
|
14
|
+
* hides nothing — it just stops the transcript opening with jargon.
|
|
15
|
+
*/
|
|
16
|
+
export function describe(block) {
|
|
17
|
+
const input = parseInput(block);
|
|
18
|
+
const command = typeof input.command === 'string' ? input.command : '';
|
|
19
|
+
if (block.name === 'Read') {
|
|
20
|
+
const path = str(input.file_path ?? input.path);
|
|
21
|
+
return { verb: 'Reading', object: documentName(path) };
|
|
22
|
+
}
|
|
23
|
+
if (block.name === 'Grep') {
|
|
24
|
+
return { verb: 'Searching for', object: str(input.pattern) };
|
|
25
|
+
}
|
|
26
|
+
if (block.name === 'Glob') {
|
|
27
|
+
return { verb: 'Looking for files', object: str(input.pattern) };
|
|
28
|
+
}
|
|
29
|
+
if (block.name === 'Bash') {
|
|
30
|
+
if (/\bgrep\b|\brg\b/.test(command)) {
|
|
31
|
+
const quoted = command.match(/["']([^"']{2,60})["']/);
|
|
32
|
+
return { verb: 'Searching for', object: quoted?.[1] ?? 'a phrase' };
|
|
33
|
+
}
|
|
34
|
+
if (/\bls\b/.test(command))
|
|
35
|
+
return { verb: 'Listing', object: listTarget(command) };
|
|
36
|
+
if (/\bfind\b/.test(command))
|
|
37
|
+
return { verb: 'Looking for files', object: '' };
|
|
38
|
+
if (/\bcat\b|\bhead\b|\bsed\b/.test(command)) {
|
|
39
|
+
return { verb: 'Reading', object: documentName(lastPath(command)) };
|
|
40
|
+
}
|
|
41
|
+
if (/\bwc\b/.test(command))
|
|
42
|
+
return { verb: 'Counting', object: '' };
|
|
43
|
+
return { verb: 'Running a command', object: '' };
|
|
44
|
+
}
|
|
45
|
+
return { verb: block.name, object: summarise(block) };
|
|
46
|
+
}
|
|
47
|
+
function parseInput(block) {
|
|
48
|
+
try {
|
|
49
|
+
return JSON.parse(block.rawInput);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return {};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const str = (value) => (typeof value === 'string' ? value : '');
|
|
56
|
+
/** A staged path ends with the document's title slug; that is its name. */
|
|
57
|
+
function documentName(path) {
|
|
58
|
+
if (!path)
|
|
59
|
+
return '';
|
|
60
|
+
const segments = path.split('/').filter((s) => s && s !== '.');
|
|
61
|
+
const page = segments.at(-1) ?? '';
|
|
62
|
+
// `.../<title-slug>/page-004.md` — the page number is noise, the slug is not.
|
|
63
|
+
if (/^page-\d+\.md$/.test(page))
|
|
64
|
+
return prettify(segments.at(-2) ?? '');
|
|
65
|
+
return prettify(page.replace(/\.md$/, ''));
|
|
66
|
+
}
|
|
67
|
+
function listTarget(command) {
|
|
68
|
+
const target = command.trim().split(/\s+/).at(-1) ?? '';
|
|
69
|
+
if (!target || target === '.' || target.startsWith('-'))
|
|
70
|
+
return 'the collections';
|
|
71
|
+
return prettify(target.replace(/\/$/, ''));
|
|
72
|
+
}
|
|
73
|
+
function lastPath(command) {
|
|
74
|
+
return (command
|
|
75
|
+
.split(/\s+/)
|
|
76
|
+
.filter((t) => t.includes('/') || t.endsWith('.md'))
|
|
77
|
+
.at(-1) ?? '');
|
|
78
|
+
}
|
|
79
|
+
/** Slugs are the corpus's own titles; a reader should see words, not kebab-case. */
|
|
80
|
+
function prettify(slug) {
|
|
81
|
+
if (!slug)
|
|
82
|
+
return '';
|
|
83
|
+
return slug.replace(/[-_]+/g, ' ');
|
|
84
|
+
}
|
|
85
|
+
/** Best-effort one-line summary of a tool call, for the collapsed row.
|
|
86
|
+
*
|
|
87
|
+
* A staged path leads with a 64-character content hash and ends with the
|
|
88
|
+
* document's title — so truncating from the LEFT, as a path naturally does,
|
|
89
|
+
* shows the reader the noise and cuts the signal. Paths are elided from the
|
|
90
|
+
* front instead, which is why this is not just `slice(0, n)`.
|
|
91
|
+
*/
|
|
92
|
+
export function summarise(block) {
|
|
93
|
+
let value = block.rawInput.slice(0, 200);
|
|
94
|
+
try {
|
|
95
|
+
const input = JSON.parse(block.rawInput);
|
|
96
|
+
const first = input.command ?? input.pattern ?? input.file_path ?? input.path ?? input.description;
|
|
97
|
+
if (typeof first === 'string')
|
|
98
|
+
value = first;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
/* mid-stream JSON is expected to be partial */
|
|
102
|
+
}
|
|
103
|
+
return value.includes('/') ? elidePath(value) : value;
|
|
104
|
+
}
|
|
105
|
+
/** Keep the last two path segments — the document title and the page. */
|
|
106
|
+
function elidePath(value) {
|
|
107
|
+
const segments = value.split('/').filter(Boolean);
|
|
108
|
+
if (segments.length <= 2)
|
|
109
|
+
return value;
|
|
110
|
+
return `…/${segments.slice(-2).join('/')}`;
|
|
111
|
+
}
|
|
112
|
+
export class Transcript {
|
|
113
|
+
blocks = $state([]);
|
|
114
|
+
/** Bumped on every applied event.
|
|
115
|
+
*
|
|
116
|
+
* Streaming grows an EXISTING block's `.text` in place, and Svelte's
|
|
117
|
+
* fine-grained tracking never sees that unless something reads it. An
|
|
118
|
+
* effect keyed on `blocks.length` therefore fires when a new block is
|
|
119
|
+
* pushed and never again — which is why a single long prose answer used to
|
|
120
|
+
* stream 870px past the fold with auto-scroll still "pinned" at the top.
|
|
121
|
+
* Consumers key on this instead. */
|
|
122
|
+
version = $state(0);
|
|
123
|
+
sessionId = $state(null);
|
|
124
|
+
model = $state(null);
|
|
125
|
+
outcome = $state(null);
|
|
126
|
+
other = $state([]);
|
|
127
|
+
/** Content-block index is per MESSAGE, so it repeats across turns; this
|
|
128
|
+
* maps the live index onto a position in the flat list. Cleared whenever a
|
|
129
|
+
* message starts, which is what stops turn two overwriting turn one. */
|
|
130
|
+
// Deliberately a plain Map, not a SvelteMap: nothing renders it, it is
|
|
131
|
+
// written on every content-block delta while an answer streams, and giving
|
|
132
|
+
// each entry its own reactive signal would buy a re-render nobody reads.
|
|
133
|
+
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
|
134
|
+
#open = new Map();
|
|
135
|
+
reset() {
|
|
136
|
+
this.blocks = [];
|
|
137
|
+
this.outcome = null;
|
|
138
|
+
this.other = [];
|
|
139
|
+
this.#open.clear();
|
|
140
|
+
}
|
|
141
|
+
apply(event) {
|
|
142
|
+
this.version += 1;
|
|
143
|
+
if (event.type === 'system' && event.subtype === 'init') {
|
|
144
|
+
this.sessionId = event.session_id ?? null;
|
|
145
|
+
this.model = event.model ?? null;
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (event.type === 'result') {
|
|
149
|
+
this.outcome = {
|
|
150
|
+
turns: event.num_turns,
|
|
151
|
+
costUsd: event.total_cost_usd,
|
|
152
|
+
durationMs: event.duration_ms,
|
|
153
|
+
isError: event.is_error
|
|
154
|
+
};
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (event.type === 'library_error') {
|
|
158
|
+
this.outcome = { isError: true, error: event.error ?? 'the agent failed' };
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
// A tool RESULT arrives as a user message carrying tool_result blocks.
|
|
162
|
+
if (event.type === 'user') {
|
|
163
|
+
for (const block of event.message?.content ?? []) {
|
|
164
|
+
if (block.type !== 'tool_result')
|
|
165
|
+
continue;
|
|
166
|
+
const target = [...this.blocks].reverse().find((b) => b.kind === 'tool' && !b.result);
|
|
167
|
+
if (target && target.kind === 'tool') {
|
|
168
|
+
target.result = renderResult(block.content);
|
|
169
|
+
target.isError = block.is_error === true;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (event.type !== 'stream_event' || !event.event) {
|
|
175
|
+
if (event.type !== 'assistant')
|
|
176
|
+
this.other.push(event);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const inner = event.event;
|
|
180
|
+
if (inner.type === 'message_start') {
|
|
181
|
+
this.#open.clear();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (inner.type === 'content_block_start' && inner.index !== undefined) {
|
|
185
|
+
const cb = inner.content_block;
|
|
186
|
+
if (!cb)
|
|
187
|
+
return;
|
|
188
|
+
const position = this.blocks.length;
|
|
189
|
+
this.#open.set(inner.index, position);
|
|
190
|
+
if (cb.type === 'text')
|
|
191
|
+
this.blocks.push({ kind: 'text', index: position, text: '' });
|
|
192
|
+
else if (cb.type === 'thinking')
|
|
193
|
+
this.blocks.push({ kind: 'thinking', index: position, text: '' });
|
|
194
|
+
else if (cb.type === 'tool_use')
|
|
195
|
+
this.blocks.push({
|
|
196
|
+
kind: 'tool',
|
|
197
|
+
index: position,
|
|
198
|
+
name: cb.name ?? 'tool',
|
|
199
|
+
rawInput: ''
|
|
200
|
+
});
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (inner.type === 'content_block_delta' && inner.index !== undefined) {
|
|
204
|
+
const position = this.#open.get(inner.index);
|
|
205
|
+
if (position === undefined)
|
|
206
|
+
return;
|
|
207
|
+
const block = this.blocks[position];
|
|
208
|
+
const delta = inner.delta;
|
|
209
|
+
if (!block || !delta)
|
|
210
|
+
return;
|
|
211
|
+
if (delta.type === 'text_delta' && block.kind === 'text')
|
|
212
|
+
block.text += delta.text ?? '';
|
|
213
|
+
else if (delta.type === 'thinking_delta' && block.kind === 'thinking')
|
|
214
|
+
block.text += delta.thinking ?? '';
|
|
215
|
+
else if (delta.type === 'input_json_delta' && block.kind === 'tool')
|
|
216
|
+
block.rawInput += delta.partial_json ?? '';
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function renderResult(content) {
|
|
221
|
+
if (typeof content === 'string')
|
|
222
|
+
return content;
|
|
223
|
+
if (Array.isArray(content)) {
|
|
224
|
+
return content
|
|
225
|
+
.map((part) => typeof part === 'object' && part !== null && 'text' in part
|
|
226
|
+
? String(part.text)
|
|
227
|
+
: '')
|
|
228
|
+
.join('');
|
|
229
|
+
}
|
|
230
|
+
return '';
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Fold a turn's flat block list into what a reader should actually see.
|
|
234
|
+
*
|
|
235
|
+
* Two problems this solves, both reported off a real transcript:
|
|
236
|
+
*
|
|
237
|
+
* 1. Fifteen tool rows stood between the question and the first word of the
|
|
238
|
+
* answer, so the answer had to be scrolled to. Contiguous activity becomes
|
|
239
|
+
* ONE group the caller can collapse.
|
|
240
|
+
* 2. "Reading pspf guidelines 2026" appeared five times in a row — five pages
|
|
241
|
+
* of one document, which is one act of reading to a human. Consecutive
|
|
242
|
+
* steps with the same label collapse to one row carrying a count.
|
|
243
|
+
*/
|
|
244
|
+
export function segment(blocks) {
|
|
245
|
+
const out = [];
|
|
246
|
+
let current = null;
|
|
247
|
+
for (const block of blocks) {
|
|
248
|
+
if (block.kind === 'text') {
|
|
249
|
+
current = null;
|
|
250
|
+
out.push(block);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
// A thinking block with no text is a row whose chevron opens on nothing.
|
|
254
|
+
// Claude Code's thinking display defaults to "omitted", so most arrive
|
|
255
|
+
// empty — rendering them is worse than dropping them.
|
|
256
|
+
if (block.kind === 'thinking' && !block.text.trim())
|
|
257
|
+
continue;
|
|
258
|
+
if (!current) {
|
|
259
|
+
current = {
|
|
260
|
+
kind: 'activity',
|
|
261
|
+
index: block.index,
|
|
262
|
+
steps: [],
|
|
263
|
+
collections: [],
|
|
264
|
+
documents: 0,
|
|
265
|
+
searches: 0
|
|
266
|
+
};
|
|
267
|
+
out.push(current);
|
|
268
|
+
}
|
|
269
|
+
const last = current.steps.at(-1);
|
|
270
|
+
if (last && sameStep(last.block, block)) {
|
|
271
|
+
last.repeats += 1;
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
current.steps.push({ block, repeats: 1 });
|
|
275
|
+
}
|
|
276
|
+
if (block.kind === 'tool')
|
|
277
|
+
tally(current, block);
|
|
278
|
+
}
|
|
279
|
+
return out;
|
|
280
|
+
}
|
|
281
|
+
function sameStep(a, b) {
|
|
282
|
+
if (a.kind !== b.kind)
|
|
283
|
+
return false;
|
|
284
|
+
if (a.kind === 'thinking')
|
|
285
|
+
return true;
|
|
286
|
+
const left = describe(a);
|
|
287
|
+
const right = describe(b);
|
|
288
|
+
return left.verb === right.verb && left.object === right.object;
|
|
289
|
+
}
|
|
290
|
+
function tally(group, block) {
|
|
291
|
+
const { verb } = describe(block);
|
|
292
|
+
if (verb === 'Searching for')
|
|
293
|
+
group.searches += 1;
|
|
294
|
+
if (verb === 'Reading')
|
|
295
|
+
group.documents += 1;
|
|
296
|
+
const collection = collectionOf(block);
|
|
297
|
+
if (collection && !group.collections.includes(collection))
|
|
298
|
+
group.collections.push(collection);
|
|
299
|
+
}
|
|
300
|
+
/** The first path segment under the corpus root IS the collection name. */
|
|
301
|
+
function collectionOf(block) {
|
|
302
|
+
const raw = block.rawInput;
|
|
303
|
+
const match = raw.match(/(?:^|["'\s/])([a-z0-9]+(?:-[a-z0-9]+)+)\/local\//);
|
|
304
|
+
return match?.[1] ?? '';
|
|
305
|
+
}
|
|
306
|
+
/** One line describing a whole investigation, for the collapsed state. */
|
|
307
|
+
export function summariseActivity(group) {
|
|
308
|
+
const parts = [];
|
|
309
|
+
if (group.searches)
|
|
310
|
+
parts.push(`${group.searches} search${group.searches === 1 ? '' : 'es'}`);
|
|
311
|
+
if (group.documents)
|
|
312
|
+
parts.push(`${group.documents} document${group.documents === 1 ? '' : 's'} read`);
|
|
313
|
+
if (group.collections.length)
|
|
314
|
+
parts.push(`${group.collections.length} collection${group.collections.length === 1 ? '' : 's'}`);
|
|
315
|
+
return parts.length ? parts.join(' · ') : 'Worked on it';
|
|
316
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@poodle64/librarian",
|
|
3
|
+
"version": "2026.9.1",
|
|
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
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": "github:radar-hooves/design-system",
|
|
8
|
+
"publishConfig": {
|
|
9
|
+
"registry": "https://registry.npmjs.org",
|
|
10
|
+
"access": "public",
|
|
11
|
+
"provenance": false
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"exports": {
|
|
17
|
+
"./package.json": "./package.json",
|
|
18
|
+
"./client": {
|
|
19
|
+
"types": "./dist/client.d.ts",
|
|
20
|
+
"svelte": "./dist/client.js"
|
|
21
|
+
},
|
|
22
|
+
"./transcript": {
|
|
23
|
+
"types": "./dist/transcript.svelte.d.ts",
|
|
24
|
+
"svelte": "./dist/transcript.svelte.js"
|
|
25
|
+
},
|
|
26
|
+
"./history": {
|
|
27
|
+
"types": "./dist/history.svelte.d.ts",
|
|
28
|
+
"svelte": "./dist/history.svelte.js"
|
|
29
|
+
},
|
|
30
|
+
"./*": {
|
|
31
|
+
"types": "./dist/components/*/index.d.ts",
|
|
32
|
+
"svelte": "./dist/components/*/index.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"svelte": "^5.54.0",
|
|
37
|
+
"@poodle64/ui": "^2026.9.2",
|
|
38
|
+
"@lucide/svelte": "^1.7.0",
|
|
39
|
+
"marked": "^18.0.11",
|
|
40
|
+
"isomorphic-dompurify": "^3.23.0",
|
|
41
|
+
"shiki": "^4.4.3"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@lucide/svelte": "^1.7.0",
|
|
45
|
+
"@poodle64/ui": "workspace:*",
|
|
46
|
+
"@sveltejs/kit": "^2.63.0",
|
|
47
|
+
"@sveltejs/package": "^2.5.8",
|
|
48
|
+
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
|
49
|
+
"@testing-library/jest-dom": "^7.0.0",
|
|
50
|
+
"@testing-library/svelte": "^5.4.2",
|
|
51
|
+
"@types/node": "^22.20.1",
|
|
52
|
+
"isomorphic-dompurify": "^3.23.0",
|
|
53
|
+
"jsdom": "^29.1.1",
|
|
54
|
+
"marked": "^18.0.11",
|
|
55
|
+
"publint": "^0.3.15",
|
|
56
|
+
"shiki": "^4.4.3",
|
|
57
|
+
"svelte": "^5.56.2",
|
|
58
|
+
"svelte-check": "^4.6.0",
|
|
59
|
+
"typescript": "^6.0.3",
|
|
60
|
+
"vite": "^8.0.16",
|
|
61
|
+
"vitest": "^4.1.10"
|
|
62
|
+
},
|
|
63
|
+
"scripts": {
|
|
64
|
+
"build": "svelte-kit sync && svelte-package && publint",
|
|
65
|
+
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
66
|
+
"test": "pnpm run build && vitest run",
|
|
67
|
+
"prepublishOnly": "pnpm run build"
|
|
68
|
+
},
|
|
69
|
+
"packageManager": "pnpm@10.28.0"
|
|
70
|
+
}
|