u-foo 1.0.3 → 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 +110 -11
- package/README.zh-CN.md +9 -7
- package/SKILLS/ufoo/SKILL.md +132 -0
- package/SKILLS/uinit/SKILL.md +78 -0
- package/SKILLS/ustatus/SKILL.md +36 -0
- package/bin/uclaude.js +13 -0
- package/bin/ucode-core.js +15 -0
- package/bin/ucode.js +125 -0
- package/bin/ucodex.js +13 -0
- package/bin/ufoo +9 -31
- package/bin/ufoo-assistant-agent.js +5 -0
- package/bin/ufoo-engine.js +25 -0
- package/bin/ufoo.js +17 -0
- package/modules/AGENTS.template.md +29 -11
- package/modules/bus/README.md +33 -25
- package/modules/bus/SKILLS/ubus/SKILL.md +19 -8
- package/modules/context/README.md +18 -40
- package/modules/context/SKILLS/uctx/SKILL.md +63 -1
- package/modules/online/SKILLS/ufoo-online/SKILL.md +144 -0
- package/package.json +25 -4
- package/scripts/import-pi-mono.js +124 -0
- package/scripts/postinstall.js +30 -0
- package/scripts/sync-claude-skills.sh +21 -0
- package/src/agent/cliRunner.js +554 -33
- package/src/agent/internalRunner.js +150 -56
- package/src/agent/launcher.js +754 -0
- package/src/agent/normalizeOutput.js +1 -1
- package/src/agent/notifier.js +340 -0
- package/src/agent/ptyRunner.js +847 -0
- package/src/agent/ptyWrapper.js +379 -0
- package/src/agent/readyDetector.js +175 -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 +46 -42
- 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 +172 -0
- package/src/bus/daemon.js +436 -0
- package/src/bus/index.js +842 -0
- package/src/bus/inject.js +315 -0
- package/src/bus/message.js +430 -0
- package/src/bus/nickname.js +88 -0
- package/src/bus/queue.js +136 -0
- package/src/bus/shake.js +26 -0
- package/src/bus/store.js +189 -0
- package/src/bus/subscriber.js +312 -0
- package/src/bus/utils.js +363 -0
- 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 +1011 -1392
- 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 +1162 -96
- 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 +56 -3
- package/src/context/decisions.js +324 -0
- package/src/context/doctor.js +183 -0
- package/src/context/index.js +55 -0
- 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 +998 -170
- package/src/daemon/ipcServer.js +99 -0
- package/src/daemon/ops.js +630 -48
- package/src/daemon/promptLoop.js +319 -0
- package/src/daemon/promptRequest.js +101 -0
- package/src/daemon/providerSessions.js +306 -0
- package/src/daemon/reporting.js +90 -0
- package/src/daemon/run.js +31 -1
- package/src/daemon/status.js +48 -8
- package/src/doctor/index.js +50 -0
- package/src/init/index.js +318 -0
- 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/skills/index.js +159 -0
- package/src/status/index.js +285 -0
- 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/terminal/detect.js +64 -0
- package/src/terminal/index.js +8 -0
- package/src/terminal/iterm2.js +126 -0
- package/src/ufoo/agentsStore.js +107 -0
- package/src/ufoo/paths.js +46 -0
- package/src/utils/banner.js +76 -0
- package/bin/uclaude +0 -65
- package/bin/ucodex +0 -65
- package/modules/bus/scripts/bus-alert.sh +0 -185
- package/modules/bus/scripts/bus-listen.sh +0 -117
- package/modules/context/ASSUMPTIONS.md +0 -7
- package/modules/context/CONSTRAINTS.md +0 -7
- package/modules/context/CONTEXT-STRUCTURE.md +0 -49
- package/modules/context/DECISION-PROTOCOL.md +0 -62
- package/modules/context/HANDOFF.md +0 -33
- package/modules/context/RULES.md +0 -15
- package/modules/context/SKILLS/README.md +0 -14
- package/modules/context/SYSTEM.md +0 -18
- package/modules/context/TEMPLATES/assumptions.md +0 -4
- package/modules/context/TEMPLATES/constraints.md +0 -4
- package/modules/context/TEMPLATES/decision.md +0 -16
- package/modules/context/TEMPLATES/project-context-readme.md +0 -6
- package/modules/context/TEMPLATES/system.md +0 -3
- package/modules/context/TEMPLATES/terminology.md +0 -4
- package/modules/context/TERMINOLOGY.md +0 -10
- package/scripts/banner.sh +0 -89
- package/scripts/bus-alert.sh +0 -6
- package/scripts/bus-autotrigger.sh +0 -6
- package/scripts/bus-daemon.sh +0 -231
- package/scripts/bus-inject.sh +0 -144
- package/scripts/bus-listen.sh +0 -6
- package/scripts/bus.sh +0 -984
- package/scripts/context-decisions.sh +0 -167
- package/scripts/context-doctor.sh +0 -72
- package/scripts/context-lint.sh +0 -110
- package/scripts/doctor.sh +0 -22
- package/scripts/init.sh +0 -247
- package/scripts/skills.sh +0 -113
- package/scripts/status.sh +0 -125
package/src/daemon/ops.js
CHANGED
|
@@ -2,17 +2,52 @@ const { spawn } = require("child_process");
|
|
|
2
2
|
const fs = require("fs");
|
|
3
3
|
const path = require("path");
|
|
4
4
|
const { loadConfig } = require("../config");
|
|
5
|
+
const { getUfooPaths } = require("../ufoo/paths");
|
|
6
|
+
const { loadAgentsData, saveAgentsData } = require("../ufoo/agentsStore");
|
|
7
|
+
const { isAgentPidAlive, getTtyProcessInfo } = require("../bus/utils");
|
|
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
|
+
}
|
|
5
39
|
|
|
6
40
|
function resolveAgentId(projectRoot, agentId) {
|
|
7
41
|
if (!agentId) return agentId;
|
|
8
42
|
if (agentId.includes(":")) return agentId;
|
|
9
|
-
const busPath =
|
|
43
|
+
const busPath = getUfooPaths(projectRoot).agentsFile;
|
|
10
44
|
try {
|
|
11
45
|
const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
|
|
12
|
-
const entries = Object.entries(bus.
|
|
46
|
+
const entries = Object.entries(bus.agents || {});
|
|
13
47
|
const match = entries.find(([, meta]) => meta?.nickname === agentId);
|
|
14
48
|
if (match) return match[0];
|
|
15
|
-
const
|
|
49
|
+
const normalized = normalizeLaunchAgent(agentId);
|
|
50
|
+
const targetType = toBusAgentType(normalized) || agentId;
|
|
16
51
|
const candidates = entries
|
|
17
52
|
.filter(([, meta]) => meta?.agent_type === targetType && meta?.status === "active")
|
|
18
53
|
.map(([id]) => id);
|
|
@@ -23,75 +58,596 @@ function resolveAgentId(projectRoot, agentId) {
|
|
|
23
58
|
return agentId;
|
|
24
59
|
}
|
|
25
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
|
+
|
|
106
|
+
function shellEscape(value) {
|
|
107
|
+
const str = String(value);
|
|
108
|
+
return `'${str.replace(/'/g, `'\\''`)}'`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function escapeAppleScriptString(str) {
|
|
112
|
+
return String(str).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
113
|
+
}
|
|
114
|
+
|
|
26
115
|
function runAppleScript(lines) {
|
|
27
116
|
return new Promise((resolve, reject) => {
|
|
28
117
|
const proc = spawn("osascript", lines.flatMap((l) => ["-e", l]));
|
|
29
118
|
let stderr = "";
|
|
119
|
+
let stdout = "";
|
|
30
120
|
proc.stderr.on("data", (d) => {
|
|
31
121
|
stderr += d.toString("utf8");
|
|
32
122
|
});
|
|
123
|
+
proc.stdout.on("data", (d) => {
|
|
124
|
+
stdout += d.toString("utf8");
|
|
125
|
+
});
|
|
33
126
|
proc.on("close", (code) => {
|
|
34
|
-
if (code === 0) resolve();
|
|
127
|
+
if (code === 0) resolve(stdout.trim());
|
|
35
128
|
else reject(new Error(stderr || "osascript failed"));
|
|
36
129
|
});
|
|
37
130
|
});
|
|
38
131
|
}
|
|
39
132
|
|
|
40
|
-
function
|
|
41
|
-
|
|
133
|
+
async function openTerminalWindow(runCmd) {
|
|
134
|
+
if (process.platform !== "darwin") {
|
|
135
|
+
throw new Error("Terminal mode is only supported on macOS");
|
|
136
|
+
}
|
|
137
|
+
|
|
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
|
+
}
|
|
156
|
+
|
|
157
|
+
const script = [
|
|
158
|
+
'tell application "Terminal"',
|
|
159
|
+
`do script "${escaped}"`,
|
|
160
|
+
"activate",
|
|
161
|
+
"end tell",
|
|
162
|
+
];
|
|
163
|
+
await runAppleScript(script);
|
|
42
164
|
}
|
|
43
165
|
|
|
44
|
-
function
|
|
45
|
-
|
|
46
|
-
|
|
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
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Spawn managed terminal agent - open a real Terminal session to run the agent
|
|
264
|
+
*/
|
|
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}`);
|
|
271
|
+
}
|
|
272
|
+
const existing = listSubscribers(projectRoot, agentType);
|
|
273
|
+
const runDir = getUfooPaths(projectRoot).runDir;
|
|
274
|
+
fs.mkdirSync(runDir, { recursive: true });
|
|
275
|
+
|
|
276
|
+
const args = Array.isArray(extraArgs) ? extraArgs : [];
|
|
277
|
+
const argText = args.length > 0 ? ` ${args.map(shellEscape).join(" ")}` : "";
|
|
278
|
+
const nickEnv = nickname ? `UFOO_NICKNAME=${shellEscape(nickname)} ` : "";
|
|
279
|
+
const modeEnv = "UFOO_LAUNCH_MODE=terminal ";
|
|
280
|
+
const envPrefix = extraEnv ? `${String(extraEnv).trim()} ` : "";
|
|
281
|
+
const titleCmd = buildTitleCmd(nickname);
|
|
282
|
+
const prefix = titleCmd ? `${titleCmd} && ` : "";
|
|
283
|
+
|
|
284
|
+
const runCmd = `cd ${shellEscape(projectRoot)} && ${prefix}${modeEnv}${nickEnv}${envPrefix}${binary}${argText}`;
|
|
285
|
+
|
|
286
|
+
await openTerminalWindow(runCmd);
|
|
287
|
+
|
|
288
|
+
const subscriberId = await waitForNewSubscriber(projectRoot, agentType, existing, 15000);
|
|
289
|
+
return { child: null, subscriberId: subscriberId || null };
|
|
47
290
|
}
|
|
48
291
|
|
|
49
|
-
async function spawnInternalAgent(projectRoot, agent, count = 1, nickname = "") {
|
|
292
|
+
async function spawnInternalAgent(projectRoot, agent, count = 1, nickname = "", processManager = null) {
|
|
50
293
|
const runner = path.join(projectRoot, "bin", "ufoo.js");
|
|
51
|
-
const logDir =
|
|
52
|
-
|
|
53
|
-
|
|
294
|
+
const logDir = getUfooPaths(projectRoot).runDir;
|
|
295
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
296
|
+
|
|
297
|
+
const crypto = require("crypto");
|
|
298
|
+
const EventBus = require("../bus");
|
|
299
|
+
const children = [];
|
|
300
|
+
const subscriberIds = [];
|
|
301
|
+
|
|
302
|
+
// 初始化 bus
|
|
303
|
+
const bus = new EventBus(projectRoot);
|
|
304
|
+
await bus.init();
|
|
305
|
+
|
|
306
|
+
const originalPid = process.pid;
|
|
307
|
+
|
|
54
308
|
for (let i = 0; i < count; i += 1) {
|
|
55
|
-
const
|
|
56
|
-
|
|
309
|
+
const logFile = path.join(logDir, `agent-${agent}-${Date.now()}-${i}.log`);
|
|
310
|
+
const errLog = fs.openSync(logFile, "a");
|
|
311
|
+
|
|
312
|
+
// 预生成 session ID
|
|
313
|
+
const sessionId = crypto.randomBytes(4).toString("hex");
|
|
314
|
+
const normalizedAgent = normalizeLaunchAgent(agent);
|
|
315
|
+
const agentType = toBusAgentType(normalizedAgent);
|
|
316
|
+
if (!agentType) {
|
|
317
|
+
throw new Error(`unsupported agent type: ${agent}`);
|
|
318
|
+
}
|
|
319
|
+
const subscriberId = `${agentType}:${sessionId}`;
|
|
320
|
+
subscriberIds.push(subscriberId);
|
|
321
|
+
|
|
322
|
+
// Daemon 预先在 bus 中注册
|
|
323
|
+
bus.loadBusData();
|
|
324
|
+
process.env.UFOO_PARENT_PID = String(originalPid);
|
|
325
|
+
|
|
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);
|
|
329
|
+
const usePty = process.env.UFOO_INTERNAL_PTY !== "0";
|
|
330
|
+
const launchMode = usePty ? "internal-pty" : "internal";
|
|
331
|
+
|
|
332
|
+
// 传递 launch_mode 和 parent PID 到 join
|
|
333
|
+
await bus.subscriberManager.join(sessionId, agentType, finalNickname, {
|
|
334
|
+
launchMode,
|
|
335
|
+
parentPid: originalPid,
|
|
336
|
+
});
|
|
337
|
+
bus.saveBusData();
|
|
338
|
+
|
|
339
|
+
const runnerCmd = usePty ? "agent-pty-runner" : "agent-runner";
|
|
340
|
+
const child = spawn(process.execPath, [runner, runnerCmd, agent], {
|
|
341
|
+
// 关键改动:不使用 detached,daemon 作为父进程
|
|
342
|
+
detached: false,
|
|
57
343
|
stdio: ["ignore", errLog, errLog],
|
|
58
344
|
cwd: projectRoot,
|
|
59
|
-
env: {
|
|
345
|
+
env: {
|
|
346
|
+
...process.env,
|
|
347
|
+
UFOO_INTERNAL_AGENT: "1",
|
|
348
|
+
UFOO_INTERNAL_PTY: usePty ? "1" : "0",
|
|
349
|
+
UFOO_SUBSCRIBER_ID: subscriberId, // 直接传递 subscriber ID
|
|
350
|
+
UFOO_NICKNAME: finalNickname,
|
|
351
|
+
UFOO_LAUNCH_MODE: usePty ? "internal-pty" : "internal",
|
|
352
|
+
UFOO_PARENT_PID: String(originalPid),
|
|
353
|
+
},
|
|
60
354
|
});
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
355
|
+
|
|
356
|
+
// Update bus data with the actual child PID so isMetaActive
|
|
357
|
+
// can detect when the ptyRunner process dies.
|
|
64
358
|
try {
|
|
65
|
-
|
|
359
|
+
bus.loadBusData();
|
|
360
|
+
if (bus.busData.agents && bus.busData.agents[subscriberId]) {
|
|
361
|
+
bus.busData.agents[subscriberId].pid = child.pid;
|
|
362
|
+
}
|
|
363
|
+
bus.saveBusData();
|
|
66
364
|
} catch {
|
|
67
|
-
// ignore
|
|
365
|
+
// ignore pid update errors
|
|
68
366
|
}
|
|
69
|
-
|
|
367
|
+
|
|
368
|
+
// 本地日志记录
|
|
369
|
+
child.on("exit", (code, signal) => {
|
|
370
|
+
try {
|
|
371
|
+
fs.closeSync(errLog);
|
|
372
|
+
} catch {
|
|
373
|
+
// ignore
|
|
374
|
+
}
|
|
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
|
+
|
|
388
|
+
if (signal) {
|
|
389
|
+
fs.appendFileSync(logFile, `\n[internal-agent] ${subscriberId} killed by signal ${signal}\n`);
|
|
390
|
+
} else {
|
|
391
|
+
fs.appendFileSync(logFile, `\n[internal-agent] ${subscriberId} exited with code ${code}\n`);
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
child.on("error", (err) => {
|
|
396
|
+
fs.appendFileSync(logFile, `\n[internal-agent] ${subscriberId} spawn failed: ${err.message}\n`);
|
|
397
|
+
try {
|
|
398
|
+
fs.closeSync(errLog);
|
|
399
|
+
} catch {
|
|
400
|
+
// ignore
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
// 注册到进程管理器(父子进程监控)
|
|
405
|
+
if (processManager) {
|
|
406
|
+
processManager.register(subscriberId, child);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
children.push(child);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return { children, subscriberIds };
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function spawnTmuxWindow(projectRoot, agent, nickname = "", extraArgs = [], extraEnv = "") {
|
|
416
|
+
return new Promise((resolve, reject) => {
|
|
417
|
+
const normalizedAgent = normalizeLaunchAgent(agent);
|
|
418
|
+
const binary = toTmuxBinary(normalizedAgent);
|
|
419
|
+
if (!binary) {
|
|
420
|
+
reject(new Error(`unsupported agent type: ${agent}`));
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
const nickEnv = nickname ? `UFOO_NICKNAME=${shellEscape(nickname)} ` : "";
|
|
424
|
+
const modeEnv = "UFOO_LAUNCH_MODE=tmux ";
|
|
425
|
+
const ttyEnv = "UFOO_TTY_OVERRIDE=$(tty) ";
|
|
426
|
+
const args = Array.isArray(extraArgs) ? extraArgs : [];
|
|
427
|
+
const envPrefix = extraEnv ? `${String(extraEnv).trim()} ` : "";
|
|
428
|
+
const argText = args.length > 0 ? ` ${args.map(shellEscape).join(" ")}` : "";
|
|
429
|
+
|
|
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}`;
|
|
434
|
+
const windowName = nickname || `${agent}-${Date.now()}`;
|
|
435
|
+
|
|
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);
|
|
443
|
+
}
|
|
444
|
+
tmuxArgs.push(runCmd);
|
|
445
|
+
|
|
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
|
+
});
|
|
70
456
|
}
|
|
71
457
|
|
|
72
|
-
async function
|
|
458
|
+
async function launchAgent(projectRoot, agent, count = 1, nickname = "", processManager = null) {
|
|
73
459
|
const config = loadConfig(projectRoot);
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
460
|
+
const mode = config.launchMode || "terminal";
|
|
461
|
+
const normalizedAgent = normalizeLaunchAgent(agent);
|
|
462
|
+
if (!normalizedAgent) {
|
|
463
|
+
throw new Error(`unsupported agent type: ${agent}`);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (mode === "internal") {
|
|
467
|
+
const result = await spawnInternalAgent(projectRoot, normalizedAgent, count, nickname, processManager);
|
|
468
|
+
return { mode: "internal", subscriberIds: result.subscriberIds };
|
|
469
|
+
}
|
|
470
|
+
if (mode === "tmux") {
|
|
471
|
+
// Check if tmux is available
|
|
472
|
+
const tmuxCheck = spawn("tmux", ["list-sessions"], { stdio: "pipe" });
|
|
473
|
+
let stdout = "";
|
|
474
|
+
tmuxCheck.stdout.on("data", (d) => {
|
|
475
|
+
stdout += d.toString("utf8");
|
|
476
|
+
});
|
|
477
|
+
const tmuxAvailable = await new Promise((resolve) => {
|
|
478
|
+
tmuxCheck.on("close", (code) => resolve(code === 0));
|
|
479
|
+
tmuxCheck.on("error", () => resolve(false));
|
|
480
|
+
});
|
|
481
|
+
if (!tmuxAvailable) {
|
|
482
|
+
throw new Error("tmux is not available or no tmux session is running");
|
|
483
|
+
}
|
|
484
|
+
// If UFOO_TMUX_SESSION not set, use first available session
|
|
485
|
+
if (!process.env.UFOO_TMUX_SESSION && stdout) {
|
|
486
|
+
const sessions = stdout.trim().split("\n");
|
|
487
|
+
if (sessions.length > 0) {
|
|
488
|
+
const firstSession = sessions[0].split(":")[0];
|
|
489
|
+
process.env.UFOO_TMUX_SESSION = firstSession;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
for (let i = 0; i < count; i += 1) {
|
|
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 || "");
|
|
496
|
+
// eslint-disable-next-line no-await-in-loop
|
|
497
|
+
await spawnTmuxWindow(projectRoot, normalizedAgent, nick);
|
|
498
|
+
}
|
|
499
|
+
return { mode: "tmux" };
|
|
77
500
|
}
|
|
501
|
+
// terminal mode - daemon 作为父进程,输出到终端窗口
|
|
78
502
|
if (process.platform !== "darwin") {
|
|
79
|
-
throw new Error("
|
|
503
|
+
throw new Error("launchAgent with terminal mode is only supported on macOS Terminal.app");
|
|
80
504
|
}
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
const nickEnv = nickname ? `UFOO_NICKNAME=${shellEscape(nickname)} ` : "";
|
|
84
|
-
const runCmd = `${cwdCmd} && ${nickEnv}${binary}`;
|
|
85
|
-
const script = [
|
|
86
|
-
'tell application "Terminal"',
|
|
87
|
-
`do script "${escapeCommand(runCmd)}"`,
|
|
88
|
-
"activate",
|
|
89
|
-
"end tell",
|
|
90
|
-
];
|
|
505
|
+
|
|
506
|
+
const subscriberIds = [];
|
|
91
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 || "");
|
|
92
511
|
// eslint-disable-next-line no-await-in-loop
|
|
93
|
-
await
|
|
512
|
+
const result = await spawnManagedTerminalAgent(projectRoot, normalizedAgent, nick, processManager);
|
|
513
|
+
if (result.subscriberId) subscriberIds.push(result.subscriberId);
|
|
94
514
|
}
|
|
515
|
+
|
|
516
|
+
return { mode: "terminal", subscriberIds };
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function normalizeAgentType(agentType) {
|
|
520
|
+
if (agentType === "claude-code") return "claude";
|
|
521
|
+
if (agentType === "codex") return "codex";
|
|
522
|
+
if (agentType === "ufoo-code") return "ufoo";
|
|
523
|
+
return agentType;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function buildResumeArgs(agent, sessionId) {
|
|
527
|
+
if (!sessionId) return [];
|
|
528
|
+
if (agent === "codex") return ["resume", sessionId];
|
|
529
|
+
if (agent === "claude") return ["--session-id", sessionId];
|
|
530
|
+
return [];
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function isActiveAgent(meta) {
|
|
534
|
+
if (!meta || meta.status !== "active") return false;
|
|
535
|
+
if (meta.pid && !isAgentPidAlive(meta.pid)) return false;
|
|
536
|
+
return true;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function collectRecoverableAgents(projectRoot, target = "") {
|
|
540
|
+
const config = loadConfig(projectRoot);
|
|
541
|
+
const mode = config.launchMode || "terminal";
|
|
542
|
+
const filePath = getUfooPaths(projectRoot).agentsFile;
|
|
543
|
+
const data = loadAgentsData(filePath);
|
|
544
|
+
const entries = Object.entries(data.agents || {});
|
|
545
|
+
|
|
546
|
+
let targets = entries;
|
|
547
|
+
if (target) {
|
|
548
|
+
if (target.includes(":")) {
|
|
549
|
+
targets = entries.filter(([id]) => id === target);
|
|
550
|
+
} else {
|
|
551
|
+
targets = entries.filter(([id, meta]) => id === target || (meta && meta.nickname === target));
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const recoverableEntries = [];
|
|
556
|
+
const skipped = [];
|
|
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
|
+
|
|
567
|
+
for (const [id, meta] of targets) {
|
|
568
|
+
if (!meta || !meta.provider_session_id) {
|
|
569
|
+
skipped.push({ id, reason: "no provider session" });
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
if (isActiveAgent(meta)) {
|
|
573
|
+
skipped.push({ id, reason: "already active" });
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
const agent = normalizeAgentType(meta.agent_type);
|
|
577
|
+
if (agent !== "codex" && agent !== "claude") {
|
|
578
|
+
skipped.push({ id, reason: "unsupported agent type" });
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
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 });
|
|
588
|
+
}
|
|
589
|
+
|
|
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) {
|
|
616
|
+
return { ok: true, resumed: [], skipped };
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Clear old nicknames to allow reuse.
|
|
620
|
+
let updated = false;
|
|
621
|
+
for (const item of recoverableEntries) {
|
|
622
|
+
if (item.meta && item.meta.nickname) {
|
|
623
|
+
data.agents[item.id] = { ...item.meta, nickname: "" };
|
|
624
|
+
updated = true;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
if (updated) {
|
|
628
|
+
saveAgentsData(filePath, data);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const resumed = [];
|
|
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) {
|
|
637
|
+
const args = buildResumeArgs(item.agent, sessionId);
|
|
638
|
+
const envPrefix = "UFOO_SKIP_SESSION_PROBE=1";
|
|
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
|
+
}
|
|
646
|
+
}
|
|
647
|
+
resumed.push({ id: item.id, nickname, agent: item.agent, sessionId, reused });
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
return { ok: true, resumed, skipped };
|
|
95
651
|
}
|
|
96
652
|
|
|
97
653
|
async function closeAgent(projectRoot, agentId) {
|
|
@@ -99,22 +655,48 @@ async function closeAgent(projectRoot, agentId) {
|
|
|
99
655
|
return false;
|
|
100
656
|
}
|
|
101
657
|
const resolvedId = resolveAgentId(projectRoot, agentId);
|
|
102
|
-
const busPath =
|
|
658
|
+
const busPath = getUfooPaths(projectRoot).agentsFile;
|
|
103
659
|
let pid = null;
|
|
660
|
+
let launchMode = "";
|
|
661
|
+
let tty = "";
|
|
662
|
+
let terminalApp = "";
|
|
104
663
|
try {
|
|
105
664
|
const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
|
|
106
|
-
const entry = bus.
|
|
107
|
-
if (entry
|
|
665
|
+
const entry = bus.agents?.[resolvedId];
|
|
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
|
+
}
|
|
108
672
|
} catch {
|
|
109
673
|
pid = null;
|
|
110
674
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
+
}
|
|
117
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);
|
|
118
700
|
}
|
|
119
701
|
|
|
120
|
-
module.exports = {
|
|
702
|
+
module.exports = { launchAgent, closeAgent, getRecoverableAgents, resumeAgents };
|