u-foo 3.0.10 → 3.0.12

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.
Files changed (70) hide show
  1. package/README.md +25 -9
  2. package/README.zh-CN.md +22 -9
  3. package/SKILLS/ubus/SKILL.md +15 -9
  4. package/SKILLS/ufoo/SKILL.md +20 -6
  5. package/dist/tui/darwin-arm64/ufoo-tui +0 -0
  6. package/dist/tui/darwin-x64/ufoo-tui +0 -0
  7. package/dist/tui/linux-arm64/ufoo-tui +0 -0
  8. package/dist/tui/linux-x64/ufoo-tui +0 -0
  9. package/package.json +11 -3
  10. package/scripts/pack-tui.js +112 -0
  11. package/scripts/postinstall.js +11 -0
  12. package/src/agents/activity/activityReconcile.js +106 -0
  13. package/src/agents/activity/activityStatePublisher.js +31 -2
  14. package/src/agents/activity/index.js +1 -0
  15. package/src/agents/launch/launcher.js +19 -0
  16. package/src/agents/launch/ptyRunner.js +20 -1
  17. package/src/agents/prompts/groupBootstrap.js +4 -0
  18. package/src/agents/prompts/native/ufoo.js +2 -0
  19. package/src/app/chat/ChatController.js +433 -0
  20. package/src/app/chat/agentDirectory.js +63 -0
  21. package/src/app/chat/agentEnter.js +70 -0
  22. package/src/app/chat/agentIdentity.js +50 -0
  23. package/src/app/chat/bootstrap.js +66 -0
  24. package/src/app/chat/commandExecutor.js +108 -0
  25. package/src/app/chat/commands.js +31 -0
  26. package/src/app/chat/dashboardView.js +6 -2
  27. package/src/app/chat/historyStore.js +181 -0
  28. package/src/app/chat/index.js +14 -2
  29. package/src/app/chat/inputSubmitHandler.js +21 -7
  30. package/src/app/chat/ipcBuilders.js +52 -0
  31. package/src/app/chat/multiWindow/paneManager.js +10 -1
  32. package/src/app/chat/multiWindow/renderer.js +1 -1
  33. package/src/app/chat/multiWindow/vtFrame.js +93 -0
  34. package/src/app/chat/streamState.js +182 -0
  35. package/src/app/cli/features/doctor.js +22 -0
  36. package/src/code/UCODE_PROMPT.md +2 -0
  37. package/src/code/UcodeController.js +156 -0
  38. package/src/code/context/planGraphService.js +4 -0
  39. package/src/code/repl.js +4 -3
  40. package/src/code/runtime/taskLoop.js +46 -50
  41. package/src/code/tui.js +13 -2
  42. package/src/code/ucodeSlashDispatch.js +241 -0
  43. package/src/coordination/bus/activate.js +3 -0
  44. package/src/runtime/contracts/schemas/ufoo-ui-v1/envelope.json +28 -0
  45. package/src/runtime/contracts/uiProtocol.js +190 -0
  46. package/src/ui/{ink/chatLogModel.js → chatLogModel.js} +2 -2
  47. package/src/ui/dashboardBridge.js +81 -0
  48. package/src/ui/format/index.js +2 -2
  49. package/src/ui/index.js +8 -4
  50. package/src/ui/multiPaneBusMirror.js +137 -0
  51. package/src/ui/multiWindowHandoff.js +232 -0
  52. package/src/ui/ptyHandoff.js +23 -0
  53. package/src/ui/rustChatHost.js +1520 -0
  54. package/src/ui/rustMultiSession.js +497 -0
  55. package/src/ui/rustUcodeHost.js +999 -0
  56. package/src/ui/scrollbackReplay.js +82 -0
  57. package/src/ui/settingsBridge.js +49 -0
  58. package/src/ui/toolMergeBridge.js +66 -0
  59. package/src/ui/tuiLauncher.js +105 -0
  60. package/src/ui/ucodeStatusLine.js +74 -0
  61. package/src/ui/uiHostServer.js +339 -0
  62. package/src/ui/MIGRATION.md +0 -334
  63. package/src/ui/ink/ChatApp.js +0 -4163
  64. package/src/ui/ink/DashboardBar.js +0 -691
  65. package/src/ui/ink/InkDemo.js +0 -96
  66. package/src/ui/ink/MultilineInput.js +0 -662
  67. package/src/ui/ink/UcodeApp.js +0 -1675
  68. package/src/ui/ink/agentMirror.js +0 -730
  69. package/src/ui/ink/chatReducer.js +0 -473
  70. package/src/ui/runInk.js +0 -66
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Agent label / id resolution for chat (Phase 0B extraction).
5
+ */
6
+
7
+ function getAgentLabelFor(meta, agentId) {
8
+ if (meta && meta.display_nickname) return meta.display_nickname;
9
+ if (meta && meta.nickname) return meta.nickname;
10
+ if (!agentId) return "";
11
+ const colon = agentId.indexOf(":");
12
+ if (colon < 0) return agentId;
13
+ const head = agentId.slice(0, colon);
14
+ const tail = agentId.slice(colon + 1).slice(0, 6);
15
+ return tail ? `${head}:${tail}` : head;
16
+ }
17
+
18
+ function buildActiveAgentLabelMap(activeAgents = [], activeAgentMeta = new Map()) {
19
+ const out = new Map();
20
+ const metaMap = activeAgentMeta instanceof Map ? activeAgentMeta : new Map();
21
+ for (const id of Array.isArray(activeAgents) ? activeAgents : []) {
22
+ out.set(id, getAgentLabelFor(metaMap.get(id), id));
23
+ }
24
+ return out;
25
+ }
26
+
27
+ function resolveActiveAgentId(label, activeAgents = [], activeAgentMeta = new Map()) {
28
+ const { resolveAgentId } = require("./agentDirectory");
29
+ const metaMap = activeAgentMeta instanceof Map ? activeAgentMeta : new Map();
30
+ return resolveAgentId({
31
+ label,
32
+ activeAgents: Array.isArray(activeAgents) ? activeAgents : [],
33
+ labelMap: buildActiveAgentLabelMap(activeAgents, metaMap),
34
+ lookupNickname: (nickname) => {
35
+ for (const [id, meta] of metaMap.entries()) {
36
+ if (!meta) continue;
37
+ if (meta.nickname === nickname || meta.scoped_nickname === nickname || meta.display_nickname === nickname) {
38
+ return id;
39
+ }
40
+ }
41
+ return null;
42
+ },
43
+ });
44
+ }
45
+
46
+ module.exports = {
47
+ getAgentLabelFor,
48
+ buildActiveAgentLabelMap,
49
+ resolveActiveAgentId,
50
+ };
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Chat bootstrap helpers (Phase 0B extraction from ChatApp).
5
+ * Headless — safe for ChatController and unit tests without Ink.
6
+ */
7
+
8
+ const path = require("path");
9
+ const fs = require("fs");
10
+ const crypto = require("crypto");
11
+
12
+ function bootstrapEnvironment(projectRoot, options = {}) {
13
+ const { canonicalProjectRoot } = require("../../runtime/projects");
14
+ const { getUfooPaths } = require("../../coordination/state/paths");
15
+ const UfooInit = require("../../app/cli/features/init");
16
+ const { isRunning } = require("../../runtime/daemon");
17
+ const { startDaemon } = require("../../app/chat/transport");
18
+
19
+ const globalMode = options && options.globalMode === true;
20
+ let activeProjectRoot = projectRoot;
21
+ try {
22
+ activeProjectRoot = canonicalProjectRoot(projectRoot);
23
+ } catch {
24
+ activeProjectRoot = path.resolve(projectRoot || process.cwd());
25
+ }
26
+
27
+ const runtimePaths = getUfooPaths(projectRoot);
28
+ const contextIndexFile = path.join(runtimePaths.ufooDir, "context", "decisions.jsonl");
29
+ const needsBootstrap = globalMode && (
30
+ !fs.existsSync(runtimePaths.ufooDir)
31
+ || !fs.existsSync(runtimePaths.busDir)
32
+ || !fs.existsSync(runtimePaths.agentDir)
33
+ || !fs.existsSync(contextIndexFile)
34
+ );
35
+
36
+ return {
37
+ activeProjectRoot,
38
+ globalMode,
39
+ runtimePaths,
40
+ needsBootstrap,
41
+ UfooInit,
42
+ isRunning,
43
+ startDaemon,
44
+ };
45
+ }
46
+
47
+ async function ensureSubscriberId(projectRoot) {
48
+ if (process.env.UFOO_SUBSCRIBER_ID) return;
49
+ const { getUfooPaths } = require("../../coordination/state/paths");
50
+ const sessionFile = path.join(getUfooPaths(projectRoot).ufooDir, "chat", "session-id.txt");
51
+ const sessionDir = path.dirname(sessionFile);
52
+ fs.mkdirSync(sessionDir, { recursive: true });
53
+ let sessionId;
54
+ if (fs.existsSync(sessionFile)) {
55
+ sessionId = fs.readFileSync(sessionFile, "utf8").trim();
56
+ } else {
57
+ sessionId = crypto.randomBytes(4).toString("hex");
58
+ fs.writeFileSync(sessionFile, sessionId, "utf8");
59
+ }
60
+ process.env.UFOO_SUBSCRIBER_ID = `claude-code:${sessionId}`;
61
+ }
62
+
63
+ module.exports = {
64
+ bootstrapEnvironment,
65
+ ensureSubscriberId,
66
+ };
@@ -9,10 +9,19 @@ const {
9
9
  loadGlobalUcodeConfig,
10
10
  saveGlobalUcodeConfig,
11
11
  normalizeControllerMode,
12
+ normalizeLaunchMode,
12
13
  SETTINGS_MODEL_DEFAULTS,
13
14
  defaultAgentModelForProvider,
14
15
  defaultRouterModelForProvider,
15
16
  } = require("../../config");
