@holon-run/agentinbox 0.5.0 → 0.7.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/dist/src/adapters.js +12 -6
- package/dist/src/cli.js +18 -5
- package/dist/src/daemon.js +36 -2
- package/dist/src/http.js +1 -0
- package/dist/src/logging.js +112 -0
- package/dist/src/paths.js +1 -0
- package/dist/src/runtime_gate.js +585 -35
- package/dist/src/service.js +75 -6
- package/dist/src/source_resolution.js +20 -20
- package/dist/src/source_schema.js +2 -2
- package/dist/src/sources/remote.js +30 -20
- package/dist/src/sources/remote_modules.js +418 -0
- package/dist/src/sources/remote_profiles.js +9 -383
- package/dist/src/store.js +13 -7
- package/dist/src/terminal.js +5 -1
- package/drizzle/migrations/0008_inbox_item_sequence.sql +21 -0
- package/drizzle/schema.ts +3 -0
- package/package.json +2 -1
- package/scripts/iterm2_cursor_probe.py +91 -0
package/dist/src/runtime_gate.js
CHANGED
|
@@ -10,9 +10,11 @@ const node_util_1 = require("node:util");
|
|
|
10
10
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
11
11
|
const node_path_1 = __importDefault(require("node:path"));
|
|
12
12
|
const node_os_1 = __importDefault(require("node:os"));
|
|
13
|
+
const logging_1 = require("./logging");
|
|
13
14
|
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
14
|
-
const DEFAULT_ITERM2_SAMPLE_DELAY_MS =
|
|
15
|
-
const DEFAULT_TMUX_SAMPLE_DELAY_MS =
|
|
15
|
+
const DEFAULT_ITERM2_SAMPLE_DELAY_MS = 900;
|
|
16
|
+
const DEFAULT_TMUX_SAMPLE_DELAY_MS = 900;
|
|
17
|
+
const NORMALIZED_BUFFER_TAIL_LINES = 20;
|
|
16
18
|
const ACTIVE_BUFFER_MARKERS = [
|
|
17
19
|
"esc to interrupt",
|
|
18
20
|
"Working (",
|
|
@@ -25,22 +27,36 @@ const TYPING_PROMPT_PREFIXES = [
|
|
|
25
27
|
class DefaultActivationGate {
|
|
26
28
|
runtimeProbes;
|
|
27
29
|
terminalProbes;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
30
|
+
logger;
|
|
31
|
+
constructor(runtimeProbes, terminalProbes, logger = new logging_1.NoopLogger()) {
|
|
32
|
+
this.logger = logger.child("activation_gate");
|
|
33
|
+
this.runtimeProbes = runtimeProbes ?? [
|
|
34
|
+
new CodexRuntimePresenceProbe(undefined, undefined, this.logger.child("codex_runtime")),
|
|
35
|
+
new ClaudeCodeRuntimePresenceProbe(undefined, undefined, this.logger.child("claude_runtime")),
|
|
36
|
+
];
|
|
37
|
+
this.terminalProbes = terminalProbes ?? [
|
|
38
|
+
new CodexTerminalStateProbe(undefined, undefined, undefined, undefined, undefined, this.logger.child("codex_terminal")),
|
|
39
|
+
new Iterm2TerminalStateProbe(undefined, {}, this.logger.child("iterm2_terminal")),
|
|
40
|
+
new ClaudeCodeTerminalStateProbe(undefined, undefined, undefined, this.logger.child("claude_terminal")),
|
|
41
|
+
new TmuxTerminalStateProbe(undefined, {}, this.logger.child("tmux_terminal")),
|
|
42
|
+
];
|
|
39
43
|
}
|
|
40
44
|
async evaluate(target) {
|
|
41
45
|
const runtimeProbe = this.runtimeProbes.find((probe) => probe.supports(target));
|
|
42
46
|
const runtimePresence = runtimeProbe ? await runtimeProbe.check(target) : "unknown";
|
|
43
47
|
if (runtimePresence === "gone") {
|
|
48
|
+
this.logger.debug("gate_decision", {
|
|
49
|
+
targetId: target.targetId,
|
|
50
|
+
runtimeKind: target.runtimeKind,
|
|
51
|
+
backend: target.backend,
|
|
52
|
+
runtimeProbe: runtimeProbe?.constructor?.name ?? null,
|
|
53
|
+
runtimePresence,
|
|
54
|
+
terminalProbe: null,
|
|
55
|
+
terminalPresence: null,
|
|
56
|
+
terminalBusy: null,
|
|
57
|
+
outcome: "offline",
|
|
58
|
+
reason: "runtime_gone",
|
|
59
|
+
});
|
|
44
60
|
return { outcome: "offline", reason: "runtime_gone" };
|
|
45
61
|
}
|
|
46
62
|
const terminalProbe = this.terminalProbes.find((probe) => probe.supports(target));
|
|
@@ -48,14 +64,62 @@ class DefaultActivationGate {
|
|
|
48
64
|
? await terminalProbe.check(target)
|
|
49
65
|
: { presence: "unknown", busy: "unknown" };
|
|
50
66
|
if (terminalState.presence === "gone") {
|
|
67
|
+
this.logger.debug("gate_decision", {
|
|
68
|
+
targetId: target.targetId,
|
|
69
|
+
runtimeKind: target.runtimeKind,
|
|
70
|
+
backend: target.backend,
|
|
71
|
+
runtimeProbe: runtimeProbe?.constructor?.name ?? null,
|
|
72
|
+
runtimePresence,
|
|
73
|
+
terminalProbe: terminalProbe?.constructor?.name ?? null,
|
|
74
|
+
terminalPresence: terminalState.presence,
|
|
75
|
+
terminalBusy: terminalState.busy,
|
|
76
|
+
outcome: "offline",
|
|
77
|
+
reason: "terminal_gone",
|
|
78
|
+
});
|
|
51
79
|
return { outcome: "offline", reason: "terminal_gone" };
|
|
52
80
|
}
|
|
53
81
|
if (terminalState.busy === "busy") {
|
|
82
|
+
this.logger.debug("gate_decision", {
|
|
83
|
+
targetId: target.targetId,
|
|
84
|
+
runtimeKind: target.runtimeKind,
|
|
85
|
+
backend: target.backend,
|
|
86
|
+
runtimeProbe: runtimeProbe?.constructor?.name ?? null,
|
|
87
|
+
runtimePresence,
|
|
88
|
+
terminalProbe: terminalProbe?.constructor?.name ?? null,
|
|
89
|
+
terminalPresence: terminalState.presence,
|
|
90
|
+
terminalBusy: terminalState.busy,
|
|
91
|
+
outcome: "defer",
|
|
92
|
+
reason: "terminal_busy",
|
|
93
|
+
});
|
|
54
94
|
return { outcome: "defer", reason: "terminal_busy" };
|
|
55
95
|
}
|
|
56
96
|
if (target.runtimeKind === "codex" && terminalState.busy === "idle") {
|
|
97
|
+
this.logger.debug("gate_decision", {
|
|
98
|
+
targetId: target.targetId,
|
|
99
|
+
runtimeKind: target.runtimeKind,
|
|
100
|
+
backend: target.backend,
|
|
101
|
+
runtimeProbe: runtimeProbe?.constructor?.name ?? null,
|
|
102
|
+
runtimePresence,
|
|
103
|
+
terminalProbe: terminalProbe?.constructor?.name ?? null,
|
|
104
|
+
terminalPresence: terminalState.presence,
|
|
105
|
+
terminalBusy: terminalState.busy,
|
|
106
|
+
outcome: "defer",
|
|
107
|
+
reason: "terminal_recently_active",
|
|
108
|
+
});
|
|
57
109
|
return { outcome: "defer", reason: "terminal_recently_active" };
|
|
58
110
|
}
|
|
111
|
+
this.logger.debug("gate_decision", {
|
|
112
|
+
targetId: target.targetId,
|
|
113
|
+
runtimeKind: target.runtimeKind,
|
|
114
|
+
backend: target.backend,
|
|
115
|
+
runtimeProbe: runtimeProbe?.constructor?.name ?? null,
|
|
116
|
+
runtimePresence,
|
|
117
|
+
terminalProbe: terminalProbe?.constructor?.name ?? null,
|
|
118
|
+
terminalPresence: terminalState.presence,
|
|
119
|
+
terminalBusy: terminalState.busy,
|
|
120
|
+
outcome: "inject",
|
|
121
|
+
reason: "gate_unknown_or_idle",
|
|
122
|
+
});
|
|
59
123
|
return { outcome: "inject", reason: "gate_unknown_or_idle" };
|
|
60
124
|
}
|
|
61
125
|
}
|
|
@@ -63,36 +127,45 @@ exports.DefaultActivationGate = DefaultActivationGate;
|
|
|
63
127
|
class CodexRuntimePresenceProbe {
|
|
64
128
|
killFn;
|
|
65
129
|
sessionFileReader;
|
|
66
|
-
|
|
130
|
+
logger;
|
|
131
|
+
constructor(killFn = (pid, signal) => process.kill(pid, signal), sessionFileReader, logger = new logging_1.NoopLogger()) {
|
|
67
132
|
this.killFn = killFn;
|
|
68
133
|
this.sessionFileReader = sessionFileReader;
|
|
134
|
+
this.logger = logger;
|
|
69
135
|
}
|
|
70
136
|
supports(target) {
|
|
71
137
|
return target.runtimeKind === "codex" && Number.isInteger(target.runtimePid);
|
|
72
138
|
}
|
|
73
139
|
async check(target) {
|
|
74
140
|
if (!Number.isInteger(target.runtimePid)) {
|
|
141
|
+
this.logger.trace("probe_result", { targetId: target.targetId, result: "unknown", reason: "missing_runtime_pid" });
|
|
75
142
|
return "unknown";
|
|
76
143
|
}
|
|
77
144
|
try {
|
|
78
145
|
this.killFn(target.runtimePid, 0);
|
|
79
146
|
if (!target.runtimeSessionId) {
|
|
147
|
+
this.logger.trace("probe_result", { targetId: target.targetId, result: "unknown", reason: "missing_runtime_session_id" });
|
|
80
148
|
return "unknown";
|
|
81
149
|
}
|
|
82
150
|
const sessionInfo = await (this.sessionFileReader ?? defaultCodexSessionFileReader)(target.runtimeSessionId);
|
|
83
151
|
if (sessionInfo === "unknown") {
|
|
152
|
+
this.logger.trace("probe_result", { targetId: target.targetId, result: "unknown", reason: "session_file_unknown" });
|
|
84
153
|
return "unknown";
|
|
85
154
|
}
|
|
86
155
|
if (!sessionInfo || sessionInfo.sessionId !== target.runtimeSessionId) {
|
|
156
|
+
this.logger.trace("probe_result", { targetId: target.targetId, result: "gone", reason: "session_file_mismatch" });
|
|
87
157
|
return "gone";
|
|
88
158
|
}
|
|
159
|
+
this.logger.trace("probe_result", { targetId: target.targetId, result: "alive" });
|
|
89
160
|
return "alive";
|
|
90
161
|
}
|
|
91
162
|
catch (error) {
|
|
92
163
|
const code = error instanceof Error && "code" in error ? String(error.code ?? "") : "";
|
|
93
164
|
if (code === "ESRCH") {
|
|
165
|
+
this.logger.trace("probe_result", { targetId: target.targetId, result: "gone", reason: "process_missing" });
|
|
94
166
|
return "gone";
|
|
95
167
|
}
|
|
168
|
+
this.logger.debug("probe_error", { targetId: target.targetId, error, result: "unknown" });
|
|
96
169
|
return "unknown";
|
|
97
170
|
}
|
|
98
171
|
}
|
|
@@ -104,12 +177,14 @@ class CodexTerminalStateProbe {
|
|
|
104
177
|
sessionFileReader;
|
|
105
178
|
statReader;
|
|
106
179
|
nowFn;
|
|
107
|
-
|
|
180
|
+
logger;
|
|
181
|
+
constructor(iTermProbe = new Iterm2TerminalStateProbe(), tmuxProbe = new TmuxTerminalStateProbe(), sessionFileReader, statReader, nowFn = () => Date.now(), logger = new logging_1.NoopLogger()) {
|
|
108
182
|
this.iTermProbe = iTermProbe;
|
|
109
183
|
this.tmuxProbe = tmuxProbe;
|
|
110
184
|
this.sessionFileReader = sessionFileReader;
|
|
111
185
|
this.statReader = statReader;
|
|
112
186
|
this.nowFn = nowFn;
|
|
187
|
+
this.logger = logger;
|
|
113
188
|
}
|
|
114
189
|
supports(target) {
|
|
115
190
|
return target.runtimeKind === "codex";
|
|
@@ -117,26 +192,66 @@ class CodexTerminalStateProbe {
|
|
|
117
192
|
async check(target) {
|
|
118
193
|
const terminalState = await this.readTerminalState(target);
|
|
119
194
|
if (!target.runtimeSessionId) {
|
|
195
|
+
this.logger.trace("probe_result", {
|
|
196
|
+
targetId: target.targetId,
|
|
197
|
+
result: terminalState,
|
|
198
|
+
reason: "missing_runtime_session_id",
|
|
199
|
+
});
|
|
120
200
|
return terminalState;
|
|
121
201
|
}
|
|
122
202
|
const sessionInfo = await (this.sessionFileReader ?? defaultCodexSessionFileReader)(target.runtimeSessionId);
|
|
123
203
|
if (sessionInfo === "unknown" || !sessionInfo || sessionInfo.sessionId !== target.runtimeSessionId) {
|
|
204
|
+
this.logger.trace("probe_result", {
|
|
205
|
+
targetId: target.targetId,
|
|
206
|
+
result: terminalState,
|
|
207
|
+
reason: "session_file_unavailable",
|
|
208
|
+
});
|
|
124
209
|
return terminalState;
|
|
125
210
|
}
|
|
126
211
|
const stat = await (this.statReader ?? defaultFileStatReader)(sessionInfo.filePath);
|
|
127
212
|
if (!stat) {
|
|
213
|
+
this.logger.trace("probe_result", {
|
|
214
|
+
targetId: target.targetId,
|
|
215
|
+
result: terminalState,
|
|
216
|
+
reason: "session_file_stat_missing",
|
|
217
|
+
});
|
|
128
218
|
return terminalState;
|
|
129
219
|
}
|
|
130
220
|
if (terminalState.busy === "busy" || terminalState.presence === "gone") {
|
|
221
|
+
this.logger.trace("probe_result", {
|
|
222
|
+
targetId: target.targetId,
|
|
223
|
+
result: terminalState,
|
|
224
|
+
reason: "terminal_probe_decisive",
|
|
225
|
+
});
|
|
131
226
|
return terminalState;
|
|
132
227
|
}
|
|
133
228
|
const ageMs = this.nowFn() - stat.mtimeMs;
|
|
134
229
|
if (ageMs < 5000) {
|
|
135
|
-
|
|
230
|
+
const result = { presence: terminalState.presence, busy: "busy" };
|
|
231
|
+
this.logger.trace("probe_result", {
|
|
232
|
+
targetId: target.targetId,
|
|
233
|
+
result,
|
|
234
|
+
reason: "session_file_recent_activity",
|
|
235
|
+
ageMs,
|
|
236
|
+
});
|
|
237
|
+
return result;
|
|
136
238
|
}
|
|
137
239
|
if (ageMs < 30000) {
|
|
138
|
-
|
|
139
|
-
|
|
240
|
+
const result = { presence: terminalState.presence, busy: "idle" };
|
|
241
|
+
this.logger.trace("probe_result", {
|
|
242
|
+
targetId: target.targetId,
|
|
243
|
+
result,
|
|
244
|
+
reason: "session_file_recently_active",
|
|
245
|
+
ageMs,
|
|
246
|
+
});
|
|
247
|
+
return result;
|
|
248
|
+
}
|
|
249
|
+
this.logger.trace("probe_result", {
|
|
250
|
+
targetId: target.targetId,
|
|
251
|
+
result: terminalState,
|
|
252
|
+
reason: "session_file_stale",
|
|
253
|
+
ageMs,
|
|
254
|
+
});
|
|
140
255
|
return terminalState;
|
|
141
256
|
}
|
|
142
257
|
async readTerminalState(target) {
|
|
@@ -153,9 +268,11 @@ exports.CodexTerminalStateProbe = CodexTerminalStateProbe;
|
|
|
153
268
|
class Iterm2TerminalStateProbe {
|
|
154
269
|
execAsync;
|
|
155
270
|
options;
|
|
156
|
-
|
|
271
|
+
logger;
|
|
272
|
+
constructor(execAsync = execFileAsync, options = {}, logger = new logging_1.NoopLogger()) {
|
|
157
273
|
this.execAsync = execAsync;
|
|
158
274
|
this.options = options;
|
|
275
|
+
this.logger = logger;
|
|
159
276
|
}
|
|
160
277
|
supports(target) {
|
|
161
278
|
// Only support iTerm2 targets that are NOT Claude Code
|
|
@@ -168,13 +285,203 @@ class Iterm2TerminalStateProbe {
|
|
|
168
285
|
async check(target) {
|
|
169
286
|
const sessionId = target.itermSessionId?.trim();
|
|
170
287
|
if (!sessionId) {
|
|
288
|
+
this.logger.trace("probe_result", {
|
|
289
|
+
targetId: target.targetId,
|
|
290
|
+
result: { presence: "unknown", busy: "unknown" },
|
|
291
|
+
reason: "missing_iterm_session_id",
|
|
292
|
+
});
|
|
171
293
|
return { presence: "unknown", busy: "unknown" };
|
|
172
294
|
}
|
|
295
|
+
// Try Python API probe first (if enabled)
|
|
296
|
+
if (this.options.enablePythonProbe !== false) {
|
|
297
|
+
const pythonResult = await this.tryPythonApiProbe(sessionId, target.runtimeKind);
|
|
298
|
+
if (pythonResult && pythonResult.presence !== "unknown") {
|
|
299
|
+
this.logger.debug("probe_result", {
|
|
300
|
+
targetId: target.targetId,
|
|
301
|
+
path: "python",
|
|
302
|
+
result: pythonResult,
|
|
303
|
+
});
|
|
304
|
+
return pythonResult;
|
|
305
|
+
}
|
|
306
|
+
this.logger.trace("probe_fallback", {
|
|
307
|
+
targetId: target.targetId,
|
|
308
|
+
from: "python",
|
|
309
|
+
to: "cli",
|
|
310
|
+
reason: pythonResult ? "python_presence_unknown" : "python_probe_unavailable",
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
// Fallback to command-line tools
|
|
314
|
+
const result = await this.checkWithCommandLineTools(sessionId, target.targetId);
|
|
315
|
+
this.logger.debug("probe_result", {
|
|
316
|
+
targetId: target.targetId,
|
|
317
|
+
path: "cli",
|
|
318
|
+
result,
|
|
319
|
+
});
|
|
320
|
+
return result;
|
|
321
|
+
}
|
|
322
|
+
async tryPythonApiProbe(sessionId, runtimeKind) {
|
|
323
|
+
try {
|
|
324
|
+
const scriptPath = this.options.pythonScriptPath ?? resolvePythonProbeScript();
|
|
325
|
+
const result = await this.execAsync("python3", [scriptPath, sessionId], {
|
|
326
|
+
env: { ...process.env, PYTHONIOENCODING: "utf-8" }
|
|
327
|
+
});
|
|
328
|
+
const data = JSON.parse(result.stdout);
|
|
329
|
+
if (data.status === "gone") {
|
|
330
|
+
this.logger.trace("probe_python_result", {
|
|
331
|
+
sessionId,
|
|
332
|
+
result: { presence: "gone", busy: "unknown" },
|
|
333
|
+
reason: "session_gone",
|
|
334
|
+
});
|
|
335
|
+
return { presence: "gone", busy: "unknown" };
|
|
336
|
+
}
|
|
337
|
+
if (data.status === "available") {
|
|
338
|
+
// If we have cursor and lines data, use cursor-aware detection
|
|
339
|
+
if (data.cursor && Array.isArray(data.lines)) {
|
|
340
|
+
const bufferTail = normalizeProbeTailFromLines(data.lines);
|
|
341
|
+
if (!bufferTail) {
|
|
342
|
+
this.logger.trace("probe_python_result", {
|
|
343
|
+
sessionId,
|
|
344
|
+
result: { presence: "available", busy: "unknown" },
|
|
345
|
+
reason: "empty_normalized_buffer",
|
|
346
|
+
});
|
|
347
|
+
return { presence: "available", busy: "unknown" };
|
|
348
|
+
}
|
|
349
|
+
const cursorPosition = { x: data.cursor.x, y: data.cursor.y };
|
|
350
|
+
const screenHeight = data.screen_height;
|
|
351
|
+
const startLine = data.start_line ?? 0;
|
|
352
|
+
// Perform cursor-aware typing detection
|
|
353
|
+
const busyStatus = evaluatePythonCursorAwareTypingPrompt(bufferTail, cursorPosition, screenHeight, startLine, runtimeKind);
|
|
354
|
+
// If cursor-aware detection indicates busy, return immediately
|
|
355
|
+
if (busyStatus === "busy") {
|
|
356
|
+
this.logger.trace("probe_python_result", {
|
|
357
|
+
sessionId,
|
|
358
|
+
result: { presence: "available", busy: "busy" },
|
|
359
|
+
reason: "cursor_aware_busy",
|
|
360
|
+
});
|
|
361
|
+
return { presence: "available", busy: "busy" };
|
|
362
|
+
}
|
|
363
|
+
// If cursor-aware detection is certain it's not busy, don't apply prompt heuristics
|
|
364
|
+
if (busyStatus === "not_busy") {
|
|
365
|
+
// Still check for buffer changes and active markers
|
|
366
|
+
await (this.options.sleep ?? sleep)(this.options.sampleDelayMs ?? DEFAULT_ITERM2_SAMPLE_DELAY_MS);
|
|
367
|
+
const secondResult = await this.execAsync("python3", [scriptPath, sessionId], {
|
|
368
|
+
env: { ...process.env, PYTHONIOENCODING: "utf-8" }
|
|
369
|
+
});
|
|
370
|
+
const secondData = JSON.parse(secondResult.stdout);
|
|
371
|
+
if (secondData.status === "available" && Array.isArray(secondData.lines)) {
|
|
372
|
+
const secondBufferTail = normalizeProbeTailFromLines(secondData.lines);
|
|
373
|
+
if (secondBufferTail && bufferTail !== secondBufferTail) {
|
|
374
|
+
this.logger.trace("probe_python_result", {
|
|
375
|
+
sessionId,
|
|
376
|
+
result: { presence: "available", busy: "busy" },
|
|
377
|
+
reason: "buffer_changed",
|
|
378
|
+
});
|
|
379
|
+
return { presence: "available", busy: "busy" };
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
// Check for active buffer markers
|
|
383
|
+
if (containsActiveBufferMarker(bufferTail)) {
|
|
384
|
+
this.logger.trace("probe_python_result", {
|
|
385
|
+
sessionId,
|
|
386
|
+
result: { presence: "available", busy: "busy" },
|
|
387
|
+
reason: "active_buffer_marker",
|
|
388
|
+
});
|
|
389
|
+
return { presence: "available", busy: "busy" };
|
|
390
|
+
}
|
|
391
|
+
this.logger.trace("probe_python_result", {
|
|
392
|
+
sessionId,
|
|
393
|
+
result: { presence: "available", busy: "unknown" },
|
|
394
|
+
reason: "cursor_aware_not_busy",
|
|
395
|
+
});
|
|
396
|
+
return { presence: "available", busy: "unknown" };
|
|
397
|
+
}
|
|
398
|
+
// If cursor-aware detection is uncertain, fall back to buffer change detection only
|
|
399
|
+
// Do NOT use generic typing prompts to avoid false positives (see #116)
|
|
400
|
+
if (busyStatus === "unknown") {
|
|
401
|
+
await (this.options.sleep ?? sleep)(this.options.sampleDelayMs ?? DEFAULT_ITERM2_SAMPLE_DELAY_MS);
|
|
402
|
+
const secondResult = await this.execAsync("python3", [scriptPath, sessionId], {
|
|
403
|
+
env: { ...process.env, PYTHONIOENCODING: "utf-8" }
|
|
404
|
+
});
|
|
405
|
+
const secondData = JSON.parse(secondResult.stdout);
|
|
406
|
+
if (secondData.status === "available" && Array.isArray(secondData.lines)) {
|
|
407
|
+
const secondBufferTail = normalizeProbeTailFromLines(secondData.lines);
|
|
408
|
+
if (secondBufferTail && bufferTail !== secondBufferTail) {
|
|
409
|
+
this.logger.trace("probe_python_result", {
|
|
410
|
+
sessionId,
|
|
411
|
+
result: { presence: "available", busy: "busy" },
|
|
412
|
+
reason: "buffer_changed_after_unknown_cursor_state",
|
|
413
|
+
});
|
|
414
|
+
return { presence: "available", busy: "busy" };
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
// Check for active buffer markers
|
|
418
|
+
if (containsActiveBufferMarker(bufferTail)) {
|
|
419
|
+
this.logger.trace("probe_python_result", {
|
|
420
|
+
sessionId,
|
|
421
|
+
result: { presence: "available", busy: "busy" },
|
|
422
|
+
reason: "active_buffer_marker_after_unknown_cursor_state",
|
|
423
|
+
});
|
|
424
|
+
return { presence: "available", busy: "busy" };
|
|
425
|
+
}
|
|
426
|
+
// Return unknown when cursor-aware detection is uncertain
|
|
427
|
+
// Do NOT fall back to generic typing prompts to avoid false positives
|
|
428
|
+
this.logger.trace("probe_python_result", {
|
|
429
|
+
sessionId,
|
|
430
|
+
result: { presence: "available", busy: "unknown" },
|
|
431
|
+
reason: "cursor_aware_unknown",
|
|
432
|
+
});
|
|
433
|
+
return { presence: "available", busy: "unknown" };
|
|
434
|
+
}
|
|
435
|
+
// This should never be reached as all cases are handled above
|
|
436
|
+
this.logger.trace("probe_python_result", {
|
|
437
|
+
sessionId,
|
|
438
|
+
result: { presence: "available", busy: "unknown" },
|
|
439
|
+
reason: "cursor_probe_fell_through",
|
|
440
|
+
});
|
|
441
|
+
return { presence: "available", busy: "unknown" };
|
|
442
|
+
}
|
|
443
|
+
// If we don't have cursor data, return unknown to trigger fallback
|
|
444
|
+
this.logger.trace("probe_python_result", {
|
|
445
|
+
sessionId,
|
|
446
|
+
result: null,
|
|
447
|
+
reason: "missing_cursor_data",
|
|
448
|
+
});
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
// Any other status means we should fall back
|
|
452
|
+
this.logger.trace("probe_python_result", {
|
|
453
|
+
sessionId,
|
|
454
|
+
result: null,
|
|
455
|
+
reason: "unexpected_python_status",
|
|
456
|
+
status: data.status,
|
|
457
|
+
});
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
catch (error) {
|
|
461
|
+
// Python API probe failed, fall back to command-line tools
|
|
462
|
+
this.logger.trace("probe_fallback", {
|
|
463
|
+
sessionId,
|
|
464
|
+
from: "python",
|
|
465
|
+
to: "cli",
|
|
466
|
+
reason: "python_probe_error",
|
|
467
|
+
error,
|
|
468
|
+
});
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
async checkWithCommandLineTools(sessionId, targetId) {
|
|
173
473
|
let it2api;
|
|
174
474
|
try {
|
|
175
475
|
it2api = resolveIterm2ApiPath(this.options.iterm2ApiPath);
|
|
176
476
|
}
|
|
177
|
-
catch {
|
|
477
|
+
catch (error) {
|
|
478
|
+
this.logger.trace("probe_fallback", {
|
|
479
|
+
targetId,
|
|
480
|
+
from: "cli",
|
|
481
|
+
to: null,
|
|
482
|
+
reason: "it2api_unavailable",
|
|
483
|
+
error,
|
|
484
|
+
});
|
|
178
485
|
return { presence: "unknown", busy: "unknown" };
|
|
179
486
|
}
|
|
180
487
|
const presence = await this.checkSessionPresence(it2api, sessionId);
|
|
@@ -192,14 +499,27 @@ class Iterm2TerminalStateProbe {
|
|
|
192
499
|
return { presence: "available", busy: "unknown" };
|
|
193
500
|
}
|
|
194
501
|
if (first !== second) {
|
|
502
|
+
this.logger.trace("probe_cli_result", {
|
|
503
|
+
sessionId,
|
|
504
|
+
result: { presence: "available", busy: "busy" },
|
|
505
|
+
reason: "buffer_changed",
|
|
506
|
+
});
|
|
195
507
|
return { presence: "available", busy: "busy" };
|
|
196
508
|
}
|
|
197
509
|
if (containsActiveBufferMarker(second)) {
|
|
510
|
+
this.logger.trace("probe_cli_result", {
|
|
511
|
+
sessionId,
|
|
512
|
+
result: { presence: "available", busy: "busy" },
|
|
513
|
+
reason: "active_buffer_marker",
|
|
514
|
+
});
|
|
198
515
|
return { presence: "available", busy: "busy" };
|
|
199
516
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
517
|
+
// Do NOT use generic typing prompts to avoid false positives (see #116)
|
|
518
|
+
this.logger.trace("probe_cli_result", {
|
|
519
|
+
sessionId,
|
|
520
|
+
result: { presence: "available", busy: "unknown" },
|
|
521
|
+
reason: "stable_quiet_buffer",
|
|
522
|
+
});
|
|
203
523
|
return { presence: "available", busy: "unknown" };
|
|
204
524
|
}
|
|
205
525
|
async checkSessionPresence(it2api, sessionId) {
|
|
@@ -208,7 +528,13 @@ class Iterm2TerminalStateProbe {
|
|
|
208
528
|
const sessionIds = parseIterm2SessionIds(result.stdout);
|
|
209
529
|
return sessionIds.includes(sessionId) ? "available" : "gone";
|
|
210
530
|
}
|
|
211
|
-
catch {
|
|
531
|
+
catch (error) {
|
|
532
|
+
this.logger.trace("probe_error", {
|
|
533
|
+
sessionId,
|
|
534
|
+
step: "list_sessions",
|
|
535
|
+
error,
|
|
536
|
+
result: "unknown",
|
|
537
|
+
});
|
|
212
538
|
return "unknown";
|
|
213
539
|
}
|
|
214
540
|
}
|
|
@@ -234,9 +560,11 @@ exports.Iterm2TerminalStateProbe = Iterm2TerminalStateProbe;
|
|
|
234
560
|
class TmuxTerminalStateProbe {
|
|
235
561
|
execAsync;
|
|
236
562
|
options;
|
|
237
|
-
|
|
563
|
+
logger;
|
|
564
|
+
constructor(execAsync = execFileAsync, options = {}, logger = new logging_1.NoopLogger()) {
|
|
238
565
|
this.execAsync = execAsync;
|
|
239
566
|
this.options = options;
|
|
567
|
+
this.logger = logger;
|
|
240
568
|
}
|
|
241
569
|
supports(target) {
|
|
242
570
|
return target.backend === "tmux" && typeof target.tmuxPaneId === "string" && target.tmuxPaneId.trim().length > 0;
|
|
@@ -244,16 +572,39 @@ class TmuxTerminalStateProbe {
|
|
|
244
572
|
async check(target) {
|
|
245
573
|
const paneId = target.tmuxPaneId?.trim();
|
|
246
574
|
if (!paneId) {
|
|
575
|
+
this.logger.trace("probe_result", {
|
|
576
|
+
targetId: target.targetId,
|
|
577
|
+
result: { presence: "unknown", busy: "unknown" },
|
|
578
|
+
reason: "missing_tmux_pane_id",
|
|
579
|
+
});
|
|
247
580
|
return { presence: "unknown", busy: "unknown" };
|
|
248
581
|
}
|
|
249
582
|
const paneState = await this.readPaneState(paneId);
|
|
250
583
|
if (paneState === "missing") {
|
|
584
|
+
this.logger.trace("probe_result", {
|
|
585
|
+
targetId: target.targetId,
|
|
586
|
+
paneId,
|
|
587
|
+
result: { presence: "gone", busy: "unknown" },
|
|
588
|
+
reason: "pane_missing",
|
|
589
|
+
});
|
|
251
590
|
return { presence: "gone", busy: "unknown" };
|
|
252
591
|
}
|
|
253
592
|
if (paneState === "unknown") {
|
|
593
|
+
this.logger.trace("probe_result", {
|
|
594
|
+
targetId: target.targetId,
|
|
595
|
+
paneId,
|
|
596
|
+
result: { presence: "unknown", busy: "unknown" },
|
|
597
|
+
reason: "pane_state_unknown",
|
|
598
|
+
});
|
|
254
599
|
return { presence: "unknown", busy: "unknown" };
|
|
255
600
|
}
|
|
256
601
|
if (paneState.dead) {
|
|
602
|
+
this.logger.trace("probe_result", {
|
|
603
|
+
targetId: target.targetId,
|
|
604
|
+
paneId,
|
|
605
|
+
result: { presence: "gone", busy: "unknown" },
|
|
606
|
+
reason: "pane_dead",
|
|
607
|
+
});
|
|
257
608
|
return { presence: "gone", busy: "unknown" };
|
|
258
609
|
}
|
|
259
610
|
const first = await this.readBufferTail(paneId);
|
|
@@ -266,26 +617,62 @@ class TmuxTerminalStateProbe {
|
|
|
266
617
|
return { presence: "available", busy: "unknown" };
|
|
267
618
|
}
|
|
268
619
|
if (first !== second) {
|
|
620
|
+
this.logger.trace("probe_result", {
|
|
621
|
+
targetId: target.targetId,
|
|
622
|
+
paneId,
|
|
623
|
+
result: { presence: "available", busy: "busy" },
|
|
624
|
+
reason: "buffer_changed",
|
|
625
|
+
});
|
|
269
626
|
return { presence: "available", busy: "busy" };
|
|
270
627
|
}
|
|
271
628
|
if (containsActiveBufferMarker(second)) {
|
|
629
|
+
this.logger.trace("probe_result", {
|
|
630
|
+
targetId: target.targetId,
|
|
631
|
+
paneId,
|
|
632
|
+
result: { presence: "available", busy: "busy" },
|
|
633
|
+
reason: "active_buffer_marker",
|
|
634
|
+
});
|
|
272
635
|
return { presence: "available", busy: "busy" };
|
|
273
636
|
}
|
|
274
|
-
|
|
637
|
+
// Use cursor-aware detection when available
|
|
638
|
+
const cursorHint = evaluateCursorAwareTypingPrompt(target, paneState, second);
|
|
639
|
+
if (cursorHint === "busy") {
|
|
640
|
+
this.logger.trace("probe_result", {
|
|
641
|
+
targetId: target.targetId,
|
|
642
|
+
paneId,
|
|
643
|
+
result: { presence: "available", busy: "busy" },
|
|
644
|
+
reason: "cursor_aware_busy",
|
|
645
|
+
});
|
|
275
646
|
return { presence: "available", busy: "busy" };
|
|
276
647
|
}
|
|
648
|
+
// Do NOT fall back to generic typing prompts when cursorHint is unknown to avoid false positives (see #116)
|
|
649
|
+
this.logger.trace("probe_result", {
|
|
650
|
+
targetId: target.targetId,
|
|
651
|
+
paneId,
|
|
652
|
+
result: { presence: "available", busy: "unknown" },
|
|
653
|
+
reason: cursorHint === "not_busy" ? "cursor_aware_not_busy" : "cursor_aware_unknown",
|
|
654
|
+
});
|
|
277
655
|
return { presence: "available", busy: "unknown" };
|
|
278
656
|
}
|
|
279
657
|
async readPaneState(paneId) {
|
|
280
658
|
try {
|
|
281
|
-
const result = await this.execAsync("tmux", [
|
|
282
|
-
|
|
659
|
+
const result = await this.execAsync("tmux", [
|
|
660
|
+
"display-message",
|
|
661
|
+
"-p",
|
|
662
|
+
"-t",
|
|
663
|
+
paneId,
|
|
664
|
+
"#{pane_active} #{pane_dead} #{cursor_x} #{cursor_y} #{pane_height}",
|
|
665
|
+
]);
|
|
666
|
+
const [active, dead, cursorX, cursorY, height] = result.stdout.trim().split(/\s+/, 5);
|
|
283
667
|
if (!active || !dead) {
|
|
284
668
|
return "unknown";
|
|
285
669
|
}
|
|
286
670
|
return {
|
|
287
671
|
active: active === "1",
|
|
288
672
|
dead: dead === "1",
|
|
673
|
+
cursorX: parseOptionalInt(cursorX),
|
|
674
|
+
cursorY: parseOptionalInt(cursorY),
|
|
675
|
+
height: parseOptionalInt(height),
|
|
289
676
|
};
|
|
290
677
|
}
|
|
291
678
|
catch (error) {
|
|
@@ -297,7 +684,7 @@ class TmuxTerminalStateProbe {
|
|
|
297
684
|
}
|
|
298
685
|
async readBufferTail(paneId) {
|
|
299
686
|
try {
|
|
300
|
-
const result = await this.execAsync("tmux", ["capture-pane", "-p", "-t", paneId, "-S",
|
|
687
|
+
const result = await this.execAsync("tmux", ["capture-pane", "-p", "-t", paneId, "-S", `-${NORMALIZED_BUFFER_TAIL_LINES}`]);
|
|
301
688
|
return normalizeBufferTail(result.stdout);
|
|
302
689
|
}
|
|
303
690
|
catch {
|
|
@@ -309,7 +696,7 @@ exports.TmuxTerminalStateProbe = TmuxTerminalStateProbe;
|
|
|
309
696
|
function containsActiveBufferMarker(bufferTail) {
|
|
310
697
|
return ACTIVE_BUFFER_MARKERS.some((marker) => bufferTail.includes(marker));
|
|
311
698
|
}
|
|
312
|
-
function hasVisibleTypingPrompt(bufferTail) {
|
|
699
|
+
function hasVisibleTypingPrompt(bufferTail, prefixes = TYPING_PROMPT_PREFIXES) {
|
|
313
700
|
const lines = bufferTail
|
|
314
701
|
.split("\n")
|
|
315
702
|
.map((line) => line.trimEnd())
|
|
@@ -318,23 +705,77 @@ function hasVisibleTypingPrompt(bufferTail) {
|
|
|
318
705
|
if (!lastLine) {
|
|
319
706
|
return false;
|
|
320
707
|
}
|
|
321
|
-
for (const prefix of
|
|
708
|
+
for (const prefix of prefixes) {
|
|
322
709
|
if (lastLine.startsWith(prefix) && lastLine.slice(prefix.length).trim().length > 0) {
|
|
323
710
|
return true;
|
|
324
711
|
}
|
|
325
712
|
}
|
|
326
713
|
return false;
|
|
327
714
|
}
|
|
715
|
+
function evaluateCursorAwareTypingPrompt(target, paneState, bufferTail) {
|
|
716
|
+
const prefix = runtimeTypingPromptPrefix(target.runtimeKind);
|
|
717
|
+
if (!prefix) {
|
|
718
|
+
return "unknown";
|
|
719
|
+
}
|
|
720
|
+
if (paneState.cursorX == null || paneState.cursorY == null || paneState.height == null) {
|
|
721
|
+
return "unknown";
|
|
722
|
+
}
|
|
723
|
+
const lines = bufferTail
|
|
724
|
+
.replace(/\r/g, "")
|
|
725
|
+
.split("\n")
|
|
726
|
+
.map((line) => line.trimEnd());
|
|
727
|
+
if (lines.length === 0) {
|
|
728
|
+
return "unknown";
|
|
729
|
+
}
|
|
730
|
+
const captureStartRow = Math.max(0, paneState.height - lines.length);
|
|
731
|
+
const lineIndex = paneState.cursorY - captureStartRow;
|
|
732
|
+
if (lineIndex < 0 || lineIndex >= lines.length) {
|
|
733
|
+
return "unknown";
|
|
734
|
+
}
|
|
735
|
+
const line = lines[lineIndex] ?? "";
|
|
736
|
+
if (!line.startsWith(prefix)) {
|
|
737
|
+
return "not_busy";
|
|
738
|
+
}
|
|
739
|
+
return paneState.cursorX > prefix.length ? "busy" : "not_busy";
|
|
740
|
+
}
|
|
741
|
+
function runtimeTypingPromptPrefix(runtimeKind) {
|
|
742
|
+
if (runtimeKind === "codex") {
|
|
743
|
+
return "› ";
|
|
744
|
+
}
|
|
745
|
+
if (runtimeKind === "claude_code") {
|
|
746
|
+
return "❯ ";
|
|
747
|
+
}
|
|
748
|
+
return null;
|
|
749
|
+
}
|
|
750
|
+
function typingPromptPrefixesForRuntime(runtimeKind) {
|
|
751
|
+
if (runtimeKind === "codex") {
|
|
752
|
+
return ["› "];
|
|
753
|
+
}
|
|
754
|
+
if (runtimeKind === "claude_code") {
|
|
755
|
+
return ["❯ ", "› "];
|
|
756
|
+
}
|
|
757
|
+
return TYPING_PROMPT_PREFIXES;
|
|
758
|
+
}
|
|
759
|
+
function parseOptionalInt(value) {
|
|
760
|
+
if (!value) {
|
|
761
|
+
return null;
|
|
762
|
+
}
|
|
763
|
+
const parsed = Number.parseInt(value, 10);
|
|
764
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
765
|
+
}
|
|
328
766
|
function normalizeBufferTail(buffer) {
|
|
329
767
|
const normalized = buffer
|
|
330
768
|
.replace(/\r/g, "")
|
|
331
769
|
.split("\n")
|
|
332
770
|
.map((line) => line.trimEnd())
|
|
333
|
-
.slice(-
|
|
771
|
+
.slice(-NORMALIZED_BUFFER_TAIL_LINES)
|
|
334
772
|
.join("\n")
|
|
335
773
|
.trim();
|
|
336
774
|
return normalized.length > 0 ? normalized : null;
|
|
337
775
|
}
|
|
776
|
+
function normalizeProbeTailFromLines(lines) {
|
|
777
|
+
return normalizeBufferTail(lines.join("\n"));
|
|
778
|
+
}
|
|
338
779
|
function resolveIterm2ApiPath(override) {
|
|
339
780
|
if (override) {
|
|
340
781
|
return override;
|
|
@@ -471,15 +912,18 @@ function isTmuxMissingPaneError(error) {
|
|
|
471
912
|
class ClaudeCodeRuntimePresenceProbe {
|
|
472
913
|
killFn;
|
|
473
914
|
sessionFileReader;
|
|
474
|
-
|
|
915
|
+
logger;
|
|
916
|
+
constructor(killFn = (pid, signal) => process.kill(pid, signal), sessionFileReader, logger = new logging_1.NoopLogger()) {
|
|
475
917
|
this.killFn = killFn;
|
|
476
918
|
this.sessionFileReader = sessionFileReader;
|
|
919
|
+
this.logger = logger;
|
|
477
920
|
}
|
|
478
921
|
supports(target) {
|
|
479
922
|
return target.runtimeKind === "claude_code" && Number.isInteger(target.runtimePid);
|
|
480
923
|
}
|
|
481
924
|
async check(target) {
|
|
482
925
|
if (!Number.isInteger(target.runtimePid)) {
|
|
926
|
+
this.logger.trace("probe_result", { targetId: target.targetId, result: "unknown", reason: "missing_runtime_pid" });
|
|
483
927
|
return "unknown";
|
|
484
928
|
}
|
|
485
929
|
try {
|
|
@@ -488,19 +932,24 @@ class ClaudeCodeRuntimePresenceProbe {
|
|
|
488
932
|
// 2. Verify session file exists and is valid
|
|
489
933
|
const sessionInfo = await (this.sessionFileReader ?? this.defaultSessionFileReader)(target.runtimePid);
|
|
490
934
|
if (!sessionInfo) {
|
|
935
|
+
this.logger.trace("probe_result", { targetId: target.targetId, result: "gone", reason: "session_file_missing" });
|
|
491
936
|
return "gone"; // Process exists but session file is invalid/missing
|
|
492
937
|
}
|
|
493
938
|
// 3. Verify session ID matches (prevent PID reuse)
|
|
494
939
|
if (target.runtimeSessionId && sessionInfo.sessionId !== target.runtimeSessionId) {
|
|
940
|
+
this.logger.trace("probe_result", { targetId: target.targetId, result: "gone", reason: "session_id_mismatch" });
|
|
495
941
|
return "gone";
|
|
496
942
|
}
|
|
943
|
+
this.logger.trace("probe_result", { targetId: target.targetId, result: "alive" });
|
|
497
944
|
return "alive";
|
|
498
945
|
}
|
|
499
946
|
catch (error) {
|
|
500
947
|
const code = error instanceof Error && "code" in error ? String(error.code ?? "") : "";
|
|
501
948
|
if (code === "ESRCH") {
|
|
949
|
+
this.logger.trace("probe_result", { targetId: target.targetId, result: "gone", reason: "process_missing" });
|
|
502
950
|
return "gone"; // Process doesn't exist
|
|
503
951
|
}
|
|
952
|
+
this.logger.debug("probe_error", { targetId: target.targetId, error, result: "unknown" });
|
|
504
953
|
return "unknown";
|
|
505
954
|
}
|
|
506
955
|
}
|
|
@@ -527,26 +976,43 @@ class ClaudeCodeTerminalStateProbe {
|
|
|
527
976
|
sessionFileReader;
|
|
528
977
|
logFileStatReader;
|
|
529
978
|
sleepFn;
|
|
530
|
-
|
|
979
|
+
logger;
|
|
980
|
+
constructor(sessionFileReader, logFileStatReader, sleepFn = sleep, logger = new logging_1.NoopLogger()) {
|
|
531
981
|
this.sessionFileReader = sessionFileReader;
|
|
532
982
|
this.logFileStatReader = logFileStatReader;
|
|
533
983
|
this.sleepFn = sleepFn;
|
|
984
|
+
this.logger = logger;
|
|
534
985
|
}
|
|
535
986
|
supports(target) {
|
|
536
987
|
return target.runtimeKind === "claude_code";
|
|
537
988
|
}
|
|
538
989
|
async check(target) {
|
|
539
990
|
if (!target.runtimePid || !target.runtimeSessionId) {
|
|
991
|
+
this.logger.trace("probe_result", {
|
|
992
|
+
targetId: target.targetId,
|
|
993
|
+
result: { presence: "unknown", busy: "unknown" },
|
|
994
|
+
reason: "missing_runtime_identity",
|
|
995
|
+
});
|
|
540
996
|
return { presence: "unknown", busy: "unknown" };
|
|
541
997
|
}
|
|
542
998
|
try {
|
|
543
999
|
// 1. Read session file to get cwd
|
|
544
1000
|
const sessionData = await (this.sessionFileReader ?? this.defaultSessionFileReader)(target.runtimePid);
|
|
545
1001
|
if (!sessionData) {
|
|
1002
|
+
this.logger.trace("probe_result", {
|
|
1003
|
+
targetId: target.targetId,
|
|
1004
|
+
result: { presence: "gone", busy: "unknown" },
|
|
1005
|
+
reason: "session_file_missing",
|
|
1006
|
+
});
|
|
546
1007
|
return { presence: "gone", busy: "unknown" };
|
|
547
1008
|
}
|
|
548
1009
|
// 2. Verify session ID matches
|
|
549
1010
|
if (sessionData.sessionId !== target.runtimeSessionId) {
|
|
1011
|
+
this.logger.trace("probe_result", {
|
|
1012
|
+
targetId: target.targetId,
|
|
1013
|
+
result: { presence: "gone", busy: "unknown" },
|
|
1014
|
+
reason: "session_id_mismatch",
|
|
1015
|
+
});
|
|
550
1016
|
return { presence: "gone", busy: "unknown" };
|
|
551
1017
|
}
|
|
552
1018
|
// 3. Construct log file path using Claude Code's encoding rules
|
|
@@ -558,10 +1024,18 @@ class ClaudeCodeTerminalStateProbe {
|
|
|
558
1024
|
const presence = target.backend === "iterm2" && target.itermSessionId
|
|
559
1025
|
? await this.checkIterm2Presence(target.itermSessionId)
|
|
560
1026
|
: "available";
|
|
1027
|
+
this.logger.trace("probe_result", {
|
|
1028
|
+
targetId: target.targetId,
|
|
1029
|
+
result: { presence, busy },
|
|
1030
|
+
});
|
|
561
1031
|
return { presence, busy };
|
|
562
1032
|
}
|
|
563
1033
|
catch (error) {
|
|
564
|
-
|
|
1034
|
+
this.logger.debug("probe_error", {
|
|
1035
|
+
targetId: target.targetId,
|
|
1036
|
+
error,
|
|
1037
|
+
result: { presence: "unknown", busy: "unknown" },
|
|
1038
|
+
});
|
|
565
1039
|
return { presence: "unknown", busy: "unknown" };
|
|
566
1040
|
}
|
|
567
1041
|
}
|
|
@@ -643,3 +1117,79 @@ function parseIterm2SessionIds(stdout) {
|
|
|
643
1117
|
})
|
|
644
1118
|
.filter((id) => id !== null);
|
|
645
1119
|
}
|
|
1120
|
+
function resolvePythonProbeScript(override) {
|
|
1121
|
+
if (override) {
|
|
1122
|
+
return override;
|
|
1123
|
+
}
|
|
1124
|
+
const scriptRelativePath = node_path_1.default.join("scripts", "iterm2_cursor_probe.py");
|
|
1125
|
+
const seen = new Set();
|
|
1126
|
+
const candidates = [];
|
|
1127
|
+
const addCandidate = (candidate) => {
|
|
1128
|
+
const normalized = node_path_1.default.normalize(candidate);
|
|
1129
|
+
if (!seen.has(normalized)) {
|
|
1130
|
+
seen.add(normalized);
|
|
1131
|
+
candidates.push(normalized);
|
|
1132
|
+
}
|
|
1133
|
+
};
|
|
1134
|
+
// Search upward from the current module's directory
|
|
1135
|
+
let currentDir = __dirname;
|
|
1136
|
+
for (let i = 0; i < 5; i++) { // Limit upward search depth
|
|
1137
|
+
addCandidate(node_path_1.default.join(currentDir, scriptRelativePath));
|
|
1138
|
+
addCandidate(node_path_1.default.join(currentDir, "dist", scriptRelativePath));
|
|
1139
|
+
addCandidate(node_path_1.default.join(currentDir, "dist", "src", scriptRelativePath));
|
|
1140
|
+
const parentDir = node_path_1.default.dirname(currentDir);
|
|
1141
|
+
if (parentDir === currentDir) {
|
|
1142
|
+
break;
|
|
1143
|
+
}
|
|
1144
|
+
currentDir = parentDir;
|
|
1145
|
+
}
|
|
1146
|
+
// Check process working directory and its dist paths
|
|
1147
|
+
addCandidate(node_path_1.default.join(process.cwd(), scriptRelativePath));
|
|
1148
|
+
addCandidate(node_path_1.default.join(process.cwd(), "dist", scriptRelativePath));
|
|
1149
|
+
addCandidate(node_path_1.default.join(process.cwd(), "dist", "src", scriptRelativePath));
|
|
1150
|
+
// Check all candidates
|
|
1151
|
+
for (const candidate of candidates) {
|
|
1152
|
+
if (node_fs_1.default.existsSync(candidate)) {
|
|
1153
|
+
return candidate;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
throw new Error(`Unable to locate iterm2_cursor_probe.py. Checked: ${candidates.join(", ")}. ` +
|
|
1157
|
+
"Ensure scripts/iterm2_cursor_probe.py is shipped with the package or copied into dist/ during build.");
|
|
1158
|
+
}
|
|
1159
|
+
function evaluatePythonCursorAwareTypingPrompt(bufferTail, cursorPosition, screenHeight, startLine, runtimeKind) {
|
|
1160
|
+
const lines = bufferTail.split("\n");
|
|
1161
|
+
const lastLineIndex = lines.length - 1;
|
|
1162
|
+
// Cursor coordinates are in full-screen rows, while bufferTail only contains
|
|
1163
|
+
// the lines from startLine to end of screen. First ensure the cursor is on the
|
|
1164
|
+
// terminal's last visible row, then map that row into the captured window.
|
|
1165
|
+
if (cursorPosition.y !== screenHeight - 1) {
|
|
1166
|
+
return "not_busy";
|
|
1167
|
+
}
|
|
1168
|
+
// Map the cursor position into the captured buffer window using startLine
|
|
1169
|
+
const cursorRowInBuffer = cursorPosition.y - startLine;
|
|
1170
|
+
// If the last visible screen row is not represented by the captured tail,
|
|
1171
|
+
// or does not map to the last captured line, we cannot evaluate reliably.
|
|
1172
|
+
if (cursorRowInBuffer < 0 || cursorRowInBuffer > lastLineIndex) {
|
|
1173
|
+
return "unknown";
|
|
1174
|
+
}
|
|
1175
|
+
if (cursorRowInBuffer !== lastLineIndex) {
|
|
1176
|
+
return "not_busy";
|
|
1177
|
+
}
|
|
1178
|
+
const lastLine = lines[lastLineIndex] || "";
|
|
1179
|
+
const promptPrefix = runtimeTypingPromptPrefix(runtimeKind);
|
|
1180
|
+
if (!promptPrefix) {
|
|
1181
|
+
return "unknown";
|
|
1182
|
+
}
|
|
1183
|
+
// Check if the last line starts with the runtime-specific prompt prefix
|
|
1184
|
+
if (lastLine.startsWith(promptPrefix)) {
|
|
1185
|
+
// Check if cursor is after the prompt prefix
|
|
1186
|
+
if (cursorPosition.x > promptPrefix.length) {
|
|
1187
|
+
const afterPrompt = lastLine.slice(promptPrefix.length).trim();
|
|
1188
|
+
// If there's content after the prompt, user is typing
|
|
1189
|
+
if (afterPrompt.length > 0) {
|
|
1190
|
+
return "busy";
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
return "not_busy";
|
|
1195
|
+
}
|