agent.libx.js 0.94.2 → 0.94.4

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
@@ -104,14 +104,14 @@ agentx --resume <id> "…" # resume a specific session
104
104
  ```
105
105
 
106
106
  - **Filesystem + Shell** — by default the CLI has **full real-filesystem access like Claude Code** (root `/` is the machine root, the launch dir is the working dir, absolute host paths and above-cwd reach both work) with a **real `/bin/sh`** (`Shell` tool) so the agent can run git, bun, node, curl, and any installed binary. Secrets (`.env`, `.ssh`, keys, `.git`) stay hidden by the jail; env secrets are scrubbed from the child shell. `--sandbox` instead operates over an in-memory copy of the working dir with a VFS-only `bash` — the real disk is never touched. `--boddb <dir>` runs over a **persistent database workspace** (a bod-db store at `<dir>` — `meta.db` tree + `files/` bytes) that survives across runs while the real disk stays untouched; DB-native by default, or add `--seed` to hydrate it from cwd on the first run. `--no-shell` forces the VFS bash in disk mode. (`/sandbox` shows the active mode.)
107
- - **Sessions** — every conversation persists to `./.agent/sessions/<id>.json`; `--continue`/`--resume` (and `/sessions`, `/resume`) pick it back up, *with memory across turns* — a REPL turn sees the previous one.
107
+ - **Sessions** — every conversation persists to `./.agent/sessions/<id>.json`; `--continue`/`--resume` (and `/sessions`, `/resume`) pick it back up, *with memory across turns* — a REPL turn sees the previous one. A global symlink index at `~/.agent/sessions/` enables cross-project lookup: `--resume 090715-335` resolves from any directory, and `/sessions all` lists every project's sessions in one picker.
108
108
  - **Diffs** — every `Edit`/`Write`/`MultiEdit` renders a colorized `+/-` diff (TTY-gated; plain when piped).
109
109
  - **Slash commands** — `/help /tools /model /compact /clear /sessions /resume /commands /init`; user-defined `./.agent/commands/<name>.md` are invokable directly as `/<name>` (the same registry the model's `SlashCommand` tool uses).
110
110
  - **Project instructions** — `./AGENTS.md` (or `CLAUDE.md`) auto-loads into every run; `/init` scaffolds one.
111
111
  - **Any provider** — set `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` / `GOOGLE_API_KEY` / `GROQ_API_KEY`; choose with `-m provider/model`.
112
112
  - **@-file mentions & headless JSON** — reference files inline in a prompt with `@path` (e.g. `explain @src/Agent.ts`); script with `-p --output-format json` to get one machine-readable result object on stdout (activity stays on stderr).
113
113
  - **Tab-completion** — `Tab` completes `/<command>` names and `@<path>` file/dir references (descends subdirs, dotfiles hidden unless typed) straight from the working tree.
114
- - **Duplex mode** — `agentx --duplex` runs the full standard REPL (slash commands, sessions, postures, rewind, MCP) with the three-tier engine driving turns: a fast voice model (`--voice-model`, default `groq/openai/gpt-oss-120b`) answers every line instantly and delegates real work to background workers built with the same wiring as a normal run (fs mode, permissions, MCP); worker activity shows as dim chrome and results are re-voiced when ready. Switch any tier live with `/model` (opens a reflex/act/think picker), or the `/voice-model` · `/think-model` shortcuts.
114
+ - **Duplex mode** — `agentx --duplex` runs the full standard REPL (slash commands, sessions, postures, rewind, MCP) with the three-tier engine driving turns: a fast voice model (`--voice-model`, default `groq/openai/gpt-oss-120b`) answers every line instantly and delegates real work to background workers built with the same wiring as a normal run (fs mode, permissions, MCP); worker activity shows as dim chrome and results are re-voiced when ready. Switch any tier live with `/model` (opens a reflex/act/think picker), or the `/voice-model` · `/think-model` shortcuts. `/tasks` lists background tasks and cancels a running one from a picker (Esc mid-turn cancels the foreground turn; Esc again at the idle prompt cancels running workers).
115
115
  - **MCP servers** — declare `mcpServers: { name: { command, args } | { url } }` in config and they're auto-mounted at startup (in parallel, with an optional `mountTimeoutMs` deadline so one slow/dead server never blocks the rest): the client does the JSON-RPC handshake (stdio or HTTP) + `tools/list`, and the discovered tools appear as `mcp__<name>__<tool>` in `/tools` (inspect with `/mcp`). A bad server is logged and skipped, never blocking the agent. For large tool sets, **deferred mode** (`makeMcpToolSearch` / `mountMcpDeferred`) exposes just two bounded tools (`ToolSearch` + `McpCall`) instead of N defs — dodging the provider tool-cap and improving selection accuracy. **`mountMcpCatalog`** goes further: a cached, hash-keyed catalog + lazy connect means a turn that uses no MCP tool opens **zero** connections, and one that uses a tool connects exactly that server — latency scales with tools-used, not servers-configured. A down server is **negative-cached** (`failureCooldownMs`) so it never re-floors a later turn at the deadline. For zero turn-path latency even on a cold process, call **`warmMcpCatalog`** at boot + on a timer (off-turn discovery) and mount with **`{ discover: 'cache-only' }`** — the turn then never synchronously connects: it serves the warmed catalog and discovers any miss in the background.
116
116
 
117
117
  ## 🧬 It improves itself
package/dist/cli.js CHANGED
@@ -1620,7 +1620,7 @@ var init_tools_shell = __esm({
1620
1620
  // cli/cli.ts
1621
1621
  import { createInterface } from "readline/promises";
1622
1622
  import { existsSync as existsSync8, readFileSync as readFileSync5, appendFileSync, mkdirSync as mkdirSync7, writeFileSync as writeFileSync6, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
1623
- import { homedir as homedir5, tmpdir as tmpdir2 } from "os";
1623
+ import { homedir as homedir6, tmpdir as tmpdir2 } from "os";
1624
1624
 
1625
1625
  // cli/clipboard.ts
1626
1626
  import { execFileSync } from "child_process";
@@ -4192,6 +4192,15 @@ Today's date: ${(/* @__PURE__ */ new Date()).toDateString()}.`;
4192
4192
  return res;
