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/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 cronScheduler = {
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: cronScheduler.listTasks(),
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: () => cronScheduler.listTasks() },
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
- createCronTask: ({ intervalMs, targets, prompt }) =>
1229
- cronScheduler.addTask({ intervalMs, targets, prompt }),
1230
- listCronTasks: () => cronScheduler.listTasks(),
1231
- stopCronTask: (id) => cronScheduler.stopTask(id),
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 || "http://127.0.0.1:8787";
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 || "http://127.0.0.1:8787";
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 || "ws://127.0.0.1:8787/ufoo/online",
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 || "http://127.0.0.1:8787",
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 || "http://127.0.0.1:8787",
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") || "http://127.0.0.1:8787",
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") || "http://127.0.0.1:8787",
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") || "ws://127.0.0.1:8787/ufoo/online",
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 (http://host:port)")
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 (http://host:port)")
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", "ws://127.0.0.1:8787/ufoo/online")
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
- if (text.startsWith("$ufoo ") || text.startsWith("/ufoo ") || text.startsWith("ufoo ")) {
1227
+ const probeMarker = parseProbeMarkerCommand(text);
1228
+ if (probeMarker) {
1219
1229
  return {
1220
1230
  kind: "probe",
1221
- output: text.split(/\s+/).slice(1).join(" ").trim(),
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 === "help" || result.kind === "probe" || result.kind === "tool" || result.kind === "error") {
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 { execSync } = require("child_process");
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("{bold}UCODE{/bold} · Ready");
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
- const ubusResult = await runUbusCommand(state, {
964
- workspaceRoot,
965
- subscriberId: autoBusSubscriberId,
966
- onMessageReceived: (msg) => {
967
- // Display the incoming message immediately
968
- const { extractAgentNickname } = require("./agent");
969
- const nickname = extractAgentNickname(msg.from) || msg.from;
970
- logText(`${nickname}: ${msg.task}`);
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
- if (!ubusResult.ok) {
974
- const nextError = String(ubusResult.error || "ubus failed");
975
- if (nextError !== autoBusError) {
976
- autoBusError = nextError;
977
- logText(`Error: ${nextError}`);
978
- }
979
- return;
980
- }
981
- autoBusError = "";
982
- if (ubusResult.handled > 0) {
983
- // Display only the replies (tasks were already shown via onMessageReceived)
984
- if (ubusResult.messageExchanges && ubusResult.messageExchanges.length > 0) {
985
- const { extractAgentNickname } = require("./agent");
986
- for (const exchange of ubusResult.messageExchanges) {
987
- const nickname = extractAgentNickname(exchange.from) || exchange.from;
988
- // Only show the reply since task was already displayed
989
- logText(`@${nickname} ${exchange.reply}`);
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
- const persisted = persistSessionState(state);
993
- if (!persisted || persisted.ok === false) {
994
- logText(`Error: failed to persist session ${state.sessionId}: ${(persisted && persisted.error) || "unknown error"}`);
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
- execSync(`ufoo bus send "${targetAgent}" "${actualLine.replace(/"/g, '\\"')}"`, {
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 === "help" || result.kind === "probe" || result.kind === "error") {
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...",