aicodeman 1.14.2 → 1.16.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 (190) hide show
  1. package/README.zh-CN.md +91 -21
  2. package/dist/cli.d.ts +32 -0
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +19 -8
  5. package/dist/cli.js.map +1 -1
  6. package/dist/hooks-config.d.ts +23 -2
  7. package/dist/hooks-config.d.ts.map +1 -1
  8. package/dist/hooks-config.js +118 -52
  9. package/dist/hooks-config.js.map +1 -1
  10. package/dist/mux-interface.d.ts +10 -0
  11. package/dist/mux-interface.d.ts.map +1 -1
  12. package/dist/respawn-patterns.d.ts.map +1 -1
  13. package/dist/respawn-patterns.js +7 -2
  14. package/dist/respawn-patterns.js.map +1 -1
  15. package/dist/session-activity.d.ts +73 -0
  16. package/dist/session-activity.d.ts.map +1 -0
  17. package/dist/session-activity.js +74 -0
  18. package/dist/session-activity.js.map +1 -0
  19. package/dist/session-cli-builder.d.ts +33 -1
  20. package/dist/session-cli-builder.d.ts.map +1 -1
  21. package/dist/session-cli-builder.js +51 -1
  22. package/dist/session-cli-builder.js.map +1 -1
  23. package/dist/session-trust-dialog.d.ts +62 -0
  24. package/dist/session-trust-dialog.d.ts.map +1 -0
  25. package/dist/session-trust-dialog.js +83 -0
  26. package/dist/session-trust-dialog.js.map +1 -0
  27. package/dist/session.d.ts +67 -0
  28. package/dist/session.d.ts.map +1 -1
  29. package/dist/session.js +199 -60
  30. package/dist/session.js.map +1 -1
  31. package/dist/tmux-manager.d.ts +18 -0
  32. package/dist/tmux-manager.d.ts.map +1 -1
  33. package/dist/tmux-manager.js +48 -6
  34. package/dist/tmux-manager.js.map +1 -1
  35. package/dist/types/api.d.ts +1 -1
  36. package/dist/types/api.d.ts.map +1 -1
  37. package/dist/types/api.js.map +1 -1
  38. package/dist/utils/index.d.ts +1 -1
  39. package/dist/utils/index.d.ts.map +1 -1
  40. package/dist/utils/index.js +1 -1
  41. package/dist/utils/index.js.map +1 -1
  42. package/dist/utils/regex-patterns.d.ts +17 -0
  43. package/dist/utils/regex-patterns.d.ts.map +1 -1
  44. package/dist/utils/regex-patterns.js +17 -0
  45. package/dist/utils/regex-patterns.js.map +1 -1
  46. package/dist/web/approval-inbox.d.ts +159 -0
  47. package/dist/web/approval-inbox.d.ts.map +1 -0
  48. package/dist/web/approval-inbox.js +322 -0
  49. package/dist/web/approval-inbox.js.map +1 -0
  50. package/dist/web/public/admin-ui.js.gz +0 -0
  51. package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
  52. package/dist/web/public/app.c8e6e44e.js +36 -0
  53. package/dist/web/public/app.c8e6e44e.js.br +0 -0
  54. package/dist/web/public/app.c8e6e44e.js.gz +0 -0
  55. package/dist/web/public/approvals-ui.js +242 -0
  56. package/dist/web/public/approvals-ui.js.br +0 -0
  57. package/dist/web/public/approvals-ui.js.gz +0 -0
  58. package/dist/web/public/{constants.e7175b95.js → constants.7d0f4047.js} +86 -0
  59. package/dist/web/public/constants.7d0f4047.js.br +0 -0
  60. package/dist/web/public/constants.7d0f4047.js.gz +0 -0
  61. package/dist/web/public/cron-ui.js.gz +0 -0
  62. package/dist/web/public/entrance-animations.js.gz +0 -0
  63. package/dist/web/public/i18n.75cf29d4.js +1 -0
  64. package/dist/web/public/i18n.75cf29d4.js.br +0 -0
  65. package/dist/web/public/i18n.75cf29d4.js.gz +0 -0
  66. package/dist/web/public/image-input.ee16ad88.js.gz +0 -0
  67. package/dist/web/public/index.html +81 -12
  68. package/dist/web/public/index.html.br +0 -0
  69. package/dist/web/public/index.html.gz +0 -0
  70. package/dist/web/public/input-cjk.63794d0b.js.gz +0 -0
  71. package/dist/web/public/{keyboard-accessory.9936227d.js → keyboard-accessory.d6c3ad11.js} +50 -0
  72. package/dist/web/public/keyboard-accessory.d6c3ad11.js.br +0 -0
  73. package/dist/web/public/keyboard-accessory.d6c3ad11.js.gz +0 -0
  74. package/dist/web/public/mobile-handlers.32fdd57f.js.gz +0 -0
  75. package/dist/web/public/mobile-overview.js +49 -0
  76. package/dist/web/public/mobile-overview.js.br +0 -0
  77. package/dist/web/public/mobile-overview.js.gz +0 -0
  78. package/dist/web/public/mobile.8a2651ba.css +1 -0
  79. package/dist/web/public/mobile.8a2651ba.css.br +0 -0
  80. package/dist/web/public/mobile.8a2651ba.css.gz +0 -0
  81. package/dist/web/public/notification-manager.5d229063.js.gz +0 -0
  82. package/dist/web/public/orchestrator-panel.js.gz +0 -0
  83. package/dist/web/public/{panels-ui.1d4203e0.js → panels-ui.3df9cd95.js} +5 -5
  84. package/dist/web/public/panels-ui.3df9cd95.js.br +0 -0
  85. package/dist/web/public/panels-ui.3df9cd95.js.gz +0 -0
  86. package/dist/web/public/ralph-panel.6de2d0f8.js.gz +0 -0
  87. package/dist/web/public/ralph-wizard.13a1831e.js.gz +0 -0
  88. package/dist/web/public/respawn-ui.ff0dae4c.js.gz +0 -0
  89. package/dist/web/public/sanitize-html.bc7078d6.js.gz +0 -0
  90. package/dist/web/public/session-ui.1356a4f4.js.gz +0 -0
  91. package/dist/web/public/{settings-ui.b41d33a5.js → settings-ui.20f9c723.js} +10 -10
  92. package/dist/web/public/settings-ui.20f9c723.js.br +0 -0
  93. package/dist/web/public/settings-ui.20f9c723.js.gz +0 -0
  94. package/dist/web/public/{styles.c4e58e13.css → styles.e33fc754.css} +1 -1
  95. package/dist/web/public/styles.e33fc754.css.br +0 -0
  96. package/dist/web/public/styles.e33fc754.css.gz +0 -0
  97. package/dist/web/public/subagent-windows.4cc8b005.js.gz +0 -0
  98. package/dist/web/public/sw.js +45 -20
  99. package/dist/web/public/sw.js.br +0 -0
  100. package/dist/web/public/sw.js.gz +0 -0
  101. package/dist/web/public/terminal-ui.430395be.js +4 -0
  102. package/dist/web/public/terminal-ui.430395be.js.br +0 -0
  103. package/dist/web/public/terminal-ui.430395be.js.gz +0 -0
  104. package/dist/web/public/ultracode-panel.js.gz +0 -0
  105. package/dist/web/public/ultracode-windows.js.gz +0 -0
  106. package/dist/web/public/upload.html.gz +0 -0
  107. package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
  108. package/dist/web/public/vendor/marked.min.js.gz +0 -0
  109. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  110. package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
  111. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  112. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  113. package/dist/web/public/vendor/xterm-predictive-echo.6cec0fba.js +4 -0
  114. package/dist/web/public/vendor/xterm-predictive-echo.6cec0fba.js.br +0 -0
  115. package/dist/web/public/vendor/xterm-predictive-echo.6cec0fba.js.gz +0 -0
  116. package/dist/web/public/vendor/xterm-zerolag-input.137ad9f0.js.gz +0 -0
  117. package/dist/web/public/vendor/xterm.css.gz +0 -0
  118. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  119. package/dist/web/public/{voice-input.085e9e73.js → voice-input.e5461859.js} +3 -0
  120. package/dist/web/public/voice-input.e5461859.js.br +0 -0
  121. package/dist/web/public/{voice-input.085e9e73.js.gz → voice-input.e5461859.js.gz} +0 -0
  122. package/dist/web/public/webview-tabs.js.gz +0 -0
  123. package/dist/web/route-helpers.d.ts.map +1 -1
  124. package/dist/web/route-helpers.js +10 -0
  125. package/dist/web/route-helpers.js.map +1 -1
  126. package/dist/web/routes/approval-routes.d.ts +19 -0
  127. package/dist/web/routes/approval-routes.d.ts.map +1 -0
  128. package/dist/web/routes/approval-routes.js +119 -0
  129. package/dist/web/routes/approval-routes.js.map +1 -0
  130. package/dist/web/routes/file-routes.d.ts.map +1 -1
  131. package/dist/web/routes/file-routes.js +24 -8
  132. package/dist/web/routes/file-routes.js.map +1 -1
  133. package/dist/web/routes/hook-event-routes.d.ts +3 -0
  134. package/dist/web/routes/hook-event-routes.d.ts.map +1 -1
  135. package/dist/web/routes/hook-event-routes.js +61 -3
  136. package/dist/web/routes/hook-event-routes.js.map +1 -1
  137. package/dist/web/routes/index.d.ts +1 -0
  138. package/dist/web/routes/index.d.ts.map +1 -1
  139. package/dist/web/routes/index.js +1 -0
  140. package/dist/web/routes/index.js.map +1 -1
  141. package/dist/web/routes/session-routes.d.ts.map +1 -1
  142. package/dist/web/routes/session-routes.js +30 -2
  143. package/dist/web/routes/session-routes.js.map +1 -1
  144. package/dist/web/schemas.d.ts +26 -0
  145. package/dist/web/schemas.d.ts.map +1 -1
  146. package/dist/web/schemas.js +40 -1
  147. package/dist/web/schemas.js.map +1 -1
  148. package/dist/web/sensitive-path.d.ts +12 -0
  149. package/dist/web/sensitive-path.d.ts.map +1 -1
  150. package/dist/web/sensitive-path.js +49 -4
  151. package/dist/web/sensitive-path.js.map +1 -1
  152. package/dist/web/server.d.ts.map +1 -1
  153. package/dist/web/server.js +31 -3
  154. package/dist/web/server.js.map +1 -1
  155. package/dist/web/session-listener-wiring.d.ts.map +1 -1
  156. package/dist/web/session-listener-wiring.js +9 -0
  157. package/dist/web/session-listener-wiring.js.map +1 -1
  158. package/dist/web/sse-events.d.ts +18 -2
  159. package/dist/web/sse-events.d.ts.map +1 -1
  160. package/dist/web/sse-events.js +20 -2
  161. package/dist/web/sse-events.js.map +1 -1
  162. package/package.json +3 -1
  163. package/scripts/postinstall.js +29 -0
  164. package/skills/codeman/SKILL.md +109 -15
  165. package/skills/codeman/reference/endpoints.md +66 -8
  166. package/skills/codeman/reference/messaging.md +216 -0
  167. package/skills/codeman/reference/recipes.md +59 -5
  168. package/dist/web/public/app.94825d85.js +0 -36
  169. package/dist/web/public/app.94825d85.js.br +0 -0
  170. package/dist/web/public/app.94825d85.js.gz +0 -0
  171. package/dist/web/public/constants.e7175b95.js.br +0 -0
  172. package/dist/web/public/constants.e7175b95.js.gz +0 -0
  173. package/dist/web/public/i18n.da34cfaf.js +0 -1
  174. package/dist/web/public/i18n.da34cfaf.js.br +0 -0
  175. package/dist/web/public/i18n.da34cfaf.js.gz +0 -0
  176. package/dist/web/public/keyboard-accessory.9936227d.js.br +0 -0
  177. package/dist/web/public/keyboard-accessory.9936227d.js.gz +0 -0
  178. package/dist/web/public/mobile.32cacde5.css +0 -1
  179. package/dist/web/public/mobile.32cacde5.css.br +0 -0
  180. package/dist/web/public/mobile.32cacde5.css.gz +0 -0
  181. package/dist/web/public/panels-ui.1d4203e0.js.br +0 -0
  182. package/dist/web/public/panels-ui.1d4203e0.js.gz +0 -0
  183. package/dist/web/public/settings-ui.b41d33a5.js.br +0 -0
  184. package/dist/web/public/settings-ui.b41d33a5.js.gz +0 -0
  185. package/dist/web/public/styles.c4e58e13.css.br +0 -0
  186. package/dist/web/public/styles.c4e58e13.css.gz +0 -0
  187. package/dist/web/public/terminal-ui.bc6819a9.js +0 -4
  188. package/dist/web/public/terminal-ui.bc6819a9.js.br +0 -0
  189. package/dist/web/public/terminal-ui.bc6819a9.js.gz +0 -0
  190. package/dist/web/public/voice-input.085e9e73.js.br +0 -0
