@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.
Files changed (38) hide show
  1. package/README.md +115 -0
  2. package/dist/client.d.ts +57 -0
  3. package/dist/client.js +60 -0
  4. package/dist/components/activity-group/activity-group.svelte +63 -0
  5. package/dist/components/activity-group/activity-group.svelte.d.ts +9 -0
  6. package/dist/components/activity-group/index.d.ts +2 -0
  7. package/dist/components/activity-group/index.js +2 -0
  8. package/dist/components/agent-transcript/agent-transcript.svelte +71 -0
  9. package/dist/components/agent-transcript/agent-transcript.svelte.d.ts +12 -0
  10. package/dist/components/agent-transcript/index.d.ts +2 -0
  11. package/dist/components/agent-transcript/index.js +2 -0
  12. package/dist/components/composer/composer.svelte +115 -0
  13. package/dist/components/composer/composer.svelte.d.ts +15 -0
  14. package/dist/components/composer/index.d.ts +3 -0
  15. package/dist/components/composer/index.js +2 -0
  16. package/dist/components/markdown/index.d.ts +3 -0
  17. package/dist/components/markdown/index.js +3 -0
  18. package/dist/components/markdown/markdown.d.ts +37 -0
  19. package/dist/components/markdown/markdown.js +132 -0
  20. package/dist/components/markdown/markdown.svelte +221 -0
  21. package/dist/components/markdown/markdown.svelte.d.ts +9 -0
  22. package/dist/components/thinking-row/index.d.ts +2 -0
  23. package/dist/components/thinking-row/index.js +2 -0
  24. package/dist/components/thinking-row/thinking-row.svelte +35 -0
  25. package/dist/components/thinking-row/thinking-row.svelte.d.ts +8 -0
  26. package/dist/components/tool-row/index.d.ts +2 -0
  27. package/dist/components/tool-row/index.js +2 -0
  28. package/dist/components/tool-row/tool-row.svelte +77 -0
  29. package/dist/components/tool-row/tool-row.svelte.d.ts +11 -0
  30. package/dist/components/working/index.d.ts +2 -0
  31. package/dist/components/working/index.js +2 -0
  32. package/dist/components/working/working.svelte +50 -0
  33. package/dist/components/working/working.svelte.d.ts +7 -0
  34. package/dist/history.svelte.d.ts +37 -0
  35. package/dist/history.svelte.js +59 -0
  36. package/dist/transcript.svelte.d.ts +105 -0
  37. package/dist/transcript.svelte.js +316 -0
  38. package/package.json +70 -0
