u-foo 1.2.14 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/cli.js CHANGED
@@ -6,6 +6,7 @@ const { socketPath, isRunning } = require("./daemon");
6
6
  const { runBusCoreCommand } = require("./cli/busCoreCommands");
7
7
  const { runCtxCommand } = require("./cli/ctxCoreCommands");
8
8
  const { runOnlineCommand } = require("./cli/onlineCoreCommands");
9
+ const { runGroupCoreCommand } = require("./cli/groupCoreCommands");
9
10
 
10
11
  function getPackageRoot() {
11
12
  return path.resolve(__dirname, "..");
@@ -561,6 +562,189 @@ async function runCli(argv) {
561
562
  }
562
563
  });
563
564
 
565
+ const group = program.command("group").description("Agent group template commands");
566
+ group
567
+ .command("templates")
568
+ .description("List available group templates")
569
+ .argument("[action]", "list", "list")
570
+ .option("--json", "Output as JSON")
571
+ .action(async (action, opts) => {
572
+ const normalizedAction = String(action || "list").trim().toLowerCase();
573
+ if (normalizedAction !== "list" && normalizedAction !== "ls") {
574
+ console.error(`Unknown group templates action: ${normalizedAction}`);
575
+ process.exitCode = 1;
576
+ return;
577
+ }
578
+ try {
579
+ await runGroupCoreCommand("templates", [normalizedAction], {
580
+ cwd: process.cwd(),
581
+ json: opts.json,
582
+ });
583
+ } catch (err) {
584
+ console.error(err.message || String(err));
585
+ process.exitCode = 1;
586
+ }
587
+ });
588
+
589
+ group
590
+ .command("template")
591
+ .description("Group template operations")
592
+ .argument("<action>", "list|show|validate|new")
593
+ .argument("[target]", "Template alias (or path for validate)")
594
+ .option("--from <alias>", "Builtin template alias (for template new)")
595
+ .option("--global", "Create template in ~/.ufoo/templates/groups")
596
+ .option("--project", "Create template in .ufoo/templates/groups (default)")
597
+ .option("--force", "Overwrite existing file (for template new)")
598
+ .option("--json", "Output as JSON")
599
+ .action(async (action, target, opts) => {
600
+ const args = [action];
601
+ if (target) args.push(target);
602
+ if (opts.from) args.push("--from", opts.from);
603
+ if (opts.global) args.push("--global");
604
+ if (opts.project) args.push("--project");
605
+ if (opts.force) args.push("--force");
606
+ if (opts.json) args.push("--json");
607
+
608
+ try {
609
+ await runGroupCoreCommand("template", args, {
610
+ cwd: process.cwd(),
611
+ json: opts.json,
612
+ });
613
+ } catch (err) {
614
+ console.error(err.message || String(err));
615
+ process.exitCode = 1;
616
+ }
617
+ });
618
+
619
+ group
620
+ .command("run")
621
+ .description("Launch an agent group from template")
622
+ .argument("<alias>", "Template alias")
623
+ .option("--instance <name>", "Group instance ID")
624
+ .option("--dry-run", "Validate and compile launch plan without starting agents")
625
+ .option("--json", "Output daemon response as JSON")
626
+ .action(async (alias, opts) => {
627
+ try {
628
+ const projectRoot = process.cwd();
629
+ await ensureDaemonRunning(projectRoot);
630
+ const resp = await sendDaemonRequest(projectRoot, {
631
+ type: "launch_group",
632
+ alias,
633
+ instance: opts.instance || "",
634
+ dry_run: opts.dryRun === true,
635
+ });
636
+ if (opts.json) {
637
+ console.log(JSON.stringify(resp?.data || {}, null, 2));
638
+ return;
639
+ }
640
+ const reply = resp?.data?.reply || "Group run requested";
641
+ console.log(reply);
642
+ if (resp?.data?.group?.ok === false) {
643
+ process.exitCode = 1;
644
+ }
645
+ } catch (err) {
646
+ console.error(err.message || String(err));
647
+ process.exitCode = 1;
648
+ }
649
+ });
650
+
651
+ group
652
+ .command("status")
653
+ .description("Show group runtime status (single group or list)")
654
+ .argument("[groupId]", "Group ID (optional)")
655
+ .option("--json", "Output daemon response as JSON")
656
+ .action(async (groupId, opts) => {
657
+ try {
658
+ const projectRoot = process.cwd();
659
+ await ensureDaemonRunning(projectRoot);
660
+ const resp = await sendDaemonRequest(projectRoot, {
661
+ type: "group_status",
662
+ group_id: groupId || "",
663
+ });
664
+ if (opts.json) {
665
+ console.log(JSON.stringify(resp?.data || {}, null, 2));
666
+ return;
667
+ }
668
+ const reply = resp?.data?.reply || "Group status requested";
669
+ console.log(reply);
670
+ const group = resp?.data?.group || {};
671
+ if (groupId && group?.group) {
672
+ console.log(JSON.stringify(group.group, null, 2));
673
+ } else if (!groupId && Array.isArray(group?.groups)) {
674
+ group.groups.forEach((item) => {
675
+ console.log(`- ${item.group_id} [${item.status}] ${item.template_alias} active=${item.members_active}/${item.members_total}`);
676
+ });
677
+ }
678
+ } catch (err) {
679
+ console.error(err.message || String(err));
680
+ process.exitCode = 1;
681
+ }
682
+ });
683
+
684
+ group
685
+ .command("diagram")
686
+ .description("Render group diagram from template alias or group runtime ID")
687
+ .argument("<target>", "Template alias or group ID")
688
+ .option("--ascii", "Render ASCII diagram (default)")
689
+ .option("--mermaid", "Render Mermaid flowchart")
690
+ .option("--json", "Output daemon response as JSON")
691
+ .action(async (target, opts) => {
692
+ try {
693
+ const projectRoot = process.cwd();
694
+ await ensureDaemonRunning(projectRoot);
695
+ const format = opts.mermaid ? "mermaid" : "ascii";
696
+ const resp = await sendDaemonRequest(projectRoot, {
697
+ type: "group_diagram",
698
+ alias: target,
699
+ group_id: target,
700
+ format,
701
+ });
702
+ if (opts.json) {
703
+ console.log(JSON.stringify(resp?.data || {}, null, 2));
704
+ return;
705
+ }
706
+ const reply = resp?.data?.reply || "Group diagram requested";
707
+ console.log(reply);
708
+ if (resp?.data?.group?.diagram) {
709
+ console.log(resp.data.group.diagram);
710
+ }
711
+ if (resp?.data?.group?.ok === false) {
712
+ process.exitCode = 1;
713
+ }
714
+ } catch (err) {
715
+ console.error(err.message || String(err));
716
+ process.exitCode = 1;
717
+ }
718
+ });
719
+
720
+ group
721
+ .command("stop")
722
+ .description("Stop a running group instance")
723
+ .argument("<groupId>", "Group ID")
724
+ .option("--json", "Output daemon response as JSON")
725
+ .action(async (groupId, opts) => {
726
+ try {
727
+ const projectRoot = process.cwd();
728
+ await ensureDaemonRunning(projectRoot);
729
+ const resp = await sendDaemonRequest(projectRoot, {
730
+ type: "stop_group",
731
+ group_id: groupId,
732
+ });
733
+ if (opts.json) {
734
+ console.log(JSON.stringify(resp?.data || {}, null, 2));
735
+ return;
736
+ }
737
+ const reply = resp?.data?.reply || "Group stop requested";
738
+ console.log(reply);
739
+ if (resp?.data?.group?.ok === false) {
740
+ process.exitCode = 1;
741
+ }
742
+ } catch (err) {
743
+ console.error(err.message || String(err));
744
+ process.exitCode = 1;
745
+ }
746
+ });
747
+
564
748
  const online = program.command("online").description("ufoo online helpers");
