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/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/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
|
@@ -202,6 +202,8 @@ class SubscriberManager {
|
|
|
202
202
|
agent_type: agentType,
|
|
203
203
|
nickname: finalNickname,
|
|
204
204
|
status: "active",
|
|
205
|
+
activity_state: "starting",
|
|
206
|
+
activity_since: getTimestamp(),
|
|
205
207
|
joined_at: existingMeta?.joined_at || getTimestamp(),
|
|
206
208
|
last_seen: getTimestamp(),
|
|
207
209
|
pid: overridePid || getJoinedPid(),
|
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`);
|
|
@@ -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
|
}
|
|
@@ -25,8 +25,17 @@ function createDaemonMessageRouter(options = {}) {
|
|
|
25
25
|
appendStreamDelta = () => {},
|
|
26
26
|
finalizeStream = () => {},
|
|
27
27
|
hasStream = () => false,
|
|
28
|
+
setTransientAgentState = () => {},
|
|
29
|
+
clearTransientAgentState = () => {},
|
|
30
|
+
refreshDashboard = () => {},
|
|
28
31
|
} = options;
|
|
29
32
|
|
|
33
|
+
function isLikelySubscriberId(value) {
|
|
34
|
+
const text = String(value || "");
|
|
35
|
+
if (!text) return false;
|
|
36
|
+
return text.includes(":") && !text.includes(" ");
|
|
37
|
+
}
|
|
38
|
+
|
|
30
39
|
function normalizeDisplayMessage(raw) {
|
|
31
40
|
let displayMessage = raw || "";
|
|
32
41
|
let streamPayload = null;
|
|
@@ -53,6 +62,14 @@ function createDaemonMessageRouter(options = {}) {
|
|
|
53
62
|
if (typeof data.phase === "string") {
|
|
54
63
|
const text = data.text || "";
|
|
55
64
|
const item = { key: data.key, text };
|
|
65
|
+
const key = typeof data.key === "string" ? data.key : "";
|
|
66
|
+
if (isLikelySubscriberId(key)) {
|
|
67
|
+
if (data.phase === BUS_STATUS_PHASES.START) {
|
|
68
|
+
setTransientAgentState(key, "working");
|
|
69
|
+
} else if (data.phase === BUS_STATUS_PHASES.DONE || data.phase === BUS_STATUS_PHASES.ERROR) {
|
|
70
|
+
clearTransientAgentState(key);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
56
73
|
if (data.phase === BUS_STATUS_PHASES.START) {
|
|
57
74
|
enqueueBusStatus(item);
|
|
58
75
|
} else if (data.phase === BUS_STATUS_PHASES.DONE || data.phase === BUS_STATUS_PHASES.ERROR) {
|
|
@@ -66,6 +83,7 @@ function createDaemonMessageRouter(options = {}) {
|
|
|
66
83
|
} else {
|
|
67
84
|
enqueueBusStatus(item);
|
|
68
85
|
}
|
|
86
|
+
refreshDashboard();
|
|
69
87
|
renderScreen();
|
|
70
88
|
return false;
|
|
71
89
|
}
|
|
@@ -301,6 +319,10 @@ function createDaemonMessageRouter(options = {}) {
|
|
|
301
319
|
|
|
302
320
|
function handleBusMessage(msg) {
|
|
303
321
|
const data = msg.data || {};
|
|
322
|
+
if (data.event === "activity_state_changed") {
|
|
323
|
+
requestStatus();
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
304
326
|
const prefix = data.event === "broadcast" ? "{gray-fg}⇢{/gray-fg}" : "{gray-fg}↔{/gray-fg}";
|
|
305
327
|
const publisher = data.publisher && data.publisher !== "unknown"
|
|
306
328
|
? data.publisher
|
|
@@ -11,6 +11,7 @@ function createDaemonTransport(options = {}) {
|
|
|
11
11
|
secondaryRetries = DAEMON_TRANSPORT_DEFAULTS.secondaryRetries,
|
|
12
12
|
retryDelayMs = DAEMON_TRANSPORT_DEFAULTS.retryDelayMs,
|
|
13
13
|
restartDelayMs = DAEMON_TRANSPORT_DEFAULTS.restartDelayMs,
|
|
14
|
+
connectTimeoutMs = DAEMON_TRANSPORT_DEFAULTS.connectTimeoutMs,
|
|
14
15
|
} = options;
|
|
15
16
|
|
|
16
17
|
let activeProjectRoot = projectRoot;
|
|
@@ -25,14 +26,24 @@ function createDaemonTransport(options = {}) {
|
|
|
25
26
|
|
|
26
27
|
async function connectClientForTarget(override = {}) {
|
|
27
28
|
const target = resolveTarget(override);
|
|
28
|
-
let client = await connectWithRetry(
|
|
29
|
+
let client = await connectWithRetry(
|
|
30
|
+
target.sockPath,
|
|
31
|
+
primaryRetries,
|
|
32
|
+
retryDelayMs,
|
|
33
|
+
{ timeoutMs: connectTimeoutMs }
|
|
34
|
+
);
|
|
29
35
|
if (!client) {
|
|
30
36
|
// Retry once with a fresh daemon start and longer wait.
|
|
31
37
|
if (!isRunning(target.projectRoot)) {
|
|
32
38
|
startDaemon(target.projectRoot);
|
|
33
39
|
await new Promise((resolve) => setTimeout(resolve, restartDelayMs));
|
|
34
40
|
}
|
|
35
|
-
client = await connectWithRetry(
|
|
41
|
+
client = await connectWithRetry(
|
|
42
|
+
target.sockPath,
|
|
43
|
+
secondaryRetries,
|
|
44
|
+
retryDelayMs,
|
|
45
|
+
{ timeoutMs: connectTimeoutMs }
|
|
46
|
+
);
|
|
36
47
|
}
|
|
37
48
|
return client;
|
|
38
49
|
}
|
|
@@ -23,6 +23,7 @@ function createDashboardKeyController(options = {}) {
|
|
|
23
23
|
clampAgentWindow = () => {},
|
|
24
24
|
clampAgentWindowWithSelection = () => {},
|
|
25
25
|
requestProjectSwitch = () => {},
|
|
26
|
+
requestCloseProject = () => {},
|
|
26
27
|
renderDashboard = () => {},
|
|
27
28
|
renderAgentDashboard = () => {},
|
|
28
29
|
renderScreen = () => {},
|
|
@@ -385,6 +386,14 @@ function createDashboardKeyController(options = {}) {
|
|
|
385
386
|
return true;
|
|
386
387
|
}
|
|
387
388
|
|
|
389
|
+
if (key.name === "x" && key.ctrl) {
|
|
390
|
+
const current = Number.isFinite(state.selectedProjectIndex) ? state.selectedProjectIndex : 0;
|
|
391
|
+
if (current >= 0 && current < projects.length) {
|
|
392
|
+
requestCloseProject(current);
|
|
393
|
+
}
|
|
394
|
+
return true;
|
|
395
|
+
}
|
|
396
|
+
|
|
388
397
|
if (key.name === "down") {
|
|
389
398
|
state.dashboardView = "agents";
|
|
390
399
|
if (!Array.isArray(state.activeAgents) || state.activeAgents.length === 0) {
|