u-foo 1.2.13 → 1.2.16
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/bin/ufoo.js +149 -0
- package/modules/online/SKILLS/ufoo-online/SKILL.md +2 -2
- package/package.json +1 -1
- package/scripts/postinstall.js +32 -14
- package/src/agent/notifier.js +5 -1
- package/src/agent/ufooAgent.js +1 -1
- package/src/bus/index.js +10 -3
- package/src/bus/inject.js +6 -3
- package/src/bus/subscriber.js +15 -2
- package/src/chat/commandExecutor.js +138 -10
- package/src/chat/commands.js +2 -1
- package/src/chat/daemonMessageRouter.js +40 -0
- package/src/chat/index.js +10 -29
- package/src/cli/onlineCoreCommands.js +11 -11
- package/src/cli.js +43 -4
- package/src/code/agent.js +74 -3
- package/src/code/tui.js +126 -34
- package/src/daemon/cronOps.js +362 -29
- package/src/daemon/index.js +64 -0
- package/src/daemon/status.js +3 -0
- package/src/online/bridge.js +9 -2
- package/src/online/client.js +1 -1
- package/src/shared/eventContract.js +1 -0
package/src/chat/index.js
CHANGED
|
@@ -34,7 +34,6 @@ const { createInputListenerController } = require("./inputListenerController");
|
|
|
34
34
|
const { createDaemonMessageRouter } = require("./daemonMessageRouter");
|
|
35
35
|
const { createChatLogController } = require("./chatLogController");
|
|
36
36
|
const { createPasteController } = require("./pasteController");
|
|
37
|
-
const { createCronScheduler } = require("./cronScheduler");
|
|
38
37
|
const { createAgentViewController } = require("./agentViewController");
|
|
39
38
|
const { createSettingsController } = require("./settingsController");
|
|
40
39
|
const { createChatLayout } = require("./layout");
|
|
@@ -87,12 +86,7 @@ async function runChat(projectRoot) {
|
|
|
87
86
|
let agentProvider = config.agentProvider;
|
|
88
87
|
let assistantEngine = normalizeAssistantEngine(config.assistantEngine);
|
|
89
88
|
let autoResume = config.autoResume !== false;
|
|
90
|
-
let
|
|
91
|
-
addTask: () => null,
|
|
92
|
-
listTasks: () => [],
|
|
93
|
-
stopTask: () => false,
|
|
94
|
-
stopAll: () => 0,
|
|
95
|
-
};
|
|
89
|
+
let cronTasks = [];
|
|
96
90
|
|
|
97
91
|
// Dynamic input height settings
|
|
98
92
|
// Layout: topLine(1) + content + bottomLine(1) + dashboard(1)
|
|
@@ -319,7 +313,6 @@ async function runChat(projectRoot) {
|
|
|
319
313
|
if (daemonCoordinator) {
|
|
320
314
|
daemonCoordinator.markExit();
|
|
321
315
|
}
|
|
322
|
-
cronScheduler.stopAll();
|
|
323
316
|
exitAgentView();
|
|
324
317
|
if (screen && screen.program && typeof screen.program.decrst === "function") {
|
|
325
318
|
screen.program.decrst(2004);
|
|
@@ -724,21 +717,6 @@ async function runChat(projectRoot) {
|
|
|
724
717
|
daemonCoordinator.send(req);
|
|
725
718
|
}
|
|
726
719
|
|
|
727
|
-
cronScheduler = createCronScheduler({
|
|
728
|
-
dispatch: ({ taskId, target, message }) => {
|
|
729
|
-
send({
|
|
730
|
-
type: IPC_REQUEST_TYPES.BUS_SEND,
|
|
731
|
-
target,
|
|
732
|
-
message,
|
|
733
|
-
});
|
|
734
|
-
queueStatusLine(`cron:${taskId} -> ${target}`);
|
|
735
|
-
},
|
|
736
|
-
onChange: () => {
|
|
737
|
-
renderDashboard();
|
|
738
|
-
screen.render();
|
|
739
|
-
},
|
|
740
|
-
});
|
|
741
|
-
|
|
742
720
|
function updatePromptBox() {
|
|
743
721
|
if (targetAgent) {
|
|
744
722
|
const label = getAgentLabel(targetAgent);
|
|
@@ -907,7 +885,7 @@ async function runChat(projectRoot) {
|
|
|
907
885
|
selectedProviderIndex,
|
|
908
886
|
selectedAssistantIndex,
|
|
909
887
|
selectedResumeIndex,
|
|
910
|
-
cronTasks
|
|
888
|
+
cronTasks,
|
|
911
889
|
providerOptions,
|
|
912
890
|
assistantOptions,
|
|
913
891
|
resumeOptions,
|
|
@@ -923,6 +901,7 @@ async function runChat(projectRoot) {
|
|
|
923
901
|
reportPendingTotal = Number.isFinite(status?.reports?.pending_total)
|
|
924
902
|
? status.reports.pending_total
|
|
925
903
|
: 0;
|
|
904
|
+
cronTasks = Array.isArray(status?.cron?.tasks) ? status.cron.tasks : [];
|
|
926
905
|
const metaList = Array.isArray(status.active_meta) ? status.active_meta : [];
|
|
927
906
|
let fallbackMap = null;
|
|
928
907
|
if (metaList.length === 0 && activeAgents.length > 0) {
|
|
@@ -1018,7 +997,7 @@ async function runChat(projectRoot) {
|
|
|
1018
997
|
agentProvider: { get: () => agentProvider },
|
|
1019
998
|
assistantEngine: { get: () => assistantEngine },
|
|
1020
999
|
autoResume: { get: () => autoResume },
|
|
1021
|
-
cronTasks: { get: () =>
|
|
1000
|
+
cronTasks: { get: () => cronTasks },
|
|
1022
1001
|
providerOptions: { get: () => providerOptions },
|
|
1023
1002
|
assistantOptions: { get: () => assistantOptions },
|
|
1024
1003
|
resumeOptions: { get: () => resumeOptions },
|
|
@@ -1225,10 +1204,12 @@ async function runChat(projectRoot) {
|
|
|
1225
1204
|
restartDaemon,
|
|
1226
1205
|
send,
|
|
1227
1206
|
requestStatus,
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1207
|
+
requestCron: (payload = {}) => {
|
|
1208
|
+
send({
|
|
1209
|
+
type: IPC_REQUEST_TYPES.CRON,
|
|
1210
|
+
...payload,
|
|
1211
|
+
});
|
|
1212
|
+
},
|
|
1232
1213
|
activateAgent: async (target) => {
|
|
1233
1214
|
const activator = new AgentActivator(projectRoot);
|
|
1234
1215
|
await activator.activate(target);
|
|
@@ -65,7 +65,7 @@ async function runOnlineToken(subscriber, opts = {}) {
|
|
|
65
65
|
const { generateToken, setToken, defaultTokensPath } = require("../online/tokens");
|
|
66
66
|
const filePath = opts.file || defaultTokensPath();
|
|
67
67
|
const token = generateToken();
|
|
68
|
-
const entry = setToken(filePath, subscriber, token, opts.server || "", {
|
|
68
|
+
const entry = setToken(filePath, subscriber, token, opts.server || "https://online.ufoo.dev", {
|
|
69
69
|
nickname: opts.nickname || "",
|
|
70
70
|
});
|
|
71
71
|
console.log(JSON.stringify({
|
|
@@ -79,7 +79,7 @@ async function runOnlineToken(subscriber, opts = {}) {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
async function runOnlineRoom(action, opts = {}, onlineAuthHeaders) {
|
|
82
|
-
const base = opts.server || "
|
|
82
|
+
const base = opts.server || "https://online.ufoo.dev";
|
|
83
83
|
const endpoint = `${base.replace(/\/$/, "")}/ufoo/online/rooms`;
|
|
84
84
|
const authHeaders = buildAuthHeaders(onlineAuthHeaders, {
|
|
85
85
|
authToken: opts.authToken,
|
|
@@ -116,7 +116,7 @@ async function runOnlineRoom(action, opts = {}, onlineAuthHeaders) {
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
async function runOnlineChannel(action, opts = {}, onlineAuthHeaders) {
|
|
119
|
-
const base = opts.server || "
|
|
119
|
+
const base = opts.server || "https://online.ufoo.dev";
|
|
120
120
|
const endpoint = `${base.replace(/\/$/, "")}/ufoo/online/channels`;
|
|
121
121
|
const authHeaders = buildAuthHeaders(onlineAuthHeaders, {
|
|
122
122
|
authToken: opts.authToken,
|
|
@@ -160,7 +160,7 @@ async function runOnlineConnect(opts = {}) {
|
|
|
160
160
|
projectRoot: opts.projectRoot || process.cwd(),
|
|
161
161
|
nickname: opts.nickname,
|
|
162
162
|
subscriberId: opts.subscriber || "",
|
|
163
|
-
url: opts.url || "
|
|
163
|
+
url: opts.url || "wss://online.ufoo.dev/ufoo/online",
|
|
164
164
|
token: opts.token || "",
|
|
165
165
|
tokenHash: opts.tokenHash || "",
|
|
166
166
|
tokenFile: opts.tokenFile || "",
|
|
@@ -220,12 +220,12 @@ async function runOnlineCommand(subcmd, payload = {}, options = {}) {
|
|
|
220
220
|
case "token":
|
|
221
221
|
return runOnlineToken(payload.subscriber, {
|
|
222
222
|
nickname: opts.nickname || "",
|
|
223
|
-
server: opts.server || "",
|
|
223
|
+
server: opts.server || "https://online.ufoo.dev",
|
|
224
224
|
file: opts.file || "",
|
|
225
225
|
});
|
|
226
226
|
case "room":
|
|
227
227
|
return runOnlineRoom(payload.action, {
|
|
228
|
-
server: opts.server || "
|
|
228
|
+
server: opts.server || "https://online.ufoo.dev",
|
|
229
229
|
authToken: opts.authToken || "",
|
|
230
230
|
tokenFile: opts.tokenFile || "",
|
|
231
231
|
subscriber: opts.subscriber || "",
|
|
@@ -236,7 +236,7 @@ async function runOnlineCommand(subcmd, payload = {}, options = {}) {
|
|
|
236
236
|
}, onlineAuthHeaders);
|
|
237
237
|
case "channel":
|
|
238
238
|
return runOnlineChannel(payload.action, {
|
|
239
|
-
server: opts.server || "
|
|
239
|
+
server: opts.server || "https://online.ufoo.dev",
|
|
240
240
|
authToken: opts.authToken || "",
|
|
241
241
|
tokenFile: opts.tokenFile || "",
|
|
242
242
|
subscriber: opts.subscriber || "",
|
|
@@ -297,14 +297,14 @@ async function runOnlineCommand(subcmd, payload = {}, options = {}) {
|
|
|
297
297
|
const subscriber = argv[1];
|
|
298
298
|
return runOnlineToken(subscriber, {
|
|
299
299
|
nickname: getFallbackOpt(argv, "--nickname"),
|
|
300
|
-
server: getFallbackOpt(argv, "--server"),
|
|
300
|
+
server: getFallbackOpt(argv, "--server") || "https://online.ufoo.dev",
|
|
301
301
|
file: getFallbackOpt(argv, "--file"),
|
|
302
302
|
});
|
|
303
303
|
}
|
|
304
304
|
case "room": {
|
|
305
305
|
const action = argv[1] || "";
|
|
306
306
|
return runOnlineRoom(action, {
|
|
307
|
-
server: getFallbackOpt(argv, "--server") || "
|
|
307
|
+
server: getFallbackOpt(argv, "--server") || "https://online.ufoo.dev",
|
|
308
308
|
authToken: getFallbackOpt(argv, "--auth-token"),
|
|
309
309
|
tokenFile: getFallbackOpt(argv, "--token-file"),
|
|
310
310
|
subscriber: getFallbackOpt(argv, "--subscriber"),
|
|
@@ -318,7 +318,7 @@ async function runOnlineCommand(subcmd, payload = {}, options = {}) {
|
|
|
318
318
|
const action = argv[1] || "";
|
|
319
319
|
const defaultChannelType = options.defaultChannelType || "public";
|
|
320
320
|
return runOnlineChannel(action, {
|
|
321
|
-
server: getFallbackOpt(argv, "--server") || "
|
|
321
|
+
server: getFallbackOpt(argv, "--server") || "https://online.ufoo.dev",
|
|
322
322
|
authToken: getFallbackOpt(argv, "--auth-token"),
|
|
323
323
|
tokenFile: getFallbackOpt(argv, "--token-file"),
|
|
324
324
|
subscriber: getFallbackOpt(argv, "--subscriber"),
|
|
@@ -339,7 +339,7 @@ async function runOnlineCommand(subcmd, payload = {}, options = {}) {
|
|
|
339
339
|
projectRoot: options.projectRoot || process.cwd(),
|
|
340
340
|
nickname: getFallbackOpt(argv, "--nickname"),
|
|
341
341
|
subscriber: getFallbackOpt(argv, "--subscriber"),
|
|
342
|
-
url: getFallbackOpt(argv, "--url") || "
|
|
342
|
+
url: getFallbackOpt(argv, "--url") || "wss://online.ufoo.dev/ufoo/online",
|
|
343
343
|
token: getFallbackOpt(argv, "--token"),
|
|
344
344
|
tokenHash: getFallbackOpt(argv, "--token-hash"),
|
|
345
345
|
tokenFile: getFallbackOpt(argv, "--token-file"),
|
package/src/cli.js
CHANGED
|
@@ -247,6 +247,45 @@ async function runCli(argv) {
|
|
|
247
247
|
const repoRoot = getPackageRoot();
|
|
248
248
|
run(process.execPath, [path.join(repoRoot, "bin", "ufoo.js"), "chat"]);
|
|
249
249
|
});
|
|
250
|
+
program
|
|
251
|
+
.command("launch")
|
|
252
|
+
.description("Launch an agent (ucode, uclaude, ucodex)")
|
|
253
|
+
.argument("<agent>", "Agent type: ucode|uclaude|ucodex|claude|codex")
|
|
254
|
+
.argument("[nickname]", "Optional nickname for the agent")
|
|
255
|
+
.action(async (agent, nickname) => {
|
|
256
|
+
try {
|
|
257
|
+
const projectRoot = process.cwd();
|
|
258
|
+
await ensureDaemonRunning(projectRoot);
|
|
259
|
+
|
|
260
|
+
// Normalize agent type
|
|
261
|
+
const agentLower = agent.toLowerCase();
|
|
262
|
+
let normalizedAgent = "";
|
|
263
|
+
if (agentLower === "ucode" || agentLower === "ufoo-code" || agentLower === "ufoo") {
|
|
264
|
+
normalizedAgent = "ucode";
|
|
265
|
+
} else if (agentLower === "uclaude" || agentLower === "claude-code" || agentLower === "claude") {
|
|
266
|
+
normalizedAgent = "claude";
|
|
267
|
+
} else if (agentLower === "ucodex" || agentLower === "codex" || agentLower === "openai") {
|
|
268
|
+
normalizedAgent = "codex";
|
|
269
|
+
} else {
|
|
270
|
+
console.error(`Unknown agent type: ${agent}`);
|
|
271
|
+
console.error("Valid types: ucode, uclaude, ucodex, claude, codex");
|
|
272
|
+
process.exitCode = 1;
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const resp = await sendDaemonRequest(projectRoot, {
|
|
277
|
+
type: "launch_agent",
|
|
278
|
+
agent: normalizedAgent,
|
|
279
|
+
nickname: nickname || "",
|
|
280
|
+
count: 1,
|
|
281
|
+
});
|
|
282
|
+
const reply = resp?.data?.reply || `Launching ${normalizedAgent} agent...`;
|
|
283
|
+
console.log(reply);
|
|
284
|
+
} catch (err) {
|
|
285
|
+
console.error(err.message || String(err));
|
|
286
|
+
process.exitCode = 1;
|
|
287
|
+
}
|
|
288
|
+
});
|
|
250
289
|
program
|
|
251
290
|
.command("resume")
|
|
252
291
|
.description("Resume agent sessions (optional nickname)")
|
|
@@ -550,7 +589,7 @@ async function runCli(argv) {
|
|
|
550
589
|
.description("Generate and store a ufoo-online token")
|
|
551
590
|
.argument("<subscriber>", "Subscriber ID (e.g., claude-code:abc123)")
|
|
552
591
|
.option("--nickname <name>", "Nickname for this agent")
|
|
553
|
-
.option("--server <url>", "Online server URL")
|
|
592
|
+
.option("--server <url>", "Online server URL", "https://online.ufoo.dev")
|
|
554
593
|
.option("--file <path>", "Tokens file path")
|
|
555
594
|
.action(async (subscriber, opts) => {
|
|
556
595
|
try {
|
|
@@ -569,7 +608,7 @@ async function runCli(argv) {
|
|
|
569
608
|
.command("room")
|
|
570
609
|
.description("Manage online rooms (HTTP)")
|
|
571
610
|
.argument("<action>", "create|list")
|
|
572
|
-
.option("--server <url>", "Online server base URL (
|
|
611
|
+
.option("--server <url>", "Online server base URL (default: https://online.ufoo.dev)")
|
|
573
612
|
.option("--auth-token <token>", "Bearer token for HTTP auth (token or token_hash)")
|
|
574
613
|
.option("--token-file <path>", "Token file path for auth lookup")
|
|
575
614
|
.option("--subscriber <id>", "Subscriber ID to resolve token")
|
|
@@ -594,7 +633,7 @@ async function runCli(argv) {
|
|
|
594
633
|
.command("channel")
|
|
595
634
|
.description("Manage online channels (HTTP)")
|
|
596
635
|
.argument("<action>", "create|list")
|
|
597
|
-
.option("--server <url>", "Online server base URL (
|
|
636
|
+
.option("--server <url>", "Online server base URL (default: https://online.ufoo.dev)")
|
|
598
637
|
.option("--auth-token <token>", "Bearer token for HTTP auth (token or token_hash)")
|
|
599
638
|
.option("--token-file <path>", "Token file path for auth lookup")
|
|
600
639
|
.option("--subscriber <id>", "Subscriber ID to resolve token")
|
|
@@ -618,7 +657,7 @@ async function runCli(argv) {
|
|
|
618
657
|
.command("connect")
|
|
619
658
|
.description("Connect to ufoo-online relay (long-running)")
|
|
620
659
|
.requiredOption("--nickname <name>", "Agent nickname")
|
|
621
|
-
.option("--url <url>", "WebSocket URL", "
|
|
660
|
+
.option("--url <url>", "WebSocket URL", "wss://online.ufoo.dev/ufoo/online")
|
|
622
661
|
.option("--subscriber <id>", "Subscriber ID (auto-generated if omitted)")
|
|
623
662
|
.option("--token <tok>", "Auth token")
|
|
624
663
|
.option("--token-hash <hash>", "Auth token hash")
|
package/src/code/agent.js
CHANGED
|
@@ -43,6 +43,14 @@ function normalizeLine(input = "") {
|
|
|
43
43
|
return String(input || "").trim();
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
function parseProbeMarkerCommand(input = "") {
|
|
47
|
+
const text = String(input || "").trim();
|
|
48
|
+
if (!text) return "";
|
|
49
|
+
// Accept only strict probe markers: "<prefix> <single-marker-token>".
|
|
50
|
+
const match = text.match(/^(?:\$ufoo|\/ufoo|ufoo)\s+([A-Za-z0-9][A-Za-z0-9._:-]{0,63})$/);
|
|
51
|
+
return match ? String(match[1] || "").trim() : "";
|
|
52
|
+
}
|
|
53
|
+
|
|
46
54
|
function parseJson(text = "") {
|
|
47
55
|
const raw = String(text || "").trim();
|
|
48
56
|
if (!raw) return {};
|
|
@@ -1209,16 +1217,18 @@ function runSingleCommand(line = "", workspaceRoot = process.cwd()) {
|
|
|
1209
1217
|
" help",
|
|
1210
1218
|
" exit|quit",
|
|
1211
1219
|
" ubus|/ubus",
|
|
1220
|
+
" bg|/bg <task>",
|
|
1212
1221
|
" resume <session-id>",
|
|
1213
1222
|
" tool <read|write|edit|bash> <args-json>",
|
|
1214
1223
|
" run <read|write|edit|bash> <args-json>",
|
|
1215
1224
|
].join("\n"),
|
|
1216
1225
|
};
|
|
1217
1226
|
}
|
|
1218
|
-
|
|
1227
|
+
const probeMarker = parseProbeMarkerCommand(text);
|
|
1228
|
+
if (probeMarker) {
|
|
1219
1229
|
return {
|
|
1220
1230
|
kind: "probe",
|
|
1221
|
-
|
|
1231
|
+
marker: probeMarker,
|
|
1222
1232
|
};
|
|
1223
1233
|
}
|
|
1224
1234
|
if (text === "ubus" || text === "/ubus") {
|
|
@@ -1226,6 +1236,26 @@ function runSingleCommand(line = "", workspaceRoot = process.cwd()) {
|
|
|
1226
1236
|
kind: "ubus",
|
|
1227
1237
|
};
|
|
1228
1238
|
}
|
|
1239
|
+
if (text === "bg" || text === "/bg") {
|
|
1240
|
+
return {
|
|
1241
|
+
kind: "error",
|
|
1242
|
+
output: "usage: bg <task>",
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
const bgMatch = text.match(/^(?:\/bg|bg)\s+(.+)$/i);
|
|
1246
|
+
if (bgMatch) {
|
|
1247
|
+
const task = String(bgMatch[1] || "").trim();
|
|
1248
|
+
if (!task) {
|
|
1249
|
+
return {
|
|
1250
|
+
kind: "error",
|
|
1251
|
+
output: "usage: bg <task>",
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
return {
|
|
1255
|
+
kind: "nl_bg",
|
|
1256
|
+
task,
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1229
1259
|
const resumeMatch = text.match(/^resume(?:\s+(.+))?$/i);
|
|
1230
1260
|
if (resumeMatch) {
|
|
1231
1261
|
const session = String(resumeMatch[1] || "").trim();
|
|
@@ -1345,6 +1375,8 @@ async function runUcodeCoreAgent({
|
|
|
1345
1375
|
});
|
|
1346
1376
|
return new Promise((resolve) => {
|
|
1347
1377
|
let chain = Promise.resolve();
|
|
1378
|
+
let backgroundSeq = 0;
|
|
1379
|
+
const backgroundRuns = new Map();
|
|
1348
1380
|
const subscriberId = String(process.env.UFOO_SUBSCRIBER_ID || "").trim();
|
|
1349
1381
|
const autoBusEnabled = shouldAutoConsumeBus(subscriberId);
|
|
1350
1382
|
let autoBusTimer = null;
|
|
@@ -1400,6 +1432,38 @@ async function runUcodeCoreAgent({
|
|
|
1400
1432
|
scheduleAutoBus();
|
|
1401
1433
|
}
|
|
1402
1434
|
|
|
1435
|
+
const startBackgroundTask = (task = "") => {
|
|
1436
|
+
backgroundSeq += 1;
|
|
1437
|
+
const jobId = `bg-${Date.now().toString(36)}-${backgroundSeq.toString(36)}`;
|
|
1438
|
+
const bgState = {
|
|
1439
|
+
workspaceRoot: state.workspaceRoot,
|
|
1440
|
+
provider: state.provider,
|
|
1441
|
+
model: state.model,
|
|
1442
|
+
engine: state.engine,
|
|
1443
|
+
context: state.context,
|
|
1444
|
+
nlMessages: Array.isArray(state.nlMessages) ? state.nlMessages.slice() : [],
|
|
1445
|
+
sessionId: "",
|
|
1446
|
+
timeoutMs: state.timeoutMs,
|
|
1447
|
+
jsonOutput: false,
|
|
1448
|
+
};
|
|
1449
|
+
const run = runNaturalLanguageTask(task, bgState)
|
|
1450
|
+
.then((nlResult) => {
|
|
1451
|
+
const summary = String(formatNlResult(nlResult, false) || "").trim();
|
|
1452
|
+
const title = nlResult && nlResult.ok ? "done" : "failed";
|
|
1453
|
+
stdout.write(`[${jobId}] ${title}: ${summary || "no summary"}\n`);
|
|
1454
|
+
printPrompt();
|
|
1455
|
+
})
|
|
1456
|
+
.catch((err) => {
|
|
1457
|
+
stdout.write(`[${jobId}] failed: ${err && err.message ? err.message : "background task failed"}\n`);
|
|
1458
|
+
printPrompt();
|
|
1459
|
+
})
|
|
1460
|
+
.finally(() => {
|
|
1461
|
+
backgroundRuns.delete(jobId);
|
|
1462
|
+
});
|
|
1463
|
+
backgroundRuns.set(jobId, run);
|
|
1464
|
+
return jobId;
|
|
1465
|
+
};
|
|
1466
|
+
|
|
1403
1467
|
const handleLine = async (line) => {
|
|
1404
1468
|
const runtimeWorkspace = String(state.workspaceRoot || workspaceRoot || process.cwd());
|
|
1405
1469
|
const result = runSingleCommand(line, runtimeWorkspace);
|
|
@@ -1407,7 +1471,10 @@ async function runUcodeCoreAgent({
|
|
|
1407
1471
|
rl.close();
|
|
1408
1472
|
return;
|
|
1409
1473
|
}
|
|
1410
|
-
if (result.kind === "
|
|
1474
|
+
if (result.kind === "probe") {
|
|
1475
|
+
return;
|
|
1476
|
+
}
|
|
1477
|
+
if (result.kind === "help" || result.kind === "tool" || result.kind === "error") {
|
|
1411
1478
|
stdout.write(`${result.output}\n`);
|
|
1412
1479
|
}
|
|
1413
1480
|
if (result.kind === "ubus") {
|
|
@@ -1442,6 +1509,10 @@ async function runUcodeCoreAgent({
|
|
|
1442
1509
|
stdout.write(`Resumed session ${resumed.sessionId} (${resumed.restoredMessages} messages).\n`);
|
|
1443
1510
|
}
|
|
1444
1511
|
}
|
|
1512
|
+
if (result.kind === "nl_bg") {
|
|
1513
|
+
const jobId = startBackgroundTask(result.task);
|
|
1514
|
+
stdout.write(`[${jobId}] started in background.\n`);
|
|
1515
|
+
}
|
|
1445
1516
|
if (result.kind === "nl") {
|
|
1446
1517
|
let streamBuffer = null;
|
|
1447
1518
|
let streamedVisible = false;
|
package/src/code/tui.js
CHANGED
|
@@ -468,7 +468,7 @@ function runUcodeTui({
|
|
|
468
468
|
} = {}) {
|
|
469
469
|
return new Promise((resolve) => {
|
|
470
470
|
const blessed = require("blessed");
|
|
471
|
-
const {
|
|
471
|
+
const { execFileSync } = require("child_process");
|
|
472
472
|
const { createChatLayout } = require("../chat/layout");
|
|
473
473
|
const { computeDashboardContent } = require("../chat/dashboardView");
|
|
474
474
|
const { escapeBlessed, stripBlessedTags } = require("../chat/text");
|
|
@@ -490,6 +490,8 @@ function runUcodeTui({
|
|
|
490
490
|
let agentListWindowStart = 0;
|
|
491
491
|
let agentSelectionMode = false;
|
|
492
492
|
let pendingTask = null;
|
|
493
|
+
const backgroundTasks = new Map();
|
|
494
|
+
let backgroundSeq = 0;
|
|
493
495
|
const logRenderState = { inCodeBlock: false };
|
|
494
496
|
const inputHistory = [];
|
|
495
497
|
let historyIndex = -1;
|
|
@@ -900,12 +902,27 @@ function runUcodeTui({
|
|
|
900
902
|
};
|
|
901
903
|
|
|
902
904
|
const updateStatus = (message = "", type = "thinking", options = {}) => {
|
|
905
|
+
const getBackgroundSuffix = () => {
|
|
906
|
+
if (!backgroundTasks || backgroundTasks.size === 0) return "";
|
|
907
|
+
let running = 0;
|
|
908
|
+
let done = 0;
|
|
909
|
+
let failed = 0;
|
|
910
|
+
for (const task of backgroundTasks.values()) {
|
|
911
|
+
const status = String(task && task.status || "").trim().toLowerCase();
|
|
912
|
+
if (status === "running") running += 1;
|
|
913
|
+
else if (status === "done") done += 1;
|
|
914
|
+
else if (status === "failed") failed += 1;
|
|
915
|
+
}
|
|
916
|
+
const total = running + done + failed;
|
|
917
|
+
if (total <= 0) return "";
|
|
918
|
+
return ` · BG ${running}/${done}/${failed}`;
|
|
919
|
+
};
|
|
903
920
|
if (statusInterval) {
|
|
904
921
|
clearInterval(statusInterval);
|
|
905
922
|
statusInterval = null;
|
|
906
923
|
}
|
|
907
924
|
if (!message) {
|
|
908
|
-
statusLine.setContent(
|
|
925
|
+
statusLine.setContent(escapeBlessed(`UCODE · Ready${getBackgroundSuffix()}`));
|
|
909
926
|
screen.render();
|
|
910
927
|
return;
|
|
911
928
|
}
|
|
@@ -918,7 +935,7 @@ function runUcodeTui({
|
|
|
918
935
|
const timerText = showTimer
|
|
919
936
|
? ` (${formatPendingElapsed(Date.now() - startedAt)},esc cancel)`
|
|
920
937
|
: "";
|
|
921
|
-
statusLine.setContent(escapeBlessed(`${indicator} ${message}${timerText}`));
|
|
938
|
+
statusLine.setContent(escapeBlessed(`${indicator} ${message}${timerText}${getBackgroundSuffix()}`));
|
|
922
939
|
statusIndex += 1;
|
|
923
940
|
screen.render();
|
|
924
941
|
};
|
|
@@ -960,39 +977,63 @@ function runUcodeTui({
|
|
|
960
977
|
autoBusError = "";
|
|
961
978
|
return;
|
|
962
979
|
}
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
980
|
+
|
|
981
|
+
// Set pending state for autoBus tasks
|
|
982
|
+
const abortController = new AbortController();
|
|
983
|
+
pendingTask = {
|
|
984
|
+
abortController,
|
|
985
|
+
startedAt: Date.now(),
|
|
986
|
+
};
|
|
987
|
+
updateStatus("Processing bus messages...", "thinking", {
|
|
988
|
+
showTimer: true,
|
|
989
|
+
startedAt: pendingTask.startedAt,
|
|
972
990
|
});
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
991
|
+
|
|
992
|
+
try {
|
|
993
|
+
const ubusResult = await runUbusCommand(state, {
|
|
994
|
+
workspaceRoot,
|
|
995
|
+
subscriberId: autoBusSubscriberId,
|
|
996
|
+
onMessageReceived: (msg) => {
|
|
997
|
+
// Display the incoming message immediately
|
|
998
|
+
const { extractAgentNickname } = require("./agent");
|
|
999
|
+
const nickname = extractAgentNickname(msg.from) || msg.from;
|
|
1000
|
+
logText(`${nickname}: ${msg.task}`);
|
|
1001
|
+
// Update status to show we're working on this specific task
|
|
1002
|
+
updateStatus("Working on task...", "thinking", {
|
|
1003
|
+
showTimer: true,
|
|
1004
|
+
startedAt: pendingTask.startedAt,
|
|
1005
|
+
});
|
|
1006
|
+
},
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
if (!ubusResult.ok) {
|
|
1010
|
+
const nextError = String(ubusResult.error || "ubus failed");
|
|
1011
|
+
if (nextError !== autoBusError) {
|
|
1012
|
+
autoBusError = nextError;
|
|
1013
|
+
logText(`Error: ${nextError}`);
|
|
990
1014
|
}
|
|
1015
|
+
return;
|
|
991
1016
|
}
|
|
992
|
-
|
|
993
|
-
if (
|
|
994
|
-
|
|
1017
|
+
autoBusError = "";
|
|
1018
|
+
if (ubusResult.handled > 0) {
|
|
1019
|
+
// Display only the replies (tasks were already shown via onMessageReceived)
|
|
1020
|
+
if (ubusResult.messageExchanges && ubusResult.messageExchanges.length > 0) {
|
|
1021
|
+
const { extractAgentNickname } = require("./agent");
|
|
1022
|
+
for (const exchange of ubusResult.messageExchanges) {
|
|
1023
|
+
const nickname = extractAgentNickname(exchange.from) || exchange.from;
|
|
1024
|
+
// Only show the reply since task was already displayed
|
|
1025
|
+
logText(`@${nickname} ${exchange.reply}`);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
const persisted = persistSessionState(state);
|
|
1029
|
+
if (!persisted || persisted.ok === false) {
|
|
1030
|
+
logText(`Error: failed to persist session ${state.sessionId}: ${(persisted && persisted.error) || "unknown error"}`);
|
|
1031
|
+
}
|
|
995
1032
|
}
|
|
1033
|
+
} finally {
|
|
1034
|
+
// Clear pending state
|
|
1035
|
+
pendingTask = null;
|
|
1036
|
+
updateStatus("", "none");
|
|
996
1037
|
}
|
|
997
1038
|
};
|
|
998
1039
|
|
|
@@ -1058,7 +1099,7 @@ function runUcodeTui({
|
|
|
1058
1099
|
if (isBusMessage && targetAgent) {
|
|
1059
1100
|
updateStatus("Sending message...", "typing");
|
|
1060
1101
|
try {
|
|
1061
|
-
|
|
1102
|
+
execFileSync("ufoo", ["bus", "send", targetAgent, actualLine], {
|
|
1062
1103
|
cwd: workspaceRoot,
|
|
1063
1104
|
encoding: "utf8",
|
|
1064
1105
|
});
|
|
@@ -1094,7 +1135,10 @@ function runUcodeTui({
|
|
|
1094
1135
|
}, payload);
|
|
1095
1136
|
return;
|
|
1096
1137
|
}
|
|
1097
|
-
if (result.kind === "
|
|
1138
|
+
if (result.kind === "probe") {
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
if (result.kind === "help" || result.kind === "error") {
|
|
1098
1142
|
logText(result.output || "");
|
|
1099
1143
|
return;
|
|
1100
1144
|
}
|
|
@@ -1142,6 +1186,54 @@ function runUcodeTui({
|
|
|
1142
1186
|
return;
|
|
1143
1187
|
}
|
|
1144
1188
|
|
|
1189
|
+
if (result.kind === "nl_bg") {
|
|
1190
|
+
backgroundSeq += 1;
|
|
1191
|
+
const jobId = `bg-${Date.now().toString(36)}-${backgroundSeq.toString(36)}`;
|
|
1192
|
+
const taskRecord = {
|
|
1193
|
+
id: jobId,
|
|
1194
|
+
task: result.task,
|
|
1195
|
+
status: "running",
|
|
1196
|
+
startedAt: Date.now(),
|
|
1197
|
+
summary: "",
|
|
1198
|
+
};
|
|
1199
|
+
backgroundTasks.set(jobId, taskRecord);
|
|
1200
|
+
updateStatus("", "none");
|
|
1201
|
+
logText(`[${jobId}] started in background.`);
|
|
1202
|
+
|
|
1203
|
+
const bgState = {
|
|
1204
|
+
workspaceRoot: state.workspaceRoot,
|
|
1205
|
+
provider: state.provider,
|
|
1206
|
+
model: state.model,
|
|
1207
|
+
engine: state.engine,
|
|
1208
|
+
context: state.context,
|
|
1209
|
+
nlMessages: Array.isArray(state.nlMessages) ? state.nlMessages.slice() : [],
|
|
1210
|
+
sessionId: "",
|
|
1211
|
+
timeoutMs: state.timeoutMs,
|
|
1212
|
+
jsonOutput: false,
|
|
1213
|
+
};
|
|
1214
|
+
|
|
1215
|
+
Promise.resolve()
|
|
1216
|
+
.then(() => runNaturalLanguageTask(result.task, bgState))
|
|
1217
|
+
.then((nlResult) => {
|
|
1218
|
+
taskRecord.status = nlResult && nlResult.ok ? "done" : "failed";
|
|
1219
|
+
taskRecord.finishedAt = Date.now();
|
|
1220
|
+
taskRecord.summary = String(formatNlResult(nlResult, false) || "").trim();
|
|
1221
|
+
const title = taskRecord.status === "done" ? "done" : "failed";
|
|
1222
|
+
logText(`[${jobId}] ${title}: ${taskRecord.summary || "no summary"}`);
|
|
1223
|
+
})
|
|
1224
|
+
.catch((err) => {
|
|
1225
|
+
taskRecord.status = "failed";
|
|
1226
|
+
taskRecord.finishedAt = Date.now();
|
|
1227
|
+
taskRecord.summary = err && err.message ? String(err.message) : "background task failed";
|
|
1228
|
+
logText(`[${jobId}] failed: ${taskRecord.summary}`);
|
|
1229
|
+
})
|
|
1230
|
+
.finally(() => {
|
|
1231
|
+
updateStatus("", "none");
|
|
1232
|
+
screen.render();
|
|
1233
|
+
});
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1145
1237
|
if (result.kind === "nl") {
|
|
1146
1238
|
const statusMessages = [
|
|
1147
1239
|
"Thinking...",
|