u-foo 1.5.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -0
- package/README.zh-CN.md +21 -0
- package/modules/AGENTS.template.md +4 -102
- package/package.json +1 -1
- package/src/agent/activityDetector.js +328 -0
- package/src/agent/activityStatePublisher.js +67 -0
- package/src/agent/activityStateWriter.js +40 -0
- package/src/agent/internalRunner.js +13 -0
- package/src/agent/launcher.js +47 -7
- package/src/agent/notifier.js +73 -4
- package/src/agent/ptyRunner.js +81 -34
- package/src/agent/ufooAgent.js +192 -6
- package/src/bus/message.js +1 -9
- package/src/bus/subscriber.js +2 -0
- package/src/bus/utils.js +10 -0
- package/src/chat/agentBar.js +21 -3
- package/src/chat/agentViewController.js +2 -0
- package/src/chat/daemonConnection.js +45 -7
- package/src/chat/daemonMessageRouter.js +22 -0
- package/src/chat/daemonTransport.js +13 -2
- package/src/chat/daemonTransportDefaults.js +1 -0
- package/src/chat/dashboardKeyController.js +9 -0
- package/src/chat/dashboardView.js +32 -9
- package/src/chat/index.js +148 -6
- package/src/chat/projectCloseController.js +119 -0
- package/src/chat/projectRuntimes.js +55 -0
- package/src/chat/statusLineController.js +52 -6
- package/src/chat/transport.js +41 -5
- package/src/daemon/index.js +12 -3
- package/src/daemon/ipcServer.js +6 -1
- package/src/daemon/ops.js +46 -12
- package/src/daemon/status.js +3 -1
- package/src/init/index.js +32 -3
- package/src/ufoo/agentsStore.js +44 -0
package/src/daemon/index.js
CHANGED
|
@@ -378,8 +378,15 @@ async function handleOps(projectRoot, ops = [], processManager = null) {
|
|
|
378
378
|
results.push({ action: "launch", ok: false, agent, count, error: err.message });
|
|
379
379
|
}
|
|
380
380
|
} else if (op.action === "close") {
|
|
381
|
-
const
|
|
382
|
-
|
|
381
|
+
const closeResult = await closeAgent(projectRoot, op.agent_id);
|
|
382
|
+
const normalizedClose = closeResult && typeof closeResult === "object"
|
|
383
|
+
? closeResult
|
|
384
|
+
: { ok: Boolean(closeResult) };
|
|
385
|
+
results.push({
|
|
386
|
+
action: "close",
|
|
387
|
+
agent_id: op.agent_id,
|
|
388
|
+
...normalizedClose,
|
|
389
|
+
});
|
|
383
390
|
} else if (op.action === "rename") {
|
|
384
391
|
const agentId = op.agent_id || "";
|
|
385
392
|
const nickname = op.nickname || "";
|
|
@@ -943,7 +950,9 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
943
950
|
const closeResult = opsResults.find((r) => r.action === "close");
|
|
944
951
|
const ok = closeResult ? closeResult.ok !== false : true;
|
|
945
952
|
const reply = ok
|
|
946
|
-
?
|
|
953
|
+
? (closeResult && closeResult.already_stopped
|
|
954
|
+
? `Closed ${agent_id} (already stopped)`
|
|
955
|
+
: `Closed ${agent_id}`)
|
|
947
956
|
: `Close failed: ${closeResult?.error || "unknown error"}`;
|
|
948
957
|
socket.write(
|
|
949
958
|
`${JSON.stringify({
|
package/src/daemon/ipcServer.js
CHANGED
|
@@ -28,6 +28,7 @@ function createDaemonIpcServer(options = {}) {
|
|
|
28
28
|
};
|
|
29
29
|
|
|
30
30
|
let lastActiveJson = "";
|
|
31
|
+
let lastMetaJson = "";
|
|
31
32
|
const statusSyncInterval = setInterval(() => {
|
|
32
33
|
if (sockets.size === 0) return;
|
|
33
34
|
try {
|
|
@@ -38,8 +39,12 @@ function createDaemonIpcServer(options = {}) {
|
|
|
38
39
|
try {
|
|
39
40
|
const status = buildStatus(projectRoot);
|
|
40
41
|
const currentActiveJson = JSON.stringify(status.active);
|
|
41
|
-
|
|
42
|
+
const currentMetaJson = JSON.stringify(
|
|
43
|
+
(status.active_meta || []).map((m) => `${m.id}:${m.activity_state || ""}`)
|
|
44
|
+
);
|
|
45
|
+
if (currentActiveJson !== lastActiveJson || currentMetaJson !== lastMetaJson) {
|
|
42
46
|
lastActiveJson = currentActiveJson;
|
|
47
|
+
lastMetaJson = currentMetaJson;
|
|
43
48
|
sendToSockets({ type: IPC_RESPONSE_TYPES.STATUS, data: status });
|
|
44
49
|
log(`status sync: active agents changed to ${status.active.length}`);
|
|
45
50
|
}
|
package/src/daemon/ops.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { spawn } = require("child_process");
|
|
1
|
+
const { spawn, spawnSync } = require("child_process");
|
|
2
2
|
const fs = require("fs");
|
|
3
3
|
const path = require("path");
|
|
4
4
|
const { loadConfig } = require("../config");
|
|
@@ -827,43 +827,55 @@ async function resumeAgents(projectRoot, target = "", processManager = null) {
|
|
|
827
827
|
}
|
|
828
828
|
|
|
829
829
|
async function closeAgent(projectRoot, agentId) {
|
|
830
|
-
if (process.platform !== "darwin") {
|
|
831
|
-
return false;
|
|
832
|
-
}
|
|
833
830
|
const resolvedId = resolveAgentId(projectRoot, agentId);
|
|
834
831
|
const busPath = getUfooPaths(projectRoot).agentsFile;
|
|
835
|
-
let pid =
|
|
832
|
+
let pid = 0;
|
|
836
833
|
let launchMode = "";
|
|
837
834
|
let tty = "";
|
|
838
835
|
let terminalApp = "";
|
|
836
|
+
let tmuxPane = "";
|
|
837
|
+
let found = false;
|
|
839
838
|
try {
|
|
840
839
|
const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
|
|
841
840
|
const entry = bus.agents?.[resolvedId];
|
|
842
841
|
if (entry) {
|
|
843
|
-
|
|
842
|
+
found = true;
|
|
843
|
+
const parsedPid = Number.parseInt(entry.pid, 10);
|
|
844
|
+
pid = Number.isFinite(parsedPid) && parsedPid > 0 ? parsedPid : 0;
|
|
844
845
|
launchMode = entry.launch_mode || "";
|
|
845
846
|
tty = entry.tty || "";
|
|
846
847
|
terminalApp = entry.terminal_app || "";
|
|
848
|
+
tmuxPane = entry.tmux_pane || "";
|
|
847
849
|
}
|
|
848
850
|
} catch {
|
|
849
|
-
|
|
851
|
+
found = false;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
if (!found) {
|
|
855
|
+
return { ok: true, already_stopped: true, resolved_agent_id: resolvedId };
|
|
850
856
|
}
|
|
857
|
+
|
|
851
858
|
const adapterRouter = createTerminalAdapterRouter();
|
|
852
859
|
const adapter = adapterRouter.getAdapter({ launchMode, agentId: resolvedId });
|
|
853
|
-
const canCloseWindow =
|
|
860
|
+
const canCloseWindow = process.platform === "darwin"
|
|
861
|
+
&& Boolean(adapter.capabilities.supportsWindowClose)
|
|
862
|
+
&& Boolean(tty);
|
|
854
863
|
|
|
855
864
|
// Close process first for faster state transition in chat.
|
|
856
865
|
let sentSignal = false;
|
|
857
|
-
|
|
866
|
+
let killErr = null;
|
|
867
|
+
if (pid > 0) {
|
|
858
868
|
try {
|
|
859
869
|
process.kill(pid, "SIGTERM");
|
|
860
870
|
sentSignal = true;
|
|
861
|
-
} catch {
|
|
871
|
+
} catch (err) {
|
|
872
|
+
killErr = err || null;
|
|
862
873
|
sentSignal = false;
|
|
863
874
|
}
|
|
864
875
|
}
|
|
865
876
|
|
|
866
|
-
|
|
877
|
+
const pidGone = pid > 0 && !sentSignal && !isAgentPidAlive(pid);
|
|
878
|
+
if (sentSignal || pid === 0 || pidGone) {
|
|
867
879
|
markAgentInactive(projectRoot, resolvedId);
|
|
868
880
|
}
|
|
869
881
|
|
|
@@ -872,7 +884,29 @@ async function closeAgent(projectRoot, agentId) {
|
|
|
872
884
|
void closeTerminalWindowByTty(tty, terminalApp).catch(() => false);
|
|
873
885
|
}
|
|
874
886
|
|
|
875
|
-
|
|
887
|
+
// Tmux pane cleanup: kill the pane after sending SIGTERM to the process.
|
|
888
|
+
if (launchMode === "tmux" && tmuxPane) {
|
|
889
|
+
try {
|
|
890
|
+
spawnSync("tmux", ["kill-pane", "-t", tmuxPane], { stdio: "ignore", timeout: 3000 });
|
|
891
|
+
} catch {
|
|
892
|
+
// ignore - pane may already be gone
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
if (sentSignal) {
|
|
897
|
+
return { ok: true, resolved_agent_id: resolvedId };
|
|
898
|
+
}
|
|
899
|
+
if (pid === 0 || pidGone) {
|
|
900
|
+
return { ok: true, already_stopped: true, resolved_agent_id: resolvedId };
|
|
901
|
+
}
|
|
902
|
+
const reason = killErr && killErr.message
|
|
903
|
+
? killErr.message
|
|
904
|
+
: "failed to stop process";
|
|
905
|
+
return {
|
|
906
|
+
ok: false,
|
|
907
|
+
error: reason,
|
|
908
|
+
resolved_agent_id: resolvedId,
|
|
909
|
+
};
|
|
876
910
|
}
|
|
877
911
|
|
|
878
912
|
module.exports = { launchAgent, closeAgent, getRecoverableAgents, resumeAgents };
|
package/src/daemon/status.js
CHANGED
|
@@ -147,7 +147,9 @@ function buildStatus(projectRoot, options = {}) {
|
|
|
147
147
|
const launch_mode = meta?.launch_mode || "unknown";
|
|
148
148
|
const tmux_pane = meta?.tmux_pane || "";
|
|
149
149
|
const tty = meta?.tty || "";
|
|
150
|
-
|
|
150
|
+
const activity_state = meta?.activity_state || "";
|
|
151
|
+
const activity_since = meta?.activity_since || "";
|
|
152
|
+
return { id, nickname, display, launch_mode, tmux_pane, tty, activity_state, activity_since };
|
|
151
153
|
});
|
|
152
154
|
|
|
153
155
|
return {
|
package/src/init/index.js
CHANGED
|
@@ -180,23 +180,52 @@ class UfooInit {
|
|
|
180
180
|
|
|
181
181
|
let content = fs.readFileSync(filePath, "utf8");
|
|
182
182
|
const marker = "<!-- ufoo-template -->";
|
|
183
|
+
const block = `${marker}\n${template}\n${marker}`;
|
|
184
|
+
|
|
183
185
|
if (content.includes(marker)) {
|
|
186
|
+
// Replace existing marker block in-place
|
|
184
187
|
const startIdx = content.indexOf(marker);
|
|
185
188
|
const endIdx = content.indexOf(marker, startIdx + marker.length);
|
|
186
189
|
if (endIdx !== -1) {
|
|
187
190
|
content =
|
|
188
191
|
content.slice(0, startIdx) +
|
|
189
|
-
|
|
192
|
+
block +
|
|
190
193
|
content.slice(endIdx + marker.length);
|
|
191
194
|
} else {
|
|
192
|
-
content
|
|
195
|
+
content =
|
|
196
|
+
content.slice(0, startIdx) + block + content.slice(startIdx + marker.length);
|
|
193
197
|
}
|
|
194
198
|
} else {
|
|
195
|
-
|
|
199
|
+
// Insert after first heading line for visibility (not buried at end)
|
|
200
|
+
const headingEnd = this.findFirstHeadingEnd(content);
|
|
201
|
+
if (headingEnd !== -1) {
|
|
202
|
+
content =
|
|
203
|
+
content.slice(0, headingEnd) +
|
|
204
|
+
`\n${block}\n\n` +
|
|
205
|
+
content.slice(headingEnd);
|
|
206
|
+
} else {
|
|
207
|
+
content = `${block}\n\n${content}`;
|
|
208
|
+
}
|
|
196
209
|
}
|
|
197
210
|
fs.writeFileSync(filePath, content, "utf8");
|
|
198
211
|
}
|
|
199
212
|
|
|
213
|
+
findFirstHeadingEnd(content) {
|
|
214
|
+
// ATX heading: # ... (allow leading indentation and EOF without trailing newline)
|
|
215
|
+
const atxHeading = content.match(/^(?:[ \t]{0,3})#{1,6}[ \t]*[^\n]*(?:\n|$)/m);
|
|
216
|
+
// Setext heading: text line + underline (=== or ---)
|
|
217
|
+
const setextHeading = content.match(/^[^\n]+\n(?:=+|-+)[ \t]*(?:\n|$)/m);
|
|
218
|
+
|
|
219
|
+
let bestMatch = null;
|
|
220
|
+
if (atxHeading && setextHeading) {
|
|
221
|
+
bestMatch = atxHeading.index <= setextHeading.index ? atxHeading : setextHeading;
|
|
222
|
+
} else {
|
|
223
|
+
bestMatch = atxHeading || setextHeading;
|
|
224
|
+
}
|
|
225
|
+
if (!bestMatch) return -1;
|
|
226
|
+
return bestMatch.index + bestMatch[0].length;
|
|
227
|
+
}
|
|
228
|
+
|
|
200
229
|
/**
|
|
201
230
|
* 初始化 context 模块
|
|
202
231
|
*/
|
package/src/ufoo/agentsStore.js
CHANGED
|
@@ -94,8 +94,52 @@ function loadAgentsData(filePath) {
|
|
|
94
94
|
return normalizeAgentsData(data);
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
function parseTimestampMs(value) {
|
|
98
|
+
const ms = Date.parse(String(value || ""));
|
|
99
|
+
return Number.isFinite(ms) ? ms : Number.NaN;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function mergeExternalActivityFields(targetMeta, diskMeta) {
|
|
103
|
+
if (!targetMeta || !diskMeta) return;
|
|
104
|
+
|
|
105
|
+
const diskState = toSafeString(diskMeta.activity_state);
|
|
106
|
+
if (!diskState) return;
|
|
107
|
+
|
|
108
|
+
const memoryState = toSafeString(targetMeta.activity_state);
|
|
109
|
+
const diskSince = toSafeString(diskMeta.activity_since);
|
|
110
|
+
const memorySince = toSafeString(targetMeta.activity_since);
|
|
111
|
+
const diskSinceMs = parseTimestampMs(diskSince);
|
|
112
|
+
const memorySinceMs = parseTimestampMs(memorySince);
|
|
113
|
+
|
|
114
|
+
if (!memoryState) {
|
|
115
|
+
targetMeta.activity_state = diskState;
|
|
116
|
+
if (diskSince) targetMeta.activity_since = diskSince;
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const preferDisk = Number.isFinite(diskSinceMs)
|
|
121
|
+
&& (!Number.isFinite(memorySinceMs) || diskSinceMs > memorySinceMs);
|
|
122
|
+
if (preferDisk) {
|
|
123
|
+
targetMeta.activity_state = diskState;
|
|
124
|
+
targetMeta.activity_since = diskSince;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
97
128
|
function saveAgentsData(filePath, data) {
|
|
98
129
|
const normalized = normalizeAgentsData(data);
|
|
130
|
+
|
|
131
|
+
// Merge externally-managed fields from disk to avoid daemon in-memory writes
|
|
132
|
+
// overwriting fresher runner/notifier state updates.
|
|
133
|
+
const disk = readJSON(filePath, null);
|
|
134
|
+
if (disk && disk.agents && normalized.agents) {
|
|
135
|
+
for (const [id, diskMeta] of Object.entries(disk.agents)) {
|
|
136
|
+
if (!diskMeta || typeof diskMeta !== "object") continue;
|
|
137
|
+
const targetMeta = normalized.agents[id];
|
|
138
|
+
if (!targetMeta || typeof targetMeta !== "object") continue;
|
|
139
|
+
mergeExternalActivityFields(targetMeta, diskMeta);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
99
143
|
writeJSON(filePath, normalized);
|
|
100
144
|
}
|
|
101
145
|
|