agent-yes 1.257.0 → 1.258.0

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 (35) hide show
  1. package/dist/{SUPPORTED_CLIS-BPPX9iGs.js → SUPPORTED_CLIS-DTjWc_om.js} +2 -2
  2. package/dist/{SUPPORTED_CLIS-BNRnFFc2.js → SUPPORTED_CLIS-Dov9qOQf.js} +3 -3
  3. package/dist/{agentShare-aCSONMZp.js → agentShare-n39adLst.js} +2 -2
  4. package/dist/{callback-CpGhgUwm.js → callback-irtAFEXY.js} +3 -3
  5. package/dist/{callback-CzYsRWjX.js → callback-tnnB0ibj.js} +2 -2
  6. package/dist/cli.js +5 -5
  7. package/dist/index.js +2 -2
  8. package/dist/{notifyDaemon-Cvnvfqw8.js → notifyDaemon-BKaX7tHT.js} +2 -2
  9. package/dist/{rustBinary-DWzZGM4A.js → rustBinary-BjMf3hJs.js} +2 -2
  10. package/dist/{schedule-ChwwFCgQ.js → schedule-D19dfoUv.js} +4 -4
  11. package/dist/{serve-DQPkrKcN.js → serve-CrQavO4J.js} +16 -16
  12. package/dist/{setup-uZqYF0KT.js → setup-D1en_TN3.js} +2 -2
  13. package/dist/subcommands-B7iBIPXn.js +10 -0
  14. package/dist/{subcommands-B04Jcb4a.js → subcommands-bd-cHNKW.js} +69 -26
  15. package/dist/{terminal-sCD-7D_2.js → terminal-eRiT4Zbo.js} +2 -2
  16. package/dist/{trayApp-CCN3zyo7.js → trayApp-BBNKEQbP.js} +2 -2
  17. package/dist/trayApp-Cef9w_n4.js +5 -0
  18. package/dist/{ts-Ct3yd-1m.js → ts-CvUvWb2n.js} +29 -4
  19. package/dist/{versionChecker-bcw3nJay.js → versionChecker-BKFAk759.js} +2 -2
  20. package/dist/{widget-B7NK6egy.js → widget-C1GqFymh.js} +3 -3
  21. package/dist/{ws-BkOJQeeZ.js → ws-CWPlW7ia.js} +2 -2
  22. package/dist/{ws-BlISbGY8.js → ws-DgHuhAPq.js} +2 -2
  23. package/lab/ui/console-logic.js +6 -3
  24. package/lab/ui/index.html +27 -5
  25. package/package.json +1 -1
  26. package/ts/index.ts +42 -2
  27. package/ts/needsInput.spec.ts +38 -0
  28. package/ts/needsInput.ts +7 -3
  29. package/ts/serve.ts +2 -1
  30. package/ts/subcommands.spec.ts +26 -0
  31. package/ts/subcommands.ts +63 -6
  32. package/ts/todoParse.spec.ts +30 -0
  33. package/ts/todoParse.ts +19 -5
  34. package/dist/subcommands-dNgWfaKE.js +0 -10
  35. package/dist/trayApp-HLGzyGrP.js +0 -5
@@ -1,8 +1,8 @@
1
- import { t as CLIS_CONFIG } from "./ts-Ct3yd-1m.js";
1
+ import { t as CLIS_CONFIG } from "./ts-CvUvWb2n.js";
2
2
 
3
3
  //#region ts/SUPPORTED_CLIS.ts
4
4
  const SUPPORTED_CLIS = Object.keys(CLIS_CONFIG);
5
5
 
6
6
  //#endregion
7
7
  export { SUPPORTED_CLIS as t };
8
- //# sourceMappingURL=SUPPORTED_CLIS-BPPX9iGs.js.map
8
+ //# sourceMappingURL=SUPPORTED_CLIS-DTjWc_om.js.map
@@ -1,11 +1,11 @@
1
1
  import "./logger-CDIsZ-Pp.js";
2
- import "./versionChecker-bcw3nJay.js";
3
- import "./ts-Ct3yd-1m.js";
2
+ import "./versionChecker-BKFAk759.js";
3
+ import "./ts-CvUvWb2n.js";
4
4
  import "./todoAutomation-BQRW614S.js";
5
5
  import "./JsonlStore-CPGIR0Wy.js";
6
6
  import "./globalPidIndex-Dq4KY2IV.js";
7
7
  import "./pidStore-Cmcc8Oej.js";
8
8
  import "./messageLog-BYFwioFY.js";
9
- import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-BPPX9iGs.js";
9
+ import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-DTjWc_om.js";
10
10
 
11
11
  export { SUPPORTED_CLIS };
@@ -3,7 +3,7 @@ import "./globalPidIndex-Dq4KY2IV.js";
3
3
  import "./messageLog-BYFwioFY.js";
4
4
  import "./e2e-CUMZC53I.js";
5
5
  import "./configShared-DtHMBCl7.js";
6
- import { P as resolveOne } from "./subcommands-B04Jcb4a.js";
6
+ import { I as resolveOne } from "./subcommands-bd-cHNKW.js";
7
7
  import "./webrtcLink-CS4D-3G1.js";
8
8
  import "./remotes-Dy4Rg8Yp.js";
9
9
  import { a as startShare } from "./share-B-e_9WBx.js";