17
+
18
+ const LAUNCH_MODE_OPTIONS = Object.freeze(["auto", "host", "terminal", "tmux", "internal"]);
19
+ const AGENT_PROVIDER_OPTIONS = Object.freeze([
20
+ { label: "codex", value: "codex-cli" },
21
+ { label: "claude", value: "claude-cli" },
22
+ { label: "agy", value: "agy-cli" },
23
+ { label: "kimi", value: "kimi-cli" },
24
+ ]);
16
25
  const { resolveTransport } = require("../../code/nativeRunner");
17
26
  const { resolveDisplayNickname } = require("../../runtime/daemon/nicknameScope");
18
27
  const { parseIntervalMs, formatIntervalMs } = require("./cronScheduler");
@@ -167,6 +176,7 @@ function createCommandExecutor(options = {}) {
167
176
  schedule = (fn, ms) => setTimeout(fn, ms),
168
177
  clearLog = null,
169
178
  fetchModelsImpl = null,
179
+ applyChatSettings = null,
170
180
  } = options;
171
181
 
172
182
  if (!projectRoot) {
@@ -998,6 +1008,96 @@ function createCommandExecutor(options = {}) {
998
1008
  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
999
1009
  }
1000
1010
 
1011
+ function clearUfooAgentIdentity() {
1012
+ try {
1013
+ const { getUfooPaths } = require("../../coordination/state/paths");
1014
+ const agentDir = getUfooPaths(projectRoot).agentDir;
1015
+ const fsModule = require("fs");
1016
+ fsModule.rmSync(path.join(agentDir, "ufoo-agent.json"), { force: true });
1017
+ fsModule.rmSync(path.join(agentDir, "ufoo-agent.history.jsonl"), { force: true });
1018
+ } catch {
1019
+ // Ignore cleanup failures.
1020
+ }
1021
+ }
1022
+
1023
+ async function notifyChatSettings(patch = {}) {
1024
+ if (typeof applyChatSettings !== "function") return;
1025
+ try {
1026
+ await applyChatSettings(patch);
1027
+ } catch {
1028
+ // Host refresh is best-effort; config is already persisted.
1029
+ }
1030
+ }
1031
+
1032
+ async function handleModeCommand(args = []) {
1033
+ const action = String(args[0] || "").trim().toLowerCase();
1034
+ const config = loadConfig(projectRoot) || {};
1035
+ const current = normalizeLaunchMode(config.launchMode || "auto");
1036
+
1037
+ if (!action || action === "show" || action === "status") {
1038
+ logMessage("system", "{cyan-fg}launch mode:{/cyan-fg}");
1039
+ logMessage("system", ` • current: ${current}`);
1040
+ logMessage("system", ` • options: ${LAUNCH_MODE_OPTIONS.join(", ")}`);
1041
+ logMessage("system", " • use: /mode <auto|host|terminal|tmux|internal>");
1042
+ return;
1043
+ }
1044
+
1045
+ if (!LAUNCH_MODE_OPTIONS.includes(action)) {
1046
+ logMessage(
1047
+ "error",
1048
+ `{white-fg}✗{/white-fg} Unknown mode: ${action}. Use: ${LAUNCH_MODE_OPTIONS.join("|")}`
1049
+ );
1050
+ return;
1051
+ }
1052
+
1053
+ const next = normalizeLaunchMode(action);
1054
+ if (next === current) {
1055
+ logMessage("system", `{cyan-fg}launch mode:{/cyan-fg} already ${next}`);
1056
+ return;
1057
+ }
1058
+
1059
+ saveConfig(projectRoot, { launchMode: next });
1060
+ logMessage("system", `{white-fg}✓{/white-fg} Launch mode: ${next}`);
1061
+ await notifyChatSettings({ launchMode: next });
1062
+ await restartDaemon(projectRoot);
1063
+ }
1064
+
1065
+ async function handleProviderCommand(args = []) {
1066
+ const action = String(args[0] || "").trim().toLowerCase();
1067
+ const config = loadConfig(projectRoot) || {};
1068
+ const current = normalizeSettingsProvider(config.agentProvider);
1069
+ const labels = AGENT_PROVIDER_OPTIONS.map((opt) => opt.label).join(", ");
1070
+
1071
+ if (!action || action === "show" || action === "status") {
1072
+ logMessage("system", "{cyan-fg}ufoo-agent provider:{/cyan-fg}");
1073
+ logMessage("system", ` • current: ${agentProviderKey(current)}`);
1074
+ logMessage("system", ` • options: ${labels}`);
1075
+ logMessage("system", " • use: /provider <codex|claude|agy|kimi>");
1076
+ return;
1077
+ }
1078
+
1079
+ const next = normalizeSettingsProvider(action, "");
1080
+ const known = AGENT_PROVIDER_OPTIONS.some((opt) => opt.label === action || opt.value === next);
1081
+ if (!next || !known) {
1082
+ logMessage(
1083
+ "error",
1084
+ `{white-fg}✗{/white-fg} Unknown provider: ${action}. Use: ${AGENT_PROVIDER_OPTIONS.map((opt) => opt.label).join("|")}`
1085
+ );
1086
+ return;
1087
+ }
1088
+
1089
+ if (next === current) {
1090
+ logMessage("system", `{cyan-fg}ufoo-agent:{/cyan-fg} already ${agentProviderKey(next)}`);
1091
+ return;
1092
+ }
1093
+
1094
+ saveConfig(projectRoot, { agentProvider: next });
1095
+ clearUfooAgentIdentity();
1096
+ logMessage("system", `{white-fg}✓{/white-fg} ufoo-agent: ${agentProviderKey(next)}`);
1097
+ await notifyChatSettings({ agentProvider: next });
1098
+ await restartDaemon(projectRoot);
1099
+ }
1100
+
1001
1101
  async function handleCronCommand(args = []) {
1002
1102
  const action = String(args[0] || "").trim().toLowerCase();
1003
1103
  if (action === "list" || action === "ls") {
@@ -1861,6 +1961,12 @@ function createCommandExecutor(options = {}) {
1861
1961
  case "cron":
1862
1962
  await handleCronCommand(args);
1863
1963
  return true;
1964
+ case "mode":
1965
+ await handleModeCommand(args);
1966
+ return true;
1967
+ case "provider":
1968
+ await handleProviderCommand(args);
1969
+ return true;
1864
1970
  case "group":
1865
1971
  await handleGroupCommand(args);
1866
1972
  return true;
@@ -1893,6 +1999,8 @@ function createCommandExecutor(options = {}) {
1893
1999
  handleRoleCommand,
1894
2000
  handleSoloCommand,
1895
2001
  handleCronCommand,
2002
+ handleModeCommand,
2003
+ handleProviderCommand,
1896
2004
  handleGroupCommand,
1897
2005
  handleSettingsCommand,
1898
2006
  handleUcodeConfigCommand,
@@ -35,6 +35,27 @@ const COMMAND_TREE = {
35
35
  stop: { desc: "Stop cron task by id or all" },
36
36
  },
37
37
  },
38
+ "/mode": {
39
+ desc: "Set agent launch mode",
40
+ children: {
41
+ show: { desc: "Show current launch mode", order: 1 },
42
+ auto: { desc: "Auto-select launch mode", order: 2 },
43
+ host: { desc: "Launch via host inject", order: 3 },
44
+ terminal: { desc: "Launch in external terminal", order: 4 },
45
+ tmux: { desc: "Launch in tmux", order: 5 },
46
+ internal: { desc: "Launch as internal agent", order: 6 },
47
+ },
48
+ },
49
+ "/provider": {
50
+ desc: "Set ufoo-agent provider",
51
+ children: {
52
+ show: { desc: "Show current agent provider", order: 1 },
53
+ codex: { desc: "Use Codex", order: 2 },
54
+ claude: { desc: "Use Claude", order: 3 },
55
+ agy: { desc: "Use Antigravity (agy)", order: 4 },
56
+ kimi: { desc: "Use Kimi Code", order: 5 },
57
+ },
58
+ },
38
59
  "/clear": { desc: "Clear chat log on screen" },
39
60
  "/group": {
40
61
  desc: "Agent group orchestration",
@@ -315,6 +336,16 @@ function describeCommandForChat(text) {
315
336
  return "Managing cron tasks";
316
337
  }
317
338
 
339
+ if (command === "mode") {
340
+ if (!sub || sub === "show" || sub === "status") return "Showing launch mode";
341
+ return `Setting launch mode to ${sub}`;
342
+ }
343
+
344
+ if (command === "provider") {
345
+ if (!sub || sub === "show" || sub === "status") return "Showing agent provider";
346
+ return `Setting agent provider to ${sub}`;
347
+ }
348
+
318
349
  if (command === "mcp") {
319
350
  if (!sub || sub === "status") return "Checking MCP bridge status";
320
351
  if (sub === "tools") return "Listing MCP tools";
@@ -48,8 +48,11 @@ function buildSummaryLine(options = {}) {
48
48
  : "none";
49
49
  let line = `{gray-fg}Agents:{/gray-fg} {cyan-fg}${agents}{/cyan-fg}`
50
50
  + ` {gray-fg}Mode:{/gray-fg} {cyan-fg}${launchMode}{/cyan-fg}`
51
- + ` {gray-fg}Agent:{/gray-fg} {cyan-fg}${providerLabel(agentProvider)}{/cyan-fg}`
52
- + ` {gray-fg}Cron:{/gray-fg} {cyan-fg}${Array.isArray(cronTasks) ? cronTasks.length : 0}{/cyan-fg}`;
51
+ + ` {gray-fg}Agent:{/gray-fg} {cyan-fg}${providerLabel(agentProvider)}{/cyan-fg}`;
52
+ const cronCount = Array.isArray(cronTasks) ? cronTasks.length : 0;
53
+ if (cronCount > 0) {
54
+ line += ` {gray-fg}Cron:{/gray-fg} {cyan-fg}${cronCount}{/cyan-fg}`;
55
+ }
53
56
  const loopPart = formatLoopSummary(loopSummary);
54
57
  if (loopPart) {
55
58
  line += ` {gray-fg}Loop:{/gray-fg} {cyan-fg}${loopPart}{/cyan-fg}`;
@@ -398,4 +401,5 @@ function computeDashboardContent(options = {}) {
398
401
  module.exports = {
399
402
  computeDashboardContent,
400
403
  providerLabel,
404
+ formatLoopSummary,
401
405
  };
@@ -0,0 +1,181 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Chat / input history persistence (Phase 0B extraction from ChatApp).
5
+ */
6
+
7
+ const path = require("path");
8
+ const fs = require("fs");
9
+ const crypto = require("crypto");
10
+ const { stripBlessedTags } = require("../../ui/chatLogModel");
11
+
12
+ function projectRootToId(projectRoot) {
13
+ try {
14
+ const { buildProjectId } = require("../../runtime/projects");
15
+ return buildProjectId(projectRoot || process.cwd());
16
+ } catch {
17
+ return crypto.createHash("sha256").update(String(projectRoot || "")).digest("hex").slice(0, 16);
18
+ }
19
+ }
20
+
21
+ function inputHistoryFilePath(projectRoot, options = {}) {
22
+ const { getUfooPaths } = require("../../coordination/state/paths");
23
+ const { globalMode } = options || {};
24
+ if (globalMode) {
25
+ const os = require("os");
26
+ const globalChatRoot = path.join(os.homedir(), ".ufoo", "chat");
27
+ const globalDir = path.join(globalChatRoot, "global-input-history");
28
+ const projectId = projectRootToId(projectRoot);
29
+ return path.join(globalDir, `${projectId}.jsonl`);
30
+ }
31
+ return path.join(getUfooPaths(projectRoot || process.cwd()).ufooDir, "chat", "input-history.jsonl");
32
+ }
33
+
34
+ function chatHistoryFilePath(projectRoot, options = {}) {
35
+ const { getUfooPaths } = require("../../coordination/state/paths");
36
+ const { globalMode } = options || {};
37
+ if (globalMode) {
38
+ const os = require("os");
39
+ const globalChatRoot = path.join(os.homedir(), ".ufoo", "chat");
40
+ const globalDir = path.join(globalChatRoot, "global-history");
41
+ const projectId = projectRootToId(projectRoot);
42
+ return path.join(globalDir, `${projectId}.jsonl`);
43
+ }
44
+ return path.join(getUfooPaths(projectRoot || process.cwd()).ufooDir, "chat", "history.jsonl");
45
+ }
46
+
47
+ function normalizeHistoryLogLines(text = "") {
48
+ const clean = stripBlessedTags(text);
49
+ return clean.split(/\r?\n/);
50
+ }
51
+
52
+ function loadChatHistory(projectRoot, cap = 200, options = {}) {
53
+ const file = chatHistoryFilePath(projectRoot, options);
54
+ try {
55
+ if (!fs.existsSync(file)) return [];
56
+ const raw = fs.readFileSync(file, "utf8");
57
+ const lines = raw.split(/\r?\n/).filter(Boolean);
58
+ const out = [];
59
+ const pushLine = (line = "", sourceType = "") => {
60
+ const value = String(line || "");
61
+ if (!value.trim()) {
62
+ if (out.length > 0) {
63
+ const last = out[out.length - 1];
64
+ const lastText = typeof last === "object" ? last.text : last;
65
+ if (lastText !== "") out.push({ text: "", sourceType: sourceType || "system" });
66
+ }
67
+ return;
68
+ }
69
+ out.push(sourceType ? { text: value, sourceType } : value);
70
+ };
71
+ for (const line of lines) {
72
+ try {
73
+ const entry = JSON.parse(line);
74
+ if (!entry) continue;
75
+ if (entry.type === "spacer") {
76
+ pushLine("", "system");
77
+ continue;
78
+ }
79
+ const text = String(entry.text || "");
80
+ if (!text) continue;
81
+ const sourceType = String(entry.type || "");
82
+ const stripped = text.replace(/\{[^{}]+\}/g, "");
83
+ for (const renderedLine of normalizeHistoryLogLines(stripped)) {
84
+ pushLine(renderedLine, sourceType);
85
+ }
86
+ } catch {
87
+ // ignore malformed lines
88
+ }
89
+ }
90
+ while (out.length > 0) {
91
+ const first = out[0];
92
+ const firstText = typeof first === "object" ? first.text : first;
93
+ if (firstText !== "") break;
94
+ out.shift();
95
+ }
96
+ while (out.length > 0) {
97
+ const last = out[out.length - 1];
98
+ const lastText = typeof last === "object" ? last.text : last;
99
+ if (lastText !== "") break;
100
+ out.pop();
101
+ }
102
+ const capped = out.slice(-cap);
103
+ while (capped.length > 0) {
104
+ const first = capped[0];
105
+ const firstText = typeof first === "object" ? first.text : first;
106
+ if (firstText !== "") break;
107
+ capped.shift();
108
+ }
109
+ return capped;
110
+ } catch {
111
+ return [];
112
+ }
113
+ }
114
+
115
+ function loadInputHistory(projectRoot, cap = 200, options = {}) {
116
+ const file = inputHistoryFilePath(projectRoot, options);
117
+ try {
118
+ if (!fs.existsSync(file)) return [];
119
+ const raw = fs.readFileSync(file, "utf8");
120
+ const lines = raw.split(/\r?\n/).filter(Boolean);
121
+ const out = [];
122
+ for (const line of lines) {
123
+ try {
124
+ const obj = JSON.parse(line);
125
+ const value = String((obj && obj.value) || "").trim();
126
+ if (value) out.push(value);
127
+ } catch {
128
+ // ignore malformed entries
129
+ }
130
+ }
131
+ return out.slice(-cap);
132
+ } catch {
133
+ return [];
134
+ }
135
+ }
136
+
137
+ function appendInputHistory(projectRoot, value, options = {}) {
138
+ const trimmed = String(value || "").trim();
139
+ if (!trimmed) return;
140
+ const file = inputHistoryFilePath(projectRoot, options);
141
+ try {
142
+ fs.mkdirSync(path.dirname(file), { recursive: true });
143
+ fs.appendFileSync(file, `${JSON.stringify({ value: trimmed, ts: Date.now() })}\n`);
144
+ } catch {
145
+ // best-effort
146
+ }
147
+ }
148
+
149
+ function appendChatHistory(projectRoot, type, text, meta = {}, options = {}) {
150
+ const value = String(text || "");
151
+ if (!value && type !== "spacer") return;
152
+ const file = chatHistoryFilePath(projectRoot, options);
153
+ try {
154
+ fs.mkdirSync(path.dirname(file), { recursive: true });
155
+ fs.appendFileSync(file, `${JSON.stringify({
156
+ ts: new Date().toISOString(),
157
+ type,
158
+ text: value,
159
+ meta: meta && typeof meta === "object" ? meta : {},
160
+ })}\n`);
161
+ } catch {
162
+ // best-effort
163
+ }
164
+ }
165
+
166
+ function chatHistoryOptionsForScope({ globalMode = false, globalScope = "controller" } = {}) {
167
+ return {
168
+ globalMode: Boolean(globalMode && globalScope !== "project"),
169
+ };
170
+ }
171
+
172
+ module.exports = {
173
+ projectRootToId,
174
+ inputHistoryFilePath,
175
+ chatHistoryFilePath,
176
+ loadChatHistory,
177
+ loadInputHistory,
178
+ appendInputHistory,
179
+ appendChatHistory,
180
+ chatHistoryOptionsForScope,
181
+ };
@@ -1,6 +1,18 @@
1
1
  async function runChat(projectRoot, options = {}) {
2
- const { runChatInk } = require("../../ui/ink/ChatApp");
3
- return runChatInk(projectRoot, options);
2
+ const { resolveTuiLaunchPlan } = require("../../ui/tuiLauncher");
3
+ const plan = resolveTuiLaunchPlan({
4
+ mode: options.tuiMode || process.env.UFOO_TUI,
5
+ });
6
+
7
+ if (plan.mode !== "rust") {
8
+ const err = new Error(`Rust TUI unavailable (${plan.reason})`);
9
+ err.code = "UFOO_TUI_UNAVAILABLE";
10
+ err.plan = plan;
11
+ throw err;
12
+ }
13
+
14
+ const { runChatRust } = require("../../ui/rustChatHost");
15
+ return runChatRust(projectRoot, { ...options, tuiMode: "rust" });
4
16
  }
5
17
 
6
18
  module.exports = { runChat };
@@ -20,6 +20,9 @@ function createInputSubmitHandler(options = {}) {
20
20
  enterAgentView = () => {},
21
21
  getAgentAdapter = () => null,
22
22
  activateAgent = async () => {},
23
+ // When /multi is on: focus the in-window TUI pane instead of OS activate.
24
+ // Return true if handled.
25
+ focusMultiPane = null,
23
26
  commitInputHistory = () => {},
24
27
  focusInput = () => {},
25
28
  renderScreen = () => {}, // Add renderScreen callback
@@ -39,6 +42,18 @@ function createInputSubmitHandler(options = {}) {
39
42
  }
40
43
 
41
44
  async function tryActivateTargetAgent(agentId) {
45
+ if (typeof focusMultiPane === "function") {
46
+ try {
47
+ const focused = await focusMultiPane(agentId);
48
+ if (focused) {
49
+ clearTargetAgent();
50
+ return true;
51
+ }
52
+ } catch {
53
+ // Fall through to OS activate / internal view.
54
+ }
55
+ }
56
+
42
57
  const adapter = getAgentAdapter(agentId);
43
58
  const capabilities = adapter && adapter.capabilities ? adapter.capabilities : null;
44
59
  const supportsActivate = Boolean(capabilities && capabilities.supportsActivate);
@@ -47,13 +62,12 @@ function createInputSubmitHandler(options = {}) {
47
62
  if (supportsActivate) {
48
63
  clearTargetAgent();
49
64
  try {
50
- if (adapter && typeof adapter.activate === "function") {
51
- adapter.activate(agentId);
52
- } else {
53
- const pendingActivation = activateAgent(agentId);
54
- if (pendingActivation && typeof pendingActivation.catch === "function") {
55
- pendingActivation.catch(() => {});
56
- }
65
+ // Always use the host-wired activator. Bare adapterRouter adapters
66
+ // expose activate() that no-ops unless activateTerminal/activateTmux
67
+ // were injected — which chat hosts do not do.
68
+ const pendingActivation = activateAgent(agentId);
69
+ if (pendingActivation && typeof pendingActivation.catch === "function") {
70
+ pendingActivation.catch(() => {});
57
71
  }
58
72
  } catch {
59
73
  // Best-effort activation.
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Chat → daemon IPC request builders (Phase 0B extraction).
5
+ */
6
+
7
+ const { resolveActiveAgentId } = require("./agentIdentity");
8
+
9
+ function buildDirectBusSendRequest({
10
+ text,
11
+ targetAgentId = null,
12
+ activeAgents = [],
13
+ activeAgentMeta = new Map(),
14
+ } = {}) {
15
+ const trimmed = String(text || "").trim();
16
+ if (!trimmed) return null;
17
+ if (targetAgentId) {
18
+ return {
19
+ target: targetAgentId,
20
+ message: trimmed,
21
+ source: "chat-direct",
22
+ };
23
+ }
24
+
25
+ const { parseAtTarget } = require("./commands");
26
+ const atTarget = parseAtTarget(trimmed);
27
+ if (!atTarget || !atTarget.message) return null;
28
+ const target = resolveActiveAgentId(atTarget.target, activeAgents, activeAgentMeta) || atTarget.target;
29
+ return {
30
+ target,
31
+ message: atTarget.message.trim(),
32
+ source: "chat-direct",
33
+ };
34
+ }
35
+
36
+ function buildPromptIpcRequest(text) {
37
+ const { IPC_REQUEST_TYPES } = require("../../runtime/contracts/eventContract");
38
+ return {
39
+ type: IPC_REQUEST_TYPES.PROMPT,
40
+ text,
41
+ request_meta: {
42
+ source: "chat-dialog",
43
+ dispatch_default_injection_mode: "immediate",
44
+ allow_relevance_queue: true,
45
+ },
46
+ };
47
+ }
48
+
49
+ module.exports = {
50
+ buildDirectBusSendRequest,
51
+ buildPromptIpcRequest,
52
+ };
@@ -139,7 +139,15 @@ function createPaneManager(options = {}) {
139
139
 
140
140
  function sendInput(data) {
141
141
  if (!focusedAgent) return;
142
- const pane = panes.get(focusedAgent);
142
+ sendInputToPane(panes.get(focusedAgent), data);
143
+ }
144
+
145
+ function sendInputToAgent(agentId, data) {
146
+ if (!agentId) return;
147
+ sendInputToPane(panes.get(agentId), data);
148
+ }
149
+
150
+ function sendInputToPane(pane, data) {
143
151
  if (!pane) return;
144
152
  if (pane.mode === "internal") {
145
153
  handleInternalInput(pane, data);
@@ -284,6 +292,7 @@ function createPaneManager(options = {}) {
284
292
  addAgent,
285
293
  removeAgent,
286
294
  sendInput,
295
+ sendInputToAgent,
287
296
  sendResize,
288
297
  cycleFocus,
289
298
  getFocused,
@@ -3,7 +3,7 @@ const {
3
3
  classifyChatLogLine,
4
4
  compactContinuationIndent,
5
5
  compactDividerLabel,
6
- } = require("../../../ui/ink/chatLogModel");
6
+ } = require("../../../ui/chatLogModel");
7
7
 
8
8
  function createRenderer(options = {}) {
9
9
  const {