@poodle64/librarian 2026.10.1 → 2026.10.2
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 +13 -10
- package/dist/components/agent-transcript/agent-transcript.svelte +20 -36
- package/dist/components/answer-mark/answer-mark.svelte +1 -1
- package/dist/components/answer-share/answer-share.svelte +1 -1
- package/dist/components/composer/choices.d.ts +12 -0
- package/dist/components/composer/choices.js +8 -0
- package/dist/components/composer/composer.svelte +49 -95
- package/dist/components/composer/composer.svelte.d.ts +3 -10
- package/dist/components/composer/index.d.ts +1 -1
- package/dist/components/composer/scope-picker.svelte +418 -0
- package/dist/components/composer/scope-picker.svelte.d.ts +14 -0
- package/dist/components/conversation/conversation.svelte +220 -99
- package/dist/components/conversation/conversation.svelte.d.ts +13 -3
- package/dist/components/conversation-list/conversation-list.svelte +22 -4
- package/dist/components/conversation-list/conversation-list.svelte.d.ts +2 -0
- package/dist/components/source-list/source-list.svelte +9 -8
- package/dist/copy.d.ts +6 -1
- package/dist/copy.js +3 -0
- package/package.json +1 -1
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
The whole reading surface: every turn, the scroll that follows the stream,
|
|
3
3
|
and the source pane beside it.
|
|
4
4
|
|
|
5
|
+
Before the first question it reads as ChatGPT's and Claude's empty chat: the
|
|
6
|
+
opening and the composer together in the middle of the panel, with nothing
|
|
7
|
+
else competing. The first question docks the composer at the foot and the
|
|
8
|
+
transcript scrolls above it. The composer is one element in both places, so
|
|
9
|
+
a draft or an attachment survives the move.
|
|
10
|
+
|
|
5
11
|
This owns the SCROLL CONTAINER, which is the reason it exists rather than the
|
|
6
12
|
host composing `AgentTranscript` in a div of its own. Follow-scroll, the
|
|
7
13
|
jump-to-latest pill and a document pane that narrows the transcript instead
|
|
@@ -60,6 +66,10 @@
|
|
|
60
66
|
/** When the last answer began, epoch ms, where this page did not see it
|
|
61
67
|
* start (`Chat.answering`): it reads as still being written. */
|
|
62
68
|
answering?: number | null;
|
|
69
|
+
/** A conversation is being read back and has no turns yet: the surface
|
|
70
|
+
* waits docked and says nothing, rather than centring a new
|
|
71
|
+
* conversation's opening for a moment and snapping away from it. */
|
|
72
|
+
reopening?: boolean;
|
|
63
73
|
/** Overrides for the package's own words. */
|
|
64
74
|
copy?: Partial<ChatCopy>;
|
|
65
75
|
/** Enables the source pane. Without it, chips render but do not open. */
|
|
@@ -91,10 +101,16 @@
|
|
|
91
101
|
* prose, its citation marks, its links) renders its own, and one that
|
|
92
102
|
* only adds to it renders `AgentTranscript` inside it. */
|
|
93
103
|
turn?: Snippet<[AgentTranscriptProps]>;
|
|
94
|
-
/**
|
|
95
|
-
* the scope
|
|
96
|
-
*
|
|
104
|
+
/** What leads the conversation: inside the scroll, under the opening and
|
|
105
|
+
* the scope, so a tall one scrolls rather than taking the panel's
|
|
106
|
+
* height; before the first question, under the centred composer, its
|
|
107
|
+
* examples and the scope. */
|
|
97
108
|
lead?: Snippet;
|
|
109
|
+
/** The host's controls for the conversation as a whole (its past
|
|
110
|
+
* questions, a new one), at the measure's trailing edge above the
|
|
111
|
+
* transcript. It stays put while the transcript scrolls and narrows
|
|
112
|
+
* with it when a pane opens. */
|
|
113
|
+
toolbar?: Snippet;
|
|
98
114
|
}
|
|
99
115
|
|
|
100
116
|
let {
|
|
@@ -110,6 +126,7 @@
|
|
|
110
126
|
onmark,
|
|
111
127
|
onshare,
|
|
112
128
|
answering = null,
|
|
129
|
+
reopening = false,
|
|
113
130
|
copy,
|
|
114
131
|
loadDocument,
|
|
115
132
|
oncite,
|
|
@@ -120,6 +137,7 @@
|
|
|
120
137
|
composer,
|
|
121
138
|
turn: presentTurn,
|
|
122
139
|
lead,
|
|
140
|
+
toolbar,
|
|
123
141
|
formatCost
|
|
124
142
|
}: Props = $props();
|
|
125
143
|
|
|
@@ -129,6 +147,10 @@
|
|
|
129
147
|
const words = $derived(resolveCopy(copy, name));
|
|
130
148
|
const who = $derived(personaName(name));
|
|
131
149
|
const opening = $derived(welcome ?? words.welcome);
|
|
150
|
+
// A turn on its way (a reopened conversation, a job's first) is not a new
|
|
151
|
+
// conversation, so neither waits centred.
|
|
152
|
+
const empty = $derived(turns.length === 0 && !reopening);
|
|
153
|
+
const centred = $derived(empty && !running && Boolean(composer));
|
|
132
154
|
|
|
133
155
|
/**
|
|
134
156
|
* When each turn arrived, for the turns this surface actually watched
|
|
@@ -247,105 +269,113 @@
|
|
|
247
269
|
</script>
|
|
248
270
|
|
|
249
271
|
<div class="ds-lib-surface">
|
|
250
|
-
<div class="ds-lib-main">
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
272
|
+
<div class="ds-lib-main" class:is-centred={centred}>
|
|
273
|
+
{#if toolbar}
|
|
274
|
+
<div class="ds-lib-toolbar">
|
|
275
|
+
<div class="ds-lib-column ds-lib-column-flush ds-lib-toolbar-row">{@render toolbar()}</div>
|
|
276
|
+
</div>
|
|
277
|
+
{/if}
|
|
278
|
+
|
|
279
|
+
<div class="ds-lib-stage">
|
|
280
|
+
<!-- The pill is positioned against the SCROLL region, not the column: the
|
|
281
|
+
composer is in the column too, and a pill over the input is a pill in
|
|
282
|
+
the way. -->
|
|
283
|
+
<div class="ds-lib-scroll-region">
|
|
284
|
+
<div
|
|
285
|
+
bind:this={viewport}
|
|
286
|
+
onscroll={scrolled}
|
|
287
|
+
role="log"
|
|
288
|
+
aria-live="polite"
|
|
289
|
+
aria-busy={running}
|
|
290
|
+
aria-label={words.conversationLabel}
|
|
291
|
+
class="ds-lib-scroll"
|
|
292
|
+
>
|
|
293
|
+
<!-- The measure, in this package's own stylesheet.
|
|
294
|
+
`--ds-lib-measure` is the one knob a host may turn, and it is a
|
|
295
|
+
token rather than a class precisely because a class here is a
|
|
296
|
+
string a consuming app's Tailwind has to find in `dist` — the
|
|
297
|
+
arrangement that silently produced `max-width: none` and a
|
|
298
|
+
2302px line in Pebblestone's production build. -->
|
|
299
|
+
<div class="ds-lib-column">
|
|
300
|
+
{#if empty}
|
|
301
|
+
<div class="ds-lib-empty">
|
|
302
|
+
<p class="ds-lib-welcome">{opening}</p>
|
|
303
|
+
{#if !centred}{@render suggestions()}{/if}
|
|
304
|
+
</div>
|
|
305
|
+
{/if}
|
|
306
|
+
|
|
307
|
+
<!-- Above the first turn, and in ONE place in the markup: the empty
|
|
308
|
+
state carries the welcome, this carries the boundary, and both
|
|
309
|
+
sit at the top of the same column so a reader meets them in
|
|
310
|
+
the same order whether or not they have asked anything yet.
|
|
311
|
+
Centred, both sit under the composer instead. -->
|
|
312
|
+
{#if !centred}
|
|
313
|
+
{#if scope}
|
|
314
|
+
<ScopeStatement statement={scope} expanded={empty} copy={words} />
|
|
284
315
|
{/if}
|
|
285
|
-
|
|
286
|
-
{/if}
|
|
287
|
-
|
|
288
|
-
<!-- Above the first turn, and in ONE place in the markup: the empty
|
|
289
|
-
state carries the welcome, this carries the boundary, and both
|
|
290
|
-
sit at the top of the same column so a reader meets them in
|
|
291
|
-
the same order whether or not they have asked anything yet. -->
|
|
292
|
-
{#if scope}
|
|
293
|
-
<ScopeStatement statement={scope} expanded={turns.length === 0} copy={words} />
|
|
294
|
-
{/if}
|
|
295
|
-
|
|
296
|
-
{@render lead?.()}
|
|
297
|
-
|
|
298
|
-
{#each turns as turn, index (turn.id)}
|
|
299
|
-
{@const last = index === turns.length - 1}
|
|
300
|
-
{@const props = {
|
|
301
|
-
question: turn.question,
|
|
302
|
-
blocks: turn.blocks,
|
|
303
|
-
outcome: turn.outcome,
|
|
304
|
-
running: running && last,
|
|
305
|
-
answering: last ? (answering ?? undefined) : undefined,
|
|
306
|
-
citations: turn.citations ?? [],
|
|
307
|
-
collectionNames,
|
|
308
|
-
copy: words,
|
|
309
|
-
name: who,
|
|
310
|
-
at: turn.at ?? stamps[turn.id],
|
|
311
|
-
artefact: turn.artefact,
|
|
312
|
-
artefactOpen: isShowing(turn),
|
|
313
|
-
onopenartefact: opener(turn),
|
|
314
|
-
describeTool,
|
|
315
|
-
formatCost,
|
|
316
|
-
oncite: oncite || loadDocument ? cite : undefined,
|
|
317
|
-
onregenerate: last && !running ? onregenerate : undefined,
|
|
318
|
-
onmark:
|
|
319
|
-
last && !running && onmark ? (verdict: Verdict) => onmark(turn, verdict) : undefined,
|
|
320
|
-
shared: turn.shared,
|
|
321
|
-
onshare:
|
|
322
|
-
onshare && !(last && running) ? (share: boolean) => onshare(turn, share) : undefined
|
|
323
|
-
} satisfies AgentTranscriptProps}
|
|
324
|
-
{#if presentTurn}
|
|
325
|
-
{@render presentTurn(props)}
|
|
326
|
-
{:else}
|
|
327
|
-
<AgentTranscript {...props} />
|
|
316
|
+
{@render lead?.()}
|
|
328
317
|
{/if}
|
|
329
|
-
|
|
318
|
+
|
|
319
|
+
{#each turns as turn, index (turn.id)}
|
|
320
|
+
{@const last = index === turns.length - 1}
|
|
321
|
+
{@const props = {
|
|
322
|
+
question: turn.question,
|
|
323
|
+
blocks: turn.blocks,
|
|
324
|
+
outcome: turn.outcome,
|
|
325
|
+
running: running && last,
|
|
326
|
+
answering: last ? (answering ?? undefined) : undefined,
|
|
327
|
+
citations: turn.citations ?? [],
|
|
328
|
+
collectionNames,
|
|
329
|
+
copy: words,
|
|
330
|
+
name: who,
|
|
331
|
+
at: turn.at ?? stamps[turn.id],
|
|
332
|
+
artefact: turn.artefact,
|
|
333
|
+
artefactOpen: isShowing(turn),
|
|
334
|
+
onopenartefact: opener(turn),
|
|
335
|
+
describeTool,
|
|
336
|
+
formatCost,
|
|
337
|
+
oncite: oncite || loadDocument ? cite : undefined,
|
|
338
|
+
onregenerate: last && !running ? onregenerate : undefined,
|
|
339
|
+
onmark:
|
|
340
|
+
last && !running && onmark ? (verdict: Verdict) => onmark(turn, verdict) : undefined,
|
|
341
|
+
shared: turn.shared,
|
|
342
|
+
onshare:
|
|
343
|
+
onshare && !(last && running) ? (share: boolean) => onshare(turn, share) : undefined
|
|
344
|
+
} satisfies AgentTranscriptProps}
|
|
345
|
+
{#if presentTurn}
|
|
346
|
+
{@render presentTurn(props)}
|
|
347
|
+
{:else}
|
|
348
|
+
<AgentTranscript {...props} />
|
|
349
|
+
{/if}
|
|
350
|
+
{/each}
|
|
351
|
+
</div>
|
|
330
352
|
</div>
|
|
353
|
+
|
|
354
|
+
{#if !follow.following && turns.length > 0}
|
|
355
|
+
<button type="button" class="ds-lib-jump" onclick={jump}>
|
|
356
|
+
<ArrowDownIcon size={14} />
|
|
357
|
+
{words.jumpToLatest}
|
|
358
|
+
</button>
|
|
359
|
+
{/if}
|
|
331
360
|
</div>
|
|
332
361
|
|
|
333
|
-
{#if
|
|
334
|
-
<
|
|
335
|
-
<
|
|
336
|
-
|
|
337
|
-
|
|
362
|
+
{#if composer}
|
|
363
|
+
<div class="ds-lib-composer">
|
|
364
|
+
<div class="ds-lib-column ds-lib-column-flush">
|
|
365
|
+
{@render composer()}
|
|
366
|
+
{#if centred}
|
|
367
|
+
<div class="ds-lib-after">
|
|
368
|
+
{@render suggestions()}
|
|
369
|
+
{#if scope}
|
|
370
|
+
<ScopeStatement statement={scope} expanded={false} copy={words} />
|
|
371
|
+
{/if}
|
|
372
|
+
{@render lead?.()}
|
|
373
|
+
</div>
|
|
374
|
+
{/if}
|
|
375
|
+
</div>
|
|
376
|
+
</div>
|
|
338
377
|
{/if}
|
|
339
378
|
</div>
|
|
340
|
-
|
|
341
|
-
{#if composer}
|
|
342
|
-
<!-- Lifted off the scroll area, and on the app's own background rather
|
|
343
|
-
than the transcript's: prose scrolling up to the exact edge of the
|
|
344
|
-
input is the one place a reader loses track of which is which. -->
|
|
345
|
-
<div class="ds-lib-composer">
|
|
346
|
-
<div class="ds-lib-column ds-lib-column-flush">{@render composer()}</div>
|
|
347
|
-
</div>
|
|
348
|
-
{/if}
|
|
349
379
|
</div>
|
|
350
380
|
|
|
351
381
|
{#if column?.kind === 'document' && loadDocument}
|
|
@@ -365,6 +395,20 @@
|
|
|
365
395
|
{/if}
|
|
366
396
|
</div>
|
|
367
397
|
|
|
398
|
+
{#snippet suggestions()}
|
|
399
|
+
{#if examples.length > 0}
|
|
400
|
+
<ul class="ds-lib-chips">
|
|
401
|
+
{#each examples.slice(0, 3) as example (example)}
|
|
402
|
+
<li>
|
|
403
|
+
<button type="button" class="ds-lib-chip" onclick={() => onexample?.(example)}>
|
|
404
|
+
{example}
|
|
405
|
+
</button>
|
|
406
|
+
</li>
|
|
407
|
+
{/each}
|
|
408
|
+
</ul>
|
|
409
|
+
{/if}
|
|
410
|
+
{/snippet}
|
|
411
|
+
|
|
368
412
|
<style>
|
|
369
413
|
/* `min-width: 0` for a host that sets this in a row beside columns of its
|
|
370
414
|
own: a flex item's automatic minimum is its widest content, and a table
|
|
@@ -384,6 +428,7 @@
|
|
|
384
428
|
flex-direction: column;
|
|
385
429
|
}
|
|
386
430
|
|
|
431
|
+
.ds-lib-stage,
|
|
387
432
|
.ds-lib-scroll-region {
|
|
388
433
|
position: relative;
|
|
389
434
|
display: flex;
|
|
@@ -392,19 +437,31 @@
|
|
|
392
437
|
flex-direction: column;
|
|
393
438
|
}
|
|
394
439
|
|
|
440
|
+
/* The transcript fades at its edges, under the toolbar and into the
|
|
441
|
+
composer, rather than stopping at a rule across the panel: the box is
|
|
442
|
+
edge enough, and a fade lies over any ground the app paints, a texture
|
|
443
|
+
included. Each fade is the column's own padding at either end. */
|
|
395
444
|
.ds-lib-scroll {
|
|
396
445
|
min-height: 0;
|
|
397
446
|
flex: 1;
|
|
398
447
|
overflow-y: auto;
|
|
399
448
|
overscroll-behavior: contain;
|
|
449
|
+
mask-image: linear-gradient(
|
|
450
|
+
to bottom,
|
|
451
|
+
transparent,
|
|
452
|
+
#000 1.25rem,
|
|
453
|
+
#000 calc(100% - 1.5rem),
|
|
454
|
+
transparent
|
|
455
|
+
);
|
|
400
456
|
}
|
|
401
457
|
|
|
458
|
+
/* Turns are told apart by space and type, never boxes. */
|
|
402
459
|
.ds-lib-column {
|
|
403
460
|
display: flex;
|
|
404
461
|
width: 100%;
|
|
405
462
|
max-width: var(--ds-lib-measure, 46rem);
|
|
406
463
|
flex-direction: column;
|
|
407
|
-
gap:
|
|
464
|
+
gap: 2.5rem;
|
|
408
465
|
margin-inline: auto;
|
|
409
466
|
padding: 1.5rem 1rem;
|
|
410
467
|
}
|
|
@@ -414,6 +471,72 @@
|
|
|
414
471
|
padding: 0;
|
|
415
472
|
}
|
|
416
473
|
|
|
474
|
+
.ds-lib-toolbar {
|
|
475
|
+
flex: none;
|
|
476
|
+
padding: 0.5rem 1rem 0;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
.ds-lib-toolbar-row {
|
|
480
|
+
flex-direction: row;
|
|
481
|
+
flex-wrap: wrap;
|
|
482
|
+
justify-content: flex-end;
|
|
483
|
+
gap: 0.25rem;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/* Centred: the stage scrolls as one, and the opening and the composer meet
|
|
487
|
+
in its middle. Auto margins centre them while they fit and let them
|
|
488
|
+
scroll from the top once a tall lead does not, where
|
|
489
|
+
`justify-content: center` would cut the top off. */
|
|
490
|
+
.is-centred .ds-lib-stage {
|
|
491
|
+
overflow-y: auto;
|
|
492
|
+
overscroll-behavior: contain;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
.is-centred .ds-lib-scroll-region {
|
|
496
|
+
flex: none;
|
|
497
|
+
margin-top: auto;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
.is-centred .ds-lib-scroll {
|
|
501
|
+
flex: none;
|
|
502
|
+
overflow: visible;
|
|
503
|
+
mask-image: none;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
.is-centred .ds-lib-scroll .ds-lib-column {
|
|
507
|
+
padding-bottom: 1.25rem;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
.is-centred .ds-lib-composer {
|
|
511
|
+
margin-bottom: auto;
|
|
512
|
+
padding-top: 0;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
.is-centred .ds-lib-empty {
|
|
516
|
+
align-items: center;
|
|
517
|
+
padding-block: 0;
|
|
518
|
+
text-align: center;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
.is-centred .ds-lib-welcome {
|
|
522
|
+
font-family: var(--ds-font-display, inherit);
|
|
523
|
+
font-size: 1.5rem;
|
|
524
|
+
line-height: 1.3;
|
|
525
|
+
text-wrap: balance;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
.ds-lib-after {
|
|
529
|
+
display: flex;
|
|
530
|
+
flex-direction: column;
|
|
531
|
+
gap: 1.5rem;
|
|
532
|
+
padding-top: 1rem;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
.ds-lib-after .ds-lib-chips {
|
|
536
|
+
flex-flow: row wrap;
|
|
537
|
+
justify-content: center;
|
|
538
|
+
}
|
|
539
|
+
|
|
417
540
|
.ds-lib-empty {
|
|
418
541
|
display: flex;
|
|
419
542
|
flex-direction: column;
|
|
@@ -501,8 +624,6 @@
|
|
|
501
624
|
|
|
502
625
|
.ds-lib-composer {
|
|
503
626
|
flex: none;
|
|
504
|
-
|
|
505
|
-
background: var(--ds-color-background);
|
|
506
|
-
padding: 0.75rem 1rem calc(0.75rem + env(safe-area-inset-bottom, 0px));
|
|
627
|
+
padding: 0.25rem 1rem calc(0.75rem + env(safe-area-inset-bottom, 0px));
|
|
507
628
|
}
|
|
508
629
|
</style>
|
|
@@ -36,6 +36,10 @@ interface Props {
|
|
|
36
36
|
/** When the last answer began, epoch ms, where this page did not see it
|
|
37
37
|
* start (`Chat.answering`): it reads as still being written. */
|
|
38
38
|
answering?: number | null;
|
|
39
|
+
/** A conversation is being read back and has no turns yet: the surface
|
|
40
|
+
* waits docked and says nothing, rather than centring a new
|
|
41
|
+
* conversation's opening for a moment and snapping away from it. */
|
|
42
|
+
reopening?: boolean;
|
|
39
43
|
/** Overrides for the package's own words. */
|
|
40
44
|
copy?: Partial<ChatCopy>;
|
|
41
45
|
/** Enables the source pane. Without it, chips render but do not open. */
|
|
@@ -67,10 +71,16 @@ interface Props {
|
|
|
67
71
|
* prose, its citation marks, its links) renders its own, and one that
|
|
68
72
|
* only adds to it renders `AgentTranscript` inside it. */
|
|
69
73
|
turn?: Snippet<[AgentTranscriptProps]>;
|
|
70
|
-
/**
|
|
71
|
-
* the scope
|
|
72
|
-
*
|
|
74
|
+
/** What leads the conversation: inside the scroll, under the opening and
|
|
75
|
+
* the scope, so a tall one scrolls rather than taking the panel's
|
|
76
|
+
* height; before the first question, under the centred composer, its
|
|
77
|
+
* examples and the scope. */
|
|
73
78
|
lead?: Snippet;
|
|
79
|
+
/** The host's controls for the conversation as a whole (its past
|
|
80
|
+
* questions, a new one), at the measure's trailing edge above the
|
|
81
|
+
* transcript. It stays put while the transcript scrolls and narrows
|
|
82
|
+
* with it when a pane opens. */
|
|
83
|
+
toolbar?: Snippet;
|
|
74
84
|
}
|
|
75
85
|
declare const Conversation: import("svelte").Component<Props, {}, "">;
|
|
76
86
|
type Conversation = ReturnType<typeof Conversation>;
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
From cadmus's Past questions (cadmus#114). Presentation only: the host hands
|
|
6
6
|
it the list and the acts (`Chat`'s own), nothing here fetches, and where it
|
|
7
|
-
sits — a column, a sheet — is the host's.
|
|
7
|
+
sits — a column, a sheet — is the host's. A host that lays it over the page
|
|
8
|
+
passes `onclose`, and the header carries the way out beside New question.
|
|
8
9
|
|
|
9
10
|
Each row's acts open under it rather than in a floating menu, and a delete
|
|
10
11
|
asks under the row too: nothing to position, nothing to trap focus in, and
|
|
@@ -17,6 +18,7 @@
|
|
|
17
18
|
import PencilIcon from '@lucide/svelte/icons/pencil';
|
|
18
19
|
import PlusIcon from '@lucide/svelte/icons/plus';
|
|
19
20
|
import TrashIcon from '@lucide/svelte/icons/trash-2';
|
|
21
|
+
import XIcon from '@lucide/svelte/icons/x';
|
|
20
22
|
import type { ConversationSummary } from '../../chat.svelte';
|
|
21
23
|
import { DEFAULT_PERSONA, resolveCopy, type ChatCopy } from '../../copy';
|
|
22
24
|
|
|
@@ -33,6 +35,8 @@
|
|
|
33
35
|
/** A row chosen; with `href`, as well as following it. */
|
|
34
36
|
onopen?: (id: string) => void;
|
|
35
37
|
onnew: () => void;
|
|
38
|
+
/** Closes the list, for a host that lays it over the page. */
|
|
39
|
+
onclose?: () => void;
|
|
36
40
|
/** Each act offered only when given; each resolves true once done. */
|
|
37
41
|
onrename?: (id: string, title: string) => Promise<boolean>;
|
|
38
42
|
ondownload?: (id: string) => Promise<boolean>;
|
|
@@ -49,6 +53,7 @@
|
|
|
49
53
|
href,
|
|
50
54
|
onopen,
|
|
51
55
|
onnew,
|
|
56
|
+
onclose,
|
|
52
57
|
onrename,
|
|
53
58
|
ondownload,
|
|
54
59
|
ondelete,
|
|
@@ -122,6 +127,16 @@
|
|
|
122
127
|
<PlusIcon size={14} />
|
|
123
128
|
<span>{words.newQuestion}</span>
|
|
124
129
|
</button>
|
|
130
|
+
{#if onclose}
|
|
131
|
+
<button
|
|
132
|
+
type="button"
|
|
133
|
+
class="ds-lib-list-close"
|
|
134
|
+
onclick={onclose}
|
|
135
|
+
aria-label={words.closePastQuestions}
|
|
136
|
+
>
|
|
137
|
+
<XIcon size={16} />
|
|
138
|
+
</button>
|
|
139
|
+
{/if}
|
|
125
140
|
</header>
|
|
126
141
|
|
|
127
142
|
{#if failed}
|
|
@@ -405,7 +420,8 @@
|
|
|
405
420
|
animation: ds-lib-list-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
|
406
421
|
}
|
|
407
422
|
|
|
408
|
-
.ds-lib-list-more
|
|
423
|
+
.ds-lib-list-more,
|
|
424
|
+
.ds-lib-list-close {
|
|
409
425
|
display: grid;
|
|
410
426
|
width: 2.75rem;
|
|
411
427
|
height: 2.75rem;
|
|
@@ -419,7 +435,8 @@
|
|
|
419
435
|
}
|
|
420
436
|
|
|
421
437
|
.ds-lib-list-more:hover,
|
|
422
|
-
.ds-lib-list-more[aria-expanded='true']
|
|
438
|
+
.ds-lib-list-more[aria-expanded='true'],
|
|
439
|
+
.ds-lib-list-close:hover {
|
|
423
440
|
color: var(--ds-color-foreground);
|
|
424
441
|
}
|
|
425
442
|
|
|
@@ -468,7 +485,8 @@
|
|
|
468
485
|
.ds-lib-list-new:focus-visible,
|
|
469
486
|
.ds-lib-list-act:focus-visible,
|
|
470
487
|
.ds-lib-list-open:focus-visible,
|
|
471
|
-
.ds-lib-list-more:focus-visible
|
|
488
|
+
.ds-lib-list-more:focus-visible,
|
|
489
|
+
.ds-lib-list-close:focus-visible {
|
|
472
490
|
outline: 2px solid var(--ds-color-ring);
|
|
473
491
|
outline-offset: -2px;
|
|
474
492
|
}
|
|
@@ -13,6 +13,8 @@ interface Props {
|
|
|
13
13
|
/** A row chosen; with `href`, as well as following it. */
|
|
14
14
|
onopen?: (id: string) => void;
|
|
15
15
|
onnew: () => void;
|
|
16
|
+
/** Closes the list, for a host that lays it over the page. */
|
|
17
|
+
onclose?: () => void;
|
|
16
18
|
/** Each act offered only when given; each resolves true once done. */
|
|
17
19
|
onrename?: (id: string, title: string) => Promise<boolean>;
|
|
18
20
|
ondownload?: (id: string) => Promise<boolean>;
|
|
@@ -66,12 +66,15 @@
|
|
|
66
66
|
text-transform: uppercase;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/* Rows under one hairline, not a box each: a source list is read down,
|
|
70
|
+
and a stack of outlines is the noise the chat was asked to lose. */
|
|
69
71
|
.ds-lib-sources-list {
|
|
70
72
|
display: flex;
|
|
71
73
|
flex-direction: column;
|
|
72
|
-
gap: 0.
|
|
74
|
+
gap: 0.125rem;
|
|
73
75
|
margin: 0;
|
|
74
|
-
|
|
76
|
+
border-top: 1px solid var(--ds-color-border);
|
|
77
|
+
padding: 0.375rem 0 0;
|
|
75
78
|
list-style: none;
|
|
76
79
|
}
|
|
77
80
|
|
|
@@ -80,8 +83,8 @@
|
|
|
80
83
|
width: 100%;
|
|
81
84
|
align-items: baseline;
|
|
82
85
|
gap: 0.5rem;
|
|
83
|
-
border:
|
|
84
|
-
border-radius: var(--ds-radius-
|
|
86
|
+
border: 0;
|
|
87
|
+
border-radius: var(--ds-radius-md);
|
|
85
88
|
background: none;
|
|
86
89
|
padding: 0.375rem 0.625rem;
|
|
87
90
|
color: inherit;
|
|
@@ -89,13 +92,10 @@
|
|
|
89
92
|
font-size: 0.875rem;
|
|
90
93
|
text-align: start;
|
|
91
94
|
cursor: pointer;
|
|
92
|
-
transition:
|
|
93
|
-
background-color 150ms ease,
|
|
94
|
-
border-color 150ms ease;
|
|
95
|
+
transition: background-color 150ms ease;
|
|
95
96
|
}
|
|
96
97
|
|
|
97
98
|
.ds-lib-source:hover:not(:disabled) {
|
|
98
|
-
border-color: var(--ds-color-border-strong);
|
|
99
99
|
background: var(--ds-color-surface-2);
|
|
100
100
|
}
|
|
101
101
|
|
|
@@ -125,6 +125,7 @@
|
|
|
125
125
|
|
|
126
126
|
.ds-lib-source-title {
|
|
127
127
|
color: var(--ds-color-foreground);
|
|
128
|
+
font-weight: 500;
|
|
128
129
|
}
|
|
129
130
|
|
|
130
131
|
.ds-lib-source-section {
|
package/dist/copy.d.ts
CHANGED
|
@@ -46,6 +46,10 @@ export interface ChatCopy {
|
|
|
46
46
|
* shown by its own title, supplied by the app. */
|
|
47
47
|
collectionsLabel: string;
|
|
48
48
|
allCollections: string;
|
|
49
|
+
/** The picker's search field, from a long list up, and what it says
|
|
50
|
+
* when nothing matches. */
|
|
51
|
+
findCollection: string;
|
|
52
|
+
noCollectionMatch: string;
|
|
49
53
|
/** Past a few turns, the quiet line above the composer that offers to
|
|
50
54
|
* start over — a follow-up re-reads the whole conversation, so each one
|
|
51
55
|
* costs more than the last. */
|
|
@@ -88,9 +92,10 @@ export interface ChatCopy {
|
|
|
88
92
|
/** `{resets}` when the allowance comes back at the turn of the day. */
|
|
89
93
|
midnight: string;
|
|
90
94
|
support: string;
|
|
91
|
-
/** The conversation list. */
|
|
95
|
+
/** The conversation list, and closing it where the host lays it over the page. */
|
|
92
96
|
pastQuestions: string;
|
|
93
97
|
newQuestion: string;
|
|
98
|
+
closePastQuestions: string;
|
|
94
99
|
readingPastQuestions: string;
|
|
95
100
|
noPastQuestions: string;
|
|
96
101
|
pastQuestionsFailed: string;
|
package/dist/copy.js
CHANGED
|
@@ -66,6 +66,8 @@ export function copyFor(name = DEFAULT_PERSONA) {
|
|
|
66
66
|
answeringPlaceholder: `${who} is answering…`,
|
|
67
67
|
collectionsLabel: `Where ${who} looks`,
|
|
68
68
|
allCollections: 'All your collections',
|
|
69
|
+
findCollection: 'Search',
|
|
70
|
+
noCollectionMatch: 'Nothing matches that.',
|
|
69
71
|
longConversation: `This conversation is getting long. A new question keeps ${who} quick.`,
|
|
70
72
|
working: [`${who} is looking`, `${who} is reading`],
|
|
71
73
|
conversationLabel: `Conversation with ${who}`,
|
|
@@ -98,6 +100,7 @@ export function copyFor(name = DEFAULT_PERSONA) {
|
|
|
98
100
|
support: 'Support',
|
|
99
101
|
pastQuestions: 'Past questions',
|
|
100
102
|
newQuestion: 'New question',
|
|
103
|
+
closePastQuestions: 'Close past questions',
|
|
101
104
|
readingPastQuestions: 'Reading your past questions…',
|
|
102
105
|
noPastQuestions: 'No past questions yet.',
|
|
103
106
|
pastQuestionsFailed: "Your past questions couldn't be read just now.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poodle64/librarian",
|
|
3
|
-
"version": "2026.10.
|
|
3
|
+
"version": "2026.10.2",
|
|
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",
|