agent.libx.js 0.94.2 → 0.94.3

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,9 +6566,11 @@ 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) {
@@ -6589,6 +6593,13 @@ var SessionStore = class {
6589
6593
  const tmp = `${path}.${process.pid}.tmp`;
6590
6594
  writeFileSync3(tmp, JSON.stringify(data));
6591
6595
  renameSync(tmp, path);
6596
+ try {
6597
+ const gd = globalDir();
6598
+ if (!existsSync5(gd)) mkdirSync4(gd, { recursive: true });
6599
+ const link2 = join6(gd, `${data.meta.id}.json`);
6600
+ if (!existsSync5(link2)) symlinkSync(path, link2);
6601
+ } catch {
6602
+ }
6592
6603
  }
6593
6604
  load(id) {
6594
6605
  if (!this.safeId(id)) {
@@ -6627,6 +6638,52 @@ var SessionStore = class {
6627
6638
  return m ? this.load(m.id) : void 0;
6628
6639
  }
6629
6640
  };
6641
+ function globalSessionLoad(idOrPrefix) {
6642
+ const gd = globalDir();
6643
+ if (!existsSync5(gd)) return void 0;
6644
+ const exact = join6(gd, `${idOrPrefix}.json`);
6645
+ if (existsSync5(exact)) {
6646
+ try {
6647
+ const target = readlinkSync(exact);
6648
+ return JSON.parse(readFileSync3(target, "utf8"));
6649
+ } catch {
6650
+ return void 0;
6651
+ }
6652
+ }
6653
+ try {
6654
+ for (const f of readdirSync(gd)) {
6655
+ if (!f.endsWith(".json")) continue;
6656
+ const base = f.slice(0, -5);
6657
+ if (base.includes(idOrPrefix) || base.endsWith(idOrPrefix)) {
6658
+ const target = readlinkSync(join6(gd, f));
6659
+ return JSON.parse(readFileSync3(target, "utf8"));
6660
+ }
6661
+ }
6662
+ } catch {
6663
+ }
6664
+ return void 0;
6665
+ }
6666
+ function globalSessionList() {
6667
+ const gd = globalDir();
6668
+ if (!existsSync5(gd)) return [];
6669
+ const metas = [];
6670
+ for (const f of readdirSync(gd)) {
6671
+ if (!f.endsWith(".json")) continue;
6672
+ try {
6673
+ const target = readlinkSync(join6(gd, f));
6674
+ if (!existsSync5(target)) {
6675
+ try {
6676
+ unlinkSync(join6(gd, f));
6677
+ } catch {
6678
+ }
6679
+ continue;
6680
+ }
6681
+ metas.push(JSON.parse(readFileSync3(target, "utf8")).meta);
6682
+ } catch {
6683
+ }
6684
+ }
6685
+ return metas.sort((a, b) => b.updated - a.updated);
6686
+ }
6630
6687
  function titleOf(messages) {
6631
6688
  const u = messages.find((m) => m.role === "user");
6632
6689
  const t = contentText(u?.content).replace(/\s+/g, " ").trim();
@@ -6930,7 +6987,7 @@ var GitCheckpointsOptions = class {
6930
6987
 
6931
6988
  // cli/permissions.ts
6932
6989
  import { existsSync as existsSync7, readFileSync as readFileSync4, writeFileSync as writeFileSync5, mkdirSync as mkdirSync6 } from "fs";
6933
- import { homedir as homedir4 } from "os";
6990
+ import { homedir as homedir5 } from "os";
6934
6991
  import { join as join8 } from "path";
6935
6992
  var RULE_RE = /^(\w+)(?:\((.+)\))?$/;
6936
6993
  function parseOne(raw, decision) {
@@ -6964,7 +7021,7 @@ function loadPersistedRules(cwd) {
6964
7021
  return {};
6965
7022
  }
6966
7023
  }
6967
- function loadClaudeSettings(cwd, home = homedir4()) {
7024
+ function loadClaudeSettings(cwd, home = homedir5()) {
6968
7025
  const files = [join8(home, ".claude", "settings.json"), join8(cwd, ".claude", "settings.json"), join8(cwd, ".claude", "settings.local.json")];
6969
7026
  let out = {};
6970
7027
  for (const p of files) {
@@ -6996,7 +7053,7 @@ function mergePerms(a, b) {
6996
7053
  }
6997
7054
  return Object.keys(out).length ? out : void 0;
6998
7055
  }
6999
- var TRUST_FILE = join8(homedir4(), ".agent", "trusted.json");
7056
+ var TRUST_FILE = join8(homedir5(), ".agent", "trusted.json");
7000
7057
  function isTrusted(cwd, file = TRUST_FILE) {
7001
7058
  try {
7002
7059
  return existsSync7(file) && JSON.parse(readFileSync4(file, "utf8")).includes(cwd);
@@ -7881,6 +7938,11 @@ function createLineEditor(out) {
7881
7938
  }
7882
7939
  return;
7883
7940
  }
7941
+ if (key?.name === "escape" && !s.buf.length && !s.menuOpen && !s.searching && !s.prevEsc && opts.onEscapeIdle?.()) {
7942
+ curRow = 0;
7943
+ redraw();
7944
+ return;
7945
+ }
7884
7946
  if (key?.meta && key.name === "p" && opts.onPickModel) {
7885
7947
  process.stdin.off("keypress", onKey);
7886
7948
  void opts.onPickModel().finally(() => {
@@ -8488,7 +8550,7 @@ Project instructions: ./AGENTS.md or ./CLAUDE.md are auto-loaded (scaffold with
8488
8550
  Auto-loaded from ./.agent/: commands/, skills/, memory/, agents/.
8489
8551
 
8490
8552
  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)
8553
+ 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
8554
  REPL completion: type / (commands+skills) or @ (files) for a LIVE menu \u2014 \u2191/\u2193 select, \u23CE/Tab accept, Esc dismiss.
8493
8555
  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
8556
  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 +9063,7 @@ function pastePathClassifier(cwd) {
9001
9063
  t = t.replace(/\\ /g, " ").replace(/^['"]|['"]$/g, "");
9002
9064
  if (/\s/.test(t)) return null;
9003
9065
  if (!/^(\/|~\/|\.\/|\.\.\/)/.test(t)) return null;
9004
- const abs = t.startsWith("~/") ? join9(homedir5(), t.slice(2)) : resolve3(cwd, t);
9066
+ const abs = t.startsWith("~/") ? join9(homedir6(), t.slice(2)) : resolve3(cwd, t);
9005
9067
  try {
9006
9068
  if (!statSync3(abs).isFile()) return null;
9007
9069
  } catch {
@@ -9106,12 +9168,15 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sen
9106
9168
  const tools = res.messages.slice(lastUser).filter((m2) => m2.role === "tool").length;
9107
9169
  const ok = res.finishReason === "stop";
9108
9170
  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 silentAbort = res.finishReason === "aborted" && !res.usage?.totalTokens;
9172
+ if (!silentAbort)
9173
+ 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
9174
  `));
9111
9175
  if (res.finishReason === "error" && res.error) {
9112
9176
  const e = res.error;
9113
9177
  err(red(` ${e?.message ?? e}`) + (e?.statusCode ? dim(` (${e.statusCode}${e.code ? " " + e.code : ""})`) : "") + "\n");
9114
9178
  }
9179
+ if (silentAbort) return { ok: false, res };
9115
9180
  session.messages = agent.transcript;
9116
9181
  session.meta.turns += 1;
9117
9182
  session.meta.tokens = (session.meta.tokens ?? 0) + (res.usage?.totalTokens ?? 0);
@@ -9133,7 +9198,7 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sen
9133
9198
  }
9134
9199
  function startSession(args, store, agent, cwd) {
9135
9200
  if (args.resume || args.cont) {
9136
- const data = args.resume ? store.load(args.resume) : store.latestData();
9201
+ const data = args.resume ? store.load(args.resume) ?? globalSessionLoad(args.resume) : store.latestData();
9137
9202
  if (data) {
9138
9203
  agent.transcript = data.messages;
9139
9204
  if (args.fork) {
@@ -9597,7 +9662,7 @@ async function repl(args, ai, cfg, cwd) {
9597
9662
  const fs = agent.options.fs;
9598
9663
  const fsBase = fs.getCwd() === "/" ? "" : fs.getCwd();
9599
9664
  const adot = (sub) => `${fsBase}/.agent/${sub}`;
9600
- const adots = (sub) => [adot(sub), `${fsBase}/.claude/${sub}`, `${homedir5()}/.agent/${sub}`, `${homedir5()}/.claude/${sub}`];
9665
+ const adots = (sub) => [adot(sub), `${fsBase}/.claude/${sub}`, `${homedir6()}/.agent/${sub}`, `${homedir6()}/.claude/${sub}`];
9601
9666
  const cmds = (await loadCommands(fs, adots("commands"))).commands;
9602
9667
  const skills = (await loadSkills(fs, adots("skills"))).skills;
9603
9668
  const histPath = join9(cwd, ".agent", "history");
@@ -9685,16 +9750,20 @@ async function repl(args, ai, cfg, cwd) {
9685
9750
  }
9686
9751
  return void 0;
9687
9752
  };
9688
- const pickSession = async () => {
9689
- const list = store.list();
9753
+ const pickSession = async (global = false) => {
9754
+ const list = global ? globalSessionList() : store.list();
9690
9755
  if (!list.length) {
9691
- err(dim(" (no saved sessions yet)\n"));
9756
+ err(dim(` (no saved sessions${global ? "" : " in this project"} yet)
9757
+ `));
9692
9758
  return;
9693
9759
  }
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 });
9760
+ const items = list.slice(0, 50).map((m) => {
9761
+ const where = global && m.cwd !== cwd ? ` \xB7 ${m.cwd.split("/").pop()}` : "";
9762
+ return { label: `${m.id} ${m.title || "(untitled)"}`, value: m.id, desc: `${ago(m.updated)} \xB7 ${m.turns} turn${m.turns === 1 ? "" : "s"}${where}` };
9763
+ });
9764
+ const id = await selectMenu(process.stderr, { title: global ? "Resume a session (all projects)" : "Resume a session", items, current: session.meta.id });
9696
9765
  if (!id) return;
9697
- const data = store.load(id);
9766
+ const data = store.load(id) ?? globalSessionLoad(id);
9698
9767
  if (data) resumeInto(data);
9699
9768
  else err(red(" no such session\n"));
9700
9769
  };
@@ -9994,6 +10063,34 @@ ${extra}` : body);
9994
10063
  const off = dx.options.thinkModel === false;
9995
10064
  const id = await dx.dispatch(a.join(" "), "think");
9996
10065
  err(dim(` \u2192 task ${id} ${off ? "(think tier off \u2014 running as act)" : "(think)"} started
10066
+ `));
10067
+ }
10068
+ }, tasks: {
10069
+ desc: "background tasks \u2014 /tasks [cancel <id>], or alone for a picker (\u21B5 cancels the selected running task)",
10070
+ run: async (a) => {
10071
+ const all = [...dx.tasks.values()];
10072
+ if (!all.length) {
10073
+ err(dim(" no background tasks\n"));
10074
+ return;
10075
+ }
10076
+ if (a[0]?.toLowerCase() === "cancel") {
10077
+ if (!a[1]) {
10078
+ err(yellow(" usage: /tasks cancel <id>\n"));
10079
+ return;
10080
+ }
10081
+ err(dim(` ${dx.cancelTask(a[1])}
10082
+ `));
10083
+ return;
10084
+ }
10085
+ const mark = (s) => s === "running" ? cyan("\u25D4 running") : s === "done" ? green("\u2713 done") : s === "cancelled" ? yellow("\u2298 cancelled") : red(`\u2717 ${s}`);
10086
+ if (!process.stderr.isTTY || !process.stdin.isTTY) {
10087
+ for (const t of all) err(` ${t.id} ${mark(t.status)} ${dim(t.label.slice(0, 60))}
10088
+ `);
10089
+ return;
10090
+ }
10091
+ 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" : "") }));
10092
+ const id = await selectMenu(process.stderr, { title: "Background tasks \xB7 \u21B5 cancel running \xB7 esc close", items });
10093
+ if (id) err(dim(` ${dx.cancelTask(String(id))}
9997
10094
  `));
9998
10095
  }
9999
10096
  } } : {},
@@ -10137,7 +10234,10 @@ ${extra}` : body);
10137
10234
  err("\x1Bc");
10138
10235
  }
10139
10236
  },
10140
- sessions: { desc: "pick a saved session to resume (interactive)", run: () => pickSession() },
10237
+ sessions: {
10238
+ desc: "pick a saved session to resume \u2014 /sessions [all] (all = across all projects)",
10239
+ run: (a) => pickSession(a[0] === "all")
10240
+ },
10141
10241
  resume: {
10142
10242
  desc: "resume a session \u2014 /resume <id>, or /resume alone to pick from a list",
10143
10243
  run: async (a) => {
@@ -10145,7 +10245,7 @@ ${extra}` : body);
10145
10245
  await pickSession();
10146
10246
  return;
10147
10247
  }
10148
- const data = store.load(a[0]);
10248
+ const data = store.load(a[0]) ?? globalSessionLoad(a[0]);
10149
10249
  if (data) resumeInto(data);
10150
10250
  else err(red(` no such session
10151
10251
  `));
@@ -10717,6 +10817,13 @@ ${extra}` : body);
10717
10817
  vimMode: cfg.editorMode === "vim",
10718
10818
  statusTickMs: dx ? 1e3 : void 0,
10719
10819
  // duplex: animate the running-task footer while idle at the prompt
10820
+ // Esc at the idle prompt with workers running → cancel them (CC-style: Esc stops the work).
10821
+ onEscapeIdle: dx ? () => {
10822
+ const running = [...dx.tasks.values()].filter((t) => t.status === "running");
10823
+ if (!running.length) return false;
10824
+ for (const t of running) err("\r\x1B[0J" + yellow(` \u2298 ${dx.cancelTask(t.id)}`) + "\n");
10825
+ return true;
10826
+ } : void 0,
10720
10827
  onCyclePosture: cyclePosture,
10721
10828
  onToggleThinking: toggleReasoning,
10722
10829
  onToggleVerbose: toggleVerbose,