@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,471 @@
1
+ /**
2
+ * EnhancedCommandInput Component
3
+ *
4
+ * An enhanced version of CommandInput that supports both slash commands
5
+ * and @ mentions. This component adds @ mention autocomplete alongside
6
+ * the existing slash command system.
7
+ *
8
+ * @module tui/components/Input/EnhancedCommandInput
9
+ */
10
+
11
+ import { Box, useInput } from "ink";
12
+ import { useCallback, useMemo, useRef, useState } from "react";
13
+ import { useInputHistory } from "../../hooks/useInputHistory.js";
14
+ import { useMentionAutocomplete } from "../../hooks/useMentionAutocomplete.js";
15
+ import type { AutocompleteOption } from "./Autocomplete.js";
16
+ import { Autocomplete } from "./Autocomplete.js";
17
+ import { MentionAutocomplete } from "./MentionAutocomplete.js";
18
+ import { parseSlashCommand, type SlashCommand } from "./slash-command-utils.js";
19
+ import { TextInput } from "./TextInput.js";
20
+
21
+ // =============================================================================
22
+ // Types
23
+ // =============================================================================
24
+
25
+ /**
26
+ * Props for the EnhancedCommandInput component.
27
+ */
28
+ export interface EnhancedCommandInputProps {
29
+ /** Callback when regular text message is submitted */
30
+ readonly onMessage: (text: string) => void;
31
+ /** Callback when a slash command is submitted */
32
+ readonly onCommand: (command: SlashCommand) => void;
33
+ /** Available command names for validation (without slash prefix) */
34
+ readonly commands?: readonly string[] | readonly AutocompleteOption[];
35
+ /** Get subcommands for a command (for two-level autocomplete) */
36
+ readonly getSubcommands?: (commandName: string) => readonly AutocompleteOption[] | undefined;
37
+ /** Get level 3 items for a command and subcommand (for three-level autocomplete) */
38
+ readonly getLevel3Items?: (
39
+ commandName: string,
40
+ arg1: string,
41
+ partial: string
42
+ ) => readonly AutocompleteOption[] | undefined;
43
+ /** Enable grouped display with categories (default: false) */
44
+ readonly groupedCommands?: boolean;
45
+ /** Category display order for grouped mode */
46
+ readonly categoryOrder?: readonly string[];
47
+ /** Category labels for i18n */
48
+ readonly categoryLabels?: Record<string, string>;
49
+ /** Placeholder text shown when input is empty */
50
+ readonly placeholder?: string;
51
+ /** Disable input interactions */
52
+ readonly disabled?: boolean;
53
+ /** Whether the input is focused (default: true) */
54
+ readonly focused?: boolean;
55
+ /** Enable multiline mode for regular messages */
56
+ readonly multiline?: boolean;
57
+ /** localStorage key for history persistence */
58
+ readonly historyKey?: string;
59
+ /** Current working directory for @ mention file suggestions */
60
+ readonly cwd?: string;
61
+ /** Enable @ mention support (default: true) */
62
+ readonly enableMentions?: boolean;
63
+ }
64
+
65
+ // =============================================================================
66
+ // Helper Functions
67
+ // =============================================================================
68
+
69
+ /**
70
+ * Check if input is a slash command.
71
+ */
72
+ function isSlashCommand(input: string): boolean {
73
+ return input.startsWith("/") && input.length > 1 && input[1] !== " ";
74
+ }
75
+
76
+ // =============================================================================
77
+ // Component
78
+ // =============================================================================
79
+
80
+ /**
81
+ * EnhancedCommandInput provides a text input with both slash command
82
+ * and @ mention support.
83
+ *
84
+ * Features:
85
+ * - Slash command parsing with argument handling
86
+ * - @ mention autocomplete for files, folders, URLs, etc.
87
+ * - Input history navigation (up/down arrows)
88
+ * - Keyboard-driven autocomplete selection
89
+ *
90
+ * @example
91
+ * ```tsx
92
+ * <EnhancedCommandInput
93
+ * onMessage={(text) => sendChat(text)}
94
+ * onCommand={(cmd) => executeCommand(cmd)}
95
+ * commands={['help', 'clear', 'exit']}
96
+ * cwd="/project"
97
+ * placeholder="Type a message, /command, or @mention..."
98
+ * />
99
+ * ```
100
+ */
101
+ export function EnhancedCommandInput({
102
+ onMessage,
103
+ onCommand,
104
+ commands,
105
+ getSubcommands,
106
+ getLevel3Items,
107
+ groupedCommands = false,
108
+ categoryOrder,
109
+ categoryLabels,
110
+ placeholder = "Type a message, /command, or @mention...",
111
+ disabled = false,
112
+ focused = true,
113
+ multiline = false,
114
+ historyKey,
115
+ cwd = process.cwd(),
116
+ enableMentions = true,
117
+ }: EnhancedCommandInputProps): React.ReactElement {
118
+ const [value, setValue] = useState("");
119
+
120
+ // Track when autocomplete just completed (to suppress Enter and move cursor)
121
+ const [autocompleteJustCompleted, setAutocompleteJustCompleted] = useState(false);
122
+
123
+ // Refs to track current autocomplete selection (avoids state delay on Enter/Tab)
124
+ const slashSelectionRef = useRef<{ index: number; hasOptions: boolean }>({
125
+ index: 0,
126
+ hasOptions: false,
127
+ });
128
+ const mentionSelectionRef = useRef<{ index: number; hasOptions: boolean }>({
129
+ index: 0,
130
+ hasOptions: false,
131
+ });
132
+
133
+ // Ref for TextInput to manage focus
134
+ const inputRef = useRef<{ focus: () => void } | null>(null);
135
+
136
+ // Slash autocomplete state
137
+ const slashAutocomplete = useMemo(() => {
138
+ if (!value.startsWith("/")) {
139
+ return {
140
+ visible: false,
141
+ active: false,
142
+ query: "",
143
+ level: 1 as const,
144
+ commandName: "",
145
+ arg1: "",
146
+ };
147
+ }
148
+
149
+ const withoutSlash = value.slice(1);
150
+ const spaceIndex = withoutSlash.indexOf(" ");
151
+
152
+ if (spaceIndex === -1) {
153
+ // Level 1: command name completion
154
+ return {
155
+ visible: true,
156
+ active: true,
157
+ query: withoutSlash,
158
+ level: 1 as const,
159
+ commandName: "",
160
+ arg1: "",
161
+ };
162
+ }
163
+
164
+ // Level 2 or 3: subcommand/arg completion
165
+ const commandName = withoutSlash.slice(0, spaceIndex);
166
+ const afterSpace = withoutSlash.slice(spaceIndex + 1);
167
+ // Check if there's a second space (potential level 3)
168
+ const secondSpaceIndex = afterSpace.indexOf(" ");
169
+
170
+ if (secondSpaceIndex === -1) {
171
+ // Level 2: subcommand completion (only one space so far)
172
+ return {
173
+ visible: true,
174
+ active: true,
175
+ query: afterSpace,
176
+ level: 2 as const,
177
+ commandName,
178
+ arg1: "",
179
+ };
180
+ }
181
+
182
+ // Level 3: third-level completion (two spaces)
183
+ const arg1 = afterSpace.slice(0, secondSpaceIndex);
184
+ const afterSecondSpace = afterSpace.slice(secondSpaceIndex + 1);
185
+ // Check if there's a third space (args after level 3)
186
+ const thirdSpaceIndex = afterSecondSpace.indexOf(" ");
187
+ const level3Query =
188
+ thirdSpaceIndex === -1 ? afterSecondSpace : afterSecondSpace.slice(0, thirdSpaceIndex);
189
+ // Only active if we're still typing the level 3 item (no third space yet)
190
+ const isActive = thirdSpaceIndex === -1;
191
+
192
+ return {
193
+ visible: true,
194
+ active: isActive,
195
+ query: level3Query,
196
+ level: 3 as const,
197
+ commandName,
198
+ arg1,
199
+ };
200
+ }, [value]);
201
+
202
+ // @ Mention autocomplete state
203
+ const mentionAutocomplete = useMentionAutocomplete(value, { cwd });
204
+
205
+ // Slash autocomplete options (computed early for activeAutocomplete check)
206
+ const slashOptions = useMemo(() => {
207
+ if (slashAutocomplete.level === 1) {
208
+ return commands ?? [];
209
+ }
210
+ // Level 2: get subcommands for the command
211
+ if (slashAutocomplete.level === 2) {
212
+ if (getSubcommands && slashAutocomplete.commandName) {
213
+ return getSubcommands(slashAutocomplete.commandName) ?? [];
214
+ }
215
+ return [];
216
+ }
217
+ // Level 3: get third-level items (e.g., providers for auth set, models for model command)
218
+ if (slashAutocomplete.level === 3) {
219
+ if (getLevel3Items && slashAutocomplete.commandName && slashAutocomplete.arg1) {
220
+ return (
221
+ getLevel3Items(
222
+ slashAutocomplete.commandName,
223
+ slashAutocomplete.arg1,
224
+ slashAutocomplete.query
225
+ ) ?? []
226
+ );
227
+ }
228
+ return [];
229
+ }
230
+ return [];
231
+ }, [
232
+ slashAutocomplete.level,
233
+ slashAutocomplete.commandName,
234
+ slashAutocomplete.arg1,
235
+ slashAutocomplete.query,
236
+ commands,
237
+ getSubcommands,
238
+ getLevel3Items,
239
+ ]);
240
+
241
+ // Determine which autocomplete is active (priority: slash > mention)
242
+ const activeAutocomplete = useMemo(() => {
243
+ // Only consider slash active if there are actually options to show
244
+ if (slashAutocomplete.visible && slashAutocomplete.active) {
245
+ // Level 1 always shows command list, level 2/3 need actual subcommands
246
+ if (slashAutocomplete.level === 1 || slashOptions.length > 0) {
247
+ return "slash" as const;
248
+ }
249
+ }
250
+ if (enableMentions && mentionAutocomplete.state.visible && mentionAutocomplete.state.active) {
251
+ return "mention" as const;
252
+ }
253
+ return null;
254
+ }, [slashAutocomplete, slashOptions.length, mentionAutocomplete.state, enableMentions]);
255
+
256
+ // History navigation
257
+ const originalInputRef = useRef<string | null>(null);
258
+ const { navigateHistory, addToHistory, currentIndex } = useInputHistory({
259
+ maxItems: 100,
260
+ persistKey: historyKey,
261
+ });
262
+
263
+ const handleHistoryUp = useCallback(() => {
264
+ if (originalInputRef.current === null) {
265
+ originalInputRef.current = value;
266
+ }
267
+ const entry = navigateHistory("up");
268
+ if (entry !== null) {
269
+ setValue(entry);
270
+ }
271
+ }, [value, navigateHistory]);
272
+
273
+ const handleHistoryDown = useCallback(() => {
274
+ const entry = navigateHistory("down");
275
+ if (entry !== null) {
276
+ setValue(entry);
277
+ } else if (originalInputRef.current !== null && currentIndex === -1) {
278
+ setValue(originalInputRef.current);
279
+ originalInputRef.current = null;
280
+ }
281
+ }, [navigateHistory, currentIndex]);
282
+
283
+ const handleChange = useCallback((newValue: string) => {
284
+ originalInputRef.current = null;
285
+ setValue(newValue);
286
+ }, []);
287
+
288
+ const handleSubmit = useCallback(
289
+ (submittedValue: string) => {
290
+ const trimmed = submittedValue.trim();
291
+
292
+ if (trimmed.length === 0) {
293
+ return;
294
+ }
295
+
296
+ originalInputRef.current = null;
297
+ addToHistory(trimmed);
298
+
299
+ if (isSlashCommand(trimmed)) {
300
+ const command = parseSlashCommand(trimmed);
301
+ onCommand(command);
302
+ } else {
303
+ onMessage(trimmed);
304
+ }
305
+
306
+ setValue("");
307
+ },
308
+ [addToHistory, onCommand, onMessage]
309
+ );
310
+
311
+ // History navigation only when no autocomplete is active
312
+ useInput(
313
+ (input, key) => {
314
+ // ↑ or Ctrl+P - previous history
315
+ if (key.upArrow || (key.ctrl && input === "p")) {
316
+ handleHistoryUp();
317
+ }
318
+ // ↓ or Ctrl+N - next history
319
+ else if (key.downArrow || (key.ctrl && input === "n")) {
320
+ handleHistoryDown();
321
+ }
322
+ },
323
+ { isActive: focused && !disabled && !multiline && activeAutocomplete === null }
324
+ );
325
+
326
+ // Extract stable values for memoization to reduce unnecessary recomputation
327
+ const slashQuery = slashAutocomplete.query;
328
+ const slashActive = slashAutocomplete.active;
329
+
330
+ // Compute filtered/sorted slash options for Enter/Tab selection
331
+ const sortedSlashOptions = useMemo(() => {
332
+ if (!slashActive || slashOptions.length === 0) return [];
333
+ // Normalize to AutocompleteOption format
334
+ const normalized = slashOptions.map((opt) => (typeof opt === "string" ? { name: opt } : opt));
335
+ // Filter by prefix (check both name and aliases)
336
+ const query = slashQuery.toLowerCase();
337
+ const filtered = query
338
+ ? normalized.filter(
339
+ (opt) =>
340
+ opt.name.toLowerCase().startsWith(query) ||
341
+ opt.aliases?.some((alias) => alias.toLowerCase().startsWith(query))
342
+ )
343
+ : normalized;
344
+ // Sort alphabetically
345
+ return [...filtered].sort((a, b) => a.name.localeCompare(b.name));
346
+ }, [slashActive, slashQuery, slashOptions]);
347
+
348
+ // Handle slash autocomplete selection
349
+ const handleSlashSelect = useCallback(
350
+ (selected: string) => {
351
+ if (slashAutocomplete.level === 1) {
352
+ // Level 1: selected is command name
353
+ setValue(`/${selected} `);
354
+ } else if (slashAutocomplete.level === 2) {
355
+ // Level 2: selected is subcommand name, preserve command
356
+ setValue(`/${slashAutocomplete.commandName} ${selected} `);
357
+ } else {
358
+ // Level 3: selected is third-level item (e.g., provider), preserve command and arg1
359
+ setValue(`/${slashAutocomplete.commandName} ${slashAutocomplete.arg1} ${selected} `);
360
+ }
361
+ setAutocompleteJustCompleted(true);
362
+ inputRef.current?.focus();
363
+ },
364
+ [slashAutocomplete.level, slashAutocomplete.commandName, slashAutocomplete.arg1]
365
+ );
366
+
367
+ // Enter/Tab interception when autocomplete is active
368
+ useInput(
369
+ (_input, key) => {
370
+ if (key.return || key.tab) {
371
+ if (activeAutocomplete === "slash") {
372
+ const { index, hasOptions } = slashSelectionRef.current;
373
+ if (hasOptions && sortedSlashOptions[index]) {
374
+ handleSlashSelect(sortedSlashOptions[index].name);
375
+ }
376
+ } else if (activeAutocomplete === "mention") {
377
+ // Mention selection is handled internally by MentionAutocomplete
378
+ // This is a fallback that shouldn't normally trigger
379
+ }
380
+ }
381
+ },
382
+ { isActive: focused && !disabled && activeAutocomplete !== null }
383
+ );
384
+
385
+ // Handle @ mention autocomplete selection
386
+ const handleMentionSelect = useCallback(
387
+ (selectedValue: string, mode: "type" | "value") => {
388
+ const newValue = mentionAutocomplete.handleSelect(selectedValue, mode);
389
+ setValue(newValue);
390
+ setAutocompleteJustCompleted(true);
391
+ inputRef.current?.focus();
392
+ },
393
+ [mentionAutocomplete]
394
+ );
395
+
396
+ // Handle autocomplete cancel
397
+ const handleAutocompleteCancel = useCallback(() => {
398
+ inputRef.current?.focus();
399
+ }, []);
400
+
401
+ // Track slash autocomplete selection state
402
+ const handleSlashSelectionChange = useCallback((index: number, hasOptions: boolean) => {
403
+ slashSelectionRef.current = { index, hasOptions };
404
+ }, []);
405
+
406
+ // Track mention autocomplete selection state
407
+ const handleMentionSelectionChange = useCallback((index: number, hasOptions: boolean) => {
408
+ mentionSelectionRef.current = { index, hasOptions };
409
+ }, []);
410
+
411
+ // Callback when cursor has been moved to end
412
+ const handleCursorMoved = useCallback(() => {
413
+ setAutocompleteJustCompleted(false);
414
+ }, []);
415
+
416
+ // Determine what to show
417
+ const showSlashAutocomplete = activeAutocomplete === "slash" && slashOptions.length > 0;
418
+ const showMentionAutocomplete =
419
+ activeAutocomplete === "mention" &&
420
+ (mentionAutocomplete.state.mode === "type" ||
421
+ mentionAutocomplete.fileSuggestions.suggestions.length > 0);
422
+
423
+ return (
424
+ <Box flexDirection="column">
425
+ <TextInput
426
+ value={value}
427
+ onChange={handleChange}
428
+ onSubmit={handleSubmit}
429
+ placeholder={placeholder}
430
+ disabled={disabled}
431
+ focused={focused}
432
+ multiline={multiline}
433
+ suppressEnter={autocompleteJustCompleted || activeAutocomplete !== null}
434
+ suppressTab={activeAutocomplete !== null}
435
+ cursorToEnd={autocompleteJustCompleted}
436
+ onCursorMoved={handleCursorMoved}
437
+ />
438
+
439
+ {/* Slash command autocomplete */}
440
+ {showSlashAutocomplete && (
441
+ <Autocomplete
442
+ input={slashAutocomplete.query}
443
+ options={slashOptions}
444
+ onSelect={handleSlashSelect}
445
+ onCancel={handleAutocompleteCancel}
446
+ onSelectionChange={handleSlashSelectionChange}
447
+ visible={slashAutocomplete.visible}
448
+ active={slashAutocomplete.active}
449
+ grouped={slashAutocomplete.level === 1 && groupedCommands}
450
+ categoryOrder={slashAutocomplete.level === 1 ? categoryOrder : undefined}
451
+ categoryLabels={slashAutocomplete.level === 1 ? categoryLabels : undefined}
452
+ />
453
+ )}
454
+
455
+ {/* @ Mention autocomplete */}
456
+ {showMentionAutocomplete && (
457
+ <MentionAutocomplete
458
+ input={mentionAutocomplete.state.filterText}
459
+ mode={mentionAutocomplete.state.mode}
460
+ mentionType={mentionAutocomplete.state.mentionType ?? undefined}
461
+ fileSuggestions={mentionAutocomplete.fileSuggestions.suggestions}
462
+ onSelect={handleMentionSelect}
463
+ onCancel={handleAutocompleteCancel}
464
+ onSelectionChange={handleMentionSelectionChange}
465
+ visible={mentionAutocomplete.state.visible}
466
+ active={mentionAutocomplete.state.active}
467
+ />
468
+ )}
469
+ </Box>
470
+ );
471
+ }
@@ -0,0 +1,236 @@
1
+ /**
2
+ * HighlightedText Component (T009-HL)
3
+ *
4
+ * React component that renders text with syntax highlighting for
5
+ * special patterns (@mentions, /commands, URLs, `code`).
6
+ *
7
+ * @module tui/components/Input/HighlightedText
8
+ */
9
+
10
+ import { Text } from "ink";
11
+ import { memo, useMemo } from "react";
12
+ import {
13
+ type HighlightResult,
14
+ type HighlightSegment,
15
+ type HighlightType,
16
+ parseHighlights,
17
+ splitSegmentAtCursor,
18
+ } from "./highlight.js";
19
+
20
+ // =============================================================================
21
+ // Types
22
+ // =============================================================================
23
+
24
+ /**
25
+ * Props for the HighlightedText component.
26
+ */
27
+ export interface HighlightedTextProps {
28
+ /** The text to highlight */
29
+ readonly text: string;
30
+ /** Pre-parsed highlight result (optional, for performance) */
31
+ readonly highlightResult?: HighlightResult;
32
+ /** Current cursor position (optional, for cursor rendering) */
33
+ readonly cursorPosition?: number;
34
+ /** Whether cursor should be shown */
35
+ readonly showCursor?: boolean;
36
+ /** Line start position for multiline support */
37
+ readonly lineStartPosition?: number;
38
+ }
39
+
40
+ // =============================================================================
41
+ // Style Mapping
42
+ // =============================================================================
43
+
44
+ /**
45
+ * Map highlight types to Ink Text props.
46
+ */
47
+ function getTextProps(type?: HighlightType): Record<string, boolean | string> {
48
+ if (!type) {
49
+ return {};
50
+ }
51
+
52
+ switch (type) {
53
+ case "mention":
54
+ return { color: "cyan" };
55
+ case "command":
56
+ return { color: "green" };
57
+ case "url":
58
+ return { color: "blue", underline: true };
59
+ case "code":
60
+ return { dimColor: true };
61
+ default:
62
+ return {};
63
+ }
64
+ }
65
+
66
+ // =============================================================================
67
+ // Segment Renderers
68
+ // =============================================================================
69
+
70
+ /**
71
+ * Render a single segment without cursor.
72
+ */
73
+ function SegmentText({ segment }: { segment: HighlightSegment }) {
74
+ const props = getTextProps(segment.type);
75
+ return <Text {...props}>{segment.text}</Text>;
76
+ }
77
+
78
+ /**
79
+ * Render a segment with cursor at specified position.
80
+ */
81
+ function SegmentWithCursor({
82
+ segment,
83
+ cursorPosition,
84
+ }: {
85
+ segment: HighlightSegment;
86
+ cursorPosition: number;
87
+ }) {
88
+ const { before, cursorChar, after } = splitSegmentAtCursor(segment, cursorPosition);
89
+ const props = getTextProps(segment.type);
90
+
91
+ return (
92
+ <Text {...props}>
93
+ {before || null}
94
+ <Text inverse>{cursorChar}</Text>
95
+ {after || null}
96
+ </Text>
97
+ );
98
+ }
99
+
100
+ // =============================================================================
101
+ // Main Component
102
+ // =============================================================================
103
+
104
+ /**
105
+ * HighlightedText renders text with syntax highlighting for special patterns.
106
+ *
107
+ * Supports:
108
+ * - @mentions (cyan)
109
+ * - /commands (green)
110
+ * - URLs (blue underline)
111
+ * - `code` (dim)
112
+ *
113
+ * @example
114
+ * ```tsx
115
+ * // Basic usage
116
+ * <HighlightedText text="Check @file.ts with /help" />
117
+ *
118
+ * // With cursor
119
+ * <HighlightedText
120
+ * text="Type /mode to change"
121
+ * cursorPosition={5}
122
+ * showCursor
123
+ * />
124
+ *
125
+ * // With pre-parsed result for performance
126
+ * const result = parseHighlights(text);
127
+ * <HighlightedText text={text} highlightResult={result} />
128
+ * ```
129
+ */
130
+ function HighlightedTextComponent({
131
+ text,
132
+ highlightResult,
133
+ cursorPosition,
134
+ showCursor = false,
135
+ lineStartPosition = 0,
136
+ }: HighlightedTextProps) {
137
+ // Parse highlights if not provided
138
+ const result = useMemo(() => {
139
+ return highlightResult ?? parseHighlights(text);
140
+ }, [text, highlightResult]);
141
+
142
+ // Adjust cursor position for line offset
143
+ const adjustedCursor =
144
+ cursorPosition !== undefined ? cursorPosition - lineStartPosition : undefined;
145
+
146
+ // Check if cursor is within this text's range
147
+ const cursorInRange =
148
+ showCursor &&
149
+ adjustedCursor !== undefined &&
150
+ adjustedCursor >= 0 &&
151
+ adjustedCursor <= text.length;
152
+
153
+ // If no highlights and no cursor, render plain text
154
+ if (!result.hasHighlights && !cursorInRange) {
155
+ return <Text>{text || " "}</Text>;
156
+ }
157
+
158
+ // If no highlights but has cursor, render with cursor
159
+ if (!result.hasHighlights && cursorInRange && adjustedCursor !== undefined) {
160
+ const before = text.slice(0, adjustedCursor);
161
+ const cursorChar = text[adjustedCursor] || " ";
162
+ const after = text.slice(adjustedCursor + 1);
163
+
164
+ return (
165
+ <Text>
166
+ {before || null}
167
+ <Text inverse>{cursorChar}</Text>
168
+ {after || null}
169
+ </Text>
170
+ );
171
+ }
172
+
173
+ // Render segments with potential cursor
174
+ return (
175
+ <Text>
176
+ {result.segments.map((segment, index) => {
177
+ // Check if cursor is in this segment
178
+ const cursorInSegment =
179
+ cursorInRange &&
180
+ adjustedCursor !== undefined &&
181
+ adjustedCursor >= segment.start - lineStartPosition &&
182
+ adjustedCursor < segment.end - lineStartPosition;
183
+
184
+ // Handle cursor at very end (after last segment)
185
+ const cursorAtEnd =
186
+ cursorInRange &&
187
+ adjustedCursor !== undefined &&
188
+ adjustedCursor === text.length &&
189
+ index === result.segments.length - 1;
190
+
191
+ if (cursorInSegment && adjustedCursor !== undefined) {
192
+ return (
193
+ <SegmentWithCursor
194
+ key={`seg-${segment.start}`}
195
+ segment={{
196
+ ...segment,
197
+ // Adjust segment positions for line offset
198
+ start: segment.start - lineStartPosition,
199
+ end: segment.end - lineStartPosition,
200
+ }}
201
+ cursorPosition={adjustedCursor}
202
+ />
203
+ );
204
+ }
205
+
206
+ // Render segment plus cursor at end if needed
207
+ if (cursorAtEnd) {
208
+ const props = getTextProps(segment.type);
209
+ return (
210
+ <Text key={`seg-${segment.start}`}>
211
+ <Text {...props}>{segment.text}</Text>
212
+ <Text inverse> </Text>
213
+ </Text>
214
+ );
215
+ }
216
+
217
+ return <SegmentText key={`seg-${segment.start}`} segment={segment} />;
218
+ })}
219
+ </Text>
220
+ );
221
+ }
222
+
223
+ /**
224
+ * Memoized HighlightedText to prevent unnecessary re-renders.
225
+ */
226
+ export const HighlightedText = memo(HighlightedTextComponent, (prev, next) => {
227
+ return (
228
+ prev.text === next.text &&
229
+ prev.cursorPosition === next.cursorPosition &&
230
+ prev.showCursor === next.showCursor &&
231
+ prev.lineStartPosition === next.lineStartPosition &&
232
+ prev.highlightResult === next.highlightResult
233
+ );
234
+ });
235
+
236
+ export default HighlightedText;