u-foo 2.4.1 → 2.4.3
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/agents/prompts/groupBootstrap.js +13 -0
- package/src/app/cli/run.js +39 -26
- package/src/coordination/bus/index.js +1 -1
- package/src/runtime/daemon/index.js +87 -35
- package/src/runtime/daemon/reportControlBus.js +150 -0
- package/src/ui/ink/ChatApp.js +20 -13
- package/src/ui/ink/MultilineInput.js +10 -2
- package/src/ui/ink/UcodeApp.js +35 -4
- package/src/ui/ink/chatReducer.js +81 -7
package/package.json
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
const crypto = require("crypto");
|
|
4
4
|
|
|
5
|
+
const SILENT_BOOTSTRAP_INSTRUCTION = [
|
|
6
|
+
"Bootstrap silence:",
|
|
7
|
+
"- This message is setup only, not a task.",
|
|
8
|
+
"- Apply these instructions silently, then wait for the next user, bus, or controller task.",
|
|
9
|
+
"- Do not reply, summarize, acknowledge, report, hand off, or call tools in response to this bootstrap message.",
|
|
10
|
+
"- Do not send `ufoo report` or `ufoo bus` until real work arrives after this bootstrap.",
|
|
11
|
+
].join("\n");
|
|
12
|
+
|
|
5
13
|
const SHARED_UFOO_PROTOCOL = [
|
|
6
14
|
"Session harness: ufoo",
|
|
7
15
|
"",
|
|
@@ -42,11 +50,15 @@ const SHARED_UFOO_PROTOCOL = [
|
|
|
42
50
|
"dedup, so don't worry about report loops.",
|
|
43
51
|
"",
|
|
44
52
|
"`ufoo report start|progress|done|error \"<short summary>\"`",
|
|
53
|
+
"Do not emulate report failures with `ufoo bus send ufoo-agent ...`.",
|
|
54
|
+
"If `ufoo report` fails, continue without a fallback bus report.",
|
|
45
55
|
"",
|
|
46
56
|
"Then continue the active task.",
|
|
47
57
|
].join("\n");
|
|
48
58
|
|
|
49
59
|
const SHARED_GROUP_PREFIX = [
|
|
60
|
+
SILENT_BOOTSTRAP_INSTRUCTION,
|
|
61
|
+
"",
|
|
50
62
|
"You are part of a ufoo multi-agent group.",
|
|
51
63
|
"",
|
|
52
64
|
"Shared rules:",
|
|
@@ -193,6 +205,7 @@ function computeBootstrapFingerprint({
|
|
|
193
205
|
}
|
|
194
206
|
|
|
195
207
|
module.exports = {
|
|
208
|
+
SILENT_BOOTSTRAP_INSTRUCTION,
|
|
196
209
|
SHARED_UFOO_PROTOCOL,
|
|
197
210
|
SHARED_GROUP_PREFIX,
|
|
198
211
|
SOLO_AGENT_PREFIX,
|
package/src/app/cli/run.js
CHANGED
|
@@ -129,6 +129,41 @@ async function sendDaemonRequest(projectRoot, payload) {
|
|
|
129
129
|
});
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
async function sendAgentReportRequest(projectRoot, report) {
|
|
133
|
+
const { normalizeReportInput } = require("../../coordination/report/store");
|
|
134
|
+
const { enqueueAgentReport } = require("../../runtime/daemon/reportControlBus");
|
|
135
|
+
const entry = normalizeReportInput(report);
|
|
136
|
+
const queued = await enqueueAgentReport(projectRoot, entry);
|
|
137
|
+
return {
|
|
138
|
+
type: "response",
|
|
139
|
+
data: {
|
|
140
|
+
reply: `Report queued (${entry.phase})`,
|
|
141
|
+
report: entry,
|
|
142
|
+
queued,
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function getReportDetail(out = {}) {
|
|
148
|
+
if (out.phase === "error") {
|
|
149
|
+
return out.error || out.summary || out.message || out.task_id;
|
|
150
|
+
}
|
|
151
|
+
return out.summary || out.message || out.task_id;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function printReportOutput(out, json = false, queued = null) {
|
|
155
|
+
if (json) {
|
|
156
|
+
console.log(JSON.stringify({
|
|
157
|
+
status: "queued",
|
|
158
|
+
report: out,
|
|
159
|
+
queued,
|
|
160
|
+
}, null, 2));
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const detail = getReportDetail(out);
|
|
164
|
+
console.log(`[report] queued ${out.phase} ${out.agent_id} ${out.task_id} ${detail}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
132
167
|
function requireOptional(name) {
|
|
133
168
|
try {
|
|
134
169
|
// eslint-disable-next-line global-require, import/no-dynamic-require
|
|
@@ -978,20 +1013,9 @@ async function runCli(argv) {
|
|
|
978
1013
|
};
|
|
979
1014
|
|
|
980
1015
|
try {
|
|
981
|
-
await
|
|
982
|
-
const resp = await sendDaemonRequest(projectRoot, {
|
|
983
|
-
type: "agent_report",
|
|
984
|
-
report,
|
|
985
|
-
});
|
|
1016
|
+
const resp = await sendAgentReportRequest(projectRoot, report);
|
|
986
1017
|
const out = resp?.data?.report || report;
|
|
987
|
-
|
|
988
|
-
console.log(JSON.stringify(out, null, 2));
|
|
989
|
-
return;
|
|
990
|
-
}
|
|
991
|
-
const detail = out.phase === "error"
|
|
992
|
-
? (out.error || out.summary || out.message || out.task_id)
|
|
993
|
-
: (out.summary || out.message || out.task_id);
|
|
994
|
-
console.log(`[report] ${out.phase} ${out.agent_id} ${out.task_id} ${detail}`);
|
|
1018
|
+
printReportOutput(out, opts.json, resp?.data?.queued || null);
|
|
995
1019
|
} catch (err) {
|
|
996
1020
|
console.error(err.message || String(err));
|
|
997
1021
|
process.exitCode = 1;
|
|
@@ -1926,20 +1950,9 @@ async function runCli(argv) {
|
|
|
1926
1950
|
|
|
1927
1951
|
(async () => {
|
|
1928
1952
|
try {
|
|
1929
|
-
await
|
|
1930
|
-
const resp = await sendDaemonRequest(process.cwd(), {
|
|
1931
|
-
type: "agent_report",
|
|
1932
|
-
report,
|
|
1933
|
-
});
|
|
1953
|
+
const resp = await sendAgentReportRequest(process.cwd(), report);
|
|
1934
1954
|
const out = resp?.data?.report || report;
|
|
1935
|
-
|
|
1936
|
-
console.log(JSON.stringify(out, null, 2));
|
|
1937
|
-
return;
|
|
1938
|
-
}
|
|
1939
|
-
const detail = out.phase === "error"
|
|
1940
|
-
? (out.error || out.summary || out.message || out.task_id)
|
|
1941
|
-
: (out.summary || out.message || out.task_id);
|
|
1942
|
-
console.log(`[report] ${out.phase} ${out.agent_id} ${out.task_id} ${detail}`);
|
|
1955
|
+
printReportOutput(out, rest.includes("--json"), resp?.data?.queued || null);
|
|
1943
1956
|
} catch (err) {
|
|
1944
1957
|
console.error(err.message || String(err));
|
|
1945
1958
|
process.exitCode = 1;
|
|
@@ -330,7 +330,7 @@ class EventBus {
|
|
|
330
330
|
injectionMode: options.injectionMode,
|
|
331
331
|
source: options.source,
|
|
332
332
|
})
|
|
333
|
-
: await this.messageManager.emit(target, eventName, data, publisher);
|
|
333
|
+
: await this.messageManager.emit(target, eventName, data, publisher, options.type);
|
|
334
334
|
const silent = options.silent === true;
|
|
335
335
|
if (!silent && eventName === "message") {
|
|
336
336
|
logOk(
|
|
@@ -21,6 +21,11 @@ const { normalizeFormat, renderGroupDiagramFromTemplate, renderGroupDiagramFromR
|
|
|
21
21
|
const { runPromptWithAssistant } = require("./promptLoop");
|
|
22
22
|
const { handlePromptRequest } = require("./promptRequest");
|
|
23
23
|
const { recordAgentReport } = require("./reporting");
|
|
24
|
+
const {
|
|
25
|
+
isAgentReportControlEvent,
|
|
26
|
+
extractAgentReportControl,
|
|
27
|
+
takeReportControlEvents,
|
|
28
|
+
} = require("./reportControlBus");
|
|
24
29
|
const { isGlobalControllerProjectRoot } = require("../projects");
|
|
25
30
|
const {
|
|
26
31
|
assignSoloRoleToExistingAgent,
|
|
@@ -855,7 +860,7 @@ async function dispatchMessages(projectRoot, dispatch = []) {
|
|
|
855
860
|
}
|
|
856
861
|
}
|
|
857
862
|
|
|
858
|
-
function startBusBridge(projectRoot, provider, onEvent, onStatus, shouldDrain) {
|
|
863
|
+
function startBusBridge(projectRoot, provider, onEvent, onStatus, shouldDrain, onReport) {
|
|
859
864
|
const state = {
|
|
860
865
|
subscriber: null,
|
|
861
866
|
queueFile: null,
|
|
@@ -867,6 +872,7 @@ function startBusBridge(projectRoot, provider, onEvent, onStatus, shouldDrain) {
|
|
|
867
872
|
};
|
|
868
873
|
const eventBus = new EventBus(projectRoot);
|
|
869
874
|
let joinInProgress = false;
|
|
875
|
+
let polling = false;
|
|
870
876
|
|
|
871
877
|
function getAgentNickname(agentId) {
|
|
872
878
|
if (!agentId) return agentId;
|
|
@@ -1057,6 +1063,27 @@ function startBusBridge(projectRoot, provider, onEvent, onStatus, shouldDrain) {
|
|
|
1057
1063
|
})();
|
|
1058
1064
|
}
|
|
1059
1065
|
|
|
1066
|
+
async function handleReportControlEvent(evt) {
|
|
1067
|
+
if (!isAgentReportControlEvent(evt)) return false;
|
|
1068
|
+
if (typeof onReport !== "function") return false;
|
|
1069
|
+
const control = extractAgentReportControl(evt);
|
|
1070
|
+
if (!control) return false;
|
|
1071
|
+
await onReport(control.report, {
|
|
1072
|
+
event: evt,
|
|
1073
|
+
requestId: control.request_id,
|
|
1074
|
+
queuedAt: control.queued_at,
|
|
1075
|
+
});
|
|
1076
|
+
return true;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
async function pollReportControlQueue() {
|
|
1080
|
+
const events = takeReportControlEvents(projectRoot);
|
|
1081
|
+
if (!events.length) return;
|
|
1082
|
+
for (const evt of events) {
|
|
1083
|
+
if (await handleReportControlEvent(evt)) continue;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1060
1087
|
function pollQueue() {
|
|
1061
1088
|
if (!state.queueFile) return;
|
|
1062
1089
|
if (!fs.existsSync(state.queueFile)) return;
|
|
@@ -1109,14 +1136,23 @@ function startBusBridge(projectRoot, provider, onEvent, onStatus, shouldDrain) {
|
|
|
1109
1136
|
}
|
|
1110
1137
|
}
|
|
1111
1138
|
|
|
1112
|
-
function poll() {
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1139
|
+
async function poll() {
|
|
1140
|
+
if (polling) return;
|
|
1141
|
+
polling = true;
|
|
1142
|
+
try {
|
|
1143
|
+
ensureSubscriber();
|
|
1144
|
+
await pollReportControlQueue();
|
|
1145
|
+
if (typeof shouldDrain === "function" && !shouldDrain()) return;
|
|
1146
|
+
pollQueue();
|
|
1147
|
+
pollWatchedEvents();
|
|
1148
|
+
} finally {
|
|
1149
|
+
polling = false;
|
|
1150
|
+
}
|
|
1117
1151
|
}
|
|
1118
1152
|
|
|
1119
|
-
const interval = setInterval(
|
|
1153
|
+
const interval = setInterval(() => {
|
|
1154
|
+
poll().catch(() => {});
|
|
1155
|
+
}, 1000);
|
|
1120
1156
|
return {
|
|
1121
1157
|
markPending(target) {
|
|
1122
1158
|
if (!target) return;
|
|
@@ -1300,11 +1336,53 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
1300
1336
|
log,
|
|
1301
1337
|
});
|
|
1302
1338
|
|
|
1339
|
+
const publishAgentReportResult = (entry) => {
|
|
1340
|
+
ipcServer.sendToSockets({
|
|
1341
|
+
type: IPC_RESPONSE_TYPES.BUS,
|
|
1342
|
+
data: {
|
|
1343
|
+
event: "controller_report",
|
|
1344
|
+
publisher: entry.agent_id,
|
|
1345
|
+
message: entry.summary || entry.message || entry.task_id,
|
|
1346
|
+
report: entry,
|
|
1347
|
+
},
|
|
1348
|
+
});
|
|
1349
|
+
ipcServer.sendToSockets({
|
|
1350
|
+
type: IPC_RESPONSE_TYPES.STATUS,
|
|
1351
|
+
data: buildRuntimeStatus(),
|
|
1352
|
+
});
|
|
1353
|
+
};
|
|
1354
|
+
|
|
1355
|
+
const handleAgentReport = async (report, options = {}) => {
|
|
1356
|
+
const source = options.source || report.source || "cli";
|
|
1357
|
+
const { entry } = await recordAgentReport({
|
|
1358
|
+
projectRoot,
|
|
1359
|
+
report: {
|
|
1360
|
+
...report,
|
|
1361
|
+
source,
|
|
1362
|
+
},
|
|
1363
|
+
onStatus: (status) => {
|
|
1364
|
+
ipcServer.sendToSockets({
|
|
1365
|
+
type: IPC_RESPONSE_TYPES.STATUS,
|
|
1366
|
+
data: status,
|
|
1367
|
+
});
|
|
1368
|
+
},
|
|
1369
|
+
log,
|
|
1370
|
+
});
|
|
1371
|
+
publishAgentReportResult(entry);
|
|
1372
|
+
return entry;
|
|
1373
|
+
};
|
|
1374
|
+
|
|
1303
1375
|
const busBridge = startBusBridge(projectRoot, provider, (evt) => {
|
|
1304
1376
|
ipcServer.sendToSockets({ type: IPC_RESPONSE_TYPES.BUS, data: evt });
|
|
1305
1377
|
}, (status) => {
|
|
1306
1378
|
ipcServer.sendToSockets({ type: IPC_RESPONSE_TYPES.STATUS, data: status });
|
|
1307
|
-
}, () => ipcServer.hasClients())
|
|
1379
|
+
}, () => ipcServer.hasClients(), async (report, meta = {}) => {
|
|
1380
|
+
try {
|
|
1381
|
+
await handleAgentReport(report || {}, { source: (report && report.source) || "bus" });
|
|
1382
|
+
} catch (err) {
|
|
1383
|
+
log(`report bus event failed request=${meta.requestId || ""} error=${err.message || String(err)}`);
|
|
1384
|
+
}
|
|
1385
|
+
});
|
|
1308
1386
|
|
|
1309
1387
|
handleIpcRequest = async (req, socket) => {
|
|
1310
1388
|
if (!req || typeof req !== "object") return;
|
|
@@ -1399,20 +1477,7 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
1399
1477
|
if (req.type === IPC_REQUEST_TYPES.AGENT_REPORT) {
|
|
1400
1478
|
try {
|
|
1401
1479
|
const report = req.report && typeof req.report === "object" ? req.report : {};
|
|
1402
|
-
const
|
|
1403
|
-
projectRoot,
|
|
1404
|
-
report: {
|
|
1405
|
-
...report,
|
|
1406
|
-
source: report.source || "cli",
|
|
1407
|
-
},
|
|
1408
|
-
onStatus: (status) => {
|
|
1409
|
-
ipcServer.sendToSockets({
|
|
1410
|
-
type: IPC_RESPONSE_TYPES.STATUS,
|
|
1411
|
-
data: status,
|
|
1412
|
-
});
|
|
1413
|
-
},
|
|
1414
|
-
log,
|
|
1415
|
-
});
|
|
1480
|
+
const entry = await handleAgentReport(report, { source: report.source || "cli" });
|
|
1416
1481
|
socket.write(
|
|
1417
1482
|
`${JSON.stringify({
|
|
1418
1483
|
type: IPC_RESPONSE_TYPES.RESPONSE,
|
|
@@ -1423,19 +1488,6 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
1423
1488
|
})}
|
|
1424
1489
|
`,
|
|
1425
1490
|
);
|
|
1426
|
-
ipcServer.sendToSockets({
|
|
1427
|
-
type: IPC_RESPONSE_TYPES.BUS,
|
|
1428
|
-
data: {
|
|
1429
|
-
event: "controller_report",
|
|
1430
|
-
publisher: entry.agent_id,
|
|
1431
|
-
message: entry.summary || entry.message || entry.task_id,
|
|
1432
|
-
report: entry,
|
|
1433
|
-
},
|
|
1434
|
-
});
|
|
1435
|
-
ipcServer.sendToSockets({
|
|
1436
|
-
type: IPC_RESPONSE_TYPES.STATUS,
|
|
1437
|
-
data: buildRuntimeStatus(),
|
|
1438
|
-
});
|
|
1439
1491
|
} catch (err) {
|
|
1440
1492
|
socket.write(
|
|
1441
1493
|
`${JSON.stringify({
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { getUfooPaths } = require("../../coordination/state/paths");
|
|
6
|
+
const {
|
|
7
|
+
appendJSONL,
|
|
8
|
+
ensureDir,
|
|
9
|
+
generateInstanceId,
|
|
10
|
+
} = require("../../coordination/bus/utils");
|
|
11
|
+
|
|
12
|
+
const REPORT_CONTROL_TARGET = "ufoo-agent";
|
|
13
|
+
const REPORT_CONTROL_EVENT = "agent_report";
|
|
14
|
+
const REPORT_CONTROL_TYPE = "control/report";
|
|
15
|
+
|
|
16
|
+
function getReportControlQueueDir(projectRoot) {
|
|
17
|
+
const paths = getUfooPaths(projectRoot);
|
|
18
|
+
return path.join(paths.busDir, "control", "report");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getReportControlQueueFile(projectRoot) {
|
|
22
|
+
return path.join(getReportControlQueueDir(projectRoot), "pending.jsonl");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function ensureReportControlQueue(projectRoot) {
|
|
26
|
+
const queueDir = getReportControlQueueDir(projectRoot);
|
|
27
|
+
ensureDir(queueDir);
|
|
28
|
+
return queueDir;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function resolveReportPublisher(report = {}) {
|
|
32
|
+
const fromReport = String(report.agent_id || report.agentId || "").trim();
|
|
33
|
+
if (fromReport) return fromReport;
|
|
34
|
+
const fromEnv = String(process.env.UFOO_SUBSCRIBER_ID || "").trim();
|
|
35
|
+
return fromEnv || "unknown-agent";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function buildReportControlData(report = {}, options = {}) {
|
|
39
|
+
return {
|
|
40
|
+
request_id: options.requestId || `report-${Date.now().toString(36)}-${generateInstanceId()}`,
|
|
41
|
+
queued_at: options.queuedAt || new Date().toISOString(),
|
|
42
|
+
report,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function buildReportControlEvent(report = {}, options = {}) {
|
|
47
|
+
const data = buildReportControlData(report, options);
|
|
48
|
+
return {
|
|
49
|
+
timestamp: data.queued_at,
|
|
50
|
+
type: REPORT_CONTROL_TYPE,
|
|
51
|
+
event: REPORT_CONTROL_EVENT,
|
|
52
|
+
publisher: options.publisher || resolveReportPublisher(report),
|
|
53
|
+
target: REPORT_CONTROL_TARGET,
|
|
54
|
+
data,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function enqueueAgentReport(projectRoot, report, options = {}) {
|
|
59
|
+
ensureReportControlQueue(projectRoot);
|
|
60
|
+
|
|
61
|
+
const event = buildReportControlEvent(report, options);
|
|
62
|
+
appendJSONL(getReportControlQueueFile(projectRoot), event);
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
queued: true,
|
|
66
|
+
request_id: event.data.request_id,
|
|
67
|
+
target: REPORT_CONTROL_TARGET,
|
|
68
|
+
targets: [REPORT_CONTROL_TARGET],
|
|
69
|
+
report,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function takeReportControlEvents(projectRoot) {
|
|
74
|
+
const queueFile = getReportControlQueueFile(projectRoot);
|
|
75
|
+
if (!fs.existsSync(queueFile)) return [];
|
|
76
|
+
|
|
77
|
+
const processingFile = `${queueFile}.processing.${process.pid}.${Date.now()}.${generateInstanceId()}`;
|
|
78
|
+
let content = "";
|
|
79
|
+
let readOk = false;
|
|
80
|
+
try {
|
|
81
|
+
fs.renameSync(queueFile, processingFile);
|
|
82
|
+
content = fs.readFileSync(processingFile, "utf8");
|
|
83
|
+
readOk = true;
|
|
84
|
+
} catch {
|
|
85
|
+
try {
|
|
86
|
+
if (fs.existsSync(processingFile)) {
|
|
87
|
+
fs.renameSync(processingFile, queueFile);
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
// ignore rollback errors
|
|
91
|
+
}
|
|
92
|
+
return [];
|
|
93
|
+
} finally {
|
|
94
|
+
if (readOk) {
|
|
95
|
+
try {
|
|
96
|
+
if (fs.existsSync(processingFile)) {
|
|
97
|
+
fs.rmSync(processingFile, { force: true });
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
// ignore cleanup errors
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return content.split(/\r?\n/)
|
|
106
|
+
.filter(Boolean)
|
|
107
|
+
.map((line) => {
|
|
108
|
+
try {
|
|
109
|
+
return JSON.parse(line);
|
|
110
|
+
} catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
.filter(Boolean);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function isAgentReportControlEvent(evt) {
|
|
118
|
+
if (!evt || typeof evt !== "object") return false;
|
|
119
|
+
if (evt.target !== REPORT_CONTROL_TARGET) return false;
|
|
120
|
+
if (evt.type !== REPORT_CONTROL_TYPE) return false;
|
|
121
|
+
if (evt.event !== REPORT_CONTROL_EVENT) return false;
|
|
122
|
+
const data = evt.data && typeof evt.data === "object" ? evt.data : {};
|
|
123
|
+
return Boolean(data.report && typeof data.report === "object");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function extractAgentReportControl(evt) {
|
|
127
|
+
if (!isAgentReportControlEvent(evt)) return null;
|
|
128
|
+
const data = evt.data && typeof evt.data === "object" ? evt.data : {};
|
|
129
|
+
return {
|
|
130
|
+
report: data.report,
|
|
131
|
+
request_id: data.request_id || "",
|
|
132
|
+
queued_at: data.queued_at || evt.timestamp || evt.ts || "",
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
module.exports = {
|
|
137
|
+
REPORT_CONTROL_TARGET,
|
|
138
|
+
REPORT_CONTROL_EVENT,
|
|
139
|
+
REPORT_CONTROL_TYPE,
|
|
140
|
+
getReportControlQueueDir,
|
|
141
|
+
getReportControlQueueFile,
|
|
142
|
+
ensureReportControlQueue,
|
|
143
|
+
resolveReportPublisher,
|
|
144
|
+
buildReportControlData,
|
|
145
|
+
buildReportControlEvent,
|
|
146
|
+
enqueueAgentReport,
|
|
147
|
+
takeReportControlEvents,
|
|
148
|
+
isAgentReportControlEvent,
|
|
149
|
+
extractAgentReportControl,
|
|
150
|
+
};
|
package/src/ui/ink/ChatApp.js
CHANGED
|
@@ -1010,8 +1010,14 @@ function inferStatusType(text = "", requestedType = "") {
|
|
|
1010
1010
|
const type = String(requestedType || "").trim().toLowerCase();
|
|
1011
1011
|
if (type === "done" || type === "success" || type === "error" || type === "idle") return type;
|
|
1012
1012
|
const clean = stripBlessedTags(String(text || "")).trim();
|
|
1013
|
-
if (/^[
|
|
1014
|
-
if (
|
|
1013
|
+
if (/^[✗!]/.test(clean) || /\b(error|failed|failure|offline)\b/i.test(clean) || /失败|错误/.test(clean)) return "error";
|
|
1014
|
+
if (
|
|
1015
|
+
/^[✓✔]/.test(clean) ||
|
|
1016
|
+
/^(done|closed|complete|completed|finished|success|succeeded|ready)\b/i.test(clean) ||
|
|
1017
|
+
/\b(processed|reconnected|switched|saved)\b/i.test(clean) ||
|
|
1018
|
+
/\bdone\s*$/i.test(clean) ||
|
|
1019
|
+
/完成|成功|已处理|已保存|已切换|已连接/.test(clean)
|
|
1020
|
+
) return "done";
|
|
1015
1021
|
return type || "typing";
|
|
1016
1022
|
}
|
|
1017
1023
|
|
|
@@ -1123,12 +1129,13 @@ function createChatApp({ React, ink, props, interactive = true }) {
|
|
|
1123
1129
|
dispatch({ type: "status/idle" });
|
|
1124
1130
|
return;
|
|
1125
1131
|
}
|
|
1132
|
+
const type = inferStatusType(clean, options.type || "typing");
|
|
1126
1133
|
dispatch({
|
|
1127
1134
|
type: "status/set",
|
|
1128
1135
|
payload: {
|
|
1129
1136
|
message: clean,
|
|
1130
|
-
type
|
|
1131
|
-
showTimer: options.showTimer === true,
|
|
1137
|
+
type,
|
|
1138
|
+
showTimer: options.showTimer === true && isAnimatedStatusType(type),
|
|
1132
1139
|
startedAt: options.startedAt || Date.now(),
|
|
1133
1140
|
},
|
|
1134
1141
|
});
|
|
@@ -1312,8 +1319,10 @@ function createChatApp({ React, ink, props, interactive = true }) {
|
|
|
1312
1319
|
|
|
1313
1320
|
useEffect(() => {
|
|
1314
1321
|
if (!stdout) return undefined;
|
|
1315
|
-
const update = () =>
|
|
1316
|
-
|
|
1322
|
+
const update = () => {
|
|
1323
|
+
const next = { cols: stdout.columns || 0, rows: stdout.rows || 0 };
|
|
1324
|
+
setSize((prev) => (prev.cols === next.cols && prev.rows === next.rows ? prev : next));
|
|
1325
|
+
};
|
|
1317
1326
|
update();
|
|
1318
1327
|
stdout.on("resize", update);
|
|
1319
1328
|
return () => stdout.off("resize", update);
|
|
@@ -1881,7 +1890,8 @@ function createChatApp({ React, ink, props, interactive = true }) {
|
|
|
1881
1890
|
useEffect(() => {
|
|
1882
1891
|
const internalStatus = state.viewingAgentId ? internalStatusLabel(internalAgentView.status) : "ready";
|
|
1883
1892
|
const internalActive = internalStatus !== "ready";
|
|
1884
|
-
const
|
|
1893
|
+
const statusType = inferStatusType(state.status.message, state.status.type);
|
|
1894
|
+
const statusAnimated = state.status.message && isAnimatedStatusType(statusType);
|
|
1885
1895
|
if ((!statusAnimated) && !internalActive) return undefined;
|
|
1886
1896
|
const timer = setInterval(() => setSpinnerTick((t) => t + 1), 100);
|
|
1887
1897
|
return () => clearInterval(timer);
|
|
@@ -1922,13 +1932,10 @@ function createChatApp({ React, ink, props, interactive = true }) {
|
|
|
1922
1932
|
dispatch({ type: "log/append", text: `Error: ${err && err.message ? err.message : err}` });
|
|
1923
1933
|
}
|
|
1924
1934
|
if (statusText) {
|
|
1925
|
-
|
|
1926
|
-
type: "status/set",
|
|
1927
|
-
payload: { message: statusText, type: "typing", showTimer: false, startedAt: Date.now() },
|
|
1928
|
-
});
|
|
1935
|
+
setStatusText(statusText, { type: "typing", showTimer: false });
|
|
1929
1936
|
}
|
|
1930
1937
|
if (restart) restartDaemonBestEffort();
|
|
1931
|
-
}, [restartDaemonBestEffort]);
|
|
1938
|
+
}, [restartDaemonBestEffort, setStatusText]);
|
|
1932
1939
|
|
|
1933
1940
|
const clearUfooAgentIdentity = useCallback(() => {
|
|
1934
1941
|
try {
|
|
@@ -3501,7 +3508,7 @@ function buildDashHints(state, targetAgentLabel) {
|
|
|
3501
3508
|
function computeStatusText(status, spinnerTick) {
|
|
3502
3509
|
const message = String((status && status.message) || "");
|
|
3503
3510
|
if (!message) return "CHAT · Ready";
|
|
3504
|
-
const type =
|
|
3511
|
+
const type = inferStatusType(message, status && status.type);
|
|
3505
3512
|
if (type === "done" || type === "success") {
|
|
3506
3513
|
const clean = stripBlessedTags(message).trim();
|
|
3507
3514
|
return /^[✓✔]/.test(clean) ? clean : `✓ ${clean}`;
|
|
@@ -482,11 +482,19 @@ function createMultilineInput({ React, ink }) {
|
|
|
482
482
|
return undefined;
|
|
483
483
|
}
|
|
484
484
|
patchStdoutForIME(out);
|
|
485
|
+
const targetRowsUp = __imeCursor.lastFrameHadNewline
|
|
486
|
+
? rowsBelowCursor
|
|
487
|
+
: Math.max(0, rowsBelowCursor - 1);
|
|
488
|
+
const alreadyParked = __imeCursor.active === true
|
|
489
|
+
&& __imeCursor.parkRowsUp === rowsBelowCursor
|
|
490
|
+
&& __imeCursor.parkCol === cursorTermCol
|
|
491
|
+
&& __imeCursor.movedUpRows === targetRowsUp;
|
|
485
492
|
// Publish the desired park target so the stdout monkey-patch can
|
|
486
493
|
// re-park after every throttled ink frame write.
|
|
487
494
|
__imeCursor.active = true;
|
|
488
495
|
__imeCursor.parkRowsUp = rowsBelowCursor;
|
|
489
496
|
__imeCursor.parkCol = cursorTermCol;
|
|
497
|
+
if (alreadyParked) return undefined;
|
|
490
498
|
// Park immediately — covers cases where ink has nothing to render
|
|
491
499
|
// (output unchanged) and won't fire a frame write at all, and keeps
|
|
492
500
|
// the caret visible between frames. Combine hide + restore + park +
|
|
@@ -496,7 +504,7 @@ function createMultilineInput({ React, ink }) {
|
|
|
496
504
|
// CRITICAL: the move-up amount must match the anchor that movedUpRows
|
|
497
505
|
// was measured against. If the last frame ended without '\n' (the
|
|
498
506
|
// full-screen path), the anchor is one row higher than the log-update
|
|
499
|
-
// case, so we use
|
|
507
|
+
// case, so we use targetRowsUp rather than parkRowsUp directly.
|
|
500
508
|
// Otherwise restoring down by movedUpRows then moving up parkRowsUp
|
|
501
509
|
// overshoots by one and leaves the hardware cursor one row above the
|
|
502
510
|
// inverse caret — the residual "ghost cursor" symptom.
|
|
@@ -505,7 +513,7 @@ function createMultilineInput({ React, ink }) {
|
|
|
505
513
|
combined += `\x1b[${__imeCursor.movedUpRows}B`;
|
|
506
514
|
__imeCursor.movedUpRows = 0;
|
|
507
515
|
}
|
|
508
|
-
combined += applyParkSequence(
|
|
516
|
+
combined += applyParkSequence(targetRowsUp);
|
|
509
517
|
out.write(combined);
|
|
510
518
|
return undefined;
|
|
511
519
|
});
|
package/src/ui/ink/UcodeApp.js
CHANGED
|
@@ -643,8 +643,10 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
643
643
|
|
|
644
644
|
useEffect(() => {
|
|
645
645
|
if (!stdout) return undefined;
|
|
646
|
-
const update = () =>
|
|
647
|
-
|
|
646
|
+
const update = () => {
|
|
647
|
+
const next = { cols: stdout.columns || 0, rows: stdout.rows || 0 };
|
|
648
|
+
setSize((prev) => (prev.cols === next.cols && prev.rows === next.rows ? prev : next));
|
|
649
|
+
};
|
|
648
650
|
update();
|
|
649
651
|
stdout.on("resize", update);
|
|
650
652
|
return () => stdout.off("resize", update);
|
|
@@ -652,7 +654,11 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
652
654
|
|
|
653
655
|
// Drive the spinner + elapsed-timer redraws while a task is in flight.
|
|
654
656
|
useEffect(() => {
|
|
655
|
-
|
|
657
|
+
const statusType = inferStatusType(status.message, status.type);
|
|
658
|
+
if (!status.message || statusType === "none" || statusType === "idle" ||
|
|
659
|
+
statusType === "done" || statusType === "success" || statusType === "error") {
|
|
660
|
+
return undefined;
|
|
661
|
+
}
|
|
656
662
|
const timer = setInterval(() => {
|
|
657
663
|
setSpinnerTick((t) => t + 1);
|
|
658
664
|
if (status.showTimer) setNowTick((t) => t + 1);
|
|
@@ -792,6 +798,22 @@ function runUcodeInkTui(props = {}) {
|
|
|
792
798
|
|
|
793
799
|
module.exports = { runUcodeInkTui, createUcodeApp, computeStatusText };
|
|
794
800
|
|
|
801
|
+
function inferStatusType(text = "", requestedType = "") {
|
|
802
|
+
const type = String(requestedType || "").trim().toLowerCase();
|
|
803
|
+
if (type === "done" || type === "success" || type === "error" || type === "idle" || type === "none") {
|
|
804
|
+
return type;
|
|
805
|
+
}
|
|
806
|
+
const clean = String(text || "").trim();
|
|
807
|
+
if (/^[✗!]/.test(clean) || /\b(error|failed|failure)\b/i.test(clean) || /失败|错误/.test(clean)) return "error";
|
|
808
|
+
if (
|
|
809
|
+
/^[✓✔]/.test(clean) ||
|
|
810
|
+
/^(done|complete|completed|finished|success|succeeded|ready)\b/i.test(clean) ||
|
|
811
|
+
/\bdone\s*$/i.test(clean) ||
|
|
812
|
+
/完成|成功/.test(clean)
|
|
813
|
+
) return "done";
|
|
814
|
+
return type || "thinking";
|
|
815
|
+
}
|
|
816
|
+
|
|
795
817
|
/**
|
|
796
818
|
* Pure status-line text builder used by the React component (and unit
|
|
797
819
|
* tests). Returns "UCODE · Ready" while idle and a spinner+message+timer
|
|
@@ -802,7 +824,16 @@ function computeStatusText(status, spinnerTick, backgroundSuffix = "") {
|
|
|
802
824
|
const message = String((status && status.message) || "");
|
|
803
825
|
const suffix = String(backgroundSuffix || "");
|
|
804
826
|
if (!message) return `UCODE · Ready${suffix}`;
|
|
805
|
-
const type =
|
|
827
|
+
const type = inferStatusType(message, status && status.type);
|
|
828
|
+
if (type === "done" || type === "success") {
|
|
829
|
+
const clean = message.trim();
|
|
830
|
+
return `${/^[✓✔]/.test(clean) ? clean : `✓ ${clean}`}${suffix}`;
|
|
831
|
+
}
|
|
832
|
+
if (type === "error") {
|
|
833
|
+
const clean = message.trim();
|
|
834
|
+
return `${/^[✗!]/.test(clean) ? clean : `✗ ${clean}`}${suffix}`;
|
|
835
|
+
}
|
|
836
|
+
if (type === "idle" || type === "none") return `${message.trim() || "UCODE · Ready"}${suffix}`;
|
|
806
837
|
const indicators = fmt.STATUS_INDICATORS[type] || fmt.STATUS_INDICATORS.thinking;
|
|
807
838
|
const indicator = indicators[Math.max(0, Math.floor(Number(spinnerTick) || 0)) % indicators.length];
|
|
808
839
|
const startedAt = Number.isFinite(status && status.startedAt) ? status.startedAt : 0;
|
|
@@ -48,6 +48,53 @@ function projectRootOf(row = {}) {
|
|
|
48
48
|
return String((row && (row.root || row.project_root || row.projectRoot)) || "");
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
function stableJson(value) {
|
|
52
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
53
|
+
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
54
|
+
const keys = Object.keys(value).sort();
|
|
55
|
+
return `{${keys.map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(",")}}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function shallowArrayEqual(left = [], right = []) {
|
|
59
|
+
if (left === right) return true;
|
|
60
|
+
if (!Array.isArray(left) || !Array.isArray(right)) return false;
|
|
61
|
+
if (left.length !== right.length) return false;
|
|
62
|
+
for (let i = 0; i < left.length; i += 1) {
|
|
63
|
+
if (left[i] !== right[i]) return false;
|
|
64
|
+
}
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function listPayloadEqual(left = [], right = []) {
|
|
69
|
+
if (left === right) return true;
|
|
70
|
+
if (!Array.isArray(left) || !Array.isArray(right)) return false;
|
|
71
|
+
if (left.length !== right.length) return false;
|
|
72
|
+
for (let i = 0; i < left.length; i += 1) {
|
|
73
|
+
if (stableJson(left[i]) !== stableJson(right[i])) return false;
|
|
74
|
+
}
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function mapPayloadEqual(left, right) {
|
|
79
|
+
if (left === right) return true;
|
|
80
|
+
if (!(left instanceof Map) || !(right instanceof Map)) return false;
|
|
81
|
+
if (left.size !== right.size) return false;
|
|
82
|
+
for (const [key, value] of left.entries()) {
|
|
83
|
+
if (!right.has(key)) return false;
|
|
84
|
+
if (stableJson(value) !== stableJson(right.get(key))) return false;
|
|
85
|
+
}
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function statusPayloadEqual(left, right) {
|
|
90
|
+
const normalize = (value = {}) => {
|
|
91
|
+
const normalized = { ...value };
|
|
92
|
+
if (normalized.showTimer !== true) normalized.startedAt = 0;
|
|
93
|
+
return normalized;
|
|
94
|
+
};
|
|
95
|
+
return stableJson(normalize(left)) === stableJson(normalize(right));
|
|
96
|
+
}
|
|
97
|
+
|
|
51
98
|
function createInitialState({ banner = [], globalMode = false, globalScope = "controller", settings = {} } = {}) {
|
|
52
99
|
const initialLaunchMode = settings.launchMode || "auto";
|
|
53
100
|
const initialAgentProvider = settings.agentProvider || "codex-cli";
|
|
@@ -174,6 +221,14 @@ function reducer(state, action) {
|
|
|
174
221
|
} else if (nextIdx >= ids.length) {
|
|
175
222
|
nextIdx = ids.length - 1;
|
|
176
223
|
}
|
|
224
|
+
if (
|
|
225
|
+
shallowArrayEqual(state.agents, ids) &&
|
|
226
|
+
mapPayloadEqual(state.activeAgentMeta, meta) &&
|
|
227
|
+
state.selectedAgentIndex === nextIdx &&
|
|
228
|
+
state.agentSelectionMode === nextMode
|
|
229
|
+
) {
|
|
230
|
+
return state;
|
|
231
|
+
}
|
|
177
232
|
return {
|
|
178
233
|
...state,
|
|
179
234
|
agents: ids,
|
|
@@ -216,12 +271,22 @@ function reducer(state, action) {
|
|
|
216
271
|
const selectedIndex = selectedRoot
|
|
217
272
|
? list.findIndex((row) => projectRootOf(row) === selectedRoot)
|
|
218
273
|
: -1;
|
|
274
|
+
const nextActiveRoot = action.activeProjectRoot || state.activeProjectRoot;
|
|
275
|
+
if (
|
|
276
|
+
listPayloadEqual(state.projects, list) &&
|
|
277
|
+
state.selectedProjectRoot === (selectedIndex >= 0 ? selectedRoot : "") &&
|
|
278
|
+
state.selectedProjectIndex === selectedIndex &&
|
|
279
|
+
state.activeProjectRoot === nextActiveRoot &&
|
|
280
|
+
state.emptyProjectsDownArmed === (list.length === 0 ? state.emptyProjectsDownArmed : false)
|
|
281
|
+
) {
|
|
282
|
+
return state;
|
|
283
|
+
}
|
|
219
284
|
return {
|
|
220
285
|
...state,
|
|
221
286
|
projects: list,
|
|
222
287
|
selectedProjectRoot: selectedIndex >= 0 ? selectedRoot : "",
|
|
223
288
|
selectedProjectIndex: selectedIndex,
|
|
224
|
-
activeProjectRoot:
|
|
289
|
+
activeProjectRoot: nextActiveRoot,
|
|
225
290
|
emptyProjectsDownArmed: list.length === 0 ? state.emptyProjectsDownArmed : false,
|
|
226
291
|
};
|
|
227
292
|
}
|
|
@@ -240,8 +305,11 @@ function reducer(state, action) {
|
|
|
240
305
|
return { ...state, projectListWindowStart: Math.max(0, action.windowStart | 0) };
|
|
241
306
|
case "scope/set":
|
|
242
307
|
return { ...state, globalScope: action.scope === "project" ? "project" : "controller" };
|
|
243
|
-
case "status/set":
|
|
244
|
-
|
|
308
|
+
case "status/set": {
|
|
309
|
+
const nextStatus = { ...state.status, ...action.payload };
|
|
310
|
+
if (statusPayloadEqual(state.status, nextStatus)) return state;
|
|
311
|
+
return { ...state, status: nextStatus };
|
|
312
|
+
}
|
|
245
313
|
case "status/idle":
|
|
246
314
|
return { ...state, status: { message: "", type: "thinking", showTimer: false, startedAt: 0 } };
|
|
247
315
|
case "history/push": {
|
|
@@ -311,10 +379,16 @@ function reducer(state, action) {
|
|
|
311
379
|
return { ...state, selectedProviderIndex: Math.max(0, action.index | 0) };
|
|
312
380
|
case "cronIndex/set":
|
|
313
381
|
return { ...state, selectedCronIndex: Math.max(-1, action.index | 0) };
|
|
314
|
-
case "cron/set":
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
return { ...state,
|
|
382
|
+
case "cron/set": {
|
|
383
|
+
const list = Array.isArray(action.list) ? action.list : [];
|
|
384
|
+
if (listPayloadEqual(state.cronTasks, list)) return state;
|
|
385
|
+
return { ...state, cronTasks: list };
|
|
386
|
+
}
|
|
387
|
+
case "loop/set": {
|
|
388
|
+
const summary = action.summary && typeof action.summary === "object" ? action.summary : null;
|
|
389
|
+
if (stableJson(state.loopSummary) === stableJson(summary)) return state;
|
|
390
|
+
return { ...state, loopSummary: summary };
|
|
391
|
+
}
|
|
318
392
|
case "stream/begin":
|
|
319
393
|
return {
|
|
320
394
|
...state,
|