@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 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 reviewer should we split the CLI parser?
83
- /plan migration propose the safest rollout for the config change
84
- /task builder implement the approved plan
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. It is designed to answer:
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 when a suggestion menu is open.
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
- /task write parser regression tests
174
- /ask is this result safe to merge?
175
- /plan prepare a migration plan
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 with its conversation history.
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:
@@ -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
  }
@@ -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 = ['cyan', 'magenta', 'yellow', 'green', 'blue', 'redBright', 'cyanBright', 'magentaBright'];
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 sa = this.loadedSession?.agents.find((a) => a.name.toLowerCase() === name.toLowerCase());
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
- return this.spawnAgent(sa.task, sa.name, sa.model, undefined, undefined, history, sa.mode ?? 'task');
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
- this.board.addNote('user', 'all', content);
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.addNote('system', 'all', `Previous session restored (${data.savedAt}). Past work:\n${tasks.join('\n')}\nFiles changed then: ${data.changedFiles.join(', ') || '(none)'}`);
665
- for (const n of data.notes.slice(-50)) {
666
- this.board.notes.push({ ...n, id: this.board.notes.length + 1 });
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 · /task creates a new agent · /ask asks · /plan plans · /quit detaches',
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 · /task crée un nouvel agent · /ask questionne · /plan planifie · /quit détache',
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 · /task crea un agente nuevo · /ask pregunta · /plan planifica · /quit desconecta',
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': '直接输入文字可指挥此代理 · /task 创建新代理 · /ask 提问 · /plan 规划 · /quit 断开',
1364
+ 'attach.hint': '直接输入文字可指挥此代理 · @all 广播 · /send 路由 · /task 创建 · /quit 断开',
1345
1365
  'grid.above': '▲ 上方还有 {n} 个代理 — PgUp',
1346
1366
  'grid.below': '▼ 下方还有 {n} 个代理 — PgDn',
1347
1367
  'm.nothing': '暂无可保存的内容。',