@lardexx/arenacode 2.11.4 → 2.11.6
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 +15 -3
- package/package.json +2 -2
- package/src/cli.js +2 -2
- package/src/tui/app.js +146 -18
package/README.md
CHANGED
|
@@ -110,6 +110,7 @@ How the copy is built and tested: [`docs/TUI.md`](docs/TUI.md).
|
|
|
110
110
|
| `enter` | send · `shift+enter` newline |
|
|
111
111
|
| `esc` | interrupt the turn · clear the input · quit |
|
|
112
112
|
| `tab` | switch mode: Build → Plan → Computer |
|
|
113
|
+
| `/sessions` (or `ctrl+x l`) | switch between sessions: every timeline is a slot, `n` opens a new one |
|
|
113
114
|
| `ctrl+p` | command palette (`↑↓` select, `enter` run, `esc` close) |
|
|
114
115
|
| `/` | slash commands with an inline list above the prompt |
|
|
115
116
|
| `@file` | attach a file from the open folder (uploaded to `uploads/`) |
|
|
@@ -118,7 +119,7 @@ How the copy is built and tested: [`docs/TUI.md`](docs/TUI.md).
|
|
|
118
119
|
| `!command` | run a command locally |
|
|
119
120
|
| `↑` / `↓` | prompt history (or move through the slash list) |
|
|
120
121
|
| `pgup` / `pgdn` | scroll the transcript |
|
|
121
|
-
| `ctrl+x` then | `q` quit · `n` new session · `l`
|
|
122
|
+
| `ctrl+x` then | `q` quit · `n` new session · `l` switch session · `t` theme · `a` Ask and Proceed / Proceed · `d` tool output · `s` stop |
|
|
122
123
|
| `ctrl+c` | interrupt, then quit |
|
|
123
124
|
|
|
124
125
|
**The same four flows as the desktop client.** The Windows app is the reference for how a task reaches the bridge,
|
|
@@ -220,8 +221,8 @@ Later, `/settings` (or the command palette, `ctrl+p`) reopens them inside the in
|
|
|
220
221
|
| `arenacode --help` | the options below |
|
|
221
222
|
| `arenacode --version` | the version of the installed package |
|
|
222
223
|
|
|
223
|
-
Everything else — URL and API key
|
|
224
|
-
file uploads
|
|
224
|
+
Everything else — URL and API key (`/settings`), sessions (`/sessions`), the task queue on the bridge
|
|
225
|
+
(`/queue`), Stop, file uploads — is inside the interface. Up to 2.11.2 these were subcommands (`init`, `config`, `ping`,
|
|
225
226
|
`doctor`, `ask`, `chat`, `ui`, `tasks`, `status`, `events`, `stop`, `files`, `cat`, `attach`, `handshake`);
|
|
226
227
|
they were removed on request, and the terminal has to be interactive: in a pipe the client prints why and exits.
|
|
227
228
|
|
|
@@ -292,6 +293,17 @@ What happens when:
|
|
|
292
293
|
the send-once-per-session rule and the git exclusion; the integration test checks that a real task creates the
|
|
293
294
|
sheet and that the bridge receives it.
|
|
294
295
|
|
|
296
|
+
### Sessions
|
|
297
|
+
|
|
298
|
+
Every timeline in the interface is a **session**: its own transcript, its own task id, its own mode. `/sessions`
|
|
299
|
+
(or `ctrl+x l`) opens the list — the current one is marked with `●` and its label is the first instruction you
|
|
300
|
+
typed in it; `enter` switches to the selected session, `n` (or the `+ new session` row) starts a fresh one.
|
|
301
|
+
`ctrl+x n` is the shortcut for a new session.
|
|
302
|
+
|
|
303
|
+
Sessions live in memory for the run of the client, the way the terminal client always behaved: nothing is
|
|
304
|
+
written to disk, and the transcript of a session that was never used stays empty. The bridge keeps the tasks
|
|
305
|
+
themselves — `/queue` shows them with their state and id.
|
|
306
|
+
|
|
295
307
|
### Shells
|
|
296
308
|
|
|
297
309
|
Terminal cards run in a real shell, and which one is not fixed:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lardexx/arenacode",
|
|
3
|
-
"version": "2.11.
|
|
3
|
+
"version": "2.11.6",
|
|
4
4
|
"description": "Terminal client for ArenaCode: talks to an arena-bridge, streams the Agent Mode timeline, and runs local commands, file operations and computer automation on your PC (windows-use.dll on Windows, Xlib/XTest on Linux, osascript on macOS). Node port of the ArenaCode desktop client.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"vendor/"
|
|
21
21
|
],
|
|
22
22
|
"scripts": {
|
|
23
|
-
"test": "node test/cli-surface.js && node test/shell.js && node test/context.js && node test/integration.js && node test/parity.js && node test/automation.js && node test/native.js"
|
|
23
|
+
"test": "node test/cli-surface.js && node test/sessions.js && node test/shell.js && node test/context.js && node test/integration.js && node test/parity.js && node test/automation.js && node test/native.js"
|
|
24
24
|
},
|
|
25
25
|
"keywords": [
|
|
26
26
|
"arenacode",
|
package/src/cli.js
CHANGED
|
@@ -379,11 +379,11 @@ async function main(argv) {
|
|
|
379
379
|
const { flags, positional } = parseArgs(argv);
|
|
380
380
|
const command = positional[0];
|
|
381
381
|
|
|
382
|
-
if (flags.version
|
|
382
|
+
if (flags.version) {
|
|
383
383
|
write(VERSION);
|
|
384
384
|
return 0;
|
|
385
385
|
}
|
|
386
|
-
if (flags.help
|
|
386
|
+
if (flags.help) return help();
|
|
387
387
|
|
|
388
388
|
if (command !== undefined && !command.startsWith('-')) {
|
|
389
389
|
// The subcommands are gone: one entry point, the interface. Saying it out loud is
|
package/src/tui/app.js
CHANGED
|
@@ -80,6 +80,12 @@ class App {
|
|
|
80
80
|
lastError: null,
|
|
81
81
|
};
|
|
82
82
|
|
|
83
|
+
// Sessions: every timeline is a slot the user can switch back to (`/sessions`).
|
|
84
|
+
this.sessions = [];
|
|
85
|
+
this.sessionIndex = 0;
|
|
86
|
+
this.sessionSeq = 0;
|
|
87
|
+
this.newSession({ silent: true });
|
|
88
|
+
|
|
83
89
|
this.term = new Term();
|
|
84
90
|
this.parser = new KeyParser((key) => this.onKey(key));
|
|
85
91
|
this.localQueue = [];
|
|
@@ -279,16 +285,11 @@ class App {
|
|
|
279
285
|
case 'q':
|
|
280
286
|
return this.quit();
|
|
281
287
|
case 'n':
|
|
282
|
-
|
|
283
|
-
st.seqItems.clear();
|
|
284
|
-
st.taskId = null;
|
|
285
|
-
st.since = 0;
|
|
286
|
-
this.toast('new session');
|
|
287
|
-
return undefined;
|
|
288
|
+
return this.newSession();
|
|
288
289
|
case 'h':
|
|
289
290
|
return this.openHelp();
|
|
290
291
|
case 'l':
|
|
291
|
-
return this.
|
|
292
|
+
return this.openSessions();
|
|
292
293
|
case 't': {
|
|
293
294
|
const names = themeNames();
|
|
294
295
|
const next = names[(names.indexOf(this.themeName) + 1) % names.length];
|
|
@@ -427,6 +428,76 @@ class App {
|
|
|
427
428
|
return undefined;
|
|
428
429
|
}
|
|
429
430
|
|
|
431
|
+
// ---------------------------------------------------------------- sessions
|
|
432
|
+
|
|
433
|
+
/** Copies the live timeline into the active session slot. */
|
|
434
|
+
saveSession() {
|
|
435
|
+
const st = this.state;
|
|
436
|
+
const slot = this.sessions[this.sessionIndex];
|
|
437
|
+
if (!slot) return;
|
|
438
|
+
slot.items = st.items;
|
|
439
|
+
slot.seqItems = st.seqItems;
|
|
440
|
+
slot.taskId = st.taskId;
|
|
441
|
+
slot.since = st.since;
|
|
442
|
+
slot.mode = st.mode;
|
|
443
|
+
if (!slot.label) {
|
|
444
|
+
const first = st.items.find((i) => i.kind === 'user' && i.text);
|
|
445
|
+
if (first) slot.label = String(first.text).replace(/@\S+/g, '').replace(/\s+/g, ' ').trim().slice(0, 48);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** Loads a slot into the live state. */
|
|
450
|
+
applySession() {
|
|
451
|
+
const st = this.state;
|
|
452
|
+
const slot = this.sessions[this.sessionIndex];
|
|
453
|
+
st.items = slot ? slot.items : [];
|
|
454
|
+
st.seqItems = slot ? slot.seqItems : new Map();
|
|
455
|
+
st.taskId = slot ? slot.taskId : null;
|
|
456
|
+
st.since = slot ? slot.since : 0;
|
|
457
|
+
st.scroll = 0;
|
|
458
|
+
st.overlay = null;
|
|
459
|
+
st.approval = null;
|
|
460
|
+
st.status = 'idle';
|
|
461
|
+
this.invalidate();
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
newSession({ silent = false } = {}) {
|
|
465
|
+
this.saveSession();
|
|
466
|
+
this.sessionSeq += 1;
|
|
467
|
+
this.sessions.push({
|
|
468
|
+
id: this.sessionSeq,
|
|
469
|
+
label: '',
|
|
470
|
+
items: [],
|
|
471
|
+
seqItems: new Map(),
|
|
472
|
+
taskId: null,
|
|
473
|
+
since: 0,
|
|
474
|
+
mode: this.state.mode,
|
|
475
|
+
startedAt: Date.now(),
|
|
476
|
+
});
|
|
477
|
+
this.sessionIndex = this.sessions.length - 1;
|
|
478
|
+
this.applySession();
|
|
479
|
+
if (!silent) this.toast(`session ${this.sessionSeq} · new`);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
activateSession(index) {
|
|
483
|
+
if (index < 0 || index >= this.sessions.length || index === this.sessionIndex) {
|
|
484
|
+
this.state.overlay = null;
|
|
485
|
+
this.invalidate();
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
this.saveSession();
|
|
489
|
+
this.sessionIndex = index;
|
|
490
|
+
this.applySession();
|
|
491
|
+
const slot = this.sessions[index];
|
|
492
|
+
this.toast(`session ${slot.id}${slot.label ? ` · ${slot.label}` : ''}`);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
async openSessions() {
|
|
496
|
+
this.saveSession();
|
|
497
|
+
this.state.overlay = { type: 'sessions', index: this.sessionIndex };
|
|
498
|
+
this.invalidate();
|
|
499
|
+
}
|
|
500
|
+
|
|
430
501
|
// ---------------------------------------------------------------- overlays
|
|
431
502
|
openPalette() {
|
|
432
503
|
this.state.overlay = { type: 'palette', query: '', index: 0 };
|
|
@@ -439,7 +510,7 @@ class App {
|
|
|
439
510
|
}
|
|
440
511
|
|
|
441
512
|
async openTasks() {
|
|
442
|
-
this.state.overlay = { type: '
|
|
513
|
+
this.state.overlay = { type: 'queue', loading: true, tasks: [], index: 0 };
|
|
443
514
|
this.invalidate();
|
|
444
515
|
const res = await this.bridge.listTasks('all');
|
|
445
516
|
if (res.status === 200) {
|
|
@@ -462,7 +533,8 @@ class App {
|
|
|
462
533
|
this.state.since = 0;
|
|
463
534
|
this.toast('new session');
|
|
464
535
|
} },
|
|
465
|
-
{ title: '
|
|
536
|
+
{ title: 'Sessions (switch between sessions)', hint: 'ctrl+x l', slash: '/sessions', run: () => this.openSessions() },
|
|
537
|
+
{ title: 'Queue: tasks on the bridge', hint: '', slash: '/queue', run: () => this.openTasks() },
|
|
466
538
|
{ title: 'Toggle detail', hint: 'ctrl+x d', run: () => {
|
|
467
539
|
this.state.details = !this.state.details;
|
|
468
540
|
this.toast(`details ${this.state.details ? 'on' : 'off'}`);
|
|
@@ -485,10 +557,14 @@ class App {
|
|
|
485
557
|
if (h) this.push({ kind: 'system', text: `bridge v${h.version} · ${h.executor_mode} · claimable ${h.tasks.claimable}` });
|
|
486
558
|
else this.toast('bridge unreachable', 'error');
|
|
487
559
|
} },
|
|
488
|
-
{ title: 'Settings
|
|
560
|
+
{ title: 'Settings: bridge URL and API key', slash: '/settings', hint: '', run: () => this.openSettings() },
|
|
489
561
|
{ title: 'Help', hint: 'f1', run: () => this.openHelp() },
|
|
490
562
|
{ title: 'Quit', hint: 'ctrl+x q', run: () => this.quit() },
|
|
491
|
-
].map((cmd) => ({
|
|
563
|
+
].map((cmd) => ({
|
|
564
|
+
...cmd,
|
|
565
|
+
slash: cmd.slash || '/' + cmd.title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/-+$/, ''),
|
|
566
|
+
group: 'Commands',
|
|
567
|
+
}));
|
|
492
568
|
}
|
|
493
569
|
|
|
494
570
|
/** `/help`, `/new`, `/tasks` … — opencode-style slash commands */
|
|
@@ -499,14 +575,15 @@ class App {
|
|
|
499
575
|
};
|
|
500
576
|
return [
|
|
501
577
|
{ slash: '/new', title: 'Start a new session', hint: 'ctrl+x n', group: 'Session', run: by('New session') },
|
|
502
|
-
{ slash: '/
|
|
578
|
+
{ slash: '/sessions', title: 'Switch between sessions', hint: 'ctrl+x l', group: 'Session', run: () => this.openSessions() },
|
|
579
|
+
{ slash: '/settings', title: 'Bridge URL and API key', hint: '', group: 'Session', run: () => this.openSettings() },
|
|
580
|
+
{ slash: '/queue', title: 'Tasks on the bridge', hint: '', group: 'Session', run: () => this.openTasks() },
|
|
503
581
|
{ slash: '/mode', title: 'Switch agent / plan', hint: 'tab', group: 'Session', run: by('Switch mode') },
|
|
504
582
|
{ slash: '/ask', title: 'Ask and Proceed: ask before commands, files and plans', hint: '', group: 'Session', run: () => this.setApproval('ask') },
|
|
505
583
|
{ slash: '/proceed', title: 'Proceed: approve the plan and run everything without asking', hint: '', group: 'Session', run: () => this.setApproval('proceed') },
|
|
506
584
|
{ slash: '/theme', title: 'Cycle the colour theme', hint: 'ctrl+x t', group: 'Session', run: by('Theme') },
|
|
507
585
|
{ slash: '/shell', title: 'Show or change the shell commands run in', hint: '', group: 'Session', run: () => this.pickShell() },
|
|
508
586
|
{ slash: '/status', title: 'Ping the bridge', hint: '', group: 'Session', run: by('Bridge status') },
|
|
509
|
-
{ slash: '/settings', title: 'Bridge URL and API key', hint: '', group: 'Session', run: () => this.openSettings() },
|
|
510
587
|
{ slash: '/help', title: 'Every keybinding', hint: 'f1', group: 'Session', run: () => this.openHelp() },
|
|
511
588
|
{ slash: '/quit', title: 'Leave arenacode', hint: 'ctrl+x q', group: 'Session', run: () => this.quit() },
|
|
512
589
|
];
|
|
@@ -652,7 +729,23 @@ class App {
|
|
|
652
729
|
return undefined;
|
|
653
730
|
}
|
|
654
731
|
|
|
655
|
-
if (ov.type === '
|
|
732
|
+
if (ov.type === 'sessions') {
|
|
733
|
+
const rows = this.sessions.length + 1; // + "new session"
|
|
734
|
+
if (key.name === 'up') ov.index = Math.max(0, ov.index - 1);
|
|
735
|
+
else if (key.name === 'down') ov.index = Math.min(rows - 1, ov.index + 1);
|
|
736
|
+
else if (key.name === 'enter') {
|
|
737
|
+
if (ov.index >= this.sessions.length) this.newSession();
|
|
738
|
+
else this.activateSession(ov.index);
|
|
739
|
+
this.state.overlay = null;
|
|
740
|
+
} else if (key.name === 'n') {
|
|
741
|
+
this.state.overlay = null;
|
|
742
|
+
this.newSession();
|
|
743
|
+
}
|
|
744
|
+
this.invalidate();
|
|
745
|
+
return undefined;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
if (ov.type === 'queue') {
|
|
656
749
|
if (key.name === 'up') {
|
|
657
750
|
ov.index = Math.max(0, ov.index - 1);
|
|
658
751
|
} else if (key.name === 'down') {
|
|
@@ -697,6 +790,12 @@ class App {
|
|
|
697
790
|
return undefined;
|
|
698
791
|
}
|
|
699
792
|
|
|
793
|
+
/** A list dialog draws group rows too: the selection index must skip them. */
|
|
794
|
+
sessionRowIndex(index) {
|
|
795
|
+
const at = Math.max(0, Math.min(this.sessions.length, index));
|
|
796
|
+
return 1 + at + (at >= this.sessions.length ? 1 : 0);
|
|
797
|
+
}
|
|
798
|
+
|
|
700
799
|
filteredCommands() {
|
|
701
800
|
const ov = this.state.overlay;
|
|
702
801
|
const all = this.commands();
|
|
@@ -816,6 +915,7 @@ class App {
|
|
|
816
915
|
}
|
|
817
916
|
st.taskId = res.json.task_id;
|
|
818
917
|
session.remember(st.taskId, st.mode);
|
|
918
|
+
this.saveSession();
|
|
819
919
|
st.since = 0;
|
|
820
920
|
st.seqItems.clear();
|
|
821
921
|
}
|
|
@@ -1443,8 +1543,11 @@ class App {
|
|
|
1443
1543
|
|
|
1444
1544
|
slashList() {
|
|
1445
1545
|
const q = this.state.input.slice(1).toLowerCase().split(' ')[0];
|
|
1446
|
-
|
|
1447
|
-
|
|
1546
|
+
// Only the curated list: merging commands() in brought the auto-generated names
|
|
1547
|
+
// (/new-session, /bridge-status-ping …) next to the good ones and showed /settings twice.
|
|
1548
|
+
return this.slashCommands()
|
|
1549
|
+
.filter((c) => !q || c.slash.replace('/', '').startsWith(q))
|
|
1550
|
+
.slice(0, 7);
|
|
1448
1551
|
}
|
|
1449
1552
|
|
|
1450
1553
|
overlaySlash(frame, W, H, list) {
|
|
@@ -1504,12 +1607,35 @@ class App {
|
|
|
1504
1607
|
items.push({ label, hint: key, group: '' });
|
|
1505
1608
|
}
|
|
1506
1609
|
dialog = D.listDialog(t, { W, size: 'large', title: 'Help', filter: '', items, index: -1, footer: [['esc', 'close']] });
|
|
1610
|
+
} else if (ov.type === 'sessions') {
|
|
1611
|
+
const items = [{ group: 'Sessions' }];
|
|
1612
|
+
this.sessions.forEach((slot, i) => {
|
|
1613
|
+
const running = i === this.sessionIndex;
|
|
1614
|
+
const label = slot.label || (slot.taskId ? `task ${slot.taskId}` : 'empty session');
|
|
1615
|
+
const hint = `${slot.items.length ? `${slot.items.length} lines` : ''}${running ? ' · current' : ''}`.trim();
|
|
1616
|
+
items.push({ label: `${running ? '●' : ' '} ${slot.id} ${R.vclip(label, 44)}`, hint, grouped: true });
|
|
1617
|
+
});
|
|
1618
|
+
items.push({ group: '' });
|
|
1619
|
+
items.push({ label: '+ new session', hint: 'n', grouped: true });
|
|
1620
|
+
dialog = D.listDialog(t, {
|
|
1621
|
+
W,
|
|
1622
|
+
size: 'large',
|
|
1623
|
+
title: 'Sessions',
|
|
1624
|
+
filter: '',
|
|
1625
|
+
items,
|
|
1626
|
+
index: this.sessionRowIndex(ov.index),
|
|
1627
|
+
footer: [
|
|
1628
|
+
['enter', 'switch'],
|
|
1629
|
+
['n', 'new'],
|
|
1630
|
+
['esc', 'close'],
|
|
1631
|
+
],
|
|
1632
|
+
});
|
|
1507
1633
|
} else {
|
|
1508
1634
|
const items = [{ group: 'Queue' }];
|
|
1509
1635
|
if (ov.loading) items.push({ label: 'loading…', dim: true });
|
|
1510
1636
|
else if (!ov.tasks.length) items.push({ label: 'no tasks', dim: true });
|
|
1511
1637
|
else for (const task of ov.tasks.slice(0, Math.max(4, H - 12))) items.push({ label: `${task.state} ${R.vclip(task.id, 22)} ${R.vclip(String(task.instruction).split('\n').slice(-1)[0], 40)}`, grouped: true });
|
|
1512
|
-
dialog = D.listDialog(t, { W, size: 'large', title: '
|
|
1638
|
+
dialog = D.listDialog(t, { W, size: 'large', title: 'Queue', filter: '', items, index: -1, footer: [['esc', 'close']] });
|
|
1513
1639
|
}
|
|
1514
1640
|
|
|
1515
1641
|
const y = D.dialogY(H, dialog.lines.length);
|
|
@@ -1586,7 +1712,9 @@ function HELP_ROWS() {
|
|
|
1586
1712
|
['Prompt', '!', 'run a command locally'],
|
|
1587
1713
|
['Prompt', 'up / down', 'history'],
|
|
1588
1714
|
['Session', 'ctrl+x n', 'new session'],
|
|
1589
|
-
['Session', 'ctrl+x l', '
|
|
1715
|
+
['Session', 'ctrl+x l', 'switch session'],
|
|
1716
|
+
['Session', '/sessions', 'switch between sessions'],
|
|
1717
|
+
['Session', '/queue', 'tasks on the bridge'],
|
|
1590
1718
|
['Session', 'ctrl+x a', 'Ask and Proceed / Proceed'],
|
|
1591
1719
|
['Session', 'ctrl+x t', 'switch theme'],
|
|
1592
1720
|
['Session', '/settings', 'bridge URL and API key'],
|