u-foo 1.5.0 → 1.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/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 +110 -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/activate.js +22 -2
- package/src/bus/daemon.js +1 -1
- package/src/bus/inject.js +29 -10
- package/src/bus/message.js +1 -9
- package/src/bus/subscriber.js +34 -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/commandExecutor.js +15 -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 +176 -8
- 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/cli.js +14 -0
- package/src/config.js +1 -0
- package/src/daemon/index.js +63 -5
- package/src/daemon/ipcServer.js +6 -1
- package/src/daemon/ops.js +189 -14
- package/src/daemon/status.js +17 -1
- package/src/init/index.js +32 -3
- package/src/terminal/adapterRouter.js +13 -1
- package/src/terminal/adapters/hostAdapter.js +409 -0
- package/src/ufoo/agentsStore.js +44 -0
package/src/agent/ufooAgent.js
CHANGED
|
@@ -10,6 +10,7 @@ const {
|
|
|
10
10
|
resolveAnthropicMessagesUrl,
|
|
11
11
|
} = require("../code/nativeRunner");
|
|
12
12
|
const { DEFAULT_ASSISTANT_TIMEOUT_MS } = require("../assistant/constants");
|
|
13
|
+
const { normalizeAgentTypeAlias } = require("../bus/utils");
|
|
13
14
|
|
|
14
15
|
function loadSessionState(projectRoot) {
|
|
15
16
|
const dir = getUfooPaths(projectRoot).agentDir;
|
|
@@ -28,13 +29,166 @@ function saveSessionState(projectRoot, state) {
|
|
|
28
29
|
fs.writeFileSync(path.join(dir, "ufoo-agent.json"), JSON.stringify(state, null, 2));
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
function toReportAgentSnapshot(value = {}) {
|
|
33
|
+
const last = value && typeof value.last === "object" ? value.last : null;
|
|
34
|
+
return {
|
|
35
|
+
agent_id: String(value && value.agent_id ? value.agent_id : ""),
|
|
36
|
+
pending_count: Number(value && value.pending_count ? value.pending_count : 0) || 0,
|
|
37
|
+
updated_at: String(value && value.updated_at ? value.updated_at : ""),
|
|
38
|
+
last: last
|
|
39
|
+
? {
|
|
40
|
+
phase: String(last.phase || ""),
|
|
41
|
+
task_id: String(last.task_id || ""),
|
|
42
|
+
ok: last.ok !== false,
|
|
43
|
+
}
|
|
44
|
+
: null,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isBusyActivityState(value = "") {
|
|
49
|
+
const state = String(value || "").trim().toLowerCase();
|
|
50
|
+
return state === "working" || state === "starting" || state === "running"
|
|
51
|
+
|| state === "waiting_input" || state === "blocked";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function clipPromptText(value = "", maxChars = 240) {
|
|
55
|
+
const text = String(value || "").replace(/\s+/g, " ").trim();
|
|
56
|
+
if (!text) return "";
|
|
57
|
+
if (text.length <= maxChars) return text;
|
|
58
|
+
return `${text.slice(0, maxChars)}...[truncated]`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function resolveHistoryAgentId(rawTarget, activeIdSet, nicknames) {
|
|
62
|
+
const target = String(rawTarget || "").trim();
|
|
63
|
+
if (!target) return "";
|
|
64
|
+
if (target === "*" || target === "broadcast") return "";
|
|
65
|
+
if (activeIdSet.has(target)) return target;
|
|
66
|
+
if (nicknames[target]) return nicknames[target];
|
|
67
|
+
|
|
68
|
+
const targetAlias = normalizeAgentTypeAlias(target);
|
|
69
|
+
if (!targetAlias) return "";
|
|
70
|
+
|
|
71
|
+
const matches = [];
|
|
72
|
+
for (const id of activeIdSet) {
|
|
73
|
+
const prefix = String(id).split(":")[0] || "";
|
|
74
|
+
const alias = normalizeAgentTypeAlias(prefix);
|
|
75
|
+
if (alias === targetAlias) matches.push(id);
|
|
76
|
+
}
|
|
77
|
+
return matches.length === 1 ? matches[0] : "";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function buildAgentPromptHistory(projectRoot, agents = [], nicknames = {}, options = {}) {
|
|
81
|
+
const perAgentLimit = Number.isFinite(options.perAgentLimit) && options.perAgentLimit > 0
|
|
82
|
+
? Math.floor(options.perAgentLimit)
|
|
83
|
+
: 6;
|
|
84
|
+
const maxFiles = Number.isFinite(options.maxFiles) && options.maxFiles > 0
|
|
85
|
+
? Math.floor(options.maxFiles)
|
|
86
|
+
: 3;
|
|
87
|
+
const eventsDir = getUfooPaths(projectRoot).busEventsDir;
|
|
88
|
+
const activeIds = new Set((Array.isArray(agents) ? agents : []).map((item) => String(item.id || "")).filter(Boolean));
|
|
89
|
+
if (activeIds.size === 0) {
|
|
90
|
+
return { per_agent: [], scanned_files: 0, matched_events: 0 };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const entries = new Map();
|
|
94
|
+
for (const item of agents) {
|
|
95
|
+
if (!item || !item.id) continue;
|
|
96
|
+
entries.set(item.id, {
|
|
97
|
+
agent_id: String(item.id),
|
|
98
|
+
nickname: String(item.nickname || ""),
|
|
99
|
+
samples: [],
|
|
100
|
+
sample_count: 0,
|
|
101
|
+
total_count: 0,
|
|
102
|
+
first_ts: "",
|
|
103
|
+
last_ts: "",
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let files = [];
|
|
108
|
+
try {
|
|
109
|
+
files = fs
|
|
110
|
+
.readdirSync(eventsDir)
|
|
111
|
+
.filter((name) => name.endsWith(".jsonl"))
|
|
112
|
+
.sort()
|
|
113
|
+
.slice(-maxFiles)
|
|
114
|
+
.reverse();
|
|
115
|
+
} catch {
|
|
116
|
+
return { per_agent: [], scanned_files: 0, matched_events: 0 };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let matchedEvents = 0;
|
|
120
|
+
for (const file of files) {
|
|
121
|
+
let lines = [];
|
|
122
|
+
try {
|
|
123
|
+
const raw = fs.readFileSync(path.join(eventsDir, file), "utf8");
|
|
124
|
+
lines = raw.split(/\r?\n/).filter(Boolean).reverse();
|
|
125
|
+
} catch {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
for (const line of lines) {
|
|
130
|
+
let evt = null;
|
|
131
|
+
try {
|
|
132
|
+
evt = JSON.parse(line);
|
|
133
|
+
} catch {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (!evt || evt.event !== "message") continue;
|
|
137
|
+
const targetAgentId = resolveHistoryAgentId(evt.target, activeIds, nicknames);
|
|
138
|
+
if (!targetAgentId) continue;
|
|
139
|
+
const prompt = evt.data && typeof evt.data.message === "string"
|
|
140
|
+
? clipPromptText(evt.data.message)
|
|
141
|
+
: "";
|
|
142
|
+
if (!prompt) continue;
|
|
143
|
+
|
|
144
|
+
const row = entries.get(targetAgentId);
|
|
145
|
+
if (!row) continue;
|
|
146
|
+
matchedEvents += 1;
|
|
147
|
+
row.total_count += 1;
|
|
148
|
+
const ts = String(evt.timestamp || evt.ts || "");
|
|
149
|
+
if (!row.last_ts) row.last_ts = ts;
|
|
150
|
+
row.first_ts = ts || row.first_ts;
|
|
151
|
+
if (row.samples.length < perAgentLimit) {
|
|
152
|
+
row.samples.push({
|
|
153
|
+
ts,
|
|
154
|
+
publisher: String(evt.publisher || ""),
|
|
155
|
+
prompt,
|
|
156
|
+
});
|
|
157
|
+
row.sample_count = row.samples.length;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const perAgent = Array.from(entries.values())
|
|
163
|
+
.filter((row) => row.total_count > 0)
|
|
164
|
+
.sort((a, b) => {
|
|
165
|
+
const left = String(a.last_ts || "");
|
|
166
|
+
const right = String(b.last_ts || "");
|
|
167
|
+
return right.localeCompare(left);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
per_agent: perAgent,
|
|
172
|
+
scanned_files: files.length,
|
|
173
|
+
matched_events: matchedEvents,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
31
177
|
function loadBusSummary(projectRoot, maxLines = 20) {
|
|
32
|
-
// Use daemon's buildStatus as the single source of truth
|
|
178
|
+
// Use daemon's buildStatus as the single source of truth.
|
|
33
179
|
let agents = [];
|
|
34
180
|
let nicknames = {};
|
|
181
|
+
let reports = { pending_total: 0, agents: [] };
|
|
182
|
+
let promptHistory = { per_agent: [], scanned_files: 0, matched_events: 0 };
|
|
183
|
+
let summary = {
|
|
184
|
+
active_count: 0,
|
|
185
|
+
busy_count: 0,
|
|
186
|
+
ready_count: 0,
|
|
187
|
+
pending_total: 0,
|
|
188
|
+
};
|
|
35
189
|
try {
|
|
36
190
|
const status = buildStatus(projectRoot);
|
|
37
|
-
const activeMeta = status.active_meta
|
|
191
|
+
const activeMeta = Array.isArray(status && status.active_meta) ? status.active_meta : [];
|
|
38
192
|
agents = activeMeta.map((item) => {
|
|
39
193
|
const nickname = item.nickname || "";
|
|
40
194
|
if (nickname) {
|
|
@@ -42,16 +196,45 @@ function loadBusSummary(projectRoot, maxLines = 20) {
|
|
|
42
196
|
}
|
|
43
197
|
return {
|
|
44
198
|
id: item.id,
|
|
199
|
+
nickname,
|
|
45
200
|
status: "active",
|
|
46
201
|
online: true,
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
202
|
+
launch_mode: String(item.launch_mode || ""),
|
|
203
|
+
activity_state: String(item.activity_state || ""),
|
|
204
|
+
activity_since: String(item.activity_since || ""),
|
|
50
205
|
};
|
|
51
206
|
});
|
|
207
|
+
|
|
208
|
+
const reportState = status && status.reports && typeof status.reports === "object"
|
|
209
|
+
? status.reports
|
|
210
|
+
: {};
|
|
211
|
+
const reportAgents = Array.isArray(reportState.agents)
|
|
212
|
+
? reportState.agents.slice(0, 50).map((item) => toReportAgentSnapshot(item))
|
|
213
|
+
: [];
|
|
214
|
+
reports = {
|
|
215
|
+
pending_total: Number(reportState.pending_total || 0) || 0,
|
|
216
|
+
agents: reportAgents,
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const busyCount = agents.filter((item) => isBusyActivityState(item.activity_state)).length;
|
|
220
|
+
summary = {
|
|
221
|
+
active_count: agents.length,
|
|
222
|
+
busy_count: busyCount,
|
|
223
|
+
ready_count: Math.max(agents.length - busyCount, 0),
|
|
224
|
+
pending_total: reports.pending_total,
|
|
225
|
+
};
|
|
226
|
+
promptHistory = buildAgentPromptHistory(projectRoot, agents, nicknames);
|
|
52
227
|
} catch {
|
|
53
228
|
agents = [];
|
|
54
229
|
nicknames = {};
|
|
230
|
+
reports = { pending_total: 0, agents: [] };
|
|
231
|
+
promptHistory = { per_agent: [], scanned_files: 0, matched_events: 0 };
|
|
232
|
+
summary = {
|
|
233
|
+
active_count: 0,
|
|
234
|
+
busy_count: 0,
|
|
235
|
+
ready_count: 0,
|
|
236
|
+
pending_total: 0,
|
|
237
|
+
};
|
|
55
238
|
}
|
|
56
239
|
|
|
57
240
|
const eventsDir = getUfooPaths(projectRoot).busEventsDir;
|
|
@@ -74,7 +257,7 @@ function loadBusSummary(projectRoot, maxLines = 20) {
|
|
|
74
257
|
recent = [];
|
|
75
258
|
}
|
|
76
259
|
|
|
77
|
-
return { agents, nicknames, recent };
|
|
260
|
+
return { agents, nicknames, reports, agent_prompt_history: promptHistory, summary, recent };
|
|
78
261
|
}
|
|
79
262
|
|
|
80
263
|
function buildSystemPrompt(context) {
|
|
@@ -106,6 +289,9 @@ function buildSystemPrompt(context) {
|
|
|
106
289
|
"- Use top-level assistant_call for project exploration, temporary shell tasks, and quick execution support.",
|
|
107
290
|
"- assistant_call fields: kind (explore|bash|mixed), task (required), context/expect (optional), provider (codex|claude|ufoo, optional), model/timeout_ms (optional).",
|
|
108
291
|
"- Prefer assistant_call over launching coding agents when the task is short-lived.",
|
|
292
|
+
"- Primary routing signal is semantic continuity from agent_prompt_history; prefer the agent that already handled similar prompts.",
|
|
293
|
+
"- Launch a new coding agent when the request is a new topic without clear ownership in existing histories.",
|
|
294
|
+
"- If best-matching target agent is busy, keep routing to that same agent (queue semantics) instead of rerouting only by idle status.",
|
|
109
295
|
"- Legacy compatibility: if model emits ops.assistant_call, daemon will still process it.",
|
|
110
296
|
"- If no action needed, return reply with empty dispatch/ops.",
|
|
111
297
|
agentGuidance,
|
package/src/bus/activate.js
CHANGED
|
@@ -40,6 +40,11 @@ class AgentActivator {
|
|
|
40
40
|
tty: meta.tty || "",
|
|
41
41
|
tmux_pane: meta.tmux_pane || "",
|
|
42
42
|
launch_mode: meta.launch_mode || "",
|
|
43
|
+
host_inject_sock: meta.host_inject_sock || "",
|
|
44
|
+
host_daemon_sock: meta.host_daemon_sock || "",
|
|
45
|
+
host_name: meta.host_name || "",
|
|
46
|
+
host_session_id: meta.host_session_id || "",
|
|
47
|
+
host_capabilities: meta.host_capabilities || null,
|
|
43
48
|
};
|
|
44
49
|
} catch (err) {
|
|
45
50
|
throw new Error(`Failed to get agent info: ${err.message}`);
|
|
@@ -156,7 +161,19 @@ end tell`;
|
|
|
156
161
|
activateTerminal,
|
|
157
162
|
activateTmux,
|
|
158
163
|
});
|
|
159
|
-
const adapter = adapterRouter.getAdapter({
|
|
164
|
+
const adapter = adapterRouter.getAdapter({
|
|
165
|
+
launchMode: info.launch_mode,
|
|
166
|
+
agentId,
|
|
167
|
+
meta: info,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
if (info.launch_mode === "host" && typeof adapter.connect === "function") {
|
|
171
|
+
try {
|
|
172
|
+
await adapter.connect();
|
|
173
|
+
} catch {
|
|
174
|
+
// fall back to seeded capabilities from bus metadata
|
|
175
|
+
}
|
|
176
|
+
}
|
|
160
177
|
|
|
161
178
|
if (!adapter.capabilities.supportsActivate) {
|
|
162
179
|
if (adapter.capabilities.supportsInternalQueueLoop) {
|
|
@@ -165,7 +182,10 @@ end tell`;
|
|
|
165
182
|
throw new Error("Cannot activate: missing tty or tmux_pane for agent");
|
|
166
183
|
}
|
|
167
184
|
|
|
168
|
-
await adapter.activate();
|
|
185
|
+
const activated = await adapter.activate();
|
|
186
|
+
if (activated === false) {
|
|
187
|
+
throw new Error("Host activation request was rejected");
|
|
188
|
+
}
|
|
169
189
|
}
|
|
170
190
|
}
|
|
171
191
|
|
package/src/bus/daemon.js
CHANGED
|
@@ -218,7 +218,7 @@ class BusDaemon {
|
|
|
218
218
|
// - notifier/injector: terminal/tmux
|
|
219
219
|
// - internal queue loop: internal/internal-pty
|
|
220
220
|
// Bus daemon only handles legacy/unknown launch modes.
|
|
221
|
-
const adapter = this.adapterRouter.getAdapter({ launchMode, agentId: subscriber });
|
|
221
|
+
const adapter = this.adapterRouter.getAdapter({ launchMode, agentId: subscriber, meta });
|
|
222
222
|
const { supportsNotifierInjector, supportsInternalQueueLoop } = adapter.capabilities;
|
|
223
223
|
if (launchMode && (supportsNotifierInjector || supportsInternalQueueLoop)) {
|
|
224
224
|
continue;
|
package/src/bus/inject.js
CHANGED
|
@@ -184,18 +184,11 @@ class Injector {
|
|
|
184
184
|
}
|
|
185
185
|
|
|
186
186
|
/**
|
|
187
|
-
*
|
|
187
|
+
* 使用指定路径的 PTY socket 注入命令
|
|
188
188
|
*/
|
|
189
|
-
async
|
|
190
|
-
const sockPath = this.getInjectSockPath(subscriber);
|
|
191
|
-
|
|
192
|
-
if (!fs.existsSync(sockPath)) {
|
|
193
|
-
throw new Error(`Inject socket not found: ${sockPath}`);
|
|
194
|
-
}
|
|
195
|
-
|
|
189
|
+
async injectPtyAtPath(sockPath, command) {
|
|
196
190
|
return new Promise((resolve, reject) => {
|
|
197
191
|
const client = net.createConnection(sockPath, () => {
|
|
198
|
-
// 发送inject请求
|
|
199
192
|
client.write(JSON.stringify({ type: "inject", command }) + "\n");
|
|
200
193
|
});
|
|
201
194
|
|
|
@@ -240,6 +233,19 @@ class Injector {
|
|
|
240
233
|
});
|
|
241
234
|
}
|
|
242
235
|
|
|
236
|
+
/**
|
|
237
|
+
* 使用 PTY socket 直接注入命令(无需macOS权限)
|
|
238
|
+
*/
|
|
239
|
+
async injectPty(subscriber, command) {
|
|
240
|
+
const sockPath = this.getInjectSockPath(subscriber);
|
|
241
|
+
|
|
242
|
+
if (!fs.existsSync(sockPath)) {
|
|
243
|
+
throw new Error(`Inject socket not found: ${sockPath}`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return this.injectPtyAtPath(sockPath, command);
|
|
247
|
+
}
|
|
248
|
+
|
|
243
249
|
/**
|
|
244
250
|
* 注入命令到订阅者的终端
|
|
245
251
|
*
|
|
@@ -260,10 +266,23 @@ class Injector {
|
|
|
260
266
|
const meta = this.getAgentMeta(subscriber) || {};
|
|
261
267
|
const launchMode = meta.launch_mode || "";
|
|
262
268
|
const adapterRouter = createTerminalAdapterRouter();
|
|
263
|
-
const adapter = adapterRouter.getAdapter({ launchMode, agentId: subscriber });
|
|
269
|
+
const adapter = adapterRouter.getAdapter({ launchMode, agentId: subscriber, meta });
|
|
264
270
|
const supportsSocket = adapter.capabilities.supportsSocketProtocol;
|
|
265
271
|
const supportsNotifier = adapter.capabilities.supportsNotifierInjector;
|
|
266
272
|
|
|
273
|
+
// 0. Try Terminal Host inject socket (ufoo Terminal Host Protocol)
|
|
274
|
+
const hostSock = (meta.host_inject_sock || "").toString();
|
|
275
|
+
if (hostSock && fs.existsSync(hostSock)) {
|
|
276
|
+
try {
|
|
277
|
+
logInject(`[inject] Using host inject socket: ${hostSock}`);
|
|
278
|
+
await this.injectPtyAtPath(hostSock, command);
|
|
279
|
+
logInject("[inject] Host inject success");
|
|
280
|
+
return;
|
|
281
|
+
} catch (err) {
|
|
282
|
+
logInject(`[inject] Host inject failed: ${err.message}, trying PTY socket`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
267
286
|
// 1. 优先尝试 PTY socket(无需任何macOS权限)
|
|
268
287
|
const injectSockPath = this.getInjectSockPath(subscriber);
|
|
269
288
|
if (fs.existsSync(injectSockPath)) {
|
package/src/bus/message.js
CHANGED
|
@@ -7,6 +7,7 @@ const {
|
|
|
7
7
|
appendJSONL,
|
|
8
8
|
readLastLine,
|
|
9
9
|
isPidAlive,
|
|
10
|
+
normalizeAgentTypeAlias,
|
|
10
11
|
} = require("./utils");
|
|
11
12
|
const NicknameManager = require("./nickname");
|
|
12
13
|
|
|
@@ -14,15 +15,6 @@ const SEQ_LOCK_TIMEOUT_MS = 5000;
|
|
|
14
15
|
const SEQ_LOCK_POLL_MS = 25;
|
|
15
16
|
const SEQ_LOCK_STALE_MS = 30000;
|
|
16
17
|
|
|
17
|
-
function normalizeAgentTypeAlias(value = "") {
|
|
18
|
-
const text = String(value || "").trim().toLowerCase();
|
|
19
|
-
if (!text) return "";
|
|
20
|
-
if (text === "codex") return "codex";
|
|
21
|
-
if (text === "claude" || text === "claude-code") return "claude-code";
|
|
22
|
-
if (text === "ufoo" || text === "ucode" || text === "ufoo-code") return "ufoo-code";
|
|
23
|
-
return text;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
18
|
/**
|
|
27
19
|
* 消息管理器
|
|
28
20
|
*/
|
package/src/bus/subscriber.js
CHANGED
|
@@ -197,11 +197,29 @@ class SubscriberManager {
|
|
|
197
197
|
const preservedTmuxPane = typeof existingMeta?.tmux_pane === "string" ? existingMeta.tmux_pane.trim() : "";
|
|
198
198
|
const tmuxPane = explicitTmuxPane || envTmuxPane || preservedTmuxPane;
|
|
199
199
|
|
|
200
|
+
const hostInjectSock = typeof options.hostInjectSock === "string"
|
|
201
|
+
? options.hostInjectSock.trim()
|
|
202
|
+
: "";
|
|
203
|
+
const hostDaemonSock = typeof options.hostDaemonSock === "string"
|
|
204
|
+
? options.hostDaemonSock.trim()
|
|
205
|
+
: "";
|
|
206
|
+
const hostName = typeof options.hostName === "string"
|
|
207
|
+
? options.hostName.trim()
|
|
208
|
+
: "";
|
|
209
|
+
const hostSessionId = typeof options.hostSessionId === "string"
|
|
210
|
+
? options.hostSessionId.trim()
|
|
211
|
+
: "";
|
|
212
|
+
const hostCapabilities = options.hostCapabilities && typeof options.hostCapabilities === "object"
|
|
213
|
+
? { ...options.hostCapabilities }
|
|
214
|
+
: null;
|
|
215
|
+
|
|
200
216
|
this.busData.agents[subscriber] = {
|
|
201
217
|
...preserved,
|
|
202
218
|
agent_type: agentType,
|
|
203
219
|
nickname: finalNickname,
|
|
204
220
|
status: "active",
|
|
221
|
+
activity_state: "starting",
|
|
222
|
+
activity_since: getTimestamp(),
|
|
205
223
|
joined_at: existingMeta?.joined_at || getTimestamp(),
|
|
206
224
|
last_seen: getTimestamp(),
|
|
207
225
|
pid: overridePid || getJoinedPid(),
|
|
@@ -211,6 +229,22 @@ class SubscriberManager {
|
|
|
211
229
|
launch_mode: launchMode,
|
|
212
230
|
};
|
|
213
231
|
|
|
232
|
+
if (hostInjectSock) {
|
|
233
|
+
this.busData.agents[subscriber].host_inject_sock = hostInjectSock;
|
|
234
|
+
}
|
|
235
|
+
if (hostDaemonSock) {
|
|
236
|
+
this.busData.agents[subscriber].host_daemon_sock = hostDaemonSock;
|
|
237
|
+
}
|
|
238
|
+
if (hostName) {
|
|
239
|
+
this.busData.agents[subscriber].host_name = hostName;
|
|
240
|
+
}
|
|
241
|
+
if (hostSessionId) {
|
|
242
|
+
this.busData.agents[subscriber].host_session_id = hostSessionId;
|
|
243
|
+
}
|
|
244
|
+
if (hostCapabilities) {
|
|
245
|
+
this.busData.agents[subscriber].host_capabilities = hostCapabilities;
|
|
246
|
+
}
|
|
247
|
+
|
|
214
248
|
const terminalApp = options.terminalApp || detectTerminalAppFromEnv();
|
|
215
249
|
if (terminalApp) {
|
|
216
250
|
this.busData.agents[subscriber].terminal_app = terminalApp;
|
package/src/bus/utils.js
CHANGED
|
@@ -331,7 +331,17 @@ function isMetaActive(meta) {
|
|
|
331
331
|
return false;
|
|
332
332
|
}
|
|
333
333
|
|
|
334
|
+
function normalizeAgentTypeAlias(value = "") {
|
|
335
|
+
const text = String(value || "").trim().toLowerCase();
|
|
336
|
+
if (!text) return "";
|
|
337
|
+
if (text === "codex") return "codex";
|
|
338
|
+
if (text === "claude" || text === "claude-code") return "claude-code";
|
|
339
|
+
if (text === "ufoo" || text === "ucode" || text === "ufoo-code") return "ufoo-code";
|
|
340
|
+
return text;
|
|
341
|
+
}
|
|
342
|
+
|
|
334
343
|
module.exports = {
|
|
344
|
+
normalizeAgentTypeAlias,
|
|
335
345
|
getTimestamp,
|
|
336
346
|
getDate,
|
|
337
347
|
generateInstanceId,
|
package/src/chat/agentBar.js
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
const { stripAnsi, truncateAnsi } = require("./text");
|
|
2
2
|
|
|
3
|
+
const ACTIVITY_INDICATORS = {
|
|
4
|
+
working: "*",
|
|
5
|
+
waiting_input: "?",
|
|
6
|
+
blocked: "!",
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const ACTIVITY_COLORS = {
|
|
10
|
+
waiting_input: "\x1b[33m", // yellow
|
|
11
|
+
blocked: "\x1b[31m", // red
|
|
12
|
+
};
|
|
13
|
+
|
|
3
14
|
function computeAgentBar(options = {}) {
|
|
4
15
|
const {
|
|
5
16
|
cols = 80,
|
|
@@ -11,6 +22,7 @@ function computeAgentBar(options = {}) {
|
|
|
11
22
|
agentListWindowStart = 0,
|
|
12
23
|
maxAgentWindow = 4,
|
|
13
24
|
getAgentLabel = (id) => id,
|
|
25
|
+
agentStates = {},
|
|
14
26
|
} = options;
|
|
15
27
|
|
|
16
28
|
const hintAnsi = `\x1b[90m│ ${hintText}\x1b[0m`;
|
|
@@ -60,12 +72,18 @@ function computeAgentBar(options = {}) {
|
|
|
60
72
|
agentParts = visible.map((agent, i) => {
|
|
61
73
|
const rawLabel = getAgentLabel(agent);
|
|
62
74
|
const label = maxLabelLen ? truncateLabel(rawLabel, maxLabelLen) : rawLabel;
|
|
75
|
+
const actState = agentStates[agent] || "";
|
|
76
|
+
const indicator = ACTIVITY_INDICATORS[actState] || "";
|
|
77
|
+
const indicatorColor = ACTIVITY_COLORS[actState] || "";
|
|
78
|
+
const prefix = indicator
|
|
79
|
+
? `${indicatorColor}${indicator}\x1b[0m`
|
|
80
|
+
: "";
|
|
63
81
|
const idx = s + i + 1; // +1 for ucode at index 0
|
|
64
82
|
if (focusMode === "dashboard" && idx === selectedAgentIndex) {
|
|
65
|
-
return
|
|
83
|
+
return `${prefix}\x1b[90;7m${label}\x1b[0m`;
|
|
66
84
|
}
|
|
67
|
-
if (agent === viewingAgent) return
|
|
68
|
-
return
|
|
85
|
+
if (agent === viewingAgent) return `${prefix}\x1b[1;36m${label}\x1b[0m`;
|
|
86
|
+
return `${prefix}\x1b[36m${label}\x1b[0m`;
|
|
69
87
|
});
|
|
70
88
|
}
|
|
71
89
|
const agentsText = activeAgents.length > 0
|
|
@@ -16,6 +16,7 @@ function createAgentViewController(options = {}) {
|
|
|
16
16
|
getAgentListWindowStart = () => 0,
|
|
17
17
|
setAgentListWindowStart = () => {},
|
|
18
18
|
getAgentLabel = (id) => id,
|
|
19
|
+
getAgentStates = () => ({}),
|
|
19
20
|
setDashboardView = () => {},
|
|
20
21
|
setScreenGrabKeys = (value) => {
|
|
21
22
|
if (screen) screen.grabKeys = Boolean(value);
|
|
@@ -79,6 +80,7 @@ function createAgentViewController(options = {}) {
|
|
|
79
80
|
agentListWindowStart: getAgentListWindowStart(),
|
|
80
81
|
maxAgentWindow,
|
|
81
82
|
getAgentLabel,
|
|
83
|
+
agentStates: getAgentStates(),
|
|
82
84
|
});
|
|
83
85
|
setAgentListWindowStart(computed.windowStart);
|
|
84
86
|
processStdout.write(`\x1b7\x1b[${rows};1H${computed.bar}\x1b8`);
|
|
@@ -29,6 +29,19 @@ function defaultResolveTerminalApp() {
|
|
|
29
29
|
return "";
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
function collectHostLaunchRequestContext(env = process.env) {
|
|
33
|
+
const hostInjectSock = String(env.UFOO_HOST_INJECT_SOCK || env.HORIZON_INJECT_SOCK || "").trim();
|
|
34
|
+
const hostDaemonSock = String(env.UFOO_HOST_DAEMON_SOCK || "").trim();
|
|
35
|
+
const hostName = String(env.UFOO_HOST_NAME || "").trim();
|
|
36
|
+
const hostSessionId = String(env.UFOO_HOST_SESSION_ID || env.HORIZON_SESSION_ID || "").trim();
|
|
37
|
+
const context = {};
|
|
38
|
+
if (hostInjectSock) context.host_inject_sock = hostInjectSock;
|
|
39
|
+
if (hostDaemonSock) context.host_daemon_sock = hostDaemonSock;
|
|
40
|
+
if (hostName) context.host_name = hostName;
|
|
41
|
+
if (hostSessionId) context.host_session_id = hostSessionId;
|
|
42
|
+
return context;
|
|
43
|
+
}
|
|
44
|
+
|
|
32
45
|
async function withCapturedConsole(capture, fn) {
|
|
33
46
|
const originalLog = console.log;
|
|
34
47
|
const originalError = console.error;
|
|
@@ -445,6 +458,7 @@ function createCommandExecutor(options = {}) {
|
|
|
445
458
|
count: Number.isFinite(count) ? count : 1,
|
|
446
459
|
nickname,
|
|
447
460
|
launch_scope: launchScope,
|
|
461
|
+
...collectHostLaunchRequestContext(),
|
|
448
462
|
};
|
|
449
463
|
const terminalApp = String(resolveTerminalApp() || "").trim().toLowerCase();
|
|
450
464
|
if (terminalApp === "terminal" || terminalApp === "iterm2") {
|
|
@@ -1126,4 +1140,5 @@ function createCommandExecutor(options = {}) {
|
|
|
1126
1140
|
|
|
1127
1141
|
module.exports = {
|
|
1128
1142
|
createCommandExecutor,
|
|
1143
|
+
collectHostLaunchRequestContext,
|
|
1129
1144
|
};
|
|
@@ -7,6 +7,7 @@ function createDaemonConnection(options = {}) {
|
|
|
7
7
|
queueStatusLine,
|
|
8
8
|
resolveStatusLine,
|
|
9
9
|
logMessage,
|
|
10
|
+
switchConnectionTimeoutMs = 18000,
|
|
10
11
|
} = options;
|
|
11
12
|
|
|
12
13
|
let connectClient = connectClientOption;
|
|
@@ -16,6 +17,35 @@ function createDaemonConnection(options = {}) {
|
|
|
16
17
|
let connectionLostNotified = false;
|
|
17
18
|
const pendingRequests = [];
|
|
18
19
|
const MAX_PENDING_REQUESTS = 50;
|
|
20
|
+
const STATUS_KEY_RECONNECT = "daemon-reconnect";
|
|
21
|
+
const STATUS_KEY_SWITCH = "daemon-switch";
|
|
22
|
+
const DEFAULT_SWITCH_TIMEOUT_MS = Number.isFinite(switchConnectionTimeoutMs)
|
|
23
|
+
&& switchConnectionTimeoutMs > 0
|
|
24
|
+
? Math.trunc(switchConnectionTimeoutMs)
|
|
25
|
+
: 18000;
|
|
26
|
+
|
|
27
|
+
function withTimeout(promiseLike, timeoutMs, timeoutMessage) {
|
|
28
|
+
const ms = Number.isFinite(timeoutMs) && timeoutMs > 0
|
|
29
|
+
? Math.trunc(timeoutMs)
|
|
30
|
+
: DEFAULT_SWITCH_TIMEOUT_MS;
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
const timer = setTimeout(() => {
|
|
33
|
+
const err = new Error(timeoutMessage || `operation timed out after ${ms}ms`);
|
|
34
|
+
err.code = "UFOO_TIMEOUT";
|
|
35
|
+
reject(err);
|
|
36
|
+
}, ms);
|
|
37
|
+
if (typeof timer.unref === "function") {
|
|
38
|
+
timer.unref();
|
|
39
|
+
}
|
|
40
|
+
Promise.resolve(promiseLike).then((value) => {
|
|
41
|
+
clearTimeout(timer);
|
|
42
|
+
resolve(value);
|
|
43
|
+
}, (err) => {
|
|
44
|
+
clearTimeout(timer);
|
|
45
|
+
reject(err);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
}
|
|
19
49
|
|
|
20
50
|
function enqueueRequest(req) {
|
|
21
51
|
if (!req || req.type === IPC_REQUEST_TYPES.STATUS) return;
|
|
@@ -91,18 +121,18 @@ function createDaemonConnection(options = {}) {
|
|
|
91
121
|
if (client && !client.destroyed) return true;
|
|
92
122
|
if (exitRequested) return false;
|
|
93
123
|
if (reconnectPromise) return reconnectPromise;
|
|
94
|
-
queueStatusLine("Reconnecting to daemon");
|
|
124
|
+
queueStatusLine("Reconnecting to daemon", { key: STATUS_KEY_RECONNECT });
|
|
95
125
|
logMessage("status", "{white-fg}⚙{/white-fg} Reconnecting to daemon...");
|
|
96
126
|
reconnectPromise = (async () => {
|
|
97
127
|
const newClient = await connectClient();
|
|
98
128
|
if (!newClient) {
|
|
99
|
-
resolveStatusLine("{gray-fg}✗{/gray-fg} Daemon offline");
|
|
129
|
+
resolveStatusLine("{gray-fg}✗{/gray-fg} Daemon offline", { key: STATUS_KEY_RECONNECT });
|
|
100
130
|
logMessage("error", "{white-fg}✗{/white-fg} Failed to reconnect to daemon");
|
|
101
131
|
return false;
|
|
102
132
|
}
|
|
103
133
|
attachClient(newClient);
|
|
104
134
|
connectionLostNotified = false;
|
|
105
|
-
resolveStatusLine("{gray-fg}✓{/gray-fg} Daemon reconnected");
|
|
135
|
+
resolveStatusLine("{gray-fg}✓{/gray-fg} Daemon reconnected", { key: STATUS_KEY_RECONNECT });
|
|
106
136
|
requestStatus();
|
|
107
137
|
return true;
|
|
108
138
|
})();
|
|
@@ -130,9 +160,17 @@ function createDaemonConnection(options = {}) {
|
|
|
130
160
|
}
|
|
131
161
|
const previousClient = client;
|
|
132
162
|
try {
|
|
133
|
-
queueStatusLine("Switching daemon connection");
|
|
134
|
-
const
|
|
163
|
+
queueStatusLine("Switching daemon connection", { key: STATUS_KEY_SWITCH });
|
|
164
|
+
const timeoutMs = Number.isFinite(next.timeoutMs) && next.timeoutMs > 0
|
|
165
|
+
? Math.trunc(next.timeoutMs)
|
|
166
|
+
: DEFAULT_SWITCH_TIMEOUT_MS;
|
|
167
|
+
const nextClient = await withTimeout(
|
|
168
|
+
nextConnectClient(),
|
|
169
|
+
timeoutMs,
|
|
170
|
+
`Switch connection timed out after ${timeoutMs}ms`
|
|
171
|
+
);
|
|
135
172
|
if (!nextClient) {
|
|
173
|
+
resolveStatusLine("{gray-fg}✗{/gray-fg} Switch failed", { key: STATUS_KEY_SWITCH });
|
|
136
174
|
return { ok: false, error: "Failed to connect target daemon" };
|
|
137
175
|
}
|
|
138
176
|
connectClient = nextConnectClient;
|
|
@@ -140,7 +178,7 @@ function createDaemonConnection(options = {}) {
|
|
|
140
178
|
if (next.callRequestStatus !== false) {
|
|
141
179
|
requestStatus();
|
|
142
180
|
}
|
|
143
|
-
resolveStatusLine("{gray-fg}✓{/gray-fg} Daemon switched");
|
|
181
|
+
resolveStatusLine("{gray-fg}✓{/gray-fg} Daemon switched", { key: STATUS_KEY_SWITCH });
|
|
144
182
|
return { ok: true };
|
|
145
183
|
} catch (err) {
|
|
146
184
|
// Keep existing connection alive on switch failures.
|
|
@@ -148,7 +186,7 @@ function createDaemonConnection(options = {}) {
|
|
|
148
186
|
client = previousClient;
|
|
149
187
|
}
|
|
150
188
|
const message = err && err.message ? err.message : String(err || "switch failed");
|
|
151
|
-
resolveStatusLine("{gray-fg}✗{/gray-fg} Switch failed");
|
|
189
|
+
resolveStatusLine("{gray-fg}✗{/gray-fg} Switch failed", { key: STATUS_KEY_SWITCH });
|
|
152
190
|
logMessage("error", `{white-fg}✗{/white-fg} ${message}`);
|
|
153
191
|
return { ok: false, error: message };
|
|
154
192
|
}
|