u-foo 3.0.9 → 3.0.11

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 (65) hide show
  1. package/README.md +25 -9
  2. package/README.zh-CN.md +22 -9
  3. package/dist/tui/darwin-arm64/ufoo-tui +0 -0
  4. package/dist/tui/darwin-x64/ufoo-tui +0 -0
  5. package/dist/tui/linux-arm64/ufoo-tui +0 -0
  6. package/dist/tui/linux-x64/ufoo-tui +0 -0
  7. package/package.json +12 -4
  8. package/scripts/pack-tui.js +112 -0
  9. package/scripts/postinstall.js +11 -0
  10. package/src/agents/activity/activityReconcile.js +106 -0
  11. package/src/agents/activity/activityStatePublisher.js +31 -2
  12. package/src/agents/activity/index.js +1 -0
  13. package/src/agents/launch/launcher.js +19 -0
  14. package/src/agents/launch/ptyRunner.js +20 -1
  15. package/src/app/chat/ChatController.js +433 -0
  16. package/src/app/chat/agentDirectory.js +63 -0
  17. package/src/app/chat/agentEnter.js +70 -0
  18. package/src/app/chat/agentIdentity.js +50 -0
  19. package/src/app/chat/bootstrap.js +66 -0
  20. package/src/app/chat/commandExecutor.js +108 -0
  21. package/src/app/chat/commands.js +38 -1
  22. package/src/app/chat/dashboardView.js +6 -2
  23. package/src/app/chat/historyStore.js +181 -0
  24. package/src/app/chat/index.js +14 -2
  25. package/src/app/chat/inputSubmitHandler.js +21 -7
  26. package/src/app/chat/ipcBuilders.js +52 -0
  27. package/src/app/chat/multiWindow/paneManager.js +10 -1
  28. package/src/app/chat/multiWindow/renderer.js +1 -1
  29. package/src/app/chat/multiWindow/vtFrame.js +93 -0
  30. package/src/app/chat/streamState.js +182 -0
  31. package/src/app/cli/features/doctor.js +22 -0
  32. package/src/code/UcodeController.js +156 -0
  33. package/src/code/context/planGraphService.js +4 -0
  34. package/src/code/repl.js +4 -3
  35. package/src/code/runtime/taskLoop.js +46 -50
  36. package/src/code/tui.js +13 -2
  37. package/src/code/ucodeSlashDispatch.js +241 -0
  38. package/src/coordination/bus/activate.js +3 -0
  39. package/src/runtime/contracts/schemas/ufoo-ui-v1/envelope.json +28 -0
  40. package/src/runtime/contracts/uiProtocol.js +190 -0
  41. package/src/ui/{ink/chatLogModel.js → chatLogModel.js} +2 -2
  42. package/src/ui/dashboardBridge.js +81 -0
  43. package/src/ui/format/index.js +2 -2
  44. package/src/ui/index.js +8 -4
  45. package/src/ui/multiPaneBusMirror.js +137 -0
  46. package/src/ui/multiWindowHandoff.js +232 -0
  47. package/src/ui/ptyHandoff.js +23 -0
  48. package/src/ui/rustChatHost.js +1520 -0
  49. package/src/ui/rustMultiSession.js +497 -0
  50. package/src/ui/rustUcodeHost.js +999 -0
  51. package/src/ui/scrollbackReplay.js +82 -0
  52. package/src/ui/settingsBridge.js +49 -0
  53. package/src/ui/toolMergeBridge.js +66 -0
  54. package/src/ui/tuiLauncher.js +105 -0
  55. package/src/ui/ucodeStatusLine.js +74 -0
  56. package/src/ui/uiHostServer.js +339 -0
  57. package/src/ui/MIGRATION.md +0 -334
  58. package/src/ui/ink/ChatApp.js +0 -4152
  59. package/src/ui/ink/DashboardBar.js +0 -691
  60. package/src/ui/ink/InkDemo.js +0 -96
  61. package/src/ui/ink/MultilineInput.js +0 -662
  62. package/src/ui/ink/UcodeApp.js +0 -1675
  63. package/src/ui/ink/agentMirror.js +0 -730
  64. package/src/ui/ink/chatReducer.js +0 -473
  65. package/src/ui/runInk.js +0 -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",
@@ -190,7 +211,13 @@ function buildCommandRegistry(tree) {
190
211
  .sort(sortCommands)
191
212
  .map((cmd) => {
192
213
  const node = tree[cmd] || {};
193
- return { cmd, ...mapNode(node) };
214
+ const entry = { cmd, ...mapNode(node) };
215
+ // Stamp priority so buildCompletions keeps launch → group → bus → ctx
216
+ // at the top of the `/` popup instead of falling back to A–Z.
217
+ if (COMMAND_ORDER_MAP.has(cmd)) {
218
+ entry.order = COMMAND_ORDER_MAP.get(cmd);
219
+ }
220
+ return entry;
194
221
  });
195
222
  }
