@xenosystem/agent-conversation 0.1.35 → 0.1.37

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.
Files changed (86) hide show
  1. package/dist/adapters/xenoAgentHostCatalog.d.ts.map +1 -1
  2. package/dist/adapters/xenoAgentHostCatalog.js +20 -5
  3. package/dist/adapters/xenoAgentHostCatalog.js.map +1 -1
  4. package/dist/components/agent/AgentView.d.ts +9 -0
  5. package/dist/components/agent/AgentView.d.ts.map +1 -1
  6. package/dist/components/agent/AgentView.js +883 -1045
  7. package/dist/components/agent/AgentView.js.map +1 -1
  8. package/dist/components/agent/GhostTextArea.d.ts +6 -0
  9. package/dist/components/agent/GhostTextArea.d.ts.map +1 -1
  10. package/dist/components/agent/GhostTextArea.js +15 -5
  11. package/dist/components/agent/GhostTextArea.js.map +1 -1
  12. package/dist/components/agent/LiveConversationCollaboration.d.ts +3 -1
  13. package/dist/components/agent/LiveConversationCollaboration.d.ts.map +1 -1
  14. package/dist/components/agent/LiveConversationCollaboration.js +3 -1
  15. package/dist/components/agent/LiveConversationCollaboration.js.map +1 -1
  16. package/dist/components/agent/MessageText.d.ts +36 -0
  17. package/dist/components/agent/MessageText.d.ts.map +1 -0
  18. package/dist/components/agent/MessageText.js +76 -0
  19. package/dist/components/agent/MessageText.js.map +1 -0
  20. package/dist/components/agent/SdkSessionPanel.d.ts +2 -0
  21. package/dist/components/agent/SdkSessionPanel.d.ts.map +1 -1
  22. package/dist/components/agent/SdkSessionPanel.js +1 -1
  23. package/dist/components/agent/SdkSessionPanel.js.map +1 -1
  24. package/dist/components/agent/composer/Composer.d.ts +282 -0
  25. package/dist/components/agent/composer/Composer.d.ts.map +1 -0
  26. package/dist/components/agent/composer/Composer.js +393 -0
  27. package/dist/components/agent/composer/Composer.js.map +1 -0
  28. package/dist/components/agent/composer/EffortCells.d.ts +12 -0
  29. package/dist/components/agent/composer/EffortCells.d.ts.map +1 -0
  30. package/dist/components/agent/composer/EffortCells.js +146 -0
  31. package/dist/components/agent/composer/EffortCells.js.map +1 -0
  32. package/dist/components/agent/composer/Tip.d.ts +19 -0
  33. package/dist/components/agent/composer/Tip.d.ts.map +1 -0
  34. package/dist/components/agent/composer/Tip.js +78 -0
  35. package/dist/components/agent/composer/Tip.js.map +1 -0
  36. package/dist/components/agent/composer/TypedMenu.d.ts +56 -0
  37. package/dist/components/agent/composer/TypedMenu.d.ts.map +1 -0
  38. package/dist/components/agent/composer/TypedMenu.js +156 -0
  39. package/dist/components/agent/composer/TypedMenu.js.map +1 -0
  40. package/dist/components/agent/mainAssistant.d.ts +9 -2
  41. package/dist/components/agent/mainAssistant.d.ts.map +1 -1
  42. package/dist/components/agent/mainAssistant.js +9 -2
  43. package/dist/components/agent/mainAssistant.js.map +1 -1
  44. package/dist/components/agent/toolCallDerive.d.ts +82 -0
  45. package/dist/components/agent/toolCallDerive.d.ts.map +1 -0
  46. package/dist/components/agent/toolCallDerive.js +741 -0
  47. package/dist/components/agent/toolCallDerive.js.map +1 -0
  48. package/dist/components/agent/transcript/Transcript.d.ts +127 -0
  49. package/dist/components/agent/transcript/Transcript.d.ts.map +1 -0
  50. package/dist/components/agent/transcript/Transcript.js +544 -0
  51. package/dist/components/agent/transcript/Transcript.js.map +1 -0
  52. package/dist/components/agent/transcript/glyphs.d.ts +83 -0
  53. package/dist/components/agent/transcript/glyphs.d.ts.map +1 -0
  54. package/dist/components/agent/transcript/glyphs.js +124 -0
  55. package/dist/components/agent/transcript/glyphs.js.map +1 -0
  56. package/dist/components/agent/transcript/stepModel.d.ts +163 -0
  57. package/dist/components/agent/transcript/stepModel.d.ts.map +1 -0
  58. package/dist/components/agent/transcript/stepModel.js +701 -0
  59. package/dist/components/agent/transcript/stepModel.js.map +1 -0
  60. package/dist/components/agent/transcript/turnModel.d.ts +124 -0
  61. package/dist/components/agent/transcript/turnModel.d.ts.map +1 -0
  62. package/dist/components/agent/transcript/turnModel.js +166 -0
  63. package/dist/components/agent/transcript/turnModel.js.map +1 -0
  64. package/dist/components/agent/transcript/useAskActions.d.ts +23 -0
  65. package/dist/components/agent/transcript/useAskActions.d.ts.map +1 -0
  66. package/dist/components/agent/transcript/useAskActions.js +92 -0
  67. package/dist/components/agent/transcript/useAskActions.js.map +1 -0
  68. package/dist/parityFixture.d.ts +54 -0
  69. package/dist/parityFixture.d.ts.map +1 -0
  70. package/dist/parityFixture.js +195 -0
  71. package/dist/parityFixture.js.map +1 -0
  72. package/dist/platformApiTypes.d.ts +10 -1
  73. package/dist/platformApiTypes.d.ts.map +1 -1
  74. package/dist/stores/agentChatStore.d.ts +61 -1
  75. package/dist/stores/agentChatStore.d.ts.map +1 -1
  76. package/dist/stores/agentChatStore.js +63 -8
  77. package/dist/stores/agentChatStore.js.map +1 -1
  78. package/dist/types/agentPermissions.d.ts +3 -1
  79. package/dist/types/agentPermissions.d.ts.map +1 -1
  80. package/dist/utils/acpExecutionAuthority.d.ts.map +1 -1
  81. package/dist/utils/acpExecutionAuthority.js +2 -0
  82. package/dist/utils/acpExecutionAuthority.js.map +1 -1
  83. package/package.json +6 -5
  84. package/src/composer.css +334 -0
  85. package/src/styles.css +7 -0
  86. package/src/transcript.css +585 -0
