@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,660 @@
1
+ /**
2
+ * useInputHistory Hook Tests (T016)
3
+ *
4
+ * Tests for the useInputHistory hook which manages input history with navigation.
5
+ *
6
+ * Coverage:
7
+ * - Navigation (up returns older, down returns newer)
8
+ * - Persistence (if persistKey provided)
9
+ * - maxItems respects limit
10
+ * - Edge cases (empty history, consecutive duplicates, boundaries)
11
+ */
12
+
13
+ import { render } from "ink-testing-library";
14
+ import type React from "react";
15
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
16
+ import {
17
+ type UseInputHistoryOptions,
18
+ type UseInputHistoryReturn,
19
+ useInputHistory,
20
+ } from "../useInputHistory.js";
21
+
22
+ // =============================================================================
23
+ // Mock localStorage
24
+ // =============================================================================
25
+
26
+ const createLocalStorageMock = () => {
27
+ let store: Record<string, string> = {};
28
+ return {
29
+ getItem: vi.fn((key: string) => store[key] || null),
30
+ setItem: vi.fn((key: string, value: string) => {
31
+ store[key] = value;
32
+ }),
33
+ removeItem: vi.fn((key: string) => {
34
+ delete store[key];
35
+ }),
36
+ clear: vi.fn(() => {
37
+ store = {};
38
+ }),
39
+ get length() {
40
+ return Object.keys(store).length;
41
+ },
42
+ key: vi.fn((index: number) => Object.keys(store)[index] || null),
43
+ _getStore: () => store,
44
+ _setStore: (newStore: Record<string, string>) => {
45
+ store = newStore;
46
+ },
47
+ };
48
+ };
49
+
50
+ let localStorageMock = createLocalStorageMock();
51
+
52
+ // =============================================================================
53
+ // Test Helper Component
54
+ // =============================================================================
55
+
56
+ /**
57
+ * Helper component that exposes hook state and methods for testing.
58
+ * We use a ref callback pattern to capture the hook return value.
59
+ */
60
+ interface TestHarnessProps {
61
+ options?: UseInputHistoryOptions;
62
+ onHookReturn: (hookReturn: UseInputHistoryReturn) => void;
63
+ }
64
+
65
+ function TestHarness({ options, onHookReturn }: TestHarnessProps): React.ReactElement {
66
+ const hookReturn = useInputHistory(options);
67
+ // Call the callback to expose hook return to tests
68
+ onHookReturn(hookReturn);
69
+ return null as unknown as React.ReactElement;
70
+ }
71
+
72
+ /**
73
+ * Simple wrapper to render and capture hook state.
74
+ */
75
+ function renderHook(options?: UseInputHistoryOptions) {
76
+ let hookReturn: UseInputHistoryReturn | null = null;
77
+
78
+ const setHookReturn = (r: UseInputHistoryReturn) => {
79
+ hookReturn = r;
80
+ };
81
+
82
+ const { rerender, unmount } = render(
83
+ <TestHarness options={options} onHookReturn={setHookReturn} />
84
+ );
85
+
86
+ return {
87
+ get current() {
88
+ if (!hookReturn) throw new Error("Hook not initialized");
89
+ return hookReturn;
90
+ },
91
+ rerender: (newOptions?: UseInputHistoryOptions) => {
92
+ rerender(<TestHarness options={newOptions} onHookReturn={setHookReturn} />);
93
+ },
94
+ unmount,
95
+ };
96
+ }
97
+
98
+ // =============================================================================
99
+ // Test Setup
100
+ // =============================================================================
101
+
102
+ describe("useInputHistory", () => {
103
+ beforeEach(() => {
104
+ // Create fresh mock for each test
105
+ localStorageMock = createLocalStorageMock();
106
+ // Setup localStorage mock
107
+ Object.defineProperty(globalThis, "localStorage", {
108
+ value: localStorageMock,
109
+ writable: true,
110
+ configurable: true,
111
+ });
112
+ vi.clearAllMocks();
113
+ });
114
+
115
+ afterEach(() => {
116
+ vi.restoreAllMocks();
117
+ });
118
+
119
+ // ===========================================================================
120
+ // Basic Functionality
121
+ // ===========================================================================
122
+
123
+ describe("Basic functionality", () => {
124
+ it("should initialize with empty history", () => {
125
+ const result = renderHook();
126
+
127
+ expect(result.current.history).toEqual([]);
128
+ expect(result.current.currentIndex).toBe(-1);
129
+ });
130
+
131
+ it("should add entry to history", () => {
132
+ const result = renderHook();
133
+
134
+ result.current.addToHistory("command1");
135
+ result.rerender();
136
+
137
+ expect(result.current.history).toEqual(["command1"]);
138
+ });
139
+
140
+ it("should add multiple entries to history", () => {
141
+ const result = renderHook();
142
+
143
+ result.current.addToHistory("command1");
144
+ result.rerender();
145
+ result.current.addToHistory("command2");
146
+ result.rerender();
147
+ result.current.addToHistory("command3");
148
+ result.rerender();
149
+
150
+ expect(result.current.history).toEqual(["command1", "command2", "command3"]);
151
+ });
152
+
153
+ it("should trim whitespace from entries", () => {
154
+ const result = renderHook();
155
+
156
+ result.current.addToHistory(" command with spaces ");
157
+ result.rerender();
158
+
159
+ expect(result.current.history).toEqual(["command with spaces"]);
160
+ });
161
+
162
+ it("should skip empty entries", () => {
163
+ const result = renderHook();
164
+
165
+ result.current.addToHistory("");
166
+ result.rerender();
167
+ result.current.addToHistory(" ");
168
+ result.rerender();
169
+ result.current.addToHistory("valid");
170
+ result.rerender();
171
+
172
+ expect(result.current.history).toEqual(["valid"]);
173
+ });
174
+
175
+ it("should skip consecutive duplicate entries", () => {
176
+ const result = renderHook();
177
+
178
+ result.current.addToHistory("command1");
179
+ result.rerender();
180
+ result.current.addToHistory("command1");
181
+ result.rerender();
182
+ result.current.addToHistory("command2");
183
+ result.rerender();
184
+ result.current.addToHistory("command2");
185
+ result.rerender();
186
+ result.current.addToHistory("command1");
187
+ result.rerender();
188
+
189
+ expect(result.current.history).toEqual(["command1", "command2", "command1"]);
190
+ });
191
+
192
+ it("should clear history", () => {
193
+ const result = renderHook();
194
+
195
+ result.current.addToHistory("command1");
196
+ result.rerender();
197
+ result.current.addToHistory("command2");
198
+ result.rerender();
199
+ result.current.clearHistory();
200
+ result.rerender();
201
+
202
+ expect(result.current.history).toEqual([]);
203
+ expect(result.current.currentIndex).toBe(-1);
204
+ });
205
+ });
206
+
207
+ // ===========================================================================
208
+ // Navigation (Up/Down)
209
+ // ===========================================================================
210
+
211
+ describe("Navigation", () => {
212
+ it("should return null when navigating empty history", () => {
213
+ const result = renderHook();
214
+
215
+ const upResult = result.current.navigateHistory("up");
216
+ expect(upResult).toBeNull();
217
+
218
+ const downResult = result.current.navigateHistory("down");
219
+ expect(downResult).toBeNull();
220
+ });
221
+
222
+ it("should navigate up to most recent entry first", () => {
223
+ const result = renderHook();
224
+
225
+ result.current.addToHistory("command1");
226
+ result.rerender();
227
+ result.current.addToHistory("command2");
228
+ result.rerender();
229
+ result.current.addToHistory("command3");
230
+ result.rerender();
231
+
232
+ const entry = result.current.navigateHistory("up");
233
+ result.rerender();
234
+
235
+ expect(entry).toBe("command3");
236
+ expect(result.current.currentIndex).toBe(2);
237
+ });
238
+
239
+ it("should navigate up through older entries", () => {
240
+ const result = renderHook();
241
+
242
+ result.current.addToHistory("command1");
243
+ result.rerender();
244
+ result.current.addToHistory("command2");
245
+ result.rerender();
246
+ result.current.addToHistory("command3");
247
+ result.rerender();
248
+
249
+ const entry1 = result.current.navigateHistory("up"); // command3
250
+ result.rerender();
251
+ const entry2 = result.current.navigateHistory("up"); // command2
252
+ result.rerender();
253
+ const entry3 = result.current.navigateHistory("up"); // command1
254
+ result.rerender();
255
+
256
+ expect(entry1).toBe("command3");
257
+ expect(entry2).toBe("command2");
258
+ expect(entry3).toBe("command1");
259
+ expect(result.current.currentIndex).toBe(0);
260
+ });
261
+
262
+ it("should return null when at oldest entry and navigating up", () => {
263
+ const result = renderHook();
264
+
265
+ result.current.addToHistory("command1");
266
+ result.rerender();
267
+ result.current.addToHistory("command2");
268
+ result.rerender();
269
+
270
+ // Navigate to oldest
271
+ result.current.navigateHistory("up"); // command2
272
+ result.rerender();
273
+ result.current.navigateHistory("up"); // command1
274
+ result.rerender();
275
+
276
+ const entry = result.current.navigateHistory("up"); // should return null
277
+ result.rerender();
278
+
279
+ expect(entry).toBeNull();
280
+ expect(result.current.currentIndex).toBe(0); // Still at oldest
281
+ });
282
+
283
+ it("should navigate down to newer entries", () => {
284
+ const result = renderHook();
285
+
286
+ result.current.addToHistory("command1");
287
+ result.rerender();
288
+ result.current.addToHistory("command2");
289
+ result.rerender();
290
+ result.current.addToHistory("command3");
291
+ result.rerender();
292
+
293
+ // Navigate up to oldest
294
+ result.current.navigateHistory("up"); // command3
295
+ result.rerender();
296
+ result.current.navigateHistory("up"); // command2
297
+ result.rerender();
298
+ result.current.navigateHistory("up"); // command1
299
+ result.rerender();
300
+
301
+ const entry1 = result.current.navigateHistory("down"); // command2
302
+ result.rerender();
303
+ const entry2 = result.current.navigateHistory("down"); // command3
304
+ result.rerender();
305
+
306
+ expect(entry1).toBe("command2");
307
+ expect(entry2).toBe("command3");
308
+ });
309
+
310
+ it("should reset index when at newest and navigating down past history", () => {
311
+ const result = renderHook();
312
+
313
+ result.current.addToHistory("command1");
314
+ result.rerender();
315
+
316
+ // Navigate up then down past the end
317
+ result.current.navigateHistory("up"); // command1
318
+ result.rerender();
319
+
320
+ result.current.navigateHistory("down"); // Returns temp entry or null
321
+ result.rerender();
322
+
323
+ // Index resets to -1
324
+ expect(result.current.currentIndex).toBe(-1);
325
+ });
326
+
327
+ it("should return null when navigating down while not navigating", () => {
328
+ const result = renderHook();
329
+
330
+ result.current.addToHistory("command1");
331
+ result.rerender();
332
+
333
+ const entry = result.current.navigateHistory("down");
334
+ result.rerender();
335
+
336
+ expect(entry).toBeNull();
337
+ });
338
+
339
+ it("should reset navigation index when adding new entry", () => {
340
+ const result = renderHook();
341
+
342
+ result.current.addToHistory("command1");
343
+ result.rerender();
344
+ result.current.addToHistory("command2");
345
+ result.rerender();
346
+
347
+ result.current.navigateHistory("up");
348
+ result.rerender();
349
+ expect(result.current.currentIndex).toBe(1);
350
+
351
+ result.current.addToHistory("command3");
352
+ result.rerender();
353
+ expect(result.current.currentIndex).toBe(-1);
354
+ });
355
+ });
356
+
357
+ // ===========================================================================
358
+ // getCurrentEntry
359
+ // ===========================================================================
360
+
361
+ describe("getCurrentEntry", () => {
362
+ it("should return null when not navigating", () => {
363
+ const result = renderHook();
364
+
365
+ result.current.addToHistory("command1");
366
+ result.rerender();
367
+
368
+ expect(result.current.getCurrentEntry()).toBeNull();
369
+ });
370
+
371
+ it("should return null for empty history", () => {
372
+ const result = renderHook();
373
+
374
+ expect(result.current.getCurrentEntry()).toBeNull();
375
+ });
376
+
377
+ it("should return current entry when navigating", () => {
378
+ const result = renderHook();
379
+
380
+ result.current.addToHistory("command1");
381
+ result.rerender();
382
+ result.current.addToHistory("command2");
383
+ result.rerender();
384
+
385
+ result.current.navigateHistory("up");
386
+ result.rerender();
387
+
388
+ expect(result.current.getCurrentEntry()).toBe("command2");
389
+
390
+ result.current.navigateHistory("up");
391
+ result.rerender();
392
+
393
+ expect(result.current.getCurrentEntry()).toBe("command1");
394
+ });
395
+ });
396
+
397
+ // ===========================================================================
398
+ // maxItems Limit
399
+ // ===========================================================================
400
+
401
+ describe("maxItems limit", () => {
402
+ it("should use default maxItems of 100", () => {
403
+ const result = renderHook();
404
+
405
+ // Add 105 entries
406
+ for (let i = 1; i <= 105; i++) {
407
+ result.current.addToHistory(`command${i}`);
408
+ result.rerender();
409
+ }
410
+
411
+ expect(result.current.history.length).toBe(100);
412
+ // Should keep the most recent 100 (6-105)
413
+ expect(result.current.history[0]).toBe("command6");
414
+ expect(result.current.history[99]).toBe("command105");
415
+ });
416
+
417
+ it("should respect custom maxItems limit", () => {
418
+ const result = renderHook({ maxItems: 5 });
419
+
420
+ for (let i = 1; i <= 10; i++) {
421
+ result.current.addToHistory(`command${i}`);
422
+ result.rerender({ maxItems: 5 });
423
+ }
424
+
425
+ expect(result.current.history.length).toBe(5);
426
+ // Should keep the most recent 5 (6-10)
427
+ expect(result.current.history).toEqual([
428
+ "command6",
429
+ "command7",
430
+ "command8",
431
+ "command9",
432
+ "command10",
433
+ ]);
434
+ });
435
+
436
+ it("should not trim history when under limit", () => {
437
+ const result = renderHook({ maxItems: 10 });
438
+
439
+ result.current.addToHistory("command1");
440
+ result.rerender({ maxItems: 10 });
441
+ result.current.addToHistory("command2");
442
+ result.rerender({ maxItems: 10 });
443
+ result.current.addToHistory("command3");
444
+ result.rerender({ maxItems: 10 });
445
+
446
+ expect(result.current.history.length).toBe(3);
447
+ });
448
+ });
449
+
450
+ // ===========================================================================
451
+ // Persistence
452
+ // ===========================================================================
453
+
454
+ describe("Persistence", () => {
455
+ const PERSIST_KEY = "test-history";
456
+
457
+ it("should save to localStorage when persistKey provided", () => {
458
+ const result = renderHook({ persistKey: PERSIST_KEY });
459
+
460
+ result.current.addToHistory("command1");
461
+ result.rerender({ persistKey: PERSIST_KEY });
462
+
463
+ expect(localStorageMock.setItem).toHaveBeenCalledWith(
464
+ PERSIST_KEY,
465
+ JSON.stringify(["command1"])
466
+ );
467
+ });
468
+
469
+ it("should load from localStorage on init", () => {
470
+ // Pre-populate localStorage
471
+ localStorageMock._setStore({
472
+ [PERSIST_KEY]: JSON.stringify(["saved1", "saved2"]),
473
+ });
474
+
475
+ const result = renderHook({ persistKey: PERSIST_KEY });
476
+
477
+ expect(result.current.history).toEqual(["saved1", "saved2"]);
478
+ });
479
+
480
+ it("should not persist without persistKey", () => {
481
+ const result = renderHook();
482
+
483
+ result.current.addToHistory("command1");
484
+ result.rerender();
485
+
486
+ // setItem should not have been called for history
487
+ expect(localStorageMock.setItem).not.toHaveBeenCalled();
488
+ });
489
+
490
+ it("should clear persisted history when clearHistory called", () => {
491
+ const result = renderHook({ persistKey: PERSIST_KEY });
492
+
493
+ result.current.addToHistory("command1");
494
+ result.rerender({ persistKey: PERSIST_KEY });
495
+ result.current.clearHistory();
496
+ result.rerender({ persistKey: PERSIST_KEY });
497
+
498
+ expect(localStorageMock.setItem).toHaveBeenLastCalledWith(PERSIST_KEY, JSON.stringify([]));
499
+ });
500
+
501
+ it("should handle invalid JSON in localStorage gracefully", () => {
502
+ localStorageMock._setStore({
503
+ [PERSIST_KEY]: "invalid json{",
504
+ });
505
+
506
+ const result = renderHook({ persistKey: PERSIST_KEY });
507
+
508
+ // Should initialize with empty array on parse error
509
+ expect(result.current.history).toEqual([]);
510
+ });
511
+
512
+ it("should filter non-string values from stored history", () => {
513
+ localStorageMock._setStore({
514
+ [PERSIST_KEY]: JSON.stringify(["valid", 123, null, "also-valid"]),
515
+ });
516
+
517
+ const result = renderHook({ persistKey: PERSIST_KEY });
518
+
519
+ expect(result.current.history).toEqual(["valid", "also-valid"]);
520
+ });
521
+
522
+ it("should handle non-array stored value gracefully", () => {
523
+ localStorageMock._setStore({
524
+ [PERSIST_KEY]: JSON.stringify({ not: "array" }),
525
+ });
526
+
527
+ const result = renderHook({ persistKey: PERSIST_KEY });
528
+
529
+ expect(result.current.history).toEqual([]);
530
+ });
531
+
532
+ it("should respect maxItems when adding after loading from storage", () => {
533
+ // Store more items than limit
534
+ const storedItems = Array.from({ length: 10 }, (_, i) => `stored${i + 1}`);
535
+ localStorageMock._setStore({
536
+ [PERSIST_KEY]: JSON.stringify(storedItems),
537
+ });
538
+
539
+ const result = renderHook({ persistKey: PERSIST_KEY, maxItems: 5 });
540
+
541
+ // Initial load has all items from storage
542
+ result.current.addToHistory("new-command");
543
+ result.rerender({ persistKey: PERSIST_KEY, maxItems: 5 });
544
+
545
+ // After adding, should be trimmed to maxItems
546
+ expect(result.current.history.length).toBe(5);
547
+ });
548
+ });
549
+
550
+ // ===========================================================================
551
+ // Edge Cases
552
+ // ===========================================================================
553
+
554
+ describe("Edge cases", () => {
555
+ it("should handle single entry history navigation", () => {
556
+ const result = renderHook();
557
+
558
+ result.current.addToHistory("only-command");
559
+ result.rerender();
560
+
561
+ const upEntry = result.current.navigateHistory("up");
562
+ result.rerender();
563
+ expect(upEntry).toBe("only-command");
564
+
565
+ // Try to go up again (at oldest)
566
+ const upAgain = result.current.navigateHistory("up");
567
+ result.rerender();
568
+ expect(upAgain).toBeNull();
569
+
570
+ // Go down (back to new entry position)
571
+ result.current.navigateHistory("down");
572
+ result.rerender();
573
+ expect(result.current.currentIndex).toBe(-1);
574
+ });
575
+
576
+ it("should handle special characters in entries", () => {
577
+ const result = renderHook();
578
+
579
+ const specialChars = [
580
+ '/help "quoted arg"',
581
+ "command with\nnewline",
582
+ "unicode: 你好世界",
583
+ "symbols: @#$%^&*()",
584
+ ];
585
+
586
+ for (const entry of specialChars) {
587
+ result.current.addToHistory(entry);
588
+ result.rerender();
589
+ }
590
+
591
+ expect(result.current.history).toEqual(specialChars);
592
+ });
593
+
594
+ it("should handle rapid navigation", () => {
595
+ const result = renderHook();
596
+
597
+ for (let i = 1; i <= 10; i++) {
598
+ result.current.addToHistory(`cmd${i}`);
599
+ result.rerender();
600
+ }
601
+
602
+ // Rapid up navigation
603
+ for (let i = 0; i < 15; i++) {
604
+ result.current.navigateHistory("up");
605
+ result.rerender();
606
+ }
607
+
608
+ // Should be at oldest entry (index 0)
609
+ expect(result.current.currentIndex).toBe(0);
610
+ expect(result.current.getCurrentEntry()).toBe("cmd1");
611
+
612
+ // Rapid down navigation
613
+ for (let i = 0; i < 15; i++) {
614
+ result.current.navigateHistory("down");
615
+ result.rerender();
616
+ }
617
+
618
+ // Should be at "new entry" position
619
+ expect(result.current.currentIndex).toBe(-1);
620
+ });
621
+
622
+ it("should handle localStorage unavailable gracefully", () => {
623
+ // Remove localStorage
624
+ Object.defineProperty(globalThis, "localStorage", {
625
+ value: undefined,
626
+ writable: true,
627
+ configurable: true,
628
+ });
629
+
630
+ const result = renderHook({ persistKey: "test-key" });
631
+
632
+ // Should not throw
633
+ result.current.addToHistory("command1");
634
+ result.rerender({ persistKey: "test-key" });
635
+
636
+ expect(result.current.history).toEqual(["command1"]);
637
+ });
638
+
639
+ it("should handle localStorage throwing errors gracefully", () => {
640
+ localStorageMock.getItem.mockImplementation(() => {
641
+ throw new Error("Storage error");
642
+ });
643
+
644
+ // Should not throw and fall back to empty history
645
+ const result = renderHook({ persistKey: "test-key" });
646
+ expect(result.current.history).toEqual([]);
647
+
648
+ // Reset mock for setItem to also throw
649
+ localStorageMock.setItem.mockImplementation(() => {
650
+ throw new Error("Storage error");
651
+ });
652
+
653
+ // Should not throw on add
654
+ result.current.addToHistory("command1");
655
+ result.rerender({ persistKey: "test-key" });
656
+
657
+ expect(result.current.history).toEqual(["command1"]);
658
+ });
659
+ });
660
+ });