@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 +8 -1
- package/package.json +1 -1
- package/src/approval.js +98 -0
- package/src/cli.js +23 -3
- package/src/config.js +1 -0
- package/src/local.js +42 -2
- package/src/session.js +33 -0
- package/src/tui/app.js +103 -9
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 · `
|
|
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.
|
|
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",
|
package/src/approval.js
ADDED
|
@@ -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(` --
|
|
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
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
|
-
|
|
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
|
-
|
|
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: (
|
|
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: (
|
|
477
|
-
{ slash: '/
|
|
478
|
-
{ slash: '/
|
|
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
|
|
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
|
-
|
|
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')
|
|
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
|
|
1470
|
+
['Prompt', 'tab', 'switch mode: Build / Plan / Computer'],
|
|
1379
1471
|
['Prompt', 'ctrl+p', 'command palette'],
|
|
1380
1472
|
['Prompt', '/', 'slash commands'],
|
|
1381
|
-
['Prompt', '@', '
|
|
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'],
|