@parallel-cli/parallel 0.4.6 → 0.4.8
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 +42 -0
- package/README.md +60 -12
- package/dist/agents/agent.js +175 -28
- package/dist/agents/tools.js +218 -20
- package/dist/commands.js +53 -0
- package/dist/controller.js +26 -13
- package/dist/coordination/blackboard.js +44 -2
- package/dist/i18n.js +69 -13
- package/dist/index.js +1 -0
- package/dist/server.js +9 -0
- package/dist/ui/AgentPanel.js +99 -25
- package/dist/ui/App.js +13 -6
- package/dist/ui/AttachApp.js +109 -15
- package/dist/ui/CommandInput.js +12 -2
- package/dist/ui/Timeline.js +5 -0
- package/dist/ui/events.js +20 -17
- package/dist/ui/theme.js +2 -2
- package/dist/ui/tokens.js +2 -2
- package/dist/ui/views.js +17 -5
- package/dist/version.js +1 -1
- package/package.json +2 -2
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
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)
|
|
@@ -22,12 +23,23 @@ export function parseAttachCommand(text) {
|
|
|
22
23
|
return { type: 'detach' };
|
|
23
24
|
if (v === '/raw')
|
|
24
25
|
return { type: 'raw' };
|
|
26
|
+
const stop = v.match(/^\/stop(?:\s+(\S+))?$/s);
|
|
27
|
+
if (stop)
|
|
28
|
+
return { type: 'stop', target: stop[1]?.trim() };
|
|
25
29
|
const at = v.match(/^@(\S+)\s+(.+)$/s);
|
|
26
30
|
if (at)
|
|
27
31
|
return { type: 'send', target: at[1], text: at[2].trim() };
|
|
28
32
|
const send = v.match(/^\/send\s+(\S+)\s+(.+)$/s);
|
|
29
33
|
if (send)
|
|
30
34
|
return { type: 'send', target: send[1], text: send[2].trim() };
|
|
35
|
+
const review = v.match(/^\/review\s+(.+)$/s);
|
|
36
|
+
if (review) {
|
|
37
|
+
return {
|
|
38
|
+
type: 'spawn',
|
|
39
|
+
text: `Review current shared-tree work: ${review[1].trim()}. Return Verdict: APPROVE | REVISE | BLOCK, Risks, Tests to run, Files to inspect, and Notes.`,
|
|
40
|
+
mode: 'ask',
|
|
41
|
+
};
|
|
42
|
+
}
|
|
31
43
|
const m = v.match(/^\/(ask|a|task|t|plan|p)\s+(.+)$/s);
|
|
32
44
|
if (m) {
|
|
33
45
|
const mode = m[1] === 'ask' || m[1] === 'a' ? 'ask' : m[1] === 'plan' || m[1] === 'p' ? 'plan' : 'task';
|
|
@@ -38,13 +50,42 @@ export function parseAttachCommand(text) {
|
|
|
38
50
|
export function formatAttachFooter(info) {
|
|
39
51
|
if (!info)
|
|
40
52
|
return 'Waiting for agent · /quit';
|
|
41
|
-
|
|
53
|
+
const control = ['thinking', 'working', 'listening', 'waiting', 'paused'].includes(info.state) ? ' · /stop' : '';
|
|
54
|
+
return `${middleTruncate(info.model, 28)} · ${formatAgentTelemetry(info)} · plain text steers${control} · /task new · /quit`;
|
|
55
|
+
}
|
|
56
|
+
function AttachStaticLine({ item, raw }) {
|
|
57
|
+
if (raw) {
|
|
58
|
+
return (_jsx(Text, { color: KIND_COLOR[item.log.kind] ?? 'white', italic: KIND_DIM[item.log.kind] ?? false, wrap: "wrap", children: item.log.text }));
|
|
59
|
+
}
|
|
60
|
+
const event = toUIEvents([item.log])[0];
|
|
61
|
+
if (!event || event.kind === 'thought')
|
|
62
|
+
return _jsx(Text, { color: UI.muted, children: " " });
|
|
63
|
+
const color = event.kind === 'error' ? UI.danger : event.kind === 'note' ? UI.note : event.kind === 'command' ? UI.accent : UI.muted;
|
|
64
|
+
const detail = event.detail.replace(/\r/g, '').split('\n').filter(Boolean).slice(0, 3).join(' ↳ ');
|
|
65
|
+
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] }));
|
|
66
|
+
}
|
|
67
|
+
export function isLaunchSystemLog(log) {
|
|
68
|
+
return log.kind === 'system' && /\bAgent\s+.+\slaunched\b|Terminal dédié ouvert|Dedicated terminal/i.test(log.text);
|
|
69
|
+
}
|
|
70
|
+
function AttachLaunchHeader({ item }) {
|
|
71
|
+
const { info } = item;
|
|
72
|
+
const mode = modeBadge(info.mode);
|
|
73
|
+
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: " " })] }));
|
|
74
|
+
}
|
|
75
|
+
function AttachResultCard({ item }) {
|
|
76
|
+
const st = STATE_META[item.info.state];
|
|
77
|
+
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 })] }));
|
|
42
78
|
}
|
|
43
79
|
export function AttachApp({ agentRef, sock }) {
|
|
44
80
|
const { exit } = useApp();
|
|
81
|
+
const { stdout } = useStdout();
|
|
45
82
|
const [info, setInfo] = useState(null);
|
|
46
83
|
const [others, setOthers] = useState([]);
|
|
84
|
+
const [launchCards, setLaunchCards] = useState([]);
|
|
85
|
+
const [resultCards, setResultCards] = useState([]);
|
|
47
86
|
const [lines, setLines] = useState([]);
|
|
87
|
+
const [timelineScroll, setTimelineScroll] = useState(0);
|
|
88
|
+
const [timelineFollowTail, setTimelineFollowTail] = useState(true);
|
|
48
89
|
const [approval, setApproval] = useState(null);
|
|
49
90
|
const [question, setQuestion] = useState(null);
|
|
50
91
|
const [gone, setGone] = useState(false);
|
|
@@ -52,6 +93,8 @@ export function AttachApp({ agentRef, sock }) {
|
|
|
52
93
|
const socketRef = useRef(null);
|
|
53
94
|
const keySeq = useRef(0);
|
|
54
95
|
const lastBellId = useRef('');
|
|
96
|
+
const launchRendered = useRef(false);
|
|
97
|
+
const renderedResultKey = useRef('');
|
|
55
98
|
useEffect(() => {
|
|
56
99
|
const socket = net.connect(sock);
|
|
57
100
|
socketRef.current = socket;
|
|
@@ -96,6 +139,21 @@ export function AttachApp({ agentRef, sock }) {
|
|
|
96
139
|
socket.destroy();
|
|
97
140
|
};
|
|
98
141
|
}, [agentRef, sock]);
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
if (!info || launchRendered.current)
|
|
144
|
+
return;
|
|
145
|
+
launchRendered.current = true;
|
|
146
|
+
setLaunchCards([{ key: ++keySeq.current, info }]);
|
|
147
|
+
}, [info?.id]);
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
if (!info || !TERMINAL_STATES.has(info.state) || !info.lastResult)
|
|
150
|
+
return;
|
|
151
|
+
const key = `${info.id}:${info.state}:${info.lastResult.length}:${info.lastResult.slice(0, 24)}`;
|
|
152
|
+
if (renderedResultKey.current === key)
|
|
153
|
+
return;
|
|
154
|
+
renderedResultKey.current = key;
|
|
155
|
+
setResultCards((prev) => [...prev, { key: ++keySeq.current, info: { ...info }, result: info.lastResult ?? '' }]);
|
|
156
|
+
}, [info?.id, info?.state, info?.lastResult]);
|
|
99
157
|
// Audible alert in THIS terminal when a new interaction arrives — the hub
|
|
100
158
|
// also rings, but the user may well be looking at the agent's terminal.
|
|
101
159
|
useEffect(() => {
|
|
@@ -123,7 +181,11 @@ export function AttachApp({ agentRef, sock }) {
|
|
|
123
181
|
setRaw((r) => !r);
|
|
124
182
|
return;
|
|
125
183
|
}
|
|
126
|
-
|
|
184
|
+
if (cmd.type === 'stop') {
|
|
185
|
+
wire({ type: 'stop', target: cmd.target || agentRef });
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
// /task|/ask|/plan|/review <text> — launch agent N+1 from this terminal.
|
|
127
189
|
if (cmd.type === 'spawn') {
|
|
128
190
|
wire({ type: 'spawn', text: cmd.text, mode: cmd.mode });
|
|
129
191
|
return;
|
|
@@ -136,26 +198,58 @@ export function AttachApp({ agentRef, sock }) {
|
|
|
136
198
|
};
|
|
137
199
|
const st = info ? STATE_META[info.state] : null;
|
|
138
200
|
const busy = info ? ['thinking', 'working', 'listening'].includes(info.state) : false;
|
|
201
|
+
const terminal = info ? TERMINAL_STATES.has(info.state) : false;
|
|
139
202
|
const interacting = Boolean(approval || question);
|
|
203
|
+
const logs = lines.map((l) => l.log);
|
|
204
|
+
const staticLines = raw ? lines : lines.filter((l) => l.log.kind !== 'llm' && !isLaunchSystemLog(l.log));
|
|
205
|
+
const timelineVisibleLogs = Math.max(8, (stdout?.rows ?? 30) - 14);
|
|
206
|
+
const maxTimelineScroll = Math.max(0, logs.length - timelineVisibleLogs);
|
|
207
|
+
const clampedTimelineScroll = Math.min(timelineScroll, maxTimelineScroll);
|
|
208
|
+
const timelineWindow = logs.slice(Math.max(0, logs.length - timelineVisibleLogs - clampedTimelineScroll), logs.length - clampedTimelineScroll);
|
|
209
|
+
const liveTimelineLogs = logs.slice(-Math.max(6, Math.min(14, Math.floor((stdout?.rows ?? 30) / 2))));
|
|
210
|
+
useEffect(() => {
|
|
211
|
+
if (timelineFollowTail)
|
|
212
|
+
setTimelineScroll(0);
|
|
213
|
+
}, [logs.length, timelineFollowTail]);
|
|
214
|
+
const scrollTimeline = (direction) => {
|
|
215
|
+
if (direction === 'up') {
|
|
216
|
+
setTimelineFollowTail(false);
|
|
217
|
+
setTimelineScroll((s) => Math.min(Math.min(s, maxTimelineScroll) + Math.max(1, Math.floor(timelineVisibleLogs / 2)), maxTimelineScroll));
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
setTimelineScroll((s) => {
|
|
221
|
+
const next = Math.max(0, Math.min(s, maxTimelineScroll) - Math.max(1, Math.floor(timelineVisibleLogs / 2)));
|
|
222
|
+
if (next === 0)
|
|
223
|
+
setTimelineFollowTail(true);
|
|
224
|
+
return next;
|
|
225
|
+
});
|
|
226
|
+
};
|
|
227
|
+
useInput((_input, key) => {
|
|
228
|
+
if (!busy || interacting || raw)
|
|
229
|
+
return;
|
|
230
|
+
if (key.pageUp)
|
|
231
|
+
scrollTimeline('up');
|
|
232
|
+
if (key.pageDown)
|
|
233
|
+
scrollTimeline('down');
|
|
234
|
+
}, { isActive: Boolean(busy && !interacting && !raw) });
|
|
140
235
|
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
|
|
236
|
+
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 ? (
|
|
237
|
+
/* While running, keep the native terminal scrollback stable: activity is
|
|
238
|
+
* appended once above via <Static>, and this live region stays tiny. */
|
|
239
|
+
_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, info.endedAt), " \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 }), busy && timelineFollowTail && liveTimelineLogs.length > 0 ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: UI.muted, bold: true, children: "Live activity" }), _jsx(Timeline, { logs: liveTimelineLogs, cols: process.stdout.columns || 100 })] })) : null, 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
240
|
.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 ? (
|
|
241
|
+
.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] })) : (
|
|
242
|
+
/* FULL panel for idle/waiting/interactions — terminal states stay compact. */
|
|
243
|
+
_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, info.endedAt), " \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
244
|
// The session's shared awareness, visible here too: what the
|
|
151
245
|
// OTHER agents are doing right now (live, same feed the agents get).
|
|
152
246
|
_jsxs(Text, { color: UI.muted, wrap: "truncate-end", children: ["Others ", ' ', others
|
|
153
247
|
.map((o) => `${o.name} [${stateLabel(o.state)}] ${truncate(o.currentAction || o.task, 40)}`)
|
|
154
|
-
.join(' · ')] })) : null,
|
|
248
|
+
.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
249
|
wire({ type: 'approve', id, approved: ok, always: !!always });
|
|
156
250
|
setApproval(null);
|
|
157
251
|
} })) : question ? (_jsx(QuestionPrompt, { question: { ...question, agentId: info?.id ?? '', resolve: noop }, pendingCount: 1, onAnswer: (id, answer) => {
|
|
158
252
|
wire({ type: 'answer', id, text: answer });
|
|
159
253
|
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] : [], 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) }) })] }));
|
|
254
|
+
} }, 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
255
|
}
|
package/dist/ui/CommandInput.js
CHANGED
|
@@ -32,6 +32,8 @@ function modeHint(value, context, targetAgent) {
|
|
|
32
32
|
return 'Task mode · execute, edit, validate';
|
|
33
33
|
if (v.startsWith('/plan') || v === '/p')
|
|
34
34
|
return 'Plan mode · asks before editing';
|
|
35
|
+
if (v.startsWith('/review'))
|
|
36
|
+
return 'Review mode · verdict, risks, tests';
|
|
35
37
|
return '↑/↓ select · Enter accept';
|
|
36
38
|
}
|
|
37
39
|
export function bestCommandCompletion(value) {
|
|
@@ -41,7 +43,7 @@ export function bestCommandCompletion(value) {
|
|
|
41
43
|
export function commandNamesForContext(context) {
|
|
42
44
|
if (context !== 'attach')
|
|
43
45
|
return undefined;
|
|
44
|
-
return ['/ask', '/a', '/task', '/t', '/plan', '/p', '/send', '/raw', '/quit', '/exit', '/detach'];
|
|
46
|
+
return ['/ask', '/a', '/task', '/t', '/plan', '/p', '/review', '/send', '/stop', '/raw', '/quit', '/exit', '/detach'];
|
|
45
47
|
}
|
|
46
48
|
export function agentArgCommand(value) {
|
|
47
49
|
const m = value.match(/^(\/\S+)\s+([^\s]*)$/);
|
|
@@ -79,7 +81,7 @@ export function wrappedPromptLines(text, width) {
|
|
|
79
81
|
function paintLine(text, width) {
|
|
80
82
|
return text.length >= width ? text.slice(0, width) : text.padEnd(width, ' ');
|
|
81
83
|
}
|
|
82
|
-
export function CommandInput({ active, placeholder, mask, context = 'hub', targetAgent, modelLabel, commandNames, agentNames = [], agents = [], width, onHeightChange, onSubmit, onEscape, notify, }) {
|
|
84
|
+
export function CommandInput({ active, placeholder, mask, context = 'hub', targetAgent, modelLabel, commandNames, agentNames = [], agents = [], width, onHeightChange, onIdleNavigation, onSubmit, onEscape, notify, }) {
|
|
83
85
|
const [value, setValue] = useState('');
|
|
84
86
|
const [attachments, setAttachments] = useState([]);
|
|
85
87
|
const [history, setHistory] = useState([]);
|
|
@@ -236,6 +238,10 @@ export function CommandInput({ active, placeholder, mask, context = 'hub', targe
|
|
|
236
238
|
setSelectedSuggestion((i) => clampSuggestionIndex(i - 1, suggestionCount));
|
|
237
239
|
return;
|
|
238
240
|
}
|
|
241
|
+
if (!value && attachments.length === 0 && onIdleNavigation) {
|
|
242
|
+
onIdleNavigation('up');
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
239
245
|
setHistIdx((i) => {
|
|
240
246
|
const ni = i === -1 ? history.length - 1 : Math.max(0, i - 1);
|
|
241
247
|
if (history[ni] !== undefined)
|
|
@@ -249,6 +255,10 @@ export function CommandInput({ active, placeholder, mask, context = 'hub', targe
|
|
|
249
255
|
setSelectedSuggestion((i) => clampSuggestionIndex(i + 1, suggestionCount));
|
|
250
256
|
return;
|
|
251
257
|
}
|
|
258
|
+
if (!value && attachments.length === 0 && onIdleNavigation) {
|
|
259
|
+
onIdleNavigation('down');
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
252
262
|
setHistIdx((i) => {
|
|
253
263
|
if (i === -1)
|
|
254
264
|
return -1;
|
package/dist/ui/Timeline.js
CHANGED
|
@@ -20,6 +20,8 @@ function itemColor(item) {
|
|
|
20
20
|
return item.category === 'change' ? UI.warn : UI.muted;
|
|
21
21
|
if (item.category === 'coordinate')
|
|
22
22
|
return UI.note;
|
|
23
|
+
if (item.label === 'next')
|
|
24
|
+
return UI.accent;
|
|
23
25
|
if (item.kind === 'thought')
|
|
24
26
|
return UI.muted;
|
|
25
27
|
if (item.kind === 'narration')
|
|
@@ -43,6 +45,9 @@ function TimelineRow({ item, cols }) {
|
|
|
43
45
|
if (item.kind === 'command') {
|
|
44
46
|
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 ?? '', max) })] }), _jsx(OutputLines, { item: item, cols: cols })] }));
|
|
45
47
|
}
|
|
48
|
+
if (item.label === 'next') {
|
|
49
|
+
return (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: UI.accent, wrap: "wrap", children: [_jsx(Text, { color: UI.muted, children: "\u2192 " }), truncate(item.detail ?? '', max)] }) }));
|
|
50
|
+
}
|
|
46
51
|
if (item.kind === 'files') {
|
|
47
52
|
const files = item.files ?? [];
|
|
48
53
|
const shown = files.slice(0, 5).join(', ');
|
package/dist/ui/events.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { oneLine } from './tokens.js';
|
|
2
|
+
import { t } from '../i18n.js';
|
|
2
3
|
function cleanToolText(text) {
|
|
3
4
|
return oneLine(text)
|
|
4
|
-
.replace(/^[
|
|
5
|
+
.replace(/^[📖📁🔍🔎📚✏🚩🧠🧩📢☑⏳❓✉✅↳]+\s*/u, '')
|
|
5
6
|
.trim();
|
|
6
7
|
}
|
|
7
8
|
function stripShellNoise(command) {
|
|
@@ -19,6 +20,12 @@ function classify(log) {
|
|
|
19
20
|
if (log.kind === 'tool_result') {
|
|
20
21
|
return { agentId: log.agentId, kind: 'command_output', label: 'output', detail: log.text.trim(), ts: log.ts, seq: log.seq };
|
|
21
22
|
}
|
|
23
|
+
if (log.kind === 'tool' && /^\s*📢/u.test(log.text)) {
|
|
24
|
+
return { agentId: log.agentId, kind: 'intent', label: 'next', detail: cleaned || text, ts: log.ts, seq: log.seq };
|
|
25
|
+
}
|
|
26
|
+
if (log.kind === 'tool' && /^\s*☑/u.test(log.text)) {
|
|
27
|
+
return { agentId: log.agentId, kind: 'note', label: 'steps', detail: cleaned || text, ts: log.ts, seq: log.seq };
|
|
28
|
+
}
|
|
22
29
|
if (log.kind === 'note')
|
|
23
30
|
return { agentId: log.agentId, kind: 'note', label: 'note', detail: cleaned || text, ts: log.ts, seq: log.seq };
|
|
24
31
|
if (log.kind === 'system')
|
|
@@ -44,8 +51,14 @@ function classify(log) {
|
|
|
44
51
|
if (/^(search)\s+/i.test(cleaned)) {
|
|
45
52
|
return { agentId: log.agentId, kind: 'file', label: 'search', detail: cleaned.replace(/^search\s+/i, ''), ts: log.ts, seq: log.seq };
|
|
46
53
|
}
|
|
47
|
-
if (/^
|
|
48
|
-
|
|
54
|
+
if (/^inspect project/i.test(cleaned)) {
|
|
55
|
+
return { agentId: log.agentId, kind: 'file', label: 'search', detail: 'project', ts: log.ts, seq: log.seq };
|
|
56
|
+
}
|
|
57
|
+
if (/^(claim|claims?):?\s+/i.test(cleaned)) {
|
|
58
|
+
return { agentId: log.agentId, kind: 'note', label: 'claim', detail: cleaned.replace(/^(claim|claims?):?\s*/i, ''), ts: log.ts, seq: log.seq };
|
|
59
|
+
}
|
|
60
|
+
if (/^(write|edit|patch)\s+/i.test(cleaned)) {
|
|
61
|
+
const label = lower.startsWith('write') ? 'write' : 'edit';
|
|
49
62
|
return { agentId: log.agentId, kind: 'file', label, detail: cleaned.replace(/^(write|edit|patch|claim|claims?)\s*/i, ''), ts: log.ts, seq: log.seq };
|
|
50
63
|
}
|
|
51
64
|
if (/^(run|exec|shell|npm|pnpm|yarn|git|node|npx)\b/i.test(cleaned)) {
|
|
@@ -105,6 +118,8 @@ function categoryFor(e) {
|
|
|
105
118
|
return 'result';
|
|
106
119
|
if (e.kind === 'note' || e.kind === 'approval' || e.kind === 'question')
|
|
107
120
|
return 'coordinate';
|
|
121
|
+
if (e.kind === 'intent')
|
|
122
|
+
return 'other';
|
|
108
123
|
if (e.kind === 'file') {
|
|
109
124
|
if (e.label === 'write' || e.label === 'edit' || e.label === 'claim')
|
|
110
125
|
return 'change';
|
|
@@ -141,20 +156,8 @@ export function summarizeCommandOutput(output, command = '', maxLines = 6) {
|
|
|
141
156
|
}
|
|
142
157
|
function narrationFor(category, previous) {
|
|
143
158
|
if (category === 'inspect' && previous === 'validate')
|
|
144
|
-
return '
|
|
145
|
-
|
|
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.';
|
|
159
|
+
return t('timeline.narration.inspectAfterValidate');
|
|
160
|
+
return t(`timeline.narration.${category}`);
|
|
158
161
|
}
|
|
159
162
|
function pushSection(out, category, ts, seq) {
|
|
160
163
|
const prev = [...out].reverse().find((i) => i.kind !== 'section');
|
package/dist/ui/theme.js
CHANGED
|
@@ -15,8 +15,8 @@ export const STATE_LABEL = {
|
|
|
15
15
|
export function stateLabel(state) {
|
|
16
16
|
return t(STATE_LABEL[state].labelKey);
|
|
17
17
|
}
|
|
18
|
-
export function elapsed(since) {
|
|
19
|
-
const s = Math.floor((
|
|
18
|
+
export function elapsed(since, until = Date.now()) {
|
|
19
|
+
const s = Math.max(0, Math.floor((until - since) / 1000));
|
|
20
20
|
if (s < 60)
|
|
21
21
|
return `${s}s`;
|
|
22
22
|
const m = Math.floor(s / 60);
|
package/dist/ui/tokens.js
CHANGED
|
@@ -48,11 +48,11 @@ export const STATE = {
|
|
|
48
48
|
waiting: 'yellow',
|
|
49
49
|
idle: 'gray',
|
|
50
50
|
};
|
|
51
|
-
/** Mode indicator colors.
|
|
51
|
+
/** Mode indicator colors. */
|
|
52
52
|
export const MODE = {
|
|
53
53
|
ask: 'yellow',
|
|
54
54
|
plan: COLOR.creamMuted,
|
|
55
|
-
task:
|
|
55
|
+
task: COLOR.cream,
|
|
56
56
|
};
|
|
57
57
|
/** Chrome / UI element colors. */
|
|
58
58
|
export const CHROME = {
|
package/dist/ui/views.js
CHANGED
|
@@ -73,6 +73,13 @@ function useScrollWindow(items, visible, anchor = 'top') {
|
|
|
73
73
|
}
|
|
74
74
|
const Above = ({ n }) => (n > 0 ? _jsxs(Text, { color: "gray", children: ["\u25B2 ", n, " \u00B7 PgUp"] }) : null);
|
|
75
75
|
const Below = ({ n }) => (n > 0 ? _jsxs(Text, { color: "gray", children: ["\u25BC ", n, " \u00B7 PgDn"] }) : null);
|
|
76
|
+
function shortTime(ts) {
|
|
77
|
+
const seconds = Math.max(0, Math.round((Date.now() - ts) / 1000));
|
|
78
|
+
if (seconds < 60)
|
|
79
|
+
return t('board.secondsAgo', { n: seconds });
|
|
80
|
+
const minutes = Math.round(seconds / 60);
|
|
81
|
+
return t('board.minutesAgo', { n: minutes });
|
|
82
|
+
}
|
|
76
83
|
/** Usable rows for a view's list, from the REAL terminal height. */
|
|
77
84
|
function useVisibleRows(overhead, min = 6) {
|
|
78
85
|
const { stdout } = useStdout();
|
|
@@ -81,13 +88,17 @@ function useVisibleRows(overhead, min = 6) {
|
|
|
81
88
|
export function BoardView({ board, bodyHeight }) {
|
|
82
89
|
const agents = [...board.agents.values()];
|
|
83
90
|
const fallbackVisible = useVisibleRows(12);
|
|
84
|
-
const visibleAgents = bodyHeight ? Math.max(1, Math.floor((bodyHeight -
|
|
91
|
+
const visibleAgents = bodyHeight ? Math.max(1, Math.floor((bodyHeight - 10) / 3)) : fallbackVisible;
|
|
85
92
|
const { slice: agentSlice, above, below } = useScrollWindow(agents, visibleAgents, 'top');
|
|
86
|
-
const sideRows = bodyHeight ? Math.max(1, Math.floor((bodyHeight - visibleAgents -
|
|
93
|
+
const sideRows = bodyHeight ? Math.max(1, Math.floor((bodyHeight - visibleAgents - 8) / 2)) : 8;
|
|
87
94
|
const activities = [...board.fileActivity.values()].sort((a, b) => b.ts - a.ts).slice(0, sideRows);
|
|
88
95
|
const notes = board.notes.slice(-sideRows);
|
|
89
96
|
const warnings = board.workMapWarnings.slice(-Math.max(2, Math.min(4, sideRows)));
|
|
90
|
-
return (_jsxs(Box, { borderStyle: "round", borderColor:
|
|
97
|
+
return (_jsxs(Box, { borderStyle: "round", borderColor: BRAND.muted, flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: BRAND.primary, children: t('board.title') }), _jsx(Text, { bold: true, color: warnings.length > 0 ? COLOR.cream : BRAND.primary, children: t('board.workMap') }), warnings.length > 0 ? (warnings.map((w) => (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { wrap: "truncate-end", children: [' ', _jsxs(Text, { color: w.level === 'conflict' ? 'redBright' : 'yellow', children: [w.level === 'conflict' ? '!' : '⚠', " "] }), _jsx(Text, { color: BRAND.primary, children: w.title }), _jsxs(Text, { color: "gray", children: [" \u2014 ", truncate(w.detail, 100)] })] }), _jsxs(Text, { color: "gray", wrap: "truncate-end", children: [' ', t('board.warningMeta', {
|
|
98
|
+
agents: w.agentNames.join(', ') || 'agents',
|
|
99
|
+
paths: w.paths.join(', ') || 'paths',
|
|
100
|
+
time: shortTime(w.ts),
|
|
101
|
+
})] })] }, w.id)))) : (_jsxs(Text, { color: "gray", children: [" ", t('board.workMapOk')] })), warnings.length > 0 ? _jsxs(Text, { color: COLOR.creamMuted, children: [" ", t('board.warningSuggestion')] }) : null, _jsx(Text, { bold: true, children: t('board.agents') }), agents.length === 0 ? (_jsxs(Text, { color: "gray", children: [" ", t('board.none')] })) : (_jsxs(_Fragment, { children: [_jsx(Above, { n: above }), agentSlice.map((a) => (_jsxs(Text, { wrap: "truncate-end", children: [' ', _jsx(Text, { color: a.color, bold: true, children: a.name }), _jsxs(Text, { color: STATE_LABEL[a.state].color, children: [' ', STATE_LABEL[a.state].icon, " ", stateLabel(a.state)] }), _jsxs(Text, { color: "gray", children: [" ", truncate(a.currentAction || a.task, 80)] }), a.claims && a.claims.length > 0 ? _jsxs(Text, { color: COLOR.cream, children: [" \u00B7 \u2691 ", truncate(a.claims.join(', '), 45)] }) : null] }, a.id))), _jsx(Below, { n: below })] })), _jsx(Text, { bold: true, children: t('board.activity') }), activities.length === 0 ? (_jsxs(Text, { color: "gray", children: [" ", t('board.noActivity')] })) : (activities.map((act) => (_jsxs(Text, { wrap: "truncate-end", children: [' ', "\u270F ", act.path, " ", _jsxs(Text, { color: "gray", children: ["\u2014 ", act.agentName, " (", act.op, ", ", Math.round((Date.now() - act.ts) / 1000), "s)"] })] }, act.path)))), _jsx(Text, { bold: true, children: t('board.notes') }), notes.map((n) => (_jsxs(Text, { wrap: "truncate-end", children: [' ', _jsxs(Text, { color: "magenta", children: [n.from, " \u2192 ", n.to] }), _jsxs(Text, { children: [": ", truncate(n.content, 140)] })] }, n.id)))] }));
|
|
91
102
|
}
|
|
92
103
|
export function NotesView({ board, bodyHeight }) {
|
|
93
104
|
const fallbackVisible = useVisibleRows(7);
|
|
@@ -102,7 +113,8 @@ export function DiffView({ board, bodyHeight }) {
|
|
|
102
113
|
const rows = bodyHeight ? Math.max(8, bodyHeight - 4) : fallbackRows;
|
|
103
114
|
const perChange = Math.max(1, Math.floor(rows / 34));
|
|
104
115
|
const { slice: changes, above, below } = useScrollWindow(board.changes, perChange, 'bottom');
|
|
105
|
-
|
|
116
|
+
const warnings = board.workMapWarnings.filter((w) => w.level !== 'info').slice(-2);
|
|
117
|
+
return (_jsxs(Box, { borderStyle: "round", borderColor: "green", flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: "green", children: t('diff.title', { total: board.changes.length }) }), warnings.map((w) => (_jsxs(Text, { color: w.level === 'conflict' ? 'redBright' : 'yellow', wrap: "truncate-end", children: ["\u26A0 ", w.title, ": ", truncate(w.paths.join(', ') || w.detail, 110)] }, w.id))), board.changes.length === 0 ? (_jsx(Text, { color: "gray", children: t('diff.empty') })) : (_jsxs(_Fragment, { children: [_jsx(Above, { n: above }), changes.map((c) => {
|
|
106
118
|
const patch = Diff.createPatch(c.path, c.before, c.after, '', '', { context: 2 });
|
|
107
119
|
const lines = patch.split('\n').slice(4, 34);
|
|
108
120
|
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { bold: true, children: [_jsx(Text, { color: BRAND.primary, children: c.path }), _jsxs(Text, { color: "gray", children: [' ', t('diff.by', { agent: c.agentName, time: new Date(c.ts).toLocaleTimeString() })] })] }), lines.map((l, i) => (_jsx(Text, { color: l.startsWith('+') ? 'green' : l.startsWith('-') ? 'red' : l.startsWith('@') ? BRAND.primary : 'gray', wrap: "truncate-end", children: l || ' ' }, i))), patch.split('\n').length > 38 ? _jsx(Text, { color: "gray", children: t('diff.trunc') }) : null] }, c.id));
|
|
@@ -159,7 +171,7 @@ export function HelpView({ bodyHeight, onSelect }) {
|
|
|
159
171
|
const above = start;
|
|
160
172
|
const below = Math.max(0, commands.length - start - slice.length);
|
|
161
173
|
const highlights = [
|
|
162
|
-
['Agent modes', ['/ask', '/task', '/plan']],
|
|
174
|
+
['Agent modes', ['/ask', '/task', '/plan', '/review']],
|
|
163
175
|
['Shell approvals', ['/approvals ask', '/approvals auto', '/approvals yolo']],
|
|
164
176
|
['Navigation', ['/focus', '/attach', '/raw', '/send']],
|
|
165
177
|
];
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export const PACKAGE_NAME = '@parallel-cli/parallel';
|
|
2
|
-
export const VERSION = '0.4.
|
|
2
|
+
export const VERSION = '0.4.7';
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parallel-cli/parallel",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"description": "Real-time
|
|
3
|
+
"version": "0.4.8",
|
|
4
|
+
"description": "Real-time coding agents that work like a live team on one shared repository.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
7
7
|
"ai",
|