@poodle64/librarian 2026.9.18 → 2026.9.20
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 +89 -16
- package/dist/citations.d.ts +3 -1
- package/dist/client.d.ts +28 -1
- package/dist/client.js +29 -8
- package/dist/components/activity-group/activity-group.svelte +6 -1
- package/dist/components/agent-transcript/agent-transcript.svelte +39 -27
- package/dist/components/agent-transcript/agent-transcript.svelte.d.ts +9 -8
- package/dist/components/artefact-card/artefact-card.svelte +100 -39
- package/dist/components/artefact-card/artefact-card.svelte.d.ts +9 -3
- package/dist/components/artefact-pane/artefact-pane.svelte +5 -5
- package/dist/components/composer/composer.svelte +46 -17
- package/dist/components/composer/composer.svelte.d.ts +13 -2
- package/dist/components/conversation/conversation.svelte +46 -9
- package/dist/components/conversation/conversation.svelte.d.ts +14 -2
- package/dist/components/tool-row/tool-row.svelte +16 -9
- package/dist/components/tool-row/tool-row.svelte.d.ts +3 -1
- package/dist/copy.d.ts +3 -0
- package/dist/copy.js +3 -1
- package/dist/session.svelte.d.ts +31 -0
- package/dist/session.svelte.js +105 -0
- package/dist/transcript.svelte.d.ts +104 -17
- package/dist/transcript.svelte.js +209 -140
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -16,8 +16,9 @@ there.
|
|
|
16
16
|
|
|
17
17
|
```text
|
|
18
18
|
src/lib/
|
|
19
|
-
client.ts ask():
|
|
19
|
+
client.ts ask() and watch(): Claude Code's OWN events, unaltered
|
|
20
20
|
transcript.svelte.ts Transcript state, the fold/segment/describe helpers
|
|
21
|
+
session.svelte.ts Session: a whole stream of runs, as turns
|
|
21
22
|
citations.ts the Citation shape, `[n]` markers, the trust mark
|
|
22
23
|
copy.ts every word this package says, and the host's overrides
|
|
23
24
|
attachments.ts what a reader may attach, and the limits
|
|
@@ -28,9 +29,9 @@ src/lib/
|
|
|
28
29
|
scope-statement/ what this room answers from, and what it does not hold
|
|
29
30
|
agent-transcript/ one question and everything Milton did answering it
|
|
30
31
|
document-pane/ the cited document, open at the cited passage
|
|
31
|
-
artefact-card/
|
|
32
|
-
artefact-pane/
|
|
33
|
-
composer/ the input box: attachments, scope chips, send/stop
|
|
32
|
+
artefact-card/ an artefact's card, in the host's words
|
|
33
|
+
artefact-pane/ an artefact that is its answer's prose, in the column
|
|
34
|
+
composer/ the input box: attachments, scope chips, a note, send/stop
|
|
34
35
|
markdown/ sanitised, streaming-safe markdown + highlighting
|
|
35
36
|
activity-group/ a whole investigation, as one quiet line
|
|
36
37
|
tool-row/ one tool call
|
|
@@ -41,12 +42,12 @@ src/lib/
|
|
|
41
42
|
## Installation
|
|
42
43
|
|
|
43
44
|
```bash
|
|
44
|
-
pnpm add @poodle64/librarian @
|
|
45
|
+
pnpm add @poodle64/librarian @lucide/svelte
|
|
45
46
|
```
|
|
46
47
|
|
|
47
|
-
`svelte`, `@
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
`svelte`, `@lucide/svelte`, `marked`, `isomorphic-dompurify` and `shiki` are
|
|
49
|
+
peer dependencies: declare them yourself so Renovate tracks their versions and
|
|
50
|
+
`pnpm ls` shows them.
|
|
50
51
|
|
|
51
52
|
**No Tailwind content-scan line, and deliberately none.** These components
|
|
52
53
|
carry their own CSS, written against the `--ds-*` tokens and compiled by
|
|
@@ -65,9 +66,6 @@ What it does need is the token layer every app already imports:
|
|
|
65
66
|
@import '@poodle64/design-tokens/tokens.css';
|
|
66
67
|
```
|
|
67
68
|
|
|
68
|
-
The composed page chrome this package borrows from `@poodle64/ui` (`Panel`)
|
|
69
|
-
still follows that package's own `@source` line, which every app has.
|
|
70
|
-
|
|
71
69
|
One knob: `--ds-lib-measure` (default `46rem`) sets the transcript's reading
|
|
72
70
|
column. Set it on any ancestor.
|
|
73
71
|
|
|
@@ -153,8 +151,6 @@ gives it a height and a composer and nothing else:
|
|
|
153
151
|
bind:value={question}
|
|
154
152
|
bind:files
|
|
155
153
|
{running}
|
|
156
|
-
scope="library"
|
|
157
|
-
onscope={() => {}}
|
|
158
154
|
onsubmit={submit}
|
|
159
155
|
onstop={() => controller?.abort()}
|
|
160
156
|
/>
|
|
@@ -184,6 +180,80 @@ Nothing here fetches on its own behalf. The library's document read is
|
|
|
184
180
|
authenticated, and a package that called it directly would be reaching past
|
|
185
181
|
the app's proxy with a session it has no business holding.
|
|
186
182
|
|
|
183
|
+
### Watching a session somebody else started
|
|
184
|
+
|
|
185
|
+
A session the APP started — a persona reading a document with nobody asking —
|
|
186
|
+
is watched rather than asked. `watch()` GETs its stream; `Session` folds it
|
|
187
|
+
into turns: the prompt and every later message on the reader's side (the CLI
|
|
188
|
+
echoes each one back under `--replay-user-messages`), each run's answer on the
|
|
189
|
+
persona's, each settled by its own `result`. A stream without deltas is folded
|
|
190
|
+
from its whole messages. Events are deduped by `uuid`, so opening the watch
|
|
191
|
+
again after sending a message folds only what is new.
|
|
192
|
+
|
|
193
|
+
```svelte
|
|
194
|
+
<script lang="ts">
|
|
195
|
+
import { watch } from '@poodle64/librarian/client';
|
|
196
|
+
import { Session } from '@poodle64/librarian/session';
|
|
197
|
+
|
|
198
|
+
const session = new Session();
|
|
199
|
+
let watching = $state(false);
|
|
200
|
+
|
|
201
|
+
async function follow(signal: AbortSignal) {
|
|
202
|
+
watching = true;
|
|
203
|
+
try {
|
|
204
|
+
for await (const event of watch({ endpoint: `/api/agent/${persona}/jobs/${id}/watch`, signal }))
|
|
205
|
+
session.apply(event);
|
|
206
|
+
} finally {
|
|
207
|
+
watching = false;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// The host's own additions: what a run handed in, carded under its answer.
|
|
212
|
+
const turns = $derived(
|
|
213
|
+
session.turns.map((turn) =>
|
|
214
|
+
turn.outcome?.structuredOutput ? { ...turn, artefact: artefactFrom(turn) } : turn
|
|
215
|
+
)
|
|
216
|
+
);
|
|
217
|
+
</script>
|
|
218
|
+
|
|
219
|
+
<Conversation
|
|
220
|
+
{turns}
|
|
221
|
+
running={watching && session.working}
|
|
222
|
+
version={session.version}
|
|
223
|
+
{name}
|
|
224
|
+
{describeTool}
|
|
225
|
+
copy={{ sources: 'Read from', notHeld: '' }}
|
|
226
|
+
onopenartefact={(turn) => (showing = turn.id)}
|
|
227
|
+
{showing}
|
|
228
|
+
oncite={(citation) => showPage(citation.document_id)}
|
|
229
|
+
>
|
|
230
|
+
{#snippet composer()}
|
|
231
|
+
<Composer
|
|
232
|
+
bind:value
|
|
233
|
+
{running}
|
|
234
|
+
{name}
|
|
235
|
+
sendWhileRunning
|
|
236
|
+
note="It carries on from where it stopped."
|
|
237
|
+
{onsubmit}
|
|
238
|
+
{onstop}
|
|
239
|
+
/>
|
|
240
|
+
{/snippet}
|
|
241
|
+
</Conversation>
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
`watch()` differs from `ask()` in one way: a clean close with no `result` is
|
|
245
|
+
not a failure, because a stopped run and a session waiting between runs both
|
|
246
|
+
end that way.
|
|
247
|
+
|
|
248
|
+
| Concern | How |
|
|
249
|
+
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
250
|
+
| The persona's tools | `describeTool(block) => ToolWords \| undefined`: the row (`verb`, `object`, `detail`), the activity line's counts (`tally`), and what it read (`source`). `undefined` leaves a call to the package's own words |
|
|
251
|
+
| What an answer read | `ToolWords.source` lists under the answer, under `copy.sources`; tapping one calls `oncite`. Nothing checked it, so it carries no trust mark |
|
|
252
|
+
| An artefact | `turn.artefact = { title, summary }` cards under the prose once the turn settles; `isAnswer: true` is an artefact that IS the prose, and replaces it |
|
|
253
|
+
| Opening it | `onopenartefact(turn)` and `showing` (the turn id the host shows). Without a handler, an `isAnswer` artefact opens in `ArtefactPane` and any other offers no action |
|
|
254
|
+
| "Answered without a source" | `copy.notHeld: ''` for a persona that answers from no shelf |
|
|
255
|
+
| Saying something mid-run | `Composer`'s `sendWhileRunning`: while the session works the box stays open and Send sits beside Stop. For a route that reads a message while the run works; a chat leaves it off and waits for its answer |
|
|
256
|
+
|
|
187
257
|
### Citations
|
|
188
258
|
|
|
189
259
|
The library emits one SSE frame after the final assistant text:
|
|
@@ -343,7 +413,7 @@ pnpm run test # build + vitest
|
|
|
343
413
|
pnpm run screenshots # the state grid, real engine (see below)
|
|
344
414
|
```
|
|
345
415
|
|
|
346
|
-
`docs/screenshots/` is
|
|
416
|
+
`docs/screenshots/` is sixteen states x three widths x both themes, taken by
|
|
347
417
|
`scripts/screenshots.mjs` against the console's `/librarian` lab route
|
|
348
418
|
running from its own static build. The same script asserts what a screenshot
|
|
349
419
|
cannot: that nothing scrolls sideways at any width, that the source pane
|
|
@@ -352,8 +422,11 @@ really draggable, that the scope statement folds once there is a
|
|
|
352
422
|
conversation over it and reopens from that line, that a follow-up chip asks
|
|
353
423
|
its question and takes the rest of the row with it, that the persona's
|
|
354
424
|
between-tool narration is nowhere in the answer prose before the activity
|
|
355
|
-
line is opened,
|
|
356
|
-
|
|
425
|
+
line is opened, that a job's artefact and the pages it read open in the
|
|
426
|
+
host's own columns and never in a pane of the package's, and that the reading
|
|
427
|
+
column holds its 46rem measure and stays centred at every width. It exits
|
|
428
|
+
non-zero on any of them. The job states fold a stream captured from the real
|
|
429
|
+
CLI (`src/test/fixtures/`).
|
|
357
430
|
|
|
358
431
|
The console's `app.css` deliberately does NOT scan this package's `dist`, so
|
|
359
432
|
the grid is taken in a consumer that compiles none of its Tailwind classes —
|
package/dist/citations.d.ts
CHANGED
|
@@ -26,7 +26,9 @@ export interface Citation {
|
|
|
26
26
|
* as null.
|
|
27
27
|
*/
|
|
28
28
|
verified_at?: string | null;
|
|
29
|
-
/** True when
|
|
29
|
+
/** True when read off the stream — the prose's Sources block, or a call
|
|
30
|
+
* that read it — rather than the library's own frame. Nothing checked it,
|
|
31
|
+
* so it carries no trust mark. */
|
|
30
32
|
derived?: boolean;
|
|
31
33
|
}
|
|
32
34
|
/** Inline `[n]` markers, in order of first appearance.
|
package/dist/client.d.ts
CHANGED
|
@@ -27,15 +27,25 @@ export interface AgentEvent {
|
|
|
27
27
|
id?: string;
|
|
28
28
|
};
|
|
29
29
|
};
|
|
30
|
+
/** A whole message; `content` is a plain string only on a user frame. */
|
|
30
31
|
message?: {
|
|
31
|
-
|
|
32
|
+
id?: string;
|
|
33
|
+
content?: Array<Record<string, unknown>> | string;
|
|
32
34
|
};
|
|
35
|
+
/** A user frame the CLI echoed back as the turn that consumes it starts
|
|
36
|
+
* (`--replay-user-messages`): the reader's side of a session. */
|
|
37
|
+
isReplay?: boolean;
|
|
38
|
+
uuid?: string;
|
|
39
|
+
/** ISO 8601, on every user and assistant frame. */
|
|
40
|
+
timestamp?: string;
|
|
33
41
|
session_id?: string;
|
|
34
42
|
result?: string;
|
|
35
43
|
num_turns?: number;
|
|
36
44
|
total_cost_usd?: number;
|
|
37
45
|
duration_ms?: number;
|
|
38
46
|
is_error?: boolean;
|
|
47
|
+
/** What a run returned under `--json-schema`, on its `result`. */
|
|
48
|
+
structured_output?: unknown;
|
|
39
49
|
error?: string;
|
|
40
50
|
detail?: string;
|
|
41
51
|
tools?: string[];
|
|
@@ -85,3 +95,20 @@ export interface AskOptions {
|
|
|
85
95
|
* where the turn is rendered.
|
|
86
96
|
*/
|
|
87
97
|
export declare function ask(options: AskOptions): AsyncGenerator<AgentEvent>;
|
|
98
|
+
export interface WatchOptions {
|
|
99
|
+
/** The session's watch route, e.g. `/api/agent/{persona}/jobs/{id}/watch`. */
|
|
100
|
+
endpoint: string;
|
|
101
|
+
signal?: AbortSignal;
|
|
102
|
+
fetch?: typeof fetch;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Async-iterate a session somebody else started: every event from the first,
|
|
106
|
+
* then live while it runs.
|
|
107
|
+
*
|
|
108
|
+
* The same frames and the same never-throw contract as `ask()`, with one
|
|
109
|
+
* difference: a clean close with no `result` is not a failure. A run the
|
|
110
|
+
* reader stopped ends exactly that way, and so does a session that is
|
|
111
|
+
* waiting between runs, so only a stream that would not open, or broke,
|
|
112
|
+
* yields `library_error`. Fold the events with `Session`.
|
|
113
|
+
*/
|
|
114
|
+
export declare function watch(options: WatchOptions): AsyncGenerator<AgentEvent>;
|
package/dist/client.js
CHANGED
|
@@ -64,14 +64,35 @@ function requestInit(options, signal) {
|
|
|
64
64
|
* where the turn is rendered.
|
|
65
65
|
*/
|
|
66
66
|
export async function* ask(options) {
|
|
67
|
-
|
|
67
|
+
yield* read(() => (options.fetch ?? fetch)(options.endpoint ?? '/api/agent/ask', requestInit(options, options.signal)), options.signal, true);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Async-iterate a session somebody else started: every event from the first,
|
|
71
|
+
* then live while it runs.
|
|
72
|
+
*
|
|
73
|
+
* The same frames and the same never-throw contract as `ask()`, with one
|
|
74
|
+
* difference: a clean close with no `result` is not a failure. A run the
|
|
75
|
+
* reader stopped ends exactly that way, and so does a session that is
|
|
76
|
+
* waiting between runs, so only a stream that would not open, or broke,
|
|
77
|
+
* yields `library_error`. Fold the events with `Session`.
|
|
78
|
+
*/
|
|
79
|
+
export async function* watch(options) {
|
|
80
|
+
yield* read(() => (options.fetch ?? fetch)(options.endpoint, {
|
|
81
|
+
headers: { Accept: 'text/event-stream' },
|
|
82
|
+
credentials: 'include',
|
|
83
|
+
signal: options.signal
|
|
84
|
+
}), options.signal, false);
|
|
85
|
+
}
|
|
86
|
+
/** One SSE response, as Claude Code's own events. `terminal` demands a
|
|
87
|
+
* `result` before the stream closes. */
|
|
88
|
+
async function* read(open, signal, terminal) {
|
|
68
89
|
let response;
|
|
69
90
|
try {
|
|
70
|
-
response = await
|
|
91
|
+
response = await open();
|
|
71
92
|
}
|
|
72
93
|
catch {
|
|
73
94
|
// A reader who pressed stop asked for this one; it is not a failure.
|
|
74
|
-
if (
|
|
95
|
+
if (signal?.aborted)
|
|
75
96
|
return;
|
|
76
97
|
yield { type: 'library_error' };
|
|
77
98
|
return;
|
|
@@ -97,7 +118,7 @@ export async function* ask(options) {
|
|
|
97
118
|
// A turn that ends with no terminal frame ended by accident. Tracked here
|
|
98
119
|
// rather than left to the renderer, which cannot tell a stream that died
|
|
99
120
|
// from one still arriving.
|
|
100
|
-
let
|
|
121
|
+
let ended = false;
|
|
101
122
|
/** One frame's `data:` lines, as the event they carry. */
|
|
102
123
|
function parse(frame) {
|
|
103
124
|
// `data:` only — the type lives inside the payload where Claude Code
|
|
@@ -123,7 +144,7 @@ export async function* ask(options) {
|
|
|
123
144
|
chunk = await reader.read();
|
|
124
145
|
}
|
|
125
146
|
catch {
|
|
126
|
-
if (
|
|
147
|
+
if (signal?.aborted)
|
|
127
148
|
return;
|
|
128
149
|
yield { type: 'library_error' };
|
|
129
150
|
return;
|
|
@@ -140,7 +161,7 @@ export async function* ask(options) {
|
|
|
140
161
|
if (!event)
|
|
141
162
|
continue;
|
|
142
163
|
if (event.type === 'result' || event.type === 'library_error')
|
|
143
|
-
|
|
164
|
+
ended = true;
|
|
144
165
|
yield event;
|
|
145
166
|
}
|
|
146
167
|
}
|
|
@@ -151,9 +172,9 @@ export async function* ask(options) {
|
|
|
151
172
|
const last = parse(buffered);
|
|
152
173
|
if (last) {
|
|
153
174
|
if (last.type === 'result' || last.type === 'library_error')
|
|
154
|
-
|
|
175
|
+
ended = true;
|
|
155
176
|
yield last;
|
|
156
177
|
}
|
|
157
|
-
if (
|
|
178
|
+
if (terminal && !ended && !signal?.aborted)
|
|
158
179
|
yield { type: 'library_error' };
|
|
159
180
|
}
|
|
@@ -52,7 +52,12 @@
|
|
|
52
52
|
<div class="ds-lib-activity-steps">
|
|
53
53
|
{#each group.steps as step (step.block.index)}
|
|
54
54
|
{#if step.block.kind === 'tool'}
|
|
55
|
-
<ToolRow
|
|
55
|
+
<ToolRow
|
|
56
|
+
block={step.block}
|
|
57
|
+
repeats={step.repeats}
|
|
58
|
+
words={step.words}
|
|
59
|
+
running={live}
|
|
60
|
+
/>
|
|
56
61
|
{:else}
|
|
57
62
|
<ThinkingRow block={step.block} active={false} />
|
|
58
63
|
{/if}
|
|
@@ -18,8 +18,11 @@
|
|
|
18
18
|
import RefreshCwIcon from '@lucide/svelte/icons/refresh-cw';
|
|
19
19
|
import {
|
|
20
20
|
readerQuestion,
|
|
21
|
+
readFrom,
|
|
21
22
|
segment,
|
|
23
|
+
type Artefact,
|
|
22
24
|
type Block,
|
|
25
|
+
type DescribeTool,
|
|
23
26
|
type Outcome,
|
|
24
27
|
type TextBlock
|
|
25
28
|
} from '../../transcript.svelte';
|
|
@@ -64,14 +67,15 @@
|
|
|
64
67
|
* and a reader scanning back for "what did I ask after lunch" needs the
|
|
65
68
|
* first one. Absent renders no time rather than a guess. */
|
|
66
69
|
at?: number;
|
|
67
|
-
/**
|
|
68
|
-
*
|
|
69
|
-
|
|
70
|
-
/** The artefact
|
|
71
|
-
|
|
72
|
-
/** Opens
|
|
73
|
-
* is `'artefact'`. */
|
|
70
|
+
/** What this turn produced for the reader to open, carded once it
|
|
71
|
+
* settles: under the prose, or in its place when it IS the prose. */
|
|
72
|
+
artefact?: Artefact;
|
|
73
|
+
/** The artefact is open now. */
|
|
74
|
+
artefactOpen?: boolean;
|
|
75
|
+
/** Opens it. Omit and the card informs but does not open. */
|
|
74
76
|
onopenartefact?: () => void;
|
|
77
|
+
/** The persona's own words for its tools, and what they read. */
|
|
78
|
+
describeTool?: DescribeTool;
|
|
75
79
|
}
|
|
76
80
|
|
|
77
81
|
let {
|
|
@@ -88,12 +92,15 @@
|
|
|
88
92
|
copy,
|
|
89
93
|
name = DEFAULT_PERSONA,
|
|
90
94
|
at,
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
onopenartefact
|
|
95
|
+
artefact,
|
|
96
|
+
artefactOpen = false,
|
|
97
|
+
onopenartefact,
|
|
98
|
+
describeTool
|
|
94
99
|
}: Props = $props();
|
|
95
100
|
|
|
96
|
-
|
|
101
|
+
// The prose IS the artefact: it reads in the column, and the transcript
|
|
102
|
+
// shows the card alone.
|
|
103
|
+
const isArtefact = $derived(artefact?.isAnswer === true);
|
|
97
104
|
|
|
98
105
|
const words = $derived(resolveCopy(copy, name));
|
|
99
106
|
const who = $derived(personaName(name));
|
|
@@ -120,7 +127,7 @@
|
|
|
120
127
|
// An activity group stays live — and therefore labelled "Working…" — only
|
|
121
128
|
// while it is the last thing in the turn. As soon as prose arrives, it
|
|
122
129
|
// settles into its count.
|
|
123
|
-
const segments = $derived(segment(blocks));
|
|
130
|
+
const segments = $derived(segment(blocks, describeTool));
|
|
124
131
|
const lastIndex = $derived(segments.at(-1)?.index ?? -1);
|
|
125
132
|
|
|
126
133
|
// An answer can arrive as SEVERAL text blocks with tool calls between them,
|
|
@@ -135,7 +142,11 @@
|
|
|
135
142
|
const split = $derived(
|
|
136
143
|
running ? { body: '', citations: [] } : splitSources(texts.at(-1)?.text ?? '')
|
|
137
144
|
);
|
|
138
|
-
|
|
145
|
+
// What the calls read, when the answer names no source of its own.
|
|
146
|
+
const read = $derived(running ? [] : readFrom(blocks, describeTool));
|
|
147
|
+
const sources = $derived(
|
|
148
|
+
resolveCitations(citations, split.citations.length > 0 ? split.citations : read)
|
|
149
|
+
);
|
|
139
150
|
|
|
140
151
|
/** The answer as a reader would paste it: every block, Sources stripped. */
|
|
141
152
|
const answer = $derived(
|
|
@@ -218,13 +229,16 @@
|
|
|
218
229
|
<article class="ds-lib-turn">
|
|
219
230
|
<!-- Trailing-edge bubble, following Claude and ChatGPT rather than a
|
|
220
231
|
VS Code-style panel: this surface is read by people who arrive with
|
|
221
|
-
those two as their model of what a chat looks like.
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
<
|
|
226
|
-
|
|
227
|
-
|
|
232
|
+
those two as their model of what a chat looks like. A session whose
|
|
233
|
+
stream never carried its prompt has no words to put here. -->
|
|
234
|
+
{#if asked}
|
|
235
|
+
<div class="ds-lib-ask">
|
|
236
|
+
<div class="ds-lib-bubble">{asked}</div>
|
|
237
|
+
{#if clock}
|
|
238
|
+
<time class="ds-lib-ask-time" datetime={clock.iso}>{clock.text}</time>
|
|
239
|
+
{/if}
|
|
240
|
+
</div>
|
|
241
|
+
{/if}
|
|
228
242
|
|
|
229
243
|
<div class="ds-lib-answer">
|
|
230
244
|
<header class="ds-lib-answer-head">
|
|
@@ -258,19 +272,17 @@
|
|
|
258
272
|
{/if}
|
|
259
273
|
{/each}
|
|
260
274
|
|
|
261
|
-
{#if
|
|
262
|
-
<ArtefactCard
|
|
263
|
-
title={title ?? 'Briefing'}
|
|
264
|
-
citationCount={sources.length}
|
|
265
|
-
onopen={() => onopenartefact?.()}
|
|
266
|
-
/>
|
|
275
|
+
{#if artefact && settled}
|
|
276
|
+
<ArtefactCard {artefact} open={artefactOpen} onopen={onopenartefact} copy={words} />
|
|
267
277
|
{/if}
|
|
268
278
|
|
|
269
279
|
{#if !isArtefact && sources.length > 0}
|
|
270
280
|
<SourceList {sources} {words} {oncite} />
|
|
271
281
|
{/if}
|
|
272
282
|
|
|
273
|
-
|
|
283
|
+
<!-- An empty `notHeld` is a host whose persona answers from no shelf,
|
|
284
|
+
where "without a source" would be a claim about nothing. -->
|
|
285
|
+
{#if !isArtefact && notHeld && words.notHeld}
|
|
274
286
|
<p class="ds-lib-not-held">{words.notHeld}</p>
|
|
275
287
|
{/if}
|
|
276
288
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Block, type Outcome } from '../../transcript.svelte';
|
|
1
|
+
import { type Artefact, type Block, type DescribeTool, type Outcome } from '../../transcript.svelte';
|
|
2
2
|
import { type Citation } from '../../citations';
|
|
3
3
|
import { type LibrarianCopy } from '../../copy';
|
|
4
4
|
interface Props {
|
|
@@ -29,14 +29,15 @@ interface Props {
|
|
|
29
29
|
* and a reader scanning back for "what did I ask after lunch" needs the
|
|
30
30
|
* first one. Absent renders no time rather than a guess. */
|
|
31
31
|
at?: number;
|
|
32
|
-
/**
|
|
33
|
-
*
|
|
34
|
-
|
|
35
|
-
/** The artefact
|
|
36
|
-
|
|
37
|
-
/** Opens
|
|
38
|
-
* is `'artefact'`. */
|
|
32
|
+
/** What this turn produced for the reader to open, carded once it
|
|
33
|
+
* settles: under the prose, or in its place when it IS the prose. */
|
|
34
|
+
artefact?: Artefact;
|
|
35
|
+
/** The artefact is open now. */
|
|
36
|
+
artefactOpen?: boolean;
|
|
37
|
+
/** Opens it. Omit and the card informs but does not open. */
|
|
39
38
|
onopenartefact?: () => void;
|
|
39
|
+
/** The persona's own words for its tools, and what they read. */
|
|
40
|
+
describeTool?: DescribeTool;
|
|
40
41
|
}
|
|
41
42
|
declare const AgentTranscript: import("svelte").Component<Props, {}, "">;
|
|
42
43
|
type AgentTranscript = ReturnType<typeof AgentTranscript>;
|
|
@@ -1,68 +1,129 @@
|
|
|
1
1
|
<!--
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
An artefact's card in the transcript: what a reader taps to open it.
|
|
3
|
+
|
|
4
|
+
It is not the artefact itself. Every word on it is the host's — what the
|
|
5
|
+
thing is called and what it holds — and opening it is the host's too, in
|
|
6
|
+
whatever column the host keeps for it; the package's own `ArtefactPane` is
|
|
7
|
+
one such column, for an artefact that is the answer's own prose.
|
|
8
|
+
|
|
9
|
+
One button, not a card with a button in it: the whole row is the target,
|
|
10
|
+
so a thumb that lands on the summary still opens it.
|
|
7
11
|
-->
|
|
8
12
|
<script lang="ts">
|
|
9
13
|
import FileTextIcon from '@lucide/svelte/icons/file-text';
|
|
10
|
-
import
|
|
14
|
+
import type { Artefact } from '../../transcript.svelte';
|
|
15
|
+
import { resolveCopy, type LibrarianCopy } from '../../copy';
|
|
11
16
|
|
|
12
17
|
interface Props {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
18
|
+
artefact: Artefact;
|
|
19
|
+
/** The host is showing it now. */
|
|
20
|
+
open?: boolean;
|
|
21
|
+
/** Omit and the card informs but does not open: a host with nowhere
|
|
22
|
+
* to show the artefact offers no action rather than a dead one. */
|
|
23
|
+
onopen?: () => void;
|
|
24
|
+
copy?: Partial<LibrarianCopy>;
|
|
16
25
|
}
|
|
17
26
|
|
|
18
|
-
let {
|
|
27
|
+
let { artefact, open = false, onopen, copy }: Props = $props();
|
|
28
|
+
|
|
29
|
+
const words = $derived(resolveCopy(copy));
|
|
19
30
|
</script>
|
|
20
31
|
|
|
21
|
-
<
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
onclick={onopen}
|
|
28
|
-
onkeydown={(event: KeyboardEvent) => {
|
|
29
|
-
if (event.key !== 'Enter' && event.key !== ' ') return;
|
|
30
|
-
event.preventDefault();
|
|
31
|
-
onopen();
|
|
32
|
-
}}
|
|
33
|
-
class="ds-lib-artefact-card"
|
|
32
|
+
<button
|
|
33
|
+
type="button"
|
|
34
|
+
class="ds-lib-artefact"
|
|
35
|
+
class:is-open={open}
|
|
36
|
+
onclick={() => onopen?.()}
|
|
37
|
+
disabled={!onopen}
|
|
34
38
|
>
|
|
35
|
-
{
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
</
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
<span class="ds-lib-artefact-icon" aria-hidden="true"><FileTextIcon size={16} /></span>
|
|
40
|
+
<span class="ds-lib-artefact-text">
|
|
41
|
+
<span class="ds-lib-artefact-title">{artefact.title}</span>
|
|
42
|
+
{#if artefact.summary}<span class="ds-lib-artefact-summary">{artefact.summary}</span>{/if}
|
|
43
|
+
</span>
|
|
44
|
+
{#if onopen}
|
|
45
|
+
<span class="ds-lib-artefact-action">{open ? words.showingArtefact : words.openArtefact}</span>
|
|
46
|
+
{/if}
|
|
47
|
+
</button>
|
|
41
48
|
|
|
42
49
|
<style>
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
Svelte's scoping hash never reaches. */
|
|
46
|
-
:global(.ds-lib-artefact-card) {
|
|
50
|
+
.ds-lib-artefact {
|
|
51
|
+
display: flex;
|
|
47
52
|
width: 100%;
|
|
48
|
-
|
|
49
|
-
|
|
53
|
+
align-items: center;
|
|
54
|
+
gap: 0.75rem;
|
|
55
|
+
border: 1px solid var(--ds-color-border);
|
|
56
|
+
border-radius: var(--ds-radius-lg);
|
|
57
|
+
background: var(--ds-color-surface-1);
|
|
58
|
+
padding: 0.625rem 0.75rem;
|
|
59
|
+
color: var(--ds-color-foreground);
|
|
60
|
+
font: inherit;
|
|
50
61
|
text-align: start;
|
|
62
|
+
cursor: pointer;
|
|
51
63
|
transition: border-color 150ms ease;
|
|
52
64
|
}
|
|
53
65
|
|
|
54
|
-
|
|
66
|
+
.ds-lib-artefact:hover:not(:disabled) {
|
|
55
67
|
border-color: var(--ds-color-border-strong);
|
|
56
68
|
}
|
|
57
69
|
|
|
58
|
-
|
|
70
|
+
.ds-lib-artefact:disabled {
|
|
71
|
+
cursor: default;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.ds-lib-artefact:focus-visible {
|
|
59
75
|
outline: 2px solid var(--ds-color-ring);
|
|
60
76
|
outline-offset: 2px;
|
|
61
77
|
}
|
|
62
78
|
|
|
63
|
-
|
|
64
|
-
|
|
79
|
+
/* The primary edge, not a fill: the card says "this is what is open" and
|
|
80
|
+
the column beside it carries the weight. */
|
|
81
|
+
.ds-lib-artefact.is-open {
|
|
82
|
+
border-color: var(--ds-color-primary);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.ds-lib-artefact-icon {
|
|
86
|
+
display: flex;
|
|
87
|
+
width: 2rem;
|
|
88
|
+
height: 2rem;
|
|
89
|
+
flex: none;
|
|
90
|
+
align-items: center;
|
|
91
|
+
justify-content: center;
|
|
92
|
+
border: 1px solid var(--ds-color-border);
|
|
93
|
+
border-radius: var(--ds-radius-md);
|
|
94
|
+
color: var(--ds-color-muted-foreground);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.ds-lib-artefact-text {
|
|
98
|
+
display: flex;
|
|
99
|
+
min-width: 0;
|
|
100
|
+
flex: 1;
|
|
101
|
+
flex-direction: column;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.ds-lib-artefact-title {
|
|
105
|
+
font-size: 0.875rem;
|
|
106
|
+
font-weight: 500;
|
|
107
|
+
line-height: 1.25rem;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.ds-lib-artefact-summary {
|
|
65
111
|
color: var(--ds-color-muted-foreground);
|
|
66
112
|
font-size: var(--ds-text-2xs);
|
|
113
|
+
overflow-wrap: anywhere;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.ds-lib-artefact-action {
|
|
117
|
+
flex: none;
|
|
118
|
+
border: 1px solid var(--ds-color-border);
|
|
119
|
+
border-radius: var(--ds-radius-md);
|
|
120
|
+
padding: 0.25rem 0.625rem;
|
|
121
|
+
font-size: 0.8125rem;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@media (prefers-reduced-motion: reduce) {
|
|
125
|
+
.ds-lib-artefact {
|
|
126
|
+
transition: none;
|
|
127
|
+
}
|
|
67
128
|
}
|
|
68
129
|
</style>
|
|
@@ -1,7 +1,13 @@
|
|
|
1
|
+
import type { Artefact } from '../../transcript.svelte';
|
|
2
|
+
import { type LibrarianCopy } from '../../copy';
|
|
1
3
|
interface Props {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
artefact: Artefact;
|
|
5
|
+
/** The host is showing it now. */
|
|
6
|
+
open?: boolean;
|
|
7
|
+
/** Omit and the card informs but does not open: a host with nowhere
|
|
8
|
+
* to show the artefact offers no action rather than a dead one. */
|
|
9
|
+
onopen?: () => void;
|
|
10
|
+
copy?: Partial<LibrarianCopy>;
|
|
5
11
|
}
|
|
6
12
|
declare const ArtefactCard: import("svelte").Component<Props, {}, "">;
|
|
7
13
|
type ArtefactCard = ReturnType<typeof ArtefactCard>;
|