@poodle64/librarian 2026.9.28 → 2026.10.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 +66 -116
- package/dist/attachments.d.ts +2 -2
- package/dist/attachments.js +3 -3
- package/dist/chat.svelte.d.ts +22 -23
- package/dist/chat.svelte.js +41 -64
- package/dist/citations.d.ts +7 -7
- package/dist/citations.js +6 -6
- package/dist/client.d.ts +16 -45
- package/dist/client.js +15 -23
- package/dist/components/activity-group/activity-group.svelte +1 -1
- package/dist/components/agent-transcript/agent-transcript.svelte +9 -110
- package/dist/components/agent-transcript/agent-transcript.svelte.d.ts +4 -16
- package/dist/components/answer-mark/answer-mark.svelte +2 -2
- package/dist/components/answer-mark/answer-mark.svelte.d.ts +2 -2
- package/dist/components/answer-share/answer-share.svelte +2 -2
- package/dist/components/answer-share/answer-share.svelte.d.ts +2 -2
- package/dist/components/artefact-card/artefact-card.svelte +2 -2
- package/dist/components/artefact-card/artefact-card.svelte.d.ts +2 -2
- package/dist/components/artefact-pane/artefact-pane.svelte +2 -2
- package/dist/components/artefact-pane/artefact-pane.svelte.d.ts +2 -2
- package/dist/components/composer/composer.svelte +51 -112
- package/dist/components/composer/composer.svelte.d.ts +21 -18
- package/dist/components/composer/index.d.ts +1 -2
- package/dist/components/conversation/conversation.svelte +7 -18
- package/dist/components/conversation/conversation.svelte.d.ts +7 -12
- package/dist/components/conversation-list/conversation-list.svelte +2 -2
- package/dist/components/conversation-list/conversation-list.svelte.d.ts +2 -2
- package/dist/components/document-pane/document-pane.svelte +2 -2
- package/dist/components/document-pane/document-pane.svelte.d.ts +2 -2
- package/dist/components/fair-use-notice/fair-use-notice.svelte +2 -2
- package/dist/components/fair-use-notice/fair-use-notice.svelte.d.ts +2 -2
- package/dist/components/scope-statement/scope-statement.svelte +5 -5
- package/dist/components/scope-statement/scope-statement.svelte.d.ts +2 -2
- package/dist/components/source-list/source-list.svelte +2 -2
- package/dist/components/source-list/source-list.svelte.d.ts +2 -2
- package/dist/components/working/working.svelte +2 -2
- package/dist/components/working/working.svelte.d.ts +2 -2
- package/dist/copy.d.ts +16 -24
- package/dist/copy.js +10 -16
- package/dist/session.svelte.d.ts +1 -1
- package/dist/session.svelte.js +6 -7
- package/dist/transcript.svelte.d.ts +18 -32
- package/dist/transcript.svelte.js +19 -40
- package/package.json +1 -5
- package/dist/history.svelte.d.ts +0 -37
- package/dist/history.svelte.js +0 -59
package/dist/session.svelte.js
CHANGED
|
@@ -12,17 +12,17 @@
|
|
|
12
12
|
* Every Claude Code event carries a `uuid`, and one already folded is
|
|
13
13
|
* skipped: a watch route replays from the first event, so opening it again
|
|
14
14
|
* after the session moves on folds only what is new. A frame the server adds
|
|
15
|
-
* itself (an error, the
|
|
15
|
+
* itself (an error, the citations) carries none, and is known by
|
|
16
16
|
* the event it follows.
|
|
17
17
|
*/
|
|
18
18
|
import { contentOf, fold, foldState } from './transcript.svelte';
|
|
19
19
|
/** The events a run is made of. One arriving after its turn settled is the
|
|
20
20
|
* next run, whether or not its prompt was echoed. */
|
|
21
21
|
const RUN = new Set(['stream_event', 'assistant', 'user', 'result']);
|
|
22
|
-
/** Everything else that lands on a turn: the
|
|
23
|
-
*
|
|
22
|
+
/** Everything else that lands on a turn: the citations, and a stream that
|
|
23
|
+
* broke. Anything else (`system`, rate limits) is the CLI
|
|
24
24
|
* talking about itself. */
|
|
25
|
-
const FOLDED = new Set([...RUN, '
|
|
25
|
+
const FOLDED = new Set([...RUN, 'agent_error', 'citations']);
|
|
26
26
|
export class Session {
|
|
27
27
|
/** One per run, in order: plain objects, so a host may spread one to add
|
|
28
28
|
* its own `artefact`. */
|
|
@@ -66,7 +66,7 @@ export class Session {
|
|
|
66
66
|
const last = this.turns.at(-1);
|
|
67
67
|
const settled = last?.outcome != null && !last.outcome.unreachable;
|
|
68
68
|
// The watch broke while nothing was running, and no run failed.
|
|
69
|
-
if (event.type === '
|
|
69
|
+
if (event.type === 'agent_error' && settled)
|
|
70
70
|
return;
|
|
71
71
|
// A run whose prompt was never echoed — a session started without the
|
|
72
72
|
// replay flag, or one that failed before it said anything — still has
|
|
@@ -88,8 +88,7 @@ export class Session {
|
|
|
88
88
|
at,
|
|
89
89
|
blocks: [],
|
|
90
90
|
outcome: null,
|
|
91
|
-
citations: []
|
|
92
|
-
suggestions: []
|
|
91
|
+
citations: []
|
|
93
92
|
});
|
|
94
93
|
this.#folds.push(foldState());
|
|
95
94
|
}
|
|
@@ -6,10 +6,8 @@
|
|
|
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
|
-
import type { AgentEvent
|
|
9
|
+
import type { AgentEvent } from './client';
|
|
10
10
|
import type { Citation } from './citations';
|
|
11
|
-
import type { LibrarianCopy } from './copy';
|
|
12
|
-
export type { Depth };
|
|
13
11
|
export interface TextBlock {
|
|
14
12
|
kind: 'text';
|
|
15
13
|
index: number;
|
|
@@ -50,9 +48,9 @@ export interface Outcome {
|
|
|
50
48
|
/**
|
|
51
49
|
* The question as the READER asked it.
|
|
52
50
|
*
|
|
53
|
-
* A host
|
|
54
|
-
*
|
|
55
|
-
*
|
|
51
|
+
* A host may prepend a system preamble to every question before it goes to
|
|
52
|
+
* the persona, and echoing that back into the transcript shows a colleague
|
|
53
|
+
* the machinery. Leading
|
|
56
54
|
* blank-line-separated paragraphs addressed to the model are dropped.
|
|
57
55
|
*
|
|
58
56
|
* The LAST paragraph is never dropped, whatever it starts with: someone whose
|
|
@@ -85,12 +83,12 @@ export interface ToolWords {
|
|
|
85
83
|
/** A host's words for its persona's tools. `undefined` leaves the call to
|
|
86
84
|
* `describe()`, the package's own. */
|
|
87
85
|
export type DescribeTool = (block: ToolBlock) => ToolWords | undefined;
|
|
88
|
-
/** What
|
|
86
|
+
/** What the persona DID, in a reader's own words — never the tool's name or the
|
|
89
87
|
* raw command it ran.
|
|
90
88
|
*
|
|
91
89
|
* The collapsed row is read by someone asking a question about documents, not
|
|
92
90
|
* by an engineer: `Bash ls -1 .` tells them nothing and looks like a leak from
|
|
93
|
-
* the
|
|
91
|
+
* the machinery. The persona never narrates how it searched, so a tool this
|
|
94
92
|
* does not recognise falls back to something that names no mechanism at all.
|
|
95
93
|
* A persona with tools of its own brings a `DescribeTool` in front of this.
|
|
96
94
|
*/
|
|
@@ -120,10 +118,8 @@ export declare class Transcript {
|
|
|
120
118
|
sessionId: string | null;
|
|
121
119
|
model: string | null;
|
|
122
120
|
outcome: Outcome | null;
|
|
123
|
-
/** Sources for the answer, from the
|
|
121
|
+
/** Sources for the answer, from the app's `citations` frame. */
|
|
124
122
|
citations: Citation[];
|
|
125
|
-
/** Follow-ups the librarian named, from the `suggestions` frame after it. */
|
|
126
|
-
suggestions: string[];
|
|
127
123
|
other: AgentEvent[];
|
|
128
124
|
reset(): void;
|
|
129
125
|
apply(event: AgentEvent): void;
|
|
@@ -133,7 +129,6 @@ export interface FoldTarget {
|
|
|
133
129
|
blocks: Block[];
|
|
134
130
|
outcome: Outcome | null;
|
|
135
131
|
citations?: Citation[];
|
|
136
|
-
suggestions?: string[];
|
|
137
132
|
}
|
|
138
133
|
/** Where one turn's fold keeps its place between events. */
|
|
139
134
|
export interface FoldState {
|
|
@@ -201,18 +196,15 @@ export interface Turn {
|
|
|
201
196
|
blocks: Block[];
|
|
202
197
|
outcome: Outcome | null;
|
|
203
198
|
citations?: Citation[];
|
|
204
|
-
/** Follow-ups offered after this answer. Absent on a turn read back from
|
|
205
|
-
* history: they belonged to the moment it was asked. */
|
|
206
|
-
suggestions?: string[];
|
|
207
199
|
/** Something this turn produced for the reader to open, carded under the
|
|
208
200
|
* answer once the turn settles. */
|
|
209
201
|
artefact?: Artefact;
|
|
210
202
|
/** What the host asked for when this was not a plain question
|
|
211
203
|
* (`'briefing'`), in its own word, as `client.ask()` sent it. */
|
|
212
204
|
kind?: string;
|
|
213
|
-
/**
|
|
214
|
-
* Absent
|
|
215
|
-
|
|
205
|
+
/** The collection this question was narrowed to, by name, as `client.ask()`
|
|
206
|
+
* sent it. Absent asked across every collection the person may read. */
|
|
207
|
+
collection?: string;
|
|
216
208
|
/** Where this answer's read-only view lives while it is shared, as the
|
|
217
209
|
* host's own address. Absent while it is not. */
|
|
218
210
|
shared?: string;
|
|
@@ -239,7 +231,7 @@ export interface Artefact {
|
|
|
239
231
|
* 2. "Reading pspf guidelines 2026" appeared five times in a row — five pages
|
|
240
232
|
* of one document, which is one act of reading to a human. Consecutive
|
|
241
233
|
* steps with the same label collapse to one row carrying a count.
|
|
242
|
-
* 3.
|
|
234
|
+
* 3. The persona's between-tool narration was rendering unfolded, as answer prose.
|
|
243
235
|
* The caller stream carries no `thinking` blocks at all — measured on
|
|
244
236
|
* production 11/09/2026 — so "Let me also check whether…" arrives as an
|
|
245
237
|
* ordinary `text` block, indistinguishable from the answer except by
|
|
@@ -254,8 +246,8 @@ export interface Artefact {
|
|
|
254
246
|
* and re-derived on every event, so re-homing is just the next call returning
|
|
255
247
|
* a different shape.
|
|
256
248
|
*
|
|
257
|
-
* The cost of the rule is a real answer that
|
|
258
|
-
*
|
|
249
|
+
* The cost of the rule is a real answer that the persona interrupts to go back to
|
|
250
|
+
* its tools: its first half folds away. That is the trade taken knowingly —
|
|
259
251
|
* the position of a block is the only signal the stream gives, and a rule read
|
|
260
252
|
* off the prose itself would be unexplainable the first time it misfired.
|
|
261
253
|
*/
|
|
@@ -263,26 +255,20 @@ export declare function segment(blocks: Block[], describeTool?: DescribeTool): S
|
|
|
263
255
|
/** One line describing a whole investigation, for the collapsed state.
|
|
264
256
|
*
|
|
265
257
|
* Counts of what the persona did are fine ("1 search · 2 documents read"); how
|
|
266
|
-
* it organises what it knows is not — no collection count, no
|
|
258
|
+
* it organises what it knows is not — no collection count, no corpus.
|
|
267
259
|
*/
|
|
268
260
|
export declare function summariseActivity(group: ActivityGroup): string;
|
|
269
261
|
/**
|
|
270
|
-
* What a settled answer took, as one quiet line: `
|
|
271
|
-
*
|
|
272
|
-
* one read back before the feature shipped), then the duration, then what it
|
|
273
|
-
* cost — only when the host can price it (`formatCost`) and the run actually
|
|
262
|
+
* What a settled answer took, as one quiet line: `56.3s`, then what it cost
|
|
263
|
+
* — only when the host can price it (`formatCost`) and the run actually
|
|
274
264
|
* spent something, since a zero-cost run (a local model) has nothing worth
|
|
275
|
-
* saying about money.
|
|
265
|
+
* saying about money: `56.3s · about A$0.34`.
|
|
276
266
|
*
|
|
277
267
|
* The host prices `outcome.costUsd`, never this package: the currency and
|
|
278
268
|
* the rate are the app's own setting, not something a shared component
|
|
279
269
|
* should be converting.
|
|
280
|
-
*
|
|
281
|
-
* A turn asked with no depth at all renders exactly what this line always
|
|
282
|
-
* has (`56.3s`, one decimal, no gap) — a host that never wires depth sees no
|
|
283
|
-
* change here either.
|
|
284
270
|
*/
|
|
285
|
-
export declare function outcomeLine(outcome: Outcome,
|
|
271
|
+
export declare function outcomeLine(outcome: Outcome, formatCost?: (usd: number) => string): string;
|
|
286
272
|
/**
|
|
287
273
|
* What a turn's calls read that a reader can open, numbered in the order it
|
|
288
274
|
* was first read: the host's `ToolWords.source`, as the sources the answer
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
/**
|
|
10
10
|
* The question as the READER asked it.
|
|
11
11
|
*
|
|
12
|
-
* A host
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* A host may prepend a system preamble to every question before it goes to
|
|
13
|
+
* the persona, and echoing that back into the transcript shows a colleague
|
|
14
|
+
* the machinery. Leading
|
|
15
15
|
* blank-line-separated paragraphs addressed to the model are dropped.
|
|
16
16
|
*
|
|
17
17
|
* The LAST paragraph is never dropped, whatever it starts with: someone whose
|
|
@@ -25,12 +25,12 @@ export function readerQuestion(question) {
|
|
|
25
25
|
start += 1;
|
|
26
26
|
return parts.slice(start).join('\n\n').trim();
|
|
27
27
|
}
|
|
28
|
-
/** What
|
|
28
|
+
/** What the persona DID, in a reader's own words — never the tool's name or the
|
|
29
29
|
* raw command it ran.
|
|
30
30
|
*
|
|
31
31
|
* The collapsed row is read by someone asking a question about documents, not
|
|
32
32
|
* by an engineer: `Bash ls -1 .` tells them nothing and looks like a leak from
|
|
33
|
-
* the
|
|
33
|
+
* the machinery. The persona never narrates how it searched, so a tool this
|
|
34
34
|
* does not recognise falls back to something that names no mechanism at all.
|
|
35
35
|
* A persona with tools of its own brings a `DescribeTool` in front of this.
|
|
36
36
|
*/
|
|
@@ -152,17 +152,14 @@ export class Transcript {
|
|
|
152
152
|
sessionId = $state(null);
|
|
153
153
|
model = $state(null);
|
|
154
154
|
outcome = $state(null);
|
|
155
|
-
/** Sources for the answer, from the
|
|
155
|
+
/** Sources for the answer, from the app's `citations` frame. */
|
|
156
156
|
citations = $state([]);
|
|
157
|
-
/** Follow-ups the librarian named, from the `suggestions` frame after it. */
|
|
158
|
-
suggestions = $state([]);
|
|
159
157
|
other = $state([]);
|
|
160
158
|
#fold = foldState();
|
|
161
159
|
reset() {
|
|
162
160
|
this.blocks = [];
|
|
163
161
|
this.outcome = null;
|
|
164
162
|
this.citations = [];
|
|
165
|
-
this.suggestions = [];
|
|
166
163
|
this.other = [];
|
|
167
164
|
this.#fold = foldState();
|
|
168
165
|
}
|
|
@@ -201,25 +198,16 @@ export function fold(into, state, event) {
|
|
|
201
198
|
};
|
|
202
199
|
return true;
|
|
203
200
|
}
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
// keeps only what its own shape admits: a `citations` frame carrying
|
|
207
|
-
// strings, or a `suggestions` frame carrying objects, is the library
|
|
208
|
-
// having changed under us, and rendering it would be worse than
|
|
209
|
-
// rendering nothing.
|
|
201
|
+
// The app emits this before the persona's answer, from the passages it
|
|
202
|
+
// retrieved for the question.
|
|
210
203
|
if (event.type === 'citations') {
|
|
211
204
|
into.citations = (event.items ?? []).filter((item) => typeof item === 'object' && item !== null);
|
|
212
205
|
return true;
|
|
213
206
|
}
|
|
214
|
-
// Last of the two, and only when the librarian named any.
|
|
215
|
-
if (event.type === 'suggestions') {
|
|
216
|
-
into.suggestions = (event.items ?? []).filter((item) => typeof item === 'string' && item.trim().length > 0);
|
|
217
|
-
return true;
|
|
218
|
-
}
|
|
219
207
|
// No persona and no sentence at this layer: `client.ts` raises this for a
|
|
220
208
|
// stream that would not open or died half-way, and the turn renders the
|
|
221
209
|
// unreachable line in whichever persona's voice the host named.
|
|
222
|
-
if (event.type === '
|
|
210
|
+
if (event.type === 'agent_error') {
|
|
223
211
|
into.outcome = { isError: true, unreachable: true, error: event.error };
|
|
224
212
|
return true;
|
|
225
213
|
}
|
|
@@ -341,7 +329,7 @@ function renderResult(content) {
|
|
|
341
329
|
* 2. "Reading pspf guidelines 2026" appeared five times in a row — five pages
|
|
342
330
|
* of one document, which is one act of reading to a human. Consecutive
|
|
343
331
|
* steps with the same label collapse to one row carrying a count.
|
|
344
|
-
* 3.
|
|
332
|
+
* 3. The persona's between-tool narration was rendering unfolded, as answer prose.
|
|
345
333
|
* The caller stream carries no `thinking` blocks at all — measured on
|
|
346
334
|
* production 11/09/2026 — so "Let me also check whether…" arrives as an
|
|
347
335
|
* ordinary `text` block, indistinguishable from the answer except by
|
|
@@ -356,8 +344,8 @@ function renderResult(content) {
|
|
|
356
344
|
* and re-derived on every event, so re-homing is just the next call returning
|
|
357
345
|
* a different shape.
|
|
358
346
|
*
|
|
359
|
-
* The cost of the rule is a real answer that
|
|
360
|
-
*
|
|
347
|
+
* The cost of the rule is a real answer that the persona interrupts to go back to
|
|
348
|
+
* its tools: its first half folds away. That is the trade taken knowingly —
|
|
361
349
|
* the position of a block is the only signal the stream gives, and a rule read
|
|
362
350
|
* off the prose itself would be unexplainable the first time it misfired.
|
|
363
351
|
*/
|
|
@@ -416,7 +404,7 @@ function sameStep(last, block, words) {
|
|
|
416
404
|
return false;
|
|
417
405
|
if (block.kind === 'thinking')
|
|
418
406
|
return true;
|
|
419
|
-
// Two narration sentences are two things
|
|
407
|
+
// Two narration sentences are two things the persona said; collapsing them to
|
|
420
408
|
// one row with a count would lose the second one entirely.
|
|
421
409
|
if (block.kind === 'text')
|
|
422
410
|
return false;
|
|
@@ -432,33 +420,24 @@ function count(group, [one, many]) {
|
|
|
432
420
|
/** One line describing a whole investigation, for the collapsed state.
|
|
433
421
|
*
|
|
434
422
|
* Counts of what the persona did are fine ("1 search · 2 documents read"); how
|
|
435
|
-
* it organises what it knows is not — no collection count, no
|
|
423
|
+
* it organises what it knows is not — no collection count, no corpus.
|
|
436
424
|
*/
|
|
437
425
|
export function summariseActivity(group) {
|
|
438
426
|
const parts = group.tallies.map((t) => `${t.count} ${t.count === 1 ? t.one : t.many}`);
|
|
439
427
|
return parts.length ? parts.join(' · ') : 'Looked into it';
|
|
440
428
|
}
|
|
441
429
|
/**
|
|
442
|
-
* What a settled answer took, as one quiet line: `
|
|
443
|
-
*
|
|
444
|
-
* one read back before the feature shipped), then the duration, then what it
|
|
445
|
-
* cost — only when the host can price it (`formatCost`) and the run actually
|
|
430
|
+
* What a settled answer took, as one quiet line: `56.3s`, then what it cost
|
|
431
|
+
* — only when the host can price it (`formatCost`) and the run actually
|
|
446
432
|
* spent something, since a zero-cost run (a local model) has nothing worth
|
|
447
|
-
* saying about money.
|
|
433
|
+
* saying about money: `56.3s · about A$0.34`.
|
|
448
434
|
*
|
|
449
435
|
* The host prices `outcome.costUsd`, never this package: the currency and
|
|
450
436
|
* the rate are the app's own setting, not something a shared component
|
|
451
437
|
* should be converting.
|
|
452
|
-
*
|
|
453
|
-
* A turn asked with no depth at all renders exactly what this line always
|
|
454
|
-
* has (`56.3s`, one decimal, no gap) — a host that never wires depth sees no
|
|
455
|
-
* change here either.
|
|
456
438
|
*/
|
|
457
|
-
export function outcomeLine(outcome,
|
|
458
|
-
const
|
|
459
|
-
if (!depth)
|
|
460
|
-
return `${seconds.toFixed(1)}s`;
|
|
461
|
-
const parts = [depth === 'quick' ? words.depthQuick : words.depthThorough, `${Math.round(seconds)} s`];
|
|
439
|
+
export function outcomeLine(outcome, formatCost) {
|
|
440
|
+
const parts = [`${((outcome.durationMs ?? 0) / 1000).toFixed(1)}s`];
|
|
462
441
|
if (formatCost && outcome.costUsd)
|
|
463
442
|
parts.push(formatCost(outcome.costUsd));
|
|
464
443
|
return parts.join(' · ');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poodle64/librarian",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.10.1",
|
|
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",
|
|
@@ -35,10 +35,6 @@
|
|
|
35
35
|
"types": "./dist/chat.svelte.d.ts",
|
|
36
36
|
"svelte": "./dist/chat.svelte.js"
|
|
37
37
|
},
|
|
38
|
-
"./history": {
|
|
39
|
-
"types": "./dist/history.svelte.d.ts",
|
|
40
|
-
"svelte": "./dist/history.svelte.js"
|
|
41
|
-
},
|
|
42
38
|
"./citations": {
|
|
43
39
|
"types": "./dist/citations.d.ts",
|
|
44
40
|
"svelte": "./dist/citations.js"
|
package/dist/history.svelte.d.ts
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Past conversations.
|
|
3
|
-
*
|
|
4
|
-
* Held in the browser, deliberately and provisionally. The agent's own session
|
|
5
|
-
* survives server-side — Claude Code keeps it and `--resume` reaches it — so
|
|
6
|
-
* what is missing is only the RENDERED transcript, which is a display concern.
|
|
7
|
-
* Persisting that here needs no schema, no migration and no decision about
|
|
8
|
-
* whose conversation it is.
|
|
9
|
-
*
|
|
10
|
-
* Namespaced by the caller: two apps, or two rooms in one app, must not share
|
|
11
|
-
* a `localStorage` key, so the namespace is a constructor argument rather than
|
|
12
|
-
* a module-level constant.
|
|
13
|
-
*/
|
|
14
|
-
export interface StoredTurn {
|
|
15
|
-
question: string;
|
|
16
|
-
answer: string;
|
|
17
|
-
}
|
|
18
|
-
export interface Conversation {
|
|
19
|
-
id: string;
|
|
20
|
-
/** The agent's session id, which is what `--resume` needs. */
|
|
21
|
-
sessionId: string | null;
|
|
22
|
-
title: string;
|
|
23
|
-
updated: number;
|
|
24
|
-
turns: StoredTurn[];
|
|
25
|
-
}
|
|
26
|
-
export declare class History {
|
|
27
|
-
#private;
|
|
28
|
-
items: Conversation[];
|
|
29
|
-
constructor(namespace: string);
|
|
30
|
-
load(): void;
|
|
31
|
-
save(conversation: Conversation): void;
|
|
32
|
-
remove(id: string): void;
|
|
33
|
-
}
|
|
34
|
-
/** One store per namespace — a distinct `localStorage` key per app or room. */
|
|
35
|
-
export declare function createHistory(namespace: string): History;
|
|
36
|
-
/** A conversation is named by its first question, trimmed to something legible. */
|
|
37
|
-
export declare function titleFrom(question: string): string;
|
package/dist/history.svelte.js
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Past conversations.
|
|
3
|
-
*
|
|
4
|
-
* Held in the browser, deliberately and provisionally. The agent's own session
|
|
5
|
-
* survives server-side — Claude Code keeps it and `--resume` reaches it — so
|
|
6
|
-
* what is missing is only the RENDERED transcript, which is a display concern.
|
|
7
|
-
* Persisting that here needs no schema, no migration and no decision about
|
|
8
|
-
* whose conversation it is.
|
|
9
|
-
*
|
|
10
|
-
* Namespaced by the caller: two apps, or two rooms in one app, must not share
|
|
11
|
-
* a `localStorage` key, so the namespace is a constructor argument rather than
|
|
12
|
-
* a module-level constant.
|
|
13
|
-
*/
|
|
14
|
-
const LIMIT = 40;
|
|
15
|
-
export class History {
|
|
16
|
-
items = $state([]);
|
|
17
|
-
#key;
|
|
18
|
-
constructor(namespace) {
|
|
19
|
-
this.#key = namespace;
|
|
20
|
-
}
|
|
21
|
-
load() {
|
|
22
|
-
if (typeof localStorage === 'undefined')
|
|
23
|
-
return;
|
|
24
|
-
try {
|
|
25
|
-
this.items = JSON.parse(localStorage.getItem(this.#key) ?? '[]');
|
|
26
|
-
}
|
|
27
|
-
catch {
|
|
28
|
-
this.items = [];
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
save(conversation) {
|
|
32
|
-
const rest = this.items.filter((c) => c.id !== conversation.id);
|
|
33
|
-
this.items = [conversation, ...rest].slice(0, LIMIT);
|
|
34
|
-
this.#flush();
|
|
35
|
-
}
|
|
36
|
-
remove(id) {
|
|
37
|
-
this.items = this.items.filter((c) => c.id !== id);
|
|
38
|
-
this.#flush();
|
|
39
|
-
}
|
|
40
|
-
#flush() {
|
|
41
|
-
if (typeof localStorage === 'undefined')
|
|
42
|
-
return;
|
|
43
|
-
try {
|
|
44
|
-
localStorage.setItem(this.#key, JSON.stringify(this.items));
|
|
45
|
-
}
|
|
46
|
-
catch {
|
|
47
|
-
// A full quota costs history, never the conversation in progress.
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
/** One store per namespace — a distinct `localStorage` key per app or room. */
|
|
52
|
-
export function createHistory(namespace) {
|
|
53
|
-
return new History(namespace);
|
|
54
|
-
}
|
|
55
|
-
/** A conversation is named by its first question, trimmed to something legible. */
|
|
56
|
-
export function titleFrom(question) {
|
|
57
|
-
const flat = question.replace(/\s+/g, ' ').trim();
|
|
58
|
-
return flat.length > 60 ? `${flat.slice(0, 57)}…` : flat;
|
|
59
|
-
}
|