@parallel-cli/parallel 0.3.3 → 0.4.0

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/dist/commands.js CHANGED
@@ -1,57 +1,64 @@
1
- import { Controller } from './controller.js';
1
+ import { Controller, normalizeShellApprovalMode } from './controller.js';
2
2
  import { createSkillTemplate, createSpecialistTemplate } from './skills.js';
3
3
  import { t } from './i18n.js';
4
4
  // Grouped by intent so /help reads as a story: create agents → steer them →
5
5
  // inspect the session → git safety net → session & config → exit.
6
6
  export const COMMANDS = [
7
7
  // create agents
8
- { name: '/spawn', args: '[Name:] <task> [--model=m] [#skill]', descKey: 'cmd.spawn' },
9
- { name: '/plan', args: '[Name:] <task> [--model=m]', descKey: 'cmd.plan' },
10
- { name: '/issue', args: '<n>', descKey: 'cmd.issue' },
11
- { name: '/specialist', args: '<name> <task> | new <name> [global]', descKey: 'cmd.specialist' },
12
- { name: '/specialists', args: '', descKey: 'cmd.specialists' },
13
- { name: '/skill', args: 'new <name> [global]', descKey: 'cmd.skill' },
14
- { name: '/skills', args: '', descKey: 'cmd.skills' },
8
+ { name: '/ask', args: '[Name:] <question> [--model=m]', descKey: 'cmd.ask', group: 'modes', aliases: ['/a'] },
9
+ { name: '/task', args: '[Name:] <task> [--model=m] [#skill]', descKey: 'cmd.task', group: 'modes', aliases: ['/t'] },
10
+ { name: '/plan', args: '[Name:] <task> [--model=m]', descKey: 'cmd.plan', group: 'modes', aliases: ['/p'] },
11
+ { name: '/issue', args: '<n>', descKey: 'cmd.issue', group: 'git' },
12
+ { name: '/specialist', args: '<name> <task> | new <name> [global]', descKey: 'cmd.specialist', group: 'modes' },
13
+ { name: '/specialists', args: '', descKey: 'cmd.specialists', group: 'views' },
14
+ { name: '/skill', args: 'new <name> [global]', descKey: 'cmd.skill', group: 'settings' },
15
+ { name: '/skills', args: '', descKey: 'cmd.skills', group: 'views' },
15
16
  // steer agents
16
- { name: '/send', args: '<agent|all> <message>', descKey: 'cmd.send' },
17
- { name: '/attach', args: '<agent|on|off>', descKey: 'cmd.attach' },
18
- { name: '/focus', args: '<agent|off>', descKey: 'cmd.focus' },
19
- { name: '/pause', args: '<agent|all>', descKey: 'cmd.pause' },
20
- { name: '/resume', args: '<agent|all>', descKey: 'cmd.resume' },
21
- { name: '/stop', args: '<agent|all>', descKey: 'cmd.stop' },
22
- { name: '/clear', args: '', descKey: 'cmd.clear' },
17
+ { name: '/send', args: '<agent|all> <message>', descKey: 'cmd.send', group: 'control' },
18
+ { name: '/attach', args: '<agent|on|off>', descKey: 'cmd.attach', group: 'control' },
19
+ { name: '/focus', args: '<agent|off>', descKey: 'cmd.focus', group: 'control' },
20
+ { name: '/pause', args: '<agent|all>', descKey: 'cmd.pause', group: 'control' },
21
+ { name: '/resume', args: '<agent|all>', descKey: 'cmd.resume', group: 'control' },
22
+ { name: '/stop', args: '<agent|all>', descKey: 'cmd.stop', group: 'control' },
23
+ { name: '/clear', args: '', descKey: 'cmd.clear', group: 'control' },
24
+ { name: '/raw', args: '', descKey: 'cmd.raw', group: 'control' },
25
+ { name: '/copy', args: '', descKey: 'cmd.copy', group: 'control' },
23
26
  // git safety net
24
- { name: '/undo', args: '[agent]', descKey: 'cmd.undo' },
25
- { name: '/commit', args: '[agent|all] [message]', descKey: 'cmd.commit' },
26
- { name: '/autocommit', args: '<on|off>', descKey: 'cmd.autocommit' },
27
+ { name: '/undo', args: '[agent]', descKey: 'cmd.undo', group: 'git' },
28
+ { name: '/commit', args: '[agent|all] [message]', descKey: 'cmd.commit', group: 'git' },
29
+ { name: '/autocommit', args: '<on|off>', descKey: 'cmd.autocommit', group: 'git' },
27
30
  // inspect the session
28
- { name: '/agents', args: '', descKey: 'cmd.agents' },
29
- { name: '/board', args: '', descKey: 'cmd.board' },
30
- { name: '/notes', args: '', descKey: 'cmd.notes' },
31
- { name: '/diff', args: '', descKey: 'cmd.diff' },
32
- { name: '/cost', args: '', descKey: 'cmd.cost' },
31
+ { name: '/agents', args: '', descKey: 'cmd.agents', group: 'views' },
32
+ { name: '/board', args: '', descKey: 'cmd.board', group: 'views' },
33
+ { name: '/notes', args: '', descKey: 'cmd.notes', group: 'views' },
34
+ { name: '/diff', args: '', descKey: 'cmd.diff', group: 'views' },
35
+ { name: '/cost', args: '', descKey: 'cmd.cost', group: 'views' },
36
+ { name: '/status', args: '', descKey: 'cmd.status', group: 'views' },
33
37
  // sessions
34
- { name: '/save', args: '[name]', descKey: 'cmd.save' },
35
- { name: '/sessions', args: '', descKey: 'cmd.sessions' },
36
- { name: '/session', args: '[n|latest]', descKey: 'cmd.session' },
37
- { name: '/restore', args: '<agent>', descKey: 'cmd.restore' },
38
+ { name: '/save', args: '[name]', descKey: 'cmd.save', group: 'git' },
39
+ { name: '/sessions', args: '', descKey: 'cmd.sessions', group: 'git' },
40
+ { name: '/session', args: '[n|latest]', descKey: 'cmd.session', group: 'git' },
41
+ { name: '/restore', args: '<agent>', descKey: 'cmd.restore', group: 'git' },
38
42
  // config
39
- { name: '/model', args: '[[provider:]model]', descKey: 'cmd.model' },
40
- { name: '/key', args: '<key>', descKey: 'cmd.key' },
41
- { name: '/approvals', args: '<ask|auto>', descKey: 'cmd.approvals' },
42
- { name: '/sound', args: '<on|off>', descKey: 'cmd.sound' },
43
- { name: '/settings', args: '', descKey: 'cmd.settings' },
44
- { name: '/settings-session', args: '', descKey: 'cmd.ssettings', aliases: ['/ssettings'] },
45
- { name: '/doctor', args: '', descKey: 'cmd.doctor' },
43
+ { name: '/model', args: '[[provider:]model]', descKey: 'cmd.model', group: 'settings' },
44
+ { name: '/key', args: '<key>', descKey: 'cmd.key', group: 'settings' },
45
+ { name: '/approvals', args: '<ask|auto|auto-safe|yolo>', descKey: 'cmd.approvals', group: 'settings' },
46
+ { name: '/sound', args: '<on|off>', descKey: 'cmd.sound', group: 'settings' },
47
+ { name: '/settings', args: '', descKey: 'cmd.settings', group: 'settings' },
48
+ { name: '/settings-session', args: '', descKey: 'cmd.ssettings', group: 'settings', aliases: ['/ssettings'] },
49
+ { name: '/doctor', args: '', descKey: 'cmd.doctor', group: 'settings' },
46
50
  // exit
47
- { name: '/help', args: '', descKey: 'cmd.help' },
48
- { name: '/quit', args: '', descKey: 'cmd.quit', aliases: ['/exit'] },
51
+ { name: '/help', args: '', descKey: 'cmd.help', group: 'other' },
52
+ { name: '/quit', args: '', descKey: 'cmd.quit', group: 'other', aliases: ['/exit'] },
49
53
  ];
