nexrall-code 0.5.130 → 0.5.131

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.
Files changed (2) hide show
  1. package/dist/index.js +251 -156
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -11208,7 +11208,7 @@ var require_securityLint = __commonJS({
11208
11208
  // So the interpolation must name something that plausibly came from the
11209
11209
  // outside: req/request/params/query/body/input/user/args, or a bare
11210
11210
  // `'...' + ident`. This trades recall for precision on purpose — thorough SQL
11211
- // review is the security-auditor agent's job, not an inline regex's.
11211
+ // review is a dedicated security review's job, not an inline regex's.
11212
11212
  //
11213
11213
  // THE GAP IS PER-ALTERNATIVE, and that asymmetry is load-bearing. The concat
11214
11214
  // branch originally shared the no-quote gap, which made it miss the QUOTED
@@ -12188,24 +12188,6 @@ var require_agentTypes = __commonJS({
12188
12188
  "get_diagnostics"
12189
12189
  ];
12190
12190
  var RESEARCH_TOOLS = [...READ_ONLY_TOOLS, "fetch_url", "web_search"];
12191
- var WRITE_TOOLS = [
12192
- ...READ_ONLY_TOOLS,
12193
- "write_file",
12194
- "edit_file",
12195
- "multi_edit",
12196
- "create_directory",
12197
- "move_file",
12198
- "copy_file",
12199
- // notebook_edit is a write tool like any other, and allowsTestOnlyWrite already
12200
- // knows its shape (`source` is cell CONTENT, not a path). Omitting it just meant
12201
- // test-writer silently could not touch notebooks.
12202
- "notebook_edit",
12203
- // Office document generation — same write-access tier as write_file, just a
12204
- // structured content shape instead of raw text (see tools/executor.ts).
12205
- "write_docx",
12206
- "write_xlsx",
12207
- "write_pptx"
12208
- ];
12209
12191
  var BUILTIN_AGENTS = [
12210
12192
  {
12211
12193
  // The catch-all, matching Claude Code's `general-purpose`.
@@ -12238,66 +12220,6 @@ var require_agentTypes = __commonJS({
12238
12220
  prompt: "You are a general-purpose engineering sub-agent. Work the task end to end: explore what you need, make the changes, and verify them with the project's own build/test commands.\n\nRules:\n- Mirror existing conventions; make the smallest correct change.\n- Verify before you claim success. If you could not verify, say so explicitly.\n- Your FINAL MESSAGE is the only thing that reaches the main agent: state what you changed (with file paths), what you ran and its outcome, and anything you deliberately left undone.",
12239
12221
  source: "builtin"
12240
12222
  },
12241
- {
12242
- name: "reviewer",
12243
- description: "Read-only code reviewer \u2014 finds correctness bugs, edge cases, and security issues in a diff or file set. Cannot modify files.",
12244
- tools: READ_ONLY_TOOLS,
12245
- source: "builtin",
12246
- prompt: [
12247
- "You are a meticulous senior code reviewer. You NEVER modify files \u2014 you only read, search, and report.",
12248
- "",
12249
- "Method:",
12250
- "1. Read the full context around every change you are asked to review; never judge a hunk in isolation.",
12251
- "2. Hunt specifically for: correctness bugs, unhandled edge cases (empty/null/unicode/concurrency/timezone),",
12252
- " security issues (injection, path traversal, secrets in code, unsafe deserialization), breaking API",
12253
- " changes (search for callers first), silent behaviour changes, and swallowed errors.",
12254
- "3. Verify test coverage: are the changed paths tested? Were assertions weakened or tests deleted?",
12255
- "4. Only use bash for read-only commands (git diff/log/show, grep, test runs). Never run mutating commands.",
12256
- "",
12257
- "Report format: \u{1F534} Critical / \u{1F7E1} Warning / \u{1F7E2} Suggestion, each with file:line and a concrete fix,",
12258
- "then a final verdict (APPROVE or REQUEST CHANGES) with a one-paragraph rationale."
12259
- ].join("\n")
12260
- },
12261
- // Promoted from the security-audit plugin to a builtin.
12262
- //
12263
- // Leaving it plugin-only was indefensible next to `reviewer` being builtin:
12264
- // reviewer's own prompt already tells it to look for security issues, so
12265
- // security IS treated as default work — yet the specialist agent for it was
12266
- // invisible unless the user happened to know the plugin existed. For an agent
12267
- // that WRITES code, "you only get a security review if you knew to install
12268
- // something" is the wrong default.
12269
- {
12270
- name: "security-auditor",
12271
- description: "Read-only security auditor \u2014 hunts injection, authz, secrets, and validation flaws in a path or diff. Cannot modify files.",
12272
- tools: READ_ONLY_TOOLS,
12273
- // No `model` override — inherits the session's, same as general-purpose.
12274
- // A previous version forced 'pro' (Claude Opus 5) here, which silently
12275
- // billed Anthropic even for a session running entirely on OpenAI/DeepSeek/
12276
- // Qwen. Consistency with the main conversation's provider matters more
12277
- // than defaulting every specialist to a fixed tier.
12278
- source: "builtin",
12279
- prompt: [
12280
- "You are a security auditor. You find real, exploitable flaws \u2014 not style issues.",
12281
- "",
12282
- "Method:",
12283
- "1. Map the attack surface FIRST: entry points (HTTP routes, message handlers, CLI args, file/network",
12284
- " input, deserialization), then trace user-controlled data inward to where it is used.",
12285
- "2. For each finding: file:line, the flaw class, a one-line exploit scenario, and the concrete fix.",
12286
- "3. Grade severity honestly: Critical = remote compromise or data breach; High = auth bypass/IDOR;",
12287
- " Medium = needs unusual preconditions; Low = hardening.",
12288
- "",
12289
- "Classes worth the most attention, in order: injection (SQL/command/template/prototype), broken",
12290
- "authz (missing ownership checks, IDOR, trusting client-supplied ids), secrets committed to source,",
12291
- "path traversal, SSRF, unsafe deserialization, missing rate limits on expensive or auth endpoints,",
12292
- "and crypto misuse (hand-rolled comparison, predictable randomness).",
12293
- "",
12294
- "Hard rules:",
12295
- "- READ-ONLY: never modify, create or delete files. bash only for read-only inspection.",
12296
- "- NEVER print a discovered secret's value. Report its location and advise rotation.",
12297
- "- Distinguish EXPLOITABLE from theoretical, and say which one each finding is.",
12298
- '- "No issues found in scope X" is a valid, useful result. Do not pad the report to look thorough.'
12299
- ].join("\n")
12300
- },
12301
12223
  // The gap Claude Code fills with its built-in `Explore`: read-heavy codebase
12302
12224
  // search that would otherwise flood the parent's context. Defaults to the
12303
12225
  // cheapest model on purpose — "find every caller of X" has no need of a
@@ -12341,7 +12263,7 @@ var require_agentTypes = __commonJS({
12341
12263
  description: "Read-only planning agent \u2014 researches a change and returns a concrete step-by-step implementation plan with risks and affected files. Cannot modify files.",
12342
12264
  tools: RESEARCH_TOOLS,
12343
12265
  // No `model` override — inherits the session's, for the same reason as
12344
- // explorer/security-auditor above: a fixed alias silently billed Anthropic
12266
+ // explorer above: a fixed alias silently billed Anthropic
12345
12267
  // regardless of the user's chosen provider.
12346
12268
  source: "builtin",
12347
12269
  prompt: [
@@ -12361,74 +12283,6 @@ var require_agentTypes = __commonJS({
12361
12283
  "- How to verify (the exact test/build command for THIS project, taken from package.json/Makefile).",
12362
12284
  "- Anything genuinely ambiguous, stated as an open question rather than a silent assumption."
12363
12285
  ].join("\n")
12364
- },
12365
- // Promoted from the test-gen plugin. Needs write access — it produces test
12366
- // files — but is deliberately forbidden from touching source, because "make the
12367
- // tests pass" is the single most common way an agent destroys signal.
12368
- {
12369
- name: "test-writer",
12370
- description: "Writes tests that follow the project's existing conventions. May create/edit TEST files only \u2014 never production source.",
12371
- tools: WRITE_TOOLS,
12372
- // Enforced, not merely requested: the permission gate refuses a write whose
12373
- // path is not a test file. Without this the allowlist would grant edit_file
12374
- // for every path and the rule below would be a suggestion the model is free
12375
- // to rationalise its way past.
12376
- testFilesOnly: true,
12377
- source: "builtin",
12378
- prompt: [
12379
- "You write tests. You may create and edit TEST files only.",
12380
- "",
12381
- "Hard rules \u2014 these are the ways test-writing agents destroy value, so they are non-negotiable:",
12382
- "- NEVER modify production source to make a test pass. If the code looks wrong, REPORT it and stop.",
12383
- "- NEVER weaken, delete or skip an existing assertion or test.",
12384
- "- A test that cannot fail is worse than no test. Every test must be able to fail for one clear reason.",
12385
- "",
12386
- "Method:",
12387
- "1. Read the existing tests FIRST and copy their conventions exactly \u2014 runner, file naming, layout,",
12388
- " assertion style, fixture/helper patterns. Never introduce a new framework.",
12389
- "2. Test observable behaviour and the contract, not private internals.",
12390
- "3. Cover the boring-but-real cases: empty input, null/undefined, unicode and non-BMP characters,",
12391
- " boundaries, error paths, concurrency where it applies.",
12392
- "4. No sleeps or wall-clock dependence \u2014 those produce the flaky tests that get deleted later.",
12393
- "5. RUN the tests you wrote and report the real output. Never claim a test passes without running it."
12394
- ].join("\n")
12395
- },
12396
- // The DevOps gap — answered with a READ-ONLY advisor, not an operator.
12397
- //
12398
- // A "DevOps agent" with write/apply access is a genuinely different risk class
12399
- // from the others here: its mistakes are `kubectl delete`, a bad `terraform
12400
- // apply`, a broken deploy pipeline — often not revertible and affecting
12401
- // production rather than a working tree. So this one diagnoses and proposes a
12402
- // diff; a human applies it. That asymmetry is the whole design.
12403
- {
12404
- name: "devops-advisor",
12405
- description: "Read-only CI/CD, container, and infrastructure advisor \u2014 diagnoses pipelines, Dockerfiles, and k8s manifests and proposes concrete fixes as a diff. Never applies changes.",
12406
- tools: RESEARCH_TOOLS,
12407
- // No `model` override — inherits the session's, for the same reason as the
12408
- // other specialists above.
12409
- source: "builtin",
12410
- prompt: [
12411
- "You are an infrastructure and delivery advisor. You DIAGNOSE and PROPOSE. You never apply changes.",
12412
- "",
12413
- "Hard rules:",
12414
- "- READ-ONLY, and stricter than the other read-only agents: bash is for INSPECTION only",
12415
- " (git log/diff, cat, grep, `kubectl get/describe`, `docker images`, `terraform plan`).",
12416
- " NEVER run anything that mutates infrastructure \u2014 no apply/delete/scale/rollout/restart/push,",
12417
- " no `terraform apply`, no `helm upgrade`. If a fix needs such a command, WRITE IT OUT for a human.",
12418
- "- Never print secret values from env files, k8s Secrets or CI variables. Reference them by name.",
12419
- "",
12420
- "Method:",
12421
- "1. Read what actually exists \u2014 workflow files, Dockerfiles, manifests, kustomize overlays, the",
12422
- ' deploy scripts \u2014 before drawing any conclusion. Never reason from what a stack "usually" looks like.',
12423
- "2. Follow the real path a change takes to production, and name the step that is broken or missing.",
12424
- "3. Check the failure modes that bite hardest: CI path filters that skip files a workload actually",
12425
- " needs, image tags that do not match what is deployed, missing health probes, absent resource",
12426
- " limits, secrets baked into images, ports/timeouts inconsistent between proxy and app, and",
12427
- " migrations that must run before the new image is live.",
12428
- "",
12429
- "Output: the diagnosis, the evidence (file:line or command output), the proposed change as a diff or",
12430
- "exact file content, and the command a human should run to apply and verify it."
12431
- ].join("\n")
12432
12286
  }
12433
12287
  ];
12434
12288
  var KNOWN_TOOL_NAMES = /* @__PURE__ */ new Set([
@@ -107812,7 +107666,7 @@ ${body}`);
107812
107666
  function formatTokenCount(n) {
107813
107667
  return n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
107814
107668
  }
107815
- function formatElapsed(ms) {
107669
+ function formatElapsed2(ms) {
107816
107670
  const s2 = Math.round(ms / 1e3);
107817
107671
  return s2 < 60 ? `${s2}s` : `${Math.floor(s2 / 60)}m ${s2 % 60}s`;
107818
107672
  }
@@ -108000,7 +107854,7 @@ ${agent.prompt}` : "",
108000
107854
  }
108001
107855
  isoState = void 0;
108002
107856
  }
108003
- const stats = `[sub-agent stats: ${subToolCalls} tool call(s) \xB7 ${formatTokenCount(subTokens)} tokens \xB7 $${subCost.toFixed(3)} \xB7 ${formatElapsed(Date.now() - subStartedAt)}]`;
107857
+ const stats = `[sub-agent stats: ${subToolCalls} tool call(s) \xB7 ${formatTokenCount(subTokens)} tokens \xB7 $${subCost.toFixed(3)} \xB7 ${formatElapsed2(Date.now() - subStartedAt)}]`;
108004
107858
  const out = { ...r2, ...childVerifications.length ? { childVerifications } : {} };
108005
107859
  if (out.error !== void 0)
108006
107860
  out.error = `${out.error}
@@ -108871,7 +108725,7 @@ Continue the work from here.` }] });
108871
108725
  hasOwnAgentStore: !!options._agentMemory,
108872
108726
  // Same derivation, same reason, for the network. The base prompt's
108873
108727
  // "Web search" section tells the run to reach the network, but
108874
- // READ_ONLY_TOOLS (explorer, security-auditor, ...) contains neither
108728
+ // READ_ONLY_TOOLS (explorer, planner, ...) contains neither
108875
108729
  // `fetch_url` nor `web_search`. MEASURED on the 195-trial 2026-08-10
108876
108730
  // deepseek-v4-pro benchmark: 29 of 51 `fetch_url` failures and 3 of 14
108877
108731
  // `web_search` failures were the permission gate refusing a sub-agent
@@ -157566,7 +157420,8 @@ function composeFooter(base, extras, columns, reserve = 0) {
157566
157420
  optional.push({ keep: 4, at: 2, segment: ctxSegment(extras.ctxPct) });
157567
157421
  if (typeof extras?.costUsd === "number")
157568
157422
  optional.push({ keep: 2, at: 3, segment: { text: money2(extras.costUsd), tone: "dim" } });
157569
- const build = (kept2) => [head, ...[...kept2].sort((x2, y) => x2.at - y.at).map((k) => k.segment), ...tail];
157423
+ let tailShown = tail.length;
157424
+ const build = (kept2) => [head, ...[...kept2].sort((x2, y) => x2.at - y.at).map((k) => k.segment), ...tail.slice(0, tailShown)];
157570
157425
  const width = (segs) => visibleLen(segs.map((s2) => s2.text).join(SEP2));
157571
157426
  let kept = [...optional];
157572
157427
  const budget = columns - 1 - reserve;
@@ -157577,6 +157432,8 @@ function composeFooter(base, extras, columns, reserve = 0) {
157577
157432
  lowest = i2;
157578
157433
  kept.splice(lowest, 1);
157579
157434
  }
157435
+ while (tailShown > 0 && width(build(kept)) > budget)
157436
+ tailShown--;
157580
157437
  return build(kept);
157581
157438
  }
157582
157439
 
@@ -157896,6 +157753,7 @@ var BUILTIN_SLASH_COMMANDS = [
157896
157753
  { name: "cost", description: "Tokens and cost of this conversation", kind: "builtin" },
157897
157754
  { name: "status", description: "Version, model, mode, directory and session at a glance", kind: "builtin" },
157898
157755
  { name: "copy", description: "Copy the last reply to the clipboard", kind: "builtin" },
157756
+ { name: "bashes", description: "List, show or stop commands moved to the background (Ctrl+B)", argHint: "[show|kill <id>]", kind: "builtin" },
157899
157757
  { name: "expand", description: "Show the full output of a folded tool call (Ctrl+O)", argHint: "[n]", kind: "builtin" },
157900
157758
  { name: "export", description: "Write this conversation to a Markdown file", argHint: "[file]", kind: "builtin" },
157901
157759
  { name: "rename", description: "Rename this session", argHint: "<title>", requiresArg: true, kind: "builtin" },
@@ -158037,6 +157895,7 @@ function runBang(cmd, opts) {
158037
157895
  let timedOut = false;
158038
157896
  let aborted = false;
158039
157897
  let settled = false;
157898
+ let detached = false;
158040
157899
  const child = spawn("bash", ["-c", cmd], {
158041
157900
  cwd: opts.cwd,
158042
157901
  env: opts.env ?? process.env,
@@ -158077,11 +157936,25 @@ function runBang(cmd, opts) {
158077
157936
  };
158078
157937
  child.stdout.on("data", take);
158079
157938
  child.stderr.on("data", take);
157939
+ opts.onSpawn?.({ kill });
158080
157940
  const timer = setTimeout(() => {
158081
157941
  timedOut = true;
158082
157942
  kill();
158083
157943
  }, timeoutMs);
157944
+ const snapshot = () => {
157945
+ const kept = head.length + tail.length;
157946
+ return { output: head + tail, droppedBytes: Math.max(0, total - kept), code: null, signal: null, timedOut, aborted };
157947
+ };
158084
157948
  const poll = setInterval(() => {
157949
+ if (detached)
157950
+ return;
157951
+ if (opts.detach?.requested) {
157952
+ detached = true;
157953
+ clearTimeout(timer);
157954
+ clearInterval(poll);
157955
+ resolve8({ ...snapshot(), detached: true });
157956
+ return;
157957
+ }
158085
157958
  if (opts.signal?.aborted && !aborted) {
158086
157959
  aborted = true;
158087
157960
  kill();
@@ -158093,8 +157966,11 @@ function runBang(cmd, opts) {
158093
157966
  settled = true;
158094
157967
  clearTimeout(timer);
158095
157968
  clearInterval(poll);
158096
- const kept = head.length + tail.length;
158097
- resolve8({ output: head + tail, droppedBytes: Math.max(0, total - kept), code, signal, timedOut, aborted });
157969
+ const r2 = { ...snapshot(), code, signal };
157970
+ if (detached)
157971
+ opts.onSettled?.(r2);
157972
+ else
157973
+ resolve8(r2);
158098
157974
  };
158099
157975
  child.on("error", (err) => {
158100
157976
  head += `${err.message}
@@ -158105,7 +157981,7 @@ function runBang(cmd, opts) {
158105
157981
  });
158106
157982
  }
158107
157983
  function bangTranscript(cmd, r2) {
158108
- const status = r2.aborted ? "interrupted by the user" : r2.timedOut ? "timed out" : r2.code === 0 ? "exit code 0" : `exit code ${r2.code ?? r2.signal ?? "?"}`;
157984
+ const status = r2.detached ? "moved to the background by the user and still running \u2014 this is the output so far" : r2.aborted ? "interrupted by the user" : r2.timedOut ? "timed out" : r2.code === 0 ? "exit code 0" : `exit code ${r2.code ?? r2.signal ?? "?"}`;
158109
157985
  const body = r2.output.length ? r2.output.replace(/\s+$/, "") : "(no output)";
158110
157986
  const cut = r2.droppedBytes > 0 ? `
158111
157987
  \u2026 ${r2.droppedBytes} bytes omitted from the middle \u2026` : "";
@@ -158727,6 +158603,10 @@ var App2 = ({ onReady }) => {
158727
158603
  const onExpand = (0, import_react35.useCallback)((cb) => {
158728
158604
  onExpandRef.current = cb;
158729
158605
  }, []);
158606
+ const onBackgroundRef = (0, import_react35.useRef)(null);
158607
+ const onBackground = (0, import_react35.useCallback)((cb) => {
158608
+ onBackgroundRef.current = cb;
158609
+ }, []);
158730
158610
  const mentionSelRef = (0, import_react35.useRef)({ query: "", idx: 0 });
158731
158611
  const mentionDismissedRef = (0, import_react35.useRef)(null);
158732
158612
  const mentionRootRef = (0, import_react35.useRef)(process.cwd());
@@ -158930,6 +158810,16 @@ var App2 = ({ onReady }) => {
158930
158810
  onExpandRef.current?.();
158931
158811
  return;
158932
158812
  }
158813
+ if (key.ctrl && char === "b") {
158814
+ if (askResolverRef.current)
158815
+ return;
158816
+ if (busyRef.current) {
158817
+ const moved = onBackgroundRef.current?.() ?? false;
158818
+ if (!moved)
158819
+ setHintText("Ctrl+B moves a running !command to the background \u2014 none is running");
158820
+ }
158821
+ return;
158822
+ }
158933
158823
  const menu = askResolverRef.current || searchRef.current || historyRef.current.browsing ? null : computeMenu(lineRef.current, cursorRef.current, slashEntriesRef.current, menuDismissedRef.current);
158934
158824
  if (menu) {
158935
158825
  const sel = menuSelRef.current.query === menu.query ? menuSelRef.current.idx : 0;
@@ -159301,7 +159191,8 @@ var App2 = ({ onReady }) => {
159301
159191
  onCycleMode,
159302
159192
  onRewind,
159303
159193
  onHelp,
159304
- onExpand
159194
+ onExpand,
159195
+ onBackground
159305
159196
  });
159306
159197
  }, []);
159307
159198
  const slices = renderInputSlices(line, cursorPos);
@@ -159589,6 +159480,126 @@ function rewindChoices(cps) {
159589
159480
  return { options, ids: newest.map((c) => c.id) };
159590
159481
  }
159591
159482
 
159483
+ // src/ui/backgroundShells.ts
159484
+ var MAX_KEPT = 64e3;
159485
+ var BackgroundShells = class {
159486
+ nextId = 1;
159487
+ jobs = /* @__PURE__ */ new Map();
159488
+ ctls = /* @__PURE__ */ new Map();
159489
+ listeners = [];
159490
+ /** Register a detached command; returns its id. `seed` is the output it already produced. */
159491
+ add(cmd, ctl, seed = "", now = Date.now()) {
159492
+ const job = {
159493
+ id: this.nextId++,
159494
+ cmd,
159495
+ startedAt: now,
159496
+ status: "running",
159497
+ code: null,
159498
+ signal: null,
159499
+ tail: seed.length > MAX_KEPT ? seed.slice(seed.length - MAX_KEPT) : seed
159500
+ };
159501
+ this.jobs.set(job.id, job);
159502
+ this.ctls.set(job.id, ctl);
159503
+ this.emit();
159504
+ return job;
159505
+ }
159506
+ /** Append streamed output (bounded to the newest MAX_KEPT characters). */
159507
+ append(id, chunk) {
159508
+ const j = this.jobs.get(id);
159509
+ if (!j || j.status !== "running")
159510
+ return;
159511
+ j.tail += chunk;
159512
+ if (j.tail.length > MAX_KEPT)
159513
+ j.tail = j.tail.slice(j.tail.length - MAX_KEPT);
159514
+ }
159515
+ /** The process exited. A job the user killed stays 'killed' whatever the exit code says. */
159516
+ finish(id, r2, now = Date.now()) {
159517
+ const j = this.jobs.get(id);
159518
+ if (!j || j.status !== "running")
159519
+ return j;
159520
+ j.status = "done";
159521
+ j.code = r2.code;
159522
+ j.signal = r2.signal;
159523
+ j.endedAt = now;
159524
+ this.ctls.delete(id);
159525
+ this.emit();
159526
+ return j;
159527
+ }
159528
+ /** Stop a running job. False when the id is unknown or it already ended. */
159529
+ kill(id, now = Date.now()) {
159530
+ const j = this.jobs.get(id);
159531
+ const ctl = this.ctls.get(id);
159532
+ if (!j || !ctl || j.status !== "running")
159533
+ return false;
159534
+ j.status = "killed";
159535
+ j.endedAt = now;
159536
+ this.ctls.delete(id);
159537
+ ctl.kill();
159538
+ this.emit();
159539
+ return true;
159540
+ }
159541
+ killAll() {
159542
+ let n = 0;
159543
+ for (const id of [...this.ctls.keys()])
159544
+ if (this.kill(id))
159545
+ n++;
159546
+ return n;
159547
+ }
159548
+ get(id) {
159549
+ return this.jobs.get(id);
159550
+ }
159551
+ list() {
159552
+ return [...this.jobs.values()];
159553
+ }
159554
+ runningCount() {
159555
+ let n = 0;
159556
+ for (const j of this.jobs.values())
159557
+ if (j.status === "running")
159558
+ n++;
159559
+ return n;
159560
+ }
159561
+ onChange(cb) {
159562
+ this.listeners.push(cb);
159563
+ }
159564
+ emit() {
159565
+ for (const l2 of this.listeners)
159566
+ l2();
159567
+ }
159568
+ };
159569
+ function formatElapsed(ms) {
159570
+ const s2 = Math.max(0, Math.floor(ms / 1e3));
159571
+ if (s2 < 60)
159572
+ return `${s2}s`;
159573
+ const m2 = Math.floor(s2 / 60);
159574
+ if (m2 < 60)
159575
+ return `${m2}m ${String(s2 % 60).padStart(2, "0")}s`;
159576
+ return `${Math.floor(m2 / 60)}h ${String(m2 % 60).padStart(2, "0")}m`;
159577
+ }
159578
+ function describeShell(j, now = Date.now()) {
159579
+ const state = j.status === "running" ? `running ${formatElapsed(now - j.startedAt)}` : j.status === "killed" ? "killed" : j.code === 0 ? "exit 0" : `exit ${j.code ?? j.signal ?? "?"}`;
159580
+ const cmd = j.cmd.length > 60 ? j.cmd.slice(0, 59) + "\u2026" : j.cmd;
159581
+ return `#${j.id} ${state.padEnd(14)} ${cmd}`;
159582
+ }
159583
+ function parseBashesArgs(arg) {
159584
+ const parts = arg.trim().split(/\s+/).filter(Boolean);
159585
+ if (parts.length === 0)
159586
+ return { kind: "list" };
159587
+ const verb = parts[0].toLowerCase();
159588
+ if (verb === "list")
159589
+ return { kind: "list" };
159590
+ if (verb === "kill" && parts[1]?.toLowerCase() === "all" && parts.length === 2)
159591
+ return { kind: "kill-all" };
159592
+ if (verb === "show" || verb === "kill") {
159593
+ if (parts.length !== 2 || !/^#?\d+$/.test(parts[1]))
159594
+ return { kind: "error", message: `Usage: /bashes ${verb} <id>` };
159595
+ const id = Number(parts[1].replace("#", ""));
159596
+ return verb === "show" ? { kind: "show", id } : { kind: "kill", id };
159597
+ }
159598
+ if (parts.length === 1 && /^#?\d+$/.test(parts[0]))
159599
+ return { kind: "show", id: Number(parts[0].replace("#", "")) };
159600
+ return { kind: "error", message: "Usage: /bashes [show <id> | kill <id> | kill all]" };
159601
+ }
159602
+
159592
159603
  // src/ui/toolOutputStore.ts
159593
159604
  var MAX_ENTRIES = 20;
159594
159605
  var MAX_ENTRY_CHARS = 4e5;
@@ -159705,6 +159716,11 @@ var CLI_VERSION = (() => {
159705
159716
  })();
159706
159717
  var sessionUsage = new UsageTotals();
159707
159718
  var toolOutputs = new ToolOutputStore();
159719
+ var bgShells = new BackgroundShells();
159720
+ var bangDetach = null;
159721
+ process.on("exit", () => {
159722
+ bgShells.killAll();
159723
+ });
159708
159724
  function printExpansion(arg) {
159709
159725
  const r2 = resolveExpand(toolOutputs, arg);
159710
159726
  if (!r2.entry) {
@@ -160695,6 +160711,7 @@ function printHelp() {
160695
160711
  ["/team [use <id|personal>]", "List your teams, or switch what usage is billed to"],
160696
160712
  ["/cost", "Tokens and cost of this conversation so far"],
160697
160713
  ["/status", "Version, model, mode, directory, session and cost at a glance"],
160714
+ ["/bashes [show|kill <id>]", "List commands you moved to the background with Ctrl+B; show their output or stop them"],
160698
160715
  ["/expand [n]", "Show the full output of a folded tool call (newest by default; Ctrl+O does the same)"],
160699
160716
  ["/copy", "Copy the last reply to the clipboard"],
160700
160717
  ["/export [file]", "Write this conversation to a Markdown file"],
@@ -160740,6 +160757,7 @@ function printHelp() {
160740
160757
  ["Esc Esc", "At an empty prompt: pick a checkpoint to rewind to (same as /rewind)"],
160741
160758
  ["?", "At an empty prompt: show this list"],
160742
160759
  ["Ctrl+O", "Expand the last folded tool output (also works while it is running; /expand [n] picks an older one)"],
160760
+ ["Ctrl+B", "While a `!command` runs: move it to the background and keep typing (/bashes lists, shows and stops them)"],
160743
160761
  ["Ctrl+L", "Redraw the screen"],
160744
160762
  ["Ctrl+D", "Delete the character under the cursor; press twice on an empty line to exit"],
160745
160763
  ["(paste)", "Multi-line paste goes into the input box \u2014 it does not send until you press Enter"]
@@ -161185,6 +161203,13 @@ ${text}` : "");
161185
161203
  getInkTerminal().onExpand(() => {
161186
161204
  printExpansion("");
161187
161205
  });
161206
+ getInkTerminal().onBackground(() => {
161207
+ if (bangDetach && !bangDetach.requested) {
161208
+ bangDetach.requested = true;
161209
+ return true;
161210
+ }
161211
+ return false;
161212
+ });
161188
161213
  getInkTerminal().onRewind(() => {
161189
161214
  void (async () => {
161190
161215
  const opts = rewindChoices(checkpoints.list());
@@ -161724,6 +161749,39 @@ ${dirList}`;
161724
161749
  printExpansion(arg);
161725
161750
  rl.prompt();
161726
161751
  return;
161752
+ case "/bashes": {
161753
+ const a = parseBashesArgs(arg);
161754
+ if (a.kind === "error") {
161755
+ console.log(source_default.yellow(" " + a.message));
161756
+ } else if (a.kind === "list") {
161757
+ const jobs = bgShells.list();
161758
+ if (!jobs.length)
161759
+ console.log(source_default.dim(" No background commands. Start `!cmd`, then press Ctrl+B to move it to the background."));
161760
+ else
161761
+ for (const j of jobs)
161762
+ console.log(" " + (j.status === "running" ? source_default.cyan(describeShell(j)) : source_default.dim(describeShell(j))));
161763
+ } else if (a.kind === "show") {
161764
+ const j = bgShells.get(a.id);
161765
+ if (!j)
161766
+ console.log(source_default.yellow(` No background command #${a.id}.`));
161767
+ else {
161768
+ console.log(source_default.cyan(" " + describeShell(j)));
161769
+ const lines = j.tail.replace(/\s+$/, "").split("\n");
161770
+ const shownLines = lines.slice(-40);
161771
+ if (lines.length > shownLines.length)
161772
+ console.log(source_default.dim(` \u2026 ${lines.length - shownLines.length} earlier line(s) not shown`));
161773
+ for (const l2 of shownLines)
161774
+ console.log(source_default.dim(" \u2502 ") + l2.split("\r").pop().slice(0, 300));
161775
+ }
161776
+ } else if (a.kind === "kill") {
161777
+ console.log(bgShells.kill(a.id) ? source_default.green(` Stopped #${a.id}.`) : source_default.yellow(` #${a.id} is not running.`));
161778
+ } else {
161779
+ const n = bgShells.killAll();
161780
+ console.log(n ? source_default.green(` Stopped ${n} background command${n === 1 ? "" : "s"}.`) : source_default.dim(" Nothing is running."));
161781
+ }
161782
+ rl.prompt();
161783
+ return;
161784
+ }
161727
161785
  case "/copy": {
161728
161786
  const text = lastAssistantText(messages);
161729
161787
  if (!text) {
@@ -162198,22 +162256,59 @@ ${text}
162198
162256
  shown++;
162199
162257
  };
162200
162258
  let result;
162259
+ let ctl = null;
162260
+ let bgId = null;
162261
+ const detach = { requested: false };
162262
+ bangDetach = detach;
162201
162263
  try {
162202
162264
  result = await runBang(bang, {
162203
162265
  cwd: workDir,
162204
162266
  signal: abortSignal,
162267
+ detach,
162268
+ onSpawn: (c) => {
162269
+ ctl = c;
162270
+ },
162205
162271
  onData: (chunk) => {
162272
+ if (bgId !== null) {
162273
+ bgShells.append(bgId, chunk);
162274
+ return;
162275
+ }
162206
162276
  const parts = (partial + chunk).split("\n");
162207
162277
  partial = parts.pop();
162208
162278
  for (const l2 of parts)
162209
162279
  showLine(l2);
162280
+ },
162281
+ onSettled: (final) => {
162282
+ if (bgId === null)
162283
+ return;
162284
+ const job = bgShells.finish(bgId, final);
162285
+ if (!job || job.status === "killed")
162286
+ return;
162287
+ const ok = final.code === 0;
162288
+ console.log((ok ? source_default.green : source_default.red)(` \u2699 background #${job.id} finished (exit ${final.code ?? final.signal}): ${job.cmd.length > 50 ? job.cmd.slice(0, 49) + "\u2026" : job.cmd}`) + source_default.dim(" /bashes show " + job.id));
162289
+ toolOutputs.add({ name: "bash", label: `!${job.cmd} (background #${job.id})`, output: final.output, isError: !ok, durationMs: 0 });
162210
162290
  }
162211
162291
  });
162212
162292
  if (partial)
162213
162293
  showLine(partial);
162214
162294
  } finally {
162295
+ bangDetach = null;
162215
162296
  agentRunning = false;
162216
162297
  }
162298
+ if (result.detached && ctl) {
162299
+ const job = bgShells.add(bang, ctl, result.output);
162300
+ bgId = job.id;
162301
+ console.log(source_default.cyan(` \u2699 Moved to background as #${job.id}`) + source_default.dim(" (/bashes to list \xB7 /bashes show " + job.id + " \xB7 /bashes kill " + job.id + ")"));
162302
+ messages.push(
162303
+ { role: "user", content: [{ type: "text", text: bangTranscript(bang, result) }] },
162304
+ { role: "assistant", content: [{ type: "text", text: "Noted the command output." }] }
162305
+ );
162306
+ updateTitle();
162307
+ saveSession(sessionId, sessionTitle || "Untitled", workDir, messages);
162308
+ rl.resume();
162309
+ rl.prompt();
162310
+ return;
162311
+ }
162217
162312
  const tag = result.aborted ? source_default.yellow(" interrupted") : result.timedOut ? source_default.yellow(" timed out") : result.code === 0 ? source_default.dim(" exit 0") : source_default.red(` exit ${result.code ?? result.signal}`);
162218
162313
  console.log(tag);
162219
162314
  if (result.output) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexrall-code",
3
- "version": "0.5.130",
3
+ "version": "0.5.131",
4
4
  "description": "Nexrall Code — AI coding assistant for your terminal (headless agent for scripts, CI and automation)",
5
5
  "keywords": [
6
6
  "ai",