@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,219 @@
1
+ /**
2
+ * Tests for Input Highlight Utilities
3
+ */
4
+ import { describe, expect, it } from "vitest";
5
+ import {
6
+ applyHighlightStyle,
7
+ findSegmentAtCursor,
8
+ getHighlightStyleDescription,
9
+ type HighlightSegment,
10
+ highlightInput,
11
+ parseHighlights,
12
+ splitSegmentAtCursor,
13
+ } from "../highlight.js";
14
+
15
+ describe("highlight", () => {
16
+ describe("parseHighlights", () => {
17
+ it("returns empty segments for empty string", () => {
18
+ const result = parseHighlights("");
19
+ expect(result.segments).toHaveLength(0);
20
+ expect(result.hasHighlights).toBe(false);
21
+ });
22
+
23
+ it("returns plain text when no highlights", () => {
24
+ const result = parseHighlights("plain text here");
25
+ expect(result.segments).toHaveLength(1);
26
+ expect(result.segments[0]?.text).toBe("plain text here");
27
+ expect(result.segments[0]?.type).toBeUndefined();
28
+ expect(result.hasHighlights).toBe(false);
29
+ });
30
+
31
+ it("detects @mentions", () => {
32
+ const result = parseHighlights("Check @file.ts");
33
+ expect(result.hasHighlights).toBe(true);
34
+ const mention = result.segments.find((s: HighlightSegment) => s.type === "mention");
35
+ expect(mention?.text).toBe("@file.ts");
36
+ });
37
+
38
+ it("detects slash commands", () => {
39
+ const result = parseHighlights("Use /help command");
40
+ expect(result.hasHighlights).toBe(true);
41
+ const command = result.segments.find((s: HighlightSegment) => s.type === "command");
42
+ expect(command?.text).toBe("/help");
43
+ });
44
+
45
+ it("detects URLs", () => {
46
+ const result = parseHighlights("Visit https://example.com");
47
+ expect(result.hasHighlights).toBe(true);
48
+ const url = result.segments.find((s: HighlightSegment) => s.type === "url");
49
+ expect(url?.text).toBe("https://example.com");
50
+ });
51
+
52
+ it("detects http URLs", () => {
53
+ const result = parseHighlights("Visit http://example.com");
54
+ const url = result.segments.find((s: HighlightSegment) => s.type === "url");
55
+ expect(url?.text).toBe("http://example.com");
56
+ });
57
+
58
+ it("detects inline code", () => {
59
+ const result = parseHighlights("Run `npm install`");
60
+ expect(result.hasHighlights).toBe(true);
61
+ const code = result.segments.find((s: HighlightSegment) => s.type === "code");
62
+ expect(code?.text).toBe("`npm install`");
63
+ });
64
+
65
+ it("handles multiple highlights", () => {
66
+ const result = parseHighlights("Check @file.ts with /help");
67
+ expect(result.hasHighlights).toBe(true);
68
+ const mention = result.segments.find((s: HighlightSegment) => s.type === "mention");
69
+ const command = result.segments.find((s: HighlightSegment) => s.type === "command");
70
+ expect(mention?.text).toBe("@file.ts");
71
+ expect(command?.text).toBe("/help");
72
+ });
73
+
74
+ it("preserves text order", () => {
75
+ const result = parseHighlights("start @mid end");
76
+ expect(result.segments).toHaveLength(3);
77
+ expect(result.segments[0]?.text).toBe("start ");
78
+ expect(result.segments[1]?.text).toBe("@mid");
79
+ expect(result.segments[2]?.text).toBe(" end");
80
+ });
81
+
82
+ it("sets correct start and end positions", () => {
83
+ const result = parseHighlights("prefix @file suffix");
84
+ const mention = result.segments.find((s: HighlightSegment) => s.type === "mention");
85
+ expect(mention?.start).toBe(7);
86
+ expect(mention?.end).toBe(12);
87
+ });
88
+ });
89
+
90
+ describe("applyHighlightStyle", () => {
91
+ it("returns plain text for undefined type", () => {
92
+ const result = applyHighlightStyle("text");
93
+ expect(result).toBe("text");
94
+ });
95
+
96
+ it("applies style to mentions and returns text", () => {
97
+ const result = applyHighlightStyle("@file", "mention");
98
+ expect(result).toContain("@file");
99
+ // Result should contain the original text (chalk may or may not add ANSI codes depending on env)
100
+ expect(typeof result).toBe("string");
101
+ });
102
+
103
+ it("applies style to commands and returns text", () => {
104
+ const result = applyHighlightStyle("/help", "command");
105
+ expect(result).toContain("/help");
106
+ expect(typeof result).toBe("string");
107
+ });
108
+
109
+ it("applies style to URLs and returns text", () => {
110
+ const result = applyHighlightStyle("https://x.com", "url");
111
+ expect(result).toContain("https://x.com");
112
+ expect(typeof result).toBe("string");
113
+ });
114
+
115
+ it("applies style to code and returns text", () => {
116
+ const result = applyHighlightStyle("`code`", "code");
117
+ expect(result).toContain("`code`");
118
+ expect(typeof result).toBe("string");
119
+ });
120
+ });
121
+
122
+ describe("getHighlightStyleDescription", () => {
123
+ it('returns "plain" for undefined type', () => {
124
+ expect(getHighlightStyleDescription()).toBe("plain");
125
+ expect(getHighlightStyleDescription(undefined)).toBe("plain");
126
+ });
127
+
128
+ it("returns correct descriptions", () => {
129
+ expect(getHighlightStyleDescription("mention")).toBe("cyan");
130
+ expect(getHighlightStyleDescription("command")).toBe("green");
131
+ expect(getHighlightStyleDescription("url")).toBe("blue underline");
132
+ expect(getHighlightStyleDescription("code")).toBe("dim");
133
+ });
134
+ });
135
+
136
+ describe("highlightInput", () => {
137
+ it("returns styled string for input with highlights", () => {
138
+ const result = highlightInput("@file /help");
139
+ expect(result).toContain("@file");
140
+ expect(result).toContain("/help");
141
+ });
142
+
143
+ it("returns plain string for input without highlights", () => {
144
+ const result = highlightInput("plain text");
145
+ expect(result).toBe("plain text");
146
+ });
147
+
148
+ it("handles empty string", () => {
149
+ expect(highlightInput("")).toBe("");
150
+ });
151
+ });
152
+
153
+ describe("findSegmentAtCursor", () => {
154
+ const segments: HighlightSegment[] = [
155
+ { text: "start ", start: 0, end: 6 },
156
+ { text: "@file", type: "mention", start: 6, end: 11 },
157
+ { text: " end", start: 11, end: 15 },
158
+ ];
159
+
160
+ it("finds segment at cursor position", () => {
161
+ const seg = findSegmentAtCursor(segments, 7);
162
+ expect(seg?.text).toBe("@file");
163
+ expect(seg?.type).toBe("mention");
164
+ });
165
+
166
+ it("finds segment at start boundary", () => {
167
+ const seg = findSegmentAtCursor(segments, 6);
168
+ expect(seg?.text).toBe("@file");
169
+ });
170
+
171
+ it("returns undefined for position beyond segments", () => {
172
+ const seg = findSegmentAtCursor(segments, 100);
173
+ expect(seg).toBeUndefined();
174
+ });
175
+
176
+ it("handles empty segments array", () => {
177
+ const seg = findSegmentAtCursor([], 5);
178
+ expect(seg).toBeUndefined();
179
+ });
180
+ });
181
+
182
+ describe("splitSegmentAtCursor", () => {
183
+ const segment: HighlightSegment = {
184
+ text: "hello",
185
+ start: 0,
186
+ end: 5,
187
+ };
188
+
189
+ it("splits segment at cursor position", () => {
190
+ const result = splitSegmentAtCursor(segment, 2);
191
+ expect(result.before).toBe("he");
192
+ expect(result.cursorChar).toBe("l");
193
+ expect(result.after).toBe("lo");
194
+ expect(result.localPosition).toBe(2);
195
+ });
196
+
197
+ it("handles cursor at start", () => {
198
+ const result = splitSegmentAtCursor(segment, 0);
199
+ expect(result.before).toBe("");
200
+ expect(result.cursorChar).toBe("h");
201
+ expect(result.after).toBe("ello");
202
+ });
203
+
204
+ it("handles cursor at end", () => {
205
+ const result = splitSegmentAtCursor(segment, 5);
206
+ expect(result.before).toBe("hello");
207
+ expect(result.cursorChar).toBe(" "); // Default when past end
208
+ expect(result.after).toBe("");
209
+ });
210
+
211
+ it("handles segment with non-zero start", () => {
212
+ const seg: HighlightSegment = { text: "world", start: 10, end: 15 };
213
+ const result = splitSegmentAtCursor(seg, 12);
214
+ expect(result.before).toBe("wo");
215
+ expect(result.cursorChar).toBe("r");
216
+ expect(result.localPosition).toBe(2);
217
+ });
218
+ });
219
+ });
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Tests for Slash Command Parsing Utilities
3
+ *
4
+ * Tests slash command parsing functionality.
5
+ */
6
+
7
+ import { describe, expect, it } from "vitest";
8
+ import { parseSlashCommand } from "../slash-command-utils.js";
9
+
10
+ describe("parseSlashCommand", () => {
11
+ it("should parse simple command without arguments", () => {
12
+ const result = parseSlashCommand("/help");
13
+
14
+ expect(result).toEqual({
15
+ name: "help",
16
+ args: [],
17
+ raw: "/help",
18
+ });
19
+ });
20
+
21
+ it("should parse command with single argument", () => {
22
+ const result = parseSlashCommand("/search query");
23
+
24
+ expect(result).toEqual({
25
+ name: "search",
26
+ args: ["query"],
27
+ raw: "/search query",
28
+ });
29
+ });
30
+
31
+ it("should parse command with multiple arguments", () => {
32
+ const result = parseSlashCommand("/config set value true");
33
+
34
+ expect(result).toEqual({
35
+ name: "config",
36
+ args: ["set", "value", "true"],
37
+ raw: "/config set value true",
38
+ });
39
+ });
40
+
41
+ it("should handle double-quoted arguments with spaces", () => {
42
+ const result = parseSlashCommand('/search "hello world"');
43
+
44
+ expect(result).toEqual({
45
+ name: "search",
46
+ args: ["hello world"],
47
+ raw: '/search "hello world"',
48
+ });
49
+ });
50
+
51
+ it("should handle single-quoted arguments with spaces", () => {
52
+ const result = parseSlashCommand("/search 'hello world'");
53
+
54
+ expect(result).toEqual({
55
+ name: "search",
56
+ args: ["hello world"],
57
+ raw: "/search 'hello world'",
58
+ });
59
+ });
60
+
61
+ it("should handle mixed quoted and unquoted arguments", () => {
62
+ const result = parseSlashCommand('/filter "user name" --type admin');
63
+
64
+ expect(result.args).toEqual(["user name", "--type", "admin"]);
65
+ });
66
+
67
+ it("should handle escaped quotes within quoted strings", () => {
68
+ const result = parseSlashCommand('/echo "say \\"hello\\""');
69
+
70
+ expect(result.args).toEqual(['say "hello"']);
71
+ });
72
+
73
+ it("should handle escaped backslash", () => {
74
+ const result = parseSlashCommand("/path C:\\\\Users\\\\test");
75
+
76
+ expect(result.args).toEqual(["C:\\Users\\test"]);
77
+ });
78
+
79
+ it("should trim whitespace from input", () => {
80
+ const result = parseSlashCommand(" /help ");
81
+
82
+ expect(result.name).toBe("help");
83
+ expect(result.raw).toBe("/help");
84
+ });
85
+
86
+ it("should handle multiple spaces between arguments", () => {
87
+ const result = parseSlashCommand("/cmd arg1 arg2");
88
+
89
+ expect(result.args).toEqual(["arg1", "arg2"]);
90
+ });
91
+
92
+ it("should handle command with only spaces after name", () => {
93
+ const result = parseSlashCommand("/help ");
94
+
95
+ expect(result.name).toBe("help");
96
+ expect(result.args).toEqual([]);
97
+ });
98
+
99
+ it("should handle flag-style arguments", () => {
100
+ const result = parseSlashCommand("/list --all -v --format=json");
101
+
102
+ expect(result.args).toEqual(["--all", "-v", "--format=json"]);
103
+ });
104
+ });
@@ -0,0 +1,362 @@
1
+ /**
2
+ * Input Highlight Utilities (T009-HL)
3
+ *
4
+ * Provides text highlighting for special patterns in user input:
5
+ * - @mentions: File paths and folder references
6
+ * - Slash commands: /mode, /model, /help, etc.
7
+ * - URLs: http:// and https:// links
8
+ * - Inline code: `backtick-wrapped` code
9
+ *
10
+ * @module tui/components/Input/highlight
11
+ */
12
+
13
+ import chalk from "chalk";
14
+
15
+ // =============================================================================
16
+ // Types
17
+ // =============================================================================
18
+
19
+ /**
20
+ * Types of highlightable patterns.
21
+ */
22
+ export type HighlightType = "mention" | "command" | "url" | "code";
23
+
24
+ /**
25
+ * A segment of highlighted text.
26
+ */
27
+ export interface HighlightSegment {
28
+ /** The text content */
29
+ readonly text: string;
30
+ /** The type of highlight, or undefined for plain text */
31
+ readonly type?: HighlightType;
32
+ /** Start index in original string */
33
+ readonly start: number;
34
+ /** End index in original string (exclusive) */
35
+ readonly end: number;
36
+ }
37
+
38
+ /**
39
+ * Result of parsing input for highlights.
40
+ */
41
+ export interface HighlightResult {
42
+ /** Array of text segments with highlight info */
43
+ readonly segments: readonly HighlightSegment[];
44
+ /** Whether any highlights were found */
45
+ readonly hasHighlights: boolean;
46
+ }
47
+
48
+ // =============================================================================
49
+ // Patterns
50
+ // =============================================================================
51
+ // Caching
52
+ // =============================================================================
53
+
54
+ /**
55
+ * LRU cache for highlight results to avoid recomputing on every keystroke.
56
+ * Limited to 50 entries to prevent memory bloat.
57
+ */
58
+ const HIGHLIGHT_CACHE_SIZE = 50;
59
+ const highlightCache = new Map<string, HighlightResult>();
60
+
61
+ /**
62
+ * Add result to cache with LRU eviction.
63
+ */
64
+ function cacheResult(input: string, result: HighlightResult): void {
65
+ // Evict oldest entry if at capacity
66
+ if (highlightCache.size >= HIGHLIGHT_CACHE_SIZE) {
67
+ const firstKey = highlightCache.keys().next().value;
68
+ if (firstKey !== undefined) {
69
+ highlightCache.delete(firstKey);
70
+ }
71
+ }
72
+ highlightCache.set(input, result);
73
+ }
74
+
75
+ /**
76
+ * Get cached result if available.
77
+ */
78
+ function getCachedResult(input: string): HighlightResult | undefined {
79
+ const cached = highlightCache.get(input);
80
+ if (cached) {
81
+ // Move to end for LRU (delete and re-add)
82
+ highlightCache.delete(input);
83
+ highlightCache.set(input, cached);
84
+ }
85
+ return cached;
86
+ }
87
+
88
+ // =============================================================================
89
+ // Patterns
90
+ // =============================================================================
91
+
92
+ /**
93
+ * Pattern definitions for highlighting.
94
+ * Order matters - patterns are matched in priority order.
95
+ */
96
+ const HIGHLIGHT_PATTERNS: readonly { type: HighlightType; pattern: RegExp }[] = [
97
+ // URLs: Match http:// or https:// followed by non-whitespace
98
+ {
99
+ type: "url",
100
+ pattern: /https?:\/\/[^\s]+/g,
101
+ },
102
+ // Inline code: Backtick-wrapped text (non-greedy, no nested backticks)
103
+ {
104
+ type: "code",
105
+ pattern: /`[^`]+`/g,
106
+ },
107
+ // Slash commands: /word at start of input or after whitespace
108
+ {
109
+ type: "command",
110
+ pattern: /(?:^|\s)(\/[a-zA-Z][a-zA-Z0-9_-]*)/g,
111
+ },
112
+ // @mentions: @path/to/file or @filename.ext
113
+ {
114
+ type: "mention",
115
+ pattern: /@[\w./-]+/g,
116
+ },
117
+ ];
118
+
119
+ // =============================================================================
120
+ // Core Functions
121
+ // =============================================================================
122
+
123
+ /**
124
+ * Find all highlight matches in the input text.
125
+ * Returns matches sorted by start position.
126
+ */
127
+ function findMatches(
128
+ input: string
129
+ ): Array<{ type: HighlightType; start: number; end: number; text: string }> {
130
+ const matches: Array<{ type: HighlightType; start: number; end: number; text: string }> = [];
131
+
132
+ for (const { type, pattern } of HIGHLIGHT_PATTERNS) {
133
+ // Reset regex state
134
+ pattern.lastIndex = 0;
135
+ let match = pattern.exec(input);
136
+
137
+ while (match !== null) {
138
+ // For command pattern, we capture group 1 (the actual command without leading whitespace)
139
+ const text = type === "command" && match[1] ? match[1] : match[0];
140
+ const start =
141
+ type === "command" && match[1] ? match.index + match[0].indexOf(match[1]) : match.index;
142
+ const end = start + text.length;
143
+
144
+ matches.push({ type, start, end, text });
145
+ match = pattern.exec(input);
146
+ }
147
+ }
148
+
149
+ // Sort by start position
150
+ return matches.sort((a, b) => a.start - b.start);
151
+ }
152
+
153
+ /**
154
+ * Remove overlapping matches, keeping earlier/higher priority matches.
155
+ */
156
+ function removeOverlaps(
157
+ matches: Array<{ type: HighlightType; start: number; end: number; text: string }>
158
+ ): Array<{ type: HighlightType; start: number; end: number; text: string }> {
159
+ const result: Array<{ type: HighlightType; start: number; end: number; text: string }> = [];
160
+
161
+ for (const match of matches) {
162
+ // Check if this match overlaps with any existing match
163
+ const overlaps = result.some(
164
+ (existing) => match.start < existing.end && match.end > existing.start
165
+ );
166
+
167
+ if (!overlaps) {
168
+ result.push(match);
169
+ }
170
+ }
171
+
172
+ return result;
173
+ }
174
+
175
+ /**
176
+ * Parse input text and identify highlighted segments.
177
+ * Results are cached to avoid redundant regex processing.
178
+ *
179
+ * @param input - The input text to parse
180
+ * @returns HighlightResult with segments and metadata
181
+ *
182
+ * @example
183
+ * ```ts
184
+ * const result = parseHighlights("Check @file.ts with /help");
185
+ * // result.segments = [
186
+ * // { text: "Check ", start: 0, end: 6 },
187
+ * // { text: "@file.ts", type: "mention", start: 6, end: 14 },
188
+ * // { text: " with ", start: 14, end: 20 },
189
+ * // { text: "/help", type: "command", start: 20, end: 25 }
190
+ * // ]
191
+ * ```
192
+ */
193
+ export function parseHighlights(input: string): HighlightResult {
194
+ if (!input) {
195
+ return { segments: [], hasHighlights: false };
196
+ }
197
+
198
+ // Check cache first
199
+ const cached = getCachedResult(input);
200
+ if (cached) {
201
+ return cached;
202
+ }
203
+
204
+ const matches = removeOverlaps(findMatches(input));
205
+ const segments: HighlightSegment[] = [];
206
+ let currentPos = 0;
207
+
208
+ for (const match of matches) {
209
+ // Add plain text before this match
210
+ if (match.start > currentPos) {
211
+ segments.push({
212
+ text: input.slice(currentPos, match.start),
213
+ start: currentPos,
214
+ end: match.start,
215
+ });
216
+ }
217
+
218
+ // Add the highlighted segment
219
+ segments.push({
220
+ text: match.text,
221
+ type: match.type,
222
+ start: match.start,
223
+ end: match.end,
224
+ });
225
+
226
+ currentPos = match.end;
227
+ }
228
+
229
+ // Add remaining plain text
230
+ if (currentPos < input.length) {
231
+ segments.push({
232
+ text: input.slice(currentPos),
233
+ start: currentPos,
234
+ end: input.length,
235
+ });
236
+ }
237
+
238
+ const result: HighlightResult = {
239
+ segments,
240
+ hasHighlights: matches.length > 0,
241
+ };
242
+
243
+ // Cache the result for future lookups
244
+ cacheResult(input, result);
245
+
246
+ return result;
247
+ }
248
+
249
+ // =============================================================================
250
+ // Styling Functions
251
+ // =============================================================================
252
+
253
+ /**
254
+ * Apply chalk styling to a highlight type.
255
+ *
256
+ * @param text - The text to style
257
+ * @param type - The highlight type
258
+ * @returns Styled string with ANSI codes
259
+ */
260
+ export function applyHighlightStyle(text: string, type?: HighlightType): string {
261
+ if (!type) {
262
+ return text;
263
+ }
264
+
265
+ switch (type) {
266
+ case "mention":
267
+ return chalk.cyan(text);
268
+ case "command":
269
+ return chalk.green(text);
270
+ case "url":
271
+ return chalk.blue.underline(text);
272
+ case "code":
273
+ return chalk.dim(text);
274
+ default:
275
+ return text;
276
+ }
277
+ }
278
+
279
+ /**
280
+ * Get the style description for a highlight type (for accessibility/testing).
281
+ *
282
+ * @param type - The highlight type
283
+ * @returns Human-readable style description
284
+ */
285
+ export function getHighlightStyleDescription(type?: HighlightType): string {
286
+ if (!type) {
287
+ return "plain";
288
+ }
289
+
290
+ switch (type) {
291
+ case "mention":
292
+ return "cyan";
293
+ case "command":
294
+ return "green";
295
+ case "url":
296
+ return "blue underline";
297
+ case "code":
298
+ return "dim";
299
+ default:
300
+ return "plain";
301
+ }
302
+ }
303
+
304
+ /**
305
+ * Apply highlights to input and return styled string.
306
+ * This is a convenience function for simple use cases.
307
+ *
308
+ * @param input - The input text to highlight
309
+ * @returns Styled string with ANSI codes
310
+ *
311
+ * @example
312
+ * ```ts
313
+ * const styled = highlightInput("Check @file.ts with /help");
314
+ * console.log(styled); // Cyan @file.ts, green /help
315
+ * ```
316
+ */
317
+ export function highlightInput(input: string): string {
318
+ const { segments } = parseHighlights(input);
319
+ return segments.map((seg) => applyHighlightStyle(seg.text, seg.type)).join("");
320
+ }
321
+
322
+ // =============================================================================
323
+ // Cursor-Aware Functions
324
+ // =============================================================================
325
+
326
+ /**
327
+ * Find which segment contains a given cursor position.
328
+ *
329
+ * @param segments - Array of highlight segments
330
+ * @param cursorPosition - The cursor position to locate
331
+ * @returns The segment containing the cursor, or undefined if not found
332
+ */
333
+ export function findSegmentAtCursor(
334
+ segments: readonly HighlightSegment[],
335
+ cursorPosition: number
336
+ ): HighlightSegment | undefined {
337
+ return segments.find((seg) => cursorPosition >= seg.start && cursorPosition < seg.end);
338
+ }
339
+
340
+ /**
341
+ * Split a segment at a cursor position for rendering with cursor indicator.
342
+ *
343
+ * @param segment - The segment to split
344
+ * @param cursorPosition - The cursor position within the segment
345
+ * @returns Object with before, cursor char, and after portions
346
+ */
347
+ export function splitSegmentAtCursor(
348
+ segment: HighlightSegment,
349
+ cursorPosition: number
350
+ ): {
351
+ before: string;
352
+ cursorChar: string;
353
+ after: string;
354
+ localPosition: number;
355
+ } {
356
+ const localPosition = cursorPosition - segment.start;
357
+ const before = segment.text.slice(0, localPosition);
358
+ const cursorChar = segment.text[localPosition] || " ";
359
+ const after = segment.text.slice(localPosition + 1);
360
+
361
+ return { before, cursorChar, after, localPosition };
362
+ }