@@ -229,4 +229,4 @@ function transformEvent(rawEvent, agentId, forwarded) {
229
229
 
230
230
  //#endregion
231
231
  export { createScopedShare, listShares, revokeAllShares, revokeShare };
232
- //# sourceMappingURL=agentShare-aCSONMZp.js.map
232
+ //# sourceMappingURL=agentShare-n39adLst.js.map
@@ -1,5 +1,5 @@
1
1
  import { t as agentYesHome } from "./agentYesHome-CtmsY6I-.js";
2
- import { P as resolveOne } from "./subcommands-B04Jcb4a.js";
2
+ import { I as resolveOne } from "./subcommands-bd-cHNKW.js";
3
3
  import { a as mintCapability, o as parseExpires } from "./callbackCore-Cg3Ye0ZS.js";
4
4
  import { randomBytes } from "node:crypto";
5
5
  import { chmodSync, readFileSync, renameSync, writeFileSync } from "node:fs";
@@ -72,7 +72,7 @@ async function resolveBase(flag) {
72
72
  if (flag) return flag;
73
73
  if (process.env.AGENT_YES_CALLBACK_BASE) return process.env.AGENT_YES_CALLBACK_BASE;
74
74
  try {
75
- const { resolveLocalServeUrl } = await import("./serve-DQPkrKcN.js");
75
+ const { resolveLocalServeUrl } = await import("./serve-CrQavO4J.js");
76
76
  const url = await resolveLocalServeUrl();
77
77
  if (url) return url;
78
78
  } catch {}
@@ -187,4 +187,4 @@ async function cmdCallback(rest) {
187
187
 
188
188
  //#endregion
189
189
  export { loadOrCreateCallbackSecret as i, isCallbackRevoked as n, loadCallbackSecretReadOnly as r, cmdCallback as t };
190
- //# sourceMappingURL=callback-CpGhgUwm.js.map
190
+ //# sourceMappingURL=callback-irtAFEXY.js.map
@@ -3,10 +3,10 @@ import "./globalPidIndex-Dq4KY2IV.js";
3
3
  import "./messageLog-BYFwioFY.js";
4
4
  import "./e2e-CUMZC53I.js";
5
5
  import "./configShared-DtHMBCl7.js";
6
- import "./subcommands-B04Jcb4a.js";
6
+ import "./subcommands-bd-cHNKW.js";
7
7
  import "./webrtcLink-CS4D-3G1.js";
8
8
  import "./remotes-Dy4Rg8Yp.js";
9
9
  import "./callbackCore-Cg3Ye0ZS.js";
10
- import { i as loadOrCreateCallbackSecret, n as isCallbackRevoked, r as loadCallbackSecretReadOnly, t as cmdCallback } from "./callback-CpGhgUwm.js";
10
+ import { i as loadOrCreateCallbackSecret, n as isCallbackRevoked, r as loadCallbackSecretReadOnly, t as cmdCallback } from "./callback-irtAFEXY.js";
11
11
 
12
12
  export { cmdCallback };
package/dist/cli.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env bun
2
2
  import { t as invokedCliName } from "./invokedCli-uqM2YYA7.js";
3
3
  import { n as logger } from "./logger-CDIsZ-Pp.js";
4
- import { i as versionString, n as displayVersion, t as checkAndAutoUpdate } from "./versionChecker-bcw3nJay.js";
5
- import { r as getRustBinary } from "./rustBinary-DWzZGM4A.js";
4
+ import { i as versionString, n as displayVersion, t as checkAndAutoUpdate } from "./versionChecker-BKFAk759.js";
5
+ import { r as getRustBinary } from "./rustBinary-BjMf3hJs.js";
6
6
  import { argv } from "process";
7
7
  import { spawn } from "child_process";
8
8
  import ms from "ms";
@@ -285,7 +285,7 @@ function buildRustArgs(argv, cliFromScript, supportedClis) {
285
285
  const rawArg = process.argv[2];
286
286
  const managerCommands = !invokedCliName(process.argv);
287
287
  const isHelpFlag = rawArg === "-h" || rawArg === "--help";
288
- const { isSubcommand, runSubcommand, cmdHelp, isUnknownManagerToken } = await import("./subcommands-dNgWfaKE.js");
288
+ const { isSubcommand, runSubcommand, cmdHelp, isUnknownManagerToken } = await import("./subcommands-B7iBIPXn.js");
289
289
  if (isHelpFlag && process.argv.length === 3) {
290
290
  await cmdHelp(managerCommands);
291
291
  process.exit(0);
@@ -295,7 +295,7 @@ function buildRustArgs(argv, cliFromScript, supportedClis) {
295
295
  process.exit(code ?? 0);
296
296
  }
297
297
  {
298
- const { SUPPORTED_CLIS } = await import("./SUPPORTED_CLIS-BNRnFFc2.js");
298
+ const { SUPPORTED_CLIS } = await import("./SUPPORTED_CLIS-Dov9qOQf.js");
299
299
  if (isUnknownManagerToken(rawArg, managerCommands, SUPPORTED_CLIS)) {
300
300
  process.stderr.write(`ay: unknown subcommand or CLI '${rawArg}'.\n See 'ay help' for subcommands. To run an agent, name a CLI:\n 'ay <cli> …' (e.g. 'ay claude …') or 'ay --cli <cli> …'.\n`);
301
301
  process.exit(1);
@@ -346,7 +346,7 @@ if (config.useRust) {
346
346
  }
347
347
  }
348
348
  if (rustBinary) {
349
- const { SUPPORTED_CLIS } = await import("./SUPPORTED_CLIS-BNRnFFc2.js");
349
+ const { SUPPORTED_CLIS } = await import("./SUPPORTED_CLIS-Dov9qOQf.js");
350
350
  const rustArgs = buildRustArgs(process.argv, config.cli, SUPPORTED_CLIS);
351
351
  if (config.verbose) {
352
352
  console.log(`[rust] Using binary: ${rustBinary}`);
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import "./logger-CDIsZ-Pp.js";
2
- import "./versionChecker-bcw3nJay.js";
3
- import { a as hasTodoStore, i as answerAsk, l as AgentContext, n as agentYes, o as listAsks, r as config, s as listAsksForProject, t as CLIS_CONFIG } from "./ts-Ct3yd-1m.js";
2
+ import "./versionChecker-BKFAk759.js";
3
+ import { a as hasTodoStore, i as answerAsk, l as AgentContext, n as agentYes, o as listAsks, r as config, s as listAsksForProject, t as CLIS_CONFIG } from "./ts-CvUvWb2n.js";
4
4
  import { _ as isKnownKind, a as renderTree, b as statesOf, c as monitorHint, d as openStore, f as DONE_STATE, g as initialState, h as canTransition, i as renderDigest, l as CycleError, m as ORPHANED_STATE, o as unblockedTasks, p as LIFECYCLES, r as openBlockers, s as describeBlock, t as reconcileTodos, u as TodoStore, v as nextStates, x as removeControlCharacters, y as requiredGate } from "./todoAutomation-BQRW614S.js";
5
5
  import "./JsonlStore-CPGIR0Wy.js";
6
6
  import "./globalPidIndex-Dq4KY2IV.js";
@@ -3,7 +3,7 @@ import "./globalPidIndex-Dq4KY2IV.js";
3
3
  import "./messageLog-BYFwioFY.js";
4
4
  import "./e2e-CUMZC53I.js";
5
5
  import "./configShared-DtHMBCl7.js";
6
- import { $ as shouldStealLock, C as listRecords, J as hostId, K as appendEvent, Q as readInbox, X as listInboxParents, Z as liveWatchers, _ as isPidAlive, c as deriveLiveState, et as daemonLockDir, j as renderLogTailLines, nt as notifyDir, q as gcInboxes, tt as daemonLockOwnerPath } from "./subcommands-B04Jcb4a.js";
6
+ import { $ as liveWatchers, J as appendEvent, N as renderLogTailLines, Q as listInboxParents, X as hostId, Y as gcInboxes, et as readInbox, it as notifyDir, l as deriveLiveState, nt as daemonLockDir, rt as daemonLockOwnerPath, tt as shouldStealLock, v as isPidAlive, w as listRecords } from "./subcommands-bd-cHNKW.js";
7
7
  import "./webrtcLink-CS4D-3G1.js";
8
8
  import "./remotes-Dy4Rg8Yp.js";
9
9
  import { mkdir, readFile, rename, rm, stat, writeFile } from "fs/promises";
@@ -836,4 +836,4 @@ async function ensureDaemon() {
836
836
 
837
837
  //#endregion
838
838
  export { daemonStatus, ensureDaemon, requestDaemonStop, runDaemon };
839
- //# sourceMappingURL=notifyDaemon-Cvnvfqw8.js.map
839
+ //# sourceMappingURL=notifyDaemon-BKaX7tHT.js.map
@@ -1,4 +1,4 @@
1
- import { r as getInstalledPackage } from "./versionChecker-bcw3nJay.js";
1
+ import { r as getInstalledPackage } from "./versionChecker-BKFAk759.js";
2
2
  import { execFileSync } from "child_process";
3
3
  import { existsSync, mkdirSync, unlinkSync } from "fs";
4
4
  import { chmod, copyFile } from "fs/promises";
@@ -243,4 +243,4 @@ async function getRustBinary(options = {}) {
243
243
 
244
244
  //#endregion
245
245
  export { findTrayLauncher as n, getRustBinary as r, findSpawnHiddenLauncher as t };
246
- //# sourceMappingURL=rustBinary-DWzZGM4A.js.map
246
+ //# sourceMappingURL=rustBinary-BjMf3hJs.js.map
@@ -1,12 +1,12 @@
1
1
  import "./logger-CDIsZ-Pp.js";
2
- import "./versionChecker-bcw3nJay.js";
3
- import "./ts-Ct3yd-1m.js";
2
+ import "./versionChecker-BKFAk759.js";
3
+ import "./ts-CvUvWb2n.js";
4
4
  import "./todoAutomation-BQRW614S.js";
5
5
  import "./JsonlStore-CPGIR0Wy.js";
6
6
  import "./globalPidIndex-Dq4KY2IV.js";
7
7
  import "./pidStore-Cmcc8Oej.js";
8
8
  import "./messageLog-BYFwioFY.js";
9
- import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-BPPX9iGs.js";
9
+ import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-DTjWc_om.js";
10
10
  import { f as resolveSpawnCwd } from "./workspaceConfig-Bxuk_MOq.js";
11
11
  import { n as liveEnv, t as ensureNodeRuntime } from "./nodeRuntime-vhOBffJ3.js";
12
12
  import { createHash } from "node:crypto";
@@ -153,4 +153,4 @@ async function cmdSchedule(rest) {
153
153
 
154
154
  //#endregion
155
155
  export { cmdSchedule };
156
- //# sourceMappingURL=schedule-ChwwFCgQ.js.map
156
+ //# sourceMappingURL=schedule-D19dfoUv.js.map
@@ -1,7 +1,7 @@
1
1
  import "./logger-CDIsZ-Pp.js";
2
- import { r as getInstalledPackage } from "./versionChecker-bcw3nJay.js";
3
- import { t as findSpawnHiddenLauncher } from "./rustBinary-DWzZGM4A.js";
4
- import { c as permissionBadge, i as answerAsk, o as listAsks, u as CLAUDE_SESSION_PIN_ENV } from "./ts-Ct3yd-1m.js";
2
+ import { r as getInstalledPackage } from "./versionChecker-BKFAk759.js";
3
+ import { t as findSpawnHiddenLauncher } from "./rustBinary-BjMf3hJs.js";
4
+ import { c as permissionBadge, i as answerAsk, o as listAsks, u as CLAUDE_SESSION_PIN_ENV } from "./ts-CvUvWb2n.js";
5
5
  import { t as agentYesHome$1 } from "./agentYesHome-CtmsY6I-.js";
6
6
  import { x as removeControlCharacters } from "./todoAutomation-BQRW614S.js";
7
7
  import "./JsonlStore-CPGIR0Wy.js";
@@ -11,17 +11,17 @@ import { r as recordInbox } from "./messageLog-BYFwioFY.js";
11
11
  import { t as pgidForWrapper } from "./reaper-C4BvNylH.js";
12
12
  import "./e2e-CUMZC53I.js";
13
13
  import "./configShared-DtHMBCl7.js";
14
- import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-BPPX9iGs.js";
15
- import { A as recentReadEdges, C as listRecords, D as readNotes, G as writeToIpc, M as renderRawLog, N as renderRawLogLines, O as readPtysize, P as resolveOne, Y as isTransientLockMkdirError, f as extractNeedsInput, j as renderLogTailLines, k as recentMessageEdges, l as deriveLiveStatus, o as controlCodeFromName, p as extractTaskCounts, rt as TYPING_BADGE, u as extractBadges, x as isUserTyping, z as snapshotStatus } from "./subcommands-B04Jcb4a.js";
14
+ import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-DTjWc_om.js";
15
+ import { A as readPtysize, F as renderRawLogLines, I as resolveOne, M as recentReadEdges, N as renderLogTailLines, O as readLogForRender, P as renderRawLog, S as isUserTyping, V as snapshotStatus, Z as isTransientLockMkdirError, at as TYPING_BADGE, d as extractBadges, j as recentMessageEdges, k as readNotes, m as extractTaskCounts, p as extractNeedsInput, q as writeToIpc, s as controlCodeFromName, u as deriveLiveStatus, w as listRecords } from "./subcommands-bd-cHNKW.js";
16
16
  import "./webrtcLink-CS4D-3G1.js";
17
17
  import "./remotes-Dy4Rg8Yp.js";
18
18
  import { i as frameVisitorMessage, s as verifyCapability, t as MAX_CALLBACK_MSG_BYTES } from "./callbackCore-Cg3Ye0ZS.js";
19
- import { n as isCallbackRevoked, r as loadCallbackSecretReadOnly } from "./callback-CpGhgUwm.js";
19
+ import { n as isCallbackRevoked, r as loadCallbackSecretReadOnly } from "./callback-irtAFEXY.js";
20
20
  import { a as getProvisionRoot, d as isProvisionAllowed, f as resolveSpawnCwd, i as getProvisionHook, l as hasProvisionHook, o as getSpawnHook, t as allowsSkipPermissions, u as hasSpawnHook } from "./workspaceConfig-Bxuk_MOq.js";
21
21
  import { n as liveEnv, t as ensureNodeRuntime } from "./nodeRuntime-vhOBffJ3.js";
22
- import { i as importProvisionModule } from "./ws-BlISbGY8.js";
22
+ import { i as importProvisionModule } from "./ws-DgHuhAPq.js";
23
23
  import { r as spawnRejectionReason } from "./spawnGate-D0E1uRP7.js";
24
- import { i as launchTray } from "./trayApp-CCN3zyo7.js";
24
+ import { i as launchTray } from "./trayApp-BBNKEQbP.js";
25
25
  import yargs from "yargs";
26
26
  import { appendFile, mkdir, open, readFile, readdir, rename, rm, stat, unlink, writeFile } from "fs/promises";
27
27
  import { arch, cpus, freemem, homedir, hostname, loadavg, platform, totalmem, uptime, userInfo } from "os";
@@ -2368,7 +2368,7 @@ Options:
2368
2368
  }
2369
2369
  });
2370
2370
  if (req.method === "GET" && p === "/api/ws") {
2371
- const ws = await import("./ws-BkOJQeeZ.js");
2371
+ const ws = await import("./ws-CWPlW7ia.js");
2372
2372
  try {
2373
2373
  await ws.loadProvision();
2374
2374
  } catch (e) {
@@ -2388,7 +2388,7 @@ Options:
2388
2388
  if (req.method === "GET" && p === "/api/ws/status") {
2389
2389
  const dirRaw = url.searchParams.get("path");
2390
2390
  if (!dirRaw) return new Response("missing ?path=<workspace dir>", { status: 400 });
2391
- const ws = await import("./ws-BkOJQeeZ.js");
2391
+ const ws = await import("./ws-CWPlW7ia.js");
2392
2392
  let prov;
2393
2393
  try {
2394
2394
  prov = await ws.loadProvision();
@@ -2431,7 +2431,7 @@ Options:
2431
2431
  try {
2432
2432
  const record = await resolveOne(keyword, defaultOpts());
2433
2433
  if (!record.log_file) return new Response(`pid ${record.pid}: no log_file`, { status: 404 });
2434
- const buf = await readFile(record.log_file);
2434
+ const buf = await readLogForRender(record.log_file);
2435
2435
  const size = await readPtysize(record.pid);
2436
2436
  const text = await renderRawLog(buf, {
2437
2437
  mode,
@@ -3145,7 +3145,7 @@ Options:
3145
3145
  const perm = body.perm ?? "r";
3146
3146
  if (perm !== "r" && perm !== "rw") return new Response(`invalid perm ${perm} (want r or rw)`, { status: 400 });
3147
3147
  try {
3148
- const { createScopedShare } = await import("./agentShare-aCSONMZp.js");
3148
+ const { createScopedShare } = await import("./agentShare-n39adLst.js");
3149
3149
  const share = await createScopedShare({
3150
3150
  agent: body.agent,
3151
3151
  perm,
@@ -3160,12 +3160,12 @@ Options:
3160
3160
  }
3161
3161
  }
3162
3162
  if (req.method === "GET" && p === "/api/shares") {
3163
- const { listShares } = await import("./agentShare-aCSONMZp.js");
3163
+ const { listShares } = await import("./agentShare-n39adLst.js");
3164
3164
  return Response.json(listShares());
3165
3165
  }
3166
3166
  const revokeM = /^\/api\/share\/([^/]+)$/.exec(p);
3167
3167
  if (req.method === "DELETE" && revokeM) {
3168
- const { revokeShare } = await import("./agentShare-aCSONMZp.js");
3168
+ const { revokeShare } = await import("./agentShare-n39adLst.js");
3169
3169
  const ok = revokeShare(decodeURIComponent(revokeM[1]));
3170
3170
  return new Response(ok ? "revoked" : "no such share", { status: ok ? 200 : 404 });
3171
3171
  }
@@ -3467,7 +3467,7 @@ Options:
3467
3467
  const shutdown = (resolve) => {
3468
3468
  if (heartbeat) clearInterval(heartbeat);
3469
3469
  closeShare?.();
3470
- import("./agentShare-aCSONMZp.js").then((m) => m.revokeAllShares()).catch(() => {});
3470
+ import("./agentShare-n39adLst.js").then((m) => m.revokeAllShares()).catch(() => {});
3471
3471
  server?.stop();
3472
3472
  Promise.resolve(releaseHostLock?.()).catch(() => {}).then(resolve);
3473
3473
  };
@@ -3480,4 +3480,4 @@ Options:
3480
3480
 
3481
3481
  //#endregion
3482
3482
  export { cmdServe, loadTokenReadOnly, mintScopedTermToken, resolveDaemonHttpBase, resolveLocalServeUrl };
3483
- //# sourceMappingURL=serve-DQPkrKcN.js.map
3483
+ //# sourceMappingURL=serve-CrQavO4J.js.map
@@ -32,7 +32,7 @@ async function cmdSetup(rest) {
32
32
  if (!existsSync(abs)) process.stderr.write(` note: that directory doesn't exist yet — create it, or agents spawned there will fail\n`);
33
33
  if (noShare) return 0;
34
34
  process.stdout.write(`\nsharing this machine to agent-yes.com…\n`);
35
- const { cmdServe } = await import("./serve-DQPkrKcN.js");
35
+ const { cmdServe } = await import("./serve-CrQavO4J.js");
36
36
  return cmdServe([
37
37
  "install",
38
38
  "--share",
@@ -42,4 +42,4 @@ async function cmdSetup(rest) {
42
42
 
43
43
  //#endregion
44
44
  export { cmdSetup };
45
- //# sourceMappingURL=setup-uZqYF0KT.js.map
45
+ //# sourceMappingURL=setup-D1en_TN3.js.map
@@ -0,0 +1,10 @@
1
+ import "./logger-CDIsZ-Pp.js";
2
+ import "./globalPidIndex-Dq4KY2IV.js";
3
+ import "./messageLog-BYFwioFY.js";
4
+ import "./e2e-CUMZC53I.js";
5
+ import "./configShared-DtHMBCl7.js";
6
+ import { A as readPtysize, B as runSubcommand, C as lastStdinAt, D as readAgentPtysize, E as menuSelectKeys, F as renderRawLogLines, G as waitForLogQuiet, H as stdinActivityPath, I as resolveOne, K as writeKeysPaced, L as resolveReadWindow, M as recentReadEdges, N as renderLogTailLines, O as readLogForRender, P as renderRawLog, R as resolveResumeArgs, S as isUserTyping, T as matchKeyword, U as stopTipForCli, V as snapshotStatus, W as submitAndConfirm, _ as isExitRequest, a as backoffWhileTyping, b as isSubcommand, c as cursorAbs, d as extractBadges, f as extractMenu, g as isAgentStuck, h as finalizedLines, i as TYPING_WINDOW_MS, j as recentMessageEdges, k as readNotes, l as deriveLiveState, m as extractTaskCounts, n as MAX_RENDER_BYTES, o as cmdHelp, p as extractNeedsInput, q as writeToIpc, r as READ_PAGE_DEFAULT, s as controlCodeFromName, t as GRACEFUL_EXIT_COMMANDS, u as deriveLiveStatus, v as isPidAlive, w as listRecords, x as isUnknownManagerToken, y as isSlashCommand, z as restartHintLines } from "./subcommands-bd-cHNKW.js";
7
+ import "./webrtcLink-CS4D-3G1.js";
8
+ import "./remotes-Dy4Rg8Yp.js";
9
+
10
+ export { cmdHelp, isSubcommand, isUnknownManagerToken, runSubcommand };
@@ -120,15 +120,19 @@ function markerOf(line) {
120
120
  if (PENDING.has(ch)) return "pending";
121
121
  return null;
122
122
  }
123
+ function isBareAnchor(line) {
124
+ const s = line.trim();
125
+ return s.startsWith(ANCHOR) && s.slice(1).trim() === "";
126
+ }
123
127
  /**
124
128
  * Find the MOST RECENT confidently-detected todo block in the rendered lines and
125
129
  * return its {done, total}. Returns null when none qualifies (caller omits the
126
130
  * badge entirely — never shows "0/0").
127
131
  *
128
132
  * A block is a maximal run of consecutive marker lines. It only counts when it
129
- * is anchored — the `⎿` glyph appears on the run's first line or the line
130
- * directly above it — and has ≥2 marker lines. The last qualifying block wins,
131
- * since the agent's current todo state is the one drawn most recently.
133
+ * is anchored — the `⎿` glyph sits on the run's FIRST line, or a bare `⎿` sits on
134
+ * the line directly above it — and has ≥2 marker lines. The last qualifying block
135
+ * wins, since the agent's current todo state is the one drawn most recently.
132
136
  */
133
137
  function parseTaskCounts(lines) {
134
138
  let best = null;
@@ -139,7 +143,7 @@ function parseTaskCounts(lines) {
139
143
  i++;
140
144
  continue;
141
145
  }
142
- let hasAnchor = i > 0 && lines[i - 1].includes(ANCHOR);
146
+ let hasAnchor = i > 0 && isBareAnchor(lines[i - 1]);
143
147
  const counts = {
144
148
  done: 0,
145
149
  inprogress: 0,
@@ -149,7 +153,7 @@ function parseTaskCounts(lines) {
149
153
  for (; j < n; j++) {
150
154
  const mk = markerOf(lines[j]);
151
155
  if (mk === null) break;
152
- if (lines[j].includes(ANCHOR)) hasAnchor = true;
156
+ if (j === i && lines[j].includes(ANCHOR)) hasAnchor = true;
153
157
  counts[mk]++;
154
158
  }
155
159
  const total = counts.done + counts.inprogress + counts.pending;
@@ -277,7 +281,7 @@ function classifyNeedsInput(lines, cfg) {
277
281
  const end = Math.min(lines.length, last + 6);
278
282
  return { question: lines.slice(start, end).map((l) => l.trim()).filter((l) => l && !isChromeLine(l)).join(" • ").slice(0, 400) };
279
283
  }
280
- const OPTION_LINE = /^[\s❯›>▶◉○●·*-]*?(\d+)\.\s/;
284
+ const OPTION_LINE = /^[\s❯›>▶◉○●·│*-]*?(\d+)\.\s/;
281
285
  /**
282
286
  * Parse the selection menu a `needs_input` agent is parked on into a cursor
283
287
  * position + the available option numbers, so a caller can compute how far the
@@ -1394,35 +1398,35 @@ async function runSubcommand(argv) {
1394
1398
  return cmdCh(rest);
1395
1399
  }
1396
1400
  case "term": {
1397
- const { cmdTerm } = await import("./terminal-sCD-7D_2.js");
1401
+ const { cmdTerm } = await import("./terminal-eRiT4Zbo.js");
1398
1402
  return cmdTerm(rest);
1399
1403
  }
1400
1404
  case "widget": {
1401
- const { cmdWidget } = await import("./widget-B7NK6egy.js");
1405
+ const { cmdWidget } = await import("./widget-C1GqFymh.js");
1402
1406
  return cmdWidget(rest);
1403
1407
  }
1404
1408
  case "mint": {
1405
- const { cmdMint } = await import("./widget-B7NK6egy.js");
1409
+ const { cmdMint } = await import("./widget-C1GqFymh.js");
1406
1410
  return cmdMint(rest);
1407
1411
  }
1408
1412
  case "serve": {
1409
- const { cmdServe } = await import("./serve-DQPkrKcN.js");
1413
+ const { cmdServe } = await import("./serve-CrQavO4J.js");
1410
1414
  return cmdServe(rest);
1411
1415
  }
1412
1416
  case "tray": {
1413
- const { cmdTray } = await import("./trayApp-HLGzyGrP.js");
1417
+ const { cmdTray } = await import("./trayApp-Cef9w_n4.js");
1414
1418
  return cmdTray(rest);
1415
1419
  }
1416
1420
  case "setup": {
1417
- const { cmdSetup } = await import("./setup-uZqYF0KT.js");
1421
+ const { cmdSetup } = await import("./setup-D1en_TN3.js");
1418
1422
  return cmdSetup(rest);
1419
1423
  }
1420
1424
  case "ws": {
1421
- const { cmdWs } = await import("./ws-BkOJQeeZ.js");
1425
+ const { cmdWs } = await import("./ws-CWPlW7ia.js");
1422
1426
  return cmdWs(rest);
1423
1427
  }
1424
1428
  case "schedule": {
1425
- const { cmdSchedule } = await import("./schedule-ChwwFCgQ.js");
1429
+ const { cmdSchedule } = await import("./schedule-D19dfoUv.js");
1426
1430
  return cmdSchedule(rest);
1427
1431
  }
1428
1432
  case "remote": {
@@ -1434,7 +1438,7 @@ async function runSubcommand(argv) {
1434
1438
  return cmdExpose(rest);
1435
1439
  }
1436
1440
  case "callback": {
1437
- const { cmdCallback } = await import("./callback-CzYsRWjX.js");
1441
+ const { cmdCallback } = await import("./callback-tnnB0ibj.js");
1438
1442
  return cmdCallback(rest);
1439
1443
  }
1440
1444
  case "reap":
@@ -2452,7 +2456,7 @@ async function cmdRead(rest, { mode }) {
2452
2456
  throw new Error(`pid ${record.pid}: log file not found at ${logPath}`);
2453
2457
  }
2454
2458
  if (!stats.isFile()) throw new Error(`pid ${record.pid}: log path is not a file: ${logPath}`);
2455
- const buf = await readFile(logPath);
2459
+ const buf = await readLogForRender(logPath);
2456
2460
  const size = await readAgentPtysize(record);
2457
2461
  const noteLabel = (await readNotes()).get(record.pid);
2458
2462
  const header = noteLabel ? `[pid ${record.pid} ${shortenPath(record.cwd)} * ${noteLabel}]` : `[pid ${record.pid} ${shortenPath(record.cwd)}]`;
@@ -2467,7 +2471,7 @@ async function cmdRead(rest, { mode }) {
2467
2471
  process.stdout.write(rendered);
2468
2472
  if (!rendered.endsWith("\n")) process.stdout.write("\n");
2469
2473
  setInterval(() => void recordRead(reader.key, record.pid), 3e4).unref?.();
2470
- return plain ? followPlainLocal(logPath, buf) : followRawLocal(logPath, buf);
2474
+ return plain ? followPlainLocal(logPath, buf, stats.size) : followRawLocal(logPath, buf, stats.size);
2471
2475
  }
2472
2476
  const allLines = await renderRawLogLines(buf, {
2473
2477
  cols: size?.cols,
@@ -2585,11 +2589,11 @@ async function watchAppend(logPath, startOffset, onChunk, onStop) {
2585
2589
  * sequences stripped. Mirrors the historical behaviour, plus prompt signal /
2586
2590
  * pipe-close handling.
2587
2591
  */
2588
- async function followRawLocal(logPath, buf) {
2592
+ async function followRawLocal(logPath, buf, startOffset = buf.length) {
2589
2593
  process.stderr.write(`following... (Ctrl-C to stop)\n`);
2590
2594
  const ansiRe = /\x1b\[[0-?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[@-Z\\-_]/g;
2591
2595
  const ctrlRe = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g;
2592
- await watchAppend(logPath, buf.length, (chunk) => {
2596
+ await watchAppend(logPath, startOffset, (chunk) => {
2593
2597
  const text = new TextDecoder().decode(chunk).replace(ansiRe, "").replace(ctrlRe, "");
2594
2598
  if (text.trim()) process.stdout.write(text.trimStart());
2595
2599
  }, () => {});
@@ -2623,7 +2627,7 @@ function finalizedLines(term, fromAbs) {
2623
2627
  * settled, so the output is clean, newline-terminated, line-buffered text a
2624
2628
  * script can read. On stop, flush the line the cursor is still sitting on.
2625
2629
  */
2626
- async function followPlainLocal(logPath, buf) {
2630
+ async function followPlainLocal(logPath, buf, startOffset = buf.length) {
2627
2631
  process.stderr.write(`following... (plain; Ctrl-C / SIGTERM to stop)\n`);
2628
2632
  const { Terminal } = await import("@xterm/headless");
2629
2633
  const term = new Terminal({
@@ -2643,7 +2647,7 @@ async function followPlainLocal(logPath, buf) {
2643
2647
  for (const line of finalizedLines(term, emitted)) process.stdout.write(line + "\n");
2644
2648
  emitted = cursorAbs(term);
2645
2649
  };
2646
- await watchAppend(logPath, buf.length, async (chunk) => {
2650
+ await watchAppend(logPath, startOffset, async (chunk) => {
2647
2651
  await feed(chunk);
2648
2652
  flushCommitted();
2649
2653
  }, () => {
@@ -2686,6 +2690,45 @@ async function readAgentPtysize(record) {
2686
2690
  return null;
2687
2691
  }
2688
2692
  /**
2693
+ * Cap on how many trailing bytes of a raw PTY log we read before rendering.
2694
+ *
2695
+ * `renderRawLogLines` replays the bytes through an xterm with a fixed 50000-line
2696
+ * scrollback, so output older than the last ~50k lines is evicted from the result
2697
+ * no matter how much we read. A runaway CLI/TUI capture can reach ~1GB; slurping
2698
+ * the whole file only to throw ~all of it away is a large memory + CPU spike (a
2699
+ * ~1GB Buffer plus a full-stream vterm replay per request). 64 MiB comfortably
2700
+ * overflows the scrollback for any realistic terminal stream, so an oversized log
2701
+ * renders to the SAME lines while the allocation stays bounded.
2702
+ */
2703
+ const MAX_RENDER_BYTES = 64 * 1024 * 1024;
2704
+ /**
2705
+ * Read a raw PTY log for full-buffer rendering, capped to the trailing
2706
+ * MAX_RENDER_BYTES. Files at or below the cap are read whole (byte-identical to a
2707
+ * plain readFile); larger files return only their tail window. This is the same
2708
+ * tail-window tradeoff `renderLogTailLines` already makes at 32KB, just larger —
2709
+ * the window's first line may be a partial (garbled) render, but it sits at the
2710
+ * scrollback-eviction boundary, ~50k lines above any tail/head/normal view.
2711
+ *
2712
+ * NOTE: the returned buffer is a rendering substrate only; its length is NOT the
2713
+ * file's byte length. Follow/watch callers must seek from the real file size
2714
+ * (`stat().size`), never from this buffer's length.
2715
+ */
2716
+ async function readLogForRender(logPath, maxBytes = MAX_RENDER_BYTES) {
2717
+ const fh = await open(logPath, "r");
2718
+ try {
2719
+ const { size } = await fh.stat();
2720
+ if (size <= maxBytes) {
2721
+ const data = await fh.readFile();
2722
+ return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
2723
+ }
2724
+ const tmp = Buffer.allocUnsafe(maxBytes);
2725
+ const { bytesRead } = await fh.read(tmp, 0, maxBytes, size - maxBytes);
2726
+ return new Uint8Array(tmp.buffer, tmp.byteOffset, bytesRead);
2727
+ } finally {
2728
+ await fh.close();
2729
+ }
2730
+ }
2731
+ /**
2689
2732
  * Feed the raw PTY bytes through @xterm/headless and emit plain text.
2690
2733
  * Same approach as koho's renderTerminalBuffer + agent-yes's XtermProxy.
2691
2734
  */
@@ -3844,7 +3887,7 @@ async function cmdAttach(rest) {
3844
3887
  const winsizeDir = path.join(ayHome, "winsize");
3845
3888
  await mkdir(winsizeDir, { recursive: true });
3846
3889
  const winsizePath = path.join(winsizeDir, String(record.pid));
3847
- const { resolveDaemonHttpBase, loadTokenReadOnly } = await import("./serve-DQPkrKcN.js");
3890
+ const { resolveDaemonHttpBase, loadTokenReadOnly } = await import("./serve-CrQavO4J.js");
3848
3891
  const daemonBase = await resolveDaemonHttpBase().catch(() => null);
3849
3892
  const daemonToken = daemonBase ? await loadTokenReadOnly().catch(() => null) ?? "" : "";
3850
3893
  const capViewer = `attach:${process.pid}`;
@@ -4554,7 +4597,7 @@ async function cmdNotify(rest) {
4554
4597
  }
4555
4598
  const ensure = async () => {
4556
4599
  if (!argv["ensure-daemon"]) return;
4557
- const { ensureDaemon } = await import("./notifyDaemon-Cvnvfqw8.js");
4600
+ const { ensureDaemon } = await import("./notifyDaemon-BKaX7tHT.js");
4558
4601
  await ensureDaemon().catch(() => null);
4559
4602
  };
4560
4603
  await heartbeatWatcher(parent, selfStartedAt);
@@ -4628,7 +4671,7 @@ async function cmdNotifyCursor(args) {
4628
4671
  }
4629
4672
  async function cmdNotifyd(rest) {
4630
4673
  const sub = rest[0] ?? "status";
4631
- const daemon = await import("./notifyDaemon-Cvnvfqw8.js");
4674
+ const daemon = await import("./notifyDaemon-BKaX7tHT.js");
4632
4675
  switch (sub) {
4633
4676
  case "run": return daemon.runDaemon();
4634
4677
  case "once": return daemon.runDaemon({ once: true });
@@ -4654,5 +4697,5 @@ async function cmdNotifyd(rest) {
4654
4697
  }
4655
4698
 
4656
4699
  //#endregion
4657
- export { shouldStealLock as $, recentReadEdges as A, stdinActivityPath as B, listRecords as C, readNotes as D, readAgentPtysize as E, resolveReadWindow as F, writeToIpc as G, submitAndConfirm as H, resolveResumeArgs as I, hostId as J, appendEvent as K, restartHintLines as L, renderRawLog as M, renderRawLogLines as N, readPtysize as O, resolveOne as P, readInbox as Q, runSubcommand as R, lastStdinAt as S, menuSelectKeys as T, waitForLogQuiet as U, stopTipForCli as V, writeKeysPaced as W, listInboxParents as X, isTransientLockMkdirError as Y, liveWatchers as Z, isPidAlive as _, cmdHelp as a, isUnknownManagerToken as b, deriveLiveState as c, extractMenu as d, daemonLockDir as et, extractNeedsInput as f, isExitRequest as g, isAgentStuck as h, backoffWhileTyping as i, renderLogTailLines as j, recentMessageEdges as k, deriveLiveStatus as l, finalizedLines as m, READ_PAGE_DEFAULT as n, notifyDir as nt, controlCodeFromName as o, extractTaskCounts as p, gcInboxes as q, TYPING_WINDOW_MS as r, TYPING_BADGE as rt, cursorAbs as s, GRACEFUL_EXIT_COMMANDS as t, daemonLockOwnerPath as tt, extractBadges as u, isSlashCommand as v, matchKeyword as w, isUserTyping as x, isSubcommand as y, snapshotStatus as z };
4658
- //# sourceMappingURL=subcommands-B04Jcb4a.js.map
4700
+ export { liveWatchers as $, readPtysize as A, runSubcommand as B, lastStdinAt as C, readAgentPtysize as D, menuSelectKeys as E, renderRawLogLines as F, waitForLogQuiet as G, stdinActivityPath as H, resolveOne as I, appendEvent as J, writeKeysPaced as K, resolveReadWindow as L, recentReadEdges as M, renderLogTailLines as N, readLogForRender as O, renderRawLog as P, listInboxParents as Q, resolveResumeArgs as R, isUserTyping as S, matchKeyword as T, stopTipForCli as U, snapshotStatus as V, submitAndConfirm as W, hostId as X, gcInboxes as Y, isTransientLockMkdirError as Z, isExitRequest as _, backoffWhileTyping as a, TYPING_BADGE as at, isSubcommand as b, cursorAbs as c, extractBadges as d, readInbox as et, extractMenu as f, isAgentStuck as g, finalizedLines as h, TYPING_WINDOW_MS as i, notifyDir as it, recentMessageEdges as j, readNotes as k, deriveLiveState as l, extractTaskCounts as m, MAX_RENDER_BYTES as n, daemonLockDir as nt, cmdHelp as o, extractNeedsInput as p, writeToIpc as q, READ_PAGE_DEFAULT as r, daemonLockOwnerPath as rt, controlCodeFromName as s, GRACEFUL_EXIT_COMMANDS as t, shouldStealLock as tt, deriveLiveStatus as u, isPidAlive as v, listRecords as w, isUnknownManagerToken as x, isSlashCommand as y, restartHintLines as z };
4701
+ //# sourceMappingURL=subcommands-bd-cHNKW.js.map
@@ -98,7 +98,7 @@ async function cmdTermMint(args) {
98
98
  if (flags.ro && flags.interactive) throw new Error("--ro and --interactive are mutually exclusive");
99
99
  const ttlSec = parseTtlSec(typeof flags.ttl === "string" ? flags.ttl : "15m");
100
100
  const canSend = flags.interactive === true;
101
- const { mintScopedTermToken } = await import("./serve-DQPkrKcN.js");
101
+ const { mintScopedTermToken } = await import("./serve-CrQavO4J.js");
102
102
  const r = await mintScopedTermToken(pid, {
103
103
  ttlSec,
104
104
  canSend
@@ -135,4 +135,4 @@ async function cmdTerm(args) {
135
135
 
136
136
  //#endregion
137
137
  export { cmdTerm };
138
- //# sourceMappingURL=terminal-sCD-7D_2.js.map
138
+ //# sourceMappingURL=terminal-eRiT4Zbo.js.map
@@ -1,4 +1,4 @@
1
- import { n as findTrayLauncher } from "./rustBinary-DWzZGM4A.js";
1
+ import { n as findTrayLauncher } from "./rustBinary-BjMf3hJs.js";
2
2
  import { t as agentYesHome } from "./agentYesHome-CtmsY6I-.js";
3
3
  import { existsSync } from "node:fs";
4
4
  import path from "node:path";
@@ -97,4 +97,4 @@ async function cmdTray(args) {
97
97
 
98
98
  //#endregion
99
99
  export { trayHiddenMarker as a, launchTray as i, hasDesktop as n, isTrayHidden as r, cmdTray as t };
100
- //# sourceMappingURL=trayApp-CCN3zyo7.js.map
100
+ //# sourceMappingURL=trayApp-BBNKEQbP.js.map
@@ -0,0 +1,5 @@
1
+ import "./versionChecker-BKFAk759.js";
2
+ import "./rustBinary-BjMf3hJs.js";
3
+ import { a as trayHiddenMarker, i as launchTray, n as hasDesktop, r as isTrayHidden, t as cmdTray } from "./trayApp-BBNKEQbP.js";
4
+
5
+ export { cmdTray };
@@ -1,6 +1,6 @@
1
1
  import { n as __esmMin, r as __exportAll } from "./chunk-cZBX9HZv.js";
2
2
  import { n as logger, t as addTransport } from "./logger-CDIsZ-Pp.js";
3
- import { r as getInstalledPackage } from "./versionChecker-bcw3nJay.js";
3
+ import { r as getInstalledPackage } from "./versionChecker-BKFAk759.js";
4
4
  import { t as agentYesHome } from "./agentYesHome-CtmsY6I-.js";
5
5
  import { d as openStore, p as LIFECYCLES, x as removeControlCharacters } from "./todoAutomation-BQRW614S.js";
6
6
  import { i as shouldUseLock, r as releaseLock, t as acquireLock } from "./runningLock-DBoONoM-.js";
@@ -11,7 +11,7 @@ import { n as register, r as sweep } from "./reaper-C4BvNylH.js";
11
11
  import { arch, platform } from "process";
12
12
  import { execSync } from "child_process";
13
13
  import { closeSync, constants, createReadStream, existsSync, mkdirSync, openSync } from "fs";
14
- import { mkdir, readFile, readdir, unlink, writeFile } from "fs/promises";
14
+ import { mkdir, open, readFile, readdir, stat, unlink, writeFile } from "fs/promises";
15
15
  import { homedir } from "os";
16
16
  import path, { dirname, join } from "path";
17
17
  import { fileURLToPath } from "url";
@@ -1390,6 +1390,28 @@ async function answerAsk(projectRoot, taskId, answer) {
1390
1390
 
1391
1391
  //#endregion
1392
1392
  //#region ts/index.ts
1393
+ const RAW_LOG_KEEP_BYTES = 80 * 1024 * 1024;
1394
+ const RAW_LOG_TRIGGER_BYTES = 160 * 1024 * 1024;
1395
+ /** Shrink a raw log to its trailing `keepBytes` **in place** (rewrite tail→front
1396
+ * + truncate), returning the new length. Deliberately NOT temp-file+rename:
1397
+ * rename swaps the inode, freezing any live follower (`ay serve` `/api/tail`,
1398
+ * incl. the agent-yes.com viewer over WebRTC) whose fd is bound to the old inode.
1399
+ * Rewriting the same inode lets serve's `if (size < offset) offset = size`
1400
+ * "truncated/rotated" guard resume the stream. Mirrors rs/src/log_files.rs. */
1401
+ async function compactRawLogTail(logPath, keepBytes = RAW_LOG_KEEP_BYTES) {
1402
+ const fh = await open(logPath, "r+");
1403
+ try {
1404
+ const { size } = await fh.stat();
1405
+ if (size <= keepBytes) return size;
1406
+ const buf = Buffer.allocUnsafe(keepBytes);
1407
+ const { bytesRead } = await fh.read(buf, 0, keepBytes, size - keepBytes);
1408
+ await fh.write(buf, 0, bytesRead, 0);
1409
+ await fh.truncate(bytesRead);
1410
+ return bytesRead;
1411
+ } finally {
1412
+ await fh.close();
1413
+ }
1414
+ }
1393
1415
  const config = await import("./agent-yes.config-DzNini0E.js").then((mod) => mod.default || mod);
1394
1416
  const CLIS_CONFIG = config.clis;
1395
1417
  /**
@@ -2049,11 +2071,14 @@ async function agentYes({ cli, cliArgs = [], skipPermissions = false, prompt, ro
2049
2071
  }).forkTo(async function rawLogger(f) {
2050
2072
  const rawLogPath = ctx.logPaths.rawLogPath;
2051
2073
  if (!rawLogPath) return f.run();
2052
- return await mkdir(path.dirname(rawLogPath), { recursive: true }).then(() => {
2074
+ return await mkdir(path.dirname(rawLogPath), { recursive: true }).then(async () => {
2053
2075
  logger.debug(`[${cli}-yes] raw logs streaming to ${rawLogPath}`);
2076
+ let rawWritten = await stat(rawLogPath).then((s) => s.size).catch(() => 0);
2054
2077
  return f.forEach(async (chars) => {
2055
2078
  if (!usedAltScreen && /\[\?(?:1049|1047|47)h/.test(chars)) usedAltScreen = true;
2056
2079
  await writeFile(rawLogPath, chars, { flag: "a" }).catch(() => null);
2080
+ rawWritten += Buffer.byteLength(chars);
2081
+ if (rawWritten > RAW_LOG_TRIGGER_BYTES) rawWritten = await compactRawLogTail(rawLogPath).catch(() => rawWritten);
2057
2082
  }).run();
2058
2083
  }).catch(() => f.run());
2059
2084
  }).by(function consoleResponder(e) {
@@ -2155,4 +2180,4 @@ function sleep(ms) {
2155
2180
 
2156
2181
  //#endregion
2157
2182
  export { hasTodoStore as a, permissionBadge as c, answerAsk as i, AgentContext as l, agentYes as n, listAsks as o, config as r, listAsksForProject as s, CLIS_CONFIG as t, CLAUDE_SESSION_PIN_ENV as u };
2158
- //# sourceMappingURL=ts-Ct3yd-1m.js.map
2183
+ //# sourceMappingURL=ts-CvUvWb2n.js.map
@@ -7,7 +7,7 @@ import { fileURLToPath } from "url";
7
7
 
8
8
  //#region package.json
9
9
  var name = "agent-yes";
10
- var version = "1.257.0";
10
+ var version = "1.258.0";
11
11
 
12
12
  //#endregion
13
13
  //#region ts/versionChecker.ts
@@ -215,4 +215,4 @@ async function displayVersion() {
215
215
 
216
216
  //#endregion
217
217
  export { versionString as i, displayVersion as n, getInstalledPackage as r, checkAndAutoUpdate as t };
218
- //# sourceMappingURL=versionChecker-bcw3nJay.js.map
218
+ //# sourceMappingURL=versionChecker-BKFAk759.js.map
@@ -28,7 +28,7 @@ function parseFlags(args, known) {
28
28
  }
29
29
  /** Resolve the local daemon base URL + token (flags override discovery). */
30
30
  async function daemonTarget(flags) {
31
- const { resolveDaemonHttpBase, loadTokenReadOnly } = await import("./serve-DQPkrKcN.js");
31
+ const { resolveDaemonHttpBase, loadTokenReadOnly } = await import("./serve-CrQavO4J.js");
32
32
  const base = typeof flags.base === "string" ? flags.base : await resolveDaemonHttpBase();
33
33
  if (!base) throw new Error("no running ay serve daemon found — start `ay serve` or pass --base <url> (e.g. http://127.0.0.1:PORT)");
34
34
  const token = typeof flags.token === "string" ? flags.token : await loadTokenReadOnly() ?? "";
@@ -186,7 +186,7 @@ async function cmdMint(args) {
186
186
  const bad = caps.filter((c) => !KNOWN_CAPS.has(c));
187
187
  if (bad.length) throw new Error(`unknown cap(s): ${bad.join(", ")} (valid: ${[...KNOWN_CAPS].join(", ")})`);
188
188
  const ttlSec = parseTtlSec(typeof flags.ttl === "string" ? flags.ttl : "15m");
189
- const { mintScopedTermToken } = await import("./serve-DQPkrKcN.js");
189
+ const { mintScopedTermToken } = await import("./serve-CrQavO4J.js");
190
190
  const r = await mintScopedTermToken(target, {
191
191
  ttlSec,
192
192
  caps
@@ -202,4 +202,4 @@ async function cmdMint(args) {
202
202
 
203
203
  //#endregion
204
204
  export { cmdMint, cmdWidget };
205
- //# sourceMappingURL=widget-B7NK6egy.js.map
205
+ //# sourceMappingURL=widget-C1GqFymh.js.map
@@ -3,9 +3,9 @@ import "./globalPidIndex-Dq4KY2IV.js";
3
3
  import "./messageLog-BYFwioFY.js";
4
4
  import "./e2e-CUMZC53I.js";
5
5
  import "./configShared-DtHMBCl7.js";
6
- import "./subcommands-B04Jcb4a.js";
6
+ import "./subcommands-bd-cHNKW.js";
7
7
  import "./webrtcLink-CS4D-3G1.js";
8
8
  import "./remotes-Dy4Rg8Yp.js";
9
- import { a as isPathInside, c as walkWorkspaces, i as importProvisionModule, l as workspaceStatus, n as cmdWs, o as loadProvision, r as collectWorkspaces, s as resolveOperand, t as WS_JSON_SCHEMA } from "./ws-BlISbGY8.js";
9
+ import { a as isPathInside, c as walkWorkspaces, i as importProvisionModule, l as workspaceStatus, n as cmdWs, o as loadProvision, r as collectWorkspaces, s as resolveOperand, t as WS_JSON_SCHEMA } from "./ws-DgHuhAPq.js";
10
10
 
11
11
  export { WS_JSON_SCHEMA, cmdWs, collectWorkspaces, isPathInside, loadProvision, workspaceStatus };
@@ -1,4 +1,4 @@
1
- import { C as listRecords } from "./subcommands-B04Jcb4a.js";
1
+ import { w as listRecords } from "./subcommands-bd-cHNKW.js";
2
2
  import { a as getProvisionRoot } from "./workspaceConfig-Bxuk_MOq.js";
3
3
  import { existsSync } from "fs";
4
4
  import { lstat, opendir } from "fs/promises";
@@ -426,4 +426,4 @@ async function cmdWs(args) {
426
426
 
427
427
  //#endregion
428
428
  export { isPathInside as a, walkWorkspaces as c, importProvisionModule as i, workspaceStatus as l, cmdWs as n, loadProvision as o, collectWorkspaces as r, resolveOperand as s, WS_JSON_SCHEMA as t };
429
- //# sourceMappingURL=ws-BlISbGY8.js.map
429
+ //# sourceMappingURL=ws-DgHuhAPq.js.map
@@ -684,9 +684,12 @@ export function selSegments(sel, myLast, vy, myRows, peerCols, myCols) {
684
684
  const from = Math.max(rA, vy),
685
685
  to = Math.min(rB, vy + myRows - 1);
686
686
  for (let r = from; r <= to; r++) {
687
- const a = r === rA ? cA : 0;
688
- const b = r === rB ? cB : myCols;
689
- segs.push({ row: r, a: Math.min(mapC(a), mapC(b)), b: Math.max(mapC(a), mapC(b)) });
687
+ // cA/cB are the PEER's columns, so they go through mapC; 0 and myCols are the
688
+ // row's own edges, already in OUR space mapping them would rescale our own
689
+ // right edge (and could even invert the span when the peer is wider).
690
+ const a = r === rA ? mapC(cA) : 0;
691
+ const b = r === rB ? mapC(cB) : myCols;
692
+ segs.push({ row: r, a: Math.min(a, b), b: Math.max(a, b) });
690
693
  }
691
694
  return segs;
692
695
  }
package/lab/ui/index.html CHANGED
@@ -1939,6 +1939,9 @@
1939
1939
  overflow: hidden;
1940
1940
  text-overflow: ellipsis;
1941
1941
  }
1942
+ .ctxmenu-item.danger {
1943
+ color: var(--red, #f85149);
1944
+ }
1942
1945
  .ctxmenu-sep {
1943
1946
  height: 1px;
1944
1947
  margin: 4px 6px;
@@ -4875,6 +4878,7 @@ my.translate = async (text, lang) => {
4875
4878
  it.className = "ctxmenu-item";
4876
4879
  it.textContent = label;
4877
4880
  it.setAttribute("role", "menuitem");
4881
+ if (opts && opts.danger) it.classList.add("danger");
4878
4882
  if (opts && opts.hint) {
4879
4883
  const h = document.createElement("span");
4880
4884
  h.className = "ctxmenu-hint";
@@ -4989,17 +4993,35 @@ my.translate = async (text, lang) => {
4989
4993
 
4990
4994
  openCtxMenu(menu, ev);
4991
4995
  }
4992
- // Right-click menu for a left-panel agent row: pin/unpin to the top of the
4993
- // list (a pin also surfaces the agent in /r/'s screen-fixed palette).
4996
+ // Right-click menu for a left-panel agent row. Same agent actions as the
4997
+ // terminal header's overflow menu (share / stop / restart / force-kill),
4998
+ // but bound to the RIGHT-CLICKED row rather than the selected agent — so you
4999
+ // can act on an agent without switching to it first. Plus pin/unpin to the
5000
+ // top of the list (a pin also surfaces the agent in /r/'s screen-fixed
5001
+ // palette). View prefs (font size, perf HUD) stay in the ⋯ menu: they're
5002
+ // properties of the terminal pane, not of an agent.
4994
5003
  function showAgentMenu(ev, key) {
4995
5004
  ev.preventDefault();
4996
5005
  hideTermMenu();
4997
5006
  const e = entries.find((x) => x._key === key);
4998
5007
  if (!e) return;
4999
- const { menu, addItem } = ctxMenuShell();
5008
+ const { menu, addItem, addSep } = ctxMenuShell();
5000
5009
  const name = cliLabel(e) || ident(e) || "pid " + e.pid;
5001
- if (isPinned(key)) addItem("Unpin “" + name + "”", { hint: "📌" }, () => togglePin(key));
5002
- else addItem("Pin “" + name + "” to top", { hint: "📌" }, () => togglePin(key));
5010
+ addItem(name, { disabled: true, hint: "pid " + e.pid }, () => {});
5011
+ addSep();
5012
+ if (isPinned(key)) addItem("Unpin from top", { hint: "📌" }, () => togglePin(key));
5013
+ else addItem("Pin to top", { hint: "📌" }, () => togglePin(key));
5014
+ // A shared read-only view can't re-share or steer its host's agent —
5015
+ // mirrors the .app.steer-agent rule that hides these in the ⋯ menu.
5016
+ if (!isReadonlyEnt(e) && !sharePermOf(e)) {
5017
+ addSep();
5018
+ addItem("👁 Share (view-only)", {}, () => shareAgent("r", e));
5019
+ addItem("✏️ Share (read-write)…", {}, () => shareAgent("rw", e));
5020
+ addSep();
5021
+ addItem("⏹ Stop agent", {}, () => stopAgent(false, e));
5022
+ addItem("↻ Restart (resume)", {}, () => restartAgent(e));
5023
+ addItem("✕ Force-kill (SIGKILL)", { danger: true }, () => stopAgent(true, e));
5024
+ }
5003
5025
  openCtxMenu(menu, ev);
5004
5026
  }
5005
5027
  // Lightweight editor: overlay + textarea of `Label | template` lines. Save
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-yes",
3
- "version": "1.257.0",
3
+ "version": "1.258.0",
4
4
  "description": "A wrapper tool that automates interactions with various AI CLI tools by automatically handling common prompts and responses.",
5
5
  "keywords": [
6
6
  "ai",
package/ts/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { execaCommandSync, parseCommandString } from "execa";
2
2
  import { fromWritable } from "from-node-stream";
3
- import { mkdir, readFile, unlink, writeFile } from "fs/promises";
3
+ import { mkdir, open, readFile, stat, unlink, writeFile } from "fs/promises";
4
4
  import path from "path";
5
5
  import DIE from "phpdie";
6
6
  import sflow from "sflow";
@@ -81,6 +81,37 @@ export type { TodoAction, LiveAgent } from "./todoAutomation.ts";
81
81
  export { listAsks, listAsksForProject, answerAsk, hasTodoStore } from "./askApi.ts";
82
82
  export type { AskItem, AnswerInput, AnswerResult } from "./askApi.ts";
83
83
 
84
+ // Raw PTY logs are an append-only capture; the reader (`readLogForRender`) only
85
+ // ever replays the trailing ~64 MiB, so older bytes are dead weight on disk (a
86
+ // runaway capture was seen at ~1 GB). Once the file passes RAW_LOG_TRIGGER_BYTES,
87
+ // compact it to its last RAW_LOG_KEEP_BYTES (kept above the render window so no
88
+ // visible output is lost). Mirrors the Rust runtime's LogWriter cap in
89
+ // rs/src/log_files.rs — keep the two in sync.
90
+ const RAW_LOG_KEEP_BYTES = 80 * 1024 * 1024;
91
+ const RAW_LOG_TRIGGER_BYTES = 160 * 1024 * 1024;
92
+
93
+ /** Shrink a raw log to its trailing `keepBytes` **in place** (rewrite tail→front
94
+ * + truncate), returning the new length. Deliberately NOT temp-file+rename:
95
+ * rename swaps the inode, freezing any live follower (`ay serve` `/api/tail`,
96
+ * incl. the agent-yes.com viewer over WebRTC) whose fd is bound to the old inode.
97
+ * Rewriting the same inode lets serve's `if (size < offset) offset = size`
98
+ * "truncated/rotated" guard resume the stream. Mirrors rs/src/log_files.rs. */
99
+ async function compactRawLogTail(logPath: string, keepBytes = RAW_LOG_KEEP_BYTES): Promise<number> {
100
+ const fh = await open(logPath, "r+");
101
+ try {
102
+ const { size } = await fh.stat();
103
+ if (size <= keepBytes) return size;
104
+ // Read the tail fully before overwriting the front so it can't be clobbered.
105
+ const buf = Buffer.allocUnsafe(keepBytes);
106
+ const { bytesRead } = await fh.read(buf, 0, keepBytes, size - keepBytes);
107
+ await fh.write(buf, 0, bytesRead, 0);
108
+ await fh.truncate(bytesRead);
109
+ return bytesRead;
110
+ } finally {
111
+ await fh.close();
112
+ }
113
+ }
114
+
84
115
  export type AgentCliConfig = {
85
116
  // cli
86
117
  install?:
@@ -1224,8 +1255,13 @@ export default async function agentYes({
1224
1255
 
1225
1256
  // try stream the raw log for realtime debugging, including control chars, note: it will be a huge file
1226
1257
  return await mkdir(path.dirname(rawLogPath), { recursive: true })
1227
- .then(() => {
1258
+ .then(async () => {
1228
1259
  logger.debug(`[${cli}-yes] raw logs streaming to ${rawLogPath}`);
1260
+ // Track size (seeded from any pre-existing log) to cap disk growth
1261
+ // without a stat() per chunk. See compactRawLogTail.
1262
+ let rawWritten = await stat(rawLogPath)
1263
+ .then((s) => s.size)
1264
+ .catch(() => 0);
1229
1265
  return f
1230
1266
  .forEach(async (chars) => {
1231
1267
  // Detect alt-screen enter (DECSET 1049/1047/47) so we know whether
@@ -1234,6 +1270,10 @@ export default async function agentYes({
1234
1270
  usedAltScreen = true;
1235
1271
  }
1236
1272
  await writeFile(rawLogPath, chars, { flag: "a" }).catch(() => null);
1273
+ rawWritten += Buffer.byteLength(chars);
1274
+ if (rawWritten > RAW_LOG_TRIGGER_BYTES) {
1275
+ rawWritten = await compactRawLogTail(rawLogPath).catch(() => rawWritten);
1276
+ }
1237
1277
  })
1238
1278
  .run();
1239
1279
  })
@@ -113,3 +113,41 @@ test("parseMenu: a 'N.M' version in an option label isn't mistaken for another o
113
113
  expect(menu!.cursor).toBe(1);
114
114
  expect(menu!.options).toEqual([1, 2]);
115
115
  });
116
+
117
+ // Claude renders its permission / AskUserQuestion dialogs INSIDE a rounded box,
118
+ // so every option row arrives with a leading "│" border (the shipped config's own
119
+ // markers show this: a typingRespond keyed on '│ Do you want to use this API key\?'
120
+ // and gemini `enter` rows like "│ ● 1. Yes, allow once"). Option rows must still be
121
+ // collected through that border — otherwise `ay select N` can only ever pick the
122
+ // option the cursor already sits on.
123
+ test("parseMenu: collects options through a box border (claude permission dialog)", () => {
124
+ const screen = [
125
+ "╭──────────────────────────────────────────────────────────╮",
126
+ "│ Do you want to proceed? │",
127
+ "│ ❯ 1. Yes │",
128
+ "│ 2. Yes, and don't ask again for rm commands in /repo │",
129
+ "│ 3. No, and tell Claude what to do differently (esc) │",
130
+ "╰──────────────────────────────────────────────────────────╯",
131
+ ];
132
+ const menu = parseMenu(screen, claude);
133
+ expect(menu!.cursor).toBe(1);
134
+ expect(menu!.options).toEqual([1, 2, 3]);
135
+ });
136
+
137
+ test("parseMenu: collects boxed bullet rows (gemini '│ ● 1. Yes, allow once')", () => {
138
+ const screen = [
139
+ "│ Apply this change? │",
140
+ "│ ❯ 1. Yes, allow once │",
141
+ "│ ● 2. Yes, allow always │",
142
+ "│ ● 3. No (esc) │",
143
+ ];
144
+ const menu = parseMenu(screen, claude);
145
+ expect(menu!.cursor).toBe(1);
146
+ expect(menu!.options).toEqual([1, 2, 3]);
147
+ });
148
+
149
+ test("parseMenu: a boxed 'N.M' version still isn't mistaken for an option", () => {
150
+ const screen = ["│ Cleanup? │", "│ ❯ 1. Delete 3.5GB │", "│ 2. Keep │"];
151
+ const menu = parseMenu(screen, claude);
152
+ expect(menu!.options).toEqual([1, 2]);
153
+ });
package/ts/needsInput.ts CHANGED
@@ -76,9 +76,13 @@ export interface MenuState {
76
76
  question: string;
77
77
  }
78
78
 
79
- // An option row: an optional cursor glyph / bullet, then "N. " (the trailing
80
- // space rejects version-like "3.5GB" that isn't a menu option).
81
- const OPTION_LINE = /^[\s❯›>▶◉○●·*-]*?(\d+)\.\s/;
79
+ // An option row: an optional box border / cursor glyph / bullet, then "N. " (the
80
+ // trailing space rejects version-like "3.5GB" that isn't a menu option). The `│`
81
+ // matters: claude/gemini render their permission + AskUserQuestion menus inside a
82
+ // rounded box, so real option rows arrive as "│ ❯ 1. Yes" — without it only the
83
+ // cursor row is ever collected and `ay select N` can't reach any other option.
84
+ // Keep `-` last in the class so it stays a literal rather than a range.
85
+ const OPTION_LINE = /^[\s❯›>▶◉○●·│*-]*?(\d+)\.\s/;
82
86
 
83
87
  /**
84
88
  * Parse the selection menu a `needs_input` agent is parked on into a cursor
package/ts/serve.ts CHANGED
@@ -26,6 +26,7 @@ import {
26
26
  isUserTyping,
27
27
  listRecords,
28
28
  readNotes,
29
+ readLogForRender,
29
30
  readPtysize,
30
31
  recentMessageEdges,
31
32
  recentReadEdges,
@@ -3090,7 +3091,7 @@ export async function cmdServe(rest: string[]): Promise<number> {
3090
3091
  const record = await resolveOne(keyword, defaultOpts());
3091
3092
  if (!record.log_file)
3092
3093
  return new Response(`pid ${record.pid}: no log_file`, { status: 404 });
3093
- const buf = await readFile(record.log_file);
3094
+ const buf = await readLogForRender(record.log_file);
3094
3095
  const size = await readPtysize(record.pid);
3095
3096
  const text = await renderRawLog(buf, { mode, n, cols: size?.cols, rows: size?.rows });
3096
3097
  return new Response(text, { headers: { "Content-Type": "text/plain; charset=utf-8" } });
@@ -28,6 +28,32 @@ async function loadModule() {
28
28
  return await import("./subcommands.ts");
29
29
  }
30
30
 
31
+ describe("subcommands.readLogForRender", () => {
32
+ it("reads a small file whole (byte-identical)", async () => {
33
+ const { readLogForRender } = await loadModule();
34
+ const p = path.join(testHome, "small.log");
35
+ const body = Buffer.from("hello\nworld\n".repeat(100));
36
+ await writeFile(p, body);
37
+ const got = await readLogForRender(p);
38
+ expect(Buffer.from(got)).toEqual(body);
39
+ });
40
+
41
+ it("caps an oversized file to its trailing window", async () => {
42
+ const { readLogForRender } = await loadModule();
43
+ const p = path.join(testHome, "big.log");
44
+ // 300 KiB of distinct 16-byte lines; cap the read at 64 KiB.
45
+ const lines: string[] = [];
46
+ for (let i = 0; i < 300 * 64; i++) lines.push(String(i).padStart(15, "0"));
47
+ const body = Buffer.from(lines.join("\n") + "\n");
48
+ await writeFile(p, body);
49
+ const cap = 64 * 1024;
50
+ const got = await readLogForRender(p, cap);
51
+ expect(got.byteLength).toBe(cap);
52
+ // The window is the tail: it ends with the file's final bytes.
53
+ expect(Buffer.from(got).subarray(-16)).toEqual(body.subarray(-16));
54
+ });
55
+ });
56
+
31
57
  describe("subcommands.controlCodeFromName", () => {
32
58
  it("maps named codes to the right control bytes", async () => {
33
59
  const { controlCodeFromName } = await loadModule();
package/ts/subcommands.ts CHANGED
@@ -2136,7 +2136,7 @@ async function cmdRead(rest: string[], { mode }: ReadOpts): Promise<number> {
2136
2136
  throw new Error(`pid ${record.pid}: log path is not a file: ${logPath}`);
2137
2137
  }
2138
2138
 
2139
- const buf = await readFile(logPath);
2139
+ const buf = await readLogForRender(logPath);
2140
2140
  const size = await readAgentPtysize(record);
2141
2141
  const notes = await readNotes();
2142
2142
  const noteLabel = notes.get(record.pid);
@@ -2154,7 +2154,12 @@ async function cmdRead(rest: string[], { mode }: ReadOpts): Promise<number> {
2154
2154
  // `ay tail -f` doesn't "expire" past the send window mid-watch.
2155
2155
  const refresh = setInterval(() => void recordRead(reader.key, record.pid), 30_000);
2156
2156
  refresh.unref?.();
2157
- return plain ? followPlainLocal(logPath, buf) : followRawLocal(logPath, buf);
2157
+ // Seed the follow from the log's REAL byte frontier (`stats.size`), not
2158
+ // `buf.length` — `buf` may be a capped tail window, so its length is not the
2159
+ // file offset. `buf` still seeds the terminal render (identical final state).
2160
+ return plain
2161
+ ? followPlainLocal(logPath, buf, stats.size)
2162
+ : followRawLocal(logPath, buf, stats.size);
2158
2163
  }
2159
2164
 
2160
2165
  // Static read: render the full log once, then window into the rendered lines
@@ -2293,7 +2298,11 @@ async function watchAppend(
2293
2298
  * sequences stripped. Mirrors the historical behaviour, plus prompt signal /
2294
2299
  * pipe-close handling.
2295
2300
  */
2296
- async function followRawLocal(logPath: string, buf: Uint8Array): Promise<number> {
2301
+ async function followRawLocal(
2302
+ logPath: string,
2303
+ buf: Uint8Array,
2304
+ startOffset = buf.length,
2305
+ ): Promise<number> {
2297
2306
  process.stderr.write(`following... (Ctrl-C to stop)\n`);
2298
2307
  // oxlint-disable-next-line no-control-regex -- intentional: strip ANSI/control
2299
2308
  const ansiRe = /\x1b\[[0-?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[@-Z\\-_]/g;
@@ -2301,7 +2310,7 @@ async function followRawLocal(logPath: string, buf: Uint8Array): Promise<number>
2301
2310
  const ctrlRe = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g;
2302
2311
  await watchAppend(
2303
2312
  logPath,
2304
- buf.length,
2313
+ startOffset,
2305
2314
  (chunk) => {
2306
2315
  const text = new TextDecoder().decode(chunk).replace(ansiRe, "").replace(ctrlRe, "");
2307
2316
  if (text.trim()) process.stdout.write(text.trimStart());
@@ -2355,7 +2364,11 @@ export function finalizedLines(term: PlainTermView, fromAbs: number): string[] {
2355
2364
  * settled, so the output is clean, newline-terminated, line-buffered text a
2356
2365
  * script can read. On stop, flush the line the cursor is still sitting on.
2357
2366
  */
2358
- async function followPlainLocal(logPath: string, buf: Uint8Array): Promise<number> {
2367
+ async function followPlainLocal(
2368
+ logPath: string,
2369
+ buf: Uint8Array,
2370
+ startOffset = buf.length,
2371
+ ): Promise<number> {
2359
2372
  process.stderr.write(`following... (plain; Ctrl-C / SIGTERM to stop)\n`);
2360
2373
  const { Terminal } = await import("@xterm/headless");
2361
2374
  const term = new Terminal({ cols: 200, rows: 50, scrollback: 50000, allowProposedApi: true });
@@ -2379,7 +2392,7 @@ async function followPlainLocal(logPath: string, buf: Uint8Array): Promise<numbe
2379
2392
 
2380
2393
  await watchAppend(
2381
2394
  logPath,
2382
- buf.length,
2395
+ startOffset,
2383
2396
  async (chunk) => {
2384
2397
  await feed(chunk);
2385
2398
  flushCommitted();
@@ -2440,6 +2453,50 @@ export async function readAgentPtysize(
2440
2453
  return null;
2441
2454
  }
2442
2455
 
2456
+ /**
2457
+ * Cap on how many trailing bytes of a raw PTY log we read before rendering.
2458
+ *
2459
+ * `renderRawLogLines` replays the bytes through an xterm with a fixed 50000-line
2460
+ * scrollback, so output older than the last ~50k lines is evicted from the result
2461
+ * no matter how much we read. A runaway CLI/TUI capture can reach ~1GB; slurping
2462
+ * the whole file only to throw ~all of it away is a large memory + CPU spike (a
2463
+ * ~1GB Buffer plus a full-stream vterm replay per request). 64 MiB comfortably
2464
+ * overflows the scrollback for any realistic terminal stream, so an oversized log
2465
+ * renders to the SAME lines while the allocation stays bounded.
2466
+ */
2467
+ export const MAX_RENDER_BYTES = 64 * 1024 * 1024;
2468
+
2469
+ /**
2470
+ * Read a raw PTY log for full-buffer rendering, capped to the trailing
2471
+ * MAX_RENDER_BYTES. Files at or below the cap are read whole (byte-identical to a
2472
+ * plain readFile); larger files return only their tail window. This is the same
2473
+ * tail-window tradeoff `renderLogTailLines` already makes at 32KB, just larger —
2474
+ * the window's first line may be a partial (garbled) render, but it sits at the
2475
+ * scrollback-eviction boundary, ~50k lines above any tail/head/normal view.
2476
+ *
2477
+ * NOTE: the returned buffer is a rendering substrate only; its length is NOT the
2478
+ * file's byte length. Follow/watch callers must seek from the real file size
2479
+ * (`stat().size`), never from this buffer's length.
2480
+ */
2481
+ export async function readLogForRender(
2482
+ logPath: string,
2483
+ maxBytes = MAX_RENDER_BYTES,
2484
+ ): Promise<Uint8Array> {
2485
+ const fh = await open(logPath, "r");
2486
+ try {
2487
+ const { size } = await fh.stat();
2488
+ if (size <= maxBytes) {
2489
+ const data = await fh.readFile();
2490
+ return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
2491
+ }
2492
+ const tmp = Buffer.allocUnsafe(maxBytes);
2493
+ const { bytesRead } = await fh.read(tmp, 0, maxBytes, size - maxBytes);
2494
+ return new Uint8Array(tmp.buffer, tmp.byteOffset, bytesRead);
2495
+ } finally {
2496
+ await fh.close();
2497
+ }
2498
+ }
2499
+
2443
2500
  /**
2444
2501
  * Feed the raw PTY bytes through @xterm/headless and emit plain text.
2445
2502
  * Same approach as koho's renderTerminalBuffer + agent-yes's XtermProxy.
@@ -65,4 +65,34 @@ describe("parseTaskCounts", () => {
65
65
  expect(parseTaskCounts([])).toBeNull();
66
66
  expect(parseTaskCounts(lines("just some logs\nnothing here"))).toBeNull();
67
67
  });
68
+
69
+ // The CLI branches EVERY tool result under the same ⎿ glyph, not just todo
70
+ // blocks — and ✓ is an ordinary success glyph in program output. So a ⎿ line
71
+ // that carries its own content is a tool-result header, not a todo anchor;
72
+ // only a BARE ⎿ anchors the markers below it.
73
+ it("does not invent a badge from a test run's ✓ output (⎿ tool-result header)", () => {
74
+ const out = parseTaskCounts(
75
+ lines(
76
+ [
77
+ "⏺ Bash(bun run test)",
78
+ " ⎿ RUN v3.2.4 /repo",
79
+ " ✓ ts/badges.spec.ts (18 tests) 12ms",
80
+ " ✓ ts/needsInput.spec.ts (12 tests) 9ms",
81
+ " ✓ ts/autoRetry.spec.ts (7 tests) 4ms",
82
+ ].join("\n"),
83
+ ),
84
+ );
85
+ expect(out).toBeNull();
86
+ });
87
+
88
+ it("does not invent a badge from ✓ prose under a ⎿ result header", () => {
89
+ const out = parseTaskCounts(
90
+ lines(
91
+ [" ⎿ Read 120 lines (ctrl+o to expand)", " ✓ Tests pass", " ✓ Lint clean"].join(
92
+ "\n",
93
+ ),
94
+ ),
95
+ );
96
+ expect(out).toBeNull();
97
+ });
68
98
  });
package/ts/todoParse.ts CHANGED
@@ -51,15 +51,27 @@ function markerOf(line: string): Marker | null {
51
51
  return null;
52
52
  }
53
53
 
54
+ // The TUI branches EVERY tool result under the same `⎿` glyph — "⎿ RUN v3.2.4",
55
+ // "⎿ Read 120 lines (ctrl+o to expand)" — not just todo blocks. Such a header
56
+ // carries its own content after the glyph, whereas a todo anchor on the line above
57
+ // its markers is bare. Requiring bare keeps a Bash run that prints ✓ lines (vitest
58
+ // output!) from self-anchoring into a phantom badge, while the todo shapes — the
59
+ // anchor ON the first marker line ("⎿ ☒ Wire up the parser") or a lone "⎿" above
60
+ // them — still qualify.
61
+ function isBareAnchor(line: string): boolean {
62
+ const s = line.trim();
63
+ return s.startsWith(ANCHOR) && s.slice(ANCHOR.length).trim() === "";
64
+ }
65
+
54
66
  /**
55
67
  * Find the MOST RECENT confidently-detected todo block in the rendered lines and
56
68
  * return its {done, total}. Returns null when none qualifies (caller omits the
57
69
  * badge entirely — never shows "0/0").
58
70
  *
59
71
  * A block is a maximal run of consecutive marker lines. It only counts when it
60
- * is anchored — the `⎿` glyph appears on the run's first line or the line
61
- * directly above it — and has ≥2 marker lines. The last qualifying block wins,
62
- * since the agent's current todo state is the one drawn most recently.
72
+ * is anchored — the `⎿` glyph sits on the run's FIRST line, or a bare `⎿` sits on
73
+ * the line directly above it — and has ≥2 marker lines. The last qualifying block
74
+ * wins, since the agent's current todo state is the one drawn most recently.
63
75
  */
64
76
  export function parseTaskCounts(lines: string[]): TaskCounts | null {
65
77
  let best: TaskCounts | null = null;
@@ -71,13 +83,15 @@ export function parseTaskCounts(lines: string[]): TaskCounts | null {
71
83
  continue;
72
84
  }
73
85
  // Start of a marker run at i.
74
- let hasAnchor = i > 0 && lines[i - 1]!.includes(ANCHOR);
86
+ let hasAnchor = i > 0 && isBareAnchor(lines[i - 1]!);
75
87
  const counts = { done: 0, inprogress: 0, pending: 0 };
76
88
  let j = i;
77
89
  for (; j < n; j++) {
78
90
  const mk = markerOf(lines[j]!);
79
91
  if (mk === null) break;
80
- if (lines[j]!.includes(ANCHOR)) hasAnchor = true;
92
+ // Only the run's FIRST line can carry the anchor (as the doc says); a that
93
+ // turns up mid-run is not an anchor for the run that already started above it.
94
+ if (j === i && lines[j]!.includes(ANCHOR)) hasAnchor = true;
81
95
  counts[mk]++;
82
96
  }
83
97
  const total = counts.done + counts.inprogress + counts.pending;
@@ -1,10 +0,0 @@
1
- import "./logger-CDIsZ-Pp.js";
2
- import "./globalPidIndex-Dq4KY2IV.js";
3
- import "./messageLog-BYFwioFY.js";
4
- import "./e2e-CUMZC53I.js";
5
- import "./configShared-DtHMBCl7.js";
6
- import { A as recentReadEdges, B as stdinActivityPath, C as listRecords, D as readNotes, E as readAgentPtysize, F as resolveReadWindow, G as writeToIpc, H as submitAndConfirm, I as resolveResumeArgs, L as restartHintLines, M as renderRawLog, N as renderRawLogLines, O as readPtysize, P as resolveOne, R as runSubcommand, S as lastStdinAt, T as menuSelectKeys, U as waitForLogQuiet, V as stopTipForCli, W as writeKeysPaced, _ as isPidAlive, a as cmdHelp, b as isUnknownManagerToken, c as deriveLiveState, d as extractMenu, f as extractNeedsInput, g as isExitRequest, h as isAgentStuck, i as backoffWhileTyping, j as renderLogTailLines, k as recentMessageEdges, l as deriveLiveStatus, m as finalizedLines, n as READ_PAGE_DEFAULT, o as controlCodeFromName, p as extractTaskCounts, r as TYPING_WINDOW_MS, s as cursorAbs, t as GRACEFUL_EXIT_COMMANDS, u as extractBadges, v as isSlashCommand, w as matchKeyword, x as isUserTyping, y as isSubcommand, z as snapshotStatus } from "./subcommands-B04Jcb4a.js";
7
- import "./webrtcLink-CS4D-3G1.js";
8
- import "./remotes-Dy4Rg8Yp.js";
9
-
10
- export { cmdHelp, isSubcommand, isUnknownManagerToken, runSubcommand };
@@ -1,5 +0,0 @@
1
- import "./versionChecker-bcw3nJay.js";
2
- import "./rustBinary-DWzZGM4A.js";
3
- import { a as trayHiddenMarker, i as launchTray, n as hasDesktop, r as isTrayHidden, t as cmdTray } from "./trayApp-CCN3zyo7.js";
4
-
5
- export { cmdTray };