@parallel-cli/parallel 0.4.7 → 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 +14 -0
- package/README.md +2 -1
- package/dist/agents/agent.js +78 -15
- package/dist/commands.js +1 -0
- package/dist/controller.js +3 -0
- package/dist/coordination/blackboard.js +7 -1
- package/dist/server.js +9 -0
- package/dist/ui/AgentPanel.js +21 -9
- package/dist/ui/App.js +3 -1
- package/dist/ui/AttachApp.js +12 -3
- package/dist/ui/CommandInput.js +1 -1
- package/dist/ui/theme.js +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to Parallel are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.4.8 - 2026-06-24
|
|
6
|
+
|
|
7
|
+
### 0.4.8 Changed
|
|
8
|
+
|
|
9
|
+
- Added clearer attached-terminal control with `/stop`, visible stop hints for active agents, and command palette support in attached terminals.
|
|
10
|
+
- Made hidden Hub progress explicit by showing `+N steps` with direct `full /focus aN` and `term /attach aN` shortcuts when rows are truncated.
|
|
11
|
+
- Froze elapsed-time telemetry once agents reach `done`, `error`, or `stopped` so finished agents no longer keep counting.
|
|
12
|
+
|
|
13
|
+
### 0.4.8 Fixed
|
|
14
|
+
|
|
15
|
+
- Fixed OpenAI-compatible `tool_calls` history failures by recording assistant tool calls and all matching tool results atomically, even when a task completes early or an agent is stopped.
|
|
16
|
+
- Repaired restored conversations with missing tool results before the next model call to prevent 400 errors after interrupted runs.
|
|
17
|
+
- Restored a bounded live activity timeline in attached terminals while preserving append-only native scrollback for final results.
|
|
18
|
+
|
|
5
19
|
## 0.4.7 - 2026-06-24
|
|
6
20
|
|
|
7
21
|
### 0.4.7 Added
|
package/README.md
CHANGED
|
@@ -167,7 +167,7 @@ Input has three explicit contexts:
|
|
|
167
167
|
|
|
168
168
|
- Hub: plain text launches a new `/task` agent. Slash suggestions show hub commands and agent arguments autocomplete for `/focus`, `/send`, `/attach`, `/pause`, `/resume`, `/stop`, `/restore`, and `/commit`.
|
|
169
169
|
- Focus: after `/focus a1`, plain text talks to the focused agent instead of spawning a new one. `/raw` affects this view only.
|
|
170
|
-
- Attach: in `parallel attach a1`, the same minimal prompt UI steers the attached agent. `/task`, `/ask`, and `/plan` spawn new agents from that terminal, while `@all ...`, `@a2 ...`, and `/send ...` route instructions through the main session.
|
|
170
|
+
- Attach: in `parallel attach a1`, the same minimal prompt UI steers the attached agent. `/stop` stops the attached agent, `/task`, `/ask`, and `/plan` spawn new agents from that terminal, while `@all ...`, `@a2 ...`, and `/send ...` route instructions through the main session.
|
|
171
171
|
|
|
172
172
|
Use `Name: task` when naming an agent:
|
|
173
173
|
|
|
@@ -237,6 +237,7 @@ plain text sends a message to this agent
|
|
|
237
237
|
/ask Reviewer: is this result safe to merge?
|
|
238
238
|
/plan Migration: prepare a migration plan
|
|
239
239
|
/review all before commit
|
|
240
|
+
/stop
|
|
240
241
|
/raw
|
|
241
242
|
/quit
|
|
242
243
|
```
|
package/dist/agents/agent.js
CHANGED
|
@@ -225,6 +225,40 @@ export class Agent {
|
|
|
225
225
|
await new Promise((r) => setTimeout(r, 300));
|
|
226
226
|
}
|
|
227
227
|
}
|
|
228
|
+
repairToolCallHistory() {
|
|
229
|
+
const repaired = [];
|
|
230
|
+
for (let i = 0; i < this.history.length; i++) {
|
|
231
|
+
const msg = this.history[i];
|
|
232
|
+
if (msg.role === 'tool') {
|
|
233
|
+
// Orphan tool messages make OpenAI-compatible APIs reject the whole
|
|
234
|
+
// request. Valid tool messages are consumed immediately after their
|
|
235
|
+
// assistant tool_calls block below.
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
repaired.push(this.history[i]);
|
|
239
|
+
const toolCalls = msg.role === 'assistant' && Array.isArray(msg.tool_calls) ? msg.tool_calls : [];
|
|
240
|
+
if (toolCalls.length === 0)
|
|
241
|
+
continue;
|
|
242
|
+
const seen = new Set();
|
|
243
|
+
while (i + 1 < this.history.length && this.history[i + 1].role === 'tool') {
|
|
244
|
+
const toolMsg = this.history[++i];
|
|
245
|
+
if (toolMsg.tool_call_id)
|
|
246
|
+
seen.add(String(toolMsg.tool_call_id));
|
|
247
|
+
repaired.push(toolMsg);
|
|
248
|
+
}
|
|
249
|
+
for (const tc of toolCalls) {
|
|
250
|
+
const id = String(tc.id ?? '');
|
|
251
|
+
if (!id || seen.has(id))
|
|
252
|
+
continue;
|
|
253
|
+
repaired.push({
|
|
254
|
+
role: 'tool',
|
|
255
|
+
tool_call_id: id,
|
|
256
|
+
content: 'Skipped: missing tool result repaired before the next model call.',
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
this.history = repaired;
|
|
261
|
+
}
|
|
228
262
|
updatePerf(delta) {
|
|
229
263
|
const current = this.board.agents.get(this.id)?.perf ?? EMPTY_PERF;
|
|
230
264
|
this.board.updateAgent(this.id, {
|
|
@@ -340,6 +374,7 @@ export class Agent {
|
|
|
340
374
|
if (this.stopped)
|
|
341
375
|
break;
|
|
342
376
|
}
|
|
377
|
+
this.repairToolCallHistory();
|
|
343
378
|
const messages = [
|
|
344
379
|
...this.history,
|
|
345
380
|
{ role: 'user', content: live.text },
|
|
@@ -385,15 +420,15 @@ export class Agent {
|
|
|
385
420
|
});
|
|
386
421
|
}
|
|
387
422
|
const msg = res.message;
|
|
388
|
-
// Persist this round into history (live context is NOT kept — rebuilt fresh each turn).
|
|
389
|
-
this.record({ role: 'user', content: '[real-time state consulted]' });
|
|
390
|
-
this.record(msg);
|
|
391
423
|
if (msg.content && msg.content.trim()) {
|
|
392
424
|
// "✻" marks thinking/commentary steps — visually distinct from tool lines.
|
|
393
425
|
this.board.log(this.id, 'llm', `✻ ${msg.content.trim().slice(0, 500)}`);
|
|
394
426
|
}
|
|
395
427
|
const toolCalls = msg.tool_calls ?? [];
|
|
396
428
|
if (toolCalls.length === 0) {
|
|
429
|
+
// Persist this round into history (live context is NOT kept — rebuilt fresh each turn).
|
|
430
|
+
this.record({ role: 'user', content: '[real-time state consulted]' });
|
|
431
|
+
this.record(msg);
|
|
397
432
|
this.record({
|
|
398
433
|
role: 'user',
|
|
399
434
|
content: 'No tool was called. If your task is finished and verified, call task_complete. Otherwise, continue with tool calls.',
|
|
@@ -402,21 +437,33 @@ export class Agent {
|
|
|
402
437
|
}
|
|
403
438
|
this.board.setAgentState(this.id, 'working');
|
|
404
439
|
let completed = false;
|
|
405
|
-
|
|
406
|
-
|
|
440
|
+
const toolResults = [];
|
|
441
|
+
const postToolMessages = [];
|
|
442
|
+
const addToolResult = (toolCallId, content) => {
|
|
443
|
+
toolResults.push({ role: 'tool', tool_call_id: toolCallId, content });
|
|
444
|
+
};
|
|
445
|
+
const addSkippedToolResults = (startIndex, content) => {
|
|
446
|
+
for (const remaining of toolCalls.slice(startIndex)) {
|
|
447
|
+
if (remaining.id)
|
|
448
|
+
addToolResult(remaining.id, content);
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
for (let i = 0; i < toolCalls.length; i++) {
|
|
452
|
+
const tc = toolCalls[i];
|
|
453
|
+
if (this.stopped) {
|
|
454
|
+
addSkippedToolResults(i, 'Skipped: the agent was stopped before this tool call executed.');
|
|
407
455
|
break;
|
|
408
|
-
|
|
456
|
+
}
|
|
457
|
+
if (tc.type !== 'function') {
|
|
458
|
+
addToolResult(tc.id, 'ERROR: unsupported tool call type.');
|
|
409
459
|
continue;
|
|
460
|
+
}
|
|
410
461
|
let args = {};
|
|
411
462
|
try {
|
|
412
463
|
args = tc.function.arguments ? JSON.parse(tc.function.arguments) : {};
|
|
413
464
|
}
|
|
414
465
|
catch {
|
|
415
|
-
|
|
416
|
-
role: 'tool',
|
|
417
|
-
tool_call_id: tc.id,
|
|
418
|
-
content: 'ERROR: invalid JSON arguments.',
|
|
419
|
-
});
|
|
466
|
+
addToolResult(tc.id, 'ERROR: invalid JSON arguments.');
|
|
420
467
|
continue;
|
|
421
468
|
}
|
|
422
469
|
const label = this.describeCall(tc.function.name, args);
|
|
@@ -428,7 +475,13 @@ export class Agent {
|
|
|
428
475
|
}
|
|
429
476
|
this.board.updateAgent(this.id, { currentAction: label.slice(0, 80) });
|
|
430
477
|
const shellStartedAt = tc.function.name === 'run_command' ? Date.now() : 0;
|
|
431
|
-
|
|
478
|
+
let result;
|
|
479
|
+
try {
|
|
480
|
+
result = await this.executor.execute(tc.function.name, args);
|
|
481
|
+
}
|
|
482
|
+
catch (err) {
|
|
483
|
+
result = `ERROR: ${err?.message ?? String(err)}`;
|
|
484
|
+
}
|
|
432
485
|
const shellMs = shellStartedAt ? Date.now() - shellStartedAt : 0;
|
|
433
486
|
const readOnlyShell = tc.function.name === 'run_command' && isReadOnlyShell(String(args.command ?? ''));
|
|
434
487
|
this.updatePerf({
|
|
@@ -440,7 +493,7 @@ export class Agent {
|
|
|
440
493
|
if (readOnlyShell) {
|
|
441
494
|
this.readOnlyShellStreak++;
|
|
442
495
|
if (this.readOnlyShellStreak >= 3) {
|
|
443
|
-
|
|
496
|
+
postToolMessages.push({
|
|
444
497
|
role: 'user',
|
|
445
498
|
content: '[PERFORMANCE CORRECTION] You are using several read-only shell micro-commands. Batch the next inspection with read_many/inspect_project or a single labelled shell command, then continue.',
|
|
446
499
|
});
|
|
@@ -465,11 +518,21 @@ export class Agent {
|
|
|
465
518
|
// is rendered as the agent's recap) — no duplicated walls of text.
|
|
466
519
|
const headline = summary.split('\n').find((l) => l.trim())?.trim() ?? 'Task complete.';
|
|
467
520
|
this.board.addNote(this.name, 'all', `✅ ${headline.slice(0, 160)}`);
|
|
468
|
-
|
|
521
|
+
addToolResult(tc.id, 'OK, task closed.');
|
|
522
|
+
addSkippedToolResults(i + 1, 'Skipped: task_complete closed the task before this tool call executed.');
|
|
469
523
|
break;
|
|
470
524
|
}
|
|
471
|
-
|
|
525
|
+
addToolResult(tc.id, result);
|
|
472
526
|
}
|
|
527
|
+
// OpenAI-compatible APIs require assistant tool_calls to be followed
|
|
528
|
+
// immediately by one tool result per tool_call_id. Keep this block
|
|
529
|
+
// atomic so aborted/skipped tools never poison a later turn or restore.
|
|
530
|
+
this.record({ role: 'user', content: '[real-time state consulted]' });
|
|
531
|
+
this.record(msg);
|
|
532
|
+
for (const toolResult of toolResults)
|
|
533
|
+
this.record(toolResult);
|
|
534
|
+
for (const postToolMessage of postToolMessages)
|
|
535
|
+
this.record(postToolMessage);
|
|
473
536
|
if (completed) {
|
|
474
537
|
this.board.setAgentState(this.id, 'done', 'done ✅');
|
|
475
538
|
return;
|
package/dist/commands.js
CHANGED
package/dist/controller.js
CHANGED
|
@@ -647,11 +647,14 @@ export class Controller extends EventEmitter {
|
|
|
647
647
|
tokensIn: a.tokensIn,
|
|
648
648
|
tokensOut: a.tokensOut,
|
|
649
649
|
cost: a.cost,
|
|
650
|
+
endedAt: a.endedAt,
|
|
650
651
|
providerName,
|
|
651
652
|
model: a.model,
|
|
652
653
|
specialist: a.specialist,
|
|
653
654
|
claims: a.claims,
|
|
654
655
|
ctxPct: a.ctxPct,
|
|
656
|
+
progressSteps: a.progressSteps,
|
|
657
|
+
perf: a.perf,
|
|
655
658
|
conversation: this.conversationFiles.get(a.id),
|
|
656
659
|
})),
|
|
657
660
|
notes: this.board.notes.slice(-200),
|
|
@@ -58,8 +58,14 @@ export class Blackboard extends EventEmitter {
|
|
|
58
58
|
if (action !== undefined)
|
|
59
59
|
a.currentAction = action;
|
|
60
60
|
// A finished agent no longer holds any declared work area.
|
|
61
|
-
if (state === 'done' || state === 'stopped' || state === 'error')
|
|
61
|
+
if (state === 'done' || state === 'stopped' || state === 'error') {
|
|
62
62
|
a.claims = undefined;
|
|
63
|
+
if (!a.endedAt)
|
|
64
|
+
a.endedAt = Date.now();
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
a.endedAt = undefined;
|
|
68
|
+
}
|
|
63
69
|
if (prev !== state)
|
|
64
70
|
this.emit('agent-event', { type: 'state', id, state, prev });
|
|
65
71
|
this.touch();
|
package/dist/server.js
CHANGED
|
@@ -118,6 +118,15 @@ export function startSessionServer(ctl) {
|
|
|
118
118
|
else
|
|
119
119
|
ctl.sendToAgent(target, text);
|
|
120
120
|
}
|
|
121
|
+
else if (msg.type === 'stop' && typeof msg.target === 'string') {
|
|
122
|
+
const target = msg.target.trim();
|
|
123
|
+
if (!target)
|
|
124
|
+
continue;
|
|
125
|
+
if (target.toLowerCase() === 'all')
|
|
126
|
+
ctl.stopAll();
|
|
127
|
+
else
|
|
128
|
+
ctl.stopAgent(target);
|
|
129
|
+
}
|
|
121
130
|
else if (msg.type === 'spawn' && typeof msg.text === 'string') {
|
|
122
131
|
// Agent N+1 can be launched from ANY terminal of the session —
|
|
123
132
|
// its own dedicated terminal then opens automatically.
|
package/dist/ui/AgentPanel.js
CHANGED
|
@@ -29,7 +29,8 @@ export function cleanHubSummary(text) {
|
|
|
29
29
|
export function formatAgentTelemetry(agent) {
|
|
30
30
|
const ctx = agent.ctxPct !== undefined ? ` · ${agent.ctxPct}% ctx` : '';
|
|
31
31
|
const perf = agent.perf ? ` · ${agent.perf.modelTurns}t/${agent.perf.toolCalls} tools` : '';
|
|
32
|
-
|
|
32
|
+
const runtime = agent.endedAt ? `ended ${elapsed(agent.startedAt, agent.endedAt)}` : elapsed(agent.startedAt);
|
|
33
|
+
return `${runtime}${ctx}${perf} · ${agent.cost === null ? '$-' : fmtCost(agent.cost)}`;
|
|
33
34
|
}
|
|
34
35
|
function firstSectionLine(text, labels) {
|
|
35
36
|
const lines = text.replace(/\r/g, '').split('\n');
|
|
@@ -106,22 +107,29 @@ export function modeBadge(mode) {
|
|
|
106
107
|
return { label: 'PLAN', color: MODE.plan };
|
|
107
108
|
return { label: 'TASK', color: MODE.task };
|
|
108
109
|
}
|
|
110
|
+
export function hiddenProgressCount(agent, max) {
|
|
111
|
+
return Math.max(0, (agent.progressSteps?.length ?? 0) - max);
|
|
112
|
+
}
|
|
109
113
|
function agentDisplayName(agent) {
|
|
110
114
|
return agent.alias && agent.alias !== agent.name ? `${agent.alias} ${agent.name}` : agent.alias || agent.name;
|
|
111
115
|
}
|
|
112
|
-
export function ProgressSteps({ agent, max = 4, cols = 100 }) {
|
|
116
|
+
export function ProgressSteps({ agent, max = 4, cols = 100, showRemaining = false, }) {
|
|
113
117
|
const steps = agent.progressSteps?.slice(0, max) ?? [];
|
|
118
|
+
const total = agent.progressSteps?.length ?? 0;
|
|
114
119
|
if (steps.length === 0)
|
|
115
120
|
return null;
|
|
116
121
|
const textMax = Math.max(20, cols - 8);
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
+
const remaining = hiddenProgressCount(agent, max);
|
|
123
|
+
const ref = agent.alias || agent.name;
|
|
124
|
+
return (_jsxs(Box, { flexDirection: "column", children: [steps.map((step, i) => {
|
|
125
|
+
const active = step.status === 'active';
|
|
126
|
+
const done = step.status === 'done';
|
|
127
|
+
return (_jsxs(Text, { color: done ? UI.ok : active ? COLOR.cream : UI.muted, wrap: "truncate-end", children: [_jsxs(Text, { color: done ? UI.ok : active ? COLOR.cream : UI.muted, children: [done ? MARK.done : active ? MARK.active : MARK.idle, " "] }), truncate(step.text, textMax)] }, `${i}-${step.text}`));
|
|
128
|
+
}), showRemaining && remaining > 0 ? (_jsxs(Text, { color: COLOR.creamMuted, wrap: "truncate-end", children: ["+", remaining, " steps \u00B7 full /focus ", ref, " \u00B7 term /attach ", ref] })) : null] }));
|
|
122
129
|
}
|
|
123
130
|
export function AgentRow({ agent, logs, cols, }) {
|
|
124
131
|
const meta = STATE_META[agent.state];
|
|
132
|
+
const terminal = agent.state === 'done' || agent.state === 'error' || agent.state === 'stopped';
|
|
125
133
|
// ── State transition pulse (Phase 5) ──
|
|
126
134
|
const prevState = useRef(agent.state);
|
|
127
135
|
const [pulse, setPulse] = useState(false);
|
|
@@ -137,7 +145,11 @@ export function AgentRow({ agent, logs, cols, }) {
|
|
|
137
145
|
const pulseColor = pulse ? 'whiteBright' : null;
|
|
138
146
|
const name = agentDisplayName(agent);
|
|
139
147
|
const mode = modeBadge(agent.mode);
|
|
140
|
-
const quickActions =
|
|
148
|
+
const quickActions = terminal
|
|
149
|
+
? agent.state === 'error'
|
|
150
|
+
? `full /focus ${agent.alias || agent.name} · term /attach ${agent.alias || agent.name} · clear /clear`
|
|
151
|
+
: `full /focus ${agent.alias || agent.name} · term /attach ${agent.alias || agent.name}`
|
|
152
|
+
: `stop /stop ${agent.alias || agent.name} · full /focus ${agent.alias || agent.name} · term /attach ${agent.alias || agent.name}`;
|
|
141
153
|
const actionBudget = Math.min(44, quickActions.length + 2);
|
|
142
154
|
const taskMax = Math.max(10, cols - 18 - actionBudget);
|
|
143
155
|
const line2Max = Math.max(10, cols - 2);
|
|
@@ -154,7 +166,7 @@ export function AgentRow({ agent, logs, cols, }) {
|
|
|
154
166
|
else if (claims) {
|
|
155
167
|
line2 = { text: claims, color: UI.warn };
|
|
156
168
|
}
|
|
157
|
-
return (_jsxs(Box, { flexDirection: "column", marginBottom: 0, paddingLeft: 1, children: [_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Text, { wrap: "truncate-end", children: [SPINNER_STATES.has(agent.state) ? (_jsx(Spinner, { color: pulseColor ?? spinnerColor(agent.state) })) : (_jsx(Text, { color: pulseColor ?? meta.color, bold: true, children: meta.mark })), _jsx(Text, { children: " " }), _jsx(Text, { color: agent.color, bold: true, children: name }), _jsxs(Text, { color: mode.color, children: [" [", mode.label, "]"] }), specialist ? _jsx(Text, { color: UI.note, children: specialist }) : null, _jsxs(Text, { color: UI.text, children: [" ", truncate(agent.task, taskMax)] })] }), _jsx(Text, { color: UI.muted, wrap: "truncate-end", children: truncate(quickActions, actionBudget) })] }), summary.length > 0 ? (_jsx(Box, { flexDirection: "column", children: summary.map((line, i) => (_jsxs(Box, { flexDirection: "row", justifyContent: i === 0 ? 'space-between' : undefined, children: [_jsxs(Text, { color: COLOR.cream, wrap: "truncate-end", children: [_jsx(Text, { color: COLOR.cream, children: "\u2022 " }), line] }), i === 0 ? _jsx(Text, { color: UI.muted, children: telemetry }) : null] }, `${i}-${line}`))) })) : line2 ? (_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsx(Text, { color: line2.color, wrap: "truncate-end", children: line2.text }), _jsx(Text, { color: UI.muted, children: telemetry })] })) : null, !agent.lastResult ? _jsx(ProgressSteps, { agent: agent, max: 3, cols: line2Max }) : null] }));
|
|
169
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 0, paddingLeft: 1, children: [_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Text, { wrap: "truncate-end", children: [SPINNER_STATES.has(agent.state) ? (_jsx(Spinner, { color: pulseColor ?? spinnerColor(agent.state) })) : (_jsx(Text, { color: pulseColor ?? meta.color, bold: true, children: meta.mark })), _jsx(Text, { children: " " }), _jsx(Text, { color: agent.color, bold: true, children: name }), _jsxs(Text, { color: mode.color, children: [" [", mode.label, "]"] }), specialist ? _jsx(Text, { color: UI.note, children: specialist }) : null, _jsxs(Text, { color: UI.text, children: [" ", truncate(agent.task, taskMax)] })] }), _jsx(Text, { color: UI.muted, wrap: "truncate-end", children: truncate(quickActions, actionBudget) })] }), summary.length > 0 ? (_jsx(Box, { flexDirection: "column", children: summary.map((line, i) => (_jsxs(Box, { flexDirection: "row", justifyContent: i === 0 ? 'space-between' : undefined, children: [_jsxs(Text, { color: COLOR.cream, wrap: "truncate-end", children: [_jsx(Text, { color: COLOR.cream, children: "\u2022 " }), line] }), i === 0 ? _jsx(Text, { color: UI.muted, children: telemetry }) : null] }, `${i}-${line}`))) })) : line2 ? (_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsx(Text, { color: line2.color, wrap: "truncate-end", children: line2.text }), _jsx(Text, { color: UI.muted, children: telemetry })] })) : null, !agent.lastResult ? _jsx(ProgressSteps, { agent: agent, max: 3, cols: line2Max, showRemaining: true }) : null] }));
|
|
158
170
|
}
|
|
159
171
|
export function AgentTranscript({ agent, logs, raw = false, scrolled = 0, cols = 100, }) {
|
|
160
172
|
const meta = STATE_META[agent.state];
|
package/dist/ui/App.js
CHANGED
|
@@ -687,7 +687,9 @@ function AgentHub({ agents, ctl, cols, scroll, visibleRows, }) {
|
|
|
687
687
|
}
|
|
688
688
|
const needsSeparator = rows.length > 0;
|
|
689
689
|
const summaryLines = agent.lastResult ? Math.min(4, Math.max(1, agent.lastResult.split('\n').filter((l) => l.trim()).length)) : 0;
|
|
690
|
-
const stepLines = !agent.lastResult && agent.progressSteps && agent.progressSteps.length > 0
|
|
690
|
+
const stepLines = !agent.lastResult && agent.progressSteps && agent.progressSteps.length > 0
|
|
691
|
+
? Math.min(3, agent.progressSteps.length) + (agent.progressSteps.length > 3 ? 1 : 0)
|
|
692
|
+
: 0;
|
|
691
693
|
const agentLines = 1 + Math.max(summaryLines, agent.currentAction || agent.claims?.length ? 1 : 0) + stepLines;
|
|
692
694
|
const neededLines = agentLines + (needsSeparator ? 1 : 0);
|
|
693
695
|
if (renderedLines + neededLines > visibleRows) {
|
package/dist/ui/AttachApp.js
CHANGED
|
@@ -23,6 +23,9 @@ export function parseAttachCommand(text) {
|
|
|
23
23
|
return { type: 'detach' };
|
|
24
24
|
if (v === '/raw')
|
|
25
25
|
return { type: 'raw' };
|
|
26
|
+
const stop = v.match(/^\/stop(?:\s+(\S+))?$/s);
|
|
27
|
+
if (stop)
|
|
28
|
+
return { type: 'stop', target: stop[1]?.trim() };
|
|
26
29
|
const at = v.match(/^@(\S+)\s+(.+)$/s);
|
|
27
30
|
if (at)
|
|
28
31
|
return { type: 'send', target: at[1], text: at[2].trim() };
|
|
@@ -47,7 +50,8 @@ export function parseAttachCommand(text) {
|
|
|
47
50
|
export function formatAttachFooter(info) {
|
|
48
51
|
if (!info)
|
|
49
52
|
return 'Waiting for agent · /quit';
|
|
50
|
-
|
|
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`;
|
|
51
55
|
}
|
|
52
56
|
function AttachStaticLine({ item, raw }) {
|
|
53
57
|
if (raw) {
|
|
@@ -177,6 +181,10 @@ export function AttachApp({ agentRef, sock }) {
|
|
|
177
181
|
setRaw((r) => !r);
|
|
178
182
|
return;
|
|
179
183
|
}
|
|
184
|
+
if (cmd.type === 'stop') {
|
|
185
|
+
wire({ type: 'stop', target: cmd.target || agentRef });
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
180
188
|
// /task|/ask|/plan|/review <text> — launch agent N+1 from this terminal.
|
|
181
189
|
if (cmd.type === 'spawn') {
|
|
182
190
|
wire({ type: 'spawn', text: cmd.text, mode: cmd.mode });
|
|
@@ -198,6 +206,7 @@ export function AttachApp({ agentRef, sock }) {
|
|
|
198
206
|
const maxTimelineScroll = Math.max(0, logs.length - timelineVisibleLogs);
|
|
199
207
|
const clampedTimelineScroll = Math.min(timelineScroll, maxTimelineScroll);
|
|
200
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))));
|
|
201
210
|
useEffect(() => {
|
|
202
211
|
if (timelineFollowTail)
|
|
203
212
|
setTimelineScroll(0);
|
|
@@ -227,11 +236,11 @@ export function AttachApp({ agentRef, sock }) {
|
|
|
227
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 ? (
|
|
228
237
|
/* While running, keep the native terminal scrollback stable: activity is
|
|
229
238
|
* 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
|
|
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
|
|
231
240
|
.map((o) => `${o.name} [${stateLabel(o.state)}] ${truncate(o.currentAction || o.task, 40)}`)
|
|
232
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] })) : (
|
|
233
242
|
/* 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 ? (
|
|
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 ? (
|
|
235
244
|
// The session's shared awareness, visible here too: what the
|
|
236
245
|
// OTHER agents are doing right now (live, same feed the agents get).
|
|
237
246
|
_jsxs(Text, { color: UI.muted, wrap: "truncate-end", children: ["Others ", ' ', others
|
package/dist/ui/CommandInput.js
CHANGED
|
@@ -43,7 +43,7 @@ export function bestCommandCompletion(value) {
|
|
|
43
43
|
export function commandNamesForContext(context) {
|
|
44
44
|
if (context !== 'attach')
|
|
45
45
|
return undefined;
|
|
46
|
-
return ['/ask', '/a', '/task', '/t', '/plan', '/p', '/review', '/send', '/raw', '/quit', '/exit', '/detach'];
|
|
46
|
+
return ['/ask', '/a', '/task', '/t', '/plan', '/p', '/review', '/send', '/stop', '/raw', '/quit', '/exit', '/detach'];
|
|
47
47
|
}
|
|
48
48
|
export function agentArgCommand(value) {
|
|
49
49
|
const m = value.match(/^(\/\S+)\s+([^\s]*)$/);
|
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);
|