@poodle64/librarian 2026.9.13 → 2026.9.15
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 +77 -19
- package/dist/client.d.ts +16 -1
- package/dist/client.js +55 -8
- package/dist/components/activity-group/activity-group.svelte +76 -8
- package/dist/components/agent-transcript/agent-transcript.svelte +346 -150
- package/dist/components/agent-transcript/agent-transcript.svelte.d.ts +7 -0
- package/dist/components/artefact-card/artefact-card.svelte +30 -2
- package/dist/components/artefact-pane/artefact-pane.svelte +141 -50
- package/dist/components/composer/composer.svelte +255 -38
- package/dist/components/composer/composer.svelte.d.ts +6 -0
- package/dist/components/conversation/conversation.svelte +254 -81
- package/dist/components/conversation/conversation.svelte.d.ts +7 -0
- package/dist/components/document-pane/document-pane.svelte +216 -33
- package/dist/components/markdown/markdown.svelte +73 -24
- package/dist/components/scope-statement/scope-statement.svelte +79 -12
- package/dist/components/source-list/index.d.ts +2 -0
- package/dist/components/source-list/index.js +2 -0
- package/dist/components/source-list/source-list.svelte +137 -0
- package/dist/components/source-list/source-list.svelte.d.ts +13 -0
- package/dist/components/thinking-row/thinking-row.svelte +69 -18
- package/dist/components/tool-row/tool-row.svelte +132 -26
- package/dist/components/working/working.svelte +51 -7
- package/dist/components/working/working.svelte.d.ts +3 -0
- package/dist/copy.d.ts +45 -10
- package/dist/copy.js +77 -32
- package/dist/transcript.svelte.d.ts +10 -0
- package/dist/transcript.svelte.js +4 -1
- package/package.json +4 -2
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import ArrowDownIcon from '@lucide/svelte/icons/arrow-down';
|
|
14
14
|
import type { Turn } from '../../transcript.svelte';
|
|
15
15
|
import type { Citation, LoadDocument } from '../../citations';
|
|
16
|
-
import { resolveCopy, type LibrarianCopy } from '../../copy';
|
|
16
|
+
import { DEFAULT_PERSONA, personaName, resolveCopy, type LibrarianCopy } from '../../copy';
|
|
17
17
|
import { FollowScroll } from '../../follow-scroll.svelte';
|
|
18
18
|
import AgentTranscript from '../agent-transcript/agent-transcript.svelte';
|
|
19
19
|
import ArtefactPane from '../artefact-pane/artefact-pane.svelte';
|
|
@@ -31,6 +31,13 @@
|
|
|
31
31
|
/** `Transcript.version` — bumped per event. Text grows in place, so a
|
|
32
32
|
* count of turns is not enough to keep the scroll following. */
|
|
33
33
|
version?: number;
|
|
34
|
+
/** Who is speaking. A slug is fine — `penny` renders as "Penny" — and
|
|
35
|
+
* every word this package says is composed from it: the working line,
|
|
36
|
+
* the failure sentences, the scope label, the composer's placeholder
|
|
37
|
+
* and the accessible name of the scroll region. Absent is the library's
|
|
38
|
+
* own Milton, which is a default rather than a hardcoding. */
|
|
39
|
+
name?: string;
|
|
40
|
+
/** The empty state's opening line. Absent takes the persona's own. */
|
|
34
41
|
welcome?: string;
|
|
35
42
|
/** Up to three, shown in the empty state as tappable pills. */
|
|
36
43
|
examples?: string[];
|
|
@@ -59,7 +66,8 @@
|
|
|
59
66
|
turns,
|
|
60
67
|
running,
|
|
61
68
|
version = 0,
|
|
62
|
-
|
|
69
|
+
name = DEFAULT_PERSONA,
|
|
70
|
+
welcome,
|
|
63
71
|
examples = [],
|
|
64
72
|
onexample,
|
|
65
73
|
scope,
|
|
@@ -71,7 +79,24 @@
|
|
|
71
79
|
composer
|
|
72
80
|
}: Props = $props();
|
|
73
81
|
|
|
74
|
-
|
|
82
|
+
// Resolved ONCE, here, and handed down whole: every child takes `copy` and
|
|
83
|
+
// none of them takes the name, so there is exactly one place a persona
|
|
84
|
+
// turns into sentences.
|
|
85
|
+
const words = $derived(resolveCopy(copy, name));
|
|
86
|
+
const who = $derived(personaName(name));
|
|
87
|
+
const opening = $derived(welcome ?? words.welcome);
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* When each turn arrived, for the turns this surface actually watched
|
|
91
|
+
* arrive.
|
|
92
|
+
*
|
|
93
|
+
* A host that persists its conversations sets `turn.at` and that wins. A
|
|
94
|
+
* host that does not gets a stamp the moment a turn appears, which is true
|
|
95
|
+
* for a live turn and deliberately absent for one read back out of
|
|
96
|
+
* history — stamping those with `Date.now()` would print this afternoon
|
|
97
|
+
* against a question asked last week.
|
|
98
|
+
*/
|
|
99
|
+
let stamps = $state<Record<string, number>>({});
|
|
75
100
|
|
|
76
101
|
let viewport = $state<HTMLElement | null>(null);
|
|
77
102
|
let column = $state<Column>(null);
|
|
@@ -92,9 +117,12 @@
|
|
|
92
117
|
// of reactive state re-triggers itself, which Svelte stops with
|
|
93
118
|
// `effect_update_depth_exceeded` — measured, on this component. Nothing
|
|
94
119
|
// renders this, so a plain variable is both correct and enough.
|
|
95
|
-
let seen =
|
|
120
|
+
let seen = -1;
|
|
96
121
|
$effect(() => {
|
|
97
|
-
if (turns.length > seen)
|
|
122
|
+
if (seen >= 0 && turns.length > seen) {
|
|
123
|
+
follow.pin();
|
|
124
|
+
for (const turn of turns.slice(seen)) stamps[turn.id] ??= Date.now();
|
|
125
|
+
}
|
|
98
126
|
seen = turns.length;
|
|
99
127
|
});
|
|
100
128
|
|
|
@@ -148,100 +176,245 @@
|
|
|
148
176
|
}
|
|
149
177
|
</script>
|
|
150
178
|
|
|
151
|
-
<div class="
|
|
152
|
-
<div class="
|
|
179
|
+
<div class="ds-lib-surface">
|
|
180
|
+
<div class="ds-lib-main">
|
|
153
181
|
<!-- The pill is positioned against the SCROLL region, not the column: the
|
|
154
182
|
composer is in the column too, and a pill over the input is a pill in
|
|
155
183
|
the way. -->
|
|
156
|
-
<div class="
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
184
|
+
<div class="ds-lib-scroll-region">
|
|
185
|
+
<div
|
|
186
|
+
bind:this={viewport}
|
|
187
|
+
onscroll={scrolled}
|
|
188
|
+
role="log"
|
|
189
|
+
aria-live="polite"
|
|
190
|
+
aria-busy={running}
|
|
191
|
+
aria-label={words.conversationLabel}
|
|
192
|
+
class="ds-lib-scroll"
|
|
193
|
+
>
|
|
194
|
+
<!-- The measure, in this package's own stylesheet.
|
|
195
|
+
`--ds-lib-measure` is the one knob a host may turn, and it is a
|
|
196
|
+
token rather than a class precisely because a class here is a
|
|
197
|
+
string a consuming app's Tailwind has to find in `dist` — the
|
|
198
|
+
arrangement that silently produced `max-width: none` and a
|
|
199
|
+
2302px line in Pebblestone's production build. -->
|
|
200
|
+
<div class="ds-lib-column">
|
|
201
|
+
{#if turns.length === 0}
|
|
202
|
+
<div class="ds-lib-empty">
|
|
203
|
+
<p class="ds-lib-welcome">{opening}</p>
|
|
204
|
+
{#if examples.length > 0}
|
|
205
|
+
<ul class="ds-lib-chips">
|
|
206
|
+
{#each examples.slice(0, 3) as example (example)}
|
|
207
|
+
<li>
|
|
208
|
+
<button type="button" class="ds-lib-chip" onclick={() => onexample?.(example)}>
|
|
209
|
+
{example}
|
|
210
|
+
</button>
|
|
211
|
+
</li>
|
|
212
|
+
{/each}
|
|
213
|
+
</ul>
|
|
214
|
+
{/if}
|
|
215
|
+
</div>
|
|
216
|
+
{/if}
|
|
217
|
+
|
|
218
|
+
<!-- Above the first turn, and in ONE place in the markup: the empty
|
|
219
|
+
state carries the welcome, this carries the boundary, and both
|
|
220
|
+
sit at the top of the same column so a reader meets them in
|
|
221
|
+
the same order whether or not they have asked anything yet. -->
|
|
222
|
+
{#if scope}
|
|
223
|
+
<ScopeStatement statement={scope} expanded={turns.length === 0} copy={words} />
|
|
224
|
+
{/if}
|
|
225
|
+
|
|
226
|
+
{#each turns as turn, index (turn.id)}
|
|
227
|
+
<AgentTranscript
|
|
228
|
+
question={turn.question}
|
|
229
|
+
blocks={turn.blocks}
|
|
230
|
+
outcome={turn.outcome}
|
|
231
|
+
running={running && index === turns.length - 1}
|
|
232
|
+
citations={turn.citations ?? []}
|
|
233
|
+
suggestions={turn.suggestions ?? []}
|
|
234
|
+
{collectionNames}
|
|
235
|
+
copy={words}
|
|
236
|
+
name={who}
|
|
237
|
+
at={turn.at ?? stamps[turn.id]}
|
|
238
|
+
kind={turn.kind}
|
|
239
|
+
title={turn.title}
|
|
240
|
+
onopenartefact={() => openArtefact(turn)}
|
|
241
|
+
oncite={cite}
|
|
242
|
+
onregenerate={index === turns.length - 1 && !running ? onregenerate : undefined}
|
|
243
|
+
onsuggest={index === turns.length - 1 && !running ? onsuggest : undefined}
|
|
244
|
+
/>
|
|
245
|
+
{/each}
|
|
246
|
+
</div>
|
|
214
247
|
</div>
|
|
215
|
-
</div>
|
|
216
248
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
<ArrowDownIcon class="size-3.5" />
|
|
224
|
-
{words.jumpToLatest}
|
|
225
|
-
</button>
|
|
226
|
-
{/if}
|
|
249
|
+
{#if !follow.following && turns.length > 0}
|
|
250
|
+
<button type="button" class="ds-lib-jump" onclick={jump}>
|
|
251
|
+
<ArrowDownIcon size={14} />
|
|
252
|
+
{words.jumpToLatest}
|
|
253
|
+
</button>
|
|
254
|
+
{/if}
|
|
227
255
|
</div>
|
|
228
256
|
|
|
229
257
|
{#if composer}
|
|
230
|
-
|
|
231
|
-
|
|
258
|
+
<!-- Lifted off the scroll area, and on the app's own background rather
|
|
259
|
+
than the transcript's: prose scrolling up to the exact edge of the
|
|
260
|
+
input is the one place a reader loses track of which is which. -->
|
|
261
|
+
<div class="ds-lib-composer">
|
|
262
|
+
<div class="ds-lib-column ds-lib-column-flush">{@render composer()}</div>
|
|
232
263
|
</div>
|
|
233
264
|
{/if}
|
|
234
|
-
|
|
235
265
|
</div>
|
|
236
266
|
|
|
237
267
|
{#if column?.kind === 'document' && loadDocument}
|
|
238
|
-
<DocumentPane
|
|
268
|
+
<DocumentPane
|
|
269
|
+
citation={column.citation}
|
|
270
|
+
{loadDocument}
|
|
271
|
+
copy={words}
|
|
272
|
+
onclose={() => (column = null)}
|
|
273
|
+
/>
|
|
239
274
|
{:else if column?.kind === 'artefact'}
|
|
240
275
|
<ArtefactPane
|
|
241
276
|
turn={column.turn}
|
|
242
|
-
{
|
|
277
|
+
copy={words}
|
|
243
278
|
onclose={() => (column = null)}
|
|
244
279
|
oncite={(citation) => cite(citation)}
|
|
245
280
|
/>
|
|
246
281
|
{/if}
|
|
247
282
|
</div>
|
|
283
|
+
|
|
284
|
+
<style>
|
|
285
|
+
.ds-lib-surface {
|
|
286
|
+
display: flex;
|
|
287
|
+
min-height: 0;
|
|
288
|
+
flex: 1;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.ds-lib-main {
|
|
292
|
+
display: flex;
|
|
293
|
+
min-width: 0;
|
|
294
|
+
min-height: 0;
|
|
295
|
+
flex: 1;
|
|
296
|
+
flex-direction: column;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.ds-lib-scroll-region {
|
|
300
|
+
position: relative;
|
|
301
|
+
display: flex;
|
|
302
|
+
min-height: 0;
|
|
303
|
+
flex: 1;
|
|
304
|
+
flex-direction: column;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.ds-lib-scroll {
|
|
308
|
+
min-height: 0;
|
|
309
|
+
flex: 1;
|
|
310
|
+
overflow-y: auto;
|
|
311
|
+
overscroll-behavior: contain;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.ds-lib-column {
|
|
315
|
+
display: flex;
|
|
316
|
+
width: 100%;
|
|
317
|
+
max-width: var(--ds-lib-measure, 46rem);
|
|
318
|
+
flex-direction: column;
|
|
319
|
+
gap: 1.5rem;
|
|
320
|
+
margin-inline: auto;
|
|
321
|
+
padding: 1.5rem 1rem;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.ds-lib-column-flush {
|
|
325
|
+
gap: 0;
|
|
326
|
+
padding: 0;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.ds-lib-empty {
|
|
330
|
+
display: flex;
|
|
331
|
+
flex-direction: column;
|
|
332
|
+
align-items: flex-start;
|
|
333
|
+
gap: 1rem;
|
|
334
|
+
padding-block: 2rem;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
.ds-lib-welcome {
|
|
338
|
+
max-width: 46ch;
|
|
339
|
+
margin: 0;
|
|
340
|
+
color: var(--ds-color-foreground);
|
|
341
|
+
font-size: 1.125rem;
|
|
342
|
+
line-height: 1.375;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.ds-lib-chips {
|
|
346
|
+
display: flex;
|
|
347
|
+
flex-direction: column;
|
|
348
|
+
align-items: flex-start;
|
|
349
|
+
gap: 0.5rem;
|
|
350
|
+
margin: 0;
|
|
351
|
+
padding: 0;
|
|
352
|
+
list-style: none;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.ds-lib-chips li {
|
|
356
|
+
max-width: 100%;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
.ds-lib-chip {
|
|
360
|
+
max-width: 100%;
|
|
361
|
+
border: 1px solid var(--ds-color-border);
|
|
362
|
+
border-radius: var(--ds-radius-full);
|
|
363
|
+
background: none;
|
|
364
|
+
padding: 0.5rem 0.875rem;
|
|
365
|
+
color: var(--ds-color-foreground);
|
|
366
|
+
font: inherit;
|
|
367
|
+
font-size: 0.875rem;
|
|
368
|
+
text-align: start;
|
|
369
|
+
cursor: pointer;
|
|
370
|
+
transition:
|
|
371
|
+
background-color 150ms ease,
|
|
372
|
+
border-color 150ms ease;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
.ds-lib-chip:hover {
|
|
376
|
+
border-color: var(--ds-color-border-strong);
|
|
377
|
+
background: var(--ds-color-surface-2);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
.ds-lib-chip:focus-visible {
|
|
381
|
+
outline: 2px solid var(--ds-color-ring);
|
|
382
|
+
outline-offset: 2px;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.ds-lib-jump {
|
|
386
|
+
position: absolute;
|
|
387
|
+
bottom: 0.75rem;
|
|
388
|
+
left: 50%;
|
|
389
|
+
display: flex;
|
|
390
|
+
align-items: center;
|
|
391
|
+
gap: 0.375rem;
|
|
392
|
+
transform: translateX(-50%);
|
|
393
|
+
border: 1px solid var(--ds-color-border);
|
|
394
|
+
border-radius: var(--ds-radius-full);
|
|
395
|
+
background: var(--ds-color-surface-1);
|
|
396
|
+
padding: 0.375rem 0.75rem;
|
|
397
|
+
color: var(--ds-color-foreground);
|
|
398
|
+
font: inherit;
|
|
399
|
+
font-size: var(--ds-text-2xs);
|
|
400
|
+
cursor: pointer;
|
|
401
|
+
box-shadow: 0 8px 20px rgb(0 0 0 / 0.18);
|
|
402
|
+
transition: background-color 150ms ease;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
.ds-lib-jump:hover {
|
|
406
|
+
background: var(--ds-color-surface-2);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
.ds-lib-jump:focus-visible {
|
|
410
|
+
outline: 2px solid var(--ds-color-ring);
|
|
411
|
+
outline-offset: 2px;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
.ds-lib-composer {
|
|
415
|
+
flex: none;
|
|
416
|
+
border-top: 1px solid var(--ds-color-border);
|
|
417
|
+
background: var(--ds-color-background);
|
|
418
|
+
padding: 0.75rem 1rem calc(0.75rem + env(safe-area-inset-bottom, 0px));
|
|
419
|
+
}
|
|
420
|
+
</style>
|
|
@@ -8,6 +8,13 @@ interface Props {
|
|
|
8
8
|
/** `Transcript.version` — bumped per event. Text grows in place, so a
|
|
9
9
|
* count of turns is not enough to keep the scroll following. */
|
|
10
10
|
version?: number;
|
|
11
|
+
/** Who is speaking. A slug is fine — `penny` renders as "Penny" — and
|
|
12
|
+
* every word this package says is composed from it: the working line,
|
|
13
|
+
* the failure sentences, the scope label, the composer's placeholder
|
|
14
|
+
* and the accessible name of the scroll region. Absent is the library's
|
|
15
|
+
* own Milton, which is a default rather than a hardcoding. */
|
|
16
|
+
name?: string;
|
|
17
|
+
/** The empty state's opening line. Absent takes the persona's own. */
|
|
11
18
|
welcome?: string;
|
|
12
19
|
/** Up to three, shown in the empty state as tappable pills. */
|
|
13
20
|
examples?: string[];
|