@lardexx/arenacode 2.11.5 → 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 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` task queue · `t` theme · `a` Ask and Proceed / Proceed · `d` tool output · `s` stop |
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, settings, the task queue, task state and timeline, files on the bridge,
224
- file uploads, Stop — is inside the interface. Up to 2.11.2 these were subcommands (`init`, `config`, `ping`,
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.5",
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,7 +379,6 @@ async function main(argv) {
379
379
  const { flags, positional } = parseArgs(argv);
380
380
  const command = positional[0];
381
381
 
382
- // Only the flags: `arenacode help` and `arenacode version` are not subcommands either.
383
382
  if (flags.version) {
384
383
  write(VERSION);
385
384
  return 0;
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
- st.items = [];
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.openTasks();
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: 'tasks', loading: true, tasks: [], index: 0 };
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: 'Task queue', hint: 'ctrl+x l', run: () => this.openTasks() },
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,13 +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', slash: '/settings', hint: '', run: () => this.openSettings() },
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
- // No slash invented from the title any more: it produced /switch-mode-build-plan-computer
492
- // and a second /theme next to the one in slashCommands(). An entry gets a slash only if
493
- // it declares one.
494
- ].map((cmd) => ({ ...cmd, slash: cmd.slash || '', group: cmd.group || 'Commands' }));
563
+ ].map((cmd) => ({
564
+ ...cmd,
565
+ slash: cmd.slash || '/' + cmd.title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/-+$/, ''),
566
+ group: 'Commands',
567
+ }));
495
568
  }
496
569
 
497
570
  /** `/help`, `/new`, `/tasks` … — opencode-style slash commands */
@@ -502,7 +575,9 @@ class App {
502
575
  };
503
576
  return [
504
577
  { slash: '/new', title: 'Start a new session', hint: 'ctrl+x n', group: 'Session', run: by('New session') },
505
- { slash: '/tasks', title: 'Show the task queue', hint: 'ctrl+x l', group: 'Session', run: () => this.openTasks() },
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() },
506
581
  { slash: '/mode', title: 'Switch agent / plan', hint: 'tab', group: 'Session', run: by('Switch mode') },
507
582
  { slash: '/ask', title: 'Ask and Proceed: ask before commands, files and plans', hint: '', group: 'Session', run: () => this.setApproval('ask') },
508
583
  { slash: '/proceed', title: 'Proceed: approve the plan and run everything without asking', hint: '', group: 'Session', run: () => this.setApproval('proceed') },
@@ -654,7 +729,23 @@ class App {
654
729
  return undefined;
655
730
  }
656
731
 
657
- if (ov.type === 'tasks') {
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') {
658
749
  if (key.name === 'up') {
659
750
  ov.index = Math.max(0, ov.index - 1);
660
751
  } else if (key.name === 'down') {
@@ -699,6 +790,12 @@ class App {
699
790
  return undefined;
700
791
  }
701
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
+
702
799
  filteredCommands() {
703
800
  const ov = this.state.overlay;
704
801
  const all = this.commands();
@@ -818,6 +915,7 @@ class App {
818
915
  }
819
916
  st.taskId = res.json.task_id;
820
917
  session.remember(st.taskId, st.mode);
918
+ this.saveSession();
821
919
  st.since = 0;
822
920
  st.seqItems.clear();
823
921
  }
@@ -1445,16 +1543,11 @@ class App {
1445
1543
 
1446
1544
  slashList() {
1447
1545
  const q = this.state.input.slice(1).toLowerCase().split(' ')[0];
1448
- // slashCommands() first: it carries the opencode wording. The palette entries whose
1449
- // slash is already taken are dropped, so typing `/` never shows the same command twice
1450
- // (/settings, /theme, /help and /quit used to appear twice each).
1451
- const seen = new Set();
1452
- const all = [...this.slashCommands(), ...this.commands()].filter((c) => {
1453
- if (!c.slash || seen.has(c.slash)) return false;
1454
- seen.add(c.slash);
1455
- return true;
1456
- });
1457
- return all.filter((c) => !q || c.slash.replace('/', '').startsWith(q)).slice(0, 7);
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);
1458
1551
  }
1459
1552
 
1460
1553
  overlaySlash(frame, W, H, list) {
@@ -1514,12 +1607,35 @@ class App {
1514
1607
  items.push({ label, hint: key, group: '' });
1515
1608
  }
1516
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
+ });
1517
1633
  } else {
1518
1634
  const items = [{ group: 'Queue' }];
1519
1635
  if (ov.loading) items.push({ label: 'loading…', dim: true });
1520
1636
  else if (!ov.tasks.length) items.push({ label: 'no tasks', dim: true });
1521
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 });
1522
- dialog = D.listDialog(t, { W, size: 'large', title: 'Sessions', filter: '', items, index: -1, footer: [['esc', 'close']] });
1638
+ dialog = D.listDialog(t, { W, size: 'large', title: 'Queue', filter: '', items, index: -1, footer: [['esc', 'close']] });
1523
1639
  }
1524
1640
 
1525
1641
  const y = D.dialogY(H, dialog.lines.length);
@@ -1596,7 +1712,9 @@ function HELP_ROWS() {
1596
1712
  ['Prompt', '!', 'run a command locally'],
1597
1713
  ['Prompt', 'up / down', 'history'],
1598
1714
  ['Session', 'ctrl+x n', 'new session'],
1599
- ['Session', 'ctrl+x l', 'task queue'],
1715
+ ['Session', 'ctrl+x l', 'switch session'],
1716
+ ['Session', '/sessions', 'switch between sessions'],
1717
+ ['Session', '/queue', 'tasks on the bridge'],
1600
1718
  ['Session', 'ctrl+x a', 'Ask and Proceed / Proceed'],
1601
1719
  ['Session', 'ctrl+x t', 'switch theme'],
1602
1720
  ['Session', '/settings', 'bridge URL and API key'],