@butlerw/vellum 0.1.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 (446) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +411 -0
  3. package/__fixtures__/responses/code-generation.json +42 -0
  4. package/__fixtures__/responses/error-response.json +20 -0
  5. package/__fixtures__/responses/hello-world.json +32 -0
  6. package/dist/auth-6MCXESOH.js +26 -0
  7. package/dist/chunk-SECXJGWA.js +597 -0
  8. package/dist/index.js +34023 -0
  9. package/package.json +67 -0
  10. package/src/__tests__/commands.e2e.test.ts +728 -0
  11. package/src/__tests__/credentials.test.ts +713 -0
  12. package/src/__tests__/mode-e2e.test.ts +391 -0
  13. package/src/__tests__/tui-integration.test.tsx +1271 -0
  14. package/src/agents/__tests__/task-persistence.test.ts +235 -0
  15. package/src/agents/commands/delegate.ts +240 -0
  16. package/src/agents/commands/index.ts +10 -0
  17. package/src/agents/commands/resume.ts +335 -0
  18. package/src/agents/index.ts +29 -0
  19. package/src/agents/task-persistence.ts +272 -0
  20. package/src/agents/task-resumption.ts +242 -0
  21. package/src/app.tsx +4737 -0
  22. package/src/commands/__tests__/.gitkeep +1 -0
  23. package/src/commands/__tests__/agents.test.ts +606 -0
  24. package/src/commands/__tests__/auth.test.ts +626 -0
  25. package/src/commands/__tests__/autocomplete.test.ts +683 -0
  26. package/src/commands/__tests__/batch.test.ts +287 -0
  27. package/src/commands/__tests__/chain-pipe-parser.test.ts +654 -0
  28. package/src/commands/__tests__/completion.test.ts +238 -0
  29. package/src/commands/__tests__/core.test.ts +363 -0
  30. package/src/commands/__tests__/executor.test.ts +496 -0
  31. package/src/commands/__tests__/exit-codes.test.ts +220 -0
  32. package/src/commands/__tests__/init.test.ts +243 -0
  33. package/src/commands/__tests__/language.test.ts +353 -0
  34. package/src/commands/__tests__/mode-cli.test.ts +667 -0
  35. package/src/commands/__tests__/model.test.ts +277 -0
  36. package/src/commands/__tests__/parser.test.ts +493 -0
  37. package/src/commands/__tests__/performance.bench.ts +380 -0
  38. package/src/commands/__tests__/registry.test.ts +534 -0
  39. package/src/commands/__tests__/resume.test.ts +449 -0
  40. package/src/commands/__tests__/security.test.ts +845 -0
  41. package/src/commands/__tests__/stream-json.test.ts +372 -0
  42. package/src/commands/__tests__/user-commands.test.ts +597 -0
  43. package/src/commands/adapters.ts +267 -0
  44. package/src/commands/agent.ts +395 -0
  45. package/src/commands/agents/generate.ts +506 -0
  46. package/src/commands/agents/index.ts +272 -0
  47. package/src/commands/agents/show.ts +271 -0
  48. package/src/commands/agents/validate.ts +387 -0
  49. package/src/commands/auth.ts +883 -0
  50. package/src/commands/autocomplete.ts +480 -0
  51. package/src/commands/batch/command.ts +388 -0
  52. package/src/commands/batch/executor.ts +361 -0
  53. package/src/commands/batch/index.ts +12 -0
  54. package/src/commands/commit.ts +235 -0
  55. package/src/commands/completion/index.ts +371 -0
  56. package/src/commands/condense.ts +191 -0
  57. package/src/commands/config.ts +344 -0
  58. package/src/commands/context-provider.ts +173 -0
  59. package/src/commands/copy.ts +329 -0
  60. package/src/commands/core/clear.ts +38 -0
  61. package/src/commands/core/exit.ts +43 -0
  62. package/src/commands/core/help.ts +354 -0
  63. package/src/commands/core/index.ts +15 -0
  64. package/src/commands/cost.ts +179 -0
  65. package/src/commands/credentials.tsx +618 -0
  66. package/src/commands/custom-agents/__tests__/custom-agents.test.ts +709 -0
  67. package/src/commands/custom-agents/create.ts +377 -0
  68. package/src/commands/custom-agents/export.ts +135 -0
  69. package/src/commands/custom-agents/import.ts +199 -0
  70. package/src/commands/custom-agents/index.ts +372 -0
  71. package/src/commands/custom-agents/info.ts +318 -0
  72. package/src/commands/custom-agents/list.ts +267 -0
  73. package/src/commands/custom-agents/validate.ts +388 -0
  74. package/src/commands/diff-mode.ts +241 -0
  75. package/src/commands/env.ts +53 -0
  76. package/src/commands/executor.ts +579 -0
  77. package/src/commands/exit-codes.ts +202 -0
  78. package/src/commands/index.ts +701 -0
  79. package/src/commands/init/index.ts +15 -0
  80. package/src/commands/init/prompts.ts +366 -0
  81. package/src/commands/init/templates/commands-readme.md +80 -0
  82. package/src/commands/init/templates/example-command.md +79 -0
  83. package/src/commands/init/templates/example-skill.md +168 -0
  84. package/src/commands/init/templates/example-workflow.md +101 -0
  85. package/src/commands/init/templates/prompts-readme.md +52 -0
  86. package/src/commands/init/templates/rules-readme.md +63 -0
  87. package/src/commands/init/templates/skills-readme.md +83 -0
  88. package/src/commands/init/templates/workflows-readme.md +94 -0
  89. package/src/commands/init.ts +391 -0
  90. package/src/commands/install.ts +90 -0
  91. package/src/commands/language.ts +191 -0
  92. package/src/commands/loaders/.gitkeep +1 -0
  93. package/src/commands/lsp.ts +199 -0
  94. package/src/commands/markdown-commands.ts +253 -0
  95. package/src/commands/mcp.ts +588 -0
  96. package/src/commands/memory/export.ts +341 -0
  97. package/src/commands/memory/index.ts +148 -0
  98. package/src/commands/memory/list.ts +261 -0
  99. package/src/commands/memory/search.ts +346 -0
  100. package/src/commands/memory/utils.ts +15 -0
  101. package/src/commands/metrics.ts +75 -0
  102. package/src/commands/migrate/index.ts +16 -0
  103. package/src/commands/migrate/prompts.ts +477 -0
  104. package/src/commands/mode.ts +331 -0
  105. package/src/commands/model.ts +298 -0
  106. package/src/commands/onboard.ts +205 -0
  107. package/src/commands/open.ts +169 -0
  108. package/src/commands/output/stream-json.ts +373 -0
  109. package/src/commands/parser/chain-parser.ts +370 -0
  110. package/src/commands/parser/index.ts +29 -0
  111. package/src/commands/parser/pipe-parser.ts +480 -0
  112. package/src/commands/parser.ts +588 -0
  113. package/src/commands/persistence.ts +355 -0
  114. package/src/commands/progress.ts +18 -0
  115. package/src/commands/prompt/index.ts +17 -0
  116. package/src/commands/prompt/validate.ts +621 -0
  117. package/src/commands/prompt-priority.ts +401 -0
  118. package/src/commands/registry.ts +374 -0
  119. package/src/commands/sandbox/index.ts +131 -0
  120. package/src/commands/security/index.ts +21 -0
  121. package/src/commands/security/input-sanitizer.ts +168 -0
  122. package/src/commands/security/permission-checker.ts +456 -0
  123. package/src/commands/security/sensitive-data.ts +350 -0
  124. package/src/commands/session/delete.ts +38 -0
  125. package/src/commands/session/export.ts +39 -0
  126. package/src/commands/session/index.ts +26 -0
  127. package/src/commands/session/list.ts +26 -0
  128. package/src/commands/session/resume.ts +562 -0
  129. package/src/commands/session/search.ts +434 -0
  130. package/src/commands/session/show.ts +26 -0
  131. package/src/commands/settings.ts +368 -0
  132. package/src/commands/setup.ts +23 -0
  133. package/src/commands/shell/index.ts +16 -0
  134. package/src/commands/shell/setup.ts +422 -0
  135. package/src/commands/shell-init.ts +50 -0
  136. package/src/commands/shell-integration/index.ts +194 -0
  137. package/src/commands/skill.ts +1220 -0
  138. package/src/commands/spec.ts +558 -0
  139. package/src/commands/status.ts +246 -0
  140. package/src/commands/theme.ts +211 -0
  141. package/src/commands/think.ts +551 -0
  142. package/src/commands/trust.ts +211 -0
  143. package/src/commands/tutorial.ts +522 -0
  144. package/src/commands/types.ts +512 -0
  145. package/src/commands/update.ts +274 -0
  146. package/src/commands/usage.ts +213 -0
  147. package/src/commands/user-commands.ts +630 -0
  148. package/src/commands/utils.ts +142 -0
  149. package/src/commands/vim.ts +152 -0
  150. package/src/commands/workflow.ts +257 -0
  151. package/src/components/header.tsx +25 -0
  152. package/src/components/input.tsx +25 -0
  153. package/src/components/message-list.tsx +32 -0
  154. package/src/components/status-bar.tsx +23 -0
  155. package/src/index.tsx +614 -0
  156. package/src/onboarding/__tests__/tutorial.test.ts +740 -0
  157. package/src/onboarding/index.ts +69 -0
  158. package/src/onboarding/tips/index.ts +9 -0
  159. package/src/onboarding/tips/tip-engine.ts +459 -0
  160. package/src/onboarding/tutorial/index.ts +88 -0
  161. package/src/onboarding/tutorial/lessons/basics.ts +151 -0
  162. package/src/onboarding/tutorial/lessons/index.ts +151 -0
  163. package/src/onboarding/tutorial/lessons/modes.ts +230 -0
  164. package/src/onboarding/tutorial/lessons/tools.ts +172 -0
  165. package/src/onboarding/tutorial/progress-tracker.ts +350 -0
  166. package/src/onboarding/tutorial/storage.ts +249 -0
  167. package/src/onboarding/tutorial/tutorial-system.ts +462 -0
  168. package/src/onboarding/tutorial/types.ts +310 -0
  169. package/src/orchestrator-singleton.ts +129 -0
  170. package/src/shutdown.ts +33 -0
  171. package/src/test/e2e/assertions.ts +267 -0
  172. package/src/test/e2e/fixtures.ts +204 -0
  173. package/src/test/e2e/harness.ts +575 -0
  174. package/src/test/e2e/index.ts +57 -0
  175. package/src/test/e2e/types.ts +228 -0
  176. package/src/test/fixtures/__tests__/fake-response-loader.test.ts +314 -0
  177. package/src/test/fixtures/fake-response-loader.ts +314 -0
  178. package/src/test/fixtures/index.ts +20 -0
  179. package/src/tui/__tests__/mcp-panel.test.tsx +82 -0
  180. package/src/tui/__tests__/mcp-wiring.test.tsx +78 -0
  181. package/src/tui/__tests__/mode-components.test.tsx +395 -0
  182. package/src/tui/__tests__/permission-ask-flow.test.tsx +138 -0
  183. package/src/tui/__tests__/sidebar-panel-data.test.tsx +148 -0
  184. package/src/tui/__tests__/tools-panel-hotkeys.test.tsx +41 -0
  185. package/src/tui/adapters/agent-adapter.ts +1008 -0
  186. package/src/tui/adapters/index.ts +48 -0
  187. package/src/tui/adapters/message-adapter.ts +315 -0
  188. package/src/tui/adapters/persistence-bridge.ts +331 -0
  189. package/src/tui/adapters/session-adapter.ts +419 -0
  190. package/src/tui/buffered-stdout.ts +223 -0
  191. package/src/tui/components/AgentProgress.tsx +424 -0
  192. package/src/tui/components/Banner/AsciiArt.ts +160 -0
  193. package/src/tui/components/Banner/Banner.tsx +355 -0
  194. package/src/tui/components/Banner/ShimmerContext.tsx +131 -0
  195. package/src/tui/components/Banner/ShimmerText.tsx +193 -0
  196. package/src/tui/components/Banner/TypeWriterGradient.tsx +321 -0
  197. package/src/tui/components/Banner/index.ts +61 -0
  198. package/src/tui/components/Banner/useShimmer.ts +241 -0
  199. package/src/tui/components/ChatView.tsx +11 -0
  200. package/src/tui/components/Checkpoint/CheckpointDiffView.tsx +371 -0
  201. package/src/tui/components/Checkpoint/SnapshotCheckpointPanel.tsx +440 -0
  202. package/src/tui/components/Checkpoint/index.ts +19 -0
  203. package/src/tui/components/CostDisplay.tsx +226 -0
  204. package/src/tui/components/InitErrorBanner.tsx +122 -0
  205. package/src/tui/components/Input/Autocomplete.tsx +603 -0
  206. package/src/tui/components/Input/EnhancedCommandInput.tsx +471 -0
  207. package/src/tui/components/Input/HighlightedText.tsx +236 -0
  208. package/src/tui/components/Input/MentionAutocomplete.tsx +375 -0
  209. package/src/tui/components/Input/TextInput.tsx +1002 -0
  210. package/src/tui/components/Input/__tests__/Autocomplete.test.tsx +374 -0
  211. package/src/tui/components/Input/__tests__/TextInput.test.tsx +241 -0
  212. package/src/tui/components/Input/__tests__/highlight.test.ts +219 -0
  213. package/src/tui/components/Input/__tests__/slash-command-utils.test.ts +104 -0
  214. package/src/tui/components/Input/highlight.ts +362 -0
  215. package/src/tui/components/Input/index.ts +36 -0
  216. package/src/tui/components/Input/slash-command-utils.ts +135 -0
  217. package/src/tui/components/Layout.tsx +432 -0
  218. package/src/tui/components/McpPanel.tsx +137 -0
  219. package/src/tui/components/MemoryPanel.tsx +448 -0
  220. package/src/tui/components/Messages/CodeBlock.tsx +527 -0
  221. package/src/tui/components/Messages/DiffView.tsx +679 -0
  222. package/src/tui/components/Messages/ImageReference.tsx +89 -0
  223. package/src/tui/components/Messages/MarkdownBlock.tsx +228 -0
  224. package/src/tui/components/Messages/MarkdownRenderer.tsx +498 -0
  225. package/src/tui/components/Messages/MessageBubble.tsx +270 -0
  226. package/src/tui/components/Messages/MessageList.tsx +1719 -0
  227. package/src/tui/components/Messages/StreamingText.tsx +216 -0
  228. package/src/tui/components/Messages/ThinkingBlock.tsx +408 -0
  229. package/src/tui/components/Messages/ToolResultPreview.tsx +243 -0
  230. package/src/tui/components/Messages/__tests__/CodeBlock.test.tsx +296 -0
  231. package/src/tui/components/Messages/__tests__/DiffView.test.tsx +239 -0
  232. package/src/tui/components/Messages/__tests__/MarkdownRenderer.test.tsx +303 -0
  233. package/src/tui/components/Messages/__tests__/MessageBubble.test.tsx +268 -0
  234. package/src/tui/components/Messages/__tests__/MessageList.test.tsx +324 -0
  235. package/src/tui/components/Messages/__tests__/StreamingText.test.tsx +215 -0
  236. package/src/tui/components/Messages/index.ts +25 -0
  237. package/src/tui/components/ModeIndicator.tsx +177 -0
  238. package/src/tui/components/ModeSelector.tsx +216 -0
  239. package/src/tui/components/ModelSelector.tsx +339 -0
  240. package/src/tui/components/OnboardingWizard.tsx +670 -0
  241. package/src/tui/components/PhaseProgressIndicator.tsx +270 -0
  242. package/src/tui/components/RateLimitIndicator.tsx +82 -0
  243. package/src/tui/components/ScreenReaderLayout.tsx +295 -0
  244. package/src/tui/components/SettingsPanel.tsx +643 -0
  245. package/src/tui/components/Sidebar/SystemStatusPanel.tsx +284 -0
  246. package/src/tui/components/Sidebar/index.ts +9 -0
  247. package/src/tui/components/Status/ModelStatusBar.tsx +270 -0
  248. package/src/tui/components/Status/index.ts +12 -0
  249. package/src/tui/components/StatusBar/AgentModeIndicator.tsx +257 -0
  250. package/src/tui/components/StatusBar/ContextProgress.tsx +167 -0
  251. package/src/tui/components/StatusBar/FileChangesIndicator.tsx +62 -0
  252. package/src/tui/components/StatusBar/GitIndicator.tsx +89 -0
  253. package/src/tui/components/StatusBar/HeaderBar.tsx +126 -0
  254. package/src/tui/components/StatusBar/ModelIndicator.tsx +157 -0
  255. package/src/tui/components/StatusBar/PersistenceStatusIndicator.tsx +210 -0
  256. package/src/tui/components/StatusBar/ResilienceIndicator.tsx +106 -0
  257. package/src/tui/components/StatusBar/SandboxIndicator.tsx +167 -0
  258. package/src/tui/components/StatusBar/StatusBar.tsx +368 -0
  259. package/src/tui/components/StatusBar/ThinkingModeIndicator.tsx +170 -0
  260. package/src/tui/components/StatusBar/TokenBreakdown.tsx +246 -0
  261. package/src/tui/components/StatusBar/TokenCounter.tsx +135 -0
  262. package/src/tui/components/StatusBar/TrustModeIndicator.tsx +130 -0
  263. package/src/tui/components/StatusBar/WorkspaceIndicator.tsx +86 -0
  264. package/src/tui/components/StatusBar/__tests__/AgentModeIndicator.test.tsx +193 -0
  265. package/src/tui/components/StatusBar/__tests__/StatusBar.test.tsx +729 -0
  266. package/src/tui/components/StatusBar/index.ts +60 -0
  267. package/src/tui/components/TipBanner.tsx +115 -0
  268. package/src/tui/components/TodoItem.tsx +208 -0
  269. package/src/tui/components/TodoPanel.tsx +455 -0
  270. package/src/tui/components/Tools/ApprovalQueue.tsx +407 -0
  271. package/src/tui/components/Tools/OptionSelector.tsx +160 -0
  272. package/src/tui/components/Tools/PermissionDialog.tsx +286 -0
  273. package/src/tui/components/Tools/ToolParams.tsx +483 -0
  274. package/src/tui/components/Tools/ToolsPanel.tsx +178 -0
  275. package/src/tui/components/Tools/__tests__/PermissionDialog.test.tsx +510 -0
  276. package/src/tui/components/Tools/__tests__/ToolParams.test.tsx +432 -0
  277. package/src/tui/components/Tools/index.ts +21 -0
  278. package/src/tui/components/TrustPrompt.tsx +279 -0
  279. package/src/tui/components/UpdateBanner.tsx +166 -0
  280. package/src/tui/components/VimModeIndicator.tsx +112 -0
  281. package/src/tui/components/backtrack/BacktrackControls.tsx +402 -0
  282. package/src/tui/components/backtrack/index.ts +13 -0
  283. package/src/tui/components/common/AutoApprovalStatus.tsx +251 -0
  284. package/src/tui/components/common/CostWarning.tsx +294 -0
  285. package/src/tui/components/common/DynamicShortcutHints.tsx +209 -0
  286. package/src/tui/components/common/EnhancedLoadingIndicator.tsx +305 -0
  287. package/src/tui/components/common/ErrorBoundary.tsx +140 -0
  288. package/src/tui/components/common/GradientText.tsx +224 -0
  289. package/src/tui/components/common/HotkeyHelpModal.tsx +193 -0
  290. package/src/tui/components/common/HotkeyHints.tsx +70 -0
  291. package/src/tui/components/common/MaxSizedBox.tsx +354 -0
  292. package/src/tui/components/common/NewMessagesBadge.tsx +65 -0
  293. package/src/tui/components/common/ProtectedFileLegend.tsx +89 -0
  294. package/src/tui/components/common/ScrollIndicator.tsx +160 -0
  295. package/src/tui/components/common/Spinner.tsx +342 -0
  296. package/src/tui/components/common/StreamingIndicator.tsx +316 -0
  297. package/src/tui/components/common/VirtualizedList/VirtualizedList.tsx +428 -0
  298. package/src/tui/components/common/VirtualizedList/hooks/index.ts +19 -0
  299. package/src/tui/components/common/VirtualizedList/hooks/useBatchedScroll.ts +64 -0
  300. package/src/tui/components/common/VirtualizedList/hooks/useScrollAnchor.ts +290 -0
  301. package/src/tui/components/common/VirtualizedList/hooks/useVirtualization.ts +340 -0
  302. package/src/tui/components/common/VirtualizedList/index.ts +30 -0
  303. package/src/tui/components/common/VirtualizedList/types.ts +107 -0
  304. package/src/tui/components/common/__tests__/NewMessagesBadge.test.tsx +74 -0
  305. package/src/tui/components/common/__tests__/ScrollIndicator.test.tsx +193 -0
  306. package/src/tui/components/common/index.ts +110 -0
  307. package/src/tui/components/index.ts +79 -0
  308. package/src/tui/components/session/CheckpointPanel.tsx +323 -0
  309. package/src/tui/components/session/RollbackDialog.tsx +169 -0
  310. package/src/tui/components/session/SessionItem.tsx +136 -0
  311. package/src/tui/components/session/SessionListPanel.tsx +252 -0
  312. package/src/tui/components/session/SessionPicker.tsx +449 -0
  313. package/src/tui/components/session/SessionPreview.tsx +240 -0
  314. package/src/tui/components/session/__tests__/session.test.tsx +408 -0
  315. package/src/tui/components/session/index.ts +28 -0
  316. package/src/tui/components/session/types.ts +116 -0
  317. package/src/tui/components/theme/__tests__/tokens.test.ts +471 -0
  318. package/src/tui/components/theme/index.ts +227 -0
  319. package/src/tui/components/theme/tokens.ts +484 -0
  320. package/src/tui/config/defaults.ts +134 -0
  321. package/src/tui/config/index.ts +17 -0
  322. package/src/tui/context/AnimationContext.tsx +284 -0
  323. package/src/tui/context/AppContext.tsx +349 -0
  324. package/src/tui/context/BracketedPasteContext.tsx +372 -0
  325. package/src/tui/context/LspContext.tsx +192 -0
  326. package/src/tui/context/McpContext.tsx +325 -0
  327. package/src/tui/context/MessagesContext.tsx +870 -0
  328. package/src/tui/context/OverflowContext.tsx +213 -0
  329. package/src/tui/context/RateLimitContext.tsx +108 -0
  330. package/src/tui/context/ResilienceContext.tsx +275 -0
  331. package/src/tui/context/RootProvider.tsx +136 -0
  332. package/src/tui/context/ScrollContext.tsx +331 -0
  333. package/src/tui/context/ToolsContext.tsx +702 -0
  334. package/src/tui/context/__tests__/BracketedPasteContext.test.tsx +416 -0
  335. package/src/tui/context/index.ts +140 -0
  336. package/src/tui/enterprise-integration.ts +282 -0
  337. package/src/tui/hooks/__tests__/useBacktrack.test.tsx +138 -0
  338. package/src/tui/hooks/__tests__/useBracketedPaste.test.tsx +222 -0
  339. package/src/tui/hooks/__tests__/useCopyMode.test.tsx +336 -0
  340. package/src/tui/hooks/__tests__/useHotkeys.ctrl-input.test.tsx +96 -0
  341. package/src/tui/hooks/__tests__/useHotkeys.test.tsx +454 -0
  342. package/src/tui/hooks/__tests__/useInputHistory.test.tsx +660 -0
  343. package/src/tui/hooks/__tests__/useLineBuffer.test.ts +295 -0
  344. package/src/tui/hooks/__tests__/useModeController.test.ts +137 -0
  345. package/src/tui/hooks/__tests__/useModeShortcuts.test.tsx +142 -0
  346. package/src/tui/hooks/__tests__/useScrollController.test.ts +464 -0
  347. package/src/tui/hooks/__tests__/useVim.test.tsx +531 -0
  348. package/src/tui/hooks/index.ts +252 -0
  349. package/src/tui/hooks/useAgentLoop.ts +712 -0
  350. package/src/tui/hooks/useAlternateBuffer.ts +398 -0
  351. package/src/tui/hooks/useAnimatedScrollbar.ts +241 -0
  352. package/src/tui/hooks/useBacktrack.ts +443 -0
  353. package/src/tui/hooks/useBracketedPaste.ts +104 -0
  354. package/src/tui/hooks/useCollapsible.ts +240 -0
  355. package/src/tui/hooks/useCopyMode.ts +382 -0
  356. package/src/tui/hooks/useCostSummary.ts +75 -0
  357. package/src/tui/hooks/useDesktopNotification.ts +414 -0
  358. package/src/tui/hooks/useDiffMode.ts +44 -0
  359. package/src/tui/hooks/useFileChangeStats.ts +110 -0
  360. package/src/tui/hooks/useFileSuggestions.ts +284 -0
  361. package/src/tui/hooks/useFlickerDetector.ts +250 -0
  362. package/src/tui/hooks/useGitStatus.ts +200 -0
  363. package/src/tui/hooks/useHotkeys.ts +579 -0
  364. package/src/tui/hooks/useImagePaste.ts +114 -0
  365. package/src/tui/hooks/useInputHighlight.ts +145 -0
  366. package/src/tui/hooks/useInputHistory.ts +246 -0
  367. package/src/tui/hooks/useKeyboardScroll.ts +209 -0
  368. package/src/tui/hooks/useLineBuffer.ts +356 -0
  369. package/src/tui/hooks/useMentionAutocomplete.ts +235 -0
  370. package/src/tui/hooks/useModeController.ts +167 -0
  371. package/src/tui/hooks/useModeShortcuts.ts +196 -0
  372. package/src/tui/hooks/usePermissionHandler.ts +146 -0
  373. package/src/tui/hooks/usePersistence.ts +480 -0
  374. package/src/tui/hooks/usePersistenceShortcuts.ts +225 -0
  375. package/src/tui/hooks/usePlaceholderRotation.ts +143 -0
  376. package/src/tui/hooks/useProviderStatus.ts +270 -0
  377. package/src/tui/hooks/useRateLimitStatus.ts +90 -0
  378. package/src/tui/hooks/useScreenReader.ts +315 -0
  379. package/src/tui/hooks/useScrollController.ts +450 -0
  380. package/src/tui/hooks/useScrollEventBatcher.ts +185 -0
  381. package/src/tui/hooks/useSidebarPanelData.ts +115 -0
  382. package/src/tui/hooks/useSmoothScroll.ts +202 -0
  383. package/src/tui/hooks/useSnapshots.ts +300 -0
  384. package/src/tui/hooks/useStateAndRef.ts +50 -0
  385. package/src/tui/hooks/useTerminalSize.ts +206 -0
  386. package/src/tui/hooks/useToolApprovalController.ts +91 -0
  387. package/src/tui/hooks/useVim.ts +334 -0
  388. package/src/tui/hooks/useWorkspace.ts +56 -0
  389. package/src/tui/i18n/__tests__/init.test.ts +278 -0
  390. package/src/tui/i18n/__tests__/language-config.test.ts +199 -0
  391. package/src/tui/i18n/__tests__/locale-detection.test.ts +250 -0
  392. package/src/tui/i18n/__tests__/settings-integration.test.ts +262 -0
  393. package/src/tui/i18n/index.ts +72 -0
  394. package/src/tui/i18n/init.ts +131 -0
  395. package/src/tui/i18n/language-config.ts +106 -0
  396. package/src/tui/i18n/locale-detection.ts +173 -0
  397. package/src/tui/i18n/settings-integration.ts +557 -0
  398. package/src/tui/i18n/tui-namespace.ts +538 -0
  399. package/src/tui/i18n/types.ts +312 -0
  400. package/src/tui/index.ts +43 -0
  401. package/src/tui/lsp-integration.ts +409 -0
  402. package/src/tui/metrics-integration.ts +366 -0
  403. package/src/tui/plugins.ts +383 -0
  404. package/src/tui/resilience.ts +342 -0
  405. package/src/tui/sandbox-integration.ts +317 -0
  406. package/src/tui/services/clipboard.ts +348 -0
  407. package/src/tui/services/fuzzy-search.ts +441 -0
  408. package/src/tui/services/index.ts +72 -0
  409. package/src/tui/services/markdown-renderer.ts +565 -0
  410. package/src/tui/services/open-external.ts +247 -0
  411. package/src/tui/services/syntax-highlighter.ts +483 -0
  412. package/src/tui/slash-commands.ts +12 -0
  413. package/src/tui/theme/index.ts +15 -0
  414. package/src/tui/theme/provider.tsx +206 -0
  415. package/src/tui/tip-integration.ts +300 -0
  416. package/src/tui/types/__tests__/ink-extended.test.ts +121 -0
  417. package/src/tui/types/ink-extended.ts +87 -0
  418. package/src/tui/utils/__tests__/bracketedPaste.test.ts +231 -0
  419. package/src/tui/utils/__tests__/heightEstimator.test.ts +157 -0
  420. package/src/tui/utils/__tests__/text-width.test.ts +158 -0
  421. package/src/tui/utils/__tests__/textSanitizer.test.ts +266 -0
  422. package/src/tui/utils/__tests__/ui-sizing.test.ts +169 -0
  423. package/src/tui/utils/bracketedPaste.ts +107 -0
  424. package/src/tui/utils/cursor-manager.ts +131 -0
  425. package/src/tui/utils/detectTerminal.ts +596 -0
  426. package/src/tui/utils/findLastSafeSplitPoint.ts +92 -0
  427. package/src/tui/utils/heightEstimator.ts +198 -0
  428. package/src/tui/utils/index.ts +91 -0
  429. package/src/tui/utils/isNarrowWidth.ts +52 -0
  430. package/src/tui/utils/stdoutGuard.ts +90 -0
  431. package/src/tui/utils/synchronized-update.ts +70 -0
  432. package/src/tui/utils/text-width.ts +225 -0
  433. package/src/tui/utils/textSanitizer.ts +225 -0
  434. package/src/tui/utils/textUtils.ts +114 -0
  435. package/src/tui/utils/ui-sizing.ts +192 -0
  436. package/src/tui-blessed/app.ts +160 -0
  437. package/src/tui-blessed/index.ts +2 -0
  438. package/src/tui-blessed/neo-blessed.d.ts +6 -0
  439. package/src/tui-blessed/test.ts +21 -0
  440. package/src/tui-blessed/types.ts +14 -0
  441. package/src/utils/icons.ts +130 -0
  442. package/src/utils/index.ts +33 -0
  443. package/src/utils/resume-hint.ts +86 -0
  444. package/src/version.ts +1 -0
  445. package/tsconfig.json +8 -0
  446. package/vitest.config.ts +35 -0