196
223
 
@@ -309,6 +336,16 @@ function describeCommandForChat(text) {
309
336
  return "Managing cron tasks";
310
337
  }
311
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
+
312
349
  if (command === "mcp") {
313
350
  if (!sub || sub === "status") return "Checking MCP bridge status";
314
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 {
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Convert a virtual-terminal screen into ANSI-styled text lines suitable for
5
+ * shipping over `multi.pane.frame`. Rust decodes these with `ansi-to-tui`.
6
+ *
7
+ * The colour/attribute-to-ANSI logic mirrors renderer.js so the rendered
8
+ * frame matches the Ink multi-window presentation.
9
+ */
10
+
11
+ const MAX_LINE_BYTES = 4096;
12
+
13
+ function attrToAnsi(attr) {
14
+ const parts = [];
15
+ if (attr.bold) parts.push("1");
16
+ if (attr.dim) parts.push("2");
17
+ if (attr.italic) parts.push("3");
18
+ if (attr.underline) parts.push("4");
19
+ if (attr.inverse) parts.push("7");
20
+ if (attr.fgRgb) {
21
+ parts.push(`38;2;${attr.fgRgb[0]};${attr.fgRgb[1]};${attr.fgRgb[2]}`);
22
+ } else if (attr.fg !== 7) {
23
+ if (attr.fg < 8) parts.push(String(30 + attr.fg));
24
+ else if (attr.fg < 16) parts.push(String(90 + attr.fg - 8));
25
+ else parts.push(`38;5;${attr.fg}`);
26
+ }
27
+ if (attr.bgRgb) {
28
+ parts.push(`48;2;${attr.bgRgb[0]};${attr.bgRgb[1]};${attr.bgRgb[2]}`);
29
+ } else if (attr.bg > 0) {
30
+ if (attr.bg < 8) parts.push(String(40 + attr.bg));
31
+ else if (attr.bg < 16) parts.push(String(100 + attr.bg - 8));
32
+ else parts.push(`48;5;${attr.bg}`);
33
+ }
34
+ return parts.length > 0 ? `\x1b[${parts.join(";")}m` : "";
35
+ }
36
+
37
+ function truncateLineBytes(line, maxBytes) {
38
+ if (!line) return "";
39
+ if (Buffer.byteLength(line, "utf8") <= maxBytes) return line;
40
+ const buf = Buffer.from(line, "utf8");
41
+ return buf.slice(0, maxBytes).toString("utf8");
42
+ }
43
+
44
+ function vtScreenToAnsiLines(vt, options = {}) {
45
+ if (!vt || typeof vt.getScreen !== "function") return [];
46
+ const { buffer, rows, cols, cursorRow, cursorCol } = vt.getScreen();
47
+ const maxRows = Number.isFinite(options.maxRows) && options.maxRows > 0
48
+ ? Math.floor(options.maxRows)
49
+ : rows;
50
+ const maxCols = Number.isFinite(options.maxCols) && options.maxCols > 0
51
+ ? Math.floor(options.maxCols)
52
+ : cols;
53
+ const cursorInverse = Boolean(options.cursorInverse);
54
+ const reset = "\x1b[0m";
55
+ const limit = Math.min(rows, maxRows);
56
+ const out = [];
57
+ for (let r = 0; r < limit; r++) {
58
+ const row = buffer[r] || [];
59
+ let line = "";
60
+ let lastAttr = "";
61
+ let col = 0;
62
+ let c = 0;
63
+ while (col < maxCols && c < cols) {
64
+ const cell = row[c];
65
+ if (!cell) {
66
+ line += " ";
67
+ col += 1;
68
+ c += 1;
69
+ continue;
70
+ }
71
+ if (cell.wideContinuation) {
72
+ c += 1;
73
+ continue;
74
+ }
75
+ const isCursor = cursorInverse && r === cursorRow && c === cursorCol;
76
+ const attr = isCursor ? { ...cell.attr, inverse: !cell.attr.inverse } : cell.attr;
77
+ const ansi = attrToAnsi(attr);
78
+ if (ansi !== lastAttr) {
79
+ if (lastAttr) line += reset;
80
+ line += ansi;
81
+ lastAttr = ansi;
82
+ }
83
+ line += cell.char || " ";
84
+ col += 1;
85
+ c += 1;
86
+ }
87
+ if (lastAttr) line += reset;
88
+ out.push(truncateLineBytes(line, MAX_LINE_BYTES));
89
+ }
90
+ return out;
91
+ }
92
+
93
+ module.exports = { vtScreenToAnsiLines, MAX_LINE_BYTES };