@poodle64/librarian 2026.9.5 → 2026.9.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +143 -44
- package/dist/attachments.d.ts +30 -0
- package/dist/attachments.js +80 -0
- package/dist/citations.d.ts +63 -0
- package/dist/citations.js +89 -0
- package/dist/client.d.ts +5 -0
- package/dist/client.js +39 -13
- package/dist/components/activity-group/activity-group.svelte +36 -44
- package/dist/components/activity-group/activity-group.svelte.d.ts +1 -1
- package/dist/components/agent-transcript/agent-transcript.svelte +166 -26
- package/dist/components/agent-transcript/agent-transcript.svelte.d.ts +8 -0
- package/dist/components/composer/composer.svelte +212 -59
- package/dist/components/composer/composer.svelte.d.ts +5 -1
- package/dist/components/conversation/conversation.svelte +179 -0
- package/dist/components/conversation/conversation.svelte.d.ts +26 -0
- package/dist/components/conversation/index.d.ts +2 -0
- package/dist/components/conversation/index.js +2 -0
- package/dist/components/document-pane/document-pane.svelte +177 -0
- package/dist/components/document-pane/document-pane.svelte.d.ts +9 -0
- package/dist/components/document-pane/index.d.ts +2 -0
- package/dist/components/document-pane/index.js +2 -0
- package/dist/components/markdown/index.d.ts +1 -1
- package/dist/components/markdown/index.js +1 -1
- package/dist/components/markdown/markdown.d.ts +13 -0
- package/dist/components/markdown/markdown.js +49 -1
- package/dist/components/markdown/markdown.svelte +85 -3
- package/dist/components/markdown/markdown.svelte.d.ts +6 -0
- package/dist/components/tool-row/tool-row.svelte +5 -11
- package/dist/components/working/working.svelte +1 -9
- package/dist/follow-scroll.svelte.d.ts +34 -0
- package/dist/follow-scroll.svelte.js +46 -0
- package/dist/transcript.svelte.d.ts +36 -4
- package/dist/transcript.svelte.js +50 -26
- package/package.json +16 -2
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
<!--
|
|
2
|
-
A whole investigation, as
|
|
2
|
+
A whole investigation, as ONE line the reader can open.
|
|
3
3
|
|
|
4
|
-
Fifteen tool rows
|
|
5
|
-
answer
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
Not a stack. Fifteen tool rows between the question and the first word of the
|
|
5
|
+
answer is the machine room on show, and a running commentary of "Let me
|
|
6
|
+
check…" is worse: it narrates a mechanism Milton is under instruction never
|
|
7
|
+
to mention. So there is exactly one row in every state — "Working…" while it
|
|
8
|
+
runs, a count of what was done once it settles — and the steps are behind a
|
|
9
|
+
disclosure for the reader who wants them.
|
|
9
10
|
-->
|
|
10
11
|
<script lang="ts">
|
|
11
12
|
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
|
|
@@ -15,49 +16,40 @@
|
|
|
15
16
|
|
|
16
17
|
interface Props {
|
|
17
18
|
group: ActivityGroup;
|
|
18
|
-
/** True while this group is the live one
|
|
19
|
+
/** True while this group is the live one. */
|
|
19
20
|
live: boolean;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
let { group, live }: Props = $props();
|
|
23
24
|
let expanded = $state(false);
|
|
24
|
-
|
|
25
|
+
|
|
26
|
+
// A live group has nothing to count yet, so counting it reads as "0
|
|
27
|
+
// searches" flickering up to the real number.
|
|
28
|
+
const label = $derived(live ? 'Working…' : summariseActivity(group));
|
|
25
29
|
</script>
|
|
26
30
|
|
|
27
|
-
|
|
28
|
-
<
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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>
|
|
31
|
+
<div>
|
|
32
|
+
<button
|
|
33
|
+
type="button"
|
|
34
|
+
class="text-muted-foreground hover:text-foreground flex items-center gap-1.5 text-sm transition-colors"
|
|
35
|
+
onclick={() => (expanded = !expanded)}
|
|
36
|
+
aria-expanded={expanded}
|
|
37
|
+
>
|
|
38
|
+
<ChevronRightIcon class="size-3.5 transition-transform {expanded ? 'rotate-90' : ''}" />
|
|
39
|
+
{#if live}
|
|
40
|
+
<span class="bg-status-info size-1.5 shrink-0 animate-pulse rounded-full"></span>
|
|
61
41
|
{/if}
|
|
62
|
-
|
|
63
|
-
|
|
42
|
+
<span>{label}</span>
|
|
43
|
+
</button>
|
|
44
|
+
{#if expanded}
|
|
45
|
+
<div class="border-border mt-2 ml-1.75 flex flex-col gap-1.5 border-l pl-3">
|
|
46
|
+
{#each group.steps as step (step.block.index)}
|
|
47
|
+
{#if step.block.kind === 'tool'}
|
|
48
|
+
<ToolRow block={step.block} repeats={step.repeats} running={live} />
|
|
49
|
+
{:else}
|
|
50
|
+
<ThinkingRow block={step.block} active={false} />
|
|
51
|
+
{/if}
|
|
52
|
+
{/each}
|
|
53
|
+
</div>
|
|
54
|
+
{/if}
|
|
55
|
+
</div>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type ActivityGroup } from '../../transcript.svelte';
|
|
2
2
|
interface Props {
|
|
3
3
|
group: ActivityGroup;
|
|
4
|
-
/** True while this group is the live one
|
|
4
|
+
/** True while this group is the live one. */
|
|
5
5
|
live: boolean;
|
|
6
6
|
}
|
|
7
7
|
declare const ActivityGroup: import("svelte").Component<Props, {}, "">;
|
|
@@ -1,12 +1,28 @@
|
|
|
1
1
|
<!--
|
|
2
|
-
One question and everything
|
|
2
|
+
One question and everything Milton did answering it.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
Three things carry the shape. The question is a bubble against the edge, so a
|
|
5
|
+
reader scanning back finds their own words without reading any of the answer.
|
|
6
|
+
The answer is plain prose held to a reading measure, because a full-width
|
|
7
|
+
line at 1440 is the single clearest tell that a surface was never read on.
|
|
8
|
+
Everything Milton DID is one quiet line above it, never a stack.
|
|
7
9
|
-->
|
|
8
10
|
<script lang="ts">
|
|
9
|
-
import
|
|
11
|
+
import CheckIcon from '@lucide/svelte/icons/check';
|
|
12
|
+
import CopyIcon from '@lucide/svelte/icons/copy';
|
|
13
|
+
import RefreshCwIcon from '@lucide/svelte/icons/refresh-cw';
|
|
14
|
+
import {
|
|
15
|
+
readerQuestion,
|
|
16
|
+
segment,
|
|
17
|
+
type Block,
|
|
18
|
+
type Outcome
|
|
19
|
+
} from '../../transcript.svelte';
|
|
20
|
+
import {
|
|
21
|
+
citationMarkers,
|
|
22
|
+
resolveCitations,
|
|
23
|
+
splitSources,
|
|
24
|
+
type Citation
|
|
25
|
+
} from '../../citations';
|
|
10
26
|
import Working from '../working/working.svelte';
|
|
11
27
|
import ActivityGroup from '../activity-group/activity-group.svelte';
|
|
12
28
|
import Markdown from '../markdown/markdown.svelte';
|
|
@@ -16,16 +32,76 @@
|
|
|
16
32
|
blocks: Block[];
|
|
17
33
|
outcome: Outcome | null;
|
|
18
34
|
running: boolean;
|
|
35
|
+
/** Sources from the library's `citations` frame. Absent falls back to
|
|
36
|
+
* whatever a "## Sources" block in the prose says. */
|
|
37
|
+
citations?: Citation[];
|
|
19
38
|
/** Forwarded to Markdown; omit if the caller has no collections to chip. */
|
|
20
39
|
collectionNames?: Set<string>;
|
|
40
|
+
/** Opens the document pane. Omit and chips render but do not open. */
|
|
41
|
+
oncite?: (citation: Citation) => void;
|
|
42
|
+
/** Offered on the LAST answer only — re-asks the same question. */
|
|
43
|
+
onregenerate?: () => void;
|
|
21
44
|
}
|
|
22
45
|
|
|
23
|
-
let {
|
|
46
|
+
let {
|
|
47
|
+
question,
|
|
48
|
+
blocks,
|
|
49
|
+
outcome,
|
|
50
|
+
running,
|
|
51
|
+
citations = [],
|
|
52
|
+
collectionNames = new Set(),
|
|
53
|
+
oncite,
|
|
54
|
+
onregenerate
|
|
55
|
+
}: Props = $props();
|
|
24
56
|
|
|
25
|
-
|
|
26
|
-
|
|
57
|
+
const asked = $derived(readerQuestion(question));
|
|
58
|
+
|
|
59
|
+
// An activity group stays live — and therefore labelled "Working…" — only
|
|
60
|
+
// while it is the last thing in the turn. As soon as prose arrives, it
|
|
61
|
+
// settles into its count.
|
|
27
62
|
const segments = $derived(segment(blocks));
|
|
28
63
|
const lastIndex = $derived(segments.at(-1)?.index ?? -1);
|
|
64
|
+
|
|
65
|
+
// The prose block a Sources list may be hiding at the end of is the last
|
|
66
|
+
// one; splitting every text segment would eat a "## Sources" heading a
|
|
67
|
+
// reader legitimately asked about mid-answer.
|
|
68
|
+
const answer = $derived(
|
|
69
|
+
segments
|
|
70
|
+
.filter((s) => s.kind === 'text')
|
|
71
|
+
.map((s) => s.text)
|
|
72
|
+
.join('\n\n')
|
|
73
|
+
);
|
|
74
|
+
const split = $derived(running ? { body: '', citations: [] } : splitSources(answer));
|
|
75
|
+
const sources = $derived(resolveCitations(citations, split.citations));
|
|
76
|
+
// Only a number the prose actually marks becomes a chip; a source list may
|
|
77
|
+
// legitimately carry an entry the answer never points at inline.
|
|
78
|
+
const numbers = $derived(
|
|
79
|
+
new Set(citationMarkers(answer).filter((n) => sources.some((c) => c.n === n)))
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
function prose(text: string, isLast: boolean): string {
|
|
83
|
+
// Only the last text segment can carry the trailing Sources block, and
|
|
84
|
+
// only once the answer has settled — mid-stream the heading may be all
|
|
85
|
+
// that has arrived.
|
|
86
|
+
return isLast && split.citations.length > 0 ? split.body : text;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const hasAnswer = $derived(segments.some((s) => s.kind === 'text'));
|
|
90
|
+
// A failed turn settles too, and "Ask again" is the one thing a reader wants
|
|
91
|
+
// from it — there is just nothing to copy.
|
|
92
|
+
const settled = $derived(!running && (hasAnswer || Boolean(outcome?.error)));
|
|
93
|
+
|
|
94
|
+
let copied = $state(false);
|
|
95
|
+
async function copy() {
|
|
96
|
+
try {
|
|
97
|
+
await navigator.clipboard.writeText(answer);
|
|
98
|
+
copied = true;
|
|
99
|
+
setTimeout(() => (copied = false), 1600);
|
|
100
|
+
} catch {
|
|
101
|
+
// A denied clipboard permission is not worth an error state on an
|
|
102
|
+
// answer the reader can still select by hand.
|
|
103
|
+
}
|
|
104
|
+
}
|
|
29
105
|
</script>
|
|
30
106
|
|
|
31
107
|
<article class="flex flex-col gap-3">
|
|
@@ -33,9 +109,9 @@
|
|
|
33
109
|
panel: this surface is read by people who arrive with those two as
|
|
34
110
|
their model of what a chat looks like. -->
|
|
35
111
|
<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"
|
|
112
|
+
class="bg-surface-3 text-foreground border-border max-w-[85%] self-end rounded-2xl border px-4 py-2.5 text-base break-words whitespace-pre-wrap"
|
|
37
113
|
>
|
|
38
|
-
{
|
|
114
|
+
{asked}
|
|
39
115
|
</div>
|
|
40
116
|
|
|
41
117
|
{#if running && segments.length === 0}
|
|
@@ -46,26 +122,90 @@
|
|
|
46
122
|
{#if seg.kind === 'activity'}
|
|
47
123
|
<ActivityGroup group={seg} live={running && seg.index === lastIndex} />
|
|
48
124
|
{:else}
|
|
49
|
-
<div class="
|
|
50
|
-
<
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
125
|
+
<div class="max-w-[72ch] min-w-0">
|
|
126
|
+
<Markdown
|
|
127
|
+
content={prose(seg.text, seg.index === lastIndex)}
|
|
128
|
+
streaming={running && seg.index === lastIndex}
|
|
129
|
+
{collectionNames}
|
|
130
|
+
citationNumbers={numbers}
|
|
131
|
+
oncite={(n) => {
|
|
132
|
+
const found = sources.find((c) => c.n === n);
|
|
133
|
+
if (found) oncite?.(found);
|
|
134
|
+
}}
|
|
135
|
+
/>
|
|
58
136
|
</div>
|
|
59
137
|
{/if}
|
|
60
138
|
{/each}
|
|
61
139
|
|
|
62
|
-
{#if
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
140
|
+
{#if sources.length > 0}
|
|
141
|
+
<section class="max-w-[72ch]">
|
|
142
|
+
<h2 class="text-muted-foreground mb-1.5 text-xs font-medium tracking-wide uppercase">
|
|
143
|
+
Sources
|
|
144
|
+
</h2>
|
|
145
|
+
<ol class="flex flex-col gap-1">
|
|
146
|
+
{#each sources as source (source.n)}
|
|
147
|
+
<li>
|
|
148
|
+
<button
|
|
149
|
+
type="button"
|
|
150
|
+
onclick={() => oncite?.(source)}
|
|
151
|
+
disabled={!source.document_id || !oncite}
|
|
152
|
+
class="border-border hover:border-border-strong hover:bg-surface-2 focus-visible:ring-ring flex w-full items-baseline gap-2 rounded-lg border px-2.5 py-1.5 text-left text-sm transition-colors focus-visible:ring-2 focus-visible:outline-none disabled:cursor-default disabled:hover:bg-transparent"
|
|
153
|
+
>
|
|
154
|
+
<span
|
|
155
|
+
class="bg-primary/15 text-foreground shrink-0 rounded px-1.5 font-mono text-xs tabular-nums"
|
|
156
|
+
>{source.n}</span
|
|
157
|
+
>
|
|
158
|
+
<span class="min-w-0">
|
|
159
|
+
<span class="text-foreground">{source.title}</span>
|
|
160
|
+
{#if source.section}<span class="text-muted-foreground">
|
|
161
|
+
· {source.section}</span
|
|
162
|
+
>{/if}
|
|
163
|
+
</span>
|
|
164
|
+
</button>
|
|
165
|
+
</li>
|
|
166
|
+
{/each}
|
|
167
|
+
</ol>
|
|
168
|
+
</section>
|
|
169
|
+
{/if}
|
|
170
|
+
|
|
171
|
+
{#if outcome?.error}
|
|
172
|
+
<p
|
|
173
|
+
class="border-status-error/40 bg-status-error/10 text-foreground max-w-[72ch] rounded-lg border px-3 py-2 text-sm"
|
|
174
|
+
role="alert"
|
|
175
|
+
>
|
|
176
|
+
{outcome.error}
|
|
69
177
|
</p>
|
|
70
178
|
{/if}
|
|
179
|
+
|
|
180
|
+
{#if settled}
|
|
181
|
+
<div class="text-muted-foreground -ml-1.5 flex items-center gap-1">
|
|
182
|
+
{#if hasAnswer}
|
|
183
|
+
<button
|
|
184
|
+
type="button"
|
|
185
|
+
onclick={copy}
|
|
186
|
+
aria-label={copied ? 'Copied' : 'Copy answer'}
|
|
187
|
+
class="hover:text-foreground hover:bg-surface-2 focus-visible:ring-ring flex items-center gap-1.5 rounded-md px-1.5 py-1 text-xs transition-colors focus-visible:ring-2 focus-visible:outline-none"
|
|
188
|
+
>
|
|
189
|
+
{#if copied}<CheckIcon class="size-3.5" />{:else}<CopyIcon class="size-3.5" />{/if}
|
|
190
|
+
<span>{copied ? 'Copied' : 'Copy'}</span>
|
|
191
|
+
</button>
|
|
192
|
+
{/if}
|
|
193
|
+
{#if onregenerate}
|
|
194
|
+
<button
|
|
195
|
+
type="button"
|
|
196
|
+
onclick={onregenerate}
|
|
197
|
+
aria-label="Ask again"
|
|
198
|
+
class="hover:text-foreground hover:bg-surface-2 focus-visible:ring-ring flex items-center gap-1.5 rounded-md px-1.5 py-1 text-xs transition-colors focus-visible:ring-2 focus-visible:outline-none"
|
|
199
|
+
>
|
|
200
|
+
<RefreshCwIcon class="size-3.5" />
|
|
201
|
+
<span>Ask again</span>
|
|
202
|
+
</button>
|
|
203
|
+
{/if}
|
|
204
|
+
{#if outcome && !outcome.error}
|
|
205
|
+
<span class="pl-1 font-mono text-xs tabular-nums opacity-70">
|
|
206
|
+
{((outcome.durationMs ?? 0) / 1000).toFixed(1)}s
|
|
207
|
+
</span>
|
|
208
|
+
{/if}
|
|
209
|
+
</div>
|
|
210
|
+
{/if}
|
|
71
211
|
</article>
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import { type Block, type Outcome } from '../../transcript.svelte';
|
|
2
|
+
import { type Citation } from '../../citations';
|
|
2
3
|
interface Props {
|
|
3
4
|
question: string;
|
|
4
5
|
blocks: Block[];
|
|
5
6
|
outcome: Outcome | null;
|
|
6
7
|
running: boolean;
|
|
8
|
+
/** Sources from the library's `citations` frame. Absent falls back to
|
|
9
|
+
* whatever a "## Sources" block in the prose says. */
|
|
10
|
+
citations?: Citation[];
|
|
7
11
|
/** Forwarded to Markdown; omit if the caller has no collections to chip. */
|
|
8
12
|
collectionNames?: Set<string>;
|
|
13
|
+
/** Opens the document pane. Omit and chips render but do not open. */
|
|
14
|
+
oncite?: (citation: Citation) => void;
|
|
15
|
+
/** Offered on the LAST answer only — re-asks the same question. */
|
|
16
|
+
onregenerate?: () => void;
|
|
9
17
|
}
|
|
10
18
|
declare const AgentTranscript: import("svelte").Component<Props, {}, "">;
|
|
11
19
|
type AgentTranscript = ReturnType<typeof AgentTranscript>;
|