@poodle64/librarian 2026.10.4 → 2026.10.5
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 +7 -5
- package/dist/components/activity-group/activity-group.svelte +99 -17
- package/dist/components/activity-group/activity-group.svelte.d.ts +4 -1
- package/dist/components/agent-transcript/agent-transcript.svelte +82 -26
- package/dist/components/agent-transcript/agent-transcript.svelte.d.ts +4 -3
- package/dist/components/artefact-pane/artefact-pane.svelte +278 -78
- package/dist/components/conversation/conversation.svelte +6 -6
- package/dist/components/conversation/conversation.svelte.d.ts +4 -4
- package/dist/components/markdown/markdown.d.ts +27 -10
- package/dist/components/markdown/markdown.js +99 -22
- package/dist/components/markdown/markdown.svelte +122 -28
- package/dist/components/markdown/markdown.svelte.d.ts +12 -4
- package/dist/components/source-fold/index.d.ts +2 -0
- package/dist/components/source-fold/index.js +2 -0
- package/dist/components/source-fold/source-fold.svelte +225 -0
- package/dist/components/source-fold/source-fold.svelte.d.ts +17 -0
- package/dist/components/source-list/source-list.svelte +5 -3
- package/dist/components/source-list/source-list.svelte.d.ts +2 -0
- package/dist/components/thinking-row/thinking-row.svelte +37 -16
- package/dist/components/thinking-row/thinking-row.svelte.d.ts +2 -0
- package/dist/components/tool-row/tool-row.svelte +130 -38
- package/dist/components/visual-block/entity-card.svelte +94 -0
- package/dist/components/visual-block/entity-card.svelte.d.ts +7 -0
- package/dist/components/visual-block/timeline.svelte +99 -0
- package/dist/components/visual-block/timeline.svelte.d.ts +7 -0
- package/dist/components/visual-block/vega-chart.svelte +110 -0
- package/dist/components/visual-block/vega-chart.svelte.d.ts +7 -0
- package/dist/components/visual-block/visual-block.svelte +53 -0
- package/dist/components/visual-block/visual-block.svelte.d.ts +13 -0
- package/dist/components/written-card/written-card.svelte +97 -49
- package/dist/components/written-card/written-card.svelte.d.ts +1 -1
- package/dist/copy.d.ts +54 -0
- package/dist/copy.js +44 -1
- package/dist/diff.d.ts +8 -0
- package/dist/diff.js +37 -0
- package/dist/steps.d.ts +28 -0
- package/dist/steps.js +168 -0
- package/dist/transcript.svelte.d.ts +4 -0
- package/dist/transcript.svelte.js +13 -2
- package/dist/visuals.d.ts +38 -0
- package/dist/visuals.js +82 -0
- package/dist/web-sources.d.ts +19 -3
- package/dist/web-sources.js +39 -7
- package/dist/written.d.ts +26 -1
- package/dist/written.js +42 -0
- package/package.json +10 -1
- package/dist/components/web-list/index.d.ts +0 -2
- package/dist/components/web-list/index.js +0 -2
- package/dist/components/web-list/web-list.svelte +0 -84
- package/dist/components/web-list/web-list.svelte.d.ts +0 -9
package/dist/steps.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A turn's steps, said in one sentence.
|
|
3
|
+
*
|
|
4
|
+
* The turn folds its steps behind one line composed from them ("Read your
|
|
5
|
+
* attachment, searched the library and the web, and wrote a minute"), the way
|
|
6
|
+
* Claude.ai folds its own. Composed here from what each step IS (its tool and,
|
|
7
|
+
* for a document, the document's own kind), never from the persona's words.
|
|
8
|
+
*/
|
|
9
|
+
import { fill } from './copy';
|
|
10
|
+
const WEB = new Set(['mcp__supacrawl__supacrawl_search', 'mcp__supacrawl__supacrawl_scrape']);
|
|
11
|
+
/** What sort of step a block is. */
|
|
12
|
+
export function stepKind(block) {
|
|
13
|
+
if (block.kind !== 'tool')
|
|
14
|
+
return 'thought';
|
|
15
|
+
const { name } = block;
|
|
16
|
+
if (name === 'mcp__library__search')
|
|
17
|
+
return 'library';
|
|
18
|
+
if (name === 'mcp__library__document_content' || name === 'mcp__library__document_detail')
|
|
19
|
+
return 'open';
|
|
20
|
+
if (WEB.has(name))
|
|
21
|
+
return 'web';
|
|
22
|
+
if (name === 'Write')
|
|
23
|
+
return 'write';
|
|
24
|
+
if (name === 'Edit')
|
|
25
|
+
return 'edit';
|
|
26
|
+
if (name.startsWith('mcp__') && name.endsWith('__ocr'))
|
|
27
|
+
return 'attachment';
|
|
28
|
+
if (name === 'Read') {
|
|
29
|
+
// A staged library page reads as a document opened; anything else Read
|
|
30
|
+
// takes is what the person attached.
|
|
31
|
+
const path = pathOf(block);
|
|
32
|
+
return /page-\d+\.md$/.test(path) ? 'open' : 'attachment';
|
|
33
|
+
}
|
|
34
|
+
return 'other';
|
|
35
|
+
}
|
|
36
|
+
function inputOf(block) {
|
|
37
|
+
try {
|
|
38
|
+
const parsed = JSON.parse(block.rawInput);
|
|
39
|
+
return typeof parsed === 'object' && parsed !== null ? parsed : {};
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const pathOf = (block) => {
|
|
46
|
+
const input = inputOf(block);
|
|
47
|
+
const path = input.file_path ?? input.path;
|
|
48
|
+
return typeof path === 'string' ? path : '';
|
|
49
|
+
};
|
|
50
|
+
/** The document a Write or Edit step made a version of, by the name it wrote to. */
|
|
51
|
+
function documentOf(block, written) {
|
|
52
|
+
const path = pathOf(block);
|
|
53
|
+
if (!path)
|
|
54
|
+
return undefined;
|
|
55
|
+
return written.filter((w) => path === w.name || path.endsWith(`/${w.name}`)).at(-1);
|
|
56
|
+
}
|
|
57
|
+
const kindWord = (doc, words) => ((doc?.kind ?? '').trim() || words.documentKind).toLowerCase();
|
|
58
|
+
const upper = (text) => text.charAt(0).toUpperCase() + text.slice(1);
|
|
59
|
+
function join(phrases, and) {
|
|
60
|
+
if (phrases.length <= 2)
|
|
61
|
+
return phrases.join(` ${and} `);
|
|
62
|
+
return `${phrases.slice(0, -1).join(', ')}, ${and} ${phrases.at(-1)}`;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The fold's line: the sentence, and what trails it ("6 steps", or the
|
|
66
|
+
* version a lone edit made).
|
|
67
|
+
*/
|
|
68
|
+
export function stepsSummary(steps, written, words) {
|
|
69
|
+
let library = false;
|
|
70
|
+
let web = false;
|
|
71
|
+
let searchAt = -1;
|
|
72
|
+
const phrases = [];
|
|
73
|
+
const add = (phrase) => {
|
|
74
|
+
if (!phrases.includes(phrase))
|
|
75
|
+
phrases.push(phrase);
|
|
76
|
+
};
|
|
77
|
+
for (const { block } of steps) {
|
|
78
|
+
const kind = stepKind(block);
|
|
79
|
+
const doc = block.kind === 'tool' ? documentOf(block, written) : undefined;
|
|
80
|
+
if (kind === 'library' || kind === 'web') {
|
|
81
|
+
if (kind === 'library')
|
|
82
|
+
library = true;
|
|
83
|
+
else
|
|
84
|
+
web = true;
|
|
85
|
+
if (searchAt < 0) {
|
|
86
|
+
searchAt = phrases.length;
|
|
87
|
+
phrases.push('');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else if (kind === 'attachment')
|
|
91
|
+
add(words.stepsRead);
|
|
92
|
+
else if (kind === 'open')
|
|
93
|
+
add(words.stepsOpened);
|
|
94
|
+
else if (kind === 'write')
|
|
95
|
+
add(fill(words.stepsWrote, { kind: kindWord(doc, words) }));
|
|
96
|
+
else if (kind === 'edit')
|
|
97
|
+
add(fill(words.stepsEdited, { kind: kindWord(doc, words) }));
|
|
98
|
+
else if (kind === 'other')
|
|
99
|
+
add(words.stepsOther);
|
|
100
|
+
}
|
|
101
|
+
if (searchAt >= 0)
|
|
102
|
+
phrases[searchAt] =
|
|
103
|
+
library && web ? words.stepsSearchedBoth : library ? words.stepsSearchedLibrary : words.stepsSearchedWeb;
|
|
104
|
+
// Thinking is told only where it is all there was.
|
|
105
|
+
if (phrases.length === 0)
|
|
106
|
+
phrases.push(words.stepsThought);
|
|
107
|
+
const only = steps.length === 1 ? steps[0].block : undefined;
|
|
108
|
+
const lone = only?.kind === 'tool' && stepKind(only) === 'edit' ? documentOf(only, written) : undefined;
|
|
109
|
+
return {
|
|
110
|
+
sentence: upper(join(phrases, words.stepsAnd)),
|
|
111
|
+
trail: lone
|
|
112
|
+
? fill(words.writtenVersion, { n: lone.version })
|
|
113
|
+
: steps.length === 1
|
|
114
|
+
? words.stepCountOne
|
|
115
|
+
: fill(words.stepCount, { n: steps.length })
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/** What is happening now: the step still running, or the latest one. */
|
|
119
|
+
export function stepsNow(steps, written, words) {
|
|
120
|
+
const step = steps.findLast(({ block }) => block.kind === 'tool' && block.result === undefined)
|
|
121
|
+
?? steps.at(-1);
|
|
122
|
+
if (!step)
|
|
123
|
+
return words.nowOther;
|
|
124
|
+
const kind = stepKind(step.block);
|
|
125
|
+
const doc = step.block.kind === 'tool' ? documentOf(step.block, written) : undefined;
|
|
126
|
+
const now = {
|
|
127
|
+
attachment: words.nowRead,
|
|
128
|
+
library: words.nowLibrary,
|
|
129
|
+
web: words.nowWeb,
|
|
130
|
+
open: words.nowOpen,
|
|
131
|
+
write: fill(words.nowWrite, { kind: kindWord(doc, words) }),
|
|
132
|
+
edit: fill(words.nowEdit, { kind: kindWord(doc, words) }),
|
|
133
|
+
thought: words.nowThought,
|
|
134
|
+
other: words.nowOther
|
|
135
|
+
};
|
|
136
|
+
return now[kind];
|
|
137
|
+
}
|
|
138
|
+
function parse(text) {
|
|
139
|
+
try {
|
|
140
|
+
return text ? JSON.parse(text) : undefined;
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/** The count at a step's right hand, where its result shows one. */
|
|
147
|
+
export function stepMeta(block, words) {
|
|
148
|
+
if (block.kind !== 'tool' || block.isError || block.result === undefined)
|
|
149
|
+
return '';
|
|
150
|
+
if (block.name === 'mcp__library__search') {
|
|
151
|
+
const found = parse(block.result);
|
|
152
|
+
const list = Array.isArray(found) ? found : (found?.hits ?? found?.results);
|
|
153
|
+
return Array.isArray(list) ? fill(words.stepFound, { n: list.length }) : '';
|
|
154
|
+
}
|
|
155
|
+
if (block.name === 'mcp__supacrawl__supacrawl_search') {
|
|
156
|
+
const found = parse(block.result)?.results;
|
|
157
|
+
if (!Array.isArray(found))
|
|
158
|
+
return '';
|
|
159
|
+
return found.length === 1 ? words.stepPageRead : fill(words.stepPagesRead, { n: found.length });
|
|
160
|
+
}
|
|
161
|
+
return '';
|
|
162
|
+
}
|
|
163
|
+
/** How long a thinking block ran, in whole seconds, where it was timed. */
|
|
164
|
+
export function thoughtSeconds(block) {
|
|
165
|
+
if (block.kind !== 'thinking' || block.startedAt === undefined || block.endedAt === undefined)
|
|
166
|
+
return undefined;
|
|
167
|
+
return Math.max(1, Math.round((block.endedAt - block.startedAt) / 1000));
|
|
168
|
+
}
|
|
@@ -18,6 +18,10 @@ export interface ThinkingBlock {
|
|
|
18
18
|
kind: 'thinking';
|
|
19
19
|
index: number;
|
|
20
20
|
text: string;
|
|
21
|
+
/** Epoch ms the block opened and closed on this page, measured live; absent
|
|
22
|
+
* for one the stream delivered whole or read back. */
|
|
23
|
+
startedAt?: number;
|
|
24
|
+
endedAt?: number;
|
|
21
25
|
}
|
|
22
26
|
export interface ToolBlock {
|
|
23
27
|
kind: 'tool';
|
|
@@ -235,7 +235,12 @@ export function fold(into, state, event) {
|
|
|
235
235
|
version: event.version,
|
|
236
236
|
media_type: typeof event.media_type === 'string' ? event.media_type : '',
|
|
237
237
|
bytes: typeof event.bytes === 'number' ? event.bytes : undefined,
|
|
238
|
-
formats: Array.isArray(event.formats) ? event.formats.map(String) : []
|
|
238
|
+
formats: Array.isArray(event.formats) ? event.formats.map(String) : [],
|
|
239
|
+
title: typeof event.title === 'string' && event.title ? event.title : event.name,
|
|
240
|
+
kind: typeof event.kind === 'string' ? event.kind : null,
|
|
241
|
+
changes: typeof event.changes === 'string' ? event.changes : null,
|
|
242
|
+
pages: typeof event.pages === 'number' ? event.pages : 0,
|
|
243
|
+
at: Date.now()
|
|
239
244
|
});
|
|
240
245
|
}
|
|
241
246
|
return true;
|
|
@@ -293,7 +298,7 @@ export function fold(into, state, event) {
|
|
|
293
298
|
if (cb.type === 'text')
|
|
294
299
|
into.blocks.push({ kind: 'text', index: position, text: '' });
|
|
295
300
|
else if (cb.type === 'thinking')
|
|
296
|
-
into.blocks.push({ kind: 'thinking', index: position, text: '' });
|
|
301
|
+
into.blocks.push({ kind: 'thinking', index: position, text: '', startedAt: Date.now() });
|
|
297
302
|
else if (cb.type === 'tool_use')
|
|
298
303
|
into.blocks.push({
|
|
299
304
|
kind: 'tool',
|
|
@@ -304,6 +309,12 @@ export function fold(into, state, event) {
|
|
|
304
309
|
});
|
|
305
310
|
return true;
|
|
306
311
|
}
|
|
312
|
+
if (inner.type === 'content_block_stop' && inner.index !== undefined) {
|
|
313
|
+
const block = into.blocks[state.open.get(inner.index) ?? -1];
|
|
314
|
+
if (block?.kind === 'thinking' && block.startedAt !== undefined)
|
|
315
|
+
block.endedAt = Date.now();
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
307
318
|
if (inner.type === 'content_block_delta' && inner.index !== undefined) {
|
|
308
319
|
const position = state.open.get(inner.index);
|
|
309
320
|
if (position === undefined)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The renderers for a persona's in-answer visuals, keyed on a fence's language.
|
|
3
|
+
*
|
|
4
|
+
* The persona writes a fenced block and the page draws it: the data is the
|
|
5
|
+
* persona's, every colour, face and size is the app's. A renderer is a
|
|
6
|
+
* component and the `parse` that turns the block's text into the `value` it
|
|
7
|
+
* draws, throwing on a block that is not one (the page then shows the code).
|
|
8
|
+
* `parse` returns anything, not JSON alone, so a renderer for a language that
|
|
9
|
+
* is not JSON registers the same way.
|
|
10
|
+
*/
|
|
11
|
+
import type { Component } from 'svelte';
|
|
12
|
+
export interface Visual {
|
|
13
|
+
component: Component<{
|
|
14
|
+
value: unknown;
|
|
15
|
+
source: string;
|
|
16
|
+
}>;
|
|
17
|
+
/** The block's text as the value its component draws; throws on a block that is invalid. */
|
|
18
|
+
parse: (source: string) => unknown;
|
|
19
|
+
}
|
|
20
|
+
export interface TimelineValue {
|
|
21
|
+
title: string;
|
|
22
|
+
events: Array<{
|
|
23
|
+
date: string;
|
|
24
|
+
title: string;
|
|
25
|
+
detail: string;
|
|
26
|
+
}>;
|
|
27
|
+
}
|
|
28
|
+
export interface CardValue {
|
|
29
|
+
name: string;
|
|
30
|
+
kind: string;
|
|
31
|
+
summary: string;
|
|
32
|
+
facts: Array<{
|
|
33
|
+
label: string;
|
|
34
|
+
value: string;
|
|
35
|
+
}>;
|
|
36
|
+
}
|
|
37
|
+
/** A chart, a timeline and an entity card. An app adds to these or replaces one through `ChatExtensions.visuals`. */
|
|
38
|
+
export declare const defaultVisuals: Record<string, Visual>;
|
package/dist/visuals.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The renderers for a persona's in-answer visuals, keyed on a fence's language.
|
|
3
|
+
*
|
|
4
|
+
* The persona writes a fenced block and the page draws it: the data is the
|
|
5
|
+
* persona's, every colour, face and size is the app's. A renderer is a
|
|
6
|
+
* component and the `parse` that turns the block's text into the `value` it
|
|
7
|
+
* draws, throwing on a block that is not one (the page then shows the code).
|
|
8
|
+
* `parse` returns anything, not JSON alone, so a renderer for a language that
|
|
9
|
+
* is not JSON registers the same way.
|
|
10
|
+
*/
|
|
11
|
+
import EntityCard from './components/visual-block/entity-card.svelte';
|
|
12
|
+
import Timeline from './components/visual-block/timeline.svelte';
|
|
13
|
+
import VegaChart from './components/visual-block/vega-chart.svelte';
|
|
14
|
+
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
15
|
+
const text = (value) => typeof value === 'string' ? value : typeof value === 'number' ? String(value) : '';
|
|
16
|
+
const object = (source) => {
|
|
17
|
+
const value = JSON.parse(source);
|
|
18
|
+
if (!isRecord(value))
|
|
19
|
+
throw new Error('not an object');
|
|
20
|
+
return value;
|
|
21
|
+
};
|
|
22
|
+
/** Any string naming an address: a spec that carries one could fetch it, or link to it carrying the conversation out. */
|
|
23
|
+
const ADDRESS = /^[a-z][a-z0-9+.-]*:|\/\//i;
|
|
24
|
+
/**
|
|
25
|
+
* A spec that names an address anywhere, by key or by value, or carries
|
|
26
|
+
* `usermeta`, whose `embedOptions` vega-embed merges over the app's own
|
|
27
|
+
* (`config` or `patch` as a URL is fetched, `actions` turns the export menu back on).
|
|
28
|
+
*/
|
|
29
|
+
function addressed(value) {
|
|
30
|
+
if (typeof value === 'string')
|
|
31
|
+
return ADDRESS.test(value.trim());
|
|
32
|
+
if (Array.isArray(value))
|
|
33
|
+
return value.some(addressed);
|
|
34
|
+
if (!isRecord(value))
|
|
35
|
+
return false;
|
|
36
|
+
return Object.entries(value).some(
|
|
37
|
+
// `$schema` names the spec's own version and is never drawn or fetched (`vega-chart` drops it).
|
|
38
|
+
([key, inner]) => key !== '$schema' && (key === 'url' || key === 'href' || key === 'usermeta' || addressed(inner)));
|
|
39
|
+
}
|
|
40
|
+
const VIEWS = ['mark', 'layer', 'hconcat', 'vconcat', 'concat', 'facet', 'repeat'];
|
|
41
|
+
function parseVega(source) {
|
|
42
|
+
const spec = object(source);
|
|
43
|
+
if (!VIEWS.some((key) => key in spec))
|
|
44
|
+
throw new Error('not a view');
|
|
45
|
+
if (addressed(spec))
|
|
46
|
+
throw new Error('names an address');
|
|
47
|
+
return spec;
|
|
48
|
+
}
|
|
49
|
+
function parseTimeline(source) {
|
|
50
|
+
const value = object(source);
|
|
51
|
+
if (!Array.isArray(value.events))
|
|
52
|
+
throw new Error('no events');
|
|
53
|
+
return {
|
|
54
|
+
title: text(value.title),
|
|
55
|
+
events: value.events.map((event) => {
|
|
56
|
+
if (!isRecord(event) || !text(event.title))
|
|
57
|
+
throw new Error('an event with no title');
|
|
58
|
+
return { date: text(event.date), title: text(event.title), detail: text(event.detail) };
|
|
59
|
+
})
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function parseCard(source) {
|
|
63
|
+
const value = object(source);
|
|
64
|
+
if (!text(value.name))
|
|
65
|
+
throw new Error('no name');
|
|
66
|
+
const facts = Array.isArray(value.facts) ? value.facts : [];
|
|
67
|
+
return {
|
|
68
|
+
name: text(value.name),
|
|
69
|
+
kind: text(value.kind),
|
|
70
|
+
summary: text(value.summary),
|
|
71
|
+
facts: facts
|
|
72
|
+
.filter(isRecord)
|
|
73
|
+
.map((fact) => ({ label: text(fact.label), value: text(fact.value) }))
|
|
74
|
+
.filter((fact) => fact.label || fact.value)
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/** A chart, a timeline and an entity card. An app adds to these or replaces one through `ChatExtensions.visuals`. */
|
|
78
|
+
export const defaultVisuals = {
|
|
79
|
+
'vega-lite': { component: VegaChart, parse: parseVega },
|
|
80
|
+
timeline: { component: Timeline, parse: parseTimeline },
|
|
81
|
+
card: { component: EntityCard, parse: parseCard }
|
|
82
|
+
};
|
package/dist/web-sources.d.ts
CHANGED
|
@@ -2,17 +2,33 @@
|
|
|
2
2
|
* The web pages an answer drew on, listed apart from the library's numbered
|
|
3
3
|
* sources: the pages the turn's supacrawl calls read, and the Markdown links
|
|
4
4
|
* in the answer itself. A search's results are what it found, not what the
|
|
5
|
-
* answer drew on, so they stay in the search's own row.
|
|
6
|
-
*
|
|
5
|
+
* answer drew on, so they stay in the search's own row (`searchPages`). The
|
|
6
|
+
* page numbers them after the library's, since the persona cites a page by its
|
|
7
|
+
* link and nothing here is a passage it was handed with a number.
|
|
7
8
|
*/
|
|
8
|
-
import type { Block } from './transcript.svelte';
|
|
9
|
+
import type { Block, ToolBlock } from './transcript.svelte';
|
|
9
10
|
export interface WebSource {
|
|
10
11
|
url: string;
|
|
11
12
|
/** The page's title, or its host where nothing gave one. */
|
|
12
13
|
title: string;
|
|
13
14
|
host: string;
|
|
15
|
+
/** The turn read this page whole (a scrape), rather than only linking it. */
|
|
16
|
+
read?: true;
|
|
17
|
+
/** The page's own date, as it gave it, where it gave one. */
|
|
18
|
+
date?: string;
|
|
14
19
|
}
|
|
20
|
+
/** A page a search found, for its step's expansion. */
|
|
21
|
+
export interface FoundPage {
|
|
22
|
+
url: string;
|
|
23
|
+
title: string;
|
|
24
|
+
host: string;
|
|
25
|
+
snippet: string;
|
|
26
|
+
}
|
|
27
|
+
/** The key two spellings of one page share. */
|
|
28
|
+
export declare const webKey: (url: string) => string;
|
|
15
29
|
/** A URL's host without a leading `www.`; the text as given where it is no URL. */
|
|
16
30
|
export declare function hostOf(url: string): string;
|
|
31
|
+
/** The pages a successful web search step found. */
|
|
32
|
+
export declare function searchPages(block: ToolBlock): FoundPage[];
|
|
17
33
|
/** The pages, once each, in the order the turn met them. */
|
|
18
34
|
export declare function webSources(blocks: Block[], answer: string): WebSource[];
|
package/dist/web-sources.js
CHANGED
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
* The web pages an answer drew on, listed apart from the library's numbered
|
|
3
3
|
* sources: the pages the turn's supacrawl calls read, and the Markdown links
|
|
4
4
|
* in the answer itself. A search's results are what it found, not what the
|
|
5
|
-
* answer drew on, so they stay in the search's own row.
|
|
6
|
-
*
|
|
5
|
+
* answer drew on, so they stay in the search's own row (`searchPages`). The
|
|
6
|
+
* page numbers them after the library's, since the persona cites a page by its
|
|
7
|
+
* link and nothing here is a passage it was handed with a number.
|
|
7
8
|
*/
|
|
9
|
+
/** The key two spellings of one page share. */
|
|
10
|
+
export const webKey = (url) => url.replace(/#.*$/, '').replace(/\/$/, '');
|
|
8
11
|
/** A URL's host without a leading `www.`; the text as given where it is no URL. */
|
|
9
12
|
export function hostOf(url) {
|
|
10
13
|
try {
|
|
@@ -26,6 +29,24 @@ function parse(text) {
|
|
|
26
29
|
return undefined;
|
|
27
30
|
}
|
|
28
31
|
}
|
|
32
|
+
/** The pages a successful web search step found. */
|
|
33
|
+
export function searchPages(block) {
|
|
34
|
+
if (block.name !== 'mcp__supacrawl__supacrawl_search' || block.isError)
|
|
35
|
+
return [];
|
|
36
|
+
const found = parse(block.result);
|
|
37
|
+
if (!Array.isArray(found?.results))
|
|
38
|
+
return [];
|
|
39
|
+
const pages = [];
|
|
40
|
+
for (const item of found.results) {
|
|
41
|
+
if (typeof item !== 'object' || item === null || !isWeb(item.url))
|
|
42
|
+
continue;
|
|
43
|
+
const host = hostOf(item.url);
|
|
44
|
+
const title = [item.title, host].find((t) => typeof t === 'string' && t.trim());
|
|
45
|
+
const snippet = [item.snippet, item.description, item.content].find((t) => typeof t === 'string' && t.trim());
|
|
46
|
+
pages.push({ url: item.url, title: title.trim(), host, snippet: (snippet ?? '').trim() });
|
|
47
|
+
}
|
|
48
|
+
return pages;
|
|
49
|
+
}
|
|
29
50
|
function fromTool(block) {
|
|
30
51
|
if (!block.name.startsWith('mcp__supacrawl__') || !block.name.endsWith('_scrape') || block.isError)
|
|
31
52
|
return [];
|
|
@@ -34,24 +55,35 @@ function fromTool(block) {
|
|
|
34
55
|
return [];
|
|
35
56
|
const page = parse(block.result);
|
|
36
57
|
const title = [page?.title, page?.metadata?.title].find((t) => typeof t === 'string');
|
|
37
|
-
|
|
58
|
+
const date = [
|
|
59
|
+
page?.metadata?.publishedTime,
|
|
60
|
+
page?.metadata?.published,
|
|
61
|
+
page?.metadata?.date,
|
|
62
|
+
page?.metadata?.modifiedTime
|
|
63
|
+
].find((d) => typeof d === 'string' && !Number.isNaN(Date.parse(d)));
|
|
64
|
+
return [{ url: input.url, title: title, date: date }];
|
|
38
65
|
}
|
|
39
66
|
/** The pages, once each, in the order the turn met them. */
|
|
40
67
|
export function webSources(blocks, answer) {
|
|
41
68
|
const seen = new Map();
|
|
42
|
-
const add = (url, title) => {
|
|
43
|
-
const key = url
|
|
69
|
+
const add = (url, title, read) => {
|
|
70
|
+
const key = webKey(url);
|
|
44
71
|
if (seen.has(key))
|
|
45
72
|
return;
|
|
46
73
|
const host = hostOf(url);
|
|
47
74
|
if (host === url)
|
|
48
75
|
return;
|
|
49
|
-
seen.set(key, {
|
|
76
|
+
seen.set(key, {
|
|
77
|
+
url,
|
|
78
|
+
title: title?.trim() || host,
|
|
79
|
+
host,
|
|
80
|
+
...(read ? { read: true, date: read.date } : {})
|
|
81
|
+
});
|
|
50
82
|
};
|
|
51
83
|
for (const block of blocks)
|
|
52
84
|
if (block.kind === 'tool')
|
|
53
85
|
for (const p of fromTool(block))
|
|
54
|
-
add(p.url, p.title);
|
|
86
|
+
add(p.url, p.title, { date: p.date });
|
|
55
87
|
for (const match of answer.matchAll(LINK))
|
|
56
88
|
add(match[2], match[1]);
|
|
57
89
|
return [...seen.values()];
|
package/dist/written.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* each version in the reading column and links it for download; the bytes
|
|
7
7
|
* come from the host's own route, never from this package.
|
|
8
8
|
*/
|
|
9
|
-
import type
|
|
9
|
+
import { type ChatCopy } from './copy';
|
|
10
10
|
import type { Turn } from './transcript.svelte';
|
|
11
11
|
/** One version of one document, as the wire names it. */
|
|
12
12
|
export interface Written {
|
|
@@ -20,6 +20,17 @@ export interface Written {
|
|
|
20
20
|
/** What the app can render it as beyond the source (`docx`, `pdf`); empty
|
|
21
21
|
* where it has no renderer. */
|
|
22
22
|
formats: string[];
|
|
23
|
+
/** What a reader calls it: "Minute: increased rent ceiling". */
|
|
24
|
+
title: string;
|
|
25
|
+
/** What sort of document it is ("minute"); null where nothing says. */
|
|
26
|
+
kind: string | null;
|
|
27
|
+
/** What this version changed since the one before it, in a phrase; null
|
|
28
|
+
* for the first, or where nothing says. */
|
|
29
|
+
changes: string | null;
|
|
30
|
+
/** How many pages it runs to; 0 where the app cannot tell. */
|
|
31
|
+
pages: number;
|
|
32
|
+
/** When this page saw the version arrive, epoch ms. Absent for one read back. */
|
|
33
|
+
at?: number;
|
|
23
34
|
}
|
|
24
35
|
/** The source of one version, to show it in the pane. */
|
|
25
36
|
export type LoadWritten = (name: string, version: number) => Promise<{
|
|
@@ -30,6 +41,20 @@ export type LoadWritten = (name: string, version: number) => Promise<{
|
|
|
30
41
|
export type WrittenUrl = (name: string, version: number, format: string) => string;
|
|
31
42
|
export declare const writtenKey: (written: Written) => string;
|
|
32
43
|
export declare const isHtml: (mediaType: string) => boolean;
|
|
44
|
+
/** The newest version of each document in `written`, in the order they first appear. */
|
|
45
|
+
export declare function newestPerName(written: Written[]): Written[];
|
|
46
|
+
/** "Minute", or "Document" where the document has no kind. */
|
|
47
|
+
export declare function kindLabel(written: Written, words: ChatCopy): string;
|
|
48
|
+
/** "Minute · 1 page · v1": kind, pages where known, version. */
|
|
49
|
+
export declare function writtenMeta(written: Written, words: ChatCopy): string;
|
|
50
|
+
/** The download a card's chip offers: Word where the app renders it, else the source as written. */
|
|
51
|
+
export declare function chipFor(written: Written, words: ChatCopy): {
|
|
52
|
+
format: string;
|
|
53
|
+
file: string;
|
|
54
|
+
label: string;
|
|
55
|
+
};
|
|
56
|
+
/** A leading `---` front matter block is metadata, not the document. */
|
|
57
|
+
export declare function stripFrontMatter(text: string): string;
|
|
33
58
|
/** Every version of `name` across the conversation, oldest first. */
|
|
34
59
|
export declare function writtenVersions(turns: Turn[], name: string): Written[];
|
|
35
60
|
/** What a version downloads as: its source, then each format the app renders. */
|
package/dist/written.js
CHANGED
|
@@ -6,8 +6,50 @@
|
|
|
6
6
|
* each version in the reading column and links it for download; the bytes
|
|
7
7
|
* come from the host's own route, never from this package.
|
|
8
8
|
*/
|
|
9
|
+
import { fill } from './copy';
|
|
9
10
|
export const writtenKey = (written) => `${written.name}@${written.version}`;
|
|
10
11
|
export const isHtml = (mediaType) => mediaType.toLowerCase().startsWith('text/html');
|
|
12
|
+
/** The newest version of each document in `written`, in the order they first appear. */
|
|
13
|
+
export function newestPerName(written) {
|
|
14
|
+
const newest = new Map();
|
|
15
|
+
for (const w of written) {
|
|
16
|
+
const had = newest.get(w.name);
|
|
17
|
+
if (!had || w.version >= had.version)
|
|
18
|
+
newest.set(w.name, w);
|
|
19
|
+
}
|
|
20
|
+
return [...newest.values()];
|
|
21
|
+
}
|
|
22
|
+
/** "Minute", or "Document" where the document has no kind. */
|
|
23
|
+
export function kindLabel(written, words) {
|
|
24
|
+
const kind = (written.kind ?? '').trim() || words.documentKind;
|
|
25
|
+
return kind.charAt(0).toUpperCase() + kind.slice(1);
|
|
26
|
+
}
|
|
27
|
+
/** "Minute · 1 page · v1": kind, pages where known, version. */
|
|
28
|
+
export function writtenMeta(written, words) {
|
|
29
|
+
const parts = [kindLabel(written, words)];
|
|
30
|
+
if (written.pages > 0)
|
|
31
|
+
parts.push(written.pages === 1 ? words.pageCount : fill(words.pagesCount, { n: written.pages }));
|
|
32
|
+
parts.push(fill(words.writtenVersion, { n: written.version }));
|
|
33
|
+
return parts.join(' · ');
|
|
34
|
+
}
|
|
35
|
+
const baseName = (name) => name.split('/').filter(Boolean).at(-1) ?? name;
|
|
36
|
+
/** The download a card's chip offers: Word where the app renders it, else the source as written. */
|
|
37
|
+
export function chipFor(written, words) {
|
|
38
|
+
const file = baseName(written.name);
|
|
39
|
+
if (written.formats.includes('docx')) {
|
|
40
|
+
const stem = file.replace(/\.[^.]+$/, '');
|
|
41
|
+
return { format: 'docx', file: `${stem}.docx`, label: words.formatWord };
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
format: 'source',
|
|
45
|
+
file,
|
|
46
|
+
label: isHtml(written.media_type) ? words.formatHtml : words.formatMarkdown
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** A leading `---` front matter block is metadata, not the document. */
|
|
50
|
+
export function stripFrontMatter(text) {
|
|
51
|
+
return text.replace(/^\uFEFF?---[ \t]*\r?\n[\s\S]*?\r?\n---[ \t]*(?:\r?\n|$)/, '').replace(/^\s*\n/, '');
|
|
52
|
+
}
|
|
11
53
|
/** Every version of `name` across the conversation, oldest first. */
|
|
12
54
|
export function writtenVersions(turns, name) {
|
|
13
55
|
const found = new Map();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poodle64/librarian",
|
|
3
|
-
"version": "2026.10.
|
|
3
|
+
"version": "2026.10.5",
|
|
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",
|
|
@@ -47,6 +47,10 @@
|
|
|
47
47
|
"types": "./dist/written.d.ts",
|
|
48
48
|
"svelte": "./dist/written.js"
|
|
49
49
|
},
|
|
50
|
+
"./visuals": {
|
|
51
|
+
"types": "./dist/visuals.d.ts",
|
|
52
|
+
"svelte": "./dist/visuals.js"
|
|
53
|
+
},
|
|
50
54
|
"./attachments": {
|
|
51
55
|
"types": "./dist/attachments.d.ts",
|
|
52
56
|
"svelte": "./dist/attachments.js"
|
|
@@ -68,6 +72,11 @@
|
|
|
68
72
|
"isomorphic-dompurify": "^3.23.0",
|
|
69
73
|
"shiki": "^4.4.3"
|
|
70
74
|
},
|
|
75
|
+
"dependencies": {
|
|
76
|
+
"vega": "6.4.0",
|
|
77
|
+
"vega-embed": "7.3.0",
|
|
78
|
+
"vega-lite": "6.4.3"
|
|
79
|
+
},
|
|
71
80
|
"devDependencies": {
|
|
72
81
|
"@lucide/svelte": "^1.7.0",
|
|
73
82
|
"@poodle64/design-tokens": "workspace:*",
|