@lardexx/arenacode 2.6.0 → 2.6.1

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/README.md CHANGED
@@ -115,7 +115,7 @@ How the copy is built and tested: [`docs/TUI.md`](docs/TUI.md).
115
115
  | `!command` | run a command locally |
116
116
  | `↑` / `↓` | prompt history (or move through the slash list) |
117
117
  | `pgup` / `pgdn` | scroll the transcript |
118
- | `ctrl+x` then | `q` quit · `n` new session · `l` task queue · `t` theme · `d` tool output · `b` sidebar · `s` stop |
118
+ | `ctrl+x` then | `q` quit · `n` new session · `l` task queue · `t` theme · `a` Ask and Proceed / Proceed · `d` tool output · `s` stop |
119
119
  | `ctrl+c` | interrupt, then quit |
120
120
 
121
121
  **The same four flows as the desktop client.** The Windows app is the reference for how a task reaches the bridge,
@@ -134,6 +134,13 @@ so the port reproduces it byte for byte:
134
134
 
135
135
  The exact strings are checked by `test/parity.js` against the ones extracted from `ArenaCode-x64.exe`.
136
136
 
137
+ **Ask and Proceed / Proceed.** The desktop's Settings switch, `ctrl+x a` here (also `/ask`, `/proceed`,
138
+ `--proceed`, and `arenacode config set approval proceed`). The default, **Ask and Proceed**, asks before commands,
139
+ file changes and plans; in Computer mode it asks once, on the plan, and after that the commands run on their own
140
+ while administrator commands still ask. **Proceed** approves its own plan, runs commands and installations and
141
+ writes files without asking: turning it on shows the desktop's own warning (*"Pericolo: modalità Proceed"*).
142
+ `--yes` is the older switch and still answers yes to commands and files, but never to a plan.
143
+
137
144
  **Dialogs.** Local actions open opencode-shaped dialogs — panel background, title on the left, `esc` on the right,
138
145
  the selected row painted in the primary colour, hints underneath: `Run once / Don't run` for terminal commands,
139
146
  `Approve / Reject / Revise` for the agent's plan, and file operations reported back to the bridge. `--yes` skips the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lardexx/arenacode",
3
- "version": "2.6.0",
3
+ "version": "2.6.1",
4
4
  "description": "Terminal client for ArenaCode: talks to an arena-bridge, streams the Agent Mode timeline, and runs local commands and file operations on your PC. Node port of the ArenaCode desktop client.",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
