@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,728 @@
1
+ /**
2
+ * Command System E2E Tests
3
+ *
4
+ * End-to-end tests covering the full command lifecycle:
5
+ * - User input → Parser → Executor → Result
6
+ * - Autocomplete flow
7
+ * - Error handling with suggestions
8
+ *
9
+ * @module cli/__tests__/commands.e2e
10
+ */
11
+
12
+ import { beforeEach, describe, expect, it } from "vitest";
13
+
14
+ import {
15
+ type AutocompleteState,
16
+ autocompleteReducer,
17
+ type CommandContext,
18
+ type CommandContextProvider,
19
+ CommandExecutor,
20
+ CommandParser,
21
+ CommandRegistry,
22
+ type CommandResult,
23
+ clearCommand,
24
+ createTestContextProvider,
25
+ exitCommand,
26
+ fuzzyScore,
27
+ getSelectedCandidate,
28
+ helpCommand,
29
+ initialAutocompleteState,
30
+ type SlashCommandDef,
31
+ setHelpRegistry,
32
+ shouldShowAutocomplete,
33
+ } from "../commands/index.js";
34
+
35
+ // =============================================================================
36
+ // Test Fixtures
37
+ // =============================================================================
38
+
39
+ /**
40
+ * Create a mock SlashCommand for testing
41
+ */
42
+ function createMockCommand(
43
+ overrides: Partial<SlashCommandDef> & { name: string }
44
+ ): SlashCommandDef {
45
+ return {
46
+ description: `Mock command: ${overrides.name}`,
47
+ kind: "builtin",
48
+ category: "system",
49
+ execute: async () => ({ kind: "success" as const }),
50
+ ...overrides,
51
+ };
52
+ }
53
+
54
+ /**
55
+ * E2E test harness for command system
56
+ */
57
+ class CommandSystemHarness {
58
+ readonly registry: CommandRegistry;
59
+ readonly parser: CommandParser;
60
+ readonly executor: CommandExecutor;
61
+ readonly contextProvider: CommandContextProvider;
62
+ private emittedEvents: Array<{ event: string; data?: unknown }> = [];
63
+
64
+ constructor() {
65
+ this.registry = new CommandRegistry();
66
+ this.parser = new CommandParser();
67
+ this.contextProvider = createTestContextProvider({
68
+ emit: (event, data) => {
69
+ this.emittedEvents.push({ event, data });
70
+ },
71
+ });
72
+ this.executor = new CommandExecutor(this.registry, this.contextProvider);
73
+ }
74
+
75
+ /**
76
+ * Register core commands for testing
77
+ */
78
+ registerCoreCommands(): void {
79
+ this.registry.register(helpCommand);
80
+ this.registry.register(clearCommand);
81
+ this.registry.register(exitCommand);
82
+ setHelpRegistry(this.registry);
83
+ }
84
+
85
+ /**
86
+ * Execute a command string and return the result
87
+ */
88
+ async execute(input: string): Promise<CommandResult> {
89
+ return this.executor.execute(input);
90
+ }
91
+
92
+ /**
93
+ * Get autocomplete candidates for input
94
+ */
95
+ getAutocompleteCandidates(input: string): AutocompleteState {
96
+ const query = input.startsWith("/") ? input.slice(1) : input;
97
+ return autocompleteReducer(initialAutocompleteState, {
98
+ type: "INPUT_CHANGE",
99
+ query,
100
+ registry: this.registry,
101
+ });
102
+ }
103
+
104
+ /**
105
+ * Simulate tab completion
106
+ */
107
+ tabComplete(state: AutocompleteState): AutocompleteState {
108
+ return autocompleteReducer(state, { type: "TAB_COMPLETE" });
109
+ }
110
+
111
+ /**
112
+ * Get emitted events
113
+ */
114
+ getEmittedEvents(): Array<{ event: string; data?: unknown }> {
115
+ return [...this.emittedEvents];
116
+ }
117
+
118
+ /**
119
+ * Clear emitted events
120
+ */
121
+ clearEvents(): void {
122
+ this.emittedEvents = [];
123
+ }
124
+ }
125
+
126
+ // =============================================================================
127
+ // E2E Test: /help Command
128
+ // =============================================================================
129
+
130
+ describe("E2E: /help command", () => {
131
+ let harness: CommandSystemHarness;
132
+
133
+ beforeEach(() => {
134
+ harness = new CommandSystemHarness();
135
+ harness.registerCoreCommands();
136
+ });
137
+
138
+ it("should receive formatted help output for /help", async () => {
139
+ const result = await harness.execute("/help");
140
+
141
+ expect(result.kind).toBe("success");
142
+ if (result.kind === "success") {
143
+ expect(result.message).toBeDefined();
144
+ expect(result.message).toContain("Available Commands");
145
+ expect(result.message).toContain("/help");
146
+ expect(result.message).toContain("/clear");
147
+ expect(result.message).toContain("/exit(quit)");
148
+ }
149
+ });
150
+
151
+ it("should show command-specific help for /help <command>", async () => {
152
+ const result = await harness.execute("/help exit");
153
+
154
+ expect(result.kind).toBe("success");
155
+ if (result.kind === "success") {
156
+ expect(result.message).toBeDefined();
157
+ expect(result.message).toContain("/exit(quit)");
158
+ expect(result.message).toContain("Exit the application");
159
+ }
160
+ });
161
+
162
+ it("should show category help for /help system", async () => {
163
+ const result = await harness.execute("/help system");
164
+
165
+ expect(result.kind).toBe("success");
166
+ if (result.kind === "success") {
167
+ expect(result.message).toBeDefined();
168
+ expect(result.message).toContain("System");
169
+ }
170
+ });
171
+
172
+ it("should handle alias /h", async () => {
173
+ const result = await harness.execute("/h");
174
+
175
+ expect(result.kind).toBe("success");
176
+ if (result.kind === "success") {
177
+ expect(result.message).toContain("Available Commands");
178
+ }
179
+ });
180
+ });
181
+
182
+ // =============================================================================
183
+ // E2E Test: /auth set Command (Interactive)
184
+ // =============================================================================
185
+
186
+ describe("E2E: /auth set command (interactive)", () => {
187
+ let harness: CommandSystemHarness;
188
+
189
+ beforeEach(() => {
190
+ harness = new CommandSystemHarness();
191
+ harness.registerCoreCommands();
192
+
193
+ // Add auth command for testing (with set subcommand)
194
+ const authCommand = createMockCommand({
195
+ name: "auth",
196
+ category: "auth",
197
+ positionalArgs: [
198
+ {
199
+ name: "subcommand",
200
+ type: "string",
201
+ description: "Subcommand: status, set, clear",
202
+ required: false,
203
+ },
204
+ {
205
+ name: "provider",
206
+ type: "string",
207
+ description: "Provider name",
208
+ required: false,
209
+ },
210
+ ],
211
+ execute: async (ctx: CommandContext) => {
212
+ const subcommand = (ctx.parsedArgs.positional[0] as string) ?? "status";
213
+ const provider =
214
+ (ctx.parsedArgs.positional[1] as string) ?? ctx.session.provider ?? "anthropic";
215
+
216
+ if (subcommand === "set") {
217
+ return {
218
+ kind: "interactive",
219
+ prompt: {
220
+ inputType: "password",
221
+ message: `Enter API key for ${provider}:`,
222
+ placeholder: "sk-...",
223
+ provider,
224
+ handler: async (value: string) => {
225
+ if (!value.trim()) {
226
+ return {
227
+ kind: "error",
228
+ code: "INVALID_ARGUMENT",
229
+ message: "API key cannot be empty",
230
+ };
231
+ }
232
+ return {
233
+ kind: "success",
234
+ message: `✅ Credential saved for ${provider}`,
235
+ };
236
+ },
237
+ onCancel: () => ({ kind: "success", message: "Auth cancelled" }),
238
+ },
239
+ };
240
+ }
241
+
242
+ return {
243
+ kind: "success",
244
+ message: "Authentication status",
245
+ };
246
+ },
247
+ });
248
+ harness.registry.register(authCommand);
249
+ });
250
+
251
+ it("should return interactive prompt for /auth set", async () => {
252
+ const result = await harness.execute("/auth set");
253
+
254
+ expect(result.kind).toBe("interactive");
255
+ if (result.kind === "interactive") {
256
+ expect(result.prompt.inputType).toBe("password");
257
+ expect(result.prompt.message).toContain("Enter API key");
258
+ expect(result.prompt.placeholder).toBe("sk-...");
259
+ }
260
+ });
261
+
262
+ it("should accept provider argument: /auth set anthropic", async () => {
263
+ const result = await harness.execute("/auth set anthropic");
264
+
265
+ expect(result.kind).toBe("interactive");
266
+ if (result.kind === "interactive") {
267
+ expect(result.prompt.message).toContain("anthropic");
268
+ expect(result.prompt.provider).toBe("anthropic");
269
+ }
270
+ });
271
+
272
+ it("should handle input submission via handler", async () => {
273
+ const result = await harness.execute("/auth set openai");
274
+
275
+ expect(result.kind).toBe("interactive");
276
+ if (result.kind === "interactive") {
277
+ const submitResult = await result.prompt.handler("sk-test-key-12345");
278
+ expect(submitResult.kind).toBe("success");
279
+ if (submitResult.kind === "success") {
280
+ expect(submitResult.message).toContain("Credential saved");
281
+ expect(submitResult.message).toContain("openai");
282
+ }
283
+ }
284
+ });
285
+
286
+ it("should validate empty input", async () => {
287
+ const result = await harness.execute("/auth set");
288
+
289
+ expect(result.kind).toBe("interactive");
290
+ if (result.kind === "interactive") {
291
+ const submitResult = await result.prompt.handler("");
292
+ expect(submitResult.kind).toBe("error");
293
+ if (submitResult.kind === "error") {
294
+ expect(submitResult.code).toBe("INVALID_ARGUMENT");
295
+ expect(submitResult.message).toContain("cannot be empty");
296
+ }
297
+ }
298
+ });
299
+
300
+ it("should handle cancellation", async () => {
301
+ const result = await harness.execute("/auth set");
302
+
303
+ expect(result.kind).toBe("interactive");
304
+ if (result.kind === "interactive") {
305
+ const cancelResult = result.prompt.onCancel?.();
306
+ expect(cancelResult?.kind).toBe("success");
307
+ if (cancelResult?.kind === "success") {
308
+ expect(cancelResult.message).toContain("cancelled");
309
+ }
310
+ }
311
+ });
312
+ });
313
+
314
+ // =============================================================================
315
+ // E2E Test: /exit Command
316
+ // =============================================================================
317
+
318
+ describe("E2E: /exit command", () => {
319
+ let harness: CommandSystemHarness;
320
+
321
+ beforeEach(() => {
322
+ harness = new CommandSystemHarness();
323
+ harness.registerCoreCommands();
324
+ });
325
+
326
+ it("should exit immediately with /exit", async () => {
327
+ harness.clearEvents();
328
+ const result = await harness.execute("/exit");
329
+
330
+ expect(result.kind).toBe("success");
331
+ if (result.kind === "success") {
332
+ expect(result.data).toEqual({ exit: true });
333
+ }
334
+
335
+ const events = harness.getEmittedEvents();
336
+ expect(events).toContainEqual({
337
+ event: "app:exit",
338
+ data: { reason: "user-command" },
339
+ });
340
+ });
341
+
342
+ it("should support quit alias", async () => {
343
+ harness.clearEvents();
344
+ const result = await harness.execute("/quit");
345
+
346
+ expect(result.kind).toBe("success");
347
+ if (result.kind === "success") {
348
+ expect(result.data).toEqual({ exit: true });
349
+ }
350
+
351
+ const events = harness.getEmittedEvents();
352
+ expect(events).toContainEqual({
353
+ event: "app:exit",
354
+ data: { reason: "user-command" },
355
+ });
356
+ });
357
+
358
+ it("should support q alias", async () => {
359
+ harness.clearEvents();
360
+ const result = await harness.execute("/q");
361
+
362
+ expect(result.kind).toBe("success");
363
+ if (result.kind === "success") {
364
+ expect(result.data).toEqual({ exit: true });
365
+ }
366
+
367
+ const events = harness.getEmittedEvents();
368
+ expect(events).toContainEqual({
369
+ event: "app:exit",
370
+ data: { reason: "user-command" },
371
+ });
372
+ });
373
+ });
374
+
375
+ // =============================================================================
376
+ // E2E Test: Unknown Command
377
+ // =============================================================================
378
+
379
+ describe("E2E: unknown command handling", () => {
380
+ let harness: CommandSystemHarness;
381
+
382
+ beforeEach(() => {
383
+ harness = new CommandSystemHarness();
384
+ harness.registerCoreCommands();
385
+ });
386
+
387
+ it("should receive error with suggestions for /xyz", async () => {
388
+ const result = await harness.execute("/xyz");
389
+
390
+ expect(result.kind).toBe("error");
391
+ if (result.kind === "error") {
392
+ expect(result.code).toBe("COMMAND_NOT_FOUND");
393
+ expect(result.message).toContain("xyz");
394
+ // Message format: "Unknown command: /xyz"
395
+ expect(result.message).toContain("Unknown command");
396
+ }
397
+ });
398
+
399
+ it("should suggest similar commands for typos", async () => {
400
+ const result = await harness.execute("/hlep"); // typo for help
401
+
402
+ expect(result.kind).toBe("error");
403
+ if (result.kind === "error") {
404
+ expect(result.code).toBe("COMMAND_NOT_FOUND");
405
+ expect(result.suggestions).toBeDefined();
406
+ expect(result.suggestions).toContain("/help");
407
+ }
408
+ });
409
+
410
+ it("should suggest /exit(quit) for /exti typo", async () => {
411
+ const result = await harness.execute("/exti");
412
+
413
+ expect(result.kind).toBe("error");
414
+ if (result.kind === "error") {
415
+ expect(result.suggestions).toBeDefined();
416
+ expect(result.suggestions).toContain("/exit(quit)");
417
+ }
418
+ });
419
+
420
+ it("should suggest /clear for /cls (known alias)", async () => {
421
+ // Note: cls is an alias, so it should resolve correctly
422
+ const result = await harness.execute("/cls");
423
+ expect(result.kind).toBe("success"); // alias should work
424
+ });
425
+
426
+ it("should handle completely unrelated command", async () => {
427
+ const result = await harness.execute("/abracadabra");
428
+
429
+ expect(result.kind).toBe("error");
430
+ if (result.kind === "error") {
431
+ expect(result.code).toBe("COMMAND_NOT_FOUND");
432
+ // May or may not have suggestions depending on distance
433
+ }
434
+ });
435
+ });
436
+
437
+ // =============================================================================
438
+ // E2E Test: Autocomplete Flow
439
+ // =============================================================================
440
+
441
+ describe("E2E: autocomplete flow", () => {
442
+ let harness: CommandSystemHarness;
443
+
444
+ beforeEach(() => {
445
+ harness = new CommandSystemHarness();
446
+ harness.registerCoreCommands();
447
+
448
+ // Add more commands for better autocomplete testing
449
+ harness.registry.register(createMockCommand({ name: "history", category: "session" }));
450
+ harness.registry.register(createMockCommand({ name: "hello", category: "debug" }));
451
+ });
452
+
453
+ it("should show candidates when typing /hel", () => {
454
+ const state = harness.getAutocompleteCandidates("/hel");
455
+
456
+ expect(state.active).toBe(true);
457
+ expect(state.candidates.length).toBeGreaterThan(0);
458
+
459
+ const names = state.candidates.map((c) => c.command.name);
460
+ expect(names).toContain("help");
461
+ expect(names).toContain("hello");
462
+ });
463
+
464
+ it("should rank exact prefix match higher", () => {
465
+ const state = harness.getAutocompleteCandidates("/help");
466
+
467
+ expect(state.candidates.length).toBeGreaterThan(0);
468
+ const firstCandidate = state.candidates[0];
469
+ expect(firstCandidate).toBeDefined();
470
+ expect(firstCandidate?.command.name).toBe("help");
471
+ });
472
+
473
+ it("should Tab complete to selected candidate", () => {
474
+ let state = harness.getAutocompleteCandidates("/hel");
475
+ expect(state.active).toBe(true);
476
+
477
+ // Get selected candidate before tab
478
+ const selectedBefore = getSelectedCandidate(state);
479
+ expect(selectedBefore).toBeDefined();
480
+
481
+ // Tab complete - returns state unchanged for caller to read selected candidate
482
+ state = harness.tabComplete(state);
483
+
484
+ // State remains active - caller uses selected candidate then dispatches CANCEL
485
+ expect(state.active).toBe(true);
486
+ expect(getSelectedCandidate(state)).toBe(selectedBefore);
487
+ });
488
+
489
+ it("should navigate candidates with SELECT_NEXT/SELECT_PREV", () => {
490
+ let state = harness.getAutocompleteCandidates("/h");
491
+ const initialIndex = state.selectedIndex;
492
+
493
+ state = autocompleteReducer(state, { type: "SELECT_NEXT" });
494
+ expect(state.selectedIndex).toBe((initialIndex + 1) % state.candidates.length);
495
+
496
+ state = autocompleteReducer(state, { type: "SELECT_PREV" });
497
+ expect(state.selectedIndex).toBe(initialIndex);
498
+ });
499
+
500
+ it("should cancel autocomplete", () => {
501
+ let state = harness.getAutocompleteCandidates("/hel");
502
+ expect(state.active).toBe(true);
503
+
504
+ state = autocompleteReducer(state, { type: "CANCEL" });
505
+ expect(state.active).toBe(false);
506
+ expect(state.candidates).toHaveLength(0);
507
+ });
508
+
509
+ it("should not show autocomplete for non-slash input", () => {
510
+ // Empty query produces inactive state
511
+ const state = harness.getAutocompleteCandidates("");
512
+ expect(shouldShowAutocomplete(state)).toBe(false);
513
+ });
514
+
515
+ it("should show autocomplete for slash input", () => {
516
+ const state = harness.getAutocompleteCandidates("/h");
517
+ expect(shouldShowAutocomplete(state)).toBe(true);
518
+ });
519
+
520
+ it("should filter by query correctly", () => {
521
+ const state = harness.getAutocompleteCandidates("/ex");
522
+
523
+ expect(state.candidates.length).toBeGreaterThan(0);
524
+ const names = state.candidates.map((c) => c.command.name);
525
+ expect(names).toContain("exit(quit)");
526
+ });
527
+ });
528
+
529
+ // =============================================================================
530
+ // E2E Test: Full Lifecycle
531
+ // =============================================================================
532
+
533
+ describe("E2E: full command lifecycle", () => {
534
+ let harness: CommandSystemHarness;
535
+
536
+ beforeEach(() => {
537
+ harness = new CommandSystemHarness();
538
+ harness.registerCoreCommands();
539
+ });
540
+
541
+ it("should handle complete flow: type → autocomplete → execute", async () => {
542
+ // Step 1: User starts typing
543
+ const input = "/cle";
544
+
545
+ // Step 2: Autocomplete activates
546
+ const autocompleteState = harness.getAutocompleteCandidates(input);
547
+ expect(autocompleteState.active).toBe(true);
548
+ expect(autocompleteState.candidates.length).toBeGreaterThan(0);
549
+
550
+ // Find clear command in candidates
551
+ const clearCandidate = autocompleteState.candidates.find((c) => c.command.name === "clear");
552
+ expect(clearCandidate).toBeDefined();
553
+
554
+ // Step 3: User completes and executes
555
+ const result = await harness.execute("/clear");
556
+
557
+ // Step 4: Verify result
558
+ expect(result.kind).toBe("success");
559
+ if (result.kind === "success") {
560
+ expect(result.clearScreen).toBe(true);
561
+ }
562
+ });
563
+
564
+ it("should handle error recovery flow", async () => {
565
+ // Step 1: User types invalid command
566
+ const badResult = await harness.execute("/cleear"); // typo
567
+
568
+ // Step 2: Get error with suggestion
569
+ expect(badResult.kind).toBe("error");
570
+ if (badResult.kind === "error") {
571
+ expect(badResult.suggestions).toContain("/clear");
572
+ }
573
+
574
+ // Step 3: User corrects and retries
575
+ const goodResult = await harness.execute("/clear");
576
+ expect(goodResult.kind).toBe("success");
577
+ });
578
+
579
+ it("should maintain state across commands", async () => {
580
+ // Execute multiple commands
581
+ const result1 = await harness.execute("/help");
582
+ expect(result1.kind).toBe("success");
583
+
584
+ const result2 = await harness.execute("/clear");
585
+ expect(result2.kind).toBe("success");
586
+
587
+ const result3 = await harness.execute("/exit");
588
+ expect(result3.kind).toBe("success");
589
+
590
+ // All should succeed independently
591
+ const events = harness.getEmittedEvents();
592
+ expect(events).toContainEqual(expect.objectContaining({ event: "app:exit" }));
593
+ });
594
+ });
595
+
596
+ // =============================================================================
597
+ // E2E Test: Parse → Execute Integration
598
+ // =============================================================================
599
+
600
+ describe("E2E: parser → executor integration", () => {
601
+ let harness: CommandSystemHarness;
602
+
603
+ beforeEach(() => {
604
+ harness = new CommandSystemHarness();
605
+ harness.registerCoreCommands();
606
+
607
+ // Add command with complex args
608
+ harness.registry.register(
609
+ createMockCommand({
610
+ name: "config",
611
+ category: "config",
612
+ positionalArgs: [
613
+ { name: "key", type: "string", description: "Config key", required: true },
614
+ ],
615
+ namedArgs: [
616
+ {
617
+ name: "value",
618
+ shorthand: "v",
619
+ type: "string",
620
+ description: "Config value",
621
+ required: false,
622
+ },
623
+ {
624
+ name: "global",
625
+ shorthand: "g",
626
+ type: "boolean",
627
+ description: "Global scope",
628
+ required: false,
629
+ default: false,
630
+ },
631
+ ],
632
+ execute: async (ctx: CommandContext) => {
633
+ const key = ctx.parsedArgs.positional[0];
634
+ const value = ctx.parsedArgs.named.value;
635
+ const global = ctx.parsedArgs.named.global;
636
+
637
+ return {
638
+ kind: "success",
639
+ message: `Config: ${key}=${value} (global=${global})`,
640
+ data: { key, value, global },
641
+ };
642
+ },
643
+ })
644
+ );
645
+ });
646
+
647
+ it("should parse and execute with positional and named args", async () => {
648
+ const result = await harness.execute("/config theme --value dark --global");
649
+
650
+ expect(result.kind).toBe("success");
651
+ if (result.kind === "success") {
652
+ expect(result.data).toEqual({
653
+ key: "theme",
654
+ value: "dark",
655
+ global: true,
656
+ });
657
+ }
658
+ });
659
+
660
+ it("should parse short flags", async () => {
661
+ const result = await harness.execute("/config theme -v light -g");
662
+
663
+ expect(result.kind).toBe("success");
664
+ if (result.kind === "success") {
665
+ expect(result.data).toEqual({
666
+ key: "theme",
667
+ value: "light",
668
+ global: true,
669
+ });
670
+ }
671
+ });
672
+
673
+ it("should parse quoted values", async () => {
674
+ const result = await harness.execute('/config message --value "Hello World"');
675
+
676
+ expect(result.kind).toBe("success");
677
+ if (result.kind === "success") {
678
+ expect(result.data).toMatchObject({
679
+ key: "message",
680
+ value: "Hello World",
681
+ });
682
+ }
683
+ });
684
+
685
+ it("should handle missing required argument", async () => {
686
+ const result = await harness.execute("/config"); // missing 'key'
687
+
688
+ expect(result.kind).toBe("error");
689
+ if (result.kind === "error") {
690
+ expect(result.code).toBe("MISSING_ARGUMENT");
691
+ }
692
+ });
693
+ });
694
+
695
+ // =============================================================================
696
+ // E2E Test: Fuzzy Score Integration
697
+ // =============================================================================
698
+
699
+ describe("E2E: fuzzy scoring in autocomplete", () => {
700
+ it("should rank exact match highest", () => {
701
+ const exactScore = fuzzyScore("help", "help");
702
+ const prefixScore = fuzzyScore("hel", "help");
703
+ const fuzzyMatch = fuzzyScore("hp", "help");
704
+
705
+ expect(exactScore).not.toBeNull();
706
+ expect(prefixScore).not.toBeNull();
707
+ expect(fuzzyMatch).not.toBeNull();
708
+
709
+ // Extract scores after null checks (safe to use optional chain in expect)
710
+ if (exactScore && prefixScore && fuzzyMatch) {
711
+ expect(exactScore.score).toBeGreaterThan(prefixScore.score);
712
+ expect(prefixScore.score).toBeGreaterThan(fuzzyMatch.score);
713
+ }
714
+ });
715
+
716
+ it("should return null for no match", () => {
717
+ const result = fuzzyScore("xyz", "help");
718
+ expect(result).toBeNull();
719
+ });
720
+
721
+ it("should handle word boundaries", () => {
722
+ const result = fuzzyScore("gc", "git-commit");
723
+
724
+ expect(result).not.toBeNull();
725
+ // g matches start, c matches after hyphen
726
+ expect(result?.ranges.length).toBe(2);
727
+ });
728
+ });