@parallel-cli/parallel 0.4.4 → 0.4.5
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 +25 -0
- package/README.md +37 -7
- package/dist/agents/tools.js +58 -2
- package/dist/controller.js +32 -7
- package/dist/coordination/blackboard.js +75 -0
- package/dist/i18n.js +8 -4
- package/dist/server.js +10 -0
- package/dist/ui/AgentPanel.js +22 -7
- package/dist/ui/App.js +7 -4
- package/dist/ui/AttachApp.js +13 -3
- package/dist/ui/CommandInput.js +60 -9
- package/dist/ui/Timeline.js +11 -9
- package/dist/ui/views.js +2 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to Parallel are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.4.5 - 2026-06-23
|
|
6
|
+
|
|
7
|
+
### 0.4.5 Added
|
|
8
|
+
|
|
9
|
+
- Added explicit hub, focus, and attach input contexts with context-specific hints and filtered command suggestions.
|
|
10
|
+
- Added agent argument autocomplete for `/focus`, `/send`, `/attach`, `/pause`, `/resume`, `/stop`, `/restore`, and `/commit`.
|
|
11
|
+
- Added attach-terminal routing for `@all`, `@agent`, and `/send`, so dedicated terminals can broadcast or steer another agent through the main session.
|
|
12
|
+
- Added richer hub rows using latest useful agent signals, specialist badges, context percentage, and responsive timeline widths.
|
|
13
|
+
- Added durable session memory for claims, recent diff excerpts, file activity, work-map warnings, agent aliases, provider/model metadata, specialist, and context usage.
|
|
14
|
+
- Added shell mutation tracking: files changed by `run_command` now appear in live diffs, file activity, and agent commit ownership.
|
|
15
|
+
- Added a non-blocking work map that surfaces overlapping claims and repeated co-edit conflicts in `/board` and agent context.
|
|
16
|
+
|
|
17
|
+
### 0.4.5 Changed
|
|
18
|
+
|
|
19
|
+
- Clarified agent naming around `Name: task` syntax in README examples.
|
|
20
|
+
- Made `/raw` visible only when it affects the active focus view.
|
|
21
|
+
- Reset focus scroll follow-tail behavior when switching focused agents.
|
|
22
|
+
- Improved session restore so `/restore` can target saved agents by name, alias, or id when their conversation is available.
|
|
23
|
+
|
|
24
|
+
### 0.4.5 Fixed
|
|
25
|
+
|
|
26
|
+
- Fixed attach-terminal `@all` being treated as plain text to the attached agent.
|
|
27
|
+
- Fixed restored note ids drifting by resynchronizing blackboard note and change sequences after loading session data.
|
|
28
|
+
- Fixed shell-created edits being invisible to `/diff`, `/board`, and `/commit`.
|
|
29
|
+
|
|
5
30
|
## 0.4.4 - 2026-06-23
|
|
6
31
|
|
|
7
32
|
### 0.4.4 Added
|
package/README.md
CHANGED
|
@@ -16,9 +16,12 @@ Parallel lets you run several AI coding agents on the same repository at the sam
|
|
|
16
16
|
- Run multiple agents in parallel on one project.
|
|
17
17
|
- Choose explicit modes: `/ask`, `/task`, and `/plan`.
|
|
18
18
|
- Type plain text to launch a task agent immediately.
|
|
19
|
+
- Use context-aware input in the hub, focus view, and attached agent terminals.
|
|
19
20
|
- Steer one agent with `@a1 ...` or broadcast with `@all ...`.
|
|
20
21
|
- Open dedicated agent terminals with native scrollback.
|
|
22
|
+
- See a richer live hub with latest agent signals, mode badges, context usage, and responsive timelines.
|
|
21
23
|
- Review agents, notes, file activity, diffs, cost, skills, specialists, and saved sessions from the TUI.
|
|
24
|
+
- Track shell-created file mutations in the same live diff feed as agent edits.
|
|
22
25
|
- Configure OpenAI-compatible providers through a guided wizard and settings panel.
|
|
23
26
|
- Use 29 provider presets across Western, Chinese, Gateway, Inference, and Local categories.
|
|
24
27
|
- Support local no-key endpoints such as Ollama and vLLM/SGLang.
|
|
@@ -79,9 +82,9 @@ Plain text launches a `/task` agent. You can launch another agent while the firs
|
|
|
79
82
|
Use explicit modes when intent matters:
|
|
80
83
|
|
|
81
84
|
```text
|
|
82
|
-
/ask
|
|
83
|
-
/plan
|
|
84
|
-
/task
|
|
85
|
+
/ask Reviewer: should we split the CLI parser?
|
|
86
|
+
/plan Migration: propose the safest rollout for the config change
|
|
87
|
+
/task Builder: implement the approved plan
|
|
85
88
|
```
|
|
86
89
|
|
|
87
90
|
Steer a running agent:
|
|
@@ -122,6 +125,19 @@ The main TUI is the Parallel hub. It is designed to answer:
|
|
|
122
125
|
- what changed in the project
|
|
123
126
|
- what model, provider, shell mode, and cost are active
|
|
124
127
|
|
|
128
|
+
Input has three explicit contexts:
|
|
129
|
+
|
|
130
|
+
- 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`.
|
|
131
|
+
- Focus: after `/focus a1`, plain text talks to the focused agent instead of spawning a new one. `/raw` affects this view only.
|
|
132
|
+
- Attach: in `parallel attach a1`, plain text 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.
|
|
133
|
+
|
|
134
|
+
Use `Name: task` when naming an agent:
|
|
135
|
+
|
|
136
|
+
```text
|
|
137
|
+
/task Tests: add regression coverage for the auth middleware
|
|
138
|
+
/plan Migration: outline the safest database rollout
|
|
139
|
+
```
|
|
140
|
+
|
|
125
141
|
Common hub commands:
|
|
126
142
|
|
|
127
143
|
- `/agents`: agent overview.
|
|
@@ -170,9 +186,12 @@ From an attached terminal:
|
|
|
170
186
|
|
|
171
187
|
```text
|
|
172
188
|
plain text sends a message to this agent
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
/
|
|
189
|
+
@all pause public interface changes until tests finish
|
|
190
|
+
@a2 re-read the API client before editing it
|
|
191
|
+
/send a2 check the new parser contract
|
|
192
|
+
/task Tests: write parser regression tests
|
|
193
|
+
/ask Reviewer: is this result safe to merge?
|
|
194
|
+
/plan Migration: prepare a migration plan
|
|
176
195
|
/raw
|
|
177
196
|
/quit
|
|
178
197
|
```
|
|
@@ -266,7 +285,14 @@ Environment variables:
|
|
|
266
285
|
- `/save [name]`: save the current session.
|
|
267
286
|
- `/sessions`: list saved sessions.
|
|
268
287
|
- `/session <n|latest>`: load a saved session snapshot. If active agents are running, use `/session <n|latest> --force` after saving/stopping what you need.
|
|
269
|
-
- `/restore <agent>`: relaunch a restored agent
|
|
288
|
+
- `/restore <agent>`: relaunch a restored agent by name, alias, or saved id when its conversation history is still available.
|
|
289
|
+
|
|
290
|
+
Session memory has two layers:
|
|
291
|
+
|
|
292
|
+
- Live memory: active agents see statuses, notes, claims, work-map warnings, file activity, and recent diffs before every model action.
|
|
293
|
+
- Durable memory: `/save` and autosave persist notes, claims, recent diff excerpts, file activity, work-map warnings, agent aliases, model/provider metadata, context usage, and conversation paths for restore.
|
|
294
|
+
|
|
295
|
+
Restore is best effort and explicit. `/session` reloads coordination memory into the blackboard; `/restore <agent>` relaunches an agent only when the saved conversation file still exists. Restored agents keep their prior task, mode, model, specialist, and conversation when available.
|
|
270
296
|
|
|
271
297
|
### Settings And Exit
|
|
272
298
|
|
|
@@ -336,6 +362,10 @@ When an agent writes a file:
|
|
|
336
362
|
|
|
337
363
|
This keeps agents moving without allowing silent overwrites.
|
|
338
364
|
|
|
365
|
+
Commands run through `run_command` are also snapshotted before and after execution. If a shell command edits, creates, or deletes tracked project files, Parallel records those mutations in `/diff`, `/board`, and `/commit` ownership just like tool-based edits.
|
|
366
|
+
|
|
367
|
+
The work map is advisory, not a lock. Agents can declare claims with `claim_files`; Parallel detects overlapping claims and repeated conflicts, then shows non-blocking warnings in `/board` and injects them into agent context so agents can coordinate before collisions become expensive.
|
|
368
|
+
|
|
339
369
|
## Headless Mode
|
|
340
370
|
|
|
341
371
|
For CI and scripts, run without the TUI:
|
package/dist/agents/tools.js
CHANGED
|
@@ -2,7 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { exec } from 'node:child_process';
|
|
4
4
|
import * as Diff from 'diff';
|
|
5
|
-
const IGNORED = new Set(['node_modules', '.git', '.parallel', 'dist', '__pycache__', '.venv', 'venv']);
|
|
5
|
+
const IGNORED = new Set(['node_modules', '.git', '.parallel', '.cursor', 'dist', '__pycache__', '.venv', 'venv']);
|
|
6
6
|
const MAX_OUTPUT = 12_000;
|
|
7
7
|
const MUTATING_TOOLS = new Set(['write_file', 'edit_file', 'claim_files', 'remember']);
|
|
8
8
|
function isMutatingShell(command) {
|
|
@@ -269,6 +269,60 @@ export class ToolExecutor {
|
|
|
269
269
|
relOf(p) {
|
|
270
270
|
return path.relative(this.projectRoot, this.resolve(p)) || '.';
|
|
271
271
|
}
|
|
272
|
+
snapshotProject() {
|
|
273
|
+
const snapshot = new Map();
|
|
274
|
+
const walk = (dir, depth) => {
|
|
275
|
+
if (depth > 8)
|
|
276
|
+
return;
|
|
277
|
+
let entries;
|
|
278
|
+
try {
|
|
279
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
for (const e of entries) {
|
|
285
|
+
if (IGNORED.has(e.name) || e.name.startsWith('.git'))
|
|
286
|
+
continue;
|
|
287
|
+
const full = path.join(dir, e.name);
|
|
288
|
+
const relPath = path.relative(this.projectRoot, full);
|
|
289
|
+
if (e.isDirectory()) {
|
|
290
|
+
walk(full, depth + 1);
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
if (!e.isFile())
|
|
294
|
+
continue;
|
|
295
|
+
try {
|
|
296
|
+
const stat = fs.statSync(full);
|
|
297
|
+
if (stat.size > 750_000)
|
|
298
|
+
continue;
|
|
299
|
+
snapshot.set(relPath, fs.readFileSync(full, 'utf8'));
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
/* binary/unreadable files are ignored for diff tracking */
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
walk(this.projectRoot, 0);
|
|
307
|
+
return snapshot;
|
|
308
|
+
}
|
|
309
|
+
recordShellMutations(before) {
|
|
310
|
+
const after = this.snapshotProject();
|
|
311
|
+
const paths = new Set([...before.keys(), ...after.keys()]);
|
|
312
|
+
let count = 0;
|
|
313
|
+
for (const relPath of [...paths].sort()) {
|
|
314
|
+
const oldContent = before.get(relPath);
|
|
315
|
+
const newContent = after.get(relPath);
|
|
316
|
+
if (oldContent === newContent)
|
|
317
|
+
continue;
|
|
318
|
+
this.board.addChange(this.agentId, relPath, oldContent ?? '', newContent ?? '');
|
|
319
|
+
this.board.recordActivity(relPath, this.agentId, 'shell');
|
|
320
|
+
count++;
|
|
321
|
+
}
|
|
322
|
+
if (count > 0)
|
|
323
|
+
this.board.log(this.agentId, 'tool', `✏ shell changed ${count} file${count === 1 ? '' : 's'}`);
|
|
324
|
+
return count;
|
|
325
|
+
}
|
|
272
326
|
async execute(name, args) {
|
|
273
327
|
try {
|
|
274
328
|
const guard = this.guardTool(name, args);
|
|
@@ -592,6 +646,7 @@ export class ToolExecutor {
|
|
|
592
646
|
}
|
|
593
647
|
this.board.setAgentState(this.agentId, 'working', `$ ${command.slice(0, 60)}`);
|
|
594
648
|
this.board.log(this.agentId, 'tool', `$ ${command}`);
|
|
649
|
+
const before = this.snapshotProject();
|
|
595
650
|
return new Promise((resolve) => {
|
|
596
651
|
exec(command, { cwd: this.projectRoot, timeout: 120_000, maxBuffer: 4 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
597
652
|
let out = '';
|
|
@@ -606,8 +661,9 @@ export class ToolExecutor {
|
|
|
606
661
|
if (out.length > MAX_OUTPUT)
|
|
607
662
|
out = out.slice(0, MAX_OUTPUT) + '\n... (output truncated)';
|
|
608
663
|
const result = out || '(no output, success)';
|
|
664
|
+
const changed = this.recordShellMutations(before);
|
|
609
665
|
this.board.log(this.agentId, 'tool_result', result);
|
|
610
|
-
resolve(result);
|
|
666
|
+
resolve(changed > 0 ? `${result}\n\nTracked shell mutations: ${changed} file${changed === 1 ? '' : 's'}.` : result);
|
|
611
667
|
});
|
|
612
668
|
});
|
|
613
669
|
}
|
package/dist/controller.js
CHANGED
|
@@ -388,7 +388,8 @@ export class Controller extends EventEmitter {
|
|
|
388
388
|
* memory intact instead of starting from scratch.
|
|
389
389
|
*/
|
|
390
390
|
respawnAgent(name) {
|
|
391
|
-
const
|
|
391
|
+
const ref = name.toLowerCase();
|
|
392
|
+
const sa = this.loadedSession?.agents.find((a) => a.name.toLowerCase() === ref || a.alias?.toLowerCase() === ref || a.id?.toLowerCase() === ref);
|
|
392
393
|
if (!sa)
|
|
393
394
|
return 'no-agent';
|
|
394
395
|
if (!sa.conversation || !fs.existsSync(sa.conversation))
|
|
@@ -406,7 +407,8 @@ export class Controller extends EventEmitter {
|
|
|
406
407
|
}
|
|
407
408
|
if (history.length === 0)
|
|
408
409
|
return 'no-conversation';
|
|
409
|
-
|
|
410
|
+
const modelSpec = sa.model ? (sa.providerName ? `${sa.providerName}:${sa.model}` : sa.model) : undefined;
|
|
411
|
+
return this.spawnAgent(sa.task, sa.name, modelSpec, undefined, sa.specialist, history, sa.mode ?? 'task');
|
|
410
412
|
}
|
|
411
413
|
pauseAgent(name) {
|
|
412
414
|
const a = this.findAgent(name);
|
|
@@ -445,7 +447,12 @@ export class Controller extends EventEmitter {
|
|
|
445
447
|
return true;
|
|
446
448
|
}
|
|
447
449
|
broadcast(content) {
|
|
448
|
-
|
|
450
|
+
const stamp = content.trim();
|
|
451
|
+
const recent = [...this.board.notes]
|
|
452
|
+
.reverse()
|
|
453
|
+
.find((n) => n.from === 'user' && n.to === 'all' && Date.now() - n.ts < 1500);
|
|
454
|
+
if (stamp && recent?.content !== stamp)
|
|
455
|
+
this.board.addNote('user', 'all', stamp);
|
|
449
456
|
let n = 0;
|
|
450
457
|
for (const [id, agent] of this.agents.entries()) {
|
|
451
458
|
const info = this.board.agents.get(id);
|
|
@@ -608,12 +615,20 @@ export class Controller extends EventEmitter {
|
|
|
608
615
|
try {
|
|
609
616
|
const dir = this.sessionsDir();
|
|
610
617
|
fs.mkdirSync(dir, { recursive: true });
|
|
618
|
+
const providerName = this.sessionProvider()?.name ?? this.session.providerName;
|
|
619
|
+
const trimChange = (c) => ({
|
|
620
|
+
...c,
|
|
621
|
+
before: c.before.length > 40_000 ? `${c.before.slice(0, 40_000)}\n/* truncated */` : c.before,
|
|
622
|
+
after: c.after.length > 40_000 ? `${c.after.slice(0, 40_000)}\n/* truncated */` : c.after,
|
|
623
|
+
});
|
|
611
624
|
const data = {
|
|
612
625
|
savedAt: new Date().toISOString(),
|
|
613
626
|
name: this.sessionName,
|
|
614
627
|
projectRoot: this.projectRoot,
|
|
615
628
|
agents: [...this.board.agents.values()].map((a) => ({
|
|
629
|
+
id: a.id,
|
|
616
630
|
name: a.name,
|
|
631
|
+
alias: a.alias,
|
|
617
632
|
task: a.task,
|
|
618
633
|
mode: a.mode,
|
|
619
634
|
state: a.state,
|
|
@@ -622,10 +637,17 @@ export class Controller extends EventEmitter {
|
|
|
622
637
|
tokensIn: a.tokensIn,
|
|
623
638
|
tokensOut: a.tokensOut,
|
|
624
639
|
cost: a.cost,
|
|
640
|
+
providerName,
|
|
625
641
|
model: a.model,
|
|
642
|
+
specialist: a.specialist,
|
|
643
|
+
claims: a.claims,
|
|
644
|
+
ctxPct: a.ctxPct,
|
|
626
645
|
conversation: this.conversationFiles.get(a.id),
|
|
627
646
|
})),
|
|
628
647
|
notes: this.board.notes.slice(-200),
|
|
648
|
+
changes: this.board.changes.slice(-80).map(trimChange),
|
|
649
|
+
fileActivity: [...this.board.fileActivity.values()].slice(-100),
|
|
650
|
+
workMapWarnings: this.board.workMapWarnings.slice(-80),
|
|
629
651
|
changedFiles: [...new Set(this.board.changes.map((c) => c.path))],
|
|
630
652
|
};
|
|
631
653
|
const file = path.join(dir, `session-${this.sessionStamp}.json`);
|
|
@@ -661,10 +683,13 @@ export class Controller extends EventEmitter {
|
|
|
661
683
|
if (data.name)
|
|
662
684
|
this.sessionName = data.name;
|
|
663
685
|
const tasks = data.agents.map((a) => `${a.name} [${a.state}] : ${a.task}${a.lastResult ? ` → ${a.lastResult}` : ''}`);
|
|
664
|
-
this.board.
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
686
|
+
this.board.hydrate({
|
|
687
|
+
notes: data.notes ?? [],
|
|
688
|
+
changes: data.changes ?? [],
|
|
689
|
+
fileActivity: data.fileActivity ?? [],
|
|
690
|
+
workMapWarnings: data.workMapWarnings ?? [],
|
|
691
|
+
});
|
|
692
|
+
this.board.addNote('system', 'all', `Previous session restored (${data.savedAt}). Past work:\n${tasks.join('\n')}\nFiles changed then: ${(data.changedFiles ?? []).join(', ') || '(none)'}`);
|
|
668
693
|
this.board.log('', 'system', t('m.sessionRestored', { date: new Date(data.savedAt).toLocaleString() }));
|
|
669
694
|
// Financial history: per-agent cost/steps/tokens of the restored session.
|
|
670
695
|
const withCost = data.agents.filter((a) => a.cost !== undefined || a.tokensIn !== undefined);
|
|
@@ -17,6 +17,7 @@ export class Blackboard extends EventEmitter {
|
|
|
17
17
|
notes = [];
|
|
18
18
|
changes = [];
|
|
19
19
|
logs = [];
|
|
20
|
+
workMapWarnings = [];
|
|
20
21
|
noteSeq = 0;
|
|
21
22
|
changeSeq = 0;
|
|
22
23
|
logSeq = 0;
|
|
@@ -42,6 +43,8 @@ export class Blackboard extends EventEmitter {
|
|
|
42
43
|
if (!a)
|
|
43
44
|
return;
|
|
44
45
|
Object.assign(a, patch);
|
|
46
|
+
if ('claims' in patch)
|
|
47
|
+
this.recomputeWorkMap();
|
|
45
48
|
this.touch();
|
|
46
49
|
}
|
|
47
50
|
setAgentState(id, state, action) {
|
|
@@ -142,11 +145,65 @@ export class Blackboard extends EventEmitter {
|
|
|
142
145
|
this.conflictCounts.set(relPath, n);
|
|
143
146
|
if (n === 3)
|
|
144
147
|
this.emit('agent-event', { type: 'conflict', path: relPath });
|
|
148
|
+
this.upsertWorkWarning({
|
|
149
|
+
id: `conflict:${relPath}`,
|
|
150
|
+
level: n >= 3 ? 'conflict' : 'warn',
|
|
151
|
+
title: 'Repeated edit conflict',
|
|
152
|
+
detail: `${relPath} has ${n} recorded co-edit collision${n === 1 ? '' : 's'}. Coordinate before touching it again.`,
|
|
153
|
+
paths: [relPath],
|
|
154
|
+
agentNames: [],
|
|
155
|
+
ts: Date.now(),
|
|
156
|
+
count: n,
|
|
157
|
+
});
|
|
145
158
|
return n;
|
|
146
159
|
}
|
|
147
160
|
lastChangeId() {
|
|
148
161
|
return this.changes.length > 0 ? this.changes[this.changes.length - 1].id : 0;
|
|
149
162
|
}
|
|
163
|
+
static normClaim(p) {
|
|
164
|
+
return p.trim().replace(/\\/g, '/').replace(/^\.\/+/, '').replace(/\/+$/, '');
|
|
165
|
+
}
|
|
166
|
+
static overlaps(a, b) {
|
|
167
|
+
const x = Blackboard.normClaim(a);
|
|
168
|
+
const y = Blackboard.normClaim(b);
|
|
169
|
+
if (!x || !y)
|
|
170
|
+
return false;
|
|
171
|
+
return x === y || x.startsWith(`${y}/`) || y.startsWith(`${x}/`);
|
|
172
|
+
}
|
|
173
|
+
upsertWorkWarning(warning) {
|
|
174
|
+
const idx = this.workMapWarnings.findIndex((w) => w.id === warning.id);
|
|
175
|
+
if (idx >= 0)
|
|
176
|
+
this.workMapWarnings[idx] = warning;
|
|
177
|
+
else
|
|
178
|
+
this.workMapWarnings.push(warning);
|
|
179
|
+
if (this.workMapWarnings.length > 100)
|
|
180
|
+
this.workMapWarnings.splice(0, this.workMapWarnings.length - 100);
|
|
181
|
+
}
|
|
182
|
+
recomputeWorkMap() {
|
|
183
|
+
const agents = [...this.agents.values()].filter((a) => a.claims && a.claims.length > 0);
|
|
184
|
+
const warnings = [];
|
|
185
|
+
for (let i = 0; i < agents.length; i++) {
|
|
186
|
+
for (let j = i + 1; j < agents.length; j++) {
|
|
187
|
+
const a = agents[i];
|
|
188
|
+
const b = agents[j];
|
|
189
|
+
const paths = (a.claims ?? []).filter((left) => (b.claims ?? []).some((right) => Blackboard.overlaps(left, right)));
|
|
190
|
+
if (paths.length === 0)
|
|
191
|
+
continue;
|
|
192
|
+
warnings.push({
|
|
193
|
+
id: `overlap:${[a.id, b.id].sort().join(':')}`,
|
|
194
|
+
level: 'warn',
|
|
195
|
+
title: 'Overlapping work areas',
|
|
196
|
+
detail: `${a.name} and ${b.name} both declared ${paths.join(', ')}.`,
|
|
197
|
+
paths,
|
|
198
|
+
agentNames: [a.name, b.name],
|
|
199
|
+
ts: Date.now(),
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const conflictWarnings = this.workMapWarnings.filter((w) => w.id.startsWith('conflict:')).slice(-20);
|
|
204
|
+
this.workMapWarnings = [...conflictWarnings, ...warnings].slice(-100);
|
|
205
|
+
return this.workMapWarnings;
|
|
206
|
+
}
|
|
150
207
|
// ---------- logs ----------
|
|
151
208
|
log(agentId, kind, text) {
|
|
152
209
|
this.logs.push({ agentId, kind, text, ts: Date.now(), seq: ++this.logSeq });
|
|
@@ -189,6 +246,13 @@ export class Blackboard extends EventEmitter {
|
|
|
189
246
|
lines.push(` • ${act.path} — ${mine ? 'you' : act.agentName} (${act.op}, ${age}s ago)`);
|
|
190
247
|
}
|
|
191
248
|
}
|
|
249
|
+
const warnings = this.workMapWarnings.filter((w) => w.level !== 'info').slice(-5);
|
|
250
|
+
if (warnings.length > 0) {
|
|
251
|
+
lines.push('Work map warnings (advisory, do not block):');
|
|
252
|
+
for (const w of warnings) {
|
|
253
|
+
lines.push(` • ${w.title}: ${w.detail}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
192
256
|
if (me)
|
|
193
257
|
lines.push(`Reminder — your task: ${me.task}`);
|
|
194
258
|
lines.push('=== END OF REAL-TIME STATE ===');
|
|
@@ -214,6 +278,8 @@ export class Blackboard extends EventEmitter {
|
|
|
214
278
|
})),
|
|
215
279
|
fileActivity: [...this.fileActivity.values()],
|
|
216
280
|
notes: this.notes.slice(-100),
|
|
281
|
+
changes: this.changes.slice(-50),
|
|
282
|
+
workMapWarnings: this.workMapWarnings.slice(-50),
|
|
217
283
|
};
|
|
218
284
|
fs.writeFileSync(path.join(dir, 'state.json'), JSON.stringify(state, null, 2));
|
|
219
285
|
}
|
|
@@ -222,4 +288,13 @@ export class Blackboard extends EventEmitter {
|
|
|
222
288
|
}
|
|
223
289
|
}, 500);
|
|
224
290
|
}
|
|
291
|
+
hydrate(data) {
|
|
292
|
+
this.notes = [...(data.notes ?? [])].sort((a, b) => a.id - b.id);
|
|
293
|
+
this.changes = [...(data.changes ?? [])].sort((a, b) => a.id - b.id);
|
|
294
|
+
this.fileActivity = new Map((data.fileActivity ?? []).map((a) => [a.path, a]));
|
|
295
|
+
this.workMapWarnings = [...(data.workMapWarnings ?? [])].sort((a, b) => a.ts - b.ts);
|
|
296
|
+
this.noteSeq = this.notes.reduce((max, n) => Math.max(max, n.id), 0);
|
|
297
|
+
this.changeSeq = this.changes.reduce((max, c) => Math.max(max, c.id), 0);
|
|
298
|
+
this.touch();
|
|
299
|
+
}
|
|
225
300
|
}
|
package/dist/i18n.js
CHANGED
|
@@ -99,6 +99,7 @@ const en = {
|
|
|
99
99
|
'board.agents': 'Agents:',
|
|
100
100
|
'board.none': '(no agents)',
|
|
101
101
|
'board.activity': 'Who works where (file activity, no locks):',
|
|
102
|
+
'board.workMap': 'Work map warnings:',
|
|
102
103
|
'board.noActivity': '(no edits yet)',
|
|
103
104
|
'board.notes': 'Latest notes:',
|
|
104
105
|
'notes.title': '✉ INTER-AGENT NOTES (last 30)',
|
|
@@ -239,7 +240,7 @@ const en = {
|
|
|
239
240
|
'attach.placeholder': 'Message to {agent} (Enter to send, /quit to detach)…',
|
|
240
241
|
'attach.waiting': 'Waiting for agent {agent}… (is the main Parallel session running?)',
|
|
241
242
|
'attach.gone': 'Session ended — this terminal is detached.',
|
|
242
|
-
'attach.hint': 'plain text steers this agent ·
|
|
243
|
+
'attach.hint': 'plain text steers this agent · @all broadcasts · /send routes · /task creates · /quit detaches',
|
|
243
244
|
'grid.above': '▲ {n} agent(s) above — PgUp',
|
|
244
245
|
'grid.below': '▼ {n} agent(s) below — PgDn',
|
|
245
246
|
'm.nothing': 'Nothing to save yet.',
|
|
@@ -473,6 +474,7 @@ const fr = {
|
|
|
473
474
|
'board.agents': 'Agents :',
|
|
474
475
|
'board.none': '(aucun agent)',
|
|
475
476
|
'board.activity': 'Qui travaille où (activité fichiers, sans verrou) :',
|
|
477
|
+
'board.workMap': 'Alertes work map :',
|
|
476
478
|
'board.noActivity': '(aucune modification pour le moment)',
|
|
477
479
|
'board.notes': 'Dernières notes :',
|
|
478
480
|
'notes.title': '✉ NOTES INTER-AGENTS (30 dernières)',
|
|
@@ -611,7 +613,7 @@ const fr = {
|
|
|
611
613
|
'attach.placeholder': 'Message à {agent} (Entrée pour envoyer, /quit pour détacher)…',
|
|
612
614
|
'attach.waiting': 'En attente de l’agent {agent}… (la session Parallel principale tourne-t-elle ?)',
|
|
613
615
|
'attach.gone': 'Session terminée — ce terminal est détaché.',
|
|
614
|
-
'attach.hint': 'texte libre pilote cet agent ·
|
|
616
|
+
'attach.hint': 'texte libre pilote cet agent · @all diffuse · /send route · /task crée · /quit détache',
|
|
615
617
|
'grid.above': '▲ {n} agent(s) au-dessus — PgUp',
|
|
616
618
|
'grid.below': '▼ {n} agent(s) en dessous — PgDn',
|
|
617
619
|
'm.nothing': 'Rien à sauvegarder pour le moment.',
|
|
@@ -838,6 +840,7 @@ const es = {
|
|
|
838
840
|
'board.agents': 'Agentes:',
|
|
839
841
|
'board.none': '(sin agentes)',
|
|
840
842
|
'board.activity': 'Quién trabaja dónde (actividad de archivos, sin bloqueos):',
|
|
843
|
+
'board.workMap': 'Avisos del mapa de trabajo:',
|
|
841
844
|
'board.noActivity': '(sin modificaciones por ahora)',
|
|
842
845
|
'board.notes': 'Últimas notas:',
|
|
843
846
|
'notes.title': '✉ NOTAS ENTRE AGENTES (últimas 30)',
|
|
@@ -976,7 +979,7 @@ const es = {
|
|
|
976
979
|
'attach.placeholder': 'Mensaje a {agent} (Enter para enviar, /quit para desconectar)…',
|
|
977
980
|
'attach.waiting': 'Esperando al agente {agent}… (¿está corriendo la sesión principal de Parallel?)',
|
|
978
981
|
'attach.gone': 'Sesión terminada — esta terminal está desconectada.',
|
|
979
|
-
'attach.hint': 'texto libre dirige a este agente ·
|
|
982
|
+
'attach.hint': 'texto libre dirige a este agente · @all difunde · /send enruta · /task crea · /quit desconecta',
|
|
980
983
|
'grid.above': '▲ {n} agente(s) arriba — PgUp',
|
|
981
984
|
'grid.below': '▼ {n} agente(s) abajo — PgDn',
|
|
982
985
|
'm.nothing': 'Nada que guardar por ahora.',
|
|
@@ -1203,6 +1206,7 @@ const zh = {
|
|
|
1203
1206
|
'board.agents': '智能体:',
|
|
1204
1207
|
'board.none': '(无智能体)',
|
|
1205
1208
|
'board.activity': '谁在哪里工作(文件活动,无锁定):',
|
|
1209
|
+
'board.workMap': '工作地图提醒:',
|
|
1206
1210
|
'board.noActivity': '(暂无修改)',
|
|
1207
1211
|
'board.notes': '最新便签:',
|
|
1208
1212
|
'notes.title': '✉ 智能体间便签(最近 30 条)',
|
|
@@ -1341,7 +1345,7 @@ const zh = {
|
|
|
1341
1345
|
'attach.placeholder': '发给 {agent} 的消息(回车发送,/quit 断开)…',
|
|
1342
1346
|
'attach.waiting': '等待代理 {agent}…(Parallel 主会话在运行吗?)',
|
|
1343
1347
|
'attach.gone': '会话已结束 — 此终端已断开。',
|
|
1344
|
-
'attach.hint': '直接输入文字可指挥此代理 ·
|
|
1348
|
+
'attach.hint': '直接输入文字可指挥此代理 · @all 广播 · /send 路由 · /task 创建 · /quit 断开',
|
|
1345
1349
|
'grid.above': '▲ 上方还有 {n} 个代理 — PgUp',
|
|
1346
1350
|
'grid.below': '▼ 下方还有 {n} 个代理 — PgDn',
|
|
1347
1351
|
'm.nothing': '暂无可保存的内容。',
|
package/dist/server.js
CHANGED
|
@@ -108,6 +108,16 @@ export function startSessionServer(ctl) {
|
|
|
108
108
|
else if (msg.type === 'answer' && typeof msg.id === 'number' && typeof msg.text === 'string') {
|
|
109
109
|
ctl.answerQuestion(msg.id, msg.text);
|
|
110
110
|
}
|
|
111
|
+
else if (msg.type === 'send' && typeof msg.target === 'string' && typeof msg.text === 'string') {
|
|
112
|
+
const text = msg.text.trim();
|
|
113
|
+
const target = msg.target.trim();
|
|
114
|
+
if (!text || !target)
|
|
115
|
+
continue;
|
|
116
|
+
if (target.toLowerCase() === 'all')
|
|
117
|
+
ctl.broadcast(text);
|
|
118
|
+
else
|
|
119
|
+
ctl.sendToAgent(target, text);
|
|
120
|
+
}
|
|
111
121
|
else if (msg.type === 'spawn' && typeof msg.text === 'string') {
|
|
112
122
|
// Agent N+1 can be launched from ANY terminal of the session —
|
|
113
123
|
// its own dedicated terminal then opens automatically.
|
package/dist/ui/AgentPanel.js
CHANGED
|
@@ -7,6 +7,7 @@ import { Md } from './Md.js';
|
|
|
7
7
|
import { Spinner } from './Spinner.js';
|
|
8
8
|
import { Timeline } from './Timeline.js';
|
|
9
9
|
import { MARK, MODE, STATE_META, UI, ANIM } from './tokens.js';
|
|
10
|
+
import { latestSignal, toUIEvents } from './events.js';
|
|
10
11
|
export const KIND_COLOR = {
|
|
11
12
|
tool: UI.accent,
|
|
12
13
|
llm: UI.muted,
|
|
@@ -26,7 +27,19 @@ export function cleanHubSummary(text) {
|
|
|
26
27
|
.trim();
|
|
27
28
|
}
|
|
28
29
|
export function formatAgentTelemetry(agent) {
|
|
29
|
-
|
|
30
|
+
const ctx = agent.ctxPct !== undefined ? ` · ${agent.ctxPct}% ctx` : '';
|
|
31
|
+
return `${elapsed(agent.startedAt)}${ctx} · ${agent.cost === null ? '$-' : fmtCost(agent.cost)}`;
|
|
32
|
+
}
|
|
33
|
+
function compactResultSummary(text, max) {
|
|
34
|
+
const clean = cleanHubSummary(text);
|
|
35
|
+
const validation = text.match(/validation[^:\n]*[:\n]\s*([^\n]+)/i)?.[1]?.trim();
|
|
36
|
+
const risk = text.match(/risks?[^:\n]*[:\n]\s*([^\n]+)/i)?.[1]?.trim() ?? text.match(/risques?[^:\n]*[:\n]\s*([^\n]+)/i)?.[1]?.trim();
|
|
37
|
+
const parts = [clean.slice(0, Math.max(40, Math.floor(max * 0.55)))];
|
|
38
|
+
if (validation)
|
|
39
|
+
parts.push(`V: ${validation}`);
|
|
40
|
+
if (risk)
|
|
41
|
+
parts.push(`R: ${risk}`);
|
|
42
|
+
return truncate(parts.join(' · '), max);
|
|
30
43
|
}
|
|
31
44
|
function ResultBlock({ agent, compact = false }) {
|
|
32
45
|
if (!agent.lastResult)
|
|
@@ -72,22 +85,24 @@ export function AgentRow({ agent, logs, cols, }) {
|
|
|
72
85
|
const taskMax = Math.max(10, cols - 18);
|
|
73
86
|
const line2Max = Math.max(10, cols - 2);
|
|
74
87
|
const telemetry = formatAgentTelemetry(agent);
|
|
88
|
+
const signal = latestSignal(agent, toUIEvents(logs));
|
|
89
|
+
const specialist = agent.specialist ? ` #${agent.specialist}` : '';
|
|
75
90
|
// Line 2 content
|
|
76
91
|
let line2 = null;
|
|
77
92
|
if (agent.lastResult) {
|
|
78
|
-
line2 = { text: `✓ ${
|
|
93
|
+
line2 = { text: `✓ ${compactResultSummary(agent.lastResult, line2Max)}`, color: UI.ok };
|
|
79
94
|
}
|
|
80
|
-
else if (
|
|
81
|
-
line2 = { text: `▸ ${truncate(
|
|
95
|
+
else if (signal) {
|
|
96
|
+
line2 = { text: `▸ ${truncate(signal, line2Max)}`, color: UI.accent };
|
|
82
97
|
}
|
|
83
98
|
else {
|
|
84
99
|
line2 = { text: meta.label, color: meta.color };
|
|
85
100
|
}
|
|
86
|
-
return (_jsxs(Box, { flexDirection: "column", marginBottom: 0, paddingLeft: 1, 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 }), mode ? (_jsxs(Text, { color: mode.color, children: [" ", mode.char] })) : null, _jsxs(Text, { color: UI.text, children: [" ", truncate(agent.task, taskMax)] })] }), _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 })] })] }));
|
|
101
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 0, paddingLeft: 1, 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 }), mode ? (_jsxs(Text, { color: mode.color, children: [" ", mode.char] })) : null, specialist ? _jsx(Text, { color: UI.note, children: specialist }) : null, _jsxs(Text, { color: UI.text, children: [" ", truncate(agent.task, taskMax)] })] }), _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 })] })] }));
|
|
87
102
|
}
|
|
88
|
-
export function AgentTranscript({ agent, logs, raw = false, scrolled = 0, }) {
|
|
103
|
+
export function AgentTranscript({ agent, logs, raw = false, scrolled = 0, cols = 100, }) {
|
|
89
104
|
const meta = STATE_META[agent.state];
|
|
90
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { children: [_jsx(Text, { color: agent.color, bold: true, children: agent.name }), agent.alias && agent.alias !== agent.name ? _jsxs(Text, { color: UI.muted, children: [" @", agent.alias] }) : null, _jsx(Text, { color: UI.muted, children: " " }), _jsxs(Text, { color: meta.color, bold: true, children: [meta.mark, " ", meta.label] })] }), _jsxs(Text, { color: UI.muted, wrap: "truncate-end", children: [agent.model, " \u00B7 ", formatAgentTelemetry(agent)] })] }), _jsxs(Text, { color: UI.muted, wrap: "wrap", children: ["Task ", _jsx(Text, { color: UI.text, children: agent.task })] }), agent.claims && agent.claims.length > 0 ? (_jsxs(Text, { color: UI.warn, wrap: "truncate-end", children: ["Claims ", agent.claims.join(' ')] })) : null, agent.currentAction ? (_jsxs(Text, { color: UI.accent, wrap: "truncate-end", children: ["Current ", truncate(agent.currentAction, 140)] })) : null, agent.state === 'done' || agent.lastResult ? _jsx(ResultBlock, { agent: agent }) : null, _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: UI.muted, bold: true, children: ["Activity", raw ? ' raw' : ''] }), _jsx(Timeline, { logs: logs, raw: raw })] }), _jsxs(Text, { color: UI.muted, wrap: "truncate-end", children: ["PgUp/PgDn scroll \u00B7 /raw toggles detail \u00B7 Esc returns", scrolled > 0 ? ` · ${scrolled} older` : ''] })] }));
|
|
105
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { children: [_jsx(Text, { color: agent.color, bold: true, children: agent.name }), agent.alias && agent.alias !== agent.name ? _jsxs(Text, { color: UI.muted, children: [" @", agent.alias] }) : null, _jsx(Text, { color: UI.muted, children: " " }), _jsxs(Text, { color: meta.color, bold: true, children: [meta.mark, " ", meta.label] })] }), _jsxs(Text, { color: UI.muted, wrap: "truncate-end", children: [agent.model, " \u00B7 ", formatAgentTelemetry(agent)] })] }), _jsxs(Text, { color: UI.muted, wrap: "wrap", children: ["Task ", _jsx(Text, { color: UI.text, children: agent.task })] }), agent.claims && agent.claims.length > 0 ? (_jsxs(Text, { color: UI.warn, wrap: "truncate-end", children: ["Claims ", agent.claims.join(' ')] })) : null, agent.currentAction ? (_jsxs(Text, { color: UI.accent, wrap: "truncate-end", children: ["Current ", truncate(agent.currentAction, 140)] })) : null, agent.state === 'done' || agent.lastResult ? _jsx(ResultBlock, { agent: agent }) : null, _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: UI.muted, bold: true, children: ["Activity", raw ? ' raw' : ''] }), _jsx(Timeline, { logs: logs, raw: raw, cols: cols })] }), _jsxs(Text, { color: UI.muted, wrap: "truncate-end", children: ["PgUp/PgDn scroll \u00B7 /raw toggles detail \u00B7 Esc returns", scrolled > 0 ? ` · ${scrolled} older` : ''] })] }));
|
|
91
106
|
}
|
|
92
107
|
export function AgentPanel({ agent, logs, width, expanded = false, }) {
|
|
93
108
|
return (_jsx(Box, { width: width, flexDirection: "column", children: expanded ? _jsx(AgentTranscript, { agent: agent, logs: logs }) : _jsx(AgentRow, { agent: agent, logs: logs, cols: 100 }) }));
|
package/dist/ui/App.js
CHANGED
|
@@ -18,7 +18,7 @@ import { SelectList, WizardStep } from './Wizard.js';
|
|
|
18
18
|
import { BRAND, CHROME, STATE, STATE_META, UI, middleTruncate } from './tokens.js';
|
|
19
19
|
const LOGO = 'Parallel';
|
|
20
20
|
// Version from package.json. Hardcoded — rootDir: "src" prevents importing ../../package.json.
|
|
21
|
-
const VERSION = '0.4.
|
|
21
|
+
const VERSION = '0.4.5';
|
|
22
22
|
function usableProvider(config) {
|
|
23
23
|
const p = getProvider(config);
|
|
24
24
|
return p && providerReady(p) && (p.defaultModel || p.models[0]) ? p : undefined;
|
|
@@ -555,7 +555,10 @@ function MainScreen({ ctl, folder, view, focus, rawLogs, systemLines, agentNames
|
|
|
555
555
|
: undefined;
|
|
556
556
|
const [scroll, setScroll] = useState(0);
|
|
557
557
|
const [focusFollowTail, setFocusFollowTail] = useState(true);
|
|
558
|
-
useEffect(() =>
|
|
558
|
+
useEffect(() => {
|
|
559
|
+
setScroll(0);
|
|
560
|
+
setFocusFollowTail(true);
|
|
561
|
+
}, [focus]);
|
|
559
562
|
const FOCUS_LOGS = Math.max(8, bodyHeight - 1);
|
|
560
563
|
const focusedLogs = focused ? ctl.board.logs.filter((l) => l.agentId === focused.id) : [];
|
|
561
564
|
const maxScroll = Math.max(0, focusedLogs.length - FOCUS_LOGS);
|
|
@@ -642,7 +645,7 @@ function MainScreen({ ctl, folder, view, focus, rawLogs, systemLines, agentNames
|
|
|
642
645
|
specialists: 'specialists',
|
|
643
646
|
};
|
|
644
647
|
const viewLabel = VIEW_LABEL[view] ?? 'control room';
|
|
645
|
-
return (_jsxs(Box, { flexDirection: "column", height: rows, children: [_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: CHROME.muted, children: ["\u256D", '─'.repeat(cols - 2), "\u256E"] }), _jsxs(Box, { flexDirection: "row", width: cols, children: [_jsx(Text, { color: CHROME.muted, children: "\u2502 " }), _jsxs(Box, { flexDirection: "row", width: cols - 4, justifyContent: "space-between", children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { bold: true, color: BRAND.primary, children: "PARALLEL" }), _jsx(Text, { color: globalDotColor, children: " \u25CF" }), _jsxs(Text, { color: view === 'agents' ? CHROME.muted : BRAND.muted, children: [" ", viewLabel] }), rawLogs ? _jsx(Text, { color: UI.warn, children: " [RAW]" }) : null] }), _jsx(Text, { color: CHROME.muted, children: middleTruncate(folder, folderMax) })] }), _jsx(Text, { color: CHROME.muted, children: " \u2502" })] }), _jsxs(Box, { flexDirection: "row", width: cols, children: [_jsx(Text, { color: CHROME.muted, children: "\u2502 " }), _jsxs(Box, { flexDirection: "row", width: cols - 4, justifyContent: agents.length > 0 ? 'space-between' : 'flex-end', children: [agents.length > 0 ? (_jsx(Box, { flexDirection: "row", children: _jsxs(Text, { children: [_jsxs(Text, { color: CHROME.muted, children: ["\u25C7 ", idleCount, " idle"] }), ' · ', _jsxs(Text, { color: workingCount > 0 ? STATE.working : CHROME.muted, children: ["\u25CF ", workingCount, " active"] }), ' · ', _jsxs(Text, { color: doneCount > 0 ? STATE.done : CHROME.muted, children: ["\u2713 ", doneCount, " done"] }), ' · ', _jsxs(Text, { color: errorCount > 0 ? STATE.error : CHROME.muted, children: ["\u2717 ", errorCount, " err"] })] }) })) : null, _jsxs(Text, { color: CHROME.muted, children: ["v", VERSION] })] }), _jsx(Text, { color: CHROME.muted, children: " \u2502" })] }), _jsxs(Text, { color: CHROME.muted, children: ["\u2570", '─'.repeat(cols - 2), "\u256F"] })] }), _jsx(Text, { children: " " }), _jsx(Box, { height: bodyHeight, overflow: "hidden", flexDirection: "column", children: view === 'settings' ? (_jsx(SettingsPanel, { ctl: ctl, scope: "global", height: bodyHeight, onClose: onEscape })) : view === 'settings-session' ? (_jsx(SettingsPanel, { ctl: ctl, scope: "session", height: bodyHeight, onClose: onEscape })) : view === 'board' ? (_jsx(BoardView, { board: ctl.board, bodyHeight: bodyHeight })) : view === 'notes' ? (_jsx(NotesView, { board: ctl.board, bodyHeight: bodyHeight })) : view === 'sessions' ? (_jsx(SessionsView, { projectRoot: ctl.projectRoot, bodyHeight: bodyHeight })) : view === 'diff' ? (_jsx(DiffView, { board: ctl.board, bodyHeight: bodyHeight })) : view === 'cost' ? (_jsx(CostView, { board: ctl.board, bodyHeight: bodyHeight })) : view === 'skills' ? (_jsx(SkillsView, { skills: ctl.getSkills(), bodyHeight: bodyHeight })) : view === 'specialists' ? (_jsx(SpecialistsView, { specialists: ctl.getSpecialists(), bodyHeight: bodyHeight })) : view === 'help' ? (_jsx(HelpView, { bodyHeight: bodyHeight })) : agents.length === 0 ? (_jsx(Box, { borderStyle: "single", borderColor: "gray", flexDirection: "column", paddingX: 1, children: _jsx(Text, { color: "gray", children: t('main.empty') }) })) : focused ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(AgentTranscript, { agent: focused, logs: visibleLogs, raw: rawLogs, scrolled: clampedScroll }), !focusFollowTail ? _jsx(Text, { color: UI.warn, children: "Viewing older \u00B7 PgDn to latest" }) : null] })) : (_jsx(AgentHub, { agents: agents, ctl: ctl, cols: cols, scroll: clampedHub, visibleRows: hubRows })) }), systemLines.length > 0 && !settingsOpen && (_jsxs(Box, { flexDirection: "column", children: [agents.length > 0 ? _jsx(Text, { color: UI.muted, bold: true, children: "Session" }) : null, (agents.length > 0
|
|
648
|
+
return (_jsxs(Box, { flexDirection: "column", height: rows, children: [_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: CHROME.muted, children: ["\u256D", '─'.repeat(cols - 2), "\u256E"] }), _jsxs(Box, { flexDirection: "row", width: cols, children: [_jsx(Text, { color: CHROME.muted, children: "\u2502 " }), _jsxs(Box, { flexDirection: "row", width: cols - 4, justifyContent: "space-between", children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { bold: true, color: BRAND.primary, children: "PARALLEL" }), _jsx(Text, { color: globalDotColor, children: " \u25CF" }), _jsxs(Text, { color: view === 'agents' ? CHROME.muted : BRAND.muted, children: [" ", viewLabel] }), rawLogs && focused ? _jsx(Text, { color: UI.warn, children: " [RAW]" }) : null] }), _jsx(Text, { color: CHROME.muted, children: middleTruncate(folder, folderMax) })] }), _jsx(Text, { color: CHROME.muted, children: " \u2502" })] }), _jsxs(Box, { flexDirection: "row", width: cols, children: [_jsx(Text, { color: CHROME.muted, children: "\u2502 " }), _jsxs(Box, { flexDirection: "row", width: cols - 4, justifyContent: agents.length > 0 ? 'space-between' : 'flex-end', children: [agents.length > 0 ? (_jsx(Box, { flexDirection: "row", children: _jsxs(Text, { children: [_jsxs(Text, { color: CHROME.muted, children: ["\u25C7 ", idleCount, " idle"] }), ' · ', _jsxs(Text, { color: workingCount > 0 ? STATE.working : CHROME.muted, children: ["\u25CF ", workingCount, " active"] }), ' · ', _jsxs(Text, { color: doneCount > 0 ? STATE.done : CHROME.muted, children: ["\u2713 ", doneCount, " done"] }), ' · ', _jsxs(Text, { color: errorCount > 0 ? STATE.error : CHROME.muted, children: ["\u2717 ", errorCount, " err"] })] }) })) : null, _jsxs(Text, { color: CHROME.muted, children: ["v", VERSION] })] }), _jsx(Text, { color: CHROME.muted, children: " \u2502" })] }), _jsxs(Text, { color: CHROME.muted, children: ["\u2570", '─'.repeat(cols - 2), "\u256F"] })] }), _jsx(Text, { children: " " }), _jsx(Box, { height: bodyHeight, overflow: "hidden", flexDirection: "column", children: view === 'settings' ? (_jsx(SettingsPanel, { ctl: ctl, scope: "global", height: bodyHeight, onClose: onEscape })) : view === 'settings-session' ? (_jsx(SettingsPanel, { ctl: ctl, scope: "session", height: bodyHeight, onClose: onEscape })) : view === 'board' ? (_jsx(BoardView, { board: ctl.board, bodyHeight: bodyHeight })) : view === 'notes' ? (_jsx(NotesView, { board: ctl.board, bodyHeight: bodyHeight })) : view === 'sessions' ? (_jsx(SessionsView, { projectRoot: ctl.projectRoot, bodyHeight: bodyHeight })) : view === 'diff' ? (_jsx(DiffView, { board: ctl.board, bodyHeight: bodyHeight })) : view === 'cost' ? (_jsx(CostView, { board: ctl.board, bodyHeight: bodyHeight })) : view === 'skills' ? (_jsx(SkillsView, { skills: ctl.getSkills(), bodyHeight: bodyHeight })) : view === 'specialists' ? (_jsx(SpecialistsView, { specialists: ctl.getSpecialists(), bodyHeight: bodyHeight })) : view === 'help' ? (_jsx(HelpView, { bodyHeight: bodyHeight })) : agents.length === 0 ? (_jsx(Box, { borderStyle: "single", borderColor: "gray", flexDirection: "column", paddingX: 1, children: _jsx(Text, { color: "gray", children: t('main.empty') }) })) : focused ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(AgentTranscript, { agent: focused, logs: visibleLogs, raw: rawLogs, scrolled: clampedScroll, cols: cols }), !focusFollowTail ? _jsx(Text, { color: UI.warn, children: "Viewing older \u00B7 PgDn to latest" }) : null] })) : (_jsx(AgentHub, { agents: agents, ctl: ctl, cols: cols, scroll: clampedHub, visibleRows: hubRows })) }), systemLines.length > 0 && !settingsOpen && (_jsxs(Box, { flexDirection: "column", children: [agents.length > 0 ? _jsx(Text, { color: UI.muted, bold: true, children: "Session" }) : null, (agents.length > 0
|
|
646
649
|
? systemLines
|
|
647
650
|
.filter((l) => !/^Ready|^Type a task|^⚡ Ready|^Default \/task|^Agent .* launched/.test(l.text))
|
|
648
651
|
.slice(-2)
|
|
@@ -654,7 +657,7 @@ function MainScreen({ ctl, folder, view, focus, rawLogs, systemLines, agentNames
|
|
|
654
657
|
// Split on \n so multiline i18n messages render correctly (Ink <Text> doesn't interpret \n).
|
|
655
658
|
const lines = l.text.split('\n');
|
|
656
659
|
return lines.map((line, j) => (_jsx(Text, { color: levelColor, wrap: "truncate-end", children: line }, `${i}-${j}`)));
|
|
657
|
-
})] })), approval && (_jsx(ApprovalPrompt, { request: approval, pendingCount: ctl.approvals.length, onAnswer: (id, ok, always) => ctl.answerApproval(id, ok, always) })), question && (_jsx(QuestionPrompt, { question: question, pendingCount: ctl.questions.length, onAnswer: (id, answer, auto) => ctl.answerQuestion(id, answer, auto) }, question.id)), _jsx(CommandInput, { active: inputActive, placeholder: focus ? `Message ${focus} or /command` : 'Task mode: describe work to run · /ask question · /plan proposal · / for commands', agentNames: agentNames, agents: agents, onSubmit: onInput, onEscape: onEscape, notify: notify }), _jsx(Text, { children: " " }), _jsxs(Box, { flexDirection: "column", children: [agents.length === 0 ? (_jsxs(Text, { children: [_jsx(Text, { color: BRAND.muted, children: "/ask /task /plan" }), _jsx(Text, { color: CHROME.muted, children: " \u00B7 Tab autocompletes \u00B7 Esc clears" })] })) : null, _jsxs(Text, { children: [_jsx(Text, { color: CHROME.muted, children: "\u2318 Parallel" }), _jsx(Text, { color: CHROME.muted, children: " \u00B7 Shell " }), _jsx(Text, { color: ctl.session.approvalMode === 'ask' ? UI.warn :
|
|
660
|
+
})] })), approval && (_jsx(ApprovalPrompt, { request: approval, pendingCount: ctl.approvals.length, onAnswer: (id, ok, always) => ctl.answerApproval(id, ok, always) })), question && (_jsx(QuestionPrompt, { question: question, pendingCount: ctl.questions.length, onAnswer: (id, answer, auto) => ctl.answerQuestion(id, answer, auto) }, question.id)), _jsx(CommandInput, { active: inputActive, placeholder: focus ? `Message ${focus} or /command` : 'Task mode: describe work to run · /ask question · /plan proposal · / for commands', context: focus ? 'focus' : 'hub', targetAgent: focused?.name, modelLabel: ctl.sessionProvider() ? `${ctl.sessionProvider()?.name}:${ctl.session.model}` : undefined, agentNames: agentNames, agents: agents, onSubmit: onInput, onEscape: onEscape, notify: notify }), _jsx(Text, { children: " " }), _jsxs(Box, { flexDirection: "column", children: [agents.length === 0 ? (_jsxs(Text, { children: [_jsx(Text, { color: BRAND.muted, children: "/ask /task /plan" }), _jsx(Text, { color: CHROME.muted, children: " \u00B7 Tab autocompletes \u00B7 Esc clears" })] })) : null, _jsxs(Text, { children: [_jsx(Text, { color: CHROME.muted, children: "\u2318 Parallel" }), _jsx(Text, { color: CHROME.muted, children: " \u00B7 Shell " }), _jsx(Text, { color: ctl.session.approvalMode === 'ask' ? UI.warn :
|
|
658
661
|
ctl.session.approvalMode === 'yolo' ? UI.danger :
|
|
659
662
|
UI.ok, children: ctl.session.approvalMode === 'auto-safe' ? 'auto' : ctl.session.approvalMode }), _jsxs(Text, { color: CHROME.muted, children: [" \u00B7 Sessions: ", Controller.listSessions(ctl.projectRoot).length] }), ctl.questions.length > 0 ? (_jsxs(Text, { color: UI.warn, children: [" \u00B7 \u2753", ctl.questions.length] })) : null, ctl.approvals.length > 0 ? (_jsxs(Text, { color: UI.warn, children: [" \u00B7 \u23F3", ctl.approvals.length] })) : null, focused ? (_jsxs(Text, { color: BRAND.muted, children: [" \u00B7 \uD83C\uDFAF ", focused.name] })) : null] })] })] }));
|
|
660
663
|
}
|
package/dist/ui/AttachApp.js
CHANGED
|
@@ -22,6 +22,12 @@ export function parseAttachCommand(text) {
|
|
|
22
22
|
return { type: 'detach' };
|
|
23
23
|
if (v === '/raw')
|
|
24
24
|
return { type: 'raw' };
|
|
25
|
+
const at = v.match(/^@(\S+)\s+(.+)$/s);
|
|
26
|
+
if (at)
|
|
27
|
+
return { type: 'send', target: at[1], text: at[2].trim() };
|
|
28
|
+
const send = v.match(/^\/send\s+(\S+)\s+(.+)$/s);
|
|
29
|
+
if (send)
|
|
30
|
+
return { type: 'send', target: send[1], text: send[2].trim() };
|
|
25
31
|
const m = v.match(/^\/(ask|a|task|t|plan|p)\s+(.+)$/s);
|
|
26
32
|
if (m) {
|
|
27
33
|
const mode = m[1] === 'ask' || m[1] === 'a' ? 'ask' : m[1] === 'plan' || m[1] === 'p' ? 'plan' : 'task';
|
|
@@ -122,6 +128,10 @@ export function AttachApp({ agentRef, sock }) {
|
|
|
122
128
|
wire({ type: 'spawn', text: cmd.text, mode: cmd.mode });
|
|
123
129
|
return;
|
|
124
130
|
}
|
|
131
|
+
if (cmd.type === 'send') {
|
|
132
|
+
wire({ type: 'send', target: cmd.target, text: cmd.text });
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
125
135
|
wire({ type: 'input', agent: agentRef, text: cmd.text });
|
|
126
136
|
};
|
|
127
137
|
const st = info ? STATE_META[info.state] : null;
|
|
@@ -134,18 +144,18 @@ export function AttachApp({ agentRef, sock }) {
|
|
|
134
144
|
* what used to leave stray blank lines in the native scrollback. */
|
|
135
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
|
|
136
146
|
.map((o) => `${o.name} [${stateLabel(o.state)}] ${truncate(o.currentAction || o.task, 40)}`)
|
|
137
|
-
.join(' · ')] })) : null, !raw ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: UI.muted, bold: true, children: t('timeline.activity') }), _jsx(Timeline, { logs: lines.map((l) => l.log) })] })) : null] })) : (
|
|
147
|
+
.join(' · ')] })) : null, !raw ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: UI.muted, bold: true, children: t('timeline.activity') }), _jsx(Timeline, { logs: lines.map((l) => l.log), cols: process.stdout.columns || 100 })] })) : null] })) : (
|
|
138
148
|
/* FULL panel when idle / waiting / done — repaints are rare here. */
|
|
139
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 ? (
|
|
140
150
|
// The session's shared awareness, visible here too: what the
|
|
141
151
|
// OTHER agents are doing right now (live, same feed the agents get).
|
|
142
152
|
_jsxs(Text, { color: UI.muted, wrap: "truncate-end", children: ["Others ", ' ', others
|
|
143
153
|
.map((o) => `${o.name} [${stateLabel(o.state)}] ${truncate(o.currentAction || o.task, 40)}`)
|
|
144
|
-
.join(' · ')] })) : null, !raw ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: UI.muted, bold: true, children: t('timeline.activity') }), _jsx(Timeline, { logs: lines.map((l) => l.log) })] })) : 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) => {
|
|
154
|
+
.join(' · ')] })) : null, !raw ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: UI.muted, bold: true, children: t('timeline.activity') }), _jsx(Timeline, { logs: lines.map((l) => l.log), cols: process.stdout.columns || 100 })] })) : 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) => {
|
|
145
155
|
wire({ type: 'approve', id, approved: ok, always: !!always });
|
|
146
156
|
setApproval(null);
|
|
147
157
|
} })) : question ? (_jsx(QuestionPrompt, { question: { ...question, agentId: info?.id ?? '', resolve: noop }, pendingCount: 1, onAnswer: (id, answer) => {
|
|
148
158
|
wire({ type: 'answer', id, text: answer });
|
|
149
159
|
setQuestion(null);
|
|
150
|
-
} }, question.id)) : null, _jsx(CommandInput, { active: !gone && !interacting, placeholder: t('attach.placeholder', { agent: info?.name ?? agentRef }), onSubmit: send, onEscape: () => exit() }), _jsx(Box, { marginTop: 1, marginBottom: 1, children: _jsx(Text, { color: "yellowBright", wrap: "truncate-end", children: formatAttachFooter(info) }) })] }));
|
|
160
|
+
} }, question.id)) : null, _jsx(CommandInput, { active: !gone && !interacting, placeholder: t('attach.placeholder', { agent: info?.name ?? agentRef }), context: "attach", targetAgent: info?.name ?? agentRef, modelLabel: info?.model, agentNames: [info?.alias, info?.name, ...others.flatMap((o) => [o.alias, o.name])].filter((n) => Boolean(n)), agents: info ? [info] : [], onSubmit: send, onEscape: () => exit() }), _jsx(Box, { marginTop: 1, marginBottom: 1, children: _jsx(Text, { color: "yellowBright", wrap: "truncate-end", children: formatAttachFooter(info) }) })] }));
|
|
151
161
|
}
|
package/dist/ui/CommandInput.js
CHANGED
|
@@ -14,12 +14,23 @@ const GROUP_LABEL = {
|
|
|
14
14
|
git: 'Git/session',
|
|
15
15
|
other: 'Other',
|
|
16
16
|
};
|
|
17
|
-
|
|
17
|
+
const AGENT_ARG_COMMANDS = new Set(['/focus', '/send', '/attach', '/pause', '/resume', '/stop', '/restore', '/commit']);
|
|
18
|
+
function modeHint(value, context, targetAgent) {
|
|
18
19
|
const v = value.trimStart().toLowerCase();
|
|
19
|
-
if (!v)
|
|
20
|
+
if (!v) {
|
|
21
|
+
if (context === 'focus')
|
|
22
|
+
return `Message ${targetAgent ?? 'focused agent'} · / for hub commands · PgUp/PgDn scroll`;
|
|
23
|
+
if (context === 'attach')
|
|
24
|
+
return `Steer ${targetAgent ?? 'agent'} · /task spawns · @all broadcasts · /quit detaches`;
|
|
20
25
|
return 'Default /task · Tab/→ autocomplete · / for commands';
|
|
21
|
-
|
|
26
|
+
}
|
|
27
|
+
if (!v.startsWith('/')) {
|
|
28
|
+
if (context === 'focus')
|
|
29
|
+
return `Will message ${targetAgent ?? 'focused agent'}`;
|
|
30
|
+
if (context === 'attach')
|
|
31
|
+
return `Will steer ${targetAgent ?? 'attached agent'}`;
|
|
22
32
|
return 'Will launch /task';
|
|
33
|
+
}
|
|
23
34
|
if (v.startsWith('/ask') || v === '/a')
|
|
24
35
|
return 'Ask mode · advice only · no edits';
|
|
25
36
|
if (v.startsWith('/task') || v === '/t')
|
|
@@ -38,7 +49,25 @@ export function bestCommandCompletion(value) {
|
|
|
38
49
|
const cmd = matchCommands(value)[0];
|
|
39
50
|
return cmd ? `${cmd.name} ` : null;
|
|
40
51
|
}
|
|
41
|
-
export function
|
|
52
|
+
export function commandNamesForContext(context) {
|
|
53
|
+
if (context !== 'attach')
|
|
54
|
+
return undefined;
|
|
55
|
+
return ['/ask', '/a', '/task', '/t', '/plan', '/p', '/send', '/raw', '/quit', '/exit', '/detach'];
|
|
56
|
+
}
|
|
57
|
+
export function agentArgCommand(value) {
|
|
58
|
+
const m = value.match(/^(\/\S+)\s+([^\s]*)$/);
|
|
59
|
+
if (!m)
|
|
60
|
+
return null;
|
|
61
|
+
const cmd = m[1].toLowerCase();
|
|
62
|
+
return AGENT_ARG_COMMANDS.has(cmd) ? cmd : null;
|
|
63
|
+
}
|
|
64
|
+
export function completeAgentArgument(value, agent) {
|
|
65
|
+
const cmd = agentArgCommand(value);
|
|
66
|
+
if (!cmd)
|
|
67
|
+
return value;
|
|
68
|
+
return `${cmd} ${agent} `;
|
|
69
|
+
}
|
|
70
|
+
export function CommandInput({ active, placeholder, mask, context = 'hub', targetAgent, modelLabel, commandNames, agentNames = [], agents = [], onSubmit, onEscape, notify, }) {
|
|
42
71
|
const [value, setValue] = useState('');
|
|
43
72
|
const [attachments, setAttachments] = useState([]);
|
|
44
73
|
const [history, setHistory] = useState([]);
|
|
@@ -87,11 +116,26 @@ export function CommandInput({ active, placeholder, mask, agentNames = [], agent
|
|
|
87
116
|
setAttachments((arr) => [...arr, { kind: 'image', n, dataUri: img.dataUri, label: img.label }]);
|
|
88
117
|
notify?.(t('input.imageAdded'));
|
|
89
118
|
};
|
|
90
|
-
const
|
|
119
|
+
const uniqueAgentNames = [...new Set(agentNames.filter(Boolean))];
|
|
120
|
+
const allowedCommands = commandNames ?? commandNamesForContext(context);
|
|
121
|
+
const commandAllowed = (c) => !allowedCommands || allowedCommands.includes(c.name) || c.aliases?.some((a) => allowedCommands.includes(a));
|
|
122
|
+
const cmdSuggestions = value.startsWith('/') && !value.includes(' ') ? matchCommands(value).filter(commandAllowed).slice(0, 10) : [];
|
|
91
123
|
const agentSuggestions = value.startsWith('@') && !value.includes(' ')
|
|
92
|
-
? ['all', ...
|
|
124
|
+
? ['all', ...uniqueAgentNames].filter((n) => n.toLowerCase().startsWith(value.slice(1).toLowerCase())).slice(0, 8)
|
|
93
125
|
: [];
|
|
94
|
-
const
|
|
126
|
+
const argCommand = agentArgCommand(value);
|
|
127
|
+
const argPrefix = argCommand ? value.split(/\s+/)[1] ?? '' : '';
|
|
128
|
+
const argSuggestions = argCommand
|
|
129
|
+
? [
|
|
130
|
+
...(argCommand === '/send' || argCommand === '/pause' || argCommand === '/resume' || argCommand === '/stop' || argCommand === '/commit'
|
|
131
|
+
? ['all']
|
|
132
|
+
: []),
|
|
133
|
+
...uniqueAgentNames,
|
|
134
|
+
]
|
|
135
|
+
.filter((n) => n.toLowerCase().startsWith(argPrefix.toLowerCase()))
|
|
136
|
+
.slice(0, 8)
|
|
137
|
+
: [];
|
|
138
|
+
const suggestionCount = cmdSuggestions.length > 0 ? cmdSuggestions.length : agentSuggestions.length > 0 ? agentSuggestions.length : argSuggestions.length;
|
|
95
139
|
const hasSuggestions = suggestionCount > 0;
|
|
96
140
|
const exactCommand = cmdSuggestions.some((c) => c.name === value.toLowerCase() || c.aliases?.some((a) => a === value.toLowerCase()));
|
|
97
141
|
useEffect(() => {
|
|
@@ -112,6 +156,11 @@ export function CommandInput({ active, placeholder, mask, agentNames = [], agent
|
|
|
112
156
|
setValue('@' + agent + ' ');
|
|
113
157
|
return true;
|
|
114
158
|
}
|
|
159
|
+
if (argSuggestions.length > 0) {
|
|
160
|
+
const agent = argSuggestions[Math.min(selectedSuggestion, argSuggestions.length - 1)];
|
|
161
|
+
setValue(completeAgentArgument(value, agent));
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
115
164
|
return false;
|
|
116
165
|
};
|
|
117
166
|
useInput((input, key) => {
|
|
@@ -211,10 +260,12 @@ export function CommandInput({ active, placeholder, mask, agentNames = [], agent
|
|
|
211
260
|
const shown = mask ? '•'.repeat(value.length) : value;
|
|
212
261
|
const byName = new Map(agents.flatMap((a) => [[a.name, a], [a.alias, a]]));
|
|
213
262
|
const commandIndexes = new Map(cmdSuggestions.map((c, i) => [c.name, i]));
|
|
214
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "gray", wrap: "truncate-end", children: modeHint(value) }), cmdSuggestions.length > 0 && (_jsx(Box, { borderStyle: "single", borderColor: "gray", flexDirection: "column", paddingX: 1, children: groupedCommands(cmdSuggestions).map(([group, commands]) => (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "gray", bold: true, children: GROUP_LABEL[group] ?? group }), commands.map((c) => ((() => {
|
|
263
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", gap: 1, children: [_jsx(Text, { color: "gray", wrap: "truncate-end", children: modeHint(value, context, targetAgent) }), _jsxs(Text, { color: "cyan", children: ["[", context, "]"] }), targetAgent ? _jsxs(Text, { color: "magenta", children: ["[", targetAgent, "]"] }) : null, modelLabel ? _jsxs(Text, { color: "yellow", children: ["[", modelLabel, "]"] }) : null] }), cmdSuggestions.length > 0 && (_jsx(Box, { borderStyle: "single", borderColor: "gray", flexDirection: "column", paddingX: 1, children: groupedCommands(cmdSuggestions).map(([group, commands]) => (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "gray", bold: true, children: GROUP_LABEL[group] ?? group }), commands.map((c) => ((() => {
|
|
215
264
|
const selected = commandIndexes.get(c.name) === selectedSuggestion;
|
|
216
265
|
return (_jsxs(Text, { children: [_jsxs(Text, { color: selected ? 'cyanBright' : 'cyan', bold: true, children: [selected ? '› ' : ' ', c.name.padEnd(14)] }), _jsx(Text, { color: "yellow", children: c.args.padEnd(22) }), _jsx(Text, { color: "gray", children: t(c.descKey) })] }, c.name));
|
|
217
266
|
})()))] }, group))) })), agentSuggestions.length > 0 && (_jsx(Box, { borderStyle: "single", borderColor: "gray", flexDirection: "column", paddingX: 1, children: agentSuggestions.map((n, i) => (_jsxs(Text, { children: [_jsxs(Text, { color: i === selectedSuggestion ? 'cyanBright' : 'cyan', bold: true, children: [i === selectedSuggestion ? '› ' : ' ', "@", n.padEnd(10)] }), _jsx(Text, { color: "gray", children: n === 'all'
|
|
218
267
|
? t('input.atAll')
|
|
219
|
-
: `${byName.get(n)?.state ?? ''} ${byName.get(n)?.mode ? `/${byName.get(n)?.mode}` : ''}` })] }, n))) })),
|
|
268
|
+
: `${byName.get(n)?.state ?? ''} ${byName.get(n)?.mode ? `/${byName.get(n)?.mode}` : ''}` })] }, n))) })), argSuggestions.length > 0 && (_jsxs(Box, { borderStyle: "single", borderColor: "gray", flexDirection: "column", paddingX: 1, children: [_jsx(Text, { color: "gray", bold: true, children: "Agents" }), argSuggestions.map((n, i) => (_jsxs(Text, { children: [_jsxs(Text, { color: i === selectedSuggestion ? 'cyanBright' : 'cyan', bold: true, children: [i === selectedSuggestion ? '› ' : ' ', n.padEnd(12)] }), _jsx(Text, { color: "gray", children: n === 'all'
|
|
269
|
+
? t('input.atAll')
|
|
270
|
+
: `${byName.get(n)?.state ?? ''} ${byName.get(n)?.mode ? `/${byName.get(n)?.mode}` : ''}` })] }, n)))] })), attachments.length > 0 && (_jsx(Box, { flexDirection: "row", gap: 1, paddingX: 1, children: attachments.map((a) => (_jsxs(Text, { color: "cyan", backgroundColor: "gray", children: [' ', a.kind === 'paste' ? t('input.attPaste', { n: a.n, lines: a.lines }) : t('input.attImage', { n: a.n, file: a.label }), ' '] }, a.n))) })), _jsxs(Box, { borderStyle: "single", borderColor: active ? 'cyan' : 'gray', paddingX: 1, children: [_jsxs(Text, { color: "cyanBright", bold: true, children: ["\u203A", ' '] }), shown ? (_jsxs(_Fragment, { children: [_jsx(Text, { children: shown }), active && _jsx(Text, { color: "cyanBright", children: "\u2588" })] })) : (_jsxs(_Fragment, { children: [active && _jsx(Text, { color: "cyanBright", children: "\u2588" }), _jsx(Text, { color: "gray", children: placeholder })] }))] })] }));
|
|
220
271
|
}
|
package/dist/ui/Timeline.js
CHANGED
|
@@ -26,20 +26,22 @@ function itemColor(item) {
|
|
|
26
26
|
return UI.text;
|
|
27
27
|
return UI.text;
|
|
28
28
|
}
|
|
29
|
-
function OutputLines({ item }) {
|
|
29
|
+
function OutputLines({ item, cols }) {
|
|
30
30
|
if (!item.output || item.output.length === 0)
|
|
31
31
|
return null;
|
|
32
|
-
|
|
32
|
+
const max = Math.max(40, cols - 8);
|
|
33
|
+
return (_jsxs(Box, { flexDirection: "column", children: [item.output.map((line, i) => (_jsxs(Text, { color: item.status === 'error' ? UI.danger : UI.muted, wrap: "truncate-end", children: [_jsx(Text, { color: UI.muted, children: i === 0 ? '└ ' : ' ' }), truncate(line, max)] }, `${item.seq ?? 0}-out-${i}`))), item.hiddenLines && item.hiddenLines > 0 ? (_jsxs(Text, { color: UI.muted, children: [' ', t('timeline.hiddenLines', { count: item.hiddenLines })] })) : null] }));
|
|
33
34
|
}
|
|
34
|
-
function TimelineRow({ item }) {
|
|
35
|
+
function TimelineRow({ item, cols }) {
|
|
36
|
+
const max = Math.max(40, cols - 8);
|
|
35
37
|
if (item.kind === 'section') {
|
|
36
|
-
return _jsx(Text, { color: UI.muted, children:
|
|
38
|
+
return _jsx(Text, { color: UI.muted, children: '─'.repeat(Math.min(Math.max(20, cols - 4), 80)) });
|
|
37
39
|
}
|
|
38
40
|
if (item.kind === 'narration') {
|
|
39
41
|
return (_jsx(Box, { marginTop: 1, marginBottom: 1, children: _jsx(Text, { color: UI.text, wrap: "wrap", children: item.detail }) }));
|
|
40
42
|
}
|
|
41
43
|
if (item.kind === 'command') {
|
|
42
|
-
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: item.status === 'error' ? UI.danger : UI.text, wrap: "truncate-end", children: [_jsx(Text, { color: UI.muted, children: "\u2022 " }), _jsxs(Text, { bold: true, children: [t('timeline.ran'), " "] }), _jsx(Text, { color: UI.accent, children: truncate(item.command ?? '',
|
|
44
|
+
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 })] }));
|
|
43
45
|
}
|
|
44
46
|
if (item.kind === 'files') {
|
|
45
47
|
const files = item.files ?? [];
|
|
@@ -48,13 +50,13 @@ function TimelineRow({ item }) {
|
|
|
48
50
|
return (_jsxs(Text, { color: itemColor(item), wrap: "truncate-end", children: [_jsx(Text, { color: UI.muted, children: "\u2022 " }), _jsxs(Text, { bold: true, children: [fileLabel(item.label, files.length), " "] }), _jsxs(Text, { color: UI.muted, children: [shown, extra] })] }));
|
|
49
51
|
}
|
|
50
52
|
if (item.output) {
|
|
51
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: itemColor(item), wrap: "truncate-end", children: [_jsx(Text, { color: UI.muted, children: "\u2022 " }), item.label] }), _jsx(OutputLines, { item: item })] }));
|
|
53
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: itemColor(item), wrap: "truncate-end", children: [_jsx(Text, { color: UI.muted, children: "\u2022 " }), item.label] }), _jsx(OutputLines, { item: item, cols: cols })] }));
|
|
52
54
|
}
|
|
53
|
-
return (_jsxs(Text, { color: itemColor(item), italic: item.kind === 'thought', wrap: "truncate-end", children: [_jsx(Text, { color: UI.muted, children: "\u2022 " }), truncate(item.detail ? `${item.label} ${item.detail}` : item.label,
|
|
55
|
+
return (_jsxs(Text, { color: itemColor(item), italic: item.kind === 'thought', wrap: "truncate-end", children: [_jsx(Text, { color: UI.muted, children: "\u2022 " }), truncate(item.detail ? `${item.label} ${item.detail}` : item.label, max)] }));
|
|
54
56
|
}
|
|
55
|
-
export function Timeline({ logs, raw = false, emptyText }) {
|
|
57
|
+
export function Timeline({ logs, raw = false, emptyText, cols = 100 }) {
|
|
56
58
|
const items = presentTimeline(logs, { raw, outputLines: raw ? 10 : 6 });
|
|
57
59
|
if (items.length === 0)
|
|
58
60
|
return _jsx(Text, { color: UI.muted, children: emptyText ?? t('timeline.empty') });
|
|
59
|
-
return (_jsx(Box, { flexDirection: "column", children: items.map((item, i) => item.kind === 'section' ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(TimelineRow, { item: item }), _jsx(Text, { color: UI.muted, children: sectionLabel(item.category) })] }, `${item.seq ?? i}-section`)) : (_jsx(TimelineRow, { item: item }, `${item.seq ?? i}-${i}`))) }));
|
|
61
|
+
return (_jsx(Box, { flexDirection: "column", children: items.map((item, i) => item.kind === 'section' ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(TimelineRow, { item: item, cols: cols }), _jsx(Text, { color: UI.muted, children: sectionLabel(item.category) })] }, `${item.seq ?? i}-section`)) : (_jsx(TimelineRow, { item: item, cols: cols }, `${item.seq ?? i}-${i}`))) }));
|
|
60
62
|
}
|
package/dist/ui/views.js
CHANGED
|
@@ -60,7 +60,8 @@ export function BoardView({ board, bodyHeight }) {
|
|
|
60
60
|
const sideRows = bodyHeight ? Math.max(1, Math.floor((bodyHeight - visibleAgents - 5) / 2)) : 8;
|
|
61
61
|
const activities = [...board.fileActivity.values()].sort((a, b) => b.ts - a.ts).slice(0, sideRows);
|
|
62
62
|
const notes = board.notes.slice(-sideRows);
|
|
63
|
-
|
|
63
|
+
const warnings = board.workMapWarnings.slice(-Math.max(2, Math.min(4, sideRows)));
|
|
64
|
+
return (_jsxs(Box, { borderStyle: "round", borderColor: "yellow", flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: "yellow", children: t('board.title') }), _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: "yellow", children: [" \u00B7 ", truncate(a.claims.join(', '), 45)] }) : null] }, a.id))), _jsx(Below, { n: below })] })), _jsx(Text, { bold: true, children: t('board.activity') }), warnings.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, color: "yellowBright", children: t('board.workMap') }), warnings.map((w) => (_jsxs(Text, { wrap: "truncate-end", children: [' ', _jsxs(Text, { color: w.level === 'conflict' ? 'redBright' : 'yellow', children: [w.level === 'conflict' ? '!' : '⚠', " "] }), _jsx(Text, { color: "yellow", children: w.title }), _jsxs(Text, { color: "gray", children: [" \u2014 ", truncate(w.detail, 120)] })] }, w.id)))] })) : null, 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)))] }));
|
|
64
65
|
}
|
|
65
66
|
export function NotesView({ board, bodyHeight }) {
|
|
66
67
|
const fallbackVisible = useVisibleRows(7);
|
package/package.json
CHANGED