@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,603 @@
1
+ /**
2
+ * Autocomplete Component (T011)
3
+ *
4
+ * A dropdown component for command/option suggestions with keyboard navigation.
5
+ * Filters options based on prefix match and displays highlighted results.
6
+ *
7
+ * @module tui/components/Input/Autocomplete
8
+ */
9
+
10
+ import { Box, Text, useInput } from "ink";
11
+ import { memo, startTransition, useCallback, useEffect, useMemo, useRef, useState } from "react";
12
+ import {
13
+ type FuzzyResult,
14
+ fuzzySearch,
15
+ getHighlightSegments,
16
+ type HighlightRange,
17
+ } from "../../services/fuzzy-search.js";
18
+ import { useTheme } from "../../theme/index.js";
19
+
20
+ // =============================================================================
21
+ // Types
22
+ // =============================================================================
23
+
24
+ /**
25
+ * Structured option for autocomplete with optional metadata.
26
+ */
27
+ export interface AutocompleteOption {
28
+ /** Option name/value */
29
+ readonly name: string;
30
+ /** Optional description */
31
+ readonly description?: string;
32
+ /** Optional category for grouping */
33
+ readonly category?: string;
34
+ /** Optional aliases for matching (e.g., quit -> exit) */
35
+ readonly aliases?: readonly string[];
36
+ }
37
+
38
+ /**
39
+ * Props for the Autocomplete component.
40
+ */
41
+ export interface AutocompleteProps {
42
+ /** Current input value to filter options against */
43
+ readonly input: string;
44
+ /** All available options to filter from (string[] for backward compat, or structured) */
45
+ readonly options: readonly string[] | readonly AutocompleteOption[];
46
+ /** Callback when an option is selected (Tab or Enter) */
47
+ readonly onSelect: (value: string) => void;
48
+ /** Callback when autocomplete is cancelled (Escape) */
49
+ readonly onCancel: () => void;
50
+ /** Callback when selection index changes (for parent to track selection state) */
51
+ readonly onSelectionChange?: (index: number, hasOptions: boolean) => void;
52
+ /** Whether the autocomplete dropdown is visible (default: true) */
53
+ readonly visible?: boolean;
54
+ /**
55
+ * Whether the autocomplete should capture keyboard input (default: same as `visible`).
56
+ *
57
+ * This allows rendering suggestions while the input cursor is no longer in the
58
+ * command token, without hijacking Enter/history behavior.
59
+ */
60
+ readonly active?: boolean;
61
+ /** Maximum number of items to show (default: 5) */
62
+ readonly maxVisible?: number;
63
+ /** Enable category grouping (default: false) */
64
+ readonly grouped?: boolean;
65
+ /** Category display order (optional, unspecified categories go last) */
66
+ readonly categoryOrder?: readonly string[];
67
+ /** Category labels for i18n (category key -> display label) */
68
+ readonly categoryLabels?: Record<string, string>;
69
+ }
70
+
71
+ // =============================================================================
72
+ // Helper Functions
73
+ // =============================================================================
74
+
75
+ /**
76
+ * Normalize options to structured format.
77
+ * Handles both string[] and AutocompleteOption[] inputs.
78
+ */
79
+ function normalizeOptions(
80
+ options: readonly string[] | readonly AutocompleteOption[]
81
+ ): AutocompleteOption[] {
82
+ if (options.length === 0) return [];
83
+
84
+ // Check if first item is a string
85
+ if (typeof options[0] === "string") {
86
+ return (options as readonly string[]).map((opt) => ({ name: opt }));
87
+ }
88
+
89
+ return [...(options as readonly AutocompleteOption[])];
90
+ }
91
+
92
+ /**
93
+ * Fuzzy filtered option with highlight information.
94
+ */
95
+ interface FilteredOption {
96
+ readonly option: AutocompleteOption;
97
+ readonly highlights: readonly HighlightRange[];
98
+ readonly score: number;
99
+ }
100
+
101
+ /**
102
+ * Filter options using fuzzy matching.
103
+ *
104
+ * @param options - All available options (normalized)
105
+ * @param input - Current input to match against
106
+ * @returns Filtered array of matching options with highlights, sorted by score
107
+ */
108
+ function filterStructuredOptions(
109
+ options: readonly AutocompleteOption[],
110
+ input: string
111
+ ): FilteredOption[] {
112
+ if (!input) {
113
+ // No input - return all options sorted alphabetically
114
+ return [...options]
115
+ .sort((a, b) => a.name.localeCompare(b.name))
116
+ .map((opt) => ({ option: opt, highlights: [], score: 0 }));
117
+ }
118
+
119
+ // Use fuzzy search on the name field
120
+ const results = fuzzySearch(options, input, "name", {
121
+ threshold: -10000, // Allow weak matches for better UX
122
+ });
123
+
124
+ // Also check aliases with separate fuzzy search
125
+ const optionsWithAliases = options.filter((opt) => opt.aliases && opt.aliases.length > 0);
126
+ const aliasMatches = new Map<AutocompleteOption, FuzzyResult<AutocompleteOption>>();
127
+
128
+ for (const opt of optionsWithAliases) {
129
+ if (!opt.aliases) continue;
130
+ for (const alias of opt.aliases) {
131
+ const aliasResult = fuzzySearch([{ ...opt, name: alias }], input, "name");
132
+ if (aliasResult.length > 0 && aliasResult[0]) {
133
+ const existing = aliasMatches.get(opt);
134
+ if (!existing || aliasResult[0].score > existing.score) {
135
+ aliasMatches.set(opt, { ...aliasResult[0], item: opt });
136
+ }
137
+ }
138
+ }
139
+ }
140
+
141
+ // Merge results: prefer name match, but include alias-only matches
142
+ const resultMap = new Map<AutocompleteOption, FilteredOption>();
143
+
144
+ for (const result of results) {
145
+ resultMap.set(result.item, {
146
+ option: result.item,
147
+ highlights: result.highlights,
148
+ score: result.score,
149
+ });
150
+ }
151
+
152
+ // Add alias matches that aren't already in results (or have better score)
153
+ for (const [opt, aliasResult] of aliasMatches) {
154
+ const existing = resultMap.get(opt);
155
+ if (!existing) {
156
+ // Not matched by name, add with empty highlights (matched via alias)
157
+ resultMap.set(opt, {
158
+ option: opt,
159
+ highlights: [], // Don't highlight name since alias matched
160
+ score: aliasResult.score,
161
+ });
162
+ }
163
+ }
164
+
165
+ // Sort by score (higher is better)
166
+ return Array.from(resultMap.values()).sort((a, b) => b.score - a.score);
167
+ }
168
+
169
+ /**
170
+ * Group options by category.
171
+ *
172
+ * @param options - Filtered options to group (already sorted by score)
173
+ * @param categoryOrder - Preferred order of categories
174
+ * @returns Map of category -> options, ordered by categoryOrder
175
+ */
176
+ function groupByCategory(
177
+ options: readonly FilteredOption[],
178
+ categoryOrder: readonly string[] = []
179
+ ): Map<string, FilteredOption[]> {
180
+ const groups = new Map<string, FilteredOption[]>();
181
+ const uncategorized: FilteredOption[] = [];
182
+
183
+ // First pass: collect all options by category
184
+ for (const filteredOpt of options) {
185
+ const category = filteredOpt.option.category || "";
186
+ if (!category) {
187
+ uncategorized.push(filteredOpt);
188
+ } else {
189
+ const group = groups.get(category);
190
+ if (group) {
191
+ group.push(filteredOpt);
192
+ } else {
193
+ groups.set(category, [filteredOpt]);
194
+ }
195
+ }
196
+ }
197
+
198
+ // Build ordered result
199
+ const result = new Map<string, FilteredOption[]>();
200
+
201
+ // Add categories in specified order first
202
+ for (const cat of categoryOrder) {
203
+ const group = groups.get(cat);
204
+ if (group && group.length > 0) {
205
+ // Sort by score within category (already sorted, but re-sort for consistency)
206
+ group.sort((a, b) => b.score - a.score);
207
+ result.set(cat, group);
208
+ groups.delete(cat);
209
+ }
210
+ }
211
+
212
+ // Add remaining categories alphabetically
213
+ const remainingCategories = Array.from(groups.keys()).sort();
214
+ for (const cat of remainingCategories) {
215
+ const group = groups.get(cat);
216
+ if (group && group.length > 0) {
217
+ group.sort((a, b) => b.score - a.score);
218
+ result.set(cat, group);
219
+ }
220
+ }
221
+
222
+ // Add uncategorized at the end if any
223
+ if (uncategorized.length > 0) {
224
+ uncategorized.sort((a, b) => b.score - a.score);
225
+ result.set("", uncategorized);
226
+ }
227
+
228
+ return result;
229
+ }
230
+
231
+ /**
232
+ * Flatten grouped options into a single array with category markers.
233
+ * Returns items in display order with indices for keyboard navigation.
234
+ */
235
+ interface FlattenedItem {
236
+ type: "category" | "option";
237
+ option?: AutocompleteOption;
238
+ category?: string;
239
+ selectableIndex?: number; // Only for options
240
+ highlights?: readonly HighlightRange[]; // For fuzzy match highlighting
241
+ }
242
+
243
+ function flattenGroupedOptions(
244
+ grouped: Map<string, FilteredOption[]>,
245
+ categoryLabels: Record<string, string> = {}
246
+ ): { items: FlattenedItem[]; selectableOptions: FilteredOption[] } {
247
+ const items: FlattenedItem[] = [];
248
+ const selectableOptions: FilteredOption[] = [];
249
+ let selectableIndex = 0;
250
+
251
+ for (const [category, options] of grouped) {
252
+ // Add category header (if category name is not empty)
253
+ if (category) {
254
+ items.push({
255
+ type: "category",
256
+ category: categoryLabels[category] || category,
257
+ });
258
+ }
259
+
260
+ // Add options
261
+ for (const filteredOpt of options) {
262
+ items.push({
263
+ type: "option",
264
+ option: filteredOpt.option,
265
+ selectableIndex,
266
+ highlights: filteredOpt.highlights,
267
+ });
268
+ selectableOptions.push(filteredOpt);
269
+ selectableIndex++;
270
+ }
271
+ }
272
+
273
+ return { items, selectableOptions };
274
+ }
275
+
276
+ function clamp(value: number, min: number, max: number): number {
277
+ return Math.max(min, Math.min(max, value));
278
+ }
279
+
280
+ /**
281
+ * Highlight the matching portions of an option using fuzzy match ranges.
282
+ *
283
+ * @param option - The option text
284
+ * @param highlights - Highlight ranges from fuzzy matching
285
+ * @returns JSX elements with highlighted matches
286
+ */
287
+ function HighlightedOption({
288
+ option,
289
+ highlights,
290
+ highlightColor,
291
+ normalColor,
292
+ }: {
293
+ option: string;
294
+ highlights: readonly HighlightRange[];
295
+ highlightColor: string;
296
+ normalColor: string;
297
+ }) {
298
+ const segments = getHighlightSegments(option, highlights);
299
+
300
+ return (
301
+ <Text>
302
+ {segments.map((segment, index) =>
303
+ segment.highlighted ? (
304
+ <Text key={`${index}-${segment.text}`} color={highlightColor} bold>
305
+ {segment.text}
306
+ </Text>
307
+ ) : (
308
+ <Text key={`${index}-${segment.text}`} color={normalColor}>
309
+ {segment.text}
310
+ </Text>
311
+ )
312
+ )}
313
+ </Text>
314
+ );
315
+ }
316
+
317
+ // =============================================================================
318
+ // Component
319
+ // =============================================================================
320
+
321
+ /**
322
+ * Autocomplete provides a dropdown for filtering and selecting options.
323
+ *
324
+ * Features:
325
+ * - Case-insensitive prefix filtering
326
+ * - Keyboard navigation (Up/Down arrows)
327
+ * - Selection via Tab or Enter
328
+ * - Cancellation via Escape
329
+ * - Highlighted matching portions
330
+ * - Scrollable list with "[X more]" indicator
331
+ * - Category grouping with headers
332
+ *
333
+ * @example
334
+ * ```tsx
335
+ * // Simple string options (backward compatible)
336
+ * <Autocomplete
337
+ * input="/he"
338
+ * options={['/help', '/history', '/hello']}
339
+ * onSelect={(cmd) => setInput(cmd)}
340
+ * onCancel={() => setShowAutocomplete(false)}
341
+ * />
342
+ *
343
+ * // Grouped options with categories
344
+ * <Autocomplete
345
+ * input="/he"
346
+ * options={[
347
+ * { name: 'help', description: 'Show help', category: 'system' },
348
+ * { name: 'history', description: 'Show history', category: 'session' },
349
+ * ]}
350
+ * grouped={true}
351
+ * categoryOrder={['system', 'session']}
352
+ * categoryLabels={{ system: 'System', session: 'Session' }}
353
+ * onSelect={(cmd) => setInput(cmd)}
354
+ * onCancel={() => setShowAutocomplete(false)}
355
+ * />
356
+ * ```
357
+ */
358
+ function AutocompleteComponent({
359
+ input,
360
+ options,
361
+ onSelect: _onSelect, // kept for API compatibility; selection handled by parent CommandInput
362
+ onCancel,
363
+ onSelectionChange,
364
+ visible = true,
365
+ active,
366
+ maxVisible = 10,
367
+ grouped = false,
368
+ categoryOrder = [],
369
+ categoryLabels = {},
370
+ }: AutocompleteProps) {
371
+ const { theme } = useTheme();
372
+
373
+ const isActive = active ?? visible;
374
+
375
+ // Currently selected index in the selectable options
376
+ const [selectedIndex, setSelectedIndex] = useState(0);
377
+
378
+ // Window start index for scrollable rendering (in terms of display items)
379
+ const [windowStart, setWindowStart] = useState(0);
380
+
381
+ // Normalize and filter options
382
+ const normalizedOptions = useMemo(() => normalizeOptions(options), [options]);
383
+
384
+ const filteredOptions = useMemo(
385
+ () => filterStructuredOptions(normalizedOptions, input),
386
+ [normalizedOptions, input]
387
+ );
388
+
389
+ // Group and flatten for display
390
+ const { displayItems, selectableOptions } = useMemo(() => {
391
+ if (!grouped) {
392
+ // Non-grouped mode: already sorted by score from filterStructuredOptions
393
+ return {
394
+ displayItems: filteredOptions.map(
395
+ (filtered, i): FlattenedItem => ({
396
+ type: "option",
397
+ option: filtered.option,
398
+ selectableIndex: i,
399
+ highlights: filtered.highlights,
400
+ })
401
+ ),
402
+ selectableOptions: filteredOptions,
403
+ };
404
+ }
405
+
406
+ // Grouped mode
407
+ const groupedMap = groupByCategory(filteredOptions, categoryOrder);
408
+ const { items, selectableOptions: selectable } = flattenGroupedOptions(
409
+ groupedMap,
410
+ categoryLabels
411
+ );
412
+ return { displayItems: items, selectableOptions: selectable };
413
+ }, [filteredOptions, grouped, categoryOrder, categoryLabels]);
414
+
415
+ // Calculate visible items (windowed) for grouped display
416
+ const { visibleItems, overflowCount } = useMemo(() => {
417
+ if (displayItems.length <= maxVisible) {
418
+ return { visibleItems: displayItems, overflowCount: 0 };
419
+ }
420
+
421
+ // Find window that includes the selected item
422
+ // We need to map selectedIndex to display item position
423
+ let selectedDisplayIndex = 0;
424
+ for (let i = 0; i < displayItems.length; i++) {
425
+ const item = displayItems[i];
426
+ if (!item) continue;
427
+ if (item.type === "option" && item.selectableIndex === selectedIndex) {
428
+ selectedDisplayIndex = i;
429
+ break;
430
+ }
431
+ }
432
+
433
+ // Adjust window to keep selected visible
434
+ let start = windowStart;
435
+ if (selectedDisplayIndex < start) {
436
+ start = selectedDisplayIndex;
437
+ } else if (selectedDisplayIndex >= start + maxVisible) {
438
+ start = selectedDisplayIndex - maxVisible + 1;
439
+ }
440
+
441
+ // Clamp to valid range
442
+ const maxStart = Math.max(0, displayItems.length - maxVisible);
443
+ start = Math.max(0, Math.min(start, maxStart));
444
+
445
+ const items = displayItems.slice(start, start + maxVisible);
446
+ const overflow = Math.max(0, displayItems.length - (start + maxVisible));
447
+
448
+ return { visibleItems: items, overflowCount: overflow };
449
+ }, [displayItems, maxVisible, windowStart, selectedIndex]);
450
+
451
+ // Track previous input to only reset selection when input actually changes
452
+ const prevInputRef = useRef(input);
453
+
454
+ // Reset selection when input value actually changes
455
+ useEffect(() => {
456
+ if (prevInputRef.current !== input) {
457
+ startTransition(() => {
458
+ setSelectedIndex(0);
459
+ setWindowStart(0);
460
+ });
461
+ prevInputRef.current = input;
462
+ }
463
+ }, [input]);
464
+
465
+ // Notify parent of selection changes
466
+ useEffect(() => {
467
+ onSelectionChange?.(selectedIndex, selectableOptions.length > 0);
468
+ }, [selectedIndex, selectableOptions.length, onSelectionChange]);
469
+
470
+ // Keep selection in bounds when filtered list changes
471
+ useEffect(() => {
472
+ if (selectableOptions.length === 0) {
473
+ setSelectedIndex(0);
474
+ setWindowStart(0);
475
+ return;
476
+ }
477
+
478
+ const clampedIndex = clamp(selectedIndex, 0, selectableOptions.length - 1);
479
+ if (clampedIndex !== selectedIndex) {
480
+ setSelectedIndex(clampedIndex);
481
+ }
482
+ }, [selectableOptions.length, selectedIndex]);
483
+
484
+ // Handle keyboard input for arrow navigation and escape only
485
+ // Note: Enter/Tab selection is handled by parent CommandInput to avoid race condition
486
+ useInput(
487
+ useCallback(
488
+ (_char, key) => {
489
+ if (!visible || selectableOptions.length === 0) return;
490
+
491
+ // Arrow down - move selection down
492
+ if (key.downArrow) {
493
+ setSelectedIndex((prev) => Math.min(prev + 1, selectableOptions.length - 1));
494
+ return;
495
+ }
496
+
497
+ // Arrow up - move selection up
498
+ if (key.upArrow) {
499
+ setSelectedIndex((prev) => Math.max(prev - 1, 0));
500
+ return;
501
+ }
502
+
503
+ // Escape - cancel autocomplete
504
+ if (key.escape) {
505
+ onCancel();
506
+ return;
507
+ }
508
+ },
509
+ [visible, selectableOptions, onCancel]
510
+ ),
511
+ { isActive: isActive && selectableOptions.length > 0 }
512
+ );
513
+
514
+ // Don't render if not visible or no matching options
515
+ if (!visible || selectableOptions.length === 0) {
516
+ return null;
517
+ }
518
+
519
+ // Theme-based styling
520
+ const borderColor = theme.semantic.border.default;
521
+ const highlightColor = theme.colors.primary;
522
+ const normalColor = theme.semantic.text.primary;
523
+ const mutedColor = theme.semantic.text.muted;
524
+ const categoryColor = theme.colors.secondary;
525
+
526
+ return (
527
+ <Box
528
+ flexDirection="column"
529
+ borderStyle="single"
530
+ borderColor={borderColor}
531
+ paddingLeft={1}
532
+ paddingRight={1}
533
+ >
534
+ {visibleItems.map((item, displayIdx) => {
535
+ if (item.type === "category") {
536
+ // Render category header
537
+ return (
538
+ <Box key={`cat-${item.category}`} marginTop={displayIdx > 0 ? 1 : 0}>
539
+ <Text color={categoryColor} bold dimColor>
540
+ ─── {item.category} ───
541
+ </Text>
542
+ </Box>
543
+ );
544
+ }
545
+
546
+ // Render option
547
+ const opt = item.option;
548
+ if (!opt) return null;
549
+ const isSelected = item.selectableIndex === selectedIndex;
550
+ const highlights = item.highlights ?? [];
551
+
552
+ return (
553
+ <Box key={`${opt.name}-${displayIdx}`} flexDirection="row">
554
+ {isSelected ? (
555
+ <Text inverse>
556
+ <Text color={highlightColor} bold>
557
+ {"› "}
558
+ </Text>
559
+ <HighlightedOption
560
+ option={opt.name}
561
+ highlights={highlights}
562
+ highlightColor={highlightColor}
563
+ normalColor={normalColor}
564
+ />
565
+ {opt.description && <Text color={mutedColor}> - {opt.description}</Text>}
566
+ </Text>
567
+ ) : (
568
+ <Text>
569
+ {" "}
570
+ <HighlightedOption
571
+ option={opt.name}
572
+ highlights={highlights}
573
+ highlightColor={highlightColor}
574
+ normalColor={normalColor}
575
+ />
576
+ {opt.description && (
577
+ <Text color={mutedColor} dimColor>
578
+ {" "}
579
+ - {opt.description}
580
+ </Text>
581
+ )}
582
+ </Text>
583
+ )}
584
+ </Box>
585
+ );
586
+ })}
587
+
588
+ {/* Show overflow indicator if there are more items */}
589
+ {overflowCount > 0 && (
590
+ <Box paddingTop={0}>
591
+ <Text color={mutedColor} dimColor>
592
+ [{overflowCount} more]
593
+ </Text>
594
+ </Box>
595
+ )}
596
+ </Box>
597
+ );
598
+ }
599
+
600
+ /**
601
+ * Memoized Autocomplete to prevent unnecessary re-renders.
602
+ */
603
+ export const Autocomplete = memo(AutocompleteComponent);