@parallel-cli/parallel 0.4.5 → 0.4.7
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 +53 -0
- package/README.md +79 -14
- package/dist/agents/agent.js +98 -14
- package/dist/agents/tools.js +218 -20
- package/dist/commands.js +83 -0
- package/dist/controller.js +24 -14
- package/dist/coordination/blackboard.js +37 -1
- package/dist/i18n.js +84 -12
- package/dist/index.js +10 -0
- package/dist/ui/AgentPanel.js +89 -31
- package/dist/ui/App.js +27 -25
- package/dist/ui/AttachApp.js +100 -15
- package/dist/ui/CommandInput.js +97 -41
- package/dist/ui/Md.js +4 -3
- package/dist/ui/SettingsPanel.js +2 -1
- package/dist/ui/Timeline.js +5 -0
- package/dist/ui/Wizard.js +11 -5
- package/dist/ui/events.js +20 -17
- package/dist/ui/theme.js +3 -3
- package/dist/ui/tokens.js +13 -8
- package/dist/ui/views.js +64 -15
- package/dist/update.js +125 -0
- package/dist/version.js +2 -0
- package/package.json +2 -2
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.5';
|
|
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 = 2;
|
|
541
|
+
const inputLines = inputRows;
|
|
542
|
+
const spacerLines = 2;
|
|
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);
|
|
@@ -630,6 +624,11 @@ function MainScreen({ ctl, folder, view, focus, rawLogs, systemLines, agentNames
|
|
|
630
624
|
: agents.some((a) => ['waiting', 'paused'].includes(a.state)) ? 'yellow'
|
|
631
625
|
: 'gray';
|
|
632
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
|
+
const workMapAlerts = ctl.board.workMapWarnings.filter((w) => w.level !== 'info');
|
|
631
|
+
const conflictAlerts = workMapAlerts.filter((w) => w.level === 'conflict');
|
|
633
632
|
// View breadcrumb: when not in agents view, show the view name instead of "control room".
|
|
634
633
|
const VIEW_LABEL = {
|
|
635
634
|
agents: 'control room',
|
|
@@ -645,7 +644,7 @@ function MainScreen({ ctl, folder, view, focus, rawLogs, systemLines, agentNames
|
|
|
645
644
|
specialists: 'specialists',
|
|
646
645
|
};
|
|
647
646
|
const viewLabel = VIEW_LABEL[view] ?? 'control room';
|
|
648
|
-
return (_jsxs(Box, { flexDirection: "column", height: rows, children: [_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color:
|
|
647
|
+
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"] }), workMapAlerts.length > 0 ? (_jsxs(Text, { color: conflictAlerts.length > 0 ? UI.danger : UI.warn, children: [" \u00B7 \u26A0 work-map ", workMapAlerts.length] })) : null] }) })) : (_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
|
|
649
648
|
? systemLines
|
|
650
649
|
.filter((l) => !/^Ready|^Type a task|^⚡ Ready|^Default \/task|^Agent .* launched/.test(l.text))
|
|
651
650
|
.slice(-2)
|
|
@@ -657,9 +656,9 @@ function MainScreen({ ctl, folder, view, focus, rawLogs, systemLines, agentNames
|
|
|
657
656
|
// Split on \n so multiline i18n messages render correctly (Ink <Text> doesn't interpret \n).
|
|
658
657
|
const lines = l.text.split('\n');
|
|
659
658
|
return lines.map((line, j) => (_jsx(Text, { color: levelColor, wrap: "truncate-end", children: line }, `${i}-${j}`)));
|
|
660
|
-
})] })), 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` : '
|
|
661
|
-
|
|
662
|
-
|
|
659
|
+
})] })), 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(Text, { children: " " }), _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(Text, { children: " " }), _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 :
|
|
660
|
+
ctl.session.approvalMode === 'yolo' ? UI.danger :
|
|
661
|
+
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, workMapAlerts.length > 0 ? (_jsx(Text, { color: conflictAlerts.length > 0 ? UI.danger : UI.warn, children: " \u00B7 \u26A0 /board" })) : null, conflictAlerts.length > 0 ? (_jsx(Text, { color: UI.danger, children: " \u00B7 run /review all" })) : null, focused ? (_jsxs(Text, { color: BRAND.muted, children: [" \u00B7 \uD83C\uDFAF ", focused.name] })) : null] }) })] }));
|
|
663
662
|
}
|
|
664
663
|
function groupAgents(agents) {
|
|
665
664
|
const needs = agents.filter((a) => ['waiting', 'paused'].includes(a.state));
|
|
@@ -687,7 +686,10 @@ function AgentHub({ agents, ctl, cols, scroll, visibleRows, }) {
|
|
|
687
686
|
continue;
|
|
688
687
|
}
|
|
689
688
|
const needsSeparator = rows.length > 0;
|
|
690
|
-
const
|
|
689
|
+
const summaryLines = agent.lastResult ? Math.min(4, Math.max(1, agent.lastResult.split('\n').filter((l) => l.trim()).length)) : 0;
|
|
690
|
+
const stepLines = !agent.lastResult && agent.progressSteps && agent.progressSteps.length > 0 ? Math.min(3, agent.progressSteps.length) : 0;
|
|
691
|
+
const agentLines = 1 + Math.max(summaryLines, agent.currentAction || agent.claims?.length ? 1 : 0) + stepLines;
|
|
692
|
+
const neededLines = agentLines + (needsSeparator ? 1 : 0);
|
|
691
693
|
if (renderedLines + neededLines > visibleRows) {
|
|
692
694
|
full = true;
|
|
693
695
|
break;
|
|
@@ -697,7 +699,7 @@ function AgentHub({ agents, ctl, cols, scroll, visibleRows, }) {
|
|
|
697
699
|
renderedLines++;
|
|
698
700
|
}
|
|
699
701
|
renderedAgents++;
|
|
700
|
-
renderedLines +=
|
|
702
|
+
renderedLines += agentLines;
|
|
701
703
|
rows.push(_jsx(AgentRow, { agent: agent, logs: ctl.board.logsFor(agent.id, 8), cols: cols }, agent.id));
|
|
702
704
|
}
|
|
703
705
|
if (full)
|
package/dist/ui/AttachApp.js
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
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
|
-
import { Box, Static, Text, useApp } from 'ink';
|
|
4
|
+
import { Box, Static, Text, useApp, useInput, useStdout } from 'ink';
|
|
5
5
|
import { ApprovalPrompt } from './ApprovalPrompt.js';
|
|
6
6
|
import { CommandInput } from './CommandInput.js';
|
|
7
|
-
import { formatAgentTelemetry, KIND_COLOR, KIND_DIM } from './AgentPanel.js';
|
|
7
|
+
import { formatAgentTelemetry, KIND_COLOR, KIND_DIM, modeBadge, ProgressSteps } from './AgentPanel.js';
|
|
8
8
|
import { Md } from './Md.js';
|
|
9
9
|
import { QuestionPrompt } from './QuestionPrompt.js';
|
|
10
|
-
import { Spinner } from './Spinner.js';
|
|
11
10
|
import { Timeline } from './Timeline.js';
|
|
11
|
+
import { toUIEvents } from './events.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
|
+
const TERMINAL_STATES = new Set(['done', 'error', 'stopped']);
|
|
17
18
|
export function parseAttachCommand(text) {
|
|
18
19
|
const v = text.trim();
|
|
19
20
|
if (!v)
|
|
@@ -28,6 +29,14 @@ export function parseAttachCommand(text) {
|
|
|
28
29
|
const send = v.match(/^\/send\s+(\S+)\s+(.+)$/s);
|
|
29
30
|
if (send)
|
|
30
31
|
return { type: 'send', target: send[1], text: send[2].trim() };
|
|
32
|
+
const review = v.match(/^\/review\s+(.+)$/s);
|
|
33
|
+
if (review) {
|
|
34
|
+
return {
|
|
35
|
+
type: 'spawn',
|
|
36
|
+
text: `Review current shared-tree work: ${review[1].trim()}. Return Verdict: APPROVE | REVISE | BLOCK, Risks, Tests to run, Files to inspect, and Notes.`,
|
|
37
|
+
mode: 'ask',
|
|
38
|
+
};
|
|
39
|
+
}
|
|
31
40
|
const m = v.match(/^\/(ask|a|task|t|plan|p)\s+(.+)$/s);
|
|
32
41
|
if (m) {
|
|
33
42
|
const mode = m[1] === 'ask' || m[1] === 'a' ? 'ask' : m[1] === 'plan' || m[1] === 'p' ? 'plan' : 'task';
|
|
@@ -40,11 +49,39 @@ export function formatAttachFooter(info) {
|
|
|
40
49
|
return 'Waiting for agent · /quit';
|
|
41
50
|
return `${middleTruncate(info.model, 28)} · ${formatAgentTelemetry(info)} · plain text steers · /task new · /quit`;
|
|
42
51
|
}
|
|
52
|
+
function AttachStaticLine({ item, raw }) {
|
|
53
|
+
if (raw) {
|
|
54
|
+
return (_jsx(Text, { color: KIND_COLOR[item.log.kind] ?? 'white', italic: KIND_DIM[item.log.kind] ?? false, wrap: "wrap", children: item.log.text }));
|
|
55
|
+
}
|
|
56
|
+
const event = toUIEvents([item.log])[0];
|
|
57
|
+
if (!event || event.kind === 'thought')
|
|
58
|
+
return _jsx(Text, { color: UI.muted, children: " " });
|
|
59
|
+
const color = event.kind === 'error' ? UI.danger : event.kind === 'note' ? UI.note : event.kind === 'command' ? UI.accent : UI.muted;
|
|
60
|
+
const detail = event.detail.replace(/\r/g, '').split('\n').filter(Boolean).slice(0, 3).join(' ↳ ');
|
|
61
|
+
return (_jsxs(Text, { color: color, wrap: "truncate-end", children: [_jsx(Text, { color: UI.muted, children: "\u2022 " }), _jsx(Text, { bold: true, children: event.label }), detail ? _jsxs(Text, { color: event.kind === 'command_output' ? UI.muted : color, children: [" ", truncate(detail, process.stdout.columns ? process.stdout.columns - 8 : 120)] }) : null] }));
|
|
62
|
+
}
|
|
63
|
+
export function isLaunchSystemLog(log) {
|
|
64
|
+
return log.kind === 'system' && /\bAgent\s+.+\slaunched\b|Terminal dédié ouvert|Dedicated terminal/i.test(log.text);
|
|
65
|
+
}
|
|
66
|
+
function AttachLaunchHeader({ item }) {
|
|
67
|
+
const { info } = item;
|
|
68
|
+
const mode = modeBadge(info.mode);
|
|
69
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: info.color, paddingX: 1, marginBottom: 1, children: [_jsx(Text, { color: UI.brand, bold: true, children: "Parallel agent terminal" }), _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: info.color, bold: true, children: info.name }), info.alias && info.alias !== info.name ? _jsxs(Text, { color: UI.muted, children: [" @", info.alias] }) : null, _jsxs(Text, { color: mode.color, children: [" [", mode.label, "]"] }), _jsx(Text, { color: UI.muted, children: " \u00B7 " }), _jsx(Text, { color: UI.text, children: middleTruncate(info.model, 36) })] }), _jsxs(Text, { color: UI.muted, wrap: "wrap", children: ["Task ", _jsx(Text, { color: UI.text, children: info.task })] }), _jsx(Text, { color: COLOR.creamMuted, children: "Dedicated terminal is ready." }), _jsx(Text, { children: " " }), _jsx(Text, { children: " " })] }));
|
|
70
|
+
}
|
|
71
|
+
function AttachResultCard({ item }) {
|
|
72
|
+
const st = STATE_META[item.info.state];
|
|
73
|
+
return (_jsxs(Box, { borderStyle: "single", borderColor: st.color, flexDirection: "column", paddingX: 1, marginTop: 1, children: [_jsxs(Text, { color: COLOR.cream, bold: true, children: ["Result \u00B7 ", item.info.name, " [", st.label, "]"] }), _jsx(Md, { text: item.result })] }));
|
|
74
|
+
}
|
|
43
75
|
export function AttachApp({ agentRef, sock }) {
|
|
44
76
|
const { exit } = useApp();
|
|
77
|
+
const { stdout } = useStdout();
|
|
45
78
|
const [info, setInfo] = useState(null);
|
|
46
79
|
const [others, setOthers] = useState([]);
|
|
80
|
+
const [launchCards, setLaunchCards] = useState([]);
|
|
81
|
+
const [resultCards, setResultCards] = useState([]);
|
|
47
82
|
const [lines, setLines] = useState([]);
|
|
83
|
+
const [timelineScroll, setTimelineScroll] = useState(0);
|
|
84
|
+
const [timelineFollowTail, setTimelineFollowTail] = useState(true);
|
|
48
85
|
const [approval, setApproval] = useState(null);
|
|
49
86
|
const [question, setQuestion] = useState(null);
|
|
50
87
|
const [gone, setGone] = useState(false);
|
|
@@ -52,6 +89,8 @@ export function AttachApp({ agentRef, sock }) {
|
|
|
52
89
|
const socketRef = useRef(null);
|
|
53
90
|
const keySeq = useRef(0);
|
|
54
91
|
const lastBellId = useRef('');
|
|
92
|
+
const launchRendered = useRef(false);
|
|
93
|
+
const renderedResultKey = useRef('');
|
|
55
94
|
useEffect(() => {
|
|
56
95
|
const socket = net.connect(sock);
|
|
57
96
|
socketRef.current = socket;
|
|
@@ -96,6 +135,21 @@ export function AttachApp({ agentRef, sock }) {
|
|
|
96
135
|
socket.destroy();
|
|
97
136
|
};
|
|
98
137
|
}, [agentRef, sock]);
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
if (!info || launchRendered.current)
|
|
140
|
+
return;
|
|
141
|
+
launchRendered.current = true;
|
|
142
|
+
setLaunchCards([{ key: ++keySeq.current, info }]);
|
|
143
|
+
}, [info?.id]);
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
if (!info || !TERMINAL_STATES.has(info.state) || !info.lastResult)
|
|
146
|
+
return;
|
|
147
|
+
const key = `${info.id}:${info.state}:${info.lastResult.length}:${info.lastResult.slice(0, 24)}`;
|
|
148
|
+
if (renderedResultKey.current === key)
|
|
149
|
+
return;
|
|
150
|
+
renderedResultKey.current = key;
|
|
151
|
+
setResultCards((prev) => [...prev, { key: ++keySeq.current, info: { ...info }, result: info.lastResult ?? '' }]);
|
|
152
|
+
}, [info?.id, info?.state, info?.lastResult]);
|
|
99
153
|
// Audible alert in THIS terminal when a new interaction arrives — the hub
|
|
100
154
|
// also rings, but the user may well be looking at the agent's terminal.
|
|
101
155
|
useEffect(() => {
|
|
@@ -123,7 +177,7 @@ export function AttachApp({ agentRef, sock }) {
|
|
|
123
177
|
setRaw((r) => !r);
|
|
124
178
|
return;
|
|
125
179
|
}
|
|
126
|
-
// /task|/ask|/plan <text> — launch agent N+1 from this terminal.
|
|
180
|
+
// /task|/ask|/plan|/review <text> — launch agent N+1 from this terminal.
|
|
127
181
|
if (cmd.type === 'spawn') {
|
|
128
182
|
wire({ type: 'spawn', text: cmd.text, mode: cmd.mode });
|
|
129
183
|
return;
|
|
@@ -136,26 +190,57 @@ export function AttachApp({ agentRef, sock }) {
|
|
|
136
190
|
};
|
|
137
191
|
const st = info ? STATE_META[info.state] : null;
|
|
138
192
|
const busy = info ? ['thinking', 'working', 'listening'].includes(info.state) : false;
|
|
193
|
+
const terminal = info ? TERMINAL_STATES.has(info.state) : false;
|
|
139
194
|
const interacting = Boolean(approval || question);
|
|
195
|
+
const logs = lines.map((l) => l.log);
|
|
196
|
+
const staticLines = raw ? lines : lines.filter((l) => l.log.kind !== 'llm' && !isLaunchSystemLog(l.log));
|
|
197
|
+
const timelineVisibleLogs = Math.max(8, (stdout?.rows ?? 30) - 14);
|
|
198
|
+
const maxTimelineScroll = Math.max(0, logs.length - timelineVisibleLogs);
|
|
199
|
+
const clampedTimelineScroll = Math.min(timelineScroll, maxTimelineScroll);
|
|
200
|
+
const timelineWindow = logs.slice(Math.max(0, logs.length - timelineVisibleLogs - clampedTimelineScroll), logs.length - clampedTimelineScroll);
|
|
201
|
+
useEffect(() => {
|
|
202
|
+
if (timelineFollowTail)
|
|
203
|
+
setTimelineScroll(0);
|
|
204
|
+
}, [logs.length, timelineFollowTail]);
|
|
205
|
+
const scrollTimeline = (direction) => {
|
|
206
|
+
if (direction === 'up') {
|
|
207
|
+
setTimelineFollowTail(false);
|
|
208
|
+
setTimelineScroll((s) => Math.min(Math.min(s, maxTimelineScroll) + Math.max(1, Math.floor(timelineVisibleLogs / 2)), maxTimelineScroll));
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
setTimelineScroll((s) => {
|
|
212
|
+
const next = Math.max(0, Math.min(s, maxTimelineScroll) - Math.max(1, Math.floor(timelineVisibleLogs / 2)));
|
|
213
|
+
if (next === 0)
|
|
214
|
+
setTimelineFollowTail(true);
|
|
215
|
+
return next;
|
|
216
|
+
});
|
|
217
|
+
};
|
|
218
|
+
useInput((_input, key) => {
|
|
219
|
+
if (!busy || interacting || raw)
|
|
220
|
+
return;
|
|
221
|
+
if (key.pageUp)
|
|
222
|
+
scrollTimeline('up');
|
|
223
|
+
if (key.pageDown)
|
|
224
|
+
scrollTimeline('down');
|
|
225
|
+
}, { isActive: Boolean(busy && !interacting && !raw) });
|
|
140
226
|
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] }));
|
|
141
|
-
return (_jsxs(Box, { flexDirection: "column", children: [raw ? (_jsx(Static, { items:
|
|
142
|
-
/*
|
|
143
|
-
*
|
|
144
|
-
|
|
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
|
|
227
|
+
return (_jsxs(Box, { flexDirection: "column", children: [!raw ? (_jsx(Static, { items: launchCards, children: (item) => _jsx(AttachLaunchHeader, { item: item }, item.key) })) : null, _jsx(Static, { items: staticLines, children: (item) => (_jsx(AttachStaticLine, { item: item, raw: raw }, item.key)) }), !raw ? (_jsx(Static, { items: resultCards, children: (item) => _jsx(AttachResultCard, { item: item }, item.key) })) : null, !raw && staticLines.length > 0 ? _jsx(Text, { color: UI.muted, children: '─'.repeat(Math.min(Math.max(20, (stdout?.columns ?? 100) - 4), 80)) }) : null, (busy || terminal) && info && st && !interacting ? (
|
|
228
|
+
/* While running, keep the native terminal scrollback stable: activity is
|
|
229
|
+
* appended once above via <Static>, and this live region stays tiny. */
|
|
230
|
+
_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: modeBadge(info.mode).color, children: ["[", modeBadge(info.mode).label, "]"] }), ' ', _jsxs(Text, { color: st.color, bold: true, children: [st.mark, " ", st.label] }), _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, _jsx(ProgressSteps, { agent: info, max: 6, cols: process.stdout.columns || 100 }), terminal && info.lastResult ? (_jsx(Text, { color: COLOR.creamMuted, children: "Result was appended above; native mouse scroll stays available." })) : null, others.length > 0 ? (_jsxs(Text, { color: UI.muted, wrap: "truncate-end", children: ["Others ", ' ', others
|
|
146
231
|
.map((o) => `${o.name} [${stateLabel(o.state)}] ${truncate(o.currentAction || o.task, 40)}`)
|
|
147
|
-
.join(' · ')] })) : null, !
|
|
148
|
-
/* FULL panel
|
|
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 ? (
|
|
232
|
+
.join(' · ')] })) : null, !timelineFollowTail ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: UI.warn, children: "Viewing older activity \u00B7 \u2193/PgDn to latest" }), _jsx(Timeline, { logs: timelineWindow, cols: process.stdout.columns || 100 })] })) : null] })) : (
|
|
233
|
+
/* FULL panel for idle/waiting/interactions — terminal states stay compact. */
|
|
234
|
+
_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: modeBadge(info.mode).color, children: [" [", modeBadge(info.mode).label, "]"] }), _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, _jsx(ProgressSteps, { agent: info, max: 6, cols: process.stdout.columns || 100 }), others.length > 0 ? (
|
|
150
235
|
// The session's shared awareness, visible here too: what the
|
|
151
236
|
// OTHER agents are doing right now (live, same feed the agents get).
|
|
152
237
|
_jsxs(Text, { color: UI.muted, wrap: "truncate-end", children: ["Others ", ' ', others
|
|
153
238
|
.map((o) => `${o.name} [${stateLabel(o.state)}] ${truncate(o.currentAction || o.task, 40)}`)
|
|
154
|
-
.join(' · ')] })) : null,
|
|
239
|
+
.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: 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) => {
|
|
155
240
|
wire({ type: 'approve', id, approved: ok, always: !!always });
|
|
156
241
|
setApproval(null);
|
|
157
242
|
} })) : question ? (_jsx(QuestionPrompt, { question: { ...question, agentId: info?.id ?? '', resolve: noop }, pendingCount: 1, onAnswer: (id, answer) => {
|
|
158
243
|
wire({ type: 'answer', id, text: answer });
|
|
159
244
|
setQuestion(null);
|
|
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] : [], onSubmit: send, onEscape: () => exit() }), _jsx(
|
|
245
|
+
} }, question.id)) : null, _jsx(Text, { children: " " }), _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, onIdleNavigation: busy && !raw ? scrollTimeline : undefined, onSubmit: send, onEscape: () => exit() }), _jsx(Text, { children: " " }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: COLOR.creamMuted, wrap: "truncate-end", children: formatAttachFooter(info) }) })] }));
|
|
161
246
|
}
|
package/dist/ui/CommandInput.js
CHANGED
|
@@ -1,28 +1,23 @@
|
|
|
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 GROUP_LABEL = {
|
|
10
|
-
modes: 'Agent modes',
|
|
11
|
-
control: 'Control',
|
|
12
|
-
views: 'Views',
|
|
13
|
-
settings: 'Settings',
|
|
14
|
-
git: 'Git/session',
|
|
15
|
-
other: 'Other',
|
|
16
|
-
};
|
|
17
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 = '› ';
|
|
18
13
|
function modeHint(value, context, targetAgent) {
|
|
19
14
|
const v = value.trimStart().toLowerCase();
|
|
20
15
|
if (!v) {
|
|
21
16
|
if (context === 'focus')
|
|
22
|
-
return `Message ${targetAgent ?? 'focused agent'} · /
|
|
17
|
+
return `Message ${targetAgent ?? 'focused agent'} · / commands`;
|
|
23
18
|
if (context === 'attach')
|
|
24
|
-
return `Steer ${targetAgent ?? 'agent'} ·
|
|
25
|
-
return '
|
|
19
|
+
return `Steer ${targetAgent ?? 'agent'} · @all broadcasts · /quit detaches`;
|
|
20
|
+
return 'Type a task or / for commands';
|
|
26
21
|
}
|
|
27
22
|
if (!v.startsWith('/')) {
|
|
28
23
|
if (context === 'focus')
|
|
@@ -37,22 +32,18 @@ function modeHint(value, context, targetAgent) {
|
|
|
37
32
|
return 'Task mode · execute, edit, validate';
|
|
38
33
|
if (v.startsWith('/plan') || v === '/p')
|
|
39
34
|
return 'Plan mode · asks before editing';
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const order = ['modes', 'control', 'views', 'settings', 'git', 'other'];
|
|
44
|
-
return order
|
|
45
|
-
.map((g) => [g, commands.filter((c) => (c.group ?? 'other') === g)])
|
|
46
|
-
.filter(([, items]) => items.length > 0);
|
|
35
|
+
if (v.startsWith('/review'))
|
|
36
|
+
return 'Review mode · verdict, risks, tests';
|
|
37
|
+
return '↑/↓ select · Enter accept';
|
|
47
38
|
}
|
|
48
39
|
export function bestCommandCompletion(value) {
|
|
49
|
-
const cmd =
|
|
40
|
+
const cmd = commandPalette(value)[0];
|
|
50
41
|
return cmd ? `${cmd.name} ` : null;
|
|
51
42
|
}
|
|
52
43
|
export function commandNamesForContext(context) {
|
|
53
44
|
if (context !== 'attach')
|
|
54
45
|
return undefined;
|
|
55
|
-
return ['/ask', '/a', '/task', '/t', '/plan', '/p', '/send', '/raw', '/quit', '/exit', '/detach'];
|
|
46
|
+
return ['/ask', '/a', '/task', '/t', '/plan', '/p', '/review', '/send', '/raw', '/quit', '/exit', '/detach'];
|
|
56
47
|
}
|
|
57
48
|
export function agentArgCommand(value) {
|
|
58
49
|
const m = value.match(/^(\/\S+)\s+([^\s]*)$/);
|
|
@@ -67,12 +58,36 @@ export function completeAgentArgument(value, agent) {
|
|
|
67
58
|
return value;
|
|
68
59
|
return `${cmd} ${agent} `;
|
|
69
60
|
}
|
|
70
|
-
export function
|
|
61
|
+
export function clampSuggestionIndex(index, count) {
|
|
62
|
+
if (count <= 0)
|
|
63
|
+
return 0;
|
|
64
|
+
return Math.max(0, Math.min(index, count - 1));
|
|
65
|
+
}
|
|
66
|
+
export function wrappedPromptLines(text, width) {
|
|
67
|
+
const usable = Math.max(8, width - PROMPT_GUTTER.length);
|
|
68
|
+
if (!text)
|
|
69
|
+
return [''];
|
|
70
|
+
const lines = [];
|
|
71
|
+
for (const logical of text.split('\n')) {
|
|
72
|
+
if (!logical) {
|
|
73
|
+
lines.push('');
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
for (let i = 0; i < logical.length; i += usable)
|
|
77
|
+
lines.push(logical.slice(i, i + usable));
|
|
78
|
+
}
|
|
79
|
+
return lines.length > 0 ? lines : [''];
|
|
80
|
+
}
|
|
81
|
+
function paintLine(text, width) {
|
|
82
|
+
return text.length >= width ? text.slice(0, width) : text.padEnd(width, ' ');
|
|
83
|
+
}
|
|
84
|
+
export function CommandInput({ active, placeholder, mask, context = 'hub', targetAgent, modelLabel, commandNames, agentNames = [], agents = [], width, onHeightChange, onIdleNavigation, onSubmit, onEscape, notify, }) {
|
|
71
85
|
const [value, setValue] = useState('');
|
|
72
86
|
const [attachments, setAttachments] = useState([]);
|
|
73
87
|
const [history, setHistory] = useState([]);
|
|
74
88
|
const [histIdx, setHistIdx] = useState(-1);
|
|
75
89
|
const [selectedSuggestion, setSelectedSuggestion] = useState(0);
|
|
90
|
+
const [cursorOn, setCursorOn] = useState(true);
|
|
76
91
|
const attSeq = useRef(0);
|
|
77
92
|
const reset = () => {
|
|
78
93
|
setValue('');
|
|
@@ -118,8 +133,7 @@ export function CommandInput({ active, placeholder, mask, context = 'hub', targe
|
|
|
118
133
|
};
|
|
119
134
|
const uniqueAgentNames = [...new Set(agentNames.filter(Boolean))];
|
|
120
135
|
const allowedCommands = commandNames ?? commandNamesForContext(context);
|
|
121
|
-
const
|
|
122
|
-
const cmdSuggestions = value.startsWith('/') && !value.includes(' ') ? matchCommands(value).filter(commandAllowed).slice(0, 10) : [];
|
|
136
|
+
const cmdSuggestions = value.startsWith('/') && !value.includes(' ') ? commandPalette(value, { allowedNames: allowedCommands }) : [];
|
|
123
137
|
const agentSuggestions = value.startsWith('@') && !value.includes(' ')
|
|
124
138
|
? ['all', ...uniqueAgentNames].filter((n) => n.toLowerCase().startsWith(value.slice(1).toLowerCase())).slice(0, 8)
|
|
125
139
|
: [];
|
|
@@ -141,23 +155,51 @@ export function CommandInput({ active, placeholder, mask, context = 'hub', targe
|
|
|
141
155
|
useEffect(() => {
|
|
142
156
|
setSelectedSuggestion(0);
|
|
143
157
|
}, [value]);
|
|
158
|
+
const safeSelectedSuggestion = clampSuggestionIndex(selectedSuggestion, suggestionCount);
|
|
144
159
|
useEffect(() => {
|
|
145
|
-
if (selectedSuggestion
|
|
146
|
-
setSelectedSuggestion(
|
|
147
|
-
}, [selectedSuggestion,
|
|
160
|
+
if (selectedSuggestion !== safeSelectedSuggestion)
|
|
161
|
+
setSelectedSuggestion(safeSelectedSuggestion);
|
|
162
|
+
}, [selectedSuggestion, safeSelectedSuggestion]);
|
|
163
|
+
useEffect(() => {
|
|
164
|
+
if (!active)
|
|
165
|
+
return;
|
|
166
|
+
const timer = setInterval(() => setCursorOn((on) => !on), 450);
|
|
167
|
+
return () => clearInterval(timer);
|
|
168
|
+
}, [active]);
|
|
169
|
+
const commandWindowStart = cmdSuggestions.length > COMMAND_PAGE_SIZE
|
|
170
|
+
? Math.min(Math.max(0, safeSelectedSuggestion - Math.floor(COMMAND_PAGE_SIZE / 2)), Math.max(0, cmdSuggestions.length - COMMAND_PAGE_SIZE))
|
|
171
|
+
: 0;
|
|
172
|
+
const shownCommandSuggestions = cmdSuggestions.slice(commandWindowStart, commandWindowStart + COMMAND_PAGE_SIZE);
|
|
173
|
+
const shown = mask ? '•'.repeat(value.length) : value;
|
|
174
|
+
const promptWidth = Math.max(20, width ?? process.stdout.columns ?? 100);
|
|
175
|
+
const inputText = shown || placeholder;
|
|
176
|
+
const promptLines = wrappedPromptLines(inputText, promptWidth);
|
|
177
|
+
const suggestionRows = cmdSuggestions.length > 0
|
|
178
|
+
? shownCommandSuggestions.length + 1
|
|
179
|
+
: agentSuggestions.length > 0
|
|
180
|
+
? agentSuggestions.length
|
|
181
|
+
: argSuggestions.length > 0
|
|
182
|
+
? argSuggestions.length + 1
|
|
183
|
+
: 0;
|
|
184
|
+
const attachmentRows = attachments.length > 0 ? 1 : 0;
|
|
185
|
+
const hintRows = value || context !== 'hub' ? 1 : 0;
|
|
186
|
+
const renderedRows = suggestionRows + attachmentRows + promptLines.length + 2 + hintRows;
|
|
187
|
+
useEffect(() => {
|
|
188
|
+
onHeightChange?.(renderedRows);
|
|
189
|
+
}, [onHeightChange, renderedRows]);
|
|
148
190
|
const completeBest = () => {
|
|
149
191
|
if (cmdSuggestions.length > 0) {
|
|
150
|
-
const cmd = cmdSuggestions[
|
|
192
|
+
const cmd = cmdSuggestions[clampSuggestionIndex(safeSelectedSuggestion, cmdSuggestions.length)];
|
|
151
193
|
setValue(`${cmd.name} `);
|
|
152
194
|
return true;
|
|
153
195
|
}
|
|
154
196
|
if (agentSuggestions.length > 0) {
|
|
155
|
-
const agent = agentSuggestions[
|
|
197
|
+
const agent = agentSuggestions[clampSuggestionIndex(safeSelectedSuggestion, agentSuggestions.length)];
|
|
156
198
|
setValue('@' + agent + ' ');
|
|
157
199
|
return true;
|
|
158
200
|
}
|
|
159
201
|
if (argSuggestions.length > 0) {
|
|
160
|
-
const agent = argSuggestions[
|
|
202
|
+
const agent = argSuggestions[clampSuggestionIndex(safeSelectedSuggestion, argSuggestions.length)];
|
|
161
203
|
setValue(completeAgentArgument(value, agent));
|
|
162
204
|
return true;
|
|
163
205
|
}
|
|
@@ -193,7 +235,11 @@ export function CommandInput({ active, placeholder, mask, context = 'hub', targe
|
|
|
193
235
|
}
|
|
194
236
|
if (key.upArrow) {
|
|
195
237
|
if (hasSuggestions) {
|
|
196
|
-
setSelectedSuggestion((i) => (i - 1
|
|
238
|
+
setSelectedSuggestion((i) => clampSuggestionIndex(i - 1, suggestionCount));
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (!value && attachments.length === 0 && onIdleNavigation) {
|
|
242
|
+
onIdleNavigation('up');
|
|
197
243
|
return;
|
|
198
244
|
}
|
|
199
245
|
setHistIdx((i) => {
|
|
@@ -206,7 +252,11 @@ export function CommandInput({ active, placeholder, mask, context = 'hub', targe
|
|
|
206
252
|
}
|
|
207
253
|
if (key.downArrow) {
|
|
208
254
|
if (hasSuggestions) {
|
|
209
|
-
setSelectedSuggestion((i) => (i + 1
|
|
255
|
+
setSelectedSuggestion((i) => clampSuggestionIndex(i + 1, suggestionCount));
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (!value && attachments.length === 0 && onIdleNavigation) {
|
|
259
|
+
onIdleNavigation('down');
|
|
210
260
|
return;
|
|
211
261
|
}
|
|
212
262
|
setHistIdx((i) => {
|
|
@@ -257,15 +307,21 @@ export function CommandInput({ active, placeholder, mask, context = 'hub', targe
|
|
|
257
307
|
}
|
|
258
308
|
setValue((v) => v + input);
|
|
259
309
|
}, { isActive: active });
|
|
260
|
-
const shown = mask ? '•'.repeat(value.length) : value;
|
|
261
310
|
const byName = new Map(agents.flatMap((a) => [[a.name, a], [a.alias, a]]));
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
311
|
+
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) => {
|
|
312
|
+
const absolute = commandWindowStart + i;
|
|
313
|
+
const selected = absolute === safeSelectedSuggestion;
|
|
314
|
+
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));
|
|
315
|
+
})] })), 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'
|
|
267
316
|
? t('input.atAll')
|
|
268
|
-
: `${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 ===
|
|
317
|
+
: `${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'
|
|
269
318
|
? t('input.atAll')
|
|
270
|
-
: `${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:
|
|
319
|
+
: `${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) => {
|
|
320
|
+
const last = i === promptLines.length - 1;
|
|
321
|
+
const placeholderCursor = !shown && active && cursorOn && i === 0;
|
|
322
|
+
const cursor = shown && active && last && cursorOn ? '█' : '';
|
|
323
|
+
const prefix = i === 0 ? PROMPT_GUTTER : ' ';
|
|
324
|
+
const content = placeholderCursor ? `█${line.slice(1)}` : `${line}${cursor}`;
|
|
325
|
+
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));
|
|
326
|
+
}), _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}` : ''] }))] }));
|
|
271
327
|
}
|