@parallel-cli/parallel 0.4.3 → 0.4.4

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,39 @@
2
2
 
3
3
  All notable changes to Parallel are documented here.
4
4
 
5
+ ## 0.4.4 - 2026-06-23
6
+
7
+ ### 0.4.4 Added
8
+
9
+ - Added tool-level safeguards for `/ask` and `/plan`: ask agents cannot mutate project state, and plan agents must get explicit approval before mutating files or running risky shell commands.
10
+ - Added session-only provider setup for `/settings-session`, with an explicit choice between temporary session use and saving globally.
11
+ - Expanded `/doctor` into actionable readiness diagnostics for provider, model, key, local endpoints, attach socket, `git`, and `gh`.
12
+ - Added scrolling/windowing budgets to long TUI views such as board, notes, diffs, cost, skills, specialists, and sessions.
13
+ - Added saved-session restore hints and clearer `/restore` errors.
14
+
15
+ ### 0.4.4 Changed
16
+
17
+ - Reworked the README to render consistently on both GitHub and npm.
18
+ - Replaced wide Markdown tables and fixed-width command blocks with npm-friendly lists.
19
+ - Removed provider-specific environment variable guidance from the public README so provider setup remains neutral.
20
+ - Documented DeepSeek only as one provider preset in the Chinese provider group, not as a special standalone setup path.
21
+ - Made `@all` steer active agents directly instead of only posting a passive note.
22
+ - Made `/plan` timeouts safe by requiring manual approval before mutations are unlocked.
23
+ - Increased the saved sessions list window from 8 to 20 and shows `/save [name]` labels in `/sessions`.
24
+ - Bumped the TUI header version to `0.4.4`.
25
+
26
+ ### 0.4.4 Fixed
27
+
28
+ - Fixed `/commit message...` with exactly one agent so the first word is treated as part of the message, not as a missing agent name.
29
+ - Fixed `/project` and `/wizard` transitions by warning when active agents are running unless `--force` is passed.
30
+ - Fixed local/custom provider setup so localhost endpoints do not require an API key and placeholder models are not considered ready.
31
+ - Fixed stale focus after agents disappear through clear, stop, project switch, session load, or restore.
32
+ - Fixed `/settings-session` key entry so provider setup reaches the session-only vs global-save choice instead of returning early.
33
+ - Fixed first-run custom provider setup so it reviews/edits endpoints and skips API keys for localhost endpoints.
34
+ - Fixed filesystem boundary checks so agent tools reject sibling paths with a shared project-root prefix.
35
+ - Fixed TUI clipping risks by budgeting the hub by rendered rows and applying body-height windowing to notes/diffs.
36
+ - Fixed Settings Escape handling so typed inputs clear before navigating back.
37
+
5
38
  ## 0.4.3 - 2026-06-23
6
39
 
7
40
  ### 0.4.3 Added
@@ -32,7 +65,7 @@ All notable changes to Parallel are documented here.
32
65
  - Fixed session settings accidentally behaving like global provider mutation in several flows.
33
66
  - Fixed bare model IDs containing `:` such as `qwen3-coder:480b`.
34
67
  - Fixed stale default provider normalization during config load and provider removal.
35
- - Fixed `DEEPSEEK_API_KEY` overriding whichever provider happened to be default; it now targets DeepSeek only.
68
+ - Fixed provider-specific environment overrides leaking into whichever provider happened to be default.
36
69
  - Fixed local endpoints being blocked by missing API keys.
37
70
  - Fixed sensitive `/key ...` entries being stored in input history.
38
71
  - Fixed tiny pseudo-TTY dimensions causing negative string repeat values in the TUI header.
@@ -49,7 +82,7 @@ All notable changes to Parallel are documented here.
49
82
 
50
83
  ### 0.4.2 Changed
51
84
 
52
- - Reworked the README provider section to be provider-agnostic instead of DeepSeek-centered.
85
+ - Reworked the README provider section to be provider-agnostic.
53
86
  - Updated provider tables, endpoint documentation, and model catalog references.
54
87
  - Removed internal docs from remote tracking and kept them out of the public package.
55
88
 
package/README.md CHANGED
@@ -96,13 +96,13 @@ Broadcast to every agent:
96
96
  @all stop changing public interfaces until the test agent finishes