@@ -0,0 +1,544 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * The XENO Agent transcript — D10 of `XENO AGENT PANEL - SPEC.md`, rendered.
4
+ *
5
+ * A user message is a plate. An assistant message is a TURN: a clock line (`▪ Working for 4s`), a
6
+ * rail of steps (28 px rows, the marker's box earned when the step settles, details as children on
7
+ * branches, asks as cards that stop the rail), the reply written as it arrives, and — after the
8
+ * fold — a receipt of consequences with a way back. Two step modes: expanded draws the rail as the
9
+ * turn works; collapsed keeps it folded and carries the current step on the clock line.
10
+ *
11
+ * Everything the transcript shows comes from `deriveTurn` (pure) over the conversation's own
12
+ * `ChatMessage`; everything it does goes back through the same store actions and bridge calls
13
+ * the previous cards used (`useAskActions`). What is not wired on a host is disabled and says so.
14
+ */
15
+ import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
16
+ import { XenoElement } from '@xenosystem/elements-react';
17
+ import { glyphDeclaration } from './glyphs.js';
18
+ import { currentThought, deriveTurn, formatElapsed } from './turnModel.js';
19
+ const MAC = typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.platform);
20
+ const KEY_ENTER = MAC ? '⌘↵' : 'Ctrl+↵';
21
+ const KEY_CANCEL = MAC ? '⌘.' : 'Ctrl+.';
22
+ export function Glyph({ name, size = 12 }) {
23
+ // The Turn Modes Bench's static glyphs are 2.25px. Pass that explicit foundry weight at the
24
+ // consumer boundary so the canonical declaration renderer and the reference share a stroke
25
+ // without forking the elements design system.
26
+ return _jsx(XenoElement, { decl: glyphDeclaration(name), size: size, strokeWidth: 2.25, "aria-hidden": "true" });
27
+ }
28
+ /** The bench's chevron: a `span.chev` holding a 10 px glyph drawn on the 24-unit grid. */
29
+ function Chevron() {
30
+ return (_jsx("span", { className: "xa-chev", children: _jsx("svg", { width: "10", height: "10", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.4", strokeLinecap: "square", "aria-hidden": "true", children: _jsx("path", { d: "M9 6l6 6-6 6" }) }) }));
31
+ }
32
+ /** A clock that ticks once a second while something is live. */
33
+ function useNow(live) {
34
+ const [now, setNow] = useState(() => Date.now());
35
+ useEffect(() => {
36
+ if (!live)
37
+ return;
38
+ setNow(Date.now());
39
+ const timer = setInterval(() => setNow(Date.now()), 1000);
40
+ return () => clearInterval(timer);
41
+ }, [live]);
42
+ return now;
43
+ }
44
+ /**
45
+ * Remembers when a turn settled, so the clock line can say how long it worked even after the
46
+ * store stops changing — the store carries when a turn started, not when it ended.
47
+ */
48
+ function useTurnTiming(turn) {
49
+ const settledAtRef = useRef(null);
50
+ const wasLive = useRef(turn.live);
51
+ if (wasLive.current && !turn.live && settledAtRef.current === null)
52
+ settledAtRef.current = Date.now();
53
+ wasLive.current = turn.live;
54
+ const now = useNow(turn.live);
55
+ if (turn.live)
56
+ return { elapsedMs: Math.max(0, now - turn.startedAt), settledAt: null };
57
+ const fromTools = turn.steps
58
+ .flatMap((step) => (step.kind === 'act' ? [step.row.toolCall.endedAt ?? 0] : []))
59
+ .reduce((max, value) => Math.max(max, value), 0);
60
+ // the host's own end time first: it is the only one that survives a reload
61
+ const settledAt = turn.endedAt ?? settledAtRef.current ?? (fromTools > turn.startedAt ? fromTools : null);
62
+ return { elapsedMs: settledAt ? settledAt - turn.startedAt : 0, settledAt };
63
+ }
64
+ // Keep the cursor in the final paragraph, as the bench does. A sibling after a block paragraph
65
+ // drops onto its own line, which looks like an extra step marker instead of a writing cursor.
66
+ function ReplyBody({ children, streaming }) {
67
+ const ref = useRef(null);
68
+ useLayoutEffect(() => {
69
+ const body = ref.current;
70
+ if (!streaming || !body)
71
+ return;
72
+ const last = body.lastElementChild;
73
+ // The markdown renderer owns its text nodes and may replace them on each delta. Add this
74
+ // presentation-only marker after that commit; remove our own node before the next one.
75
+ const target = last?.matches('p') ? last : body;
76
+ const caret = document.createElement('span');
77
+ caret.className = 'xa-caret';
78
+ caret.setAttribute('aria-hidden', 'true');
79
+ target.appendChild(caret);
80
+ return () => caret.remove();
81
+ }, [children, streaming]);
82
+ return _jsx("div", { className: "xa-body", ref: ref, children: children });
83
+ }
84
+ function currentStepTicker(turn) {
85
+ // a plan waiting for a go is what the rail is doing, and the line says so in the plan's own terms
86
+ const planask = turn.steps.find((step) => step.kind === 'planask' && step.row.ask?.status === 'pending');
87
+ if (planask)
88
+ return { key: `plan:${planask.tasks.length}`, glyph: 'list', verb: 'Asking for a go on', object: planSteps(planask.tasks.length), mono: false, joiner: ' ' };
89
+ const current = turn.steps[turn.steps.length - 1];
90
+ if (current?.kind === 'note')
91
+ return { key: `note:${current.id}`, glyph: null, verb: current.text, object: '', mono: false, joiner: ' ' };
92
+ if (current?.kind === 'plan')
93
+ return { key: `plan:${current.id}`, glyph: 'list', verb: 'Planned', object: planSteps(current.tasks.length), mono: false, joiner: ' ' };
94
+ const rows = turn.steps.filter((step) => step.kind === 'act');
95
+ const live = [...rows].reverse().find((step) => step.row.live) || rows[rows.length - 1];
96
+ if (live) {
97
+ const row = live.row;
98
+ const spoken = spokenStep(row);
99
+ return { key: `${row.family}:${spoken.verb}:${spoken.object}`, glyph: row.glyph, verb: spoken.verb, object: spoken.object, mono: row.objectMono, joiner: spoken.joiner };
100
+ }
101
+ const think = turn.steps.find((step) => step.kind === 'think');
102
+ if (think?.live)
103
+ return { key: `think:${currentThought(think.text)}`, glyph: 'brain', verb: 'Thinking ·', object: currentThought(think.text), mono: false, joiner: ' ' };
104
+ return null;
105
+ }
106
+ /**
107
+ * What a row SAYS — its verb and object as spoken, with an ask phrased as the asking: "Asking to use
108
+ * Discord", "Asking to run", "Asking" beside the question. One function, because the row and the
109
+ * clock line's ticker must say the same thing; they used to compute it separately, and the ticker
110
+ * carried the tool's raw verb ("Opened") while the row said "Asking to use Discord".
111
+ */
112
+ function spokenStep(row) {
113
+ const ask = row.ask;
114
+ const verb = ask && ask.status === 'pending'
115
+ ? (ask.kind === 'grant' ? `Asking to use ${ask.what}` : ask.kind === 'approve' ? 'Asking to run' : 'Asking')
116
+ : ask && ask.kind === 'grant' && ask.status === 'yes' ? `${ask.what} — allowed for this ${ask.whatScope === 'this session' ? 'session' : 'conversation'}`
117
+ : ask && ask.kind === 'approve' && ask.status === 'yes' ? 'Approved'
118
+ : ask && (ask.status === 'no' || ask.status === 'cancelled') ? (ask.kind === 'question' ? 'Asked' : 'Refused')
119
+ : row.verb;
120
+ const object = ask && ask.kind === 'approve' ? ask.body || row.object : ask && ask.kind === 'grant' ? '' : row.object;
121
+ // a question is set off from its asking with a dash (bench: "Asking — Three threads match…")
122
+ const joiner = ask && ask.kind === 'question' && ask.status === 'pending' ? ' — ' : ' ';
123
+ return { verb, object, joiner };
124
+ }
125
+ function tickerText(item) {
126
+ if (!item.object)
127
+ return item.verb;
128
+ // one run of text, as the bench writes it; only a mono object stands in its own span
129
+ return item.mono ? _jsxs(_Fragment, { children: [item.verb, item.joiner, _jsx("span", { className: "xa-mono", children: item.object })] }) : `${item.verb}${item.joiner}${item.object}`;
130
+ }
131
+ function Ticker({ item }) {
132
+ const [shown, setShown] = useState({ cur: item, prev: null, first: true });
133
+ const key = item?.key ?? null;
134
+ useEffect(() => {
135
+ setShown((state) => {
136
+ if ((state.cur?.key ?? null) === key)
137
+ return state;
138
+ return { cur: item, prev: state.cur, first: state.cur === null };
139
+ });
140
+ const timer = setTimeout(() => setShown((state) => (state.prev ? { ...state, prev: null } : state)), 300);
141
+ return () => clearTimeout(timer);
142
+ }, [key]); // eslint-disable-line react-hooks/exhaustive-deps
143
+ const sameGlyph = shown.prev && shown.cur && shown.prev.glyph === shown.cur.glyph;
144
+ return (_jsxs("span", { className: "xa-cur", "data-cur2": true, children: [_jsxs("span", { className: "xa-slot", children: [shown.prev && !sameGlyph && _jsx("span", { className: "xa-gl xa-out", children: shown.prev.glyph ? _jsx(Glyph, { name: shown.prev.glyph }) : _jsx("span", { className: "xa-sq" }) }), shown.cur && _jsx("span", { className: `xa-gl ${shown.first ? 'xa-first' : sameGlyph ? '' : 'xa-in'}`, children: shown.cur.glyph ? _jsx(Glyph, { name: shown.cur.glyph }) : _jsx("span", { className: "xa-sq" }) }, shown.cur.key)] }), _jsxs("span", { className: "xa-tx", children: [shown.prev && _jsx("span", { className: "xa-t xa-out", children: tickerText(shown.prev) }), shown.cur && _jsx("span", { className: `xa-t ${shown.first ? 'xa-first' : 'xa-in'}`, children: tickerText(shown.cur) }, `${shown.cur.key}:t`)] })] }));
145
+ }
146
+ function ClockLine({ turn, elapsedMs, open, onToggle, stepsMode, holdingRail = false }) {
147
+ const [phase, setPhase] = useState(turn.live ? 'live' : 'settled');
148
+ const readRef = useRef(null);
149
+ useLayoutEffect(() => {
150
+ if (turn.live) {
151
+ setPhase('live');
152
+ return;
153
+ }
154
+ if (phase !== 'live' || holdingRail)
155
+ return;
156
+ // the fold: the square blurs away, then the words glide 17 px left into its place
157
+ const turnElement = readRef.current?.closest('.xa-turn');
158
+ const markTimer = setTimeout(() => { setPhase('settling'); turnElement?.classList.add('xa-settle'); }, 300);
159
+ const timer = setTimeout(() => {
160
+ setPhase('settled');
161
+ const read = readRef.current;
162
+ if (read && !window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
163
+ read.style.transition = 'none';
164
+ read.style.transform = 'translateX(17px)';
165
+ requestAnimationFrame(() => {
166
+ read.style.transition = 'transform 280ms var(--xa-glide)';
167
+ read.style.transform = 'translateX(0)';
168
+ });
169
+ }
170
+ }, 500);
171
+ return () => { clearTimeout(markTimer); clearTimeout(timer); turnElement?.classList.remove('xa-settle'); };
172
+ }, [turn.live, holdingRail]); // eslint-disable-line react-hooks/exhaustive-deps
173
+ const elapsed = formatElapsed(elapsedMs);
174
+ const still = turn.live && elapsedMs >= 30_000;
175
+ const steps = turn.stepCount;
176
+ const read = turn.live
177
+ ? (still ? _jsxs(_Fragment, { children: ["Still working ", _jsxs("span", { className: "xa-n", children: ["\u00B7 ", elapsed] })] }) : _jsxs(_Fragment, { children: ["Working for ", elapsed] }))
178
+ : _jsxs(_Fragment, { children: [elapsedMs > 0 ? `Worked for ${elapsed}` : 'Worked', steps ? _jsxs("span", { className: "xa-n", children: [" \u00B7 ", steps, " step", steps === 1 ? '' : 's'] }) : null] });
179
+ // The line's whole anatomy is always present — dot, reading, divider, current step, chevron, shots —
180
+ // and the stylesheet decides what shows, as in the bench. Rendering the divider and the current step
181
+ // only in collapsed mode meant the elements did not exist to fade in, and the bench's rules for them
182
+ // (`.line .vr`, `.line .cur2`) had nothing to match.
183
+ const ticker = turn.live ? currentStepTicker(turn) : null;
184
+ return (_jsxs("div", { className: "xa-line", "data-line": true, onClick: turn.live && stepsMode !== 'collapsed' ? undefined : onToggle, role: turn.live && stepsMode !== 'collapsed' ? undefined : 'button', "aria-expanded": open, children: [_jsx("span", { className: `xa-dot${phase === 'settled' ? ' xa-gone' : ''}` }), _jsx("span", { ref: readRef, className: "xa-read xa-num", "data-read": true, children: read }), _jsx("span", { className: "xa-vr" }), _jsx(Ticker, { item: ticker }), _jsx(Chevron, {}), _jsx("span", { className: "xa-shots", "data-shots": true, children: !open && turn.steps.flatMap(step => step.kind === 'act' && step.row.detail?.kind === 'frames' ? step.row.detail.frames : []).map((frame, index) => _jsx("img", { className: "xa-frame", src: frame.src, alt: frame.alt || 'Captured screen' }, `${frame.src}:${index}`)) })] }));
185
+ }
186
+ // ── details ────────────────────────────────────────────────────────────────────
187
+ function DetailView({ detail, row, actions }) {
188
+ switch (detail.kind) {
189
+ case 'output':
190
+ return (_jsxs("div", { className: "xa-detail xa-output", children: [detail.text ? _jsx("pre", { className: "xa-mono", children: detail.text }) : detail.streaming ? _jsx("pre", { className: "xa-mono", style: { color: 'var(--xa-t6)' }, children: "\u2026" }) : null, detail.error && _jsx("pre", { className: "xa-mono xa-err", children: detail.error }), (detail.foot.duration || detail.foot.exit || detail.foot.cwd) && (_jsxs("div", { className: "xa-foot xa-num", children: [detail.foot.duration && _jsx("span", { children: detail.foot.duration }), detail.foot.exit && _jsx("span", { children: detail.foot.exit }), detail.foot.cwd && _jsx("span", { className: "xa-cwd xa-mono", title: detail.foot.cwd, children: detail.foot.cwd })] }))] }));
191
+ case 'slice':
192
+ return (_jsxs("div", { className: "xa-detail xa-slice", children: [_jsx("pre", { className: "xa-mono", children: detail.lines.map((line, index) => (_jsxs(React.Fragment, { children: [_jsx("span", { className: "xa-gut xa-num", children: (detail.from + index).toLocaleString('en-US') }), line, index < detail.lines.length - 1 ? '\n' : ''] }, index))) }), detail.more > 0 && _jsxs("div", { className: "xa-more xa-num", children: ["\u2026 ", detail.more.toLocaleString('en-US'), " more line", detail.more === 1 ? '' : 's'] })] }));
193
+ case 'diff':
194
+ return (_jsx("div", { className: "xa-detail xa-diff", children: detail.hunks.map((hunk) => (_jsxs("div", { className: "xa-hk", children: [_jsxs("div", { className: "xa-hunk", children: [_jsx("span", { className: "xa-mono", children: hunk.header }), detail.path && _jsx("span", { className: "xa-mono", style: { color: 'var(--xa-t6)' }, children: detail.path.split(/[/\\]/).pop() }), _jsxs("span", { className: "xa-keep", children: [detail.diff && actions.openDiff && _jsx("button", { type: "button", onClick: () => actions.openDiff?.(detail.diff), children: "Open" }), _jsx("button", { type: "button", disabled: true, title: "Landing a single hunk is not available on this host yet", children: "Keep" }), _jsx("button", { type: "button", disabled: true, title: "Undoing a single hunk is not available on this host yet", children: "Undo" })] })] }), _jsx("pre", { className: "xa-mono", children: hunk.lines.map((line, index) => (_jsxs("span", { className: line.kind === 'added' ? 'xa-add' : line.kind === 'removed' ? 'xa-del' : undefined, children: [line.kind === 'added' ? '+ ' : line.kind === 'removed' ? '− ' : ' ', line.text, index < hunk.lines.length - 1 ? '\n' : ''] }, index))) })] }, hunk.id))) }));
195
+ case 'list':
196
+ return (
197
+ /* the bench's list: what, then where or when at the right — no glyph, the row's own marks it */
198
+ _jsx("div", { className: "xa-detail xa-list", children: detail.items.map((item, index) => (_jsxs("div", { className: "xa-it", children: [item.path && actions.openPath
199
+ ? _jsx("button", { type: "button", className: "xa-k", title: item.path, onClick: () => actions.openPath?.(item.path), children: item.label })
200
+ : _jsx("span", { className: "xa-k", title: item.path, children: item.label }), item.where && _jsx("span", { className: "xa-where xa-num", children: item.where })] }, index))) }));
201
+ case 'matches':
202
+ return (_jsx("div", { className: "xa-detail xa-matches", children: detail.groups.map((group) => (_jsxs("div", { className: "xa-grp", children: [_jsxs("div", { className: "xa-file", children: [_jsx("span", { className: "xa-fg", children: _jsx(Glyph, { name: "file" }) }), actions.openPath
203
+ ? _jsx("button", { type: "button", className: "xa-k xa-mono", onClick: () => actions.openPath?.(group.file), children: group.file })
204
+ : _jsx("span", { className: "xa-k xa-mono", children: group.file }), _jsx("span", { className: "xa-where xa-num", children: group.lines.length })] }), _jsx("div", { className: "xa-lns", children: group.lines.map((line, index) => (_jsxs("div", { className: "xa-ln", children: [_jsx("span", { className: "xa-n xa-num", children: line.n }), _jsx("span", { className: "xa-s xa-mono", children: line.text })] }, index))) })] }, group.file))) }));
205
+ case 'sources':
206
+ return _jsx(SourcesDetail, { items: detail.items, actions: actions });
207
+ case 'prose':
208
+ return _jsx("div", { className: "xa-detail xa-prose", children: detail.text });
209
+ case 'frames':
210
+ return (_jsx("div", { className: "xa-detail xa-frames", children: detail.frames.map((frame, index) => _jsx("img", { className: "xa-frame", src: frame.src, alt: frame.alt || `frame ${index + 1}` }, index)) }));
211
+ default:
212
+ return null;
213
+ }
214
+ }
215
+ function SourcesDetail({ items, actions }) {
216
+ return (_jsx("div", { className: "xa-detail xa-sources", children: items.map((item, index) => (_jsxs("div", { className: "xa-it", children: [_jsx("span", { className: "xa-n xa-num", children: index + 1 }), _jsx("span", { className: "xa-fav", "aria-hidden": "true" }), _jsx("button", { type: "button", className: "xa-k", title: item.url, onClick: () => actions.openExternal?.(item.url), children: item.title }), _jsx("span", { className: "xa-where", children: item.domain }), _jsx("span", { className: "xa-go", children: _jsx(Glyph, { name: "arrow-up-right", size: 10 }) })] }, item.url))) }));
217
+ }
218
+ // ── the ask card ───────────────────────────────────────────────────────────────
219
+ function AskCard({ row, ask, actions, children }) {
220
+ const [draft, setDraft] = useState('');
221
+ const [picked, setPicked] = useState(null);
222
+ const [sending, setSending] = useState(false);
223
+ const [error, setError] = useState(null);
224
+ const [menu, setMenu] = useState(false);
225
+ const pending = ask.status === 'pending';
226
+ const { permission, askUser } = ask;
227
+ const placement = ask.host?.kind === 'workspace-placement' && actions.workspacePlacement?.workspacePlacementId === ask.host.workspacePlacementId ? actions.workspacePlacement : null;
228
+ const goForward = useCallback(async () => {
229
+ if (!pending || sending)
230
+ return;
231
+ if (ask.host?.kind === 'choose-directory') {
232
+ actions.chooseDirectory?.(row.toolCall);
233
+ return;
234
+ }
235
+ if (ask.host?.kind === 'workspace-placement') {
236
+ placement?.onChoose('inbox');
237
+ return;
238
+ }
239
+ if (ask.kind === 'question' && askUser) {
240
+ const value = picked ?? (ask.options?.length && !draft.trim() ? ask.options[0] : draft);
241
+ setSending(true);
242
+ setError(null);
243
+ const failure = await actions.ask.answer(askUser, value, picked ?? (ask.options?.includes(value) ? value : undefined));
244
+ setSending(false);
245
+ if (failure)
246
+ setError(failure);
247
+ return;
248
+ }
249
+ if (permission)
250
+ actions.ask.grant(row.toolCall, permission);
251
+ }, [pending, sending, ask, askUser, permission, picked, draft, actions, row.toolCall, placement]);
252
+ const cancel = useCallback(() => {
253
+ if (!pending || ask.host)
254
+ return;
255
+ if (ask.kind === 'question' && askUser) {
256
+ actions.ask.cancelQuestion(askUser);
257
+ return;
258
+ }
259
+ if (permission)
260
+ actions.ask.deny(permission);
261
+ }, [pending, ask.kind, askUser, permission, actions]);
262
+ // ⌘↵ takes the way forward, ⌘. cancels — while the card is the pending one
263
+ useEffect(() => {
264
+ if (!pending)
265
+ return;
266
+ const onKey = (event) => {
267
+ if (!(event.metaKey || event.ctrlKey))
268
+ return;
269
+ if (event.key === 'Enter') {
270
+ event.preventDefault();
271
+ void goForward();
272
+ }
273
+ if (event.key === '.') {
274
+ event.preventDefault();
275
+ cancel();
276
+ }
277
+ };
278
+ document.addEventListener('keydown', onKey);
279
+ return () => document.removeEventListener('keydown', onKey);
280
+ }, [pending, goForward, cancel]);
281
+ const answered = !pending;
282
+ // the bench's card names the thing asked for — the app window — while its rail row carries the lock
283
+ const headerGlyph = ask.kind === 'grant' ? (ask.subject === 'app' ? 'app-window' : ask.subject === 'folder' ? 'folder' : 'lock') : ask.kind === 'approve' ? 'terminal' : ask.kind === 'plan' ? 'list' : 'help';
284
+ return (_jsxs("div", { className: `xa-card${answered ? ' xa-answered' : ''}${ask.status === 'yes' ? ' xa-ok' : ask.status === 'no' || ask.status === 'cancelled' ? ' xa-bad' : ''}`, "data-ask": ask.kind, children: [_jsxs("div", { className: "xa-hd", children: [_jsx(Glyph, { name: headerGlyph }), ask.title, ask.scope && _jsx("span", { className: "xa-right", children: ask.scope })] }), _jsxs("div", { className: "xa-bd", children: [_jsxs("div", { className: "xa-what", children: [_jsx("span", { className: "xa-sq" }), _jsx("span", { children: ask.what }), ask.whatScope && _jsx("span", { className: "xa-scope", children: ask.whatScope })] }), children, ask.host?.kind === 'workspace-placement' ? (_jsxs("div", { className: "xa-opts", children: [_jsxs("button", { type: "button", className: "xa-opt", disabled: !placement || placement.resolving, onClick: () => placement?.onChoose('inbox'), children: [_jsx("span", { className: "xa-osq" }), "Keep in Inbox"] }), placement?.existingWorkspaceName && _jsxs("button", { type: "button", className: "xa-opt", disabled: placement.resolving, onClick: () => placement.onChoose('reuse'), children: [_jsx("span", { className: "xa-osq" }), "Move to ", placement.existingWorkspaceName] }), _jsxs("button", { type: "button", className: "xa-opt", disabled: !placement || placement.resolving, onClick: () => placement?.onChoose('create'), children: [_jsx("span", { className: "xa-osq" }), placement?.resolving ? 'Working…' : `Create ${placement?.suggestedWorkspaceName || 'a workspace'}`] })] })) : ask.kind === 'question' ? (_jsxs(_Fragment, { children: [ask.body && _jsx("div", { children: ask.body }), ask.options && (_jsx("div", { className: "xa-opts", children: ask.options.map((option) => (_jsxs("button", { type: "button", className: `xa-opt${(picked ?? ask.answer) === option ? ' xa-on' : ''}`, onClick: () => { setPicked(option); if (!answered)
285
+ void actions.ask.answer(askUser, option, option).then((failure) => failure && setError(failure)); }, children: [_jsx("span", { className: "xa-osq" }), option] }, option))) })), pending && !ask.options && (_jsx("div", { className: "xa-free", children: _jsx("input", { value: draft, onChange: (event) => setDraft(event.target.value), placeholder: "Your answer", onKeyDown: (event) => { if (event.key === 'Enter') {
286
+ event.preventDefault();
287
+ void goForward();
288
+ } } }) }))] })) : ask.body ? (
289
+ // a sentence is just the card's words (bench: a bare text node in `.bd`); a command is a block
290
+ ask.bodyMono ? _jsx("pre", { className: "xa-mono", children: ask.body }) : ask.body) : null] }), error && _jsx("div", { className: "xa-error", children: error }), _jsxs("div", { className: "xa-ft", children: [_jsxs("span", { className: "xa-cacts", children: [ask.host ? _jsx("span", { className: "xa-hint", children: ask.host.kind === 'choose-directory' ? 'the picker decides' : 'pick one' }) : _jsxs("button", { type: "button", className: "xa-b", onClick: cancel, disabled: sending, children: ["Cancel ", _jsx("kbd", { children: KEY_CANCEL })] }), ask.host?.kind === 'choose-directory' ? (_jsx("button", { type: "button", className: "xa-b xa-main", onClick: () => void goForward(), disabled: !actions.chooseDirectory, title: actions.chooseDirectory ? undefined : 'This surface has no folder picker', children: ask.mainLabel })) : ask.host?.kind === 'workspace-placement' ? null : ask.kind === 'question' ? (ask.options
291
+ ? _jsxs("span", { className: "xa-hint", children: ["pick one \u00B7 ", _jsx("kbd", { children: KEY_ENTER }), " takes the first"] })
292
+ : _jsxs("button", { type: "button", className: "xa-b xa-main", onClick: () => void goForward(), disabled: sending || !draft.trim(), children: ["Send ", _jsx("kbd", { children: KEY_ENTER })] })) : (
293
+ /* the bench's footer holds its buttons directly — no wrapper, so none here either */
294
+ _jsxs(_Fragment, { children: [(ask.canAllowAlways || ask.canDenyAlways) && (_jsxs("span", { className: "xa-more", children: [_jsx("button", { type: "button", className: "xa-b", onClick: () => setMenu((value) => !value), "aria-haspopup": "menu", "aria-expanded": menu, children: "Remember\u2026" }), menu && (_jsxs("div", { className: "xa-menu", role: "menu", children: [ask.canAllowAlways && permission && _jsx("button", { type: "button", role: "menuitem", onClick: () => { setMenu(false); actions.ask.grantAlways(row.toolCall, permission); }, children: "Always allow this" }), ask.canDenyAlways && permission && _jsx("button", { type: "button", role: "menuitem", onClick: () => { setMenu(false); actions.ask.denyAlways(permission); }, children: "Always refuse this" })] }))] })), _jsxs("button", { type: "button", className: "xa-b xa-main", onClick: () => void goForward(), disabled: !actions.ask.canAnswer, title: actions.ask.canAnswer ? undefined : 'This surface cannot answer the agent', children: [ask.mainLabel, " ", _jsx("kbd", { children: KEY_ENTER })] })] }))] }), _jsx("span", { className: "xa-verdict", children: ask.verdict })] })] }));
295
+ }
296
+ // ── rows ───────────────────────────────────────────────────────────────────────
297
+ function StepRowView({ row, actions, last, reveal = 0 }) {
298
+ const [open, setOpen] = useState(false);
299
+ useEffect(() => { if (reveal)
300
+ setOpen(true); }, [reveal]);
301
+ const ask = row.ask;
302
+ // A backgrounded run's row is answered by the RUN, not by the tool call that started it: that call
303
+ // completed the moment the command was detached, and everything after belongs to the task.
304
+ const run = row.background?.action === 'started' && actions.background ? actions.background.find(row.background.taskId) : null;
305
+ const runLive = !!run && run.status === 'running';
306
+ const background = row.background?.action === 'started'
307
+ ? {
308
+ taskId: row.background.taskId,
309
+ live: run ? runLive : true,
310
+ verb: run ? (runLive ? 'Running' : run.status === 'stopped' ? 'Stopped' : 'Ran') : 'Started',
311
+ result: run
312
+ ? [
313
+ run.status === 'running' ? 'in the background' : run.status === 'stopped' ? 'stopped' : typeof run.exitCode === 'number' ? 'exit ' + run.exitCode : run.status,
314
+ formatElapsed((runLive ? Date.now() : run.updatedAt) - run.startedAt),
315
+ ].filter(Boolean).join(' · ')
316
+ : row.result,
317
+ streamed: run?.lastOutput,
318
+ busy: run?.busy,
319
+ }
320
+ : null;
321
+ const askClass = ask ? ` xa-ask${ask.status === 'yes' ? ' xa-ok' : ask.status === 'no' || ask.status === 'cancelled' ? ' xa-bad' : ''}${ask.status !== 'pending' && !last ? ' xa-joined' : ''}` : '';
322
+ const toneClass = !ask && row.tone !== 'none' && !row.live ? ` xa-${row.tone}` : '';
323
+ const hasDetail = !!row.detail;
324
+ const { verb, object } = spokenStep(row);
325
+ return (_jsxs("div", {
326
+ // a pending ask IS the live step (bench `.xrow.now.ask`): the rail has stopped at it, and `now` is
327
+ // what draws its marker breathing and its glyph in motion
328
+ className: `xa-row${(background ? background.live : row.live) || ask?.status === 'pending' ? ' xa-now' : ''}${askClass}${toneClass}${hasDetail || background?.streamed ? ' xa-hasd' : ''}${open ? ' xa-open' : ''}`, "data-step": row.family, onClick: hasDetail && !row.live ? (event) => { if (!event.target.closest('.xa-dwrap, button, a'))
329
+ setOpen((value) => !value); } : undefined, children: [_jsx("span", { className: "xa-ic", children: _jsx(Glyph, { name: row.glyph }) }), _jsx("span", { className: ask ? 'xa-what' : 'xa-verb', children: background ? background.verb : verb }), object && _jsx("span", { className: `xa-obj${row.objectMono || (ask?.kind === 'approve') ? ' xa-mono' : ''}`, title: object, children: object }), background && actions.background && (_jsxs("span", { className: "xa-rowacts", children: [_jsx("button", { type: "button", disabled: !!background.busy, onClick: (event) => { event.stopPropagation(); actions.background.output(background.taskId); }, title: "Read this run's output so far", children: background.busy === 'output' ? 'Reading…' : 'Output' }), background.live && _jsx("button", { type: "button", disabled: !!background.busy, onClick: (event) => { event.stopPropagation(); actions.background.stop(background.taskId); }, title: "Stop this background run", children: background.busy === 'stop' ? 'Stopping…' : 'Stop' })] })), (background ? background.result : row.result) && !ask && _jsx("span", { className: `xa-res xa-num${row.resultLate ? ' xa-late' : ''}`, children: background ? background.result : row.result }), row.detail?.kind === 'frames' && _jsx("span", { className: "xa-frames", children: row.detail.frames.map((frame, index) => _jsx("img", { className: "xa-frame", src: frame.src, alt: frame.alt || 'Captured screen' }, `${frame.src}:${index}`)) }), hasDetail && _jsx(Chevron, {}), ask && _jsxs(_Fragment, { children: [_jsx("span", { className: "xa-brk" }), _jsx("div", { className: "xa-cardslot", children: _jsx(AskCard, { row: row, ask: ask, actions: actions }) })] }), row.detail
330
+ ? _jsx("div", { className: "xa-dwrap", children: _jsx("div", { children: _jsx(DetailView, { detail: background?.streamed && row.detail.kind === 'output' ? { ...row.detail, text: background.streamed, streaming: background.live } : row.detail, row: row, actions: actions }) }) })
331
+ : background?.streamed
332
+ ? _jsx("div", { className: "xa-dwrap", children: _jsx("div", { children: _jsx(DetailView, { detail: { kind: 'output', text: background.streamed, streaming: background.live, foot: {} }, row: row, actions: actions }) }) })
333
+ : null] }));
334
+ }
335
+ function ThinkRow({ step, durationMs }) {
336
+ const [open, setOpen] = useState(false);
337
+ const label = step.live ? 'Thinking' : durationMs && durationMs >= 1000 ? `Thought for ${formatElapsed(durationMs)}` : 'Thought';
338
+ return (_jsxs("div", { className: `xa-row xa-think${step.live ? ' xa-now' : ''}${open ? ' xa-open' : ''}`, children: [_jsx("span", { className: "xa-ic", children: _jsx(Glyph, { name: "brain" }) }), _jsxs("span", { className: "xa-head", onClick: () => { if (!step.live)
339
+ setOpen((value) => !value); }, role: "button", "aria-expanded": open, children: [_jsx("span", { className: "xa-what", children: label }), _jsx("span", { className: "xa-dot2", children: "\u00B7" }), _jsx("span", { className: "xa-curthought", children: currentThought(step.text) }, currentThought(step.text)), _jsx(Chevron, {})] }), _jsx("span", { className: "xa-fullwrap", children: _jsx("span", { className: "xa-full", children: step.text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).map((line, index) => _jsx("p", { children: line }, `${index}:${line}`)) }) })] }));
340
+ }
341
+ /** The agent's words BETWEEN two steps — its own sentence, not a step, and not a reply body. */
342
+ function NoteRow({ text }) {
343
+ return (_jsxs("div", { className: "xa-row xa-note", children: [_jsx("span", { className: "xa-ic" }), _jsx("span", { children: text })] }));
344
+ }
345
+ const planSteps = (count) => `${count} step${count === 1 ? '' : 's'}`;
346
+ /**
347
+ * One plan item, marked with where the plan has got to. The tray and both plan rows draw the same
348
+ * one. When nothing is explicitly in progress the FIRST unfinished step is the one marked `now` —
349
+ * the list always says where the plan stands, as the bench's does, rather than going blank between
350
+ * two steps.
351
+ */
352
+ function PlanList({ tasks, marked = true }) {
353
+ const current = tasks.some((task) => task.status === 'in_progress')
354
+ ? null
355
+ : tasks.find((task) => task.status !== 'done' && task.status !== 'blocked')?.id ?? null;
356
+ return (_jsx("div", { className: "xa-plan-list", children: tasks.map((task) => (_jsxs("div", { className: `xa-item${!marked ? '' : task.status === 'done' ? ' xa-done' : task.status === 'in_progress' || task.id === current ? ' xa-now' : task.status === 'blocked' ? ' xa-blocked' : ''}`, children: [_jsx("span", { className: "xa-sq" }), _jsx("span", { children: task.text })] }, task.id))) }));
357
+ }
358
+ function PlanHead({ tasks, done, onToggle, open }) {
359
+ return (_jsxs("span", { className: "xa-head", onClick: onToggle, role: "button", "aria-expanded": open, children: [_jsx("span", { className: "xa-verb", children: "Planned" }), _jsx("span", { className: "xa-obj", children: planSteps(tasks.length) }), _jsx("span", { className: "xa-res xa-num", children: `${done} of ${tasks.length}` }), _jsx(Chevron, {})] }));
360
+ }
361
+ function PlanRow({ tasks, done }) {
362
+ const [open, setOpen] = useState(false);
363
+ return (_jsxs("div", { className: `xa-row xa-plan${open ? ' xa-open' : ''}`, children: [_jsx("span", { className: "xa-ic", children: _jsx(Glyph, { name: "list" }) }), _jsx(PlanHead, { tasks: tasks, done: done, open: open, onToggle: () => setOpen((value) => !value) }), _jsx("div", { className: "xa-planwrap", children: _jsx("div", { children: _jsx(PlanList, { tasks: tasks }) }) })] }));
364
+ }
365
+ /**
366
+ * The plan AND the go it is waiting for, in one row (bench `.xrow.plan.ask`). The rail has stopped
367
+ * here, so this row is the live one; the card restates the steps as they will be run, unmarked —
368
+ * nothing has started, and marking them would say otherwise.
369
+ */
370
+ function PlanAskRow({ step, actions }) {
371
+ const [open, setOpen] = useState(false);
372
+ const ask = step.row.ask;
373
+ const pending = ask.status === 'pending';
374
+ return (_jsxs("div", { className: `xa-row xa-plan xa-ask${pending ? ' xa-now' : ask.status === 'yes' ? ' xa-ok' : ' xa-bad'}${open ? ' xa-open' : ''}`, "data-step": "plan", children: [_jsx("span", { className: "xa-ic", children: _jsx(Glyph, { name: "list" }) }), _jsx(PlanHead, { tasks: step.tasks, done: step.done, open: open, onToggle: () => setOpen((value) => !value) }), _jsx("div", { className: "xa-planwrap", children: _jsx("div", { children: _jsx(PlanList, { tasks: step.tasks }) }) }), _jsx("span", { className: "xa-brk" }), _jsx("div", { className: "xa-cardslot", children: _jsx(AskCard, { row: step.row, ask: { ...ask, scope: planSteps(step.tasks.length) }, actions: actions, children: _jsx(PlanList, { tasks: step.tasks, marked: false }) }) })] }));
375
+ }
376
+ function HousekeepingRow({ rows, actions }) {
377
+ const [open, setOpen] = useState(false);
378
+ return (_jsxs("div", { className: `xa-row xa-hasd${open ? ' xa-open' : ''}`, onClick: (event) => { if (!event.target.closest('.xa-dwrap'))
379
+ setOpen((value) => !value); }, children: [_jsx("span", { className: "xa-ic", children: _jsx(Glyph, { name: "eye-off" }) }), _jsx("span", { className: "xa-verb", children: "Housekeeping" }), _jsxs("span", { className: "xa-obj", children: [rows.length, " step", rows.length === 1 ? '' : 's', " hidden"] }), _jsx(Chevron, {}), _jsx("div", { className: "xa-dwrap", children: _jsx("div", { children: _jsx("div", { className: "xa-detail xa-nest", children: _jsx("div", { className: "xa-rail", children: rows.map((row, index) => _jsx(StepRowView, { row: row, actions: actions, last: index === rows.length - 1 }, row.id)) }) }) }) })] }));
380
+ }
381
+ function EndingRow({ ending, actions, onShowOutput }) {
382
+ const tone = ending.kind === 'credits' ? 'xa-warn' : ending.kind === 'failed' ? 'xa-bad' : 'xa-stop';
383
+ const glyph = ending.kind === 'credits' ? 'alert' : 'x';
384
+ return (_jsxs("div", { className: `xa-row ${tone}`, style: { alignItems: 'flex-start' }, children: [_jsx("span", { className: "xa-ic", children: _jsx(Glyph, { name: glyph }) }), _jsx("span", { className: "xa-what", style: { paddingTop: 1 }, children: ending.text }), _jsx("span", { className: "xa-why", style: { paddingTop: 1 }, children: ending.why.map((part, index) => (typeof part === 'string'
385
+ ? _jsx(React.Fragment, { children: part }, index)
386
+ : _jsx("code", { children: part.code }, index))) }), ending.kind === 'interrupted' && _jsx("button", { type: "button", className: "xa-act", onClick: actions.resume, disabled: !actions.resume, title: actions.resume ? 'Restart the interrupted turn through the host' : 'This surface cannot run a turn again', children: "Continue" }), ending.kind === 'failed' && ending.action === 'show-output' && _jsx("button", { type: "button", className: "xa-act", onClick: onShowOutput, children: "Show output" }), ending.kind === 'credits' && _jsx("button", { type: "button", className: "xa-act", onClick: () => actions.openExternal?.('https://xenostudio.ai/account'), disabled: !actions.openExternal, children: "Get credits" })] }));
387
+ }
388
+ // ── the receipt ────────────────────────────────────────────────────────────────
389
+ function FilesReceipt({ turn, actions }) {
390
+ const [review, setReview] = useState(false);
391
+ const files = turn.receipt?.files || [];
392
+ if (!files.length)
393
+ return null;
394
+ const added = files.reduce((sum, file) => sum + file.added, 0);
395
+ const removed = files.reduce((sum, file) => sum + file.removed, 0);
396
+ return (_jsxs("div", { className: "xa-files", children: [_jsxs("div", { className: "xa-hd", children: [_jsx("span", { className: "xa-ic", children: _jsx(Glyph, { name: "edit" }) }), _jsxs("span", { className: "xa-t", children: [`Edited ${files.length} file${files.length === 1 ? '' : 's'}`, _jsxs("span", { className: "xa-n xa-num", children: [_jsx("span", { className: "xa-add", children: `+${added}` }), _jsx("span", { className: "xa-del", children: `−${removed}` })] })] }), _jsx("button", { type: "button", className: "xa-b", disabled: true, title: "Reverting these edits is not available on this host yet", children: "Undo" }), _jsx("button", { type: "button", className: "xa-b", onClick: () => setReview((value) => !value), "aria-expanded": review, children: "Review" })] }), files.map((file) => (_jsxs("div", { className: "xa-frow", children: [actions.openPath
397
+ ? _jsx("button", { type: "button", className: "xa-name xa-mono", title: file.path, onClick: () => actions.openPath?.(file.path), children: file.name })
398
+ : _jsx("span", { className: "xa-name xa-mono", title: file.path, children: file.name }), _jsxs("span", { className: "xa-n xa-num", children: [_jsx("span", { className: "xa-add", children: `+${file.added}` }), _jsx("span", { className: "xa-del", children: `−${file.removed}` })] })] }, file.path))), review && (_jsx("div", { className: "xa-review", children: files.map((file) => file.row.detail?.kind === 'diff' ? _jsx(DetailView, { detail: file.row.detail, row: file.row, actions: actions }, file.path) : null) }))] }));
399
+ }
400
+ // ── the turn ───────────────────────────────────────────────────────────────────
401
+ export function TranscriptTurn({ msg, stepsMode, actions, renderMarkdown, tasks, model, latest }) {
402
+ const turn = useMemo(() => deriveTurn(msg, { tasks }), [msg, tasks]);
403
+ const { elapsedMs, settledAt } = useTurnTiming(turn);
404
+ const [userOpen, setUserOpen] = useState(null);
405
+ const wasLive = useRef(turn.live);
406
+ const [holdingRail, setHoldingRail] = useState(false);
407
+ useLayoutEffect(() => {
408
+ const finished = wasLive.current && !turn.live;
409
+ wasLive.current = turn.live;
410
+ if (turn.live) {
411
+ setHoldingRail(false);
412
+ return;
413
+ }
414
+ if (!finished)
415
+ return;
416
+ setHoldingRail(true);
417
+ const timer = setTimeout(() => setHoldingRail(false), 1400);
418
+ return () => clearTimeout(timer);
419
+ }, [turn.live]);
420
+ const railLive = turn.live || holdingRail;
421
+ const open = railLive ? (stepsMode === 'collapsed' ? userOpen === true : true) : userOpen === true;
422
+ const [sourcesOpen, setSourcesOpen] = useState(false);
423
+ const [copied, setCopied] = useState('idle');
424
+ const copyTimer = useRef(null);
425
+ const railRef = useRef(null);
426
+ const screenshotPositions = useRef([]);
427
+ useLayoutEffect(() => {
428
+ const rail = railRef.current;
429
+ if (!rail)
430
+ return;
431
+ if (open) {
432
+ screenshotPositions.current = Array.from(rail.querySelectorAll('.xa-row > .xa-frames > .xa-frame'), frame => frame.getBoundingClientRect());
433
+ return;
434
+ }
435
+ if (window.matchMedia('(prefers-reduced-motion: reduce)').matches)
436
+ return;
437
+ const shots = rail.closest('.xa-turn')?.querySelectorAll('.xa-shots .xa-frame');
438
+ const animations = [];
439
+ shots?.forEach((shot, index) => {
440
+ const from = screenshotPositions.current[index];
441
+ if (!from)
442
+ return;
443
+ const to = shot.getBoundingClientRect();
444
+ animations.push(shot.animate([
445
+ { transform: `translate(${from.left - to.left}px, ${from.top - to.top}px)` },
446
+ { transform: 'none' },
447
+ ], { duration: 340, easing: 'cubic-bezier(.3,0,.2,1)' }));
448
+ });
449
+ return () => animations.forEach(animation => animation.cancel());
450
+ }, [open, turn.steps]);
451
+ // how long the thought took: measured from when the thought went live to when it stopped, and
452
+ // handed to the surface to keep — otherwise reopening the conversation loses it
453
+ const thinkStartRef = useRef(null);
454
+ const thinkMsRef = useRef(null);
455
+ const [measuredThinkMs, setMeasuredThinkMs] = useState(null);
456
+ const think = turn.steps.find((step) => step.kind === 'think');
457
+ useEffect(() => {
458
+ if (think?.live && thinkStartRef.current === null)
459
+ thinkStartRef.current = Date.now();
460
+ if (think && !think.live && thinkStartRef.current !== null && thinkMsRef.current === null) {
461
+ thinkMsRef.current = Date.now() - thinkStartRef.current;
462
+ setMeasuredThinkMs(thinkMsRef.current);
463
+ actions.rememberThinkingTime?.(thinkMsRef.current);
464
+ }
465
+ }, [think, actions.rememberThinkingTime]);
466
+ const thinkMs = msg.thinkingMs ?? measuredThinkMs;
467
+ const hasRail = turn.steps.length > 0;
468
+ const [outputRequest, setOutputRequest] = useState({ id: '', revision: 0 });
469
+ const showOutput = useCallback(() => {
470
+ setUserOpen(true);
471
+ const output = [...turn.steps].reverse().find(step => step.kind === 'act' && step.row.detail?.kind === 'output');
472
+ setOutputRequest(previous => ({ id: output?.id ?? '', revision: previous.revision + 1 }));
473
+ }, [turn.steps]);
474
+ // a copy that was refused says so — a control that reports success it did not have is a bug
475
+ const copyReply = useCallback(() => {
476
+ if (copyTimer.current)
477
+ clearTimeout(copyTimer.current);
478
+ Promise.resolve()
479
+ .then(() => actions.copy(turn.reply))
480
+ .then(() => setCopied('done'), () => setCopied('failed'))
481
+ .then(() => { copyTimer.current = setTimeout(() => setCopied('idle'), 1600); });
482
+ }, [actions, turn.reply]);
483
+ useEffect(() => () => { if (copyTimer.current)
484
+ clearTimeout(copyTimer.current); }, []);
485
+ const settledTime = settledAt ? new Date(settledAt) : new Date(msg.timestamp);
486
+ const time = settledTime.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
487
+ // the surface hands down the model's DISPLAY name for this message; the raw id is the fallback
488
+ const modelName = (model || msg.model || '').replace(/^local:/, '');
489
+ // and how long the turn took, from the host's own end time where it has one — so it survives a reload
490
+ const took = settledAt && settledAt > turn.startedAt ? formatElapsed(settledAt - turn.startedAt) : null;
491
+ return (_jsxs("div", { className: "xa-agent", "data-turn": msg.id, children: [hasRail && (
492
+ /* `xa-open` is the PERSON's choice, as in the bench (`.turn.open`): it rotates the chevron and
493
+ unfolds a turn that is folded. A live turn in expanded mode shows its rail without it — marking
494
+ every live turn open drew its chevron rotated, which the bench never does. `open` itself still
495
+ says whether the rail is showing, for the clock line's aria-expanded. */
496
+ _jsxs("div", { className: `xa-turn${railLive ? ' xa-live' : ''}${!turn.live ? ' xa-done' : ''}${userOpen === true ? ' xa-open' : ''}${turn.asking ? ' xa-asking' : ''}${turn.opening ? ' xa-opening' : ''}`, children: [_jsx(ClockLine, { turn: turn, elapsedMs: elapsedMs, open: open, onToggle: () => setUserOpen((value) => !(value ?? (turn.live ? true : false))), stepsMode: stepsMode, holdingRail: holdingRail }), _jsx("div", { className: "xa-railwrap", children: _jsx("div", { className: "xa-rail", ref: railRef, children: turn.steps.map((step, index) => {
497
+ const last = index === turn.steps.length - 1;
498
+ switch (step.kind) {
499
+ case 'think': return _jsx(ThinkRow, { step: step, durationMs: thinkMs }, step.id);
500
+ case 'note': return _jsx(NoteRow, { text: step.text }, step.id);
501
+ case 'act': return _jsx(StepRowView, { row: step.row, actions: actions, last: last, reveal: outputRequest.id === step.id ? outputRequest.revision : 0 }, step.id);
502
+ case 'housekeeping': return _jsx(HousekeepingRow, { rows: step.rows, actions: actions }, step.id);
503
+ case 'plan': return _jsx(PlanRow, { tasks: step.tasks, done: step.done }, step.id);
504
+ case 'planask': return _jsx(PlanAskRow, { step: step, actions: actions }, step.id);
505
+ case 'ending': return _jsx(EndingRow, { ending: step.ending, actions: actions, onShowOutput: showOutput }, step.id);
506
+ default: return null;
507
+ }
508
+ }) }) })] })), !hasRail && turn.live && !turn.reply && (_jsx("div", { className: "xa-turn xa-live xa-opening", children: _jsx(ClockLine, { turn: turn, elapsedMs: elapsedMs, open: false, onToggle: () => { }, stepsMode: stepsMode }) })), (turn.reply || turn.replyStreaming) && (_jsx(ReplyBody, { streaming: turn.replyStreaming, children: renderMarkdown(turn.reply) })), !turn.live && turn.sources.length > 0 && (_jsxs(_Fragment, { children: [_jsxs("div", { className: `xa-srcline${sourcesOpen ? ' xa-open' : ''}`, onClick: () => setSourcesOpen((value) => !value), role: "button", "aria-expanded": sourcesOpen, children: [_jsx("span", { className: "xa-stack", children: turn.sources.slice(0, 3).map((source) => _jsx("span", { className: "xa-fav" }, source.url)) }), _jsxs("span", { children: [turn.sources.length, " source", turn.sources.length === 1 ? '' : 's'] }), _jsx(Chevron, {})] }), _jsx("div", { className: `xa-srcwrap${sourcesOpen ? ' xa-open' : ''}`, children: _jsx("div", { children: _jsx(SourcesDetail, { items: turn.sources, actions: actions }) }) })] })), !turn.live && _jsx(FilesReceipt, { turn: turn, actions: actions }), !turn.live && turn.reply && (_jsxs("div", { className: `xa-acts xa-rfoot${latest ? ' xa-on' : ''}`, children: [actions.retry && _jsxs("button", { type: "button", className: "xa-b", onClick: actions.retry, children: [_jsx(Glyph, { name: "refresh", size: 11 }), "Retry"] }), _jsxs("button", { type: "button", className: `xa-b${copied === 'done' ? ' xa-done' : copied === 'failed' ? ' xa-failed' : ''}`, onClick: copyReply, "aria-live": "polite", children: [_jsx(Glyph, { name: copied === 'done' ? 'check' : copied === 'failed' ? 'x' : 'copy', size: 11 }), copied === 'done' ? 'Copied' : copied === 'failed' ? 'Copy failed. Try again.' : 'Copy'] }), actions.edit && _jsxs("button", { type: "button", className: "xa-b", onClick: actions.edit, children: [_jsx(Glyph, { name: "edit", size: 11 }), "Edit"] }), _jsx("span", { className: "xa-vr" }), ['good', 'poor'].map((value) => (_jsx("button", { type: "button", className: `xa-b xa-tog${msg.rating === value ? ' xa-on' : ''}`, "data-tog": value, "aria-pressed": msg.rating === value, disabled: !actions.rate, title: value === 'good' ? 'Good reply' : 'Poor reply', onClick: () => actions.rate?.(msg.rating === value ? null : value), children: _jsx(Glyph, { name: value === 'good' ? 'thumbs-up' : 'thumbs-down', size: 11 }) }, value))), _jsx("span", { className: "xa-meta xa-num", children: [modelName, took ?? time].filter(Boolean).join(' · ') })] }))] }));
509
+ }
510
+ // ── the plan on the composer's top edge ────────────────────────────────────────
511
+ /**
512
+ * The background runs a conversation is holding — a bar on the composer's top edge, the Tasks tray's
513
+ * shape. It exists because a backgrounded command outlives the turn that started it: without it the
514
+ * only place a run appeared was the workbench, which a panel host does not mount.
515
+ */
516
+ export function BackgroundTray({ runs, open, onToggle, onOutput, onStop }) {
517
+ if (!runs.length)
518
+ return null;
519
+ const running = runs.filter((run) => run.status === 'running');
520
+ const current = running[0] || runs[0];
521
+ return (_jsxs("div", { className: `xa-transcript xa-tasks xa-bgruns${open ? ' xa-open' : ''}`, "data-background-runs": true, children: [_jsxs("button", { type: "button", className: "xa-bar", onClick: onToggle, "aria-expanded": open, children: [_jsx("span", { className: "xa-label", children: running.length ? 'Running' : 'Ran' }), _jsx("span", { className: "xa-curtask xa-mono", children: current.command || current.taskId }), _jsx("span", { className: "xa-cnt xa-num", children: running.length ? `${running.length} of ${runs.length}` : String(runs.length) }), _jsx(Chevron, {})] }), _jsx("div", { className: "xa-listwrap", children: _jsx("div", { className: "xa-list", children: _jsx("div", { className: "xa-in", children: runs.map((run) => (_jsxs("div", { className: `xa-item${run.status === 'running' ? ' xa-now' : run.status === 'failed' ? ' xa-blocked' : ' xa-done'}`, children: [_jsx("span", { className: "xa-sq" }), _jsx("span", { className: "xa-mono", title: run.command || run.taskId, children: run.command || run.taskId }), _jsx("span", { className: "xa-runres xa-num", children: run.status === 'running' ? formatElapsed(Date.now() - run.startedAt) : typeof run.exitCode === 'number' ? `exit ${run.exitCode}` : run.status }), _jsxs("span", { className: "xa-rowacts", children: [_jsx("button", { type: "button", disabled: !!run.busy, onClick: () => onOutput(run.taskId), children: run.busy === 'output' ? 'Reading…' : 'Output' }), run.status === 'running' && _jsx("button", { type: "button", disabled: !!run.busy, onClick: () => onStop(run.taskId), children: run.busy === 'stop' ? 'Stopping…' : 'Stop' })] })] }, run.taskId))) }) }) })] }));
522
+ }
523
+ /**
524
+ * The sub-agents this conversation has spawned (D11's Sub-work family, seen from the dock). In this
525
+ * runtime a delegation IS a child conversation, so the tray lists those rather than inventing a
526
+ * second record of them — and opening one is opening that conversation, which is where its own rail
527
+ * already lives.
528
+ */
529
+ export function AgentsTray({ runs, open, onToggle, onOpen }) {
530
+ if (!runs.length)
531
+ return null;
532
+ const live = runs.filter((run) => run.live);
533
+ const current = live[0] || runs[0];
534
+ return (_jsxs("div", { className: `xa-transcript xa-tasks xa-agents${open ? ' xa-open' : ''}`, "data-delegated-runs": true, children: [_jsxs("button", { type: "button", className: "xa-bar", onClick: onToggle, "aria-expanded": open, children: [_jsx("span", { className: "xa-label", children: live.length ? 'Delegated' : 'Delegated · done' }), _jsx("span", { className: "xa-curtask", children: current.current || current.title }), _jsx("span", { className: "xa-cnt xa-num", children: live.length ? `${live.length} of ${runs.length}` : String(runs.length) }), _jsx(Chevron, {})] }), _jsx("div", { className: "xa-listwrap", children: _jsx("div", { className: "xa-list", children: _jsx("div", { className: "xa-in", children: runs.map((run) => (_jsxs("div", { className: `xa-item${run.live ? ' xa-now' : ' xa-done'}`, children: [_jsx("span", { className: "xa-sq" }), _jsx("span", { title: run.title, children: run.title }), _jsx("span", { className: "xa-runres xa-num", children: run.live ? formatElapsed(Date.now() - run.startedAt) : formatElapsed(run.updatedAt - run.startedAt) }), _jsx("span", { className: "xa-rowacts", children: _jsx("button", { type: "button", onClick: () => onOpen(run.conversationId), title: "Open this sub-agent's conversation", children: "Open" }) })] }, run.conversationId))) }) }) })] }));
535
+ }
536
+ export function TasksTray({ tasks, open, onToggle }) {
537
+ if (!tasks.length)
538
+ return null;
539
+ const done = tasks.filter((task) => task.status === 'done').length;
540
+ const current = tasks.find((task) => task.status === 'in_progress') || tasks.find((task) => task.status === 'pending') || tasks[tasks.length - 1];
541
+ const finished = done === tasks.length;
542
+ return (_jsxs("div", { className: `xa-transcript xa-tasks${open ? ' xa-open' : ''}`, "data-tasks": true, children: [_jsxs("button", { type: "button", className: "xa-bar", onClick: onToggle, "aria-expanded": open, children: [_jsx("span", { className: "xa-label", children: finished ? 'Done' : 'Tasks' }), _jsx("span", { className: "xa-curtask", children: finished ? '' : current.text }), _jsxs("span", { className: "xa-cnt xa-num", children: [done, " of ", tasks.length] }), _jsx(Chevron, {})] }), _jsx("div", { className: "xa-listwrap", children: _jsx("div", { className: "xa-list", children: _jsx("div", { className: "xa-in", children: tasks.map((task) => _jsxs("div", { className: `xa-item${task.status === 'done' ? ' xa-done' : task.status === 'in_progress' ? ' xa-now' : task.status === 'blocked' ? ' xa-blocked' : ''}`, children: [_jsx("span", { className: "xa-sq" }), _jsx("span", { children: task.text })] }, task.id)) }) }) })] }));
543
+ }
544
+ //# sourceMappingURL=Transcript.js.map