u-foo 2.3.0 → 2.3.2
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/package.json +1 -1
- package/src/agent/activityStatePublisher.js +6 -2
- package/src/agent/notifier.js +12 -3
- package/src/bus/subscriber.js +6 -4
- package/src/chat/commandExecutor.js +17 -12
- package/src/chat/daemonMessageRouter.js +14 -10
- package/src/chat/index.js +2 -0
- package/src/chat/inputSubmitHandler.js +9 -8
- package/src/chat/projectCloseController.js +7 -3
- package/src/chat/settingsController.js +4 -2
package/package.json
CHANGED
|
@@ -12,6 +12,7 @@ const { writeActivityState } = require("./activityStateWriter");
|
|
|
12
12
|
* @param {string} options.subscriber - Subscriber ID (e.g. "claude-code:abc123")
|
|
13
13
|
* @param {string} options.projectRoot - Project root (unused, kept for API compat)
|
|
14
14
|
* @param {boolean} [options.force=true] - Force overwrite priority-protected states
|
|
15
|
+
* publish(state, extra, { force }) can override this default for one transition.
|
|
15
16
|
*/
|
|
16
17
|
function createActivityStatePublisher(options = {}) {
|
|
17
18
|
const {
|
|
@@ -22,10 +23,13 @@ function createActivityStatePublisher(options = {}) {
|
|
|
22
23
|
|
|
23
24
|
let lastState = "";
|
|
24
25
|
|
|
25
|
-
function publish(state, extra = {}) {
|
|
26
|
+
function publish(state, extra = {}, publishOptions = {}) {
|
|
26
27
|
if (state === lastState) return false;
|
|
27
28
|
const since = extra.since || undefined;
|
|
28
|
-
const
|
|
29
|
+
const effectiveForce = typeof publishOptions.force === "boolean"
|
|
30
|
+
? publishOptions.force
|
|
31
|
+
: force;
|
|
32
|
+
const changed = writeActivityState(agentsFile, subscriber, state, { since, force: effectiveForce });
|
|
29
33
|
if (!changed) return false;
|
|
30
34
|
lastState = state;
|
|
31
35
|
// Write to bus events directory for daemon bridge to pick up.
|
package/src/agent/notifier.js
CHANGED
|
@@ -123,8 +123,10 @@ class AgentNotifier {
|
|
|
123
123
|
* 更新 activity_state(terminal/tmux agent 基础支持)
|
|
124
124
|
* 基于消息投递推断 WORKING,无 pending 时推断 IDLE
|
|
125
125
|
*/
|
|
126
|
-
updateActivityState(state) {
|
|
127
|
-
return this.activityPublisher.publish(state
|
|
126
|
+
updateActivityState(state, options = {}) {
|
|
127
|
+
return this.activityPublisher.publish(state, {}, {
|
|
128
|
+
force: typeof options.force === "boolean" ? options.force : undefined,
|
|
129
|
+
});
|
|
128
130
|
}
|
|
129
131
|
|
|
130
132
|
getCurrentActivityState() {
|
|
@@ -375,7 +377,14 @@ class AgentNotifier {
|
|
|
375
377
|
|
|
376
378
|
this.lastCount = this.getMessageCount();
|
|
377
379
|
if (this._launcherReady && (!this.lastWorkingAt || nowMs - this.lastWorkingAt >= this.workingHoldMs)) {
|
|
378
|
-
this.
|
|
380
|
+
const currentActivityState = this.getCurrentActivityState();
|
|
381
|
+
if (currentActivityState !== "waiting_input" && currentActivityState !== "blocked") {
|
|
382
|
+
if (currentActivityState === "working") {
|
|
383
|
+
this.updateActivityState("idle", { force: true });
|
|
384
|
+
} else {
|
|
385
|
+
this.updateActivityState("idle");
|
|
386
|
+
}
|
|
387
|
+
}
|
|
379
388
|
}
|
|
380
389
|
this.refreshTitle();
|
|
381
390
|
this.updateHeartbeat();
|
package/src/bus/subscriber.js
CHANGED
|
@@ -122,10 +122,11 @@ class SubscriberManager {
|
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
async cleanupDuplicateTty(currentSubscriber, ttyPath) {
|
|
125
|
+
async cleanupDuplicateTty(currentSubscriber, ttyPath, options = {}) {
|
|
126
126
|
if (!ttyPath) return null;
|
|
127
127
|
if (!this.busData.agents) return null;
|
|
128
128
|
|
|
129
|
+
const currentAgentType = String(options.agentType || "").trim();
|
|
129
130
|
let inheritedNickname = null;
|
|
130
131
|
const entries = Object.entries(this.busData.agents);
|
|
131
132
|
for (const [id, meta] of entries) {
|
|
@@ -136,8 +137,9 @@ class SubscriberManager {
|
|
|
136
137
|
: (await this.queueManager.readTty(id));
|
|
137
138
|
if (!metaTty) continue;
|
|
138
139
|
if (metaTty === ttyPath) {
|
|
139
|
-
|
|
140
|
-
|
|
140
|
+
const sameAgentType = !currentAgentType || meta?.agent_type === currentAgentType;
|
|
141
|
+
// Inherit user-set nickname only when replacing the same agent type.
|
|
142
|
+
if (sameAgentType && meta.nickname && !inheritedNickname) {
|
|
141
143
|
inheritedNickname = meta.nickname;
|
|
142
144
|
}
|
|
143
145
|
// Remove stale subscriber using same tty
|
|
@@ -227,7 +229,7 @@ class SubscriberManager {
|
|
|
227
229
|
// 清理同一 tty 的旧订阅者(避免重复启动污染)
|
|
228
230
|
// Inherit nickname from displaced entry when this is a new subscriber
|
|
229
231
|
// with no explicit nickname (e.g. session restart on same TTY)
|
|
230
|
-
const inheritedNickname = await this.cleanupDuplicateTty(subscriber, finalTty);
|
|
232
|
+
const inheritedNickname = await this.cleanupDuplicateTty(subscriber, finalTty, { agentType });
|
|
231
233
|
if (inheritedNickname && !nickname && !existingMeta) {
|
|
232
234
|
finalNickname = inheritedNickname;
|
|
233
235
|
if (!finalScopedNickname) finalScopedNickname = inheritedNickname;
|
|
@@ -117,6 +117,7 @@ function createCommandExecutor(options = {}) {
|
|
|
117
117
|
parseCommand = () => null,
|
|
118
118
|
escapeBlessed = (value) => String(value || ""),
|
|
119
119
|
logMessage = () => {},
|
|
120
|
+
resolveStatusLine = null,
|
|
120
121
|
renderScreen = () => {},
|
|
121
122
|
getActiveAgents = () => [],
|
|
122
123
|
getActiveAgentMetaMap = () => new Map(),
|
|
@@ -157,6 +158,10 @@ function createCommandExecutor(options = {}) {
|
|
|
157
158
|
throw new Error("createCommandExecutor requires projectRoot");
|
|
158
159
|
}
|
|
159
160
|
|
|
161
|
+
const statusMsg = typeof resolveStatusLine === "function"
|
|
162
|
+
? resolveStatusLine
|
|
163
|
+
: (text) => logMessage("status", text);
|
|
164
|
+
|
|
160
165
|
async function handleDoctorCommand() {
|
|
161
166
|
logMessage("system", "{white-fg}⚙{/white-fg} Running health check...");
|
|
162
167
|
|
|
@@ -221,51 +226,51 @@ function createCommandExecutor(options = {}) {
|
|
|
221
226
|
|
|
222
227
|
if (subcommand === "start") {
|
|
223
228
|
if (isDaemonRunning(targetRoot)) {
|
|
224
|
-
|
|
229
|
+
statusMsg("{gray-fg}⚠{/gray-fg} Daemon already running");
|
|
225
230
|
} else {
|
|
226
|
-
|
|
231
|
+
statusMsg("{gray-fg}⚙{/gray-fg} Starting daemon...");
|
|
227
232
|
startDaemon(targetRoot);
|
|
228
233
|
await sleep(1000);
|
|
229
234
|
if (isDaemonRunning(targetRoot)) {
|
|
230
|
-
|
|
235
|
+
statusMsg("{gray-fg}✓{/gray-fg} Daemon started");
|
|
231
236
|
} else {
|
|
232
|
-
|
|
237
|
+
statusMsg("{gray-fg}✗{/gray-fg} Failed to start daemon");
|
|
233
238
|
}
|
|
234
239
|
}
|
|
235
240
|
return;
|
|
236
241
|
}
|
|
237
242
|
|
|
238
243
|
if (subcommand === "stop") {
|
|
239
|
-
|
|
244
|
+
statusMsg("{gray-fg}⚙{/gray-fg} Stopping daemon...");
|
|
240
245
|
stopDaemon(targetRoot);
|
|
241
246
|
await sleep(1000);
|
|
242
247
|
if (!isDaemonRunning(targetRoot)) {
|
|
243
|
-
|
|
248
|
+
statusMsg("{gray-fg}✓{/gray-fg} Daemon stopped");
|
|
244
249
|
} else {
|
|
245
|
-
|
|
250
|
+
statusMsg("{gray-fg}✗{/gray-fg} Failed to stop daemon");
|
|
246
251
|
}
|
|
247
252
|
return;
|
|
248
253
|
}
|
|
249
254
|
|
|
250
255
|
if (subcommand === "restart") {
|
|
251
|
-
|
|
256
|
+
statusMsg("{gray-fg}⚙{/gray-fg} Restarting daemon...");
|
|
252
257
|
stopDaemon(targetRoot);
|
|
253
258
|
await sleep(500);
|
|
254
259
|
startDaemon(targetRoot);
|
|
255
260
|
await sleep(1000);
|
|
256
261
|
if (isDaemonRunning(targetRoot)) {
|
|
257
|
-
|
|
262
|
+
statusMsg("{gray-fg}✓{/gray-fg} Daemon restarted");
|
|
258
263
|
} else {
|
|
259
|
-
|
|
264
|
+
statusMsg("{gray-fg}✗{/gray-fg} Failed to restart daemon");
|
|
260
265
|
}
|
|
261
266
|
return;
|
|
262
267
|
}
|
|
263
268
|
|
|
264
269
|
if (subcommand === "status") {
|
|
265
270
|
if (isDaemonRunning(targetRoot)) {
|
|
266
|
-
|
|
271
|
+
statusMsg("{gray-fg}✓{/gray-fg} Daemon is running");
|
|
267
272
|
} else {
|
|
268
|
-
|
|
273
|
+
statusMsg("{gray-fg}✗{/gray-fg} Daemon is not running");
|
|
269
274
|
}
|
|
270
275
|
return;
|
|
271
276
|
}
|
|
@@ -37,6 +37,14 @@ function createDaemonMessageRouter(options = {}) {
|
|
|
37
37
|
return text.includes(":") && !text.includes(" ");
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
function speakerPrefix(label, color = "cyan") {
|
|
41
|
+
const escapedLabel = escapeBlessed(label);
|
|
42
|
+
if (color === "white") {
|
|
43
|
+
return `{white-fg}${escapedLabel}{/white-fg} {gray-fg}·{/gray-fg} `;
|
|
44
|
+
}
|
|
45
|
+
return `{cyan-fg}${escapedLabel}{/cyan-fg} {gray-fg}·{/gray-fg} `;
|
|
46
|
+
}
|
|
47
|
+
|
|
40
48
|
function normalizeDisplayMessage(raw) {
|
|
41
49
|
let displayMessage = raw || "";
|
|
42
50
|
let streamPayload = null;
|
|
@@ -77,9 +85,9 @@ function createDaemonMessageRouter(options = {}) {
|
|
|
77
85
|
resolveBusStatus(item);
|
|
78
86
|
if (text) {
|
|
79
87
|
const prefix = data.phase === BUS_STATUS_PHASES.ERROR
|
|
80
|
-
? "{
|
|
81
|
-
: "{
|
|
82
|
-
|
|
88
|
+
? "{gray-fg}✗{/gray-fg}"
|
|
89
|
+
: "{gray-fg}✓{/gray-fg}";
|
|
90
|
+
resolveStatusLine(`${prefix} ${escapeBlessed(text)}`, data);
|
|
83
91
|
}
|
|
84
92
|
} else {
|
|
85
93
|
enqueueBusStatus(item);
|
|
@@ -214,7 +222,7 @@ function createDaemonMessageRouter(options = {}) {
|
|
|
214
222
|
);
|
|
215
223
|
// Suppress lifecycle confirmations from chat history — status line plus structured payload is enough.
|
|
216
224
|
if (!isLifecycleStatusOnly && !isGroupStartedConfirmation) {
|
|
217
|
-
logMessage("reply",
|
|
225
|
+
logMessage("reply", `${speakerPrefix("ufoo", "white")}${escapeBlessed(replyText)}`);
|
|
218
226
|
}
|
|
219
227
|
}
|
|
220
228
|
|
|
@@ -352,15 +360,11 @@ function createDaemonMessageRouter(options = {}) {
|
|
|
352
360
|
const publisher = report.agent_id || data.publisher || "ufoo-agent";
|
|
353
361
|
const displayName = resolveAgentDisplayName(publisher);
|
|
354
362
|
const detail = report.summary || report.message || data.message || report.task_id || "report";
|
|
355
|
-
logMessage(
|
|
356
|
-
"system",
|
|
357
|
-
`{gray-fg}↥{/gray-fg} {cyan-fg}${escapeBlessed(displayName)}{/cyan-fg} {gray-fg}→ ufoo-agent{/gray-fg} ${escapeBlessed(detail)}`
|
|
358
|
-
);
|
|
363
|
+
logMessage("bus", `${speakerPrefix(displayName)}${escapeBlessed(detail)}`);
|
|
359
364
|
requestStatus();
|
|
360
365
|
renderScreen();
|
|
361
366
|
return true;
|
|
362
367
|
}
|
|
363
|
-
const prefix = data.event === "broadcast" ? "{gray-fg}⇢{/gray-fg}" : "{gray-fg}↔{/gray-fg}";
|
|
364
368
|
const publisher = data.publisher && data.publisher !== "unknown"
|
|
365
369
|
? data.publisher
|
|
366
370
|
: (data.event === "broadcast" ? "broadcast" : "bus");
|
|
@@ -399,7 +403,7 @@ function createDaemonMessageRouter(options = {}) {
|
|
|
399
403
|
}
|
|
400
404
|
|
|
401
405
|
const pendingBeforeMessage = getPendingState(publisher, displayName);
|
|
402
|
-
const prefixLabel =
|
|
406
|
+
const prefixLabel = speakerPrefix(displayName);
|
|
403
407
|
const continuationPrefix = " ".repeat(stripBlessedTags(prefixLabel).length);
|
|
404
408
|
|
|
405
409
|
if (streamPayload) {
|
package/src/chat/index.js
CHANGED
|
@@ -1905,6 +1905,7 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1905
1905
|
renderDashboard,
|
|
1906
1906
|
renderScreen: () => screen.render(),
|
|
1907
1907
|
logMessage,
|
|
1908
|
+
resolveStatusLine,
|
|
1908
1909
|
escapeBlessed,
|
|
1909
1910
|
});
|
|
1910
1911
|
|
|
@@ -1919,6 +1920,7 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1919
1920
|
parseCommand,
|
|
1920
1921
|
escapeBlessed,
|
|
1921
1922
|
logMessage,
|
|
1923
|
+
resolveStatusLine,
|
|
1922
1924
|
renderScreen: () => screen.render(),
|
|
1923
1925
|
getActiveAgents: () => activeAgents,
|
|
1924
1926
|
getActiveAgentMetaMap: () => activeAgentMetaMap,
|
|
@@ -30,6 +30,10 @@ function createInputSubmitHandler(options = {}) {
|
|
|
30
30
|
throw new Error("createInputSubmitHandler requires a mutable state object");
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
function userPrefix() {
|
|
34
|
+
return "{white-fg}you{/white-fg} {gray-fg}·{/gray-fg}";
|
|
35
|
+
}
|
|
36
|
+
|
|
33
37
|
async function tryActivateTargetAgent(agentId) {
|
|
34
38
|
const adapter = getAgentAdapter(agentId);
|
|
35
39
|
const capabilities = adapter && adapter.capabilities ? adapter.capabilities : null;
|
|
@@ -88,7 +92,7 @@ function createInputSubmitHandler(options = {}) {
|
|
|
88
92
|
const label = getAgentLabel(state.targetAgent);
|
|
89
93
|
logMessage(
|
|
90
94
|
"user",
|
|
91
|
-
|
|
95
|
+
`${userPrefix()} {magenta-fg}@${escapeBlessed(label)}{/magenta-fg} ${escapeBlessed(text)}`
|
|
92
96
|
);
|
|
93
97
|
renderScreen(); // Immediately render the user message
|
|
94
98
|
markPendingDelivery(state.targetAgent);
|
|
@@ -114,10 +118,7 @@ function createInputSubmitHandler(options = {}) {
|
|
|
114
118
|
return;
|
|
115
119
|
}
|
|
116
120
|
setTargetAgent(resolvedTarget);
|
|
117
|
-
|
|
118
|
-
"status",
|
|
119
|
-
`{white-fg}⚙{/white-fg} Target selected: @${escapeBlessed(atTarget.target)}`
|
|
120
|
-
);
|
|
121
|
+
queueStatusLine(`Target selected: @${escapeBlessed(atTarget.target)}`);
|
|
121
122
|
focusInput();
|
|
122
123
|
return;
|
|
123
124
|
}
|
|
@@ -125,7 +126,7 @@ function createInputSubmitHandler(options = {}) {
|
|
|
125
126
|
const message = atTarget.message.trim();
|
|
126
127
|
logMessage(
|
|
127
128
|
"user",
|
|
128
|
-
|
|
129
|
+
`${userPrefix()} {magenta-fg}@${escapeBlessed(atTarget.target)}{/magenta-fg} ${escapeBlessed(message)}`
|
|
129
130
|
);
|
|
130
131
|
renderScreen(); // Immediately render the user message
|
|
131
132
|
markPendingDelivery(resolvedTarget);
|
|
@@ -142,7 +143,7 @@ function createInputSubmitHandler(options = {}) {
|
|
|
142
143
|
|
|
143
144
|
if (text.startsWith("/")) {
|
|
144
145
|
if (shouldEchoCommandInChat(text)) {
|
|
145
|
-
logMessage("user",
|
|
146
|
+
logMessage("user", `${userPrefix()} ${escapeBlessed(text)}`);
|
|
146
147
|
renderScreen(); // Render slash command immediately
|
|
147
148
|
}
|
|
148
149
|
try {
|
|
@@ -188,7 +189,7 @@ function createInputSubmitHandler(options = {}) {
|
|
|
188
189
|
allow_relevance_queue: true,
|
|
189
190
|
},
|
|
190
191
|
});
|
|
191
|
-
logMessage("user",
|
|
192
|
+
logMessage("user", `${userPrefix()} ${escapeBlessed(text)}`);
|
|
192
193
|
renderScreen(); // Render plain text message immediately
|
|
193
194
|
}
|
|
194
195
|
|
|
@@ -23,10 +23,14 @@ function createProjectCloseController(options = {}) {
|
|
|
23
23
|
renderDashboard = () => {},
|
|
24
24
|
renderScreen = () => {},
|
|
25
25
|
logMessage = () => {},
|
|
26
|
+
resolveStatusLine = null,
|
|
26
27
|
escapeBlessed = (value) => String(value || ""),
|
|
27
28
|
} = options;
|
|
28
29
|
|
|
29
30
|
let closingProject = false;
|
|
31
|
+
const statusMsg = typeof resolveStatusLine === "function"
|
|
32
|
+
? resolveStatusLine
|
|
33
|
+
: (text) => logMessage("status", text);
|
|
30
34
|
|
|
31
35
|
function pickFallbackProjectRoot(targetProjectRoot) {
|
|
32
36
|
const rows = Array.isArray(getProjects()) ? getProjects() : [];
|
|
@@ -61,7 +65,7 @@ function createProjectCloseController(options = {}) {
|
|
|
61
65
|
|
|
62
66
|
closingProject = true;
|
|
63
67
|
try {
|
|
64
|
-
|
|
68
|
+
statusMsg(`{gray-fg}⚙{/gray-fg} Closing project ${escapedName} daemon and agents...`);
|
|
65
69
|
|
|
66
70
|
let switchedTo = "";
|
|
67
71
|
if (activeProjectRoot === projectRoot) {
|
|
@@ -89,9 +93,9 @@ function createProjectCloseController(options = {}) {
|
|
|
89
93
|
renderScreen();
|
|
90
94
|
|
|
91
95
|
if (wasRunning) {
|
|
92
|
-
|
|
96
|
+
statusMsg(`{gray-fg}✓{/gray-fg} Closed project ${escapedName} daemon and agents`);
|
|
93
97
|
} else {
|
|
94
|
-
|
|
98
|
+
statusMsg(`{gray-fg}✓{/gray-fg} Project ${escapedName} daemon already stopped`);
|
|
95
99
|
}
|
|
96
100
|
|
|
97
101
|
return {
|
|
@@ -75,7 +75,8 @@ function createSettingsController(options = {}) {
|
|
|
75
75
|
setSelectedProviderIndex(Math.max(0, providerOptions.findIndex((opt) => opt.value === next)));
|
|
76
76
|
saveConfig(projectRoot, { agentProvider: next });
|
|
77
77
|
clearUfooAgentIdentity();
|
|
78
|
-
logMessage("status",
|
|
78
|
+
const statusMsg = resolveStatusLine || ((text) => logMessage("status", text));
|
|
79
|
+
statusMsg(`{gray-fg}⚙{/gray-fg} ufoo-agent: ${providerLabel(next)}`);
|
|
79
80
|
renderDashboard();
|
|
80
81
|
renderScreen();
|
|
81
82
|
void restartDaemon();
|
|
@@ -89,7 +90,8 @@ function createSettingsController(options = {}) {
|
|
|
89
90
|
setSelectedResumeIndex(next ? 0 : 1);
|
|
90
91
|
saveConfig(projectRoot, { autoResume: next });
|
|
91
92
|
const label = next ? "Resume previous session" : "Start new session";
|
|
92
|
-
logMessage("status",
|
|
93
|
+
const statusMsg = resolveStatusLine || ((text) => logMessage("status", text));
|
|
94
|
+
statusMsg(`{gray-fg}⚙{/gray-fg} Resume mode: ${label}`);
|
|
93
95
|
renderDashboard();
|
|
94
96
|
renderScreen();
|
|
95
97
|
return true;
|