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
@@ -0,0 +1,974 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Pure formatting + input-math helpers for the ink-based TUIs under
5
+ * src/ui/ink/. No terminal widget import is allowed in this module.
6
+ */
7
+
8
+ const chalk = require("chalk");
9
+ const pkg = require("../../../package.json");
10
+
11
+ const UCODE_BANNER_LINES = [
12
+ "█ █ █▀▀ █▀█ █▀▄ █▀▀",
13
+ "█ █ █ █ █ █ █ █▀ ",
14
+ "▀▀▀ ▀▀▀ ▀▀▀ ▀▀ ▀▀▀",
15
+ ];
16
+
17
+ const UCODE_VERSION = String((pkg && pkg.version) || "dev");
18
+
19
+ const STATUS_INDICATORS = {
20
+ thinking: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
21
+ typing: ["◐", "◓", "◑", "◒"],
22
+ waiting: ["∙", "∙∙", "∙∙∙", "∙∙", "∙"],
23
+ };
24
+
25
+ // Friendly labels for the tool-call events surfaced in the status line.
26
+ // Keep this list in sync with the keys handled by buildMergedToolSummaryText.
27
+ const TOOL_LABELS = {
28
+ read: "Reading file",
29
+ write: "Writing file",
30
+ edit: "Editing file",
31
+ bash: "Running command",
32
+ };
33
+
34
+ const ANSI_PATTERN = /\x1B\[[0-9;?]*[ -/]*[@-~]/g;
35
+
36
+ function charDisplayWidth(char = "") {
37
+ if (!char) return 0;
38
+ const code = char.codePointAt(0) || 0;
39
+ if (code === 0) return 0;
40
+ if (code < 32 || (code >= 0x7f && code < 0xa0)) return 0;
41
+ if ((code >= 0x0300 && code <= 0x036f) ||
42
+ (code >= 0x1ab0 && code <= 0x1aff) ||
43
+ (code >= 0x1dc0 && code <= 0x1dff) ||
44
+ (code >= 0x20d0 && code <= 0x20ff) ||
45
+ (code >= 0xfe20 && code <= 0xfe2f)) {
46
+ return 0;
47
+ }
48
+ if ((code >= 0x1100 && code <= 0x115f) ||
49
+ code === 0x2329 ||
50
+ code === 0x232a ||
51
+ (code >= 0x2e80 && code <= 0xa4cf) ||
52
+ (code >= 0xac00 && code <= 0xd7a3) ||
53
+ (code >= 0xf900 && code <= 0xfaff) ||
54
+ (code >= 0xfe10 && code <= 0xfe19) ||
55
+ (code >= 0xfe30 && code <= 0xfe6f) ||
56
+ (code >= 0xff00 && code <= 0xff60) ||
57
+ (code >= 0xffe0 && code <= 0xffe6) ||
58
+ (code >= 0x1f300 && code <= 0x1faff)) {
59
+ return 2;
60
+ }
61
+ return 1;
62
+ }
63
+
64
+ function displayCellWidth(text = "") {
65
+ return Array.from(String(text || "").replace(ANSI_PATTERN, "")).reduce(
66
+ (sum, char) => sum + charDisplayWidth(char),
67
+ 0
68
+ );
69
+ }
70
+
71
+ class StreamBuffer {
72
+ constructor(writer, options = {}) {
73
+ this.writer = writer;
74
+ this.buffer = "";
75
+ this.delay = options.delay || 8;
76
+ this.chunkSize = options.chunkSize || 3;
77
+ this.isStreaming = false;
78
+ this.streamPromise = null;
79
+ }
80
+
81
+ async write(text) {
82
+ this.buffer += text;
83
+ if (!this.isStreaming) {
84
+ this.isStreaming = true;
85
+ this.streamPromise = this.flush();
86
+ }
87
+ return this.streamPromise;
88
+ }
89
+
90
+ async flush() {
91
+ while (this.buffer.length > 0) {
92
+ const chunk = this.buffer.slice(0, this.chunkSize);
93
+ this.buffer = this.buffer.slice(this.chunkSize);
94
+ this.writer(chunk);
95
+ if (this.buffer.length > 0) {
96
+ await new Promise((resolve) => setTimeout(resolve, this.delay));
97
+ }
98
+ }
99
+ this.isStreaming = false;
100
+ }
101
+
102
+ async finish() {
103
+ if (this.isStreaming) {
104
+ await this.streamPromise;
105
+ }
106
+ if (this.buffer.length > 0) {
107
+ this.writer(this.buffer);
108
+ this.buffer = "";
109
+ }
110
+ }
111
+ }
112
+
113
+ function normalizeModelLabel(model = "") {
114
+ const text = String(model || "").trim();
115
+ if (text) return text;
116
+ return "default";
117
+ }
118
+
119
+ function buildUcodeBannerLines({ model = "", engine = "ufoo-core", nickname = "", agentId = "", workspaceRoot = "", sessionId = "", width = 0 } = {}) {
120
+ const modelLabel = normalizeModelLabel(model);
121
+ void width;
122
+ void engine;
123
+ void nickname;
124
+ void agentId;
125
+
126
+ const path = require("path");
127
+ const os = require("os");
128
+ const currentDir = workspaceRoot || process.cwd();
129
+ const homeDir = os.homedir();
130
+
131
+ let shortPath = currentDir;
132
+ if (currentDir.startsWith(homeDir)) {
133
+ shortPath = currentDir.replace(homeDir, "~");
134
+ }
135
+ shortPath = path.normalize(shortPath);
136
+
137
+ const logoLines = UCODE_BANNER_LINES.map((line) => chalk.cyan(line));
138
+ const infoLines = [];
139
+ infoLines.push(`${chalk.dim("Version:")} ${chalk.cyan.bold(UCODE_VERSION)}`);
140
+ infoLines.push(`${chalk.dim("Model:")} ${chalk.yellow(modelLabel)}`);
141
+ infoLines.push(`${chalk.dim("Dictionary:")} ${chalk.gray(shortPath)}`);
142
+ const normalizedSessionId = String(sessionId || "").trim();
143
+ if (normalizedSessionId) {
144
+ infoLines.push(`${chalk.dim("Session:")} ${chalk.gray(normalizedSessionId)}`);
145
+ }
146
+ const logoPadding = " ".repeat(
147
+ UCODE_BANNER_LINES.reduce((max, line) => Math.max(max, String(line || "").length), 0)
148
+ );
149
+ const rows = Math.max(logoLines.length, infoLines.length);
150
+
151
+ return Array.from({ length: rows }, (_, index) => {
152
+ const logoLine = logoLines[index] || logoPadding;
153
+ const info = infoLines[index] || "";
154
+ return ` ${logoLine} ${info}`;
155
+ });
156
+ }
157
+
158
+ function shouldUseUcodeTui({ stdin, stdout, jsonOutput, forceTui = false, disableTui = false } = {}) {
159
+ if (disableTui) return false;
160
+ if (jsonOutput) return false;
161
+ if (forceTui) return true;
162
+ return Boolean(stdin && stdin.isTTY && stdout && stdout.isTTY);
163
+ }
164
+
165
+ function parseActiveAgentsFromBusStatus(busStatus = "") {
166
+ const lines = String(busStatus || "").replace(ANSI_PATTERN, "").split(/\r?\n/);
167
+ const agents = [];
168
+ let inOnlineSection = false;
169
+
170
+ for (const line of lines) {
171
+ const trimmed = String(line || "").trim();
172
+ if (!trimmed) continue;
173
+
174
+ if (/^Online agents:\s*$/i.test(trimmed)) {
175
+ inOnlineSection = true;
176
+ continue;
177
+ }
178
+ if (!inOnlineSection) continue;
179
+
180
+ if (/^\(none\)$/i.test(trimmed)) {
181
+ continue;
182
+ }
183
+
184
+ if (/^[A-Za-z][A-Za-z ]+:\s*$/.test(trimmed)) {
185
+ break;
186
+ }
187
+
188
+ const rawId = trimmed.replace(/\s+\([^)]+\)\s*$/, "");
189
+ if (!rawId) continue;
190
+ const [type, ...idParts] = rawId.split(":");
191
+ const id = idParts.join(":");
192
+ if (!type) continue;
193
+
194
+ agents.push({
195
+ type,
196
+ id,
197
+ status: "active",
198
+ fullId: rawId,
199
+ nickname: (trimmed.match(/\(([^)]+)\)\s*$/) || [])[1] || "",
200
+ });
201
+ }
202
+
203
+ if (agents.length === 0) {
204
+ for (const line of lines) {
205
+ const trimmed = String(line || "").trim();
206
+ const match = trimmed.match(/^([a-z-]+):([a-f0-9]+)\s+\((active|idle)\)$/);
207
+ if (!match) continue;
208
+ agents.push({
209
+ type: match[1],
210
+ id: match[2],
211
+ status: match[3],
212
+ fullId: `${match[1]}:${match[2]}`,
213
+ nickname: "",
214
+ });
215
+ }
216
+ }
217
+
218
+ return agents;
219
+ }
220
+
221
+ function loadActiveAgents(workspaceRoot) {
222
+ try {
223
+ const { execSync } = require("child_process");
224
+ const busStatus = execSync("ufoo bus status", {
225
+ cwd: workspaceRoot,
226
+ encoding: "utf8",
227
+ });
228
+ return parseActiveAgentsFromBusStatus(busStatus);
229
+ } catch {
230
+ return [];
231
+ }
232
+ }
233
+
234
+ function renderLogLinesWithMarkdown(text = "", state = {}, escapeFn = (value) => String(value || "")) {
235
+ const { renderMarkdownLines } = require("./markdownRenderer");
236
+ return renderMarkdownLines(text, state, escapeFn);
237
+ }
238
+
239
+ function shouldEnterAgentSelection(inputValue = "") {
240
+ const text = String(inputValue || "");
241
+ const trimmed = text.trim();
242
+ return !trimmed;
243
+ }
244
+
245
+ function resolveAgentSelectionOnDown({
246
+ agentSelectionMode = false,
247
+ selectedAgentIndex = -1,
248
+ totalAgents = 0,
249
+ } = {}) {
250
+ const total = Number.isFinite(totalAgents) ? Math.max(0, Math.floor(totalAgents)) : 0;
251
+ if (total <= 0) return { action: "none", index: -1 };
252
+ if (agentSelectionMode) {
253
+ const keep = selectedAgentIndex >= 0 && selectedAgentIndex < total ? selectedAgentIndex : 0;
254
+ return { action: "hold", index: keep };
255
+ }
256
+ const enter = selectedAgentIndex >= 0 && selectedAgentIndex < total ? selectedAgentIndex : 0;
257
+ return { action: "enter", index: enter };
258
+ }
259
+
260
+ function cycleAgentSelectionIndex(selectedAgentIndex = -1, totalAgents = 0, direction = "right") {
261
+ const total = Number.isFinite(totalAgents) ? Math.max(0, Math.floor(totalAgents)) : 0;
262
+ if (total <= 0) return -1;
263
+ const current = selectedAgentIndex >= 0 && selectedAgentIndex < total ? selectedAgentIndex : 0;
264
+ if (direction === "left") {
265
+ return (current - 1 + total) % total;
266
+ }
267
+ return (current + 1) % total;
268
+ }
269
+
270
+ function shouldClearAgentSelectionOnUp({
271
+ agentSelectionMode = false,
272
+ inputValue = "",
273
+ } = {}) {
274
+ return Boolean(agentSelectionMode && shouldEnterAgentSelection(inputValue));
275
+ }
276
+
277
+ function moveCursorHorizontally(cursorPos = 0, inputValue = "", direction = "right") {
278
+ const text = String(inputValue || "");
279
+ const max = text.length;
280
+ const pos = Number.isFinite(cursorPos) ? Math.max(0, Math.floor(cursorPos)) : 0;
281
+ if (direction === "left") return Math.max(0, pos - 1);
282
+ return Math.min(max, pos + 1);
283
+ }
284
+
285
+ function clampCursorPos(cursorPos = 0, inputValue = "") {
286
+ const text = String(inputValue || "");
287
+ const pos = Number.isFinite(cursorPos) ? Math.floor(cursorPos) : 0;
288
+ return Math.max(0, Math.min(text.length, pos));
289
+ }
290
+
291
+ function findLogicalLineStart(inputValue = "", cursorPos = 0) {
292
+ const text = String(inputValue || "");
293
+ const pos = clampCursorPos(cursorPos, text);
294
+ const prevNewline = text.lastIndexOf("\n", Math.max(0, pos - 1));
295
+ return prevNewline === -1 ? 0 : prevNewline + 1;
296
+ }
297
+
298
+ function findLogicalLineEnd(inputValue = "", cursorPos = 0) {
299
+ const text = String(inputValue || "");
300
+ const pos = clampCursorPos(cursorPos, text);
301
+ const nextNewline = text.indexOf("\n", pos);
302
+ return nextNewline === -1 ? text.length : nextNewline;
303
+ }
304
+
305
+ function moveCursorToVisualLineBoundary({
306
+ cursorPos = 0,
307
+ inputValue = "",
308
+ width = 80,
309
+ boundary = "start",
310
+ strWidth,
311
+ } = {}) {
312
+ const inputMath = require("../../app/chat/inputMath");
313
+ const text = String(inputValue || "");
314
+ const normalizedWidth = Number.isFinite(width) ? Math.max(1, Math.floor(width)) : 1;
315
+ const pos = clampCursorPos(cursorPos, text);
316
+ const { row } = inputMath.getCursorRowCol(text, pos, normalizedWidth, strWidth);
317
+ if (boundary === "end") {
318
+ return inputMath.getCursorPosForRowCol(text, row, normalizedWidth, normalizedWidth, strWidth);
319
+ }
320
+ return inputMath.getCursorPosForRowCol(text, row, 0, normalizedWidth, strWidth);
321
+ }
322
+
323
+ function moveCursorVertically({
324
+ cursorPos = 0,
325
+ inputValue = "",
326
+ width = 80,
327
+ direction = "down",
328
+ preferredCol = null,
329
+ strWidth,
330
+ } = {}) {
331
+ const inputMath = require("../../app/chat/inputMath");
332
+ const text = String(inputValue || "");
333
+ const normalizedWidth = Number.isFinite(width) ? Math.max(1, Math.floor(width)) : 1;
334
+ const pos = clampCursorPos(cursorPos, text);
335
+ const { row, col } = inputMath.getCursorRowCol(text, pos, normalizedWidth, strWidth);
336
+ const totalRows = inputMath.countLines(text, normalizedWidth, strWidth);
337
+ const targetCol = Number.isFinite(preferredCol) ? preferredCol : col;
338
+
339
+ if (direction === "up") {
340
+ if (row <= 0) {
341
+ return { moved: false, nextCursorPos: pos, preferredCol: targetCol, boundary: "top" };
342
+ }
343
+ return {
344
+ moved: true,
345
+ nextCursorPos: inputMath.getCursorPosForRowCol(text, row - 1, targetCol, normalizedWidth, strWidth),
346
+ preferredCol: targetCol,
347
+ boundary: "",
348
+ };
349
+ }
350
+
351
+ if (row >= totalRows - 1) {
352
+ return { moved: false, nextCursorPos: pos, preferredCol: targetCol, boundary: "bottom" };
353
+ }
354
+ return {
355
+ moved: true,
356
+ nextCursorPos: inputMath.getCursorPosForRowCol(text, row + 1, targetCol, normalizedWidth, strWidth),
357
+ preferredCol: targetCol,
358
+ boundary: "",
359
+ };
360
+ }
361
+
362
+ function deleteWordBeforeCursor(inputValue = "", cursorPos = 0) {
363
+ const text = String(inputValue || "");
364
+ const pos = clampCursorPos(cursorPos, text);
365
+ if (pos <= 0) return { value: text, cursorPos: pos };
366
+ const before = text.slice(0, pos);
367
+ const after = text.slice(pos);
368
+ const match = before.match(/\s*\S+\s*$/);
369
+ const start = match ? pos - match[0].length : Math.max(0, pos - 1);
370
+ return {
371
+ value: before.slice(0, start) + after,
372
+ cursorPos: start,
373
+ };
374
+ }
375
+
376
+ function moveCursorByWord(inputValue = "", cursorPos = 0, direction = "forward") {
377
+ const text = String(inputValue || "");
378
+ const pos = clampCursorPos(cursorPos, text);
379
+ if (direction === "backward") {
380
+ const before = text.slice(0, pos);
381
+ const trimmedEnd = before.search(/\S\s*$/) >= 0 ? before.replace(/\s+$/, "") : before;
382
+ const match = trimmedEnd.match(/\S+$/);
383
+ return match ? trimmedEnd.length - match[0].length : 0;
384
+ }
385
+ const after = text.slice(pos);
386
+ const match = after.match(/^\s*\S+/);
387
+ return match ? Math.min(text.length, pos + match[0].length) : text.length;
388
+ }
389
+
390
+ function resolveHistoryDownTransition({
391
+ inputHistory = [],
392
+ historyIndex = 0,
393
+ currentValue = "",
394
+ } = {}) {
395
+ const history = Array.isArray(inputHistory) ? inputHistory : [];
396
+ if (history.length <= 0) {
397
+ return {
398
+ moved: false,
399
+ nextHistoryIndex: Number.isFinite(historyIndex) ? Math.max(0, Math.floor(historyIndex)) : 0,
400
+ nextValue: String(currentValue || ""),
401
+ };
402
+ }
403
+ const currentIndex = Number.isFinite(historyIndex) ? Math.max(0, Math.floor(historyIndex)) : 0;
404
+ if (currentIndex >= history.length) {
405
+ return {
406
+ moved: false,
407
+ nextHistoryIndex: history.length,
408
+ nextValue: String(currentValue || ""),
409
+ };
410
+ }
411
+ const nextHistoryIndex = Math.min(history.length, currentIndex + 1);
412
+ const nextValue = nextHistoryIndex >= history.length ? "" : String(history[nextHistoryIndex] || "");
413
+ const moved = nextHistoryIndex !== currentIndex || nextValue !== String(currentValue || "");
414
+ return {
415
+ moved,
416
+ nextHistoryIndex,
417
+ nextValue,
418
+ };
419
+ }
420
+
421
+ function filterSelectableAgents(agents = [], selfSubscriberId = "") {
422
+ const selfId = String(selfSubscriberId || "").trim();
423
+ const list = Array.isArray(agents) ? agents : [];
424
+ if (!selfId) {
425
+ return list.filter((agent) => {
426
+ const fullId = String(agent && agent.fullId ? agent.fullId : "").trim();
427
+ const type = String(agent && agent.type ? agent.type : "").trim();
428
+ if (fullId === "ufoo-agent") return false;
429
+ if (type === "ufoo-agent") return false;
430
+ return true;
431
+ });
432
+ }
433
+ return list.filter((agent) => {
434
+ const fullId = String(agent && agent.fullId ? agent.fullId : "").trim();
435
+ const type = String(agent && agent.type ? agent.type : "").trim();
436
+ if (!fullId) return true;
437
+ if (fullId === "ufoo-agent") return false;
438
+ if (type === "ufoo-agent") return false;
439
+ return fullId !== selfId;
440
+ });
441
+ }
442
+
443
+ function stripLeakedEscapeTags(text = "") {
444
+ const source = String(text == null ? "" : text);
445
+ const withoutClosedTags = source.replace(/\{[^{}\n]*escape[^{}\n]*\}/gi, "");
446
+ const withoutDanglingEscape = withoutClosedTags.replace(/\{\s*\/?\s*escape[\s\S]*$/gi, "");
447
+ return withoutDanglingEscape.replace(/\{\s*\/?\s*e?s?c?a?p?e?[^{}\n]*$/gi, "");
448
+ }
449
+
450
+ function findTrailingEscapeTagPrefix(text = "") {
451
+ const raw = String(text == null ? "" : text);
452
+ if (!raw) return "";
453
+ const windowSize = 40;
454
+ const tail = raw.slice(Math.max(0, raw.length - windowSize));
455
+ const braceIndex = tail.lastIndexOf("{");
456
+ if (braceIndex < 0) return "";
457
+ const suffix = tail.slice(braceIndex);
458
+ if (suffix.includes("}")) return "";
459
+
460
+ const compact = suffix.toLowerCase().replace(/\s+/g, "");
461
+ if (!compact.startsWith("{")) return "";
462
+ if (/^\{\/?e?s?c?a?p?e?[^}]*$/.test(compact)) {
463
+ return suffix;
464
+ }
465
+ return "";
466
+ }
467
+
468
+ function createEscapeTagStripper() {
469
+ let carry = "";
470
+
471
+ return {
472
+ write(chunk = "") {
473
+ const incoming = String(chunk == null ? "" : chunk);
474
+ if (!incoming && !carry) return "";
475
+ const combined = `${carry}${incoming}`;
476
+ const trailing = findTrailingEscapeTagPrefix(combined);
477
+ const safeText = trailing
478
+ ? combined.slice(0, combined.length - trailing.length)
479
+ : combined;
480
+ carry = trailing;
481
+ return stripLeakedEscapeTags(safeText);
482
+ },
483
+ flush() {
484
+ if (!carry) return "";
485
+ const rest = "";
486
+ carry = "";
487
+ return rest;
488
+ },
489
+ };
490
+ }
491
+
492
+ function formatPendingElapsed(ms = 0) {
493
+ const totalSeconds = Math.max(0, Math.floor(Number(ms) / 1000));
494
+ return `${totalSeconds} s`;
495
+ }
496
+
497
+ function normalizeBashToolCommand(args = {}, payload = {}) {
498
+ const argObj = args && typeof args === "object" ? args : {};
499
+ const resObj = payload && typeof payload === "object" ? payload : {};
500
+ const command = String(argObj.command || argObj.cmd || "").trim();
501
+ const code = Number.isFinite(resObj.code) ? `exit ${resObj.code}` : "";
502
+ return [command, code].filter(Boolean).join(" · ");
503
+ }
504
+
505
+ function normalizeToolMergeEntry(entry = {}) {
506
+ const source = entry && typeof entry === "object" ? entry : {};
507
+ const tool = String(source.tool || "").trim().toLowerCase() || "tool";
508
+ const detail = String(source.detail || "").trim();
509
+ const isError = Boolean(source.isError);
510
+ const errorText = String(source.errorText || "").trim();
511
+ const summary = [tool, detail].filter(Boolean).join(" · ") || tool;
512
+ return {
513
+ tool,
514
+ detail,
515
+ isError,
516
+ errorText,
517
+ summary,
518
+ };
519
+ }
520
+
521
+ function buildMergedToolSummaryText(entries = []) {
522
+ const list = Array.isArray(entries)
523
+ ? entries.map((item) => normalizeToolMergeEntry(item))
524
+ : [];
525
+ const count = list.length;
526
+ if (count <= 0) return "Ran tool";
527
+ const first = list[0];
528
+ if (count === 1) return `Ran ${first.summary}`;
529
+ const errorCount = list.filter((item) => item.isError).length;
530
+ const errorSuffix = errorCount > 0 ? ` · ${errorCount} error${errorCount === 1 ? "" : "s"}` : "";
531
+ return `Ran ${first.summary} · … +${count - 1} calls${errorSuffix}`;
532
+ }
533
+
534
+ function buildMergedToolExpandedLines(entries = []) {
535
+ const list = Array.isArray(entries)
536
+ ? entries.map((item) => normalizeToolMergeEntry(item))
537
+ : [];
538
+ const maxLength = 120;
539
+ return list.map((item) => {
540
+ const base = item.summary;
541
+ let line;
542
+ if (!item.isError) {
543
+ line = base;
544
+ } else {
545
+ line = item.errorText ? `${base} · error: ${item.errorText}` : `${base} · error`;
546
+ }
547
+ if (line.length > maxLength) {
548
+ return line.slice(0, maxLength - 3) + "...";
549
+ }
550
+ return line;
551
+ });
552
+ }
553
+
554
+ // Composed live-row text for an in-flight tool group: shows the merged
555
+ // summary, plus a "(Ctrl+O expand)" hint once at least two entries are
556
+ // present.
557
+ function buildToolMergeRowText(entries = []) {
558
+ const list = Array.isArray(entries) ? entries : [];
559
+ const summary = buildMergedToolSummaryText(list);
560
+ if (list.length >= 2) return `· ${summary} (Ctrl+O expand)`;
561
+ return `· ${summary}`;
562
+ }
563
+
564
+ /**
565
+ * Lay out the global-mode project rail inside a single line. Like
566
+ * planAgentsFooter, but with two differences:
567
+ * - the caller provides `windowStart` so the rail can scroll horizontally
568
+ * under cursor control rather than dropping items at the end;
569
+ * - we normally avoid truncating individual labels, but the selected
570
+ * project is always represented by at least one visible chip.
571
+ *
572
+ * Returns { items, windowStart, leftMore, rightMore } where items is the
573
+ * sub-array of `labels` that fits and windowStart is the (possibly
574
+ * adjusted) starting index after clamping for the selection cursor.
575
+ */
576
+ function planProjectsRail({
577
+ labels = [],
578
+ selectedIndex = -1,
579
+ windowStart = 0,
580
+ maxCells = 80,
581
+ } = {}) {
582
+ const items = Array.isArray(labels) ? labels.map(String) : [];
583
+ if (items.length === 0) {
584
+ return { items: [], windowStart: 0, leftMore: false, rightMore: false };
585
+ }
586
+ const budget = Math.max(1, Math.floor(Number(maxCells) || 0));
587
+ const sepWidth = displayCellWidth(" ");
588
+ const moreLeft = "< ";
589
+ const moreRight = " >";
590
+ const moreLeftWidth = displayCellWidth(moreLeft);
591
+ const moreRightWidth = displayCellWidth(moreRight);
592
+ const overflowMarker = "...";
593
+
594
+ const truncateToCells = (label = "", cells = 1) => {
595
+ const limit = Math.max(1, Math.floor(Number(cells) || 0));
596
+ const text = String(label || "");
597
+ if (displayCellWidth(text) <= limit) return text;
598
+ const markerWidth = displayCellWidth(overflowMarker);
599
+ if (limit <= markerWidth) return overflowMarker.slice(0, limit);
600
+ let out = "";
601
+ let used = 0;
602
+ const bodyLimit = limit - markerWidth;
603
+ for (const ch of text) {
604
+ const width = displayCellWidth(ch);
605
+ if (used + width > bodyLimit) break;
606
+ out += ch;
607
+ used += width;
608
+ }
609
+ return `${out || text.slice(0, 1)}${overflowMarker}`;
610
+ };
611
+
612
+ // Clamp the requested windowStart so the cursor is visible.
613
+ let start = Math.max(0, Math.min(items.length - 1, Math.floor(Number(windowStart) || 0)));
614
+ if (selectedIndex >= 0 && selectedIndex < items.length && selectedIndex < start) {
615
+ start = selectedIndex;
616
+ }
617
+
618
+ // Greedy fit forward from `start`, reserving room for the < and > arrows
619
+ // when we can't fit everything.
620
+ const tryFit = (s) => {
621
+ const out = [];
622
+ let used = 0;
623
+ for (let i = s; i < items.length; i += 1) {
624
+ const label = items[i];
625
+ const labelWidth = displayCellWidth(label);
626
+ const lead = out.length === 0 ? 0 : sepWidth;
627
+ const reserveLeft = s > 0 ? moreLeftWidth : 0;
628
+ const reserveRight = i < items.length - 1 ? moreRightWidth : 0;
629
+ if (used + lead + labelWidth + reserveLeft + reserveRight > budget) break;
630
+ out.push({ index: i, label });
631
+ used += lead + labelWidth;
632
+ }
633
+ return out;
634
+ };
635
+
636
+ let visible = tryFit(start);
637
+ // If the selected index would fall past the end of the visible window,
638
+ // slide forward until it's covered.
639
+ if (selectedIndex >= 0) {
640
+ while (visible.length > 0 && visible[visible.length - 1].index < selectedIndex && start < items.length - 1) {
641
+ start += 1;
642
+ visible = tryFit(start);
643
+ }
644
+ }
645
+ // Never let the window slide so far that the selection drops off.
646
+ if (selectedIndex >= 0 && visible.length > 0 && visible[0].index > selectedIndex) {
647
+ start = selectedIndex;
648
+ visible = tryFit(start);
649
+ }
650
+
651
+ if (visible.length === 0) {
652
+ const fallbackIndex = selectedIndex >= 0 && selectedIndex < items.length ? selectedIndex : start;
653
+ start = fallbackIndex;
654
+ const reserveLeft = start > 0 ? moreLeftWidth : 0;
655
+ const reserveRight = fallbackIndex < items.length - 1 ? moreRightWidth : 0;
656
+ const labelBudget = Math.max(1, budget - reserveLeft - reserveRight);
657
+ visible = [{
658
+ index: fallbackIndex,
659
+ label: truncateToCells(items[fallbackIndex], labelBudget),
660
+ }];
661
+ }
662
+
663
+ return {
664
+ items: visible.map((v) => ({ label: v.label, absoluteIndex: v.index })),
665
+ windowStart: start,
666
+ leftMore: start > 0,
667
+ rightMore: visible.length > 0 && visible[visible.length - 1].index < items.length - 1,
668
+ };
669
+ }
670
+
671
+ /**
672
+ * Lay out the Agents footer inside a fixed cell budget. Returns:
673
+ * { items: [{ label, selected, truncated }], overflowed, hint }
674
+ *
675
+ * `hint` is the rendered "+N more" suffix (or "" when nothing was dropped),
676
+ * already including its leading separator. Callers should render
677
+ * items[0..n-1] separated by " " then append hint with no extra spacing.
678
+ *
679
+ * The planner reserves room for the worst-case hint width up front so the
680
+ * trailing label never has to be removed once we decide to print "+N more".
681
+ *
682
+ * `labels` is the array of strings to render (already prefixed with "@").
683
+ * `selectedIndex` is the agent under the selection cursor (or -1).
684
+ * `maxCells` is the total visual width available for the agent strip,
685
+ * separators included.
686
+ */
687
+ function planAgentsFooter(labels = [], selectedIndex = -1, maxCells = 80) {
688
+ const items = Array.isArray(labels) ? labels.map(String) : [];
689
+ const budget = Math.max(1, Math.floor(Number(maxCells) || 0));
690
+ const sepText = " ";
691
+ const sepWidth = displayCellWidth(sepText);
692
+ const overflowMarker = "...";
693
+ const overflowMarkerWidth = displayCellWidth(overflowMarker);
694
+
695
+ // Reserve worst-case "+N more" width once, where N can be at most
696
+ // labels.length. We treat this as a hard upper bound so we never have
697
+ // to backtrack and pop a label after committing to it.
698
+ const worstCaseHint = items.length > 0
699
+ ? ` +${items.length} more`
700
+ : "";
701
+ const worstCaseHintWidth = displayCellWidth(worstCaseHint);
702
+
703
+ const out = [];
704
+ let used = 0;
705
+ let firstOverflowAt = -1;
706
+
707
+ for (let i = 0; i < items.length; i += 1) {
708
+ const label = items[i];
709
+ const labelWidth = displayCellWidth(label);
710
+ const lead = out.length === 0 ? 0 : sepWidth;
711
+ const remainingItems = items.length - i - 1;
712
+ // Always keep room for the hint when there's at least one item that
713
+ // might not fit later. When this is the last label, the hint is empty
714
+ // so no reservation is needed.
715
+ const reserveHint = remainingItems > 0 ? worstCaseHintWidth : 0;
716
+
717
+ if (used + lead + labelWidth + reserveHint <= budget) {
718
+ out.push({ label, selected: i === selectedIndex, truncated: false });
719
+ used += lead + labelWidth;
720
+ continue;
721
+ }
722
+
723
+ // Try to fit a truncated version: room for "..." + at least 1 cell.
724
+ const reserveForCurrent = remainingItems > 0 ? worstCaseHintWidth : 0;
725
+ const remaining = budget - used - lead - overflowMarkerWidth - reserveForCurrent;
726
+ if (remaining > 0) {
727
+ let acc = "";
728
+ let accWidth = 0;
729
+ for (const ch of label) {
730
+ const w = displayCellWidth(ch);
731
+ if (accWidth + w > remaining) break;
732
+ acc += ch;
733
+ accWidth += w;
734
+ }
735
+ if (acc) {
736
+ out.push({
737
+ label: `${acc}${overflowMarker}`,
738
+ selected: i === selectedIndex,
739
+ truncated: true,
740
+ });
741
+ used += lead + accWidth + overflowMarkerWidth;
742
+ firstOverflowAt = i + 1;
743
+ break;
744
+ }
745
+ }
746
+ firstOverflowAt = i;
747
+ break;
748
+ }
749
+
750
+ const overflowed = firstOverflowAt < 0 ? 0 : items.length - firstOverflowAt;
751
+ const hint = overflowed > 0 ? ` +${overflowed} more` : "";
752
+ return { items: out, overflowed, hint };
753
+ }
754
+
755
+ /**
756
+ * Build a list of inline-completion suggestions for the current input.
757
+ * Returns at most `limit` items; an empty list means "no popup".
758
+ *
759
+ * Triggers:
760
+ * "/<prefix>" top-level slash commands matching <prefix>
761
+ * "/<cmd> <prefix>" sub-commands of <cmd> matching <prefix>
762
+ * "/<cmd> <sub> <prefix>" sub-sub-commands (e.g. /settings agent set)
763
+ * "@<prefix>" known agent ids/labels matching <prefix>
764
+ * Anything else returns no suggestions.
765
+ */
766
+ function buildCompletions({
767
+ text = "",
768
+ agents = [],
769
+ agentLabels = [],
770
+ commands = [],
771
+ commandTree = null,
772
+ groupTemplates = [],
773
+ soloProfiles = [],
774
+ limit = 8,
775
+ } = {}) {
776
+ const raw = String(text || "");
777
+ if (!raw) return [];
778
+ const trimmed = raw.trimStart();
779
+ const endsWithWhitespace = /\s$/.test(trimmed);
780
+
781
+ if (trimmed.startsWith("/")) {
782
+ const parts = trimmed.split(/\s+/);
783
+ const head = parts[0]; // "/launch"
784
+ const tail = parts.slice(1);
785
+
786
+ // Dynamic argument completion for /group run <alias> and
787
+ // /solo run <profile>. These pull from runtime sources (group
788
+ // templates, prompt-profile registry) rather than COMMAND_TREE.
789
+ const dynList = (head === "/group" && tail[0] === "run")
790
+ ? groupTemplates
791
+ : (head === "/solo" && tail[0] === "run")
792
+ ? soloProfiles
793
+ : null;
794
+ if (dynList && (tail.length >= 2 || trimmed.endsWith(" "))) {
795
+ const partial = String(tail[1] || "").toLowerCase();
796
+ const out = [];
797
+ for (const item of (Array.isArray(dynList) ? dynList : [])) {
798
+ const id = String((item && (item.alias || item.cmd || item.id || item.name)) || "");
799
+ if (!id) continue;
800
+ if (partial && !id.toLowerCase().startsWith(partial)) continue;
801
+ const desc = String((item && (item.desc || item.summary || item.description || item.source)) || "");
802
+ out.push({
803
+ kind: "argument",
804
+ label: `${head} ${tail[0]} ${id}`,
805
+ replace: `${head} ${tail[0]} ${id} `,
806
+ description: desc,
807
+ hasChildren: false,
808
+ });
809
+ if (out.length >= limit) break;
810
+ }
811
+ if (partial && out.length === 1) {
812
+ const candidate = String(out[0].replace || "").trim().split(/\s+/).pop() || "";
813
+ if (candidate.toLowerCase() === partial && !out[0].hasChildren) return [];
814
+ }
815
+ return out;
816
+ }
817
+
818
+ // Sub-command completion: "/cmd <prefix>" or "/cmd sub <prefix>".
819
+ if (tail.length >= 1 && commandTree) {
820
+ const headKey = head.startsWith("/") ? head : `/${head}`;
821
+ let node = commandTree[headKey];
822
+ if (!node || typeof node !== "object") return [];
823
+ // Walk into nested children for everything but the last token.
824
+ for (let i = 0; i < tail.length - 1; i += 1) {
825
+ const segment = tail[i];
826
+ if (!segment) return [];
827
+ const next = node && node.children && node.children[segment];
828
+ if (!next) return [];
829
+ node = next;
830
+ }
831
+ const children = node && node.children;
832
+ if (!children || typeof children !== "object") return [];
833
+ const partial = String(tail[tail.length - 1] || "").toLowerCase();
834
+ const prefixSoFar = `${head} ${tail.slice(0, -1).join(" ")}`.replace(/\s+$/, "");
835
+ // Sort by `order` (when present) then alphabetically — matches the
836
+ // sortCommands helper used by the blessed completion popup.
837
+ const entries = Object.keys(children).map((name) => ({
838
+ name,
839
+ ...children[name],
840
+ }));
841
+ entries.sort((a, b) => {
842
+ const orderA = Number.isFinite(a.order) ? a.order : 999;
843
+ const orderB = Number.isFinite(b.order) ? b.order : 999;
844
+ if (orderA !== orderB) return orderA - orderB;
845
+ return a.name.localeCompare(b.name);
846
+ });
847
+ const out = [];
848
+ for (const entry of entries) {
849
+ if (!entry.name.toLowerCase().startsWith(partial)) continue;
850
+ const hasDynamicArguments = (head === "/group" && entry.name === "run")
851
+ || (head === "/solo" && entry.name === "run");
852
+ out.push({
853
+ kind: "subcommand",
854
+ label: `${prefixSoFar} ${entry.name}`.trim(),
855
+ replace: `${prefixSoFar} ${entry.name} `.replace(/^\s+/, ""),
856
+ description: String(entry.desc || entry.summary || entry.description || ""),
857
+ hasChildren: Boolean((entry.children && typeof entry.children === "object") || hasDynamicArguments),
858
+ });
859
+ if (out.length >= limit) break;
860
+ }
861
+ if (!endsWithWhitespace && out.length === 1) {
862
+ const candidate = String(out[0].replace || "").trim().split(/\s+/).pop() || "";
863
+ if (candidate.toLowerCase() === partial && !out[0].hasChildren) return [];
864
+ }
865
+ return out;
866
+ }
867
+
868
+ // Top-level command completion.
869
+ const after = trimmed.slice(1);
870
+ const prefix = after.toLowerCase();
871
+ const list = Array.isArray(commands) ? commands : [];
872
+ const out = [];
873
+ for (const item of list) {
874
+ // Registry entries already include the leading '/' in `cmd`. Strip
875
+ // it before matching the user's prefix and put it back when we
876
+ // render so we don't end up with '//cron'.
877
+ const rawName = String((item && item.cmd) || item || "");
878
+ const bare = rawName.startsWith("/") ? rawName.slice(1) : rawName;
879
+ const lower = bare.toLowerCase();
880
+ if (!bare) continue;
881
+ if (!lower.startsWith(prefix)) continue;
882
+ out.push({
883
+ kind: "command",
884
+ label: `/${bare}`,
885
+ replace: `/${bare} `,
886
+ description: String((item && (item.desc || item.summary || item.description)) || ""),
887
+ hasChildren: Boolean(commandTree && commandTree[`/${bare}`] && commandTree[`/${bare}`].children),
888
+ });
889
+ if (out.length >= limit) break;
890
+ }
891
+ if (!endsWithWhitespace && out.length === 1) {
892
+ const candidate = String(out[0].replace || "").trim().replace(/^\//, "").toLowerCase();
893
+ if (candidate === prefix && !out[0].hasChildren) return [];
894
+ }
895
+ return out;
896
+ }
897
+
898
+ if (trimmed.startsWith("@")) {
899
+ const after = trimmed.slice(1);
900
+ if (after.includes(" ")) return [];
901
+ const prefix = after.toLowerCase();
902
+ const idList = Array.isArray(agents) ? agents : [];
903
+ const labelList = Array.isArray(agentLabels) ? agentLabels : [];
904
+ const seen = new Set();
905
+ const out = [];
906
+ for (let i = 0; i < idList.length; i += 1) {
907
+ const id = String(idList[i] || "");
908
+ const label = String((labelList[i] != null ? labelList[i] : id) || "");
909
+ if (!id) continue;
910
+ if (seen.has(id)) continue;
911
+ const idMatch = id.toLowerCase().startsWith(prefix);
912
+ const labelMatch = label.toLowerCase().startsWith(prefix);
913
+ if (!idMatch && !labelMatch) continue;
914
+ seen.add(id);
915
+ out.push({
916
+ kind: "agent",
917
+ label: `@${label}`,
918
+ replace: `@${label} `,
919
+ description: id !== label ? id : "",
920
+ });
921
+ if (out.length >= limit) break;
922
+ }
923
+ if (out.length === 1) {
924
+ const candidate = String(out[0].label || "").replace(/^@/, "").toLowerCase();
925
+ if (candidate === prefix) return [];
926
+ }
927
+ return out;
928
+ }
929
+
930
+ return [];
931
+ }
932
+
933
+ module.exports = {
934
+ ANSI_PATTERN,
935
+ STATUS_INDICATORS,
936
+ StreamBuffer,
937
+ TOOL_LABELS,
938
+ UCODE_BANNER_LINES,
939
+ UCODE_VERSION,
940
+ buildMergedToolExpandedLines,
941
+ buildMergedToolSummaryText,
942
+ buildToolMergeRowText,
943
+ buildCompletions,
944
+ buildUcodeBannerLines,
945
+ charDisplayWidth,
946
+ clampCursorPos,
947
+ createEscapeTagStripper,
948
+ cycleAgentSelectionIndex,
949
+ deleteWordBeforeCursor,
950
+ displayCellWidth,
951
+ filterSelectableAgents,
952
+ findLogicalLineEnd,
953
+ findLogicalLineStart,
954
+ findTrailingEscapeTagPrefix,
955
+ formatPendingElapsed,
956
+ loadActiveAgents,
957
+ moveCursorByWord,
958
+ moveCursorHorizontally,
959
+ moveCursorToVisualLineBoundary,
960
+ moveCursorVertically,
961
+ normalizeBashToolCommand,
962
+ normalizeModelLabel,
963
+ normalizeToolMergeEntry,
964
+ parseActiveAgentsFromBusStatus,
965
+ planAgentsFooter,
966
+ planProjectsRail,
967
+ renderLogLinesWithMarkdown,
968
+ resolveAgentSelectionOnDown,
969
+ resolveHistoryDownTransition,
970
+ shouldClearAgentSelectionOnUp,
971
+ shouldEnterAgentSelection,
972
+ shouldUseUcodeTui,
973
+ stripLeakedEscapeTags,
974
+ };