package/README.md ADDED
@@ -0,0 +1,115 @@
1
+ # @poodle64/librarian
2
+
3
+ Milton's conversation surface, as a Svelte 5 package: the stream client, the
4
+ transcript state, and the chat components (transcript, composer, markdown,
5
+ tool/thinking rows, the working indicator). An app renders the librarian
6
+ instead of rebuilding it.
7
+
8
+ Owned by the library — this is Milton's surface; design-system is its press.
9
+ Change it here, consume it there.
10
+
11
+ ## What is here
12
+
13
+ ```text
14
+ src/lib/
15
+ client.ts ask(): streams Claude Code's OWN events, unaltered
16
+ transcript.svelte.ts Transcript state, the fold/segment/describe helpers
17
+ history.svelte.ts the browser-held conversation list, namespaced per caller
18
+ components/
19
+ agent-transcript/ one question and everything the agent did answering it
20
+ composer/ the input box: value, scope chips, send/stop
21
+ markdown/ sanitised, streaming-safe markdown + syntax highlighting
22
+ activity-group/ a run of tool calls, collapsed to one line
23
+ tool-row/ one tool call
24
+ thinking-row/ one thinking block
25
+ working/ the pre-first-token "something is happening" indicator
26
+ ```
27
+
28
+ Deliberately excluded: the library console's own `CorpusTree`, `DocumentPane`
29
+ and collection picker. Those are furniture for browsing a corpus, not part of
30
+ talking to Milton, and stay in the library's own frontend.
31
+
32
+ ## Installation
33
+
34
+ ```bash
35
+ pnpm add @poodle64/librarian @poodle64/ui @lucide/svelte
36
+ ```
37
+
38
+ `svelte`, `@poodle64/ui`, `@lucide/svelte`, `marked`, `isomorphic-dompurify`
39
+ and `shiki` are peer dependencies: declare them yourself so Renovate tracks
40
+ their versions and `pnpm ls` shows them.
41
+
42
+ ## Consuming the package
43
+
44
+ Every export is its own subpath, matching `@poodle64/ui`'s convention:
45
+
46
+ ```svelte
47
+ <script lang="ts">
48
+ import { ask } from '@poodle64/librarian/client';
49
+ import { Transcript } from '@poodle64/librarian/transcript';
50
+ import AgentTranscript from '@poodle64/librarian/agent-transcript';
51
+ import Composer from '@poodle64/librarian/composer';
52
+
53
+ let question = $state('');
54
+ let running = $state(false);
55
+ const transcript = new Transcript();
56
+
57
+ async function submit() {
58
+ const asked = question.trim();
59
+ if (!asked || running) return;
60
+
61
+ question = '';
62
+ running = true;
63
+ transcript.reset();
64
+
65
+ try {
66
+ for await (const event of ask({ question: asked, endpoint: '/api/caller/ask' })) {
67
+ transcript.apply(event);
68
+ if (event.type === 'result' || event.type === 'library_error') running = false;
69
+ }
70
+ } finally {
71
+ running = false;
72
+ }
73
+ }
74
+ </script>
75
+
76
+ <AgentTranscript {question} blocks={transcript.blocks} outcome={transcript.outcome} {running} />
77
+
78
+ <Composer
79
+ bind:value={question}
80
+ {running}
81
+ scope="library"
82
+ onscope={() => {}}
83
+ onsubmit={submit}
84
+ onstop={() => {}}
85
+ />
86
+ ```
87
+
88
+ `ask()`'s `endpoint` defaults to `/api/agent/ask`; pass whatever route the
89
+ consuming app mounts (a room's `/api/rooms/{id}/ask`, a caller's
90
+ `/api/caller/ask`) and an optional `fetch` for a caller-authenticated wrapper.
91
+
92
+ `createHistory(namespace)` from `@poodle64/librarian/history` gives each app,
93
+ or each room inside an app, its own `localStorage` key, so two consumers
94
+ never collide on one conversation list:
95
+
96
+ ```ts
97
+ import { createHistory, titleFrom } from '@poodle64/librarian/history';
98
+
99
+ const history = createHistory('cadmus.rooms.defence-personnel');
100
+ history.load();
101
+ ```
102
+
103
+ ## Verifying a change
104
+
105
+ ```bash
106
+ pnpm run build # svelte-package + publint
107
+ pnpm run check # svelte-check
108
+ pnpm run test # build + vitest
109
+ ```
110
+
111
+ ## Releasing
112
+
113
+ 1. Change a component; bump `version` in `package.json` (CalVer).
114
+ 2. `pnpm build`, which runs `svelte-package` then `publint`.
115
+ 3. Commit, tag `librarian-v<version>`, push the tag; CI publishes to public npm.
@@ -0,0 +1,57 @@
1
+ /**
2
+ * The agent stream.
3
+ *
4
+ * Deliberately thin: it frames SSE and hands back Claude Code's OWN events,
5
+ * unaltered. There is no mapping onto a house vocabulary here or anywhere
6
+ * else — the backend forwards what the CLI emits and this forwards what the
7
+ * backend sent, so the Console renders the real thing and a new Claude Code
8
+ * event type needs no change on either side.
9
+ */
10
+ /** One Claude Code stream event. Typed only where we branch on it. */
11
+ export interface AgentEvent {
12
+ type: string;
13
+ subtype?: string;
14
+ event?: {
15
+ type: string;
16
+ index?: number;
17
+ delta?: {
18
+ type: string;
19
+ text?: string;
20
+ thinking?: string;
21
+ partial_json?: string;
22
+ };
23
+ content_block?: {
24
+ type: string;
25
+ name?: string;
26
+ id?: string;
27
+ };
28
+ };
29
+ message?: {
30
+ content?: Array<Record<string, unknown>>;
31
+ };
32
+ session_id?: string;
33
+ result?: string;
34
+ num_turns?: number;
35
+ total_cost_usd?: number;
36
+ duration_ms?: number;
37
+ is_error?: boolean;
38
+ error?: string;
39
+ detail?: string;
40
+ tools?: string[];
41
+ model?: string;
42
+ [key: string]: unknown;
43
+ }
44
+ export interface AskOptions {
45
+ question: string;
46
+ resume?: string | null;
47
+ subtree?: string;
48
+ /** Collections this question may see. Empty means all of them. */
49
+ collections?: string[];
50
+ signal?: AbortSignal;
51
+ /** Where the ask lands. Each app mounts its own ask route. */
52
+ endpoint?: string;
53
+ /** Override for the ambient `fetch`, e.g. a caller-authenticated wrapper. */
54
+ fetch?: typeof fetch;
55
+ }
56
+ /** Async-iterate the events of one question. */
57
+ export declare function ask(options: AskOptions): AsyncGenerator<AgentEvent>;
package/dist/client.js ADDED
@@ -0,0 +1,60 @@
1
+ /**
2
+ * The agent stream.
3
+ *
4
+ * Deliberately thin: it frames SSE and hands back Claude Code's OWN events,
5
+ * unaltered. There is no mapping onto a house vocabulary here or anywhere
6
+ * else — the backend forwards what the CLI emits and this forwards what the
7
+ * backend sent, so the Console renders the real thing and a new Claude Code
8
+ * event type needs no change on either side.
9
+ */
10
+ /** Async-iterate the events of one question. */
11
+ export async function* ask(options) {
12
+ const doFetch = options.fetch ?? fetch;
13
+ const response = await doFetch(options.endpoint ?? '/api/agent/ask', {
14
+ method: 'POST',
15
+ headers: { 'Content-Type': 'application/json' },
16
+ credentials: 'include',
17
+ body: JSON.stringify({
18
+ question: options.question,
19
+ resume: options.resume ?? null,
20
+ subtree: options.subtree ?? '',
21
+ collections: options.collections ?? []
22
+ }),
23
+ signal: options.signal
24
+ });
25
+ if (!response.ok || !response.body) {
26
+ yield { type: 'library_error', error: `The agent answered ${response.status}.` };
27
+ return;
28
+ }
29
+ const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
30
+ // Frames are separated by a blank line and split across network reads at
31
+ // arbitrary points, so the tail of each read is carried rather than parsed.
32
+ let buffered = '';
33
+ for (;;) {
34
+ const { done, value } = await reader.read();
35
+ if (done)
36
+ break;
37
+ buffered += value;
38
+ let boundary = buffered.indexOf('\n\n');
39
+ while (boundary !== -1) {
40
+ const frame = buffered.slice(0, boundary);
41
+ buffered = buffered.slice(boundary + 2);
42
+ boundary = buffered.indexOf('\n\n');
43
+ // `data:` only — the type lives inside the payload where Claude Code
44
+ // puts it, so there is no second place to look.
45
+ const data = frame
46
+ .split('\n')
47
+ .filter((l) => l.startsWith('data:'))
48
+ .map((l) => l.slice(5).trim())
49
+ .join('');
50
+ if (!data)
51
+ continue;
52
+ try {
53
+ yield JSON.parse(data);
54
+ }
55
+ catch {
56
+ /* a partial frame at end of stream is not an error */
57
+ }
58
+ }
59
+ }
60
+ }
@@ -0,0 +1,63 @@
1
+ <!--
2
+ A whole investigation, as one line the reader can open.
3
+
4
+ Fifteen tool rows stood between the question and the first word of the
5
+ answer, so the answer had to be scrolled to. While the agent is working the
6
+ steps are worth watching, so they show live. The moment prose starts
7
+ arriving they fold to a single summary — the same move Claude makes with
8
+ "Researched for 49s".
9
+ -->
10
+ <script lang="ts">
11
+ import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
12
+ import { summariseActivity, type ActivityGroup } from '../../transcript.svelte';
13
+ import ToolRow from '../tool-row/tool-row.svelte';
14
+ import ThinkingRow from '../thinking-row/thinking-row.svelte';
15
+
16
+ interface Props {
17
+ group: ActivityGroup;
18
+ /** True while this group is the live one — it stays open. */
19
+ live: boolean;
20
+ }
21
+
22
+ let { group, live }: Props = $props();
23
+ let expanded = $state(false);
24
+ const open = $derived(live || expanded);
25
+ </script>
26
+
27
+ {#if live}
28
+ <div class="flex flex-col gap-1.5">
29
+ {#each group.steps as step (step.block.index)}
30
+ {#if step.block.kind === 'tool'}
31
+ <ToolRow block={step.block} repeats={step.repeats} running={true} />
32
+ {:else}
33
+ <ThinkingRow
34
+ block={step.block}
35
+ active={step.block.index === group.steps.at(-1)?.block.index}
36
+ />
37
+ {/if}
38
+ {/each}
39
+ </div>
40
+ {:else}
41
+ <div>
42
+ <button
43
+ type="button"
44
+ class="text-muted-foreground hover:text-foreground flex items-center gap-1.5 text-sm transition-colors"
45
+ onclick={() => (expanded = !expanded)}
46
+ aria-expanded={expanded}
47
+ >
48
+ <ChevronRightIcon class="size-3.5 transition-transform {expanded ? 'rotate-90' : ''}" />
49
+ <span>{summariseActivity(group)}</span>
50
+ </button>
51
+ {#if open}
52
+ <div class="border-border mt-2 ml-1.75 flex flex-col gap-1.5 border-l pl-3">
53
+ {#each group.steps as step (step.block.index)}
54
+ {#if step.block.kind === 'tool'}
55
+ <ToolRow block={step.block} repeats={step.repeats} running={false} />
56
+ {:else}
57
+ <ThinkingRow block={step.block} active={false} />
58
+ {/if}
59
+ {/each}
60
+ </div>
61
+ {/if}
62
+ </div>
63
+ {/if}
@@ -0,0 +1,9 @@
1
+ import { type ActivityGroup } from '../../transcript.svelte';
2
+ interface Props {
3
+ group: ActivityGroup;
4
+ /** True while this group is the live one — it stays open. */
5
+ live: boolean;
6
+ }
7
+ declare const ActivityGroup: import("svelte").Component<Props, {}, "">;
8
+ type ActivityGroup = ReturnType<typeof ActivityGroup>;
9
+ export default ActivityGroup;
@@ -0,0 +1,2 @@
1
+ export { default as ActivityGroup } from './activity-group.svelte';
2
+ export { default } from './activity-group.svelte';
@@ -0,0 +1,2 @@
1
+ export { default as ActivityGroup } from './activity-group.svelte';
2
+ export { default } from './activity-group.svelte';
@@ -0,0 +1,71 @@
1
+ <!--
2
+ One question and everything the agent did answering it.
3
+
4
+ User turn as a rounded bubble, assistant prose led by a status dot, tool
5
+ calls as rows in the flow, thinking as stacked phase labels that dim once
6
+ superseded.
7
+ -->
8
+ <script lang="ts">
9
+ import { segment, type Block, type Outcome } from '../../transcript.svelte';
10
+ import Working from '../working/working.svelte';
11
+ import ActivityGroup from '../activity-group/activity-group.svelte';
12
+ import Markdown from '../markdown/markdown.svelte';
13
+
14
+ interface Props {
15
+ question: string;
16
+ blocks: Block[];
17
+ outcome: Outcome | null;
18
+ running: boolean;
19
+ /** Forwarded to Markdown; omit if the caller has no collections to chip. */
20
+ collectionNames?: Set<string>;
21
+ }
22
+
23
+ let { question, blocks, outcome, running, collectionNames = new Set() }: Props = $props();
24
+
25
+ // An activity group stays live — and therefore open — only while it is the
26
+ // last thing in the turn. As soon as prose arrives after it, it folds.
27
+ const segments = $derived(segment(blocks));
28
+ const lastIndex = $derived(segments.at(-1)?.index ?? -1);
29
+ </script>
30
+
31
+ <article class="flex flex-col gap-3">
32
+ <!-- Right-aligned, following Claude and ChatGPT rather than a VS Code-style
33
+ panel: this surface is read by people who arrive with those two as
34
+ their model of what a chat looks like. -->
35
+ <div
36
+ class="bg-surface-3 text-foreground border-border max-w-[85%] self-end rounded-2xl border px-4 py-2.5 text-base"
37
+ >
38
+ {question}
39
+ </div>
40
+
41
+ {#if running && segments.length === 0}
42
+ <Working />
43
+ {/if}
44
+
45
+ {#each segments as seg (seg.index)}
46
+ {#if seg.kind === 'activity'}
47
+ <ActivityGroup group={seg} live={running && seg.index === lastIndex} />
48
+ {:else}
49
+ <div class="flex gap-2.5">
50
+ <span class="bg-muted-foreground/40 mt-2.5 size-1.5 shrink-0 rounded-full"></span>
51
+ <div class="min-w-0 flex-1">
52
+ <Markdown
53
+ content={seg.text}
54
+ streaming={running && seg.index === lastIndex}
55
+ {collectionNames}
56
+ />
57
+ </div>
58
+ </div>
59
+ {/if}
60
+ {/each}
61
+
62
+ {#if outcome}
63
+ <p class="text-muted-foreground pl-4 font-mono text-xs tabular-nums">
64
+ {#if outcome.error}
65
+ {outcome.error}
66
+ {:else}
67
+ {outcome.turns} turns · {((outcome.durationMs ?? 0) / 1000).toFixed(1)}s
68
+ {/if}
69
+ </p>
70
+ {/if}
71
+ </article>
@@ -0,0 +1,12 @@
1
+ import { type Block, type Outcome } from '../../transcript.svelte';
2
+ interface Props {
3
+ question: string;
4
+ blocks: Block[];
5
+ outcome: Outcome | null;
6
+ running: boolean;
7
+ /** Forwarded to Markdown; omit if the caller has no collections to chip. */
8
+ collectionNames?: Set<string>;
9
+ }
10
+ declare const AgentTranscript: import("svelte").Component<Props, {}, "">;
11
+ type AgentTranscript = ReturnType<typeof AgentTranscript>;
12
+ export default AgentTranscript;
@@ -0,0 +1,2 @@
1
+ export { default as AgentTranscript } from './agent-transcript.svelte';
2
+ export { default } from './agent-transcript.svelte';
@@ -0,0 +1,2 @@
1
+ export { default as AgentTranscript } from './agent-transcript.svelte';
2
+ export { default } from './agent-transcript.svelte';
@@ -0,0 +1,115 @@
1
+ <!--
2
+ The composer: one rounded box holding the input AND a footer row.
3
+
4
+ Two things carry it: the chips sit BELOW the input inside the same box
5
+ rather than beside it, and the trailing control is a circular filled accent
6
+ button. The empty→typing transition lives almost entirely in that control —
7
+ the box chrome does not resize.
8
+
9
+ The composer stays enabled while the agent works. Queuing the next message
10
+ is not built yet; disabling the box would be a different and worse
11
+ behaviour than the one being copied, so it is not disabled.
12
+ -->
13
+ <script lang="ts">
14
+ import ArrowUpIcon from '@lucide/svelte/icons/arrow-up';
15
+ import SquareIcon from '@lucide/svelte/icons/square';
16
+
17
+ export type Scope = 'document' | 'collection' | 'library';
18
+
19
+ interface Props {
20
+ value: string;
21
+ running: boolean;
22
+ scope: Scope;
23
+ /** Names for the two narrower scopes; absent means that scope is unavailable. */
24
+ documentName?: string;
25
+ collectionName?: string;
26
+ onscope: (scope: Scope) => void;
27
+ onsubmit: () => void;
28
+ onstop: () => void;
29
+ }
30
+
31
+ let {
32
+ value = $bindable(),
33
+ running,
34
+ scope,
35
+ documentName,
36
+ collectionName,
37
+ onscope,
38
+ onsubmit,
39
+ onstop
40
+ }: Props = $props();
41
+
42
+ // What the agent is being asked about — chat with this document, this
43
+ // collection, or the whole library. Callers that have no narrower scope
44
+ // pass neither name and only "All collections" ever renders.
45
+ const choices = $derived(
46
+ [
47
+ documentName ? { id: 'document' as const, label: documentName } : null,
48
+ collectionName ? { id: 'collection' as const, label: collectionName } : null,
49
+ { id: 'library' as const, label: 'All collections' }
50
+ ].filter((c) => c !== null)
51
+ );
52
+
53
+ // Three chips in a narrow column clipped all three to fragments
54
+ // ("defence-s…", "All colle…"). Wrapping beats truncating: a chip a reader
55
+ // cannot finish reading is not a control, it is decoration.
56
+
57
+ function keydown(event: KeyboardEvent) {
58
+ // Enter sends, Shift+Enter breaks the line — the convention in every
59
+ // reference and the one a user will try first.
60
+ if (event.key === 'Enter' && !event.shiftKey) {
61
+ event.preventDefault();
62
+ if (value.trim()) onsubmit();
63
+ }
64
+ }
65
+ </script>
66
+
67
+ <div
68
+ class="border-border bg-surface-1 focus-within:border-primary/60 rounded-xl border transition-colors"
69
+ >
70
+ <textarea
71
+ bind:value
72
+ onkeydown={keydown}
73
+ rows="2"
74
+ placeholder={running ? 'Queue another message…' : 'Ask anything'}
75
+ class="text-foreground placeholder:text-muted-foreground max-h-52 w-full resize-none bg-transparent px-4 pt-3 pb-2 text-base outline-none"
76
+ ></textarea>
77
+
78
+ <div class="flex items-center gap-2 px-3 pb-2.5">
79
+ <div class="flex min-w-0 flex-wrap items-center gap-1">
80
+ {#each choices as choice (choice.id)}
81
+ <button
82
+ type="button"
83
+ onclick={() => onscope(choice.id)}
84
+ class="max-w-full truncate rounded-full px-2 py-0.5 text-xs transition-colors {scope ===
85
+ choice.id
86
+ ? 'bg-primary/15 text-foreground border-primary/40 border'
87
+ : 'text-muted-foreground hover:text-foreground border border-transparent'}"
88
+ >
89
+ {choice.label}
90
+ </button>
91
+ {/each}
92
+ </div>
93
+ <span class="flex-1"></span>
94
+ {#if running}
95
+ <button
96
+ type="button"
97
+ onclick={onstop}
98
+ aria-label="Stop"
99
+ class="bg-foreground text-background hover:bg-foreground/90 flex size-8 items-center justify-center rounded-full transition-colors"
100
+ >
101
+ <SquareIcon class="size-3.5 fill-current" />
102
+ </button>
103
+ {:else}
104
+ <button
105
+ type="button"
106
+ onclick={onsubmit}
107
+ disabled={!value.trim()}
108
+ aria-label="Send"
109
+ class="bg-primary text-primary-foreground hover:bg-primary/90 disabled:hover:bg-primary flex size-8 items-center justify-center rounded-full transition-all disabled:opacity-30"
110
+ >
111
+ <ArrowUpIcon class="size-4" />
112
+ </button>
113
+ {/if}
114
+ </div>
115
+ </div>
@@ -0,0 +1,15 @@
1
+ export type Scope = 'document' | 'collection' | 'library';
2
+ interface Props {
3
+ value: string;
4
+ running: boolean;
5
+ scope: Scope;
6
+ /** Names for the two narrower scopes; absent means that scope is unavailable. */
7
+ documentName?: string;
8
+ collectionName?: string;
9
+ onscope: (scope: Scope) => void;
10
+ onsubmit: () => void;
11
+ onstop: () => void;
12
+ }
13
+ declare const Composer: import("svelte").Component<Props, {}, "value">;
14
+ type Composer = ReturnType<typeof Composer>;
15
+ export default Composer;
@@ -0,0 +1,3 @@
1
+ export { default as Composer } from './composer.svelte';
2
+ export { default } from './composer.svelte';
3
+ export type { Scope } from './composer.svelte';
@@ -0,0 +1,2 @@
1
+ export { default as Composer } from './composer.svelte';
2
+ export { default } from './composer.svelte';
@@ -0,0 +1,3 @@
1
+ export { default as Markdown } from './markdown.svelte';
2
+ export { default } from './markdown.svelte';
3
+ export { balance, render, markCollections, highlight } from './markdown';
@@ -0,0 +1,3 @@
1
+ export { default as Markdown } from './markdown.svelte';
2
+ export { default } from './markdown.svelte';
3
+ export { balance, render, markCollections, highlight } from './markdown';
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Markdown for a STREAMING agent transcript.
3
+ *
4
+ * Two things separate this from calling `marked()` on a string, and both are
5
+ * what make a streaming transcript look finished rather than glitchy:
6
+ *
7
+ * 1. Half-arrived markdown is not valid markdown. A code fence that has opened
8
+ * and not yet closed makes `marked` treat the rest of the answer as code, so
9
+ * the message visibly flips between prose and a grey slab on every token.
10
+ * Unterminated constructs are closed before parsing.
11
+ * 2. Highlighting every code block on every token is wasted work the user never
12
+ * sees — the block changes again a few milliseconds later. Highlighting is a
13
+ * separate pass the caller runs once the text has settled.
14
+ *
15
+ * Sanitised on the way out, always. The content is model output rendered as
16
+ * HTML, which is exactly the case DOMPurify exists for.
17
+ */
18
+ /** Close anything the stream has opened but not yet finished. */
19
+ export declare function balance(markdown: string): string;
20
+ export declare function render(markdown: string, { streaming }?: {
21
+ streaming?: boolean | undefined;
22
+ }): string;
23
+ /**
24
+ * Mark every inline `code` span that names a known collection.
25
+ *
26
+ * The agent puts identifiers in backticks — document titles, requirement
27
+ * numbers, collection names — and they all render alike. Matching against
28
+ * the caller's real list rather than a prompt convention means the prompt
29
+ * cannot drift out of sync with the rendering.
30
+ */
31
+ export declare function markCollections(root: HTMLElement, collections: Set<string>): void;
32
+ /**
33
+ * Replace every `<pre><code>` in a rendered fragment with a highlighted one.
34
+ * Runs against the DOM node rather than the HTML string so it can be applied
35
+ * after paint without re-parsing the markdown.
36
+ */
37
+ export declare function highlight(root: HTMLElement): Promise<void>;