@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.
- package/dist/adapters/xenoAgentHostCatalog.d.ts.map +1 -1
- package/dist/adapters/xenoAgentHostCatalog.js +20 -5
- package/dist/adapters/xenoAgentHostCatalog.js.map +1 -1
- package/dist/components/agent/AgentView.d.ts +9 -0
- package/dist/components/agent/AgentView.d.ts.map +1 -1
- package/dist/components/agent/AgentView.js +883 -1045
- package/dist/components/agent/AgentView.js.map +1 -1
- package/dist/components/agent/GhostTextArea.d.ts +6 -0
- package/dist/components/agent/GhostTextArea.d.ts.map +1 -1
- package/dist/components/agent/GhostTextArea.js +15 -5
- package/dist/components/agent/GhostTextArea.js.map +1 -1
- package/dist/components/agent/LiveConversationCollaboration.d.ts +3 -1
- package/dist/components/agent/LiveConversationCollaboration.d.ts.map +1 -1
- package/dist/components/agent/LiveConversationCollaboration.js +3 -1
- package/dist/components/agent/LiveConversationCollaboration.js.map +1 -1
- package/dist/components/agent/MessageText.d.ts +36 -0
- package/dist/components/agent/MessageText.d.ts.map +1 -0
- package/dist/components/agent/MessageText.js +76 -0
- package/dist/components/agent/MessageText.js.map +1 -0
- package/dist/components/agent/SdkSessionPanel.d.ts +2 -0
- package/dist/components/agent/SdkSessionPanel.d.ts.map +1 -1
- package/dist/components/agent/SdkSessionPanel.js +1 -1
- package/dist/components/agent/SdkSessionPanel.js.map +1 -1
- package/dist/components/agent/composer/Composer.d.ts +282 -0
- package/dist/components/agent/composer/Composer.d.ts.map +1 -0
- package/dist/components/agent/composer/Composer.js +393 -0
- package/dist/components/agent/composer/Composer.js.map +1 -0
- package/dist/components/agent/composer/EffortCells.d.ts +12 -0
- package/dist/components/agent/composer/EffortCells.d.ts.map +1 -0
- package/dist/components/agent/composer/EffortCells.js +146 -0
- package/dist/components/agent/composer/EffortCells.js.map +1 -0
- package/dist/components/agent/composer/Tip.d.ts +19 -0
- package/dist/components/agent/composer/Tip.d.ts.map +1 -0
- package/dist/components/agent/composer/Tip.js +78 -0
- package/dist/components/agent/composer/Tip.js.map +1 -0
- package/dist/components/agent/composer/TypedMenu.d.ts +56 -0
- package/dist/components/agent/composer/TypedMenu.d.ts.map +1 -0
- package/dist/components/agent/composer/TypedMenu.js +156 -0
- package/dist/components/agent/composer/TypedMenu.js.map +1 -0
- package/dist/components/agent/mainAssistant.d.ts +9 -2
- package/dist/components/agent/mainAssistant.d.ts.map +1 -1
- package/dist/components/agent/mainAssistant.js +9 -2
- package/dist/components/agent/mainAssistant.js.map +1 -1
- package/dist/components/agent/toolCallDerive.d.ts +82 -0
- package/dist/components/agent/toolCallDerive.d.ts.map +1 -0
- package/dist/components/agent/toolCallDerive.js +741 -0
- package/dist/components/agent/toolCallDerive.js.map +1 -0
- package/dist/components/agent/transcript/Transcript.d.ts +127 -0
- package/dist/components/agent/transcript/Transcript.d.ts.map +1 -0
- package/dist/components/agent/transcript/Transcript.js +544 -0
- package/dist/components/agent/transcript/Transcript.js.map +1 -0
- package/dist/components/agent/transcript/glyphs.d.ts +83 -0
- package/dist/components/agent/transcript/glyphs.d.ts.map +1 -0
- package/dist/components/agent/transcript/glyphs.js +124 -0
- package/dist/components/agent/transcript/glyphs.js.map +1 -0
- package/dist/components/agent/transcript/stepModel.d.ts +163 -0
- package/dist/components/agent/transcript/stepModel.d.ts.map +1 -0
- package/dist/components/agent/transcript/stepModel.js +701 -0
- package/dist/components/agent/transcript/stepModel.js.map +1 -0
- package/dist/components/agent/transcript/turnModel.d.ts +124 -0
- package/dist/components/agent/transcript/turnModel.d.ts.map +1 -0
- package/dist/components/agent/transcript/turnModel.js +166 -0
- package/dist/components/agent/transcript/turnModel.js.map +1 -0
- package/dist/components/agent/transcript/useAskActions.d.ts +23 -0
- package/dist/components/agent/transcript/useAskActions.d.ts.map +1 -0
- package/dist/components/agent/transcript/useAskActions.js +92 -0
- package/dist/components/agent/transcript/useAskActions.js.map +1 -0
- package/dist/parityFixture.d.ts +54 -0
- package/dist/parityFixture.d.ts.map +1 -0
- package/dist/parityFixture.js +195 -0
- package/dist/parityFixture.js.map +1 -0
- package/dist/platformApiTypes.d.ts +10 -1
- package/dist/platformApiTypes.d.ts.map +1 -1
- package/dist/stores/agentChatStore.d.ts +61 -1
- package/dist/stores/agentChatStore.d.ts.map +1 -1
- package/dist/stores/agentChatStore.js +63 -8
- package/dist/stores/agentChatStore.js.map +1 -1
- package/dist/types/agentPermissions.d.ts +3 -1
- package/dist/types/agentPermissions.d.ts.map +1 -1
- package/dist/utils/acpExecutionAuthority.d.ts.map +1 -1
- package/dist/utils/acpExecutionAuthority.js +2 -0
- package/dist/utils/acpExecutionAuthority.js.map +1 -1
- package/package.json +6 -5
- package/src/composer.css +334 -0
- package/src/styles.css +7 -0
- 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
|