50
- export function matchCommands(input) {
54
+ export function visibleCommands() {
55
+ return COMMANDS.filter((c) => !c.hidden);
56
+ }
57
+ export function matchCommands(input, opts = {}) {
51
58
  if (!input.startsWith('/'))
52
59
  return [];
53
60
  const word = input.split(/\s+/)[0].toLowerCase();
54
- return COMMANDS.filter((c) => c.name.startsWith(word) || c.aliases?.some((a) => a.startsWith(word)));
61
+ return COMMANDS.filter((c) => opts.includeHidden || !c.hidden).filter((c) => c.name.startsWith(word) || c.aliases?.some((a) => a.startsWith(word)));
55
62
  }
56
63
  function agentList(ctl) {
57
64
  return [...ctl.board.agents.values()].map((a) => a.name).join(', ') || t('m.none');
@@ -64,18 +71,14 @@ function soloAgent(ctl) {
64
71
  const list = [...ctl.board.agents.values()];
65
72
  return list.length === 1 ? list[0].name : null;
66
73
  }
67
- /** Appended to the task in plan-first mode (/plan): no edits before approval. */
68
- const PLAN_FIRST = `
69
-
70
- PLAN-FIRST MODE — MANDATORY: before modifying ANY file, explore the code (list_files, read_file, search), then present your implementation plan to the user with ask_user: the question is the full plan (steps + files you will touch + risks), the options are ["Approve", "Revise"], recommended = "Approve". If the user answers "Revise", ask what to change (ask_user) and present an updated plan. Start editing files ONLY after an explicit "Approve".`;
71
- function spawnFrom(arg, ctl, ui, images, specialist, plan = false) {
74
+ function spawnFrom(arg, ctl, ui, images, specialist, mode = 'task') {
72
75
  const p = ctl.sessionProvider();
73
76
  if (!p)
74
- return ui.system(t('m.missingProvider'));
77
+ return ui.system(t('m.missingProvider'), 'error');
75
78
  if (!p.apiKey)
76
- return ui.system(t('m.missingKey', { name: p.name }));
79
+ return ui.system(t('m.missingKey', { name: p.name }), 'error');
77
80
  if (!ctl.session.model && !p.defaultModel && !p.models[0])
78
- return ui.system(t('m.missingModel', { name: p.name }));
81
+ return ui.system(t('m.missingModel', { name: p.name }), 'error');
79
82
  // optional --model=xxx flag
80
83
  let model;
81
84
  let task = arg;
@@ -101,14 +104,14 @@ function spawnFrom(arg, ctl, ui, images, specialist, plan = false) {
101
104
  }
102
105
  // optional "Name:" prefix
103
106
  const named = task.match(/^([\p{L}\p{N}_-]{1,16}):\s+(.+)$/su);
104
- const finalTask = (named ? named[2] : task) + (plan ? PLAN_FIRST : '');
105
- const agent = ctl.spawnAgent(finalTask, named ? named[1] : undefined, model, images, specialist);
107
+ const finalTask = named ? named[2] : task;
108
+ const agent = ctl.spawnAgent(finalTask, named ? named[1] : undefined, model, images, specialist, undefined, mode);
106
109
  if (!agent)
107
- return ui.system(specialist ? t('m.noSpecialist', { name: specialist }) : t('m.spawnFail'));
110
+ return ui.system(specialist ? t('m.noSpecialist', { name: specialist }) : t('m.spawnFail'), 'error');
108
111
  ui.system(t('m.spawned', { name: agent.name, model: model ? ` (${model})` : '' }) +
109
- (plan ? ' 📋plan' : '') +
112
+ ` /${mode}` +
110
113
  (specialist ? ` 🎓${specialist}` : '') +
111
- (forced.length > 0 ? ` 🧩${forced.join(',')}` : ''));
114
+ (forced.length > 0 ? ` 🧩${forced.join(',')}` : ''), 'info');
112
115
  }
113
116
  export function executeInput(raw, ctl, ui, images) {
114
117
  const input = raw.trim();
@@ -117,20 +120,20 @@ export function executeInput(raw, ctl, ui, images) {
117
120
  // "@Agent message" or "@all message" → live instruction
118
121
  if (input.startsWith('@')) {
119
122
  if (images?.length)
120
- ui.system(t('m.imagesIgnored'));
123
+ ui.system(t('m.imagesIgnored'), 'warn');
121
124
  const m = input.match(/^@(\S+)\s+(.+)$/s);
122
125
  if (!m)
123
- return ui.system(t('m.usageAt'));
126
+ return ui.system(t('m.usageAt'), 'warn');
124
127
  const [, target, content] = m;
125
128
  if (target.toLowerCase() === 'all') {
126
129
  ctl.broadcast(content);
127
- ui.system(t('m.broadcast'));
130
+ ui.system(t('m.broadcast'), 'ok');
128
131
  }
129
132
  else if (ctl.sendToAgent(target, content)) {
130
- ui.system(t('m.sent', { target }));
133
+ ui.system(t('m.sent', { target }), 'ok');
131
134
  }
132
135
  else {
133
- ui.system(t('m.notFound', { target, list: agentList(ctl) }));
136
+ ui.system(t('m.notFound', { target, list: agentList(ctl) }), 'error');
134
137
  }
135
138
  return;
136
139
  }
@@ -140,51 +143,68 @@ export function executeInput(raw, ctl, ui, images) {
140
143
  return;
141
144
  }
142
145
  if (images?.length)
143
- ui.system(t('m.imagesIgnored'));
144
- const [cmd, ...rest] = input.split(/\s+/);
146
+ ui.system(t('m.imagesIgnored'), 'warn');
147
+ const [rawCmd, ...rest] = input.split(/\s+/);
148
+ const cmd = rawCmd.toLowerCase() === '/a'
149
+ ? '/ask'
150
+ : rawCmd.toLowerCase() === '/t'
151
+ ? '/task'
152
+ : rawCmd.toLowerCase() === '/p'
153
+ ? '/plan'
154
+ : rawCmd.toLowerCase() === '/ssettings'
155
+ ? '/settings-session'
156
+ : rawCmd.toLowerCase() === '/exit'
157
+ ? '/quit'
158
+ : rawCmd;
145
159
  const arg = rest.join(' ').trim();
146
160
  switch (cmd.toLowerCase()) {
147
- case '/spawn': {
161
+ case '/ask': {
148
162
  if (!arg)
149
- return ui.system(t('m.usageSpawn'));
150
- spawnFrom(arg, ctl, ui, images);
163
+ return ui.system(t('m.usageAsk'), 'warn');
164
+ spawnFrom(arg, ctl, ui, images, undefined, 'ask');
165
+ return;
166
+ }
167
+ case '/task': {
168
+ if (!arg)
169
+ return ui.system(t('m.usageSpawn'), 'warn');
170
+ spawnFrom(arg, ctl, ui, images, undefined, 'task');
151
171
  return;
152
172
  }
153
173
  case '/plan': {
154
174
  // Plan-first agent: presents its plan (ask_user) and waits for approval
155
175
  // before touching any file.
156
176
  if (!arg)
157
- return ui.system(t('m.usagePlan'));
158
- spawnFrom(arg, ctl, ui, images, undefined, true);
177
+ return ui.system(t('m.usagePlan'), 'warn');
178
+ spawnFrom(arg, ctl, ui, images, undefined, 'plan');
159
179
  return;
160
180
  }
161
181
  case '/issue': {
162
182
  // Import a task from GitHub Issues (requires the gh CLI, authenticated).
163
183
  const n = Number.parseInt(arg, 10);
164
184
  if (!arg || Number.isNaN(n))
165
- return ui.system(t('m.usageIssue'));
185
+ return ui.system(t('m.usageIssue'), 'warn');
166
186
  const issue = ctl.fetchIssue(n);
167
187
  if ('error' in issue) {
168
- return ui.system(issue.error === 'gh-missing' ? t('m.ghMissing') : t('m.issueFail', { msg: issue.error }));
188
+ return ui.system(issue.error === 'gh-missing' ? t('m.ghMissing') : t('m.issueFail', { msg: issue.error }), 'error');
169
189
  }
170
190
  const task = `GitHub issue #${issue.number}: ${issue.title}\n\n${issue.body || '(no description)'}\n\nResolve this issue.`;
171
191
  const agent = ctl.spawnAgent(task);
172
192
  if (!agent)
173
- return ui.system(t('m.spawnFail'));
174
- ui.system(t('m.issueSpawned', { n: String(issue.number), name: agent.name, title: issue.title.slice(0, 60) }));
193
+ return ui.system(t('m.spawnFail'), 'error');
194
+ ui.system(t('m.issueSpawned', { n: String(issue.number), name: agent.name, title: issue.title.slice(0, 60) }), 'info');
175
195
  return;
176
196
  }
177
197
  case '/undo': {
178
198
  // Revert the agent's LAST file change (blackboard checkpoint).
179
199
  const who = arg || soloAgent(ctl);
180
200
  if (!who)
181
- return ui.system(t('m.usageUndo'));
201
+ return ui.system(t('m.usageUndo'), 'warn');
182
202
  const r = ctl.undoAgent(who);
183
203
  if (r === null)
184
- return ui.system(t('m.notFound', { target: who, list: agentList(ctl) }));
204
+ return ui.system(t('m.notFound', { target: who, list: agentList(ctl) }), 'error');
185
205
  if (r === 'none')
186
- return ui.system(t('m.undoNone', { name: who }));
187
- ui.system(t('m.undone', { name: who, path: r.path }) + (r.conflict ? ' ' + t('m.undoConflict') : ''));
206
+ return ui.system(t('m.undoNone', { name: who }), 'info');
207
+ ui.system(t('m.undone', { name: who, path: r.path }) + (r.conflict ? ' ' + t('m.undoConflict') : ''), r.conflict ? 'warn' : 'ok');
188
208
  return;
189
209
  }
190
210
  case '/commit': {
@@ -192,21 +212,21 @@ export function executeInput(raw, ctl, ui, images) {
192
212
  const [target0, ...msg] = rest;
193
213
  const target = target0 || soloAgent(ctl);
194
214
  if (!target)
195
- return ui.system(t('m.usageCommit'));
215
+ return ui.system(t('m.usageCommit'), 'warn');
196
216
  const r = ctl.commitFor(target, msg.join(' ').trim() || undefined);
197
217
  if (r.ok)
198
- return ui.system(t('m.committed', { name: target, files: String(r.files) }));
218
+ return ui.system(t('m.committed', { name: target, files: String(r.files) }), 'ok');
199
219
  if (r.reason === 'not-found')
200
- return ui.system(t('m.notFound', { target, list: agentList(ctl) }));
220
+ return ui.system(t('m.notFound', { target, list: agentList(ctl) }), 'error');
201
221
  if (r.reason === 'no-changes')
202
- return ui.system(t('m.commitNone', { name: target }));
203
- return ui.system(t('m.commitFail', { msg: r.detail ?? '' }));
222
+ return ui.system(t('m.commitNone', { name: target }), 'info');
223
+ return ui.system(t('m.commitFail', { msg: r.detail ?? '' }), 'error');
204
224
  }
205
225
  case '/autocommit': {
206
226
  if (arg !== 'on' && arg !== 'off')
207
- return ui.system(t('m.usageAutocommit', { state: ctl.autoCommit ? 'on' : 'off' }));
227
+ return ui.system(t('m.usageAutocommit', { state: ctl.autoCommit ? 'on' : 'off' }), 'warn');
208
228
  ctl.autoCommit = arg === 'on';
209
- ui.system(t('m.autocommit', { state: arg }));
229
+ ui.system(t('m.autocommit', { state: arg }), 'info');
210
230
  return;
211
231
  }
212
232
  case '/agents':
@@ -224,27 +244,27 @@ export function executeInput(raw, ctl, ui, images) {
224
244
  }
225
245
  const sessions = Controller.listSessions(ctl.projectRoot);
226
246
  if (sessions.length === 0)
227
- return ui.system(t('m.usageSession'));
247
+ return ui.system(t('m.usageSession'), 'warn');
228
248
  const idx = arg.toLowerCase() === 'latest' ? 0 : Number.parseInt(arg, 10) - 1;
229
249
  const session = sessions[idx];
230
250
  if (!session)
231
- return ui.system(t('m.usageSession'));
251
+ return ui.system(t('m.usageSession'), 'warn');
232
252
  ctl.loadSession(session.data);
233
- ui.system(t('m.sessionLoaded', { date: new Date(session.data.savedAt).toLocaleString() }));
253
+ ui.system(t('m.sessionLoaded', { date: new Date(session.data.savedAt).toLocaleString() }), 'ok');
234
254
  return;
235
255
  }
236
256
  case '/restore': {
237
257
  // Relaunch an agent from the restored session with its FULL conversation.
238
258
  if (!arg)
239
- return ui.system(t('m.usageRestore'));
259
+ return ui.system(t('m.usageRestore'), 'warn');
240
260
  if (!ctl.loadedSession)
241
- return ui.system(t('m.usageSession'));
261
+ return ui.system(t('m.usageSession'), 'warn');
242
262
  const res = ctl.respawnAgent(arg);
243
263
  if (res === 'no-conversation')
244
- return ui.system(t('m.noConversation', { name: arg }));
264
+ return ui.system(t('m.noConversation', { name: arg }), 'error');
245
265
  if (!res)
246
- return ui.system(t('m.spawnFail'));
247
- ui.system(t('m.restored', { name: res.name }));
266
+ return ui.system(t('m.spawnFail'), 'error');
267
+ ui.system(t('m.restored', { name: res.name }), 'ok');
248
268
  return;
249
269
  }
250
270
  case '/attach': {
@@ -252,55 +272,74 @@ export function executeInput(raw, ctl, ui, images) {
252
272
  // terminal per agent, connected to this session.
253
273
  const who = arg || soloAgent(ctl);
254
274
  if (!who)
255
- return ui.system(t('m.usageAttach', { state: ctl.autoAttach ? 'on' : 'off' }));
275
+ return ui.system(t('m.usageAttach', { state: ctl.autoAttach ? 'on' : 'off' }), 'warn');
256
276
  if (who === 'on' || who === 'off') {
257
277
  ctl.autoAttach = who === 'on';
258
- ui.system(t('m.attachAuto', { state: who }));
278
+ ui.system(t('m.attachAuto', { state: who }), 'info');
259
279
  return;
260
280
  }
261
281
  const a = ctl.board.getAgentByName(who);
262
282
  if (!a)
263
- return ui.system(t('m.notFound', { target: who, list: agentList(ctl) }));
283
+ return ui.system(t('m.notFound', { target: who, list: agentList(ctl) }), 'error');
264
284
  if (!ctl.attachEnabled)
265
- return ui.system(t('m.attachManual', { cmd: `parallel attach ${a.alias}` }));
285
+ return ui.system(t('m.attachManual', { cmd: `parallel attach ${a.alias}` }), 'warn');
266
286
  const r = ctl.openTerminal(a.alias);
267
287
  ui.system(r === 'opened'
268
288
  ? t('m.attachOpened', { name: a.name })
269
- : t('m.attachManual', { cmd: `parallel attach ${a.alias}` }));
289
+ : t('m.attachManual', { cmd: `parallel attach ${a.alias}` }), r === 'opened' ? 'ok' : 'warn');
270
290
  return;
271
291
  }
272
292
  case '/focus': {
273
293
  const who = arg || soloAgent(ctl);
274
294
  if (!who)
275
- return ui.system(t('m.usageFocus'));
295
+ return ui.system(t('m.usageFocus'), 'warn');
276
296
  if (!ui.setFocus)
277
- return;
297
+ return ui.system(t('m.focusOff'), 'info');
278
298
  if (who.toLowerCase() === 'off') {
279
299
  ui.setFocus(null);
280
- ui.system(t('m.focusOff'));
300
+ ui.system(t('m.focusOff'), 'info');
281
301
  return;
282
302
  }
283
303
  const a = ctl.board.getAgentByName(who);
284
304
  if (!a)
285
- return ui.system(t('m.notFound', { target: who, list: agentList(ctl) }));
305
+ return ui.system(t('m.notFound', { target: who, list: agentList(ctl) }), 'error');
286
306
  ui.setFocus(a.name);
287
- ui.system(t('m.focusOn', { name: a.name }));
307
+ ui.system(t('m.focusOn', { name: a.name }), 'ok');
288
308
  return;
289
309
  }
290
310
  case '/doctor': {
291
311
  const p = ctl.sessionProvider();
292
312
  if (!p)
293
- return ui.system(t('m.missingProvider'));
313
+ return ui.system(t('m.missingProvider'), 'error');
294
314
  if (!p.apiKey)
295
- return ui.system(t('m.missingKey', { name: p.name }));
315
+ return ui.system(t('m.missingKey', { name: p.name }), 'error');
296
316
  if (!ctl.session.model && !p.defaultModel && !p.models[0])
297
- return ui.system(t('m.missingModel', { name: p.name }));
298
- ui.system(t('m.doctorOk', { pm: `${p.name}:${ctl.session.model || p.defaultModel || p.models[0]}` }));
317
+ return ui.system(t('m.missingModel', { name: p.name }), 'error');
318
+ ui.system(t('m.doctorOk', { pm: `${p.name}:${ctl.session.model || p.defaultModel || p.models[0]}` }), 'ok');
299
319
  return;
300
320
  }
301
321
  case '/cost':
302
322
  ui.setView('cost');
303
323
  return;
324
+ case '/status': {
325
+ const p = ctl.sessionProvider();
326
+ const agents = [...ctl.board.agents.values()];
327
+ const active = agents.filter((a) => ['working', 'thinking', 'listening', 'waiting'].includes(a.state)).length;
328
+ const cost = agents.reduce((s, a) => s + (a.cost ?? 0), 0);
329
+ const changed = new Set(ctl.board.changes.map((c) => c.path)).size;
330
+ const pm = p ? `${p.name}:${ctl.session.model}` : '-';
331
+ // Multiline: each metric on its own line for readability.
332
+ ui.system(t('m.status', { pm, approval: ctl.session.approvalMode, total: agents.length, active, changed, cost: cost.toFixed(3) }), 'info');
333
+ return;
334
+ }
335
+ case '/raw':
336
+ ui.toggleRaw?.();
337
+ // The rawLogs state is toggled by toggleRaw — the caller in App.tsx
338
+ // provides the current value via a closure; we let App.tsx decide which message.
339
+ return;
340
+ case '/copy':
341
+ ui.copyLatest?.();
342
+ return;
304
343
  case '/skills':
305
344
  ui.setView('skills');
306
345
  return;
@@ -311,41 +350,41 @@ export function executeInput(raw, ctl, ui, images) {
311
350
  // /skill new <name> [global] → create a template file to edit
312
351
  const m = arg.match(/^new\s+([\p{L}\p{N}_-]+)(\s+global)?$/iu);
313
352
  if (!m)
314
- return ui.system(t('m.usageSkill'));
353
+ return ui.system(t('m.usageSkill'), 'warn');
315
354
  try {
316
355
  const file = createSkillTemplate(m[1], '', m[2] ? 'global' : 'project', ctl.projectRoot);
317
- ui.system(t('m.skillCreated', { file }));
356
+ ui.system(t('m.skillCreated', { file }), 'ok');
318
357
  }
319
358
  catch (e) {
320
- ui.system(t('m.alreadyExists', { msg: e?.message ?? '' }));
359
+ ui.system(t('m.alreadyExists', { msg: e?.message ?? '' }), 'error');
321
360
  }
322
361
  return;
323
362
  }
324
363
  case '/specialist': {
325
364
  if (!arg)
326
- return ui.system(t('m.usageSpecialist'));
365
+ return ui.system(t('m.usageSpecialist'), 'warn');
327
366
  // /specialist new <name> [global] → create a template file
328
367
  const created = arg.match(/^new\s+([\p{L}\p{N}_-]+)(\s+global)?$/iu);
329
368
  if (created) {
330
369
  try {
331
370
  const file = createSpecialistTemplate(created[1], '', created[2] ? 'global' : 'project', ctl.projectRoot);
332
- ui.system(t('m.specCreated', { file }));
371
+ ui.system(t('m.specCreated', { file }), 'ok');
333
372
  }
334
373
  catch (e) {
335
- ui.system(t('m.alreadyExists', { msg: e?.message ?? '' }));
374
+ ui.system(t('m.alreadyExists', { msg: e?.message ?? '' }), 'error');
336
375
  }
337
376
  return;
338
377
  }
339
378
  // /specialist <name> [Name:] <task> → spawn an agent with this persona
340
379
  const m = arg.match(/^([\p{L}\p{N}_-]+)\s+(.+)$/su);
341
380
  if (!m)
342
- return ui.system(t('m.usageSpecialist'));
381
+ return ui.system(t('m.usageSpecialist'), 'warn');
343
382
  const exists = ctl.getSpecialists().some((s) => s.name === m[1].toLowerCase());
344
383
  if (!exists) {
345
384
  const list = ctl.getSpecialists().map((s) => s.name).join(', ') || t('m.none');
346
- return ui.system(t('m.noSpecialist', { name: m[1] }) + ` (${list})`);
385
+ return ui.system(t('m.noSpecialist', { name: m[1] }) + ` (${list})`, 'error');
347
386
  }
348
- spawnFrom(m[2], ctl, ui, images, m[1].toLowerCase());
387
+ spawnFrom(m[2], ctl, ui, images, m[1].toLowerCase(), 'task');
349
388
  return;
350
389
  }
351
390
  case '/board':
@@ -361,48 +400,51 @@ export function executeInput(raw, ctl, ui, images) {
361
400
  const [target, ...msg] = rest;
362
401
  const content = msg.join(' ').trim();
363
402
  if (!target || !content)
364
- return ui.system(t('m.usageSend'));
403
+ return ui.system(t('m.usageSend'), 'warn');
365
404
  executeInput(`@${target} ${content}`, ctl, ui);
366
405
  return;
367
406
  }
368
407
  case '/pause': {
369
408
  const who = arg || soloAgent(ctl);
370
409
  if (!who)
371
- return ui.system(t('m.usagePause'));
410
+ return ui.system(t('m.usagePause'), 'warn');
372
411
  if (who === 'all') {
373
412
  for (const a of ctl.board.agents.values())
374
413
  ctl.pauseAgent(a.name);
375
- ui.system(t('m.allPaused'));
414
+ ui.system(t('m.allPaused'), 'ok');
376
415
  }
377
416
  else {
378
- ui.system(ctl.pauseAgent(who) ? t('m.paused', { name: who }) : t('m.notFound', { target: who, list: agentList(ctl) }));
417
+ const ok = ctl.pauseAgent(who);
418
+ ui.system(ok ? t('m.paused', { name: who }) : t('m.notFound', { target: who, list: agentList(ctl) }), ok ? 'ok' : 'error');
379
419
  }
380
420
  return;
381
421
  }
382
422
  case '/resume': {
383
423
  const who = arg || soloAgent(ctl);
384
424
  if (!who)
385
- return ui.system(t('m.usageResume'));
425
+ return ui.system(t('m.usageResume'), 'warn');
386
426
  if (who === 'all') {
387
427
  for (const a of ctl.board.agents.values())
388
428
  ctl.resumeAgent(a.name);
389
- ui.system(t('m.allResumed'));
429
+ ui.system(t('m.allResumed'), 'ok');
390
430
  }
391
431
  else {
392
- ui.system(ctl.resumeAgent(who) ? t('m.resumed', { name: who }) : t('m.notFound', { target: who, list: agentList(ctl) }));
432
+ const ok = ctl.resumeAgent(who);
433
+ ui.system(ok ? t('m.resumed', { name: who }) : t('m.notFound', { target: who, list: agentList(ctl) }), ok ? 'ok' : 'error');
393
434
  }
394
435
  return;
395
436
  }
396
437
  case '/stop': {
397
438
  const who = arg || soloAgent(ctl);
398
439
  if (!who)
399
- return ui.system(t('m.usageStop'));
440
+ return ui.system(t('m.usageStop'), 'warn');
400
441
  if (who === 'all') {
401
442
  ctl.stopAll();
402
- ui.system(t('m.allStopped'));
443
+ ui.system(t('m.allStopped'), 'ok');
403
444
  }
404
445
  else {
405
- ui.system(ctl.stopAgent(who) ? t('m.stopped', { name: who }) : t('m.notFound', { target: who, list: agentList(ctl) }));
446
+ const ok = ctl.stopAgent(who);
447
+ ui.system(ok ? t('m.stopped', { name: who }) : t('m.notFound', { target: who, list: agentList(ctl) }), ok ? 'ok' : 'error');
406
448
  }
407
449
  return;
408
450
  }
@@ -410,71 +452,73 @@ export function executeInput(raw, ctl, ui, images) {
410
452
  case '/model': {
411
453
  if (!arg) {
412
454
  const p = ctl.sessionProvider();
413
- return ui.system(t('m.model', { pm: p ? `${p.name}:${ctl.session.model}` : '—' }));
455
+ return ui.system(t('m.model', { pm: p ? `${p.name}:${ctl.session.model}` : '—' }), 'info');
414
456
  }
415
457
  const r = ctl.setSessionModel(arg);
416
458
  if (!r) {
417
459
  const provName = arg.includes(':') ? arg.split(':')[0] : arg;
418
- return ui.system(t('m.noProvider', { name: provName, list: ctl.config.providers.map((p) => p.name).join(', ') || t('m.none') }));
460
+ return ui.system(t('m.noProvider', { name: provName, list: ctl.config.providers.map((p) => p.name).join(', ') || t('m.none') }), 'error');
419
461
  }
420
- ui.system(t('m.modelSet', { pm: `${r.provider}:${r.model}` }));
462
+ ui.system(t('m.modelSet', { pm: `${r.provider}:${r.model}` }), 'ok');
421
463
  return;
422
464
  }
423
465
  case '/settings':
424
466
  ui.setView('settings');
425
467
  return;
426
468
  case '/settings-session':
427
- case '/ssettings':
428
469
  ui.setView('settings-session');
429
470
  return;
430
471
  // SESSION-only approvals & sound (global defaults editable in /settings).
431
472
  case '/approvals': {
432
- if (arg !== 'ask' && arg !== 'auto')
433
- return ui.system(t('m.usageApprovals'));
434
- ctl.setSessionApprovalMode(arg);
435
- ui.system(t('m.approvals', { mode: arg }) + (arg === 'auto' ? t('m.approvalsWarn') : ''));
473
+ const mode = normalizeShellApprovalMode(arg);
474
+ if (!mode)
475
+ return ui.system(t('m.usageApprovals'), 'warn');
476
+ ctl.setSessionApprovalMode(mode);
477
+ const approvalLevel = mode === 'yolo' ? 'warn' : 'ok';
478
+ ui.system(t('m.approvals', { mode }) + (mode === 'auto-safe' ? t('m.approvalsWarn') : mode === 'yolo' ? t('m.approvalsYoloWarn') : ''), approvalLevel);
436
479
  return;
437
480
  }
438
481
  case '/sound': {
439
482
  if (arg !== 'on' && arg !== 'off')
440
- return ui.system(t('m.usageSound', { state: ctl.session.soundEnabled ? 'on' : 'off' }));
483
+ return ui.system(t('m.usageSound', { state: ctl.session.soundEnabled ? 'on' : 'off' }), 'warn');
441
484
  ctl.setSessionSound(arg === 'on');
442
- ui.system(t('m.sound', { state: arg }));
485
+ ui.system(t('m.sound', { state: arg }), 'ok');
443
486
  return;
444
487
  }
445
488
  case '/save': {
446
489
  const file = ctl.saveSession(arg || undefined);
447
- ui.system(file ? (arg ? t('m.savedAs', { name: arg }) : t('m.saved')) : t('m.nothing'));
490
+ ui.system(file ? (arg ? t('m.savedAs', { name: arg }) : t('m.saved')) : t('m.nothing'), file ? 'ok' : 'warn');
448
491
  return;
449
492
  }
450
493
  case '/key': {
451
494
  if (!arg)
452
- return ui.system(t('m.usageKey'));
495
+ return ui.system(t('m.usageKey'), 'warn');
453
496
  const ok = ctl.setApiKey(arg);
454
- ui.system(ok ? t('m.keySaved', { name: ctl.sessionProvider()?.name ?? '?' }) : t('m.spawnFail'));
497
+ ui.system(ok ? t('m.keySaved', { name: ctl.sessionProvider()?.name ?? '?' }) : t('m.spawnFail'), ok ? 'ok' : 'error');
455
498
  return;
456
499
  }
457
500
  case '/clear': {
501
+ let cleared = 0;
458
502
  for (const [id, a] of [...ctl.board.agents.entries()]) {
459
503
  if (['done', 'stopped', 'error'].includes(a.state)) {
460
504
  ctl.board.agents.delete(id);
461
505
  ctl.agents.delete(id);
506
+ cleared++;
462
507
  }
463
508
  }
464
509
  ctl.emit('update');
465
- ui.system(t('m.cleared'));
510
+ ui.system(cleared > 0 ? t('m.clearedN', { n: cleared }) : t('m.clearedNone'), 'ok');
466
511
  return;
467
512
  }
468
513
  case '/help':
469
514
  ui.setView('help');
470
515
  return;
471
516
  case '/quit':
472
- case '/exit':
473
517
  ctl.saveSession();
474
518
  ctl.stopAll();
475
519
  ui.exit();
476
520
  return;
477
521
  default:
478
- ui.system(t('m.unknown', { cmd }));
522
+ ui.system(t('m.unknown', { cmd }), 'error');
479
523
  }
480
524
  }