@parallel-cli/parallel 0.3.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +146 -83
- package/dist/agents/agent.js +24 -2
- package/dist/agents/tools.js +4 -2
- package/dist/commands.js +179 -135
- package/dist/config.js +9 -0
- package/dist/controller.js +38 -5
- package/dist/i18n.js +160 -40
- package/dist/index.js +4 -2
- package/dist/server.js +2 -1
- package/dist/ui/AgentPanel.js +85 -16
- package/dist/ui/App.js +191 -61
- package/dist/ui/AttachApp.js +46 -21
- package/dist/ui/CommandInput.js +56 -15
- package/dist/ui/SettingsPanel.js +9 -2
- package/dist/ui/Timeline.js +60 -0
- package/dist/ui/events.js +229 -0
- package/dist/ui/theme.js +5 -4
- package/dist/ui/tokens.js +77 -0
- package/dist/ui/views.js +9 -3
- package/package.json +2 -2
package/dist/ui/CommandInput.js
CHANGED
|
@@ -6,7 +6,39 @@ import { t } from '../i18n.js';
|
|
|
6
6
|
import { readClipboardImage } from './clipboard.js';
|
|
7
7
|
/** A paste is "long" when it spans multiple lines — it then collapses into a chip. */
|
|
8
8
|
const PASTE_MIN_LINES = 2;
|
|
9
|
-
|
|
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
|
+
function modeHint(value) {
|
|
18
|
+
const v = value.trimStart().toLowerCase();
|
|
19
|
+
if (!v)
|
|
20
|
+
return 'Default /task · Tab/→ autocomplete · / for commands';
|
|
21
|
+
if (!v.startsWith('/'))
|
|
22
|
+
return 'Will launch /task';
|
|
23
|
+
if (v.startsWith('/ask') || v === '/a')
|
|
24
|
+
return 'Ask mode · advice only · no edits';
|
|
25
|
+
if (v.startsWith('/task') || v === '/t')
|
|
26
|
+
return 'Task mode · execute, edit, validate';
|
|
27
|
+
if (v.startsWith('/plan') || v === '/p')
|
|
28
|
+
return 'Plan mode · asks before editing';
|
|
29
|
+
return 'Tab/→ accepts the best suggestion';
|
|
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);
|
|
36
|
+
}
|
|
37
|
+
export function bestCommandCompletion(value) {
|
|
38
|
+
const cmd = matchCommands(value)[0];
|
|
39
|
+
return cmd ? `${cmd.name} ` : null;
|
|
40
|
+
}
|
|
41
|
+
export function CommandInput({ active, placeholder, mask, agentNames = [], agents = [], onSubmit, onEscape, notify }) {
|
|
10
42
|
const [value, setValue] = useState('');
|
|
11
43
|
const [attachments, setAttachments] = useState([]);
|
|
12
44
|
const [history, setHistory] = useState([]);
|
|
@@ -53,6 +85,22 @@ export function CommandInput({ active, placeholder, mask, agentNames = [], onSub
|
|
|
53
85
|
setAttachments((arr) => [...arr, { kind: 'image', n, dataUri: img.dataUri, label: img.label }]);
|
|
54
86
|
notify?.(t('input.imageAdded'));
|
|
55
87
|
};
|
|
88
|
+
const completeBest = () => {
|
|
89
|
+
const cmd = bestCommandCompletion(value);
|
|
90
|
+
if (cmd) {
|
|
91
|
+
setValue(cmd);
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
if (value.startsWith('@')) {
|
|
95
|
+
const frag = value.slice(1).toLowerCase();
|
|
96
|
+
const m = ['all', ...agentNames].find((n) => n.toLowerCase().startsWith(frag));
|
|
97
|
+
if (m) {
|
|
98
|
+
setValue('@' + m + ' ');
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return false;
|
|
103
|
+
};
|
|
56
104
|
useInput((input, key) => {
|
|
57
105
|
if (key.escape) {
|
|
58
106
|
if (value || attachments.length > 0)
|
|
@@ -100,18 +148,8 @@ export function CommandInput({ active, placeholder, mask, agentNames = [], onSub
|
|
|
100
148
|
});
|
|
101
149
|
return;
|
|
102
150
|
}
|
|
103
|
-
if (key.tab) {
|
|
104
|
-
|
|
105
|
-
if (cmds.length > 0) {
|
|
106
|
-
setValue(cmds[0].name + ' ');
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
if (value.startsWith('@')) {
|
|
110
|
-
const frag = value.slice(1).toLowerCase();
|
|
111
|
-
const m = ['all', ...agentNames].find((n) => n.toLowerCase().startsWith(frag));
|
|
112
|
-
if (m)
|
|
113
|
-
setValue('@' + m + ' ');
|
|
114
|
-
}
|
|
151
|
+
if (key.tab || key.rightArrow) {
|
|
152
|
+
completeBest();
|
|
115
153
|
return;
|
|
116
154
|
}
|
|
117
155
|
if (key.ctrl && input === 'u') {
|
|
@@ -145,10 +183,13 @@ export function CommandInput({ active, placeholder, mask, agentNames = [], onSub
|
|
|
145
183
|
}
|
|
146
184
|
setValue((v) => v + input);
|
|
147
185
|
}, { isActive: active });
|
|
148
|
-
const cmdSuggestions = value.startsWith('/') && !value.includes(' ') ? matchCommands(value).slice(0,
|
|
186
|
+
const cmdSuggestions = value.startsWith('/') && !value.includes(' ') ? matchCommands(value).slice(0, 10) : [];
|
|
149
187
|
const agentSuggestions = value.startsWith('@') && !value.includes(' ')
|
|
150
188
|
? ['all', ...agentNames].filter((n) => n.toLowerCase().startsWith(value.slice(1).toLowerCase())).slice(0, 8)
|
|
151
189
|
: [];
|
|
152
190
|
const shown = mask ? '•'.repeat(value.length) : value;
|
|
153
|
-
|
|
191
|
+
const byName = new Map(agents.flatMap((a) => [[a.name, a], [a.alias, a]]));
|
|
192
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "gray", wrap: "truncate-end", children: modeHint(value) }), cmdSuggestions.length > 0 && (_jsx(Box, { borderStyle: "single", borderColor: "gray", flexDirection: "column", paddingX: 1, children: groupedCommands(cmdSuggestions).map(([group, commands]) => (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "gray", bold: true, children: GROUP_LABEL[group] ?? group }), commands.map((c) => (_jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: c.name.padEnd(14) }), _jsx(Text, { color: "yellow", children: c.args.padEnd(22) }), _jsx(Text, { color: "gray", children: t(c.descKey) })] }, c.name)))] }, group))) })), agentSuggestions.length > 0 && (_jsx(Box, { borderStyle: "single", borderColor: "gray", flexDirection: "column", paddingX: 1, children: agentSuggestions.map((n) => (_jsxs(Text, { children: [_jsxs(Text, { color: "cyan", bold: true, children: ["@", n.padEnd(10)] }), _jsx(Text, { color: "gray", children: n === 'all'
|
|
193
|
+
? t('input.atAll')
|
|
194
|
+
: `${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: "cyan", backgroundColor: "gray", 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, { borderStyle: "single", borderColor: active ? 'cyan' : 'gray', paddingX: 1, children: [_jsxs(Text, { color: "cyanBright", bold: true, children: ["\u203A", ' '] }), shown ? (_jsxs(_Fragment, { children: [_jsx(Text, { children: shown }), active && _jsx(Text, { color: "cyanBright", children: "\u2588" })] })) : (_jsxs(_Fragment, { children: [active && _jsx(Text, { color: "cyanBright", children: "\u2588" }), _jsx(Text, { color: "gray", children: placeholder })] }))] })] }));
|
|
154
195
|
}
|
package/dist/ui/SettingsPanel.js
CHANGED
|
@@ -10,6 +10,13 @@ function masked(key) {
|
|
|
10
10
|
return '—';
|
|
11
11
|
return '••••' + key.slice(-4);
|
|
12
12
|
}
|
|
13
|
+
function nextApprovalMode(mode) {
|
|
14
|
+
if (mode === 'ask')
|
|
15
|
+
return 'auto-safe';
|
|
16
|
+
if (mode === 'auto-safe')
|
|
17
|
+
return 'yolo';
|
|
18
|
+
return 'ask';
|
|
19
|
+
}
|
|
13
20
|
/**
|
|
14
21
|
* /settings → scope 'global' : persisted in ~/.parallel/config.json
|
|
15
22
|
* /settings-session → scope 'session' : this session only, never persisted
|
|
@@ -76,9 +83,9 @@ export function SettingsPanel({ ctl, scope, onClose, }) {
|
|
|
76
83
|
return setStep({ id: 'newSpecialist' });
|
|
77
84
|
if (v === 'approvals') {
|
|
78
85
|
if (scope === 'global')
|
|
79
|
-
ctl.setGlobalApprovalMode(cfg.approvalMode
|
|
86
|
+
ctl.setGlobalApprovalMode(nextApprovalMode(cfg.approvalMode));
|
|
80
87
|
else
|
|
81
|
-
ctl.setSessionApprovalMode(ctl.session.approvalMode
|
|
88
|
+
ctl.setSessionApprovalMode(nextApprovalMode(ctl.session.approvalMode));
|
|
82
89
|
if (scope === 'global')
|
|
83
90
|
saved();
|
|
84
91
|
return;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { t } from '../i18n.js';
|
|
4
|
+
import { presentTimeline } from './events.js';
|
|
5
|
+
import { truncate } from './theme.js';
|
|
6
|
+
import { UI } from './tokens.js';
|
|
7
|
+
function sectionLabel(category) {
|
|
8
|
+
return t(`timeline.section.${category}`);
|
|
9
|
+
}
|
|
10
|
+
function fileLabel(label, count) {
|
|
11
|
+
const key = label === 'write' ? 'timeline.wroteFiles' : label === 'edit' ? 'timeline.editedFiles' : label === 'search' ? 'timeline.searched' : label === 'list' ? 'timeline.listed' : 'timeline.readFiles';
|
|
12
|
+
return t(key, { count });
|
|
13
|
+
}
|
|
14
|
+
function itemColor(item) {
|
|
15
|
+
if (item.status === 'error')
|
|
16
|
+
return UI.danger;
|
|
17
|
+
if (item.kind === 'command')
|
|
18
|
+
return UI.accent;
|
|
19
|
+
if (item.kind === 'files')
|
|
20
|
+
return item.category === 'change' ? UI.warn : UI.muted;
|
|
21
|
+
if (item.category === 'coordinate')
|
|
22
|
+
return UI.note;
|
|
23
|
+
if (item.kind === 'thought')
|
|
24
|
+
return UI.muted;
|
|
25
|
+
if (item.kind === 'narration')
|
|
26
|
+
return UI.text;
|
|
27
|
+
return UI.text;
|
|
28
|
+
}
|
|
29
|
+
function OutputLines({ item }) {
|
|
30
|
+
if (!item.output || item.output.length === 0)
|
|
31
|
+
return null;
|
|
32
|
+
return (_jsxs(Box, { flexDirection: "column", children: [item.output.map((line, i) => (_jsxs(Text, { color: item.status === 'error' ? UI.danger : UI.muted, wrap: "truncate-end", children: [_jsx(Text, { color: UI.muted, children: i === 0 ? '└ ' : ' ' }), truncate(line, 180)] }, `${item.seq ?? 0}-out-${i}`))), item.hiddenLines && item.hiddenLines > 0 ? (_jsxs(Text, { color: UI.muted, children: [' ', t('timeline.hiddenLines', { count: item.hiddenLines })] })) : null] }));
|
|
33
|
+
}
|
|
34
|
+
function TimelineRow({ item }) {
|
|
35
|
+
if (item.kind === 'section') {
|
|
36
|
+
return _jsx(Text, { color: UI.muted, children: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" });
|
|
37
|
+
}
|
|
38
|
+
if (item.kind === 'narration') {
|
|
39
|
+
return (_jsx(Box, { marginTop: 1, marginBottom: 1, children: _jsx(Text, { color: UI.text, wrap: "wrap", children: item.detail }) }));
|
|
40
|
+
}
|
|
41
|
+
if (item.kind === 'command') {
|
|
42
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: item.status === 'error' ? UI.danger : UI.text, wrap: "truncate-end", children: [_jsx(Text, { color: UI.muted, children: "\u2022 " }), _jsxs(Text, { bold: true, children: [t('timeline.ran'), " "] }), _jsx(Text, { color: UI.accent, children: truncate(item.command ?? '', 160) })] }), _jsx(OutputLines, { item: item })] }));
|
|
43
|
+
}
|
|
44
|
+
if (item.kind === 'files') {
|
|
45
|
+
const files = item.files ?? [];
|
|
46
|
+
const shown = files.slice(0, 5).join(', ');
|
|
47
|
+
const extra = files.length > 5 ? ` +${files.length - 5}` : '';
|
|
48
|
+
return (_jsxs(Text, { color: itemColor(item), wrap: "truncate-end", children: [_jsx(Text, { color: UI.muted, children: "\u2022 " }), _jsxs(Text, { bold: true, children: [fileLabel(item.label, files.length), " "] }), _jsxs(Text, { color: UI.muted, children: [shown, extra] })] }));
|
|
49
|
+
}
|
|
50
|
+
if (item.output) {
|
|
51
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: itemColor(item), wrap: "truncate-end", children: [_jsx(Text, { color: UI.muted, children: "\u2022 " }), item.label] }), _jsx(OutputLines, { item: item })] }));
|
|
52
|
+
}
|
|
53
|
+
return (_jsxs(Text, { color: itemColor(item), italic: item.kind === 'thought', wrap: "truncate-end", children: [_jsx(Text, { color: UI.muted, children: "\u2022 " }), truncate(item.detail ? `${item.label} ${item.detail}` : item.label, 180)] }));
|
|
54
|
+
}
|
|
55
|
+
export function Timeline({ logs, raw = false, emptyText }) {
|
|
56
|
+
const items = presentTimeline(logs, { raw, outputLines: raw ? 10 : 6 });
|
|
57
|
+
if (items.length === 0)
|
|
58
|
+
return _jsx(Text, { color: UI.muted, children: emptyText ?? t('timeline.empty') });
|
|
59
|
+
return (_jsx(Box, { flexDirection: "column", children: items.map((item, i) => item.kind === 'section' ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(TimelineRow, { item: item }), _jsx(Text, { color: UI.muted, children: sectionLabel(item.category) })] }, `${item.seq ?? i}-section`)) : (_jsx(TimelineRow, { item: item }, `${item.seq ?? i}-${i}`))) }));
|
|
60
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { oneLine } from './tokens.js';
|
|
2
|
+
function cleanToolText(text) {
|
|
3
|
+
return oneLine(text)
|
|
4
|
+
.replace(/^[📖📁🔍✏🚩🧠🧩📢⏳❓✉✅↳]+\s*/u, '')
|
|
5
|
+
.trim();
|
|
6
|
+
}
|
|
7
|
+
function stripShellNoise(command) {
|
|
8
|
+
return command
|
|
9
|
+
.replace(/\s+2>&1\b/g, '')
|
|
10
|
+
.replace(/\s+\|\s*cat\b/g, '')
|
|
11
|
+
.trim();
|
|
12
|
+
}
|
|
13
|
+
function classify(log) {
|
|
14
|
+
const text = oneLine(log.text);
|
|
15
|
+
const cleaned = cleanToolText(log.text);
|
|
16
|
+
const lower = cleaned.toLowerCase();
|
|
17
|
+
if (log.kind === 'error')
|
|
18
|
+
return { agentId: log.agentId, kind: 'error', label: 'error', detail: text, ts: log.ts, seq: log.seq };
|
|
19
|
+
if (log.kind === 'tool_result') {
|
|
20
|
+
return { agentId: log.agentId, kind: 'command_output', label: 'output', detail: log.text.trim(), ts: log.ts, seq: log.seq };
|
|
21
|
+
}
|
|
22
|
+
if (log.kind === 'note')
|
|
23
|
+
return { agentId: log.agentId, kind: 'note', label: 'note', detail: cleaned || text, ts: log.ts, seq: log.seq };
|
|
24
|
+
if (log.kind === 'system')
|
|
25
|
+
return { agentId: log.agentId, kind: 'system', label: 'system', detail: cleaned || text, ts: log.ts, seq: log.seq };
|
|
26
|
+
if (log.kind === 'llm')
|
|
27
|
+
return { agentId: log.agentId, kind: 'thought', label: 'thinking', detail: cleaned.replace(/^✻\s*/, ''), ts: log.ts, seq: log.seq };
|
|
28
|
+
if (/^\$\s*/.test(cleaned)) {
|
|
29
|
+
return {
|
|
30
|
+
agentId: log.agentId,
|
|
31
|
+
kind: 'command',
|
|
32
|
+
label: 'run',
|
|
33
|
+
detail: stripShellNoise(cleaned.replace(/^\$\s*/, '')),
|
|
34
|
+
ts: log.ts,
|
|
35
|
+
seq: log.seq,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (/^(read|opened)\s+/i.test(cleaned)) {
|
|
39
|
+
return { agentId: log.agentId, kind: 'file', label: 'read', detail: cleaned.replace(/^(read|opened)\s+/i, ''), ts: log.ts, seq: log.seq };
|
|
40
|
+
}
|
|
41
|
+
if (/^(ls|list)\s+/i.test(cleaned)) {
|
|
42
|
+
return { agentId: log.agentId, kind: 'file', label: 'list', detail: cleaned.replace(/^(ls|list)\s+/i, ''), ts: log.ts, seq: log.seq };
|
|
43
|
+
}
|
|
44
|
+
if (/^(search)\s+/i.test(cleaned)) {
|
|
45
|
+
return { agentId: log.agentId, kind: 'file', label: 'search', detail: cleaned.replace(/^search\s+/i, ''), ts: log.ts, seq: log.seq };
|
|
46
|
+
}
|
|
47
|
+
if (/^(write|edit|patch|claim|claims?)\s+/i.test(cleaned)) {
|
|
48
|
+
const label = lower.startsWith('claim') ? 'claim' : lower.startsWith('write') ? 'write' : 'edit';
|
|
49
|
+
return { agentId: log.agentId, kind: 'file', label, detail: cleaned.replace(/^(write|edit|patch|claim|claims?)\s*/i, ''), ts: log.ts, seq: log.seq };
|
|
50
|
+
}
|
|
51
|
+
if (/^(run|exec|shell|npm|pnpm|yarn|git|node|npx)\b/i.test(cleaned)) {
|
|
52
|
+
return { agentId: log.agentId, kind: 'command', label: 'run', detail: stripShellNoise(cleaned), ts: log.ts, seq: log.seq };
|
|
53
|
+
}
|
|
54
|
+
if (lower.includes('approval') || lower.includes('approve')) {
|
|
55
|
+
return { agentId: log.agentId, kind: 'approval', label: 'approval', detail: cleaned || text, ts: log.ts, seq: log.seq };
|
|
56
|
+
}
|
|
57
|
+
if (lower.includes('ask') || lower.includes('question')) {
|
|
58
|
+
return { agentId: log.agentId, kind: 'question', label: 'question', detail: cleaned || text, ts: log.ts, seq: log.seq };
|
|
59
|
+
}
|
|
60
|
+
return { agentId: log.agentId, kind: log.kind === 'tool' ? 'tool' : 'system', label: log.kind === 'tool' ? 'tool' : 'info', detail: cleaned || text, ts: log.ts, seq: log.seq };
|
|
61
|
+
}
|
|
62
|
+
export function toUIEvents(logs) {
|
|
63
|
+
return logs.map(classify);
|
|
64
|
+
}
|
|
65
|
+
export function compactEvents(events) {
|
|
66
|
+
const out = [];
|
|
67
|
+
for (let i = 0; i < events.length; i++) {
|
|
68
|
+
const e = events[i];
|
|
69
|
+
if (e.kind !== 'file' || e.label !== 'read') {
|
|
70
|
+
out.push(e);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const reads = [e];
|
|
74
|
+
while (i + 1 < events.length && events[i + 1].kind === 'file' && events[i + 1].label === 'read') {
|
|
75
|
+
reads.push(events[++i]);
|
|
76
|
+
}
|
|
77
|
+
if (reads.length < 3) {
|
|
78
|
+
out.push(...reads);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const files = reads.flatMap((r) => r.detail.split(/\s+/).filter(Boolean));
|
|
82
|
+
out.push({
|
|
83
|
+
...e,
|
|
84
|
+
detail: `${files.slice(0, 5).join(', ')}${files.length > 5 ? ` +${files.length - 5}` : ''}`,
|
|
85
|
+
label: `read ${files.length}`,
|
|
86
|
+
ts: reads[reads.length - 1].ts,
|
|
87
|
+
seq: reads[reads.length - 1].seq,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
function commandCategory(command) {
|
|
93
|
+
if (/\b(test|build|tsc|lint|check|node\s+--test)\b/i.test(command))
|
|
94
|
+
return 'validate';
|
|
95
|
+
if (/\bgit\s+(add|commit|push|pull|fetch|merge|switch|checkout|branch)\b/i.test(command))
|
|
96
|
+
return 'publish';
|
|
97
|
+
if (/\bgit\s+(status|log|show|diff)\b/i.test(command))
|
|
98
|
+
return 'inspect';
|
|
99
|
+
if (/\b(find|pwd|ls|cat|sed|rg|grep|head|tail)\b/i.test(command))
|
|
100
|
+
return 'inspect';
|
|
101
|
+
return 'validate';
|
|
102
|
+
}
|
|
103
|
+
function categoryFor(e) {
|
|
104
|
+
if (e.kind === 'error')
|
|
105
|
+
return 'result';
|
|
106
|
+
if (e.kind === 'note' || e.kind === 'approval' || e.kind === 'question')
|
|
107
|
+
return 'coordinate';
|
|
108
|
+
if (e.kind === 'file') {
|
|
109
|
+
if (e.label === 'write' || e.label === 'edit' || e.label === 'claim')
|
|
110
|
+
return 'change';
|
|
111
|
+
return 'inspect';
|
|
112
|
+
}
|
|
113
|
+
if (e.kind === 'command') {
|
|
114
|
+
return commandCategory(e.detail);
|
|
115
|
+
}
|
|
116
|
+
return 'other';
|
|
117
|
+
}
|
|
118
|
+
function filesFrom(events) {
|
|
119
|
+
return events.flatMap((e) => e.detail.split(/[,\s]+/).filter(Boolean));
|
|
120
|
+
}
|
|
121
|
+
export function summarizeCommandOutput(output, command = '', maxLines = 6) {
|
|
122
|
+
const rawLines = output
|
|
123
|
+
.replace(/\r/g, '')
|
|
124
|
+
.split('\n')
|
|
125
|
+
.map((l) => l.trimEnd())
|
|
126
|
+
.filter((l) => l.trim().length > 0);
|
|
127
|
+
const status = /\b(error|failed|fatal|exit code: [1-9]|not found|denied)\b/i.test(output) ? 'error' : 'ok';
|
|
128
|
+
if (/npm\s+test|node\s+--test|test:pty|tsc|npm\s+run\s+build/i.test(command) && status === 'ok') {
|
|
129
|
+
const passed = rawLines.find((l) => /passed|pass|ok\b/i.test(l));
|
|
130
|
+
return { lines: [passed ? `Passed: ${passed.replace(/^#\s*/, '')}` : 'Passed'], hiddenLines: Math.max(0, rawLines.length - 1), status };
|
|
131
|
+
}
|
|
132
|
+
if (rawLines.length <= maxLines)
|
|
133
|
+
return { lines: rawLines.length > 0 ? rawLines : ['(no output, success)'], hiddenLines: 0, status };
|
|
134
|
+
const head = Math.max(1, Math.floor(maxLines / 2));
|
|
135
|
+
const tail = Math.max(1, maxLines - head);
|
|
136
|
+
return {
|
|
137
|
+
lines: [...rawLines.slice(0, head), ...rawLines.slice(rawLines.length - tail)],
|
|
138
|
+
hiddenLines: rawLines.length - maxLines,
|
|
139
|
+
status,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function narrationFor(category, previous) {
|
|
143
|
+
if (category === 'inspect' && previous === 'validate')
|
|
144
|
+
return 'Cette piste ne suffit pas, donc je reviens inspecter le projet pour confirmer l’état réel.';
|
|
145
|
+
if (category === 'inspect')
|
|
146
|
+
return 'Je vérifie l’état du projet et les fichiers concernés avant de conclure.';
|
|
147
|
+
if (category === 'change')
|
|
148
|
+
return 'Je modifie maintenant les fichiers ciblés en gardant le changement aussi petit que possible.';
|
|
149
|
+
if (category === 'validate')
|
|
150
|
+
return 'Je lance les validations locales pour vérifier que les changements tiennent techniquement.';
|
|
151
|
+
if (category === 'publish')
|
|
152
|
+
return 'Je prépare la synchronisation Git après avoir vérifié l’état local.';
|
|
153
|
+
if (category === 'coordinate')
|
|
154
|
+
return 'Je traite les échanges et les décisions nécessaires pour avancer proprement.';
|
|
155
|
+
if (category === 'result')
|
|
156
|
+
return 'Je vérifie le résultat final et les éventuelles erreurs importantes.';
|
|
157
|
+
return 'Je poursuis l’activité en cours.';
|
|
158
|
+
}
|
|
159
|
+
function pushSection(out, category, ts, seq) {
|
|
160
|
+
const prev = [...out].reverse().find((i) => i.kind !== 'section');
|
|
161
|
+
if (category === 'other')
|
|
162
|
+
return;
|
|
163
|
+
if (!prev) {
|
|
164
|
+
out.push({ kind: 'narration', category, label: 'narration', detail: narrationFor(category), ts, seq });
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (prev.category === category)
|
|
168
|
+
return;
|
|
169
|
+
out.push({ kind: 'section', category, label: category, ts, seq });
|
|
170
|
+
out.push({ kind: 'narration', category, label: 'narration', detail: narrationFor(category, prev.category), ts, seq });
|
|
171
|
+
}
|
|
172
|
+
export function presentTimeline(logs, options = {}) {
|
|
173
|
+
const events = options.raw ? toUIEvents(logs) : toUIEvents(logs).filter((e) => e.kind !== 'thought');
|
|
174
|
+
const out = [];
|
|
175
|
+
for (let i = 0; i < events.length; i++) {
|
|
176
|
+
const e = events[i];
|
|
177
|
+
const category = categoryFor(e);
|
|
178
|
+
pushSection(out, category, e.ts, e.seq);
|
|
179
|
+
if (options.raw && e.kind === 'thought') {
|
|
180
|
+
out.push({ kind: 'thought', category, label: e.label, detail: e.detail, ts: e.ts, seq: e.seq });
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (e.kind === 'file') {
|
|
184
|
+
const group = [e];
|
|
185
|
+
while (i + 1 < events.length && events[i + 1].kind === 'file' && events[i + 1].label === e.label) {
|
|
186
|
+
group.push(events[++i]);
|
|
187
|
+
}
|
|
188
|
+
const files = filesFrom(group);
|
|
189
|
+
out.push({ kind: 'files', category, label: e.label, files, ts: group[group.length - 1].ts, seq: group[group.length - 1].seq });
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (e.kind === 'command') {
|
|
193
|
+
let output;
|
|
194
|
+
if (i + 1 < events.length && events[i + 1].kind === 'command_output') {
|
|
195
|
+
output = summarizeCommandOutput(events[++i].detail, e.detail, options.outputLines ?? 6);
|
|
196
|
+
}
|
|
197
|
+
out.push({
|
|
198
|
+
kind: 'command',
|
|
199
|
+
category,
|
|
200
|
+
label: 'run',
|
|
201
|
+
command: e.detail,
|
|
202
|
+
output: output?.lines,
|
|
203
|
+
hiddenLines: output?.hiddenLines,
|
|
204
|
+
status: output?.status,
|
|
205
|
+
ts: e.ts,
|
|
206
|
+
seq: e.seq,
|
|
207
|
+
});
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (e.kind === 'command_output' && options.raw) {
|
|
211
|
+
const output = summarizeCommandOutput(e.detail, '', options.outputLines ?? 8);
|
|
212
|
+
out.push({ kind: 'event', category, label: 'output', output: output.lines, hiddenLines: output.hiddenLines, status: output.status, ts: e.ts, seq: e.seq });
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
out.push({ kind: 'event', category, label: e.label, detail: e.detail, status: e.kind === 'error' ? 'error' : 'ok', ts: e.ts, seq: e.seq });
|
|
216
|
+
}
|
|
217
|
+
return out;
|
|
218
|
+
}
|
|
219
|
+
export function latestSignal(agent, events) {
|
|
220
|
+
if (agent.currentAction)
|
|
221
|
+
return agent.currentAction;
|
|
222
|
+
const last = [...events].reverse().find((e) => {
|
|
223
|
+
const item = e;
|
|
224
|
+
return item.kind !== 'thought' && Boolean(item.detail || item.command || item.files?.length);
|
|
225
|
+
});
|
|
226
|
+
if (last)
|
|
227
|
+
return last.command ? `run ${last.command}` : last.files?.length ? `${last.label} ${last.files[0]}` : `${last.label} ${last.detail}`;
|
|
228
|
+
return agent.lastResult ? 'result ready' : agent.task;
|
|
229
|
+
}
|
package/dist/ui/theme.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { t } from '../i18n.js';
|
|
2
|
+
import { STATE } from './tokens.js';
|
|
2
3
|
/** Strong visual cues: icon + label (i18n key) + color per state. */
|
|
3
4
|
export const STATE_LABEL = {
|
|
4
|
-
idle: { icon: '◇', labelKey: 'st.idle', color:
|
|
5
|
-
thinking: { icon: '🧠', labelKey: 'st.thinking', color:
|
|
5
|
+
idle: { icon: '◇', labelKey: 'st.idle', color: STATE.idle },
|
|
6
|
+
thinking: { icon: '🧠', labelKey: 'st.thinking', color: STATE.thinking },
|
|
6
7
|
listening: { icon: '👂', labelKey: 'st.listening', color: 'cyanBright' },
|
|
7
8
|
working: { icon: '🔨', labelKey: 'st.working', color: 'green' },
|
|
8
9
|
waiting: { icon: '✋', labelKey: 'st.waiting', color: 'magenta' },
|
|
9
10
|
paused: { icon: '⏸', labelKey: 'st.paused', color: 'blue' },
|
|
10
|
-
done: { icon: '✅', labelKey: 'st.done', color:
|
|
11
|
+
done: { icon: '✅', labelKey: 'st.done', color: STATE.done },
|
|
11
12
|
error: { icon: '✖', labelKey: 'st.error', color: 'red' },
|
|
12
|
-
stopped: { icon: '⏹', labelKey: 'st.stopped', color:
|
|
13
|
+
stopped: { icon: '⏹', labelKey: 'st.stopped', color: STATE.error },
|
|
13
14
|
};
|
|
14
15
|
export function stateLabel(state) {
|
|
15
16
|
return t(STATE_LABEL[state].labelKey);
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
export const MARK = {
|
|
2
|
+
active: '●',
|
|
3
|
+
idle: '◌',
|
|
4
|
+
done: '✓',
|
|
5
|
+
error: '!',
|
|
6
|
+
waiting: '?',
|
|
7
|
+
arrow: '▸',
|
|
8
|
+
};
|
|
9
|
+
export const UI = {
|
|
10
|
+
brand: 'cyanBright',
|
|
11
|
+
accent: 'cyan',
|
|
12
|
+
muted: 'gray',
|
|
13
|
+
text: 'white',
|
|
14
|
+
ok: 'greenBright',
|
|
15
|
+
warn: 'yellow',
|
|
16
|
+
danger: 'redBright',
|
|
17
|
+
note: 'magentaBright',
|
|
18
|
+
};
|
|
19
|
+
export const STATE_META = {
|
|
20
|
+
waiting: { mark: MARK.waiting, label: 'needs input', color: UI.warn, rank: 0 },
|
|
21
|
+
paused: { mark: MARK.waiting, label: 'paused', color: UI.warn, rank: 1 },
|
|
22
|
+
listening: { mark: MARK.active, label: 'listening', color: UI.accent, rank: 2 },
|
|
23
|
+
thinking: { mark: MARK.active, label: 'thinking', color: UI.accent, rank: 3 },
|
|
24
|
+
working: { mark: MARK.active, label: 'working', color: UI.accent, rank: 4 },
|
|
25
|
+
error: { mark: MARK.error, label: 'error', color: UI.danger, rank: 5 },
|
|
26
|
+
stopped: { mark: MARK.error, label: 'stopped', color: UI.danger, rank: 6 },
|
|
27
|
+
done: { mark: MARK.done, label: 'done', color: UI.ok, rank: 7 },
|
|
28
|
+
idle: { mark: MARK.idle, label: 'idle', color: UI.muted, rank: 8 },
|
|
29
|
+
};
|
|
30
|
+
// ─── Hub redesign tokens (Phase 0) ───────────────────────────────────────────
|
|
31
|
+
/** Brand colors — logotype, borders, focus indicator. */
|
|
32
|
+
export const BRAND = {
|
|
33
|
+
primary: 'cyanBright',
|
|
34
|
+
muted: 'cyan',
|
|
35
|
+
};
|
|
36
|
+
/** Semantic state colors mapped to agent states. */
|
|
37
|
+
export const STATE = {
|
|
38
|
+
working: 'cyan',
|
|
39
|
+
thinking: 'yellow',
|
|
40
|
+
listening: 'yellow',
|
|
41
|
+
done: 'greenBright',
|
|
42
|
+
error: 'redBright',
|
|
43
|
+
waiting: 'yellow',
|
|
44
|
+
idle: 'gray',
|
|
45
|
+
};
|
|
46
|
+
/** Mode indicator colors. `task` is undefined — it renders no mark. */
|
|
47
|
+
export const MODE = {
|
|
48
|
+
ask: 'yellow',
|
|
49
|
+
plan: 'blue',
|
|
50
|
+
task: undefined,
|
|
51
|
+
};
|
|
52
|
+
/** Chrome / UI element colors. */
|
|
53
|
+
export const CHROME = {
|
|
54
|
+
separator: 'gray',
|
|
55
|
+
muted: 'gray',
|
|
56
|
+
dim: 'dim',
|
|
57
|
+
};
|
|
58
|
+
/** Animation tokens. */
|
|
59
|
+
export const ANIM = {
|
|
60
|
+
spinner: 'dots',
|
|
61
|
+
pulseMs: 400,
|
|
62
|
+
spinnerIntervalMs: 80,
|
|
63
|
+
};
|
|
64
|
+
/** Plain 7-bit ASCII logotype — renders in every terminal. */
|
|
65
|
+
export const ASCII_LOGO = 'PARALLEL';
|
|
66
|
+
export function middleTruncate(text, max) {
|
|
67
|
+
if (text.length <= max)
|
|
68
|
+
return text;
|
|
69
|
+
if (max <= 3)
|
|
70
|
+
return text.slice(0, max);
|
|
71
|
+
const left = Math.ceil((max - 1) / 2);
|
|
72
|
+
const right = Math.floor((max - 1) / 2);
|
|
73
|
+
return `${text.slice(0, left)}…${text.slice(text.length - right)}`;
|
|
74
|
+
}
|
|
75
|
+
export function oneLine(text) {
|
|
76
|
+
return text.replace(/\s+/g, ' ').trim();
|
|
77
|
+
}
|
package/dist/ui/views.js
CHANGED
|
@@ -2,7 +2,7 @@ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import { useState } from 'react';
|
|
3
3
|
import { Box, Text, useInput, useStdout } from 'ink';
|
|
4
4
|
import * as Diff from 'diff';
|
|
5
|
-
import {
|
|
5
|
+
import { visibleCommands } from '../commands.js';
|
|
6
6
|
import { Controller } from '../controller.js';
|
|
7
7
|
import { fmtCost } from '../pricing.js';
|
|
8
8
|
import { STATE_LABEL, stateLabel, truncate } from './theme.js';
|
|
@@ -89,6 +89,12 @@ export function SessionsView({ projectRoot }) {
|
|
|
89
89
|
export function HelpView() {
|
|
90
90
|
// Intro (4) + title + blank lines (2) + footer (2) + border/input/status ≈ 16 rows of overhead.
|
|
91
91
|
const visible = useVisibleRows(16);
|
|
92
|
-
const
|
|
93
|
-
|
|
92
|
+
const commands = visibleCommands();
|
|
93
|
+
const { slice, above, below } = useScrollWindow(commands, visible, 'top');
|
|
94
|
+
const highlights = [
|
|
95
|
+
['Agent modes', ['/ask', '/task', '/plan']],
|
|
96
|
+
['Shell approvals', ['/approvals ask', '/approvals auto', '/approvals yolo']],
|
|
97
|
+
['Navigation', ['/focus', '/attach', '/raw', '/send']],
|
|
98
|
+
];
|
|
99
|
+
return (_jsxs(Box, { borderStyle: "round", borderColor: "cyan", flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: t('help.title') }), _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { bold: true, children: t('help.l1a') }), t('help.l1b'), _jsx(Text, { bold: true, children: t('help.l1c') }), "."] }), _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { bold: true, children: t('help.l2a') }), t('help.l2b'), _jsx(Text, { bold: true, children: t('help.l2c') }), t('help.l2d')] }), _jsx(Text, { wrap: "truncate-end", children: t('help.l3') }), _jsx(Text, { children: " " }), highlights.map(([label, names]) => (_jsxs(Text, { wrap: "truncate-end", children: [_jsxs(Text, { color: "cyan", bold: true, children: [label, ": "] }), _jsx(Text, { color: "gray", children: names.join(' ') })] }, label))), _jsx(Text, { color: "gray", wrap: "truncate-end", children: "Keyboard: Tab/\u2192 autocomplete \u00B7 Esc back/clear \u00B7 PgUp/PgDn scroll \u00B7 Ctrl+U clear \u00B7 Ctrl+V image" }), _jsx(Text, { children: " " }), _jsx(Above, { n: above }), slice.map((c) => (_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: "cyan", bold: true, children: c.name.padEnd(18) }), _jsx(Text, { color: "yellow", children: c.args.padEnd(24) }), _jsxs(Text, { color: "gray", children: [t(c.descKey), c.aliases?.length ? ` (= ${c.aliases.join(', ')})` : ''] })] }, c.name))), _jsx(Below, { n: below }), _jsx(Text, { children: " " }), _jsx(Text, { color: "gray", wrap: "truncate-end", children: t('help.states') }), _jsx(Text, { color: "gray", wrap: "truncate-end", children: t('help.keys') })] }));
|
|
94
100
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parallel-cli/parallel",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Real-time multi-agent coding CLI with shared context, adaptive co-editing, dedicated agent terminals, and headless CI runs.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"scripts": {
|
|
40
40
|
"build": "tsc && chmod +x dist/index.js",
|
|
41
41
|
"dev": "tsc --watch",
|
|
42
|
-
"test": "npm run build && node --test test/*.test.mjs && npm run test:pty",
|
|
42
|
+
"test": "npm run build && ( [ -d test ] && node --test test/*.test.mjs && npm run test:pty || echo 'test/ directory not found, skipping tests' )",
|
|
43
43
|
"test:pty": "sh test/pty.sh",
|
|
44
44
|
"start": "node dist/index.js",
|
|
45
45
|
"prepublishOnly": "npm run build"
|