565
749
  online
566
750
  .command("server")
@@ -616,6 +800,7 @@ async function runCli(argv) {
616
800
  .option("--name <room>", "Room name (optional)")
617
801
  .option("--type <type>", "Room type (public|private)")
618
802
  .option("--password <pwd>", "Room password (private only)")
803
+ .option("--created-by <name>", "Room creator nickname metadata")
619
804
  .action(async (action, opts) => {
620
805
  try {
621
806
  await runOnlineCommand("room", { action, opts }, {
@@ -640,6 +825,7 @@ async function runCli(argv) {
640
825
  .option("--nickname <name>", "Nickname to resolve token")
641
826
  .option("--name <name>", "Channel name (unique)")
642
827
  .option("--type <type>", "Channel type (world|public)")
828
+ .option("--created-by <name>", "Channel creator nickname metadata")
643
829
  .action(async (action, opts) => {
644
830
  try {
645
831
  await runOnlineCommand("channel", { action, opts }, {
@@ -934,11 +1120,17 @@ async function runCli(argv) {
934
1120
  console.log(" ufoo init [--modules <list>] [--project <dir>]");
935
1121
  console.log(" ufoo skills list");
936
1122
  console.log(" ufoo skills install <name|all> [--target <dir> | --codex | --agents]");
1123
+ console.log(" ufoo group templates [list|ls] [--json]");
1124
+ console.log(" ufoo group template <list|show|validate|new> [target] [--from <builtin>] [--global] [--force] [--json]");
1125
+ console.log(" ufoo group run <alias> [--instance <name>] [--dry-run] [--json]");
1126
+ console.log(" ufoo group status [groupId] [--json]");
1127
+ console.log(" ufoo group diagram <alias|groupId> [--ascii|--mermaid] [--json]");
1128
+ console.log(" ufoo group stop <groupId> [--json]");
937
1129
  console.log(" ufoo online server [--port 8787] [--host 127.0.0.1] [--token-file <path>]");
938
1130
  console.log(" ufoo online token <subscriber> [--nickname <name>] [--server <url>] [--file <path>]");
939
- console.log(" ufoo online room create [--name <room>] --type public|private [--password <pwd>] [--server <url>]");
1131
+ console.log(" ufoo online room create [--name <room>] --type public|private [--password <pwd>] [--created-by <name>] [--server <url>]");
940
1132
  console.log(" ufoo online room list [--server <url>]");
941
- console.log(" ufoo online channel create --name <name> [--type world|public] [--server <url>]");
1133
+ console.log(" ufoo online channel create --name <name> [--type world|public] [--created-by <name>] [--server <url>]");
942
1134
  console.log(" ufoo online channel list [--server <url>]");
943
1135
  console.log(" ufoo online connect --nickname <name> [--join <ch>] [--room <id> --room-password <pwd>] [...]");
944
1136
  console.log(" ufoo online send --nickname <name> --text <msg> [--channel <ch>] [--room <id>]");
@@ -1271,6 +1463,137 @@ async function runCli(argv) {
1271
1463
  process.exitCode = 1;
1272
1464
  return;
1273
1465
  }
1466
+ if (cmd === "group") {
1467
+ const sub = String(rest[0] || "").trim().toLowerCase();
1468
+
1469
+ (async () => {
1470
+ try {
1471
+ if (sub === "templates") {
1472
+ const action = String(rest[1] || "list").trim().toLowerCase();
1473
+ if (action !== "list" && action !== "ls") {
1474
+ throw new Error(`Unknown group templates action: ${action}`);
1475
+ }
1476
+ const json = rest.includes("--json");
1477
+ await runGroupCoreCommand("templates", [action], {
1478
+ cwd: process.cwd(),
1479
+ json,
1480
+ });
1481
+ return;
1482
+ }
1483
+
1484
+ if (sub === "template") {
1485
+ const action = String(rest[1] || "list").trim().toLowerCase();
1486
+ const args = [action, ...rest.slice(2)];
1487
+ const json = rest.includes("--json");
1488
+ await runGroupCoreCommand("template", args, {
1489
+ cwd: process.cwd(),
1490
+ json,
1491
+ });
1492
+ return;
1493
+ }
1494
+
1495
+ if (sub === "run") {
1496
+ const alias = String(rest[1] || "").trim();
1497
+ if (!alias) throw new Error("group run requires <alias>");
1498
+ const instanceIdx = rest.indexOf("--instance");
1499
+ const instance = instanceIdx !== -1 ? String(rest[instanceIdx + 1] || "").trim() : "";
1500
+ const dryRun = rest.includes("--dry-run");
1501
+ const outputJson = rest.includes("--json");
1502
+
1503
+ const projectRoot = process.cwd();
1504
+ await ensureDaemonRunning(projectRoot);
1505
+ const resp = await sendDaemonRequest(projectRoot, {
1506
+ type: "launch_group",
1507
+ alias,
1508
+ instance,
1509
+ dry_run: dryRun,
1510
+ });
1511
+ if (outputJson) {
1512
+ console.log(JSON.stringify(resp?.data || {}, null, 2));
1513
+ return;
1514
+ }
1515
+ console.log(resp?.data?.reply || "Group run requested");
1516
+ if (resp?.data?.group?.ok === false) process.exitCode = 1;
1517
+ return;
1518
+ }
1519
+
1520
+ if (sub === "status") {
1521
+ const groupId = rest[1] && !rest[1].startsWith("--") ? rest[1] : "";
1522
+ const outputJson = rest.includes("--json");
1523
+ const projectRoot = process.cwd();
1524
+ await ensureDaemonRunning(projectRoot);
1525
+ const resp = await sendDaemonRequest(projectRoot, {
1526
+ type: "group_status",
1527
+ group_id: groupId,
1528
+ });
1529
+ if (outputJson) {
1530
+ console.log(JSON.stringify(resp?.data || {}, null, 2));
1531
+ return;
1532
+ }
1533
+ console.log(resp?.data?.reply || "Group status requested");
1534
+ const group = resp?.data?.group || {};
1535
+ if (groupId && group?.group) {
1536
+ console.log(JSON.stringify(group.group, null, 2));
1537
+ } else if (!groupId && Array.isArray(group?.groups)) {
1538
+ group.groups.forEach((item) => {
1539
+ console.log(`- ${item.group_id} [${item.status}] ${item.template_alias} active=${item.members_active}/${item.members_total}`);
1540
+ });
1541
+ }
1542
+ return;
1543
+ }
1544
+
1545
+ if (sub === "stop") {
1546
+ const groupId = String(rest[1] || "").trim();
1547
+ if (!groupId) throw new Error("group stop requires <groupId>");
1548
+ const outputJson = rest.includes("--json");
1549
+ const projectRoot = process.cwd();
1550
+ await ensureDaemonRunning(projectRoot);
1551
+ const resp = await sendDaemonRequest(projectRoot, {
1552
+ type: "stop_group",
1553
+ group_id: groupId,
1554
+ });
1555
+ if (outputJson) {
1556
+ console.log(JSON.stringify(resp?.data || {}, null, 2));
1557
+ return;
1558
+ }
1559
+ console.log(resp?.data?.reply || "Group stop requested");
1560
+ if (resp?.data?.group?.ok === false) process.exitCode = 1;
1561
+ return;
1562
+ }
1563
+
1564
+ if (sub === "diagram") {
1565
+ const target = String(rest[1] || "").trim();
1566
+ if (!target) throw new Error("group diagram requires <alias|groupId>");
1567
+ const outputJson = rest.includes("--json");
1568
+ const format = rest.includes("--mermaid") ? "mermaid" : "ascii";
1569
+ const projectRoot = process.cwd();
1570
+ await ensureDaemonRunning(projectRoot);
1571
+ const resp = await sendDaemonRequest(projectRoot, {
1572
+ type: "group_diagram",
1573
+ alias: target,
1574
+ group_id: target,
1575
+ format,
1576
+ });
1577
+ if (outputJson) {
1578
+ console.log(JSON.stringify(resp?.data || {}, null, 2));
1579
+ return;
1580
+ }
1581
+ console.log(resp?.data?.reply || "Group diagram requested");
1582
+ if (resp?.data?.group?.diagram) {
1583
+ console.log(resp.data.group.diagram);
1584
+ }
1585
+ if (resp?.data?.group?.ok === false) process.exitCode = 1;
1586
+ return;
1587
+ }
1588
+
1589
+ throw new Error("group requires templates|template|run|status|diagram|stop subcommand");
1590
+ } catch (err) {
1591
+ console.error(err.message || String(err));
1592
+ process.exitCode = 1;
1593
+ }
1594
+ })();
1595
+ return;
1596
+ }
1274
1597
  if (cmd === "online") {
1275
1598
  const sub = rest[0] || "";
1276
1599
  if (!sub) {
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
  };
@@ -1082,7 +1099,7 @@ function runUcodeTui({
1082
1099
  if (isBusMessage && targetAgent) {
1083
1100
  updateStatus("Sending message...", "typing");
1084
1101
  try {
1085
- execSync(`ufoo bus send "${targetAgent}" "${actualLine.replace(/"/g, '\\"')}"`, {
1102
+ execFileSync("ufoo", ["bus", "send", targetAgent, actualLine], {
1086
1103
  cwd: workspaceRoot,
1087
1104
  encoding: "utf8",
1088
1105
  });
@@ -1118,7 +1135,10 @@ function runUcodeTui({
1118
1135
  }, payload);
1119
1136
  return;
1120
1137
  }
1121
- 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") {
1122
1142
  logText(result.output || "");
1123
1143
  return;
1124
1144
  }
@@ -1166,6 +1186,54 @@ function runUcodeTui({
1166
1186
  return;
1167
1187
  }
1168
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
+
1169
1237
  if (result.kind === "nl") {
1170
1238
  const statusMessages = [
1171
1239
  "Thinking...",