4193
4193
  });
4194
4194
  }
4195
+ /** Cancel a running background task — shared by the CancelTask tool and the CLI /tasks picker. */
4196
+ cancelTask(id) {
4197
+ const rec = this.tasks.get(id);
4198
+ if (!rec) return `No task '${id}'.`;
4199
+ if (rec.status !== "running") return `Task ${rec.id} is already ${rec.status}.`;
4200
+ rec.status = "cancelled";
4201
+ rec.controller.abort();
4202
+ return `Task ${rec.id} (${rec.label}) cancelled.`;
4203
+ }
4195
4204
  /** Resolve when all queued voice turns AND all in-flight worker tasks have settled (tests, graceful shutdown). */
4196
4205
  async idle() {
4197
4206
  while (true) {
@@ -4608,14 +4617,7 @@ Another agent just implemented the above. Independently check the CURRENT state
4608
4617
  name: "CancelTask",
4609
4618
  description: "Cancel a running background task by id.",
4610
4619
  parameters: { type: "object", required: ["id"], properties: { id: { type: "string" } } },
4611
- run: async ({ id }) => {
4612
- const rec = this.tasks.get(String(id));
4613
- if (!rec) return `No task '${id}'.`;
4614
- if (rec.status !== "running") return `Task ${rec.id} is already ${rec.status}.`;
4615
- rec.status = "cancelled";
4616
- rec.controller.abort();
4617
- return `Task ${rec.id} (${rec.label}) cancelled.`;
4618
- }
4620
+ run: async ({ id }) => this.cancelTask(String(id))
4619
4621
  };
4620
4622
  }
4621
4623
  };
@@ -6564,19 +6566,22 @@ function formatDiff(ops, opts = {}) {
6564
6566
  }
