u-foo 2.3.31 → 2.4.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.
Files changed (236) hide show
  1. package/README.md +157 -213
  2. package/README.zh-CN.md +151 -197
  3. package/SKILLS/ufoo/SKILL.md +8 -8
  4. package/bin/uagy.js +69 -0
  5. package/bin/uclaude.js +2 -2
  6. package/bin/ucode.js +4 -4
  7. package/bin/ucodex.js +2 -2
  8. package/bin/ufoo.js +5 -23
  9. package/modules/AGENTS.template.md +1 -1
  10. package/modules/bus/SKILLS/ubus/SKILL.md +35 -10
  11. package/package.json +9 -5
  12. package/scripts/chat-app-smoke.js +30 -0
  13. package/scripts/global-chat-switch-benchmark.js +5 -5
  14. package/scripts/ink-demo.js +23 -0
  15. package/scripts/ink-smoke.js +30 -0
  16. package/scripts/ucode-app-smoke.js +36 -0
  17. package/src/{agent → agents/activity}/activityDetector.js +39 -2
  18. package/src/{agent → agents/activity}/activityStatePublisher.js +1 -1
  19. package/src/{agent → agents/activity}/activityStateWriter.js +2 -2
  20. package/src/{agent → agents/activity}/activityTracker.js +1 -1
  21. package/src/agents/activity/index.js +8 -0
  22. package/src/{agent → agents/controller}/controllerToolExecutor.js +4 -4
  23. package/src/agents/controller/index.js +8 -0
  24. package/src/{agent → agents/controller}/loopObservability.js +2 -2
  25. package/src/{agent → agents/controller}/loopRuntime.js +1 -1
  26. package/src/{agent → agents/controller}/ufooAgent.js +9 -9
  27. package/src/agents/index.js +10 -0
  28. package/src/agents/internal/index.js +3 -0
  29. package/src/{agent → agents/internal}/internalRunner.js +45 -22
  30. package/src/agents/launch/agyConversation.js +159 -0
  31. package/src/agents/launch/index.js +12 -0
  32. package/src/{agent → agents/launch}/launchEnvironment.js +2 -3
  33. package/src/{agent → agents/launch}/launcher.js +64 -21
  34. package/src/{agent → agents/launch}/notifier.js +23 -12
  35. package/src/{agent → agents/launch}/ptyRunner.js +44 -12
  36. package/src/{agent → agents/launch}/ptyWrapper.js +2 -2
  37. package/src/{agent → agents/launch}/publisherRouting.js +1 -1
  38. package/src/{agent → agents/launch}/readyDetector.js +23 -0
  39. package/src/{agent → agents/prompts}/defaultBootstrap.js +63 -4
  40. package/src/{group/bootstrap.js → agents/prompts/groupBootstrap.js} +41 -6
  41. package/src/agents/prompts/index.js +8 -0
  42. package/src/{code/prompts → agents/prompts/native}/index.js +1 -1
  43. package/src/{agent → agents/providers}/claudeThreadProvider.js +1 -1
  44. package/src/{agent → agents/providers}/codexThreadProvider.js +1 -1
  45. package/src/{agent → agents/providers}/directAuthStatus.js +184 -1
  46. package/src/agents/providers/index.js +13 -0
  47. package/src/{agent → agents/providers}/upstreamTransport.js +2 -2
  48. package/src/{chat → app/chat}/agentSockets.js +1 -1
  49. package/src/{chat → app/chat}/commandExecutor.js +56 -28
  50. package/src/{chat → app/chat}/commands.js +119 -5
  51. package/src/{chat → app/chat}/daemonConnection.js +1 -1
  52. package/src/{chat → app/chat}/daemonMessageRouter.js +54 -4
  53. package/src/{chat → app/chat}/daemonTransport.js +2 -1
  54. package/src/{chat → app/chat}/dashboardView.js +2 -21
  55. package/src/app/chat/index.js +6 -0
  56. package/src/{chat → app/chat}/inputSubmitHandler.js +38 -13
  57. package/src/{chat → app/chat}/internalAgentLogHistory.js +1 -1
  58. package/src/app/chat/multiWindow/index.js +268 -0
  59. package/src/app/chat/multiWindow/paneLayout.js +84 -0
  60. package/src/app/chat/multiWindow/paneManager.js +299 -0
  61. package/src/app/chat/multiWindow/renderer.js +384 -0
  62. package/src/app/chat/multiWindow/virtualTerminal.js +327 -0
  63. package/src/{chat → app/chat}/projectCloseController.js +1 -1
  64. package/src/app/chat/shellCommand.js +42 -0
  65. package/src/{chat → app/chat}/transport.js +16 -3
  66. package/src/{cli → app/cli}/ctxCoreCommands.js +3 -3
  67. package/src/{doctor/index.js → app/cli/features/doctor.js} +1 -1
  68. package/src/{init/index.js → app/cli/features/init.js} +14 -32
  69. package/src/{cli → app/cli}/groupCoreCommands.js +2 -2
  70. package/src/app/cli/index.js +9 -0
  71. package/src/{cli → app/cli}/onlineCoreCommands.js +5 -5
  72. package/src/{cli.js → app/cli/run.js} +62 -59
  73. package/src/app/index.js +6 -0
  74. package/src/code/agent.js +10 -9
  75. package/src/code/index.js +2 -0
  76. package/src/code/launcher/index.js +9 -0
  77. package/src/{agent → code/launcher}/ucode.js +7 -8
  78. package/src/{agent → code/launcher}/ucodeBootstrap.js +3 -3
  79. package/src/{agent → code/launcher}/ucodeBuild.js +2 -2
  80. package/src/{agent → code/launcher}/ucodeDoctor.js +2 -2
  81. package/src/{agent → code/launcher}/ucodeRuntimeConfig.js +1 -2
  82. package/src/code/nativeRunner.js +4 -4
  83. package/src/code/taskDecomposer.js +5 -4
  84. package/src/code/tui.js +39 -1997
  85. package/src/config.js +15 -2
  86. package/src/{bus → coordination/bus}/activate.js +2 -2
  87. package/src/{bus → coordination/bus}/daemon.js +15 -5
  88. package/src/coordination/bus/envelope.js +173 -0
  89. package/src/{bus → coordination/bus}/index.js +7 -3
  90. package/src/{bus → coordination/bus}/inject.js +11 -3
  91. package/src/{bus → coordination/bus}/message.js +1 -1
  92. package/src/coordination/bus/messageMeta.js +130 -0
  93. package/src/coordination/bus/promptEnvelope.js +65 -0
  94. package/src/{bus → coordination/bus}/shake.js +1 -1
  95. package/src/{bus → coordination/bus}/store.js +3 -3
  96. package/src/{bus → coordination/bus}/subscriber.js +2 -2
  97. package/src/{bus → coordination/bus}/utils.js +2 -2
  98. package/src/{history → coordination/history}/inputTimeline.js +5 -5
  99. package/src/coordination/index.js +10 -0
  100. package/src/{memory → coordination/memory}/historySearch.js +1 -1
  101. package/src/{memory → coordination/memory}/index.js +3 -3
  102. package/src/{report → coordination/report}/store.js +2 -2
  103. package/src/{ufoo → coordination/state}/agentRegistryDiagnostics.js +43 -0
  104. package/src/{status → coordination/status}/index.js +3 -3
  105. package/src/online/bridge.js +2 -2
  106. package/src/{controller → orchestration/controller}/flags.js +1 -1
  107. package/src/{controller → orchestration/controller}/gateRouter.js +1 -1
  108. package/src/orchestration/controller/index.js +10 -0
  109. package/src/{controller → orchestration/controller}/shadowGuard.js +1 -1
  110. package/src/orchestration/groups/bootstrap.js +3 -0
  111. package/src/orchestration/groups/index.js +10 -0
  112. package/src/orchestration/groups/promptProfiles.js +3 -0
  113. package/src/{group → orchestration/groups}/templates.js +1 -1
  114. package/src/{group → orchestration/groups}/validateTemplate.js +1 -1
  115. package/src/orchestration/index.js +7 -0
  116. package/src/orchestration/solo/index.js +3 -0
  117. package/src/{daemon → runtime/daemon}/agentProcessManager.js +1 -1
  118. package/src/{daemon → runtime/daemon}/cronOps.js +3 -2
  119. package/src/{daemon → runtime/daemon}/groupOrchestrator.js +26 -9
  120. package/src/{daemon → runtime/daemon}/index.js +273 -79
  121. package/src/{daemon → runtime/daemon}/ipcServer.js +24 -2
  122. package/src/{daemon → runtime/daemon}/nicknameScope.js +6 -3
  123. package/src/{daemon → runtime/daemon}/ops.js +48 -61
  124. package/src/{daemon → runtime/daemon}/promptLoop.js +1 -1
  125. package/src/{daemon → runtime/daemon}/promptRequest.js +13 -8
  126. package/src/runtime/daemon/providerSessions.js +230 -0
  127. package/src/{daemon → runtime/daemon}/reporting.js +4 -4
  128. package/src/{daemon → runtime/daemon}/run.js +12 -5
  129. package/src/{daemon → runtime/daemon}/soloBootstrap.js +7 -7
  130. package/src/{daemon → runtime/daemon}/status.js +5 -5
  131. package/src/runtime/index.js +10 -0
  132. package/src/runtime/process/nodeExecutable.js +26 -0
  133. package/src/{projects → runtime/projects}/registry.js +1 -1
  134. package/src/{projects → runtime/projects}/runtimes.js +1 -1
  135. package/src/{terminal → runtime/terminal}/adapterRouter.js +0 -10
  136. package/src/{terminal → runtime/terminal}/adapters/internalAdapter.js +0 -4
  137. package/src/tools/handlers/common.js +1 -1
  138. package/src/tools/handlers/listAgents.js +1 -1
  139. package/src/tools/handlers/memory.js +3 -3
  140. package/src/tools/handlers/readBusSummary.js +1 -1
  141. package/src/tools/handlers/readOpenDecisions.js +1 -1
  142. package/src/tools/handlers/readProjectRegistry.js +1 -1
  143. package/src/tools/handlers/readPromptHistory.js +2 -2
  144. package/src/tools/schemaFixtures.js +1 -1
  145. package/src/ui/MIGRATION.md +336 -0
  146. package/src/ui/format/index.js +974 -0
  147. package/src/ui/index.js +9 -0
  148. package/src/ui/ink/ChatApp.js +3674 -0
  149. package/src/ui/ink/DashboardBar.js +685 -0
  150. package/src/ui/ink/InkDemo.js +96 -0
  151. package/src/ui/ink/MultilineInput.js +612 -0
  152. package/src/ui/ink/UcodeApp.js +822 -0
  153. package/src/ui/ink/agentMirror.js +730 -0
  154. package/src/ui/ink/chatReducer.js +359 -0
  155. package/src/ui/runInk.js +57 -0
  156. package/src/bus/messageMeta.js +0 -52
  157. package/src/chat/agentViewController.js +0 -1072
  158. package/src/chat/chatLogController.js +0 -138
  159. package/src/chat/completionController.js +0 -533
  160. package/src/chat/dashboardKeyController.js +0 -573
  161. package/src/chat/index.js +0 -2214
  162. package/src/chat/inputHistoryController.js +0 -135
  163. package/src/chat/inputListenerController.js +0 -470
  164. package/src/chat/layout.js +0 -186
  165. package/src/chat/pasteController.js +0 -81
  166. package/src/chat/statusLineController.js +0 -223
  167. package/src/chat/streamTracker.js +0 -156
  168. package/src/code/config +0 -0
  169. package/src/daemon/providerSessions.js +0 -488
  170. package/src/terminal/adapters/internalPtyAdapter.js +0 -42
  171. /package/src/{code/prompts → agents/prompts/native}/actions.js +0 -0
  172. /package/src/{code/prompts → agents/prompts/native}/efficiency.js +0 -0
  173. /package/src/{code/prompts → agents/prompts/native}/environment.js +0 -0
  174. /package/src/{code/prompts → agents/prompts/native}/identity.js +0 -0
  175. /package/src/{code/prompts → agents/prompts/native}/safety.js +0 -0
  176. /package/src/{code/prompts → agents/prompts/native}/sections.js +0 -0
  177. /package/src/{code/prompts → agents/prompts/native}/system.js +0 -0
  178. /package/src/{code/prompts → agents/prompts/native}/tasks.js +0 -0
  179. /package/src/{code/prompts → agents/prompts/native}/toolDescriptions/bash.js +0 -0
  180. /package/src/{code/prompts → agents/prompts/native}/toolDescriptions/edit.js +0 -0
  181. /package/src/{code/prompts → agents/prompts/native}/toolDescriptions/read.js +0 -0
  182. /package/src/{code/prompts → agents/prompts/native}/toolDescriptions/write.js +0 -0
  183. /package/src/{code/prompts → agents/prompts/native}/ufoo.js +0 -0
  184. /package/src/{group → agents/prompts}/promptProfiles.js +0 -0
  185. /package/src/{agent → agents/providers}/claudeEventTranslator.js +0 -0
  186. /package/src/{agent → agents/providers}/claudeOauthTokenReader.js +0 -0
  187. /package/src/{agent → agents/providers}/claudeSessionFiles.js +0 -0
  188. /package/src/{agent → agents/providers}/codexEventTranslator.js +0 -0
  189. /package/src/{agent → agents/providers}/credentials/claude.js +0 -0
  190. /package/src/{agent → agents/providers}/credentials/codex.js +0 -0
  191. /package/src/{agent → agents/providers}/credentials/index.js +0 -0
  192. /package/src/{chat → app/chat}/agentBar.js +0 -0
  193. /package/src/{chat → app/chat}/agentDirectory.js +0 -0
  194. /package/src/{chat → app/chat}/cronScheduler.js +0 -0
  195. /package/src/{chat → app/chat}/daemonCoordinator.js +0 -0
  196. /package/src/{chat → app/chat}/daemonReconnect.js +0 -0
  197. /package/src/{chat → app/chat}/daemonTransportDefaults.js +0 -0
  198. /package/src/{chat → app/chat}/inputMath.js +0 -0
  199. /package/src/{chat → app/chat}/rawKeyMap.js +0 -0
  200. /package/src/{chat → app/chat}/settingsController.js +0 -0
  201. /package/src/{chat → app/chat}/text.js +0 -0
  202. /package/src/{chat → app/chat}/transientAgentState.js +0 -0
  203. /package/src/{cli → app/cli}/busCoreCommands.js +0 -0
  204. /package/src/{skills/index.js → app/cli/features/skills.js} +0 -0
  205. /package/src/{bus → coordination/bus}/nickname.js +0 -0
  206. /package/src/{bus → coordination/bus}/queue.js +0 -0
  207. /package/src/{context → coordination/context}/decisions.js +0 -0
  208. /package/src/{context → coordination/context}/doctor.js +0 -0
  209. /package/src/{context → coordination/context}/index.js +0 -0
  210. /package/src/{context → coordination/context}/sync.js +0 -0
  211. /package/src/{ufoo → coordination/state}/agentsStore.js +0 -0
  212. /package/src/{ufoo → coordination/state}/paths.js +0 -0
  213. /package/src/{controller → orchestration/controller}/launchRouting.js +0 -0
  214. /package/src/{controller → orchestration/controller}/routerFastPath.js +0 -0
  215. /package/src/{controller → orchestration/controller}/routerFinalize.js +0 -0
  216. /package/src/{group → orchestration/groups}/diagram.js +0 -0
  217. /package/src/{group → orchestration/groups}/templateValidation.js +0 -0
  218. /package/src/{solo → orchestration/solo}/commands.js +0 -0
  219. /package/src/{shared → runtime/contracts}/eventContract.js +0 -0
  220. /package/src/{shared → runtime/contracts}/ptySocketContract.js +0 -0
  221. /package/src/{providerapi → runtime/privacy}/redactor.js +0 -0
  222. /package/src/{providerapi → runtime/privacy}/shadowDiff.js +0 -0
  223. /package/src/{projects → runtime/projects}/identity.js +0 -0
  224. /package/src/{projects → runtime/projects}/index.js +0 -0
  225. /package/src/{projects → runtime/projects}/projectId.js +0 -0
  226. /package/src/{terminal → runtime/terminal}/adapterContract.js +0 -0
  227. /package/src/{terminal → runtime/terminal}/adapters/externalAdapter.js +0 -0
  228. /package/src/{terminal → runtime/terminal}/adapters/hostAdapter.js +0 -0
  229. /package/src/{terminal → runtime/terminal}/adapters/internalQueueAdapter.js +0 -0
  230. /package/src/{terminal → runtime/terminal}/adapters/terminalAdapter.js +0 -0
  231. /package/src/{terminal → runtime/terminal}/adapters/tmuxAdapter.js +0 -0
  232. /package/src/{terminal → runtime/terminal}/detect.js +0 -0
  233. /package/src/{terminal → runtime/terminal}/index.js +0 -0
  234. /package/src/{terminal → runtime/terminal}/iterm2.js +0 -0
  235. /package/src/{utils → ui/format}/banner.js +0 -0
  236. /package/src/{shared → ui/format}/markdownRenderer.js +0 -0