@@ -0,0 +1,683 @@
1
+ /**
2
+ * Autocomplete Unit Tests
3
+ *
4
+ * Tests for the autocomplete state management including:
5
+ * - fuzzyScore function with various patterns
6
+ * - Candidate sorting by score
7
+ * - Navigation (prev/next) with wrapping
8
+ * - Tab completion
9
+ * - Cancel/reset behavior
10
+ * - Highlight computation
11
+ *
12
+ * @module cli/commands/__tests__/autocomplete
13
+ */
14
+
15
+ import { beforeEach, describe, expect, it } from "vitest";
16
+
17
+ import {
18
+ type AutocompleteState,
19
+ autocompleteReducer,
20
+ computeHighlights,
21
+ fuzzyScore,
22
+ getSelectedCandidate,
23
+ initialAutocompleteState,
24
+ shouldShowAutocomplete,
25
+ } from "../autocomplete.js";
26
+ import { CommandRegistry } from "../registry.js";
27
+ import type { SlashCommand } from "../types.js";
28
+
29
+ // =============================================================================
30
+ // Test Fixtures
31
+ // =============================================================================
32
+
33
+ /**
34
+ * Create a mock SlashCommand for testing
35
+ */
36
+ function createMockCommand(overrides: Partial<SlashCommand> & { name: string }): SlashCommand {
37
+ return {
38
+ description: `Mock command: ${overrides.name}`,
39
+ kind: "builtin",
40
+ category: "system",
41
+ execute: async () => ({ kind: "success" as const }),
42
+ ...overrides,
43
+ };
44
+ }
45
+
46
+ /**
47
+ * Create a registry with common test commands
48
+ */
49
+ function createTestRegistry(): CommandRegistry {
50
+ const registry = new CommandRegistry();
51
+
52
+ registry.register(createMockCommand({ name: "help", aliases: ["h", "?"] }));
53
+ registry.register(createMockCommand({ name: "history", category: "session" }));
54
+ registry.register(createMockCommand({ name: "clear", aliases: ["cls"] }));
55
+ registry.register(createMockCommand({ name: "exit", aliases: ["quit", "q"] }));
56
+ registry.register(createMockCommand({ name: "login", category: "auth", aliases: ["signin"] }));
57
+ registry.register(createMockCommand({ name: "logout", category: "auth", aliases: ["signout"] }));
58
+ registry.register(createMockCommand({ name: "git-commit", category: "tools" }));
59
+ registry.register(createMockCommand({ name: "git-status", category: "tools" }));
60
+
61
+ return registry;
62
+ }
63
+
64
+ // =============================================================================
65
+ // T026: fuzzyScore Tests
66
+ // =============================================================================
67
+
68
+ describe("fuzzyScore", () => {
69
+ describe("exact match", () => {
70
+ it("should return highest score for exact match", () => {
71
+ const result = fuzzyScore("help", "help");
72
+
73
+ expect(result).not.toBeNull();
74
+ expect(result?.score).toBe(140); // 100 (exact) + 10*4 (chars)
75
+ expect(result?.ranges).toEqual([[0, 4]]);
76
+ });
77
+
78
+ it("should be case-insensitive for exact match", () => {
79
+ const result = fuzzyScore("HELP", "help");
80
+
81
+ expect(result).not.toBeNull();
82
+ expect(result?.score).toBe(140);
83
+ });
84
+ });
85
+
86
+ describe("prefix match", () => {
87
+ it("should give high score for prefix match", () => {
88
+ const result = fuzzyScore("h", "help");
89
+
90
+ expect(result).not.toBeNull();
91
+ // 80 (prefix) + 10 (char) - 3 (unmatched penalty for 'elp')
92
+ expect(result?.score).toBe(87);
93
+ expect(result?.ranges).toEqual([[0, 1]]);
94
+ });
95
+
96
+ it("should score longer prefix higher", () => {
97
+ const result1 = fuzzyScore("h", "help");
98
+ const result2 = fuzzyScore("he", "help");
99
+ const result3 = fuzzyScore("hel", "help");
100
+
101
+ expect(result1).not.toBeNull();
102
+ expect(result2).not.toBeNull();
103
+ expect(result3).not.toBeNull();
104
+ // Safe comparison after null checks
105
+ if (result1 && result2 && result3) {
106
+ expect(result2.score).toBeGreaterThan(result1.score);
107
+ expect(result3.score).toBeGreaterThan(result2.score);
108
+ }
109
+ });
110
+
111
+ it("should return full prefix range", () => {
112
+ const result = fuzzyScore("hel", "help");
113
+
114
+ expect(result?.ranges).toEqual([[0, 3]]);
115
+ });
116
+ });
117
+
118
+ describe("fuzzy match", () => {
119
+ it("should match non-consecutive characters", () => {
120
+ const result = fuzzyScore("hp", "help");
121
+
122
+ expect(result).not.toBeNull();
123
+ expect(result?.ranges).toEqual([
124
+ [0, 1],
125
+ [3, 4],
126
+ ]);
127
+ });
128
+
129
+ it("should return null for no match", () => {
130
+ const result = fuzzyScore("xyz", "help");
131
+
132
+ expect(result).toBeNull();
133
+ });
134
+
135
+ it("should return null for partial query match", () => {
136
+ const result = fuzzyScore("helpx", "help");
137
+
138
+ expect(result).toBeNull();
139
+ });
140
+ });
141
+
142
+ describe("consecutive character bonus", () => {
143
+ it("should give bonus for consecutive matches", () => {
144
+ // 'he' in 'help' is consecutive
145
+ const consecutiveResult = fuzzyScore("he", "help");
146
+ // 'hp' in 'help' is not consecutive
147
+ const nonConsecutiveResult = fuzzyScore("hp", "help");
148
+
149
+ expect(consecutiveResult).not.toBeNull();
150
+ expect(nonConsecutiveResult).not.toBeNull();
151
+ // Safe comparison after null checks
152
+ if (consecutiveResult && nonConsecutiveResult) {
153
+ expect(consecutiveResult.score).toBeGreaterThan(nonConsecutiveResult.score);
154
+ }
155
+ });
156
+ });
157
+
158
+ describe("word boundary bonus", () => {
159
+ it("should give bonus for match at word boundary", () => {
160
+ // 'gc' should match 'g' at start and 'c' after hyphen
161
+ const result = fuzzyScore("gc", "git-commit");
162
+
163
+ expect(result).not.toBeNull();
164
+ // Should have word boundary bonus for both 'g' and 'c'
165
+ expect(result?.score).toBeGreaterThan(20); // Base would be ~20 without bonus
166
+ });
167
+
168
+ it("should match first character with word boundary bonus", () => {
169
+ const result = fuzzyScore("g", "git");
170
+
171
+ expect(result).not.toBeNull();
172
+ // First character always gets word boundary bonus
173
+ });
174
+ });
175
+
176
+ describe("empty query", () => {
177
+ it("should return score 0 with empty ranges for empty query", () => {
178
+ const result = fuzzyScore("", "help");
179
+
180
+ expect(result).not.toBeNull();
181
+ expect(result?.score).toBe(0);
182
+ expect(result?.ranges).toEqual([]);
183
+ });
184
+ });
185
+
186
+ describe("score comparison", () => {
187
+ it("should rank exact > prefix > fuzzy", () => {
188
+ const exact = fuzzyScore("help", "help");
189
+ const prefix = fuzzyScore("hel", "help");
190
+ const fuzzy = fuzzyScore("hp", "help");
191
+
192
+ expect(exact).not.toBeNull();
193
+ expect(prefix).not.toBeNull();
194
+ expect(fuzzy).not.toBeNull();
195
+ expect(exact?.score).toBeGreaterThan(prefix?.score ?? 0);
196
+ expect(prefix?.score).toBeGreaterThan(fuzzy?.score ?? 0);
197
+ });
198
+ });
199
+ });
200
+
201
+ // =============================================================================
202
+ // T026: Candidate Sorting Tests
203
+ // =============================================================================
204
+
205
+ describe("autocompleteReducer candidate sorting", () => {
206
+ let registry: CommandRegistry;
207
+
208
+ beforeEach(() => {
209
+ registry = createTestRegistry();
210
+ });
211
+
212
+ it("should sort candidates by score descending", () => {
213
+ const state = autocompleteReducer(initialAutocompleteState, {
214
+ type: "INPUT_CHANGE",
215
+ query: "h",
216
+ registry,
217
+ });
218
+
219
+ expect(state.candidates.length).toBeGreaterThan(0);
220
+
221
+ // Verify sorted by score descending
222
+ for (let i = 1; i < state.candidates.length; i++) {
223
+ const prev = state.candidates[i - 1];
224
+ const curr = state.candidates[i];
225
+ expect(prev).toBeDefined();
226
+ expect(curr).toBeDefined();
227
+ // Safe comparison after defined checks
228
+ if (prev && curr) {
229
+ expect(prev.score).toBeGreaterThanOrEqual(curr.score);
230
+ }
231
+ }
232
+ });
233
+
234
+ it("should sort alphabetically for equal scores", () => {
235
+ const state = autocompleteReducer(initialAutocompleteState, {
236
+ type: "INPUT_CHANGE",
237
+ query: "git",
238
+ registry,
239
+ });
240
+
241
+ // Both git-commit and git-status should have same prefix score
242
+ const gitCommands = state.candidates.filter((c) => c.command.name.startsWith("git-"));
243
+ expect(gitCommands.length).toBe(2);
244
+
245
+ // Should be alphabetically sorted
246
+ expect(gitCommands[0]?.command.name).toBe("git-commit");
247
+ expect(gitCommands[1]?.command.name).toBe("git-status");
248
+ });
249
+
250
+ it("should include commands matching query", () => {
251
+ const state = autocompleteReducer(initialAutocompleteState, {
252
+ type: "INPUT_CHANGE",
253
+ query: "log",
254
+ registry,
255
+ });
256
+
257
+ const names = state.candidates.map((c) => c.command.name);
258
+ expect(names).toContain("login");
259
+ expect(names).toContain("logout");
260
+ });
261
+ });
262
+
263
+ // =============================================================================
264
+ // T026: Navigation Tests
265
+ // =============================================================================
266
+
267
+ describe("autocompleteReducer navigation", () => {
268
+ let registry: CommandRegistry;
269
+ let stateWithCandidates: AutocompleteState;
270
+
271
+ beforeEach(() => {
272
+ registry = createTestRegistry();
273
+ stateWithCandidates = autocompleteReducer(initialAutocompleteState, {
274
+ type: "INPUT_CHANGE",
275
+ query: "h",
276
+ registry,
277
+ });
278
+ });
279
+
280
+ describe("SELECT_NEXT", () => {
281
+ it("should increment selectedIndex", () => {
282
+ const state = autocompleteReducer(stateWithCandidates, { type: "SELECT_NEXT" });
283
+
284
+ expect(state.selectedIndex).toBe(1);
285
+ });
286
+
287
+ it("should wrap to 0 at end", () => {
288
+ let state = stateWithCandidates;
289
+ const candidateCount = state.candidates.length;
290
+
291
+ // Navigate to last item
292
+ for (let i = 0; i < candidateCount - 1; i++) {
293
+ state = autocompleteReducer(state, { type: "SELECT_NEXT" });
294
+ }
295
+ expect(state.selectedIndex).toBe(candidateCount - 1);
296
+
297
+ // Next should wrap to 0
298
+ state = autocompleteReducer(state, { type: "SELECT_NEXT" });
299
+ expect(state.selectedIndex).toBe(0);
300
+ });
301
+
302
+ it("should not change state if no candidates", () => {
303
+ const emptyState: AutocompleteState = {
304
+ ...initialAutocompleteState,
305
+ active: true,
306
+ candidates: [],
307
+ };
308
+
309
+ const state = autocompleteReducer(emptyState, { type: "SELECT_NEXT" });
310
+
311
+ expect(state.selectedIndex).toBe(0);
312
+ });
313
+
314
+ it("should not change state if inactive", () => {
315
+ const state = autocompleteReducer(initialAutocompleteState, { type: "SELECT_NEXT" });
316
+
317
+ expect(state.selectedIndex).toBe(0);
318
+ });
319
+ });
320
+
321
+ describe("SELECT_PREV", () => {
322
+ it("should decrement selectedIndex", () => {
323
+ // First go to index 1
324
+ let state = autocompleteReducer(stateWithCandidates, { type: "SELECT_NEXT" });
325
+ expect(state.selectedIndex).toBe(1);
326
+
327
+ // Then go back to 0
328
+ state = autocompleteReducer(state, { type: "SELECT_PREV" });
329
+ expect(state.selectedIndex).toBe(0);
330
+ });
331
+
332
+ it("should wrap to end at start", () => {
333
+ const candidateCount = stateWithCandidates.candidates.length;
334
+
335
+ // At index 0, prev should wrap to last
336
+ const state = autocompleteReducer(stateWithCandidates, { type: "SELECT_PREV" });
337
+
338
+ expect(state.selectedIndex).toBe(candidateCount - 1);
339
+ });
340
+
341
+ it("should not change state if no candidates", () => {
342
+ const emptyState: AutocompleteState = {
343
+ ...initialAutocompleteState,
344
+ active: true,
345
+ candidates: [],
346
+ };
347
+
348
+ const state = autocompleteReducer(emptyState, { type: "SELECT_PREV" });
349
+
350
+ expect(state.selectedIndex).toBe(0);
351
+ });
352
+ });
353
+ });
354
+
355
+ // =============================================================================
356
+ // T026: Tab Complete Tests
357
+ // =============================================================================
358
+
359
+ describe("autocompleteReducer TAB_COMPLETE", () => {
360
+ let registry: CommandRegistry;
361
+
362
+ beforeEach(() => {
363
+ registry = createTestRegistry();
364
+ });
365
+
366
+ it("should return state unchanged for completion", () => {
367
+ const stateWithCandidates = autocompleteReducer(initialAutocompleteState, {
368
+ type: "INPUT_CHANGE",
369
+ query: "h",
370
+ registry,
371
+ });
372
+
373
+ const state = autocompleteReducer(stateWithCandidates, { type: "TAB_COMPLETE" });
374
+
375
+ // State should be same - caller reads selectedIndex to get completion
376
+ expect(state).toEqual(stateWithCandidates);
377
+ });
378
+
379
+ it("should allow reading selected candidate after TAB_COMPLETE", () => {
380
+ let state = autocompleteReducer(initialAutocompleteState, {
381
+ type: "INPUT_CHANGE",
382
+ query: "h",
383
+ registry,
384
+ });
385
+
386
+ // Navigate to second item
387
+ state = autocompleteReducer(state, { type: "SELECT_NEXT" });
388
+
389
+ // Tab complete
390
+ state = autocompleteReducer(state, { type: "TAB_COMPLETE" });
391
+
392
+ // Should be able to get selected candidate
393
+ const selected = getSelectedCandidate(state);
394
+ expect(selected).toBeDefined();
395
+ expect(selected?.command).toBeDefined();
396
+ });
397
+ });
398
+
399
+ // =============================================================================
400
+ // T026: Cancel Tests
401
+ // =============================================================================
402
+
403
+ describe("autocompleteReducer CANCEL", () => {
404
+ let registry: CommandRegistry;
405
+
406
+ beforeEach(() => {
407
+ registry = createTestRegistry();
408
+ });
409
+
410
+ it("should reset state to initial", () => {
411
+ const stateWithCandidates = autocompleteReducer(initialAutocompleteState, {
412
+ type: "INPUT_CHANGE",
413
+ query: "h",
414
+ registry,
415
+ });
416
+
417
+ const state = autocompleteReducer(stateWithCandidates, { type: "CANCEL" });
418
+
419
+ expect(state.active).toBe(false);
420
+ expect(state.query).toBe("");
421
+ expect(state.candidates).toEqual([]);
422
+ expect(state.selectedIndex).toBe(0);
423
+ });
424
+
425
+ it("should work even if already inactive", () => {
426
+ const state = autocompleteReducer(initialAutocompleteState, { type: "CANCEL" });
427
+
428
+ expect(state.active).toBe(false);
429
+ });
430
+ });
431
+
432
+ // =============================================================================
433
+ // T026: INPUT_CHANGE Tests
434
+ // =============================================================================
435
+
436
+ describe("autocompleteReducer INPUT_CHANGE", () => {
437
+ let registry: CommandRegistry;
438
+
439
+ beforeEach(() => {
440
+ registry = createTestRegistry();
441
+ });
442
+
443
+ it("should activate autocomplete with matching query", () => {
444
+ const state = autocompleteReducer(initialAutocompleteState, {
445
+ type: "INPUT_CHANGE",
446
+ query: "h",
447
+ registry,
448
+ });
449
+
450
+ expect(state.active).toBe(true);
451
+ expect(state.query).toBe("h");
452
+ expect(state.candidates.length).toBeGreaterThan(0);
453
+ });
454
+
455
+ it("should deactivate autocomplete with empty query", () => {
456
+ // First activate
457
+ let state = autocompleteReducer(initialAutocompleteState, {
458
+ type: "INPUT_CHANGE",
459
+ query: "h",
460
+ registry,
461
+ });
462
+
463
+ // Then clear query
464
+ state = autocompleteReducer(state, {
465
+ type: "INPUT_CHANGE",
466
+ query: "",
467
+ registry,
468
+ });
469
+
470
+ expect(state.active).toBe(false);
471
+ });
472
+
473
+ it("should reset selectedIndex on query change", () => {
474
+ let state = autocompleteReducer(initialAutocompleteState, {
475
+ type: "INPUT_CHANGE",
476
+ query: "h",
477
+ registry,
478
+ });
479
+
480
+ // Navigate down
481
+ state = autocompleteReducer(state, { type: "SELECT_NEXT" });
482
+ expect(state.selectedIndex).toBe(1);
483
+
484
+ // Change query should reset selection
485
+ state = autocompleteReducer(state, {
486
+ type: "INPUT_CHANGE",
487
+ query: "he",
488
+ registry,
489
+ });
490
+
491
+ expect(state.selectedIndex).toBe(0);
492
+ });
493
+
494
+ it("should populate highlights map", () => {
495
+ const state = autocompleteReducer(initialAutocompleteState, {
496
+ type: "INPUT_CHANGE",
497
+ query: "h",
498
+ registry,
499
+ });
500
+
501
+ expect(state.highlights.size).toBe(state.candidates.length);
502
+
503
+ // Each candidate should have highlights
504
+ for (const candidate of state.candidates) {
505
+ expect(state.highlights.has(candidate.command.name)).toBe(true);
506
+ }
507
+ });
508
+
509
+ it("should handle non-matching query", () => {
510
+ const state = autocompleteReducer(initialAutocompleteState, {
511
+ type: "INPUT_CHANGE",
512
+ query: "xyz123",
513
+ registry,
514
+ });
515
+
516
+ expect(state.active).toBe(false);
517
+ expect(state.candidates).toEqual([]);
518
+ });
519
+ });
520
+
521
+ // =============================================================================
522
+ // T026: computeHighlights Tests
523
+ // =============================================================================
524
+
525
+ describe("computeHighlights", () => {
526
+ it("should return empty map for empty candidates", () => {
527
+ const result = computeHighlights([]);
528
+
529
+ expect(result.size).toBe(0);
530
+ });
531
+
532
+ it("should map command names to match ranges", () => {
533
+ const candidates = [
534
+ {
535
+ command: createMockCommand({ name: "help" }),
536
+ score: 100,
537
+ matchRanges: [[0, 1]] as [number, number][],
538
+ },
539
+ {
540
+ command: createMockCommand({ name: "history" }),
541
+ score: 80,
542
+ matchRanges: [[0, 1]] as [number, number][],
543
+ },
544
+ ];
545
+
546
+ const result = computeHighlights(candidates);
547
+
548
+ expect(result.size).toBe(2);
549
+ expect(result.get("help")).toEqual([[0, 1]]);
550
+ expect(result.get("history")).toEqual([[0, 1]]);
551
+ });
552
+
553
+ it("should preserve multiple ranges", () => {
554
+ const candidates = [
555
+ {
556
+ command: createMockCommand({ name: "git-commit" }),
557
+ score: 50,
558
+ matchRanges: [
559
+ [0, 1],
560
+ [4, 5],
561
+ ] as [number, number][],
562
+ },
563
+ ];
564
+
565
+ const result = computeHighlights(candidates);
566
+
567
+ expect(result.get("git-commit")).toEqual([
568
+ [0, 1],
569
+ [4, 5],
570
+ ]);
571
+ });
572
+ });
573
+
574
+ // =============================================================================
575
+ // T026: Utility Function Tests
576
+ // =============================================================================
577
+
578
+ describe("getSelectedCandidate", () => {
579
+ let registry: CommandRegistry;
580
+
581
+ beforeEach(() => {
582
+ registry = createTestRegistry();
583
+ });
584
+
585
+ it("should return undefined when inactive", () => {
586
+ const result = getSelectedCandidate(initialAutocompleteState);
587
+
588
+ expect(result).toBeUndefined();
589
+ });
590
+
591
+ it("should return undefined when no candidates", () => {
592
+ const state: AutocompleteState = {
593
+ ...initialAutocompleteState,
594
+ active: true,
595
+ candidates: [],
596
+ };
597
+
598
+ const result = getSelectedCandidate(state);
599
+
600
+ expect(result).toBeUndefined();
601
+ });
602
+
603
+ it("should return selected candidate", () => {
604
+ const state = autocompleteReducer(initialAutocompleteState, {
605
+ type: "INPUT_CHANGE",
606
+ query: "h",
607
+ registry,
608
+ });
609
+
610
+ const result = getSelectedCandidate(state);
611
+
612
+ expect(result).toBeDefined();
613
+ expect(result).toBe(state.candidates[0]);
614
+ });
615
+ });
616
+
617
+ describe("shouldShowAutocomplete", () => {
618
+ let registry: CommandRegistry;
619
+
620
+ beforeEach(() => {
621
+ registry = createTestRegistry();
622
+ });
623
+
624
+ it("should return false when inactive", () => {
625
+ expect(shouldShowAutocomplete(initialAutocompleteState)).toBe(false);
626
+ });
627
+
628
+ it("should return false when no candidates", () => {
629
+ const state: AutocompleteState = {
630
+ ...initialAutocompleteState,
631
+ active: true,
632
+ candidates: [],
633
+ };
634
+
635
+ expect(shouldShowAutocomplete(state)).toBe(false);
636
+ });
637
+
638
+ it("should return true when active with candidates", () => {
639
+ const state = autocompleteReducer(initialAutocompleteState, {
640
+ type: "INPUT_CHANGE",
641
+ query: "h",
642
+ registry,
643
+ });
644
+
645
+ expect(shouldShowAutocomplete(state)).toBe(true);
646
+ });
647
+ });
648
+
649
+ // =============================================================================
650
+ // T026: Alias Matching Tests
651
+ // =============================================================================
652
+
653
+ describe("autocomplete with aliases", () => {
654
+ let registry: CommandRegistry;
655
+
656
+ beforeEach(() => {
657
+ registry = createTestRegistry();
658
+ });
659
+
660
+ it("should match command by alias", () => {
661
+ const state = autocompleteReducer(initialAutocompleteState, {
662
+ type: "INPUT_CHANGE",
663
+ query: "cls",
664
+ registry,
665
+ });
666
+
667
+ // Should find 'clear' command via its 'cls' alias
668
+ const clearCandidate = state.candidates.find((c) => c.command.name === "clear");
669
+ expect(clearCandidate).toBeDefined();
670
+ });
671
+
672
+ it("should prefer better alias match over command name", () => {
673
+ const state = autocompleteReducer(initialAutocompleteState, {
674
+ type: "INPUT_CHANGE",
675
+ query: "q",
676
+ registry,
677
+ });
678
+
679
+ // Should find 'exit' command via its 'q' alias
680
+ const exitCandidate = state.candidates.find((c) => c.command.name === "exit");
681
+ expect(exitCandidate).toBeDefined();
682
+ });
683
+ });