@xdevops/issue-auto-finish 1.0.86 → 1.0.87
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/dist/AIRunnerRegistry-II3WWSFN.js +31 -0
- package/dist/PtyRunner-6UGI5STW.js +22 -0
- package/dist/TerminalManager-RT2N7N5R.js +8 -0
- package/dist/ai-runner/AIRunner.d.ts +9 -1
- package/dist/ai-runner/AIRunner.d.ts.map +1 -1
- package/dist/ai-runner/AIRunnerRegistry.d.ts +37 -1
- package/dist/ai-runner/AIRunnerRegistry.d.ts.map +1 -1
- package/dist/ai-runner/PtyRunner.d.ts +114 -0
- package/dist/ai-runner/PtyRunner.d.ts.map +1 -0
- package/dist/ai-runner/index.d.ts +3 -1
- package/dist/ai-runner/index.d.ts.map +1 -1
- package/dist/{ai-runner-SVUNA3FX.js → ai-runner-HLA44WI6.js} +12 -3
- package/dist/{analyze-SXXPE5XL.js → analyze-ZIXNC5GN.js} +10 -8
- package/dist/{analyze-SXXPE5XL.js.map → analyze-ZIXNC5GN.js.map} +1 -1
- package/dist/{braindump-4E5SDMSZ.js → braindump-56WAY2RD.js} +10 -8
- package/dist/{braindump-4E5SDMSZ.js.map → braindump-56WAY2RD.js.map} +1 -1
- package/dist/{chunk-ICXB2WP5.js → chunk-2MESXJEZ.js} +3 -3
- package/dist/{chunk-P4O4ZXEC.js → chunk-2YQHKXLL.js} +40 -19
- package/dist/chunk-2YQHKXLL.js.map +1 -0
- package/dist/chunk-AVGZH64A.js +211 -0
- package/dist/chunk-AVGZH64A.js.map +1 -0
- package/dist/{chunk-OUPJMHAL.js → chunk-IP3QTP5A.js} +1026 -764
- package/dist/chunk-IP3QTP5A.js.map +1 -0
- package/dist/chunk-KC5S66OZ.js +177 -0
- package/dist/chunk-KC5S66OZ.js.map +1 -0
- package/dist/{chunk-4QV6D34Y.js → chunk-M5C2WILQ.js} +8 -6
- package/dist/{chunk-4QV6D34Y.js.map → chunk-M5C2WILQ.js.map} +1 -1
- package/dist/{chunk-FWEW5E3B.js → chunk-NZHKAPU6.js} +35 -5
- package/dist/chunk-NZHKAPU6.js.map +1 -0
- package/dist/{chunk-KTYPZTF4.js → chunk-O3WEV5W3.js} +10 -2
- package/dist/chunk-O3WEV5W3.js.map +1 -0
- package/dist/{chunk-QO5VTSMI.js → chunk-QZZGIZWC.js} +455 -202
- package/dist/chunk-QZZGIZWC.js.map +1 -0
- package/dist/{chunk-4LFNFRCL.js → chunk-SAMTXC4A.js} +91 -214
- package/dist/chunk-SAMTXC4A.js.map +1 -0
- package/dist/chunk-U237JSLB.js +1 -0
- package/dist/chunk-U237JSLB.js.map +1 -0
- package/dist/chunk-U6GWFTKA.js +657 -0
- package/dist/chunk-U6GWFTKA.js.map +1 -0
- package/dist/{chunk-HOFYJEJ4.js → chunk-UBQLXQ7I.js} +11 -11
- package/dist/cli/setup/env-metadata.d.ts.map +1 -1
- package/dist/cli.js +8 -7
- package/dist/cli.js.map +1 -1
- package/dist/{config-QLINHCHD.js → config-WTRSZLOC.js} +4 -3
- package/dist/config-WTRSZLOC.js.map +1 -0
- package/dist/config-schema.d.ts +17 -1
- package/dist/config-schema.d.ts.map +1 -1
- package/dist/config.d.ts +20 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/errors/PhaseAbortedError.d.ts +3 -3
- package/dist/errors/PhaseAbortedError.d.ts.map +1 -1
- package/dist/errors-S3BWYA4I.js +43 -0
- package/dist/errors-S3BWYA4I.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -11
- package/dist/{init-TDKDC6YP.js → init-QQDXGTPB.js} +7 -6
- package/dist/{init-TDKDC6YP.js.map → init-QQDXGTPB.js.map} +1 -1
- package/dist/lib.js +9 -7
- package/dist/lib.js.map +1 -1
- package/dist/orchestrator/IssueProcessingContext.d.ts +39 -21
- package/dist/orchestrator/IssueProcessingContext.d.ts.map +1 -1
- package/dist/orchestrator/PipelineOrchestrator.d.ts +10 -1
- package/dist/orchestrator/PipelineOrchestrator.d.ts.map +1 -1
- package/dist/orchestrator/steps/PhaseLoopStep.d.ts +1 -1
- package/dist/orchestrator/steps/PhaseLoopStep.d.ts.map +1 -1
- package/dist/orchestrator/steps/SetupStep.d.ts.map +1 -1
- package/dist/persistence/PlanPersistence.d.ts +7 -1
- package/dist/persistence/PlanPersistence.d.ts.map +1 -1
- package/dist/phases/BasePhase.d.ts +31 -42
- package/dist/phases/BasePhase.d.ts.map +1 -1
- package/dist/phases/BuildPhase.d.ts.map +1 -1
- package/dist/phases/PhaseFactory.d.ts +2 -3
- package/dist/phases/PhaseFactory.d.ts.map +1 -1
- package/dist/phases/PhaseOutcome.d.ts +42 -0
- package/dist/phases/PhaseOutcome.d.ts.map +1 -0
- package/dist/phases/PlanPhase.d.ts +1 -1
- package/dist/phases/PlanPhase.d.ts.map +1 -1
- package/dist/phases/ReleasePhase.d.ts +8 -18
- package/dist/phases/ReleasePhase.d.ts.map +1 -1
- package/dist/phases/UatPhase.d.ts +7 -24
- package/dist/phases/UatPhase.d.ts.map +1 -1
- package/dist/phases/VerifyPhase.d.ts +4 -4
- package/dist/phases/VerifyPhase.d.ts.map +1 -1
- package/dist/poller/IssuePoller.d.ts.map +1 -1
- package/dist/prompts/release-templates.d.ts.map +1 -1
- package/dist/prompts/templates.d.ts.map +1 -1
- package/dist/{restart-4NSHDOX3.js → restart-BMILTP5X.js} +6 -5
- package/dist/{restart-4NSHDOX3.js.map → restart-BMILTP5X.js.map} +1 -1
- package/dist/run.js +14 -11
- package/dist/run.js.map +1 -1
- package/dist/settings/ExperimentalSettings.d.ts +1 -1
- package/dist/settings/ExperimentalSettings.d.ts.map +1 -1
- package/dist/start-6QRW6IJI.js +15 -0
- package/dist/start-6QRW6IJI.js.map +1 -0
- package/dist/terminal/TerminalManager.d.ts +62 -0
- package/dist/terminal/TerminalManager.d.ts.map +1 -0
- package/dist/terminal/TerminalWebSocket.d.ts +9 -0
- package/dist/terminal/TerminalWebSocket.d.ts.map +1 -0
- package/dist/tracker/ExecutableTask.d.ts +4 -2
- package/dist/tracker/ExecutableTask.d.ts.map +1 -1
- package/dist/tracker/IssueState.d.ts +11 -1
- package/dist/tracker/IssueState.d.ts.map +1 -1
- package/dist/tracker/IssueTracker.d.ts +19 -1
- package/dist/tracker/IssueTracker.d.ts.map +1 -1
- package/dist/web/WebServer.d.ts +4 -0
- package/dist/web/WebServer.d.ts.map +1 -1
- package/dist/web/routes/terminal.d.ts +11 -0
- package/dist/web/routes/terminal.d.ts.map +1 -0
- package/dist/webhook/CommandExecutor.d.ts.map +1 -1
- package/package.json +7 -1
- package/src/web/frontend/dist/assets/index-COYziOhv.css +1 -0
- package/src/web/frontend/dist/assets/index-D_oTMuJU.js +151 -0
- package/src/web/frontend/dist/index.html +2 -2
- package/dist/chunk-4LFNFRCL.js.map +0 -1
- package/dist/chunk-DADQSKPL.js +0 -1
- package/dist/chunk-FWEW5E3B.js.map +0 -1
- package/dist/chunk-KTYPZTF4.js.map +0 -1
- package/dist/chunk-OUPJMHAL.js.map +0 -1
- package/dist/chunk-P4O4ZXEC.js.map +0 -1
- package/dist/chunk-QO5VTSMI.js.map +0 -1
- package/dist/start-XZIBPLC2.js +0 -14
- package/src/web/frontend/dist/assets/index-BWVpNmFm.js +0 -133
- package/src/web/frontend/dist/assets/index-C7lorIa0.css +0 -1
- /package/dist/{ai-runner-SVUNA3FX.js.map → AIRunnerRegistry-II3WWSFN.js.map} +0 -0
- /package/dist/{chunk-DADQSKPL.js.map → PtyRunner-6UGI5STW.js.map} +0 -0
- /package/dist/{config-QLINHCHD.js.map → TerminalManager-RT2N7N5R.js.map} +0 -0
- /package/dist/{start-XZIBPLC2.js.map → ai-runner-HLA44WI6.js.map} +0 -0
- /package/dist/{chunk-ICXB2WP5.js.map → chunk-2MESXJEZ.js.map} +0 -0
- /package/dist/{chunk-HOFYJEJ4.js.map → chunk-UBQLXQ7I.js.map} +0 -0
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getPtyProfile,
|
|
3
|
+
getRegistryEntry,
|
|
4
|
+
getRunnerCapabilities,
|
|
5
|
+
isShuttingDown,
|
|
6
|
+
resolveModelForRunner
|
|
7
|
+
} from "./chunk-SAMTXC4A.js";
|
|
8
|
+
import {
|
|
9
|
+
logger
|
|
10
|
+
} from "./chunk-GF2RRYHB.js";
|
|
11
|
+
|
|
12
|
+
// src/ai-runner/PtyRunner.ts
|
|
13
|
+
import fs from "fs";
|
|
14
|
+
import path from "path";
|
|
15
|
+
var logger2 = logger.child("PtyRunner");
|
|
16
|
+
var ANSI_RE = /\x1b\[[?><=]*[0-9;]*[a-zA-Z~]|\x1b\][^\x07]*\x07|\x1b\(B/g;
|
|
17
|
+
function stripAnsi(str) {
|
|
18
|
+
return str.replace(ANSI_RE, "");
|
|
19
|
+
}
|
|
20
|
+
function extractIidFromPath(workDir) {
|
|
21
|
+
const match = workDir.match(/issue-(\d+)/);
|
|
22
|
+
return match ? parseInt(match[1], 10) : void 0;
|
|
23
|
+
}
|
|
24
|
+
function isIdlePrompt(stripped) {
|
|
25
|
+
const lines = stripped.split("\n").filter((l) => l.trim());
|
|
26
|
+
if (lines.length === 0) return false;
|
|
27
|
+
const last = lines[lines.length - 1].trim();
|
|
28
|
+
if (/^[❯>$%]\s*$/.test(last) || /[❯>]\s*$/.test(last)) return true;
|
|
29
|
+
if (lines.some((l) => {
|
|
30
|
+
const t = l.trim();
|
|
31
|
+
if (!/(?:^|\s)❯/.test(t)) return false;
|
|
32
|
+
if (/❯\s*$/.test(t)) return true;
|
|
33
|
+
return !/❯\s+[A-Za-z]{3,}/.test(t);
|
|
34
|
+
})) return true;
|
|
35
|
+
if (lines.some((l) => /^>/.test(l.trim())) && /⏵/.test(stripped)) return true;
|
|
36
|
+
if (/→/.test(stripped) && /\/\s*commands/.test(stripped) && !/ctrl\+c\s*to\s*stop/i.test(stripped)) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
var SPINNER_CHARS = "\u2736\u273B\u273D\u2722\u273E\u25C6\u2756\u25CF\u25D0\u25D1\u25D2\u25D3\u25CB\u280B\u2819\u2839\u2838\u283C\u2834\u2826\u2827\u2807\u280F\u2802\u2812\u2810\u2808\u2820\u2824\xB7*\u2026\u22EF\u2B21\u2B22";
|
|
42
|
+
var SPINNER_RE = new RegExp(`^[${SPINNER_CHARS}\\s]+$`);
|
|
43
|
+
var SPINNER_FRAGMENT_RE = new RegExp(`^[${SPINNER_CHARS}][a-zA-Z]{0,3}$`);
|
|
44
|
+
var STATUS_BAR_RE = /bypass permissions|shift\+tab|ctrl\+[a-z].*(?:to |edit)|to interrupt|to cycle|⏵.*(?:bypass|permission)/;
|
|
45
|
+
var THINKING_RE = /^[^\w]*\w[\w-]*…\s*$/;
|
|
46
|
+
var SEPARATOR_RE = /^[─━═-]{10,}$/;
|
|
47
|
+
var EFFORT_RE = /^[◐◑◒◓]\s+(?:low|medium|high)\s+·\s+\/effort$/;
|
|
48
|
+
var CLAUDE_STATUS_BAR_RE = new RegExp(`^[${SPINNER_CHARS}]\\s*\\d+[smh]`);
|
|
49
|
+
var CURSOR_ARTIFACT_RE = /^\d{1,3}$/;
|
|
50
|
+
var CURSOR_GENERATING_RE = /^[⬡⬢]\s*Generating/;
|
|
51
|
+
var CURSOR_FOOTER_RE = /^\/\s*commands\s*·\s*@\s*files\s*·\s*!\s*shell$/;
|
|
52
|
+
var CURSOR_STATUS_RE = /^▶︎?\s*Auto-run/;
|
|
53
|
+
var CURSOR_MODEL_RE = /^(?:Opus|Claude|GPT|Gemini|当前使用的模型)\s*/;
|
|
54
|
+
var BOX_DRAWING_RE = /^[┌┐└┘│─┬┴├┤╭╮╰╯═║╔╗╚╝╠╣╦╩╬\s]*$/;
|
|
55
|
+
var QUEUED_MSG_RE = /Press\s*up\s*to\s*edit\s*queued\s*messages/;
|
|
56
|
+
var TRUST_DIALOG_RE = /trust\s*(?:the\s*files\s*in\s*)?this\s*(?:folder|workspace)|I\s*trust\s*this/i;
|
|
57
|
+
var PERMISSION_DIALOG_RE = /Do\s*you\s*want\s*to\s*proceed\s*\?|(?:❯|>)\s*\d+\.\s*Yes\b|Allow\s+(?:this|the)\s+(?:action|command)\s*\?/i;
|
|
58
|
+
function containsActiveWork(stripped) {
|
|
59
|
+
const lines = stripped.split(/[\r\n]+/).filter((l) => l.trim());
|
|
60
|
+
let inTipBlock = false;
|
|
61
|
+
return lines.some((line) => {
|
|
62
|
+
const t = line.trim();
|
|
63
|
+
if (t.length === 0) return false;
|
|
64
|
+
if (/Tip:/i.test(t)) {
|
|
65
|
+
inTipBlock = true;
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
if (inTipBlock) return false;
|
|
69
|
+
if (/ctrl\+o to expand/i.test(t)) return false;
|
|
70
|
+
if (/^⎿/.test(t)) return false;
|
|
71
|
+
if (/^[❯>$%]\s*$/.test(t) || /[❯>]\s*$/.test(t)) return false;
|
|
72
|
+
if (isTuiNoise(t)) return false;
|
|
73
|
+
return true;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
function isTuiNoise(line) {
|
|
77
|
+
const t = line.replace(/\r/g, "").trim();
|
|
78
|
+
if (t.length === 0) return true;
|
|
79
|
+
if (SPINNER_RE.test(t)) return true;
|
|
80
|
+
if (t.length <= 4 && SPINNER_FRAGMENT_RE.test(t)) return true;
|
|
81
|
+
if (STATUS_BAR_RE.test(t)) return true;
|
|
82
|
+
if (THINKING_RE.test(t)) return true;
|
|
83
|
+
if (SEPARATOR_RE.test(t)) return true;
|
|
84
|
+
if (EFFORT_RE.test(t)) return true;
|
|
85
|
+
if (CLAUDE_STATUS_BAR_RE.test(t)) return true;
|
|
86
|
+
if (QUEUED_MSG_RE.test(t)) return true;
|
|
87
|
+
if (CURSOR_ARTIFACT_RE.test(t)) return true;
|
|
88
|
+
if (CURSOR_GENERATING_RE.test(t)) return true;
|
|
89
|
+
if (CURSOR_FOOTER_RE.test(t)) return true;
|
|
90
|
+
if (CURSOR_STATUS_RE.test(t)) return true;
|
|
91
|
+
if (CURSOR_MODEL_RE.test(t)) return true;
|
|
92
|
+
if (BOX_DRAWING_RE.test(t) && t.length > 1) return true;
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
var PtyRunner = class {
|
|
96
|
+
constructor(nvmNodeVersion, terminalManager, defaultAgentMode, phaseAgentMap, globalModel, idleDetectMs = 3e4) {
|
|
97
|
+
this.nvmNodeVersion = nvmNodeVersion;
|
|
98
|
+
this.terminalManager = terminalManager;
|
|
99
|
+
this.defaultAgentMode = defaultAgentMode;
|
|
100
|
+
this.phaseAgentMap = phaseAgentMap;
|
|
101
|
+
this.globalModel = globalModel;
|
|
102
|
+
this.idleDetectMs = idleDetectMs;
|
|
103
|
+
}
|
|
104
|
+
/** workDir → active session info (sessionId + current agent type) */
|
|
105
|
+
sessions = /* @__PURE__ */ new Map();
|
|
106
|
+
/** Sessions that were forcefully killed (via killByWorkDir/killAll). Checked by
|
|
107
|
+
* detectCompletion's onExit handler to report failure instead of success. */
|
|
108
|
+
killedSessions = /* @__PURE__ */ new Set();
|
|
109
|
+
// ---- AIRunner interface ---------------------------------------------------
|
|
110
|
+
async run(options) {
|
|
111
|
+
if (isShuttingDown()) {
|
|
112
|
+
logger2.warn("PtyRunner skipped \u2014 service is shutting down");
|
|
113
|
+
return { success: false, output: "Service shutting down", exitCode: null };
|
|
114
|
+
}
|
|
115
|
+
const { prompt, workDir, timeoutMs, onStreamEvent, phaseName } = options;
|
|
116
|
+
const agentMode = this.resolveAgentForPhase(phaseName);
|
|
117
|
+
const startMode = options.mode;
|
|
118
|
+
logger2.info("PtyRunner.run()", { workDir, timeoutMs, phaseName, agentMode });
|
|
119
|
+
const { sessionId, isNew } = this.ensureSession(workDir, agentMode, startMode);
|
|
120
|
+
if (isNew) {
|
|
121
|
+
logger2.info("Waiting for AI agent prompt (new session)", { sessionId, phaseName });
|
|
122
|
+
await this.waitForPrompt(sessionId, 3e5);
|
|
123
|
+
} else {
|
|
124
|
+
logger2.info("Waiting for AI agent prompt (reused session)", { sessionId, phaseName });
|
|
125
|
+
await this.waitForPrompt(sessionId, 1e4);
|
|
126
|
+
}
|
|
127
|
+
if (startMode === "plan" && this.shouldUseNativePlan(agentMode)) {
|
|
128
|
+
return this.runNativePlanMode(sessionId, isNew, options, agentMode, workDir);
|
|
129
|
+
}
|
|
130
|
+
await this.ensurePlanMode(sessionId, agentMode, startMode === "plan", workDir);
|
|
131
|
+
await this.writeCommand(sessionId, "/clear", agentMode);
|
|
132
|
+
await new Promise((resolve) => setTimeout(resolve, 2e3));
|
|
133
|
+
const promptFile = this.writePromptFile(workDir, prompt);
|
|
134
|
+
const instruction = `Please read and follow all instructions in ${promptFile}`;
|
|
135
|
+
await this.writeCommand(sessionId, instruction, agentMode);
|
|
136
|
+
const result = await this.detectCompletion(sessionId, options, onStreamEvent);
|
|
137
|
+
logger2.info("PtyRunner phase completed", {
|
|
138
|
+
workDir,
|
|
139
|
+
agentMode,
|
|
140
|
+
phaseName,
|
|
141
|
+
timedOut: result.timedOut,
|
|
142
|
+
outputLength: result.output.length
|
|
143
|
+
});
|
|
144
|
+
return {
|
|
145
|
+
success: !result.timedOut,
|
|
146
|
+
output: result.output,
|
|
147
|
+
errorMessage: result.timedOut ? result.timeoutType === "idle" ? "AI \u957F\u65F6\u95F4\u65E0\u54CD\u5E94\uFF0C\u5DF2\u8D85\u65F6\u7EC8\u6B62" : "\u6267\u884C\u8D85\u65F6" : void 0,
|
|
148
|
+
sessionId,
|
|
149
|
+
exitCode: result.timedOut ? null : 0,
|
|
150
|
+
timeoutType: result.timeoutType
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
killAll() {
|
|
154
|
+
for (const [, info] of this.sessions) {
|
|
155
|
+
this.killedSessions.add(info.sessionId);
|
|
156
|
+
this.terminalManager.destroy(info.sessionId);
|
|
157
|
+
}
|
|
158
|
+
this.sessions.clear();
|
|
159
|
+
logger2.info("PtyRunner: all managed sessions destroyed");
|
|
160
|
+
}
|
|
161
|
+
killByWorkDir(targetWorkDir) {
|
|
162
|
+
const info = this.sessions.get(targetWorkDir);
|
|
163
|
+
if (!info) return 0;
|
|
164
|
+
this.killedSessions.add(info.sessionId);
|
|
165
|
+
this.terminalManager.destroy(info.sessionId);
|
|
166
|
+
this.sessions.delete(targetWorkDir);
|
|
167
|
+
return 1;
|
|
168
|
+
}
|
|
169
|
+
interruptByWorkDir(targetWorkDir) {
|
|
170
|
+
const info = this.sessions.get(targetWorkDir);
|
|
171
|
+
if (!info) return false;
|
|
172
|
+
const session = this.terminalManager.get(info.sessionId);
|
|
173
|
+
if (!session) {
|
|
174
|
+
this.sessions.delete(targetWorkDir);
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
this.terminalManager.write(info.sessionId, "");
|
|
178
|
+
logger2.info("Interrupted PTY session for retry", {
|
|
179
|
+
workDir: targetWorkDir,
|
|
180
|
+
sessionId: info.sessionId
|
|
181
|
+
});
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
// ---- Agent resolution ----------------------------------------------------
|
|
185
|
+
/** Get the Enter key sequence for the given agent mode */
|
|
186
|
+
getEnterKey(agentMode) {
|
|
187
|
+
const profile = getPtyProfile(agentMode);
|
|
188
|
+
return profile?.enterKey ?? "\r";
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Write a command to the PTY and press Enter.
|
|
192
|
+
*
|
|
193
|
+
* Some agents (codebuddy) use input coalescing in their TUI: when text and
|
|
194
|
+
* Enter arrive in the same PTY write, the Enter is treated as a newline
|
|
195
|
+
* character (paste) instead of triggering message submission. For these
|
|
196
|
+
* agents (PtyProfile.separateEnter=true), text and Enter are sent in
|
|
197
|
+
* separate writes with a 150ms gap so the Enter key is recognized as
|
|
198
|
+
* a standalone key press.
|
|
199
|
+
*/
|
|
200
|
+
async writeCommand(sessionId, text, agentMode) {
|
|
201
|
+
const enterKey = this.getEnterKey(agentMode);
|
|
202
|
+
const profile = getPtyProfile(agentMode);
|
|
203
|
+
if (profile?.separateEnter) {
|
|
204
|
+
this.terminalManager.write(sessionId, text);
|
|
205
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
206
|
+
this.terminalManager.write(sessionId, enterKey);
|
|
207
|
+
} else {
|
|
208
|
+
this.terminalManager.write(sessionId, text + enterKey);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/** Resolve agent mode for a given phase (fallback to default) */
|
|
212
|
+
resolveAgentForPhase(phaseName) {
|
|
213
|
+
if (phaseName && this.phaseAgentMap[phaseName]) {
|
|
214
|
+
return this.phaseAgentMap[phaseName];
|
|
215
|
+
}
|
|
216
|
+
return this.defaultAgentMode;
|
|
217
|
+
}
|
|
218
|
+
/** Look up PtyProfile from registry + resolve agent-specific binary and model */
|
|
219
|
+
resolveProfileAndModel(agentMode) {
|
|
220
|
+
const profile = getPtyProfile(agentMode);
|
|
221
|
+
if (!profile) {
|
|
222
|
+
throw new Error(
|
|
223
|
+
`Agent "${agentMode}" has no PtyProfile \u2014 not supported in PTY mode. Compatible agents: claude-internal, codebuddy, cursor-agent`
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
const entry = getRegistryEntry(agentMode);
|
|
227
|
+
const binary = entry && process.env[entry.binaryEnvKey] || entry?.defaultBinary || agentMode;
|
|
228
|
+
const model = this.globalModel ? resolveModelForRunner(agentMode, this.globalModel) : void 0;
|
|
229
|
+
return { profile, model, binary };
|
|
230
|
+
}
|
|
231
|
+
// ---- Session management ---------------------------------------------------
|
|
232
|
+
ensureSession(workDir, agentMode, startMode) {
|
|
233
|
+
const existing = this.sessions.get(workDir);
|
|
234
|
+
if (existing && existing.agentMode !== agentMode) {
|
|
235
|
+
logger2.info("Agent switched, destroying old PTY session", {
|
|
236
|
+
workDir,
|
|
237
|
+
oldAgent: existing.agentMode,
|
|
238
|
+
newAgent: agentMode,
|
|
239
|
+
sessionId: existing.sessionId
|
|
240
|
+
});
|
|
241
|
+
this.terminalManager.destroy(existing.sessionId);
|
|
242
|
+
this.sessions.delete(workDir);
|
|
243
|
+
}
|
|
244
|
+
if (existing && existing.agentMode === agentMode) {
|
|
245
|
+
if (this.terminalManager.get(existing.sessionId)) {
|
|
246
|
+
logger2.info("Reusing existing PTY session (same agent)", {
|
|
247
|
+
workDir,
|
|
248
|
+
agentMode,
|
|
249
|
+
sessionId: existing.sessionId
|
|
250
|
+
});
|
|
251
|
+
return { sessionId: existing.sessionId, isNew: false };
|
|
252
|
+
}
|
|
253
|
+
this.sessions.delete(workDir);
|
|
254
|
+
}
|
|
255
|
+
const orphan = this.terminalManager.findByWorkDir(workDir);
|
|
256
|
+
if (orphan) {
|
|
257
|
+
logger2.info("Destroying orphaned PTY session on workDir", {
|
|
258
|
+
workDir,
|
|
259
|
+
sessionId: orphan.id
|
|
260
|
+
});
|
|
261
|
+
this.terminalManager.destroy(orphan.id);
|
|
262
|
+
}
|
|
263
|
+
const { profile, model, binary } = this.resolveProfileAndModel(agentMode);
|
|
264
|
+
const args = profile.buildPtyArgs({ model, startMode });
|
|
265
|
+
const issueIid = extractIidFromPath(workDir);
|
|
266
|
+
const info = this.terminalManager.create({
|
|
267
|
+
workDir,
|
|
268
|
+
issueIid,
|
|
269
|
+
command: binary,
|
|
270
|
+
args,
|
|
271
|
+
managed: true
|
|
272
|
+
});
|
|
273
|
+
this.sessions.set(workDir, {
|
|
274
|
+
sessionId: info.id,
|
|
275
|
+
agentMode,
|
|
276
|
+
// Always set initial mode to defaultModeName — CLI args always use bypass
|
|
277
|
+
// (startMode is applied at runtime via Shift+Tab in ensurePlanMode).
|
|
278
|
+
currentMode: profile.defaultModeName ?? "bypass",
|
|
279
|
+
startedWithMode: startMode
|
|
280
|
+
});
|
|
281
|
+
logger2.info("Created new PTY session", {
|
|
282
|
+
workDir,
|
|
283
|
+
agentMode,
|
|
284
|
+
binary,
|
|
285
|
+
args,
|
|
286
|
+
sessionId: info.id,
|
|
287
|
+
pid: info.pid,
|
|
288
|
+
issueIid
|
|
289
|
+
});
|
|
290
|
+
return { sessionId: info.id, isNew: true };
|
|
291
|
+
}
|
|
292
|
+
// ---- Prompt delivery ------------------------------------------------------
|
|
293
|
+
/**
|
|
294
|
+
* Wait for the AI agent to show its idle prompt (❯ or >), indicating
|
|
295
|
+
* it is ready to accept input. Used before sending /clear and instructions
|
|
296
|
+
* to prevent commands from arriving before the agent is initialized.
|
|
297
|
+
*/
|
|
298
|
+
waitForPrompt(sessionId, timeoutMs = 6e4) {
|
|
299
|
+
return new Promise((resolve) => {
|
|
300
|
+
let promptSeen = false;
|
|
301
|
+
let trustDialogHandled = false;
|
|
302
|
+
let stabilityTimer;
|
|
303
|
+
const STABILITY_MS = 3e3;
|
|
304
|
+
const TUI_READY_RE = /bypass\s*permissions|shift\+?\s*tab\s*to\s*cycle/i;
|
|
305
|
+
let tuiReady = false;
|
|
306
|
+
let bannerSeen = false;
|
|
307
|
+
let silenceTimer;
|
|
308
|
+
const SILENCE_READY_MS = 8e3;
|
|
309
|
+
const timer = setTimeout(() => {
|
|
310
|
+
if (stabilityTimer) clearTimeout(stabilityTimer);
|
|
311
|
+
if (silenceTimer) clearTimeout(silenceTimer);
|
|
312
|
+
subscription.dispose();
|
|
313
|
+
logger2.warn("Timed out waiting for AI agent prompt", { sessionId, timeoutMs });
|
|
314
|
+
resolve();
|
|
315
|
+
}, timeoutMs);
|
|
316
|
+
const done = (reason) => {
|
|
317
|
+
clearTimeout(timer);
|
|
318
|
+
if (stabilityTimer) clearTimeout(stabilityTimer);
|
|
319
|
+
if (silenceTimer) clearTimeout(silenceTimer);
|
|
320
|
+
subscription.dispose();
|
|
321
|
+
logger2.info("AI agent prompt detected", { sessionId, reason });
|
|
322
|
+
resolve();
|
|
323
|
+
};
|
|
324
|
+
const resetSilenceTimer = () => {
|
|
325
|
+
if (!bannerSeen) return;
|
|
326
|
+
if (silenceTimer) clearTimeout(silenceTimer);
|
|
327
|
+
silenceTimer = setTimeout(() => {
|
|
328
|
+
if (promptSeen) return;
|
|
329
|
+
logger2.info("Banner shown and PTY silent \u2014 treating agent as ready", { sessionId });
|
|
330
|
+
done("silence-after-banner");
|
|
331
|
+
}, SILENCE_READY_MS);
|
|
332
|
+
};
|
|
333
|
+
const subscription = this.terminalManager.onData(sessionId, (data) => {
|
|
334
|
+
const stripped = stripAnsi(data);
|
|
335
|
+
const nonEmptyLines = stripped.split("\n").filter((l) => l.trim());
|
|
336
|
+
const isNoise = nonEmptyLines.length === 0 || nonEmptyLines.every((l) => isTuiNoise(l));
|
|
337
|
+
if (!bannerSeen && /Claude\s*Code/i.test(stripped)) {
|
|
338
|
+
bannerSeen = true;
|
|
339
|
+
}
|
|
340
|
+
resetSilenceTimer();
|
|
341
|
+
if (!trustDialogHandled && TRUST_DIALOG_RE.test(stripped)) {
|
|
342
|
+
trustDialogHandled = true;
|
|
343
|
+
logger2.info("Trust dialog detected, auto-confirming", { sessionId });
|
|
344
|
+
setTimeout(() => this.terminalManager.write(sessionId, "\r"), 500);
|
|
345
|
+
}
|
|
346
|
+
if (PERMISSION_DIALOG_RE.test(stripped)) {
|
|
347
|
+
logger2.info("Permission dialog detected in waitForPrompt, auto-confirming", { sessionId });
|
|
348
|
+
setTimeout(() => this.terminalManager.write(sessionId, "\r"), 500);
|
|
349
|
+
}
|
|
350
|
+
if (isIdlePrompt(stripped)) {
|
|
351
|
+
promptSeen = true;
|
|
352
|
+
}
|
|
353
|
+
if (!tuiReady && TUI_READY_RE.test(stripped)) {
|
|
354
|
+
tuiReady = true;
|
|
355
|
+
}
|
|
356
|
+
if (promptSeen || tuiReady) {
|
|
357
|
+
if (stabilityTimer && isNoise) {
|
|
358
|
+
} else {
|
|
359
|
+
if (stabilityTimer) clearTimeout(stabilityTimer);
|
|
360
|
+
const reason = promptSeen ? "idle-prompt-stable" : "status-bar-ready";
|
|
361
|
+
stabilityTimer = setTimeout(() => done(reason), STABILITY_MS);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
// ---- Interactive mode switching --------------------------------------------
|
|
368
|
+
/**
|
|
369
|
+
* Switch the PTY session to (or away from) plan mode by pressing the mode
|
|
370
|
+
* cycle key (Shift+Tab) until the target mode is detected in the output.
|
|
371
|
+
*
|
|
372
|
+
* This is a no-op when the agent has no modeCycleKey configured, or the
|
|
373
|
+
* session is already in the desired mode.
|
|
374
|
+
*/
|
|
375
|
+
async ensurePlanMode(sessionId, agentMode, wantPlan, workDir) {
|
|
376
|
+
const profile = getPtyProfile(agentMode);
|
|
377
|
+
if (!profile?.modeCycleKey || !profile.detectMode || !profile.planModeName) return;
|
|
378
|
+
const session = this.sessions.get(workDir);
|
|
379
|
+
if (!session) return;
|
|
380
|
+
const targetMode = wantPlan ? profile.planModeName : profile.defaultModeName ?? "bypass";
|
|
381
|
+
if (session.currentMode === targetMode) {
|
|
382
|
+
logger2.info("PTY already in target mode", { sessionId, targetMode });
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
const MAX_ATTEMPTS = 5;
|
|
386
|
+
for (let i = 0; i < MAX_ATTEMPTS; i++) {
|
|
387
|
+
const outputPromise = this.collectRecentOutput(sessionId, 3e3);
|
|
388
|
+
this.terminalManager.write(sessionId, profile.modeCycleKey);
|
|
389
|
+
const recentOutput = await outputPromise;
|
|
390
|
+
const detected = profile.detectMode(recentOutput);
|
|
391
|
+
if (detected) {
|
|
392
|
+
session.currentMode = detected;
|
|
393
|
+
if (detected === targetMode) {
|
|
394
|
+
logger2.info("PTY mode switched", {
|
|
395
|
+
sessionId,
|
|
396
|
+
agentMode,
|
|
397
|
+
targetMode,
|
|
398
|
+
attempts: i + 1
|
|
399
|
+
});
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
logger2.warn("Failed to switch PTY mode after max attempts", {
|
|
405
|
+
sessionId,
|
|
406
|
+
agentMode,
|
|
407
|
+
targetMode,
|
|
408
|
+
current: session.currentMode
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Briefly subscribe to PTY output and collect all data emitted during a
|
|
413
|
+
* window. Used by ensurePlanMode to read the mode indicator after pressing
|
|
414
|
+
* the cycle key.
|
|
415
|
+
*/
|
|
416
|
+
collectRecentOutput(sessionId, durationMs) {
|
|
417
|
+
return new Promise((resolve) => {
|
|
418
|
+
const chunks = [];
|
|
419
|
+
const subscription = this.terminalManager.onData(sessionId, (data) => {
|
|
420
|
+
chunks.push(stripAnsi(data));
|
|
421
|
+
});
|
|
422
|
+
setTimeout(() => {
|
|
423
|
+
subscription.dispose();
|
|
424
|
+
resolve(chunks.join(""));
|
|
425
|
+
}, durationMs);
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
// ---- Native plan mode (two-phase execution) --------------------------------
|
|
429
|
+
/**
|
|
430
|
+
* Whether the agent supports the two-phase native plan strategy:
|
|
431
|
+
* bypass start → Shift+Tab to plan → execute → Shift+Tab to bypass → commit file.
|
|
432
|
+
*
|
|
433
|
+
* Conditions: runner does NOT natively handle plan mode on its own (nativePlanMode=false)
|
|
434
|
+
* AND the PTY profile supports interactive mode switching (modeCycleKey + planModeName).
|
|
435
|
+
*/
|
|
436
|
+
shouldUseNativePlan(agentMode) {
|
|
437
|
+
const caps = getRunnerCapabilities(agentMode);
|
|
438
|
+
const profile = getPtyProfile(agentMode);
|
|
439
|
+
return !caps?.nativePlanMode && !!profile?.modeCycleKey && !!profile?.planModeName;
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Two-phase plan execution:
|
|
443
|
+
* Phase 1 — Switch to plan mode, send plan prompt, wait for idle (no artifact gate).
|
|
444
|
+
* Phase 2 — Switch to bypass, send commit prompt, wait for artifact file on disk.
|
|
445
|
+
*/
|
|
446
|
+
async runNativePlanMode(sessionId, isNew, options, agentMode, workDir) {
|
|
447
|
+
logger2.info("Native plan mode: switching to plan", { sessionId, agentMode });
|
|
448
|
+
await this.ensurePlanMode(sessionId, agentMode, true, workDir);
|
|
449
|
+
if (!isNew) {
|
|
450
|
+
await this.writeCommand(sessionId, "/clear", agentMode);
|
|
451
|
+
await new Promise((resolve) => setTimeout(resolve, 2e3));
|
|
452
|
+
}
|
|
453
|
+
const promptFile = this.writePromptFile(workDir, options.prompt);
|
|
454
|
+
await this.writeCommand(
|
|
455
|
+
sessionId,
|
|
456
|
+
`Please read and follow all instructions in ${promptFile}`,
|
|
457
|
+
agentMode
|
|
458
|
+
);
|
|
459
|
+
const planResult = await this.detectCompletion(sessionId, {
|
|
460
|
+
...options,
|
|
461
|
+
artifactCheck: void 0
|
|
462
|
+
}, options.onStreamEvent);
|
|
463
|
+
if (planResult.timedOut) {
|
|
464
|
+
logger2.warn("Native plan mode: plan phase timed out", { sessionId });
|
|
465
|
+
return this.buildRunResult(planResult, sessionId);
|
|
466
|
+
}
|
|
467
|
+
logger2.info("Native plan mode: switching to bypass for commit", { sessionId });
|
|
468
|
+
await this.ensurePlanMode(sessionId, agentMode, false, workDir);
|
|
469
|
+
await this.writeCommand(sessionId, "/clear", agentMode);
|
|
470
|
+
await new Promise((resolve) => setTimeout(resolve, 2e3));
|
|
471
|
+
const commitPrompt = this.buildPlanCommitPrompt(options);
|
|
472
|
+
await this.writeCommand(sessionId, commitPrompt, agentMode);
|
|
473
|
+
const commitResult = await this.detectCompletion(
|
|
474
|
+
sessionId,
|
|
475
|
+
options,
|
|
476
|
+
options.onStreamEvent,
|
|
477
|
+
true
|
|
478
|
+
);
|
|
479
|
+
logger2.info("Native plan mode completed", {
|
|
480
|
+
sessionId,
|
|
481
|
+
timedOut: commitResult.timedOut
|
|
482
|
+
});
|
|
483
|
+
return this.buildRunResult(commitResult, sessionId);
|
|
484
|
+
}
|
|
485
|
+
/** Build the prompt that instructs the agent to write the plan to disk. */
|
|
486
|
+
buildPlanCommitPrompt(options) {
|
|
487
|
+
const paths = options.artifactPaths ?? [];
|
|
488
|
+
if (paths.length === 0) {
|
|
489
|
+
return "Write the implementation plan you just created to the project directory.";
|
|
490
|
+
}
|
|
491
|
+
const pathList = paths.map((p) => `- ${p}`).join("\n");
|
|
492
|
+
return "\u4F60\u521A\u624D\u5728\u89C4\u5212\u6A21\u5F0F\u4E0B\u5236\u5B9A\u4E86\u4E00\u4EFD\u5B9E\u65BD\u8BA1\u5212\u3002\u73B0\u5728\u8BF7\u5C06\u5B8C\u6574\u7684\u8BA1\u5212\u5185\u5BB9\u5199\u5165\u4EE5\u4E0B\u6587\u4EF6\uFF1A\n\n" + pathList + "\n\n\u8981\u6C42\uFF1A\n1. \u5C06\u4F60\u521A\u624D\u89C4\u5212\u7684\u5B8C\u6574\u5185\u5BB9\uFF08\u9700\u6C42\u5206\u6790\u3001\u7CFB\u7EDF\u8BBE\u8BA1\u3001\u5B9E\u65BD Todolist\uFF09\u5199\u5165\u4E0A\u8FF0\u6587\u4EF6\n2. \u4FDD\u6301 Markdown \u683C\u5F0F\n3. \u53EA\u5199\u5165\u6587\u4EF6\uFF0C\u4E0D\u8981\u4FEE\u6539\u4EFB\u4F55\u9879\u76EE\u4EE3\u7801";
|
|
493
|
+
}
|
|
494
|
+
/** Map detectCompletion result to RunResult. */
|
|
495
|
+
buildRunResult(detectResult, sessionId) {
|
|
496
|
+
return {
|
|
497
|
+
success: !detectResult.timedOut,
|
|
498
|
+
output: detectResult.output,
|
|
499
|
+
errorMessage: detectResult.timedOut ? detectResult.timeoutType === "idle" ? "AI \u957F\u65F6\u95F4\u65E0\u54CD\u5E94\uFF0C\u5DF2\u8D85\u65F6\u7EC8\u6B62" : "\u6267\u884C\u8D85\u65F6" : void 0,
|
|
500
|
+
sessionId,
|
|
501
|
+
exitCode: detectResult.timedOut ? null : 0,
|
|
502
|
+
timeoutType: detectResult.timeoutType
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
writePromptFile(workDir, prompt) {
|
|
506
|
+
const dir = path.join(workDir, ".claude-plan");
|
|
507
|
+
if (!fs.existsSync(dir)) {
|
|
508
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
509
|
+
}
|
|
510
|
+
const relPath = ".claude-plan/.phase-prompt.md";
|
|
511
|
+
const absPath = path.join(workDir, relPath);
|
|
512
|
+
fs.writeFileSync(absPath, prompt, "utf-8");
|
|
513
|
+
return relPath;
|
|
514
|
+
}
|
|
515
|
+
// ---- Completion detection -------------------------------------------------
|
|
516
|
+
detectCompletion(sessionId, options, onStreamEvent, skipEchoDetection) {
|
|
517
|
+
return new Promise((resolve) => {
|
|
518
|
+
const outputLines = [];
|
|
519
|
+
let lastOutputTime = Date.now();
|
|
520
|
+
let hasSubstantiveOutput = false;
|
|
521
|
+
let echoConsumed = skipEchoDetection ?? false;
|
|
522
|
+
let resolved = false;
|
|
523
|
+
let debounceTimer;
|
|
524
|
+
const idleTimeoutMs = options.idleTimeoutMs ?? 6e5;
|
|
525
|
+
const timeoutMs = options.timeoutMs;
|
|
526
|
+
const finish = (result) => {
|
|
527
|
+
if (resolved) return;
|
|
528
|
+
resolved = true;
|
|
529
|
+
cleanup();
|
|
530
|
+
resolve(result);
|
|
531
|
+
};
|
|
532
|
+
const wallTimer = setTimeout(() => {
|
|
533
|
+
finish({
|
|
534
|
+
output: outputLines.join(""),
|
|
535
|
+
timedOut: true,
|
|
536
|
+
timeoutType: "wall-clock"
|
|
537
|
+
});
|
|
538
|
+
}, timeoutMs);
|
|
539
|
+
const idleCheck = setInterval(() => {
|
|
540
|
+
if (!hasSubstantiveOutput) return;
|
|
541
|
+
if (Date.now() - lastOutputTime >= idleTimeoutMs) {
|
|
542
|
+
finish({
|
|
543
|
+
output: outputLines.join(""),
|
|
544
|
+
timedOut: true,
|
|
545
|
+
timeoutType: "idle"
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
}, 5e3);
|
|
549
|
+
const subscription = this.terminalManager.onData(sessionId, (data) => {
|
|
550
|
+
if (resolved) return;
|
|
551
|
+
const stripped = stripAnsi(data);
|
|
552
|
+
const nonEmptyLines = stripped.split("\n").filter((l) => l.trim());
|
|
553
|
+
const isNoise = nonEmptyLines.length === 0 || nonEmptyLines.every((l) => isTuiNoise(l));
|
|
554
|
+
const isIdle = isIdlePrompt(stripped);
|
|
555
|
+
const isMixedFrame = isIdle && containsActiveWork(stripped);
|
|
556
|
+
const hasRealContent = !isNoise && stripped.trim().length > 0;
|
|
557
|
+
if (hasRealContent && (!isIdle || isMixedFrame)) {
|
|
558
|
+
lastOutputTime = Date.now();
|
|
559
|
+
}
|
|
560
|
+
if (!echoConsumed && stripped.includes(".phase-prompt.md")) {
|
|
561
|
+
echoConsumed = true;
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
if (QUEUED_MSG_RE.test(stripped)) {
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
if (PERMISSION_DIALOG_RE.test(stripped)) {
|
|
568
|
+
logger2.info("Permission dialog detected, auto-confirming", { sessionId });
|
|
569
|
+
setTimeout(() => {
|
|
570
|
+
if (!resolved) this.terminalManager.write(sessionId, "\r");
|
|
571
|
+
}, 500);
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
if (echoConsumed && !hasSubstantiveOutput && !isNoise && stripped.trim().length > 0) {
|
|
575
|
+
hasSubstantiveOutput = true;
|
|
576
|
+
}
|
|
577
|
+
outputLines.push(stripped);
|
|
578
|
+
if (onStreamEvent) {
|
|
579
|
+
for (const line of stripped.split("\n").filter((l) => l.trim())) {
|
|
580
|
+
if (!isTuiNoise(line)) {
|
|
581
|
+
onStreamEvent({
|
|
582
|
+
type: "raw",
|
|
583
|
+
content: line,
|
|
584
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
if (hasSubstantiveOutput && isIdlePrompt(stripped)) {
|
|
590
|
+
if (isMixedFrame) {
|
|
591
|
+
} else if (debounceTimer && isNoise) {
|
|
592
|
+
} else {
|
|
593
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
594
|
+
const scheduleDebounce = () => {
|
|
595
|
+
debounceTimer = setTimeout(() => {
|
|
596
|
+
if (resolved) return;
|
|
597
|
+
const recentActivityMs = Date.now() - lastOutputTime;
|
|
598
|
+
if (recentActivityMs < 5e3) {
|
|
599
|
+
scheduleDebounce();
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
const artifactReady = options.artifactCheck ? options.artifactCheck() : true;
|
|
603
|
+
if (!artifactReady) {
|
|
604
|
+
logger2.info("Idle prompt detected but artifacts not ready, continuing to wait", {
|
|
605
|
+
sessionId
|
|
606
|
+
});
|
|
607
|
+
scheduleDebounce();
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
finish({
|
|
611
|
+
output: outputLines.join(""),
|
|
612
|
+
timedOut: false,
|
|
613
|
+
timeoutType: void 0
|
|
614
|
+
});
|
|
615
|
+
}, 5e3);
|
|
616
|
+
};
|
|
617
|
+
scheduleDebounce();
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
});
|
|
621
|
+
this.terminalManager.onExit(sessionId, (exitCode) => {
|
|
622
|
+
if (!resolved) {
|
|
623
|
+
const wasKilled = this.killedSessions.delete(sessionId);
|
|
624
|
+
logger2.warn("PTY process exited during phase", { sessionId, exitCode, wasKilled });
|
|
625
|
+
for (const [wd, info] of this.sessions) {
|
|
626
|
+
if (info.sessionId === sessionId) {
|
|
627
|
+
this.sessions.delete(wd);
|
|
628
|
+
break;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
finish({
|
|
632
|
+
output: outputLines.join(""),
|
|
633
|
+
timedOut: wasKilled,
|
|
634
|
+
timeoutType: wasKilled ? "wall-clock" : void 0
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
const cleanup = () => {
|
|
639
|
+
clearTimeout(wallTimer);
|
|
640
|
+
clearInterval(idleCheck);
|
|
641
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
642
|
+
subscription.dispose();
|
|
643
|
+
};
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
export {
|
|
649
|
+
stripAnsi,
|
|
650
|
+
isIdlePrompt,
|
|
651
|
+
TRUST_DIALOG_RE,
|
|
652
|
+
PERMISSION_DIALOG_RE,
|
|
653
|
+
containsActiveWork,
|
|
654
|
+
isTuiNoise,
|
|
655
|
+
PtyRunner
|
|
656
|
+
};
|
|
657
|
+
//# sourceMappingURL=chunk-U6GWFTKA.js.map
|