u-foo 1.0.6 → 1.1.9
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.md +44 -4
- package/SKILLS/ufoo/SKILL.md +17 -2
- package/SKILLS/uinit/SKILL.md +8 -3
- package/bin/ucode-core.js +15 -0
- package/bin/ucode.js +125 -0
- package/bin/ufoo-assistant-agent.js +5 -0
- package/bin/ufoo-engine.js +25 -0
- package/bin/ufoo.js +4 -0
- package/modules/AGENTS.template.md +14 -4
- package/modules/bus/README.md +8 -5
- package/modules/bus/SKILLS/ubus/SKILL.md +5 -4
- package/modules/context/SKILLS/uctx/SKILL.md +3 -1
- package/modules/online/SKILLS/ufoo-online/SKILL.md +144 -0
- package/package.json +12 -3
- package/scripts/import-pi-mono.js +124 -0
- package/scripts/postinstall.js +20 -49
- package/scripts/sync-claude-skills.sh +21 -0
- package/src/agent/cliRunner.js +524 -31
- package/src/agent/internalRunner.js +76 -9
- package/src/agent/launcher.js +97 -45
- package/src/agent/normalizeOutput.js +1 -1
- package/src/agent/notifier.js +144 -4
- package/src/agent/ptyRunner.js +480 -10
- package/src/agent/ptyWrapper.js +28 -3
- package/src/agent/readyDetector.js +16 -0
- package/src/agent/ucode.js +443 -0
- package/src/agent/ucodeBootstrap.js +113 -0
- package/src/agent/ucodeBuild.js +67 -0
- package/src/agent/ucodeDoctor.js +184 -0
- package/src/agent/ucodeRuntimeConfig.js +129 -0
- package/src/agent/ufooAgent.js +11 -2
- package/src/assistant/agent.js +260 -0
- package/src/assistant/bridge.js +172 -0
- package/src/assistant/engine.js +252 -0
- package/src/assistant/stdio.js +58 -0
- package/src/assistant/ufooEngineCli.js +306 -0
- package/src/bus/activate.js +27 -11
- package/src/bus/daemon.js +133 -5
- package/src/bus/index.js +137 -80
- package/src/bus/inject.js +47 -17
- package/src/bus/message.js +145 -17
- package/src/bus/nickname.js +3 -1
- package/src/bus/queue.js +6 -1
- package/src/bus/store.js +189 -0
- package/src/bus/subscriber.js +20 -4
- package/src/bus/utils.js +9 -3
- package/src/chat/agentBar.js +117 -0
- package/src/chat/agentDirectory.js +88 -0
- package/src/chat/agentSockets.js +225 -0
- package/src/chat/agentViewController.js +298 -0
- package/src/chat/chatLogController.js +115 -0
- package/src/chat/commandExecutor.js +700 -0
- package/src/chat/commands.js +132 -0
- package/src/chat/completionController.js +414 -0
- package/src/chat/cronScheduler.js +160 -0
- package/src/chat/daemonConnection.js +166 -0
- package/src/chat/daemonCoordinator.js +64 -0
- package/src/chat/daemonMessageRouter.js +257 -0
- package/src/chat/daemonReconnect.js +41 -0
- package/src/chat/daemonTransport.js +36 -0
- package/src/chat/daemonTransportDefaults.js +10 -0
- package/src/chat/dashboardKeyController.js +480 -0
- package/src/chat/dashboardView.js +154 -0
- package/src/chat/index.js +935 -2909
- package/src/chat/inputHistoryController.js +105 -0
- package/src/chat/inputListenerController.js +304 -0
- package/src/chat/inputMath.js +104 -0
- package/src/chat/inputSubmitHandler.js +171 -0
- package/src/chat/layout.js +165 -0
- package/src/chat/pasteController.js +81 -0
- package/src/chat/rawKeyMap.js +42 -0
- package/src/chat/settingsController.js +132 -0
- package/src/chat/statusLineController.js +177 -0
- package/src/chat/streamTracker.js +138 -0
- package/src/chat/text.js +70 -0
- package/src/chat/transport.js +61 -0
- package/src/cli/busCoreCommands.js +59 -0
- package/src/cli/ctxCoreCommands.js +199 -0
- package/src/cli/onlineCoreCommands.js +379 -0
- package/src/cli.js +741 -238
- package/src/code/README.md +29 -0
- package/src/code/UCODE_PROMPT.md +32 -0
- package/src/code/agent.js +1651 -0
- package/src/code/cli.js +158 -0
- package/src/code/config +0 -0
- package/src/code/dispatch.js +42 -0
- package/src/code/index.js +70 -0
- package/src/code/nativeRunner.js +1213 -0
- package/src/code/runtime.js +154 -0
- package/src/code/sessionStore.js +162 -0
- package/src/code/taskDecomposer.js +269 -0
- package/src/code/tools/bash.js +53 -0
- package/src/code/tools/common.js +42 -0
- package/src/code/tools/edit.js +70 -0
- package/src/code/tools/read.js +44 -0
- package/src/code/tools/write.js +35 -0
- package/src/code/tui.js +1580 -0
- package/src/config.js +47 -1
- package/src/context/decisions.js +12 -2
- package/src/context/index.js +18 -1
- package/src/context/sync.js +127 -0
- package/src/daemon/agentProcessManager.js +74 -0
- package/src/daemon/cronOps.js +241 -0
- package/src/daemon/index.js +661 -488
- package/src/daemon/ipcServer.js +99 -0
- package/src/daemon/ops.js +417 -179
- package/src/daemon/promptLoop.js +319 -0
- package/src/daemon/promptRequest.js +101 -0
- package/src/daemon/providerSessions.js +32 -17
- package/src/daemon/reporting.js +90 -0
- package/src/daemon/run.js +2 -5
- package/src/daemon/status.js +24 -1
- package/src/init/index.js +68 -14
- package/src/online/bridge.js +663 -0
- package/src/online/client.js +245 -0
- package/src/online/runner.js +253 -0
- package/src/online/server.js +992 -0
- package/src/online/tokens.js +103 -0
- package/src/report/store.js +331 -0
- package/src/shared/eventContract.js +35 -0
- package/src/shared/ptySocketContract.js +21 -0
- package/src/status/index.js +50 -17
- package/src/terminal/adapterContract.js +87 -0
- package/src/terminal/adapterRouter.js +84 -0
- package/src/terminal/adapters/externalAdapter.js +14 -0
- package/src/terminal/adapters/internalAdapter.js +13 -0
- package/src/terminal/adapters/internalPtyAdapter.js +42 -0
- package/src/terminal/adapters/internalQueueAdapter.js +37 -0
- package/src/terminal/adapters/terminalAdapter.js +31 -0
- package/src/terminal/adapters/tmuxAdapter.js +30 -0
- package/src/ufoo/agentsStore.js +69 -3
- package/src/utils/banner.js +5 -2
- package/scripts/.archived/bash-to-js-migration/README.md +0 -46
- package/scripts/.archived/bash-to-js-migration/banner.sh +0 -89
- package/scripts/.archived/bash-to-js-migration/bus-alert.sh +0 -6
- package/scripts/.archived/bash-to-js-migration/bus-autotrigger.sh +0 -6
- package/scripts/.archived/bash-to-js-migration/bus-daemon.sh +0 -231
- package/scripts/.archived/bash-to-js-migration/bus-inject.sh +0 -176
- package/scripts/.archived/bash-to-js-migration/bus-listen.sh +0 -6
- package/scripts/.archived/bash-to-js-migration/bus.sh +0 -986
- package/scripts/.archived/bash-to-js-migration/context-decisions.sh +0 -167
- package/scripts/.archived/bash-to-js-migration/context-doctor.sh +0 -72
- package/scripts/.archived/bash-to-js-migration/context-lint.sh +0 -110
- package/scripts/.archived/bash-to-js-migration/doctor.sh +0 -22
- package/scripts/.archived/bash-to-js-migration/init.sh +0 -247
- package/scripts/.archived/bash-to-js-migration/skills.sh +0 -113
- package/scripts/.archived/bash-to-js-migration/status.sh +0 -125
- package/scripts/banner.sh +0 -2
- package/src/bus/API_DESIGN.md +0 -204
package/src/daemon/ops.js
CHANGED
|
@@ -1,11 +1,41 @@
|
|
|
1
|
-
const { spawn
|
|
1
|
+
const { spawn } = require("child_process");
|
|
2
2
|
const fs = require("fs");
|
|
3
3
|
const path = require("path");
|
|
4
4
|
const { loadConfig } = require("../config");
|
|
5
5
|
const { getUfooPaths } = require("../ufoo/paths");
|
|
6
6
|
const { loadAgentsData, saveAgentsData } = require("../ufoo/agentsStore");
|
|
7
|
-
const { isAgentPidAlive } = require("../bus/utils");
|
|
7
|
+
const { isAgentPidAlive, getTtyProcessInfo } = require("../bus/utils");
|
|
8
8
|
const { isITerm2 } = require("../terminal/detect");
|
|
9
|
+
const { createTerminalAdapterRouter } = require("../terminal/adapterRouter");
|
|
10
|
+
|
|
11
|
+
function normalizeLaunchAgent(agent = "") {
|
|
12
|
+
const value = String(agent || "").trim().toLowerCase();
|
|
13
|
+
if (value === "codex") return "codex";
|
|
14
|
+
if (value === "claude" || value === "claude-code") return "claude";
|
|
15
|
+
if (value === "ufoo" || value === "ucode" || value === "ufoo-code") return "ufoo";
|
|
16
|
+
return "";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function toBusAgentType(agent = "") {
|
|
20
|
+
if (agent === "codex") return "codex";
|
|
21
|
+
if (agent === "claude") return "claude-code";
|
|
22
|
+
if (agent === "ufoo") return "ufoo-code";
|
|
23
|
+
return "";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function toTerminalBinary(agent = "") {
|
|
27
|
+
if (agent === "codex") return "./bin/ucodex.js";
|
|
28
|
+
if (agent === "claude") return "./bin/uclaude.js";
|
|
29
|
+
if (agent === "ufoo") return "./bin/ucode.js";
|
|
30
|
+
return "";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function toTmuxBinary(agent = "") {
|
|
34
|
+
if (agent === "codex") return "ucodex";
|
|
35
|
+
if (agent === "claude") return "uclaude";
|
|
36
|
+
if (agent === "ufoo") return "ucode";
|
|
37
|
+
return "";
|
|
38
|
+
}
|
|
9
39
|
|
|
10
40
|
function resolveAgentId(projectRoot, agentId) {
|
|
11
41
|
if (!agentId) return agentId;
|
|
@@ -16,7 +46,8 @@ function resolveAgentId(projectRoot, agentId) {
|
|
|
16
46
|
const entries = Object.entries(bus.agents || {});
|
|
17
47
|
const match = entries.find(([, meta]) => meta?.nickname === agentId);
|
|
18
48
|
if (match) return match[0];
|
|
19
|
-
const
|
|
49
|
+
const normalized = normalizeLaunchAgent(agentId);
|
|
50
|
+
const targetType = toBusAgentType(normalized) || agentId;
|
|
20
51
|
const candidates = entries
|
|
21
52
|
.filter(([, meta]) => meta?.agent_type === targetType && meta?.status === "active")
|
|
22
53
|
.map(([id]) => id);
|
|
@@ -27,6 +58,51 @@ function resolveAgentId(projectRoot, agentId) {
|
|
|
27
58
|
return agentId;
|
|
28
59
|
}
|
|
29
60
|
|
|
61
|
+
function markAgentInactive(projectRoot, agentId) {
|
|
62
|
+
if (!agentId) return false;
|
|
63
|
+
const filePath = getUfooPaths(projectRoot).agentsFile;
|
|
64
|
+
const data = loadAgentsData(filePath);
|
|
65
|
+
const meta = data.agents?.[agentId];
|
|
66
|
+
if (!meta) return false;
|
|
67
|
+
data.agents[agentId] = {
|
|
68
|
+
...meta,
|
|
69
|
+
status: "inactive",
|
|
70
|
+
last_seen: new Date().toISOString(),
|
|
71
|
+
};
|
|
72
|
+
saveAgentsData(filePath, data);
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
function listSubscribers(projectRoot, agentType) {
|
|
78
|
+
const busPath = getUfooPaths(projectRoot).agentsFile;
|
|
79
|
+
try {
|
|
80
|
+
const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
|
|
81
|
+
return Object.entries(bus.agents || {})
|
|
82
|
+
.filter(([, meta]) => meta && meta.agent_type === agentType && meta.status === "active")
|
|
83
|
+
.map(([id]) => id);
|
|
84
|
+
} catch {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function waitForNewSubscriber(projectRoot, agentType, existing, timeoutMs = 15000) {
|
|
90
|
+
const start = Date.now();
|
|
91
|
+
const seen = new Set(existing || []);
|
|
92
|
+
while (Date.now() - start < timeoutMs) {
|
|
93
|
+
const current = listSubscribers(projectRoot, agentType);
|
|
94
|
+
const diff = current.find((id) => !seen.has(id));
|
|
95
|
+
if (diff) return diff;
|
|
96
|
+
// eslint-disable-next-line no-await-in-loop
|
|
97
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function escapeCommand(cmd) {
|
|
103
|
+
return cmd.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
104
|
+
}
|
|
105
|
+
|
|
30
106
|
function shellEscape(value) {
|
|
31
107
|
const str = String(value);
|
|
32
108
|
return `'${str.replace(/'/g, `'\\''`)}'`;
|
|
@@ -36,75 +112,181 @@ function escapeAppleScriptString(str) {
|
|
|
36
112
|
return String(str).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
37
113
|
}
|
|
38
114
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
115
|
+
function runAppleScript(lines) {
|
|
116
|
+
return new Promise((resolve, reject) => {
|
|
117
|
+
const proc = spawn("osascript", lines.flatMap((l) => ["-e", l]));
|
|
118
|
+
let stderr = "";
|
|
119
|
+
let stdout = "";
|
|
120
|
+
proc.stderr.on("data", (d) => {
|
|
121
|
+
stderr += d.toString("utf8");
|
|
122
|
+
});
|
|
123
|
+
proc.stdout.on("data", (d) => {
|
|
124
|
+
stdout += d.toString("utf8");
|
|
125
|
+
});
|
|
126
|
+
proc.on("close", (code) => {
|
|
127
|
+
if (code === 0) resolve(stdout.trim());
|
|
128
|
+
else reject(new Error(stderr || "osascript failed"));
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function openTerminalWindow(runCmd) {
|
|
45
134
|
if (process.platform !== "darwin") {
|
|
46
135
|
throw new Error("Terminal mode is only supported on macOS");
|
|
47
136
|
}
|
|
48
137
|
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
138
|
+
const escaped = escapeAppleScriptString(runCmd);
|
|
139
|
+
|
|
140
|
+
if (isITerm2()) {
|
|
141
|
+
try {
|
|
142
|
+
const script = [
|
|
143
|
+
'tell application "iTerm2"',
|
|
144
|
+
" tell current window",
|
|
145
|
+
` create tab with default profile command "${escaped}"`,
|
|
146
|
+
" end tell",
|
|
147
|
+
" activate",
|
|
148
|
+
"end tell",
|
|
149
|
+
];
|
|
150
|
+
await runAppleScript(script);
|
|
151
|
+
return;
|
|
152
|
+
} catch {
|
|
153
|
+
// fall back to Terminal.app
|
|
154
|
+
}
|
|
155
|
+
}
|
|
53
156
|
|
|
54
157
|
const script = [
|
|
55
158
|
'tell application "Terminal"',
|
|
56
|
-
`do script "${
|
|
159
|
+
`do script "${escaped}"`,
|
|
57
160
|
"activate",
|
|
58
161
|
"end tell",
|
|
59
162
|
];
|
|
163
|
+
await runAppleScript(script);
|
|
164
|
+
}
|
|
60
165
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
166
|
+
async function closeTerminalWindowByTty(ttyPath, preferApp = "") {
|
|
167
|
+
if (process.platform !== "darwin") return false;
|
|
168
|
+
if (!ttyPath) return false;
|
|
169
|
+
|
|
170
|
+
const escaped = escapeAppleScriptString(ttyPath);
|
|
171
|
+
|
|
172
|
+
const tryITerm = async () => {
|
|
173
|
+
const script = [
|
|
174
|
+
'tell application "iTerm2"',
|
|
175
|
+
" repeat with w in windows",
|
|
176
|
+
" repeat with t in tabs of w",
|
|
177
|
+
" repeat with s in sessions of t",
|
|
178
|
+
` if tty of s is \"${escaped}\" then`,
|
|
179
|
+
" close t",
|
|
180
|
+
' return "ok"',
|
|
181
|
+
" end if",
|
|
182
|
+
" end repeat",
|
|
183
|
+
" end repeat",
|
|
184
|
+
" end repeat",
|
|
185
|
+
"end tell",
|
|
186
|
+
'return "not found"',
|
|
187
|
+
];
|
|
188
|
+
const res = await runAppleScript(script);
|
|
189
|
+
return res === "ok";
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const tryTerminal = async () => {
|
|
193
|
+
const script = [
|
|
194
|
+
'tell application "Terminal"',
|
|
195
|
+
" repeat with w in windows",
|
|
196
|
+
" repeat with t in tabs of w",
|
|
197
|
+
` if tty of t is \"${escaped}\" then`,
|
|
198
|
+
" close t",
|
|
199
|
+
" if (count of tabs of w) is 0 then close w",
|
|
200
|
+
' return "ok"',
|
|
201
|
+
" end if",
|
|
202
|
+
" end repeat",
|
|
203
|
+
" end repeat",
|
|
204
|
+
"end tell",
|
|
205
|
+
'return "not found"',
|
|
206
|
+
];
|
|
207
|
+
const res = await runAppleScript(script);
|
|
208
|
+
return res === "ok";
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const prefer = (preferApp || "").toLowerCase();
|
|
212
|
+
const order = prefer === "terminal"
|
|
213
|
+
? [tryTerminal, tryITerm]
|
|
214
|
+
: prefer === "iterm2"
|
|
215
|
+
? [tryITerm, tryTerminal]
|
|
216
|
+
: [tryITerm, tryTerminal];
|
|
217
|
+
|
|
218
|
+
for (const attempt of order) {
|
|
219
|
+
try {
|
|
220
|
+
if (await attempt()) return true;
|
|
221
|
+
} catch {
|
|
222
|
+
// ignore and try next
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function buildTitleCmd(title) {
|
|
229
|
+
if (!title) return "";
|
|
230
|
+
return `printf '\\033]0;%s\\007' ${shellEscape(title)}`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function buildResumeCommand(projectRoot, agent, sessionId) {
|
|
234
|
+
const binary = toTerminalBinary(agent);
|
|
235
|
+
if (!binary) {
|
|
236
|
+
throw new Error(`unsupported agent for resume: ${agent}`);
|
|
237
|
+
}
|
|
238
|
+
const args = buildResumeArgs(agent, sessionId);
|
|
239
|
+
const argText = args.length > 0 ? ` ${args.map(shellEscape).join(" ")}` : "";
|
|
240
|
+
const skipProbeEnv = "UFOO_SKIP_SESSION_PROBE=1 ";
|
|
241
|
+
return `cd ${shellEscape(projectRoot)} && ${skipProbeEnv}${binary}${argText}`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function tryReuseTerminal(projectRoot, subscriberId, meta, agent, sessionId) {
|
|
245
|
+
if (!meta || !meta.tty) return false;
|
|
246
|
+
const info = getTtyProcessInfo(meta.tty);
|
|
247
|
+
if (!info.alive || info.hasAgent || !info.idle) return false;
|
|
248
|
+
const titleCmd = buildTitleCmd(meta.nickname || "");
|
|
249
|
+
const baseCmd = buildResumeCommand(projectRoot, agent, sessionId);
|
|
250
|
+
const command = titleCmd ? `${titleCmd} && ${baseCmd}` : baseCmd;
|
|
251
|
+
try {
|
|
252
|
+
const EventBus = require("../bus");
|
|
253
|
+
const bus = new EventBus(projectRoot);
|
|
254
|
+
bus.ensureBus();
|
|
255
|
+
await bus.inject(subscriberId, command);
|
|
256
|
+
return true;
|
|
257
|
+
} catch {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
72
260
|
}
|
|
73
261
|
|
|
74
262
|
/**
|
|
75
|
-
*
|
|
76
|
-
* 使用 AppleScript 控制 iTerm2,比 Terminal.app 更丰富的功能
|
|
263
|
+
* Spawn managed terminal agent - open a real Terminal session to run the agent
|
|
77
264
|
*/
|
|
78
|
-
async function
|
|
79
|
-
|
|
80
|
-
|
|
265
|
+
async function spawnManagedTerminalAgent(projectRoot, agent, nickname = "", processManager = null, extraArgs = [], extraEnv = "") {
|
|
266
|
+
const normalizedAgent = normalizeLaunchAgent(agent);
|
|
267
|
+
const binary = toTerminalBinary(normalizedAgent);
|
|
268
|
+
const agentType = toBusAgentType(normalizedAgent);
|
|
269
|
+
if (!binary || !agentType) {
|
|
270
|
+
throw new Error(`unsupported agent type: ${agent}`);
|
|
81
271
|
}
|
|
272
|
+
const existing = listSubscribers(projectRoot, agentType);
|
|
273
|
+
const runDir = getUfooPaths(projectRoot).runDir;
|
|
274
|
+
fs.mkdirSync(runDir, { recursive: true });
|
|
82
275
|
|
|
83
|
-
const
|
|
276
|
+
const args = Array.isArray(extraArgs) ? extraArgs : [];
|
|
277
|
+
const argText = args.length > 0 ? ` ${args.map(shellEscape).join(" ")}` : "";
|
|
84
278
|
const nickEnv = nickname ? `UFOO_NICKNAME=${shellEscape(nickname)} ` : "";
|
|
85
279
|
const modeEnv = "UFOO_LAUNCH_MODE=terminal ";
|
|
86
|
-
const
|
|
280
|
+
const envPrefix = extraEnv ? `${String(extraEnv).trim()} ` : "";
|
|
281
|
+
const titleCmd = buildTitleCmd(nickname);
|
|
282
|
+
const prefix = titleCmd ? `${titleCmd} && ` : "";
|
|
87
283
|
|
|
88
|
-
const
|
|
89
|
-
'tell application "iTerm2"',
|
|
90
|
-
" tell current window",
|
|
91
|
-
` create tab with default profile command "${escapeAppleScriptString(runCmd)}"`,
|
|
92
|
-
" end tell",
|
|
93
|
-
" activate",
|
|
94
|
-
"end tell",
|
|
95
|
-
];
|
|
284
|
+
const runCmd = `cd ${shellEscape(projectRoot)} && ${prefix}${modeEnv}${nickEnv}${envPrefix}${binary}${argText}`;
|
|
96
285
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
stderr += d.toString("utf8");
|
|
102
|
-
});
|
|
103
|
-
proc.on("close", (code) => {
|
|
104
|
-
if (code === 0) resolve();
|
|
105
|
-
else reject(new Error(stderr || "Failed to open iTerm2 tab"));
|
|
106
|
-
});
|
|
107
|
-
});
|
|
286
|
+
await openTerminalWindow(runCmd);
|
|
287
|
+
|
|
288
|
+
const subscriberId = await waitForNewSubscriber(projectRoot, agentType, existing, 15000);
|
|
289
|
+
return { child: null, subscriberId: subscriberId || null };
|
|
108
290
|
}
|
|
109
291
|
|
|
110
292
|
async function spawnInternalAgent(projectRoot, agent, count = 1, nickname = "", processManager = null) {
|
|
@@ -129,7 +311,11 @@ async function spawnInternalAgent(projectRoot, agent, count = 1, nickname = "",
|
|
|
129
311
|
|
|
130
312
|
// 预生成 session ID
|
|
131
313
|
const sessionId = crypto.randomBytes(4).toString("hex");
|
|
132
|
-
const
|
|
314
|
+
const normalizedAgent = normalizeLaunchAgent(agent);
|
|
315
|
+
const agentType = toBusAgentType(normalizedAgent);
|
|
316
|
+
if (!agentType) {
|
|
317
|
+
throw new Error(`unsupported agent type: ${agent}`);
|
|
318
|
+
}
|
|
133
319
|
const subscriberId = `${agentType}:${sessionId}`;
|
|
134
320
|
subscriberIds.push(subscriberId);
|
|
135
321
|
|
|
@@ -137,7 +323,9 @@ async function spawnInternalAgent(projectRoot, agent, count = 1, nickname = "",
|
|
|
137
323
|
bus.loadBusData();
|
|
138
324
|
process.env.UFOO_PARENT_PID = String(originalPid);
|
|
139
325
|
|
|
140
|
-
|
|
326
|
+
// For ucode/ufoo agents, default nickname to "ucode" if not specified
|
|
327
|
+
const defaultNickname = agentType === "ufoo-code" ? "ucode" : agent;
|
|
328
|
+
const finalNickname = count > 1 ? `${nickname || defaultNickname}-${i + 1}` : (nickname || defaultNickname);
|
|
141
329
|
const usePty = process.env.UFOO_INTERNAL_PTY !== "0";
|
|
142
330
|
const launchMode = usePty ? "internal-pty" : "internal";
|
|
143
331
|
|
|
@@ -165,6 +353,18 @@ async function spawnInternalAgent(projectRoot, agent, count = 1, nickname = "",
|
|
|
165
353
|
},
|
|
166
354
|
});
|
|
167
355
|
|
|
356
|
+
// Update bus data with the actual child PID so isMetaActive
|
|
357
|
+
// can detect when the ptyRunner process dies.
|
|
358
|
+
try {
|
|
359
|
+
bus.loadBusData();
|
|
360
|
+
if (bus.busData.agents && bus.busData.agents[subscriberId]) {
|
|
361
|
+
bus.busData.agents[subscriberId].pid = child.pid;
|
|
362
|
+
}
|
|
363
|
+
bus.saveBusData();
|
|
364
|
+
} catch {
|
|
365
|
+
// ignore pid update errors
|
|
366
|
+
}
|
|
367
|
+
|
|
168
368
|
// 本地日志记录
|
|
169
369
|
child.on("exit", (code, signal) => {
|
|
170
370
|
try {
|
|
@@ -173,6 +373,18 @@ async function spawnInternalAgent(projectRoot, agent, count = 1, nickname = "",
|
|
|
173
373
|
// ignore
|
|
174
374
|
}
|
|
175
375
|
|
|
376
|
+
// Mark agent as inactive when its process exits
|
|
377
|
+
try {
|
|
378
|
+
bus.loadBusData();
|
|
379
|
+
if (bus.busData.agents && bus.busData.agents[subscriberId]) {
|
|
380
|
+
bus.busData.agents[subscriberId].status = "inactive";
|
|
381
|
+
bus.busData.agents[subscriberId].last_seen = new Date().toISOString();
|
|
382
|
+
}
|
|
383
|
+
bus.saveBusData();
|
|
384
|
+
} catch {
|
|
385
|
+
// ignore
|
|
386
|
+
}
|
|
387
|
+
|
|
176
388
|
if (signal) {
|
|
177
389
|
fs.appendFileSync(logFile, `\n[internal-agent] ${subscriberId} killed by signal ${signal}\n`);
|
|
178
390
|
} else {
|
|
@@ -200,41 +412,14 @@ async function spawnInternalAgent(projectRoot, agent, count = 1, nickname = "",
|
|
|
200
412
|
return { children, subscriberIds };
|
|
201
413
|
}
|
|
202
414
|
|
|
203
|
-
/**
|
|
204
|
-
* Find the first idle tmux pane in the SAME window as ufoo chat.
|
|
205
|
-
* Looks for panes running a plain shell, excluding the chat pane itself.
|
|
206
|
-
* Returns the pane target (e.g. "%5") or null.
|
|
207
|
-
*/
|
|
208
|
-
function findIdleTmuxPane() {
|
|
209
|
-
const myPaneId = process.env.TMUX_PANE || "";
|
|
210
|
-
if (!myPaneId) return null;
|
|
211
|
-
|
|
212
|
-
// List panes in the same window as ufoo chat
|
|
213
|
-
const result = spawnSync("tmux", [
|
|
214
|
-
"list-panes", "-t", myPaneId,
|
|
215
|
-
"-F", "#{pane_id}\t#{pane_current_command}",
|
|
216
|
-
], { stdio: "pipe", encoding: "utf8" });
|
|
217
|
-
|
|
218
|
-
if (result.status !== 0 || !result.stdout) return null;
|
|
219
|
-
|
|
220
|
-
const shells = new Set(["bash", "zsh", "fish", "sh", "dash", "ksh", "login"]);
|
|
221
|
-
|
|
222
|
-
for (const line of result.stdout.trim().split("\n")) {
|
|
223
|
-
const [paneId, cmd] = line.split("\t");
|
|
224
|
-
if (!paneId || !cmd) continue;
|
|
225
|
-
// Skip ufoo chat's own pane
|
|
226
|
-
if (paneId === myPaneId) continue;
|
|
227
|
-
// Only use panes running a plain shell
|
|
228
|
-
if (shells.has(path.basename(cmd))) {
|
|
229
|
-
return paneId;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
return null;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
415
|
function spawnTmuxWindow(projectRoot, agent, nickname = "", extraArgs = [], extraEnv = "") {
|
|
236
416
|
return new Promise((resolve, reject) => {
|
|
237
|
-
const
|
|
417
|
+
const normalizedAgent = normalizeLaunchAgent(agent);
|
|
418
|
+
const binary = toTmuxBinary(normalizedAgent);
|
|
419
|
+
if (!binary) {
|
|
420
|
+
reject(new Error(`unsupported agent type: ${agent}`));
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
238
423
|
const nickEnv = nickname ? `UFOO_NICKNAME=${shellEscape(nickname)} ` : "";
|
|
239
424
|
const modeEnv = "UFOO_LAUNCH_MODE=tmux ";
|
|
240
425
|
const ttyEnv = "UFOO_TTY_OVERRIDE=$(tty) ";
|
|
@@ -242,61 +427,46 @@ function spawnTmuxWindow(projectRoot, agent, nickname = "", extraArgs = [], extr
|
|
|
242
427
|
const envPrefix = extraEnv ? `${String(extraEnv).trim()} ` : "";
|
|
243
428
|
const argText = args.length > 0 ? ` ${args.map(shellEscape).join(" ")}` : "";
|
|
244
429
|
|
|
245
|
-
//
|
|
246
|
-
|
|
430
|
+
// IMPORTANT: Set TMUX_PANE inside the new window using tmux display-message
|
|
431
|
+
// This ensures the agent gets the correct pane ID for command injection
|
|
432
|
+
const setPaneEnv = `export TMUX_PANE=$(tmux display-message -p '#{pane_id}'); `;
|
|
433
|
+
const runCmd = `cd ${shellEscape(projectRoot)} && ${setPaneEnv}${modeEnv}${nickEnv}${ttyEnv}${envPrefix}${binary}${argText}`;
|
|
247
434
|
const windowName = nickname || `${agent}-${Date.now()}`;
|
|
248
|
-
const targetSession = process.env.UFOO_TMUX_SESSION || "";
|
|
249
435
|
|
|
250
|
-
//
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
let stderr = "";
|
|
258
|
-
proc.stderr.on("data", (d) => { stderr += d.toString("utf8"); });
|
|
259
|
-
proc.on("close", (code) => {
|
|
260
|
-
if (code === 0) resolve();
|
|
261
|
-
else reject(new Error(stderr || "tmux send-keys failed"));
|
|
262
|
-
});
|
|
263
|
-
} else {
|
|
264
|
-
// No idle pane — split current window to create a new pane
|
|
265
|
-
const splitTarget = myPane || (targetSession ? `${targetSession}:` : "");
|
|
266
|
-
const splitArgs = ["split-window", "-d", "-h"];
|
|
267
|
-
if (splitTarget) splitArgs.push("-t", splitTarget);
|
|
268
|
-
splitArgs.push(runCmd);
|
|
269
|
-
|
|
270
|
-
const proc = spawn("tmux", splitArgs);
|
|
271
|
-
let stderr = "";
|
|
272
|
-
proc.stderr.on("data", (d) => { stderr += d.toString("utf8"); });
|
|
273
|
-
proc.on("close", (code) => {
|
|
274
|
-
if (code === 0) resolve();
|
|
275
|
-
else reject(new Error(stderr || "tmux split-window failed"));
|
|
276
|
-
});
|
|
436
|
+
// Use detached mode (-d) to avoid stealing focus
|
|
437
|
+
// Use -a flag to insert after current window, avoiding index conflicts
|
|
438
|
+
// Use target session from env or current session
|
|
439
|
+
const targetSession = process.env.UFOO_TMUX_SESSION || "";
|
|
440
|
+
const tmuxArgs = ["new-window", "-a", "-d", "-n", windowName];
|
|
441
|
+
if (targetSession) {
|
|
442
|
+
tmuxArgs.push("-t", targetSession);
|
|
277
443
|
}
|
|
278
|
-
|
|
279
|
-
}
|
|
444
|
+
tmuxArgs.push(runCmd);
|
|
280
445
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
446
|
+
const proc = spawn("tmux", tmuxArgs);
|
|
447
|
+
let stderr = "";
|
|
448
|
+
proc.stderr.on("data", (d) => {
|
|
449
|
+
stderr += d.toString("utf8");
|
|
450
|
+
});
|
|
451
|
+
proc.on("close", (code) => {
|
|
452
|
+
if (code === 0) resolve();
|
|
453
|
+
else reject(new Error(stderr || "tmux new-window failed"));
|
|
454
|
+
});
|
|
455
|
+
});
|
|
291
456
|
}
|
|
292
457
|
|
|
293
458
|
async function launchAgent(projectRoot, agent, count = 1, nickname = "", processManager = null) {
|
|
294
459
|
const config = loadConfig(projectRoot);
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
460
|
+
const mode = config.launchMode || "terminal";
|
|
461
|
+
const normalizedAgent = normalizeLaunchAgent(agent);
|
|
462
|
+
if (!normalizedAgent) {
|
|
463
|
+
throw new Error(`unsupported agent type: ${agent}`);
|
|
298
464
|
}
|
|
299
465
|
|
|
466
|
+
if (mode === "internal") {
|
|
467
|
+
const result = await spawnInternalAgent(projectRoot, normalizedAgent, count, nickname, processManager);
|
|
468
|
+
return { mode: "internal", subscriberIds: result.subscriberIds };
|
|
469
|
+
}
|
|
300
470
|
if (mode === "tmux") {
|
|
301
471
|
// Check if tmux is available
|
|
302
472
|
const tmuxCheck = spawn("tmux", ["list-sessions"], { stdio: "pipe" });
|
|
@@ -320,36 +490,36 @@ async function launchAgent(projectRoot, agent, count = 1, nickname = "", process
|
|
|
320
490
|
}
|
|
321
491
|
}
|
|
322
492
|
for (let i = 0; i < count; i += 1) {
|
|
323
|
-
|
|
493
|
+
// Use "ucode" as default nickname for ufoo/ucode agents
|
|
494
|
+
const defaultNick = normalizedAgent === "ufoo" ? "ucode" : normalizedAgent;
|
|
495
|
+
const nick = count > 1 ? `${nickname || defaultNick}-${i + 1}` : (nickname || "");
|
|
324
496
|
// eslint-disable-next-line no-await-in-loop
|
|
325
|
-
await spawnTmuxWindow(projectRoot,
|
|
497
|
+
await spawnTmuxWindow(projectRoot, normalizedAgent, nick);
|
|
326
498
|
}
|
|
327
499
|
return { mode: "tmux" };
|
|
328
500
|
}
|
|
501
|
+
// terminal mode - daemon 作为父进程,输出到终端窗口
|
|
502
|
+
if (process.platform !== "darwin") {
|
|
503
|
+
throw new Error("launchAgent with terminal mode is only supported on macOS Terminal.app");
|
|
504
|
+
}
|
|
329
505
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
} else {
|
|
339
|
-
await spawnTerminalAgent(projectRoot, agent, nick);
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
return { mode: "terminal" };
|
|
506
|
+
const subscriberIds = [];
|
|
507
|
+
for (let i = 0; i < count; i += 1) {
|
|
508
|
+
// Use "ucode" as default nickname for ufoo/ucode agents
|
|
509
|
+
const defaultNick = normalizedAgent === "ufoo" ? "ucode" : normalizedAgent;
|
|
510
|
+
const nick = count > 1 ? `${nickname || defaultNick}-${i + 1}` : (nickname || "");
|
|
511
|
+
// eslint-disable-next-line no-await-in-loop
|
|
512
|
+
const result = await spawnManagedTerminalAgent(projectRoot, normalizedAgent, nick, processManager);
|
|
513
|
+
if (result.subscriberId) subscriberIds.push(result.subscriberId);
|
|
343
514
|
}
|
|
344
515
|
|
|
345
|
-
|
|
346
|
-
const result = await spawnInternalAgent(projectRoot, agent, count, nickname, processManager);
|
|
347
|
-
return { mode: "internal", subscriberIds: result.subscriberIds };
|
|
516
|
+
return { mode: "terminal", subscriberIds };
|
|
348
517
|
}
|
|
349
518
|
|
|
350
519
|
function normalizeAgentType(agentType) {
|
|
351
520
|
if (agentType === "claude-code") return "claude";
|
|
352
521
|
if (agentType === "codex") return "codex";
|
|
522
|
+
if (agentType === "ufoo-code") return "ufoo";
|
|
353
523
|
return agentType;
|
|
354
524
|
}
|
|
355
525
|
|
|
@@ -366,9 +536,9 @@ function isActiveAgent(meta) {
|
|
|
366
536
|
return true;
|
|
367
537
|
}
|
|
368
538
|
|
|
369
|
-
|
|
539
|
+
function collectRecoverableAgents(projectRoot, target = "") {
|
|
370
540
|
const config = loadConfig(projectRoot);
|
|
371
|
-
const mode = config.launchMode || "
|
|
541
|
+
const mode = config.launchMode || "terminal";
|
|
372
542
|
const filePath = getUfooPaths(projectRoot).agentsFile;
|
|
373
543
|
const data = loadAgentsData(filePath);
|
|
374
544
|
const entries = Object.entries(data.agents || {});
|
|
@@ -378,13 +548,22 @@ async function resumeAgents(projectRoot, target = "", processManager = null) {
|
|
|
378
548
|
if (target.includes(":")) {
|
|
379
549
|
targets = entries.filter(([id]) => id === target);
|
|
380
550
|
} else {
|
|
381
|
-
targets = entries.filter(([, meta]) => meta && meta.nickname === target);
|
|
551
|
+
targets = entries.filter(([id, meta]) => id === target || (meta && meta.nickname === target));
|
|
382
552
|
}
|
|
383
553
|
}
|
|
384
554
|
|
|
385
|
-
const
|
|
555
|
+
const recoverableEntries = [];
|
|
386
556
|
const skipped = [];
|
|
387
557
|
|
|
558
|
+
if (target && targets.length === 0) {
|
|
559
|
+
return {
|
|
560
|
+
mode,
|
|
561
|
+
data,
|
|
562
|
+
recoverableEntries,
|
|
563
|
+
skipped: [{ id: target, reason: "target not found" }],
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
|
|
388
567
|
for (const [id, meta] of targets) {
|
|
389
568
|
if (!meta || !meta.provider_session_id) {
|
|
390
569
|
skipped.push({ id, reason: "no provider session" });
|
|
@@ -399,16 +578,47 @@ async function resumeAgents(projectRoot, target = "", processManager = null) {
|
|
|
399
578
|
skipped.push({ id, reason: "unsupported agent type" });
|
|
400
579
|
continue;
|
|
401
580
|
}
|
|
402
|
-
|
|
581
|
+
|
|
582
|
+
if (mode === "internal") {
|
|
583
|
+
skipped.push({ id, reason: "internal mode not supported for resume" });
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
recoverableEntries.push({ id, meta, agent });
|
|
403
588
|
}
|
|
404
589
|
|
|
405
|
-
|
|
590
|
+
return {
|
|
591
|
+
mode,
|
|
592
|
+
data,
|
|
593
|
+
recoverableEntries,
|
|
594
|
+
skipped,
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function getRecoverableAgents(projectRoot, target = "") {
|
|
599
|
+
const { mode, recoverableEntries, skipped } = collectRecoverableAgents(projectRoot, target);
|
|
600
|
+
const recoverable = recoverableEntries.map((item) => ({
|
|
601
|
+
id: item.id,
|
|
602
|
+
nickname: item.meta.nickname || "",
|
|
603
|
+
agent: item.agent,
|
|
604
|
+
sessionId: item.meta.provider_session_id || "",
|
|
605
|
+
launchMode: item.meta.launch_mode || "",
|
|
606
|
+
lastSeen: item.meta.last_seen || "",
|
|
607
|
+
}));
|
|
608
|
+
return { ok: true, mode, recoverable, skipped };
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
async function resumeAgents(projectRoot, target = "", processManager = null) {
|
|
612
|
+
const filePath = getUfooPaths(projectRoot).agentsFile;
|
|
613
|
+
const { mode, data, recoverableEntries, skipped } = collectRecoverableAgents(projectRoot, target);
|
|
614
|
+
|
|
615
|
+
if (recoverableEntries.length === 0) {
|
|
406
616
|
return { ok: true, resumed: [], skipped };
|
|
407
617
|
}
|
|
408
618
|
|
|
409
|
-
// Clear old nicknames to allow reuse
|
|
619
|
+
// Clear old nicknames to allow reuse.
|
|
410
620
|
let updated = false;
|
|
411
|
-
for (const item of
|
|
621
|
+
for (const item of recoverableEntries) {
|
|
412
622
|
if (item.meta && item.meta.nickname) {
|
|
413
623
|
data.agents[item.id] = { ...item.meta, nickname: "" };
|
|
414
624
|
updated = true;
|
|
@@ -419,46 +629,74 @@ async function resumeAgents(projectRoot, target = "", processManager = null) {
|
|
|
419
629
|
}
|
|
420
630
|
|
|
421
631
|
const resumed = [];
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
const sessionId = item.meta.provider_session_id;
|
|
632
|
+
for (const item of recoverableEntries) {
|
|
633
|
+
const nickname = item.meta.nickname || "";
|
|
634
|
+
const sessionId = item.meta.provider_session_id;
|
|
635
|
+
const reused = await tryReuseTerminal(projectRoot, item.id, item.meta, item.agent, sessionId);
|
|
636
|
+
if (!reused) {
|
|
428
637
|
const args = buildResumeArgs(item.agent, sessionId);
|
|
429
638
|
const envPrefix = "UFOO_SKIP_SESSION_PROBE=1";
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
639
|
+
if (mode === "tmux") {
|
|
640
|
+
// eslint-disable-next-line no-await-in-loop
|
|
641
|
+
await spawnTmuxWindow(projectRoot, item.agent, nickname, args, envPrefix);
|
|
642
|
+
} else {
|
|
643
|
+
// eslint-disable-next-line no-await-in-loop
|
|
644
|
+
await spawnManagedTerminalAgent(projectRoot, item.agent, nickname, processManager, args, envPrefix);
|
|
645
|
+
}
|
|
433
646
|
}
|
|
434
|
-
|
|
647
|
+
resumed.push({ id: item.id, nickname, agent: item.agent, sessionId, reused });
|
|
435
648
|
}
|
|
436
649
|
|
|
437
|
-
// internal 模式暂不支持 resume(需要用户手动启动)
|
|
438
|
-
for (const item of resumable) {
|
|
439
|
-
skipped.push({ id: item.id, reason: "internal mode requires manual restart" });
|
|
440
|
-
}
|
|
441
650
|
return { ok: true, resumed, skipped };
|
|
442
651
|
}
|
|
443
652
|
|
|
444
653
|
async function closeAgent(projectRoot, agentId) {
|
|
654
|
+
if (process.platform !== "darwin") {
|
|
655
|
+
return false;
|
|
656
|
+
}
|
|
445
657
|
const resolvedId = resolveAgentId(projectRoot, agentId);
|
|
446
658
|
const busPath = getUfooPaths(projectRoot).agentsFile;
|
|
447
659
|
let pid = null;
|
|
660
|
+
let launchMode = "";
|
|
661
|
+
let tty = "";
|
|
662
|
+
let terminalApp = "";
|
|
448
663
|
try {
|
|
449
664
|
const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
|
|
450
665
|
const entry = bus.agents?.[resolvedId];
|
|
451
|
-
if (entry
|
|
666
|
+
if (entry) {
|
|
667
|
+
if (entry.pid) pid = entry.pid;
|
|
668
|
+
launchMode = entry.launch_mode || "";
|
|
669
|
+
tty = entry.tty || "";
|
|
670
|
+
terminalApp = entry.terminal_app || "";
|
|
671
|
+
}
|
|
452
672
|
} catch {
|
|
453
673
|
pid = null;
|
|
454
674
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
675
|
+
const adapterRouter = createTerminalAdapterRouter();
|
|
676
|
+
const adapter = adapterRouter.getAdapter({ launchMode, agentId: resolvedId });
|
|
677
|
+
const canCloseWindow = adapter.capabilities.supportsWindowClose && tty;
|
|
678
|
+
|
|
679
|
+
// Close process first for faster state transition in chat.
|
|
680
|
+
let sentSignal = false;
|
|
681
|
+
if (pid) {
|
|
682
|
+
try {
|
|
683
|
+
process.kill(pid, "SIGTERM");
|
|
684
|
+
sentSignal = true;
|
|
685
|
+
} catch {
|
|
686
|
+
sentSignal = false;
|
|
687
|
+
}
|
|
461
688
|
}
|
|
689
|
+
|
|
690
|
+
if (sentSignal || (!pid && canCloseWindow)) {
|
|
691
|
+
markAgentInactive(projectRoot, resolvedId);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
if (canCloseWindow) {
|
|
695
|
+
// Non-blocking: don't hold close response on AppleScript window operations.
|
|
696
|
+
void closeTerminalWindowByTty(tty, terminalApp).catch(() => false);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
return sentSignal || (!pid && canCloseWindow);
|
|
462
700
|
}
|
|
463
701
|
|
|
464
|
-
module.exports = { launchAgent, closeAgent, resumeAgents };
|
|
702
|
+
module.exports = { launchAgent, closeAgent, getRecoverableAgents, resumeAgents };
|