package/src/code/tui.js CHANGED
@@ -1,2010 +1,51 @@
1
- const chalk = require("chalk");
2
- const pkg = require("../../package.json");
1
+ const fmt = require("../ui/format");
3
2
 
4
- const UCODE_BANNER_LINES = [
5
- "█ █ █▀▀ █▀█ █▀▄ █▀▀",
6
- "█ █ █ █ █ █ █ █▀ ",
7
- "▀▀▀ ▀▀▀ ▀▀▀ ▀▀ ▀▀▀",
8
- ];
9
-
10
- const UCODE_VERSION = String((pkg && pkg.version) || "dev");
11
-
12
- // Status indicators
13
- const STATUS_INDICATORS = {
14
- thinking: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
15
- typing: ["◐", "◓", "◑", "◒"],
16
- waiting: ["∙", "∙∙", "∙∙∙", "∙∙", "∙"],
17
- };
18
-
19
- const ANSI_PATTERN = /\x1B\[[0-9;?]*[ -/]*[@-~]/g;
20
-
21
- function charDisplayWidth(char = "") {
22
- if (!char) return 0;
23
- const code = char.codePointAt(0) || 0;
24
- if (code === 0) return 0;
25
- if (code < 32 || (code >= 0x7f && code < 0xa0)) return 0;
26
- if ((code >= 0x0300 && code <= 0x036f) ||
27
- (code >= 0x1ab0 && code <= 0x1aff) ||
28
- (code >= 0x1dc0 && code <= 0x1dff) ||
29
- (code >= 0x20d0 && code <= 0x20ff) ||
30
- (code >= 0xfe20 && code <= 0xfe2f)) {
31
- return 0;
32
- }
33
- if ((code >= 0x1100 && code <= 0x115f) ||
34
- code === 0x2329 ||
35
- code === 0x232a ||
36
- (code >= 0x2e80 && code <= 0xa4cf) ||
37
- (code >= 0xac00 && code <= 0xd7a3) ||
38
- (code >= 0xf900 && code <= 0xfaff) ||
39
- (code >= 0xfe10 && code <= 0xfe19) ||
40
- (code >= 0xfe30 && code <= 0xfe6f) ||
41
- (code >= 0xff00 && code <= 0xff60) ||
42
- (code >= 0xffe0 && code <= 0xffe6) ||
43
- (code >= 0x1f300 && code <= 0x1faff)) {
44
- return 2;
45
- }
46
- return 1;
47
- }
48
-
49
- function displayCellWidth(text = "") {
50
- return Array.from(String(text || "").replace(ANSI_PATTERN, "")).reduce(
51
- (sum, char) => sum + charDisplayWidth(char),
52
- 0
53
- );
54
- }
55
-
56
- function safeRead(getter, fallback = undefined) {
57
- try {
58
- return getter();
59
- } catch {
60
- return fallback;
61
- }
62
- }
63
-
64
- function resolveLogContentWidth({ logBox = null, screen = null, fallback = 80 } = {}) {
65
- const coords = safeRead(() => logBox && typeof logBox._getCoords === "function" ? logBox._getCoords() : null, null);
66
- if (coords && Number.isFinite(coords.xl) && Number.isFinite(coords.xi)) {
67
- return Math.max(1, coords.xl - coords.xi);
68
- }
69
- const width = safeRead(() => logBox && logBox.width, null);
70
- if (typeof width === "number") return Math.max(1, width);
71
- const screenWidth = safeRead(() => screen && screen.width, null);
72
- if (typeof screenWidth === "number") return Math.max(1, screenWidth);
73
- const screenCols = safeRead(() => screen && screen.cols, null);
74
- if (typeof screenCols === "number") return Math.max(1, screenCols);
75
- return Math.max(1, fallback);
76
- }
77
-
78
- function formatHighlightedUserInput(text = "", {
79
- width = 80,
80
- escapeText = (value) => String(value || ""),
81
- } = {}) {
82
- const plain = String(text || "").trim();
83
- if (!plain) return "";
84
- const targetWidth = Math.max(1, Math.floor(Number(width) || 80) - 1);
85
- const prefix = " → ";
86
- const suffix = " ";
87
- const contentWidth = displayCellWidth(`${prefix}${plain}${suffix}`);
88
- const pad = " ".repeat(Math.max(0, targetWidth - contentWidth));
89
- return `{cyan-bg}{white-fg}${prefix}${escapeText(plain)}${suffix}${pad}{/white-fg}{/cyan-bg}`;
90
- }
91
-
92
- // Stream buffer for smooth output
93
- class StreamBuffer {
94
- constructor(writer, options = {}) {
95
- this.writer = writer;
96
- this.buffer = "";
97
- this.delay = options.delay || 8; // ms between chunks
98
- this.chunkSize = options.chunkSize || 3; // chars per chunk
99
- this.isStreaming = false;
100
- this.streamPromise = null;
101
- }
102
-
103
- async write(text) {
104
- this.buffer += text;
105
- if (!this.isStreaming) {
106
- this.isStreaming = true;
107
- this.streamPromise = this.flush();
108
- }
109
- return this.streamPromise;
110
- }
111
-
112
- async flush() {
113
- while (this.buffer.length > 0) {
114
- const chunk = this.buffer.slice(0, this.chunkSize);
115
- this.buffer = this.buffer.slice(this.chunkSize);
116
- this.writer(chunk);
117
- if (this.buffer.length > 0) {
118
- await new Promise(resolve => setTimeout(resolve, this.delay));
119
- }
120
- }
121
- this.isStreaming = false;
122
- }
123
-
124
- async finish() {
125
- if (this.isStreaming) {
126
- await this.streamPromise;
127
- }
128
- if (this.buffer.length > 0) {
129
- this.writer(this.buffer);
130
- this.buffer = "";
131
- }
132
- }
133
- }
134
-
135
- function normalizeModelLabel(model = "") {
136
- const text = String(model || "").trim();
137
- if (text) return text;
138
- return "default";
139
- }
140
-
141
- function buildUcodeBannerLines({ model = "", engine = "ufoo-core", nickname = "", agentId = "", workspaceRoot = "", sessionId = "", width = 0 } = {}) {
142
- const modelLabel = normalizeModelLabel(model);
143
- void width;
144
- void engine; // Not using engine anymore
145
- void nickname;
146
- void agentId;
147
-
148
- // Get current working directory with ~ for home
149
- const path = require("path");
150
- const os = require("os");
151
- const currentDir = workspaceRoot || process.cwd();
152
- const homeDir = os.homedir();
153
-
154
- // Replace home directory with ~
155
- let shortPath = currentDir;
156
- if (currentDir.startsWith(homeDir)) {
157
- shortPath = currentDir.replace(homeDir, "~");
158
- }
159
-
160
- const logoLines = UCODE_BANNER_LINES.map((line) => chalk.cyan(line));
161
- const infoLines = [];
162
- infoLines.push(`${chalk.dim("Version:")} ${chalk.cyan.bold(UCODE_VERSION)}`);
163
- infoLines.push(`${chalk.dim("Model:")} ${chalk.yellow(modelLabel)}`);
164
- infoLines.push(`${chalk.dim("Dictionary:")} ${chalk.gray(shortPath)}`);
165
- const normalizedSessionId = String(sessionId || "").trim();
166
- if (normalizedSessionId) {
167
- infoLines.push(`${chalk.dim("Session:")} ${chalk.gray(normalizedSessionId)}`);
168
- }
169
- const logoPadding = " ".repeat(
170
- UCODE_BANNER_LINES.reduce((max, line) => Math.max(max, String(line || "").length), 0)
171
- );
172
- const rows = Math.max(logoLines.length, infoLines.length);
173
-
174
- return Array.from({ length: rows }, (_, index) => {
175
- const logoLine = logoLines[index] || logoPadding;
176
- const info = infoLines[index] || "";
177
- return ` ${logoLine} ${info}`;
178
- });
179
- }
180
-
181
- function escapeBlessedLiteral(text) {
182
- const raw = String(text == null ? "" : text);
183
- const safe = raw.replace(/\{\/escape\}/g, "{open}/escape{close}");
184
- return `{escape}${safe}{/escape}`;
185
- }
186
-
187
- function buildUcodeBannerBlessedLines({
188
- model = "",
189
- engine = "ufoo-core",
190
- nickname = "",
191
- agentId = "",
192
- workspaceRoot = "",
193
- sessionId = "",
194
- width = 0,
195
- } = {}) {
196
- const modelLabel = normalizeModelLabel(model);
197
- void width;
198
- void engine; // Not using engine anymore
199
- void nickname;
200
- void agentId;
201
-
202
- const path = require("path");
203
- const os = require("os");
204
- const currentDir = workspaceRoot || process.cwd();
205
- const homeDir = os.homedir();
206
-
207
- let shortPath = currentDir;
208
- if (currentDir.startsWith(homeDir)) {
209
- shortPath = currentDir.replace(homeDir, "~");
210
- }
211
- shortPath = path.normalize(shortPath);
212
-
213
- const logoLines = UCODE_BANNER_LINES.map(
214
- (line) => `{cyan-fg}${escapeBlessedLiteral(line)}{/cyan-fg}`
215
- );
216
- const infoLines = [
217
- `{gray-fg}Version:{/gray-fg} {cyan-fg}{bold}${escapeBlessedLiteral(UCODE_VERSION)}{/bold}{/cyan-fg}`,
218
- `{gray-fg}Model:{/gray-fg} {yellow-fg}${escapeBlessedLiteral(modelLabel)}{/yellow-fg}`,
219
- `{gray-fg}Dictionary:{/gray-fg} {gray-fg}${escapeBlessedLiteral(shortPath)}{/gray-fg}`,
220
- ];
221
- const normalizedSessionId = String(sessionId || "").trim();
222
- if (normalizedSessionId) {
223
- infoLines.push(`{gray-fg}Session:{/gray-fg} {gray-fg}${escapeBlessedLiteral(normalizedSessionId)}{/gray-fg}`);
224
- }
225
- const logoPadding = " ".repeat(
226
- UCODE_BANNER_LINES.reduce((max, line) => Math.max(max, String(line || "").length), 0)
227
- );
228
- const rows = Math.max(logoLines.length, infoLines.length);
229
-
230
- return Array.from({ length: rows }, (_, index) => {
231
- const logoLine = logoLines[index] || logoPadding;
232
- const info = infoLines[index] || "";
233
- return ` ${logoLine} ${info}`;
234
- });
235
- }
236
-
237
- function shouldUseUcodeTui({ stdin, stdout, jsonOutput, forceTui = false, disableTui = false } = {}) {
238
- if (disableTui) return false;
239
- if (jsonOutput) return false;
240
- if (forceTui) return true;
241
- return Boolean(stdin && stdin.isTTY && stdout && stdout.isTTY);
242
- }
243
-
244
- // Helper function to load agents from bus
245
- function parseActiveAgentsFromBusStatus(busStatus = "") {
246
- const lines = String(busStatus || "").replace(ANSI_PATTERN, "").split(/\r?\n/);
247
- const agents = [];
248
- let inOnlineSection = false;
249
-
250
- for (const line of lines) {
251
- const trimmed = String(line || "").trim();
252
- if (!trimmed) continue;
253
-
254
- if (/^Online agents:\s*$/i.test(trimmed)) {
255
- inOnlineSection = true;
256
- continue;
257
- }
258
- if (!inOnlineSection) continue;
259
-
260
- if (/^\(none\)$/i.test(trimmed)) {
261
- continue;
262
- }
263
-
264
- // Next heading means we have left the online agents section
265
- if (/^[A-Za-z][A-Za-z ]+:\s*$/.test(trimmed)) {
266
- break;
267
- }
268
-
269
- const rawId = trimmed.replace(/\s+\([^)]+\)\s*$/, "");
270
- if (!rawId) continue;
271
- const [type, ...idParts] = rawId.split(":");
272
- const id = idParts.join(":");
273
- if (!type) continue;
274
-
275
- agents.push({
276
- type,
277
- id,
278
- status: "active",
279
- fullId: rawId,
280
- nickname: (trimmed.match(/\(([^)]+)\)\s*$/) || [])[1] || "",
281
- });
282
- }
283
-
284
- // Fallback for legacy output: "type:id (active|idle)"
285
- if (agents.length === 0) {
286
- for (const line of lines) {
287
- const trimmed = String(line || "").trim();
288
- const match = trimmed.match(/^([a-z-]+):([a-f0-9]+)\s+\((active|idle)\)$/);
289
- if (!match) continue;
290
- agents.push({
291
- type: match[1],
292
- id: match[2],
293
- status: match[3],
294
- fullId: `${match[1]}:${match[2]}`,
295
- nickname: "",
296
- });
297
- }
298
- }
299
-
300
- return agents;
301
- }
302
-
303
- function loadActiveAgents(workspaceRoot) {
304
- try {
305
- const { execSync } = require("child_process");
306
- const busStatus = execSync("ufoo bus status", {
307
- cwd: workspaceRoot,
308
- encoding: "utf8",
309
- });
310
- return parseActiveAgentsFromBusStatus(busStatus);
311
- } catch {
312
- return [];
313
- }
314
- }
315
-
316
- function renderLogLinesWithMarkdown(text = "", state = {}, escapeFn = (value) => String(value || "")) {
317
- const { renderMarkdownLines } = require("../shared/markdownRenderer");
318
- return renderMarkdownLines(text, state, escapeFn);
319
- }
320
-
321
- function shouldEnterAgentSelection(inputValue = "") {
322
- const text = String(inputValue || "");
323
- const trimmed = text.trim();
324
- return !trimmed;
325
- }
326
-
327
- function resolveAgentSelectionOnDown({
328
- agentSelectionMode = false,
329
- selectedAgentIndex = -1,
330
- totalAgents = 0,
331
- } = {}) {
332
- const total = Number.isFinite(totalAgents) ? Math.max(0, Math.floor(totalAgents)) : 0;
333
- if (total <= 0) return { action: "none", index: -1 };
334
- if (agentSelectionMode) {
335
- const keep = selectedAgentIndex >= 0 && selectedAgentIndex < total ? selectedAgentIndex : 0;
336
- return { action: "hold", index: keep };
337
- }
338
- const enter = selectedAgentIndex >= 0 && selectedAgentIndex < total ? selectedAgentIndex : 0;
339
- return { action: "enter", index: enter };
340
- }
341
-
342
- function cycleAgentSelectionIndex(selectedAgentIndex = -1, totalAgents = 0, direction = "right") {
343
- const total = Number.isFinite(totalAgents) ? Math.max(0, Math.floor(totalAgents)) : 0;
344
- if (total <= 0) return -1;
345
- const current = selectedAgentIndex >= 0 && selectedAgentIndex < total ? selectedAgentIndex : 0;
346
- if (direction === "left") {
347
- return (current - 1 + total) % total;
348
- }
349
- return (current + 1) % total;
350
- }
351
-
352
- function shouldClearAgentSelectionOnUp({
353
- agentSelectionMode = false,
354
- inputValue = "",
355
- } = {}) {
356
- return Boolean(agentSelectionMode && shouldEnterAgentSelection(inputValue));
357
- }
358
-
359
- function moveCursorHorizontally(cursorPos = 0, inputValue = "", direction = "right") {
360
- const text = String(inputValue || "");
361
- const max = text.length;
362
- const pos = Number.isFinite(cursorPos) ? Math.max(0, Math.floor(cursorPos)) : 0;
363
- if (direction === "left") return Math.max(0, pos - 1);
364
- return Math.min(max, pos + 1);
365
- }
366
-
367
- function clampCursorPos(cursorPos = 0, inputValue = "") {
368
- const text = String(inputValue || "");
369
- const pos = Number.isFinite(cursorPos) ? Math.floor(cursorPos) : 0;
370
- return Math.max(0, Math.min(text.length, pos));
371
- }
372
-
373
- function findLogicalLineStart(inputValue = "", cursorPos = 0) {
374
- const text = String(inputValue || "");
375
- const pos = clampCursorPos(cursorPos, text);
376
- const prevNewline = text.lastIndexOf("\n", Math.max(0, pos - 1));
377
- return prevNewline === -1 ? 0 : prevNewline + 1;
378
- }
379
-
380
- function findLogicalLineEnd(inputValue = "", cursorPos = 0) {
381
- const text = String(inputValue || "");
382
- const pos = clampCursorPos(cursorPos, text);
383
- const nextNewline = text.indexOf("\n", pos);
384
- return nextNewline === -1 ? text.length : nextNewline;
385
- }
386
-
387
- function moveCursorToVisualLineBoundary({
388
- cursorPos = 0,
389
- inputValue = "",
390
- width = 80,
391
- boundary = "start",
392
- strWidth,
393
- } = {}) {
394
- const inputMath = require("../chat/inputMath");
395
- const text = String(inputValue || "");
396
- const normalizedWidth = Number.isFinite(width) ? Math.max(1, Math.floor(width)) : 1;
397
- const pos = clampCursorPos(cursorPos, text);
398
- const { row } = inputMath.getCursorRowCol(text, pos, normalizedWidth, strWidth);
399
- if (boundary === "end") {
400
- return inputMath.getCursorPosForRowCol(text, row, normalizedWidth, normalizedWidth, strWidth);
401
- }
402
- return inputMath.getCursorPosForRowCol(text, row, 0, normalizedWidth, strWidth);
403
- }
404
-
405
- function moveCursorVertically({
406
- cursorPos = 0,
407
- inputValue = "",
408
- width = 80,
409
- direction = "down",
410
- preferredCol = null,
411
- strWidth,
412
- } = {}) {
413
- const inputMath = require("../chat/inputMath");
414
- const text = String(inputValue || "");
415
- const normalizedWidth = Number.isFinite(width) ? Math.max(1, Math.floor(width)) : 1;
416
- const pos = clampCursorPos(cursorPos, text);
417
- const { row, col } = inputMath.getCursorRowCol(text, pos, normalizedWidth, strWidth);
418
- const totalRows = inputMath.countLines(text, normalizedWidth, strWidth);
419
- const targetCol = Number.isFinite(preferredCol) ? preferredCol : col;
420
-
421
- if (direction === "up") {
422
- if (row <= 0) {
423
- return { moved: false, nextCursorPos: pos, preferredCol: targetCol, boundary: "top" };
424
- }
425
- return {
426
- moved: true,
427
- nextCursorPos: inputMath.getCursorPosForRowCol(text, row - 1, targetCol, normalizedWidth, strWidth),
428
- preferredCol: targetCol,
429
- boundary: "",
430
- };
431
- }
432
-
433
- if (row >= totalRows - 1) {
434
- return { moved: false, nextCursorPos: pos, preferredCol: targetCol, boundary: "bottom" };
435
- }
436
- return {
437
- moved: true,
438
- nextCursorPos: inputMath.getCursorPosForRowCol(text, row + 1, targetCol, normalizedWidth, strWidth),
439
- preferredCol: targetCol,
440
- boundary: "",
441
- };
442
- }
443
-
444
- function deleteWordBeforeCursor(inputValue = "", cursorPos = 0) {
445
- const text = String(inputValue || "");
446
- const pos = clampCursorPos(cursorPos, text);
447
- if (pos <= 0) return { value: text, cursorPos: pos };
448
- const before = text.slice(0, pos);
449
- const after = text.slice(pos);
450
- const match = before.match(/\s*\S+\s*$/);
451
- const start = match ? pos - match[0].length : Math.max(0, pos - 1);
452
- return {
453
- value: before.slice(0, start) + after,
454
- cursorPos: start,
455
- };
456
- }
457
-
458
- function moveCursorByWord(inputValue = "", cursorPos = 0, direction = "forward") {
459
- const text = String(inputValue || "");
460
- const pos = clampCursorPos(cursorPos, text);
461
- if (direction === "backward") {
462
- const before = text.slice(0, pos);
463
- const trimmedEnd = before.search(/\S\s*$/) >= 0 ? before.replace(/\s+$/, "") : before;
464
- const match = trimmedEnd.match(/\S+$/);
465
- return match ? trimmedEnd.length - match[0].length : 0;
466
- }
467
- const after = text.slice(pos);
468
- const match = after.match(/^\s*\S+/);
469
- return match ? Math.min(text.length, pos + match[0].length) : text.length;
470
- }
471
-
472
- function resolveHistoryDownTransition({
473
- inputHistory = [],
474
- historyIndex = 0,
475
- currentValue = "",
476
- } = {}) {
477
- const history = Array.isArray(inputHistory) ? inputHistory : [];
478
- if (history.length <= 0) {
479
- return {
480
- moved: false,
481
- nextHistoryIndex: Number.isFinite(historyIndex) ? Math.max(0, Math.floor(historyIndex)) : 0,
482
- nextValue: String(currentValue || ""),
483
- };
484
- }
485
- const currentIndex = Number.isFinite(historyIndex) ? Math.max(0, Math.floor(historyIndex)) : 0;
486
- const nextHistoryIndex = Math.min(history.length, currentIndex + 1);
487
- const nextValue = nextHistoryIndex >= history.length ? "" : String(history[nextHistoryIndex] || "");
488
- const moved = nextHistoryIndex !== currentIndex || nextValue !== String(currentValue || "");
489
- return {
490
- moved,
491
- nextHistoryIndex,
492
- nextValue,
493
- };
494
- }
495
-
496
- function filterSelectableAgents(agents = [], selfSubscriberId = "") {
497
- const selfId = String(selfSubscriberId || "").trim();
498
- const list = Array.isArray(agents) ? agents : [];
499
- if (!selfId) {
500
- return list.filter((agent) => {
501
- const fullId = String(agent && agent.fullId ? agent.fullId : "").trim();
502
- const type = String(agent && agent.type ? agent.type : "").trim();
503
- if (fullId === "ufoo-agent") return false;
504
- if (type === "ufoo-agent") return false;
505
- return true;
506
- });
507
- }
508
- return list.filter((agent) => {
509
- const fullId = String(agent && agent.fullId ? agent.fullId : "").trim();
510
- const type = String(agent && agent.type ? agent.type : "").trim();
511
- if (!fullId) return true;
512
- if (fullId === "ufoo-agent") return false;
513
- if (type === "ufoo-agent") return false;
514
- return fullId !== selfId;
515
- });
516
- }
517
-
518
- function stripLeakedEscapeTags(text = "") {
519
- const source = String(text == null ? "" : text);
520
- const withoutClosedTags = source.replace(/\{[^{}\n]*escape[^{}\n]*\}/gi, "");
521
- const withoutDanglingEscape = withoutClosedTags.replace(/\{\s*\/?\s*escape[\s\S]*$/gi, "");
522
- return withoutDanglingEscape.replace(/\{\s*\/?\s*e?s?c?a?p?e?[^{}\n]*$/gi, "");
523
- }
524
-
525
- function findTrailingEscapeTagPrefix(text = "") {
526
- const raw = String(text == null ? "" : text);
527
- if (!raw) return "";
528
- const windowSize = 40;
529
- const tail = raw.slice(Math.max(0, raw.length - windowSize));
530
- const braceIndex = tail.lastIndexOf("{");
531
- if (braceIndex < 0) return "";
532
- const suffix = tail.slice(braceIndex);
533
- if (suffix.includes("}")) return "";
534
-
535
- const compact = suffix.toLowerCase().replace(/\s+/g, "");
536
- if (!compact.startsWith("{")) return "";
537
- if (/^\{\/?e?s?c?a?p?e?[^}]*$/.test(compact)) {
538
- return suffix;
539
- }
540
- return "";
541
- }
542
-
543
- function createEscapeTagStripper() {
544
- let carry = "";
545
-
546
- return {
547
- write(chunk = "") {
548
- const incoming = String(chunk == null ? "" : chunk);
549
- if (!incoming && !carry) return "";
550
- const combined = `${carry}${incoming}`;
551
- const trailing = findTrailingEscapeTagPrefix(combined);
552
- const safeText = trailing
553
- ? combined.slice(0, combined.length - trailing.length)
554
- : combined;
555
- carry = trailing;
556
- return stripLeakedEscapeTags(safeText);
557
- },
558
- flush() {
559
- if (!carry) return "";
560
- // carry only stores trailing prefixes of escape tags; do not emit it
561
- // to avoid leaking partial markers like "{/escape" at stream end.
562
- const rest = "";
563
- carry = "";
564
- return rest;
565
- },
566
- };
567
- }
568
-
569
- function formatPendingElapsed(ms = 0) {
570
- const totalSeconds = Math.max(0, Math.floor(Number(ms) / 1000));
571
- return `${totalSeconds} s`;
572
- }
573
-
574
- function normalizeBashToolCommand(args = {}, payload = {}) {
575
- const argObj = args && typeof args === "object" ? args : {};
576
- const resObj = payload && typeof payload === "object" ? payload : {};
577
- const command = String(argObj.command || argObj.cmd || "").trim();
578
- const code = Number.isFinite(resObj.code) ? `exit ${resObj.code}` : "";
579
- return [command, code].filter(Boolean).join(" · ");
580
- }
581
-
582
- function normalizeToolMergeEntry(entry = {}) {
583
- const source = entry && typeof entry === "object" ? entry : {};
584
- const tool = String(source.tool || "").trim().toLowerCase() || "tool";
585
- const detail = String(source.detail || "").trim();
586
- const isError = Boolean(source.isError);
587
- const errorText = String(source.errorText || "").trim();
588
- const summary = [tool, detail].filter(Boolean).join(" · ") || tool;
589
- return {
590
- tool,
591
- detail,
592
- isError,
593
- errorText,
594
- summary,
595
- };
596
- }
597
-
598
- function buildMergedToolSummaryText(entries = []) {
599
- const list = Array.isArray(entries)
600
- ? entries.map((item) => normalizeToolMergeEntry(item))
601
- : [];
602
- const count = list.length;
603
- if (count <= 0) return "Ran tool";
604
- const first = list[0];
605
- if (count === 1) return `Ran ${first.summary}`;
606
- const errorCount = list.filter((item) => item.isError).length;
607
- const errorSuffix = errorCount > 0 ? ` · ${errorCount} error${errorCount === 1 ? "" : "s"}` : "";
608
- return `Ran ${first.summary} · … +${count - 1} calls${errorSuffix}`;
609
- }
610
-
611
- function buildMergedToolExpandedLines(entries = []) {
612
- const list = Array.isArray(entries)
613
- ? entries.map((item) => normalizeToolMergeEntry(item))
614
- : [];
615
- const maxLength = 120; // Max length for expanded lines
616
- return list.map((item, index) => {
617
- const base = item.summary;
618
- let line;
619
- if (!item.isError) {
620
- line = base;
621
- } else {
622
- line = item.errorText ? `${base} · error: ${item.errorText}` : `${base} · error`;
623
- }
624
- // Truncate long lines
625
- if (line.length > maxLength) {
626
- return line.slice(0, maxLength - 3) + "...";
627
- }
628
- return line;
629
- });
630
- }
631
-
632
- function runUcodeTui({
633
- stdin = process.stdin,
634
- stdout = process.stdout,
635
- runSingleCommand = () => ({ kind: "empty" }),
636
- runNaturalLanguageTask = async () => ({ ok: true, summary: "ok" }),
637
- runUbusCommand = async () => ({ ok: false, error: "ubus unsupported", summary: "" }),
638
- formatNlResult = () => "ok",
639
- workspaceRoot = process.cwd(),
640
- state = {},
641
- resumeSessionState = () => ({ ok: false, error: "resume unsupported", sessionId: "", restoredMessages: 0 }),
642
- persistSessionState = () => ({ ok: true }),
643
- autoBus = {},
644
- } = {}) {
645
- return new Promise((resolve) => {
646
- const blessed = require("blessed");
647
- const { execFileSync } = require("child_process");
648
- const { createChatLayout } = require("../chat/layout");
649
- const { computeDashboardContent } = require("../chat/dashboardView");
650
- const { escapeBlessed, stripBlessedTags } = require("../chat/text");
651
- const currentSubscriberId = String(process.env.UFOO_SUBSCRIBER_ID || "").trim();
652
- const autoBusEnabled = Boolean(autoBus && autoBus.enabled);
653
- const autoBusSubscriberId = String((autoBus && autoBus.subscriberId) || currentSubscriberId || "").trim();
654
- const getAutoBusPendingCount = typeof (autoBus && autoBus.getPendingCount) === "function"
655
- ? autoBus.getPendingCount
656
- : () => 0;
657
-
658
- let closing = false;
659
- let chain = Promise.resolve();
660
- let statusInterval = null;
661
- let statusIndex = 0;
662
- let activeAgents = [];
663
- let activeAgentMetaMap = new Map();
664
- let targetAgent = null;
665
- let selectedAgentIndex = -1;
666
- let agentListWindowStart = 0;
667
- let agentSelectionMode = false;
668
- let pendingTask = null;
669
- const backgroundTasks = new Map();
670
- let backgroundSeq = 0;
671
- const logRenderState = { inCodeBlock: false };
672
- const inputHistory = [];
673
- let historyIndex = -1;
674
- let activeToolMerge = null;
675
- let lastMergedToolGroup = null;
676
- let toolMergeId = 0;
677
- let cursorPos = 0;
678
- let preferredCol = null;
679
- let currentInputHeight = 4;
680
- const MIN_INPUT_CONTENT_HEIGHT = 1;
681
- const MAX_INPUT_CONTENT_HEIGHT = 8;
682
- const DASHBOARD_HEIGHT = 1;
683
- let autoBusTimer = null;
684
- let autoBusQueued = false;
685
- let autoBusError = "";
686
- const inputMath = require("../chat/inputMath");
687
-
688
- const {
689
- screen,
690
- logBox,
691
- statusLine,
692
- completionPanel,
693
- dashboard,
694
- inputTopLine,
695
- promptBox,
696
- input,
697
- } = createChatLayout({
698
- blessed,
699
- currentInputHeight: 4,
700
- version: UCODE_VERSION,
701
- logBorder: false,
702
- logScrollbar: false,
703
- });
704
-
705
- if (completionPanel && typeof completionPanel.hide === "function") {
706
- completionPanel.hide();
707
- }
708
-
709
- const getAgentTag = (agent) => {
710
- if (!agent) return "";
711
- if (agent.id) return `${agent.type}:${agent.id.slice(0, 6)}`;
712
- return agent.type;
713
- };
714
-
715
- const getAgentLabel = (id) => {
716
- const meta = activeAgentMetaMap.get(id);
717
- if (!meta) return id;
718
- if (meta.nickname) return meta.nickname;
719
- return getAgentTag(meta);
720
- };
721
-
722
- const refreshAgents = () => {
723
- const list = filterSelectableAgents(
724
- loadActiveAgents(workspaceRoot),
725
- currentSubscriberId
726
- );
727
- activeAgents = list.map((agent) => agent.fullId);
728
- activeAgentMetaMap = new Map(list.map((agent) => [agent.fullId, agent]));
729
- if (targetAgent && !activeAgentMetaMap.has(targetAgent)) {
730
- targetAgent = null;
731
- }
732
- selectedAgentIndex = targetAgent ? activeAgents.indexOf(targetAgent) : -1;
733
- };
734
-
735
- const setPrompt = () => {
736
- const content = targetAgent ? `>@${getAgentLabel(targetAgent)}` : ">";
737
- promptBox.setContent(content);
738
- const plain = stripBlessedTags(content);
739
- promptBox.width = Math.max(2, plain.length + 1);
740
- input.left = promptBox.width;
741
- input.width = `100%-${promptBox.width}`;
742
- resizeInput();
743
- };
744
-
745
- // --- Cursor position helpers (mirrors chat inputListenerController) ---
746
- const getInnerWidth = () => {
747
- const promptWidth = typeof promptBox.width === "number" ? promptBox.width : 2;
748
- return inputMath.getInnerWidth({ input, screen, promptWidth });
749
- };
750
-
751
- const getWrapWidth = () => inputMath.getWrapWidth(input, getInnerWidth());
752
-
753
- const resetPreferredCol = () => {
754
- preferredCol = null;
755
- };
756
-
757
- const ensureInputCursorVisible = () => {
758
- const innerWidth = getWrapWidth();
759
- if (innerWidth <= 0) return;
760
- const totalRows = inputMath.countLines(input.value || "", innerWidth, (v) => input.strWidth(v));
761
- const visibleRows = Math.max(1, input.height || 1);
762
- const { row } = inputMath.getCursorRowCol(input.value || "", cursorPos, innerWidth, (v) => input.strWidth(v));
763
- let base = input.childBase || 0;
764
- const maxBase = Math.max(0, totalRows - visibleRows);
765
- if (row < base) base = row;
766
- else if (row >= base + visibleRows) base = row - visibleRows + 1;
767
- if (base > maxBase) base = maxBase;
768
- if (base < 0) base = 0;
769
- if (base !== input.childBase) {
770
- input.childBase = base;
771
- if (typeof input.scrollTo === "function") input.scrollTo(base);
772
- }
773
- };
774
-
775
- const resizeInput = () => {
776
- const innerWidth = getWrapWidth();
777
- if (innerWidth <= 0) return;
778
- const totalRows = inputMath.countLines(input.value || "", innerWidth, (v) => input.strWidth(v));
779
- const contentHeight = Math.min(
780
- MAX_INPUT_CONTENT_HEIGHT,
781
- Math.max(MIN_INPUT_CONTENT_HEIGHT, totalRows)
782
- );
783
- const targetHeight = contentHeight + DASHBOARD_HEIGHT + 2;
784
- if (targetHeight !== currentInputHeight) {
785
- currentInputHeight = targetHeight;
786
- input.height = contentHeight;
787
- promptBox.height = contentHeight;
788
- if (inputTopLine) inputTopLine.bottom = currentInputHeight - 1;
789
- }
790
- statusLine.bottom = currentInputHeight;
791
- logBox.height = Math.max(1, screen.height - currentInputHeight - 1);
792
- ensureInputCursorVisible();
793
- };
794
-
795
- const renderInput = () => {
796
- resizeInput();
797
- ensureInputCursorVisible();
798
- input._updateCursor();
799
- screen.render();
800
- };
801
-
802
- const setCursor = (nextPos) => {
803
- cursorPos = clampCursorPos(nextPos, input.value || "");
804
- ensureInputCursorVisible();
805
- input._updateCursor();
806
- screen.render();
807
- };
808
-
809
- const setInputValue = (value) => {
810
- input.setValue(value || "");
811
- cursorPos = (value || "").length;
812
- resetPreferredCol();
813
- renderInput();
814
- };
815
-
816
- const replaceInputRange = (start, end, replacement = "") => {
817
- const value = input.value || "";
818
- const safeStart = clampCursorPos(start, value);
819
- const safeEnd = clampCursorPos(end, value);
820
- const from = Math.min(safeStart, safeEnd);
821
- const to = Math.max(safeStart, safeEnd);
822
- input.value = value.slice(0, from) + String(replacement || "") + value.slice(to);
823
- cursorPos = from + String(replacement || "").length;
824
- resetPreferredCol();
825
- renderInput();
826
- };
827
-
828
- const insertTextAtCursor = (text = "") => {
829
- const normalized = inputMath.normalizePaste(text);
830
- if (!normalized) return;
831
- replaceInputRange(cursorPos, cursorPos, normalized);
832
- };
833
-
834
- const deleteBeforeCursor = () => {
835
- if (cursorPos <= 0) return;
836
- replaceInputRange(cursorPos - 1, cursorPos, "");
837
- };
838
-
839
- const deleteAtCursor = () => {
840
- const value = input.value || "";
841
- if (cursorPos >= value.length) return;
842
- replaceInputRange(cursorPos, cursorPos + 1, "");
843
- };
844
-
845
- const deleteToBoundary = (boundary) => {
846
- const value = input.value || "";
847
- const innerWidth = getWrapWidth();
848
- const target = boundary === "end"
849
- ? moveCursorToVisualLineBoundary({
850
- cursorPos,
851
- inputValue: value,
852
- width: innerWidth,
853
- boundary: "end",
854
- strWidth: (v) => input.strWidth(v),
855
- })
856
- : moveCursorToVisualLineBoundary({
857
- cursorPos,
858
- inputValue: value,
859
- width: innerWidth,
860
- boundary: "start",
861
- strWidth: (v) => input.strWidth(v),
862
- });
863
- if (target === cursorPos && boundary === "end" && value[cursorPos] === "\n") {
864
- replaceInputRange(cursorPos, cursorPos + 1, "");
865
- return;
866
- }
867
- if (target === cursorPos && boundary === "start" && value[cursorPos - 1] === "\n") {
868
- replaceInputRange(cursorPos - 1, cursorPos, "");
869
- return;
870
- }
871
- replaceInputRange(Math.min(cursorPos, target), Math.max(cursorPos, target), "");
872
- };
873
-
874
- // Override _updateCursor to use our tracked cursorPos
875
- input._updateCursor = function () {
876
- if (this.screen.focused !== this) return;
877
- let lpos;
878
- try { lpos = this._getCoords(); } catch { return; }
879
- if (!lpos) return;
880
- const innerWidth = getWrapWidth();
881
- if (innerWidth <= 0) return;
882
- ensureInputCursorVisible();
883
- const { row, col } = inputMath.getCursorRowCol(this.value || "", cursorPos, innerWidth, (v) => this.strWidth(v));
884
- const scrollOffset = this.childBase || 0;
885
- const displayRow = row - scrollOffset;
886
- const safeCol = Math.min(Math.max(0, col), innerWidth - 1);
887
- const cy = lpos.yi + displayRow;
888
- const cx = lpos.xi + safeCol;
889
- this.screen.program.cup(cy, cx);
890
- this.screen.program.showCursor();
891
- };
892
-
893
- // Override _listener to support cursor-aware editing
894
- let lastKeyRef = null;
895
- let skipSubmitKeyRef = null;
896
- input._listener = function (ch, key) {
897
- const keyName = key && key.name;
898
-
899
- // Dedup: blessed delivers the same key object via element 'keypress' event
900
- // from both readInput's __listener binding and screen's focused.emit('keypress').
901
- // Use object identity to skip the duplicate delivery.
902
- if (key && key === lastKeyRef) return;
903
- lastKeyRef = key || null;
904
-
905
- if (keyName === "escape") return;
906
-
907
- if (keyName === "return" || keyName === "enter") {
908
- const value = this.value || "";
909
- if (key && (key.shift || key.meta)) {
910
- insertTextAtCursor("\n");
911
- skipSubmitKeyRef = key || true;
912
- return;
913
- }
914
- if (cursorPos > 0 && value[cursorPos - 1] === "\\") {
915
- replaceInputRange(cursorPos - 1, cursorPos, "\n");
916
- skipSubmitKeyRef = key || true;
917
- return;
918
- }
919
- return;
920
- }
921
-
922
- // Arrow keys handled by input.key() handlers below
923
- if (keyName === "left" || keyName === "right" || keyName === "up" || keyName === "down") return;
924
-
925
- if (key && key.ctrl) {
926
- if (keyName === "a") {
927
- setCursor(moveCursorToVisualLineBoundary({
928
- cursorPos,
929
- inputValue: this.value || "",
930
- width: getWrapWidth(),
931
- boundary: "start",
932
- strWidth: (v) => this.strWidth(v),
933
- }));
934
- resetPreferredCol();
935
- return;
936
- }
937
- if (keyName === "e") {
938
- setCursor(moveCursorToVisualLineBoundary({
939
- cursorPos,
940
- inputValue: this.value || "",
941
- width: getWrapWidth(),
942
- boundary: "end",
943
- strWidth: (v) => this.strWidth(v),
944
- }));
945
- resetPreferredCol();
946
- return;
947
- }
948
- if (keyName === "b") {
949
- setCursor(moveCursorHorizontally(cursorPos, this.value || "", "left"));
950
- resetPreferredCol();
951
- return;
952
- }
953
- if (keyName === "f") {
954
- setCursor(moveCursorHorizontally(cursorPos, this.value || "", "right"));
955
- resetPreferredCol();
956
- return;
957
- }
958
- if (keyName === "d") {
959
- deleteAtCursor();
960
- return;
961
- }
962
- if (keyName === "h") {
963
- deleteBeforeCursor();
964
- return;
965
- }
966
- if (keyName === "k") {
967
- deleteToBoundary("end");
968
- return;
969
- }
970
- if (keyName === "u") {
971
- deleteToBoundary("start");
972
- return;
973
- }
974
- if (keyName === "w") {
975
- const next = deleteWordBeforeCursor(this.value || "", cursorPos);
976
- this.value = next.value;
977
- cursorPos = next.cursorPos;
978
- resetPreferredCol();
979
- renderInput();
980
- return;
981
- }
982
- }
983
-
984
- if (key && key.meta) {
985
- if (keyName === "b") {
986
- setCursor(moveCursorByWord(this.value || "", cursorPos, "backward"));
987
- resetPreferredCol();
988
- return;
989
- }
990
- if (keyName === "f") {
991
- setCursor(moveCursorByWord(this.value || "", cursorPos, "forward"));
992
- resetPreferredCol();
993
- return;
994
- }
995
- if (keyName === "d") {
996
- const end = moveCursorByWord(this.value || "", cursorPos, "forward");
997
- replaceInputRange(cursorPos, end, "");
998
- return;
999
- }
1000
- }
1001
-
1002
- if (keyName === "backspace") {
1003
- if (key && (key.meta || key.ctrl)) {
1004
- const next = deleteWordBeforeCursor(this.value || "", cursorPos);
1005
- this.value = next.value;
1006
- cursorPos = next.cursorPos;
1007
- resetPreferredCol();
1008
- renderInput();
1009
- } else {
1010
- deleteBeforeCursor();
1011
- }
1012
- return;
1013
- }
1014
-
1015
- if (keyName === "delete") {
1016
- if (key && key.meta) {
1017
- deleteToBoundary("end");
1018
- } else {
1019
- deleteAtCursor();
1020
- }
1021
- return;
1022
- }
1023
-
1024
- if (keyName === "home") {
1025
- setCursor(moveCursorToVisualLineBoundary({
1026
- cursorPos,
1027
- inputValue: this.value || "",
1028
- width: getWrapWidth(),
1029
- boundary: "start",
1030
- strWidth: (v) => this.strWidth(v),
1031
- }));
1032
- resetPreferredCol();
1033
- return;
1034
- }
1035
-
1036
- if (keyName === "end") {
1037
- setCursor(moveCursorToVisualLineBoundary({
1038
- cursorPos,
1039
- inputValue: this.value || "",
1040
- width: getWrapWidth(),
1041
- boundary: "end",
1042
- strWidth: (v) => this.strWidth(v),
1043
- }));
1044
- resetPreferredCol();
1045
- return;
1046
- }
1047
-
1048
- if (ch && ch.length > 1 && (!keyName || keyName.length !== 1)) {
1049
- insertTextAtCursor(ch);
1050
- return;
1051
- }
1052
-
1053
- // Normal character insertion at cursor position
1054
- const insertChar = (ch && ch.length === 1) ? ch : (keyName && keyName.length === 1 ? keyName : null);
1055
- if (insertChar && !/^[\x00-\x08\x0b-\x0c\x0e-\x1f\x7f]$/.test(insertChar)) {
1056
- insertTextAtCursor(insertChar);
1057
- }
1058
- };
1059
-
1060
- const renderDashboard = () => {
1061
- let hint = "No target agents";
1062
- if (activeAgents.length > 0) {
1063
- if (targetAgent) {
1064
- hint = `↓ select ${getAgentLabel(targetAgent)} · ←/→ switch · ↑ clear`;
1065
- } else {
1066
- hint = "↓ select target · ←/→ switch";
1067
- }
1068
- }
1069
- const computed = computeDashboardContent({
1070
- focusMode: "dashboard",
1071
- dashboardView: "agents",
1072
- activeAgents,
1073
- selectedAgentIndex,
1074
- agentListWindowStart,
1075
- maxAgentWindow: 4,
1076
- getAgentLabel,
1077
- dashHints: { agents: hint, agentsEmpty: hint },
1078
- });
1079
- agentListWindowStart = computed.windowStart;
1080
- dashboard.setContent(computed.content);
1081
- screen.render();
1082
- };
1083
-
1084
- const logText = (text = "") => {
1085
- activeToolMerge = null;
1086
- firstToolInGroup = true; // Reset tool group flag when switching back to text
1087
- const sanitized = stripLeakedEscapeTags(text);
1088
- const lines = renderLogLinesWithMarkdown(
1089
- sanitized,
1090
- logRenderState,
1091
- escapeBlessed
1092
- );
1093
- for (const line of lines) {
1094
- logBox.log(line);
1095
- }
1096
- screen.render();
1097
- };
1098
-
1099
- const logUserInput = (text = "") => {
1100
- activeToolMerge = null;
1101
- const line = formatHighlightedUserInput(text, {
1102
- width: resolveLogContentWidth({ logBox, screen, fallback: (stdout && stdout.columns) || 80 }),
1103
- escapeText: escapeBlessed,
1104
- });
1105
- if (!line) return;
1106
- logBox.log(line);
1107
- logBox.log(""); // Add line break after user input
1108
- screen.render();
1109
- };
1110
-
1111
- const logControlAction = (text = "") => {
1112
- activeToolMerge = null;
1113
- const plain = String(text || "").trim();
1114
- if (!plain) return;
1115
- logBox.log(`{gray-fg}⚙{/gray-fg} ${escapeBlessed(plain)}`);
1116
- screen.render();
1117
- };
1118
-
1119
- const summarizeToolDetail = (tool = "", args = {}, payload = {}) => {
1120
- const toolName = String(tool || "").trim().toLowerCase();
1121
- const argObj = args && typeof args === "object" ? args : {};
1122
- const resObj = payload && typeof payload === "object" ? payload : {};
1123
-
1124
- if (toolName === "read") {
1125
- const target = String(resObj.path || argObj.path || argObj.file || "").trim();
1126
- const lineInfo = Number.isFinite(resObj.totalLines) ? `${resObj.totalLines} lines` : "";
1127
- return [target, lineInfo].filter(Boolean).join(" · ");
1128
- }
1129
- if (toolName === "write") {
1130
- const target = String(resObj.path || argObj.path || argObj.file || "").trim();
1131
- const mode = String(resObj.mode || argObj.mode || (argObj.append ? "append" : "overwrite")).trim();
1132
- const bytes = Number.isFinite(resObj.bytes) ? `${resObj.bytes} bytes` : "";
1133
- return [target, mode, bytes].filter(Boolean).join(" · ");
1134
- }
1135
- if (toolName === "edit") {
1136
- const target = String(resObj.path || argObj.path || argObj.file || "").trim();
1137
- const replacements = Number.isFinite(resObj.replacements) ? `${resObj.replacements} replacements` : "";
1138
- return [target, replacements].filter(Boolean).join(" · ");
1139
- }
1140
- if (toolName === "bash") {
1141
- return normalizeBashToolCommand(argObj, resObj);
1142
- }
1143
- return "";
1144
- };
1145
-
1146
- const truncateText = (text = "", maxLength = 80) => {
1147
- const str = String(text || "");
1148
- if (str.length <= maxLength) return str;
1149
- return str.slice(0, maxLength - 3) + "...";
1150
- };
1151
-
1152
- const renderSingleToolEntryLine = (entry = {}) => {
1153
- const item = normalizeToolMergeEntry(entry);
1154
- const marker = item.isError ? "{red-fg}•{/red-fg}" : "{cyan-fg}•{/cyan-fg}";
1155
- const summary = buildMergedToolSummaryText([item]);
1156
- const truncated = truncateText(summary, 100);
1157
- return `${marker} ${escapeBlessed(truncated)}`;
1158
- };
1159
-
1160
- const renderCollapsedToolMergeLine = (entries = []) => {
1161
- const summary = buildMergedToolSummaryText(entries);
1162
- const hasError = entries.some((item) => normalizeToolMergeEntry(item).isError);
1163
- const marker = hasError ? "{red-fg}•{/red-fg}" : "{cyan-fg}•{/cyan-fg}";
1164
- return `${marker} ${escapeBlessed(summary)} {gray-fg}(Ctrl+O expand){/gray-fg}`;
1165
- };
1166
-
1167
- let firstToolInGroup = true;
1168
-
1169
- const logToolHint = (entry = {}, payload = {}) => {
1170
- const tool = String(entry.tool || "").trim().toLowerCase();
1171
- if (!tool) return;
1172
- const resObj = payload && typeof payload === "object" ? payload : {};
1173
- const isError = String(entry.phase || "").trim().toLowerCase() === "error" || resObj.ok === false;
1174
- const detail = summarizeToolDetail(tool, entry.args, resObj);
1175
- const errorText = String(entry.error || resObj.error || "").trim();
1176
-
1177
- const toolEntry = normalizeToolMergeEntry({
1178
- tool,
1179
- detail,
1180
- isError,
1181
- errorText,
1182
- });
1183
-
1184
- if (activeToolMerge) {
1185
- activeToolMerge.entries.push(toolEntry);
1186
- // Only show collapsed format for 2+ tool calls
1187
- if (activeToolMerge.entries.length === 2) {
1188
- // Convert first single line to collapsed format
1189
- logBox.setLine(activeToolMerge.lineIndex, renderCollapsedToolMergeLine(activeToolMerge.entries));
1190
- } else if (activeToolMerge.entries.length > 2) {
1191
- logBox.setLine(activeToolMerge.lineIndex, renderCollapsedToolMergeLine(activeToolMerge.entries));
1192
- }
1193
- if (activeToolMerge.entries.length > 1) {
1194
- lastMergedToolGroup = activeToolMerge;
1195
- }
1196
- } else {
1197
- // Add line break before first tool call
1198
- if (firstToolInGroup) {
1199
- logBox.log("");
1200
- firstToolInGroup = false;
1201
- }
1202
- logBox.log(renderSingleToolEntryLine(toolEntry));
1203
- activeToolMerge = {
1204
- id: ++toolMergeId,
1205
- lineIndex: logBox.getLines().length - 1,
1206
- entries: [toolEntry],
1207
- expanded: false,
1208
- };
1209
- }
1210
- screen.render();
1211
- };
1212
-
1213
- const renderSingleMarkdownLine = (rawLine = "", options = {}) => {
1214
- const preview = Boolean(options.preview);
1215
- const renderState = preview
1216
- ? { inCodeBlock: Boolean(logRenderState.inCodeBlock) }
1217
- : logRenderState;
1218
- const rendered = renderLogLinesWithMarkdown(rawLine, renderState, escapeBlessed);
1219
- return rendered[0] || "";
1220
- };
1221
-
1222
- const createNlStreamState = () => {
1223
- activeToolMerge = null;
1224
- firstToolInGroup = true; // Reset flag for new response
1225
- logBox.log(""); // Add empty line to start the response
1226
- return {
1227
- lineIndex: logBox.getLines().length - 1,
1228
- buffer: "",
1229
- full: "",
1230
- seenVisibleContent: false,
1231
- };
1232
- };
1233
-
1234
- const appendNlStreamDelta = (streamState, delta) => {
1235
- if (!streamState) return;
1236
- const chunk = stripLeakedEscapeTags(String(delta || ""));
1237
- if (!chunk) return;
1238
-
1239
- streamState.full += chunk;
1240
- streamState.buffer += chunk;
1241
-
1242
- const parts = streamState.buffer.split("\n");
1243
- if (parts.length > 1) {
1244
- const completed = parts.slice(0, -1);
1245
- for (const line of completed) {
1246
- const hasVisible = /[^\s]/.test(line);
1247
- if (!streamState.seenVisibleContent && !hasVisible) {
1248
- continue;
1249
- }
1250
- if (hasVisible) {
1251
- streamState.seenVisibleContent = true;
1252
- }
1253
- const rendered = renderSingleMarkdownLine(line);
1254
- logBox.setLine(streamState.lineIndex, rendered);
1255
- logBox.pushLine("");
1256
- streamState.lineIndex = logBox.getLines().length - 1;
1257
- }
1258
- streamState.buffer = parts[parts.length - 1];
1259
- }
1260
-
1261
- const previewHasVisible = /[^\s]/.test(streamState.buffer);
1262
- if (!streamState.seenVisibleContent && !previewHasVisible) {
1263
- return;
1264
- }
1265
- if (previewHasVisible) {
1266
- streamState.seenVisibleContent = true;
1267
- }
1268
- const previewLine = renderSingleMarkdownLine(streamState.buffer, { preview: true });
1269
- logBox.setLine(streamState.lineIndex, previewLine);
1270
- screen.render();
1271
- };
1272
-
1273
- const finalizeNlStream = (streamState) => {
1274
- if (!streamState) return { lastChar: "" };
1275
- streamState.buffer = stripLeakedEscapeTags(streamState.buffer);
1276
- const rendered = renderSingleMarkdownLine(streamState.buffer);
1277
- logBox.setLine(streamState.lineIndex, rendered);
1278
- screen.render();
1279
- const full = String(streamState.full || "");
1280
- return { lastChar: full ? full.charAt(full.length - 1) : "" };
1281
- };
1282
-
1283
- const updateStatus = (message = "", type = "thinking", options = {}) => {
1284
- const getBackgroundSuffix = () => {
1285
- if (!backgroundTasks || backgroundTasks.size === 0) return "";
1286
- let running = 0;
1287
- let done = 0;
1288
- let failed = 0;
1289
- for (const task of backgroundTasks.values()) {
1290
- const status = String(task && task.status || "").trim().toLowerCase();
1291
- if (status === "running") running += 1;
1292
- else if (status === "done") done += 1;
1293
- else if (status === "failed") failed += 1;
1294
- }
1295
- const total = running + done + failed;
1296
- if (total <= 0) return "";
1297
- return ` · BG ${running}/${done}/${failed}`;
1298
- };
1299
- if (statusInterval) {
1300
- clearInterval(statusInterval);
1301
- statusInterval = null;
1302
- }
1303
- if (!message) {
1304
- statusLine.setContent(escapeBlessed(`UCODE · Ready · Enter send · Shift/Alt+Enter newline · PgUp/PgDn log · Ctrl+O tools${getBackgroundSuffix()}`));
1305
- screen.render();
1306
- return;
1307
- }
1308
- const showTimer = Boolean(options.showTimer);
1309
- const startedAt = Number.isFinite(options.startedAt) ? options.startedAt : Date.now();
1310
- const indicators = STATUS_INDICATORS[type] || STATUS_INDICATORS.thinking;
1311
- statusIndex = 0;
1312
- const draw = () => {
1313
- const indicator = indicators[statusIndex % indicators.length];
1314
- const timerText = showTimer
1315
- ? ` (${formatPendingElapsed(Date.now() - startedAt)},esc cancel)`
1316
- : "";
1317
- statusLine.setContent(escapeBlessed(`${indicator} ${message}${timerText}${getBackgroundSuffix()}`));
1318
- statusIndex += 1;
1319
- screen.render();
1320
- };
1321
- draw();
1322
- if (type !== "none") {
1323
- statusInterval = setInterval(draw, 100);
1324
- }
1325
- };
1326
-
1327
- const closeWithCode = (code = 0) => {
1328
- if (closing) return;
1329
- closing = true;
1330
- if (autoBusTimer) {
1331
- clearInterval(autoBusTimer);
1332
- autoBusTimer = null;
1333
- }
1334
- if (statusInterval) {
1335
- clearInterval(statusInterval);
1336
- statusInterval = null;
1337
- }
1338
- if (pendingTask && pendingTask.abortController && !pendingTask.abortController.signal.aborted) {
1339
- try {
1340
- pendingTask.abortController.abort();
1341
- } catch {
1342
- // ignore
1343
- }
1344
- }
1345
- try {
1346
- screen.destroy();
1347
- } catch {
1348
- // ignore
1349
- }
1350
- resolve({ code });
1351
- };
1352
-
1353
- const runAutoBusOnce = async () => {
1354
- if (!autoBusEnabled || closing || pendingTask) return;
1355
- if (Number(getAutoBusPendingCount()) <= 0) {
1356
- autoBusError = "";
1357
- return;
1358
- }
1359
-
1360
- // Set pending state for autoBus tasks
1361
- const abortController = new AbortController();
1362
- pendingTask = {
1363
- abortController,
1364
- startedAt: Date.now(),
1365
- };
1366
- updateStatus("Processing bus messages...", "thinking", {
1367
- showTimer: true,
1368
- startedAt: pendingTask.startedAt,
1369
- });
1370
-
1371
- try {
1372
- const ubusResult = await runUbusCommand(state, {
1373
- workspaceRoot,
1374
- subscriberId: autoBusSubscriberId,
1375
- signal: abortController.signal,
1376
- onMessageReceived: (msg) => {
1377
- // Display the incoming message immediately
1378
- const { extractAgentNickname } = require("./agent");
1379
- const nickname = extractAgentNickname(msg.from) || msg.from;
1380
- logText(`${nickname}: ${msg.task}`);
1381
- // Update status to show we're working on this specific task
1382
- updateStatus("Working on task...", "thinking", {
1383
- showTimer: true,
1384
- startedAt: pendingTask.startedAt,
1385
- });
1386
- },
1387
- });
1388
-
1389
- if (!ubusResult.ok) {
1390
- const nextError = String(ubusResult.error || "ubus failed");
1391
- if (nextError !== autoBusError) {
1392
- autoBusError = nextError;
1393
- logText(`Error: ${nextError}`);
1394
- }
1395
- return;
1396
- }
1397
- autoBusError = "";
1398
- if (ubusResult.handled > 0) {
1399
- // Display only the replies (tasks were already shown via onMessageReceived)
1400
- if (ubusResult.messageExchanges && ubusResult.messageExchanges.length > 0) {
1401
- const { extractAgentNickname } = require("./agent");
1402
- for (const exchange of ubusResult.messageExchanges) {
1403
- const nickname = extractAgentNickname(exchange.from) || exchange.from;
1404
- // Only show the reply since task was already displayed
1405
- logText(`@${nickname} ${exchange.reply}`);
1406
- }
1407
- }
1408
- const persisted = persistSessionState(state);
1409
- if (!persisted || persisted.ok === false) {
1410
- logText(`Error: failed to persist session ${state.sessionId}: ${(persisted && persisted.error) || "unknown error"}`);
1411
- }
1412
- }
1413
- } finally {
1414
- // Clear pending state
1415
- pendingTask = null;
1416
- updateStatus("", "none");
1417
- }
1418
- };
1419
-
1420
- const scheduleAutoBus = () => {
1421
- if (!autoBusEnabled || closing || autoBusQueued || pendingTask) return;
1422
- if (Number(getAutoBusPendingCount()) <= 0) return;
1423
- autoBusQueued = true;
1424
- chain = chain
1425
- .then(() => runAutoBusOnce())
1426
- .catch(() => {})
1427
- .finally(() => {
1428
- autoBusQueued = false;
1429
- });
1430
- };
1431
-
1432
- const resolveTargetToken = (token = "") => {
1433
- const text = String(token || "").trim();
1434
- if (!text) return "";
1435
-
1436
- if (text.includes(":")) {
1437
- const match = activeAgents.find((id) => id === text || id.startsWith(text));
1438
- if (match) return match;
1439
- }
1440
-
1441
- const normalized = text.toLowerCase();
1442
- for (const id of activeAgents) {
1443
- const meta = activeAgentMetaMap.get(id);
1444
- if (!meta) continue;
1445
- const nick = String(meta.nickname || "").toLowerCase();
1446
- if (nick && (nick === normalized || nick.startsWith(normalized))) return id;
1447
- }
1448
- return "";
1449
- };
1450
-
1451
- const executeLine = async (line) => {
1452
- const normalizedLine = String(line || "").replace(/\r?\n/g, " ").trim();
1453
- if (!normalizedLine) return;
1454
- logUserInput(normalizedLine);
1455
-
1456
- refreshAgents();
1457
-
1458
- let actualLine = normalizedLine;
1459
- let isBusMessage = false;
1460
-
1461
- if (targetAgent) {
1462
- isBusMessage = true;
1463
- }
1464
-
1465
- const mentionMatch = normalizedLine.match(/^@(\S+)\s+(.+)$/);
1466
- if (mentionMatch) {
1467
- const [, token, message] = mentionMatch;
1468
- const resolved = resolveTargetToken(token);
1469
- if (resolved) {
1470
- isBusMessage = true;
1471
- actualLine = message;
1472
- targetAgent = resolved;
1473
- selectedAgentIndex = activeAgents.indexOf(resolved);
1474
- setPrompt();
1475
- renderDashboard();
1476
- }
1477
- }
1478
-
1479
- if (isBusMessage && targetAgent) {
1480
- updateStatus("Sending message...", "typing");
1481
- try {
1482
- execFileSync("ufoo", ["bus", "send", targetAgent, actualLine], {
1483
- cwd: workspaceRoot,
1484
- encoding: "utf8",
1485
- });
1486
- updateStatus("", "none");
1487
- logText(`✓ Message sent to ${getAgentLabel(targetAgent)}`);
1488
- } catch (err) {
1489
- updateStatus("", "none");
1490
- const msg = err && err.message ? err.message : "unknown error";
1491
- logText(`Failed to send message: ${msg}`);
1492
- }
1493
- targetAgent = null;
1494
- selectedAgentIndex = -1;
1495
- agentSelectionMode = false;
1496
- setPrompt();
1497
- renderDashboard();
1498
- return;
1499
- }
1500
-
1501
- const runtimeWorkspace = String((state && state.workspaceRoot) || workspaceRoot || process.cwd());
1502
- const result = runSingleCommand(actualLine, runtimeWorkspace);
1503
- if (result.kind === "empty") return;
1504
- if (result.kind === "exit") {
1505
- closeWithCode(0);
1506
- return;
1507
- }
1508
- if (result.kind === "tool") {
1509
- const payload = result.result && typeof result.result === "object" ? result.result : {};
1510
- logToolHint({
1511
- tool: result.tool,
1512
- args: result.args,
1513
- phase: payload.ok === false ? "error" : "end",
1514
- error: payload.error || "",
1515
- }, payload);
1516
- return;
1517
- }
1518
- if (result.kind === "probe") {
1519
- return;
1520
- }
1521
- if (result.kind === "help" || result.kind === "error") {
1522
- logText(result.output || "");
1523
- return;
1524
- }
1525
- if (result.kind === "ubus") {
1526
- updateStatus("Checking bus messages...", "typing");
1527
- const ubusResult = await runUbusCommand(state, {
1528
- workspaceRoot,
1529
- onMessageReceived: (msg) => {
1530
- // Display the incoming message immediately
1531
- const { extractAgentNickname } = require("./agent");
1532
- const nickname = extractAgentNickname(msg.from) || msg.from;
1533
- logText(`${nickname}: ${msg.task}`);
1534
- },
1535
- });
1536
- updateStatus("", "none");
1537
- if (!ubusResult.ok) {
1538
- logText(`Error: ${ubusResult.error}`);
1539
- return;
1540
- }
1541
-
1542
- // Display only the replies (tasks were already shown via onMessageReceived)
1543
- if (ubusResult.messageExchanges && ubusResult.messageExchanges.length > 0) {
1544
- const { extractAgentNickname } = require("./agent");
1545
- for (const exchange of ubusResult.messageExchanges) {
1546
- const nickname = extractAgentNickname(exchange.from) || exchange.from;
1547
- // Only show the reply since task was already displayed
1548
- logText(`@${nickname} ${exchange.reply}`);
1549
- }
1550
- } else if (ubusResult.handled === 0) {
1551
- logText("ubus: no pending messages.");
1552
- }
1553
- const persisted = persistSessionState(state);
1554
- if (!persisted || persisted.ok === false) {
1555
- logText(`Error: failed to persist session ${state.sessionId}: ${(persisted && persisted.error) || "unknown error"}`);
1556
- }
1557
- return;
1558
- }
1559
- if (result.kind === "resume") {
1560
- const resumed = resumeSessionState(state, result.sessionId, workspaceRoot);
1561
- if (!resumed.ok) {
1562
- logText(`Error: ${resumed.error}`);
1563
- return;
1564
- }
1565
- logText(`Resumed session ${resumed.sessionId} (${resumed.restoredMessages} messages).`);
1566
- return;
1567
- }
1568
-
1569
- if (result.kind === "nl_bg") {
1570
- backgroundSeq += 1;
1571
- const jobId = `bg-${Date.now().toString(36)}-${backgroundSeq.toString(36)}`;
1572
- const taskRecord = {
1573
- id: jobId,
1574
- task: result.task,
1575
- status: "running",
1576
- startedAt: Date.now(),
1577
- summary: "",
1578
- };
1579
- backgroundTasks.set(jobId, taskRecord);
1580
- updateStatus("", "none");
1581
- logText(`[${jobId}] started in background.`);
1582
-
1583
- const bgState = {
1584
- workspaceRoot: state.workspaceRoot,
1585
- provider: state.provider,
1586
- model: state.model,
1587
- engine: state.engine,
1588
- context: state.context,
1589
- nlMessages: Array.isArray(state.nlMessages) ? state.nlMessages.slice() : [],
1590
- sessionId: "",
1591
- timeoutMs: state.timeoutMs,
1592
- jsonOutput: false,
1593
- };
1594
-
1595
- Promise.resolve()
1596
- .then(() => runNaturalLanguageTask(result.task, bgState))
1597
- .then((nlResult) => {
1598
- taskRecord.status = nlResult && nlResult.ok ? "done" : "failed";
1599
- taskRecord.finishedAt = Date.now();
1600
- taskRecord.summary = String(formatNlResult(nlResult, false) || "").trim();
1601
- const title = taskRecord.status === "done" ? "done" : "failed";
1602
- logText(`[${jobId}] ${title}: ${taskRecord.summary || "no summary"}`);
1603
- })
1604
- .catch((err) => {
1605
- taskRecord.status = "failed";
1606
- taskRecord.finishedAt = Date.now();
1607
- taskRecord.summary = err && err.message ? String(err.message) : "background task failed";
1608
- logText(`[${jobId}] failed: ${taskRecord.summary}`);
1609
- })
1610
- .finally(() => {
1611
- updateStatus("", "none");
1612
- screen.render();
1613
- });
1614
- return;
1615
- }
1616
-
1617
- if (result.kind === "nl") {
1618
- const abortController = new AbortController();
1619
- const escapeStripper = createEscapeTagStripper();
1620
- pendingTask = {
1621
- abortController,
1622
- startedAt: Date.now(),
1623
- };
1624
- const TOOL_LABELS = {
1625
- read: "Reading file",
1626
- write: "Writing file",
1627
- edit: "Editing file",
1628
- bash: "Running command",
1629
- };
1630
- const setNlStatus = (msg) => {
1631
- updateStatus(msg, "thinking", {
1632
- showTimer: true,
1633
- startedAt: pendingTask.startedAt,
1634
- });
1635
- };
1636
- setNlStatus("Waiting for model...");
1637
- let streamState = null;
1638
- let renderedToolLogCount = 0;
1639
- let nlResult = null;
1640
- try {
1641
- nlResult = await runNaturalLanguageTask(result.task, state, {
1642
- signal: abortController.signal,
1643
- onPhase: (event) => {
1644
- if (!event || typeof event !== "object") return;
1645
- if (event.type === "request_start") {
1646
- setNlStatus("Waiting for model...");
1647
- } else if (event.type === "thinking_delta") {
1648
- setNlStatus("Thinking...");
1649
- } else if (event.type === "text_delta") {
1650
- setNlStatus("Generating response...");
1651
- } else if (event.type === "tool_request") {
1652
- const label = TOOL_LABELS[String(event.name || "").toLowerCase()] || `Calling ${event.name}`;
1653
- setNlStatus(`${label}...`);
1654
- }
1655
- },
1656
- onDelta: (delta) => {
1657
- const text = escapeStripper.write(String(delta || ""));
1658
- if (!text) return;
1659
- if (!streamState) {
1660
- streamState = createNlStreamState();
1661
- }
1662
- appendNlStreamDelta(streamState, text);
1663
- },
1664
- onToolLog: (entry) => {
1665
- renderedToolLogCount += 1;
1666
- if (entry && entry.tool && entry.phase === "start") {
1667
- const label = TOOL_LABELS[String(entry.tool || "").toLowerCase()] || `Calling ${entry.tool}`;
1668
- setNlStatus(`${label}...`);
1669
- }
1670
- logToolHint(entry);
1671
- },
1672
- });
1673
- const tail = escapeStripper.flush();
1674
- if (tail) {
1675
- if (!streamState) {
1676
- streamState = createNlStreamState();
1677
- }
1678
- appendNlStreamDelta(streamState, tail);
1679
- }
1680
- let finalStreamInfo = { lastChar: "" };
1681
- if (streamState) {
1682
- finalStreamInfo = finalizeNlStream(streamState);
1683
- }
1684
- if (Array.isArray(nlResult && nlResult.logs) && nlResult.logs.length > renderedToolLogCount) {
1685
- for (const entry of nlResult.logs.slice(renderedToolLogCount)) {
1686
- logToolHint(entry);
1687
- }
1688
- }
1689
- const streamed = Boolean(nlResult && nlResult.streamed);
1690
- const hasVisibleStreamText = Boolean(
1691
- streamState
1692
- && typeof streamState.full === "string"
1693
- && /[^\s]/.test(streamState.full)
1694
- );
1695
- const streamLastChar = nlResult && typeof nlResult.streamLastChar === "string"
1696
- ? nlResult.streamLastChar.slice(-1)
1697
- : finalStreamInfo.lastChar;
1698
- if (streamed && hasVisibleStreamText && streamLastChar !== "\n") {
1699
- logBox.log("");
1700
- screen.render();
1701
- }
1702
- const shouldSkipSummary = Boolean(streamed && nlResult && nlResult.ok && hasVisibleStreamText);
1703
- if (!shouldSkipSummary) {
1704
- logText(formatNlResult(nlResult, false));
1705
- }
1706
- const persisted = persistSessionState(state);
1707
- if (!persisted || persisted.ok === false) {
1708
- logText(`Error: failed to persist session ${state.sessionId}: ${(persisted && persisted.error) || "unknown error"}`);
1709
- }
1710
- } finally {
1711
- pendingTask = null;
1712
- updateStatus("", "none");
1713
- }
1714
- }
1715
- };
1716
-
1717
- const submitInput = (value = "") => {
1718
- const raw = String(value || "");
1719
- const trimmed = raw.trim();
1720
- input.setValue("");
1721
- cursorPos = 0;
1722
- resetPreferredCol();
1723
- resizeInput();
1724
- screen.render();
1725
- agentSelectionMode = false;
1726
-
1727
- if (trimmed) {
1728
- inputHistory.push(trimmed);
1729
- }
1730
- historyIndex = inputHistory.length;
1731
-
1732
- chain = chain
1733
- .then(() => executeLine(raw))
1734
- .catch((err) => {
1735
- updateStatus("", "none");
1736
- logText(`Error: ${err && err.message ? err.message : "agent loop failed"}`);
1737
- })
1738
- .finally(() => {
1739
- if (closing) return;
1740
- refreshAgents();
1741
- setPrompt();
1742
- renderDashboard();
1743
- input.focus();
1744
- screen.render();
1745
- });
1746
- };
1747
-
1748
- input.key(["enter"], (ch, key) => {
1749
- if (skipSubmitKeyRef && (!key || skipSubmitKeyRef === key || skipSubmitKeyRef === true)) {
1750
- skipSubmitKeyRef = null;
1751
- return false;
1752
- }
1753
- submitInput(input.getValue());
1754
- return false;
1755
- });
1756
- input.key(["up"], () => {
1757
- const currentValue = input.getValue();
1758
- if (shouldClearAgentSelectionOnUp({
1759
- agentSelectionMode,
1760
- inputValue: currentValue,
1761
- })) {
1762
- targetAgent = null;
1763
- selectedAgentIndex = -1;
1764
- agentSelectionMode = false;
1765
- setPrompt();
1766
- renderDashboard();
1767
- // Target selection cleared - removed redundant log
1768
- input.focus();
1769
- return false;
1770
- }
1771
- if (currentValue) {
1772
- const move = moveCursorVertically({
1773
- cursorPos,
1774
- inputValue: currentValue,
1775
- width: getWrapWidth(),
1776
- direction: "up",
1777
- preferredCol,
1778
- strWidth: (v) => input.strWidth(v),
1779
- });
1780
- preferredCol = move.preferredCol;
1781
- if (move.moved) {
1782
- setCursor(move.nextCursorPos);
1783
- return false;
1784
- }
1785
- }
1786
- if (inputHistory.length === 0) return false;
1787
- historyIndex = Math.max(0, historyIndex - 1);
1788
- setInputValue(inputHistory[historyIndex] || "");
1789
- return false;
1790
- });
1791
- input.key(["down"], () => {
1792
- const currentValue = input.getValue();
1793
- if (currentValue) {
1794
- const move = moveCursorVertically({
1795
- cursorPos,
1796
- inputValue: currentValue,
1797
- width: getWrapWidth(),
1798
- direction: "down",
1799
- preferredCol,
1800
- strWidth: (v) => input.strWidth(v),
1801
- });
1802
- preferredCol = move.preferredCol;
1803
- if (move.moved) {
1804
- setCursor(move.nextCursorPos);
1805
- return false;
1806
- }
1807
- }
1808
- const historyTransition = resolveHistoryDownTransition({
1809
- inputHistory,
1810
- historyIndex,
1811
- currentValue,
1812
- });
1813
- if (historyTransition.moved) {
1814
- historyIndex = historyTransition.nextHistoryIndex;
1815
- setInputValue(historyTransition.nextValue);
1816
- return false;
1817
- }
1818
-
1819
- if (shouldEnterAgentSelection(currentValue)) {
1820
- const cachedAgents = Array.isArray(activeAgents) ? activeAgents.slice() : [];
1821
- const cachedMeta = activeAgentMetaMap instanceof Map ? new Map(activeAgentMetaMap) : new Map();
1822
- if (!agentSelectionMode) {
1823
- refreshAgents();
1824
- }
1825
- if (!agentSelectionMode && activeAgents.length === 0 && cachedAgents.length > 0) {
1826
- activeAgents = cachedAgents;
1827
- activeAgentMetaMap = cachedMeta;
1828
- }
1829
- const decision = resolveAgentSelectionOnDown({
1830
- agentSelectionMode,
1831
- selectedAgentIndex,
1832
- totalAgents: activeAgents.length,
1833
- });
1834
- if (decision.action === "enter") {
1835
- selectedAgentIndex = decision.index;
1836
- targetAgent = activeAgents[selectedAgentIndex];
1837
- agentSelectionMode = true;
1838
- setPrompt();
1839
- renderDashboard();
1840
- // Removed redundant target selection log
1841
- input.focus();
1842
- return false;
1843
- }
1844
- if (decision.action === "hold") {
1845
- return false;
1846
- }
1847
- }
1848
- return false;
1849
- });
1850
- input.key(["left"], () => {
1851
- const currentValue = input.getValue();
1852
- if (agentSelectionMode && shouldEnterAgentSelection(currentValue)) {
1853
- if (activeAgents.length === 0) refreshAgents();
1854
- if (activeAgents.length === 0) return false;
1855
- selectedAgentIndex = cycleAgentSelectionIndex(selectedAgentIndex, activeAgents.length, "left");
1856
- targetAgent = activeAgents[selectedAgentIndex];
1857
- setPrompt();
1858
- renderDashboard();
1859
- // Removed redundant target switch log
1860
- input.focus();
1861
- return false;
1862
- }
1863
- const next = moveCursorHorizontally(cursorPos, currentValue, "left");
1864
- if (next !== cursorPos) {
1865
- setCursor(next);
1866
- resetPreferredCol();
1867
- }
1868
- return false;
1869
- });
1870
- input.key(["right"], () => {
1871
- const currentValue = input.getValue();
1872
- if (agentSelectionMode && shouldEnterAgentSelection(currentValue)) {
1873
- if (activeAgents.length === 0) refreshAgents();
1874
- if (activeAgents.length === 0) return false;
1875
- selectedAgentIndex = cycleAgentSelectionIndex(selectedAgentIndex, activeAgents.length, "right");
1876
- targetAgent = activeAgents[selectedAgentIndex];
1877
- setPrompt();
1878
- renderDashboard();
1879
- // Removed redundant target switch log
1880
- input.focus();
1881
- return false;
1882
- }
1883
- const next = moveCursorHorizontally(cursorPos, currentValue, "right");
1884
- if (next !== cursorPos) {
1885
- setCursor(next);
1886
- resetPreferredCol();
1887
- }
1888
- return false;
1889
- });
1890
-
1891
- screen.key(["tab"], () => {
1892
- refreshAgents();
1893
- if (activeAgents.length === 0) return;
1894
- if (selectedAgentIndex < 0) selectedAgentIndex = 0;
1895
- else selectedAgentIndex = (selectedAgentIndex + 1) % activeAgents.length;
1896
- targetAgent = activeAgents[selectedAgentIndex];
1897
- agentSelectionMode = true;
1898
- setPrompt();
1899
- renderDashboard();
1900
- // Removed redundant target switch log
1901
- input.focus();
1902
- });
1903
- screen.key(["S-tab"], () => {
1904
- refreshAgents();
1905
- if (activeAgents.length === 0) return;
1906
- if (selectedAgentIndex < 0) selectedAgentIndex = 0;
1907
- else selectedAgentIndex = (selectedAgentIndex - 1 + activeAgents.length) % activeAgents.length;
1908
- targetAgent = activeAgents[selectedAgentIndex];
1909
- agentSelectionMode = true;
1910
- setPrompt();
1911
- renderDashboard();
1912
- // Removed redundant target switch log
1913
- input.focus();
1914
- });
1915
- screen.key(["C-o"], () => {
1916
- if (!lastMergedToolGroup || lastMergedToolGroup.expanded) return;
1917
- if (!Array.isArray(lastMergedToolGroup.entries) || lastMergedToolGroup.entries.length < 2) return;
1918
- const lines = buildMergedToolExpandedLines(lastMergedToolGroup.entries);
1919
- for (let i = 0; i < lines.length; i += 1) {
1920
- const branch = i === lines.length - 1 ? "└" : "│";
1921
- logBox.log(`{gray-fg}${branch}{/gray-fg} ${escapeBlessed(lines[i])}`);
1922
- }
1923
- lastMergedToolGroup.expanded = true;
1924
- if (activeToolMerge && activeToolMerge.id === lastMergedToolGroup.id) {
1925
- activeToolMerge = null;
1926
- }
1927
- screen.render();
1928
- });
1929
- screen.key(["pageup"], () => {
1930
- logBox.scroll(-Math.max(1, Math.floor((logBox.height || 10) / 2)));
1931
- screen.render();
1932
- });
1933
- screen.key(["pagedown"], () => {
1934
- logBox.scroll(Math.max(1, Math.floor((logBox.height || 10) / 2)));
1935
- screen.render();
1936
- });
1937
- input.key(["escape"], () => {
1938
- if (pendingTask && pendingTask.abortController && !pendingTask.abortController.signal.aborted) {
1939
- try {
1940
- pendingTask.abortController.abort();
1941
- } catch {
1942
- // ignore
1943
- }
1944
- logControlAction("Cancellation requested. Stopping the current task...");
1945
- updateStatus("Cancelling...", "waiting", {
1946
- showTimer: true,
1947
- startedAt: pendingTask.startedAt,
1948
- });
1949
- return false;
1950
- }
1951
- targetAgent = null;
1952
- selectedAgentIndex = -1;
1953
- agentSelectionMode = false;
1954
- setInputValue("");
1955
- setPrompt();
1956
- renderDashboard();
1957
- // Target selection cleared - removed redundant log
1958
- input.focus();
1959
- return false;
1960
- });
1961
- screen.key(["C-c"], () => closeWithCode(0));
1962
- screen.on("resize", () => {
1963
- renderDashboard();
1964
- screen.render();
1965
- });
1966
-
1967
- const nickname = process.env.UFOO_NICKNAME || "";
1968
- const subscriberId = currentSubscriberId;
1969
- const agentId = subscriberId.includes(":") ? subscriberId.split(":")[1] : "";
1970
- const bannerLines = buildUcodeBannerBlessedLines({
1971
- model: state.model || process.env.UFOO_UCODE_MODEL || "",
1972
- engine: state.engine || "ufoo-core",
1973
- nickname,
1974
- agentId,
1975
- workspaceRoot,
1976
- sessionId: state.sessionId || "",
1977
- width: (stdout && stdout.columns) || 80,
1978
- });
1979
- for (const line of bannerLines) {
1980
- logBox.log(String(line || ""));
1981
- }
1982
- logBox.log("");
3
+ const {
4
+ STATUS_INDICATORS,
5
+ StreamBuffer,
6
+ UCODE_BANNER_LINES,
7
+ UCODE_VERSION,
8
+ buildMergedToolExpandedLines,
9
+ buildMergedToolSummaryText,
10
+ buildUcodeBannerLines,
11
+ clampCursorPos,
12
+ createEscapeTagStripper,
13
+ cycleAgentSelectionIndex,
14
+ deleteWordBeforeCursor,
15
+ displayCellWidth,
16
+ filterSelectableAgents,
17
+ findLogicalLineEnd,
18
+ findLogicalLineStart,
19
+ formatPendingElapsed,
20
+ loadActiveAgents,
21
+ moveCursorByWord,
22
+ moveCursorHorizontally,
23
+ moveCursorToVisualLineBoundary,
24
+ moveCursorVertically,
25
+ normalizeBashToolCommand,
26
+ normalizeToolMergeEntry,
27
+ parseActiveAgentsFromBusStatus,
28
+ renderLogLinesWithMarkdown,
29
+ resolveAgentSelectionOnDown,
30
+ resolveHistoryDownTransition,
31
+ shouldClearAgentSelectionOnUp,
32
+ shouldEnterAgentSelection,
33
+ shouldUseUcodeTui,
34
+ stripLeakedEscapeTags,
35
+ } = fmt;
1983
36
 
1984
- refreshAgents();
1985
- setPrompt();
1986
- updateStatus("", "none");
1987
- renderDashboard();
1988
- if (autoBusEnabled) {
1989
- autoBusTimer = setInterval(() => {
1990
- scheduleAutoBus();
1991
- }, 800);
1992
- scheduleAutoBus();
1993
- }
1994
- input.focus();
1995
- screen.render();
1996
- });
37
+ function runUcodeTui(props = {}) {
38
+ const { runUcodeInkTui } = require("../ui/ink/UcodeApp");
39
+ return runUcodeInkTui(props);
1997
40
  }
1998
41
 
1999
42
  module.exports = {
43
+ STATUS_INDICATORS,
2000
44
  UCODE_BANNER_LINES,
2001
45
  UCODE_VERSION,
2002
46
  StreamBuffer,
2003
47
  displayCellWidth,
2004
- resolveLogContentWidth,
2005
- formatHighlightedUserInput,
2006
48
  buildUcodeBannerLines,
2007
- buildUcodeBannerBlessedLines,
2008
49
  parseActiveAgentsFromBusStatus,
2009
50
  shouldUseUcodeTui,
2010
51
  renderLogLinesWithMarkdown,
@@ -2029,5 +70,6 @@ module.exports = {
2029
70
  normalizeToolMergeEntry,
2030
71
  buildMergedToolSummaryText,
2031
72
  buildMergedToolExpandedLines,
73
+ loadActiveAgents,
2032
74
  runUcodeTui,
2033
75
  };