@parallel-cli/parallel 0.4.4 → 0.4.6
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 +50 -0
- package/README.md +57 -9
- package/dist/agents/tools.js +58 -2
- package/dist/commands.js +31 -0
- package/dist/controller.js +33 -8
- package/dist/coordination/blackboard.js +75 -0
- package/dist/i18n.js +24 -4
- package/dist/index.js +9 -0
- package/dist/server.js +10 -0
- package/dist/ui/AgentPanel.js +24 -13
- package/dist/ui/App.js +24 -24
- package/dist/ui/AttachApp.js +14 -4
- package/dist/ui/CommandInput.js +137 -40
- package/dist/ui/Md.js +4 -3
- package/dist/ui/SettingsPanel.js +2 -1
- package/dist/ui/Timeline.js +11 -9
- package/dist/ui/Wizard.js +11 -5
- package/dist/ui/theme.js +3 -3
- package/dist/ui/tokens.js +11 -6
- package/dist/ui/views.js +49 -11
- package/dist/update.js +125 -0
- package/dist/version.js +2 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,56 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to Parallel are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.4.6 - 2026-06-24
|
|
6
|
+
|
|
7
|
+
### 0.4.6 Added
|
|
8
|
+
|
|
9
|
+
- Added an interactive npm update prompt on startup with daily cache, CI/headless/attach skips, `PARALLEL_SKIP_UPDATE_CHECK=1`, and `--no-update`.
|
|
10
|
+
- Added a Codex-like empty hub with a quieter framed header, cream-toned accents, and a full-width prompt block with distinct background.
|
|
11
|
+
- Added a complete paginated slash palette driven by one deterministic rendered order.
|
|
12
|
+
- Added selectable `/help` command navigation with visible highlight and Enter-to-run behavior.
|
|
13
|
+
- Added localized prompt placeholder copy and an i18n audit to keep all used UI keys translated.
|
|
14
|
+
|
|
15
|
+
### 0.4.6 Changed
|
|
16
|
+
|
|
17
|
+
- Reduced default Hub noise by removing persistent startup toasts, duplicate task hints, and always-on command footer lines.
|
|
18
|
+
- Replaced blue/cyan UI accents with a softer cream theme across hub, palettes, wizard/settings lists, markdown summaries, and attached agent terminals.
|
|
19
|
+
- Made the prompt block start at three rows, grow when input wraps, and show the blinking cursor on the first placeholder character while empty.
|
|
20
|
+
- Reused the same minimal prompt treatment in dedicated agent terminals and toned down their footer.
|
|
21
|
+
- Simplified agent rows so secondary telemetry only appears when there is a useful latest signal or result.
|
|
22
|
+
- Moved command palette priority to shared command helpers so autocomplete, help, and rendering stay aligned.
|
|
23
|
+
|
|
24
|
+
### 0.4.6 Fixed
|
|
25
|
+
|
|
26
|
+
- Fixed slash palette Up/Down navigation jumping to visually unrelated commands.
|
|
27
|
+
- Fixed `/help` being scrollable but not actually selectable.
|
|
28
|
+
- Fixed Wizard and Settings selection lists wrapping around unexpectedly by clamping navigation at list boundaries.
|
|
29
|
+
|
|
30
|
+
## 0.4.5 - 2026-06-23
|
|
31
|
+
|
|
32
|
+
### 0.4.5 Added
|
|
33
|
+
|
|
34
|
+
- Added explicit hub, focus, and attach input contexts with context-specific hints and filtered command suggestions.
|
|
35
|
+
- Added agent argument autocomplete for `/focus`, `/send`, `/attach`, `/pause`, `/resume`, `/stop`, `/restore`, and `/commit`.
|
|
36
|
+
- Added attach-terminal routing for `@all`, `@agent`, and `/send`, so dedicated terminals can broadcast or steer another agent through the main session.
|
|
37
|
+
- Added richer hub rows using latest useful agent signals, specialist badges, context percentage, and responsive timeline widths.
|
|
38
|
+
- Added durable session memory for claims, recent diff excerpts, file activity, work-map warnings, agent aliases, provider/model metadata, specialist, and context usage.
|
|
39
|
+
- Added shell mutation tracking: files changed by `run_command` now appear in live diffs, file activity, and agent commit ownership.
|
|
40
|
+
- Added a non-blocking work map that surfaces overlapping claims and repeated co-edit conflicts in `/board` and agent context.
|
|
41
|
+
|
|
42
|
+
### 0.4.5 Changed
|
|
43
|
+
|
|
44
|
+
- Clarified agent naming around `Name: task` syntax in README examples.
|
|
45
|
+
- Made `/raw` visible only when it affects the active focus view.
|
|
46
|
+
- Reset focus scroll follow-tail behavior when switching focused agents.
|
|
47
|
+
- Improved session restore so `/restore` can target saved agents by name, alias, or id when their conversation is available.
|
|
48
|
+
|
|
49
|
+
### 0.4.5 Fixed
|
|
50
|
+
|
|
51
|
+
- Fixed attach-terminal `@all` being treated as plain text to the attached agent.
|
|
52
|
+
- Fixed restored note ids drifting by resynchronizing blackboard note and change sequences after loading session data.
|
|
53
|
+
- Fixed shell-created edits being invisible to `/diff`, `/board`, and `/commit`.
|
|
54
|
+
|
|
5
55
|
## 0.4.4 - 2026-06-23
|
|
6
56
|
|
|
7
57
|
### 0.4.4 Added
|
package/README.md
CHANGED
|
@@ -16,13 +16,17 @@ 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
|
+
- Use a cleaner Codex-like hub with a framed header, focused prompt bar, and quieter empty state.
|
|
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.
|
|
25
28
|
- Keep shell execution controlled with `ask`, `auto-safe`, or `yolo` approvals.
|
|
29
|
+
- Get prompted for npm updates at startup, with an explicit skip path.
|
|
26
30
|
- Save and restore project sessions.
|
|
27
31
|
- Run headless multi-agent jobs for CI or scripts.
|
|
28
32
|
|
|
@@ -79,9 +83,9 @@ Plain text launches a `/task` agent. You can launch another agent while the firs
|
|
|
79
83
|
Use explicit modes when intent matters:
|
|
80
84
|
|
|
81
85
|
```text
|
|
82
|
-
/ask
|
|
83
|
-
/plan
|
|
84
|
-
/task
|
|
86
|
+
/ask Reviewer: should we split the CLI parser?
|
|
87
|
+
/plan Migration: propose the safest rollout for the config change
|
|
88
|
+
/task Builder: implement the approved plan
|
|
85
89
|
```
|
|
86
90
|
|
|
87
91
|
Steer a running agent:
|
|
@@ -114,7 +118,9 @@ Plain text is equivalent to `/task`.
|
|
|
114
118
|
|
|
115
119
|
## Control Room
|
|
116
120
|
|
|
117
|
-
The main TUI is the Parallel hub.
|
|
121
|
+
The main TUI is the Parallel hub. The default view stays intentionally quiet: a Codex-like framed header, cream-toned accents, a focused prompt block, and detailed status moved into explicit views.
|
|
122
|
+
|
|
123
|
+
It is designed to answer:
|
|
118
124
|
|
|
119
125
|
- what needs your input
|
|
120
126
|
- which agents are working
|
|
@@ -122,6 +128,19 @@ The main TUI is the Parallel hub. It is designed to answer:
|
|
|
122
128
|
- what changed in the project
|
|
123
129
|
- what model, provider, shell mode, and cost are active
|
|
124
130
|
|
|
131
|
+
Input has three explicit contexts:
|
|
132
|
+
|
|
133
|
+
- 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`.
|
|
134
|
+
- Focus: after `/focus a1`, plain text talks to the focused agent instead of spawning a new one. `/raw` affects this view only.
|
|
135
|
+
- 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.
|
|
136
|
+
|
|
137
|
+
Use `Name: task` when naming an agent:
|
|
138
|
+
|
|
139
|
+
```text
|
|
140
|
+
/task Tests: add regression coverage for the auth middleware
|
|
141
|
+
/plan Migration: outline the safest database rollout
|
|
142
|
+
```
|
|
143
|
+
|
|
125
144
|
Common hub commands:
|
|
126
145
|
|
|
127
146
|
- `/agents`: agent overview.
|
|
@@ -141,9 +160,11 @@ Commands are typed in the control room input. When a long view is open, use Esca
|
|
|
141
160
|
Keyboard behavior:
|
|
142
161
|
|
|
143
162
|
- `/` opens slash command suggestions.
|
|
144
|
-
- Up/Down selects suggestions
|
|
163
|
+
- Up/Down selects suggestions in the same order they appear.
|
|
145
164
|
- Enter accepts the selected suggestion.
|
|
146
165
|
- Tab or Right accepts the best completion.
|
|
166
|
+
- `/help` is keyboard navigable: Up/Down moves the visible selection, PgUp/PgDn pages, and Enter runs the selected command.
|
|
167
|
+
- Wizard, settings, slash suggestions, and help views use clamped keyboard selection so the highlight does not jump, disappear, or wrap unexpectedly.
|
|
147
168
|
- PgUp/PgDn scrolls the hub or focus view even while the input is active. Up/Down scrolls long views and navigates suggestions/history.
|
|
148
169
|
- Escape returns to the agents view or clears the input.
|
|
149
170
|
|
|
@@ -170,9 +191,12 @@ From an attached terminal:
|
|
|
170
191
|
|
|
171
192
|
```text
|
|
172
193
|
plain text sends a message to this agent
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
/
|
|
194
|
+
@all pause public interface changes until tests finish
|
|
195
|
+
@a2 re-read the API client before editing it
|
|
196
|
+
/send a2 check the new parser contract
|
|
197
|
+
/task Tests: write parser regression tests
|
|
198
|
+
/ask Reviewer: is this result safe to merge?
|
|
199
|
+
/plan Migration: prepare a migration plan
|
|
176
200
|
/raw
|
|
177
201
|
/quit
|
|
178
202
|
```
|
|
@@ -220,6 +244,19 @@ Environment variables:
|
|
|
220
244
|
- `PARALLEL_BASE_URL`: override the default provider base URL.
|
|
221
245
|
- `PARALLEL_MODEL`: override the session model.
|
|
222
246
|
- `PARALLEL_NO_ALT_SCREEN=1`: disable the alternate terminal screen.
|
|
247
|
+
- `PARALLEL_SKIP_UPDATE_CHECK=1`: disable npm update checks.
|
|
248
|
+
|
|
249
|
+
## Updates
|
|
250
|
+
|
|
251
|
+
On interactive startup, Parallel checks npm at most once per day for a newer `@parallel-cli/parallel` version. The check is skipped in `attach`, `--headless`, `--first-run`, CI, non-TTY sessions, or when `PARALLEL_SKIP_UPDATE_CHECK=1` is set.
|
|
252
|
+
|
|
253
|
+
When an update is available, Parallel asks before running:
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
npm install -g @parallel-cli/parallel
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
If the update succeeds, restart Parallel to run the new version. Use `parallel --no-update` for a one-off launch without checking.
|
|
223
260
|
|
|
224
261
|
## Commands
|
|
225
262
|
|
|
@@ -266,7 +303,14 @@ Environment variables:
|
|
|
266
303
|
- `/save [name]`: save the current session.
|
|
267
304
|
- `/sessions`: list saved sessions.
|
|
268
305
|
- `/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
|
|
306
|
+
- `/restore <agent>`: relaunch a restored agent by name, alias, or saved id when its conversation history is still available.
|
|
307
|
+
|
|
308
|
+
Session memory has two layers:
|
|
309
|
+
|
|
310
|
+
- Live memory: active agents see statuses, notes, claims, work-map warnings, file activity, and recent diffs before every model action.
|
|
311
|
+
- 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.
|
|
312
|
+
|
|
313
|
+
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
314
|
|
|
271
315
|
### Settings And Exit
|
|
272
316
|
|
|
@@ -336,6 +380,10 @@ When an agent writes a file:
|
|
|
336
380
|
|
|
337
381
|
This keeps agents moving without allowing silent overwrites.
|
|
338
382
|
|
|
383
|
+
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.
|
|
384
|
+
|
|
385
|
+
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.
|
|
386
|
+
|
|
339
387
|
## Headless Mode
|
|
340
388
|
|
|
341
389
|
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/commands.js
CHANGED
|
@@ -60,12 +60,43 @@ export const COMMANDS = [
|
|
|
60
60
|
export function visibleCommands() {
|
|
61
61
|
return COMMANDS.filter((c) => !c.hidden);
|
|
62
62
|
}
|
|
63
|
+
const COMMAND_GROUP_ORDER = ['modes', 'control', 'views', 'settings', 'git', 'other'];
|
|
64
|
+
const COMMAND_PALETTE_PRIORITY = [
|
|
65
|
+
'/ask',
|
|
66
|
+
'/task',
|
|
67
|
+
'/plan',
|
|
68
|
+
'/send',
|
|
69
|
+
'/focus',
|
|
70
|
+
'/attach',
|
|
71
|
+
'/agents',
|
|
72
|
+
'/board',
|
|
73
|
+
'/diff',
|
|
74
|
+
'/settings',
|
|
75
|
+
'/help',
|
|
76
|
+
'/quit',
|
|
77
|
+
];
|
|
78
|
+
function commandRank(c) {
|
|
79
|
+
const priority = COMMAND_PALETTE_PRIORITY.indexOf(c.name);
|
|
80
|
+
if (priority !== -1)
|
|
81
|
+
return priority;
|
|
82
|
+
const group = COMMAND_GROUP_ORDER.indexOf(c.group ?? 'other');
|
|
83
|
+
return COMMAND_PALETTE_PRIORITY.length + group * 100 + COMMANDS.indexOf(c);
|
|
84
|
+
}
|
|
85
|
+
export function sortCommandsForPalette(commands) {
|
|
86
|
+
return [...commands].sort((a, b) => commandRank(a) - commandRank(b) || a.name.localeCompare(b.name));
|
|
87
|
+
}
|
|
63
88
|
export function matchCommands(input, opts = {}) {
|
|
64
89
|
if (!input.startsWith('/'))
|
|
65
90
|
return [];
|
|
66
91
|
const word = input.split(/\s+/)[0].toLowerCase();
|
|
67
92
|
return COMMANDS.filter((c) => opts.includeHidden || !c.hidden).filter((c) => c.name.startsWith(word) || c.aliases?.some((a) => a.startsWith(word)));
|
|
68
93
|
}
|
|
94
|
+
export function commandPalette(input, opts = {}) {
|
|
95
|
+
const allowed = opts.allowedNames
|
|
96
|
+
? (c) => opts.allowedNames.includes(c.name) || c.aliases?.some((a) => opts.allowedNames.includes(a))
|
|
97
|
+
: () => true;
|
|
98
|
+
return sortCommandsForPalette(matchCommands(input, opts).filter(allowed));
|
|
99
|
+
}
|
|
69
100
|
function agentList(ctl) {
|
|
70
101
|
return [...ctl.board.agents.values()].map((a) => a.name).join(', ') || t('m.none');
|
|
71
102
|
}
|
package/dist/controller.js
CHANGED
|
@@ -9,7 +9,7 @@ import { saveConfig, getProvider, upsertProvider } from './config.js';
|
|
|
9
9
|
import { priceFor, fmtCost } from './pricing.js';
|
|
10
10
|
import { loadSkills, loadSpecialists } from './skills.js';
|
|
11
11
|
import { t } from './i18n.js';
|
|
12
|
-
const AGENT_COLORS = ['
|
|
12
|
+
const AGENT_COLORS = ['#f3e7c7', 'magenta', 'yellow', 'green', 'whiteBright', 'redBright', '#c8bfa6', 'magentaBright'];
|
|
13
13
|
export function normalizeShellApprovalMode(mode) {
|
|
14
14
|
if (mode === 'ask' || mode === 'auto-safe' || mode === 'yolo')
|
|
15
15
|
return mode;
|
|
@@ -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
|
@@ -76,6 +76,10 @@ const en = {
|
|
|
76
76
|
'main.ready1': '⚡ Ready — folder: {folder}',
|
|
77
77
|
'main.ready2': 'Type a task + Enter to launch your first agent. /help for help.',
|
|
78
78
|
'main.empty': 'No agents yet. Type a task + Enter to launch your first agent — then launch more at any time, even while they work.',
|
|
79
|
+
'main.prompt': 'Example: Redesign the UI',
|
|
80
|
+
'main.emptyCard.tagline': 'Multi-agent coding from one terminal.',
|
|
81
|
+
'main.emptyCard.cta': 'Describe work below to launch the first agent.',
|
|
82
|
+
'main.emptyCard.hints': '/ for commands · @agent to steer · /help for shortcuts',
|
|
79
83
|
'main.status': 'Enter = new agent N+1 (even while others work) · @Name = real-time instruction · /help · views: /agents /board /diff /notes',
|
|
80
84
|
'main.placeholder': 'Type a task (= new agent N+1) · @Agent message · /command',
|
|
81
85
|
'agent.summary': 'Summary',
|
|
@@ -99,6 +103,7 @@ const en = {
|
|
|
99
103
|
'board.agents': 'Agents:',
|
|
100
104
|
'board.none': '(no agents)',
|
|
101
105
|
'board.activity': 'Who works where (file activity, no locks):',
|
|
106
|
+
'board.workMap': 'Work map warnings:',
|
|
102
107
|
'board.noActivity': '(no edits yet)',
|
|
103
108
|
'board.notes': 'Latest notes:',
|
|
104
109
|
'notes.title': '✉ INTER-AGENT NOTES (last 30)',
|
|
@@ -239,7 +244,7 @@ const en = {
|
|
|
239
244
|
'attach.placeholder': 'Message to {agent} (Enter to send, /quit to detach)…',
|
|
240
245
|
'attach.waiting': 'Waiting for agent {agent}… (is the main Parallel session running?)',
|
|
241
246
|
'attach.gone': 'Session ended — this terminal is detached.',
|
|
242
|
-
'attach.hint': 'plain text steers this agent ·
|
|
247
|
+
'attach.hint': 'plain text steers this agent · @all broadcasts · /send routes · /task creates · /quit detaches',
|
|
243
248
|
'grid.above': '▲ {n} agent(s) above — PgUp',
|
|
244
249
|
'grid.below': '▼ {n} agent(s) below — PgDn',
|
|
245
250
|
'm.nothing': 'Nothing to save yet.',
|
|
@@ -453,6 +458,10 @@ const fr = {
|
|
|
453
458
|
'main.ready1': '⚡ Prêt — dossier : {folder}',
|
|
454
459
|
'main.ready2': "Tape une tâche + Entrée pour lancer ton premier agent. /help pour l'aide.",
|
|
455
460
|
'main.empty': "Aucun agent pour le moment. Tape une tâche + Entrée pour lancer ton premier agent — puis relances-en d'autres à tout moment, même pendant qu'ils travaillent.",
|
|
461
|
+
'main.prompt': 'Exemple : Fais moi une refonte UI',
|
|
462
|
+
'main.emptyCard.tagline': 'Code multi-agent depuis un terminal.',
|
|
463
|
+
'main.emptyCard.cta': 'Décris le travail ci-dessous pour lancer le premier agent.',
|
|
464
|
+
'main.emptyCard.hints': '/ pour les commandes · @agent pour piloter · /help pour les raccourcis',
|
|
456
465
|
'main.status': 'Entrée = nouvel agent N+1 (même pendant que les autres travaillent) · @Nom = instruction temps réel · /help · vues : /agents /board /diff /notes',
|
|
457
466
|
'main.placeholder': 'Tape une tâche (= nouvel agent N+1) · @Agent message · /commande',
|
|
458
467
|
'agent.summary': 'Récapitulatif',
|
|
@@ -473,6 +482,7 @@ const fr = {
|
|
|
473
482
|
'board.agents': 'Agents :',
|
|
474
483
|
'board.none': '(aucun agent)',
|
|
475
484
|
'board.activity': 'Qui travaille où (activité fichiers, sans verrou) :',
|
|
485
|
+
'board.workMap': 'Alertes work map :',
|
|
476
486
|
'board.noActivity': '(aucune modification pour le moment)',
|
|
477
487
|
'board.notes': 'Dernières notes :',
|
|
478
488
|
'notes.title': '✉ NOTES INTER-AGENTS (30 dernières)',
|
|
@@ -611,7 +621,7 @@ const fr = {
|
|
|
611
621
|
'attach.placeholder': 'Message à {agent} (Entrée pour envoyer, /quit pour détacher)…',
|
|
612
622
|
'attach.waiting': 'En attente de l’agent {agent}… (la session Parallel principale tourne-t-elle ?)',
|
|
613
623
|
'attach.gone': 'Session terminée — ce terminal est détaché.',
|
|
614
|
-
'attach.hint': 'texte libre pilote cet agent ·
|
|
624
|
+
'attach.hint': 'texte libre pilote cet agent · @all diffuse · /send route · /task crée · /quit détache',
|
|
615
625
|
'grid.above': '▲ {n} agent(s) au-dessus — PgUp',
|
|
616
626
|
'grid.below': '▼ {n} agent(s) en dessous — PgDn',
|
|
617
627
|
'm.nothing': 'Rien à sauvegarder pour le moment.',
|
|
@@ -818,6 +828,10 @@ const es = {
|
|
|
818
828
|
'main.ready1': '⚡ Listo — carpeta: {folder}',
|
|
819
829
|
'main.ready2': 'Escribe una tarea + Enter para lanzar tu primer agente. /help para ayuda.',
|
|
820
830
|
'main.empty': 'Aún no hay agentes. Escribe una tarea + Enter para lanzar tu primer agente — luego lanza más en cualquier momento, incluso mientras trabajan.',
|
|
831
|
+
'main.prompt': 'Ejemplo: hazme un rediseño de UI',
|
|
832
|
+
'main.emptyCard.tagline': 'Código multiagente desde un terminal.',
|
|
833
|
+
'main.emptyCard.cta': 'Describe el trabajo abajo para lanzar el primer agente.',
|
|
834
|
+
'main.emptyCard.hints': '/ para comandos · @agente para dirigir · /help para atajos',
|
|
821
835
|
'main.status': 'Enter = nuevo agente N+1 (incluso mientras otros trabajan) · @Nombre = instrucción en tiempo real · /help · vistas: /agents /board /diff /notes',
|
|
822
836
|
'main.placeholder': 'Escribe una tarea (= nuevo agente N+1) · @Agente mensaje · /comando',
|
|
823
837
|
'agent.summary': 'Resumen',
|
|
@@ -838,6 +852,7 @@ const es = {
|
|
|
838
852
|
'board.agents': 'Agentes:',
|
|
839
853
|
'board.none': '(sin agentes)',
|
|
840
854
|
'board.activity': 'Quién trabaja dónde (actividad de archivos, sin bloqueos):',
|
|
855
|
+
'board.workMap': 'Avisos del mapa de trabajo:',
|
|
841
856
|
'board.noActivity': '(sin modificaciones por ahora)',
|
|
842
857
|
'board.notes': 'Últimas notas:',
|
|
843
858
|
'notes.title': '✉ NOTAS ENTRE AGENTES (últimas 30)',
|
|
@@ -976,7 +991,7 @@ const es = {
|
|
|
976
991
|
'attach.placeholder': 'Mensaje a {agent} (Enter para enviar, /quit para desconectar)…',
|
|
977
992
|
'attach.waiting': 'Esperando al agente {agent}… (¿está corriendo la sesión principal de Parallel?)',
|
|
978
993
|
'attach.gone': 'Sesión terminada — esta terminal está desconectada.',
|
|
979
|
-
'attach.hint': 'texto libre dirige a este agente ·
|
|
994
|
+
'attach.hint': 'texto libre dirige a este agente · @all difunde · /send enruta · /task crea · /quit desconecta',
|
|
980
995
|
'grid.above': '▲ {n} agente(s) arriba — PgUp',
|
|
981
996
|
'grid.below': '▼ {n} agente(s) abajo — PgDn',
|
|
982
997
|
'm.nothing': 'Nada que guardar por ahora.',
|
|
@@ -1183,6 +1198,10 @@ const zh = {
|
|
|
1183
1198
|
'main.ready1': '⚡ 就绪 — 文件夹:{folder}',
|
|
1184
1199
|
'main.ready2': '输入任务 + 回车即可启动第一个智能体。/help 查看帮助。',
|
|
1185
1200
|
'main.empty': '尚无智能体。输入任务 + 回车启动第一个 — 之后可随时启动更多,即使它们正在工作。',
|
|
1201
|
+
'main.prompt': '示例:帮我重做 UI',
|
|
1202
|
+
'main.emptyCard.tagline': '在一个终端中进行多智能体编码。',
|
|
1203
|
+
'main.emptyCard.cta': '在下方描述工作以启动第一个智能体。',
|
|
1204
|
+
'main.emptyCard.hints': '/ 查看命令 · @智能体 可指挥 · /help 查看快捷键',
|
|
1186
1205
|
'main.status': '回车 = 新智能体 N+1(即使其他智能体正在工作)· @名称 = 实时指令 · /help · 视图:/agents /board /diff /notes',
|
|
1187
1206
|
'main.placeholder': '输入任务(= 新智能体 N+1)· @智能体 消息 · /命令',
|
|
1188
1207
|
'agent.summary': '摘要',
|
|
@@ -1203,6 +1222,7 @@ const zh = {
|
|
|
1203
1222
|
'board.agents': '智能体:',
|
|
1204
1223
|
'board.none': '(无智能体)',
|
|
1205
1224
|
'board.activity': '谁在哪里工作(文件活动,无锁定):',
|
|
1225
|
+
'board.workMap': '工作地图提醒:',
|
|
1206
1226
|
'board.noActivity': '(暂无修改)',
|
|
1207
1227
|
'board.notes': '最新便签:',
|
|
1208
1228
|
'notes.title': '✉ 智能体间便签(最近 30 条)',
|
|
@@ -1341,7 +1361,7 @@ const zh = {
|
|
|
1341
1361
|
'attach.placeholder': '发给 {agent} 的消息(回车发送,/quit 断开)…',
|
|
1342
1362
|
'attach.waiting': '等待代理 {agent}…(Parallel 主会话在运行吗?)',
|
|
1343
1363
|
'attach.gone': '会话已结束 — 此终端已断开。',
|
|
1344
|
-
'attach.hint': '直接输入文字可指挥此代理 ·
|
|
1364
|
+
'attach.hint': '直接输入文字可指挥此代理 · @all 广播 · /send 路由 · /task 创建 · /quit 断开',
|
|
1345
1365
|
'grid.above': '▲ 上方还有 {n} 个代理 — PgUp',
|
|
1346
1366
|
'grid.below': '▼ 下方还有 {n} 个代理 — PgDn',
|
|
1347
1367
|
'm.nothing': '暂无可保存的内容。',
|