u-foo 2.3.32 → 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 (235) 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 +5 -5
  12. package/scripts/chat-app-smoke.js +1 -1
  13. package/scripts/global-chat-switch-benchmark.js +5 -5
  14. package/scripts/ink-demo.js +1 -1
  15. package/scripts/ink-smoke.js +1 -1
  16. package/scripts/ucode-app-smoke.js +1 -1
  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 +50 -26
  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 +45 -3
  53. package/src/{chat → app/chat}/dashboardView.js +2 -1
  54. package/src/app/chat/index.js +6 -0
  55. package/src/{chat → app/chat}/inputSubmitHandler.js +4 -13
  56. package/src/{chat → app/chat}/internalAgentLogHistory.js +1 -1
  57. package/src/app/chat/multiWindow/index.js +268 -0
  58. package/src/app/chat/multiWindow/paneLayout.js +84 -0
  59. package/src/app/chat/multiWindow/paneManager.js +299 -0
  60. package/src/app/chat/multiWindow/renderer.js +384 -0
  61. package/src/app/chat/multiWindow/virtualTerminal.js +327 -0
  62. package/src/{chat → app/chat}/transport.js +1 -1
  63. package/src/{cli → app/cli}/ctxCoreCommands.js +3 -3
  64. package/src/{doctor/index.js → app/cli/features/doctor.js} +1 -1
  65. package/src/{init/index.js → app/cli/features/init.js} +14 -32
  66. package/src/{cli → app/cli}/groupCoreCommands.js +2 -2
  67. package/src/app/cli/index.js +9 -0
  68. package/src/{cli → app/cli}/onlineCoreCommands.js +5 -5
  69. package/src/{cli.js → app/cli/run.js} +59 -57
  70. package/src/app/index.js +6 -0
  71. package/src/code/agent.js +10 -9
  72. package/src/code/index.js +2 -0
  73. package/src/code/launcher/index.js +9 -0
  74. package/src/{agent → code/launcher}/ucode.js +7 -8
  75. package/src/{agent → code/launcher}/ucodeBootstrap.js +3 -3
  76. package/src/{agent → code/launcher}/ucodeBuild.js +2 -2
  77. package/src/{agent → code/launcher}/ucodeDoctor.js +2 -2
  78. package/src/{agent → code/launcher}/ucodeRuntimeConfig.js +1 -2
  79. package/src/code/nativeRunner.js +4 -4
  80. package/src/code/tui.js +3 -1454
  81. package/src/config.js +15 -2
  82. package/src/{bus → coordination/bus}/activate.js +2 -2
  83. package/src/{bus → coordination/bus}/daemon.js +15 -5
  84. package/src/coordination/bus/envelope.js +173 -0
  85. package/src/{bus → coordination/bus}/index.js +7 -3
  86. package/src/{bus → coordination/bus}/inject.js +11 -3
  87. package/src/{bus → coordination/bus}/message.js +1 -1
  88. package/src/coordination/bus/messageMeta.js +130 -0
  89. package/src/coordination/bus/promptEnvelope.js +65 -0
  90. package/src/{bus → coordination/bus}/shake.js +1 -1
  91. package/src/{bus → coordination/bus}/store.js +3 -3
  92. package/src/{bus → coordination/bus}/subscriber.js +2 -2
  93. package/src/{bus → coordination/bus}/utils.js +2 -2
  94. package/src/{history → coordination/history}/inputTimeline.js +5 -5
  95. package/src/coordination/index.js +10 -0
  96. package/src/{memory → coordination/memory}/historySearch.js +1 -1
  97. package/src/{memory → coordination/memory}/index.js +3 -3
  98. package/src/{report → coordination/report}/store.js +2 -2
  99. package/src/{status → coordination/status}/index.js +3 -3
  100. package/src/online/bridge.js +2 -2
  101. package/src/{controller → orchestration/controller}/flags.js +1 -1
  102. package/src/{controller → orchestration/controller}/gateRouter.js +1 -1
  103. package/src/orchestration/controller/index.js +10 -0
  104. package/src/{controller → orchestration/controller}/shadowGuard.js +1 -1
  105. package/src/orchestration/groups/bootstrap.js +3 -0
  106. package/src/orchestration/groups/index.js +10 -0
  107. package/src/orchestration/groups/promptProfiles.js +3 -0
  108. package/src/{group → orchestration/groups}/templates.js +1 -1
  109. package/src/{group → orchestration/groups}/validateTemplate.js +1 -1
  110. package/src/orchestration/index.js +7 -0
  111. package/src/orchestration/solo/index.js +3 -0
  112. package/src/{daemon → runtime/daemon}/agentProcessManager.js +1 -1
  113. package/src/{daemon → runtime/daemon}/cronOps.js +3 -2
  114. package/src/{daemon → runtime/daemon}/groupOrchestrator.js +26 -9
  115. package/src/{daemon → runtime/daemon}/index.js +105 -53
  116. package/src/{daemon → runtime/daemon}/ipcServer.js +1 -1
  117. package/src/{daemon → runtime/daemon}/nicknameScope.js +6 -3
  118. package/src/{daemon → runtime/daemon}/ops.js +48 -61
  119. package/src/{daemon → runtime/daemon}/promptLoop.js +1 -1
  120. package/src/{daemon → runtime/daemon}/promptRequest.js +7 -7
  121. package/src/runtime/daemon/providerSessions.js +230 -0
  122. package/src/{daemon → runtime/daemon}/reporting.js +4 -4
  123. package/src/{daemon → runtime/daemon}/run.js +4 -4
  124. package/src/{daemon → runtime/daemon}/soloBootstrap.js +7 -7
  125. package/src/{daemon → runtime/daemon}/status.js +5 -5
  126. package/src/runtime/index.js +10 -0
  127. package/src/{projects → runtime/projects}/registry.js +1 -1
  128. package/src/{terminal → runtime/terminal}/adapterRouter.js +0 -10
  129. package/src/{terminal → runtime/terminal}/adapters/internalAdapter.js +0 -4
  130. package/src/tools/handlers/common.js +1 -1
  131. package/src/tools/handlers/listAgents.js +1 -1
  132. package/src/tools/handlers/memory.js +3 -3
  133. package/src/tools/handlers/readBusSummary.js +1 -1
  134. package/src/tools/handlers/readOpenDecisions.js +1 -1
  135. package/src/tools/handlers/readProjectRegistry.js +1 -1
  136. package/src/tools/handlers/readPromptHistory.js +2 -2
  137. package/src/tools/schemaFixtures.js +1 -1
  138. package/src/ui/MIGRATION.md +42 -88
  139. package/src/ui/format/index.js +5 -28
  140. package/src/ui/index.js +1 -1
  141. package/src/ui/{components → ink}/ChatApp.js +812 -88
  142. package/src/ui/ink/DashboardBar.js +685 -0
  143. package/src/ui/{components → ink}/MultilineInput.js +230 -5
  144. package/src/ui/{components → ink}/UcodeApp.js +16 -7
  145. package/src/ui/{components → ink}/agentMirror.js +24 -19
  146. package/src/ui/{components → ink}/chatReducer.js +29 -7
  147. package/src/bus/messageMeta.js +0 -52
  148. package/src/chat/agentViewController.js +0 -1072
  149. package/src/chat/chatLogController.js +0 -138
  150. package/src/chat/completionController.js +0 -533
  151. package/src/chat/dashboardKeyController.js +0 -533
  152. package/src/chat/index.js +0 -2222
  153. package/src/chat/inputHistoryController.js +0 -135
  154. package/src/chat/inputListenerController.js +0 -470
  155. package/src/chat/layout.js +0 -186
  156. package/src/chat/pasteController.js +0 -81
  157. package/src/chat/statusLineController.js +0 -223
  158. package/src/chat/streamTracker.js +0 -156
  159. package/src/code/config +0 -0
  160. package/src/daemon/providerSessions.js +0 -488
  161. package/src/terminal/adapters/internalPtyAdapter.js +0 -42
  162. package/src/ui/components/DashboardBar.js +0 -417
  163. /package/src/{code/prompts → agents/prompts/native}/actions.js +0 -0
  164. /package/src/{code/prompts → agents/prompts/native}/efficiency.js +0 -0
  165. /package/src/{code/prompts → agents/prompts/native}/environment.js +0 -0
  166. /package/src/{code/prompts → agents/prompts/native}/identity.js +0 -0
  167. /package/src/{code/prompts → agents/prompts/native}/safety.js +0 -0
  168. /package/src/{code/prompts → agents/prompts/native}/sections.js +0 -0
  169. /package/src/{code/prompts → agents/prompts/native}/system.js +0 -0
  170. /package/src/{code/prompts → agents/prompts/native}/tasks.js +0 -0
  171. /package/src/{code/prompts → agents/prompts/native}/toolDescriptions/bash.js +0 -0
  172. /package/src/{code/prompts → agents/prompts/native}/toolDescriptions/edit.js +0 -0
  173. /package/src/{code/prompts → agents/prompts/native}/toolDescriptions/read.js +0 -0
  174. /package/src/{code/prompts → agents/prompts/native}/toolDescriptions/write.js +0 -0
  175. /package/src/{code/prompts → agents/prompts/native}/ufoo.js +0 -0
  176. /package/src/{group → agents/prompts}/promptProfiles.js +0 -0
  177. /package/src/{agent → agents/providers}/claudeEventTranslator.js +0 -0
  178. /package/src/{agent → agents/providers}/claudeOauthTokenReader.js +0 -0
  179. /package/src/{agent → agents/providers}/claudeSessionFiles.js +0 -0
  180. /package/src/{agent → agents/providers}/codexEventTranslator.js +0 -0
  181. /package/src/{agent → agents/providers}/credentials/claude.js +0 -0
  182. /package/src/{agent → agents/providers}/credentials/codex.js +0 -0
  183. /package/src/{agent → agents/providers}/credentials/index.js +0 -0
  184. /package/src/{chat → app/chat}/agentBar.js +0 -0
  185. /package/src/{chat → app/chat}/agentDirectory.js +0 -0
  186. /package/src/{chat → app/chat}/cronScheduler.js +0 -0
  187. /package/src/{chat → app/chat}/daemonCoordinator.js +0 -0
  188. /package/src/{chat → app/chat}/daemonReconnect.js +0 -0
  189. /package/src/{chat → app/chat}/daemonTransport.js +0 -0
  190. /package/src/{chat → app/chat}/daemonTransportDefaults.js +0 -0
  191. /package/src/{chat → app/chat}/inputMath.js +0 -0
  192. /package/src/{chat → app/chat}/projectCloseController.js +0 -0
  193. /package/src/{chat → app/chat}/rawKeyMap.js +0 -0
  194. /package/src/{chat → app/chat}/settingsController.js +0 -0
  195. /package/src/{chat → app/chat}/shellCommand.js +0 -0
  196. /package/src/{chat → app/chat}/text.js +0 -0
  197. /package/src/{chat → app/chat}/transientAgentState.js +0 -0
  198. /package/src/{cli → app/cli}/busCoreCommands.js +0 -0
  199. /package/src/{skills/index.js → app/cli/features/skills.js} +0 -0
  200. /package/src/{bus → coordination/bus}/nickname.js +0 -0
  201. /package/src/{bus → coordination/bus}/queue.js +0 -0
  202. /package/src/{context → coordination/context}/decisions.js +0 -0
  203. /package/src/{context → coordination/context}/doctor.js +0 -0
  204. /package/src/{context → coordination/context}/index.js +0 -0
  205. /package/src/{context → coordination/context}/sync.js +0 -0
  206. /package/src/{ufoo → coordination/state}/agentRegistryDiagnostics.js +0 -0
  207. /package/src/{ufoo → coordination/state}/agentsStore.js +0 -0
  208. /package/src/{ufoo → coordination/state}/paths.js +0 -0
  209. /package/src/{controller → orchestration/controller}/launchRouting.js +0 -0
  210. /package/src/{controller → orchestration/controller}/routerFastPath.js +0 -0
  211. /package/src/{controller → orchestration/controller}/routerFinalize.js +0 -0
  212. /package/src/{group → orchestration/groups}/diagram.js +0 -0
  213. /package/src/{group → orchestration/groups}/templateValidation.js +0 -0
  214. /package/src/{solo → orchestration/solo}/commands.js +0 -0
  215. /package/src/{shared → runtime/contracts}/eventContract.js +0 -0
  216. /package/src/{shared → runtime/contracts}/ptySocketContract.js +0 -0
  217. /package/src/{providerapi → runtime/privacy}/redactor.js +0 -0
  218. /package/src/{providerapi → runtime/privacy}/shadowDiff.js +0 -0
  219. /package/src/{utils → runtime/process}/nodeExecutable.js +0 -0
  220. /package/src/{projects → runtime/projects}/identity.js +0 -0
  221. /package/src/{projects → runtime/projects}/index.js +0 -0
  222. /package/src/{projects → runtime/projects}/projectId.js +0 -0
  223. /package/src/{projects → runtime/projects}/runtimes.js +0 -0
  224. /package/src/{terminal → runtime/terminal}/adapterContract.js +0 -0
  225. /package/src/{terminal → runtime/terminal}/adapters/externalAdapter.js +0 -0
  226. /package/src/{terminal → runtime/terminal}/adapters/hostAdapter.js +0 -0
  227. /package/src/{terminal → runtime/terminal}/adapters/internalQueueAdapter.js +0 -0
  228. /package/src/{terminal → runtime/terminal}/adapters/terminalAdapter.js +0 -0
  229. /package/src/{terminal → runtime/terminal}/adapters/tmuxAdapter.js +0 -0
  230. /package/src/{terminal → runtime/terminal}/detect.js +0 -0
  231. /package/src/{terminal → runtime/terminal}/index.js +0 -0
  232. /package/src/{terminal → runtime/terminal}/iterm2.js +0 -0
  233. /package/src/{utils → ui/format}/banner.js +0 -0
  234. /package/src/{shared → ui/format}/markdownRenderer.js +0 -0
  235. /package/src/ui/{components → ink}/InkDemo.js +0 -0
