@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 +35 -2
- package/README.md +77 -94
- package/dist/agents/agent.js +2 -2
- package/dist/agents/tools.js +47 -5
- package/dist/commands.js +104 -18
- package/dist/config.js +31 -1
- package/dist/controller.js +36 -7
- package/dist/i18n.js +132 -28
- package/dist/index.js +3 -3
- package/dist/ui/App.js +81 -60
- package/dist/ui/SettingsPanel.js +86 -18
- package/dist/ui/views.js +38 -15
- package/package.json +1 -1
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
|
|
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
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
|
package/dist/agents/agent.js
CHANGED
|
@@ -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 "
|
|
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,
|
package/dist/agents/tools.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
246
|
-
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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 (
|
|
150
|
+
if (providerNeedsApiKey(p) && !p.apiKey)
|
|
82
151
|
return ui.system(t('m.missingKey', { name: p.name }), 'error');
|
|
83
|
-
|
|
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
|
|
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,
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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).
|