@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.
@@ -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 { STATE_LABEL, stateLabel, elapsed, truncate } from './theme.js';
5
+ import { elapsed, truncate } from './theme.js';
6
6
  import { Md } from './Md.js';
7
- import { t } from '../i18n.js';
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: 'cyanBright',
10
- llm: 'gray',
11
- error: 'red',
12
- note: 'magentaBright',
13
- system: 'yellow',
14
- info: 'white',
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
- const st = STATE_LABEL[agent.state];
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 { AgentPanel } from './AgentPanel.js';
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
- const LOGO = '⚡ P A R A L L E L';
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 [systemLines, setSystemLines] = useState(directFolder ? [t('main.ready1', { folder: directFolder }), t('main.ready2')] : []);
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([t('main.ready1', { folder }), t('main.ready2')]);
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 narrow = cols < 110;
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 = 20;
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
- // Grid scroll: when more agents than fit on screen, PgUp/PgDn slides a
345
- // window over the agent panels (with ▲/▼ indicators so you know where you are).
346
- const GRID_CAP = narrow ? 2 : 4;
347
- const [gridScroll, setGridScroll] = useState(0);
348
- const maxGridScroll = Math.max(0, agents.length - GRID_CAP);
349
- const clampedGrid = Math.min(gridScroll, maxGridScroll);
350
- const visibleAgents = agents.length > GRID_CAP ? agents.slice(clampedGrid, clampedGrid + GRID_CAP) : agents;
351
- // Solo scroll: with a SINGLE agent on the agents view, PgUp/PgDn scrolls
352
- // its log history (same behaviour as /focus, without needing to focus).
353
- const solo = agents.length === 1 ? agents[0] : null;
354
- const [soloScroll, setSoloScroll] = useState(0);
355
- const soloLogs = solo ? ctl.board.logs.filter((l) => l.agentId === solo.id) : [];
356
- const maxSoloScroll = Math.max(0, soloLogs.length - logsPerAgent);
357
- const clampedSolo = Math.min(soloScroll, maxSoloScroll);
358
- // Esc always returns to the agents view, even while approval is shown.
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
- setScroll((s) => Math.min(s + 10, maxScroll));
365
- if (key.pageDown)
366
- setScroll((s) => Math.max(0, s - 10));
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
- // Scroll only on the agents view — every other long view
370
- // (/help, /notes, /diff…) owns PgUp/PgDn for its own scrolling.
371
- if (solo) {
372
- if (key.pageUp)
373
- setSoloScroll((s) => Math.min(Math.min(s, maxSoloScroll) + 5, maxSoloScroll));
374
- if (key.pageDown)
375
- setSoloScroll((s) => Math.max(0, Math.min(s, maxSoloScroll) - 5));
376
- }
377
- else {
378
- if (key.pageUp)
379
- setGridScroll((s) => Math.max(0, Math.min(s, maxGridScroll) - 1));
380
- if (key.pageDown)
381
- setGridScroll((s) => Math.min(Math.min(s, maxGridScroll) + 1, maxGridScroll));
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
- const p = ctl.sessionProvider();
386
- const activeCount = agents.filter((a) => ['working', 'thinking', 'listening', 'waiting'].includes(a.state)).length;
387
- const totalCost = agents.reduce((s, a) => s + (a.cost ?? 0), 0);
388
- return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Box, { children: [_jsxs(Text, { backgroundColor: "cyan", color: "black", bold: true, children: [' ', "\u2302 HUB", ' '] }), _jsxs(Text, { bold: true, color: "cyanBright", children: [' ', LOGO] })] }), _jsxs(Text, { color: "gray", children: [p ? `${p.name}:${ctl.session.model}` : '—', " \u00B7 ", p ? p.baseUrl.replace(/^https?:\/\//, '') : '', " \u00B7", ' ', ctl.session.approvalMode, " \u00B7 ", ctl.session.soundEnabled ? '🔔' : '🔕'] })] }), _jsxs(Text, { color: "gray", wrap: "truncate-end", children: ["\uD83D\uDCC1 ", folder] }), 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: "round", borderColor: "gray", flexDirection: "column", paddingX: 1, children: _jsx(Text, { color: "gray", children: t('main.empty') }) })) : focused ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(AgentPanel, { agent: focused, logs: visibleLogs, width: "100%", expanded: true }), _jsxs(Text, { color: "gray", wrap: "truncate-end", children: [t('m.focusHint', { name: focused.name }), clampedScroll > 0 ? ` · ↑${clampedScroll}` : ''] })] })) : (_jsxs(Box, { flexDirection: "column", children: [clampedGrid > 0 ? _jsx(Text, { color: "gray", children: t('grid.above', { n: clampedGrid }) }) : null, _jsx(Box, { flexWrap: "wrap", children: visibleAgents.map((a) => (_jsx(AgentPanel, { agent: a, logs: solo
389
- ? soloLogs.slice(Math.max(0, soloLogs.length - logsPerAgent - clampedSolo), soloLogs.length - clampedSolo)
390
- : ctl.board.logsFor(a.id, logsPerAgent), width: width }, a.id))) }), solo && clampedSolo > 0 ? (_jsxs(Text, { color: "gray", wrap: "truncate-end", children: ["\u2191", clampedSolo, " \u00B7 PgDn \u21E3"] })) : null, agents.length - clampedGrid - visibleAgents.length > 0 ? (_jsx(Text, { color: "gray", children: t('grid.below', { n: agents.length - clampedGrid - visibleAgents.length }) })) : null] })), systemLines.length > 0 && !settingsOpen && (_jsx(Box, { flexDirection: "column", children: systemLines.map((l, i) => (_jsx(Text, { color: "gray", wrap: "truncate-end", children: l }, i))) })), 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: t('main.placeholder'), agentNames: agentNames, onSubmit: onInput, onEscape: onEscape, notify: notify }), _jsx(Text, { color: "gray", wrap: "truncate-end", children: agents.length === 0
391
- ? t('main.status')
392
- : t('status.bar', {
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
  }
@@ -1,17 +1,39 @@
1
- import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
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 { STATE_LABEL, stateLabel, elapsed, truncate } from './theme.js';
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 v = text.trim();
87
- if (!v)
109
+ const cmd = parseAttachCommand(text);
110
+ if (!cmd)
88
111
  return;
89
- if (v === '/quit' || v === '/exit' || v === '/detach') {
112
+ if (cmd.type === 'detach') {
90
113
  exit();
91
114
  return;
92
115
  }
93
- // /spawn <task> launch agent N+1 from THIS terminal; its own dedicated
94
- // terminal opens automatically (the main TUI stays the session hub).
95
- const spawn = v.match(/^\/spawn\s+(.+)$/s);
96
- if (spawn) {
97
- wire({ type: 'spawn', text: spawn[1] });
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: v });
125
+ wire({ type: 'input', agent: agentRef, text: cmd.text });
101
126
  };
102
- const st = info ? STATE_LABEL[info.state] : null;
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: [_jsxs(Text, { backgroundColor: info?.color ?? 'gray', color: "black", bold: true, children: [' ', "\u26D3 ", t('attach.banner'), ' '] }), info ? (_jsxs(Text, { color: info.color, bold: true, children: [' ', "\u25C6 ", info.name, info.alias && info.alias !== info.name ? _jsxs(Text, { color: "gray", children: [" @", info.alias] }) : null] })) : null] }));
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: [_jsxs(Text, { backgroundColor: info.color, color: "black", bold: true, children: [' ', "\u26D3 ", info.name, ' '] }), ' ', _jsxs(Text, { backgroundColor: st.color, color: "black", bold: true, children: [' ', st.icon, " ", stateLabel(info.state), ' '] }), ' ', _jsx(Spinner, { color: info.color }), _jsxs(Text, { color: "gray", children: [' ', "\u00B7 ", elapsed(info.startedAt), " \u00B7 ", info.steps, " st \u00B7", ' ', Math.round((info.tokensIn + info.tokensOut) / 1000), "k \u00B7", ' '] }), _jsx(Text, { color: "greenBright", children: info.cost === null ? '$—' : fmtCost(info.cost) })] }), info.currentAction ? (_jsxs(Text, { color: info.color, wrap: "truncate-end", children: ["\u25B8 ", truncate(info.currentAction, 120)] })) : null, others.length > 0 ? (_jsxs(Text, { color: "gray", wrap: "truncate-end", children: ["\u21C4", ' ', others
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: "round", 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, { backgroundColor: st.color, color: "black", bold: true, children: [' ', st.icon, " ", stateLabel(info.state), ' '] })] }), _jsxs(Text, { color: "gray", wrap: "truncate-end", children: [truncate(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 ? 'redBright' : info.ctxPct >= 70 ? 'yellowBright' : 'gray', children: ["\u25D4", info.ctxPct, "% \u00B7", ' '] })) : null, _jsx(Text, { color: "greenBright", children: info.cost === null ? '$—' : fmtCost(info.cost) })] })] }), _jsxs(Text, { color: "gray", wrap: "wrap", children: ["\u25E6 ", info.task] }), info.currentAction ? (_jsxs(Text, { color: info.color, wrap: "truncate-end", children: ["\u25B8 ", truncate(info.currentAction, 160)] })) : null, others.length > 0 ? (
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: "gray", wrap: "truncate-end", children: ["\u21C4", ' ', others
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: "greenBright", bold: true, children: t('agent.summary') }), _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: "redBright", children: t('attach.gone') }) : null] })), approval ? (_jsx(ApprovalPrompt, { request: { ...approval, agentId: info?.id ?? '', resolve: noop }, pendingCount: 1, onAnswer: (id, ok, always) => {
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: "gray", wrap: "truncate-end", children: t('attach.hint') })] }));
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
  }