@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,240 @@
1
+ /**
2
+ * useCollapsible Hook
3
+ *
4
+ * Generic collapsible state management for TUI components.
5
+ * Supports keyboard toggles, animation timing, and state persistence.
6
+ *
7
+ * @module tui/hooks/useCollapsible
8
+ */
9
+
10
+ import { useInput } from "ink";
11
+ import { useCallback, useEffect, useState } from "react";
12
+
13
+ // =============================================================================
14
+ // Types
15
+ // =============================================================================
16
+
17
+ /**
18
+ * Options for the useCollapsible hook.
19
+ */
20
+ export interface UseCollapsibleOptions {
21
+ /** Initial collapsed state (default: true) */
22
+ readonly initialCollapsed?: boolean;
23
+ /** Key to toggle collapse state (default: none - manual toggle only) */
24
+ readonly toggleKey?: string;
25
+ /** Whether keyboard toggle is enabled (default: true if toggleKey provided) */
26
+ readonly keyboardEnabled?: boolean;
27
+ /** Unique ID for persistence (if provided, state persists across renders) */
28
+ readonly persistenceId?: string;
29
+ /** Animation duration in ms for expand/collapse (default: 0 - instant) */
30
+ readonly animationDuration?: number;
31
+ /** Callback when state changes */
32
+ readonly onToggle?: (collapsed: boolean) => void;
33
+ }
34
+
35
+ /**
36
+ * Return value of useCollapsible hook.
37
+ */
38
+ export interface UseCollapsibleReturn {
39
+ /** Whether the content is currently collapsed */
40
+ readonly isCollapsed: boolean;
41
+ /** Toggle the collapsed state */
42
+ readonly toggle: () => void;
43
+ /** Expand the content */
44
+ readonly expand: () => void;
45
+ /** Collapse the content */
46
+ readonly collapse: () => void;
47
+ /** Set collapsed state directly */
48
+ readonly setCollapsed: (collapsed: boolean) => void;
49
+ /** Whether an animation is in progress */
50
+ readonly isAnimating: boolean;
51
+ /** Animation progress (0-1) for custom animations */
52
+ readonly animationProgress: number;
53
+ }
54
+
55
+ // =============================================================================
56
+ // Storage for Persistence
57
+ // =============================================================================
58
+
59
+ /** In-memory storage for collapsible states */
60
+ const collapsibleStateStorage = new Map<string, boolean>();
61
+
62
+ // =============================================================================
63
+ // Hook Implementation
64
+ // =============================================================================
65
+
66
+ /**
67
+ * Hook for managing collapsible state with optional keyboard toggle and persistence.
68
+ *
69
+ * @param options - Configuration options
70
+ * @returns Collapsible state and controls
71
+ *
72
+ * @example
73
+ * ```tsx
74
+ * // Basic usage with keyboard toggle
75
+ * const { isCollapsed, toggle } = useCollapsible({
76
+ * initialCollapsed: true,
77
+ * toggleKey: 't',
78
+ * });
79
+ *
80
+ * // With persistence
81
+ * const { isCollapsed, toggle } = useCollapsible({
82
+ * initialCollapsed: true,
83
+ * toggleKey: 't',
84
+ * persistenceId: 'thinking-block-1',
85
+ * });
86
+ *
87
+ * // With callback
88
+ * const { isCollapsed } = useCollapsible({
89
+ * toggleKey: 't',
90
+ * onToggle: (collapsed) => console.log('Collapsed:', collapsed),
91
+ * });
92
+ * ```
93
+ */
94
+ export function useCollapsible(options: UseCollapsibleOptions = {}): UseCollapsibleReturn {
95
+ const {
96
+ initialCollapsed = true,
97
+ toggleKey,
98
+ keyboardEnabled = Boolean(toggleKey),
99
+ persistenceId,
100
+ animationDuration = 0,
101
+ onToggle,
102
+ } = options;
103
+
104
+ // Initialize from persistence if available
105
+ const getInitialState = (): boolean => {
106
+ if (persistenceId && collapsibleStateStorage.has(persistenceId)) {
107
+ return collapsibleStateStorage.get(persistenceId) ?? initialCollapsed;
108
+ }
109
+ return initialCollapsed;
110
+ };
111
+
112
+ const [isCollapsed, setIsCollapsed] = useState(getInitialState);
113
+ const [isAnimating, setIsAnimating] = useState(false);
114
+ const [animationProgress, setAnimationProgress] = useState(isCollapsed ? 0 : 1);
115
+
116
+ // Update persistence when state changes
117
+ useEffect(() => {
118
+ if (persistenceId) {
119
+ collapsibleStateStorage.set(persistenceId, isCollapsed);
120
+ }
121
+ }, [isCollapsed, persistenceId]);
122
+
123
+ // Animation effect
124
+ useEffect(() => {
125
+ if (animationDuration <= 0) {
126
+ setAnimationProgress(isCollapsed ? 0 : 1);
127
+ return;
128
+ }
129
+
130
+ setIsAnimating(true);
131
+ // Capture current progress at effect start (before animation begins)
132
+ const startProgress = isCollapsed ? 1 : 0; // Start from opposite of target
133
+ const targetProgress = isCollapsed ? 0 : 1;
134
+ const startTime = Date.now();
135
+
136
+ // Store timer ref for cleanup
137
+ const animationTimerRef = { current: null as ReturnType<typeof setTimeout> | null };
138
+
139
+ const animate = () => {
140
+ const elapsed = Date.now() - startTime;
141
+ const progress = Math.min(elapsed / animationDuration, 1);
142
+
143
+ // Ease-out cubic for smooth deceleration
144
+ const eased = 1 - (1 - progress) ** 3;
145
+ const currentProgress = startProgress + (targetProgress - startProgress) * eased;
146
+
147
+ setAnimationProgress(currentProgress);
148
+
149
+ if (progress < 1) {
150
+ animationTimerRef.current = setTimeout(animate, 16); // ~60fps
151
+ } else {
152
+ setIsAnimating(false);
153
+ }
154
+ };
155
+
156
+ animationTimerRef.current = setTimeout(animate, 0);
157
+
158
+ return () => {
159
+ if (animationTimerRef.current) {
160
+ clearTimeout(animationTimerRef.current);
161
+ }
162
+ };
163
+ }, [isCollapsed, animationDuration]);
164
+
165
+ // Toggle function
166
+ const toggle = useCallback(() => {
167
+ setIsCollapsed((prev) => {
168
+ const next = !prev;
169
+ onToggle?.(next);
170
+ return next;
171
+ });
172
+ }, [onToggle]);
173
+
174
+ // Expand function
175
+ const expand = useCallback(() => {
176
+ if (isCollapsed) {
177
+ setIsCollapsed(false);
178
+ onToggle?.(false);
179
+ }
180
+ }, [isCollapsed, onToggle]);
181
+
182
+ // Collapse function
183
+ const collapse = useCallback(() => {
184
+ if (!isCollapsed) {
185
+ setIsCollapsed(true);
186
+ onToggle?.(true);
187
+ }
188
+ }, [isCollapsed, onToggle]);
189
+
190
+ // Direct setter
191
+ const setCollapsed = useCallback(
192
+ (collapsed: boolean) => {
193
+ if (collapsed !== isCollapsed) {
194
+ setIsCollapsed(collapsed);
195
+ onToggle?.(collapsed);
196
+ }
197
+ },
198
+ [isCollapsed, onToggle]
199
+ );
200
+
201
+ // Keyboard toggle handler
202
+ useInput(
203
+ (input, _key) => {
204
+ if (keyboardEnabled && toggleKey && input.toLowerCase() === toggleKey.toLowerCase()) {
205
+ toggle();
206
+ }
207
+ },
208
+ { isActive: keyboardEnabled }
209
+ );
210
+
211
+ return {
212
+ isCollapsed,
213
+ toggle,
214
+ expand,
215
+ collapse,
216
+ setCollapsed,
217
+ isAnimating,
218
+ animationProgress,
219
+ };
220
+ }
221
+
222
+ // =============================================================================
223
+ // Utility Functions
224
+ // =============================================================================
225
+
226
+ /**
227
+ * Clear persisted collapsible state for a given ID.
228
+ */
229
+ export function clearCollapsibleState(persistenceId: string): void {
230
+ collapsibleStateStorage.delete(persistenceId);
231
+ }
232
+
233
+ /**
234
+ * Clear all persisted collapsible states.
235
+ */
236
+ export function clearAllCollapsibleStates(): void {
237
+ collapsibleStateStorage.clear();
238
+ }
239
+
240
+ export default useCollapsible;
@@ -0,0 +1,382 @@
1
+ /**
2
+ * useCopyMode Hook (T055)
3
+ *
4
+ * React hook for terminal text copy selection mode.
5
+ * Provides visual selection mode with keyboard navigation and clipboard copy.
6
+ *
7
+ * @module @vellum/cli
8
+ */
9
+
10
+ import { exec } from "node:child_process";
11
+ import { useCallback, useState } from "react";
12
+
13
+ import { getActiveStdout } from "../buffered-stdout.js";
14
+
15
+ /**
16
+ * State for copy mode selection.
17
+ */
18
+ export interface CopyModeState {
19
+ /** Whether copy mode is active */
20
+ active: boolean;
21
+ /** Starting line of selection (0-indexed) */
22
+ startLine: number;
23
+ /** Ending line of selection (0-indexed) */
24
+ endLine: number;
25
+ /** Starting column of selection (0-indexed) */
26
+ startCol: number;
27
+ /** Ending column of selection (0-indexed) */
28
+ endCol: number;
29
+ }
30
+
31
+ /**
32
+ * Return value of useCopyMode hook.
33
+ */
34
+ export interface UseCopyModeReturn {
35
+ /** Current copy mode state */
36
+ state: CopyModeState;
37
+ /** Enter visual selection mode */
38
+ enterCopyMode: () => void;
39
+ /** Exit visual selection mode */
40
+ exitCopyMode: () => void;
41
+ /** Expand selection in a direction */
42
+ expandSelection: (direction: "up" | "down" | "left" | "right") => void;
43
+ /** Copy the selected content to clipboard */
44
+ copySelection: (content: string[][]) => Promise<void>;
45
+ /** Check if a position is within the selection */
46
+ isInSelection: (line: number, col: number) => boolean;
47
+ }
48
+
49
+ /**
50
+ * Initial state for copy mode.
51
+ */
52
+ const initialState: CopyModeState = {
53
+ active: false,
54
+ startLine: 0,
55
+ endLine: 0,
56
+ startCol: 0,
57
+ endCol: 0,
58
+ };
59
+
60
+ /**
61
+ * Copy text to system clipboard using platform-specific methods.
62
+ *
63
+ * Supports:
64
+ * - macOS: pbcopy
65
+ * - Windows: clip (via PowerShell for Unicode support)
66
+ * - Linux: xclip or xsel
67
+ * - Fallback: OSC 52 escape sequence for terminal emulators
68
+ *
69
+ * @param text - Text to copy to clipboard
70
+ * @returns Promise that resolves when copy is complete
71
+ */
72
+ async function copyToClipboard(text: string): Promise<void> {
73
+ if (!text) return;
74
+
75
+ const platform = process.platform;
76
+
77
+ return new Promise((resolve, reject) => {
78
+ let command: string;
79
+ const encoding: BufferEncoding = "utf8";
80
+
81
+ switch (platform) {
82
+ case "darwin":
83
+ command = "pbcopy";
84
+ break;
85
+ case "win32":
86
+ // Use PowerShell for proper Unicode support on Windows
87
+ command = `powershell -NoProfile -Command "$input | Set-Clipboard"`;
88
+ break;
89
+ case "linux":
90
+ // Try xclip first, fallback to xsel
91
+ command = "xclip -selection clipboard";
92
+ break;
93
+ default:
94
+ // For other platforms, try OSC 52 escape sequence
95
+ try {
96
+ const base64Text = Buffer.from(text, "utf8").toString("base64");
97
+ getActiveStdout().write(`\x1b]52;c;${base64Text}\x07`);
98
+ resolve();
99
+ return;
100
+ } catch {
101
+ reject(new Error(`Unsupported platform: ${platform}`));
102
+ return;
103
+ }
104
+ }
105
+
106
+ const child = exec(command, { encoding }, (error) => {
107
+ if (error) {
108
+ // On Linux, try xsel as fallback
109
+ if (platform === "linux") {
110
+ const fallback = exec("xsel --clipboard --input", { encoding }, (fallbackError) => {
111
+ if (fallbackError) {
112
+ // Last resort: OSC 52
113
+ try {
114
+ const base64Text = Buffer.from(text, "utf8").toString("base64");
115
+ getActiveStdout().write(`\x1b]52;c;${base64Text}\x07`);
116
+ resolve();
117
+ } catch {
118
+ reject(fallbackError);
119
+ }
120
+ } else {
121
+ resolve();
122
+ }
123
+ });
124
+ fallback.stdin?.write(text);
125
+ fallback.stdin?.end();
126
+ } else {
127
+ reject(error);
128
+ }
129
+ } else {
130
+ resolve();
131
+ }
132
+ });
133
+
134
+ child.stdin?.write(text);
135
+ child.stdin?.end();
136
+ });
137
+ }
138
+
139
+ /**
140
+ * Extract selected text from 2D content array.
141
+ *
142
+ * @param content - 2D array of characters (content[line][col])
143
+ * @param startLine - Starting line index
144
+ * @param endLine - Ending line index
145
+ * @param startCol - Starting column index
146
+ * @param endCol - Ending column index
147
+ * @returns Selected text as a string
148
+ */
149
+ function extractSelection(
150
+ content: string[][],
151
+ startLine: number,
152
+ endLine: number,
153
+ startCol: number,
154
+ endCol: number
155
+ ): string {
156
+ if (content.length === 0) return "";
157
+
158
+ // Normalize bounds (ensure start <= end)
159
+ const minLine = Math.min(startLine, endLine);
160
+ const maxLine = Math.max(startLine, endLine);
161
+ const minCol = Math.min(startCol, endCol);
162
+ const maxCol = Math.max(startCol, endCol);
163
+
164
+ // Clamp to valid ranges
165
+ const clampedMinLine = Math.max(0, Math.min(minLine, content.length - 1));
166
+ const clampedMaxLine = Math.max(0, Math.min(maxLine, content.length - 1));
167
+
168
+ const lines: string[] = [];
169
+
170
+ for (let line = clampedMinLine; line <= clampedMaxLine; line++) {
171
+ const lineContent = content[line] ?? [];
172
+ if (lineContent.length === 0) {
173
+ lines.push("");
174
+ continue;
175
+ }
176
+
177
+ const lineMinCol = Math.max(0, Math.min(minCol, lineContent.length - 1));
178
+ const lineMaxCol = Math.max(0, Math.min(maxCol, lineContent.length - 1));
179
+
180
+ // For single line selection, use exact columns
181
+ // For multi-line, first line uses startCol to end, middle lines use full line, last line uses start to endCol
182
+ if (clampedMinLine === clampedMaxLine) {
183
+ // Single line: exact column range
184
+ lines.push(lineContent.slice(lineMinCol, lineMaxCol + 1).join(""));
185
+ } else if (line === clampedMinLine) {
186
+ // First line: from minCol to end
187
+ lines.push(lineContent.slice(lineMinCol).join(""));
188
+ } else if (line === clampedMaxLine) {
189
+ // Last line: from start to maxCol
190
+ lines.push(lineContent.slice(0, lineMaxCol + 1).join(""));
191
+ } else {
192
+ // Middle lines: full line
193
+ lines.push(lineContent.join(""));
194
+ }
195
+ }
196
+
197
+ return lines.join("\n");
198
+ }
199
+
200
+ /**
201
+ * Hook for managing terminal text copy selection mode.
202
+ *
203
+ * Provides visual selection mode with keyboard navigation and clipboard copy.
204
+ * Enter with 'v' key, extend with arrow keys, copy with 'y', exit with Escape.
205
+ *
206
+ * @returns Copy mode state and control functions
207
+ *
208
+ * @example
209
+ * ```tsx
210
+ * function TerminalOutput() {
211
+ * const { state, enterCopyMode, expandSelection, copySelection, isInSelection } = useCopyMode();
212
+ *
213
+ * useInput((input, key) => {
214
+ * if (!state.active && input === 'v') {
215
+ * enterCopyMode();
216
+ * } else if (state.active) {
217
+ * if (key.upArrow) expandSelection('up');
218
+ * if (key.downArrow) expandSelection('down');
219
+ * if (key.leftArrow) expandSelection('left');
220
+ * if (key.rightArrow) expandSelection('right');
221
+ * if (input === 'y') {
222
+ * copySelection(content);
223
+ * }
224
+ * }
225
+ * });
226
+ *
227
+ * return (
228
+ * <Box>
229
+ * {lines.map((line, lineNum) => (
230
+ * <Text key={lineNum}>
231
+ * {line.map((char, colNum) => (
232
+ * <Text
233
+ * key={colNum}
234
+ * inverse={state.active && isInSelection(lineNum, colNum)}
235
+ * >
236
+ * {char}
237
+ * </Text>
238
+ * ))}
239
+ * </Text>
240
+ * ))}
241
+ * </Box>
242
+ * );
243
+ * }
244
+ * ```
245
+ */
246
+ export function useCopyMode(): UseCopyModeReturn {
247
+ const [state, setState] = useState<CopyModeState>(initialState);
248
+
249
+ /**
250
+ * Enter visual selection mode.
251
+ * Selection starts at position (0, 0).
252
+ */
253
+ const enterCopyMode = useCallback(() => {
254
+ setState({
255
+ active: true,
256
+ startLine: 0,
257
+ endLine: 0,
258
+ startCol: 0,
259
+ endCol: 0,
260
+ });
261
+ }, []);
262
+
263
+ /**
264
+ * Exit visual selection mode.
265
+ * Resets all selection state.
266
+ */
267
+ const exitCopyMode = useCallback(() => {
268
+ setState(initialState);
269
+ }, []);
270
+
271
+ /**
272
+ * Expand selection in the specified direction.
273
+ * Moves the end position while keeping start position fixed.
274
+ */
275
+ const expandSelection = useCallback(
276
+ (direction: "up" | "down" | "left" | "right") => {
277
+ if (!state.active) return;
278
+
279
+ setState((prev) => {
280
+ const next = { ...prev };
281
+
282
+ switch (direction) {
283
+ case "up":
284
+ next.endLine = Math.max(0, prev.endLine - 1);
285
+ break;
286
+ case "down":
287
+ next.endLine = prev.endLine + 1;
288
+ break;
289
+ case "left":
290
+ next.endCol = Math.max(0, prev.endCol - 1);
291
+ break;
292
+ case "right":
293
+ next.endCol = prev.endCol + 1;
294
+ break;
295
+ }
296
+
297
+ return next;
298
+ });
299
+ },
300
+ [state.active]
301
+ );
302
+
303
+ /**
304
+ * Copy the selected content to clipboard.
305
+ * Automatically exits copy mode after successful copy.
306
+ *
307
+ * @param content - 2D array where content[line][col] is a character
308
+ */
309
+ const copySelection = useCallback(
310
+ async (content: string[][]): Promise<void> => {
311
+ if (!state.active) return;
312
+
313
+ const selectedText = extractSelection(
314
+ content,
315
+ state.startLine,
316
+ state.endLine,
317
+ state.startCol,
318
+ state.endCol
319
+ );
320
+
321
+ if (selectedText) {
322
+ await copyToClipboard(selectedText);
323
+ }
324
+
325
+ // Exit copy mode after copy
326
+ setState(initialState);
327
+ },
328
+ [state]
329
+ );
330
+
331
+ /**
332
+ * Check if a position is within the current selection.
333
+ * Handles both forward and backward selections.
334
+ *
335
+ * @param line - Line index to check
336
+ * @param col - Column index to check
337
+ * @returns True if the position is within the selection
338
+ */
339
+ const isInSelection = useCallback(
340
+ (line: number, col: number): boolean => {
341
+ if (!state.active) return false;
342
+
343
+ // Normalize selection bounds
344
+ const minLine = Math.min(state.startLine, state.endLine);
345
+ const maxLine = Math.max(state.startLine, state.endLine);
346
+ const minCol = Math.min(state.startCol, state.endCol);
347
+ const maxCol = Math.max(state.startCol, state.endCol);
348
+
349
+ // Check if line is in range
350
+ if (line < minLine || line > maxLine) return false;
351
+
352
+ // For single line selection, check exact column range
353
+ if (minLine === maxLine) {
354
+ return col >= minCol && col <= maxCol;
355
+ }
356
+
357
+ // For multi-line selection:
358
+ // - First line: from minCol to end
359
+ // - Middle lines: entire line
360
+ // - Last line: from start to maxCol
361
+ if (line === minLine) {
362
+ return col >= minCol;
363
+ }
364
+ if (line === maxLine) {
365
+ return col <= maxCol;
366
+ }
367
+
368
+ // Middle line: all columns are selected
369
+ return true;
370
+ },
371
+ [state]
372
+ );
373
+
374
+ return {
375
+ state,
376
+ enterCopyMode,
377
+ exitCopyMode,
378
+ expandSelection,
379
+ copySelection,
380
+ isInSelection,
381
+ };
382
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Cost Summary Hook
3
+ *
4
+ * Provides aggregated cost information for the current session.
5
+ * Placeholder implementation - to be expanded.
6
+ *
7
+ * @module tui/hooks/useCostSummary
8
+ */
9
+
10
+ import { useEffect, useState } from "react";
11
+
12
+ // =============================================================================
13
+ // Types
14
+ // =============================================================================
15
+
16
+ export interface CostSummary {
17
+ /** Total cost in USD */
18
+ totalCost: number;
19
+ /** Total input tokens */
20
+ inputTokens: number;
21
+ /** Total output tokens */
22
+ outputTokens: number;
23
+ /** Number of API calls */
24
+ apiCalls: number;
25
+ }
26
+
27
+ export interface UseCostSummaryResult {
28
+ /** Current cost summary */
29
+ summary: CostSummary;
30
+ /** Whether data is loading */
31
+ isLoading: boolean;
32
+ /** Any error that occurred */
33
+ error: Error | null;
34
+ /** Reset the cost summary */
35
+ reset: () => void;
36
+ }
37
+
38
+ // =============================================================================
39
+ // Hook
40
+ // =============================================================================
41
+
42
+ const defaultSummary: CostSummary = {
43
+ totalCost: 0,
44
+ inputTokens: 0,
45
+ outputTokens: 0,
46
+ apiCalls: 0,
47
+ };
48
+
49
+ /**
50
+ * Hook to track and display cost summary
51
+ */
52
+ export function useCostSummary(): UseCostSummaryResult {
53
+ const [summary, setSummary] = useState<CostSummary>(defaultSummary);
54
+ const [isLoading, setIsLoading] = useState(false);
55
+ const [error, setError] = useState<Error | null>(null);
56
+
57
+ const reset = () => {
58
+ setSummary(defaultSummary);
59
+ setError(null);
60
+ };
61
+
62
+ useEffect(() => {
63
+ // Placeholder: In real implementation, subscribe to cost events
64
+ setIsLoading(false);
65
+ }, []);
66
+
67
+ return {
68
+ summary,
69
+ isLoading,
70
+ error,
71
+ reset,
72
+ };
73
+ }
74
+
75
+ export default useCostSummary;