agent.libx.js 0.94.1 → 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 +3 -2
- package/dist/cli.js +133 -26
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +10 -8
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -89,6 +89,7 @@ Beyond file tools, the runtime ships the higher-altitude pieces too — each an
|
|
|
89
89
|
- **Streaming** (`stream: true` → `text_delta` via `HostBridge.notify`) and **context compaction** (`compaction: { maxMessages }` → edge-safe summarize-and-boundary). Defaults preserve the original non-stream, drop-oldest behavior.
|
|
90
90
|
- **Multi-turn + project context** — `Agent.send()` continues a conversation across turns (vs `run()`, which starts fresh); **project instructions** (`instructionFiles`: `AGENTS.md`/`CLAUDE.md` at the FS root) inject into the system prompt.
|
|
91
91
|
- **DuplexAgent** (`src/duplex.ts`) — voice-optimized three-tier engine (reflex/act/think): a fast reflex agent streams instant replies and self-selects escalation — `Act` for standard tool work (Sonnet-class), `Think` for deep reasoning (Opus-class, configurable, default on). Results are pushed back and re-voiced by the reflex (turn mutex, coalesced completions, `TaskStatus`/`CancelTask`). See [`mind/10`](./mind/10-duplex.md).
|
|
92
|
+
- **Scheduler** (`src/scheduler.ts`) — in-process scheduled tasks: one-off (`{at}`), interval (`{everyMs}`), cron (`{cron}`). Four agent tools (`ScheduleTask`/`ScheduleList`/`ScheduleCancel`/`Wakeup`). Fires while the session is alive; persisted in session JSON, re-armed on `--resume`. See [`mind/12`](./mind/12-scheduler.md).
|
|
92
93
|
- **Budget kill-switches** — always-on per-run guards (`maxTokens`/`timeoutMs`/`maxRepeats`/`maxToolCalls`/`signal` → `finishReason` `budget`/`timeout`/`loop`/`max_tool_calls`/`aborted`) protect the API spend against runaway loops. The *enforceable* billing cap is server-side in the web key-proxy: a VFS-backed budget config (`/.agent/budget.json`, USD-metered, hot-reloaded, $100/wk default) a browser client can't bypass. See [`web/`](./web) and [`mind/06`](./mind/06-agent-features.md).
|
|
93
94
|
|
|
94
95
|
## The `agentx` CLI
|
|
@@ -103,14 +104,14 @@ agentx --resume <id> "…" # resume a specific session
|
|
|
103
104
|
```
|
|
104
105
|
|
|
105
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.)
|
|
106
|
-
- **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.
|
|
107
108
|
- **Diffs** — every `Edit`/`Write`/`MultiEdit` renders a colorized `+/-` diff (TTY-gated; plain when piped).
|
|
108
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).
|
|
109
110
|
- **Project instructions** — `./AGENTS.md` (or `CLAUDE.md`) auto-loads into every run; `/init` scaffolds one.
|
|
110
111
|
- **Any provider** — set `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` / `GOOGLE_API_KEY` / `GROQ_API_KEY`; choose with `-m provider/model`.
|
|
111
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).
|
|
112
113
|
- **Tab-completion** — `Tab` completes `/<command>` names and `@<path>` file/dir references (descends subdirs, dotfiles hidden unless typed) straight from the working tree.
|
|
113
|
-
- **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).
|
|
114
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.
|
|
115
116
|
|
|
116
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
|
|
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
|
|
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 =
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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}`, `${
|
|
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(
|
|
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) =>
|
|
9695
|
-
|
|
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: {
|
|
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,
|