@@ -1,138 +0,0 @@
1
- function createChatLogController(options = {}) {
2
- const {
3
- logBox,
4
- fsModule,
5
- historyDir: historyDirOption,
6
- historyFile: historyFileOption,
7
- now = () => new Date().toISOString(),
8
- } = options;
9
-
10
- if (!logBox || typeof logBox.log !== "function") {
11
- throw new Error("createChatLogController requires logBox.log");
12
- }
13
- if (!fsModule) {
14
- throw new Error("createChatLogController requires fsModule");
15
- }
16
- if (!historyDirOption || !historyFileOption) {
17
- throw new Error("createChatLogController requires historyDir/historyFile");
18
- }
19
- let historyDir = historyDirOption;
20
- let historyFile = historyFileOption;
21
-
22
- const SPACED_TYPES = new Set(["user", "reply", "bus", "dispatch", "error"]);
23
- let lastLogWasSpacer = false;
24
- let hasLoggedAny = false;
25
-
26
- function appendHistory(entry) {
27
- fsModule.mkdirSync(historyDir, { recursive: true });
28
- fsModule.appendFileSync(historyFile, `${JSON.stringify(entry)}\n`);
29
- }
30
-
31
- function shouldSpace(type, text) {
32
- if (SPACED_TYPES.has(type)) return true;
33
- if (typeof text === "string" && /daemon/i.test(text)) return true;
34
- return false;
35
- }
36
-
37
- function writeSpacer(writeHistory = true) {
38
- if (lastLogWasSpacer || !hasLoggedAny) return;
39
- logBox.log(" ");
40
- if (writeHistory) {
41
- appendHistory({
42
- ts: now(),
43
- type: "spacer",
44
- text: "",
45
- meta: {},
46
- });
47
- }
48
- lastLogWasSpacer = true;
49
- hasLoggedAny = true;
50
- }
51
-
52
- function recordLog(type, text, meta = {}, writeHistory = true) {
53
- if (type !== "spacer" && shouldSpace(type, text)) {
54
- writeSpacer(writeHistory);
55
- }
56
- logBox.log(text);
57
- if (writeHistory) {
58
- appendHistory({
59
- ts: now(),
60
- type,
61
- text,
62
- meta,
63
- });
64
- }
65
- lastLogWasSpacer = false;
66
- hasLoggedAny = true;
67
- }
68
-
69
- function logMessage(type, text, meta = {}) {
70
- recordLog(type, text, meta, true);
71
- }
72
-
73
- function markStreamStart() {
74
- lastLogWasSpacer = false;
75
- hasLoggedAny = true;
76
- }
77
-
78
- function loadHistory(limit = 2000) {
79
- try {
80
- const raw = fsModule.readFileSync(historyFile, "utf8").trim();
81
- if (!raw) return;
82
- const lines = raw.split(/\r?\n/).filter(Boolean);
83
- const items = lines.slice(-limit).map((line) => JSON.parse(line));
84
- const hasSpacer = items.some((item) => item && item.type === "spacer");
85
- for (const item of items) {
86
- if (!item) continue;
87
- if (item.type === "spacer") {
88
- writeSpacer(false);
89
- continue;
90
- }
91
- if (!item.text) continue;
92
- if (hasSpacer) {
93
- logBox.log(item.text);
94
- lastLogWasSpacer = false;
95
- hasLoggedAny = true;
96
- } else {
97
- recordLog(item.type || "unknown", item.text, item.meta || {}, false);
98
- }
99
- }
100
- } catch (err) {
101
- if (err && err.code === "ENOENT") {
102
- return;
103
- }
104
- if (err && typeof console !== "undefined" && typeof console.warn === "function") {
105
- console.warn(`chat history load failed (${historyFile}): ${err.message || err}`);
106
- }
107
- }
108
- }
109
-
110
- function setHistoryTarget(next = {}) {
111
- if (!next.historyDir || !next.historyFile) {
112
- throw new Error("setHistoryTarget requires historyDir/historyFile");
113
- }
114
- historyDir = next.historyDir;
115
- historyFile = next.historyFile;
116
- }
117
-
118
- function resetViewState() {
119
- // Callers are expected to clear logBox separately; this only resets spacing trackers.
120
- lastLogWasSpacer = false;
121
- hasLoggedAny = false;
122
- }
123
-
124
- return {
125
- appendHistory,
126
- writeSpacer,
127
- recordLog,
128
- logMessage,
129
- markStreamStart,
130
- loadHistory,
131
- setHistoryTarget,
132
- resetViewState,
133
- };
134
- }
135
-
136
- module.exports = {
137
- createChatLogController,
138
- };
@@ -1,533 +0,0 @@
1
- const FALLBACK_LAUNCH_SUBCOMMANDS = [
2
- { cmd: "claude", desc: "Launch Claude agent" },
3
- { cmd: "codex", desc: "Launch Codex agent" },
4
- { cmd: "ucode", desc: "Launch ucode core agent" },
5
- ];
6
-
7
- function sortSubcommandEntries(a, b) {
8
- const aOrder = Number.isFinite(a && a.order) ? a.order : Number.POSITIVE_INFINITY;
9
- const bOrder = Number.isFinite(b && b.order) ? b.order : Number.POSITIVE_INFINITY;
10
- if (aOrder !== bOrder) return aOrder - bOrder;
11
- return String(a && a.cmd ? a.cmd : "").localeCompare(String(b && b.cmd ? b.cmd : ""), "en", { sensitivity: "base" });
12
- }
13
-
14
- function mapSubcommandSuggestions(subs = [], parentCmd, tokenIndex, filterText = "") {
15
- const normalizedFilter = String(filterText || "").toLowerCase();
16
- return subs
17
- .filter((sub) => String(sub && sub.cmd ? sub.cmd : "").toLowerCase().startsWith(normalizedFilter))
18
- .map((sub) => ({
19
- ...sub,
20
- isSubcommand: true,
21
- parentCmd,
22
- tokenIndex,
23
- }))
24
- .sort(sortSubcommandEntries);
25
- }
26
-
27
- function buildNestedSubcommandSuggestions(subs = [], mainCmd, parts = [], endsWithSpace = false) {
28
- let currentSubs = Array.isArray(subs) ? subs : [];
29
- if (currentSubs.length === 0) return [];
30
-
31
- for (let index = 1; index < parts.length; index += 1) {
32
- const token = String(parts[index] || "");
33
- const exact = currentSubs.find((sub) =>
34
- String(sub && sub.cmd ? sub.cmd : "").toLowerCase() === token.toLowerCase()
35
- );
36
- const isLastToken = index === parts.length - 1;
37
-
38
- if (exact && Array.isArray(exact.subcommands) && exact.subcommands.length > 0) {
39
- if (!isLastToken || endsWithSpace) {
40
- currentSubs = exact.subcommands;
41
- continue;
42
- }
43
- }
44
-
45
- if (exact && isLastToken && endsWithSpace) {
46
- return [];
47
- }
48
-
49
- return mapSubcommandSuggestions(currentSubs, mainCmd, index, token);
50
- }
51
-
52
- return mapSubcommandSuggestions(currentSubs, mainCmd, parts.length, "");
53
- }
54
-
55
- function createCompletionController(options = {}) {
56
- const {
57
- input,
58
- screen,
59
- completionPanel,
60
- promptBox,
61
- commandRegistry = [],
62
- getGroupTemplateCandidates = () => [],
63
- getSoloProfileCandidates = () => [],
64
- getMentionCandidates = () => [],
65
- normalizeCommandPrefix = () => {},
66
- truncateText = (text) => String(text || ""),
67
- getCurrentInputHeight = () => 4,
68
- getCursorPos = () => 0,
69
- setCursorPos = () => {},
70
- resetPreferredCol = () => {},
71
- updateDraftFromInput = () => {},
72
- renderScreen = () => {},
73
- setImmediateFn = setImmediate,
74
- clearImmediateFn = clearImmediate,
75
- } = options;
76
-
77
- if (!input || !screen || !completionPanel || !promptBox) {
78
- throw new Error("createCompletionController requires input/screen/completionPanel/promptBox");
79
- }
80
-
81
- const state = {
82
- active: false,
83
- commands: [],
84
- index: 0,
85
- scrollOffset: 0,
86
- visibleCount: 0,
87
- enterSuppressed: false,
88
- enterReset: null,
89
- };
90
-
91
- function setPanelLayout() {
92
- const availableHeight = Math.max(1, screen.height - getCurrentInputHeight() - 1);
93
- const maxVisible = Math.max(1, availableHeight - 2);
94
- state.visibleCount = Math.min(7, state.commands.length, maxVisible);
95
- completionPanel.height = Math.min(availableHeight, state.visibleCount + 2);
96
- completionPanel.bottom = getCurrentInputHeight() - 1;
97
- }
98
-
99
- function render() {
100
- if (!state.active || state.commands.length === 0) return;
101
-
102
- const panelVisible = Math.max(1, (completionPanel.height || 1) - 2);
103
- const maxVisible = state.visibleCount
104
- ? Math.max(1, Math.min(state.visibleCount, panelVisible))
105
- : panelVisible;
106
-
107
- if (state.index < state.scrollOffset) {
108
- state.scrollOffset = state.index;
109
- } else if (state.index >= state.scrollOffset + maxVisible) {
110
- state.scrollOffset = state.index - maxVisible + 1;
111
- }
112
-
113
- const visibleStart = state.scrollOffset;
114
- const visibleEnd = Math.min(state.scrollOffset + maxVisible, state.commands.length);
115
- const visibleCommands = state.commands.slice(visibleStart, visibleEnd);
116
-
117
- const panelWidth = typeof completionPanel.width === "number"
118
- ? completionPanel.width
119
- : screen.width;
120
-
121
- const lines = visibleCommands.map((item, i) => {
122
- const actualIndex = visibleStart + i;
123
- const cmdText = item.cmd;
124
- const descText = item.desc || "";
125
- const cmdPart = actualIndex === state.index
126
- ? `{inverse}${cmdText}{/inverse}`
127
- : `{cyan-fg}${cmdText}{/cyan-fg}`;
128
- const indent = " ".repeat(promptBox.width || 2);
129
- const maxDescWidth = Math.max(0, panelWidth - indent.length - cmdText.length - 2);
130
- const trimmedDesc = truncateText(descText, maxDescWidth);
131
- const descPart = trimmedDesc ? `{gray-fg}${trimmedDesc}{/gray-fg}` : "";
132
- return descPart ? `${indent}${cmdPart} ${descPart}` : `${indent}${cmdPart}`;
133
- });
134
-
135
- completionPanel.setContent(lines.join("\n"));
136
- renderScreen();
137
- }
138
-
139
- function hide() {
140
- state.active = false;
141
- state.commands = [];
142
- state.index = 0;
143
- state.scrollOffset = 0;
144
- state.visibleCount = 0;
145
- completionPanel.hidden = true;
146
- renderScreen();
147
- }
148
-
149
- function buildCommands(filterText) {
150
- const mentionMatch = String(filterText || "").match(/^@([^\s]*)$/);
151
- if (mentionMatch) {
152
- const mentionFilter = String(mentionMatch[1] || "").trim().toLowerCase();
153
- const rawCandidates = Array.isArray(getMentionCandidates()) ? getMentionCandidates() : [];
154
- const seen = new Set();
155
- const items = [];
156
- for (const item of rawCandidates) {
157
- const id = String(item && item.id ? item.id : "").trim();
158
- const label = String(item && item.label ? item.label : id).trim();
159
- if (!id && !label) continue;
160
- const rawToken = label || id;
161
- const normalizedToken = rawToken.replace(/^@+/, "");
162
- if (!normalizedToken || /\s/.test(normalizedToken)) continue;
163
- const tokenLower = normalizedToken.toLowerCase();
164
- const idLower = id.toLowerCase();
165
- if (
166
- mentionFilter
167
- && !tokenLower.startsWith(mentionFilter)
168
- && !idLower.startsWith(mentionFilter)
169
- ) {
170
- continue;
171
- }
172
- if (seen.has(normalizedToken)) continue;
173
- seen.add(normalizedToken);
174
- const desc = id && id !== normalizedToken ? id : "";
175
- items.push({
176
- cmd: `@${normalizedToken}`,
177
- desc,
178
- isMention: true,
179
- mentionTarget: normalizedToken,
180
- });
181
- }
182
- return items.sort((a, b) => a.cmd.localeCompare(b.cmd));
183
- }
184
-
185
- const endsWithSpace = /\s$/.test(filterText);
186
- const trimmed = filterText.trim();
187
- if (!trimmed) {
188
- return [];
189
- }
190
-
191
- const parts = trimmed.split(/\s+/);
192
- const mainCmd = parts[0];
193
- const isLaunch = mainCmd && mainCmd.toLowerCase() === "/launch";
194
- const isGroup = mainCmd && mainCmd.toLowerCase() === "/group";
195
- const isSolo = mainCmd && mainCmd.toLowerCase() === "/solo";
196
- const wantsSubcommands = (parts.length > 1 || (endsWithSpace && parts.length === 1));
197
-
198
- if (isGroup) {
199
- const groupSubcommand = String(parts[1] || "").trim().toLowerCase();
200
- const wantsGroupRunArgs = groupSubcommand === "run" && (parts.length > 2 || endsWithSpace);
201
- if (wantsGroupRunArgs) {
202
- const aliasFilter = String(parts[2] || "").trim().toLowerCase();
203
- return (Array.isArray(getGroupTemplateCandidates()) ? getGroupTemplateCandidates() : [])
204
- .map((item) => {
205
- const alias = String(item && item.alias ? item.alias : item && item.cmd ? item.cmd : "").trim();
206
- if (!alias) return null;
207
- const desc = String(item && item.desc ? item.desc : item && item.name ? item.name : "").trim();
208
- const source = String(item && item.source ? item.source : "").trim();
209
- const detail = [desc, source].filter(Boolean).join(" · ");
210
- return {
211
- cmd: alias,
212
- desc: detail,
213
- isArgumentSuggestion: true,
214
- argumentPrefix: "/group run",
215
- };
216
- })
217
- .filter((item) => item && (!aliasFilter || item.cmd.toLowerCase().startsWith(aliasFilter)))
218
- .sort((a, b) => a.cmd.localeCompare(b.cmd, "en", { sensitivity: "base" }));
219
- }
220
- }
221
-
222
- if (isSolo) {
223
- const soloSubcommand = String(parts[1] || "").trim().toLowerCase();
224
- const wantsSoloRunArgs = soloSubcommand === "run" && (parts.length > 2 || endsWithSpace);
225
- if (wantsSoloRunArgs) {
226
- const profileFilter = String(parts[2] || "").trim().toLowerCase();
227
- return (Array.isArray(getSoloProfileCandidates()) ? getSoloProfileCandidates() : [])
228
- .map((item) => {
229
- const profileId = String(item && item.cmd ? item.cmd : item && item.id ? item.id : "").trim();
230
- if (!profileId) return null;
231
- const desc = String(item && item.desc ? item.desc : item && item.summary ? item.summary : "").trim();
232
- return {
233
- cmd: profileId,
234
- desc,
235
- isArgumentSuggestion: true,
236
- argumentPrefix: "/solo run",
237
- };
238
- })
239
- .filter((item) => item && (!profileFilter || item.cmd.toLowerCase().startsWith(profileFilter)))
240
- .sort((a, b) => a.cmd.localeCompare(b.cmd, "en", { sensitivity: "base" }));
241
- }
242
- }
243
-
244
- if ((wantsSubcommands || isLaunch) && mainCmd && mainCmd.startsWith("/")) {
245
- const subFilter = parts[1] || "";
246
- const mainCmdObj = commandRegistry.find((item) =>
247
- item.cmd.toLowerCase() === mainCmd.toLowerCase()
248
- );
249
- if ((mainCmdObj && mainCmdObj.subcommands) || isLaunch) {
250
- const baseSubs = mainCmdObj && mainCmdObj.subcommands ? mainCmdObj.subcommands : [];
251
- let subs = baseSubs;
252
- if (isLaunch) {
253
- const merged = new Map();
254
- for (const sub of [...baseSubs, ...FALLBACK_LAUNCH_SUBCOMMANDS]) {
255
- if (!sub || !sub.cmd) continue;
256
- merged.set(sub.cmd, sub);
257
- }
258
- subs = Array.from(merged.values());
259
- }
260
- if (isLaunch) {
261
- return mapSubcommandSuggestions(subs, mainCmd, 1, subFilter);
262
- }
263
- return buildNestedSubcommandSuggestions(subs, mainCmd, parts, endsWithSpace);
264
- }
265
- return [];
266
- }
267
-
268
- const filterLower = trimmed.toLowerCase();
269
- return commandRegistry.filter((item) => item.cmd.toLowerCase().startsWith(filterLower));
270
- }
271
-
272
- function show(filterText) {
273
- normalizeCommandPrefix();
274
-
275
- let nextFilter = filterText;
276
- if (nextFilter !== input.value) {
277
- nextFilter = input.value;
278
- }
279
-
280
- if (nextFilter.startsWith("//")) {
281
- nextFilter = nextFilter.replace(/^\/+/, "/");
282
- input.value = nextFilter;
283
- setCursorPos(Math.min(getCursorPos(), input.value.length));
284
- }
285
-
286
- if (!nextFilter) {
287
- hide();
288
- return;
289
- }
290
-
291
- const commands = buildCommands(nextFilter);
292
- if (commands.length === 0) {
293
- hide();
294
- return;
295
- }
296
-
297
- state.commands = commands;
298
- state.active = true;
299
- state.index = 0;
300
- state.scrollOffset = 0;
301
- setPanelLayout();
302
- completionPanel.hidden = false;
303
- render();
304
- }
305
-
306
- function pageSize() {
307
- const panelVisible = Math.max(1, (completionPanel.height || 2) - 2);
308
- return state.visibleCount
309
- ? Math.max(1, Math.min(state.visibleCount, panelVisible))
310
- : panelVisible;
311
- }
312
-
313
- function up() {
314
- if (state.commands.length === 0) return;
315
- state.index = state.index <= 0 ? state.commands.length - 1 : state.index - 1;
316
- render();
317
- }
318
-
319
- function down() {
320
- if (state.commands.length === 0) return;
321
- state.index = state.index >= state.commands.length - 1 ? 0 : state.index + 1;
322
- render();
323
- }
324
-
325
- function pageUp() {
326
- if (state.commands.length === 0) return;
327
- state.index = Math.max(0, state.index - pageSize());
328
- render();
329
- }
330
-
331
- function pageDown() {
332
- if (state.commands.length === 0) return;
333
- state.index = Math.min(state.commands.length - 1, state.index + pageSize());
334
- render();
335
- }
336
-
337
- function preview(selected) {
338
- const current = input.value || "";
339
- const trimmed = current.trim();
340
- const endsWithSpace = /\s$/.test(current);
341
-
342
- if (selected.isMention) {
343
- const mentionTarget = String(selected.mentionTarget || selected.cmd || "").replace(/^@+/, "");
344
- const completedCore = `@${mentionTarget}`;
345
- const isComplete = (trimmed === completedCore && endsWithSpace) || trimmed.startsWith(`${completedCore} `);
346
- return { text: `${completedCore} `, isComplete };
347
- }
348
-
349
- if (selected.isSubcommand) {
350
- const parts = trimmed.split(/\s+/);
351
- const tokenIndex = Number.isInteger(selected.tokenIndex) ? selected.tokenIndex : 1;
352
- while (parts.length <= tokenIndex) parts.push("");
353
- parts[tokenIndex] = selected.cmd;
354
- const completedCore = parts.slice(0, tokenIndex + 1).join(" ").trim();
355
- const isComplete = trimmed === completedCore || trimmed.startsWith(`${completedCore} `);
356
- return { text: `${completedCore} `, isComplete };
357
- }
358
-
359
- if (selected.isArgumentSuggestion) {
360
- const prefix = String(selected.argumentPrefix || "").trim();
361
- const completedCore = prefix ? `${prefix} ${selected.cmd}` : selected.cmd;
362
- const isComplete = trimmed === completedCore || trimmed.startsWith(`${completedCore} `);
363
- return { text: `${completedCore} `, isComplete };
364
- }
365
-
366
- const completedCore = selected.cmd;
367
- const hasChildren = selected.subcommands && selected.subcommands.length > 0;
368
- const isComplete =
369
- (trimmed === completedCore && (!hasChildren || endsWithSpace)) ||
370
- trimmed.startsWith(`${completedCore} `);
371
- return { text: `${completedCore} `, isComplete };
372
- }
373
-
374
- function applyPreview(nextPreview) {
375
- input.value = nextPreview.text;
376
- setCursorPos(input.value.length);
377
- resetPreferredCol();
378
- if (typeof input._updateCursor === "function") {
379
- input._updateCursor();
380
- }
381
- updateDraftFromInput();
382
- renderScreen();
383
- }
384
-
385
- function confirm() {
386
- if (!state.active || state.commands.length === 0) return;
387
-
388
- const selected = state.commands[state.index];
389
- if (selected.isMention) {
390
- const mentionTarget = String(selected.mentionTarget || selected.cmd || "").replace(/^@+/, "");
391
- input.value = `@${mentionTarget} `;
392
- } else if (selected.isSubcommand) {
393
- const parts = input.value.split(/\s+/);
394
- const tokenIndex = Number.isInteger(selected.tokenIndex) ? selected.tokenIndex : 1;
395
- while (parts.length <= tokenIndex) parts.push("");
396
- parts[tokenIndex] = selected.cmd;
397
- input.value = `${parts.slice(0, tokenIndex + 1).join(" ")} `;
398
- } else if (selected.isArgumentSuggestion) {
399
- const prefix = String(selected.argumentPrefix || "").trim();
400
- input.value = prefix ? `${prefix} ${selected.cmd} ` : `${selected.cmd} `;
401
- } else {
402
- input.value = `${selected.cmd} `;
403
- }
404
-
405
- setCursorPos(input.value.length);
406
- resetPreferredCol();
407
- if (typeof input._updateCursor === "function") {
408
- input._updateCursor();
409
- }
410
- updateDraftFromInput();
411
-
412
- if (selected.subcommands && selected.subcommands.length > 0) {
413
- show(input.value);
414
- } else if (
415
- selected.isSubcommand
416
- && ((selected.parentCmd === "/group" && selected.cmd === "run")
417
- || (selected.parentCmd === "/solo" && selected.cmd === "run"))
418
- ) {
419
- show(input.value);
420
- } else {
421
- hide();
422
- }
423
-
424
- renderScreen();
425
- }
426
-
427
- function handleKey(ch, key = {}) {
428
- if (!state.active) return false;
429
-
430
- if (key.name === "up") {
431
- up();
432
- return true;
433
- }
434
- if (key.name === "down") {
435
- down();
436
- return true;
437
- }
438
- if (key.name === "tab") {
439
- confirm();
440
- return true;
441
- }
442
- if (key.name === "pageup") {
443
- pageUp();
444
- return true;
445
- }
446
- if (key.name === "pagedown") {
447
- pageDown();
448
- return true;
449
- }
450
-
451
- if (key.name === "enter" || key.name === "return") {
452
- if (state.enterSuppressed) {
453
- return true;
454
- }
455
- const selected = state.commands[state.index];
456
- if (selected) {
457
- const nextPreview = preview(selected);
458
- if (!nextPreview.isComplete) {
459
- applyPreview(nextPreview);
460
- if (selected.subcommands && selected.subcommands.length > 0) {
461
- show(input.value);
462
- } else if (
463
- selected.isSubcommand
464
- && ((selected.parentCmd === "/group" && selected.cmd === "run")
465
- || (selected.parentCmd === "/solo" && selected.cmd === "run"))
466
- ) {
467
- show(input.value);
468
- } else {
469
- hide();
470
- }
471
- state.enterSuppressed = true;
472
- if (state.enterReset) clearImmediateFn(state.enterReset);
473
- state.enterReset = setImmediateFn(() => {
474
- state.enterSuppressed = false;
475
- });
476
- return true;
477
- }
478
- }
479
- hide();
480
- state.enterSuppressed = true;
481
- if (state.enterReset) clearImmediateFn(state.enterReset);
482
- state.enterReset = setImmediateFn(() => {
483
- state.enterSuppressed = false;
484
- });
485
- return false;
486
- }
487
-
488
- if (key.name === "escape") {
489
- hide();
490
- return true;
491
- }
492
-
493
- if (ch === " ") {
494
- const currentInput = (input.value || "").trim();
495
- if (currentInput.startsWith("/") && !currentInput.includes(" ")) {
496
- return false;
497
- }
498
- if (currentInput.startsWith("@") && !currentInput.includes(" ")) {
499
- return false;
500
- }
501
- hide();
502
- return false;
503
- }
504
-
505
- return false;
506
- }
507
-
508
- function reflow() {
509
- if (!state.active) return;
510
- setPanelLayout();
511
- render();
512
- }
513
-
514
- function jumpToLast() {
515
- if (state.commands.length === 0) return;
516
- state.index = state.commands.length - 1;
517
- render();
518
- }
519
-
520
- return {
521
- show,
522
- hide,
523
- handleKey,
524
- reflow,
525
- isActive: () => state.active,
526
- getCommandCount: () => state.commands.length,
527
- jumpToLast,
528
- };
529
- }
530
-
531
- module.exports = {
532
- createCompletionController,
533
- };