@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,395 @@
1
+ /**
2
+ * Mode Components Tests (T048)
3
+ *
4
+ * Tests for TUI mode components:
5
+ * - ModeIndicator: Mode display with icons and colors
6
+ * - ModeSelector: Interactive mode selection
7
+ * - PhaseProgressIndicator: Spec phase progress display
8
+ *
9
+ * @module tui/__tests__/mode-components.test
10
+ */
11
+
12
+ import type { CodingMode } from "@vellum/core";
13
+ import { getIcons, type IconSet } from "@vellum/shared";
14
+ import { render } from "ink-testing-library";
15
+ import type React from "react";
16
+ import { beforeEach, describe, expect, it, vi } from "vitest";
17
+ import { ModeIndicator } from "../components/ModeIndicator.js";
18
+ import { ModeSelector } from "../components/ModeSelector.js";
19
+ import { PhaseProgressIndicator } from "../components/PhaseProgressIndicator.js";
20
+ import { ThemeProvider } from "../theme/index.js";
21
+
22
+ // Icons are fetched in beforeEach to ensure setup has run first
23
+ let icons: IconSet;
24
+
25
+ beforeEach(() => {
26
+ // Get icons after setup has configured Unicode mode
27
+ icons = getIcons();
28
+ });
29
+
30
+ // =============================================================================
31
+ // Test Helpers
32
+ // =============================================================================
33
+
34
+ /**
35
+ * Wrapper to provide theme context for tests
36
+ */
37
+ function renderWithTheme(element: React.ReactElement) {
38
+ return render(<ThemeProvider>{element}</ThemeProvider>);
39
+ }
40
+
41
+ // =============================================================================
42
+ // ModeIndicator Tests
43
+ // =============================================================================
44
+
45
+ describe("ModeIndicator", () => {
46
+ describe("Mode Icons", () => {
47
+ it("should render vibe mode with icon", () => {
48
+ const { lastFrame } = renderWithTheme(<ModeIndicator mode="vibe" />);
49
+ const frame = lastFrame() ?? "";
50
+ expect(frame).toContain(icons.vibe);
51
+ expect(frame).toContain("vibe");
52
+ });
53
+
54
+ it("should render plan mode with icon", () => {
55
+ const { lastFrame } = renderWithTheme(<ModeIndicator mode="plan" />);
56
+ const frame = lastFrame() ?? "";
57
+ expect(frame).toContain(icons.plan);
58
+ expect(frame).toContain("plan");
59
+ });
60
+
61
+ it("should render spec mode with icon", () => {
62
+ const { lastFrame } = renderWithTheme(<ModeIndicator mode="spec" />);
63
+ const frame = lastFrame() ?? "";
64
+ expect(frame).toContain(icons.spec);
65
+ expect(frame).toContain("spec");
66
+ });
67
+ });
68
+
69
+ describe("Spec Phase Display", () => {
70
+ it("should show phase progress when in spec mode", () => {
71
+ const { lastFrame } = renderWithTheme(<ModeIndicator mode="spec" specPhase={3} />);
72
+ const frame = lastFrame() ?? "";
73
+ expect(frame).toContain(icons.spec);
74
+ expect(frame).toContain("spec");
75
+ expect(frame).toContain("(3/6");
76
+ expect(frame).toContain("Design");
77
+ });
78
+
79
+ it("should show phase 1 (Research) correctly", () => {
80
+ const { lastFrame } = renderWithTheme(<ModeIndicator mode="spec" specPhase={1} />);
81
+ const frame = lastFrame() ?? "";
82
+ expect(frame).toContain("(1/6");
83
+ expect(frame).toContain("Research");
84
+ });
85
+
86
+ it("should show phase 6 (Validation) correctly", () => {
87
+ const { lastFrame } = renderWithTheme(<ModeIndicator mode="spec" specPhase={6} />);
88
+ const frame = lastFrame() ?? "";
89
+ expect(frame).toContain("(6/6");
90
+ expect(frame).toContain("Validation");
91
+ });
92
+
93
+ it("should not show phase for non-spec modes", () => {
94
+ const { lastFrame } = renderWithTheme(<ModeIndicator mode="vibe" specPhase={3} />);
95
+ const frame = lastFrame() ?? "";
96
+ expect(frame).not.toContain("(3/6)");
97
+ });
98
+
99
+ it("should clamp invalid phase numbers", () => {
100
+ const { lastFrame } = renderWithTheme(<ModeIndicator mode="spec" specPhase={10} />);
101
+ const frame = lastFrame() ?? "";
102
+ expect(frame).toContain("(6/6");
103
+ });
104
+ });
105
+
106
+ describe("Compact Mode", () => {
107
+ it("should show only icon in compact mode", () => {
108
+ const { lastFrame } = renderWithTheme(<ModeIndicator mode="vibe" compact />);
109
+ const frame = lastFrame() ?? "";
110
+ expect(frame).toContain(icons.vibe);
111
+ expect(frame).not.toContain("vibe ");
112
+ });
113
+
114
+ it("should show phase numbers in compact spec mode", () => {
115
+ const { lastFrame } = renderWithTheme(<ModeIndicator mode="spec" specPhase={4} compact />);
116
+ const frame = lastFrame() ?? "";
117
+ expect(frame).toContain(icons.spec);
118
+ expect(frame).toContain("(4/6)");
119
+ // Should NOT show the phase name in compact mode
120
+ expect(frame).not.toContain("Tasks");
121
+ });
122
+ });
123
+ });
124
+
125
+ // =============================================================================
126
+ // ModeSelector Tests
127
+ // =============================================================================
128
+
129
+ describe("ModeSelector", () => {
130
+ describe("Rendering", () => {
131
+ it("should render all three modes", () => {
132
+ const onSelect = vi.fn();
133
+ const { lastFrame } = renderWithTheme(
134
+ <ModeSelector currentMode="vibe" onSelect={onSelect} isActive={false} />
135
+ );
136
+ const frame = lastFrame() ?? "";
137
+
138
+ expect(frame).toContain(icons.vibe);
139
+ expect(frame).toContain("vibe");
140
+ expect(frame).toContain(icons.plan);
141
+ expect(frame).toContain("plan");
142
+ expect(frame).toContain(icons.spec);
143
+ expect(frame).toContain("spec");
144
+ });
145
+
146
+ it("should show shortcut keys", () => {
147
+ const onSelect = vi.fn();
148
+ const { lastFrame } = renderWithTheme(
149
+ <ModeSelector currentMode="vibe" onSelect={onSelect} isActive={false} />
150
+ );
151
+ const frame = lastFrame() ?? "";
152
+
153
+ expect(frame).toContain("[1]");
154
+ expect(frame).toContain("[2]");
155
+ expect(frame).toContain("[3]");
156
+ });
157
+
158
+ it("should indicate current mode", () => {
159
+ const onSelect = vi.fn();
160
+ const { lastFrame } = renderWithTheme(
161
+ <ModeSelector currentMode="plan" onSelect={onSelect} isActive={false} />
162
+ );
163
+ const frame = lastFrame() ?? "";
164
+
165
+ // Translation key is used in tests - checks correct key is applied
166
+ expect(frame).toContain("modeSelector.current");
167
+ });
168
+
169
+ it("should show mode descriptions by default", () => {
170
+ const onSelect = vi.fn();
171
+ const { lastFrame } = renderWithTheme(
172
+ <ModeSelector currentMode="vibe" onSelect={onSelect} isActive={false} />
173
+ );
174
+ const frame = lastFrame() ?? "";
175
+
176
+ expect(frame).toContain("Fast autonomous coding");
177
+ });
178
+
179
+ it("should hide descriptions when showDescriptions is false", () => {
180
+ const onSelect = vi.fn();
181
+ const { lastFrame } = renderWithTheme(
182
+ <ModeSelector
183
+ currentMode="vibe"
184
+ onSelect={onSelect}
185
+ isActive={false}
186
+ showDescriptions={false}
187
+ />
188
+ );
189
+ const frame = lastFrame() ?? "";
190
+
191
+ expect(frame).not.toContain("Fast autonomous coding");
192
+ });
193
+ });
194
+
195
+ describe("Keyboard Navigation", () => {
196
+ it("should show help text", () => {
197
+ const onSelect = vi.fn();
198
+ const { lastFrame } = renderWithTheme(
199
+ <ModeSelector currentMode="vibe" onSelect={onSelect} isActive />
200
+ );
201
+ const frame = lastFrame() ?? "";
202
+
203
+ // Translation key is used in tests - checks correct key is applied
204
+ expect(frame).toContain("modeSelector.keybindings");
205
+ });
206
+
207
+ // Note: Testing stdin/keyboard input with ink-testing-library is complex
208
+ // because useInput handlers are called asynchronously. The keyboard
209
+ // shortcuts (1, 2, 3) are tested implicitly through the component
210
+ // implementation and manual testing. The rendering tests above
211
+ // verify the UI elements are present.
212
+
213
+ it("should show focus indicator when active", () => {
214
+ const onSelect = vi.fn();
215
+ const { lastFrame } = renderWithTheme(
216
+ <ModeSelector currentMode="vibe" onSelect={onSelect} isActive />
217
+ );
218
+ const frame = lastFrame() ?? "";
219
+
220
+ // Focus indicator should be present
221
+ expect(frame).toContain("❯");
222
+ });
223
+
224
+ it("should not call onSelect when inactive", () => {
225
+ const onSelect = vi.fn();
226
+ renderWithTheme(<ModeSelector currentMode="vibe" onSelect={onSelect} isActive={false} />);
227
+
228
+ // With isActive=false, the selector should not process input
229
+ expect(onSelect).not.toHaveBeenCalled();
230
+ });
231
+ });
232
+ });
233
+
234
+ // =============================================================================
235
+ // PhaseProgressIndicator Tests
236
+ // =============================================================================
237
+
238
+ describe("PhaseProgressIndicator", () => {
239
+ describe("Horizontal Progress Bar", () => {
240
+ it("should render progress segments", () => {
241
+ const { lastFrame } = renderWithTheme(<PhaseProgressIndicator currentPhase={3} />);
242
+ const frame = lastFrame() ?? "";
243
+
244
+ // Should contain progress bar characters
245
+ expect(frame).toContain("█"); // Completed
246
+ expect(frame).toContain("▓"); // Current
247
+ expect(frame).toContain("░"); // Pending
248
+ });
249
+
250
+ it("should show first phase correctly", () => {
251
+ const { lastFrame } = renderWithTheme(<PhaseProgressIndicator currentPhase={1} showLabels />);
252
+ const frame = lastFrame() ?? "";
253
+
254
+ expect(frame).toContain("Research");
255
+ expect(frame).toContain("(1/6)");
256
+ });
257
+
258
+ it("should show last phase correctly", () => {
259
+ const { lastFrame } = renderWithTheme(<PhaseProgressIndicator currentPhase={6} showLabels />);
260
+ const frame = lastFrame() ?? "";
261
+
262
+ expect(frame).toContain("Validation");
263
+ expect(frame).toContain("(6/6)");
264
+ });
265
+
266
+ it("should show percentage when enabled", () => {
267
+ const { lastFrame } = renderWithTheme(
268
+ <PhaseProgressIndicator currentPhase={4} showPercentage />
269
+ );
270
+ const frame = lastFrame() ?? "";
271
+
272
+ // Phase 4 = 3 completed = 50%
273
+ expect(frame).toContain("50%");
274
+ });
275
+
276
+ it("should show 0% for phase 1", () => {
277
+ const { lastFrame } = renderWithTheme(
278
+ <PhaseProgressIndicator currentPhase={1} showPercentage />
279
+ );
280
+ const frame = lastFrame() ?? "";
281
+
282
+ expect(frame).toContain("0%");
283
+ });
284
+
285
+ it("should clamp phase to valid range", () => {
286
+ const { lastFrame } = renderWithTheme(
287
+ <PhaseProgressIndicator currentPhase={10} showLabels />
288
+ );
289
+ const frame = lastFrame() ?? "";
290
+
291
+ expect(frame).toContain("Validation");
292
+ expect(frame).toContain("(6/6)");
293
+ });
294
+ });
295
+
296
+ describe("Vertical Progress List", () => {
297
+ it("should render all phases in vertical mode", () => {
298
+ const { lastFrame } = renderWithTheme(
299
+ <PhaseProgressIndicator currentPhase={3} orientation="vertical" />
300
+ );
301
+ const frame = lastFrame() ?? "";
302
+
303
+ expect(frame).toContain("1. Research");
304
+ expect(frame).toContain("2. Requirements");
305
+ expect(frame).toContain("3. Design");
306
+ expect(frame).toContain("4. Tasks");
307
+ expect(frame).toContain("5. Implementation");
308
+ expect(frame).toContain("6. Validation");
309
+ });
310
+
311
+ it("should show completed phases with checkmark", () => {
312
+ const { lastFrame } = renderWithTheme(
313
+ <PhaseProgressIndicator currentPhase={3} orientation="vertical" />
314
+ );
315
+ const frame = lastFrame() ?? "";
316
+
317
+ // Phases 1 and 2 should be completed
318
+ expect(frame).toContain(icons.check);
319
+ });
320
+
321
+ it("should show current phase with bullet", () => {
322
+ const { lastFrame } = renderWithTheme(
323
+ <PhaseProgressIndicator currentPhase={3} orientation="vertical" />
324
+ );
325
+ const frame = lastFrame() ?? "";
326
+
327
+ expect(frame).toContain(icons.bullet);
328
+ });
329
+
330
+ it("should show pending phases with circle", () => {
331
+ const { lastFrame } = renderWithTheme(
332
+ <PhaseProgressIndicator currentPhase={3} orientation="vertical" />
333
+ );
334
+ const frame = lastFrame() ?? "";
335
+
336
+ expect(frame).toContain(icons.pending);
337
+ });
338
+
339
+ it("should show progress summary with percentage", () => {
340
+ const { lastFrame } = renderWithTheme(
341
+ <PhaseProgressIndicator currentPhase={4} orientation="vertical" showPercentage />
342
+ );
343
+ const frame = lastFrame() ?? "";
344
+
345
+ expect(frame).toContain("Progress:");
346
+ expect(frame).toContain("50%");
347
+ expect(frame).toContain("3/6 completed");
348
+ });
349
+ });
350
+
351
+ describe("Custom Width", () => {
352
+ it("should respect custom width", () => {
353
+ const { lastFrame } = renderWithTheme(<PhaseProgressIndicator currentPhase={3} width={12} />);
354
+ const frame = lastFrame() ?? "";
355
+
356
+ // Width 12 / 6 phases = 2 chars per segment
357
+ // Just verify it renders without errors
358
+ expect(frame).toBeTruthy();
359
+ });
360
+ });
361
+ });
362
+
363
+ // =============================================================================
364
+ // Accessibility Tests
365
+ // =============================================================================
366
+
367
+ describe("Accessibility", () => {
368
+ describe("ModeIndicator", () => {
369
+ it("should use semantic colors for modes", () => {
370
+ // Each mode should have a distinct color
371
+ const modes: CodingMode[] = ["vibe", "plan", "spec"];
372
+
373
+ for (const mode of modes) {
374
+ const { lastFrame } = renderWithTheme(<ModeIndicator mode={mode} />);
375
+ const frame = lastFrame() ?? "";
376
+ // Just verify it renders without errors
377
+ expect(frame).toBeTruthy();
378
+ }
379
+ });
380
+ });
381
+
382
+ describe("PhaseProgressIndicator", () => {
383
+ it("should provide textual alternatives in vertical mode", () => {
384
+ const { lastFrame } = renderWithTheme(
385
+ <PhaseProgressIndicator currentPhase={3} orientation="vertical" showPercentage />
386
+ );
387
+ const frame = lastFrame() ?? "";
388
+
389
+ // Should have textual descriptions
390
+ expect(frame).toContain("Research");
391
+ expect(frame).toContain("Requirements");
392
+ expect(frame).toContain("Design");
393
+ });
394
+ });
395
+ });
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Permission Ask Flow Integration Tests
3
+ *
4
+ * Verifies that the core permission "ask" flow can be driven by the TUI layer:
5
+ * - A permission prompt creates a pending tool execution in ToolsContext
6
+ * - PermissionDialog can approve (once/always) or reject
7
+ * - Approving resolves the underlying ask promise (allowing execution to resume)
8
+ */
9
+
10
+ import type { AskContext, PermissionInfo } from "@vellum/core";
11
+ import { Text } from "ink";
12
+ import { render } from "ink-testing-library";
13
+ import { act, useEffect, useRef, useState } from "react";
14
+ import { describe, expect, it } from "vitest";
15
+ import { PermissionDialog, RootProvider, useTools } from "../index.js";
16
+
17
+ describe("Permission ask flow", () => {
18
+ it("shows PermissionDialog for an ask decision and resumes execution on approve", async () => {
19
+ function TestComponent() {
20
+ const { pendingApproval, permissionAskHandler, respondToPermissionRequest } = useTools();
21
+
22
+ const [response, setResponse] = useState<string | null>(null);
23
+ const startedRef = useRef(false);
24
+ const approvedRef = useRef(false);
25
+
26
+ useEffect(() => {
27
+ let cancelled = false;
28
+
29
+ if (startedRef.current) {
30
+ return;
31
+ }
32
+ startedRef.current = true;
33
+
34
+ void (async () => {
35
+ const abortController = new AbortController();
36
+
37
+ const createdAt = Date.now();
38
+
39
+ const info: PermissionInfo = {
40
+ id: "perm-1",
41
+ type: "tool",
42
+ sessionId: "sess-1",
43
+ messageId: "msg-1",
44
+ title: "Allow shell?",
45
+ callId: "call-1",
46
+ metadata: {
47
+ toolName: "shell",
48
+ params: {
49
+ command: "echo hi",
50
+ },
51
+ },
52
+ time: {
53
+ created: createdAt,
54
+ },
55
+ };
56
+
57
+ const ctx: AskContext = {
58
+ timeoutMs: 5_000,
59
+ signal: abortController.signal,
60
+ };
61
+
62
+ const result = await permissionAskHandler(info, ctx);
63
+
64
+ if (cancelled) {
65
+ return;
66
+ }
67
+
68
+ setResponse(result ?? null);
69
+ })();
70
+
71
+ return () => {
72
+ cancelled = true;
73
+ };
74
+ }, [permissionAskHandler]);
75
+
76
+ const pending = pendingApproval[0];
77
+
78
+ useEffect(() => {
79
+ if (!pending) {
80
+ return;
81
+ }
82
+
83
+ if (approvedRef.current) {
84
+ return;
85
+ }
86
+
87
+ approvedRef.current = true;
88
+
89
+ // Delay approval so the dialog has time to render and be asserted against.
90
+ const timer = setTimeout(() => {
91
+ respondToPermissionRequest(pending.id, "once");
92
+ }, 100);
93
+
94
+ return () => {
95
+ clearTimeout(timer);
96
+ };
97
+ }, [pending, respondToPermissionRequest]);
98
+
99
+ if (pending) {
100
+ return (
101
+ <PermissionDialog
102
+ execution={pending}
103
+ riskLevel="high"
104
+ onApprove={() => respondToPermissionRequest(pending.id, "once")}
105
+ onApproveAlways={() => respondToPermissionRequest(pending.id, "always")}
106
+ onReject={() => respondToPermissionRequest(pending.id, "reject")}
107
+ />
108
+ );
109
+ }
110
+
111
+ return <Text>{response ? `response: ${response}` : "waiting"}</Text>;
112
+ }
113
+
114
+ const { lastFrame } = render(
115
+ <RootProvider theme="dark">
116
+ <TestComponent />
117
+ </RootProvider>
118
+ );
119
+
120
+ // Let the permission prompt surface (before auto-approve fires).
121
+ await act(async () => {
122
+ await new Promise((resolve) => setTimeout(resolve, 50));
123
+ });
124
+
125
+ const before = lastFrame() ?? "";
126
+ expect(before).toContain("shell");
127
+ expect(before).toContain("High Risk");
128
+ expect(before).toContain("echo hi");
129
+
130
+ // Let approval + ask resolution complete.
131
+ await act(async () => {
132
+ await new Promise((resolve) => setTimeout(resolve, 200));
133
+ });
134
+
135
+ const after = lastFrame() ?? "";
136
+ expect(after).toContain("response: once");
137
+ }, 10_000);
138
+ });
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Sidebar panel data refresh tests
3
+ *
4
+ * Verifies the data loader hook that backs MemoryPanel and TodoPanel:
5
+ * - initial load
6
+ * - refresh on panel open
7
+ * - refresh after relevant tool execution completion
8
+ */
9
+
10
+ import type { MemoryEntry } from "@vellum/core";
11
+ import { Text } from "ink";
12
+ import { render } from "ink-testing-library";
13
+ import type React from "react";
14
+ import { describe, expect, it, vi } from "vitest";
15
+ import type { TodoItemData } from "../components/TodoItem.js";
16
+ import type { ToolExecution } from "../context/ToolsContext.js";
17
+ import {
18
+ getLastCompletedToolExecutionId,
19
+ shouldRefreshFromToolExecution,
20
+ useSidebarPanelData,
21
+ } from "../hooks/useSidebarPanelData.js";
22
+
23
+ function Harness(props: {
24
+ readonly sidebarVisible: boolean;
25
+ readonly sidebarContent: "todo" | "memory" | "tools" | "mcp";
26
+ readonly executions: readonly ToolExecution[];
27
+ readonly loadTodos: () => Promise<readonly TodoItemData[]>;
28
+ readonly loadMemories: () => Promise<readonly MemoryEntry[]>;
29
+ }): React.JSX.Element {
30
+ const { todoItems, memoryEntries } = useSidebarPanelData({
31
+ sidebarVisible: props.sidebarVisible,
32
+ sidebarContent: props.sidebarContent,
33
+ executions: props.executions,
34
+ loadTodos: props.loadTodos,
35
+ loadMemories: props.loadMemories,
36
+ });
37
+
38
+ return <Text>{`todos:${todoItems.length} memories:${memoryEntries.length}`}</Text>;
39
+ }
40
+
41
+ describe("sidebar panel data", () => {
42
+ it("should identify the last completed tool execution id", () => {
43
+ const executions: ToolExecution[] = [
44
+ { id: "1", toolName: "read_file", params: {}, status: "running" },
45
+ { id: "2", toolName: "todo_manage", params: {}, status: "complete" },
46
+ { id: "3", toolName: "save_memory", params: {}, status: "pending" },
47
+ { id: "4", toolName: "todo_manage", params: {}, status: "complete" },
48
+ ];
49
+
50
+ expect(getLastCompletedToolExecutionId(executions)).toBe("4");
51
+ });
52
+
53
+ it("should decide which panels to refresh from a tool execution", () => {
54
+ const todoExec: ToolExecution = {
55
+ id: "1",
56
+ toolName: "todo_manage",
57
+ params: {},
58
+ status: "complete",
59
+ };
60
+
61
+ const memoryExec: ToolExecution = {
62
+ id: "2",
63
+ toolName: "save_memory",
64
+ params: {},
65
+ status: "complete",
66
+ };
67
+
68
+ expect(shouldRefreshFromToolExecution(todoExec)).toEqual({
69
+ refreshTodos: true,
70
+ refreshMemories: false,
71
+ });
72
+
73
+ expect(shouldRefreshFromToolExecution(memoryExec)).toEqual({
74
+ refreshTodos: false,
75
+ refreshMemories: true,
76
+ });
77
+ });
78
+
79
+ it("should refresh on panel open and after relevant tool completion", async () => {
80
+ const loadTodos = vi.fn<() => Promise<readonly TodoItemData[]>>().mockResolvedValue([
81
+ {
82
+ id: 1,
83
+ title: "Task A",
84
+ status: "pending",
85
+ createdAt: new Date().toISOString(),
86
+ },
87
+ ]);
88
+
89
+ const loadMemories = vi.fn<() => Promise<readonly MemoryEntry[]>>().mockResolvedValue([
90
+ {
91
+ key: "k1",
92
+ type: "context",
93
+ content: "c1",
94
+ createdAt: new Date("2024-01-01T00:00:00.000Z"),
95
+ updatedAt: new Date("2024-01-01T00:00:00.000Z"),
96
+ metadata: { tags: [], importance: 0.5 },
97
+ },
98
+ ]);
99
+
100
+ const { lastFrame, rerender } = render(
101
+ <Harness
102
+ sidebarVisible={false}
103
+ sidebarContent="tools"
104
+ executions={[]}
105
+ loadTodos={loadTodos}
106
+ loadMemories={loadMemories}
107
+ />
108
+ );
109
+
110
+ // Initial load is best-effort; allow microtasks to flush.
111
+ await Promise.resolve();
112
+
113
+ expect(loadTodos).toHaveBeenCalledTimes(1);
114
+ expect(loadMemories).toHaveBeenCalledTimes(1);
115
+
116
+ // Opening the todo panel should trigger a refresh.
117
+ rerender(
118
+ <Harness
119
+ sidebarVisible={true}
120
+ sidebarContent="todo"
121
+ executions={[]}
122
+ loadTodos={loadTodos}
123
+ loadMemories={loadMemories}
124
+ />
125
+ );
126
+ await Promise.resolve();
127
+ expect(loadTodos).toHaveBeenCalledTimes(2);
128
+
129
+ // Completing todo_manage should trigger a refresh.
130
+ rerender(
131
+ <Harness
132
+ sidebarVisible={true}
133
+ sidebarContent="todo"
134
+ executions={[{ id: "e1", toolName: "todo_manage", params: {}, status: "complete" }]}
135
+ loadTodos={loadTodos}
136
+ loadMemories={loadMemories}
137
+ />
138
+ );
139
+ await Promise.resolve();
140
+
141
+ expect(loadTodos).toHaveBeenCalledTimes(3);
142
+
143
+ // Should have rendered counts at least once.
144
+ const frame = lastFrame() ?? "";
145
+ expect(frame).toContain("todos:");
146
+ expect(frame).toContain("memories:");
147
+ });
148
+ });