6565
6567
 
6566
6568
  // cli/session.ts
6567
- import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync3, readdirSync, renameSync } from "fs";
6569
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync3, readdirSync, renameSync, symlinkSync, unlinkSync, readlinkSync } from "fs";
6570
+ import { homedir as homedir4 } from "os";
6568
6571
  import { join as join6 } from "path";
6569
6572
  var log15 = forComponent("session");
6573
+ var globalDir = () => join6(homedir4(), ".agent", "sessions");
6570
6574
  var SessionStore = class {
6571
6575
  dir;
6572
6576
  constructor(cwd) {
6573
6577
  this.dir = join6(cwd, ".agent", "sessions");
6574
6578
  }
6575
- /** Sortable, human-readable id: `YYYYMMDD-HHMMSS-mmm`. */
6576
- newId(now5 = Date.now()) {
6579
+ /** Sortable, human-readable id: `YYYYMMDD-HHMMSS-<folder>`. */
6580
+ newId(now5 = Date.now(), cwd) {
6577
6581
  const d = new Date(now5);
6578
6582
  const p = (n, w = 2) => String(n).padStart(w, "0");
6579
- return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}-${p(d.getMilliseconds(), 3)}`;
6583
+ const slug2 = (cwd ?? process.cwd()).split("/").pop()?.replace(/[^A-Za-z0-9_-]/g, "") || "session";
6584
+ return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}-${slug2}`;
6580
6585
  }
6581
6586
  /** A session id must be one safe path segment — blocks `../`-style traversal via --resume/load/save. */
6582
6587
  safeId(id) {
@@ -6589,6 +6594,13 @@ var SessionStore = class {
6589
6594
  const tmp = `${path}.${process.pid}.tmp`;
6590
6595
  writeFileSync3(tmp, JSON.stringify(data));
6591
6596
  renameSync(tmp, path);
6597
+ try {
6598
+ const gd = globalDir();
6599
+ if (!existsSync5(gd)) mkdirSync4(gd, { recursive: true });
6600
+ const link2 = join6(gd, `${data.meta.id}.json`);
6601
+ if (!existsSync5(link2)) symlinkSync(path, link2);
6602
+ } catch {
6603
+ }
6592
6604
  }
6593
6605
  load(id) {
6594
6606
  if (!this.safeId(id)) {
@@ -6627,6 +6639,52 @@ var SessionStore = class {
6627
6639
  return m ? this.load(m.id) : void 0;
6628
6640
  }
6629
6641
  };
6642
+ function globalSessionLoad(idOrPrefix) {
6643
+ const gd = globalDir();
6644
+ if (!existsSync5(gd)) return void 0;
6645
+ const exact = join6(gd, `${idOrPrefix}.json`);
6646
+ if (existsSync5(exact)) {
6647
+ try {
6648
+ const target = readlinkSync(exact);
6649
+ return JSON.parse(readFileSync3(target, "utf8"));
6650
+ } catch {
6651
+ return void 0;
6652
+ }
6653
+ }
6654
+ try {
6655
+ for (const f of readdirSync(gd)) {
6656
+ if (!f.endsWith(".json")) continue;
6657
+ const base = f.slice(0, -5);
6658
+ if (base.includes(idOrPrefix) || base.endsWith(idOrPrefix)) {
6659
+ const target = readlinkSync(join6(gd, f));
6660
+ return JSON.parse(readFileSync3(target, "utf8"));
6661
+ }
6662
+ }
6663
+ } catch {
6664
+ }
6665
+ return void 0;
6666
+ }
6667
+ function globalSessionList() {
6668
+ const gd = globalDir();
6669
+ if (!existsSync5(gd)) return [];
6670
+ const metas = [];
6671
+ for (const f of readdirSync(gd)) {
6672
+ if (!f.endsWith(".json")) continue;
6673
+ try {
6674
+ const target = readlinkSync(join6(gd, f));
6675
+ if (!existsSync5(target)) {
6676
+ try {
6677
+ unlinkSync(join6(gd, f));
6678
+ } catch {
6679
+ }
6680
+ continue;
6681
+ }
6682
+ metas.push(JSON.parse(readFileSync3(target, "utf8")).meta);
6683
+ } catch {
6684
+ }
6685
+ }
6686
+ return metas.sort((a, b) => b.updated - a.updated);
6687
+ }
6630
6688
  function titleOf(messages) {
6631
6689
  const u = messages.find((m) => m.role === "user");
6632
6690
  const t = contentText(u?.content).replace(/\s+/g, " ").trim();
@@ -6930,7 +6988,7 @@ var GitCheckpointsOptions = class {
6930
6988
 
6931
6989
  // cli/permissions.ts
6932
6990
  import { existsSync as existsSync7, readFileSync as readFileSync4, writeFileSync as writeFileSync5, mkdirSync as mkdirSync6 } from "fs";
6933
- import { homedir as homedir4 } from "os";
6991
+ import { homedir as homedir5 } from "os";
6934
6992
  import { join as join8 } from "path";
6935
6993
  var RULE_RE = /^(\w+)(?:\((.+)\))?$/;
6936
6994
  function parseOne(raw, decision) {
@@ -6964,7 +7022,7 @@ function loadPersistedRules(cwd) {
6964
7022
  return {};
6965
7023
  }
6966
7024
  }
6967
- function loadClaudeSettings(cwd, home = homedir4()) {
7025
+ function loadClaudeSettings(cwd, home = homedir5()) {
6968
7026
  const files = [join8(home, ".claude", "settings.json"), join8(cwd, ".claude", "settings.json"), join8(cwd, ".claude", "settings.local.json")];
6969
7027
  let out = {};
6970
7028
  for (const p of files) {
@@ -6996,7 +7054,7 @@ function mergePerms(a, b) {
6996
7054
  }
6997
7055
  return Object.keys(out).length ? out : void 0;
6998
7056
  }
6999
- var TRUST_FILE = join8(homedir4(), ".agent", "trusted.json");
7057
+ var TRUST_FILE = join8(homedir5(), ".agent", "trusted.json");
7000
7058
  function isTrusted(cwd, file = TRUST_FILE) {
7001
7059
  try {
7002
7060
  return existsSync7(file) && JSON.parse(readFileSync4(file, "utf8")).includes(cwd);
@@ -7881,6 +7939,11 @@ function createLineEditor(out) {
7881
7939
  }
7882
7940
  return;
7883
7941
  }
7942
+ if (key?.name === "escape" && !s.buf.length && !s.menuOpen && !s.searching && !s.prevEsc && opts.onEscapeIdle?.()) {
7943
+ curRow = 0;
7944
+ redraw();
7945
+ return;
7946
+ }
7884
7947
  if (key?.meta && key.name === "p" && opts.onPickModel) {
7885
7948
  process.stdin.off("keypress", onKey);
7886
7949
  void opts.onPickModel().finally(() => {
@@ -8488,7 +8551,7 @@ Project instructions: ./AGENTS.md or ./CLAUDE.md are auto-loaded (scaffold with
8488
8551
  Auto-loaded from ./.agent/: commands/, skills/, memory/, agents/.
8489
8552
 
8490
8553
  REPL shortcuts: !<cmd> runs a shell command inline \xB7 #<note> saves a memory \xB7 @path inlines a file
8491
- REPL slash commands: /help /version /tools /permissions /status /cost /context /cwd /model /reasoning /config /rename /compact /rewind /undo /clear /sessions /resume /commands /skills /mcp /init /export /paste /goal /exit (duplex: /act /think /voice /voice-model /think-model)
8554
+ REPL slash commands: /help /version /tools /permissions /status /cost /context /cwd /model /reasoning /config /rename /compact /rewind /undo /clear /sessions /resume /commands /skills /mcp /init /export /paste /goal /exit (duplex: /act /think /tasks /voice /voice-model /think-model)
8492
8555
  REPL completion: type / (commands+skills) or @ (files) for a LIVE menu \u2014 \u2191/\u2193 select, \u23CE/Tab accept, Esc dismiss.
8493
8556
  REPL multi-line: Option/Alt+Enter inserts a newline, or end a line with \\ to continue. Esc cancels a running turn / clears the input line; double-Esc jumps back to edit a previous message.
8494
8557
  REPL shortcuts: Shift+Tab cycles permission posture (ask \u2192 accept-edits \u2192 plan) \xB7 Alt+T toggles reasoning \xB7 Alt+P switches model \xB7 Ctrl+O toggles verbose tool output \xB7 \u2192 or Tab accepts the dim history ghost-suggestion \xB7 Alt+S/Ctrl+S stash/unstash.
@@ -9001,7 +9064,7 @@ function pastePathClassifier(cwd) {
9001
9064
  t = t.replace(/\\ /g, " ").replace(/^['"]|['"]$/g, "");
9002
9065
  if (/\s/.test(t)) return null;
9003
9066
  if (!/^(\/|~\/|\.\/|\.\.\/)/.test(t)) return null;
9004
- const abs = t.startsWith("~/") ? join9(homedir5(), t.slice(2)) : resolve3(cwd, t);
9067
+ const abs = t.startsWith("~/") ? join9(homedir6(), t.slice(2)) : resolve3(cwd, t);
9005
9068
  try {
9006
9069
  if (!statSync3(abs).isFile()) return null;
9007
9070
  } catch {
@@ -9105,13 +9168,16 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sen
9105
9168
  const lastUser = res.messages.map((m2) => m2.role).lastIndexOf("user");
9106
9169
  const tools = res.messages.slice(lastUser).filter((m2) => m2.role === "tool").length;
9107
9170
  const ok = res.finishReason === "stop";
9108
- const shortId = session.meta.id.slice(-10);
9109
- err("\n" + (process.stderr.isTTY ? "\r\x1B[0J" : "") + (ok ? green(" \u2713 done") : red(` \u2717 ${res.finishReason}`)) + dim(` \xB7 ${res.steps} steps \xB7 ${tools} tools \xB7 ${tok}${secs}s \xB7 ${shortId}
9171
+ const shortId = session.meta.id.replace(/^\d{8}-/, "");
9172
+ const silentAbort = res.finishReason === "aborted" && !res.usage?.totalTokens;
9173
+ if (!silentAbort)
9174
+ err("\n" + (process.stderr.isTTY ? "\r\x1B[0J" : "") + (ok ? green(" \u2713 done") : red(` \u2717 ${res.finishReason}`)) + dim(` \xB7 ${res.steps} steps \xB7 ${tools} tools \xB7 ${tok}${secs}s \xB7 ${shortId}
9110
9175
  `));
9111
9176
  if (res.finishReason === "error" && res.error) {
9112
9177
  const e = res.error;
9113
9178
  err(red(` ${e?.message ?? e}`) + (e?.statusCode ? dim(` (${e.statusCode}${e.code ? " " + e.code : ""})`) : "") + "\n");
9114
9179
  }
9180
+ if (silentAbort) return { ok: false, res };
9115
9181
  session.messages = agent.transcript;
9116
9182
  session.meta.turns += 1;
9117
9183
  session.meta.tokens = (session.meta.tokens ?? 0) + (res.usage?.totalTokens ?? 0);
@@ -9133,12 +9199,12 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sen
9133
9199
  }
9134
9200
  function startSession(args, store, agent, cwd) {
9135
9201
  if (args.resume || args.cont) {
9136
- const data = args.resume ? store.load(args.resume) : store.latestData();
9202
+ const data = args.resume ? store.load(args.resume) ?? globalSessionLoad(args.resume) : store.latestData();
9137
9203
  if (data) {
9138
9204
  agent.transcript = data.messages;
9139
9205
  if (args.fork) {
9140
9206
  const now6 = Date.now();
9141
- const forked = { meta: { ...data.meta, id: args.sessionId ?? store.newId(now6), created: now6, updated: now6, turns: data.meta.turns }, messages: data.messages };
9207
+ const forked = { meta: { ...data.meta, id: args.sessionId ?? store.newId(now6, cwd), created: now6, updated: now6, turns: data.meta.turns }, messages: data.messages };
9142
9208
  err(dim(` forked ${data.meta.id} \u2192 ${forked.meta.id} (${data.meta.turns} turns)
9143
9209
  `));
9144
9210
  if (!args.task) printHistory(data.messages);
@@ -9153,7 +9219,7 @@ function startSession(args, store, agent, cwd) {
9153
9219
  `));
9154
9220
  }
9155
9221
  const now5 = Date.now();
9156
- const id = args.sessionId ?? store.newId(now5);
9222
+ const id = args.sessionId ?? store.newId(now5, cwd);
9157
9223
  if (!args.task) err(dim(` session ${id}
9158
9224
  `));
9159
9225
  return { meta: { id, created: now5, updated: now5, cwd, model: agent.options.model, turns: 0, title: "" }, messages: [] };
@@ -9597,7 +9663,7 @@ async function repl(args, ai, cfg, cwd) {
9597
9663
  const fs = agent.options.fs;
9598
9664
  const fsBase = fs.getCwd() === "/" ? "" : fs.getCwd();
9599
9665
  const adot = (sub) => `${fsBase}/.agent/${sub}`;
9600
- const adots = (sub) => [adot(sub), `${fsBase}/.claude/${sub}`, `${homedir5()}/.agent/${sub}`, `${homedir5()}/.claude/${sub}`];
9666
+ const adots = (sub) => [adot(sub), `${fsBase}/.claude/${sub}`, `${homedir6()}/.agent/${sub}`, `${homedir6()}/.claude/${sub}`];
9601
9667
  const cmds = (await loadCommands(fs, adots("commands"))).commands;
9602
9668
  const skills = (await loadSkills(fs, adots("skills"))).skills;
9603
9669
  const histPath = join9(cwd, ".agent", "history");
@@ -9685,16 +9751,20 @@ async function repl(args, ai, cfg, cwd) {
9685
9751
  }
9686
9752
  return void 0;
9687
9753
  };
9688
- const pickSession = async () => {
9689
- const list = store.list();
9754
+ const pickSession = async (global = false) => {
9755
+ const list = global ? globalSessionList() : store.list();
9690
9756
  if (!list.length) {
9691
- err(dim(" (no saved sessions yet)\n"));
9757
+ err(dim(` (no saved sessions${global ? "" : " in this project"} yet)
9758
+ `));
9692
9759
  return;
9693
9760
  }
9694
- const items = list.slice(0, 50).map((m) => ({ label: `${m.id} ${m.title || "(untitled)"}`, value: m.id, desc: `${ago(m.updated)} \xB7 ${m.turns} turn${m.turns === 1 ? "" : "s"}` }));
9695
- const id = await selectMenu(process.stderr, { title: "Resume a session", items, current: session.meta.id });
9761
+ const items = list.slice(0, 50).map((m) => {
9762
+ const where = global && m.cwd !== cwd ? ` \xB7 ${m.cwd.split("/").pop()}` : "";
9763
+ return { label: `${m.id} ${m.title || "(untitled)"}`, value: m.id, desc: `${ago(m.updated)} \xB7 ${m.turns} turn${m.turns === 1 ? "" : "s"}${where}` };
9764
+ });
9765
+ const id = await selectMenu(process.stderr, { title: global ? "Resume a session (all projects)" : "Resume a session", items, current: session.meta.id });
9696
9766
  if (!id) return;
9697
- const data = store.load(id);
9767
+ const data = store.load(id) ?? globalSessionLoad(id);
9698
9768
  if (data) resumeInto(data);
9699
9769
  else err(red(" no such session\n"));
9700
9770
  };
@@ -9994,6 +10064,34 @@ ${extra}` : body);
9994
10064
  const off = dx.options.thinkModel === false;
9995
10065
  const id = await dx.dispatch(a.join(" "), "think");
9996
10066
  err(dim(` \u2192 task ${id} ${off ? "(think tier off \u2014 running as act)" : "(think)"} started
10067
+ `));
10068
+ }
10069
+ }, tasks: {
10070
+ desc: "background tasks \u2014 /tasks [cancel <id>], or alone for a picker (\u21B5 cancels the selected running task)",
10071
+ run: async (a) => {
10072
+ const all = [...dx.tasks.values()];
10073
+ if (!all.length) {
10074
+ err(dim(" no background tasks\n"));
10075
+ return;
10076
+ }
10077
+ if (a[0]?.toLowerCase() === "cancel") {
10078
+ if (!a[1]) {
10079
+ err(yellow(" usage: /tasks cancel <id>\n"));
10080
+ return;
10081
+ }
10082
+ err(dim(` ${dx.cancelTask(a[1])}
10083
+ `));
10084
+ return;
10085
+ }
10086
+ const mark = (s) => s === "running" ? cyan("\u25D4 running") : s === "done" ? green("\u2713 done") : s === "cancelled" ? yellow("\u2298 cancelled") : red(`\u2717 ${s}`);
10087
+ if (!process.stderr.isTTY || !process.stdin.isTTY) {
10088
+ for (const t of all) err(` ${t.id} ${mark(t.status)} ${dim(t.label.slice(0, 60))}
10089
+ `);
10090
+ return;
10091
+ }
10092
+ const items = all.map((t) => ({ label: `${t.id} ${t.label.slice(0, 60)}`, value: t.id, desc: mark(t.status) + (t.status === "running" ? " \xB7 \u21B5 to cancel" : "") }));
10093
+ const id = await selectMenu(process.stderr, { title: "Background tasks \xB7 \u21B5 cancel running \xB7 esc close", items });
10094
+ if (id) err(dim(` ${dx.cancelTask(String(id))}
9997
10095
  `));
9998
10096
  }
9999
10097
  } } : {},
@@ -10137,7 +10235,10 @@ ${extra}` : body);
10137
10235
  err("\x1Bc");
10138
10236
  }
10139
10237
  },
10140
- sessions: { desc: "pick a saved session to resume (interactive)", run: () => pickSession() },
10238
+ sessions: {
10239
+ desc: "pick a saved session to resume \u2014 /sessions [all] (all = across all projects)",
10240
+ run: (a) => pickSession(a[0] === "all")
10241
+ },
10141
10242
  resume: {
10142
10243
  desc: "resume a session \u2014 /resume <id>, or /resume alone to pick from a list",
10143
10244
  run: async (a) => {
@@ -10145,7 +10246,7 @@ ${extra}` : body);
10145
10246
  await pickSession();
10146
10247
  return;
10147
10248
  }
10148
- const data = store.load(a[0]);
10249
+ const data = store.load(a[0]) ?? globalSessionLoad(a[0]);
10149
10250
  if (data) resumeInto(data);
10150
10251
  else err(red(` no such session
10151
10252
  `));
@@ -10717,6 +10818,13 @@ ${extra}` : body);
10717
10818
  vimMode: cfg.editorMode === "vim",
10718
10819
  statusTickMs: dx ? 1e3 : void 0,
10719
10820
  // duplex: animate the running-task footer while idle at the prompt
10821
+ // Esc at the idle prompt with workers running → cancel them (CC-style: Esc stops the work).
10822
+ onEscapeIdle: dx ? () => {
10823
+ const running = [...dx.tasks.values()].filter((t) => t.status === "running");
10824
+ if (!running.length) return false;
10825
+ for (const t of running) err("\r\x1B[0J" + yellow(` \u2298 ${dx.cancelTask(t.id)}`) + "\n");
10826
+ return true;
10827
+ } : void 0,
10720
10828
  onCyclePosture: cyclePosture,
10721
10829
  onToggleThinking: toggleReasoning,
10722
10830
  onToggleVerbose: toggleVerbose,