@parallel-cli/parallel 0.4.4 → 0.4.6
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/CHANGELOG.md +50 -0
- package/README.md +57 -9
- package/dist/agents/tools.js +58 -2
- package/dist/commands.js +31 -0
- package/dist/controller.js +33 -8
- package/dist/coordination/blackboard.js +75 -0
- package/dist/i18n.js +24 -4
- package/dist/index.js +9 -0
- package/dist/server.js +10 -0
- package/dist/ui/AgentPanel.js +24 -13
- package/dist/ui/App.js +24 -24
- package/dist/ui/AttachApp.js +14 -4
- package/dist/ui/CommandInput.js +137 -40
- package/dist/ui/Md.js +4 -3
- package/dist/ui/SettingsPanel.js +2 -1
- package/dist/ui/Timeline.js +11 -9
- package/dist/ui/Wizard.js +11 -5
- package/dist/ui/theme.js +3 -3
- package/dist/ui/tokens.js +11 -6
- package/dist/ui/views.js +49 -11
- package/dist/update.js +125 -0
- package/dist/version.js +2 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import { App } from './ui/App.js';
|
|
|
8
8
|
import { Controller } from './controller.js';
|
|
9
9
|
import { loadConfig, providerReady, setConfigHome } from './config.js';
|
|
10
10
|
import { setLang } from './i18n.js';
|
|
11
|
+
import { maybeRunStartupUpdate } from './update.js';
|
|
11
12
|
const argv = process.argv.slice(2);
|
|
12
13
|
function takeFlagValue(flag) {
|
|
13
14
|
const i = argv.indexOf(flag);
|
|
@@ -26,6 +27,9 @@ if (headless)
|
|
|
26
27
|
const jsonOut = argv.includes('--json');
|
|
27
28
|
if (jsonOut)
|
|
28
29
|
argv.splice(argv.indexOf('--json'), 1);
|
|
30
|
+
const noUpdate = argv.includes('--no-update');
|
|
31
|
+
if (noUpdate)
|
|
32
|
+
argv.splice(argv.indexOf('--no-update'), 1);
|
|
29
33
|
const configHome = takeFlagValue('--config-home');
|
|
30
34
|
if (argv.includes('--help') || argv.includes('-h')) {
|
|
31
35
|
console.log(`⚡ Parallel — real-time parallel coding agents.
|
|
@@ -39,6 +43,8 @@ Usage:
|
|
|
39
43
|
parallel --first-run Test the first-run wizard with a temporary config home
|
|
40
44
|
parallel --config-home <dir> [folder]
|
|
41
45
|
Use <dir>/config.json instead of ~/.parallel/config.json
|
|
46
|
+
parallel --no-update [folder]
|
|
47
|
+
Start without checking npm for a newer Parallel version
|
|
42
48
|
parallel --headless "task1" ["task2"…] [--json]
|
|
43
49
|
No TUI: one agent per task in the current folder,
|
|
44
50
|
auto-approved commands, summary (or JSON) on stdout — for CI
|
|
@@ -48,6 +54,7 @@ Environment variables:
|
|
|
48
54
|
PARALLEL_MODEL Default model
|
|
49
55
|
PARALLEL_BASE_URL OpenAI-compatible endpoint
|
|
50
56
|
PARALLEL_NO_ALT_SCREEN=1 Disable the alternate terminal screen.
|
|
57
|
+
PARALLEL_SKIP_UPDATE_CHECK=1 Disable npm update checks.
|
|
51
58
|
|
|
52
59
|
Inside the TUI:
|
|
53
60
|
<task> + Enter Launch agent N+1 — even while the others are working
|
|
@@ -172,6 +179,8 @@ if (!process.stdout.isTTY) {
|
|
|
172
179
|
console.error('Parallel requires an interactive terminal (TTY).');
|
|
173
180
|
process.exit(1);
|
|
174
181
|
}
|
|
182
|
+
if (await maybeRunStartupUpdate(firstRun || noUpdate))
|
|
183
|
+
process.exit(0);
|
|
175
184
|
const config = loadConfig();
|
|
176
185
|
if (config.language)
|
|
177
186
|
setLang(config.language);
|
package/dist/server.js
CHANGED
|
@@ -108,6 +108,16 @@ export function startSessionServer(ctl) {
|
|
|
108
108
|
else if (msg.type === 'answer' && typeof msg.id === 'number' && typeof msg.text === 'string') {
|
|
109
109
|
ctl.answerQuestion(msg.id, msg.text);
|
|
110
110
|
}
|
|
111
|
+
else if (msg.type === 'send' && typeof msg.target === 'string' && typeof msg.text === 'string') {
|
|
112
|
+
const text = msg.text.trim();
|
|
113
|
+
const target = msg.target.trim();
|
|
114
|
+
if (!text || !target)
|
|
115
|
+
continue;
|
|
116
|
+
if (target.toLowerCase() === 'all')
|
|
117
|
+
ctl.broadcast(text);
|
|
118
|
+
else
|
|
119
|
+
ctl.sendToAgent(target, text);
|
|
120
|
+
}
|
|
111
121
|
else if (msg.type === 'spawn' && typeof msg.text === 'string') {
|
|
112
122
|
// Agent N+1 can be launched from ANY terminal of the session —
|
|
113
123
|
// its own dedicated terminal then opens automatically.
|
package/dist/ui/AgentPanel.js
CHANGED
|
@@ -6,7 +6,8 @@ import { elapsed, truncate } from './theme.js';
|
|
|
6
6
|
import { Md } from './Md.js';
|
|
7
7
|
import { Spinner } from './Spinner.js';
|
|
8
8
|
import { Timeline } from './Timeline.js';
|
|
9
|
-
import { MARK, MODE, STATE_META, UI, ANIM } from './tokens.js';
|
|
9
|
+
import { MARK, MODE, STATE_META, UI, ANIM, COLOR } from './tokens.js';
|
|
10
|
+
import { latestSignal, toUIEvents } from './events.js';
|
|
10
11
|
export const KIND_COLOR = {
|
|
11
12
|
tool: UI.accent,
|
|
12
13
|
llm: UI.muted,
|
|
@@ -26,7 +27,19 @@ export function cleanHubSummary(text) {
|
|
|
26
27
|
.trim();
|
|
27
28
|
}
|
|
28
29
|
export function formatAgentTelemetry(agent) {
|
|
29
|
-
|
|
30
|
+
const ctx = agent.ctxPct !== undefined ? ` · ${agent.ctxPct}% ctx` : '';
|
|
31
|
+
return `${elapsed(agent.startedAt)}${ctx} · ${agent.cost === null ? '$-' : fmtCost(agent.cost)}`;
|
|
32
|
+
}
|
|
33
|
+
function compactResultSummary(text, max) {
|
|
34
|
+
const clean = cleanHubSummary(text);
|
|
35
|
+
const validation = text.match(/validation[^:\n]*[:\n]\s*([^\n]+)/i)?.[1]?.trim();
|
|
36
|
+
const risk = text.match(/risks?[^:\n]*[:\n]\s*([^\n]+)/i)?.[1]?.trim() ?? text.match(/risques?[^:\n]*[:\n]\s*([^\n]+)/i)?.[1]?.trim();
|
|
37
|
+
const parts = [clean.slice(0, Math.max(40, Math.floor(max * 0.55)))];
|
|
38
|
+
if (validation)
|
|
39
|
+
parts.push(`V: ${validation}`);
|
|
40
|
+
if (risk)
|
|
41
|
+
parts.push(`R: ${risk}`);
|
|
42
|
+
return truncate(parts.join(' · '), max);
|
|
30
43
|
}
|
|
31
44
|
function ResultBlock({ agent, compact = false }) {
|
|
32
45
|
if (!agent.lastResult)
|
|
@@ -39,7 +52,7 @@ function ResultBlock({ agent, compact = false }) {
|
|
|
39
52
|
const SPINNER_STATES = new Set(['thinking', 'working', 'listening', 'waiting']);
|
|
40
53
|
function spinnerColor(state) {
|
|
41
54
|
if (state === 'working')
|
|
42
|
-
return
|
|
55
|
+
return COLOR.cream;
|
|
43
56
|
return 'yellow'; // thinking, listening, waiting
|
|
44
57
|
}
|
|
45
58
|
function modeChar(mode) {
|
|
@@ -72,22 +85,20 @@ export function AgentRow({ agent, logs, cols, }) {
|
|
|
72
85
|
const taskMax = Math.max(10, cols - 18);
|
|
73
86
|
const line2Max = Math.max(10, cols - 2);
|
|
74
87
|
const telemetry = formatAgentTelemetry(agent);
|
|
75
|
-
|
|
88
|
+
const signal = latestSignal(agent, toUIEvents(logs));
|
|
89
|
+
const specialist = agent.specialist ? ` #${agent.specialist}` : '';
|
|
76
90
|
let line2 = null;
|
|
77
91
|
if (agent.lastResult) {
|
|
78
|
-
line2 = { text: `✓ ${
|
|
79
|
-
}
|
|
80
|
-
else if (agent.currentAction) {
|
|
81
|
-
line2 = { text: `▸ ${truncate(agent.currentAction, line2Max)}`, color: UI.accent };
|
|
92
|
+
line2 = { text: `✓ ${compactResultSummary(agent.lastResult, line2Max)}`, color: UI.ok };
|
|
82
93
|
}
|
|
83
|
-
else {
|
|
84
|
-
line2 = { text:
|
|
94
|
+
else if (signal && signal !== agent.task) {
|
|
95
|
+
line2 = { text: `▸ ${truncate(signal, line2Max)}`, color: UI.accent };
|
|
85
96
|
}
|
|
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 })] })] }));
|
|
97
|
+
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, specialist ? _jsx(Text, { color: UI.note, children: specialist }) : null, _jsxs(Text, { color: UI.text, children: [" ", truncate(agent.task, taskMax)] })] }), line2 ? (_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 })] })) : null] }));
|
|
87
98
|
}
|
|
88
|
-
export function AgentTranscript({ agent, logs, raw = false, scrolled = 0, }) {
|
|
99
|
+
export function AgentTranscript({ agent, logs, raw = false, scrolled = 0, cols = 100, }) {
|
|
89
100
|
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` : ''] })] }));
|
|
101
|
+
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, cols: cols })] }), _jsxs(Text, { color: UI.muted, wrap: "truncate-end", children: ["PgUp/PgDn scroll \u00B7 /raw toggles detail \u00B7 Esc returns", scrolled > 0 ? ` · ${scrolled} older` : ''] })] }));
|
|
91
102
|
}
|
|
92
103
|
export function AgentPanel({ agent, logs, width, expanded = false, }) {
|
|
93
104
|
return (_jsx(Box, { width: width, flexDirection: "column", children: expanded ? _jsx(AgentTranscript, { agent: agent, logs: logs }) : _jsx(AgentRow, { agent: agent, logs: logs, cols: 100 }) }));
|
package/dist/ui/App.js
CHANGED
|
@@ -16,9 +16,8 @@ import { SettingsPanel } from './SettingsPanel.js';
|
|
|
16
16
|
import { BoardView, CostView, DiffView, HelpView, NotesView, SessionsView, SkillsView, SpecialistsView } from './views.js';
|
|
17
17
|
import { SelectList, WizardStep } from './Wizard.js';
|
|
18
18
|
import { BRAND, CHROME, STATE, STATE_META, UI, middleTruncate } from './tokens.js';
|
|
19
|
+
import { VERSION } from '../version.js';
|
|
19
20
|
const LOGO = 'Parallel';
|
|
20
|
-
// Version from package.json. Hardcoded — rootDir: "src" prevents importing ../../package.json.
|
|
21
|
-
const VERSION = '0.4.4';
|
|
22
21
|
function usableProvider(config) {
|
|
23
22
|
const p = getProvider(config);
|
|
24
23
|
return p && providerReady(p) && (p.defaultModel || p.models[0]) ? p : undefined;
|
|
@@ -65,12 +64,7 @@ export function App({ config, initialFolder }) {
|
|
|
65
64
|
// Focus mode (/focus <agent>): plain input is routed to that agent.
|
|
66
65
|
const [focus, setFocus] = useState(null);
|
|
67
66
|
const [rawLogs, setRawLogs] = useState(false);
|
|
68
|
-
const [systemLines, setSystemLines] = useState(
|
|
69
|
-
? [
|
|
70
|
-
{ text: t('main.ready1', { folder: directFolder }), level: 'ok' },
|
|
71
|
-
{ text: t('main.ready2'), level: 'info' },
|
|
72
|
-
]
|
|
73
|
-
: []);
|
|
67
|
+
const [systemLines, setSystemLines] = useState([]);
|
|
74
68
|
const [inputReady, setInputReady] = useState(Boolean(directFolder));
|
|
75
69
|
const ctl = ctlRef.current;
|
|
76
70
|
const leaveCurrentProject = () => {
|
|
@@ -277,10 +271,7 @@ export function App({ config, initialFolder }) {
|
|
|
277
271
|
enterMain();
|
|
278
272
|
};
|
|
279
273
|
const enterMain = () => {
|
|
280
|
-
setSystemLines([
|
|
281
|
-
{ text: t('main.ready1', { folder }), level: 'ok' },
|
|
282
|
-
{ text: t('main.ready2'), level: 'info' },
|
|
283
|
-
]);
|
|
274
|
+
setSystemLines([]);
|
|
284
275
|
setPhase('main');
|
|
285
276
|
setInputReady(false);
|
|
286
277
|
setTimeout(() => setInputReady(true), 350);
|
|
@@ -311,7 +302,7 @@ export function App({ config, initialFolder }) {
|
|
|
311
302
|
if (phase !== 'main') {
|
|
312
303
|
const totalSteps = 5;
|
|
313
304
|
const sessionProvider = ctl ? ctl.sessionProvider() : getProvider(config);
|
|
314
|
-
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color:
|
|
305
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: BRAND.primary, children: LOGO }), _jsx(Text, { color: "gray", children: t('tagline') }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [phase === 'lang' && (_jsx(WizardStep, { step: 1, total: totalSteps, title: t('wiz.lang.title'), children: _jsx(SelectList, { items: LANGS.map((l) => ({ label: l.label, value: l.code })), height: wizardListHeight, onSelect: chooseLang }) })), phase === 'folder' && (_jsxs(WizardStep, { step: 2, total: totalSteps, title: t('wiz.folder.title'), footer: t('wiz.folder.footer'), children: [wizardError ? _jsx(Text, { color: "red", children: wizardError }) : null, _jsx(SelectList, { items: [
|
|
315
306
|
{ label: process.cwd(), value: process.cwd(), hint: t('wiz.folder.current') },
|
|
316
307
|
...config.recentFolders
|
|
317
308
|
.filter((f) => f !== process.cwd())
|
|
@@ -523,6 +514,10 @@ export function App({ config, initialFolder }) {
|
|
|
523
514
|
}
|
|
524
515
|
}, notify: ui.system }));
|
|
525
516
|
}
|
|
517
|
+
function EmptyHub({ bodyHeight }) {
|
|
518
|
+
const topPad = Math.max(0, Math.floor((bodyHeight - 3) / 2));
|
|
519
|
+
return (_jsxs(Box, { flexDirection: "column", children: [topPad > 0 ? _jsx(Box, { height: topPad }) : null, _jsx(Text, { color: UI.text, children: t('main.emptyCard.cta') }), _jsx(Text, { color: CHROME.muted, children: t('main.emptyCard.hints') })] }));
|
|
520
|
+
}
|
|
526
521
|
function MainScreen({ ctl, folder, view, focus, rawLogs, systemLines, agentNames, approval, question, inputActive, onInput, onEscape, notify, }) {
|
|
527
522
|
const agents = [...ctl.board.agents.values()];
|
|
528
523
|
// Adapt the layout to the REAL terminal size (never resize the user's terminal).
|
|
@@ -530,11 +525,10 @@ function MainScreen({ ctl, folder, view, focus, rawLogs, systemLines, agentNames
|
|
|
530
525
|
const cols = Math.max(20, stdout?.columns ?? 100);
|
|
531
526
|
const rows = Math.max(12, stdout?.rows ?? 30);
|
|
532
527
|
const settingsOpen = view === 'settings' || view === 'settings-session';
|
|
528
|
+
const [inputRows, setInputRows] = useState(3);
|
|
533
529
|
// Height budget: fixed sections → body gets the remainder.
|
|
534
|
-
const headerLines = 4; //
|
|
535
|
-
const
|
|
536
|
-
const footerLine1 = agents.length === 0 ? 1 : 0;
|
|
537
|
-
const footerLines = footerLine1 + footerLine2;
|
|
530
|
+
const headerLines = 4; // Codex-like framed header: top + 2 content lines + bottom
|
|
531
|
+
const footerLines = 1;
|
|
538
532
|
// System messages: count actual rendered lines (including \n splits + "Session" label).
|
|
539
533
|
const systemMsgLines = systemLines.length > 0 && !settingsOpen
|
|
540
534
|
? (agents.length > 0 ? 1 : 0) + // "Session" label
|
|
@@ -544,8 +538,8 @@ function MainScreen({ ctl, folder, view, focus, rawLogs, systemLines, agentNames
|
|
|
544
538
|
.slice(-2)
|
|
545
539
|
: systemLines).reduce((sum, l) => sum + l.text.split('\n').length, 0)
|
|
546
540
|
: 0;
|
|
547
|
-
const inputLines =
|
|
548
|
-
const spacerLines =
|
|
541
|
+
const inputLines = inputRows;
|
|
542
|
+
const spacerLines = 1;
|
|
549
543
|
const approvalHeight = approval ? 6 : 0;
|
|
550
544
|
const questionHeight = question ? 7 : 0;
|
|
551
545
|
const bodyHeight = Math.max(1, rows - headerLines - footerLines - systemMsgLines - inputLines - spacerLines - approvalHeight - questionHeight);
|
|
@@ -555,7 +549,10 @@ function MainScreen({ ctl, folder, view, focus, rawLogs, systemLines, agentNames
|
|
|
555
549
|
: undefined;
|
|
556
550
|
const [scroll, setScroll] = useState(0);
|
|
557
551
|
const [focusFollowTail, setFocusFollowTail] = useState(true);
|
|
558
|
-
useEffect(() =>
|
|
552
|
+
useEffect(() => {
|
|
553
|
+
setScroll(0);
|
|
554
|
+
setFocusFollowTail(true);
|
|
555
|
+
}, [focus]);
|
|
559
556
|
const FOCUS_LOGS = Math.max(8, bodyHeight - 1);
|
|
560
557
|
const focusedLogs = focused ? ctl.board.logs.filter((l) => l.agentId === focused.id) : [];
|
|
561
558
|
const maxScroll = Math.max(0, focusedLogs.length - FOCUS_LOGS);
|
|
@@ -627,6 +624,9 @@ function MainScreen({ ctl, folder, view, focus, rawLogs, systemLines, agentNames
|
|
|
627
624
|
: agents.some((a) => ['waiting', 'paused'].includes(a.state)) ? 'yellow'
|
|
628
625
|
: 'gray';
|
|
629
626
|
const folderMax = Math.max(10, cols - 40);
|
|
627
|
+
const provider = ctl.sessionProvider();
|
|
628
|
+
const providerModel = provider ? `${provider.name}:${ctl.session.model}` : 'no model';
|
|
629
|
+
const headerColor = view === 'agents' && agents.length === 0 ? BRAND.muted : CHROME.muted;
|
|
630
630
|
// View breadcrumb: when not in agents view, show the view name instead of "control room".
|
|
631
631
|
const VIEW_LABEL = {
|
|
632
632
|
agents: 'control room',
|
|
@@ -642,7 +642,7 @@ function MainScreen({ ctl, folder, view, focus, rawLogs, systemLines, agentNames
|
|
|
642
642
|
specialists: 'specialists',
|
|
643
643
|
};
|
|
644
644
|
const viewLabel = VIEW_LABEL[view] ?? 'control room';
|
|
645
|
-
return (_jsxs(Box, { flexDirection: "column", height: rows, children: [_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color:
|
|
645
|
+
return (_jsxs(Box, { flexDirection: "column", height: rows, children: [_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: headerColor, children: ["\u256D", '─'.repeat(cols - 2), "\u256E"] }), _jsxs(Box, { flexDirection: "row", width: cols, children: [_jsx(Text, { color: headerColor, 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: [" ", agents.length === 0 ? 'ready' : viewLabel] }), rawLogs && focused ? _jsx(Text, { color: UI.warn, children: " [RAW]" }) : null] }), _jsx(Text, { color: CHROME.muted, children: middleTruncate(folder, folderMax) })] }), _jsx(Text, { color: headerColor, children: " \u2502" })] }), _jsxs(Box, { flexDirection: "row", width: cols, children: [_jsx(Text, { color: headerColor, children: "\u2502 " }), _jsxs(Box, { flexDirection: "row", width: cols - 4, justifyContent: "space-between", 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"] })] }) })) : (_jsx(Text, { color: CHROME.muted, children: providerModel })), _jsxs(Text, { color: CHROME.muted, children: ["v", VERSION] })] }), _jsx(Text, { color: headerColor, children: " \u2502" })] }), _jsxs(Text, { color: headerColor, children: ["\u2570", '─'.repeat(cols - 2), "\u256F"] })] }), _jsx(Box, { height: bodyHeight, overflow: "hidden", flexDirection: "column", children: view === 'settings' ? (_jsx(SettingsPanel, { ctl: ctl, scope: "global", height: bodyHeight, onClose: onEscape })) : view === 'settings-session' ? (_jsx(SettingsPanel, { ctl: ctl, scope: "session", height: bodyHeight, onClose: onEscape })) : view === 'board' ? (_jsx(BoardView, { board: ctl.board, bodyHeight: bodyHeight })) : view === 'notes' ? (_jsx(NotesView, { board: ctl.board, bodyHeight: bodyHeight })) : view === 'sessions' ? (_jsx(SessionsView, { projectRoot: ctl.projectRoot, bodyHeight: bodyHeight })) : view === 'diff' ? (_jsx(DiffView, { board: ctl.board, bodyHeight: bodyHeight })) : view === 'cost' ? (_jsx(CostView, { board: ctl.board, bodyHeight: bodyHeight })) : view === 'skills' ? (_jsx(SkillsView, { skills: ctl.getSkills(), bodyHeight: bodyHeight })) : view === 'specialists' ? (_jsx(SpecialistsView, { specialists: ctl.getSpecialists(), bodyHeight: bodyHeight })) : view === 'help' ? (_jsx(HelpView, { bodyHeight: bodyHeight, onSelect: (cmd) => onInput(cmd) })) : agents.length === 0 ? (_jsx(EmptyHub, { bodyHeight: bodyHeight })) : focused ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(AgentTranscript, { agent: focused, logs: visibleLogs, raw: rawLogs, scrolled: clampedScroll, cols: cols }), !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
|
|
646
646
|
? systemLines
|
|
647
647
|
.filter((l) => !/^Ready|^Type a task|^⚡ Ready|^Default \/task|^Agent .* launched/.test(l.text))
|
|
648
648
|
.slice(-2)
|
|
@@ -654,9 +654,9 @@ function MainScreen({ ctl, folder, view, focus, rawLogs, systemLines, agentNames
|
|
|
654
654
|
// Split on \n so multiline i18n messages render correctly (Ink <Text> doesn't interpret \n).
|
|
655
655
|
const lines = l.text.split('\n');
|
|
656
656
|
return lines.map((line, j) => (_jsx(Text, { color: levelColor, wrap: "truncate-end", children: line }, `${i}-${j}`)));
|
|
657
|
-
})] })), 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` : '
|
|
658
|
-
|
|
659
|
-
|
|
657
|
+
})] })), 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` : t('main.prompt'), context: focus ? 'focus' : 'hub', targetAgent: focused?.name, modelLabel: ctl.sessionProvider() ? `${ctl.sessionProvider()?.name}:${ctl.session.model}` : undefined, agentNames: agentNames, agents: agents, width: cols, onHeightChange: setInputRows, onSubmit: onInput, onEscape: onEscape, notify: notify }), _jsx(Box, { flexDirection: "column", children: _jsxs(Text, { children: [_jsx(Text, { color: CHROME.muted, children: "/ for commands" }), _jsx(Text, { color: CHROME.muted, children: " \u00B7 Shell " }), _jsx(Text, { color: ctl.session.approvalMode === 'ask' ? UI.warn :
|
|
658
|
+
ctl.session.approvalMode === 'yolo' ? UI.danger :
|
|
659
|
+
UI.ok, children: ctl.session.approvalMode === 'auto-safe' ? 'auto' : ctl.session.approvalMode }), agents.length > 0 ? _jsxs(Text, { color: CHROME.muted, children: [" \u00B7 Sessions ", Controller.listSessions(ctl.projectRoot).length] }) : null, 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] }) })] }));
|
|
660
660
|
}
|
|
661
661
|
function groupAgents(agents) {
|
|
662
662
|
const needs = agents.filter((a) => ['waiting', 'paused'].includes(a.state));
|
package/dist/ui/AttachApp.js
CHANGED
|
@@ -12,7 +12,7 @@ import { Timeline } from './Timeline.js';
|
|
|
12
12
|
import { stateLabel, elapsed, truncate } from './theme.js';
|
|
13
13
|
import { fmtCost } from '../pricing.js';
|
|
14
14
|
import { t } from '../i18n.js';
|
|
15
|
-
import { STATE_META, UI, middleTruncate } from './tokens.js';
|
|
15
|
+
import { COLOR, STATE_META, UI, middleTruncate } from './tokens.js';
|
|
16
16
|
const noop = () => { };
|
|
17
17
|
export function parseAttachCommand(text) {
|
|
18
18
|
const v = text.trim();
|
|
@@ -22,6 +22,12 @@ export function parseAttachCommand(text) {
|
|
|
22
22
|
return { type: 'detach' };
|
|
23
23
|
if (v === '/raw')
|
|
24
24
|
return { type: 'raw' };
|
|
25
|
+
const at = v.match(/^@(\S+)\s+(.+)$/s);
|
|
26
|
+
if (at)
|
|
27
|
+
return { type: 'send', target: at[1], text: at[2].trim() };
|
|
28
|
+
const send = v.match(/^\/send\s+(\S+)\s+(.+)$/s);
|
|
29
|
+
if (send)
|
|
30
|
+
return { type: 'send', target: send[1], text: send[2].trim() };
|
|
25
31
|
const m = v.match(/^\/(ask|a|task|t|plan|p)\s+(.+)$/s);
|
|
26
32
|
if (m) {
|
|
27
33
|
const mode = m[1] === 'ask' || m[1] === 'a' ? 'ask' : m[1] === 'plan' || m[1] === 'p' ? 'plan' : 'task';
|
|
@@ -122,6 +128,10 @@ export function AttachApp({ agentRef, sock }) {
|
|
|
122
128
|
wire({ type: 'spawn', text: cmd.text, mode: cmd.mode });
|
|
123
129
|
return;
|
|
124
130
|
}
|
|
131
|
+
if (cmd.type === 'send') {
|
|
132
|
+
wire({ type: 'send', target: cmd.target, text: cmd.text });
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
125
135
|
wire({ type: 'input', agent: agentRef, text: cmd.text });
|
|
126
136
|
};
|
|
127
137
|
const st = info ? STATE_META[info.state] : null;
|
|
@@ -134,18 +144,18 @@ export function AttachApp({ agentRef, sock }) {
|
|
|
134
144
|
* what used to leave stray blank lines in the native scrollback. */
|
|
135
145
|
_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
|
|
136
146
|
.map((o) => `${o.name} [${stateLabel(o.state)}] ${truncate(o.currentAction || o.task, 40)}`)
|
|
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] })) : (
|
|
147
|
+
.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), cols: process.stdout.columns || 100 })] })) : null] })) : (
|
|
138
148
|
/* FULL panel when idle / waiting / done — repaints are rare here. */
|
|
139
149
|
_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 ? (
|
|
140
150
|
// The session's shared awareness, visible here too: what the
|
|
141
151
|
// OTHER agents are doing right now (live, same feed the agents get).
|
|
142
152
|
_jsxs(Text, { color: UI.muted, wrap: "truncate-end", children: ["Others ", ' ', others
|
|
143
153
|
.map((o) => `${o.name} [${stateLabel(o.state)}] ${truncate(o.currentAction || o.task, 40)}`)
|
|
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) => {
|
|
154
|
+
.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), cols: process.stdout.columns || 100 })] })) : 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) => {
|
|
145
155
|
wire({ type: 'approve', id, approved: ok, always: !!always });
|
|
146
156
|
setApproval(null);
|
|
147
157
|
} })) : question ? (_jsx(QuestionPrompt, { question: { ...question, agentId: info?.id ?? '', resolve: noop }, pendingCount: 1, onAnswer: (id, answer) => {
|
|
148
158
|
wire({ type: 'answer', id, text: answer });
|
|
149
159
|
setQuestion(null);
|
|
150
|
-
} }, question.id)) : null, _jsx(CommandInput, { active: !gone && !interacting, placeholder: t('attach.placeholder', { agent: info?.name ?? agentRef }), onSubmit: send, onEscape: () => exit() }), _jsx(Box, {
|
|
160
|
+
} }, question.id)) : null, _jsx(CommandInput, { active: !gone && !interacting, placeholder: t('attach.placeholder', { agent: info?.name ?? agentRef }), context: "attach", targetAgent: info?.name ?? agentRef, modelLabel: info?.model, agentNames: [info?.alias, info?.name, ...others.flatMap((o) => [o.alias, o.name])].filter((n) => Boolean(n)), agents: info ? [info] : [], width: process.stdout.columns || 100, onSubmit: send, onEscape: () => exit() }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: COLOR.creamMuted, wrap: "truncate-end", children: formatAttachFooter(info) }) })] }));
|
|
151
161
|
}
|
package/dist/ui/CommandInput.js
CHANGED
|
@@ -1,49 +1,91 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { Box, Text, useInput } from 'ink';
|
|
4
|
-
import {
|
|
4
|
+
import { commandPalette } from '../commands.js';
|
|
5
5
|
import { t } from '../i18n.js';
|
|
6
6
|
import { readClipboardImage } from './clipboard.js';
|
|
7
|
+
import { BRAND, COLOR } from './tokens.js';
|
|
7
8
|
/** A paste is "long" when it spans multiple lines — it then collapses into a chip. */
|
|
8
9
|
const PASTE_MIN_LINES = 2;
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
settings: 'Settings',
|
|
14
|
-
git: 'Git/session',
|
|
15
|
-
other: 'Other',
|
|
16
|
-
};
|
|
17
|
-
function modeHint(value) {
|
|
10
|
+
const AGENT_ARG_COMMANDS = new Set(['/focus', '/send', '/attach', '/pause', '/resume', '/stop', '/restore', '/commit']);
|
|
11
|
+
const COMMAND_PAGE_SIZE = 9;
|
|
12
|
+
const PROMPT_GUTTER = '› ';
|
|
13
|
+
function modeHint(value, context, targetAgent) {
|
|
18
14
|
const v = value.trimStart().toLowerCase();
|
|
19
|
-
if (!v)
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
if (!v) {
|
|
16
|
+
if (context === 'focus')
|
|
17
|
+
return `Message ${targetAgent ?? 'focused agent'} · / commands`;
|
|
18
|
+
if (context === 'attach')
|
|
19
|
+
return `Steer ${targetAgent ?? 'agent'} · @all broadcasts · /quit detaches`;
|
|
20
|
+
return 'Type a task or / for commands';
|
|
21
|
+
}
|
|
22
|
+
if (!v.startsWith('/')) {
|
|
23
|
+
if (context === 'focus')
|
|
24
|
+
return `Will message ${targetAgent ?? 'focused agent'}`;
|
|
25
|
+
if (context === 'attach')
|
|
26
|
+
return `Will steer ${targetAgent ?? 'attached agent'}`;
|
|
22
27
|
return 'Will launch /task';
|
|
28
|
+
}
|
|
23
29
|
if (v.startsWith('/ask') || v === '/a')
|
|
24
30
|
return 'Ask mode · advice only · no edits';
|
|
25
31
|
if (v.startsWith('/task') || v === '/t')
|
|
26
32
|
return 'Task mode · execute, edit, validate';
|
|
27
33
|
if (v.startsWith('/plan') || v === '/p')
|
|
28
34
|
return 'Plan mode · asks before editing';
|
|
29
|
-
return '
|
|
30
|
-
}
|
|
31
|
-
function groupedCommands(commands) {
|
|
32
|
-
const order = ['modes', 'control', 'views', 'settings', 'git', 'other'];
|
|
33
|
-
return order
|
|
34
|
-
.map((g) => [g, commands.filter((c) => (c.group ?? 'other') === g)])
|
|
35
|
-
.filter(([, items]) => items.length > 0);
|
|
35
|
+
return '↑/↓ select · Enter accept';
|
|
36
36
|
}
|
|
37
37
|
export function bestCommandCompletion(value) {
|
|
38
|
-
const cmd =
|
|
38
|
+
const cmd = commandPalette(value)[0];
|
|
39
39
|
return cmd ? `${cmd.name} ` : null;
|
|
40
40
|
}
|
|
41
|
-
export function
|
|
41
|
+
export function commandNamesForContext(context) {
|
|
42
|
+
if (context !== 'attach')
|
|
43
|
+
return undefined;
|
|
44
|
+
return ['/ask', '/a', '/task', '/t', '/plan', '/p', '/send', '/raw', '/quit', '/exit', '/detach'];
|
|
45
|
+
}
|
|
46
|
+
export function agentArgCommand(value) {
|
|
47
|
+
const m = value.match(/^(\/\S+)\s+([^\s]*)$/);
|
|
48
|
+
if (!m)
|
|
49
|
+
return null;
|
|
50
|
+
const cmd = m[1].toLowerCase();
|
|
51
|
+
return AGENT_ARG_COMMANDS.has(cmd) ? cmd : null;
|
|
52
|
+
}
|
|
53
|
+
export function completeAgentArgument(value, agent) {
|
|
54
|
+
const cmd = agentArgCommand(value);
|
|
55
|
+
if (!cmd)
|
|
56
|
+
return value;
|
|
57
|
+
return `${cmd} ${agent} `;
|
|
58
|
+
}
|
|
59
|
+
export function clampSuggestionIndex(index, count) {
|
|
60
|
+
if (count <= 0)
|
|
61
|
+
return 0;
|
|
62
|
+
return Math.max(0, Math.min(index, count - 1));
|
|
63
|
+
}
|
|
64
|
+
export function wrappedPromptLines(text, width) {
|
|
65
|
+
const usable = Math.max(8, width - PROMPT_GUTTER.length);
|
|
66
|
+
if (!text)
|
|
67
|
+
return [''];
|
|
68
|
+
const lines = [];
|
|
69
|
+
for (const logical of text.split('\n')) {
|
|
70
|
+
if (!logical) {
|
|
71
|
+
lines.push('');
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
for (let i = 0; i < logical.length; i += usable)
|
|
75
|
+
lines.push(logical.slice(i, i + usable));
|
|
76
|
+
}
|
|
77
|
+
return lines.length > 0 ? lines : [''];
|
|
78
|
+
}
|
|
79
|
+
function paintLine(text, width) {
|
|
80
|
+
return text.length >= width ? text.slice(0, width) : text.padEnd(width, ' ');
|
|
81
|
+
}
|
|
82
|
+
export function CommandInput({ active, placeholder, mask, context = 'hub', targetAgent, modelLabel, commandNames, agentNames = [], agents = [], width, onHeightChange, onSubmit, onEscape, notify, }) {
|
|
42
83
|
const [value, setValue] = useState('');
|
|
43
84
|
const [attachments, setAttachments] = useState([]);
|
|
44
85
|
const [history, setHistory] = useState([]);
|
|
45
86
|
const [histIdx, setHistIdx] = useState(-1);
|
|
46
87
|
const [selectedSuggestion, setSelectedSuggestion] = useState(0);
|
|
88
|
+
const [cursorOn, setCursorOn] = useState(true);
|
|
47
89
|
const attSeq = useRef(0);
|
|
48
90
|
const reset = () => {
|
|
49
91
|
setValue('');
|
|
@@ -87,31 +129,78 @@ export function CommandInput({ active, placeholder, mask, agentNames = [], agent
|
|
|
87
129
|
setAttachments((arr) => [...arr, { kind: 'image', n, dataUri: img.dataUri, label: img.label }]);
|
|
88
130
|
notify?.(t('input.imageAdded'));
|
|
89
131
|
};
|
|
90
|
-
const
|
|
132
|
+
const uniqueAgentNames = [...new Set(agentNames.filter(Boolean))];
|
|
133
|
+
const allowedCommands = commandNames ?? commandNamesForContext(context);
|
|
134
|
+
const cmdSuggestions = value.startsWith('/') && !value.includes(' ') ? commandPalette(value, { allowedNames: allowedCommands }) : [];
|
|
91
135
|
const agentSuggestions = value.startsWith('@') && !value.includes(' ')
|
|
92
|
-
? ['all', ...
|
|
136
|
+
? ['all', ...uniqueAgentNames].filter((n) => n.toLowerCase().startsWith(value.slice(1).toLowerCase())).slice(0, 8)
|
|
93
137
|
: [];
|
|
94
|
-
const
|
|
138
|
+
const argCommand = agentArgCommand(value);
|
|
139
|
+
const argPrefix = argCommand ? value.split(/\s+/)[1] ?? '' : '';
|
|
140
|
+
const argSuggestions = argCommand
|
|
141
|
+
? [
|
|
142
|
+
...(argCommand === '/send' || argCommand === '/pause' || argCommand === '/resume' || argCommand === '/stop' || argCommand === '/commit'
|
|
143
|
+
? ['all']
|
|
144
|
+
: []),
|
|
145
|
+
...uniqueAgentNames,
|
|
146
|
+
]
|
|
147
|
+
.filter((n) => n.toLowerCase().startsWith(argPrefix.toLowerCase()))
|
|
148
|
+
.slice(0, 8)
|
|
149
|
+
: [];
|
|
150
|
+
const suggestionCount = cmdSuggestions.length > 0 ? cmdSuggestions.length : agentSuggestions.length > 0 ? agentSuggestions.length : argSuggestions.length;
|
|
95
151
|
const hasSuggestions = suggestionCount > 0;
|
|
96
152
|
const exactCommand = cmdSuggestions.some((c) => c.name === value.toLowerCase() || c.aliases?.some((a) => a === value.toLowerCase()));
|
|
97
153
|
useEffect(() => {
|
|
98
154
|
setSelectedSuggestion(0);
|
|
99
155
|
}, [value]);
|
|
156
|
+
const safeSelectedSuggestion = clampSuggestionIndex(selectedSuggestion, suggestionCount);
|
|
157
|
+
useEffect(() => {
|
|
158
|
+
if (selectedSuggestion !== safeSelectedSuggestion)
|
|
159
|
+
setSelectedSuggestion(safeSelectedSuggestion);
|
|
160
|
+
}, [selectedSuggestion, safeSelectedSuggestion]);
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
if (!active)
|
|
163
|
+
return;
|
|
164
|
+
const timer = setInterval(() => setCursorOn((on) => !on), 450);
|
|
165
|
+
return () => clearInterval(timer);
|
|
166
|
+
}, [active]);
|
|
167
|
+
const commandWindowStart = cmdSuggestions.length > COMMAND_PAGE_SIZE
|
|
168
|
+
? Math.min(Math.max(0, safeSelectedSuggestion - Math.floor(COMMAND_PAGE_SIZE / 2)), Math.max(0, cmdSuggestions.length - COMMAND_PAGE_SIZE))
|
|
169
|
+
: 0;
|
|
170
|
+
const shownCommandSuggestions = cmdSuggestions.slice(commandWindowStart, commandWindowStart + COMMAND_PAGE_SIZE);
|
|
171
|
+
const shown = mask ? '•'.repeat(value.length) : value;
|
|
172
|
+
const promptWidth = Math.max(20, width ?? process.stdout.columns ?? 100);
|
|
173
|
+
const inputText = shown || placeholder;
|
|
174
|
+
const promptLines = wrappedPromptLines(inputText, promptWidth);
|
|
175
|
+
const suggestionRows = cmdSuggestions.length > 0
|
|
176
|
+
? shownCommandSuggestions.length + 1
|
|
177
|
+
: agentSuggestions.length > 0
|
|
178
|
+
? agentSuggestions.length
|
|
179
|
+
: argSuggestions.length > 0
|
|
180
|
+
? argSuggestions.length + 1
|
|
181
|
+
: 0;
|
|
182
|
+
const attachmentRows = attachments.length > 0 ? 1 : 0;
|
|
183
|
+
const hintRows = value || context !== 'hub' ? 1 : 0;
|
|
184
|
+
const renderedRows = suggestionRows + attachmentRows + promptLines.length + 2 + hintRows;
|
|
100
185
|
useEffect(() => {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}, [selectedSuggestion, suggestionCount]);
|
|
186
|
+
onHeightChange?.(renderedRows);
|
|
187
|
+
}, [onHeightChange, renderedRows]);
|
|
104
188
|
const completeBest = () => {
|
|
105
189
|
if (cmdSuggestions.length > 0) {
|
|
106
|
-
const cmd = cmdSuggestions[
|
|
190
|
+
const cmd = cmdSuggestions[clampSuggestionIndex(safeSelectedSuggestion, cmdSuggestions.length)];
|
|
107
191
|
setValue(`${cmd.name} `);
|
|
108
192
|
return true;
|
|
109
193
|
}
|
|
110
194
|
if (agentSuggestions.length > 0) {
|
|
111
|
-
const agent = agentSuggestions[
|
|
195
|
+
const agent = agentSuggestions[clampSuggestionIndex(safeSelectedSuggestion, agentSuggestions.length)];
|
|
112
196
|
setValue('@' + agent + ' ');
|
|
113
197
|
return true;
|
|
114
198
|
}
|
|
199
|
+
if (argSuggestions.length > 0) {
|
|
200
|
+
const agent = argSuggestions[clampSuggestionIndex(safeSelectedSuggestion, argSuggestions.length)];
|
|
201
|
+
setValue(completeAgentArgument(value, agent));
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
115
204
|
return false;
|
|
116
205
|
};
|
|
117
206
|
useInput((input, key) => {
|
|
@@ -144,7 +233,7 @@ export function CommandInput({ active, placeholder, mask, agentNames = [], agent
|
|
|
144
233
|
}
|
|
145
234
|
if (key.upArrow) {
|
|
146
235
|
if (hasSuggestions) {
|
|
147
|
-
setSelectedSuggestion((i) => (i - 1
|
|
236
|
+
setSelectedSuggestion((i) => clampSuggestionIndex(i - 1, suggestionCount));
|
|
148
237
|
return;
|
|
149
238
|
}
|
|
150
239
|
setHistIdx((i) => {
|
|
@@ -157,7 +246,7 @@ export function CommandInput({ active, placeholder, mask, agentNames = [], agent
|
|
|
157
246
|
}
|
|
158
247
|
if (key.downArrow) {
|
|
159
248
|
if (hasSuggestions) {
|
|
160
|
-
setSelectedSuggestion((i) => (i + 1
|
|
249
|
+
setSelectedSuggestion((i) => clampSuggestionIndex(i + 1, suggestionCount));
|
|
161
250
|
return;
|
|
162
251
|
}
|
|
163
252
|
setHistIdx((i) => {
|
|
@@ -208,13 +297,21 @@ export function CommandInput({ active, placeholder, mask, agentNames = [], agent
|
|
|
208
297
|
}
|
|
209
298
|
setValue((v) => v + input);
|
|
210
299
|
}, { isActive: active });
|
|
211
|
-
const shown = mask ? '•'.repeat(value.length) : value;
|
|
212
300
|
const byName = new Map(agents.flatMap((a) => [[a.name, a], [a.alias, a]]));
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
301
|
+
return (_jsxs(Box, { flexDirection: "column", children: [cmdSuggestions.length > 0 && (_jsxs(Box, { borderStyle: "single", borderColor: "gray", flexDirection: "column", paddingX: 1, children: [_jsxs(Text, { color: "gray", children: ["Commands ", Math.min(safeSelectedSuggestion + 1, cmdSuggestions.length), "/", cmdSuggestions.length, " \u00B7 \u2191/\u2193"] }), shownCommandSuggestions.map((c, i) => {
|
|
302
|
+
const absolute = commandWindowStart + i;
|
|
303
|
+
const selected = absolute === safeSelectedSuggestion;
|
|
304
|
+
return (_jsxs(Text, { children: [_jsxs(Text, { color: selected ? COLOR.cream : COLOR.creamMuted, bold: true, children: [selected ? '› ' : ' ', c.name.padEnd(13)] }), _jsx(Text, { color: "gray", children: t(c.descKey) })] }, c.name));
|
|
305
|
+
})] })), agentSuggestions.length > 0 && (_jsx(Box, { borderStyle: "single", borderColor: "gray", flexDirection: "column", paddingX: 1, children: agentSuggestions.map((n, i) => (_jsxs(Text, { children: [_jsxs(Text, { color: i === safeSelectedSuggestion ? COLOR.cream : COLOR.creamMuted, bold: true, children: [i === safeSelectedSuggestion ? '› ' : ' ', "@", n.padEnd(10)] }), _jsx(Text, { color: "gray", children: n === 'all'
|
|
218
306
|
? t('input.atAll')
|
|
219
|
-
: `${byName.get(n)?.state ?? ''} ${byName.get(n)?.mode ? `/${byName.get(n)?.mode}` : ''}` })] }, n))) })),
|
|
307
|
+
: `${byName.get(n)?.state ?? ''} ${byName.get(n)?.mode ? `/${byName.get(n)?.mode}` : ''}` })] }, n))) })), argSuggestions.length > 0 && (_jsxs(Box, { borderStyle: "single", borderColor: "gray", flexDirection: "column", paddingX: 1, children: [_jsx(Text, { color: "gray", bold: true, children: "Agents" }), argSuggestions.map((n, i) => (_jsxs(Text, { children: [_jsxs(Text, { color: i === safeSelectedSuggestion ? COLOR.cream : COLOR.creamMuted, bold: true, children: [i === safeSelectedSuggestion ? '› ' : ' ', n.padEnd(12)] }), _jsx(Text, { color: "gray", children: n === 'all'
|
|
308
|
+
? t('input.atAll')
|
|
309
|
+
: `${byName.get(n)?.state ?? ''} ${byName.get(n)?.mode ? `/${byName.get(n)?.mode}` : ''}` })] }, n)))] })), attachments.length > 0 && (_jsx(Box, { flexDirection: "row", gap: 1, paddingX: 1, children: attachments.map((a) => (_jsxs(Text, { color: COLOR.cream, backgroundColor: COLOR.promptBackground, children: [' ', a.kind === 'paste' ? t('input.attPaste', { n: a.n, lines: a.lines }) : t('input.attImage', { n: a.n, file: a.label }), ' '] }, a.n))) })), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { backgroundColor: COLOR.promptBackground, children: paintLine('', promptWidth) }), promptLines.map((line, i) => {
|
|
310
|
+
const last = i === promptLines.length - 1;
|
|
311
|
+
const placeholderCursor = !shown && active && cursorOn && i === 0;
|
|
312
|
+
const cursor = shown && active && last && cursorOn ? '█' : '';
|
|
313
|
+
const prefix = i === 0 ? PROMPT_GUTTER : ' ';
|
|
314
|
+
const content = placeholderCursor ? `█${line.slice(1)}` : `${line}${cursor}`;
|
|
315
|
+
return (_jsxs(Text, { backgroundColor: COLOR.promptBackground, children: [_jsx(Text, { color: active ? BRAND.primary : BRAND.muted, backgroundColor: COLOR.promptBackground, bold: true, children: prefix }), _jsx(Text, { color: shown ? 'white' : COLOR.creamMuted, backgroundColor: COLOR.promptBackground, children: paintLine(content, promptWidth - prefix.length) })] }, i));
|
|
316
|
+
}), _jsx(Text, { backgroundColor: COLOR.promptBackground, children: paintLine('', promptWidth) })] }), (value || context !== 'hub') && (_jsxs(Text, { color: "gray", wrap: "truncate-end", children: [modeHint(value, context, targetAgent), targetAgent ? ` · ${targetAgent}` : '', modelLabel && value ? ` · ${modelLabel}` : ''] }))] }));
|
|
220
317
|
}
|