@parallel-cli/parallel 0.3.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +146 -83
- package/dist/agents/agent.js +24 -2
- package/dist/agents/tools.js +4 -2
- package/dist/commands.js +179 -135
- package/dist/config.js +9 -0
- package/dist/controller.js +38 -5
- package/dist/i18n.js +160 -40
- package/dist/index.js +4 -2
- package/dist/server.js +2 -1
- package/dist/ui/AgentPanel.js +85 -16
- package/dist/ui/App.js +191 -61
- package/dist/ui/AttachApp.js +46 -21
- package/dist/ui/CommandInput.js +56 -15
- package/dist/ui/SettingsPanel.js +9 -2
- package/dist/ui/Timeline.js +60 -0
- package/dist/ui/events.js +229 -0
- package/dist/ui/theme.js +5 -4
- package/dist/ui/tokens.js +77 -0
- package/dist/ui/views.js +9 -3
- package/package.json +2 -2
package/dist/ui/AgentPanel.js
CHANGED
|
@@ -1,25 +1,94 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useRef, useState, useEffect } from 'react';
|
|
2
3
|
import { Box, Text } from 'ink';
|
|
3
|
-
import { Spinner } from './Spinner.js';
|
|
4
4
|
import { fmtCost } from '../pricing.js';
|
|
5
|
-
import {
|
|
5
|
+
import { elapsed, truncate } from './theme.js';
|
|
6
6
|
import { Md } from './Md.js';
|
|
7
|
-
import {
|
|
7
|
+
import { Spinner } from './Spinner.js';
|
|
8
|
+
import { Timeline } from './Timeline.js';
|
|
9
|
+
import { MARK, MODE, STATE_META, UI, ANIM } from './tokens.js';
|
|
8
10
|
export const KIND_COLOR = {
|
|
9
|
-
tool:
|
|
10
|
-
llm:
|
|
11
|
-
error:
|
|
12
|
-
note:
|
|
13
|
-
system:
|
|
14
|
-
info:
|
|
11
|
+
tool: UI.accent,
|
|
12
|
+
llm: UI.muted,
|
|
13
|
+
error: UI.danger,
|
|
14
|
+
note: UI.note,
|
|
15
|
+
system: UI.warn,
|
|
16
|
+
info: UI.text,
|
|
15
17
|
};
|
|
16
|
-
/** Thinking/commentary lines (kind 'llm') are dimmed + italic, à la Codex/Claude Code. */
|
|
17
18
|
export const KIND_DIM = { llm: true };
|
|
19
|
+
export function cleanHubSummary(text) {
|
|
20
|
+
return text
|
|
21
|
+
.replace(/^#{1,6}\s+/gm, '')
|
|
22
|
+
.replace(/\*\*(.*?)\*\*/g, '$1')
|
|
23
|
+
.replace(/`([^`]+)`/g, '$1')
|
|
24
|
+
.replace(/^\s*[-*]\s+/gm, '')
|
|
25
|
+
.replace(/\s+/g, ' ')
|
|
26
|
+
.trim();
|
|
27
|
+
}
|
|
28
|
+
export function formatAgentTelemetry(agent) {
|
|
29
|
+
return `${elapsed(agent.startedAt)} · ${agent.cost === null ? '$-' : fmtCost(agent.cost)}`;
|
|
30
|
+
}
|
|
31
|
+
function ResultBlock({ agent, compact = false }) {
|
|
32
|
+
if (!agent.lastResult)
|
|
33
|
+
return null;
|
|
34
|
+
if (compact) {
|
|
35
|
+
return (_jsxs(Text, { wrap: "truncate-end", children: [_jsxs(Text, { color: UI.ok, children: [MARK.done, " "] }), _jsx(Text, { children: truncate(agent.lastResult, 110) })] }));
|
|
36
|
+
}
|
|
37
|
+
return (_jsxs(Box, { borderStyle: "single", borderColor: "gray", flexDirection: "column", paddingX: 1, marginTop: 1, children: [_jsx(Text, { color: UI.ok, bold: true, children: "Result" }), _jsx(Md, { text: agent.lastResult })] }));
|
|
38
|
+
}
|
|
39
|
+
const SPINNER_STATES = new Set(['thinking', 'working', 'listening', 'waiting']);
|
|
40
|
+
function spinnerColor(state) {
|
|
41
|
+
if (state === 'working')
|
|
42
|
+
return 'cyan';
|
|
43
|
+
return 'yellow'; // thinking, listening, waiting
|
|
44
|
+
}
|
|
45
|
+
function modeChar(mode) {
|
|
46
|
+
if (mode === 'ask')
|
|
47
|
+
return { char: '?', color: MODE.ask };
|
|
48
|
+
if (mode === 'plan')
|
|
49
|
+
return { char: '△', color: MODE.plan };
|
|
50
|
+
return null; // task = no mark
|
|
51
|
+
}
|
|
52
|
+
function agentDisplayName(agent) {
|
|
53
|
+
return agent.alias && agent.alias !== agent.name ? `${agent.alias} ${agent.name}` : agent.alias || agent.name;
|
|
54
|
+
}
|
|
55
|
+
export function AgentRow({ agent, logs, cols, }) {
|
|
56
|
+
const meta = STATE_META[agent.state];
|
|
57
|
+
// ── State transition pulse (Phase 5) ──
|
|
58
|
+
const prevState = useRef(agent.state);
|
|
59
|
+
const [pulse, setPulse] = useState(false);
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (agent.state !== prevState.current) {
|
|
62
|
+
setPulse(true);
|
|
63
|
+
const timer = setTimeout(() => setPulse(false), ANIM.pulseMs);
|
|
64
|
+
prevState.current = agent.state;
|
|
65
|
+
return () => clearTimeout(timer);
|
|
66
|
+
}
|
|
67
|
+
}, [agent.state]);
|
|
68
|
+
// Pulse bumps the mark/spinner color to whiteBright for 400ms
|
|
69
|
+
const pulseColor = pulse ? 'whiteBright' : null;
|
|
70
|
+
const name = agentDisplayName(agent);
|
|
71
|
+
const mode = modeChar(agent.mode);
|
|
72
|
+
const taskMax = Math.max(10, cols - 18);
|
|
73
|
+
const line2Max = Math.max(10, cols - 2);
|
|
74
|
+
const telemetry = formatAgentTelemetry(agent);
|
|
75
|
+
// Line 2 content
|
|
76
|
+
let line2 = null;
|
|
77
|
+
if (agent.lastResult) {
|
|
78
|
+
line2 = { text: `✓ ${truncate(cleanHubSummary(agent.lastResult), line2Max)}`, color: UI.ok };
|
|
79
|
+
}
|
|
80
|
+
else if (agent.currentAction) {
|
|
81
|
+
line2 = { text: `▸ ${truncate(agent.currentAction, line2Max)}`, color: UI.accent };
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
line2 = { text: meta.label, color: meta.color };
|
|
85
|
+
}
|
|
86
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 0, paddingLeft: 1, children: [_jsxs(Text, { wrap: "truncate-end", children: [SPINNER_STATES.has(agent.state) ? (_jsx(Spinner, { color: pulseColor ?? spinnerColor(agent.state) })) : (_jsx(Text, { color: pulseColor ?? meta.color, bold: true, children: meta.mark })), _jsx(Text, { children: " " }), _jsx(Text, { color: agent.color, bold: true, children: name }), mode ? (_jsxs(Text, { color: mode.color, children: [" ", mode.char] })) : null, _jsxs(Text, { color: UI.text, children: [" ", truncate(agent.task, taskMax)] })] }), _jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsx(Text, { color: line2.color, wrap: "truncate-end", children: line2.text }), _jsx(Text, { color: UI.muted, children: telemetry })] })] }));
|
|
87
|
+
}
|
|
88
|
+
export function AgentTranscript({ agent, logs, raw = false, scrolled = 0, }) {
|
|
89
|
+
const meta = STATE_META[agent.state];
|
|
90
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { children: [_jsx(Text, { color: agent.color, bold: true, children: agent.name }), agent.alias && agent.alias !== agent.name ? _jsxs(Text, { color: UI.muted, children: [" @", agent.alias] }) : null, _jsx(Text, { color: UI.muted, children: " " }), _jsxs(Text, { color: meta.color, bold: true, children: [meta.mark, " ", meta.label] })] }), _jsxs(Text, { color: UI.muted, wrap: "truncate-end", children: [agent.model, " \u00B7 ", formatAgentTelemetry(agent)] })] }), _jsxs(Text, { color: UI.muted, wrap: "wrap", children: ["Task ", _jsx(Text, { color: UI.text, children: agent.task })] }), agent.claims && agent.claims.length > 0 ? (_jsxs(Text, { color: UI.warn, wrap: "truncate-end", children: ["Claims ", agent.claims.join(' ')] })) : null, agent.currentAction ? (_jsxs(Text, { color: UI.accent, wrap: "truncate-end", children: ["Current ", truncate(agent.currentAction, 140)] })) : null, agent.state === 'done' || agent.lastResult ? _jsx(ResultBlock, { agent: agent }) : null, _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: UI.muted, bold: true, children: ["Activity", raw ? ' raw' : ''] }), _jsx(Timeline, { logs: logs, raw: raw })] }), _jsxs(Text, { color: UI.muted, wrap: "truncate-end", children: ["PgUp/PgDn scroll \u00B7 /raw toggles detail \u00B7 Esc returns", scrolled > 0 ? ` · ${scrolled} older` : ''] })] }));
|
|
91
|
+
}
|
|
18
92
|
export function AgentPanel({ agent, logs, width, expanded = false, }) {
|
|
19
|
-
|
|
20
|
-
const busy = agent.state === 'thinking' || agent.state === 'working' || agent.state === 'listening';
|
|
21
|
-
return (_jsx(Box, { width: width, paddingX: 0, children: _jsxs(Box, { borderStyle: agent.state === 'listening' ? 'double' : 'round', borderColor: agent.state === 'error' ? 'red' : agent.state === 'listening' ? 'cyanBright' : agent.color, flexDirection: "column", paddingX: 1, flexGrow: 1, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Box, { children: [_jsxs(Text, { color: agent.color, bold: true, children: ["\u25C6 ", agent.name, agent.alias && agent.alias !== agent.name ? _jsxs(Text, { color: "gray", children: [" @", agent.alias] }) : null, ' '] }), _jsxs(Text, { backgroundColor: st.color, color: "black", bold: true, children: [' ', st.icon, " ", stateLabel(agent.state), ' '] }), busy && (_jsxs(Text, { children: [' ', _jsx(Spinner, { color: agent.color })] }))] }), _jsxs(Text, { color: "gray", wrap: "truncate-end", children: [agent.specialist ? `🎓${agent.specialist} · ` : '', truncate(agent.model, 18), " \u00B7 ", elapsed(agent.startedAt), " \u00B7 ", agent.steps, " st \u00B7", ' ', Math.round((agent.tokensIn + agent.tokensOut) / 1000), "k \u00B7", ' ', agent.ctxPct !== undefined ? (_jsxs(Text, { color: agent.ctxPct >= 90 ? 'redBright' : agent.ctxPct >= 70 ? 'yellowBright' : 'gray', children: ["\u25D4", agent.ctxPct, "% \u00B7", ' '] })) : null, _jsx(Text, { color: "greenBright", children: agent.cost === null ? '$—' : fmtCost(agent.cost) })] })] }), _jsxs(Text, { color: "gray", wrap: expanded ? 'wrap' : 'truncate-end', children: ["\u25E6 ", expanded ? agent.task : truncate(agent.task, 120)] }), agent.claims && agent.claims.length > 0 ? (_jsxs(Text, { color: "yellowBright", wrap: "truncate-end", children: ["\uD83D\uDEA9 ", agent.claims.join(' ')] })) : null, agent.currentAction ? (_jsxs(Text, { color: agent.color, wrap: "truncate-end", children: ["\u25B8 ", truncate(agent.currentAction, 120)] })) : null, agent.lastResult ? (_jsxs(Box, { borderStyle: "single", borderColor: "gray", flexDirection: "column", paddingX: 1, marginTop: 1, children: [_jsx(Text, { color: "greenBright", bold: true, children: t('agent.summary') }), expanded || agent.state === 'done' ? (
|
|
22
|
-
// A finished agent's summary is the deliverable: show it ENTIRELY,
|
|
23
|
-
// wrapped and lightly formatted — never truncated.
|
|
24
|
-
_jsx(Md, { text: agent.lastResult })) : (_jsx(Text, { color: "white", wrap: "truncate-end", children: truncate(agent.lastResult, 260) }))] })) : null, _jsx(Box, { flexDirection: "column", marginTop: 0, children: logs.map((l, i) => (_jsx(Text, { color: KIND_COLOR[l.kind] ?? 'white', italic: KIND_DIM[l.kind] ?? false, wrap: "truncate-end", children: truncate(l.text, expanded ? 220 : 140) }, i))) })] }) }));
|
|
93
|
+
return (_jsx(Box, { width: width, flexDirection: "column", children: expanded ? _jsx(AgentTranscript, { agent: agent, logs: logs }) : _jsx(AgentRow, { agent: agent, logs: logs, cols: 100 }) }));
|
|
25
94
|
}
|
package/dist/ui/App.js
CHANGED
|
@@ -7,16 +7,18 @@ import { Controller } from '../controller.js';
|
|
|
7
7
|
import { startSessionServer } from '../server.js';
|
|
8
8
|
import { executeInput } from '../commands.js';
|
|
9
9
|
import { PROVIDER_PRESETS, getProvider, rememberFolder, saveConfig } from '../config.js';
|
|
10
|
-
import { fmtCost } from '../pricing.js';
|
|
11
10
|
import { LANGS, setLang, t } from '../i18n.js';
|
|
12
|
-
import {
|
|
11
|
+
import { AgentRow, AgentTranscript } from './AgentPanel.js';
|
|
13
12
|
import { ApprovalPrompt } from './ApprovalPrompt.js';
|
|
14
13
|
import { QuestionPrompt } from './QuestionPrompt.js';
|
|
15
14
|
import { CommandInput } from './CommandInput.js';
|
|
16
15
|
import { SettingsPanel } from './SettingsPanel.js';
|
|
17
16
|
import { BoardView, CostView, DiffView, HelpView, NotesView, SessionsView, SkillsView, SpecialistsView } from './views.js';
|
|
18
17
|
import { SelectList, WizardStep } from './Wizard.js';
|
|
19
|
-
|
|
18
|
+
import { BRAND, CHROME, STATE, STATE_META, UI, middleTruncate } from './tokens.js';
|
|
19
|
+
const LOGO = 'Parallel';
|
|
20
|
+
// Version from package.json (v0.3.3). Hardcoded — rootDir: "src" prevents importing ../../package.json.
|
|
21
|
+
const VERSION = '0.3.3';
|
|
20
22
|
function usableProvider(config) {
|
|
21
23
|
const p = getProvider(config);
|
|
22
24
|
return p && p.apiKey && (p.defaultModel || p.models[0]) ? p : undefined;
|
|
@@ -60,7 +62,13 @@ export function App({ config, initialFolder }) {
|
|
|
60
62
|
const [view, setView] = useState('agents');
|
|
61
63
|
// Focus mode (/focus <agent>): plain input is routed to that agent.
|
|
62
64
|
const [focus, setFocus] = useState(null);
|
|
63
|
-
const [
|
|
65
|
+
const [rawLogs, setRawLogs] = useState(false);
|
|
66
|
+
const [systemLines, setSystemLines] = useState(directFolder
|
|
67
|
+
? [
|
|
68
|
+
{ text: t('main.ready1', { folder: directFolder }), level: 'ok' },
|
|
69
|
+
{ text: t('main.ready2'), level: 'info' },
|
|
70
|
+
]
|
|
71
|
+
: []);
|
|
64
72
|
const [inputReady, setInputReady] = useState(Boolean(directFolder));
|
|
65
73
|
const ctl = ctlRef.current;
|
|
66
74
|
// Re-render (throttled) on every blackboard/controller update.
|
|
@@ -97,11 +105,33 @@ export function App({ config, initialFolder }) {
|
|
|
97
105
|
}, [ctl]);
|
|
98
106
|
const ui = useMemo(() => ({
|
|
99
107
|
setView,
|
|
100
|
-
system: (line) => setSystemLines((ls) => [...ls.slice(-5), line]),
|
|
108
|
+
system: (line, level) => setSystemLines((ls) => [...ls.slice(-5), { text: line, level }]),
|
|
101
109
|
exit: () => {
|
|
102
110
|
setTimeout(() => exit(), 50);
|
|
103
111
|
},
|
|
104
112
|
setFocus,
|
|
113
|
+
toggleRaw: () => setRawLogs((v) => {
|
|
114
|
+
const next = !v;
|
|
115
|
+
setSystemLines((ls) => [
|
|
116
|
+
...ls.slice(-5),
|
|
117
|
+
{ text: t(next ? 'm.rawOn' : 'm.rawOff'), level: 'info' },
|
|
118
|
+
]);
|
|
119
|
+
return next;
|
|
120
|
+
}),
|
|
121
|
+
copyLatest: () => {
|
|
122
|
+
const agents = [...(ctlRef.current?.board.agents.values() ?? [])].filter((a) => a.lastResult);
|
|
123
|
+
const latest = agents.sort((a, b) => b.startedAt - a.startedAt)[0];
|
|
124
|
+
if (!latest?.lastResult) {
|
|
125
|
+
setSystemLines((ls) => [...ls.slice(-5), { text: t('m.copyNone'), level: 'warn' }]);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const encoded = Buffer.from(latest.lastResult).toString('base64');
|
|
129
|
+
process.stdout.write(`\x1b]52;c;${encoded}\x07`);
|
|
130
|
+
setSystemLines((ls) => [
|
|
131
|
+
...ls.slice(-5),
|
|
132
|
+
{ text: t('m.copyDone', { name: latest.name }), level: 'ok' },
|
|
133
|
+
]);
|
|
134
|
+
},
|
|
105
135
|
}), [exit]);
|
|
106
136
|
// ---------- wizard transitions ----------
|
|
107
137
|
// In normal launches, a complete config goes straight to the main TUI.
|
|
@@ -195,7 +225,10 @@ export function App({ config, initialFolder }) {
|
|
|
195
225
|
enterMain();
|
|
196
226
|
};
|
|
197
227
|
const enterMain = () => {
|
|
198
|
-
setSystemLines([
|
|
228
|
+
setSystemLines([
|
|
229
|
+
{ text: t('main.ready1', { folder }), level: 'ok' },
|
|
230
|
+
{ text: t('main.ready2'), level: 'info' },
|
|
231
|
+
]);
|
|
199
232
|
setPhase('main');
|
|
200
233
|
setInputReady(false);
|
|
201
234
|
setTimeout(() => setInputReady(true), 350);
|
|
@@ -301,7 +334,7 @@ export function App({ config, initialFolder }) {
|
|
|
301
334
|
const question = approval ? undefined : ctl.questions[0]; // approvals take priority
|
|
302
335
|
const settingsOpen = view === 'settings' || view === 'settings-session';
|
|
303
336
|
const inputActive = inputReady && !approval && !question && !settingsOpen;
|
|
304
|
-
return (_jsx(MainScreen, { ctl: ctl, folder: folder, view: view, focus: focus, systemLines: systemLines, agentNames: agentNames, approval: approval, question: question, inputActive: inputActive, onInput: (value, images) => {
|
|
337
|
+
return (_jsx(MainScreen, { ctl: ctl, folder: folder, view: view, focus: focus, rawLogs: rawLogs, systemLines: systemLines, agentNames: agentNames, approval: approval, question: question, inputActive: inputActive, onInput: (value, images) => {
|
|
305
338
|
const v = value.trim();
|
|
306
339
|
// Focus mode: plain text goes straight to the focused agent.
|
|
307
340
|
if (focus && v && !v.startsWith('/') && !v.startsWith('@')) {
|
|
@@ -315,86 +348,183 @@ export function App({ config, initialFolder }) {
|
|
|
315
348
|
setView('agents');
|
|
316
349
|
else if (focus) {
|
|
317
350
|
setFocus(null);
|
|
318
|
-
ui.system(t('m.focusOff'));
|
|
351
|
+
ui.system(t('m.focusOff'), 'info');
|
|
319
352
|
}
|
|
320
353
|
}, notify: ui.system }));
|
|
321
354
|
}
|
|
322
|
-
function MainScreen({ ctl, folder, view, focus, systemLines, agentNames, approval, question, inputActive, onInput, onEscape, notify, }) {
|
|
355
|
+
function MainScreen({ ctl, folder, view, focus, rawLogs, systemLines, agentNames, approval, question, inputActive, onInput, onEscape, notify, }) {
|
|
323
356
|
const agents = [...ctl.board.agents.values()];
|
|
324
357
|
// Adapt the layout to the REAL terminal size (never resize the user's terminal).
|
|
325
358
|
const { stdout } = useStdout();
|
|
326
359
|
const cols = stdout?.columns ?? 100;
|
|
327
|
-
const
|
|
328
|
-
const logsPerAgent = agents.length <= 1 ? 10 : agents.length <= 2 ? 7 : 5;
|
|
329
|
-
const width = agents.length === 1 || narrow ? '100%' : '50%';
|
|
360
|
+
const rows = stdout?.rows ?? 30;
|
|
330
361
|
const settingsOpen = view === 'settings' || view === 'settings-session';
|
|
362
|
+
// Height budget: fixed sections → body gets the remainder.
|
|
363
|
+
const headerLines = 4; // border-box header (top border + 2 content lines + bottom border)
|
|
364
|
+
const footerLine2 = 1; // always shown
|
|
365
|
+
const footerLine1 = agents.length === 0 ? 1 : 0;
|
|
366
|
+
const footerLines = footerLine1 + footerLine2;
|
|
367
|
+
// System messages: count actual rendered lines (including \n splits + "Session" label).
|
|
368
|
+
const systemMsgLines = systemLines.length > 0 && !settingsOpen
|
|
369
|
+
? (agents.length > 0 ? 1 : 0) + // "Session" label
|
|
370
|
+
(agents.length > 0
|
|
371
|
+
? systemLines
|
|
372
|
+
.filter((l) => !/^Ready|^Type a task|^⚡ Ready|^Default \/task|^Agent .* launched/.test(l.text))
|
|
373
|
+
.slice(-2)
|
|
374
|
+
: systemLines).reduce((sum, l) => sum + l.text.split('\n').length, 0)
|
|
375
|
+
: 0;
|
|
376
|
+
const inputLines = 4; // modeHint (1) + input border box (3)
|
|
377
|
+
const spacerLines = 2; // after header + before footer
|
|
378
|
+
const approvalHeight = approval ? 6 : 0;
|
|
379
|
+
const questionHeight = question ? 7 : 0;
|
|
380
|
+
const bodyHeight = Math.max(1, rows - headerLines - footerLines - systemMsgLines - inputLines - spacerLines - approvalHeight - questionHeight);
|
|
331
381
|
// Focus mode: one agent rendered alone, with scrollback (PgUp/PgDn).
|
|
332
382
|
const focused = focus
|
|
333
383
|
? agents.find((a) => a.name.toLowerCase() === focus.toLowerCase())
|
|
334
384
|
: undefined;
|
|
335
385
|
const [scroll, setScroll] = useState(0);
|
|
386
|
+
const [focusFollowTail, setFocusFollowTail] = useState(true);
|
|
336
387
|
useEffect(() => setScroll(0), [focus]);
|
|
337
|
-
const FOCUS_LOGS =
|
|
388
|
+
const FOCUS_LOGS = Math.max(8, bodyHeight - 1);
|
|
338
389
|
const focusedLogs = focused ? ctl.board.logs.filter((l) => l.agentId === focused.id) : [];
|
|
339
390
|
const maxScroll = Math.max(0, focusedLogs.length - FOCUS_LOGS);
|
|
340
391
|
const clampedScroll = Math.min(scroll, maxScroll);
|
|
341
392
|
const visibleLogs = focused
|
|
342
393
|
? focusedLogs.slice(Math.max(0, focusedLogs.length - FOCUS_LOGS - clampedScroll), focusedLogs.length - clampedScroll)
|
|
343
394
|
: [];
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
const
|
|
347
|
-
const
|
|
348
|
-
const
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
//
|
|
395
|
+
const [hubScroll, setHubScroll] = useState(0);
|
|
396
|
+
const [hubFollowTail, setHubFollowTail] = useState(true);
|
|
397
|
+
const hubRows = Math.max(6, bodyHeight - 2);
|
|
398
|
+
const maxHubScroll = Math.max(0, agents.length - hubRows);
|
|
399
|
+
const clampedHub = Math.min(hubScroll, maxHubScroll);
|
|
400
|
+
const logSeq = ctl.board.logs.length > 0 ? ctl.board.logs[ctl.board.logs.length - 1].seq ?? ctl.board.logs.length : 0;
|
|
401
|
+
useEffect(() => {
|
|
402
|
+
if (focusFollowTail)
|
|
403
|
+
setScroll(0);
|
|
404
|
+
}, [logSeq, focused?.state, focusFollowTail]);
|
|
405
|
+
useEffect(() => {
|
|
406
|
+
if (hubFollowTail)
|
|
407
|
+
setHubScroll(0);
|
|
408
|
+
}, [logSeq, agents.length, hubFollowTail]);
|
|
409
|
+
// Scroll helpers (also used by mouse wheel handler below).
|
|
410
|
+
const scrollFocusUp = () => {
|
|
411
|
+
setFocusFollowTail(false);
|
|
412
|
+
setScroll((s) => Math.min(s + 1, maxScroll));
|
|
413
|
+
};
|
|
414
|
+
const scrollFocusDown = () => {
|
|
415
|
+
setScroll((s) => {
|
|
416
|
+
const next = Math.max(0, s - 1);
|
|
417
|
+
if (next === 0)
|
|
418
|
+
setFocusFollowTail(true);
|
|
419
|
+
return next;
|
|
420
|
+
});
|
|
421
|
+
};
|
|
422
|
+
const scrollHubUp = () => {
|
|
423
|
+
setHubFollowTail(false);
|
|
424
|
+
setHubScroll((s) => Math.min(Math.min(s, maxHubScroll) + 1, maxHubScroll));
|
|
425
|
+
};
|
|
426
|
+
const scrollHubDown = () => {
|
|
427
|
+
setHubScroll((s) => {
|
|
428
|
+
const next = Math.max(0, Math.min(s, maxHubScroll) - 1);
|
|
429
|
+
if (next === 0)
|
|
430
|
+
setHubFollowTail(true);
|
|
431
|
+
return next;
|
|
432
|
+
});
|
|
433
|
+
};
|
|
434
|
+
// Keyboard: Esc / PgUp-PgDn / Up-Down arrows.
|
|
435
|
+
// When CommandInput is NOT focused, Up/Down scroll the hub or focused agent;
|
|
436
|
+
// when it IS focused, CommandInput's own useInput sees them first (history nav).
|
|
359
437
|
useInput((_input, key) => {
|
|
360
438
|
if (key.escape)
|
|
361
439
|
onEscape();
|
|
362
440
|
if (focused) {
|
|
363
|
-
if (key.pageUp)
|
|
364
|
-
|
|
365
|
-
if (key.pageDown)
|
|
366
|
-
|
|
441
|
+
if (key.pageUp || key.upArrow)
|
|
442
|
+
scrollFocusUp();
|
|
443
|
+
if (key.pageDown || key.downArrow)
|
|
444
|
+
scrollFocusDown();
|
|
367
445
|
}
|
|
368
446
|
else if (view === 'agents') {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
if (
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
447
|
+
if (key.pageUp || key.upArrow)
|
|
448
|
+
scrollHubUp();
|
|
449
|
+
if (key.pageDown || key.downArrow)
|
|
450
|
+
scrollHubDown();
|
|
451
|
+
}
|
|
452
|
+
}, { isActive: !inputActive });
|
|
453
|
+
const idleCount = agents.filter((a) => a.state === 'idle').length;
|
|
454
|
+
const workingCount = agents.filter((a) => ['working', 'thinking', 'listening'].includes(a.state)).length;
|
|
455
|
+
const doneCount = agents.filter((a) => a.state === 'done').length;
|
|
456
|
+
const errorCount = agents.filter((a) => ['error', 'stopped'].includes(a.state)).length;
|
|
457
|
+
const globalDotColor = workingCount > 0 ? 'green'
|
|
458
|
+
: agents.some((a) => ['waiting', 'paused'].includes(a.state)) ? 'yellow'
|
|
459
|
+
: 'gray';
|
|
460
|
+
const folderMax = Math.max(10, cols - 40);
|
|
461
|
+
// View breadcrumb: when not in agents view, show the view name instead of "control room".
|
|
462
|
+
const VIEW_LABEL = {
|
|
463
|
+
agents: 'control room',
|
|
464
|
+
board: 'coordination',
|
|
465
|
+
diff: 'diffs',
|
|
466
|
+
notes: 'notes',
|
|
467
|
+
help: 'help',
|
|
468
|
+
settings: 'settings',
|
|
469
|
+
'settings-session': 'session settings',
|
|
470
|
+
sessions: 'sessions',
|
|
471
|
+
cost: 'cost',
|
|
472
|
+
skills: 'skills',
|
|
473
|
+
specialists: 'specialists',
|
|
474
|
+
};
|
|
475
|
+
const viewLabel = VIEW_LABEL[view] ?? 'control room';
|
|
476
|
+
return (_jsxs(Box, { flexDirection: "column", height: rows, children: [_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: CHROME.muted, children: ["\u256D", '─'.repeat(cols - 2), "\u256E"] }), _jsxs(Box, { flexDirection: "row", width: cols, children: [_jsx(Text, { color: CHROME.muted, children: "\u2502 " }), _jsxs(Box, { flexDirection: "row", width: cols - 4, justifyContent: "space-between", children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { bold: true, color: BRAND.primary, children: "PARALLEL" }), _jsx(Text, { color: globalDotColor, children: " \u25CF" }), _jsxs(Text, { color: view === 'agents' ? CHROME.muted : BRAND.muted, children: [" ", viewLabel] }), rawLogs ? _jsx(Text, { color: UI.warn, children: " [RAW]" }) : null] }), _jsx(Text, { color: CHROME.muted, children: middleTruncate(folder, folderMax) })] }), _jsx(Text, { color: CHROME.muted, children: " \u2502" })] }), _jsxs(Box, { flexDirection: "row", width: cols, children: [_jsx(Text, { color: CHROME.muted, children: "\u2502 " }), _jsxs(Box, { flexDirection: "row", width: cols - 4, justifyContent: agents.length > 0 ? 'space-between' : 'flex-end', children: [agents.length > 0 ? (_jsx(Box, { flexDirection: "row", children: _jsxs(Text, { children: [_jsxs(Text, { color: CHROME.muted, children: ["\u25C7 ", idleCount, " idle"] }), ' · ', _jsxs(Text, { color: workingCount > 0 ? STATE.working : CHROME.muted, children: ["\u25CF ", workingCount, " active"] }), ' · ', _jsxs(Text, { color: doneCount > 0 ? STATE.done : CHROME.muted, children: ["\u2713 ", doneCount, " done"] }), ' · ', _jsxs(Text, { color: errorCount > 0 ? STATE.error : CHROME.muted, children: ["\u2717 ", errorCount, " err"] })] }) })) : null, _jsxs(Text, { color: CHROME.muted, children: ["v", VERSION] })] }), _jsx(Text, { color: CHROME.muted, children: " \u2502" })] }), _jsxs(Text, { color: CHROME.muted, children: ["\u2570", '─'.repeat(cols - 2), "\u256F"] })] }), _jsx(Text, { children: " " }), _jsx(Box, { height: bodyHeight, overflow: "hidden", flexDirection: "column", children: view === 'settings' ? (_jsx(SettingsPanel, { ctl: ctl, scope: "global", onClose: onEscape })) : view === 'settings-session' ? (_jsx(SettingsPanel, { ctl: ctl, scope: "session", onClose: onEscape })) : view === 'board' ? (_jsx(BoardView, { board: ctl.board })) : view === 'notes' ? (_jsx(NotesView, { board: ctl.board })) : view === 'sessions' ? (_jsx(SessionsView, { projectRoot: ctl.projectRoot })) : view === 'diff' ? (_jsx(DiffView, { board: ctl.board })) : view === 'cost' ? (_jsx(CostView, { board: ctl.board })) : view === 'skills' ? (_jsx(SkillsView, { skills: ctl.getSkills() })) : view === 'specialists' ? (_jsx(SpecialistsView, { specialists: ctl.getSpecialists() })) : view === 'help' ? (_jsx(HelpView, {})) : agents.length === 0 ? (_jsx(Box, { borderStyle: "single", borderColor: "gray", flexDirection: "column", paddingX: 1, children: _jsx(Text, { color: "gray", children: t('main.empty') }) })) : focused ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(AgentTranscript, { agent: focused, logs: visibleLogs, raw: rawLogs, scrolled: clampedScroll }), !focusFollowTail ? _jsx(Text, { color: UI.warn, children: "Viewing older \u00B7 PgDn to latest" }) : null] })) : (_jsx(AgentHub, { agents: agents, ctl: ctl, cols: cols, scroll: clampedHub, visibleRows: hubRows })) }), systemLines.length > 0 && !settingsOpen && (_jsxs(Box, { flexDirection: "column", children: [agents.length > 0 ? _jsx(Text, { color: UI.muted, bold: true, children: "Session" }) : null, (agents.length > 0
|
|
477
|
+
? systemLines
|
|
478
|
+
.filter((l) => !/^Ready|^Type a task|^⚡ Ready|^Default \/task|^Agent .* launched/.test(l.text))
|
|
479
|
+
.slice(-2)
|
|
480
|
+
: systemLines).flatMap((l, i) => {
|
|
481
|
+
const levelColor = l.level === 'ok' ? UI.ok :
|
|
482
|
+
l.level === 'warn' ? UI.warn :
|
|
483
|
+
l.level === 'error' ? UI.danger :
|
|
484
|
+
'gray';
|
|
485
|
+
// Split on \n so multiline i18n messages render correctly (Ink <Text> doesn't interpret \n).
|
|
486
|
+
const lines = l.text.split('\n');
|
|
487
|
+
return lines.map((line, j) => (_jsx(Text, { color: levelColor, wrap: "truncate-end", children: line }, `${i}-${j}`)));
|
|
488
|
+
})] })), approval && (_jsx(ApprovalPrompt, { request: approval, pendingCount: ctl.approvals.length, onAnswer: (id, ok, always) => ctl.answerApproval(id, ok, always) })), question && (_jsx(QuestionPrompt, { question: question, pendingCount: ctl.questions.length, onAnswer: (id, answer, auto) => ctl.answerQuestion(id, answer, auto) }, question.id)), _jsx(CommandInput, { active: inputActive, placeholder: focus ? `Message ${focus} or /command` : 'Task mode: describe work to run · /ask question · /plan proposal · / for commands', agentNames: agentNames, agents: agents, onSubmit: onInput, onEscape: onEscape, notify: notify }), _jsx(Text, { children: " " }), _jsxs(Box, { flexDirection: "column", children: [agents.length === 0 ? (_jsxs(Text, { children: [_jsx(Text, { color: BRAND.muted, children: "/ask /task /plan" }), _jsx(Text, { color: CHROME.muted, children: " \u00B7 Tab autocompletes \u00B7 Esc clears" })] })) : null, _jsxs(Text, { children: [_jsx(Text, { color: CHROME.muted, children: "\u2318 Parallel" }), _jsx(Text, { color: CHROME.muted, children: " \u00B7 Shell " }), _jsx(Text, { color: ctl.session.approvalMode === 'ask' ? UI.warn :
|
|
489
|
+
ctl.session.approvalMode === 'yolo' ? UI.danger :
|
|
490
|
+
UI.ok, children: ctl.session.approvalMode === 'auto-safe' ? 'auto' : ctl.session.approvalMode }), _jsxs(Text, { color: CHROME.muted, children: [" \u00B7 Sessions: ", Controller.listSessions(ctl.projectRoot).length] }), ctl.questions.length > 0 ? (_jsxs(Text, { color: UI.warn, children: [" \u00B7 \u2753", ctl.questions.length] })) : null, ctl.approvals.length > 0 ? (_jsxs(Text, { color: UI.warn, children: [" \u00B7 \u23F3", ctl.approvals.length] })) : null, focused ? (_jsxs(Text, { color: BRAND.muted, children: [" \u00B7 \uD83C\uDFAF ", focused.name] })) : null] })] })] }));
|
|
491
|
+
}
|
|
492
|
+
function groupAgents(agents) {
|
|
493
|
+
const needs = agents.filter((a) => ['waiting', 'paused'].includes(a.state));
|
|
494
|
+
const working = agents.filter((a) => ['working', 'thinking', 'listening', 'idle'].includes(a.state));
|
|
495
|
+
const errors = agents.filter((a) => ['error', 'stopped'].includes(a.state));
|
|
496
|
+
const completed = agents.filter((a) => a.state === 'done');
|
|
497
|
+
return [
|
|
498
|
+
{ title: 'Needs input', color: UI.warn, agents: needs },
|
|
499
|
+
{ title: 'Working', color: UI.accent, agents: working },
|
|
500
|
+
{ title: 'Errors', color: UI.danger, agents: errors },
|
|
501
|
+
{ title: 'Completed', color: UI.ok, agents: completed },
|
|
502
|
+
].filter((g) => g.agents.length > 0);
|
|
503
|
+
}
|
|
504
|
+
function AgentHub({ agents, ctl, cols, scroll, visibleRows, }) {
|
|
505
|
+
const groups = groupAgents([...agents].sort((a, b) => STATE_META[a.state].rank - STATE_META[b.state].rank || a.startedAt - b.startedAt));
|
|
506
|
+
let skipped = scroll;
|
|
507
|
+
let rendered = 0;
|
|
508
|
+
const rows = [];
|
|
509
|
+
for (const group of groups) {
|
|
510
|
+
const groupRows = [];
|
|
511
|
+
for (const agent of group.agents) {
|
|
512
|
+
if (skipped > 0) {
|
|
513
|
+
skipped--;
|
|
514
|
+
continue;
|
|
382
515
|
}
|
|
516
|
+
if (rendered >= visibleRows)
|
|
517
|
+
continue;
|
|
518
|
+
rendered++;
|
|
519
|
+
groupRows.push(_jsx(AgentRow, { agent: agent, logs: ctl.board.logsFor(agent.id, 8), cols: cols }, agent.id));
|
|
383
520
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
agents: agents.length,
|
|
394
|
-
active: activeCount,
|
|
395
|
-
cost: fmtCost(totalCost),
|
|
396
|
-
}) +
|
|
397
|
-
(ctl.questions.length > 0 ? ` · ❓${ctl.questions.length}` : '') +
|
|
398
|
-
(ctl.approvals.length > 0 ? ` · ⏳${ctl.approvals.length}` : '') +
|
|
399
|
-
(focused ? ` · 🎯 ${focused.name}` : '') })] }));
|
|
521
|
+
if (groupRows.length === 0)
|
|
522
|
+
continue;
|
|
523
|
+
if (rows.length > 0) {
|
|
524
|
+
rows.push(_jsx(Text, { color: CHROME.separator, children: '─'.repeat(cols - 2) }, `sep-${group.title}`));
|
|
525
|
+
}
|
|
526
|
+
rows.push(_jsx(Box, { flexDirection: "column", children: groupRows }, group.title));
|
|
527
|
+
}
|
|
528
|
+
const below = Math.max(0, agents.length - scroll - rendered);
|
|
529
|
+
return (_jsxs(Box, { flexDirection: "column", children: [scroll > 0 ? _jsxs(Text, { color: CHROME.muted, children: ["\u25B2 ", scroll, " older \u00B7 PgDn to latest"] }) : null, rows, below > 0 ? _jsxs(Text, { color: CHROME.muted, children: ["\u25BC ", below, " more \u00B7 PgUp"] }) : null] }));
|
|
400
530
|
}
|
package/dist/ui/AttachApp.js
CHANGED
|
@@ -1,17 +1,39 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useRef, useState } from 'react';
|
|
3
3
|
import net from 'node:net';
|
|
4
4
|
import { Box, Static, Text, useApp } from 'ink';
|
|
5
5
|
import { ApprovalPrompt } from './ApprovalPrompt.js';
|
|
6
6
|
import { CommandInput } from './CommandInput.js';
|
|
7
|
-
import { KIND_COLOR, KIND_DIM } from './AgentPanel.js';
|
|
7
|
+
import { formatAgentTelemetry, KIND_COLOR, KIND_DIM } from './AgentPanel.js';
|
|
8
8
|
import { Md } from './Md.js';
|
|
9
9
|
import { QuestionPrompt } from './QuestionPrompt.js';
|
|
10
10
|
import { Spinner } from './Spinner.js';
|
|
11
|
-
import {
|
|
11
|
+
import { Timeline } from './Timeline.js';
|
|
12
|
+
import { stateLabel, elapsed, truncate } from './theme.js';
|
|
12
13
|
import { fmtCost } from '../pricing.js';
|
|
13
14
|
import { t } from '../i18n.js';
|
|
15
|
+
import { STATE_META, UI, middleTruncate } from './tokens.js';
|
|
14
16
|
const noop = () => { };
|
|
17
|
+
export function parseAttachCommand(text) {
|
|
18
|
+
const v = text.trim();
|
|
19
|
+
if (!v)
|
|
20
|
+
return null;
|
|
21
|
+
if (v === '/quit' || v === '/exit' || v === '/detach')
|
|
22
|
+
return { type: 'detach' };
|
|
23
|
+
if (v === '/raw')
|
|
24
|
+
return { type: 'raw' };
|
|
25
|
+
const m = v.match(/^\/(ask|a|task|t|plan|p)\s+(.+)$/s);
|
|
26
|
+
if (m) {
|
|
27
|
+
const mode = m[1] === 'ask' || m[1] === 'a' ? 'ask' : m[1] === 'plan' || m[1] === 'p' ? 'plan' : 'task';
|
|
28
|
+
return { type: 'spawn', text: m[2].trim(), mode };
|
|
29
|
+
}
|
|
30
|
+
return { type: 'input', text: v };
|
|
31
|
+
}
|
|
32
|
+
export function formatAttachFooter(info) {
|
|
33
|
+
if (!info)
|
|
34
|
+
return 'Waiting for agent · /quit';
|
|
35
|
+
return `${middleTruncate(info.model, 28)} · ${formatAgentTelemetry(info)} · plain text steers · /task new · /quit`;
|
|
36
|
+
}
|
|
15
37
|
export function AttachApp({ agentRef, sock }) {
|
|
16
38
|
const { exit } = useApp();
|
|
17
39
|
const [info, setInfo] = useState(null);
|
|
@@ -20,6 +42,7 @@ export function AttachApp({ agentRef, sock }) {
|
|
|
20
42
|
const [approval, setApproval] = useState(null);
|
|
21
43
|
const [question, setQuestion] = useState(null);
|
|
22
44
|
const [gone, setGone] = useState(false);
|
|
45
|
+
const [raw, setRaw] = useState(false);
|
|
23
46
|
const socketRef = useRef(null);
|
|
24
47
|
const keySeq = useRef(0);
|
|
25
48
|
const lastBellId = useRef('');
|
|
@@ -83,44 +106,46 @@ export function AttachApp({ agentRef, sock }) {
|
|
|
83
106
|
socketRef.current?.write(JSON.stringify(msg) + '\n');
|
|
84
107
|
};
|
|
85
108
|
const send = (text) => {
|
|
86
|
-
const
|
|
87
|
-
if (!
|
|
109
|
+
const cmd = parseAttachCommand(text);
|
|
110
|
+
if (!cmd)
|
|
88
111
|
return;
|
|
89
|
-
if (
|
|
112
|
+
if (cmd.type === 'detach') {
|
|
90
113
|
exit();
|
|
91
114
|
return;
|
|
92
115
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
116
|
+
if (cmd.type === 'raw') {
|
|
117
|
+
setRaw((r) => !r);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
// /task|/ask|/plan <text> — launch agent N+1 from this terminal.
|
|
121
|
+
if (cmd.type === 'spawn') {
|
|
122
|
+
wire({ type: 'spawn', text: cmd.text, mode: cmd.mode });
|
|
98
123
|
return;
|
|
99
124
|
}
|
|
100
|
-
wire({ type: 'input', agent: agentRef, text:
|
|
125
|
+
wire({ type: 'input', agent: agentRef, text: cmd.text });
|
|
101
126
|
};
|
|
102
|
-
const st = info ?
|
|
127
|
+
const st = info ? STATE_META[info.state] : null;
|
|
103
128
|
const busy = info ? ['thinking', 'working', 'listening'].includes(info.state) : false;
|
|
104
129
|
const interacting = Boolean(approval || question);
|
|
105
|
-
const banner = (_jsxs(Text, { wrap: "truncate-end", children: [
|
|
106
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: lines, children: (item) => (_jsx(Text, { color: KIND_COLOR[item.log.kind] ?? 'white', italic: KIND_DIM[item.log.kind] ?? false, wrap: "wrap", children: item.log.text }, item.key)) }), busy && info && st && !interacting ? (
|
|
130
|
+
const banner = (_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: UI.brand, bold: true, children: t('attach.banner') }), info ? (_jsxs(Text, { color: info.color, bold: true, children: [' ', info.name, info.alias && info.alias !== info.name ? _jsxs(Text, { color: UI.muted, children: [" @", info.alias] }) : null] })) : null] }));
|
|
131
|
+
return (_jsxs(Box, { flexDirection: "column", children: [raw ? (_jsx(Static, { items: lines, children: (item) => (_jsx(Text, { color: KIND_COLOR[item.log.kind] ?? 'white', italic: KIND_DIM[item.log.kind] ?? false, wrap: "wrap", children: item.log.text }, item.key)) })) : null, busy && info && st && !interacting ? (
|
|
107
132
|
/* COMPACT region while the agent runs: small + borderless, so Ink's
|
|
108
133
|
* constant repaints (spinner ticks) never erase tall zones — this is
|
|
109
134
|
* what used to leave stray blank lines in the native scrollback. */
|
|
110
|
-
_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { wrap: "truncate-end", children: [
|
|
135
|
+
_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: info.color, bold: true, children: info.alias || info.name }), ' ', _jsxs(Text, { color: st.color, bold: true, children: [st.mark, " ", st.label] }), ' ', _jsx(Spinner, { color: info.color }), _jsxs(Text, { color: UI.muted, children: [' ', "\u00B7 ", elapsed(info.startedAt), " \u00B7 ", info.steps, " st \u00B7", ' ', Math.round((info.tokensIn + info.tokensOut) / 1000), "k \u00B7", ' '] }), _jsx(Text, { color: UI.ok, children: info.cost === null ? '$-' : fmtCost(info.cost) })] }), info.currentAction ? (_jsxs(Text, { color: info.color, wrap: "truncate-end", children: ["Current ", truncate(info.currentAction, 120)] })) : null, others.length > 0 ? (_jsxs(Text, { color: UI.muted, wrap: "truncate-end", children: ["Others ", ' ', others
|
|
111
136
|
.map((o) => `${o.name} [${stateLabel(o.state)}] ${truncate(o.currentAction || o.task, 40)}`)
|
|
112
|
-
.join(' · ')] })) : null] })) : (
|
|
137
|
+
.join(' · ')] })) : null, !raw ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: UI.muted, bold: true, children: t('timeline.activity') }), _jsx(Timeline, { logs: lines.map((l) => l.log) })] })) : null] })) : (
|
|
113
138
|
/* FULL panel when idle / waiting / done — repaints are rare here. */
|
|
114
|
-
_jsxs(Box, { borderStyle: "
|
|
139
|
+
_jsxs(Box, { borderStyle: "single", borderColor: info?.color ?? 'gray', flexDirection: "column", paddingX: 1, marginTop: 1, children: [info && st ? (_jsxs(_Fragment, { children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Box, { children: [banner, _jsxs(Text, { color: st.color, bold: true, children: [' ', st.mark, " ", st.label] })] }), _jsxs(Text, { color: UI.muted, wrap: "truncate-end", children: [middleTruncate(info.model, 18), " \u00B7 ", elapsed(info.startedAt), " \u00B7 ", info.steps, " st \u00B7", ' ', Math.round((info.tokensIn + info.tokensOut) / 1000), "k \u00B7", ' ', info.ctxPct !== undefined ? (_jsxs(Text, { color: info.ctxPct >= 90 ? UI.danger : info.ctxPct >= 70 ? UI.warn : UI.muted, children: [info.ctxPct, "% \u00B7", ' '] })) : null, _jsx(Text, { color: UI.ok, children: info.cost === null ? '$-' : fmtCost(info.cost) })] })] }), _jsxs(Text, { color: UI.muted, wrap: "wrap", children: ["Task ", _jsx(Text, { color: UI.text, children: info.task })] }), info.currentAction ? (_jsxs(Text, { color: info.color, wrap: "truncate-end", children: ["Current ", truncate(info.currentAction, 160)] })) : null, others.length > 0 ? (
|
|
115
140
|
// The session's shared awareness, visible here too: what the
|
|
116
141
|
// OTHER agents are doing right now (live, same feed the agents get).
|
|
117
|
-
_jsxs(Text, { color:
|
|
142
|
+
_jsxs(Text, { color: UI.muted, wrap: "truncate-end", children: ["Others ", ' ', others
|
|
118
143
|
.map((o) => `${o.name} [${stateLabel(o.state)}] ${truncate(o.currentAction || o.task, 40)}`)
|
|
119
|
-
.join(' · ')] })) : null, info.lastResult && (info.state === 'done' || info.state === 'error' || info.state === 'stopped') ? (_jsxs(Box, { borderStyle: "single", borderColor: "gray", flexDirection: "column", paddingX: 1, marginTop: 1, children: [_jsx(Text, { color:
|
|
144
|
+
.join(' · ')] })) : null, !raw ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: UI.muted, bold: true, children: t('timeline.activity') }), _jsx(Timeline, { logs: lines.map((l) => l.log) })] })) : null, info.lastResult && (info.state === 'done' || info.state === 'error' || info.state === 'stopped') ? (_jsxs(Box, { borderStyle: "single", borderColor: "gray", flexDirection: "column", paddingX: 1, marginTop: 1, children: [_jsx(Text, { color: UI.ok, bold: true, children: "Result" }), _jsx(Md, { text: info.lastResult })] })) : null] })) : (_jsx(Text, { color: "gray", children: gone ? t('attach.gone') : t('attach.waiting', { agent: agentRef }) })), gone && info ? _jsx(Text, { color: UI.danger, children: t('attach.gone') }) : null] })), approval ? (_jsx(ApprovalPrompt, { request: { ...approval, agentId: info?.id ?? '', resolve: noop }, pendingCount: 1, onAnswer: (id, ok, always) => {
|
|
120
145
|
wire({ type: 'approve', id, approved: ok, always: !!always });
|
|
121
146
|
setApproval(null);
|
|
122
147
|
} })) : question ? (_jsx(QuestionPrompt, { question: { ...question, agentId: info?.id ?? '', resolve: noop }, pendingCount: 1, onAnswer: (id, answer) => {
|
|
123
148
|
wire({ type: 'answer', id, text: answer });
|
|
124
149
|
setQuestion(null);
|
|
125
|
-
} }, question.id)) : null, _jsx(CommandInput, { active: !gone && !interacting, placeholder: t('attach.placeholder', { agent: info?.name ?? agentRef }), onSubmit: send, onEscape: () => exit() }), _jsx(Text, { color: "
|
|
150
|
+
} }, question.id)) : null, _jsx(CommandInput, { active: !gone && !interacting, placeholder: t('attach.placeholder', { agent: info?.name ?? agentRef }), onSubmit: send, onEscape: () => exit() }), _jsx(Box, { marginTop: 1, marginBottom: 1, children: _jsx(Text, { color: "yellowBright", wrap: "truncate-end", children: formatAttachFooter(info) }) })] }));
|
|
126
151
|
}
|