package/dist/session.js CHANGED
@@ -37,7 +37,9 @@ import { probeRemoteCliVersion } from './remote-hosts.js';
37
37
  import { TaskTracker } from './task-tracker.js';
38
38
  import { RalphTracker } from './ralph-tracker.js';
39
39
  import { BashToolParser } from './bash-tool-parser.js';
40
- import { BufferAccumulator, ANSI_ESCAPE_PATTERN_FULL, TOKEN_PATTERN, SPINNER_PATTERN, MAX_SESSION_TOKENS, execPattern, getClaudeCliVersion, getClaudeBinaryPath, spawnPtyWithHelperRepair, resolveLocalShell, } from './utils/index.js';
40
+ import { isTrustDialogScreen, TRUST_DIALOG_WINDOW_MS, TRUST_DIALOG_RETRY_MS, TRUST_DIALOG_MAX_ATTEMPTS, TRUST_DIALOG_SCAN_BYTES, } from './session-trust-dialog.js';
41
+ import { trackActivityStreak, isSustainedActivity, isPaneQuiet, IDLE_RECHECK_MS, PANE_PROBE_MIN_INTERVAL_MS, PANE_PROBE_RECHECK_MS, } from './session-activity.js';
42
+ import { BufferAccumulator, ANSI_ESCAPE_PATTERN_FULL, TOKEN_PATTERN, SPINNER_PATTERN, CLAUDE_WORKING_LINE_PATTERN, MAX_SESSION_TOKENS, execPattern, getClaudeCliVersion, getClaudeBinaryPath, spawnPtyWithHelperRepair, resolveLocalShell, } from './utils/index.js';
41
43
  import { MAX_TERMINAL_BUFFER_SIZE, TRIM_TERMINAL_TO as TERMINAL_BUFFER_TRIM_SIZE, MAX_TEXT_OUTPUT_SIZE, TRIM_TEXT_TO as TEXT_OUTPUT_TRIM_SIZE, MAX_MESSAGES, MAX_LINE_BUFFER_SIZE, } from './config/buffer-limits.js';
