@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,215 @@
1
+ /**
2
+ * StreamingText Component Tests
3
+ *
4
+ * Tests for the StreamingText component which displays text with an animated
5
+ * blinking cursor while streaming.
6
+ *
7
+ * @module tui/components/Messages/__tests__/StreamingText.test
8
+ */
9
+
10
+ import { render } from "ink-testing-library";
11
+ import { act } from "react";
12
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
13
+ import { AnimationProvider } from "../../../context/AnimationContext.js";
14
+ import { StreamingText } from "../StreamingText.js";
15
+
16
+ // =============================================================================
17
+ // Test Setup
18
+ // =============================================================================
19
+
20
+ beforeEach(() => {
21
+ vi.useFakeTimers();
22
+ });
23
+
24
+ afterEach(() => {
25
+ vi.useRealTimers();
26
+ });
27
+
28
+ // =============================================================================
29
+ // Tests
30
+ // =============================================================================
31
+
32
+ describe("StreamingText", () => {
33
+ describe("rendering", () => {
34
+ it("renders text content", () => {
35
+ const { lastFrame } = render(<StreamingText content="Hello world" isStreaming={false} />);
36
+
37
+ expect(lastFrame()).toContain("Hello world");
38
+ });
39
+
40
+ it("renders empty content", () => {
41
+ const { lastFrame } = render(<StreamingText content="" isStreaming={false} />);
42
+
43
+ // Should render without cursor when not streaming
44
+ expect(lastFrame()).toBe("");
45
+ });
46
+ });
47
+
48
+ describe("cursor behavior", () => {
49
+ it("shows cursor when streaming", () => {
50
+ // Disable typewriter effect for immediate content display test
51
+ const { lastFrame } = render(
52
+ <StreamingText content="Typing..." isStreaming={true} typewriterEffect={false} />
53
+ );
54
+
55
+ expect(lastFrame()).toContain("Typing...");
56
+ expect(lastFrame()).toContain("▊"); // Default cursor
57
+ });
58
+
59
+ it("hides cursor when not streaming", () => {
60
+ const { lastFrame } = render(<StreamingText content="Complete" isStreaming={false} />);
61
+
62
+ expect(lastFrame()).toContain("Complete");
63
+ expect(lastFrame()).not.toContain("▊");
64
+ });
65
+
66
+ it("uses custom cursor character", () => {
67
+ // Disable typewriter effect for immediate content display test
68
+ const { lastFrame } = render(
69
+ <StreamingText
70
+ content="Custom"
71
+ isStreaming={true}
72
+ cursorChar="_"
73
+ typewriterEffect={false}
74
+ />
75
+ );
76
+
77
+ expect(lastFrame()).toContain("Custom");
78
+ expect(lastFrame()).toContain("_");
79
+ expect(lastFrame()).not.toContain("▊");
80
+ });
81
+
82
+ it("blinks cursor when animation frames advance", async () => {
83
+ const { lastFrame } = render(
84
+ <AnimationProvider tickInterval={100}>
85
+ <StreamingText
86
+ content="Blinking"
87
+ isStreaming={true}
88
+ cursorBlink={true}
89
+ typewriterEffect={false}
90
+ />
91
+ </AnimationProvider>
92
+ );
93
+
94
+ // Initially cursor is visible
95
+ expect(lastFrame()).toContain("▊");
96
+
97
+ // After 4 ticks (frame=4), cursor should be hidden
98
+ await act(async () => {
99
+ vi.advanceTimersByTime(400);
100
+ });
101
+ expect(lastFrame()).not.toContain("▊");
102
+
103
+ // After 4 more ticks (frame=8), cursor should be visible again
104
+ await act(async () => {
105
+ vi.advanceTimersByTime(400);
106
+ });
107
+ expect(lastFrame()).toContain("▊");
108
+ });
109
+
110
+ it("does not blink cursor when cursorBlink is false", () => {
111
+ const { lastFrame } = render(
112
+ <AnimationProvider tickInterval={100}>
113
+ <StreamingText
114
+ content="No blink"
115
+ isStreaming={true}
116
+ cursorBlink={false}
117
+ typewriterEffect={false}
118
+ />
119
+ </AnimationProvider>
120
+ );
121
+
122
+ // Cursor is visible
123
+ expect(lastFrame()).toContain("▊");
124
+
125
+ // After 500ms, cursor should still be visible (no blinking)
126
+ act(() => {
127
+ vi.advanceTimersByTime(500);
128
+ });
129
+ expect(lastFrame()).toContain("▊");
130
+
131
+ // After another 500ms, still visible
132
+ act(() => {
133
+ vi.advanceTimersByTime(500);
134
+ });
135
+ expect(lastFrame()).toContain("▊");
136
+ });
137
+ });
138
+
139
+ describe("onComplete callback", () => {
140
+ it("calls onComplete when streaming changes from true to false", () => {
141
+ const onComplete = vi.fn();
142
+
143
+ const { rerender } = render(
144
+ <StreamingText content="Streaming..." isStreaming={true} onComplete={onComplete} />
145
+ );
146
+
147
+ // Not called while streaming
148
+ expect(onComplete).not.toHaveBeenCalled();
149
+
150
+ // Update to not streaming
151
+ act(() => {
152
+ rerender(<StreamingText content="Done!" isStreaming={false} onComplete={onComplete} />);
153
+ });
154
+
155
+ // Now onComplete should be called
156
+ expect(onComplete).toHaveBeenCalledTimes(1);
157
+ });
158
+
159
+ it("does not call onComplete when initially not streaming", () => {
160
+ const onComplete = vi.fn();
161
+
162
+ render(<StreamingText content="Not streaming" isStreaming={false} onComplete={onComplete} />);
163
+
164
+ // Should not be called on initial render when not streaming
165
+ expect(onComplete).not.toHaveBeenCalled();
166
+ });
167
+
168
+ it("does not call onComplete when streaming remains true", () => {
169
+ const onComplete = vi.fn();
170
+
171
+ const { rerender } = render(
172
+ <StreamingText content="Still streaming..." isStreaming={true} onComplete={onComplete} />
173
+ );
174
+
175
+ // Update content but keep streaming
176
+ act(() => {
177
+ rerender(
178
+ <StreamingText content="More content..." isStreaming={true} onComplete={onComplete} />
179
+ );
180
+ });
181
+
182
+ expect(onComplete).not.toHaveBeenCalled();
183
+ });
184
+
185
+ it("works without onComplete callback", () => {
186
+ const { rerender } = render(<StreamingText content="Streaming..." isStreaming={true} />);
187
+
188
+ // Should not throw when onComplete is undefined
189
+ expect(() => {
190
+ act(() => {
191
+ rerender(<StreamingText content="Done!" isStreaming={false} />);
192
+ });
193
+ }).not.toThrow();
194
+ });
195
+ });
196
+
197
+ describe("animation cleanup", () => {
198
+ it("cleans up animation interval on unmount", () => {
199
+ const clearIntervalSpy = vi.spyOn(global, "clearInterval");
200
+
201
+ const { unmount } = render(
202
+ <AnimationProvider tickInterval={100}>
203
+ <StreamingText content="Test" isStreaming={true} cursorBlink={true} />
204
+ </AnimationProvider>
205
+ );
206
+
207
+ act(() => {
208
+ unmount();
209
+ });
210
+
211
+ expect(clearIntervalSpy).toHaveBeenCalled();
212
+ clearIntervalSpy.mockRestore();
213
+ });
214
+ });
215
+ });
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Message Components
3
+ *
4
+ * Components for displaying messages and conversation history in the Vellum TUI.
5
+ */
6
+
7
+ export { CodeBlock, type CodeBlockProps } from "./CodeBlock.js";
8
+ export { DiffView, type DiffViewProps } from "./DiffView.js";
9
+ export { MarkdownBlock, type MarkdownBlockProps, MarkdownBlockSync } from "./MarkdownBlock.js";
10
+ export { MarkdownRenderer, type MarkdownRendererProps } from "./MarkdownRenderer.js";
11
+ export { MessageBubble, type MessageBubbleProps } from "./MessageBubble.js";
12
+ export { MessageList, type MessageListProps } from "./MessageList.js";
13
+ export { StreamingText, type StreamingTextProps } from "./StreamingText.js";
14
+ export {
15
+ CompactThinkingIndicator,
16
+ type CompactThinkingIndicatorProps,
17
+ ThinkingBlock,
18
+ type ThinkingBlockProps,
19
+ } from "./ThinkingBlock.js";
20
+ export {
21
+ SHELL_TOOL_MAX_LINES,
22
+ TOOL_RESULT_MAX_LINES,
23
+ ToolResultPreview,
24
+ type ToolResultPreviewProps,
25
+ } from "./ToolResultPreview.js";
@@ -0,0 +1,177 @@
1
+ /**
2
+ * ModeIndicator Component (T043, T044)
3
+ *
4
+ * TUI component for displaying the current coding mode with visual styling.
5
+ * Shows mode icon, name, and optional spec phase progress.
6
+ *
7
+ * @module tui/components/ModeIndicator
8
+ */
9
+
10
+ import type { CodingMode, SpecPhase } from "@vellum/core";
11
+ import { SPEC_PHASE_CONFIG, SPEC_PHASES } from "@vellum/core";
12
+ import { getIcons } from "@vellum/shared";
13
+ import { Box, Text } from "ink";
14
+ import type React from "react";
15
+ import { useMemo } from "react";
16
+ import { useTheme } from "../theme/index.js";
17
+
18
+ // =============================================================================
19
+ // Types
20
+ // =============================================================================
21
+
22
+ /**
23
+ * Props for the ModeIndicator component.
24
+ */
25
+ export interface ModeIndicatorProps {
26
+ /** The current coding mode */
27
+ readonly mode: CodingMode;
28
+ /** Current spec phase (only for spec mode, 1-6) */
29
+ readonly specPhase?: number;
30
+ /** Whether to show in compact mode (icon only) */
31
+ readonly compact?: boolean;
32
+ }
33
+
34
+ // =============================================================================
35
+ // Constants
36
+ // =============================================================================
37
+
38
+ /**
39
+ * Get mode icons for each coding mode.
40
+ * Uses centralized icon system with auto-detection.
41
+ */
42
+ function getModeIcons(): Record<CodingMode, string> {
43
+ const icons = getIcons();
44
+ return {
45
+ vibe: icons.vibe,
46
+ plan: icons.plan,
47
+ spec: icons.spec,
48
+ };
49
+ }
50
+
51
+ /**
52
+ * Mode display names.
53
+ */
54
+ const MODE_NAMES: Record<CodingMode, string> = {
55
+ vibe: "vibe",
56
+ plan: "plan",
57
+ spec: "spec",
58
+ } as const;
59
+
60
+ /**
61
+ * Mode colors mapped to theme semantic colors.
62
+ * - vibe: green (success) - fast, autonomous
63
+ * - plan: blue (info) - structured planning
64
+ * - spec: purple (primary) - detailed specification
65
+ */
66
+ const MODE_COLOR_KEYS: Record<CodingMode, "success" | "info" | "primary"> = {
67
+ vibe: "success",
68
+ plan: "info",
69
+ spec: "primary",
70
+ } as const;
71
+
72
+ // =============================================================================
73
+ // Helper Functions
74
+ // =============================================================================
75
+
76
+ /**
77
+ * Get the color for a coding mode from the theme.
78
+ */
79
+ function getModeColor(mode: CodingMode, theme: ReturnType<typeof useTheme>["theme"]): string {
80
+ const colorKey = MODE_COLOR_KEYS[mode];
81
+ return theme.colors[colorKey];
82
+ }
83
+
84
+ /**
85
+ * Get the phase name from phase number.
86
+ */
87
+ function getPhaseName(phaseNumber: number): string {
88
+ const phase = SPEC_PHASES[phaseNumber - 1] as SpecPhase | undefined;
89
+ if (!phase) {
90
+ return `Phase ${phaseNumber}`;
91
+ }
92
+ return SPEC_PHASE_CONFIG[phase].name;
93
+ }
94
+
95
+ // =============================================================================
96
+ // ModeIndicator Component
97
+ // =============================================================================
98
+
99
+ /**
100
+ * ModeIndicator - Displays the current coding mode with visual styling.
101
+ *
102
+ * Features:
103
+ * - Mode icon (using centralized icon system)
104
+ * - Color-coded mode name (green, blue, purple)
105
+ * - Spec phase progress indicator when in spec mode
106
+ * - Compact mode for space-constrained layouts
107
+ *
108
+ * @example
109
+ * ```tsx
110
+ * // Basic usage
111
+ * <ModeIndicator mode="vibe" />
112
+ *
113
+ * // With spec phase
114
+ * <ModeIndicator mode="spec" specPhase={3} />
115
+ *
116
+ * // Compact mode
117
+ * <ModeIndicator mode="plan" compact />
118
+ * ```
119
+ */
120
+ export function ModeIndicator({
121
+ mode,
122
+ specPhase,
123
+ compact = false,
124
+ }: ModeIndicatorProps): React.ReactElement {
125
+ const { theme } = useTheme();
126
+
127
+ // Get mode display properties
128
+ const modeIcons = getModeIcons();
129
+ const icon = modeIcons[mode];
130
+ const name = MODE_NAMES[mode];
131
+ const color = getModeColor(mode, theme);
132
+
133
+ // Build phase display for spec mode
134
+ const phaseDisplay = useMemo(() => {
135
+ if (mode !== "spec" || specPhase === undefined) {
136
+ return null;
137
+ }
138
+
139
+ const totalPhases = SPEC_PHASES.length;
140
+ const validPhase = Math.max(1, Math.min(specPhase, totalPhases));
141
+
142
+ if (compact) {
143
+ return ` (${validPhase}/${totalPhases})`;
144
+ }
145
+
146
+ const phaseName = getPhaseName(validPhase);
147
+ return ` (${validPhase}/${totalPhases}: ${phaseName})`;
148
+ }, [mode, specPhase, compact]);
149
+
150
+ // Compact mode: icon only with optional phase
151
+ if (compact) {
152
+ return (
153
+ <Box>
154
+ <Text color={color}>
155
+ {icon}
156
+ {phaseDisplay}
157
+ </Text>
158
+ </Box>
159
+ );
160
+ }
161
+
162
+ // Full mode: icon + name + phase
163
+ return (
164
+ <Box>
165
+ <Text color={color}>
166
+ {icon} {name}
167
+ {phaseDisplay}
168
+ </Text>
169
+ </Box>
170
+ );
171
+ }
172
+
173
+ // =============================================================================
174
+ // Exports
175
+ // =============================================================================
176
+
177
+ export type { CodingMode };
@@ -0,0 +1,216 @@
1
+ /**
2
+ * ModeSelector Component (T045)
3
+ *
4
+ * TUI component for selecting coding modes with keyboard navigation.
5
+ * Renders three selectable options (vibe, plan, spec) with visual feedback.
6
+ *
7
+ * @module tui/components/ModeSelector
8
+ */
9
+
10
+ import type { CodingMode } from "@vellum/core";
11
+ import { BUILTIN_CODING_MODES, CODING_MODES } from "@vellum/core";
12
+ import { getIcons } from "@vellum/shared";
13
+ import { Box, Text, useInput } from "ink";
14
+ import type React from "react";
15
+ import { useCallback, useState } from "react";
16
+ import { useTUITranslation } from "../i18n/index.js";
17
+ import { useTheme } from "../theme/index.js";
18
+
19
+ // =============================================================================
20
+ // Types
21
+ // =============================================================================
22
+
23
+ /**
24
+ * Props for the ModeSelector component.
25
+ */
26
+ export interface ModeSelectorProps {
27
+ /** The currently active mode */
28
+ readonly currentMode: CodingMode;
29
+ /** Callback when a mode is selected */
30
+ readonly onSelect: (mode: CodingMode) => void;
31
+ /** Whether the selector is focused/active */
32
+ readonly isActive?: boolean;
33
+ /** Whether to show mode descriptions */
34
+ readonly showDescriptions?: boolean;
35
+ }
36
+
37
+ // =============================================================================
38
+ // Constants
39
+ // =============================================================================
40
+
41
+ /**
42
+ * Get mode icons for visual identification.
43
+ * Uses centralized icon system with auto-detection.
44
+ */
45
+ function getModeIcons(): Record<CodingMode, string> {
46
+ const icons = getIcons();
47
+ return {
48
+ vibe: icons.vibe,
49
+ plan: icons.plan,
50
+ spec: icons.spec,
51
+ };
52
+ }
53
+
54
+ /**
55
+ * Keyboard shortcuts for quick mode selection.
56
+ */
57
+ const MODE_SHORTCUTS: Record<CodingMode, string> = {
58
+ vibe: "1",
59
+ plan: "2",
60
+ spec: "3",
61
+ } as const;
62
+
63
+ /**
64
+ * Mode colors mapped to theme semantic colors.
65
+ */
66
+ const MODE_COLOR_KEYS: Record<CodingMode, "success" | "info" | "primary"> = {
67
+ vibe: "success",
68
+ plan: "info",
69
+ spec: "primary",
70
+ } as const;
71
+
72
+ // =============================================================================
73
+ // ModeSelector Component
74
+ // =============================================================================
75
+
76
+ /**
77
+ * ModeSelector - Interactive component for selecting coding modes.
78
+ *
79
+ * Features:
80
+ * - Arrow key navigation (up/down or j/k)
81
+ * - Number shortcuts (1, 2, 3)
82
+ * - Enter to confirm selection
83
+ * - Visual indication of current mode
84
+ * - Highlight of focused option
85
+ *
86
+ * @example
87
+ * ```tsx
88
+ * function MyComponent() {
89
+ * const [mode, setMode] = useState<CodingMode>('vibe');
90
+ *
91
+ * return (
92
+ * <ModeSelector
93
+ * currentMode={mode}
94
+ * onSelect={setMode}
95
+ * isActive
96
+ * />
97
+ * );
98
+ * }
99
+ * ```
100
+ */
101
+ export function ModeSelector({
102
+ currentMode,
103
+ onSelect,
104
+ isActive = true,
105
+ showDescriptions = true,
106
+ }: ModeSelectorProps): React.ReactElement {
107
+ const { theme } = useTheme();
108
+ const { t } = useTUITranslation();
109
+ const modes = CODING_MODES;
110
+
111
+ // Track focused index for keyboard navigation
112
+ const [focusedIndex, setFocusedIndex] = useState(() => modes.indexOf(currentMode));
113
+
114
+ // Handle keyboard input
115
+ useInput(
116
+ useCallback(
117
+ (input: string, key) => {
118
+ if (!isActive) return;
119
+
120
+ // Arrow navigation
121
+ if (key.upArrow || input === "k") {
122
+ setFocusedIndex((prev) => (prev > 0 ? prev - 1 : modes.length - 1));
123
+ return;
124
+ }
125
+
126
+ if (key.downArrow || input === "j") {
127
+ setFocusedIndex((prev) => (prev < modes.length - 1 ? prev + 1 : 0));
128
+ return;
129
+ }
130
+
131
+ // Confirm selection
132
+ if (key.return) {
133
+ const selectedMode = modes[focusedIndex];
134
+ if (selectedMode) {
135
+ onSelect(selectedMode);
136
+ }
137
+ return;
138
+ }
139
+
140
+ // Number shortcuts
141
+ const modeIndex = parseInt(input, 10) - 1;
142
+ if (modeIndex >= 0 && modeIndex < modes.length) {
143
+ const selectedMode = modes[modeIndex];
144
+ if (selectedMode) {
145
+ setFocusedIndex(modeIndex);
146
+ onSelect(selectedMode);
147
+ }
148
+ }
149
+ },
150
+ [isActive, focusedIndex, modes, onSelect]
151
+ ),
152
+ { isActive }
153
+ );
154
+
155
+ return (
156
+ <Box flexDirection="column" paddingX={1}>
157
+ <Box marginBottom={1}>
158
+ <Text bold>{t("modeSelector.title")}</Text>
159
+ </Box>
160
+
161
+ {modes.map((mode, index) => {
162
+ const isFocused = index === focusedIndex && isActive;
163
+ const isCurrent = mode === currentMode;
164
+ const modeIcons = getModeIcons();
165
+ const icon = modeIcons[mode];
166
+ const shortcut = MODE_SHORTCUTS[mode];
167
+ const colorKey = MODE_COLOR_KEYS[mode];
168
+ const color = theme.colors[colorKey];
169
+ const config = BUILTIN_CODING_MODES[mode];
170
+
171
+ return (
172
+ <Box key={mode} flexDirection="column">
173
+ <Box>
174
+ {/* Focus indicator */}
175
+ <Text color={isFocused ? color : undefined}>{isFocused ? "❯ " : " "}</Text>
176
+
177
+ {/* Shortcut key */}
178
+ <Text dimColor>[{shortcut}]</Text>
179
+ <Text> </Text>
180
+
181
+ {/* Mode icon and name */}
182
+ <Text color={color} bold={isCurrent}>
183
+ {icon} {mode}
184
+ </Text>
185
+
186
+ {/* Current indicator */}
187
+ {isCurrent && (
188
+ <Text color={theme.semantic.text.muted}> {t("modeSelector.current")}</Text>
189
+ )}
190
+ </Box>
191
+
192
+ {/* Description (when enabled) */}
193
+ {showDescriptions && (
194
+ <Box marginLeft={6}>
195
+ <Text dimColor wrap="truncate-end">
196
+ {config.description}
197
+ </Text>
198
+ </Box>
199
+ )}
200
+ </Box>
201
+ );
202
+ })}
203
+
204
+ {/* Help text */}
205
+ <Box marginTop={1}>
206
+ <Text dimColor>{t("modeSelector.keybindings")}</Text>
207
+ </Box>
208
+ </Box>
209
+ );
210
+ }
211
+
212
+ // =============================================================================
213
+ // Exports
214
+ // =============================================================================
215
+
216
+ export type { CodingMode };