@parallel-cli/parallel 0.4.6 → 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 +28 -0
- package/README.md +58 -11
- package/dist/agents/agent.js +98 -14
- package/dist/agents/tools.js +218 -20
- package/dist/commands.js +52 -0
- package/dist/controller.js +23 -13
- package/dist/coordination/blackboard.js +37 -1
- package/dist/i18n.js +69 -13
- package/dist/index.js +1 -0
- package/dist/ui/AgentPanel.js +87 -25
- package/dist/ui/App.js +11 -6
- package/dist/ui/AttachApp.js +99 -14
- package/dist/ui/CommandInput.js +12 -2
- package/dist/ui/Timeline.js +5 -0
- package/dist/ui/events.js +20 -17
- 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)
|
|
@@ -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] : [], 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) }) })] }));
|
|
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
|
@@ -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', '/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/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.7",
|
|
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",
|