u-foo 1.0.6 → 1.2.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 (149) hide show
  1. package/README.md +247 -23
  2. package/SKILLS/ufoo/SKILL.md +17 -2
  3. package/SKILLS/uinit/SKILL.md +8 -3
  4. package/bin/ucode-core.js +15 -0
  5. package/bin/ucode.js +125 -0
  6. package/bin/ufoo-assistant-agent.js +5 -0
  7. package/bin/ufoo-engine.js +25 -0
  8. package/bin/ufoo.js +4 -0
  9. package/modules/AGENTS.template.md +14 -4
  10. package/modules/bus/README.md +8 -5
  11. package/modules/bus/SKILLS/ubus/SKILL.md +5 -4
  12. package/modules/context/SKILLS/uctx/SKILL.md +3 -1
  13. package/modules/online/SKILLS/ufoo-online/SKILL.md +144 -0
  14. package/package.json +12 -3
  15. package/scripts/import-pi-mono.js +124 -0
  16. package/scripts/postinstall.js +20 -49
  17. package/scripts/sync-claude-skills.sh +21 -0
  18. package/src/agent/cliRunner.js +524 -31
  19. package/src/agent/internalRunner.js +76 -9
  20. package/src/agent/launcher.js +97 -45
  21. package/src/agent/normalizeOutput.js +1 -1
  22. package/src/agent/notifier.js +144 -4
  23. package/src/agent/ptyRunner.js +480 -10
  24. package/src/agent/ptyWrapper.js +28 -3
  25. package/src/agent/readyDetector.js +16 -0
  26. package/src/agent/ucode.js +443 -0
  27. package/src/agent/ucodeBootstrap.js +113 -0
  28. package/src/agent/ucodeBuild.js +67 -0
  29. package/src/agent/ucodeDoctor.js +184 -0
  30. package/src/agent/ucodeRuntimeConfig.js +129 -0
  31. package/src/agent/ufooAgent.js +168 -28
  32. package/src/assistant/agent.js +260 -0
  33. package/src/assistant/bridge.js +172 -0
  34. package/src/assistant/engine.js +252 -0
  35. package/src/assistant/stdio.js +58 -0
  36. package/src/assistant/ufooEngineCli.js +306 -0
  37. package/src/bus/activate.js +27 -11
  38. package/src/bus/daemon.js +133 -5
  39. package/src/bus/index.js +137 -80
  40. package/src/bus/inject.js +47 -17
  41. package/src/bus/message.js +145 -17
  42. package/src/bus/nickname.js +3 -1
  43. package/src/bus/queue.js +6 -1
  44. package/src/bus/store.js +189 -0
  45. package/src/bus/subscriber.js +20 -4
  46. package/src/bus/utils.js +9 -3
  47. package/src/chat/agentBar.js +117 -0
  48. package/src/chat/agentDirectory.js +88 -0
  49. package/src/chat/agentSockets.js +225 -0
  50. package/src/chat/agentViewController.js +298 -0
  51. package/src/chat/chatLogController.js +115 -0
  52. package/src/chat/commandExecutor.js +700 -0
  53. package/src/chat/commands.js +132 -0
  54. package/src/chat/completionController.js +414 -0
  55. package/src/chat/cronScheduler.js +160 -0
  56. package/src/chat/daemonConnection.js +166 -0
  57. package/src/chat/daemonCoordinator.js +64 -0
  58. package/src/chat/daemonMessageRouter.js +257 -0
  59. package/src/chat/daemonReconnect.js +41 -0
  60. package/src/chat/daemonTransport.js +36 -0
  61. package/src/chat/daemonTransportDefaults.js +10 -0
  62. package/src/chat/dashboardKeyController.js +480 -0
  63. package/src/chat/dashboardView.js +157 -0
  64. package/src/chat/index.js +938 -2910
  65. package/src/chat/inputHistoryController.js +105 -0
  66. package/src/chat/inputListenerController.js +304 -0
  67. package/src/chat/inputMath.js +104 -0
  68. package/src/chat/inputSubmitHandler.js +171 -0
  69. package/src/chat/layout.js +165 -0
  70. package/src/chat/pasteController.js +81 -0
  71. package/src/chat/rawKeyMap.js +42 -0
  72. package/src/chat/settingsController.js +133 -0
  73. package/src/chat/statusLineController.js +177 -0
  74. package/src/chat/streamTracker.js +138 -0
  75. package/src/chat/text.js +70 -0
  76. package/src/chat/transport.js +61 -0
  77. package/src/cli/busCoreCommands.js +59 -0
  78. package/src/cli/ctxCoreCommands.js +199 -0
  79. package/src/cli/onlineCoreCommands.js +379 -0
  80. package/src/cli.js +741 -238
  81. package/src/code/README.md +29 -0
  82. package/src/code/UCODE_PROMPT.md +32 -0
  83. package/src/code/agent.js +1651 -0
  84. package/src/code/cli.js +158 -0
  85. package/src/code/config +0 -0
  86. package/src/code/dispatch.js +42 -0
  87. package/src/code/index.js +70 -0
  88. package/src/code/nativeRunner.js +1213 -0
  89. package/src/code/runtime.js +154 -0
  90. package/src/code/sessionStore.js +162 -0
  91. package/src/code/taskDecomposer.js +269 -0
  92. package/src/code/tools/bash.js +53 -0
  93. package/src/code/tools/common.js +42 -0
  94. package/src/code/tools/edit.js +70 -0
  95. package/src/code/tools/read.js +44 -0
  96. package/src/code/tools/write.js +35 -0
  97. package/src/code/tui.js +1587 -0
  98. package/src/config.js +50 -2
  99. package/src/context/decisions.js +12 -2
  100. package/src/context/index.js +18 -1
  101. package/src/context/sync.js +127 -0
  102. package/src/daemon/agentProcessManager.js +74 -0
  103. package/src/daemon/cronOps.js +241 -0
  104. package/src/daemon/index.js +662 -489
  105. package/src/daemon/ipcServer.js +99 -0
  106. package/src/daemon/ops.js +417 -179
  107. package/src/daemon/promptLoop.js +319 -0
  108. package/src/daemon/promptRequest.js +101 -0
  109. package/src/daemon/providerSessions.js +32 -17
  110. package/src/daemon/reporting.js +90 -0
  111. package/src/daemon/run.js +2 -5
  112. package/src/daemon/status.js +24 -1
  113. package/src/init/index.js +68 -14
  114. package/src/online/bridge.js +663 -0
  115. package/src/online/client.js +245 -0
  116. package/src/online/runner.js +253 -0
  117. package/src/online/server.js +992 -0
  118. package/src/online/tokens.js +103 -0
  119. package/src/report/store.js +331 -0
  120. package/src/shared/eventContract.js +35 -0
  121. package/src/shared/ptySocketContract.js +21 -0
  122. package/src/status/index.js +50 -17
  123. package/src/terminal/adapterContract.js +87 -0
  124. package/src/terminal/adapterRouter.js +84 -0
  125. package/src/terminal/adapters/externalAdapter.js +14 -0
  126. package/src/terminal/adapters/internalAdapter.js +13 -0
  127. package/src/terminal/adapters/internalPtyAdapter.js +42 -0
  128. package/src/terminal/adapters/internalQueueAdapter.js +37 -0
  129. package/src/terminal/adapters/terminalAdapter.js +31 -0
  130. package/src/terminal/adapters/tmuxAdapter.js +30 -0
  131. package/src/ufoo/agentsStore.js +69 -3
  132. package/src/utils/banner.js +5 -2
  133. package/scripts/.archived/bash-to-js-migration/README.md +0 -46
  134. package/scripts/.archived/bash-to-js-migration/banner.sh +0 -89
  135. package/scripts/.archived/bash-to-js-migration/bus-alert.sh +0 -6
  136. package/scripts/.archived/bash-to-js-migration/bus-autotrigger.sh +0 -6
  137. package/scripts/.archived/bash-to-js-migration/bus-daemon.sh +0 -231
  138. package/scripts/.archived/bash-to-js-migration/bus-inject.sh +0 -176
  139. package/scripts/.archived/bash-to-js-migration/bus-listen.sh +0 -6
  140. package/scripts/.archived/bash-to-js-migration/bus.sh +0 -986
  141. package/scripts/.archived/bash-to-js-migration/context-decisions.sh +0 -167
  142. package/scripts/.archived/bash-to-js-migration/context-doctor.sh +0 -72
  143. package/scripts/.archived/bash-to-js-migration/context-lint.sh +0 -110
  144. package/scripts/.archived/bash-to-js-migration/doctor.sh +0 -22
  145. package/scripts/.archived/bash-to-js-migration/init.sh +0 -247
  146. package/scripts/.archived/bash-to-js-migration/skills.sh +0 -113
  147. package/scripts/.archived/bash-to-js-migration/status.sh +0 -125
  148. package/scripts/banner.sh +0 -2
  149. package/src/bus/API_DESIGN.md +0 -204
