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.
- package/README.zh-CN.md +91 -21
- package/dist/cli.d.ts +32 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +19 -8
- package/dist/cli.js.map +1 -1
- package/dist/hooks-config.d.ts +23 -2
- package/dist/hooks-config.d.ts.map +1 -1
- package/dist/hooks-config.js +118 -52
- package/dist/hooks-config.js.map +1 -1
- package/dist/mux-interface.d.ts +10 -0
- package/dist/mux-interface.d.ts.map +1 -1
- package/dist/respawn-patterns.d.ts.map +1 -1
- package/dist/respawn-patterns.js +7 -2
- package/dist/respawn-patterns.js.map +1 -1
- package/dist/session-activity.d.ts +73 -0
- package/dist/session-activity.d.ts.map +1 -0
- package/dist/session-activity.js +74 -0
- package/dist/session-activity.js.map +1 -0
- package/dist/session-cli-builder.d.ts +33 -1
- package/dist/session-cli-builder.d.ts.map +1 -1
- package/dist/session-cli-builder.js +51 -1
- package/dist/session-cli-builder.js.map +1 -1
- package/dist/session-trust-dialog.d.ts +62 -0
- package/dist/session-trust-dialog.d.ts.map +1 -0
- package/dist/session-trust-dialog.js +83 -0
- package/dist/session-trust-dialog.js.map +1 -0
- package/dist/session.d.ts +67 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +199 -60
- package/dist/session.js.map +1 -1
- package/dist/tmux-manager.d.ts +18 -0
- package/dist/tmux-manager.d.ts.map +1 -1
- package/dist/tmux-manager.js +48 -6
- package/dist/tmux-manager.js.map +1 -1
- package/dist/types/api.d.ts +1 -1
- package/dist/types/api.d.ts.map +1 -1
- package/dist/types/api.js.map +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/regex-patterns.d.ts +17 -0
- package/dist/utils/regex-patterns.d.ts.map +1 -1
- package/dist/utils/regex-patterns.js +17 -0
- package/dist/utils/regex-patterns.js.map +1 -1
- package/dist/web/approval-inbox.d.ts +159 -0
- package/dist/web/approval-inbox.d.ts.map +1 -0
- package/dist/web/approval-inbox.js +322 -0
- package/dist/web/approval-inbox.js.map +1 -0
- package/dist/web/public/admin-ui.js.gz +0 -0
- package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
- package/dist/web/public/app.c8e6e44e.js +36 -0
- package/dist/web/public/app.c8e6e44e.js.br +0 -0
- package/dist/web/public/app.c8e6e44e.js.gz +0 -0
- package/dist/web/public/approvals-ui.js +242 -0
- package/dist/web/public/approvals-ui.js.br +0 -0
- package/dist/web/public/approvals-ui.js.gz +0 -0
- package/dist/web/public/{constants.e7175b95.js → constants.7d0f4047.js} +86 -0
- package/dist/web/public/constants.7d0f4047.js.br +0 -0
- package/dist/web/public/constants.7d0f4047.js.gz +0 -0
- package/dist/web/public/cron-ui.js.gz +0 -0
- package/dist/web/public/entrance-animations.js.gz +0 -0
- package/dist/web/public/i18n.75cf29d4.js +1 -0
- package/dist/web/public/i18n.75cf29d4.js.br +0 -0
- package/dist/web/public/i18n.75cf29d4.js.gz +0 -0
- package/dist/web/public/image-input.ee16ad88.js.gz +0 -0
- package/dist/web/public/index.html +81 -12
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/public/input-cjk.63794d0b.js.gz +0 -0
- package/dist/web/public/{keyboard-accessory.9936227d.js → keyboard-accessory.d6c3ad11.js} +50 -0
- package/dist/web/public/keyboard-accessory.d6c3ad11.js.br +0 -0
- package/dist/web/public/keyboard-accessory.d6c3ad11.js.gz +0 -0
- package/dist/web/public/mobile-handlers.32fdd57f.js.gz +0 -0
- package/dist/web/public/mobile-overview.js +49 -0
- package/dist/web/public/mobile-overview.js.br +0 -0
- package/dist/web/public/mobile-overview.js.gz +0 -0
- package/dist/web/public/mobile.8a2651ba.css +1 -0
- package/dist/web/public/mobile.8a2651ba.css.br +0 -0
- package/dist/web/public/mobile.8a2651ba.css.gz +0 -0
- package/dist/web/public/notification-manager.5d229063.js.gz +0 -0
- package/dist/web/public/orchestrator-panel.js.gz +0 -0
- package/dist/web/public/{panels-ui.1d4203e0.js → panels-ui.3df9cd95.js} +5 -5
- package/dist/web/public/panels-ui.3df9cd95.js.br +0 -0
- package/dist/web/public/panels-ui.3df9cd95.js.gz +0 -0
- package/dist/web/public/ralph-panel.6de2d0f8.js.gz +0 -0
- package/dist/web/public/ralph-wizard.13a1831e.js.gz +0 -0
- package/dist/web/public/respawn-ui.ff0dae4c.js.gz +0 -0
- package/dist/web/public/sanitize-html.bc7078d6.js.gz +0 -0
- package/dist/web/public/session-ui.1356a4f4.js.gz +0 -0
- package/dist/web/public/{settings-ui.b41d33a5.js → settings-ui.20f9c723.js} +10 -10
- package/dist/web/public/settings-ui.20f9c723.js.br +0 -0
- package/dist/web/public/settings-ui.20f9c723.js.gz +0 -0
- package/dist/web/public/{styles.c4e58e13.css → styles.e33fc754.css} +1 -1
- package/dist/web/public/styles.e33fc754.css.br +0 -0
- package/dist/web/public/styles.e33fc754.css.gz +0 -0
- package/dist/web/public/subagent-windows.4cc8b005.js.gz +0 -0
- package/dist/web/public/sw.js +45 -20
- package/dist/web/public/sw.js.br +0 -0
- package/dist/web/public/sw.js.gz +0 -0
- package/dist/web/public/terminal-ui.430395be.js +4 -0
- package/dist/web/public/terminal-ui.430395be.js.br +0 -0
- package/dist/web/public/terminal-ui.430395be.js.gz +0 -0
- package/dist/web/public/ultracode-panel.js.gz +0 -0
- package/dist/web/public/ultracode-windows.js.gz +0 -0
- package/dist/web/public/upload.html.gz +0 -0
- package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
- package/dist/web/public/vendor/marked.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-predictive-echo.6cec0fba.js +4 -0
- package/dist/web/public/vendor/xterm-predictive-echo.6cec0fba.js.br +0 -0
- package/dist/web/public/vendor/xterm-predictive-echo.6cec0fba.js.gz +0 -0
- package/dist/web/public/vendor/xterm-zerolag-input.137ad9f0.js.gz +0 -0
- package/dist/web/public/vendor/xterm.css.gz +0 -0
- package/dist/web/public/vendor/xterm.min.js.gz +0 -0
- package/dist/web/public/{voice-input.085e9e73.js → voice-input.e5461859.js} +3 -0
- package/dist/web/public/voice-input.e5461859.js.br +0 -0
- package/dist/web/public/{voice-input.085e9e73.js.gz → voice-input.e5461859.js.gz} +0 -0
- package/dist/web/public/webview-tabs.js.gz +0 -0
- package/dist/web/route-helpers.d.ts.map +1 -1
- package/dist/web/route-helpers.js +10 -0
- package/dist/web/route-helpers.js.map +1 -1
- package/dist/web/routes/approval-routes.d.ts +19 -0
- package/dist/web/routes/approval-routes.d.ts.map +1 -0
- package/dist/web/routes/approval-routes.js +119 -0
- package/dist/web/routes/approval-routes.js.map +1 -0
- package/dist/web/routes/file-routes.d.ts.map +1 -1
- package/dist/web/routes/file-routes.js +24 -8
- package/dist/web/routes/file-routes.js.map +1 -1
- package/dist/web/routes/hook-event-routes.d.ts +3 -0
- package/dist/web/routes/hook-event-routes.d.ts.map +1 -1
- package/dist/web/routes/hook-event-routes.js +61 -3
- package/dist/web/routes/hook-event-routes.js.map +1 -1
- package/dist/web/routes/index.d.ts +1 -0
- package/dist/web/routes/index.d.ts.map +1 -1
- package/dist/web/routes/index.js +1 -0
- package/dist/web/routes/index.js.map +1 -1
- package/dist/web/routes/session-routes.d.ts.map +1 -1
- package/dist/web/routes/session-routes.js +30 -2
- package/dist/web/routes/session-routes.js.map +1 -1
- package/dist/web/schemas.d.ts +26 -0
- package/dist/web/schemas.d.ts.map +1 -1
- package/dist/web/schemas.js +40 -1
- package/dist/web/schemas.js.map +1 -1
- package/dist/web/sensitive-path.d.ts +12 -0
- package/dist/web/sensitive-path.d.ts.map +1 -1
- package/dist/web/sensitive-path.js +49 -4
- package/dist/web/sensitive-path.js.map +1 -1
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +31 -3
- package/dist/web/server.js.map +1 -1
- package/dist/web/session-listener-wiring.d.ts.map +1 -1
- package/dist/web/session-listener-wiring.js +9 -0
- package/dist/web/session-listener-wiring.js.map +1 -1
- package/dist/web/sse-events.d.ts +18 -2
- package/dist/web/sse-events.d.ts.map +1 -1
- package/dist/web/sse-events.js +20 -2
- package/dist/web/sse-events.js.map +1 -1
- package/package.json +3 -1
- package/scripts/postinstall.js +29 -0
- package/skills/codeman/SKILL.md +109 -15
- package/skills/codeman/reference/endpoints.md +66 -8
- package/skills/codeman/reference/messaging.md +216 -0
- package/skills/codeman/reference/recipes.md +59 -5
- package/dist/web/public/app.94825d85.js +0 -36
- package/dist/web/public/app.94825d85.js.br +0 -0
- package/dist/web/public/app.94825d85.js.gz +0 -0
- package/dist/web/public/constants.e7175b95.js.br +0 -0
- package/dist/web/public/constants.e7175b95.js.gz +0 -0
- package/dist/web/public/i18n.da34cfaf.js +0 -1
- package/dist/web/public/i18n.da34cfaf.js.br +0 -0
- package/dist/web/public/i18n.da34cfaf.js.gz +0 -0
- package/dist/web/public/keyboard-accessory.9936227d.js.br +0 -0
- package/dist/web/public/keyboard-accessory.9936227d.js.gz +0 -0
- package/dist/web/public/mobile.32cacde5.css +0 -1
- package/dist/web/public/mobile.32cacde5.css.br +0 -0
- package/dist/web/public/mobile.32cacde5.css.gz +0 -0
- package/dist/web/public/panels-ui.1d4203e0.js.br +0 -0
- package/dist/web/public/panels-ui.1d4203e0.js.gz +0 -0
- package/dist/web/public/settings-ui.b41d33a5.js.br +0 -0
- package/dist/web/public/settings-ui.b41d33a5.js.gz +0 -0
- package/dist/web/public/styles.c4e58e13.css.br +0 -0
- package/dist/web/public/styles.c4e58e13.css.gz +0 -0
- package/dist/web/public/terminal-ui.bc6819a9.js +0 -4
- package/dist/web/public/terminal-ui.bc6819a9.js.br +0 -0
- package/dist/web/public/terminal-ui.bc6819a9.js.gz +0 -0
- 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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1619
|
-
//
|
|
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 (
|
|
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.
|
|
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
|
}
|