97
97
  ```
98
98
 
99
+ `@all` steers active agents in real time. Finished, stopped, or errored agents are not relaunched by a broadcast.
100
+
99
101
  ## Agent Modes
100
102
 
101
- | Mode | Use it for | Behavior |
102
- | --- | --- | --- |
103
- | `/ask` | Questions, reviews, audits, tradeoffs | Answers and advises without editing files. |
104
- | `/task` | Implementation work | Executes, edits, validates, and summarizes. |
105
- | `/plan` | Risky or unclear work | Inspects first, presents a plan, then edits only after approval. |
103
+ - `/ask`: questions, reviews, audits, and tradeoffs. The agent answers and advises; mutating tools and shell commands are blocked.
104
+ - `/task`: implementation work. The agent can execute, edit, validate, and summarize.
105
+ - `/plan`: risky or unclear work. The agent inspects first, presents a plan, then edits only after explicit approval. A timeout does not approve the plan.
106
106
 
107
107
  Aliases:
108
108
 
@@ -124,19 +124,19 @@ The main TUI is the Parallel hub. It is designed to answer:
124
124
 
125
125
  Common hub commands:
126
126
 
127
- ```text
128
- /agents agent overview
129
- /focus a1 inspect and steer one agent
130
- /raw toggle raw detail in focus view
131
- /board shared blackboard, claims, notes, file activity
132
- /diff live diff history
133
- /cost token and cost breakdown
134
- /sessions saved sessions
135
- /settings global settings
136
- /settings-session session-only settings
137
- /project [folder] change project folder
138
- /wizard rerun setup wizard
139
- ```
127
+ - `/agents`: agent overview.
128
+ - `/focus a1`: inspect and steer one agent.
129
+ - `/raw`: toggle raw detail in focus view.
130
+ - `/board`: shared blackboard, claims, notes, and file activity.
131
+ - `/diff`: live diff history.
132
+ - `/cost`: token and cost breakdown.
133
+ - `/sessions`: saved sessions.
134
+ - `/settings`: global settings.
135
+ - `/settings-session`: session-only settings.
136
+ - `/project [folder]`: change project folder.
137
+ - `/wizard`: rerun setup wizard.
138
+
139
+ Commands are typed in the control room input. When a long view is open, use Escape to return to the agents view/input.
140
140
 
141
141
  Keyboard behavior:
142
142
 
@@ -144,7 +144,7 @@ Keyboard behavior:
144
144
  - Up/Down selects suggestions when a suggestion menu is open.
145
145
  - Enter accepts the selected suggestion.
146
146
  - Tab or Right accepts the best completion.
147
- - Up/Down scrolls long views such as `/help`.
147
+ - PgUp/PgDn scrolls the hub or focus view even while the input is active. Up/Down scrolls long views and navigates suggestions/history.
148
148
  - Escape returns to the agents view or clears the input.
149
149
 
150
150
  Best terminal size is around `120x34`. Parallel adapts to smaller terminals, but the hub is most readable with enough width for model, folder, status, and agent summaries.
@@ -202,101 +202,86 @@ Provider setup is guided in both the first-run wizard and `/settings`:
202
202
  4. Enter the provider API key if the provider requires one.
203
203
  5. Save globally or use the provider/model for the current session.
204
204
 
205
- Local providers such as Ollama and vLLM/SGLang do not require an API key. You can still review and edit their endpoints.
205
+ Local providers such as Ollama, vLLM/SGLang, and custom localhost OpenAI-compatible endpoints do not require an API key. You can still review and edit their endpoints. Ollama/local OpenAI-compatible endpoints can detect models from `/models`; vLLM/SGLang requires replacing the `your-model-here` placeholder before it is considered ready.
206
206
 
207
207
  Useful settings commands:
208
208
 
209
- ```text
210
- /settings global language, providers, keys, defaults, approvals
211
- /settings-session temporary model, provider, approvals, sound
212
- /model show current session model
213
- /model provider:id switch model for this session
214
- /doctor check provider/model/API key readiness
215
- ```
209
+ - `/settings`: global language, providers, keys, defaults, and approvals.
210
+ - `/settings-session`: temporary model, provider, approvals, and sound. New providers can be used for this session only or saved globally.
211
+ - `/model`: show the current session model.
212
+ - `/model provider:id`: switch model for this session.
213
+ - `/doctor`: check provider, model, API key, local endpoint reachability, attach socket, `git`, and `gh`.
216
214
 
217
215
  Configuration is stored in `~/.parallel/config.json`.
218
216
 
219
217
  Environment variables:
220
218
 
221
- | Variable | Purpose |
222
- | --- | --- |
223
- | `PARALLEL_API_KEY` | API key for the current default provider. |
224
- | `DEEPSEEK_API_KEY` | API key for the DeepSeek provider only. |
225
- | `PARALLEL_BASE_URL` | Override the default provider base URL. |
226
- | `PARALLEL_MODEL` | Override the session model. |
227
- | `PARALLEL_NO_ALT_SCREEN=1` | Disable the alternate terminal screen. |
219
+ - `PARALLEL_API_KEY`: API key for the current default provider.
220
+ - `PARALLEL_BASE_URL`: override the default provider base URL.
221
+ - `PARALLEL_MODEL`: override the session model.
222
+ - `PARALLEL_NO_ALT_SCREEN=1`: disable the alternate terminal screen.
228
223
 
229
224
  ## Commands
230
225
 
231
226
  ### Create Agents
232
227
 
233
- | Command | Description |
234
- | --- | --- |
235
- | `/ask [Name:] <question> [--model=m]` | Launch an ask-only agent. |
236
- | `/task [Name:] <task> [--model=m] [#skill]` | Launch a task agent. Plain text does the same. |
237
- | `/plan [Name:] <task> [--model=m]` | Launch a plan-first agent. |
238
- | `/issue <n>` | Spawn a task from a GitHub issue using the `gh` CLI. |
239
- | `/specialist <name> <task>` | Spawn with a specialist persona. |
240
- | `/specialist new <name> [global]` | Create a specialist template. |
241
- | `/skill new <name> [global]` | Create a skill template. |
228
+ - `/ask [Name:] <question> [--model=m]`: launch an ask-only agent.
229
+ - `/task [Name:] <task> [--model=m] [#skill]`: launch a task agent. Plain text does the same.
230
+ - `/plan [Name:] <task> [--model=m]`: launch a plan-first agent. It cannot mutate files or run risky shell commands until you manually approve the plan.
231
+ - `/issue <n>`: spawn a task from a GitHub issue. Requires the `gh` CLI, a GitHub repository, and `gh auth login`.
232
+ - `/specialist <name> <task>`: spawn with a specialist persona.
233
+ - `/specialist new <name> [global]`: create a specialist template.
234
+ - `/skill new <name> [global]`: create a skill template.
242
235
 
243
236
  ### Steer Agents
244
237
 
245
- | Command | Description |
246
- | --- | --- |
247
- | `@agent <message>` | Send a live instruction to one agent. |
248
- | `@all <message>` | Broadcast an instruction to all agents. |
249
- | `/send <agent\|all> <message>` | Command form of live steering. |
250
- | `/attach <agent\|on\|off>` | Open an agent terminal or toggle automatic terminals. |
251
- | `/focus <agent\|off>` | Route plain input to one agent instead of spawning new agents. |
252
- | `/pause <agent\|all>` | Pause at the next action boundary. |
253
- | `/resume <agent\|all>` | Resume paused agents. |
254
- | `/stop <agent\|all>` | Stop running agents. |
255
- | `/clear` | Remove finished agents from the current display. |
256
- | `/raw` | Toggle conversation-raw view. |
257
- | `/copy` | Copy the latest completed result to clipboard. |
238
+ - `@agent <message>`: send a live instruction to one agent.
239
+ - `@all <message>`: broadcast an instruction to all agents.
240
+ - `/send <agent|all> <message>`: command form of live steering.
241
+ - `/attach <agent|on|off>`: open an agent terminal or toggle automatic terminals.
242
+ - `/focus <agent|off>`: route plain input to one agent instead of spawning new agents.
243
+ - `/pause <agent|all>`: pause at the next action boundary.
244
+ - `/resume <agent|all>`: resume paused agents.
245
+ - `/stop <agent|all>`: stop running agents.
246
+ - `/clear`: remove finished agents from the current display.
247
+ - `/raw`: toggle conversation-raw view.
248
+ - `/copy`: copy the latest completed result to clipboard.
258
249
 
259
250
  ### Git Safety
260
251
 
261
- | Command | Description |
262
- | --- | --- |
263
- | `/undo [agent]` | Revert the last file change made by an agent, with conflict detection. |
264
- | `/commit [agent\|all] [message]` | Commit files touched by an agent or by all agents. |
265
- | `/autocommit <on\|off>` | Commit each agent's changes automatically when it finishes. |
252
+ - `/undo [agent]`: revert the last file change made by an agent, with conflict detection.
253
+ - `/commit [agent|all] [message]`: commit only files touched by the selected agent or by all agents. It does not run `git add -A`. With exactly one agent, `/commit message...` uses that agent and treats the rest as the message.
254
+ - `/autocommit <on|off>`: commit each agent's touched files automatically when it finishes. This is session-only.
266
255
 
267
256
  ### Views And Sessions
268
257
 
269
- | Command | Description |
270
- | --- | --- |
271
- | `/agents` | Agent overview. |
272
- | `/board` | Shared blackboard, file activity, claims, and notes. |
273
- | `/notes` | Full notes history. |
274
- | `/diff` | Live diff history. |
275
- | `/cost` | Token and cost breakdown. |
276
- | `/status` | Session model, approval mode, agents, cost snapshot. |
277
- | `/skills` | Available skills. |
278
- | `/specialists` | Available specialists. |
279
- | `/save [name]` | Save the current session. |
280
- | `/sessions` | List saved sessions. |
281
- | `/session <n\|latest>` | Restore a saved session. |
282
- | `/restore <agent>` | Relaunch a restored agent with its conversation history. |
258
+ - `/agents`: agent overview.
259
+ - `/board`: shared blackboard, file activity, claims, and notes.
260
+ - `/notes`: full notes history.
261
+ - `/diff`: live diff history.
262
+ - `/cost`: token and cost breakdown.
263
+ - `/status`: session model, approval mode, agents, and cost snapshot.
264
+ - `/skills`: available skills.
265
+ - `/specialists`: available specialists.
266
+ - `/save [name]`: save the current session.
267
+ - `/sessions`: list saved sessions.
268
+ - `/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.
283
270
 
284
271
  ### Settings And Exit
285
272
 
286
- | Command | Description |
287
- | --- | --- |
288
- | `/model [[provider:]model]` | Show or switch the session model. |
289
- | `/approvals <ask\|auto\|auto-safe\|yolo>` | Set shell approvals for this session. |
290
- | `/sound <on\|off>` | Toggle terminal bell notifications. |
291
- | `/settings` | Edit global language, providers, keys, defaults, and approvals. |
292
- | `/settings-session` | Edit session-only model, approvals, and sound. |
293
- | `/project [folder]` | Change project folder or reopen the folder picker. |
294
- | `/folder [folder]` | Alias for `/project`. |
295
- | `/wizard` | Relaunch the setup wizard. |
296
- | `/setup` | Alias for `/wizard`. |
297
- | `/doctor` | Check provider, key, and model configuration. |
298
- | `/help` | Full command reference. |
299
- | `/quit` | Save the session and exit. |
273
+ - `/model [[provider:]model]`: show or switch the session model.
274
+ - `/approvals <ask|auto|auto-safe|yolo>`: set shell approvals for this session.
275
+ - `/sound <on|off>`: toggle terminal bell notifications.
276
+ - `/settings`: edit global language, providers, keys, defaults, and approvals.
277
+ - `/settings-session`: edit session-only model, provider, approvals, and sound.
278
+ - `/project [folder]`: change project folder or reopen the folder picker. If agents are active, use `/project [folder] --force` after saving/stopping what you need.
279
+ - `/folder [folder]`: alias for `/project`.
280
+ - `/wizard`: relaunch the setup wizard. If agents are active, use `/wizard --force` after saving/stopping what you need.
281
+ - `/setup`: alias for `/wizard`.
282
+ - `/doctor`: run local readiness diagnostics for provider, key, model, endpoint, attach socket, and Git tooling.
283
+ - `/help`: full command reference.
284
+ - `/quit`: save the session and exit.
300
285
 
301
286
  When there is exactly one agent, commands such as `/undo`, `/focus`, `/pause`, `/resume`, `/stop`, and `/commit` can omit the agent name.
302
287
 
@@ -310,11 +295,9 @@ Parallel separates agent modes from shell approval behavior.
310
295
  /approvals yolo
311
296
  ```
312
297
 
313
- | Mode | Behavior |
314
- | --- | --- |
315
- | `ask` | Ask before shell commands unless explicitly allowed. |
316
- | `auto-safe` | Auto-approve safe inspection/build/test commands and ask for risky commands. |
317
- | `yolo` | Auto-approve every shell command. Intended for trusted/headless usage only. |
298
+ - `ask`: ask before shell commands unless explicitly allowed.
299
+ - `auto-safe`: auto-approve safe inspection/build/test commands and ask for risky commands.
300
+ - `yolo`: auto-approve every shell command. Intended for trusted/headless usage only.
318
301
 
319
302
  `auto` is accepted as a compatibility spelling for `auto-safe`.
320
303
 
@@ -27,7 +27,7 @@ ${mode === 'ask'
27
27
  - Explore first with read-only tools.
28
28
  - Before modifying any file or running mutating commands, call ask_user with a concrete implementation plan.
29
29
  - The plan must include steps, files you expect to touch, risks, and validation.
30
- - Use options ["Approve", "Revise"], recommended "Approve".
30
+ - Use options ["Approve", "Revise"], recommended "Revise" so timeout never approves changes.
31
31
  - Start editing only after explicit "Approve".
32
32
  - Finish with task_complete using this user-facing structure in ${userLang}: "Plan appliqué", "Ce que j’ai modifié", "Validation", "Risques restants".`
33
33
  : `TASK MODE:
@@ -97,7 +97,7 @@ export class Agent {
97
97
  this.llm = opts.llm;
98
98
  this.board = opts.board;
99
99
  this.maxSteps = opts.maxSteps;
100
- this.executor = new ToolExecutor(opts.board, opts.id, opts.name, opts.projectRoot, opts.requestApproval, opts.requestQuestion, opts.skills);
100
+ this.executor = new ToolExecutor(opts.board, opts.id, opts.name, opts.projectRoot, opts.requestApproval, opts.requestQuestion, opts.skills, opts.mode);
101
101
  const info = {
102
102
  id: opts.id,
103
103
  name: opts.name,
@@ -4,6 +4,19 @@ import { exec } from 'node:child_process';
4
4
  import * as Diff from 'diff';
5
5
  const IGNORED = new Set(['node_modules', '.git', '.parallel', 'dist', '__pycache__', '.venv', 'venv']);
6
6
  const MAX_OUTPUT = 12_000;
7
+ const MUTATING_TOOLS = new Set(['write_file', 'edit_file', 'claim_files', 'remember']);
8
+ function isMutatingShell(command) {
9
+ const c = command.toLowerCase();
10
+ if (/\b(rm|mv|cp|chmod|chown|mkdir|touch|truncate)\b/.test(c))
11
+ return true;
12
+ if (/\b(git\s+(add|commit|push|pull|merge|rebase|checkout|switch|reset|clean|stash|tag))\b/.test(c))
13
+ return true;
14
+ if (/\b(npm|pnpm|yarn)\s+(install|add|remove|update|audit\s+fix)\b/.test(c))
15
+ return true;
16
+ if (/[>|]\s*(sh|bash)\b/.test(c) || /\b(curl|wget)\b.*\|\s*(sh|bash)/.test(c))
17
+ return true;
18
+ return false;
19
+ }
7
20
  export const TOOL_DEFINITIONS = [
8
21
  {
9
22
  type: 'function',
@@ -228,11 +241,13 @@ export class ToolExecutor {
228
241
  requestApproval;
229
242
  requestQuestion;
230
243
  skills;
244
+ mode;
231
245
  /** Last content this agent has seen for each file — basis of adaptive merging. */
232
246
  lastRead = new Map();
233
247
  /** Questions already asked — capped at 3 per task. */
234
248
  questionsAsked = 0;
235
- constructor(board, agentId, agentName, projectRoot, requestApproval, requestQuestion, skills) {
249
+ planApproved = false;
250
+ constructor(board, agentId, agentName, projectRoot, requestApproval, requestQuestion, skills, mode = 'task') {
236
251
  this.board = board;
237
252
  this.agentId = agentId;
238
253
  this.agentName = agentName;
@@ -240,10 +255,13 @@ export class ToolExecutor {
240
255
  this.requestApproval = requestApproval;
241
256
  this.requestQuestion = requestQuestion;
242
257
  this.skills = skills;
258
+ this.mode = mode;
243
259
  }
244
260
  resolve(rel) {
245
- const abs = path.resolve(this.projectRoot, rel);
246
- if (!abs.startsWith(path.resolve(this.projectRoot))) {
261
+ const root = path.resolve(this.projectRoot);
262
+ const abs = path.resolve(root, rel);
263
+ const relative = path.relative(root, abs);
264
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
247
265
  throw new Error(`Path outside the project refused: ${rel}`);
248
266
  }
249
267
  return abs;
@@ -253,6 +271,9 @@ export class ToolExecutor {
253
271
  }
254
272
  async execute(name, args) {
255
273
  try {
274
+ const guard = this.guardTool(name, args);
275
+ if (guard)
276
+ return guard;
256
277
  switch (name) {
257
278
  case 'list_files':
258
279
  return this.listFiles(args?.path ?? '.');
@@ -293,6 +314,22 @@ export class ToolExecutor {
293
314
  return `ERROR: ${err?.message ?? String(err)}`;
294
315
  }
295
316
  }
317
+ guardTool(name, args) {
318
+ if (this.mode === 'ask') {
319
+ if (MUTATING_TOOLS.has(name) || name === 'run_command') {
320
+ return `DENIED: this agent is in /ask mode. It can inspect and advise, but cannot modify files, run shell commands, claim files, or write memory.`;
321
+ }
322
+ }
323
+ if (this.mode === 'plan' && !this.planApproved) {
324
+ if (MUTATING_TOOLS.has(name)) {
325
+ return `DENIED: this agent is in /plan mode and the plan has not been approved yet. Present the plan with ask_user and wait for "Approve" before modifying project state.`;
326
+ }
327
+ if (name === 'run_command' && isMutatingShell(String(args?.command ?? ''))) {
328
+ return `DENIED: this shell command looks mutating. In /plan mode, run only read-only inspection before approval; ask_user for plan approval first.`;
329
+ }
330
+ }
331
+ return null;
332
+ }
296
333
  /**
297
334
  * Ask the user a multiple-choice question. NEVER blocks forever: the UI shows
298
335
  * a visible 30s countdown and falls back to the recommended option (auto-run).
@@ -312,9 +349,14 @@ export class ToolExecutor {
312
349
  this.questionsAsked++;
313
350
  this.board.setAgentState(this.agentId, 'waiting', `question: ${question.slice(0, 60)}`);
314
351
  this.board.log(this.agentId, 'note', `❓ ${question}`);
315
- const answer = await this.requestQuestion(this.agentId, question, options, recommended);
352
+ const response = await this.requestQuestion(this.agentId, question, options, recommended);
353
+ const answer = response.answer;
354
+ if (this.mode === 'plan' && !response.auto && answer.trim().toLowerCase().startsWith('approve')) {
355
+ this.planApproved = true;
356
+ }
316
357
  this.board.setAgentState(this.agentId, 'working');
317
- return `The user answered: "${answer}". Act on this choice now (${3 - this.questionsAsked} question(s) left for this task).`;
358
+ const source = response.auto ? 'The timeout auto-selected' : 'The user answered';
359
+ return `${source}: "${answer}". Act on this choice now (${3 - this.questionsAsked} question(s) left for this task).`;
318
360
  }
319
361
  /** Declare (advisory) work areas — visible to the user and the other agents. */
320
362
  claimFiles(args) {
package/dist/commands.js CHANGED
@@ -1,6 +1,9 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { execFileSync } from 'node:child_process';
1
4
  import { Controller, normalizeShellApprovalMode } from './controller.js';
2
5
  import { createSkillTemplate, createSpecialistTemplate } from './skills.js';
3
- import { providerReady } from './config.js';
6
+ import { isLocalProvider, isPlaceholderModel, providerNeedsApiKey } from './config.js';
4
7
  import { t } from './i18n.js';
5
8
  // Grouped by intent so /help reads as a story: create agents → steer them →
6
9
  // inspect the session → git safety net → session & config → exit.
@@ -66,6 +69,72 @@ export function matchCommands(input, opts = {}) {
66
69
  function agentList(ctl) {
67
70
  return [...ctl.board.agents.values()].map((a) => a.name).join(', ') || t('m.none');
68
71
  }
72
+ function commandExists(name) {
73
+ try {
74
+ execFileSync('which', [name], { stdio: 'ignore' });
75
+ return true;
76
+ }
77
+ catch {
78
+ return false;
79
+ }
80
+ }
81
+ async function localEndpointReachable(baseUrl) {
82
+ let timeout;
83
+ try {
84
+ const controller = new AbortController();
85
+ timeout = setTimeout(() => controller.abort(), 1500);
86
+ const resp = await fetch(baseUrl.replace(/\/+$/, '') + '/models', { signal: controller.signal });
87
+ return resp.ok;
88
+ }
89
+ catch {
90
+ return false;
91
+ }
92
+ finally {
93
+ if (timeout)
94
+ clearTimeout(timeout);
95
+ }
96
+ }
97
+ async function doctorReport(ctl, ui) {
98
+ const p = ctl.sessionProvider();
99
+ const lines = [];
100
+ let level = 'ok';
101
+ if (!p) {
102
+ ui.system(t('m.doctorReport', { lines: t('m.doctorNoProvider') }), 'error');
103
+ return;
104
+ }
105
+ const activeModel = ctl.session.model || p.defaultModel || p.models[0] || '';
106
+ lines.push(t('m.doctorProvider', { provider: p.name, model: activeModel || '—' }));
107
+ if (providerNeedsApiKey(p) && !p.apiKey) {
108
+ level = 'error';
109
+ lines.push(t('m.doctorKeyMissing'));
110
+ }
111
+ else {
112
+ lines.push(providerNeedsApiKey(p) ? t('m.doctorKeyOk') : t('m.doctorKeySkipped'));
113
+ }
114
+ if (isPlaceholderModel(activeModel)) {
115
+ level = 'error';
116
+ lines.push(t('m.doctorModelMissing'));
117
+ }
118
+ else {
119
+ lines.push(t('m.doctorModelOk', { model: activeModel }));
120
+ }
121
+ if (isLocalProvider(p)) {
122
+ const reachable = await localEndpointReachable(p.baseUrl);
123
+ if (reachable) {
124
+ lines.push(t('m.doctorEndpointOk', { url: p.baseUrl }));
125
+ }
126
+ else {
127
+ if (level !== 'error')
128
+ level = 'warn';
129
+ lines.push(t('m.doctorEndpointFail', { url: p.baseUrl }));
130
+ }
131
+ }
132
+ const sock = path.join(ctl.projectRoot, '.parallel', 'session.sock');
133
+ lines.push(fs.existsSync(sock) ? t('m.doctorAttachOk') : t('m.doctorAttachMissing'));
134
+ lines.push(commandExists('git') ? t('m.doctorGitOk') : t('m.doctorGitMissing'));
135
+ lines.push(commandExists('gh') ? t('m.doctorGhOk') : t('m.doctorGhMissing'));
136
+ ui.system(t('m.doctorReport', { lines: lines.join('\n') }), level);
137
+ }
69
138
  /**
70
139
  * Single-agent ergonomics: when the session has exactly ONE agent, commands
71
140
  * that target an agent (/undo, /focus, /pause…) work without naming it.
@@ -78,10 +147,12 @@ function spawnFrom(arg, ctl, ui, images, specialist, mode = 'task') {
78
147
  const p = ctl.sessionProvider();
79
148
  if (!p)
80
149
  return ui.system(t('m.missingProvider'), 'error');
81
- if (!providerReady(p))
150
+ if (providerNeedsApiKey(p) && !p.apiKey)
82
151
  return ui.system(t('m.missingKey', { name: p.name }), 'error');
83
- if (!ctl.session.model && !p.defaultModel && !p.models[0])
152
+ const activeModel = ctl.session.model || p.defaultModel || p.models[0] || '';
153
+ if (isPlaceholderModel(activeModel)) {
84
154
  return ui.system(t('m.missingModel', { name: p.name }), 'error');
155
+ }
85
156
  // optional --model=xxx flag
86
157
  let model;
87
158
  let task = arg;
@@ -129,8 +200,8 @@ export function executeInput(raw, ctl, ui, images) {
129
200
  return ui.system(t('m.usageAt'), 'warn');
130
201
  const [, target, content] = m;
131
202
  if (target.toLowerCase() === 'all') {
132
- ctl.broadcast(content);
133
- ui.system(t('m.broadcast'), 'ok');
203
+ const n = ctl.broadcast(content);
204
+ ui.system(t('m.broadcast', { n }), n > 0 ? 'ok' : 'warn');
134
205
  }
135
206
  else if (ctl.sendToAgent(target, content)) {
136
207
  ui.system(t('m.sent', { target }), 'ok');
@@ -217,10 +288,18 @@ export function executeInput(raw, ctl, ui, images) {
217
288
  case '/commit': {
218
289
  // Commit the files touched by one agent (or all) — staged by explicit path.
219
290
  const [target0, ...msg] = rest;
220
- const target = target0 || soloAgent(ctl);
291
+ const solo = soloAgent(ctl);
292
+ const target = target0 && (target0.toLowerCase() === 'all' || ctl.board.getAgentByName(target0))
293
+ ? target0
294
+ : target0 && solo
295
+ ? solo
296
+ : target0 || solo;
297
+ const message = target0 && target === solo && target0.toLowerCase() !== solo?.toLowerCase() && !ctl.board.getAgentByName(target0)
298
+ ? rest.join(' ').trim()
299
+ : msg.join(' ').trim();
221
300
  if (!target)
222
301
  return ui.system(t('m.usageCommit'), 'warn');
223
- const r = ctl.commitFor(target, msg.join(' ').trim() || undefined);
302
+ const r = ctl.commitFor(target, message || undefined);
224
303
  if (r.ok)
225
304
  return ui.system(t('m.committed', { name: target, files: String(r.files) }), 'ok');
226
305
  if (r.reason === 'not-found')
@@ -249,15 +328,19 @@ export function executeInput(raw, ctl, ui, images) {
249
328
  ui.setView('sessions');
250
329
  return;
251
330
  }
331
+ const force = rest.includes('--force');
332
+ const selector = rest.filter((part) => part !== '--force').join(' ').trim();
252
333
  const sessions = Controller.listSessions(ctl.projectRoot);
253
334
  if (sessions.length === 0)
254
335
  return ui.system(t('m.usageSession'), 'warn');
255
- const idx = arg.toLowerCase() === 'latest' ? 0 : Number.parseInt(arg, 10) - 1;
336
+ const idx = selector.toLowerCase() === 'latest' ? 0 : Number.parseInt(selector, 10) - 1;
256
337
  const session = sessions[idx];
257
338
  if (!session)
258
339
  return ui.system(t('m.usageSession'), 'warn');
340
+ if (ctl.hasRunningAgents() && !force)
341
+ return ui.system(t('m.sessionActive'), 'warn');
259
342
  ctl.loadSession(session.data);
260
- ui.system(t('m.sessionLoaded', { date: new Date(session.data.savedAt).toLocaleString() }), 'ok');
343
+ ui.system(t('m.sessionLoaded', { date: new Date(session.data.savedAt).toLocaleString() }) + '\n' + t('m.sessionRestoreHint'), 'ok');
261
344
  return;
262
345
  }
263
346
  case '/restore': {
@@ -267,6 +350,8 @@ export function executeInput(raw, ctl, ui, images) {
267
350
  if (!ctl.loadedSession)
268
351
  return ui.system(t('m.usageSession'), 'warn');
269
352
  const res = ctl.respawnAgent(arg);
353
+ if (res === 'no-agent')
354
+ return ui.system(t('m.noRestoredAgent', { name: arg }), 'error');
270
355
  if (res === 'no-conversation')
271
356
  return ui.system(t('m.noConversation', { name: arg }), 'error');
272
357
  if (!res)
@@ -315,14 +400,7 @@ export function executeInput(raw, ctl, ui, images) {
315
400
  return;
316
401
  }
317
402
  case '/doctor': {
318
- const p = ctl.sessionProvider();
319
- if (!p)
320
- return ui.system(t('m.missingProvider'), 'error');
321
- if (!providerReady(p))
322
- return ui.system(t('m.missingKey', { name: p.name }), 'error');
323
- if (!ctl.session.model && !p.defaultModel && !p.models[0])
324
- return ui.system(t('m.missingModel', { name: p.name }), 'error');
325
- ui.system(t('m.doctorOk', { pm: `${p.name}:${ctl.session.model || p.defaultModel || p.models[0]}` }), 'ok');
403
+ void doctorReport(ctl, ui);
326
404
  return;
327
405
  }
328
406
  case '/cost':
@@ -476,9 +554,17 @@ export function executeInput(raw, ctl, ui, images) {
476
554
  ui.setView('settings-session');
477
555
  return;
478
556
  case '/project':
479
- ui.openProject?.(arg || undefined);
557
+ {
558
+ const force = rest.includes('--force');
559
+ const folderArg = rest.filter((part) => part !== '--force').join(' ').trim();
560
+ if (ctl.hasRunningAgents() && !force)
561
+ return ui.system(t('m.projectActive'), 'warn');
562
+ ui.openProject?.(folderArg || undefined);
563
+ }
480
564
  return;
481
565
  case '/wizard':
566
+ if (ctl.hasRunningAgents() && arg !== '--force')
567
+ return ui.system(t('m.wizardActive'), 'warn');
482
568
  ui.openWizard?.();
483
569
  return;
484
570
  // SESSION-only approvals & sound (global defaults editable in /settings).