@@ -0,0 +1,1587 @@
1
+ const chalk = require("chalk");
2
+ const pkg = require("../../package.json");
3
+
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
+ // Stream buffer for smooth output
22
+ class StreamBuffer {
23
+ constructor(writer, options = {}) {
24
+ this.writer = writer;
25
+ this.buffer = "";
26
+ this.delay = options.delay || 8; // ms between chunks
27
+ this.chunkSize = options.chunkSize || 3; // chars per chunk
28
+ this.isStreaming = false;
29
+ this.streamPromise = null;
30
+ }
31
+
32
+ async write(text) {
33
+ this.buffer += text;
34
+ if (!this.isStreaming) {
35
+ this.isStreaming = true;
36
+ this.streamPromise = this.flush();
37
+ }
38
+ return this.streamPromise;
39
+ }
40
+
41
+ async flush() {
42
+ while (this.buffer.length > 0) {
43
+ const chunk = this.buffer.slice(0, this.chunkSize);
44
+ this.buffer = this.buffer.slice(this.chunkSize);
45
+ this.writer(chunk);
46
+ if (this.buffer.length > 0) {
47
+ await new Promise(resolve => setTimeout(resolve, this.delay));
48
+ }
49
+ }
50
+ this.isStreaming = false;
51
+ }
52
+
53
+ async finish() {
54
+ if (this.isStreaming) {
55
+ await this.streamPromise;
56
+ }
57
+ if (this.buffer.length > 0) {
58
+ this.writer(this.buffer);
59
+ this.buffer = "";
60
+ }
61
+ }
62
+ }
63
+
64
+ function normalizeModelLabel(model = "") {
65
+ const text = String(model || "").trim();
66
+ if (text) return text;
67
+ return "default";
68
+ }
69
+
70
+ function buildUcodeBannerLines({ model = "", engine = "ufoo-core", nickname = "", agentId = "", workspaceRoot = "", sessionId = "", width = 0 } = {}) {
71
+ const modelLabel = normalizeModelLabel(model);
72
+ void width;
73
+ void engine; // Not using engine anymore
74
+ void nickname;
75
+ void agentId;
76
+
77
+ // Get current working directory with ~ for home
78
+ const path = require("path");
79
+ const os = require("os");
80
+ const currentDir = workspaceRoot || process.cwd();
81
+ const homeDir = os.homedir();
82
+
83
+ // Replace home directory with ~
84
+ let shortPath = currentDir;
85
+ if (currentDir.startsWith(homeDir)) {
86
+ shortPath = currentDir.replace(homeDir, "~");
87
+ }
88
+
89
+ const logoLines = UCODE_BANNER_LINES.map((line) => chalk.cyan(line));
90
+ const infoLines = [];
91
+ infoLines.push(`${chalk.dim("Version:")} ${chalk.cyan.bold(UCODE_VERSION)}`);
92
+ infoLines.push(`${chalk.dim("Model:")} ${chalk.yellow(modelLabel)}`);
93
+ infoLines.push(`${chalk.dim("Dictionary:")} ${chalk.gray(shortPath)}`);
94
+ const normalizedSessionId = String(sessionId || "").trim();
95
+ if (normalizedSessionId) {
96
+ infoLines.push(`${chalk.dim("Session:")} ${chalk.gray(normalizedSessionId)}`);
97
+ }
98
+ const logoPadding = " ".repeat(
99
+ UCODE_BANNER_LINES.reduce((max, line) => Math.max(max, String(line || "").length), 0)
100
+ );
101
+ const rows = Math.max(logoLines.length, infoLines.length);
102
+
103
+ return Array.from({ length: rows }, (_, index) => {
104
+ const logoLine = logoLines[index] || logoPadding;
105
+ const info = infoLines[index] || "";
106
+ return ` ${logoLine} ${info}`;
107
+ });
108
+ }
109
+
110
+ function escapeBlessedLiteral(text) {
111
+ const raw = String(text == null ? "" : text);
112
+ const safe = raw.replace(/\{\/escape\}/g, "{open}/escape{close}");
113
+ return `{escape}${safe}{/escape}`;
114
+ }
115
+
116
+ function buildUcodeBannerBlessedLines({
117
+ model = "",
118
+ engine = "ufoo-core",
119
+ nickname = "",
120
+ agentId = "",
121
+ workspaceRoot = "",
122
+ sessionId = "",
123
+ width = 0,
124
+ } = {}) {
125
+ const modelLabel = normalizeModelLabel(model);
126
+ void width;
127
+ void engine; // Not using engine anymore
128
+ void nickname;
129
+ void agentId;
130
+
131
+ const path = require("path");
132
+ const os = require("os");
133
+ const currentDir = workspaceRoot || process.cwd();
134
+ const homeDir = os.homedir();
135
+
136
+ let shortPath = currentDir;
137
+ if (currentDir.startsWith(homeDir)) {
138
+ shortPath = currentDir.replace(homeDir, "~");
139
+ }
140
+ shortPath = path.normalize(shortPath);
141
+
142
+ const logoLines = UCODE_BANNER_LINES.map(
143
+ (line) => `{cyan-fg}${escapeBlessedLiteral(line)}{/cyan-fg}`
144
+ );
145
+ const infoLines = [
146
+ `{gray-fg}Version:{/gray-fg} {cyan-fg}{bold}${escapeBlessedLiteral(UCODE_VERSION)}{/bold}{/cyan-fg}`,
147
+ `{gray-fg}Model:{/gray-fg} {yellow-fg}${escapeBlessedLiteral(modelLabel)}{/yellow-fg}`,
148
+ `{gray-fg}Dictionary:{/gray-fg} {gray-fg}${escapeBlessedLiteral(shortPath)}{/gray-fg}`,
149
+ ];
150
+ const normalizedSessionId = String(sessionId || "").trim();
151
+ if (normalizedSessionId) {
152
+ infoLines.push(`{gray-fg}Session:{/gray-fg} {gray-fg}${escapeBlessedLiteral(normalizedSessionId)}{/gray-fg}`);
153
+ }
154
+ const logoPadding = " ".repeat(
155
+ UCODE_BANNER_LINES.reduce((max, line) => Math.max(max, String(line || "").length), 0)
156
+ );
157
+ const rows = Math.max(logoLines.length, infoLines.length);
158
+
159
+ return Array.from({ length: rows }, (_, index) => {
160
+ const logoLine = logoLines[index] || logoPadding;
161
+ const info = infoLines[index] || "";
162
+ return ` ${logoLine} ${info}`;
163
+ });
164
+ }
165
+
166
+ function shouldUseUcodeTui({ stdin, stdout, jsonOutput, forceTui = false, disableTui = false } = {}) {
167
+ if (disableTui) return false;
168
+ if (jsonOutput) return false;
169
+ if (forceTui) return true;
170
+ return Boolean(stdin && stdin.isTTY && stdout && stdout.isTTY);
171
+ }
172
+
173
+ // Helper function to load agents from bus
174
+ function parseActiveAgentsFromBusStatus(busStatus = "") {
175
+ const lines = String(busStatus || "").replace(ANSI_PATTERN, "").split(/\r?\n/);
176
+ const agents = [];
177
+ let inOnlineSection = false;
178
+
179
+ for (const line of lines) {
180
+ const trimmed = String(line || "").trim();
181
+ if (!trimmed) continue;
182
+
183
+ if (/^Online agents:\s*$/i.test(trimmed)) {
184
+ inOnlineSection = true;
185
+ continue;
186
+ }
187
+ if (!inOnlineSection) continue;
188
+
189
+ if (/^\(none\)$/i.test(trimmed)) {
190
+ continue;
191
+ }
192
+
193
+ // Next heading means we have left the online agents section
194
+ if (/^[A-Za-z][A-Za-z ]+:\s*$/.test(trimmed)) {
195
+ break;
196
+ }
197
+
198
+ const rawId = trimmed.replace(/\s+\([^)]+\)\s*$/, "");
199
+ if (!rawId) continue;
200
+ const [type, ...idParts] = rawId.split(":");
201
+ const id = idParts.join(":");
202
+ if (!type) continue;
203
+
204
+ agents.push({
205
+ type,
206
+ id,
207
+ status: "active",
208
+ fullId: rawId,
209
+ nickname: (trimmed.match(/\(([^)]+)\)\s*$/) || [])[1] || "",
210
+ });
211
+ }
212
+
213
+ // Fallback for legacy output: "type:id (active|idle)"
214
+ if (agents.length === 0) {
215
+ for (const line of lines) {
216
+ const trimmed = String(line || "").trim();
217
+ const match = trimmed.match(/^([a-z-]+):([a-f0-9]+)\s+\((active|idle)\)$/);
218
+ if (!match) continue;
219
+ agents.push({
220
+ type: match[1],
221
+ id: match[2],
222
+ status: match[3],
223
+ fullId: `${match[1]}:${match[2]}`,
224
+ nickname: "",
225
+ });
226
+ }
227
+ }
228
+
229
+ return agents;
230
+ }
231
+
232
+ function loadActiveAgents(workspaceRoot) {
233
+ try {
234
+ const { execSync } = require("child_process");
235
+ const busStatus = execSync("ufoo bus status", {
236
+ cwd: workspaceRoot,
237
+ encoding: "utf8",
238
+ });
239
+ return parseActiveAgentsFromBusStatus(busStatus);
240
+ } catch {
241
+ return [];
242
+ }
243
+ }
244
+
245
+ function renderLogLinesWithMarkdown(text = "", state = {}, escapeFn = (value) => String(value || "")) {
246
+ const renderState = state && typeof state === "object" ? state : {};
247
+ if (typeof renderState.inCodeBlock !== "boolean") {
248
+ renderState.inCodeBlock = false;
249
+ }
250
+
251
+ const renderInlineCode = (input = "") => {
252
+ const source = String(input || "");
253
+ if (!source) return "";
254
+ if (!source.includes("`")) return escapeFn(source);
255
+
256
+ let out = "";
257
+ let cursor = 0;
258
+ const pattern = /`([^`\n]+)`/g;
259
+ let match = pattern.exec(source);
260
+ while (match) {
261
+ const index = Number(match.index) || 0;
262
+ if (index > cursor) {
263
+ out += escapeFn(source.slice(cursor, index));
264
+ }
265
+ out += `{yellow-fg}${escapeFn(match[1])}{/yellow-fg}`;
266
+ cursor = index + match[0].length;
267
+ match = pattern.exec(source);
268
+ }
269
+ if (cursor < source.length) {
270
+ out += escapeFn(source.slice(cursor));
271
+ }
272
+ return out;
273
+ };
274
+
275
+ const lines = String(text || "").split(/\r?\n/);
276
+ const out = [];
277
+
278
+ for (const line of lines) {
279
+ const raw = stripLeakedEscapeTags(String(line || ""));
280
+ const fenceMatch = raw.match(/^(\s*)(`{3,}|~{3,})(.*)$/);
281
+ if (fenceMatch) {
282
+ if (!renderState.inCodeBlock) {
283
+ const language = String(fenceMatch[3] || "").trim();
284
+ const label = language
285
+ ? `┌ code:${escapeFn(language)}`
286
+ : "┌ code";
287
+ out.push(`{gray-fg}${label}{/gray-fg}`);
288
+ renderState.inCodeBlock = true;
289
+ } else {
290
+ out.push("{gray-fg}└{/gray-fg}");
291
+ renderState.inCodeBlock = false;
292
+ }
293
+ continue;
294
+ }
295
+
296
+ if (renderState.inCodeBlock) {
297
+ out.push(`{gray-fg}│{/gray-fg} {white-fg}${escapeFn(raw)}{/white-fg}`);
298
+ } else {
299
+ const headingMatch = raw.match(/^(\s*)(#{1,6})\s+(.*)$/);
300
+ if (headingMatch) {
301
+ const indent = escapeFn(headingMatch[1] || "");
302
+ const marks = escapeFn(headingMatch[2] || "");
303
+ const content = renderInlineCode(headingMatch[3] || "");
304
+ out.push(`${indent}{cyan-fg}${marks}{/cyan-fg} {bold}${content}{/bold}`);
305
+ continue;
306
+ }
307
+
308
+ const quoteMatch = raw.match(/^(\s*)>\s?(.*)$/);
309
+ if (quoteMatch) {
310
+ const indent = escapeFn(quoteMatch[1] || "");
311
+ const content = renderInlineCode(quoteMatch[2] || "");
312
+ out.push(`${indent}{gray-fg}▍{/gray-fg} ${content}`);
313
+ continue;
314
+ }
315
+
316
+ const bulletMatch = raw.match(/^(\s*)([-*+])\s+(.*)$/);
317
+ if (bulletMatch) {
318
+ const indent = escapeFn(bulletMatch[1] || "");
319
+ const content = renderInlineCode(bulletMatch[3] || "");
320
+ out.push(`${indent}{gray-fg}•{/gray-fg} ${content}`);
321
+ continue;
322
+ }
323
+
324
+ const orderedMatch = raw.match(/^(\s*)(\d+)\.\s+(.*)$/);
325
+ if (orderedMatch) {
326
+ const indent = escapeFn(orderedMatch[1] || "");
327
+ const order = escapeFn(orderedMatch[2] || "");
328
+ const content = renderInlineCode(orderedMatch[3] || "");
329
+ out.push(`${indent}{gray-fg}${order}.{/gray-fg} ${content}`);
330
+ continue;
331
+ }
332
+
333
+ const errorMatch = raw.match(/^(\s*)(Error:\s+.*)$/i);
334
+ if (errorMatch) {
335
+ const indent = escapeFn(errorMatch[1] || "");
336
+ const content = renderInlineCode(errorMatch[2] || "");
337
+ out.push(`${indent}{red-fg}${content}{/red-fg}`);
338
+ continue;
339
+ }
340
+
341
+ out.push(renderInlineCode(raw));
342
+ }
343
+ }
344
+
345
+ return out;
346
+ }
347
+
348
+ function shouldEnterAgentSelection(inputValue = "") {
349
+ const text = String(inputValue || "");
350
+ const trimmed = text.trim();
351
+ return !trimmed;
352
+ }
353
+
354
+ function resolveAgentSelectionOnDown({
355
+ agentSelectionMode = false,
356
+ selectedAgentIndex = -1,
357
+ totalAgents = 0,
358
+ } = {}) {
359
+ const total = Number.isFinite(totalAgents) ? Math.max(0, Math.floor(totalAgents)) : 0;
360
+ if (total <= 0) return { action: "none", index: -1 };
361
+ if (agentSelectionMode) {
362
+ const keep = selectedAgentIndex >= 0 && selectedAgentIndex < total ? selectedAgentIndex : 0;
363
+ return { action: "hold", index: keep };
364
+ }
365
+ const enter = selectedAgentIndex >= 0 && selectedAgentIndex < total ? selectedAgentIndex : 0;
366
+ return { action: "enter", index: enter };
367
+ }
368
+
369
+ function cycleAgentSelectionIndex(selectedAgentIndex = -1, totalAgents = 0, direction = "right") {
370
+ const total = Number.isFinite(totalAgents) ? Math.max(0, Math.floor(totalAgents)) : 0;
371
+ if (total <= 0) return -1;
372
+ const current = selectedAgentIndex >= 0 && selectedAgentIndex < total ? selectedAgentIndex : 0;
373
+ if (direction === "left") {
374
+ return (current - 1 + total) % total;
375
+ }
376
+ return (current + 1) % total;
377
+ }
378
+
379
+ function shouldClearAgentSelectionOnUp({
380
+ agentSelectionMode = false,
381
+ inputValue = "",
382
+ } = {}) {
383
+ return Boolean(agentSelectionMode && shouldEnterAgentSelection(inputValue));
384
+ }
385
+
386
+ function moveCursorHorizontally(cursorPos = 0, inputValue = "", direction = "right") {
387
+ const text = String(inputValue || "");
388
+ const max = text.length;
389
+ const pos = Number.isFinite(cursorPos) ? Math.max(0, Math.floor(cursorPos)) : 0;
390
+ if (direction === "left") return Math.max(0, pos - 1);
391
+ return Math.min(max, pos + 1);
392
+ }
393
+
394
+ function resolveHistoryDownTransition({
395
+ inputHistory = [],
396
+ historyIndex = 0,
397
+ currentValue = "",
398
+ } = {}) {
399
+ const history = Array.isArray(inputHistory) ? inputHistory : [];
400
+ if (history.length <= 0) {
401
+ return {
402
+ moved: false,
403
+ nextHistoryIndex: Number.isFinite(historyIndex) ? Math.max(0, Math.floor(historyIndex)) : 0,
404
+ nextValue: String(currentValue || ""),
405
+ };
406
+ }
407
+ const currentIndex = Number.isFinite(historyIndex) ? Math.max(0, Math.floor(historyIndex)) : 0;
408
+ const nextHistoryIndex = Math.min(history.length, currentIndex + 1);
409
+ const nextValue = nextHistoryIndex >= history.length ? "" : String(history[nextHistoryIndex] || "");
410
+ const moved = nextHistoryIndex !== currentIndex || nextValue !== String(currentValue || "");
411
+ return {
412
+ moved,
413
+ nextHistoryIndex,
414
+ nextValue,
415
+ };
416
+ }
417
+
418
+ function filterSelectableAgents(agents = [], selfSubscriberId = "") {
419
+ const selfId = String(selfSubscriberId || "").trim();
420
+ const list = Array.isArray(agents) ? agents : [];
421
+ if (!selfId) {
422
+ return list.filter((agent) => {
423
+ const fullId = String(agent && agent.fullId ? agent.fullId : "").trim();
424
+ const type = String(agent && agent.type ? agent.type : "").trim();
425
+ if (fullId === "ufoo-agent") return false;
426
+ if (type === "ufoo-agent") return false;
427
+ return true;
428
+ });
429
+ }
430
+ return list.filter((agent) => {
431
+ const fullId = String(agent && agent.fullId ? agent.fullId : "").trim();
432
+ const type = String(agent && agent.type ? agent.type : "").trim();
433
+ if (!fullId) return true;
434
+ if (fullId === "ufoo-agent") return false;
435
+ if (type === "ufoo-agent") return false;
436
+ return fullId !== selfId;
437
+ });
438
+ }
439
+
440
+ function stripLeakedEscapeTags(text = "") {
441
+ const source = String(text == null ? "" : text);
442
+ const withoutClosedTags = source.replace(/\{[^{}\n]*escape[^{}\n]*\}/gi, "");
443
+ const withoutDanglingEscape = withoutClosedTags.replace(/\{\s*\/?\s*escape[\s\S]*$/gi, "");
444
+ return withoutDanglingEscape.replace(/\{\s*\/?\s*e?s?c?a?p?e?[^{}\n]*$/gi, "");
445
+ }
446
+
447
+ function findTrailingEscapeTagPrefix(text = "") {
448
+ const raw = String(text == null ? "" : text);
449
+ if (!raw) return "";
450
+ const windowSize = 40;
451
+ const tail = raw.slice(Math.max(0, raw.length - windowSize));
452
+ const braceIndex = tail.lastIndexOf("{");
453
+ if (braceIndex < 0) return "";
454
+ const suffix = tail.slice(braceIndex);
455
+ if (suffix.includes("}")) return "";
456
+
457
+ const compact = suffix.toLowerCase().replace(/\s+/g, "");
458
+ if (!compact.startsWith("{")) return "";
459
+ if (/^\{\/?e?s?c?a?p?e?[^}]*$/.test(compact)) {
460
+ return suffix;
461
+ }
462
+ return "";
463
+ }
464
+
465
+ function createEscapeTagStripper() {
466
+ let carry = "";
467
+
468
+ return {
469
+ write(chunk = "") {
470
+ const incoming = String(chunk == null ? "" : chunk);
471
+ if (!incoming && !carry) return "";
472
+ const combined = `${carry}${incoming}`;
473
+ const trailing = findTrailingEscapeTagPrefix(combined);
474
+ const safeText = trailing
475
+ ? combined.slice(0, combined.length - trailing.length)
476
+ : combined;
477
+ carry = trailing;
478
+ return stripLeakedEscapeTags(safeText);
479
+ },
480
+ flush() {
481
+ if (!carry) return "";
482
+ // carry only stores trailing prefixes of escape tags; do not emit it
483
+ // to avoid leaking partial markers like "{/escape" at stream end.
484
+ const rest = "";
485
+ carry = "";
486
+ return rest;
487
+ },
488
+ };
489
+ }
490
+
491
+ function formatPendingElapsed(ms = 0) {
492
+ const totalSeconds = Math.max(0, Math.floor(Number(ms) / 1000));
493
+ return `${totalSeconds} s`;
494
+ }
495
+
496
+ function normalizeBashToolCommand(args = {}, payload = {}) {
497
+ const argObj = args && typeof args === "object" ? args : {};
498
+ const resObj = payload && typeof payload === "object" ? payload : {};
499
+ const command = String(argObj.command || argObj.cmd || "").trim();
500
+ const code = Number.isFinite(resObj.code) ? `exit ${resObj.code}` : "";
501
+ return [command, code].filter(Boolean).join(" · ");
502
+ }
503
+
504
+ function normalizeToolMergeEntry(entry = {}) {
505
+ const source = entry && typeof entry === "object" ? entry : {};
506
+ const tool = String(source.tool || "").trim().toLowerCase() || "tool";
507
+ const detail = String(source.detail || "").trim();
508
+ const isError = Boolean(source.isError);
509
+ const errorText = String(source.errorText || "").trim();
510
+ const summary = [tool, detail].filter(Boolean).join(" · ") || tool;
511
+ return {
512
+ tool,
513
+ detail,
514
+ isError,
515
+ errorText,
516
+ summary,
517
+ };
518
+ }
519
+
520
+ function buildMergedToolSummaryText(entries = []) {
521
+ const list = Array.isArray(entries)
522
+ ? entries.map((item) => normalizeToolMergeEntry(item))
523
+ : [];
524
+ const count = list.length;
525
+ if (count <= 0) return "Ran tool";
526
+ const first = list[0];
527
+ if (count === 1) return `Ran ${first.summary}`;
528
+ const errorCount = list.filter((item) => item.isError).length;
529
+ const errorSuffix = errorCount > 0 ? ` · ${errorCount} error${errorCount === 1 ? "" : "s"}` : "";
530
+ return `Ran ${first.summary} · … +${count - 1} calls${errorSuffix}`;
531
+ }
532
+
533
+ function buildMergedToolExpandedLines(entries = []) {
534
+ const list = Array.isArray(entries)
535
+ ? entries.map((item) => normalizeToolMergeEntry(item))
536
+ : [];
537
+ const maxLength = 120; // Max length for expanded lines
538
+ return list.map((item, index) => {
539
+ const base = item.summary;
540
+ let line;
541
+ if (!item.isError) {
542
+ line = base;
543
+ } else {
544
+ line = item.errorText ? `${base} · error: ${item.errorText}` : `${base} · error`;
545
+ }
546
+ // Truncate long lines
547
+ if (line.length > maxLength) {
548
+ return line.slice(0, maxLength - 3) + "...";
549
+ }
550
+ return line;
551
+ });
552
+ }
553
+
554
+ function runUcodeTui({
555
+ stdin = process.stdin,
556
+ stdout = process.stdout,
557
+ runSingleCommand = () => ({ kind: "empty" }),
558
+ runNaturalLanguageTask = async () => ({ ok: true, summary: "ok" }),
559
+ runUbusCommand = async () => ({ ok: false, error: "ubus unsupported", summary: "" }),
560
+ formatNlResult = () => "ok",
561
+ workspaceRoot = process.cwd(),
562
+ state = {},
563
+ resumeSessionState = () => ({ ok: false, error: "resume unsupported", sessionId: "", restoredMessages: 0 }),
564
+ persistSessionState = () => ({ ok: true }),
565
+ autoBus = {},
566
+ } = {}) {
567
+ return new Promise((resolve) => {
568
+ const blessed = require("blessed");
569
+ const { execSync } = require("child_process");
570
+ const { createChatLayout } = require("../chat/layout");
571
+ const { computeDashboardContent } = require("../chat/dashboardView");
572
+ const { escapeBlessed, stripBlessedTags } = require("../chat/text");
573
+ const currentSubscriberId = String(process.env.UFOO_SUBSCRIBER_ID || "").trim();
574
+ const autoBusEnabled = Boolean(autoBus && autoBus.enabled);
575
+ const autoBusSubscriberId = String((autoBus && autoBus.subscriberId) || currentSubscriberId || "").trim();
576
+ const getAutoBusPendingCount = typeof (autoBus && autoBus.getPendingCount) === "function"
577
+ ? autoBus.getPendingCount
578
+ : () => 0;
579
+
580
+ let closing = false;
581
+ let chain = Promise.resolve();
582
+ let statusInterval = null;
583
+ let statusIndex = 0;
584
+ let activeAgents = [];
585
+ let activeAgentMetaMap = new Map();
586
+ let targetAgent = null;
587
+ let selectedAgentIndex = -1;
588
+ let agentListWindowStart = 0;
589
+ let agentSelectionMode = false;
590
+ let pendingTask = null;
591
+ const logRenderState = { inCodeBlock: false };
592
+ const inputHistory = [];
593
+ let historyIndex = -1;
594
+ let activeToolMerge = null;
595
+ let lastMergedToolGroup = null;
596
+ let toolMergeId = 0;
597
+ let cursorPos = 0;
598
+ let autoBusTimer = null;
599
+ let autoBusQueued = false;
600
+ let autoBusError = "";
601
+ const inputMath = require("../chat/inputMath");
602
+
603
+ const {
604
+ screen,
605
+ logBox,
606
+ statusLine,
607
+ completionPanel,
608
+ dashboard,
609
+ promptBox,
610
+ input,
611
+ } = createChatLayout({
612
+ blessed,
613
+ currentInputHeight: 4,
614
+ version: UCODE_VERSION,
615
+ });
616
+
617
+ if (completionPanel && typeof completionPanel.hide === "function") {
618
+ completionPanel.hide();
619
+ }
620
+
621
+ const getAgentTag = (agent) => {
622
+ if (!agent) return "";
623
+ if (agent.id) return `${agent.type}:${agent.id.slice(0, 6)}`;
624
+ return agent.type;
625
+ };
626
+
627
+ const getAgentLabel = (id) => {
628
+ const meta = activeAgentMetaMap.get(id);
629
+ if (!meta) return id;
630
+ if (meta.nickname) return meta.nickname;
631
+ return getAgentTag(meta);
632
+ };
633
+
634
+ const refreshAgents = () => {
635
+ const list = filterSelectableAgents(
636
+ loadActiveAgents(workspaceRoot),
637
+ currentSubscriberId
638
+ );
639
+ activeAgents = list.map((agent) => agent.fullId);
640
+ activeAgentMetaMap = new Map(list.map((agent) => [agent.fullId, agent]));
641
+ if (targetAgent && !activeAgentMetaMap.has(targetAgent)) {
642
+ targetAgent = null;
643
+ }
644
+ selectedAgentIndex = targetAgent ? activeAgents.indexOf(targetAgent) : -1;
645
+ };
646
+
647
+ const setPrompt = () => {
648
+ const content = targetAgent ? `>@${getAgentLabel(targetAgent)}` : ">";
649
+ promptBox.setContent(content);
650
+ const plain = stripBlessedTags(content);
651
+ promptBox.width = Math.max(2, plain.length + 1);
652
+ input.left = promptBox.width;
653
+ input.width = `100%-${promptBox.width}`;
654
+ };
655
+
656
+ // --- Cursor position helpers (mirrors chat inputListenerController) ---
657
+ const getInnerWidth = () => {
658
+ const promptWidth = typeof promptBox.width === "number" ? promptBox.width : 2;
659
+ return inputMath.getInnerWidth({ input, screen, promptWidth });
660
+ };
661
+
662
+ const getWrapWidth = () => inputMath.getWrapWidth(input, getInnerWidth());
663
+
664
+ const ensureInputCursorVisible = () => {
665
+ const innerWidth = getInnerWidth();
666
+ if (innerWidth <= 0) return;
667
+ const totalRows = inputMath.countLines(input.value || "", innerWidth, (v) => input.strWidth(v));
668
+ const visibleRows = Math.max(1, input.height || 1);
669
+ const { row } = inputMath.getCursorRowCol(input.value || "", cursorPos, innerWidth, (v) => input.strWidth(v));
670
+ let base = input.childBase || 0;
671
+ const maxBase = Math.max(0, totalRows - visibleRows);
672
+ if (row < base) base = row;
673
+ else if (row >= base + visibleRows) base = row - visibleRows + 1;
674
+ if (base > maxBase) base = maxBase;
675
+ if (base < 0) base = 0;
676
+ if (base !== input.childBase) {
677
+ input.childBase = base;
678
+ if (typeof input.scrollTo === "function") input.scrollTo(base);
679
+ }
680
+ };
681
+
682
+ // Override _updateCursor to use our tracked cursorPos
683
+ input._updateCursor = function () {
684
+ if (this.screen.focused !== this) return;
685
+ let lpos;
686
+ try { lpos = this._getCoords(); } catch { return; }
687
+ if (!lpos) return;
688
+ const innerWidth = getWrapWidth();
689
+ if (innerWidth <= 0) return;
690
+ ensureInputCursorVisible();
691
+ const { row, col } = inputMath.getCursorRowCol(this.value || "", cursorPos, innerWidth, (v) => this.strWidth(v));
692
+ const scrollOffset = this.childBase || 0;
693
+ const displayRow = row - scrollOffset;
694
+ const safeCol = Math.min(Math.max(0, col), innerWidth - 1);
695
+ const cy = lpos.yi + displayRow;
696
+ const cx = lpos.xi + safeCol;
697
+ this.screen.program.cup(cy, cx);
698
+ this.screen.program.showCursor();
699
+ };
700
+
701
+ // Override _listener to support cursor-aware editing
702
+ const origDone = input._done ? input._done.bind(input) : null;
703
+ let lastKeyRef = null;
704
+ input._listener = function (ch, key) {
705
+ const keyName = key && key.name;
706
+
707
+ // Dedup: blessed delivers the same key object via element 'keypress' event
708
+ // from both readInput's __listener binding and screen's focused.emit('keypress').
709
+ // Use object identity to skip the duplicate delivery.
710
+ if (key && key === lastKeyRef) return;
711
+ lastKeyRef = key || null;
712
+
713
+ // Let enter/return/escape pass through to blessed key handlers
714
+ if (keyName === "return" || keyName === "enter" || keyName === "escape") return;
715
+
716
+ // Arrow keys handled by input.key() handlers below
717
+ if (keyName === "left" || keyName === "right" || keyName === "up" || keyName === "down") return;
718
+
719
+ if (keyName === "backspace") {
720
+ if (cursorPos > 0 && this.value) {
721
+ this.value = this.value.slice(0, cursorPos - 1) + this.value.slice(cursorPos);
722
+ cursorPos -= 1;
723
+ ensureInputCursorVisible();
724
+ this._updateCursor();
725
+ this.screen.render();
726
+ }
727
+ return;
728
+ }
729
+
730
+ if (keyName === "delete") {
731
+ if (this.value && cursorPos < this.value.length) {
732
+ this.value = this.value.slice(0, cursorPos) + this.value.slice(cursorPos + 1);
733
+ ensureInputCursorVisible();
734
+ this._updateCursor();
735
+ this.screen.render();
736
+ }
737
+ return;
738
+ }
739
+
740
+ if (keyName === "home") {
741
+ cursorPos = 0;
742
+ ensureInputCursorVisible();
743
+ this._updateCursor();
744
+ this.screen.render();
745
+ return;
746
+ }
747
+
748
+ if (keyName === "end") {
749
+ cursorPos = (this.value || "").length;
750
+ ensureInputCursorVisible();
751
+ this._updateCursor();
752
+ this.screen.render();
753
+ return;
754
+ }
755
+
756
+ // Normal character insertion at cursor position
757
+ const insertChar = (ch && ch.length === 1) ? ch : (keyName && keyName.length === 1 ? keyName : null);
758
+ if (insertChar && !/^[\x00-\x08\x0b-\x0c\x0e-\x1f\x7f]$/.test(insertChar)) {
759
+ this.value = (this.value || "").slice(0, cursorPos) + insertChar + (this.value || "").slice(cursorPos);
760
+ cursorPos += 1;
761
+ ensureInputCursorVisible();
762
+ this._updateCursor();
763
+ this.screen.render();
764
+ }
765
+ };
766
+
767
+ // Helper to set input value and reset cursor to end
768
+ const setInputValue = (value) => {
769
+ input.setValue(value || "");
770
+ cursorPos = (value || "").length;
771
+ ensureInputCursorVisible();
772
+ input._updateCursor();
773
+ screen.render();
774
+ };
775
+
776
+ const renderDashboard = () => {
777
+ let hint = "No target agents";
778
+ if (activeAgents.length > 0) {
779
+ if (targetAgent) {
780
+ hint = `↓ select ${getAgentLabel(targetAgent)} · ←/→ switch · ↑ clear`;
781
+ } else {
782
+ hint = "↓ select target · ←/→ switch";
783
+ }
784
+ }
785
+ const computed = computeDashboardContent({
786
+ focusMode: "dashboard",
787
+ dashboardView: "agents",
788
+ activeAgents,
789
+ selectedAgentIndex,
790
+ agentListWindowStart,
791
+ maxAgentWindow: 4,
792
+ getAgentLabel,
793
+ dashHints: { agents: hint, agentsEmpty: hint },
794
+ });
795
+ agentListWindowStart = computed.windowStart;
796
+ dashboard.setContent(computed.content);
797
+ screen.render();
798
+ };
799
+
800
+ const logText = (text = "") => {
801
+ activeToolMerge = null;
802
+ firstToolInGroup = true; // Reset tool group flag when switching back to text
803
+ const sanitized = stripLeakedEscapeTags(text);
804
+ const lines = renderLogLinesWithMarkdown(
805
+ sanitized,
806
+ logRenderState,
807
+ escapeBlessed
808
+ );
809
+ for (const line of lines) {
810
+ logBox.log(line);
811
+ }
812
+ screen.render();
813
+ };
814
+
815
+ const logUserInput = (text = "") => {
816
+ activeToolMerge = null;
817
+ const plain = String(text || "").trim();
818
+ if (!plain) return;
819
+ const content = ` → ${escapeBlessed(plain)} `;
820
+ const visibleLen = plain.length + 4; // " → " + text + " "
821
+ const boxWidth = (logBox.width || 80) - 2; // subtract border/padding
822
+ const pad = boxWidth > visibleLen ? " ".repeat(boxWidth - visibleLen) : "";
823
+ logBox.log(`{cyan-bg}{white-fg}${content}${pad}{/white-fg}{/cyan-bg}`);
824
+ logBox.log(""); // Add line break after user input
825
+ screen.render();
826
+ };
827
+
828
+ const logControlAction = (text = "") => {
829
+ activeToolMerge = null;
830
+ const plain = String(text || "").trim();
831
+ if (!plain) return;
832
+ logBox.log(`{gray-fg}⚙{/gray-fg} ${escapeBlessed(plain)}`);
833
+ screen.render();
834
+ };
835
+
836
+ const summarizeToolDetail = (tool = "", args = {}, payload = {}) => {
837
+ const toolName = String(tool || "").trim().toLowerCase();
838
+ const argObj = args && typeof args === "object" ? args : {};
839
+ const resObj = payload && typeof payload === "object" ? payload : {};
840
+
841
+ if (toolName === "read") {
842
+ const target = String(resObj.path || argObj.path || argObj.file || "").trim();
843
+ const lineInfo = Number.isFinite(resObj.totalLines) ? `${resObj.totalLines} lines` : "";
844
+ return [target, lineInfo].filter(Boolean).join(" · ");
845
+ }
846
+ if (toolName === "write") {
847
+ const target = String(resObj.path || argObj.path || argObj.file || "").trim();
848
+ const mode = String(resObj.mode || argObj.mode || (argObj.append ? "append" : "overwrite")).trim();
849
+ const bytes = Number.isFinite(resObj.bytes) ? `${resObj.bytes} bytes` : "";
850
+ return [target, mode, bytes].filter(Boolean).join(" · ");
851
+ }
852
+ if (toolName === "edit") {
853
+ const target = String(resObj.path || argObj.path || argObj.file || "").trim();
854
+ const replacements = Number.isFinite(resObj.replacements) ? `${resObj.replacements} replacements` : "";
855
+ return [target, replacements].filter(Boolean).join(" · ");
856
+ }
857
+ if (toolName === "bash") {
858
+ return normalizeBashToolCommand(argObj, resObj);
859
+ }
860
+ return "";
861
+ };
862
+
863
+ const truncateText = (text = "", maxLength = 80) => {
864
+ const str = String(text || "");
865
+ if (str.length <= maxLength) return str;
866
+ return str.slice(0, maxLength - 3) + "...";
867
+ };
868
+
869
+ const renderSingleToolEntryLine = (entry = {}) => {
870
+ const item = normalizeToolMergeEntry(entry);
871
+ const marker = item.isError ? "{red-fg}•{/red-fg}" : "{cyan-fg}•{/cyan-fg}";
872
+ const summary = buildMergedToolSummaryText([item]);
873
+ const truncated = truncateText(summary, 100);
874
+ return `${marker} ${escapeBlessed(truncated)}`;
875
+ };
876
+
877
+ const renderCollapsedToolMergeLine = (entries = []) => {
878
+ const summary = buildMergedToolSummaryText(entries);
879
+ const hasError = entries.some((item) => normalizeToolMergeEntry(item).isError);
880
+ const marker = hasError ? "{red-fg}•{/red-fg}" : "{cyan-fg}•{/cyan-fg}";
881
+ return `${marker} ${escapeBlessed(summary)} {gray-fg}(Ctrl+O expand){/gray-fg}`;
882
+ };
883
+
884
+ let firstToolInGroup = true;
885
+
886
+ const logToolHint = (entry = {}, payload = {}) => {
887
+ const tool = String(entry.tool || "").trim().toLowerCase();
888
+ if (!tool) return;
889
+ const resObj = payload && typeof payload === "object" ? payload : {};
890
+ const isError = String(entry.phase || "").trim().toLowerCase() === "error" || resObj.ok === false;
891
+ const detail = summarizeToolDetail(tool, entry.args, resObj);
892
+ const errorText = String(entry.error || resObj.error || "").trim();
893
+
894
+ const toolEntry = normalizeToolMergeEntry({
895
+ tool,
896
+ detail,
897
+ isError,
898
+ errorText,
899
+ });
900
+
901
+ if (activeToolMerge) {
902
+ activeToolMerge.entries.push(toolEntry);
903
+ // Only show collapsed format for 2+ tool calls
904
+ if (activeToolMerge.entries.length === 2) {
905
+ // Convert first single line to collapsed format
906
+ logBox.setLine(activeToolMerge.lineIndex, renderCollapsedToolMergeLine(activeToolMerge.entries));
907
+ } else if (activeToolMerge.entries.length > 2) {
908
+ logBox.setLine(activeToolMerge.lineIndex, renderCollapsedToolMergeLine(activeToolMerge.entries));
909
+ }
910
+ if (activeToolMerge.entries.length > 1) {
911
+ lastMergedToolGroup = activeToolMerge;
912
+ }
913
+ } else {
914
+ // Add line break before first tool call
915
+ if (firstToolInGroup) {
916
+ logBox.log("");
917
+ firstToolInGroup = false;
918
+ }
919
+ logBox.log(renderSingleToolEntryLine(toolEntry));
920
+ activeToolMerge = {
921
+ id: ++toolMergeId,
922
+ lineIndex: logBox.getLines().length - 1,
923
+ entries: [toolEntry],
924
+ expanded: false,
925
+ };
926
+ }
927
+ screen.render();
928
+ };
929
+
930
+ const renderSingleMarkdownLine = (rawLine = "", options = {}) => {
931
+ const preview = Boolean(options.preview);
932
+ const renderState = preview
933
+ ? { inCodeBlock: Boolean(logRenderState.inCodeBlock) }
934
+ : logRenderState;
935
+ const rendered = renderLogLinesWithMarkdown(rawLine, renderState, escapeBlessed);
936
+ return rendered[0] || "";
937
+ };
938
+
939
+ const createNlStreamState = () => {
940
+ activeToolMerge = null;
941
+ firstToolInGroup = true; // Reset flag for new response
942
+ logBox.log(""); // Add empty line to start the response
943
+ return {
944
+ lineIndex: logBox.getLines().length - 1,
945
+ buffer: "",
946
+ full: "",
947
+ seenVisibleContent: false,
948
+ };
949
+ };
950
+
951
+ const appendNlStreamDelta = (streamState, delta) => {
952
+ if (!streamState) return;
953
+ const chunk = stripLeakedEscapeTags(String(delta || ""));
954
+ if (!chunk) return;
955
+
956
+ streamState.full += chunk;
957
+ streamState.buffer += chunk;
958
+
959
+ const parts = streamState.buffer.split("\n");
960
+ if (parts.length > 1) {
961
+ const completed = parts.slice(0, -1);
962
+ for (const line of completed) {
963
+ const hasVisible = /[^\s]/.test(line);
964
+ if (!streamState.seenVisibleContent && !hasVisible) {
965
+ continue;
966
+ }
967
+ if (hasVisible) {
968
+ streamState.seenVisibleContent = true;
969
+ }
970
+ const rendered = renderSingleMarkdownLine(line);
971
+ logBox.setLine(streamState.lineIndex, rendered);
972
+ logBox.pushLine("");
973
+ streamState.lineIndex = logBox.getLines().length - 1;
974
+ }
975
+ streamState.buffer = parts[parts.length - 1];
976
+ }
977
+
978
+ const previewHasVisible = /[^\s]/.test(streamState.buffer);
979
+ if (!streamState.seenVisibleContent && !previewHasVisible) {
980
+ return;
981
+ }
982
+ if (previewHasVisible) {
983
+ streamState.seenVisibleContent = true;
984
+ }
985
+ const previewLine = renderSingleMarkdownLine(streamState.buffer, { preview: true });
986
+ logBox.setLine(streamState.lineIndex, previewLine);
987
+ screen.render();
988
+ };
989
+
990
+ const finalizeNlStream = (streamState) => {
991
+ if (!streamState) return { lastChar: "" };
992
+ streamState.buffer = stripLeakedEscapeTags(streamState.buffer);
993
+ const rendered = renderSingleMarkdownLine(streamState.buffer);
994
+ logBox.setLine(streamState.lineIndex, rendered);
995
+ screen.render();
996
+ const full = String(streamState.full || "");
997
+ return { lastChar: full ? full.charAt(full.length - 1) : "" };
998
+ };
999
+
1000
+ const updateStatus = (message = "", type = "thinking", options = {}) => {
1001
+ if (statusInterval) {
1002
+ clearInterval(statusInterval);
1003
+ statusInterval = null;
1004
+ }
1005
+ if (!message) {
1006
+ statusLine.setContent("{bold}UCODE{/bold} · Ready");
1007
+ screen.render();
1008
+ return;
1009
+ }
1010
+ const showTimer = Boolean(options.showTimer);
1011
+ const startedAt = Number.isFinite(options.startedAt) ? options.startedAt : Date.now();
1012
+ const indicators = STATUS_INDICATORS[type] || STATUS_INDICATORS.thinking;
1013
+ statusIndex = 0;
1014
+ const draw = () => {
1015
+ const indicator = indicators[statusIndex % indicators.length];
1016
+ const timerText = showTimer
1017
+ ? ` (${formatPendingElapsed(Date.now() - startedAt)},esc cancel)`
1018
+ : "";
1019
+ statusLine.setContent(escapeBlessed(`${indicator} ${message}${timerText}`));
1020
+ statusIndex += 1;
1021
+ screen.render();
1022
+ };
1023
+ draw();
1024
+ if (type !== "none") {
1025
+ statusInterval = setInterval(draw, 100);
1026
+ }
1027
+ };
1028
+
1029
+ const closeWithCode = (code = 0) => {
1030
+ if (closing) return;
1031
+ closing = true;
1032
+ if (autoBusTimer) {
1033
+ clearInterval(autoBusTimer);
1034
+ autoBusTimer = null;
1035
+ }
1036
+ if (statusInterval) {
1037
+ clearInterval(statusInterval);
1038
+ statusInterval = null;
1039
+ }
1040
+ if (pendingTask && pendingTask.abortController && !pendingTask.abortController.signal.aborted) {
1041
+ try {
1042
+ pendingTask.abortController.abort();
1043
+ } catch {
1044
+ // ignore
1045
+ }
1046
+ }
1047
+ try {
1048
+ screen.destroy();
1049
+ } catch {
1050
+ // ignore
1051
+ }
1052
+ resolve({ code });
1053
+ };
1054
+
1055
+ const runAutoBusOnce = async () => {
1056
+ if (!autoBusEnabled || closing || pendingTask) return;
1057
+ if (Number(getAutoBusPendingCount()) <= 0) {
1058
+ autoBusError = "";
1059
+ return;
1060
+ }
1061
+ const ubusResult = await runUbusCommand(state, {
1062
+ workspaceRoot,
1063
+ subscriberId: autoBusSubscriberId,
1064
+ onMessageReceived: (msg) => {
1065
+ // Display the incoming message immediately
1066
+ const { extractAgentNickname } = require("./agent");
1067
+ const nickname = extractAgentNickname(msg.from) || msg.from;
1068
+ logText(`${nickname}: ${msg.task}`);
1069
+ },
1070
+ });
1071
+ if (!ubusResult.ok) {
1072
+ const nextError = String(ubusResult.error || "ubus failed");
1073
+ if (nextError !== autoBusError) {
1074
+ autoBusError = nextError;
1075
+ logText(`Error: ${nextError}`);
1076
+ }
1077
+ return;
1078
+ }
1079
+ autoBusError = "";
1080
+ if (ubusResult.handled > 0) {
1081
+ // Display only the replies (tasks were already shown via onMessageReceived)
1082
+ if (ubusResult.messageExchanges && ubusResult.messageExchanges.length > 0) {
1083
+ const { extractAgentNickname } = require("./agent");
1084
+ for (const exchange of ubusResult.messageExchanges) {
1085
+ const nickname = extractAgentNickname(exchange.from) || exchange.from;
1086
+ // Only show the reply since task was already displayed
1087
+ logText(`@${nickname} ${exchange.reply}`);
1088
+ }
1089
+ }
1090
+ const persisted = persistSessionState(state);
1091
+ if (!persisted || persisted.ok === false) {
1092
+ logText(`Error: failed to persist session ${state.sessionId}: ${(persisted && persisted.error) || "unknown error"}`);
1093
+ }
1094
+ }
1095
+ };
1096
+
1097
+ const scheduleAutoBus = () => {
1098
+ if (!autoBusEnabled || closing || autoBusQueued || pendingTask) return;
1099
+ if (Number(getAutoBusPendingCount()) <= 0) return;
1100
+ autoBusQueued = true;
1101
+ chain = chain
1102
+ .then(() => runAutoBusOnce())
1103
+ .catch(() => {})
1104
+ .finally(() => {
1105
+ autoBusQueued = false;
1106
+ });
1107
+ };
1108
+
1109
+ const resolveTargetToken = (token = "") => {
1110
+ const text = String(token || "").trim();
1111
+ if (!text) return "";
1112
+
1113
+ if (text.includes(":")) {
1114
+ const match = activeAgents.find((id) => id === text || id.startsWith(text));
1115
+ if (match) return match;
1116
+ }
1117
+
1118
+ const normalized = text.toLowerCase();
1119
+ for (const id of activeAgents) {
1120
+ const meta = activeAgentMetaMap.get(id);
1121
+ if (!meta) continue;
1122
+ const nick = String(meta.nickname || "").toLowerCase();
1123
+ if (nick && (nick === normalized || nick.startsWith(normalized))) return id;
1124
+ }
1125
+ return "";
1126
+ };
1127
+
1128
+ const executeLine = async (line) => {
1129
+ const normalizedLine = String(line || "").replace(/\r?\n/g, " ").trim();
1130
+ if (!normalizedLine) return;
1131
+ logUserInput(normalizedLine);
1132
+
1133
+ refreshAgents();
1134
+
1135
+ let actualLine = normalizedLine;
1136
+ let isBusMessage = false;
1137
+
1138
+ if (targetAgent) {
1139
+ isBusMessage = true;
1140
+ }
1141
+
1142
+ const mentionMatch = normalizedLine.match(/^@(\S+)\s+(.+)$/);
1143
+ if (mentionMatch) {
1144
+ const [, token, message] = mentionMatch;
1145
+ const resolved = resolveTargetToken(token);
1146
+ if (resolved) {
1147
+ isBusMessage = true;
1148
+ actualLine = message;
1149
+ targetAgent = resolved;
1150
+ selectedAgentIndex = activeAgents.indexOf(resolved);
1151
+ setPrompt();
1152
+ renderDashboard();
1153
+ }
1154
+ }
1155
+
1156
+ if (isBusMessage && targetAgent) {
1157
+ updateStatus("Sending message...", "typing");
1158
+ try {
1159
+ execSync(`ufoo bus send "${targetAgent}" "${actualLine.replace(/"/g, '\\"')}"`, {
1160
+ cwd: workspaceRoot,
1161
+ encoding: "utf8",
1162
+ });
1163
+ updateStatus("", "none");
1164
+ logText(`✓ Message sent to ${getAgentLabel(targetAgent)}`);
1165
+ } catch (err) {
1166
+ updateStatus("", "none");
1167
+ const msg = err && err.message ? err.message : "unknown error";
1168
+ logText(`Failed to send message: ${msg}`);
1169
+ }
1170
+ targetAgent = null;
1171
+ selectedAgentIndex = -1;
1172
+ agentSelectionMode = false;
1173
+ setPrompt();
1174
+ renderDashboard();
1175
+ return;
1176
+ }
1177
+
1178
+ const runtimeWorkspace = String((state && state.workspaceRoot) || workspaceRoot || process.cwd());
1179
+ const result = runSingleCommand(actualLine, runtimeWorkspace);
1180
+ if (result.kind === "empty") return;
1181
+ if (result.kind === "exit") {
1182
+ closeWithCode(0);
1183
+ return;
1184
+ }
1185
+ if (result.kind === "tool") {
1186
+ const payload = result.result && typeof result.result === "object" ? result.result : {};
1187
+ logToolHint({
1188
+ tool: result.tool,
1189
+ args: result.args,
1190
+ phase: payload.ok === false ? "error" : "end",
1191
+ error: payload.error || "",
1192
+ }, payload);
1193
+ return;
1194
+ }
1195
+ if (result.kind === "help" || result.kind === "probe" || result.kind === "error") {
1196
+ logText(result.output || "");
1197
+ return;
1198
+ }
1199
+ if (result.kind === "ubus") {
1200
+ updateStatus("Checking bus messages...", "typing");
1201
+ const ubusResult = await runUbusCommand(state, {
1202
+ workspaceRoot,
1203
+ onMessageReceived: (msg) => {
1204
+ // Display the incoming message immediately
1205
+ const { extractAgentNickname } = require("./agent");
1206
+ const nickname = extractAgentNickname(msg.from) || msg.from;
1207
+ logText(`${nickname}: ${msg.task}`);
1208
+ },
1209
+ });
1210
+ updateStatus("", "none");
1211
+ if (!ubusResult.ok) {
1212
+ logText(`Error: ${ubusResult.error}`);
1213
+ return;
1214
+ }
1215
+
1216
+ // Display only the replies (tasks were already shown via onMessageReceived)
1217
+ if (ubusResult.messageExchanges && ubusResult.messageExchanges.length > 0) {
1218
+ const { extractAgentNickname } = require("./agent");
1219
+ for (const exchange of ubusResult.messageExchanges) {
1220
+ const nickname = extractAgentNickname(exchange.from) || exchange.from;
1221
+ // Only show the reply since task was already displayed
1222
+ logText(`@${nickname} ${exchange.reply}`);
1223
+ }
1224
+ } else if (ubusResult.handled === 0) {
1225
+ logText("ubus: no pending messages.");
1226
+ }
1227
+ const persisted = persistSessionState(state);
1228
+ if (!persisted || persisted.ok === false) {
1229
+ logText(`Error: failed to persist session ${state.sessionId}: ${(persisted && persisted.error) || "unknown error"}`);
1230
+ }
1231
+ return;
1232
+ }
1233
+ if (result.kind === "resume") {
1234
+ const resumed = resumeSessionState(state, result.sessionId, workspaceRoot);
1235
+ if (!resumed.ok) {
1236
+ logText(`Error: ${resumed.error}`);
1237
+ return;
1238
+ }
1239
+ logText(`Resumed session ${resumed.sessionId} (${resumed.restoredMessages} messages).`);
1240
+ return;
1241
+ }
1242
+
1243
+ if (result.kind === "nl") {
1244
+ const statusMessages = [
1245
+ "Thinking...",
1246
+ "Processing your request...",
1247
+ "Analyzing...",
1248
+ "Working on it...",
1249
+ ];
1250
+ const randomStatus = statusMessages[Math.floor(Math.random() * statusMessages.length)];
1251
+ const abortController = new AbortController();
1252
+ const escapeStripper = createEscapeTagStripper();
1253
+ pendingTask = {
1254
+ abortController,
1255
+ startedAt: Date.now(),
1256
+ };
1257
+ updateStatus(randomStatus, "thinking", {
1258
+ showTimer: true,
1259
+ startedAt: pendingTask.startedAt,
1260
+ });
1261
+ let streamState = null;
1262
+ let renderedToolLogCount = 0;
1263
+ const nlResult = await runNaturalLanguageTask(result.task, state, {
1264
+ signal: abortController.signal,
1265
+ onDelta: (delta) => {
1266
+ const text = escapeStripper.write(String(delta || ""));
1267
+ if (!text) return;
1268
+ if (!streamState) {
1269
+ streamState = createNlStreamState();
1270
+ }
1271
+ appendNlStreamDelta(streamState, text);
1272
+ },
1273
+ onToolLog: (entry) => {
1274
+ renderedToolLogCount += 1;
1275
+ logToolHint(entry);
1276
+ },
1277
+ });
1278
+ const tail = escapeStripper.flush();
1279
+ if (tail) {
1280
+ if (!streamState) {
1281
+ streamState = createNlStreamState();
1282
+ }
1283
+ appendNlStreamDelta(streamState, tail);
1284
+ }
1285
+ pendingTask = null;
1286
+ updateStatus("", "none");
1287
+ let finalStreamInfo = { lastChar: "" };
1288
+ if (streamState) {
1289
+ finalStreamInfo = finalizeNlStream(streamState);
1290
+ }
1291
+ if (Array.isArray(nlResult && nlResult.logs) && nlResult.logs.length > renderedToolLogCount) {
1292
+ for (const entry of nlResult.logs.slice(renderedToolLogCount)) {
1293
+ logToolHint(entry);
1294
+ }
1295
+ }
1296
+ const streamed = Boolean(nlResult && nlResult.streamed);
1297
+ const hasVisibleStreamText = Boolean(
1298
+ streamState
1299
+ && typeof streamState.full === "string"
1300
+ && /[^\s]/.test(streamState.full)
1301
+ );
1302
+ const streamLastChar = nlResult && typeof nlResult.streamLastChar === "string"
1303
+ ? nlResult.streamLastChar.slice(-1)
1304
+ : finalStreamInfo.lastChar;
1305
+ if (streamed && hasVisibleStreamText && streamLastChar !== "\n") {
1306
+ logBox.log("");
1307
+ screen.render();
1308
+ }
1309
+ const shouldSkipSummary = Boolean(streamed && nlResult && nlResult.ok && hasVisibleStreamText);
1310
+ if (!shouldSkipSummary) {
1311
+ logText(formatNlResult(nlResult, false));
1312
+ }
1313
+ const persisted = persistSessionState(state);
1314
+ if (!persisted || persisted.ok === false) {
1315
+ logText(`Error: failed to persist session ${state.sessionId}: ${(persisted && persisted.error) || "unknown error"}`);
1316
+ }
1317
+ }
1318
+ };
1319
+
1320
+ const submitInput = (value = "") => {
1321
+ const raw = String(value || "");
1322
+ const trimmed = raw.trim();
1323
+ input.setValue("");
1324
+ cursorPos = 0;
1325
+ screen.render();
1326
+ agentSelectionMode = false;
1327
+
1328
+ if (trimmed) {
1329
+ inputHistory.push(trimmed);
1330
+ }
1331
+ historyIndex = inputHistory.length;
1332
+
1333
+ chain = chain
1334
+ .then(() => executeLine(raw))
1335
+ .catch((err) => {
1336
+ updateStatus("", "none");
1337
+ logText(`Error: ${err && err.message ? err.message : "agent loop failed"}`);
1338
+ })
1339
+ .finally(() => {
1340
+ if (closing) return;
1341
+ refreshAgents();
1342
+ setPrompt();
1343
+ renderDashboard();
1344
+ input.focus();
1345
+ screen.render();
1346
+ });
1347
+ };
1348
+
1349
+ input.key(["enter"], () => {
1350
+ submitInput(input.getValue());
1351
+ return false;
1352
+ });
1353
+ input.key(["up"], () => {
1354
+ const currentValue = input.getValue();
1355
+ if (shouldClearAgentSelectionOnUp({
1356
+ agentSelectionMode,
1357
+ inputValue: currentValue,
1358
+ })) {
1359
+ const previousTarget = targetAgent;
1360
+ targetAgent = null;
1361
+ selectedAgentIndex = -1;
1362
+ agentSelectionMode = false;
1363
+ setPrompt();
1364
+ renderDashboard();
1365
+ // Target selection cleared - removed redundant log
1366
+ input.focus();
1367
+ return false;
1368
+ }
1369
+ if (inputHistory.length === 0) return;
1370
+ historyIndex = Math.max(0, historyIndex - 1);
1371
+ setInputValue(inputHistory[historyIndex] || "");
1372
+ });
1373
+ input.key(["down"], () => {
1374
+ const currentValue = input.getValue();
1375
+ const historyTransition = resolveHistoryDownTransition({
1376
+ inputHistory,
1377
+ historyIndex,
1378
+ currentValue,
1379
+ });
1380
+ if (historyTransition.moved) {
1381
+ historyIndex = historyTransition.nextHistoryIndex;
1382
+ setInputValue(historyTransition.nextValue);
1383
+ return false;
1384
+ }
1385
+
1386
+ if (shouldEnterAgentSelection(currentValue)) {
1387
+ const cachedAgents = Array.isArray(activeAgents) ? activeAgents.slice() : [];
1388
+ const cachedMeta = activeAgentMetaMap instanceof Map ? new Map(activeAgentMetaMap) : new Map();
1389
+ if (!agentSelectionMode) {
1390
+ refreshAgents();
1391
+ }
1392
+ if (!agentSelectionMode && activeAgents.length === 0 && cachedAgents.length > 0) {
1393
+ activeAgents = cachedAgents;
1394
+ activeAgentMetaMap = cachedMeta;
1395
+ }
1396
+ const decision = resolveAgentSelectionOnDown({
1397
+ agentSelectionMode,
1398
+ selectedAgentIndex,
1399
+ totalAgents: activeAgents.length,
1400
+ });
1401
+ if (decision.action === "enter") {
1402
+ selectedAgentIndex = decision.index;
1403
+ targetAgent = activeAgents[selectedAgentIndex];
1404
+ agentSelectionMode = true;
1405
+ setPrompt();
1406
+ renderDashboard();
1407
+ // Removed redundant target selection log
1408
+ input.focus();
1409
+ return false;
1410
+ }
1411
+ if (decision.action === "hold") {
1412
+ return false;
1413
+ }
1414
+ }
1415
+ return false;
1416
+ });
1417
+ input.key(["left"], () => {
1418
+ const currentValue = input.getValue();
1419
+ if (agentSelectionMode && shouldEnterAgentSelection(currentValue)) {
1420
+ if (activeAgents.length === 0) refreshAgents();
1421
+ if (activeAgents.length === 0) return false;
1422
+ selectedAgentIndex = cycleAgentSelectionIndex(selectedAgentIndex, activeAgents.length, "left");
1423
+ targetAgent = activeAgents[selectedAgentIndex];
1424
+ setPrompt();
1425
+ renderDashboard();
1426
+ // Removed redundant target switch log
1427
+ input.focus();
1428
+ return false;
1429
+ }
1430
+ const next = moveCursorHorizontally(cursorPos, currentValue, "left");
1431
+ if (next !== cursorPos) {
1432
+ cursorPos = next;
1433
+ ensureInputCursorVisible();
1434
+ input._updateCursor();
1435
+ screen.render();
1436
+ }
1437
+ return false;
1438
+ });
1439
+ input.key(["right"], () => {
1440
+ const currentValue = input.getValue();
1441
+ if (agentSelectionMode && shouldEnterAgentSelection(currentValue)) {
1442
+ if (activeAgents.length === 0) refreshAgents();
1443
+ if (activeAgents.length === 0) return false;
1444
+ selectedAgentIndex = cycleAgentSelectionIndex(selectedAgentIndex, activeAgents.length, "right");
1445
+ targetAgent = activeAgents[selectedAgentIndex];
1446
+ setPrompt();
1447
+ renderDashboard();
1448
+ // Removed redundant target switch log
1449
+ input.focus();
1450
+ return false;
1451
+ }
1452
+ const next = moveCursorHorizontally(cursorPos, currentValue, "right");
1453
+ if (next !== cursorPos) {
1454
+ cursorPos = next;
1455
+ ensureInputCursorVisible();
1456
+ input._updateCursor();
1457
+ screen.render();
1458
+ }
1459
+ return false;
1460
+ });
1461
+
1462
+ screen.key(["tab"], () => {
1463
+ refreshAgents();
1464
+ if (activeAgents.length === 0) return;
1465
+ if (selectedAgentIndex < 0) selectedAgentIndex = 0;
1466
+ else selectedAgentIndex = (selectedAgentIndex + 1) % activeAgents.length;
1467
+ targetAgent = activeAgents[selectedAgentIndex];
1468
+ agentSelectionMode = true;
1469
+ setPrompt();
1470
+ renderDashboard();
1471
+ // Removed redundant target switch log
1472
+ input.focus();
1473
+ });
1474
+ screen.key(["S-tab"], () => {
1475
+ refreshAgents();
1476
+ if (activeAgents.length === 0) return;
1477
+ if (selectedAgentIndex < 0) selectedAgentIndex = 0;
1478
+ else selectedAgentIndex = (selectedAgentIndex - 1 + activeAgents.length) % activeAgents.length;
1479
+ targetAgent = activeAgents[selectedAgentIndex];
1480
+ agentSelectionMode = true;
1481
+ setPrompt();
1482
+ renderDashboard();
1483
+ // Removed redundant target switch log
1484
+ input.focus();
1485
+ });
1486
+ screen.key(["C-o"], () => {
1487
+ if (!lastMergedToolGroup || lastMergedToolGroup.expanded) return;
1488
+ if (!Array.isArray(lastMergedToolGroup.entries) || lastMergedToolGroup.entries.length < 2) return;
1489
+ const lines = buildMergedToolExpandedLines(lastMergedToolGroup.entries);
1490
+ for (let i = 0; i < lines.length; i += 1) {
1491
+ const branch = i === lines.length - 1 ? "└" : "│";
1492
+ logBox.log(`{gray-fg}${branch}{/gray-fg} ${escapeBlessed(lines[i])}`);
1493
+ }
1494
+ lastMergedToolGroup.expanded = true;
1495
+ if (activeToolMerge && activeToolMerge.id === lastMergedToolGroup.id) {
1496
+ activeToolMerge = null;
1497
+ }
1498
+ screen.render();
1499
+ });
1500
+ input.key(["escape"], () => {
1501
+ if (pendingTask && pendingTask.abortController && !pendingTask.abortController.signal.aborted) {
1502
+ try {
1503
+ pendingTask.abortController.abort();
1504
+ } catch {
1505
+ // ignore
1506
+ }
1507
+ logControlAction("Cancellation requested. Stopping the current task...");
1508
+ updateStatus("Cancelling...", "waiting", {
1509
+ showTimer: true,
1510
+ startedAt: pendingTask.startedAt,
1511
+ });
1512
+ return false;
1513
+ }
1514
+ const previousTarget = targetAgent;
1515
+ targetAgent = null;
1516
+ selectedAgentIndex = -1;
1517
+ agentSelectionMode = false;
1518
+ input.setValue("");
1519
+ setPrompt();
1520
+ renderDashboard();
1521
+ // Target selection cleared - removed redundant log
1522
+ input.focus();
1523
+ return false;
1524
+ });
1525
+ screen.key(["C-c"], () => closeWithCode(0));
1526
+ screen.on("resize", () => {
1527
+ renderDashboard();
1528
+ screen.render();
1529
+ });
1530
+
1531
+ const nickname = process.env.UFOO_NICKNAME || "";
1532
+ const subscriberId = currentSubscriberId;
1533
+ const agentId = subscriberId.includes(":") ? subscriberId.split(":")[1] : "";
1534
+ const bannerLines = buildUcodeBannerBlessedLines({
1535
+ model: state.model || process.env.UFOO_UCODE_MODEL || "",
1536
+ engine: state.engine || "ufoo-core",
1537
+ nickname,
1538
+ agentId,
1539
+ workspaceRoot,
1540
+ sessionId: state.sessionId || "",
1541
+ width: (stdout && stdout.columns) || 80,
1542
+ });
1543
+ for (const line of bannerLines) {
1544
+ logBox.log(String(line || ""));
1545
+ }
1546
+ logBox.log("");
1547
+
1548
+ refreshAgents();
1549
+ setPrompt();
1550
+ updateStatus("", "none");
1551
+ renderDashboard();
1552
+ if (autoBusEnabled) {
1553
+ autoBusTimer = setInterval(() => {
1554
+ scheduleAutoBus();
1555
+ }, 800);
1556
+ scheduleAutoBus();
1557
+ }
1558
+ input.focus();
1559
+ screen.render();
1560
+ });
1561
+ }
1562
+
1563
+ module.exports = {
1564
+ UCODE_BANNER_LINES,
1565
+ UCODE_VERSION,
1566
+ StreamBuffer,
1567
+ buildUcodeBannerLines,
1568
+ buildUcodeBannerBlessedLines,
1569
+ parseActiveAgentsFromBusStatus,
1570
+ shouldUseUcodeTui,
1571
+ renderLogLinesWithMarkdown,
1572
+ shouldEnterAgentSelection,
1573
+ resolveAgentSelectionOnDown,
1574
+ cycleAgentSelectionIndex,
1575
+ shouldClearAgentSelectionOnUp,
1576
+ moveCursorHorizontally,
1577
+ resolveHistoryDownTransition,
1578
+ filterSelectableAgents,
1579
+ stripLeakedEscapeTags,
1580
+ createEscapeTagStripper,
1581
+ formatPendingElapsed,
1582
+ normalizeBashToolCommand,
1583
+ normalizeToolMergeEntry,
1584
+ buildMergedToolSummaryText,
1585
+ buildMergedToolExpandedLines,
1586
+ runUcodeTui,
1587
+ };