@invana/assistant 0.0.32

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/dist/index.js ADDED
@@ -0,0 +1,1892 @@
1
+ import * as React6 from 'react';
2
+ import { ANSWER_KINDS, BLOCK_RENDERERS, ASK_KINDS, useStreamedSpec, ScopeBlock, CitationsBlock, NarrativeBlock, Placeholder, ActionRow, SuggestionChips, patchesOf as patchesOf$1, fromNdjson as fromNdjson$1, fromEventSource as fromEventSource$1, playScript as playScript$1, offsetScript as offsetScript$1, applyBlockPatch, BlockPatchError } from '@invana/blocks';
3
+ export { ANSWER_KINDS, ASK_KINDS, BLOCKS, ConfirmCard, Placeholder, SuggestionChips, scriptLength } from '@invana/blocks';
4
+ import { cn, EmissionCard as EmissionCard$1, EmissionBody as EmissionBody$1, ChatSessionCaret as ChatSessionCaret$1, ChatSession as ChatSession$1, ChatSessionStatusBar as ChatSessionStatusBar$1, ChatSessionTaskGroup as ChatSessionTaskGroup$1, ChatSessionPromptRow as ChatSessionPromptRow$1, ChatSessionComposer as ChatSessionComposer$1, ChatSessionMessage as ChatSessionMessage$1, ChatSessionTaskRow as ChatSessionTaskRow$1, ChatSessionActivitySubLine as ChatSessionActivitySubLine$1, ChatSessionDisclosure as ChatSessionDisclosure$1, ChatSessionProgressLine as ChatSessionProgressLine$1, ChatSessionActivityRow as ChatSessionActivityRow$1, ClarifyCard as ClarifyCard$1, Eyebrow, PropertyList, PropertyRow, Skeleton, ChatSessionMessageOptions as ChatSessionMessageOptions$1, ChatSessionContextChip as ChatSessionContextChip$1, ChatSessionDisclosureCode as ChatSessionDisclosureCode$1, ChatSessionDisclosureSteps as ChatSessionDisclosureSteps$1, CitationMarker as CitationMarker$1, ClarifyActions as ClarifyActions$1, ClarifyFootnote as ClarifyFootnote$1, EmissionHeader as EmissionHeader$1, TemplatePicker as TemplatePicker$1, chatSessionGutterClass as chatSessionGutterClass$1 } from '@invana/ui';
5
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
6
+ import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@invana/forms';
7
+
8
+ // src/styles/chat-session.tsx
9
+ function answerToPage(turn) {
10
+ const blocks = turn.blocks.filter(
11
+ (b) => BLOCK_RENDERERS[b.kind] != null && b.status !== "loading" && b.status !== "empty"
12
+ );
13
+ return { title: turn.title, sections: [{ blocks }] };
14
+ }
15
+
16
+ // src/answers/index.ts
17
+ var EmissionCard = EmissionCard$1;
18
+ var EmissionBody = EmissionBody$1;
19
+ var EmissionHeader = EmissionHeader$1;
20
+ var CitationMarker = CitationMarker$1;
21
+ var TemplatePicker = TemplatePicker$1;
22
+ var ChatSessionFrame = ChatSession$1;
23
+ var ChatSessionMessage = ChatSessionMessage$1;
24
+ var ChatSessionMessageOptions = ChatSessionMessageOptions$1;
25
+ var ChatSessionComposer = ChatSessionComposer$1;
26
+ var ChatSessionActivityRow = ChatSessionActivityRow$1;
27
+ var ChatSessionActivitySubLine = ChatSessionActivitySubLine$1;
28
+ var chatSessionGutterClass = chatSessionGutterClass$1;
29
+ var ChatSessionPromptRow = ChatSessionPromptRow$1;
30
+ var ChatSessionProgressLine = ChatSessionProgressLine$1;
31
+ var ChatSessionCaret = ChatSessionCaret$1;
32
+ var ChatSessionDisclosure = ChatSessionDisclosure$1;
33
+ var ChatSessionDisclosureCode = ChatSessionDisclosureCode$1;
34
+ var ChatSessionDisclosureSteps = ChatSessionDisclosureSteps$1;
35
+ var ChatSessionTaskRow = ChatSessionTaskRow$1;
36
+ var ChatSessionTaskGroup = ChatSessionTaskGroup$1;
37
+ var ChatSessionStatusBar = ChatSessionStatusBar$1;
38
+ var ChatSessionContextChip = ChatSessionContextChip$1;
39
+ var focus = /* @__PURE__ */ new Map();
40
+ var listeners = /* @__PURE__ */ new Set();
41
+ function set(turn, n) {
42
+ if (focus.get(turn) === n) return;
43
+ focus.set(turn, n);
44
+ listeners.forEach((l) => l());
45
+ }
46
+ function subscribe(listener) {
47
+ listeners.add(listener);
48
+ return () => listeners.delete(listener);
49
+ }
50
+ function useCiteFocus(turn, initial) {
51
+ const pointed = React6.useSyncExternalStore(
52
+ subscribe,
53
+ () => focus.get(turn),
54
+ () => void 0
55
+ );
56
+ return [pointed ?? initial, (n) => set(turn, n)];
57
+ }
58
+ function toEvent(turn, action, value, block) {
59
+ switch (action) {
60
+ case "reply":
61
+ return { type: "reply", turn, value };
62
+ case "change":
63
+ return { type: "change", turn, value };
64
+ case "skip":
65
+ return { type: "skip", turn };
66
+ case "prompt":
67
+ return { type: "prompt", text: String(value) };
68
+ case "scope": {
69
+ const edit = value;
70
+ return typeof edit?.part === "number" ? { type: "scope", turn, part: edit.part, value: edit.value } : void 0;
71
+ }
72
+ case "open":
73
+ return block >= 0 ? { type: "open", turn, block } : void 0;
74
+ case "action":
75
+ return { type: "action", turn, action: String(value) };
76
+ default:
77
+ return value === void 0 ? { type: "action", turn, action } : { type: "action", turn, action, value };
78
+ }
79
+ }
80
+ function answerBlock(kind) {
81
+ const Renderer = BLOCK_RENDERERS[kind];
82
+ function AnswerBlock({ turn, block, onEvent }) {
83
+ const index = turn.blocks.indexOf(block);
84
+ return /* @__PURE__ */ jsx(
85
+ Renderer,
86
+ {
87
+ spec: block,
88
+ onAction: (action, value) => {
89
+ const event = toEvent(turn.id, action, value, index);
90
+ if (event) onEvent(event);
91
+ }
92
+ }
93
+ );
94
+ }
95
+ AnswerBlock.displayName = `Answer(${kind})`;
96
+ return AnswerBlock;
97
+ }
98
+ function askBlock(kind) {
99
+ const Renderer = BLOCK_RENDERERS[kind];
100
+ function AskBlock({ turn, options, onEvent }) {
101
+ return /* @__PURE__ */ jsx(
102
+ Renderer,
103
+ {
104
+ spec: options,
105
+ state: turn.state,
106
+ value: turn.value,
107
+ id: turn.id,
108
+ onAction: (action, value) => {
109
+ const event = toEvent(turn.id, action, value, -1);
110
+ if (event) onEvent(event);
111
+ }
112
+ }
113
+ );
114
+ }
115
+ AskBlock.displayName = `Ask(${kind})`;
116
+ return AskBlock;
117
+ }
118
+ function NarrativeAnswer({ block, turn }) {
119
+ const [active, setActive] = useCiteFocus(turn.id, block.active);
120
+ const writing = turn.state === "running" && turn.blocks[turn.blocks.length - 1] === block;
121
+ return /* @__PURE__ */ jsx(
122
+ NarrativeBlock,
123
+ {
124
+ spec: block,
125
+ active,
126
+ onActiveChange: setActive,
127
+ trailing: writing ? /* @__PURE__ */ jsx(ChatSessionCaret, {}) : null
128
+ }
129
+ );
130
+ }
131
+ function CitationsAnswer({ block, turn }) {
132
+ const [active] = useCiteFocus(turn.id, block.active);
133
+ return /* @__PURE__ */ jsx(CitationsBlock, { spec: block, active });
134
+ }
135
+ function ScopeAnswer({ block, turn, onEvent }) {
136
+ const fromEnvelope = !turn.blocks.includes(block);
137
+ const fixed = fromEnvelope && turn.envelope?.freshness ? [block.parts.length - 1] : void 0;
138
+ return /* @__PURE__ */ jsx(
139
+ ScopeBlock,
140
+ {
141
+ spec: block,
142
+ fixed,
143
+ onAction: (action, value) => {
144
+ const event = toEvent(turn.id, action, value, -1);
145
+ if (event) onEvent(event);
146
+ }
147
+ }
148
+ );
149
+ }
150
+ var ProposalBody = answerBlock("proposal");
151
+ function ProposalAnswer(props) {
152
+ return /* @__PURE__ */ jsx(EmissionCard, { kind: "proposal", title: props.block.title, citation: props.block.done?.at, children: /* @__PURE__ */ jsx(EmissionBody, { children: /* @__PURE__ */ jsx(ProposalBody, { ...props }) }) });
153
+ }
154
+ var ANSWER_WRAPPERS = {
155
+ narrative: NarrativeAnswer,
156
+ citations: CitationsAnswer,
157
+ scope: ScopeAnswer,
158
+ proposal: ProposalAnswer
159
+ };
160
+ var ANSWER_RENDERERS = Object.fromEntries(
161
+ ANSWER_KINDS.map((kind) => [kind, BLOCK_RENDERERS[kind] ? ANSWER_WRAPPERS[kind] ?? answerBlock(kind) : null])
162
+ );
163
+ var ASK_RENDERERS = Object.fromEntries(
164
+ ASK_KINDS.map((kind) => [kind, BLOCK_RENDERERS[kind] ? askBlock(kind) : null])
165
+ );
166
+
167
+ // src/conversations/registry.ts
168
+ var BUILT_IN_ASKS = ASK_RENDERERS;
169
+ var BUILT_IN_BLOCKS = ANSWER_RENDERERS;
170
+ var BUILT_IN_ASK_TRAITS = {
171
+ suggestions: { frame: "none" },
172
+ confirm: { kind: "confirm" },
173
+ approval: { kind: "proposal" }
174
+ };
175
+ var BUILT_IN_BLOCK_TRAITS = {
176
+ proposal: { placement: "own" }
177
+ };
178
+ function resolveRegistry(extra) {
179
+ const askTraits = { ...BUILT_IN_ASK_TRAITS, ...extra?.askTraits };
180
+ const blockTraits = { ...BUILT_IN_BLOCK_TRAITS, ...extra?.blockTraits };
181
+ return {
182
+ asks: { ...BUILT_IN_ASKS, ...extra?.asks },
183
+ blocks: { ...BUILT_IN_BLOCKS, ...extra?.blocks },
184
+ askTraits: (kind) => ({ frame: "card", ...askTraits[kind] }),
185
+ blockTraits: (kind) => ({ placement: "card", ...blockTraits[kind] })
186
+ };
187
+ }
188
+ var PatchError = class extends Error {
189
+ };
190
+ function onTurn(spec, id, fn) {
191
+ const index = spec.turns.findIndex((t) => t.id === id);
192
+ if (index < 0) throw new PatchError(`No turn "${id}" in conversation "${spec.id}".`);
193
+ const turns = spec.turns.slice();
194
+ turns[index] = fn(turns[index]);
195
+ return { ...spec, turns };
196
+ }
197
+ function asAnswer(turn, op) {
198
+ if (turn.role !== "assistant" || turn.kind !== "answer") {
199
+ throw new PatchError(`"${op}" applies to an answer; turn "${turn.id}" is not one.`);
200
+ }
201
+ return turn;
202
+ }
203
+ function onStep(answer, id, op, fn) {
204
+ const trace = answer.trace ?? [];
205
+ const at = trace.findIndex((s) => s.id === id);
206
+ if (at < 0) throw new PatchError(`"${op}": no step "${id}" on turn "${answer.id}".`);
207
+ return { ...answer, trace: trace.map((s, i) => i === at ? fn(s) : s) };
208
+ }
209
+ function patchBlock(answer, op, index, patch) {
210
+ const at = index ?? answer.blocks.length - 1;
211
+ const block = answer.blocks[at];
212
+ if (!block) throw new PatchError(`"${op}": turn "${answer.id}" has no block ${at}.`);
213
+ const blocks = answer.blocks.slice();
214
+ try {
215
+ blocks[at] = applyBlockPatch(block, patch);
216
+ } catch (error) {
217
+ if (error instanceof BlockPatchError) {
218
+ throw new PatchError(`"${op}": block ${at} on turn "${answer.id}": ${error.message}`);
219
+ }
220
+ throw error;
221
+ }
222
+ return { ...answer, blocks };
223
+ }
224
+ function applyPatch(spec, patch) {
225
+ switch (patch.op) {
226
+ case "add-turn":
227
+ if (spec.turns.some((t) => t.id === patch.turn.id)) {
228
+ throw new PatchError(`Turn "${patch.turn.id}" already exists.`);
229
+ }
230
+ return { ...spec, turns: [...spec.turns, patch.turn] };
231
+ case "set-state":
232
+ return onTurn(spec, patch.turn, (turn) => {
233
+ if (turn.role !== "assistant") throw new PatchError(`Turn "${turn.id}" has no state.`);
234
+ if (turn.kind === "ask") {
235
+ return {
236
+ ...turn,
237
+ state: patch.state,
238
+ ...patch.value !== void 0 ? { value: patch.value } : {}
239
+ };
240
+ }
241
+ return { ...turn, state: patch.state };
242
+ });
243
+ case "add-block":
244
+ return onTurn(spec, patch.turn, (turn) => {
245
+ const answer = asAnswer(turn, patch.op);
246
+ return { ...answer, blocks: [...answer.blocks, patch.block] };
247
+ });
248
+ case "patch-block":
249
+ return onTurn(spec, patch.turn, (turn) => patchBlock(asAnswer(turn, patch.op), patch.op, patch.block, patch.patch));
250
+ case "update-block":
251
+ return onTurn(
252
+ spec,
253
+ patch.turn,
254
+ (turn) => patchBlock(asAnswer(turn, patch.op), patch.op, patch.block, { op: "set", fields: patch.fields })
255
+ );
256
+ case "append-text":
257
+ return onTurn(
258
+ spec,
259
+ patch.turn,
260
+ (turn) => patchBlock(asAnswer(turn, patch.op), patch.op, patch.block, { op: "append", at: [patch.field ?? "text"], text: patch.text })
261
+ );
262
+ case "add-trace-step":
263
+ return onTurn(spec, patch.turn, (turn) => {
264
+ const answer = asAnswer(turn, patch.op);
265
+ const trace = answer.trace ?? [];
266
+ const at = patch.step.id ? trace.findIndex((s) => s.id === patch.step.id) : -1;
267
+ const next = at >= 0 ? trace.map((s, i) => i === at ? patch.step : s) : [...trace, patch.step];
268
+ return { ...answer, trace: next };
269
+ });
270
+ case "update-trace-step":
271
+ return onTurn(
272
+ spec,
273
+ patch.turn,
274
+ (turn) => onStep(asAnswer(turn, patch.op), patch.step, patch.op, (s) => ({ ...s, ...patch.fields, id: s.id }))
275
+ );
276
+ case "append-thinking":
277
+ return onTurn(
278
+ spec,
279
+ patch.turn,
280
+ (turn) => onStep(asAnswer(turn, patch.op), patch.step, patch.op, (s) => ({
281
+ ...s,
282
+ thinking: (s.thinking ?? "") + patch.text
283
+ }))
284
+ );
285
+ case "update-turn":
286
+ return onTurn(spec, patch.turn, (turn) => ({ ...turn, ...patch.fields, id: turn.id }));
287
+ case "set-records": {
288
+ const models = spec.access?.models ?? [];
289
+ if (!spec.access || !models.some((m) => m.id === patch.model)) {
290
+ throw new PatchError(`"set-records": no model "${patch.model}" in conversation "${spec.id}".`);
291
+ }
292
+ return {
293
+ ...spec,
294
+ access: {
295
+ ...spec.access,
296
+ models: models.map((m) => m.id === patch.model ? { ...m, records: patch.records } : m)
297
+ }
298
+ };
299
+ }
300
+ case "update-spec":
301
+ return { ...spec, ...patch.fields, id: spec.id, turns: spec.turns };
302
+ }
303
+ }
304
+ function applyPatches(spec, patches) {
305
+ return patches.reduce(applyPatch, spec);
306
+ }
307
+ var patchesOf = (source) => patchesOf$1(source);
308
+ var fromNdjson = (input) => fromNdjson$1(input);
309
+ var fromEventSource = (source, options) => fromEventSource$1(source, options);
310
+ var playScript = (script, options) => playScript$1(script, options);
311
+ var offsetScript = (script, ms) => offsetScript$1(script, ms);
312
+ function deltas(text, by) {
313
+ if (by === "char") return text.match(/[\s\S]{1,3}/g) ?? [];
314
+ return text.match(/\S+\s*|\s+/g) ?? [];
315
+ }
316
+ function textDeltas(turn, text, { from = 0, every = 40, by = "word", block, field } = {}) {
317
+ return deltas(text, by).map((piece, i) => ({
318
+ at: from + i * every,
319
+ patch: {
320
+ op: "append-text",
321
+ turn,
322
+ text: piece,
323
+ ...block === void 0 ? {} : { block },
324
+ ...field === void 0 ? {} : { field }
325
+ }
326
+ }));
327
+ }
328
+ function thinkingDeltas(turn, step, text, { from = 0, every = 40, by = "word" } = {}) {
329
+ return deltas(text, by).map((piece, i) => ({
330
+ at: from + i * every,
331
+ patch: { op: "append-thinking", turn, step, text: piece }
332
+ }));
333
+ }
334
+ var LIVE_STEP = /* @__PURE__ */ new Set(["running", "retrying"]);
335
+ var isLive = (turn) => turn.state === "running";
336
+ function isRunning(spec) {
337
+ return spec.turns.some((t) => t.role === "assistant" && t.kind === "answer" && isLive(t));
338
+ }
339
+ function stopPatches(spec, now = Date.now()) {
340
+ const patches = [];
341
+ for (const turn of spec.turns) {
342
+ if (turn.role !== "assistant" || turn.kind !== "answer" || !isLive(turn)) continue;
343
+ for (const step of turn.trace ?? []) {
344
+ if (!step.id || !LIVE_STEP.has(step.state)) continue;
345
+ const started2 = step.startedAt ? Date.parse(step.startedAt) : NaN;
346
+ patches.push({
347
+ op: "update-trace-step",
348
+ turn: turn.id,
349
+ step: step.id,
350
+ fields: {
351
+ state: "stopped",
352
+ thinking: void 0,
353
+ ...Number.isNaN(started2) ? {} : { duration: now - started2 }
354
+ }
355
+ });
356
+ }
357
+ const started = turn.startedAt ? Date.parse(turn.startedAt) : NaN;
358
+ patches.push({
359
+ op: "update-turn",
360
+ turn: turn.id,
361
+ fields: {
362
+ at: new Date(now).toISOString(),
363
+ ...Number.isNaN(started) ? {} : { duration: now - started }
364
+ }
365
+ });
366
+ patches.push({ op: "set-state", turn: turn.id, state: "stopped" });
367
+ }
368
+ return patches;
369
+ }
370
+ var ChatSessionContext = React6.createContext(null);
371
+ function useChatSessionContext() {
372
+ const value = React6.useContext(ChatSessionContext);
373
+ if (!value) throw new Error("A ChatSession part was rendered outside <ChatSession>.");
374
+ return value;
375
+ }
376
+ var DEFAULT_ICONS = {
377
+ send: "\u2191",
378
+ stop: "\u25A0",
379
+ attach: "+",
380
+ close: "\xD7",
381
+ retry: "\u21BB",
382
+ copy: "\u29C9",
383
+ steps: "\u2261",
384
+ rateUp: "+1",
385
+ rateDown: "\u22121"
386
+ };
387
+ function useViewState() {
388
+ const [steps, setSteps] = React6.useState({});
389
+ const [records, setRecords] = React6.useState({});
390
+ const [ratings, setRatings] = React6.useState({});
391
+ const stepsOpen = React6.useCallback((turnId, live) => steps[turnId] ?? live, [steps]);
392
+ const toggleSteps = React6.useCallback(
393
+ (turnId, open) => setSteps((prev) => ({ ...prev, [turnId]: open ?? !(prev[turnId] ?? false) })),
394
+ []
395
+ );
396
+ const recordOpen = React6.useCallback(
397
+ (turnId, stepId) => !!records[`${turnId}/${stepId}`],
398
+ [records]
399
+ );
400
+ const toggleRecord = React6.useCallback((turnId, stepId, open) => {
401
+ const key = `${turnId}/${stepId}`;
402
+ setRecords((prev) => ({ ...prev, [key]: open ?? !prev[key] }));
403
+ }, []);
404
+ const rating = React6.useCallback((turnId) => ratings[turnId], [ratings]);
405
+ const setRating = React6.useCallback(
406
+ (turnId, value) => setRatings((prev) => ({ ...prev, [turnId]: value })),
407
+ []
408
+ );
409
+ return { stepsOpen, toggleSteps, recordOpen, toggleRecord, rating, setRating };
410
+ }
411
+ function useClock(fixed, live, every = 250) {
412
+ const [now, setNow] = React6.useState(() => fixed ?? Date.now());
413
+ React6.useEffect(() => {
414
+ if (fixed !== void 0 || !live) return;
415
+ const timer = setInterval(() => setNow(Date.now()), every);
416
+ return () => clearInterval(timer);
417
+ }, [fixed, live, every]);
418
+ return fixed ?? now;
419
+ }
420
+
421
+ // src/styles/base/format.ts
422
+ function formatDuration(ms, { spaced = false } = {}) {
423
+ const gap = spaced ? " " : "";
424
+ if (!Number.isFinite(ms) || ms < 0) return `0${gap}ms`;
425
+ if (ms < 1e3) return `${Math.round(ms)}${gap}ms`;
426
+ if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}${gap}s`;
427
+ const pair = (big, bigUnit, small, smallUnit) => small ? `${big}${gap}${bigUnit} ${small}${gap}${smallUnit}` : `${big}${gap}${bigUnit}`;
428
+ if (ms < 36e5) return pair(Math.floor(ms / 6e4), "m", Math.round(ms % 6e4 / 1e3), "s");
429
+ return pair(Math.floor(ms / 36e5), "h", Math.round(ms % 36e5 / 6e4), "m");
430
+ }
431
+ function elapsedSince(iso, now) {
432
+ if (!iso) return void 0;
433
+ const then = Date.parse(iso);
434
+ return Number.isNaN(then) ? void 0 : Math.max(0, now - then);
435
+ }
436
+ function clockTime(iso) {
437
+ if (!iso) return void 0;
438
+ const date = new Date(iso);
439
+ if (Number.isNaN(date.getTime())) return void 0;
440
+ return date.toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit", hour12: false });
441
+ }
442
+ var dayKey = (d) => `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
443
+ function sameDay(a, b) {
444
+ if (!a || !b) return true;
445
+ return dayKey(new Date(a)) === dayKey(new Date(b));
446
+ }
447
+ function dayLabel(iso, now) {
448
+ const date = new Date(iso);
449
+ const written = date.toLocaleDateString("en-GB", { weekday: "short", day: "numeric", month: "short" });
450
+ const today = new Date(now);
451
+ if (dayKey(date) === dayKey(today)) return `Today \xB7 ${written}`;
452
+ const yesterday = new Date(now - 864e5);
453
+ if (dayKey(date) === dayKey(yesterday)) return `Yesterday \xB7 ${written}`;
454
+ return written;
455
+ }
456
+
457
+ // src/styles/base/model.ts
458
+ function exchangesOf(spec) {
459
+ const out = [];
460
+ for (const turn of spec.turns) {
461
+ if (turn.role === "analyst") {
462
+ out.push({ id: turn.id, prompt: turn, replies: [] });
463
+ continue;
464
+ }
465
+ const last = out[out.length - 1];
466
+ if (last) last.replies.push(turn);
467
+ else out.push({ id: turn.id, replies: [turn] });
468
+ }
469
+ return out;
470
+ }
471
+ var isAnswer = (turn) => turn.role === "assistant" && turn.kind === "answer";
472
+ var isAsk = (turn) => turn.role === "assistant" && turn.kind === "ask";
473
+ var ACTIVE = /* @__PURE__ */ new Set(["running", "retrying", "waiting"]);
474
+ function currentStep(answer) {
475
+ return answer.trace?.find((s) => ACTIVE.has(s.state));
476
+ }
477
+ function stepTime(step, now) {
478
+ if (step.state === "running" || step.state === "retrying") return elapsedSince(step.startedAt, now);
479
+ return step.duration;
480
+ }
481
+ function answerTime(answer, now) {
482
+ if (isLive(answer)) return elapsedSince(answer.startedAt, now);
483
+ if (answer.duration !== void 0) return answer.duration;
484
+ if (answer.startedAt && answer.at) {
485
+ const span = Date.parse(answer.at) - Date.parse(answer.startedAt);
486
+ if (!Number.isNaN(span)) return span;
487
+ }
488
+ const steps = answer.trace ?? [];
489
+ return steps.some((s) => s.duration !== void 0) ? steps.reduce((sum, s) => sum + (s.duration ?? 0), 0) : void 0;
490
+ }
491
+ function runOutcome(answer) {
492
+ if (answer.state === "error") return "failed";
493
+ if (answer.state === "stopped") return "stopped";
494
+ if (isLive(answer)) return answer.trace?.some((s) => s.state === "waiting") ? "waiting" : "live";
495
+ return "done";
496
+ }
497
+ function stepCount(answer) {
498
+ const steps = answer.trace ?? [];
499
+ return { done: steps.filter((s) => s.state === "done").length, total: steps.length };
500
+ }
501
+ function exchangeState(exchange, blocks = () => true) {
502
+ let waiting = false;
503
+ for (const reply of exchange.replies) {
504
+ if (isAsk(reply) && reply.state === "pending" && blocks(reply)) waiting = true;
505
+ if (isAnswer(reply)) {
506
+ const outcome = runOutcome(reply);
507
+ if (outcome === "live") return "running";
508
+ if (outcome === "waiting") waiting = true;
509
+ }
510
+ }
511
+ return waiting ? "waiting" : "settled";
512
+ }
513
+ function threadCounts(spec, blocks) {
514
+ let running = 0;
515
+ let waiting = 0;
516
+ let steps = 0;
517
+ for (const exchange of exchangesOf(spec)) {
518
+ const state = exchangeState(exchange, blocks);
519
+ if (state === "running") running++;
520
+ if (state === "waiting") waiting++;
521
+ for (const reply of exchange.replies) if (isAnswer(reply)) steps += reply.trace?.length ?? 0;
522
+ }
523
+ return { running, waiting, steps };
524
+ }
525
+ function plainText(answer) {
526
+ return answer.blocks.map((b) => b.text).filter((t) => typeof t === "string" && t.length > 0).join("\n\n").replace(/\*\*(.+?)\*\*/g, "$1").replace(/\[(\d+)\]/g, "");
527
+ }
528
+ var turnDomId = (spec, turnId) => `chat-${spec.id}-${turnId}`;
529
+ function SessionHeader({ onClose, className }) {
530
+ const ctx = useChatSessionContext();
531
+ const prompts = ctx.spec.turns.filter((t) => t.role === "analyst").length;
532
+ return /* @__PURE__ */ jsxs("div", { className: cn("flex h-[34px] shrink-0 items-center gap-2 border-b border-border px-3", className), children: [
533
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate font-medium", children: ctx.spec.title }),
534
+ prompts ? /* @__PURE__ */ jsxs("span", { className: "shrink-0 text-muted-foreground tabular-nums", children: [
535
+ "\xB7 ",
536
+ prompts,
537
+ " ",
538
+ prompts === 1 ? "question" : "questions"
539
+ ] }) : null,
540
+ onClose ? /* @__PURE__ */ jsx(
541
+ "button",
542
+ {
543
+ type: "button",
544
+ onClick: onClose,
545
+ "aria-label": "Close the session",
546
+ title: "Close",
547
+ className: "ml-auto inline-flex size-[22px] shrink-0 items-center justify-center rounded-control text-muted-foreground hover:bg-accent hover:text-foreground",
548
+ children: ctx.icons.close
549
+ }
550
+ ) : null
551
+ ] });
552
+ }
553
+ var SETTLED_RATEABLE = /* @__PURE__ */ new Set(["complete", "partial"]);
554
+ function AnswerActions({ turn }) {
555
+ const ctx = useChatSessionContext();
556
+ const running = ctx.spec.turns.some((t) => isAnswer(t) && runOutcome(t) === "live");
557
+ const rating = ctx.rating(turn.id) ?? turn.rating;
558
+ const text = plainText(turn);
559
+ const rate = (value) => {
560
+ const next = rating === value ? 0 : value;
561
+ ctx.rate(turn.id, next);
562
+ ctx.emit({ type: "rate", turn: turn.id, value: next });
563
+ };
564
+ const built = {
565
+ retry: () => [
566
+ {
567
+ icon: ctx.icons.retry,
568
+ label: "Run again",
569
+ disabled: running,
570
+ onClick: () => ctx.emit({ type: "retry", turn: turn.id })
571
+ }
572
+ ],
573
+ copy: () => text ? [
574
+ {
575
+ icon: ctx.icons.copy,
576
+ label: "Copy",
577
+ onClick: () => {
578
+ void navigator.clipboard?.writeText(text);
579
+ ctx.emit({ type: "copy", turn: turn.id, text });
580
+ }
581
+ }
582
+ ] : [],
583
+ steps: () => {
584
+ if (!turn.trace?.length) return [];
585
+ const open = ctx.stepsOpen(turn.id, false);
586
+ return [{ icon: ctx.icons.steps, label: "Steps", active: open, onClick: () => ctx.setSteps(turn.id, !open) }];
587
+ },
588
+ rate: () => SETTLED_RATEABLE.has(turn.state) ? [
589
+ {
590
+ icon: ctx.icons.rateUp,
591
+ label: "Good answer",
592
+ active: rating === 1,
593
+ activeClassName: "text-success hover:text-success",
594
+ onClick: () => rate(1)
595
+ },
596
+ {
597
+ icon: ctx.icons.rateDown,
598
+ label: "Not what I wanted",
599
+ active: rating === -1,
600
+ activeClassName: "text-destructive hover:text-destructive",
601
+ onClick: () => rate(-1)
602
+ }
603
+ ] : []
604
+ };
605
+ const actions = ctx.actions.flatMap(
606
+ (a) => typeof a === "string" ? built[a]() : !a.states || a.states.includes(turn.state) ? [{ icon: a.icon, label: a.label, onClick: () => ctx.emit({ type: "action", turn: turn.id, action: a.id }) }] : []
607
+ );
608
+ if (!actions.length) return null;
609
+ return /* @__PURE__ */ jsx(
610
+ ChatSessionMessageOptions,
611
+ {
612
+ actions: actions.map((a) => ({
613
+ ...a,
614
+ align: "end",
615
+ className: cn(
616
+ "size-5 hover:bg-transparent [&_svg]:!size-[11px]",
617
+ !a.active && "text-muted-foreground/60 hover:text-foreground"
618
+ )
619
+ }))
620
+ }
621
+ );
622
+ }
623
+ function AnswerLine({ turn, children }) {
624
+ return /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [
625
+ children ? /* @__PURE__ */ jsx("div", { className: "min-w-0 shrink truncate", children }) : null,
626
+ /* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1", children: /* @__PURE__ */ jsx(AnswerActions, { turn }) })
627
+ ] });
628
+ }
629
+ function KeyHints({ running }) {
630
+ return running ? /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx("span", { children: "esc stop" }) }) : /* @__PURE__ */ jsxs(Fragment, { children: [
631
+ /* @__PURE__ */ jsx("span", { children: "\u21B5 send" }),
632
+ /* @__PURE__ */ jsx("span", { children: "\u21E7\u21B5 newline" })
633
+ ] });
634
+ }
635
+ function useStopKey(running, stop) {
636
+ React6.useEffect(() => {
637
+ if (!running) return;
638
+ const onKey = (e) => {
639
+ if (e.key !== "Escape" || e.defaultPrevented) return;
640
+ e.preventDefault();
641
+ stop();
642
+ };
643
+ document.addEventListener("keydown", onKey);
644
+ return () => document.removeEventListener("keydown", onKey);
645
+ }, [running, stop]);
646
+ }
647
+ var ClarifyCard = ClarifyCard$1;
648
+ var ClarifyFootnote = ClarifyFootnote$1;
649
+ var ClarifyActions = ClarifyActions$1;
650
+
651
+ // src/conversations/relative-time.ts
652
+ function relativeTime(iso, now) {
653
+ const then = Date.parse(iso);
654
+ if (Number.isNaN(then)) return void 0;
655
+ const minutes = Math.floor((now - then) / 6e4);
656
+ if (minutes < 1) return "just now";
657
+ if (minutes < 60) return `${minutes} min ago`;
658
+ const hours = Math.floor(minutes / 60);
659
+ if (hours < 24) return `${hours} h ago`;
660
+ return new Date(then).toLocaleDateString("en-GB", { day: "numeric", month: "short" });
661
+ }
662
+
663
+ // src/grammar/stages.ts
664
+ var STAGES = [
665
+ { id: "frame", name: "Frame" },
666
+ { id: "scope", name: "Scope" },
667
+ { id: "check", name: "Check data" },
668
+ { id: "analyse", name: "Analyse" },
669
+ { id: "explain", name: "Explain" },
670
+ { id: "act", name: "Act" },
671
+ { id: "monitor", name: "Monitor" }
672
+ ];
673
+
674
+ // src/grammar/patterns.ts
675
+ var PATTERNS = [
676
+ { id: "snapshot", name: "KPI snapshot", blocks: ["grid", "timeseries", "narrative", "scope"] },
677
+ { id: "bridge", name: "Change explanation", blocks: ["narrative", "waterfall", "ranked", "citations"] },
678
+ { id: "comparison", name: "Comparison", blocks: ["bars", "table", "caveat"] },
679
+ { id: "ranking", name: "Ranked list", blocks: ["ranked", "table"] },
680
+ { id: "profile", name: "Segment profile", blocks: ["table", "bars", "narrative"] },
681
+ { id: "cohort", name: "Cohort matrix", blocks: ["matrix", "narrative"] },
682
+ { id: "forecast", name: "Forecast", blocks: ["timeseries", "record", "caveat"] },
683
+ { id: "anomaly", name: "Anomaly report", blocks: ["timeseries", "timeline", "ranked"] },
684
+ { id: "drivers", name: "Driver ranking", blocks: ["ranked", "narrative", "caveat"] },
685
+ { id: "readout", name: "Experiment readout", blocks: ["metric", "record", "caveat", "proposal"] },
686
+ { id: "scenariot", name: "Scenario table", blocks: ["record", "table", "ranked"] },
687
+ { id: "funnelv", name: "Funnel", blocks: ["funnel", "table"] },
688
+ { id: "spread", name: "Distribution", blocks: ["histogram", "table"] },
689
+ { id: "quality", name: "Quality report", blocks: ["checks", "table", "files"] },
690
+ { id: "e360", name: "Entity 360", blocks: ["record", "timeline", "table"] },
691
+ { id: "relmap", name: "Relationship map", blocks: ["subgraph", "ranked"] },
692
+ { id: "memo", name: "Findings memo", blocks: ["narrative", "citations", "caveat"] },
693
+ { id: "recommend", name: "Recommendation", blocks: ["table", "ranked", "proposal"] },
694
+ { id: "delivery", name: "Scheduled delivery", blocks: ["files", "proposal", "record"] },
695
+ { id: "shortlist", name: "Candidate shortlist", blocks: ["attr", "histogram", "caveat", "proposal"] },
696
+ { id: "significance", name: "Significance test", blocks: ["test", "box", "evidence", "caveat"] },
697
+ { id: "effects", name: "Effect estimates", blocks: ["coef", "forest", "method", "caveat"] },
698
+ { id: "association", name: "Association", blocks: ["correlation", "scatter", "caveat"] },
699
+ { id: "spc", name: "Control report", blocks: ["control", "pareto", "decomposition"] },
700
+ { id: "timeto", name: "Time to event", blocks: ["survival", "quantiles", "dumbbell", "caveat"] },
701
+ { id: "modelcheck", name: "Model check", blocks: ["modeleval", "profile", "pivot", "tornado"] }
702
+ ];
703
+
704
+ // src/grammar/asks.ts
705
+ var ASK_INTENTS = [
706
+ { id: "clarify", name: "Clarify what was meant", stage: "frame", kind: "single" },
707
+ { id: "measure", name: "Choose the measure", stage: "frame", kind: "single" },
708
+ { id: "definition", name: "Confirm a definition", stage: "frame", kind: "confirm" },
709
+ { id: "hunch", name: "Share a hunch", stage: "frame", kind: "long" },
710
+ { id: "window", name: "Time window", stage: "scope", kind: "period" },
711
+ { id: "baseline", name: "Compare against", stage: "scope", kind: "single" },
712
+ { id: "filter", name: "Filter and segment", stage: "scope", kind: "multi" },
713
+ { id: "granularity", name: "Granularity", stage: "scope", kind: "quick" },
714
+ { id: "entities", name: "Pick entities", stage: "scope", kind: "entity" },
715
+ { id: "threshold", name: "Set a threshold", stage: "scope", kind: "number" },
716
+ { id: "ambiguity", name: "Resolve ambiguity", stage: "check", kind: "single" },
717
+ { id: "dataissue", name: "Handle a data issue", stage: "check", kind: "single" },
718
+ { id: "cost", name: "Approve an expensive run", stage: "check", kind: "confirm" },
719
+ { id: "method", name: "Choose a method", stage: "analyse", kind: "single" },
720
+ { id: "assumptions", name: "Confirm assumptions", stage: "analyse", kind: "multi" },
721
+ { id: "confidence", name: "Confidence level", stage: "analyse", kind: "quick" },
722
+ { id: "objectives", name: "Weigh objectives", stage: "analyse", kind: "weights" },
723
+ { id: "scenario", name: "Scenario inputs", stage: "analyse", kind: "form" },
724
+ { id: "format", name: "Choose the output", stage: "explain", kind: "quick" },
725
+ { id: "rate", name: "Rate the answer", stage: "explain", kind: "scale" },
726
+ { id: "next", name: "Suggest what's next", stage: "explain", kind: "suggestions" },
727
+ { id: "approve", name: "Approve an action", stage: "act", kind: "approval" },
728
+ { id: "schedule", name: "Schedule and share", stage: "monitor", kind: "multistep" },
729
+ { id: "reading", name: "Check my reading", stage: "frame", kind: "interpretation" },
730
+ { id: "fork", name: "Pick a reading", stage: "frame", kind: "fork" },
731
+ { id: "range", name: "Set a range", stage: "scope", kind: "range" },
732
+ { id: "model", name: "Specify the model", stage: "analyse", kind: "modelspec" },
733
+ { id: "plan", name: "Review the plan", stage: "analyse", kind: "plan" },
734
+ { id: "hypothesis", name: "State the hypothesis", stage: "analyse", kind: "hypothesis" }
735
+ ];
736
+
737
+ // src/grammar/answers.ts
738
+ var ANSWER_INTENTS = [
739
+ { id: "say", name: "Say it in words", kinds: ["narrative"] },
740
+ { id: "figure", name: "One number, or a few", kinds: ["metric", "grid"] },
741
+ { id: "trend", name: "Change over time", kinds: ["timeseries", "control"] },
742
+ { id: "compare", name: "Groups side by side", kinds: ["bars", "dumbbell", "matrix", "funnel"] },
743
+ { id: "rank", name: "Leaders and laggards", kinds: ["ranked", "pareto", "tornado"] },
744
+ { id: "explain-change", name: "What moved the total", kinds: ["waterfall", "decomposition"] },
745
+ { id: "records", name: "Rows, or one entity", kinds: ["table", "record", "attr", "pivot", "profile"] },
746
+ { id: "sequence", name: "What happened when", kinds: ["timeline", "heatstrip"] },
747
+ { id: "spread", name: "The shape of the values", kinds: ["histogram", "box", "quantiles", "survival"] },
748
+ { id: "relate", name: "How things connect", kinds: ["scatter", "correlation", "subgraph"] },
749
+ { id: "stats", name: "Test and model results", kinds: ["test", "coef", "forest", "evidence", "modeleval"] },
750
+ { id: "trust", name: "Method, sources, scope and limits", kinds: ["method", "citations", "scope", "caveat", "cannot", "checks"] },
751
+ { id: "act", name: "A proposed action, or a file", kinds: ["proposal", "files"] },
752
+ { id: "run.steps", name: "What the run did, in order", kinds: ["trace", "gantt"] },
753
+ { id: "run.activity", name: "Which layers were busy", kinds: ["activity"] }
754
+ ];
755
+
756
+ // src/grammar/flows.ts
757
+ var FLOWS = [
758
+ { id: "kpi", name: "KPI check", template: "How is {measure} tracking against {baseline}?", stages: ["frame", "scope", "analyse", "explain"], asks: ["measure", "window", "baseline"], pattern: "snapshot" },
759
+ { id: "diagnose", name: "Root cause", template: "Why did {measure} change in {period}?", stages: ["frame", "scope", "check", "analyse", "explain", "act"], asks: ["measure", "window", "baseline", "hunch"], pattern: "bridge" },
760
+ { id: "compare", name: "Comparison & benchmark", template: "How does {A} compare with {B} on {measures}?", stages: ["frame", "scope", "analyse", "explain"], asks: ["entities", "measure", "confidence"], pattern: "comparison" },
761
+ { id: "rank", name: "Top-N ranking", template: "Which {entities} lead or lag on {measure}?", stages: ["frame", "scope", "analyse", "explain", "act"], asks: ["measure", "filter", "threshold"], pattern: "ranking" },
762
+ { id: "segment", name: "Segmentation", template: "What groups exist within {population}, and how do they differ?", stages: ["frame", "scope", "analyse", "explain"], asks: ["filter", "method", "assumptions"], pattern: "profile" },
763
+ { id: "cohort", name: "Cohort & retention", template: "How do {cohorts} behave over time?", stages: ["frame", "scope", "analyse", "explain"], asks: ["definition", "window", "granularity"], pattern: "cohort" },
764
+ { id: "forecast", name: "Trend & forecast", template: "Where is {measure} heading?", stages: ["frame", "scope", "analyse", "explain", "monitor"], asks: ["window", "granularity", "method", "assumptions"], pattern: "forecast" },
765
+ { id: "anomaly", name: "Anomaly & monitoring", template: "Tell me when {measure} behaves unusually.", stages: ["scope", "analyse", "act", "monitor"], asks: ["measure", "threshold", "approve", "schedule"], pattern: "anomaly" },
766
+ { id: "drivers", name: "Driver analysis", template: "What moves {measure}?", stages: ["frame", "analyse", "explain"], asks: ["measure", "method", "assumptions"], pattern: "drivers" },
767
+ { id: "experiment", name: "Experiment readout", template: "Did {change} work?", stages: ["frame", "check", "analyse", "explain", "act"], asks: ["entities", "definition", "confidence"], pattern: "readout" },
768
+ { id: "scenario", name: "Scenario & what-if", template: "What happens to {outcome} if {input} changes?", stages: ["frame", "scope", "analyse", "explain", "act"], asks: ["scenario", "assumptions"], pattern: "scenariot" },
769
+ { id: "funnel", name: "Funnel & journey", template: "Where do {entities} drop out of {process}?", stages: ["frame", "scope", "analyse", "explain"], asks: ["definition", "window", "filter"], pattern: "funnelv" },
770
+ { id: "distribution", name: "Distribution & outliers", template: "What does the spread of {measure} look like, and what sits outside it?", stages: ["scope", "analyse", "explain"], asks: ["measure", "filter", "threshold"], pattern: "spread" },
771
+ { id: "audit", name: "Data audit & reconciliation", template: "Can I trust {dataset}? Do {A} and {B} agree?", stages: ["check", "analyse", "explain", "act"], asks: ["definition", "dataissue", "cost"], pattern: "quality" },
772
+ { id: "entity", name: "Entity lookup (360)", template: "Tell me everything about {entity}.", stages: ["frame", "check", "explain"], asks: ["ambiguity", "entities"], pattern: "e360" },
773
+ { id: "network", name: "Relationships & networks", template: "How are {entities} connected?", stages: ["frame", "scope", "check", "analyse", "explain"], asks: ["entities", "threshold", "cost"], pattern: "relmap" },
774
+ { id: "qual", name: "Qualitative synthesis", template: "What are people saying about {topic}?", stages: ["frame", "scope", "analyse", "explain"], asks: ["filter", "window", "hunch"], pattern: "memo" },
775
+ { id: "recommend", name: "Recommendation & priority", template: "What should we do about {problem}, and in what order?", stages: ["analyse", "explain", "act"], asks: ["assumptions", "method", "approve"], pattern: "recommend" },
776
+ { id: "report", name: "Report & schedule", template: "Send me {analysis} every {cadence}.", stages: ["explain", "act", "monitor"], asks: ["format", "schedule", "approve"], pattern: "delivery" },
777
+ { id: "combine", name: "Combination design", template: "Which combination of {candidates} best meets {objectives}?", stages: ["frame", "scope", "analyse", "explain", "act"], asks: ["entities", "objectives", "threshold", "assumptions"], pattern: "shortlist" },
778
+ { id: "significance", name: "Significance test", template: "Is the difference in {measure} between {A} and {B} real?", stages: ["frame", "scope", "analyse", "explain"], asks: ["hypothesis", "confidence", "assumptions"], pattern: "significance" },
779
+ { id: "effects", name: "Effect estimation", template: "How much does each {factor} change {outcome}, holding the others fixed?", stages: ["frame", "scope", "analyse", "explain"], asks: ["model", "plan", "assumptions"], pattern: "effects" },
780
+ { id: "association", name: "Correlation & association", template: "Which {measures} move together, and how strongly?", stages: ["frame", "scope", "check", "analyse", "explain"], asks: ["reading", "filter", "range"], pattern: "association" },
781
+ { id: "spc", name: "Process control", template: "Is {process} in control, and what causes most of the defects?", stages: ["scope", "analyse", "explain", "monitor"], asks: ["window", "granularity", "range"], pattern: "spc" },
782
+ { id: "timeto", name: "Time to event", template: "How long until {event}, and does it differ by {group}?", stages: ["frame", "scope", "analyse", "explain"], asks: ["definition", "fork", "window"], pattern: "timeto" },
783
+ { id: "modelcheck", name: "Model check", template: "How well does {model} predict {outcome}, and where does it fail?", stages: ["check", "analyse", "explain", "act"], asks: ["model", "range", "plan"], pattern: "modelcheck" }
784
+ ];
785
+ var STAGE_NAME = new Map(STAGES.map((s) => [s.id, s.name]));
786
+ var stageName = (stage) => (STAGE_NAME.get(stage) ?? stage).toLowerCase();
787
+ function AskView({ turn, registry, onEvent, now }) {
788
+ const Renderer = registry.asks[turn.ask.kind];
789
+ const traits = registry.askTraits(turn.ask.kind);
790
+ if (Renderer && traits.frame === "none") {
791
+ return /* @__PURE__ */ jsx(Renderer, { turn, options: turn.ask, onEvent });
792
+ }
793
+ const time = turn.state === "answered" && turn.answeredAt ? relativeTime(turn.answeredAt, now) : void 0;
794
+ return /* @__PURE__ */ jsx(
795
+ ClarifyCard,
796
+ {
797
+ state: turn.state,
798
+ kind: traits.kind,
799
+ step: stageName(turn.stage),
800
+ waiting: turn.state === "pending" ? turn.waiting : void 0,
801
+ time,
802
+ children: Renderer ? /* @__PURE__ */ jsx(Renderer, { turn, options: turn.ask, onEvent }) : /* @__PURE__ */ jsx(Placeholder, { kind: turn.ask.kind, options: turn.ask })
803
+ }
804
+ );
805
+ }
806
+ var LINES = [[0.6], [0.75], [0.5]];
807
+ var CHART = [[1, 110], [0.55, 7]];
808
+ var ROWS = [[0.92], [0.74], [0.55], [0.38], [0.4, 7]];
809
+ var SHAPE = {
810
+ metric: [[0.45], [0.3, 22], [0.55]],
811
+ grid: [[1, 56]],
812
+ record: [[0.6], [0.75], [0.5], [0.68]],
813
+ table: [[1, 7], [1], [1], [1]],
814
+ attr: [[1, 7], [1], [1], [1]],
815
+ pivot: [[1, 7], [1], [1], [1]],
816
+ coef: [[1, 7], [1], [1], [1]],
817
+ files: [[0.7], [0.55], [0.62]],
818
+ ranked: ROWS,
819
+ timeline: ROWS,
820
+ funnel: ROWS,
821
+ pareto: ROWS,
822
+ tornado: ROWS,
823
+ forest: ROWS,
824
+ dumbbell: ROWS,
825
+ timeseries: CHART,
826
+ bars: CHART,
827
+ waterfall: CHART,
828
+ histogram: CHART,
829
+ matrix: CHART,
830
+ correlation: CHART,
831
+ scatter: CHART,
832
+ box: CHART,
833
+ control: CHART,
834
+ survival: CHART,
835
+ decomposition: CHART,
836
+ modeleval: CHART,
837
+ quantiles: CHART,
838
+ subgraph: CHART
839
+ };
840
+ function BlockSkeleton({ kind }) {
841
+ const bars = SHAPE[kind] ?? LINES;
842
+ return /* @__PURE__ */ jsx("div", { role: "status", "aria-label": "Loading", className: "flex flex-col gap-1.5 py-0.5", children: bars.map(([width, height], i) => /* @__PURE__ */ jsx(
843
+ Skeleton,
844
+ {
845
+ style: { width: `${width * 100}%`, height: height ?? 9 }
846
+ },
847
+ i
848
+ )) });
849
+ }
850
+ function BlockEmpty({
851
+ text,
852
+ suggestions,
853
+ onSuggest
854
+ }) {
855
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
856
+ /* @__PURE__ */ jsx("p", { className: "m-0 rounded-md border border-dashed border-border px-2 py-3.5 text-center text-sm text-muted-foreground", children: text }),
857
+ suggestions?.length ? /* @__PURE__ */ jsx(SuggestionChips, { items: suggestions, onSelect: onSuggest }) : null
858
+ ] });
859
+ }
860
+ function BlockCaption({ text, tone }) {
861
+ return /* @__PURE__ */ jsx(
862
+ "p",
863
+ {
864
+ className: cn(
865
+ "m-0 text-xs",
866
+ tone === "warn" ? "text-warning" : tone === "bad" ? "text-destructive" : "text-muted-foreground"
867
+ ),
868
+ children: text
869
+ }
870
+ );
871
+ }
872
+ function BlockView({ block, turn, registry, onEvent }) {
873
+ const Renderer = registry.blocks[block.kind];
874
+ const body = block.status === "loading" ? /* @__PURE__ */ jsx(BlockSkeleton, { kind: block.kind }) : block.status === "empty" ? /* @__PURE__ */ jsx(
875
+ BlockEmpty,
876
+ {
877
+ text: block.emptyText ?? "Nothing to show.",
878
+ suggestions: block.emptySuggestions,
879
+ onSuggest: (text) => onEvent({ type: "prompt", text })
880
+ }
881
+ ) : !Renderer ? /* @__PURE__ */ jsx(Placeholder, { kind: block.kind, options: block }) : /* @__PURE__ */ jsx(Renderer, { block, turn, onEvent });
882
+ if (!block.caption || block.status) return body;
883
+ return /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-col gap-1.5", children: [
884
+ body,
885
+ /* @__PURE__ */ jsx(BlockCaption, { text: block.caption, tone: block.captionTone })
886
+ ] });
887
+ }
888
+ function envelopeBlocks(turn) {
889
+ const env = turn.envelope;
890
+ if (!env) return { top: [], bottom: [] };
891
+ const parts = [...env.scope ?? [], ...env.freshness ? [`as of ${env.freshness}`] : []];
892
+ const top = parts.length ? [{ kind: "scope", parts }] : [];
893
+ const bottom = [
894
+ ...env.method ? [{ kind: "method", label: "method", code: env.method }] : [],
895
+ ...(env.caveats ?? []).map((c) => ({ kind: "caveat", label: c.label, text: c.text }))
896
+ ];
897
+ return { top, bottom };
898
+ }
899
+ function splitBlocks(turn, registry) {
900
+ const { top, bottom } = envelopeBlocks(turn);
901
+ const own = turn.blocks.filter((b) => registry.blockTraits(b.kind).placement === "own");
902
+ const inCard = turn.blocks.filter((b) => registry.blockTraits(b.kind).placement !== "own");
903
+ return { inCard: [...top, ...inCard, ...bottom], own, hasEvidence: inCard.length > 0 };
904
+ }
905
+ function citationOf(turn) {
906
+ const g = turn.envelope?.grounding;
907
+ return g ? `cite \xB7 ${g.records.toLocaleString("en-GB")}${g.noun ? ` ${g.noun}` : ""}` : void 0;
908
+ }
909
+ function OutcomeView({
910
+ outcome,
911
+ turn,
912
+ onEvent,
913
+ tone,
914
+ className
915
+ }) {
916
+ return /* @__PURE__ */ jsxs(
917
+ "div",
918
+ {
919
+ className: cn(
920
+ "flex min-w-0 flex-wrap items-center gap-x-3 gap-y-1.5 rounded-md border border-border px-2 py-1.5",
921
+ tone === "bad" && "border-destructive/50",
922
+ className
923
+ ),
924
+ children: [
925
+ outcome.text ? /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 text-muted-foreground", children: outcome.text }) : null,
926
+ outcome.actions?.length ? /* @__PURE__ */ jsx("div", { className: cn("min-w-0", outcome.text ? "shrink-0" : "flex-1"), children: /* @__PURE__ */ jsx(
927
+ ActionRow,
928
+ {
929
+ actions: outcome.actions,
930
+ onAction: (action) => onEvent({ type: "action", turn: turn.id, action })
931
+ }
932
+ ) }) : null
933
+ ]
934
+ }
935
+ );
936
+ }
937
+ function BlockStack({ children, className }) {
938
+ return /* @__PURE__ */ jsx("div", { className: cn("flex min-w-0 flex-col gap-3", className), children });
939
+ }
940
+ function PathIcon({ path, className }) {
941
+ return /* @__PURE__ */ jsx(
942
+ "svg",
943
+ {
944
+ viewBox: "0 0 16 16",
945
+ fill: "none",
946
+ stroke: "currentColor",
947
+ strokeWidth: 1.3,
948
+ strokeLinecap: "round",
949
+ strokeLinejoin: "round",
950
+ "aria-hidden": true,
951
+ className: cn("size-3.5 shrink-0", className),
952
+ children: /* @__PURE__ */ jsx("path", { d: path })
953
+ }
954
+ );
955
+ }
956
+ function ComposerSelect({ control, value, onValueChange, grow }) {
957
+ return /* @__PURE__ */ jsxs(Select, { value, onValueChange, children: [
958
+ /* @__PURE__ */ jsxs(
959
+ SelectTrigger,
960
+ {
961
+ "aria-label": control.label,
962
+ title: control.label,
963
+ className: cn(
964
+ "h-control-sm gap-1 border-0 bg-transparent px-2 shadow-none hover:bg-accent",
965
+ grow ? "w-full min-w-0" : "w-auto shrink-0",
966
+ control.quiet && "text-muted-foreground"
967
+ ),
968
+ children: [
969
+ control.icon ? /* @__PURE__ */ jsx(PathIcon, { path: control.icon }) : null,
970
+ /* @__PURE__ */ jsx(SelectValue, {})
971
+ ]
972
+ }
973
+ ),
974
+ /* @__PURE__ */ jsx(SelectContent, { children: control.options.map((o) => /* @__PURE__ */ jsx(SelectItem, { value: o.value, children: o.label }, o.value)) })
975
+ ] });
976
+ }
977
+ function AttachmentChip({ name, onRemove }) {
978
+ return /* @__PURE__ */ jsxs("span", { className: "inline-flex max-w-40 items-center gap-1 rounded-control border border-border bg-muted/40 px-2 py-1 text-sm", children: [
979
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: name }),
980
+ onRemove ? /* @__PURE__ */ jsx(
981
+ "button",
982
+ {
983
+ type: "button",
984
+ onClick: onRemove,
985
+ "aria-label": `Remove ${name}`,
986
+ className: "shrink-0 rounded-sm px-0.5 text-muted-foreground hover:bg-accent hover:text-foreground",
987
+ children: "\xD7"
988
+ }
989
+ ) : null
990
+ ] });
991
+ }
992
+ function initialSettings(composer) {
993
+ const out = {};
994
+ for (const c of composer?.controls ?? []) out[c.id] = c.default ?? c.options[0]?.value ?? "";
995
+ return out;
996
+ }
997
+ function ComposerBar({ isRunning: isRunning2, onStop }) {
998
+ const { spec, emit, icons, setComposer } = useChatSessionContext();
999
+ const composer = spec.composer;
1000
+ const controls = composer?.controls ?? [];
1001
+ const [draft, setDraft] = React6.useState("");
1002
+ const [files, setFiles] = React6.useState([]);
1003
+ const [picks, setPicks] = React6.useState({});
1004
+ const settings = initialSettings(composer);
1005
+ for (const c of controls) {
1006
+ const pick = picks[c.id];
1007
+ if (pick !== void 0 && c.options.some((o) => o.value === pick)) settings[c.id] = pick;
1008
+ }
1009
+ const fileInput = React6.useRef(null);
1010
+ const picked = controls.map((c) => c.options.find((o) => o.value === settings[c.id])).filter(Boolean);
1011
+ const placeholder = picked.find((o) => o?.placeholder)?.placeholder ?? composer?.placeholder;
1012
+ const mono = picked.some((o) => o?.mono);
1013
+ const setting = (id, value) => {
1014
+ setPicks((prev) => ({ ...prev, [id]: value }));
1015
+ emit({ type: "setting", id, value });
1016
+ };
1017
+ const send = () => {
1018
+ const text = draft.trim();
1019
+ if (!text && !files.length) return;
1020
+ emit({
1021
+ type: "prompt",
1022
+ text,
1023
+ ...controls.length ? { settings } : {},
1024
+ ...files.length ? { files } : {}
1025
+ });
1026
+ setDraft("");
1027
+ setFiles([]);
1028
+ };
1029
+ const attach = composer?.attach;
1030
+ const attachOptions = typeof attach === "object" ? attach : {};
1031
+ const select = (c, grow) => /* @__PURE__ */ jsx(
1032
+ ComposerSelect,
1033
+ {
1034
+ control: c,
1035
+ value: settings[c.id] ?? "",
1036
+ onValueChange: (v) => setting(c.id, v),
1037
+ grow
1038
+ },
1039
+ c.id
1040
+ );
1041
+ const start = controls.filter((c) => (c.align ?? "start") === "start");
1042
+ const end = controls.filter((c) => c.align === "end");
1043
+ return /* @__PURE__ */ jsx("div", { ref: setComposer, children: /* @__PURE__ */ jsx(
1044
+ ChatSessionComposer,
1045
+ {
1046
+ value: draft,
1047
+ onChange: setDraft,
1048
+ onSend: send,
1049
+ onStop,
1050
+ isRunning: isRunning2,
1051
+ placeholder,
1052
+ textareaClassName: cn("h-[72px] min-h-14", mono && "font-mono"),
1053
+ sendIcon: icons.send,
1054
+ stopIcon: icons.stop,
1055
+ sendDisabled: isRunning2 || !draft.trim() && !files.length,
1056
+ attachments: files.length ? files.map((f, i) => /* @__PURE__ */ jsx(
1057
+ AttachmentChip,
1058
+ {
1059
+ name: f.name,
1060
+ onRemove: () => setFiles((prev) => prev.filter((_, j) => j !== i))
1061
+ },
1062
+ `${f.name}-${i}`
1063
+ )) : void 0,
1064
+ toolbarStart: start.length ? /* @__PURE__ */ jsx(Fragment, { children: start.map((c, i) => select(c, i === start.length - 1)) }) : void 0,
1065
+ toolbarEnd: end.length || attach ? /* @__PURE__ */ jsxs(Fragment, { children: [
1066
+ end.map((c) => select(c)),
1067
+ attach ? /* @__PURE__ */ jsxs(Fragment, { children: [
1068
+ /* @__PURE__ */ jsx(
1069
+ "input",
1070
+ {
1071
+ ref: fileInput,
1072
+ type: "file",
1073
+ hidden: true,
1074
+ accept: attachOptions.accept,
1075
+ multiple: attachOptions.multiple ?? true,
1076
+ onChange: (e) => {
1077
+ const list = e.target.files;
1078
+ if (list?.length) setFiles((prev) => [...prev, ...Array.from(list)]);
1079
+ e.target.value = "";
1080
+ }
1081
+ }
1082
+ ),
1083
+ /* @__PURE__ */ jsx(
1084
+ "button",
1085
+ {
1086
+ type: "button",
1087
+ onClick: () => fileInput.current?.click(),
1088
+ title: "Attach files",
1089
+ "aria-label": "Attach files",
1090
+ className: "inline-flex size-7 shrink-0 items-center justify-center rounded-control text-muted-foreground hover:bg-accent",
1091
+ children: icons.attach
1092
+ }
1093
+ )
1094
+ ] }) : null
1095
+ ] }) : void 0
1096
+ }
1097
+ ) });
1098
+ }
1099
+ var ROW_STATUS = {
1100
+ pending: "queued",
1101
+ running: "running",
1102
+ done: "success",
1103
+ failed: "error",
1104
+ waiting: "needs-input",
1105
+ retrying: "needs-input",
1106
+ stopped: "needs-input"
1107
+ };
1108
+ var TONE = {
1109
+ failed: "text-destructive",
1110
+ waiting: "text-warning",
1111
+ retrying: "text-warning",
1112
+ stopped: "text-warning"
1113
+ };
1114
+ function stepDescription(step) {
1115
+ const detail = step.state === "failed" ? step.error ?? step.detail : step.detail;
1116
+ switch (step.state) {
1117
+ case "retrying":
1118
+ return [`retrying ${step.attempt ?? 2}${step.attempts ? `/${step.attempts}` : ""}`, detail].filter(Boolean).join(" \xB7 ");
1119
+ case "waiting":
1120
+ return ["needs input", detail].filter(Boolean).join(" \xB7 ");
1121
+ case "stopped":
1122
+ return "stopped";
1123
+ default:
1124
+ return detail;
1125
+ }
1126
+ }
1127
+ function IoRows({ label, rows }) {
1128
+ return /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-col gap-0.5", children: [
1129
+ /* @__PURE__ */ jsx(Eyebrow, { children: label }),
1130
+ /* @__PURE__ */ jsx(PropertyList, { labelWidth: "auto", variant: "summary", children: rows.map((row, i) => /* @__PURE__ */ jsx(PropertyRow, { label: row.label, mono: row.code, children: row.code ? /* @__PURE__ */ jsx("pre", { className: "m-0 whitespace-pre-wrap break-words", children: row.value }) : row.value }, `${row.label}-${i}`)) })
1131
+ ] });
1132
+ }
1133
+ function StepRecord({ step }) {
1134
+ const input = step.io?.input ?? [];
1135
+ const output = step.io?.output ?? [];
1136
+ return /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-col gap-2", children: [
1137
+ input.length ? /* @__PURE__ */ jsx(IoRows, { label: "Input", rows: input }) : null,
1138
+ output.length ? /* @__PURE__ */ jsx(IoRows, { label: "Output", rows: output }) : null,
1139
+ step.error ? /* @__PURE__ */ jsx(IoRows, { label: "Error", rows: [{ label: "cause", value: step.error }] }) : null
1140
+ ] });
1141
+ }
1142
+ var hasRecord = (step) => !!(step.io?.input?.length || step.io?.output?.length || step.error);
1143
+ function StepRow({ turn, step, detail = true, onSelect, suffix }) {
1144
+ const ctx = useChatSessionContext();
1145
+ const id = step.id ?? step.label;
1146
+ const time = stepTime(step, ctx.now);
1147
+ const description = [stepDescription(step), suffix].filter(Boolean).join(" \xB7 ");
1148
+ const open = detail && ctx.recordOpen(turn.id, id);
1149
+ const select = onSelect ?? (hasRecord(step) ? () => ctx.toggleRecord(turn.id, id) : ctx.opensRuns ? () => ctx.emit({ type: "open-run", turn: turn.id, step: step.id }) : void 0);
1150
+ return /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-col", children: [
1151
+ /* @__PURE__ */ jsx(
1152
+ ChatSessionTaskRow,
1153
+ {
1154
+ status: ROW_STATUS[step.state],
1155
+ name: step.label,
1156
+ description: description ? /* @__PURE__ */ jsx("span", { className: TONE[step.state], children: description }) : void 0,
1157
+ meta: time !== void 0 ? formatDuration(time) : void 0,
1158
+ onClick: select
1159
+ }
1160
+ ),
1161
+ detail && step.thinking && (step.state === "running" || step.state === "retrying") ? /* @__PURE__ */ jsx(ChatSessionActivitySubLine, { className: "pl-[22px] italic", children: step.thinking }) : null,
1162
+ open && hasRecord(step) ? /* @__PURE__ */ jsx(
1163
+ ChatSessionDisclosure,
1164
+ {
1165
+ className: "my-0.5 ml-[22px]",
1166
+ label: /* @__PURE__ */ jsx("span", { className: "font-mono", children: step.key ?? step.label }),
1167
+ meta: [
1168
+ step.attempt ? `attempt ${step.attempt}` : void 0,
1169
+ step.duration !== void 0 ? formatDuration(step.duration) : void 0
1170
+ ].filter(Boolean).join(" \xB7 "),
1171
+ open: true,
1172
+ onOpenChange: () => ctx.toggleRecord(turn.id, id, false),
1173
+ children: /* @__PURE__ */ jsx(StepRecord, { step })
1174
+ }
1175
+ ) : null
1176
+ ] });
1177
+ }
1178
+ function StepList({ turn, className }) {
1179
+ return /* @__PURE__ */ jsx("div", { className: cn("flex min-w-0 flex-col gap-px", className), children: (turn.trace ?? []).map((step, i) => /* @__PURE__ */ jsx(StepRow, { turn, step }, step.id ?? i)) });
1180
+ }
1181
+ var VERB = { done: "Thought for", failed: "Failed after", stopped: "Stopped after" };
1182
+ function RunSummaryLine({ turn }) {
1183
+ const ctx = useChatSessionContext();
1184
+ const outcome = runOutcome(turn);
1185
+ const verb = VERB[outcome === "failed" || outcome === "stopped" ? outcome : "done"];
1186
+ const time = answerTime(turn, ctx.now);
1187
+ const { done, total } = stepCount(turn);
1188
+ const open = ctx.stepsOpen(turn.id, false);
1189
+ return /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center gap-2 text-muted-foreground", children: [
1190
+ /* @__PURE__ */ jsx("span", { className: "select-none text-border", "aria-hidden": true, children: "\u273B" }),
1191
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0 truncate", children: [
1192
+ verb,
1193
+ " ",
1194
+ time !== void 0 ? /* @__PURE__ */ jsx(
1195
+ "button",
1196
+ {
1197
+ type: "button",
1198
+ className: "tabular-nums underline-offset-2 hover:text-foreground hover:underline",
1199
+ onClick: () => ctx.opensRuns ? ctx.emit({ type: "open-run", turn: turn.id }) : ctx.setSteps(turn.id, !open),
1200
+ children: formatDuration(time)
1201
+ }
1202
+ ) : "\u2014",
1203
+ total ? ` \xB7 ${done} of ${total} steps` : null
1204
+ ] }),
1205
+ total ? /* @__PURE__ */ jsx(
1206
+ "button",
1207
+ {
1208
+ type: "button",
1209
+ "aria-expanded": open,
1210
+ "aria-label": open ? "Fold the steps" : "Show the steps",
1211
+ className: "hover:text-foreground",
1212
+ onClick: () => ctx.setSteps(turn.id, !open),
1213
+ children: /* @__PURE__ */ jsx("span", { className: cn("inline-block transition-transform", open && "rotate-90"), children: "\u25B8" })
1214
+ }
1215
+ ) : null
1216
+ ] });
1217
+ }
1218
+ var ANSWER_DOT = {
1219
+ live: "pending",
1220
+ waiting: "info",
1221
+ done: "success",
1222
+ failed: "error",
1223
+ stopped: "warning"
1224
+ };
1225
+ var ASK_DOT = {
1226
+ pending: "info",
1227
+ answered: "success",
1228
+ skipped: "default",
1229
+ superseded: "default",
1230
+ expired: "default"
1231
+ };
1232
+ function useJump() {
1233
+ const ctx = useChatSessionContext();
1234
+ return (turnId) => {
1235
+ ctx.setView("chat");
1236
+ ctx.toggleSteps(turnId, true);
1237
+ ctx.scrollToTurn(turnId);
1238
+ };
1239
+ }
1240
+ function CliAnswer({ turn }) {
1241
+ const ctx = useChatSessionContext();
1242
+ const outcome = runOutcome(turn);
1243
+ const live = outcome === "live" || outcome === "waiting";
1244
+ const { inCard, own, hasEvidence } = splitBlocks(turn, ctx.registry);
1245
+ const blocks = [...inCard, ...own];
1246
+ const step = currentStep(turn);
1247
+ const time = answerTime(turn, ctx.now);
1248
+ const showSteps = !!turn.trace?.length && (live || ctx.stepsOpen(turn.id, false));
1249
+ const body = live && !hasEvidence ? /* @__PURE__ */ jsx(
1250
+ ChatSessionProgressLine,
1251
+ {
1252
+ className: "-ml-[22px]",
1253
+ elapsed: time !== void 0 ? formatDuration(time) : void 0,
1254
+ spinner: outcome === "waiting" ? /* @__PURE__ */ jsx("span", { className: "size-1.5 rounded-full bg-info", "aria-hidden": true }) : void 0,
1255
+ children: outcome === "waiting" ? "Waiting on your answer" : step ? `${step.label}\u2026` : "Planning\u2026"
1256
+ }
1257
+ ) : blocks.length ? /* @__PURE__ */ jsx(BlockStack, { className: "whitespace-normal", children: blocks.map((b, i) => /* @__PURE__ */ jsx(BlockView, { block: b, turn, registry: ctx.registry, onEvent: ctx.emit }, `${b.kind}-${i}`)) }) : outcome === "failed" ? "The run failed." : outcome === "stopped" ? "Stopped by you." : null;
1258
+ return /* @__PURE__ */ jsx(
1259
+ ChatSessionActivityRow,
1260
+ {
1261
+ status: ANSWER_DOT[outcome],
1262
+ meta: !live && turn.meta ? turn.meta : void 0,
1263
+ footer: /* @__PURE__ */ jsxs(Fragment, { children: [
1264
+ live && turn.trace?.length ? /* @__PURE__ */ jsx(StepList, { turn, className: "pt-0.5" }) : null,
1265
+ !live ? (
1266
+ // The run in one line, and what can be done with the answer at its right.
1267
+ /* @__PURE__ */ jsx(AnswerLine, { turn, children: turn.trace?.length ? /* @__PURE__ */ jsx(RunSummaryLine, { turn }) : null })
1268
+ ) : null,
1269
+ !live && showSteps ? /* @__PURE__ */ jsx(StepList, { turn, className: "pt-0.5" }) : null,
1270
+ turn.outcome && !live ? /* @__PURE__ */ jsx(
1271
+ OutcomeView,
1272
+ {
1273
+ className: "mt-0.5",
1274
+ outcome: turn.outcome,
1275
+ turn,
1276
+ onEvent: ctx.emit,
1277
+ tone: outcome === "failed" ? "bad" : void 0
1278
+ }
1279
+ ) : null,
1280
+ outcome === "stopped" ? /* @__PURE__ */ jsx(ChatSessionActivitySubLine, { children: "Interrupted \xB7 ask again, or narrow the question" }) : null
1281
+ ] }),
1282
+ children: body
1283
+ }
1284
+ );
1285
+ }
1286
+ function CliAsk({ turn }) {
1287
+ const ctx = useChatSessionContext();
1288
+ return /* @__PURE__ */ jsx(ChatSessionActivityRow, { status: ASK_DOT[turn.state] ?? "default", children: /* @__PURE__ */ jsx("div", { className: "whitespace-normal", children: /* @__PURE__ */ jsx(AskView, { turn, registry: ctx.registry, onEvent: ctx.emit, now: ctx.now }) }) });
1289
+ }
1290
+ function CliExchange({ exchange }) {
1291
+ const ctx = useChatSessionContext();
1292
+ const prompt = exchange.prompt;
1293
+ return /* @__PURE__ */ jsxs("div", { className: "contents", children: [
1294
+ prompt ? /* @__PURE__ */ jsx("div", { id: turnDomId(ctx.spec, prompt.id), className: "scroll-mt-3", children: /* @__PURE__ */ jsx(ChatSessionPromptRow, { meta: prompt.at ? /* @__PURE__ */ jsx("span", { className: "tabular-nums", children: clockTime(prompt.at) }) : void 0, children: prompt.text }) }) : null,
1295
+ exchange.replies.map((reply) => /* @__PURE__ */ jsx("div", { id: turnDomId(ctx.spec, reply.id), className: "min-w-0 scroll-mt-3", children: isAnswer(reply) ? /* @__PURE__ */ jsx(CliAnswer, { turn: reply }) : /* @__PURE__ */ jsx(CliAsk, { turn: reply }) }, reply.id))
1296
+ ] });
1297
+ }
1298
+ var WEIGHT = { running: 0, waiting: 1, settled: 2 };
1299
+ function CliTasks() {
1300
+ const ctx = useChatSessionContext();
1301
+ const jump = useJump();
1302
+ const blocks = (ask) => ctx.registry.askTraits(ask.ask.kind).frame === "card";
1303
+ const groups = exchangesOf(ctx.spec).filter((e) => e.replies.some((r) => isAnswer(r) && r.trace?.length)).reverse().sort((a, b) => WEIGHT[exchangeState(a, blocks)] - WEIGHT[exchangeState(b, blocks)]);
1304
+ return /* @__PURE__ */ jsx(Fragment, { children: groups.map((exchange) => {
1305
+ const state = exchangeState(exchange, blocks);
1306
+ const answers = exchange.replies.filter(isAnswer);
1307
+ const total = answers.reduce((sum, a) => sum + (answerTime(a, ctx.now) ?? 0), 0);
1308
+ const when = clockTime(exchange.prompt?.at);
1309
+ const said = state === "running" ? "running" : state === "waiting" ? "waiting on you" : formatDuration(total);
1310
+ return /* @__PURE__ */ jsx(
1311
+ ChatSessionTaskGroup,
1312
+ {
1313
+ heading: /* @__PURE__ */ jsxs("div", { className: "flex justify-between gap-2", children: [
1314
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate", title: exchange.prompt?.text, children: exchange.prompt?.text ?? ctx.spec.title }),
1315
+ /* @__PURE__ */ jsx("span", { className: "shrink-0 font-normal text-muted-foreground tabular-nums", children: [when, said].filter(Boolean).join(" \xB7 ") })
1316
+ ] }),
1317
+ children: answers.flatMap(
1318
+ (a) => (a.trace ?? []).map((s, i) => /* @__PURE__ */ jsx(StepRow, { turn: a, step: s, detail: false, onSelect: () => jump(a.id) }, `${a.id}-${s.id ?? i}`))
1319
+ )
1320
+ },
1321
+ exchange.id
1322
+ );
1323
+ }) });
1324
+ }
1325
+ function PinnedSteps() {
1326
+ const ctx = useChatSessionContext();
1327
+ const jump = useJump();
1328
+ const live = ctx.spec.turns.filter(
1329
+ (t) => isAnswer(t) && (runOutcome(t) === "live" || runOutcome(t) === "waiting")
1330
+ );
1331
+ if (!live.length) return null;
1332
+ return /* @__PURE__ */ jsx("div", { className: "border-b border-border px-3 py-1.5", children: live.map((a) => {
1333
+ const step = currentStep(a) ?? a.trace?.[0];
1334
+ return step ? /* @__PURE__ */ jsx(StepRow, { turn: a, step, detail: false, onSelect: () => jump(a.id) }, a.id) : null;
1335
+ }) });
1336
+ }
1337
+ function CliSession({ running, onStop, header, onClose, emptyState, className }) {
1338
+ const ctx = useChatSessionContext();
1339
+ const counts = threadCounts(ctx.spec, (ask) => ctx.registry.askTraits(ask.ask.kind).frame === "card");
1340
+ const tasks = ctx.view === "tasks";
1341
+ const viewButton = (target, label) => /* @__PURE__ */ jsx(
1342
+ "button",
1343
+ {
1344
+ type: "button",
1345
+ "aria-pressed": ctx.view === target,
1346
+ onClick: () => ctx.setView(target),
1347
+ className: ctx.view === target ? "font-medium text-foreground" : "hover:text-foreground",
1348
+ children: label
1349
+ }
1350
+ );
1351
+ return /* @__PURE__ */ jsxs("div", { className: cn("flex h-full min-h-0 flex-col bg-background", className), children: [
1352
+ header === true ? /* @__PURE__ */ jsx(SessionHeader, { onClose }) : header || null,
1353
+ /* @__PURE__ */ jsx(
1354
+ ChatSessionFrame,
1355
+ {
1356
+ className: "min-h-0 flex-1",
1357
+ autoScrollKey: `${ctx.view}-${ctx.spec.turns.length}`,
1358
+ emptyState,
1359
+ bodyClassName: tasks ? "gap-[18px]" : void 0,
1360
+ footer: /* @__PURE__ */ jsxs("div", { className: "border-t border-border", children: [
1361
+ !tasks ? /* @__PURE__ */ jsx(PinnedSteps, {}) : null,
1362
+ /* @__PURE__ */ jsx(ComposerBar, { isRunning: running, onStop }),
1363
+ /* @__PURE__ */ jsx(
1364
+ ChatSessionStatusBar,
1365
+ {
1366
+ className: "pt-0",
1367
+ start: /* @__PURE__ */ jsxs(Fragment, { children: [
1368
+ viewButton("chat", "Chat"),
1369
+ viewButton("tasks", `Tasks (${counts.steps})`),
1370
+ counts.running ? /* @__PURE__ */ jsxs("span", { className: "text-primary", children: [
1371
+ counts.running,
1372
+ " running"
1373
+ ] }) : counts.waiting ? /* @__PURE__ */ jsxs("span", { className: "text-warning", children: [
1374
+ counts.waiting,
1375
+ " needs input"
1376
+ ] }) : null
1377
+ ] }),
1378
+ end: ctx.spec.composer?.hints === false ? void 0 : /* @__PURE__ */ jsx(KeyHints, { running })
1379
+ }
1380
+ )
1381
+ ] }),
1382
+ children: tasks ? /* @__PURE__ */ jsx(CliTasks, {}) : exchangesOf(ctx.spec).map((e) => /* @__PURE__ */ jsx(CliExchange, { exchange: e }, e.id))
1383
+ }
1384
+ )
1385
+ ] });
1386
+ }
1387
+
1388
+ // src/styles/types.ts
1389
+ var DEFAULT_ANSWER_ACTIONS = ["retry", "copy", "steps", "rate"];
1390
+ var TurnLabel = React6.forwardRef(
1391
+ ({ align = "start", className, children, ...props }, ref) => /* @__PURE__ */ jsx(
1392
+ "div",
1393
+ {
1394
+ ref,
1395
+ className: cn(
1396
+ "font-mono text-xs tracking-wider text-muted-foreground uppercase",
1397
+ align === "end" && "text-right",
1398
+ className
1399
+ ),
1400
+ ...props,
1401
+ children
1402
+ }
1403
+ )
1404
+ );
1405
+ TurnLabel.displayName = "TurnLabel";
1406
+ var RUN_WORDS = { done: "Answered in", failed: "Failed after", stopped: "Stopped after" };
1407
+ function RunLine({ turn }) {
1408
+ const ctx = useChatSessionContext();
1409
+ const outcome = runOutcome(turn);
1410
+ const time = answerTime(turn, ctx.now);
1411
+ const at = clockTime(turn.at);
1412
+ const words = RUN_WORDS[outcome === "failed" || outcome === "stopped" ? outcome : "done"];
1413
+ const open = ctx.stepsOpen(turn.id, false);
1414
+ return /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center gap-1.5 text-sm text-muted-foreground", children: [
1415
+ at ? /* @__PURE__ */ jsx("span", { className: "tabular-nums", children: at }) : null,
1416
+ at && time !== void 0 ? /* @__PURE__ */ jsx("span", { "aria-hidden": true, children: "\xB7" }) : null,
1417
+ time !== void 0 ? /* @__PURE__ */ jsxs(
1418
+ "button",
1419
+ {
1420
+ type: "button",
1421
+ "aria-haspopup": ctx.opensRuns ? "dialog" : void 0,
1422
+ "aria-expanded": ctx.opensRuns ? void 0 : open,
1423
+ className: "underline decoration-dotted underline-offset-2 hover:text-foreground",
1424
+ onClick: () => ctx.opensRuns ? ctx.emit({ type: "open-run", turn: turn.id }) : ctx.setSteps(turn.id, !open),
1425
+ children: [
1426
+ words,
1427
+ " ",
1428
+ formatDuration(time, { spaced: true })
1429
+ ]
1430
+ }
1431
+ ) : null
1432
+ ] });
1433
+ }
1434
+ function WebAnswer({ turn, chrome = true }) {
1435
+ const ctx = useChatSessionContext();
1436
+ const outcome = runOutcome(turn);
1437
+ const live = outcome === "live" || outcome === "waiting";
1438
+ const { inCard, own, hasEvidence } = splitBlocks(turn, ctx.registry);
1439
+ const block = (b, i) => /* @__PURE__ */ jsx(BlockView, { block: b, turn, registry: ctx.registry, onEvent: ctx.emit }, `${b.kind}-${i}`);
1440
+ const steps = !!turn.trace?.length && (live || ctx.stepsOpen(turn.id, outcome === "failed"));
1441
+ return /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-col gap-2", children: [
1442
+ live && !turn.trace?.length && !hasEvidence ? /* @__PURE__ */ jsx(ChatSessionMessage, { role: "assistant", status: "running", children: "Working" }) : null,
1443
+ steps ? /* @__PURE__ */ jsx(EmissionCard, { kind: live ? outcome === "waiting" ? "waiting" : "running" : "steps", children: /* @__PURE__ */ jsx(EmissionBody, { children: /* @__PURE__ */ jsx(StepList, { turn }) }) }) : null,
1444
+ hasEvidence ? /* @__PURE__ */ jsx(
1445
+ EmissionCard,
1446
+ {
1447
+ kind: turn.label ?? turn.pattern ?? "answer",
1448
+ title: turn.title,
1449
+ citation: citationOf(turn) ?? turn.aside ?? (live && !inCard.some((b) => b.status === "loading") ? "writing\u2026" : void 0),
1450
+ note: turn.state === "partial" ? turn.state : void 0,
1451
+ children: /* @__PURE__ */ jsx(EmissionBody, { children: inCard.map(block) })
1452
+ }
1453
+ ) : null,
1454
+ own.map(block),
1455
+ !hasEvidence && !own.length && (outcome === "failed" || outcome === "stopped") ? /* @__PURE__ */ jsx(ChatSessionMessage, { role: "assistant", status: outcome === "failed" ? "error" : "stopped", children: outcome === "failed" ? "The run failed." : "Stopped by you." }) : null,
1456
+ turn.outcome && !live ? /* @__PURE__ */ jsx(OutcomeView, { outcome: turn.outcome, turn, onEvent: ctx.emit, tone: outcome === "failed" ? "bad" : void 0 }) : null,
1457
+ chrome && !live ? /* @__PURE__ */ jsx(AnswerLine, { turn, children: /* @__PURE__ */ jsx(RunLine, { turn }) }) : null
1458
+ ] });
1459
+ }
1460
+ function WebExchange({ exchange, dayBreak }) {
1461
+ const ctx = useChatSessionContext();
1462
+ const you = ctx.spec.analyst ?? "You";
1463
+ const assistant = ctx.spec.assistant ?? "Assistant";
1464
+ const prompt = exchange.prompt;
1465
+ const sentAt = clockTime(prompt?.at);
1466
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1467
+ dayBreak ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-sm text-muted-foreground", role: "separator", children: [
1468
+ /* @__PURE__ */ jsx("span", { className: "h-px flex-1 bg-border" }),
1469
+ /* @__PURE__ */ jsx("span", { children: dayBreak }),
1470
+ /* @__PURE__ */ jsx("span", { className: "h-px flex-1 bg-border" })
1471
+ ] }) : null,
1472
+ prompt ? /* @__PURE__ */ jsxs("div", { id: turnDomId(ctx.spec, prompt.id), className: "flex scroll-mt-3 flex-col gap-1", children: [
1473
+ /* @__PURE__ */ jsx(TurnLabel, { align: "end", children: you }),
1474
+ /* @__PURE__ */ jsx(ChatSessionMessage, { role: "user", children: prompt.text }),
1475
+ sentAt ? /* @__PURE__ */ jsx("span", { className: "self-end text-sm text-muted-foreground tabular-nums", children: sentAt }) : null
1476
+ ] }) : null,
1477
+ exchange.replies.length ? /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-col gap-2", children: [
1478
+ /* @__PURE__ */ jsx(TurnLabel, { children: assistant }),
1479
+ exchange.replies.map((reply) => /* @__PURE__ */ jsx("div", { id: turnDomId(ctx.spec, reply.id), className: "min-w-0 scroll-mt-3", children: isAnswer(reply) ? /* @__PURE__ */ jsx(WebAnswer, { turn: reply }) : /* @__PURE__ */ jsx(AskView, { turn: reply, registry: ctx.registry, onEvent: ctx.emit, now: ctx.now }) }, reply.id))
1480
+ ] }) : null
1481
+ ] });
1482
+ }
1483
+ function dayBreaks(exchanges, now) {
1484
+ let last;
1485
+ return exchanges.map((exchange) => {
1486
+ const at = exchange.prompt?.at;
1487
+ if (!at) return void 0;
1488
+ const label = last === void 0 || !sameDay(last, at) ? dayLabel(at, now) : void 0;
1489
+ last = at;
1490
+ return label;
1491
+ });
1492
+ }
1493
+ function WebSession({ running, onStop, header, onClose, emptyState, className }) {
1494
+ const ctx = useChatSessionContext();
1495
+ const exchanges = exchangesOf(ctx.spec);
1496
+ const breaks = dayBreaks(exchanges, ctx.now);
1497
+ return /* @__PURE__ */ jsxs("div", { className: cn("flex h-full min-h-0 flex-col bg-background", className), children: [
1498
+ header === true ? /* @__PURE__ */ jsx(SessionHeader, { onClose }) : header || null,
1499
+ /* @__PURE__ */ jsx(
1500
+ ChatSessionFrame,
1501
+ {
1502
+ className: "min-h-0 flex-1",
1503
+ autoScrollKey: ctx.spec.turns.length,
1504
+ emptyState,
1505
+ bodyClassName: "mx-auto w-full max-w-[52rem] gap-5",
1506
+ footer: /* @__PURE__ */ jsxs("div", { className: "mx-auto w-full max-w-[52rem]", children: [
1507
+ /* @__PURE__ */ jsx(ComposerBar, { isRunning: running, onStop }),
1508
+ /* @__PURE__ */ jsx(
1509
+ ChatSessionStatusBar,
1510
+ {
1511
+ className: "pt-0",
1512
+ start: ctx.spec.assistant ? /* @__PURE__ */ jsx("span", { className: "truncate", children: ctx.spec.assistant }) : void 0,
1513
+ end: ctx.spec.composer?.hints === false ? void 0 : /* @__PURE__ */ jsx(KeyHints, { running })
1514
+ }
1515
+ )
1516
+ ] }),
1517
+ children: exchanges.map((exchange, i) => /* @__PURE__ */ jsx(WebExchange, { exchange, dayBreak: breaks[i] }, exchange.id))
1518
+ }
1519
+ )
1520
+ ] });
1521
+ }
1522
+ var handlerName = (type) => `on${type.replace(/(^|-)([a-z])/g, (_, __, c) => c.toUpperCase())}`;
1523
+ var ChatSession = React6.forwardRef(function ChatSession2(props, ref) {
1524
+ const {
1525
+ spec,
1526
+ variant = "web",
1527
+ stream,
1528
+ onStreamEnd,
1529
+ onStreamError,
1530
+ registry,
1531
+ icons,
1532
+ now: fixedNow,
1533
+ onClose,
1534
+ header,
1535
+ emptyState,
1536
+ view: controlledView,
1537
+ defaultView = "chat",
1538
+ actions = DEFAULT_ANSWER_ACTIONS,
1539
+ className
1540
+ } = props;
1541
+ const { live, stop: stopStream } = useStreamedSpec(spec, stream, applyPatches, {
1542
+ onEnd: onStreamEnd,
1543
+ onError: onStreamError,
1544
+ stopPatches
1545
+ });
1546
+ const running = live.turns.some((t) => isAnswer(t) && runOutcome(t) === "live");
1547
+ const now = useClock(fixedNow, running);
1548
+ const resolved = React6.useMemo(() => resolveRegistry(registry), [registry]);
1549
+ const resolvedIcons = React6.useMemo(() => ({ ...DEFAULT_ICONS, ...icons }), [icons]);
1550
+ const viewState = useViewState();
1551
+ const [uncontrolledView, setUncontrolledView] = React6.useState(defaultView);
1552
+ const view = controlledView ?? uncontrolledView;
1553
+ const root = React6.useRef(null);
1554
+ const composer = React6.useRef(null);
1555
+ const setComposer = React6.useCallback((element) => {
1556
+ composer.current = element;
1557
+ }, []);
1558
+ const latest = React6.useRef(props);
1559
+ const liveRef = React6.useRef(live);
1560
+ React6.useLayoutEffect(() => {
1561
+ latest.current = props;
1562
+ liveRef.current = live;
1563
+ });
1564
+ const emit = React6.useCallback((event) => {
1565
+ const handler = latest.current[handlerName(event.type)];
1566
+ if (typeof handler === "function") handler(event);
1567
+ latest.current.onEvent?.(event);
1568
+ }, []);
1569
+ const stop = React6.useCallback(() => {
1570
+ stopStream();
1571
+ emit({ type: "stop" });
1572
+ }, [stopStream, emit]);
1573
+ useStopKey(running, stop);
1574
+ const setView = React6.useCallback(
1575
+ (next) => {
1576
+ if (latest.current.view === void 0) setUncontrolledView(next);
1577
+ latest.current.onViewChange?.(next);
1578
+ },
1579
+ []
1580
+ );
1581
+ const scrollToTurn = React6.useCallback((turnId) => {
1582
+ setTimeout(() => {
1583
+ const id = turnDomId(liveRef.current, turnId);
1584
+ root.current?.querySelector(`[id="${CSS.escape(id)}"]`)?.scrollIntoView({ block: "start", behavior: "smooth" });
1585
+ }, 80);
1586
+ }, []);
1587
+ const focusComposer = React6.useCallback(() => {
1588
+ composer.current?.querySelector("textarea")?.focus();
1589
+ }, []);
1590
+ React6.useImperativeHandle(
1591
+ ref,
1592
+ () => ({
1593
+ scrollToTurn,
1594
+ showSteps: (turnId, open = true) => viewState.toggleSteps(turnId, open),
1595
+ openStep: (turnId, stepId) => {
1596
+ viewState.toggleSteps(turnId, true);
1597
+ viewState.toggleRecord(turnId, stepId, true);
1598
+ scrollToTurn(turnId);
1599
+ },
1600
+ setView,
1601
+ focusComposer
1602
+ }),
1603
+ [scrollToTurn, setView, focusComposer, viewState]
1604
+ );
1605
+ const context = {
1606
+ spec: live,
1607
+ registry: resolved,
1608
+ emit,
1609
+ icons: resolvedIcons,
1610
+ now,
1611
+ opensRuns: typeof props.onOpenRun === "function",
1612
+ stepsOpen: viewState.stepsOpen,
1613
+ toggleSteps: viewState.toggleSteps,
1614
+ setSteps: (turnId, open) => {
1615
+ viewState.toggleSteps(turnId, open);
1616
+ emit({ type: "toggle-steps", turn: turnId, open });
1617
+ },
1618
+ actions,
1619
+ recordOpen: viewState.recordOpen,
1620
+ toggleRecord: viewState.toggleRecord,
1621
+ rating: viewState.rating,
1622
+ rate: viewState.setRating,
1623
+ view,
1624
+ setView,
1625
+ scrollToTurn,
1626
+ focusComposer,
1627
+ setComposer
1628
+ };
1629
+ const shared = {
1630
+ running,
1631
+ onStop: stop,
1632
+ header: header === void 0 ? !!live.title : header,
1633
+ onClose,
1634
+ emptyState,
1635
+ className
1636
+ };
1637
+ return /* @__PURE__ */ jsx(ChatSessionContext.Provider, { value: context, children: /* @__PURE__ */ jsx("div", { ref: root, "data-variant": variant, className: "contents", children: variant === "cli" ? /* @__PURE__ */ jsx(CliSession, { ...shared }) : /* @__PURE__ */ jsx(WebSession, { ...shared }) }) });
1638
+ });
1639
+ function useChatSession(initial) {
1640
+ const [spec, setSpec] = React6.useState(initial);
1641
+ const current = React6.useRef(spec);
1642
+ const controllers = React6.useRef(/* @__PURE__ */ new Set());
1643
+ const [streams, setStreams] = React6.useState(0);
1644
+ const set2 = React6.useCallback((next) => {
1645
+ current.current = next;
1646
+ setSpec(next);
1647
+ }, []);
1648
+ const apply = React6.useCallback(
1649
+ (patch) => set2(applyPatches(current.current, Array.isArray(patch) ? patch : [patch])),
1650
+ [set2]
1651
+ );
1652
+ const run = React6.useCallback(
1653
+ async (open) => {
1654
+ const controller = new AbortController();
1655
+ controllers.current.add(controller);
1656
+ setStreams((n) => n + 1);
1657
+ try {
1658
+ for await (const batch of patchesOf(open(controller.signal))) {
1659
+ if (controller.signal.aborted) break;
1660
+ set2(applyPatches(current.current, batch));
1661
+ }
1662
+ } finally {
1663
+ controllers.current.delete(controller);
1664
+ setStreams((n) => n - 1);
1665
+ }
1666
+ return current.current;
1667
+ },
1668
+ [set2]
1669
+ );
1670
+ const stream = React6.useCallback((source) => run(() => source), [run]);
1671
+ const play = React6.useCallback(
1672
+ (script, options) => run((signal) => playScript(script, { ...options, signal })),
1673
+ [run]
1674
+ );
1675
+ const abortAll = () => {
1676
+ for (const c of controllers.current) c.abort();
1677
+ controllers.current.clear();
1678
+ };
1679
+ const stop = React6.useCallback(() => {
1680
+ abortAll();
1681
+ set2(applyPatches(current.current, stopPatches(current.current)));
1682
+ }, [set2]);
1683
+ const reset = React6.useCallback(
1684
+ (next) => {
1685
+ abortAll();
1686
+ set2(next);
1687
+ },
1688
+ [set2]
1689
+ );
1690
+ React6.useEffect(() => () => abortAll(), []);
1691
+ return { spec, apply, stream, play, stop, reset, streaming: streams > 0, running: isRunning(spec) };
1692
+ }
1693
+ function ChatSessionTurn({ turn, onEvent, registry, now }) {
1694
+ const resolved = React6.useMemo(() => resolveRegistry(registry), [registry]);
1695
+ const view = useViewState();
1696
+ const [clock] = React6.useState(() => now ?? Date.now());
1697
+ const emit = React6.useCallback((event) => onEvent?.(event), [onEvent]);
1698
+ const context = {
1699
+ spec: { id: `turn-${turn.id}`, turns: [turn] },
1700
+ registry: resolved,
1701
+ emit,
1702
+ icons: DEFAULT_ICONS,
1703
+ now: now ?? clock,
1704
+ opensRuns: false,
1705
+ stepsOpen: view.stepsOpen,
1706
+ toggleSteps: view.toggleSteps,
1707
+ setSteps: (turnId, open) => {
1708
+ view.toggleSteps(turnId, open);
1709
+ emit({ type: "toggle-steps", turn: turnId, open });
1710
+ },
1711
+ actions: [],
1712
+ recordOpen: view.recordOpen,
1713
+ toggleRecord: view.toggleRecord,
1714
+ rating: view.rating,
1715
+ rate: view.setRating,
1716
+ view: "chat",
1717
+ setView: () => {
1718
+ },
1719
+ scrollToTurn: () => {
1720
+ },
1721
+ focusComposer: () => {
1722
+ },
1723
+ setComposer: () => {
1724
+ }
1725
+ };
1726
+ return /* @__PURE__ */ jsx(ChatSessionContext.Provider, { value: context, children: turn.role === "analyst" ? /* @__PURE__ */ jsx(ChatSessionMessage, { role: "user", children: turn.text }) : turn.kind === "ask" ? /* @__PURE__ */ jsx(AskView, { turn, registry: resolved, onEvent: emit, now: context.now }) : /* @__PURE__ */ jsx(WebAnswer, { turn, chrome: false }) });
1727
+ }
1728
+
1729
+ // src/protocol/events.ts
1730
+ var EVENT_TYPES = [
1731
+ "prompt",
1732
+ "reply",
1733
+ "skip",
1734
+ "change",
1735
+ "action",
1736
+ "scope",
1737
+ "open",
1738
+ "template",
1739
+ "refine",
1740
+ "rate",
1741
+ "stop",
1742
+ "retry",
1743
+ "open-run",
1744
+ "setting",
1745
+ "copy",
1746
+ "toggle-steps"
1747
+ ];
1748
+
1749
+ // src/protocol/validate.ts
1750
+ var ASK_IDS = new Set(ASK_KINDS);
1751
+ var BLOCK_IDS = new Set(ANSWER_KINDS);
1752
+ var STAGE_IDS = new Set(STAGES.map((s) => s.id));
1753
+ var FLOW_IDS = new Set(FLOWS.map((f) => f.id));
1754
+ var PATTERN_BY = new Map(PATTERNS.map((p) => [p.id, p.blocks]));
1755
+ var ASK_STATES = /* @__PURE__ */ new Set(["pending", "answered", "skipped", "superseded", "expired"]);
1756
+ var ANSWER_STATES = /* @__PURE__ */ new Set(["running", "partial", "complete", "cannot", "error", "stopped"]);
1757
+ var STEP_STATES = /* @__PURE__ */ new Set(["done", "running", "pending", "failed", "waiting", "retrying", "stopped"]);
1758
+ var notIso = (value) => value !== void 0 && (typeof value !== "string" || Number.isNaN(Date.parse(value)));
1759
+ var CARRIED = {
1760
+ scope: (a) => !!a.envelope?.scope?.length,
1761
+ method: (a) => !!a.envelope?.method,
1762
+ caveat: (a) => !!a.envelope?.caveats?.length,
1763
+ trace: (a) => !!a.trace?.length
1764
+ };
1765
+ var ANYWHERE = /* @__PURE__ */ new Set(["narrative", "citations"]);
1766
+ function checkPattern(turn, issues) {
1767
+ if (!turn.pattern) return;
1768
+ const expected = PATTERN_BY.get(turn.pattern);
1769
+ if (!expected) return;
1770
+ const present = new Set(turn.blocks.map((b) => b.kind));
1771
+ for (const kind of expected) {
1772
+ const carried = CARRIED[kind];
1773
+ const has = carried ? carried(turn) : present.has(kind);
1774
+ if (!has) {
1775
+ issues.push({
1776
+ level: "warning",
1777
+ turn: turn.id,
1778
+ message: `Pattern "${turn.pattern}" expects "${kind}", which this answer does not have.`
1779
+ });
1780
+ }
1781
+ }
1782
+ for (const kind of present) {
1783
+ if (!expected.includes(kind) && !ANYWHERE.has(kind)) {
1784
+ issues.push({
1785
+ level: "warning",
1786
+ turn: turn.id,
1787
+ message: `Block "${kind}" is not part of pattern "${turn.pattern}".`
1788
+ });
1789
+ }
1790
+ }
1791
+ }
1792
+ function validate(spec, options = {}) {
1793
+ const issues = [];
1794
+ const error = (message, turn) => issues.push({ level: "error", turn, message });
1795
+ const asks = /* @__PURE__ */ new Set([...ASK_IDS, ...options.extraAsks ?? []]);
1796
+ const blocks = /* @__PURE__ */ new Set([...BLOCK_IDS, ...options.extraBlocks ?? []]);
1797
+ if (typeof spec?.id !== "string" || !spec.id) error("The conversation has no id.");
1798
+ if (spec?.analyst !== void 0 && (typeof spec.analyst !== "string" || !spec.analyst)) {
1799
+ error("The analyst's name is not a non-empty string.");
1800
+ }
1801
+ const controls = spec?.composer?.controls ?? [];
1802
+ const controlIds = /* @__PURE__ */ new Set();
1803
+ for (const control of controls) {
1804
+ if (!control?.id) error("A composer control has no id.");
1805
+ else if (controlIds.has(control.id)) error(`Composer control id "${control.id}" is used twice.`);
1806
+ else controlIds.add(control.id);
1807
+ if (!control?.options?.length) error(`Composer control "${control?.id}" has no options.`);
1808
+ else if (control.default !== void 0 && !control.options.some((o) => o.value === control.default)) {
1809
+ error(`Composer control "${control.id}" defaults to "${control.default}", which is not one of its options.`);
1810
+ }
1811
+ }
1812
+ if (!Array.isArray(spec?.turns)) {
1813
+ error("The conversation has no turns array.");
1814
+ return issues;
1815
+ }
1816
+ const seen = /* @__PURE__ */ new Set();
1817
+ for (const turn of spec.turns) {
1818
+ const id = typeof turn?.id === "string" ? turn.id : void 0;
1819
+ if (!id) {
1820
+ error("A turn has no id.");
1821
+ continue;
1822
+ }
1823
+ if (seen.has(id)) error(`Turn id "${id}" is used twice.`, id);
1824
+ seen.add(id);
1825
+ if (turn.role === "analyst") {
1826
+ if (typeof turn.text !== "string" || !turn.text) error("An analyst turn has no text.", id);
1827
+ if (notIso(turn.at)) error(`at "${turn.at}" is not an ISO time.`, id);
1828
+ continue;
1829
+ }
1830
+ if (turn.role !== "assistant") {
1831
+ error(`Unknown role "${turn.role}".`, id);
1832
+ continue;
1833
+ }
1834
+ if (turn.kind === "ask") {
1835
+ if (!STAGE_IDS.has(turn.stage)) error(`Unknown stage "${turn.stage}".`, id);
1836
+ if (!ASK_STATES.has(turn.state)) error(`Unknown ask state "${turn.state}".`, id);
1837
+ if (!asks.has(turn.ask?.kind)) error(`Unknown ask block "${turn.ask?.kind}".`, id);
1838
+ if (turn.answeredAt !== void 0 && Number.isNaN(Date.parse(turn.answeredAt))) {
1839
+ error(`answeredAt "${turn.answeredAt}" is not an ISO time.`, id);
1840
+ }
1841
+ if (turn.ask?.kind === "multistep") {
1842
+ for (const step of turn.ask.steps ?? []) {
1843
+ if (!asks.has(step.kind)) error(`Unknown ask block "${step.kind}" in step "${step.id}".`, id);
1844
+ }
1845
+ }
1846
+ continue;
1847
+ }
1848
+ if (turn.kind === "answer") {
1849
+ if (!ANSWER_STATES.has(turn.state)) error(`Unknown answer state "${turn.state}".`, id);
1850
+ if (turn.flow && !FLOW_IDS.has(turn.flow)) error(`Unknown flow "${turn.flow}".`, id);
1851
+ if (turn.pattern && !PATTERN_BY.has(turn.pattern)) error(`Unknown pattern "${turn.pattern}".`, id);
1852
+ if (notIso(turn.at)) error(`at "${turn.at}" is not an ISO time.`, id);
1853
+ if (notIso(turn.startedAt)) error(`startedAt "${turn.startedAt}" is not an ISO time.`, id);
1854
+ const stepIds = /* @__PURE__ */ new Set();
1855
+ for (const step of turn.trace ?? []) {
1856
+ if (!STEP_STATES.has(step?.state)) error(`Unknown step state "${step?.state}" on "${step?.label}".`, id);
1857
+ if (notIso(step?.startedAt)) error(`Step "${step.label}" startedAt "${step.startedAt}" is not an ISO time.`, id);
1858
+ if (step?.id) {
1859
+ if (stepIds.has(step.id)) error(`Step id "${step.id}" is used twice.`, id);
1860
+ stepIds.add(step.id);
1861
+ }
1862
+ }
1863
+ for (const action of turn.outcome?.actions ?? []) {
1864
+ if (!action?.id || !action.label) error("An outcome action has no id or label.", id);
1865
+ }
1866
+ if (!Array.isArray(turn.blocks)) {
1867
+ error("An answer has no blocks array.", id);
1868
+ continue;
1869
+ }
1870
+ for (const block of turn.blocks) {
1871
+ if (!blocks.has(block?.kind)) error(`Unknown block "${block?.kind}".`, id);
1872
+ else if (block.kind in CARRIED) {
1873
+ error(`"${block.kind}" is part of the answer, not a block: send it in the ${block.kind === "trace" ? `answer's "trace"` : "envelope"}.`, id);
1874
+ }
1875
+ if (block?.status !== void 0 && block.status !== "loading" && block.status !== "empty") {
1876
+ error(`Unknown block status "${block.status}".`, id);
1877
+ }
1878
+ if (block?.status === "empty" && !block.emptyText) {
1879
+ error(`An empty "${block.kind}" block does not say what is missing in emptyText.`, id);
1880
+ }
1881
+ }
1882
+ checkPattern(turn, issues);
1883
+ continue;
1884
+ }
1885
+ error(`Unknown assistant turn kind "${turn.kind}".`, id);
1886
+ }
1887
+ return issues;
1888
+ }
1889
+
1890
+ export { ANSWER_INTENTS, ASK_INTENTS, BUILT_IN_ASKS, BUILT_IN_ASK_TRAITS, BUILT_IN_BLOCKS, BUILT_IN_BLOCK_TRAITS, ChatSession, ChatSessionActivityRow, ChatSessionActivitySubLine, ChatSessionCaret, ChatSessionComposer, ChatSessionContextChip, ChatSessionDisclosure, ChatSessionDisclosureCode, ChatSessionDisclosureSteps, ChatSessionFrame, ChatSessionMessage, ChatSessionMessageOptions, ChatSessionProgressLine, ChatSessionPromptRow, ChatSessionStatusBar, ChatSessionTaskGroup, ChatSessionTaskRow, ChatSessionTurn, CitationMarker, ClarifyActions, ClarifyCard, ClarifyFootnote, DEFAULT_ANSWER_ACTIONS, EVENT_TYPES, EmissionBody, EmissionCard, EmissionHeader, FLOWS, PATTERNS, PatchError, STAGES, TemplatePicker, TurnLabel, answerTime, answerToPage, applyPatch, applyPatches, chatSessionGutterClass, clockTime, currentStep, dayLabel, elapsedSince, exchangesOf, formatDuration, fromEventSource, fromNdjson, isLive, isRunning, offsetScript, patchesOf, plainText, playScript, resolveRegistry, runOutcome, stepTime, stopPatches, textDeltas, thinkingDeltas, threadCounts, useChatSession, validate };
1891
+ //# sourceMappingURL=index.js.map
1892
+ //# sourceMappingURL=index.js.map