@@ -0,0 +1,98 @@
1
+ 'use strict';
2
+
3
+ // The desktop client has one setting, Settings → "Ask and Proceed" (default) or
4
+ // "Proceed", and it changes every approval the client makes:
5
+ //
6
+ // Ask and Proceed (IDE mode) file changes wait for Accept/Reject, commands wait for Run
7
+ // Ask and Proceed (Computer) one confirmation, on the plan; then commands run alone
8
+ // (commands with administrator rights still ask)
9
+ // Proceed the client approves its own plan, runs commands and
10
+ // installations and uses mouse and keyboard without asking
11
+ //
12
+ // `--yes` is the old switch of this client and keeps meaning "say yes to commands
13
+ // and file changes", but never to a plan: a plan is the user's decision.
14
+ const APPROVALS = ['ask', 'proceed'];
15
+
16
+ const DANGER = {
17
+ it: {
18
+ title: 'Pericolo: modalità Proceed',
19
+ body:
20
+ 'PERICOLO: in Proceed il Computer Agent approva da solo il piano, esegue comandi e installazioni ' +
21
+ 'e usa mouse e tastiera senza chiedere. Può cancellare o modificare dati e programmi.',
22
+ },
23
+ en: {
24
+ title: 'Danger: Proceed mode',
25
+ body:
26
+ 'DANGER: in Proceed the Computer Agent approves its own plan, runs commands and installations, and ' +
27
+ 'uses mouse and keyboard without asking. It can delete or change data and programs.',
28
+ },
29
+ es: {
30
+ title: 'Peligro: modo Proceed',
31
+ body:
32
+ 'PELIGRO: en Proceed el Computer Agent aprueba su propio plan, ejecuta comandos e instalaciones y ' +
33
+ 'usa el ratón y el teclado sin preguntar. Puede borrar o modificar datos y programas.',
34
+ },
35
+ };
36
+
37
+ const HELP_TEXT = {
38
+ it: {
39
+ ide:
40
+ 'Ask and Proceed: Arena chiede conferma per comandi, modifiche ai file e piani. ' +
41
+ 'Proceed: esegue tutto da solo.',
42
+ computer:
43
+ 'Ask and Proceed: il Computer Agent chiede conferma una sola volta, sul piano. Dopo ' +
44
+ "l'approvazione esegue i comandi da solo (i comandi con permessi di amministratore chiedono " +
45
+ 'ancora conferma). Proceed: agisce da solo, senza chiedere nulla.',
46
+ },
47
+ en: {
48
+ ide:
49
+ 'Ask and Proceed: Arena asks before commands, file changes and plans. ' +
50
+ 'Proceed: does everything on its own.',
51
+ computer:
52
+ 'Ask and Proceed: the Computer Agent asks for confirmation once, on the plan. After approval ' +
53
+ 'it runs commands on its own (commands with administrator rights still ask). Proceed: it acts ' +
54
+ 'on its own, without asking anything.',
55
+ },
56
+ es: {
57
+ ide:
58
+ 'Ask and Proceed: Arena pide confirmación para comandos, cambios en archivos y planes. ' +
59
+ 'Proceed: hace todo por sí solo.',
60
+ computer:
61
+ 'Ask and Proceed: el Computer Agent pide confirmación una sola vez, sobre el plan. Tras ' +
62
+ 'aprobarlo ejecuta los comandos por sí solo (los comandos con permisos de administrador ' +
63
+ 'siguen pidiendo confirmación). Proceed: actúa por sí solo, sin preguntar nada.',
64
+ },
65
+ };
66
+
67
+ function normaliseApproval(value) {
68
+ return APPROVALS.includes(value) ? value : 'ask';
69
+ }
70
+
71
+ /**
72
+ * What the client should do with one request from the agent.
73
+ * kind: 'plan' | 'fs' | 'exec'
74
+ * Returns 'auto' (answer yes without showing anything) or 'ask' (show the card).
75
+ */
76
+ function decide({ approval = 'ask', mode = 'agent', planApproved = false, kind, admin = false, yes = false }) {
77
+ if (normaliseApproval(approval) === 'proceed') return 'auto';
78
+ if (kind === 'plan') return 'ask';
79
+ if (yes) return 'auto';
80
+ if (kind === 'fs') return 'ask';
81
+ if (kind === 'exec') {
82
+ if (admin) return 'ask';
83
+ if (mode === 'computer-agent' && planApproved) return 'auto';
84
+ return 'ask';
85
+ }
86
+ return 'ask';
87
+ }
88
+
89
+ function dangerText(lang) {
90
+ return DANGER[lang] || DANGER.en;
91
+ }
92
+
93
+ function approvalHelp(lang, mode) {
94
+ const set = HELP_TEXT[lang] || HELP_TEXT.en;
95
+ return mode === 'computer-agent' ? set.computer : set.ide;
96
+ }
97
+
98
+ module.exports = { APPROVALS, decide, dangerText, approvalHelp, normaliseApproval, DANGER, HELP_TEXT };
package/src/cli.js CHANGED
@@ -11,6 +11,8 @@ const {
11
11
  const { Bridge } = require('./bridge');
12
12
  const { MODE_LINES, MODES, normaliseMode } = require('./modes');
13
13
  const { attachRefs, decorate } = require('./attach');
14
+ const { decide, dangerText, normaliseApproval } = require('./approval');
15
+ const session = require('./session');
14
16
  const { buildMeta, readProjectContext } = require('./meta');
15
17
  const { followTask } = require('./turn');
16
18
  const { c, write, ask, banner, truncate, indentLines } = require('./ui');
@@ -37,7 +39,7 @@ const INSTALL_HINT = process.platform === 'win32'
37
39
  // argument parsing
38
40
  // --------------------------------------------------------------------------- //
39
41
 
40
- const BOOLEAN_FLAGS = new Set(['yes', 'json', 'help', 'quiet', 'version', 'confirm-writes', 'no-pc', 'piped']);
42
+ const BOOLEAN_FLAGS = new Set(['yes', 'json', 'help', 'quiet', 'version', 'confirm-writes', 'no-pc', 'piped', 'proceed', 'ask']);
41
43
 
42
44
  function parseArgs(argv) {
43
45
  const flags = {};
@@ -100,6 +102,7 @@ function context(flags) {
100
102
  yes: Boolean(flags.yes),
101
103
  json: Boolean(flags.json),
102
104
  confirmWrites: Boolean(flags['confirm-writes']),
105
+ approval: normaliseApproval(flags.proceed ? 'proceed' : flags.ask ? 'ask' : cfg.approval),
103
106
  idleTimeout: Number(flags['idle-timeout'] || cfg.idle_timeout || 180),
104
107
  bridge: new Bridge(url, key, device),
105
108
  };
@@ -191,7 +194,13 @@ function cmdConfig(positional, flags) {
191
194
  const name = key;
192
195
  const val = rest.join(' ');
193
196
  if (!name || val === '') {
194
- write(`usage: ${SELF} config set <url|key|lang|mode|pc_access> <value>`);
197
+ write(`usage: ${SELF} config set <url|key|lang|mode|approval|pc_access> <value>`);
198
+ return 2;
199
+ }
200
+ if (name === 'approval' && !['ask', 'proceed'].includes(String(val))) {
201
+ write(c.red(' ✗ approval must be ask or proceed'));
202
+ write(c.gray(` ask ${'Ask and Proceed'}: the client asks before commands, file changes and plans`));
203
+ write(c.gray(` proceed Proceed: the client does everything on its own`));
195
204
  return 2;
196
205
  }
197
206
  const patch = {};
@@ -200,6 +209,12 @@ function cmdConfig(positional, flags) {
200
209
  else patch[name] = val;
201
210
  const file = patchConfig(patch);
202
211
  write(` ${c.green('✓')} ${name} saved ${c.gray(file)}`);
212
+ if (name === 'approval' && patch.approval === 'proceed') {
213
+ const d = dangerText(cfg.lang);
214
+ write('');
215
+ write(` ${c.bold(c.red('⚠ ' + d.title))}`);
216
+ write(indentLines(c.yellow(d.body), ' '));
217
+ }
203
218
  return 0;
204
219
  }
205
220
  if (action === 'clear') {
@@ -283,6 +298,7 @@ async function createTask(ctx, instruction, { extraInstructions = '' } = {}) {
283
298
  if (extraInstructions) body.instruction = `${instruction}\n\n${extraInstructions}`;
284
299
 
285
300
  const res = await ctx.bridge.createTask(body);
301
+ if (res.status === 202 || res.status === 200) session.remember(res.json && res.json.task_id, ctx.mode);
286
302
  if (res.status !== 202 && res.status !== 200) {
287
303
  const reason = (res.json && (res.json.error || res.json.hint)) || `HTTP ${res.status}`;
288
304
  throw new Error(reason);
@@ -322,6 +338,7 @@ async function runInstruction(ctx, text, { attachments = [] } = {}) {
322
338
  folder: ctx.folder,
323
339
  lang: ctx.lang,
324
340
  yes: ctx.yes,
341
+ approval: ctx.approval,
325
342
  confirmWrites: ctx.confirmWrites,
326
343
  idleTimeout: ctx.idleTimeout,
327
344
  });
@@ -381,6 +398,7 @@ async function cmdUi(flags) {
381
398
  lang: ctx.lang,
382
399
  mode: ctx.mode,
383
400
  yes: ctx.yes,
401
+ approval: ctx.approval,
384
402
  confirmWrites: ctx.confirmWrites,
385
403
  });
386
404
  await app.start();
@@ -743,7 +761,9 @@ function help() {
743
761
  write(` --lang it|en|es language for instructions and cards`);
744
762
  write(` --no-pc tell the agent the PC terminal is off`);
745
763
  write(` --yes run local commands without asking`);
746
- write(` --confirm-writes ask before write/replace/rm/mv on your folder`);
764
+ write(` --ask Ask and Proceed: ask before commands, file changes and plans (default)
765
+ --proceed Proceed: approve the plan and run everything without asking
766
+ --confirm-writes ask before write/replace/rm/mv on your folder`);
747
767
  write(` --attach a,b upload files with the task`);
748
768
  write(` --json raw JSON output where available`);
749
769
  write('');
package/src/config.js CHANGED
@@ -29,6 +29,7 @@ const DEFAULTS = {
29
29
  folder: '',
30
30
  lang: 'it',
31
31
  mode: 'agent',
32
+ approval: 'ask',
32
33
  pc_access: true,
33
34
  device: '',
34
35
  vision: false,
package/src/local.js CHANGED
@@ -1,5 +1,8 @@
1
1
  'use strict';
2
2
 
3
+ const { decide } = require('./approval');
4
+ const session = require('./session');
5
+
3
6
  const fs = require('node:fs');
4
7
  const os = require('node:os');
5
8
  const path = require('node:path');
@@ -98,7 +101,18 @@ async function handleLocalExec(bridge, taskId, request, opts) {
98
101
  if (admin) write(` ${c.yellow('!')} ${t(lang, 'Richiede i permessi di amministratore: qui il comando gira senza UAC.', 'Needs administrator rights: here it runs without UAC.', 'Necesita permisos de administrador: aquí se ejecuta sin UAC.')}`);
99
102
  if (sandboxReason) write(` ${c.yellow('!')} ${t(lang, 'L’agente chiede di installare sul PC', 'The agent asks to install on the PC', 'El agente pide instalar en el PC')}: ${c.gray(sandboxReason)}`);
100
103
 
101
- let approved = opts.yes === true;
104
+ const verdict = decide({
105
+ approval: opts.approval,
106
+ mode: session.modeOf(taskId),
107
+ planApproved: session.planApproved(taskId),
108
+ kind: 'exec',
109
+ admin: Boolean(admin),
110
+ yes: opts.yes === true,
111
+ });
112
+ let approved = verdict === 'auto';
113
+ if (verdict === 'auto') {
114
+ write(` ${c.green('✓')} ${c.gray(t(lang, 'approvato dal client (Proceed)', 'approved by the client (Proceed)', 'aprobado por el cliente (Proceed)'))}`);
115
+ }
102
116
  if (!approved) {
103
117
  const key = await askKey(` ${t(lang, 'Eseguo? [r] sì [d] no', 'Run it? [r] yes [d] no', '¿Ejecuto? [r] sí [d] no')} `, ['r', 'd']);
104
118
  approved = key === 'r';
@@ -368,7 +382,18 @@ async function handleLocalFs(bridge, taskId, request, opts) {
368
382
  write(` ${c.bold(c.yellow('▤ files'))} ${c.dim('·')} ${c.cyan(op)} ${c.gray(request.path || '.')}${request.to ? c.gray(` → ${request.to}`) : ''}`);
369
383
 
370
384
  const mutating = ['write', 'replace', 'rm', 'mv', 'mkdir'].includes(op);
371
- if (mutating && opts.confirmWrites) {
385
+ const verdict = mutating
386
+ ? decide({
387
+ approval: opts.approval,
388
+ mode: session.modeOf(taskId),
389
+ planApproved: session.planApproved(taskId),
390
+ kind: 'fs',
391
+ yes: opts.yes === true,
392
+ })
393
+ : 'auto';
394
+ // --confirm-writes keeps asking even when the client runs in Proceed
395
+ const mustAsk = mutating && (verdict === 'ask' || opts.confirmWrites);
396
+ if (mustAsk) {
372
397
  const key = await askKey(` ${t(lang, 'Applico? [s] sì [n] no', 'Apply? [y] yes [n] no', '¿Aplico? [s] sí [n] no')} `, ['s', 'y', 'n']);
373
398
  if (key === 'n') {
374
399
  await bridge.localResult(taskId, { rid, output: JSON.stringify({ ok: false, error: 'rejected by the user' }), code: 1 });
@@ -406,6 +431,20 @@ async function handleLocalUi(bridge, taskId, request, opts) {
406
431
  const lang = opts.lang;
407
432
 
408
433
  if (op === 'plan') {
434
+ const planVerdict = decide({
435
+ approval: opts.approval,
436
+ mode: session.modeOf(taskId),
437
+ kind: 'plan',
438
+ yes: opts.yes === true,
439
+ });
440
+ if (planVerdict === 'auto') {
441
+ session.markPlanApproved(taskId);
442
+ write('');
443
+ write(` ${c.bold(c.magenta('✦ plan'))} ${c.dim('·')} ${t(lang, 'approvato dal client (Proceed)', 'approved by the client (Proceed)', 'aprobado por el cliente (Proceed)')}`);
444
+ write(indentLines(request.text || '', ' '));
445
+ await bridge.localResult(taskId, { rid, output: JSON.stringify({ ok: true, approved: true }), code: 0 });
446
+ return true;
447
+ }
409
448
  write('');
410
449
  write(` ${c.bold(c.magenta('✦ plan'))} ${c.dim('·')} ${t(lang, 'da approvare', 'to approve', 'para aprobar')}`);
411
450
  write(indentLines(request.text || '', ' '));
@@ -415,6 +454,7 @@ async function handleLocalUi(bridge, taskId, request, opts) {
415
454
  ['a', 'r', 'f'],
416
455
  );
417
456
  if (key === 'a') {
457
+ session.markPlanApproved(taskId);
418
458
  await bridge.localResult(taskId, { rid, output: JSON.stringify({ ok: true, approved: true }), code: 0 });
419
459
  write(` ${c.green('✓')} ${t(lang, 'piano approvato', 'plan approved', 'plan aprobado')}`);
420
460
  } else if (key === 'f') {
package/src/session.js ADDED
@@ -0,0 +1,33 @@
1
+ 'use strict';
2
+
3
+ // Per-task state the approval rules need: the mode the task was started in and
4
+ // whether its plan has already been approved (Computer Agent mode runs its
5
+ // commands alone after that one approval).
6
+ const tasks = new Map();
7
+
8
+ function remember(taskId, mode) {
9
+ if (!taskId) return;
10
+ tasks.set(taskId, { mode: mode || 'agent', planApproved: false });
11
+ }
12
+
13
+ function modeOf(taskId) {
14
+ const t = tasks.get(taskId);
15
+ return t ? t.mode : 'agent';
16
+ }
17
+
18
+ function markPlanApproved(taskId) {
19
+ const t = tasks.get(taskId);
20
+ if (t) t.planApproved = true;
21
+ else if (taskId) tasks.set(taskId, { mode: 'agent', planApproved: true });
22
+ }
23
+
24
+ function planApproved(taskId) {
25
+ const t = tasks.get(taskId);
26
+ return Boolean(t && t.planApproved);
27
+ }
28
+
29
+ function forget(taskId) {
30
+ tasks.delete(taskId);
31
+ }
32
+
33
+ module.exports = { remember, modeOf, markPlanApproved, planApproved, forget };
package/src/tui/app.js CHANGED
@@ -20,6 +20,8 @@ const { readConfig, patchConfig, deviceId, resolveUrl, resolveKey, openFolder, V
20
20
  const { MODE_LINES, modeLabel, nextMode, normaliseMode } = require('../modes');
21
21
  const { attachRefs, attachImage, decorate, strings } = require('../attach');
22
22
  const { grabImage, readClipboardText, stamp } = require('../clipboard');
23
+ const { decide, dangerText, approvalHelp, normaliseApproval } = require('../approval');
24
+ const session = require('../session');
23
25
 
24
26
  const AGENT_PROMPT = (() => {
25
27
  try {
@@ -46,6 +48,7 @@ class App {
46
48
  this.theme = getTheme(this.themeName);
47
49
  this.autoApprove = Boolean(opts.yes);
48
50
  this.confirmWrites = Boolean(opts.confirmWrites);
51
+ this.approval = normaliseApproval(opts.approval);
49
52
 
50
53
  this.state = {
51
54
  mode: normaliseMode(opts.mode || cfg.mode || 'agent'),
@@ -93,6 +96,12 @@ class App {
93
96
  });
94
97
  this.term.on('resize', () => this.invalidate());
95
98
  this.running = true;
99
+ if (this.approval === 'proceed') {
100
+ this.push({
101
+ kind: 'error',
102
+ text: `approval: Proceed · ${approvalHelp(this.lang, this.state.mode)}`,
103
+ });
104
+ }
96
105
 
97
106
  const interval = setInterval(() => {
98
107
  this.state.spinner = (this.state.spinner + 1) % SPINNER.length;
@@ -285,6 +294,8 @@ class App {
285
294
  return undefined;
286
295
  case 's':
287
296
  return this.stopTurn();
297
+ case 'a':
298
+ return this.toggleApproval();
288
299
  case 'p':
289
300
  return this.openPalette();
290
301
  default:
@@ -450,6 +461,7 @@ class App {
450
461
  this.state.mode = nextMode(this.state.mode);
451
462
  this.toast(`mode: ${modeLabel(this.state.mode)}`);
452
463
  } },
464
+ { title: `Approval: ${this.approval === 'proceed' ? 'Proceed' : 'Ask and Proceed'}`, hint: 'ctrl+x a', run: () => this.toggleApproval() },
453
465
  { title: 'Theme', hint: 'ctrl+x t', run: () => {
454
466
  const names = themeNames();
455
467
  const next = names[(names.indexOf(this.themeName) + 1) % names.length];
@@ -470,17 +482,62 @@ class App {
470
482
 
471
483
  /** `/help`, `/new`, `/tasks` … — opencode-style slash commands */
472
484
  slashCommands() {
485
+ const by = (prefix) => {
486
+ const found = this.commands().find((cmd) => cmd.title.startsWith(prefix));
487
+ return () => (found ? found.run() : undefined);
488
+ };
473
489
  return [
474
- { slash: '/new', title: 'Start a new session', hint: 'ctrl+x n', group: 'Session', run: () => this.commands()[1].run() },
490
+ { slash: '/new', title: 'Start a new session', hint: 'ctrl+x n', group: 'Session', run: by('New session') },
475
491
  { slash: '/tasks', title: 'Show the task queue', hint: 'ctrl+x l', group: 'Session', run: () => this.openTasks() },
476
- { slash: '/mode', title: 'Switch agent / plan', hint: 'tab', group: 'Session', run: () => this.commands()[5].run() },
477
- { slash: '/theme', title: 'Cycle the colour theme', hint: 'ctrl+x t', group: 'Session', run: () => this.commands()[6].run() },
478
- { slash: '/status', title: 'Ping the bridge', hint: '', group: 'Session', run: () => this.commands()[7].run() },
492
+ { slash: '/mode', title: 'Switch agent / plan', hint: 'tab', group: 'Session', run: by('Switch mode') },
493
+ { slash: '/ask', title: 'Ask and Proceed: ask before commands, files and plans', hint: '', group: 'Session', run: () => this.setApproval('ask') },
494
+ { slash: '/proceed', title: 'Proceed: approve the plan and run everything without asking', hint: '', group: 'Session', run: () => this.setApproval('proceed') },
495
+ { slash: '/theme', title: 'Cycle the colour theme', hint: 'ctrl+x t', group: 'Session', run: by('Theme') },
496
+ { slash: '/status', title: 'Ping the bridge', hint: '', group: 'Session', run: by('Bridge status') },
479
497
  { slash: '/help', title: 'Every keybinding', hint: 'f1', group: 'Session', run: () => this.openHelp() },
480
498
  { slash: '/quit', title: 'Leave arenacode', hint: 'ctrl+x q', group: 'Session', run: () => this.quit() },
481
499
  ];
482
500
  }
483
501
 
502
+ /** Ask and Proceed ↔ Proceed, the desktop's Settings switch. */
503
+ async toggleApproval() {
504
+ await this.setApproval(this.approval === 'proceed' ? 'ask' : 'proceed');
505
+ }
506
+
507
+ async setApproval(mode) {
508
+ const next = normaliseApproval(mode);
509
+ if (next === this.approval) {
510
+ this.toast(`approval: ${next === 'proceed' ? 'Proceed' : 'Ask and Proceed'}`);
511
+ return;
512
+ }
513
+ if (next === 'proceed') {
514
+ const danger = dangerText(this.lang);
515
+ const answer = await this.confirm({
516
+ title: danger.title,
517
+ subject: danger.body,
518
+ options: [
519
+ { key: 'y', label: 'enable Proceed', tone: 'err' },
520
+ { key: 'n', label: 'keep Ask and Proceed', tone: 'ok' },
521
+ ],
522
+ });
523
+ if (answer !== 'y') {
524
+ this.toast('approval: Ask and Proceed');
525
+ return;
526
+ }
527
+ }
528
+ this.approval = next;
529
+ try {
530
+ patchConfig({ approval: next });
531
+ } catch {
532
+ /* a read-only HOME keeps the switch in memory only */
533
+ }
534
+ this.push({
535
+ kind: next === 'proceed' ? 'error' : 'system',
536
+ text: `approval: ${next === 'proceed' ? 'Proceed' : 'Ask and Proceed'} · ${approvalHelp(this.lang, this.state.mode)}`,
537
+ });
538
+ this.toast(`approval: ${next === 'proceed' ? 'Proceed' : 'Ask and Proceed'}`, next === 'proceed' ? 'warn' : 'ok');
539
+ }
540
+
484
541
  onOverlayKey(key) {
485
542
  const ov = this.state.overlay;
486
543
  if (key.name === 'escape') {
@@ -659,6 +716,7 @@ class App {
659
716
  return;
660
717
  }
661
718
  st.taskId = res.json.task_id;
719
+ session.remember(st.taskId, st.mode);
662
720
  st.since = 0;
663
721
  st.seqItems.clear();
664
722
  }
@@ -839,7 +897,15 @@ class App {
839
897
 
840
898
  if (type === 'exec') {
841
899
  item.kindDetail = payload.command;
842
- const answer = this.autoApprove
900
+ const verdict = decide({
901
+ approval: this.approval,
902
+ mode: session.modeOf(taskId),
903
+ planApproved: session.planApproved(taskId),
904
+ kind: 'exec',
905
+ admin: Boolean(payload.admin),
906
+ yes: this.autoApprove,
907
+ });
908
+ const answer = verdict === 'auto'
843
909
  ? 'y'
844
910
  : await this.confirm({
845
911
  title: 'terminal',
@@ -873,7 +939,16 @@ class App {
873
939
  if (type === 'fs') {
874
940
  const op = String(payload.op || '');
875
941
  const mutating = ['write', 'replace', 'rm', 'mv', 'mkdir'].includes(op);
876
- if (mutating && this.confirmWrites) {
942
+ const fsVerdict = mutating
943
+ ? decide({
944
+ approval: this.approval,
945
+ mode: session.modeOf(taskId),
946
+ planApproved: session.planApproved(taskId),
947
+ kind: 'fs',
948
+ yes: this.autoApprove,
949
+ })
950
+ : 'auto';
951
+ if (mutating && (fsVerdict === 'ask' || this.confirmWrites)) {
877
952
  const approved = (await this.confirm({
878
953
  title: `file · ${op}`,
879
954
  subject: payload.path || '',
@@ -910,6 +985,20 @@ class App {
910
985
  if (type === 'ui') {
911
986
  const op = String(payload.op || '');
912
987
  if (op === 'plan') {
988
+ const planVerdict = decide({
989
+ approval: this.approval,
990
+ mode: session.modeOf(taskId),
991
+ kind: 'plan',
992
+ yes: false,
993
+ });
994
+ if (planVerdict === 'auto') {
995
+ session.markPlanApproved(taskId);
996
+ this.push({ kind: 'system', text: `plan approved by the client (Proceed): ${String(payload.text || '').split('\n')[0]}` });
997
+ await this.bridge.localResult(taskId, { rid: payload.rid, output: JSON.stringify({ ok: true, approved: true }), code: 0 });
998
+ item.state = 'done';
999
+ this.invalidate();
1000
+ return;
1001
+ }
913
1002
  const answer = await this.confirm({
914
1003
  title: 'plan',
915
1004
  subject: payload.text || '',
@@ -921,7 +1010,10 @@ class App {
921
1010
  ],
922
1011
  });
923
1012
  let payloadOut;
924
- if (answer === 'a') payloadOut = { ok: true, approved: true };
1013
+ if (answer === 'a') {
1014
+ payloadOut = { ok: true, approved: true };
1015
+ session.markPlanApproved(taskId);
1016
+ }
925
1017
  else if (answer === 'f') {
926
1018
  const feedback = await this.prompt('how should it be revised?');
927
1019
  payloadOut = { ok: false, approved: false, revise: true, feedback: feedback || '' };
@@ -1375,14 +1467,16 @@ function HELP_ROWS() {
1375
1467
  ['Prompt', 'enter', 'send the message'],
1376
1468
  ['Prompt', 'shift+enter', 'newline'],
1377
1469
  ['Prompt', 'esc', 'interrupt the turn'],
1378
- ['Prompt', 'tab', 'switch agent / plan'],
1470
+ ['Prompt', 'tab', 'switch mode: Build / Plan / Computer'],
1379
1471
  ['Prompt', 'ctrl+p', 'command palette'],
1380
1472
  ['Prompt', '/', 'slash commands'],
1381
- ['Prompt', '@', 'attach a file from the folder'],
1473
+ ['Prompt', '@', 'mention a file or a folder'],
1474
+ ['Prompt', 'ctrl+v', 'paste: a clipboard image becomes an attachment'],
1382
1475
  ['Prompt', '!', 'run a command locally'],
1383
1476
  ['Prompt', 'up / down', 'history'],
1384
1477
  ['Session', 'ctrl+x n', 'new session'],
1385
1478
  ['Session', 'ctrl+x l', 'task queue'],
1479
+ ['Session', 'ctrl+x a', 'Ask and Proceed / Proceed'],
1386
1480
  ['Session', 'ctrl+x t', 'switch theme'],
1387
1481
  ['Session', 'ctrl+x d', 'toggle tool output'],
1388
1482
  ['Session', 'pgup / pgdn', 'scroll'],