@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,115 @@
1
+ /**
2
+ * Sidebar panel data loaders.
3
+ *
4
+ * The MemoryPanel and TodoPanel are presentational components; the data is loaded
5
+ * by the app shell. This hook provides:
6
+ * - initial loading
7
+ * - refresh on panel open
8
+ * - refresh after relevant tool executions
9
+ */
10
+
11
+ import type { MemoryEntry } from "@vellum/core";
12
+ import { useCallback, useEffect, useRef, useState } from "react";
13
+ import type { TodoItemData } from "../components/TodoItem.js";
14
+ import type { ToolExecution } from "../context/ToolsContext.js";
15
+
16
+ export type SidebarContent = "todo" | "memory" | "tools" | "mcp" | "help" | "snapshots";
17
+
18
+ export type SidebarPanelDataOptions = {
19
+ readonly sidebarVisible: boolean;
20
+ readonly sidebarContent: SidebarContent;
21
+ readonly executions: readonly ToolExecution[];
22
+
23
+ readonly loadTodos: () => Promise<readonly TodoItemData[]>;
24
+ readonly loadMemories: () => Promise<readonly MemoryEntry[]>;
25
+ };
26
+
27
+ export type SidebarPanelData = {
28
+ readonly todoItems: readonly TodoItemData[];
29
+ readonly memoryEntries: readonly MemoryEntry[];
30
+ readonly refreshTodos: () => void;
31
+ readonly refreshMemories: () => void;
32
+ };
33
+
34
+ export function getLastCompletedToolExecutionId(
35
+ executions: readonly ToolExecution[]
36
+ ): string | undefined {
37
+ for (let i = executions.length - 1; i >= 0; i -= 1) {
38
+ const exec = executions[i];
39
+ if (exec?.status === "complete") return exec.id;
40
+ }
41
+ return undefined;
42
+ }
43
+
44
+ export function shouldRefreshFromToolExecution(execution: ToolExecution | undefined): {
45
+ readonly refreshTodos: boolean;
46
+ readonly refreshMemories: boolean;
47
+ } {
48
+ const toolName = execution?.toolName;
49
+ if (!toolName) return { refreshTodos: false, refreshMemories: false };
50
+
51
+ return {
52
+ refreshTodos: toolName === "todo_manage",
53
+ refreshMemories: toolName === "save_memory" || toolName === "recall_memory",
54
+ };
55
+ }
56
+
57
+ export function useSidebarPanelData(options: SidebarPanelDataOptions): SidebarPanelData {
58
+ const { sidebarVisible, sidebarContent, executions, loadTodos, loadMemories } = options;
59
+
60
+ const [todoItems, setTodoItems] = useState<readonly TodoItemData[]>([]);
61
+ const [memoryEntries, setMemoryEntries] = useState<readonly MemoryEntry[]>([]);
62
+
63
+ const refreshTodos = useCallback(() => {
64
+ void loadTodos()
65
+ .then(setTodoItems)
66
+ .catch(() => {
67
+ setTodoItems([]);
68
+ });
69
+ }, [loadTodos]);
70
+
71
+ const refreshMemories = useCallback(() => {
72
+ void loadMemories()
73
+ .then(setMemoryEntries)
74
+ .catch(() => {
75
+ setMemoryEntries([]);
76
+ });
77
+ }, [loadMemories]);
78
+
79
+ // Initial load (best-effort).
80
+ useEffect(() => {
81
+ refreshTodos();
82
+ refreshMemories();
83
+ }, [refreshMemories, refreshTodos]);
84
+
85
+ // Refresh when the relevant panel is opened.
86
+ useEffect(() => {
87
+ if (!sidebarVisible) return;
88
+
89
+ if (sidebarContent === "todo") {
90
+ refreshTodos();
91
+ }
92
+
93
+ if (sidebarContent === "memory") {
94
+ refreshMemories();
95
+ }
96
+ }, [refreshMemories, refreshTodos, sidebarContent, sidebarVisible]);
97
+
98
+ // Refresh when relevant tool executions complete.
99
+ const lastHandledExecutionIdRef = useRef<string | undefined>(undefined);
100
+
101
+ useEffect(() => {
102
+ const executionId = getLastCompletedToolExecutionId(executions);
103
+ if (!executionId || executionId === lastHandledExecutionIdRef.current) return;
104
+
105
+ const execution = executions.find((e) => e.id === executionId);
106
+ const decision = shouldRefreshFromToolExecution(execution);
107
+
108
+ if (decision.refreshTodos) refreshTodos();
109
+ if (decision.refreshMemories) refreshMemories();
110
+
111
+ lastHandledExecutionIdRef.current = executionId;
112
+ }, [executions, refreshMemories, refreshTodos]);
113
+
114
+ return { todoItems, memoryEntries, refreshTodos, refreshMemories };
115
+ }
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Smooth Scroll Hook
3
+ *
4
+ * Provides smooth animated scrolling with easing functions.
5
+ * Uses setInterval-based updates (terminal-safe, no requestAnimationFrame).
6
+ *
7
+ * @module tui/hooks/useSmoothScroll
8
+ */
9
+
10
+ import { useCallback, useRef, useState } from "react";
11
+
12
+ // =============================================================================
13
+ // Types
14
+ // =============================================================================
15
+
16
+ /**
17
+ * Easing function type: maps progress (0-1) to eased value (0-1)
18
+ */
19
+ export type EasingFunction = (t: number) => number;
20
+
21
+ /**
22
+ * Configuration for smooth scroll behavior
23
+ */
24
+ export interface SmoothScrollConfig {
25
+ /** Animation duration in ms (default: 150) */
26
+ readonly duration?: number;
27
+ /** Easing function (default: easeOutCubic) */
28
+ readonly easing?: EasingFunction;
29
+ /** Frame interval in ms (default: 16 ~= 60fps) */
30
+ readonly frameInterval?: number;
31
+ }
32
+
33
+ /**
34
+ * Return type for useSmoothScroll hook
35
+ */
36
+ export interface UseSmoothScrollReturn {
37
+ /** Current interpolated scroll position */
38
+ readonly position: number;
39
+ /** Whether currently animating */
40
+ readonly isAnimating: boolean;
41
+ /** Start smooth scroll to target position */
42
+ readonly scrollTo: (target: number) => void;
43
+ /** Immediately jump to position (cancel any animation) */
44
+ readonly jumpTo: (target: number) => void;
45
+ /** Cancel current animation */
46
+ readonly cancel: () => void;
47
+ }
48
+
49
+ // =============================================================================
50
+ // Easing Functions
51
+ // =============================================================================
52
+
53
+ /**
54
+ * Standard easing functions for scroll animations
55
+ */
56
+ export const easings = {
57
+ /** Linear - no easing */
58
+ linear: (t: number): number => t,
59
+
60
+ /** Ease out cubic - fast start, slow end (default) */
61
+ easeOutCubic: (t: number): number => 1 - (1 - t) ** 3,
62
+
63
+ /** Ease out quad - gentler ease out */
64
+ easeOutQuad: (t: number): number => 1 - (1 - t) ** 2,
65
+
66
+ /** Ease in out quad - smooth both ends */
67
+ easeInOutQuad: (t: number): number => (t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2),
68
+
69
+ /** Ease out expo - very fast start */
70
+ easeOutExpo: (t: number): number => (t === 1 ? 1 : 1 - 2 ** (-10 * t)),
71
+ } as const;
72
+
73
+ // =============================================================================
74
+ // Constants
75
+ // =============================================================================
76
+
77
+ const DEFAULT_CONFIG: Required<SmoothScrollConfig> = {
78
+ duration: 150,
79
+ easing: easings.easeOutCubic,
80
+ frameInterval: 16, // ~60fps
81
+ };
82
+
83
+ // =============================================================================
84
+ // Hook
85
+ // =============================================================================
86
+
87
+ /**
88
+ * Hook for smooth animated scrolling
89
+ *
90
+ * @param initialPosition - Starting scroll position
91
+ * @param config - Optional animation configuration
92
+ * @returns Smooth scroll state and controls
93
+ *
94
+ * @example
95
+ * ```tsx
96
+ * const { position, scrollTo, isAnimating } = useSmoothScroll(0);
97
+ *
98
+ * // Smooth scroll to line 50
99
+ * scrollTo(50);
100
+ *
101
+ * // Use position for rendering
102
+ * <VirtualizedList offsetFromBottom={position} />
103
+ * ```
104
+ */
105
+ export function useSmoothScroll(
106
+ initialPosition: number,
107
+ config: SmoothScrollConfig = {}
108
+ ): UseSmoothScrollReturn {
109
+ // Merge config with defaults
110
+ const { duration, easing, frameInterval } = { ...DEFAULT_CONFIG, ...config };
111
+
112
+ // State
113
+ const [position, setPosition] = useState(initialPosition);
114
+ const [isAnimating, setIsAnimating] = useState(false);
115
+
116
+ // Refs for animation state
117
+ const animationRef = useRef<ReturnType<typeof setInterval> | null>(null);
118
+ const startPositionRef = useRef(initialPosition);
119
+ const targetPositionRef = useRef(initialPosition);
120
+ const startTimeRef = useRef(0);
121
+
122
+ /**
123
+ * Cancel any running animation
124
+ */
125
+ const cancel = useCallback(() => {
126
+ if (animationRef.current) {
127
+ clearInterval(animationRef.current);
128
+ animationRef.current = null;
129
+ }
130
+ setIsAnimating(false);
131
+ }, []);
132
+
133
+ /**
134
+ * Immediately jump to position (no animation)
135
+ */
136
+ const jumpTo = useCallback(
137
+ (target: number) => {
138
+ cancel();
139
+ setPosition(target);
140
+ startPositionRef.current = target;
141
+ targetPositionRef.current = target;
142
+ },
143
+ [cancel]
144
+ );
145
+
146
+ /**
147
+ * Start smooth scroll to target position
148
+ */
149
+ const scrollTo = useCallback(
150
+ (target: number) => {
151
+ // Cancel any existing animation
152
+ cancel();
153
+
154
+ // Get current position as start
155
+ const start = position;
156
+
157
+ // Skip animation if already at target or very close
158
+ if (Math.abs(target - start) < 0.5) {
159
+ setPosition(target);
160
+ return;
161
+ }
162
+
163
+ // Store animation parameters
164
+ startPositionRef.current = start;
165
+ targetPositionRef.current = target;
166
+ startTimeRef.current = Date.now();
167
+
168
+ setIsAnimating(true);
169
+
170
+ // Animation tick
171
+ const tick = () => {
172
+ const elapsed = Date.now() - startTimeRef.current;
173
+ const progress = Math.min(elapsed / duration, 1);
174
+ const easedProgress = easing(progress);
175
+
176
+ const newPosition =
177
+ startPositionRef.current +
178
+ (targetPositionRef.current - startPositionRef.current) * easedProgress;
179
+
180
+ setPosition(newPosition);
181
+
182
+ if (progress >= 1) {
183
+ // Animation complete - snap to exact target
184
+ setPosition(targetPositionRef.current);
185
+ cancel();
186
+ }
187
+ };
188
+
189
+ // Start animation loop
190
+ animationRef.current = setInterval(tick, frameInterval);
191
+ },
192
+ [cancel, position, duration, easing, frameInterval]
193
+ );
194
+
195
+ return {
196
+ position,
197
+ isAnimating,
198
+ scrollTo,
199
+ jumpTo,
200
+ cancel,
201
+ };
202
+ }
@@ -0,0 +1,300 @@
1
+ /**
2
+ * useSnapshots Hook
3
+ *
4
+ * Provides access to the Snapshot system for managing file state checkpoints.
5
+ * Uses the shadow Git repository in .vellum/.git-shadow/ for tracking.
6
+ *
7
+ * @module tui/hooks/useSnapshots
8
+ */
9
+
10
+ import { Snapshot, SnapshotError, SnapshotErrorCode, type SnapshotInfo } from "@vellum/core";
11
+ import { useCallback, useEffect, useRef, useState } from "react";
12
+
13
+ // =============================================================================
14
+ // Types
15
+ // =============================================================================
16
+
17
+ /**
18
+ * Result of a restore operation.
19
+ */
20
+ export interface RestoreResult {
21
+ /** Whether the restore succeeded */
22
+ readonly success: boolean;
23
+ /** List of files restored */
24
+ readonly files: readonly string[];
25
+ /** Error message if failed */
26
+ readonly error?: string;
27
+ }
28
+
29
+ /**
30
+ * Return type for the useSnapshots hook.
31
+ */
32
+ export interface UseSnapshotsResult {
33
+ /** List of available snapshots (newest first) */
34
+ readonly snapshots: readonly SnapshotInfo[];
35
+ /** Whether snapshots are currently loading */
36
+ readonly isLoading: boolean;
37
+ /** Error message if any operation failed */
38
+ readonly error: string | null;
39
+ /** Whether the snapshot system is initialized */
40
+ readonly isInitialized: boolean;
41
+ /** Refresh the list of snapshots */
42
+ readonly refresh: () => Promise<void>;
43
+ /** Restore files to a specific snapshot */
44
+ readonly restore: (hash: string) => Promise<RestoreResult>;
45
+ /** Get diff between current state and a snapshot */
46
+ readonly diff: (hash: string) => Promise<string>;
47
+ /** Take a new snapshot */
48
+ readonly take: (message?: string) => Promise<string>;
49
+ /** Initialize the snapshot system if needed */
50
+ readonly initialize: () => Promise<boolean>;
51
+ }
52
+
53
+ // =============================================================================
54
+ // Constants
55
+ // =============================================================================
56
+
57
+ /** Maximum number of snapshots to show */
58
+ const MAX_SNAPSHOTS = 10;
59
+
60
+ // =============================================================================
61
+ // Hook Implementation
62
+ // =============================================================================
63
+
64
+ /**
65
+ * Hook for managing file state snapshots.
66
+ *
67
+ * Provides methods to list, create, restore, and diff snapshots.
68
+ * The snapshot system uses a shadow Git repository to track file states
69
+ * independently of the user's main repository.
70
+ *
71
+ * @param workingDir - The working directory path (defaults to cwd)
72
+ * @returns Snapshot management functions and state
73
+ *
74
+ * @example
75
+ * ```tsx
76
+ * function SnapshotPanel() {
77
+ * const {
78
+ * snapshots,
79
+ * isLoading,
80
+ * error,
81
+ * refresh,
82
+ * restore,
83
+ * take
84
+ * } = useSnapshots();
85
+ *
86
+ * if (isLoading) return <Text>Loading...</Text>;
87
+ *
88
+ * return (
89
+ * <Box flexDirection="column">
90
+ * {snapshots.map(s => (
91
+ * <Text key={s.hash}>{s.hash.slice(0, 7)} - {s.message}</Text>
92
+ * ))}
93
+ * </Box>
94
+ * );
95
+ * }
96
+ * ```
97
+ */
98
+ export function useSnapshots(workingDir?: string): UseSnapshotsResult {
99
+ const resolvedDir = workingDir ?? process.cwd();
100
+
101
+ // State
102
+ const [snapshots, setSnapshots] = useState<readonly SnapshotInfo[]>([]);
103
+ const [isLoading, setIsLoading] = useState(true);
104
+ const [error, setError] = useState<string | null>(null);
105
+ const [isInitialized, setIsInitialized] = useState(false);
106
+
107
+ // Ref to track if we're mounted
108
+ const mountedRef = useRef(true);
109
+
110
+ /**
111
+ * Initialize the snapshot system.
112
+ */
113
+ const initialize = useCallback(async (): Promise<boolean> => {
114
+ try {
115
+ const result = await Snapshot.init(resolvedDir);
116
+ if (result.ok) {
117
+ if (mountedRef.current) {
118
+ setIsInitialized(true);
119
+ }
120
+ return true;
121
+ }
122
+ if (mountedRef.current) {
123
+ setError(result.error.message);
124
+ }
125
+ return false;
126
+ } catch (err) {
127
+ if (mountedRef.current) {
128
+ setError(err instanceof Error ? err.message : "Failed to initialize snapshots");
129
+ }
130
+ return false;
131
+ }
132
+ }, [resolvedDir]);
133
+
134
+ /**
135
+ * Refresh the list of snapshots.
136
+ */
137
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Snapshot refresh requires multiple state checks and error handling paths
138
+ const refresh = useCallback(async (): Promise<void> => {
139
+ if (!mountedRef.current) return;
140
+
141
+ setIsLoading(true);
142
+ setError(null);
143
+
144
+ try {
145
+ // Check if initialized
146
+ const initialized = await Snapshot.isInitialized(resolvedDir);
147
+ if (!initialized) {
148
+ if (mountedRef.current) {
149
+ setSnapshots([]);
150
+ setIsInitialized(false);
151
+ setIsLoading(false);
152
+ }
153
+ return;
154
+ }
155
+
156
+ if (mountedRef.current) {
157
+ setIsInitialized(true);
158
+ }
159
+
160
+ // List snapshots
161
+ const result = await Snapshot.listSnapshots(resolvedDir);
162
+
163
+ if (!mountedRef.current) return;
164
+
165
+ if (result.ok) {
166
+ // Limit to MAX_SNAPSHOTS
167
+ setSnapshots(result.value.slice(0, MAX_SNAPSHOTS));
168
+ } else {
169
+ // Handle not initialized error gracefully
170
+ if (result.error.code === SnapshotErrorCode.NOT_INITIALIZED) {
171
+ setSnapshots([]);
172
+ setIsInitialized(false);
173
+ } else {
174
+ setError(result.error.message);
175
+ }
176
+ }
177
+ } catch (err) {
178
+ if (mountedRef.current) {
179
+ setError(err instanceof Error ? err.message : "Failed to load snapshots");
180
+ }
181
+ } finally {
182
+ if (mountedRef.current) {
183
+ setIsLoading(false);
184
+ }
185
+ }
186
+ }, [resolvedDir]);
187
+
188
+ /**
189
+ * Restore files to a specific snapshot.
190
+ */
191
+ const restore = useCallback(
192
+ async (hash: string): Promise<RestoreResult> => {
193
+ try {
194
+ const result = await Snapshot.restore(resolvedDir, hash);
195
+
196
+ if (result.ok) {
197
+ // Refresh after successful restore
198
+ void refresh();
199
+ return {
200
+ success: true,
201
+ files: result.value,
202
+ };
203
+ }
204
+
205
+ return {
206
+ success: false,
207
+ files: [],
208
+ error: result.error.message,
209
+ };
210
+ } catch (err) {
211
+ return {
212
+ success: false,
213
+ files: [],
214
+ error: err instanceof Error ? err.message : "Failed to restore snapshot",
215
+ };
216
+ }
217
+ },
218
+ [resolvedDir, refresh]
219
+ );
220
+
221
+ /**
222
+ * Get diff between current state and a snapshot.
223
+ */
224
+ const diff = useCallback(
225
+ async (hash: string): Promise<string> => {
226
+ try {
227
+ const result = await Snapshot.diff(resolvedDir, hash);
228
+
229
+ if (result.ok) {
230
+ return result.value || "(no changes)";
231
+ }
232
+
233
+ return `Error: ${result.error.message}`;
234
+ } catch (err) {
235
+ return `Error: ${err instanceof Error ? err.message : "Failed to get diff"}`;
236
+ }
237
+ },
238
+ [resolvedDir]
239
+ );
240
+
241
+ /**
242
+ * Take a new snapshot.
243
+ */
244
+ const take = useCallback(
245
+ async (message?: string): Promise<string> => {
246
+ try {
247
+ // Ensure initialized
248
+ const initialized = await Snapshot.isInitialized(resolvedDir);
249
+ if (!initialized) {
250
+ const initResult = await Snapshot.init(resolvedDir);
251
+ if (!initResult.ok) {
252
+ throw new SnapshotError(initResult.error.message, SnapshotErrorCode.OPERATION_FAILED);
253
+ }
254
+ if (mountedRef.current) {
255
+ setIsInitialized(true);
256
+ }
257
+ }
258
+
259
+ // Track all files
260
+ const result = await Snapshot.track(resolvedDir, [], message ?? "Manual checkpoint");
261
+
262
+ if (result.ok) {
263
+ // Refresh after successful snapshot
264
+ void refresh();
265
+ return result.value;
266
+ }
267
+
268
+ throw new SnapshotError(result.error.message, result.error.code);
269
+ } catch (err) {
270
+ if (err instanceof SnapshotError) {
271
+ throw err;
272
+ }
273
+ throw new Error(err instanceof Error ? err.message : "Failed to take snapshot");
274
+ }
275
+ },
276
+ [resolvedDir, refresh]
277
+ );
278
+
279
+ // Initial load
280
+ useEffect(() => {
281
+ mountedRef.current = true;
282
+ void refresh();
283
+
284
+ return () => {
285
+ mountedRef.current = false;
286
+ };
287
+ }, [refresh]);
288
+
289
+ return {
290
+ snapshots,
291
+ isLoading,
292
+ error,
293
+ isInitialized,
294
+ refresh,
295
+ restore,
296
+ diff,
297
+ take,
298
+ initialize,
299
+ };
300
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * useStateAndRef Hook
3
+ *
4
+ * Combines useState and useRef to provide both reactive state and
5
+ * a stable ref for use in callbacks without stale closures.
6
+ *
7
+ * Pattern from Gemini CLI - solves the problem where callbacks
8
+ * capture stale state values.
9
+ *
10
+ * @module tui/hooks/useStateAndRef
11
+ */
12
+
13
+ import { useCallback, useRef, useState } from "react";
14
+
15
+ /**
16
+ * Combines useState and useRef to provide both reactive state and
17
+ * a stable ref for use in callbacks without stale closures.
18
+ *
19
+ * Pattern from Gemini CLI - solves the problem where callbacks
20
+ * capture stale state values.
21
+ *
22
+ * @example
23
+ * const [messages, messagesRef, setMessages] = useStateAndRef<Message[]>([]);
24
+ *
25
+ * // In callbacks, read from ref for latest value:
26
+ * const handleText = useCallback((text) => {
27
+ * const current = messagesRef.current; // Always latest
28
+ * setMessages([...current, { text }]);
29
+ * }, []); // Empty deps = stable callback
30
+ *
31
+ * @param initialValue - Initial state value
32
+ * @returns Tuple of [state, ref, setState]
33
+ */
34
+ export function useStateAndRef<T>(
35
+ initialValue: T
36
+ ): readonly [T, React.RefObject<T>, (newValue: T | ((prev: T) => T)) => void] {
37
+ const [state, setState] = useState<T>(initialValue);
38
+ const ref = useRef<T>(initialValue);
39
+
40
+ const setStateAndRef = useCallback((newValue: T | ((prev: T) => T)) => {
41
+ const resolved =
42
+ typeof newValue === "function" ? (newValue as (prev: T) => T)(ref.current) : newValue;
43
+ ref.current = resolved;
44
+ setState(resolved);
45
+ }, []);
46
+
47
+ return [state, ref, setStateAndRef] as const;
48
+ }
49
+
50
+ export default useStateAndRef;