42
44
  import { DEFAULT_TMUX_HISTORY_LIMIT } from './config/terminal-history.js';
43
45
  import { EXEC_TIMEOUT_MS } from './config/exec-timeout.js';
@@ -267,7 +269,13 @@ export class Session extends EventEmitter {
267
269
  _lastPromptTime = 0;
268
270
  activityTimeout = null;
269
271
  _awaitingIdleConfirmation = false; // Prevents timeout reset during idle detection
270
- _trustDialogAccepted = false; // Prevents repeated trust dialog auto-accept
272
+ _activityStreak = null; // Unbroken run of PTY repaints (working detection)
273
+ _lastPaneProbeAt = 0; // Throttle for the tmux screen probe
274
+ _lastPaneProbeWorking = null; // Its last verdict (null = could not read)
275
+ _trustDialogAccepted = false; // Stops the trust-dialog scan (answered, or given up)
276
+ _trustDialogAttempts = 0; // Enter presses sent at the trust dialog
277
+ _lastTrustDialogScanAt = 0; // Throttle for the trust-dialog screen read
278
+ _interactiveStartedAt = 0; // When the interactive pane launched (bounds that scan)
271
279
  _taskTracker;
272
280
  // Token tracking for auto-clear
273
281
  _totalInputTokens = 0;
@@ -1103,6 +1111,7 @@ export class Session extends EventEmitter {
1103
1111
  sessionId: this.id,
1104
1112
  workingDir: this.workingDir,
1105
1113
  mode: this.mode,
1114
+ name: this._name,
1106
1115
  niceConfig: this._niceConfig,
1107
1116
  model: this._model,
1108
1117
  claudeMode: this._claudeMode,
@@ -1209,6 +1218,11 @@ export class Session extends EventEmitter {
1209
1218
  if (this.ptyProcess) {
1210
1219
  throw new Error('Session already has a running process');
1211
1220
  }
1221
+ // Bounds the workspace-trust scan (see _maybeAcceptTrustDialog). Stamped here
1222
+ // rather than at PTY spawn so a slow mux attach still counts as startup.
1223
+ this._interactiveStartedAt = Date.now();
1224
+ this._trustDialogAttempts = 0;
1225
+ this._lastTrustDialogScanAt = 0;
1212
1226
  // COD-118: if the PTY exit breaker has tripped (repeated non-zero exits in a
1213
1227
  // short window), refuse to respawn. This is the uniform choke point that stops
1214
1228
  // automatic recovery/reconnect callers from re-creating a crash-looping PTY.
@@ -1399,7 +1413,7 @@ export class Session extends EventEmitter {
1399
1413
  try {
1400
1414
  // Pass --session-id to use the SAME ID as the Codeman session
1401
1415
  // This ensures subagents can be directly matched to the correct tab
1402
- const args = buildInteractiveArgs(this.id, this._claudeMode, this._model, this._allowedTools, this._effort);
1416
+ const args = buildInteractiveArgs(this.id, this._claudeMode, this._model, this._allowedTools, this._effort, this._name, getClaudeCliVersion());
1403
1417
  this.ptyProcess = spawnPtyWithHelperRepair(() => pty.spawn(getClaudeBinaryPath(), args, {
1404
1418
  name: 'xterm-256color',
1405
1419
  cols: 120,
@@ -1427,54 +1441,9 @@ export class Session extends EventEmitter {
1427
1441
  return; // Skip if only filtered sequences
1428
1442
  this._handleTerminalOutput(data);
1429
1443
  // === Auto-accept workspace trust dialog ===
1430
- // Claude CLI 2.x shows "Yes, I trust this folder" prompt on first launch per directory.
1431
- // Codeman sessions run permission-skipping or classifier-guarded (auto) modes, so auto-accept.
1432
- if (!this._trustDialogAccepted && data.includes('trust this folder')) {
1433
- this._trustDialogAccepted = true;
1434
- console.log(`[Session] Auto-accepting workspace trust dialog for: ${this.id}`);
1435
- // Send Enter to accept the default selection ("Yes, I trust this folder")
1436
- this.writeViaMux('\r');
1437
- }
1444
+ this._maybeAcceptTrustDialog();
1438
1445
  // === Idle/working detection runs on every chunk (latency-sensitive) ===
1439
- // Detect if Claude is working or at prompt
1440
- // The prompt line contains "❯" when waiting for input
1441
- if (data.includes('❯') || data.includes('\u276f')) {
1442
- // Only start a new timeout if we're not already awaiting idle confirmation
1443
- // This prevents status bar redraws (which include ❯) from resetting the timer
1444
- if (!this._awaitingIdleConfirmation) {
1445
- if (this.activityTimeout)
1446
- clearTimeout(this.activityTimeout);
1447
- this._awaitingIdleConfirmation = true;
1448
- this.activityTimeout = setTimeout(() => {
1449
- this._awaitingIdleConfirmation = false;
1450
- // Emit idle if either:
1451
- // 1. Claude was working and is now at prompt (normal case)
1452
- // 2. Session just started and is ready (status is 'busy' but _isWorking is false)
1453
- const wasWorking = this._isWorking;
1454
- const isInitialReady = this._status === 'busy' && !this._isWorking;
1455
- if (wasWorking || isInitialReady) {
1456
- this._isWorking = false;
1457
- this._status = 'idle';
1458
- this._lastPromptTime = Date.now();
1459
- this.emit('idle');
1460
- }
1461
- }, IDLE_DETECTION_DELAY_MS);
1462
- }
1463
- }
1464
- // Detect when Claude starts working (thinking, writing, etc)
1465
- // Fast path: check spinner characters on raw data (Unicode, never in ANSI sequences)
1466
- const hasSpinner = SPINNER_PATTERN.test(data);
1467
- if (hasSpinner) {
1468
- if (!this._isWorking) {
1469
- this._isWorking = true;
1470
- this._status = 'busy';
1471
- this.emit('working');
1472
- this._autoOps.notifyWorking();
1473
- }
1474
- this._awaitingIdleConfirmation = false;
1475
- if (this.activityTimeout)
1476
- clearTimeout(this.activityTimeout);
1477
- }
1446
+ this._detectInteractiveActivity(data);
1478
1447
  // === Expensive processing (ANSI strip, Ralph, bash parser) is throttled ===
1479
1448
  // Instead of running regex-heavy parsers on every PTY chunk, we accumulate
1480
1449
  // raw data and process at most every EXPENSIVE_PROCESS_INTERVAL_MS.
@@ -1522,6 +1491,7 @@ export class Session extends EventEmitter {
1522
1491
  this._pid = null;
1523
1492
  this._status = 'idle';
1524
1493
  this._awaitingIdleConfirmation = false;
1494
+ this._activityStreak = null;
1525
1495
  // Clear all timers to prevent memory leaks
1526
1496
  if (this.activityTimeout) {
1527
1497
  clearTimeout(this.activityTimeout);
@@ -1572,6 +1542,176 @@ export class Session extends EventEmitter {
1572
1542
  get respawnBlocked() {
1573
1543
  return this._respawnBlocked;
1574
1544
  }
1545
+ /**
1546
+ * Answer Claude's workspace-trust dialog, which blocks a fresh case until
1547
+ * someone presses Enter. Codeman sessions run permission-skipping or
1548
+ * classifier-guarded modes, so the answer is always "yes, I trust this folder".
1549
+ *
1550
+ * Reads the RENDERED SCREEN rather than the chunk that just arrived. tmux
1551
+ * repaints a row with cursor-forward escapes in place of spaces, so the wire
1552
+ * carries `I\x1b[Ctrust\x1b[Cthis\x1b[Cfolder` and the old
1553
+ * `data.includes('trust this folder')` could never match: the auto-accept had
1554
+ * been dead for every session that hit the dialog. The screen is also what
1555
+ * makes a retry safe, since the terminal buffer is append-only and keeps the
1556
+ * dialog in its tail long after it has been answered.
1557
+ *
1558
+ * Three guards keep an Enter press off a live session: a startup-only window,
1559
+ * a two-marker match (isTrustDialogScreen), and an attempt cap.
1560
+ */
1561
+ _maybeAcceptTrustDialog() {
1562
+ if (this._trustDialogAccepted)
1563
+ return;
1564
+ const now = Date.now();
1565
+ if (now - this._interactiveStartedAt > TRUST_DIALOG_WINDOW_MS) {
1566
+ this._trustDialogAccepted = true; // window closed; anything matching now is not the dialog
1567
+ return;
1568
+ }
1569
+ if (now - this._lastTrustDialogScanAt < TRUST_DIALOG_RETRY_MS)
1570
+ return;
1571
+ this._lastTrustDialogScanAt = now;
1572
+ // Prefer the pane; fall back to the buffer tail on a direct-PTY session,
1573
+ // where there is no screen to read.
1574
+ const screen = (this._mux && this._muxSession ? this._mux.capturePaneText?.(this._muxSession.muxName) : null) ??
1575
+ this._terminalBuffer.value.slice(-TRUST_DIALOG_SCAN_BYTES);
1576
+ if (!isTrustDialogScreen(screen))
1577
+ return;
1578
+ this._trustDialogAttempts++;
1579
+ if (this._trustDialogAttempts > TRUST_DIALOG_MAX_ATTEMPTS) {
1580
+ this._trustDialogAccepted = true; // leave it to the user rather than keep typing
1581
+ console.warn(`[Session] Workspace trust dialog did not clear after retries: ${this.id}`);
1582
+ return;
1583
+ }
1584
+ console.log(`[Session] Auto-accepting workspace trust dialog for: ${this.id} (attempt ${this._trustDialogAttempts})`);
1585
+ // Enter confirms the highlighted default, "1. Yes, I trust this folder".
1586
+ this.writeViaMux('\r');
1587
+ }
1588
+ /**
1589
+ * Per-chunk working/idle detection for an interactive pane. Split out of the
1590
+ * PTY `onData` handler so it can be unit tested without spawning one.
1591
+ *
1592
+ * @param data raw PTY chunk, ANSI included
1593
+ */
1594
+ _detectInteractiveActivity(data) {
1595
+ // The prompt line contains "❯" when Claude is waiting for input. It only ARMS
1596
+ // the check and is NOT evidence the turn ended: Claude redraws the composer
1597
+ // about once a second all the way through a turn, which is exactly how a
1598
+ // working session used to flip to idle two seconds in. _confirmIdle() waits
1599
+ // for the pane to actually go quiet before believing it.
1600
+ if (data.includes('❯')) {
1601
+ // Only start a new timeout if we're not already awaiting idle confirmation.
1602
+ // This prevents status bar redraws (which include the prompt) from resetting it.
1603
+ if (!this._awaitingIdleConfirmation) {
1604
+ if (this.activityTimeout)
1605
+ clearTimeout(this.activityTimeout);
1606
+ this._awaitingIdleConfirmation = true;
1607
+ this.activityTimeout = setTimeout(() => this._confirmIdle(), IDLE_DETECTION_DELAY_MS);
1608
+ }
1609
+ }
1610
+ // Detect when Claude starts working (thinking, writing, etc).
1611
+ // Fast path: spinner characters on raw data (Unicode, never inside ANSI sequences).
1612
+ if (SPINNER_PATTERN.test(data))
1613
+ this._markWorking();
1614
+ // Activity fallback: current Claude Code animates `✻ Actualizing…` instead of a
1615
+ // braille spinner, so the fast path above misses entire turns, and matching the
1616
+ // new status line does not rescue it either (tmux repaints partially, so the
1617
+ // complete line reaches the PTY only every few tens of seconds). An unbroken run
1618
+ // of repaints is the signal that survives. See session-activity.ts for the
1619
+ // measurement. Claude only: an external CLI's TUI has no ❯, so nothing would
1620
+ // ever arm the idle confirmation and such a session would latch busy forever.
1621
+ if (!isExternalCliMode(this.mode)) {
1622
+ this._activityStreak = trackActivityStreak(this._activityStreak, Date.now());
1623
+ // A streak is the TRIGGER to look, not the verdict: typing into the composer
1624
+ // also produces a steady stream of repaints. The screen settles it, and only
1625
+ // an explicit "no working line" vetoes; a probe that cannot read the pane
1626
+ // (null) leaves the streak in charge.
1627
+ if (!this._isWorking && isSustainedActivity(this._activityStreak) && this._probePaneWorking() !== false) {
1628
+ this._markWorking();
1629
+ }
1630
+ }
1631
+ }
1632
+ /**
1633
+ * Ask the pane what it is rendering right now.
1634
+ *
1635
+ * The PTY stream cannot answer this on its own: measured on a live worker,
1636
+ * Claude repaints roughly once a second for most of a turn but can then sit
1637
+ * completely silent for tens of seconds inside a single tool call, while the
1638
+ * `✻ Elucidating… (39s · ↓ 2.0k tokens)` line stays on screen the whole time.
1639
+ * Silence therefore proves nothing, and the rendered frame is the only cheap
1640
+ * source that is right in both directions.
1641
+ *
1642
+ * Costs one `capture-pane`, floored at PANE_PROBE_MIN_INTERVAL_MS per session
1643
+ * and only ever called at a transition, never on the output hot path.
1644
+ *
1645
+ * @returns true/false when the screen could be read, null when it could not
1646
+ * (no mux, capture failed, tests). Callers must treat null as "no evidence"
1647
+ * and fall back to their stream heuristics.
1648
+ */
1649
+ _probePaneWorking() {
1650
+ if (!this._mux || !this._muxSession)
1651
+ return null;
1652
+ const now = Date.now();
1653
+ if (now - this._lastPaneProbeAt < PANE_PROBE_MIN_INTERVAL_MS)
1654
+ return this._lastPaneProbeWorking;
1655
+ this._lastPaneProbeAt = now;
1656
+ const text = this._mux.capturePaneText?.(this._muxSession.muxName) ?? null;
1657
+ this._lastPaneProbeWorking = text === null ? null : CLAUDE_WORKING_LINE_PATTERN.test(text);
1658
+ return this._lastPaneProbeWorking;
1659
+ }
1660
+ /**
1661
+ * Mark the pane as working. Idempotent: `working` is emitted on the transition
1662
+ * only, so the per-chunk detectors can all call it freely.
1663
+ *
1664
+ * Deliberately does NOT cancel a pending idle confirmation. That confirmation
1665
+ * is what eventually notices the turn ended, and it already refuses to fire
1666
+ * while the pane is noisy, and cancelling it here would leave a session that
1667
+ * finished during a lull with nothing armed to ever call it idle.
1668
+ */
1669
+ _markWorking() {
1670
+ if (this._isWorking)
1671
+ return;
1672
+ this._isWorking = true;
1673
+ this._status = 'busy';
1674
+ this.emit('working');
1675
+ this._autoOps.notifyWorking();
1676
+ }
1677
+ /**
1678
+ * Decide whether the armed idle confirmation is real.
1679
+ *
1680
+ * A ❯ sighting alone means nothing (Claude redraws the composer through the
1681
+ * whole turn), so the pane must ALSO have gone quiet. While output is still
1682
+ * flowing the check re-arms instead of concluding. That loop is a timestamp
1683
+ * compare every IDLE_RECHECK_MS and ends the moment the pane falls silent.
1684
+ */
1685
+ _confirmIdle() {
1686
+ if (this._isStopped) {
1687
+ this._awaitingIdleConfirmation = false;
1688
+ return;
1689
+ }
1690
+ if (!isPaneQuiet(this._lastActivityAt, Date.now())) {
1691
+ this.activityTimeout = setTimeout(() => this._confirmIdle(), IDLE_RECHECK_MS);
1692
+ return; // stays _awaitingIdleConfirmation, so ❯ redraws do not pile up timers
1693
+ }
1694
+ // Quiet is necessary but NOT sufficient: a turn can go silent mid-tool-call.
1695
+ // Ask the screen before concluding, and keep asking on a slow cadence.
1696
+ if (this._probePaneWorking() === true) {
1697
+ this._markWorking();
1698
+ this.activityTimeout = setTimeout(() => this._confirmIdle(), PANE_PROBE_RECHECK_MS);
1699
+ return;
1700
+ }
1701
+ this._awaitingIdleConfirmation = false;
1702
+ this.activityTimeout = null;
1703
+ // Emit idle if either:
1704
+ // 1. Claude was working and is now at prompt (normal case)
1705
+ // 2. Session just started and is ready (status is 'busy' but _isWorking is false)
1706
+ const wasWorking = this._isWorking;
1707
+ const isInitialReady = this._status === 'busy' && !this._isWorking;
1708
+ if (wasWorking || isInitialReady) {
1709
+ this._isWorking = false;
1710
+ this._status = 'idle';
1711
+ this._lastPromptTime = Date.now();
1712
+ this.emit('idle');
1713
+ }
1714
+ }
1575
1715
  /**
1576
1716
  * Process expensive parsers (ANSI strip, Ralph, bash tool, token, CLI info, task descriptions).
1577
1717
  * Called on a throttled schedule (every EXPENSIVE_PROCESS_INTERVAL_MS) instead of on every
@@ -1615,21 +1755,20 @@ export class Session extends EventEmitter {
1615
1755
  if (rawData.includes('(') && rawData.includes(')')) {
1616
1756
  this.parseTaskDescriptionsFromTerminalData(getCleanData());
1617
1757
  }
1618
- // Work keyword detection (text-based, needs clean data)
1619
- // Only check if spinner didn't already trigger working state
1758
+ // Work detection (text-based, needs clean data: the status line is coloured,
1759
+ // so raw data has escape sequences between the `…` and the elapsed timer).
1760
+ // Only check if a faster path didn't already trigger working state.
1620
1761
  if (!this._isWorking) {
1621
1762
  const cleanData = getCleanData();
1622
- if (cleanData.includes('Thinking') ||
1763
+ if (CLAUDE_WORKING_LINE_PATTERN.test(cleanData) ||
1764
+ // Legacy gerunds. Current Claude randomizes the word ("Actualizing…",
1765
+ // "Finagling…"), so these catch only a fraction of turns; the pattern
1766
+ // above and the activity streak carry the rest.
1767
+ cleanData.includes('Thinking') ||
1623
1768
  cleanData.includes('Writing') ||
1624
1769
  cleanData.includes('Reading') ||
1625
1770
  cleanData.includes('Running')) {
1626
- this._isWorking = true;
1627
- this._status = 'busy';
1628
- this.emit('working');
1629
- this._autoOps.notifyWorking();
1630
- this._awaitingIdleConfirmation = false;
1631
- if (this.activityTimeout)
1632
- clearTimeout(this.activityTimeout);
1771
+ this._markWorking();
1633
1772
  }
1634
1773
  }
1635
1774
  }