@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,374 @@
1
+ /**
2
+ * Autocomplete Component Tests (T011)
3
+ *
4
+ * Tests for the Autocomplete dropdown component.
5
+ */
6
+
7
+ import { render } from "ink-testing-library";
8
+ import type React from "react";
9
+ import { act } from "react";
10
+ import { describe, expect, it, vi } from "vitest";
11
+ import { ThemeProvider } from "../../../theme/index.js";
12
+ import { Autocomplete } from "../Autocomplete.js";
13
+
14
+ /**
15
+ * Wrapper to provide theme context for tests
16
+ */
17
+ function renderWithTheme(element: React.ReactElement) {
18
+ return render(<ThemeProvider>{element}</ThemeProvider>);
19
+ }
20
+
21
+ describe("Autocomplete", () => {
22
+ const defaultOptions = ["/help", "/history", "/hello", "/clear", "/quit"];
23
+
24
+ describe("Rendering", () => {
25
+ it("should render without crashing when visible", () => {
26
+ const onSelect = vi.fn();
27
+ const onCancel = vi.fn();
28
+
29
+ const { lastFrame } = renderWithTheme(
30
+ <Autocomplete
31
+ input="/he"
32
+ options={defaultOptions}
33
+ onSelect={onSelect}
34
+ onCancel={onCancel}
35
+ visible={true}
36
+ />
37
+ );
38
+
39
+ expect(lastFrame()).toBeDefined();
40
+ });
41
+
42
+ it("should render nothing when not visible", () => {
43
+ const onSelect = vi.fn();
44
+ const onCancel = vi.fn();
45
+
46
+ const { lastFrame } = renderWithTheme(
47
+ <Autocomplete
48
+ input="/he"
49
+ options={defaultOptions}
50
+ onSelect={onSelect}
51
+ onCancel={onCancel}
52
+ visible={false}
53
+ />
54
+ );
55
+
56
+ // Should render empty or minimal when not visible
57
+ const frame = lastFrame() ?? "";
58
+ expect(frame.trim()).toBe("");
59
+ });
60
+
61
+ it("should render nothing when no options match", () => {
62
+ const onSelect = vi.fn();
63
+ const onCancel = vi.fn();
64
+
65
+ const { lastFrame } = renderWithTheme(
66
+ <Autocomplete
67
+ input="/xyz"
68
+ options={defaultOptions}
69
+ onSelect={onSelect}
70
+ onCancel={onCancel}
71
+ visible={true}
72
+ />
73
+ );
74
+
75
+ const frame = lastFrame() ?? "";
76
+ expect(frame.trim()).toBe("");
77
+ });
78
+
79
+ it("should render nothing when input is empty", () => {
80
+ const onSelect = vi.fn();
81
+ const onCancel = vi.fn();
82
+
83
+ const { lastFrame } = renderWithTheme(
84
+ <Autocomplete
85
+ input=""
86
+ options={defaultOptions}
87
+ onSelect={onSelect}
88
+ onCancel={onCancel}
89
+ visible={true}
90
+ />
91
+ );
92
+
93
+ const frame = lastFrame() ?? "";
94
+ // Empty input shows all options (useful for '/' picker)
95
+ expect(frame).toContain("help");
96
+ expect(frame).toContain("history");
97
+ });
98
+ });
99
+
100
+ describe("Filtering", () => {
101
+ it("should filter options by prefix (case-insensitive)", () => {
102
+ const onSelect = vi.fn();
103
+ const onCancel = vi.fn();
104
+
105
+ const { lastFrame } = renderWithTheme(
106
+ <Autocomplete
107
+ input="/he"
108
+ options={defaultOptions}
109
+ onSelect={onSelect}
110
+ onCancel={onCancel}
111
+ visible={true}
112
+ />
113
+ );
114
+
115
+ const frame = lastFrame() ?? "";
116
+ // Should show /help and /hello (prefix match)
117
+ expect(frame).toContain("help");
118
+ expect(frame).toContain("hello");
119
+ // Should NOT show /history (different prefix)
120
+ expect(frame).not.toContain("history");
121
+ // Should NOT show /clear or /quit
122
+ expect(frame).not.toContain("clear");
123
+ expect(frame).not.toContain("quit");
124
+ });
125
+
126
+ it("should be case-insensitive when filtering", () => {
127
+ const onSelect = vi.fn();
128
+ const onCancel = vi.fn();
129
+ const options = ["/Help", "/HELLO", "/History"];
130
+
131
+ const { lastFrame } = renderWithTheme(
132
+ <Autocomplete
133
+ input="/he"
134
+ options={options}
135
+ onSelect={onSelect}
136
+ onCancel={onCancel}
137
+ visible={true}
138
+ />
139
+ );
140
+
141
+ const frame = lastFrame() ?? "";
142
+ // Should match regardless of case
143
+ expect(frame).toContain("Help");
144
+ expect(frame).toContain("HELLO");
145
+ });
146
+
147
+ it("should show all matching options when input is a common prefix", () => {
148
+ const onSelect = vi.fn();
149
+ const onCancel = vi.fn();
150
+
151
+ const { lastFrame } = renderWithTheme(
152
+ <Autocomplete
153
+ input="/h"
154
+ options={defaultOptions}
155
+ onSelect={onSelect}
156
+ onCancel={onCancel}
157
+ visible={true}
158
+ />
159
+ );
160
+
161
+ const frame = lastFrame() ?? "";
162
+ // Should show /help, /history, and /hello
163
+ expect(frame).toContain("help");
164
+ expect(frame).toContain("history");
165
+ expect(frame).toContain("hello");
166
+ });
167
+ });
168
+
169
+ describe("maxVisible", () => {
170
+ it("should limit displayed options to maxVisible", () => {
171
+ const onSelect = vi.fn();
172
+ const onCancel = vi.fn();
173
+ // Use equal-length options so fuzzy scoring doesn't reorder them
174
+ const manyOptions = ["/opt1", "/opt2", "/opt3", "/opt4", "/opt5", "/opt6", "/opt7"];
175
+
176
+ const { lastFrame } = renderWithTheme(
177
+ <Autocomplete
178
+ input="/opt"
179
+ options={manyOptions}
180
+ onSelect={onSelect}
181
+ onCancel={onCancel}
182
+ visible={true}
183
+ maxVisible={3}
184
+ />
185
+ );
186
+
187
+ const frame = lastFrame() ?? "";
188
+ // Should show exactly 3 options and overflow indicator
189
+ const optionMatches = frame.match(/opt\d/g) ?? [];
190
+ expect(optionMatches.length).toBe(3);
191
+ // Should show "4 more" indicator
192
+ expect(frame).toContain("4 more");
193
+ });
194
+
195
+ it("should not show overflow indicator when all options fit", () => {
196
+ const onSelect = vi.fn();
197
+ const onCancel = vi.fn();
198
+
199
+ const { lastFrame } = renderWithTheme(
200
+ <Autocomplete
201
+ input="/he"
202
+ options={defaultOptions}
203
+ onSelect={onSelect}
204
+ onCancel={onCancel}
205
+ visible={true}
206
+ maxVisible={5}
207
+ />
208
+ );
209
+
210
+ const frame = lastFrame() ?? "";
211
+ // Only 2 options match (/help, /hello), so no overflow
212
+ expect(frame).not.toContain("more");
213
+ });
214
+
215
+ it("should use default maxVisible of 10", () => {
216
+ const onSelect = vi.fn();
217
+ const onCancel = vi.fn();
218
+ // Need more than 10 options to see overflow
219
+ const manyOptions = [
220
+ "/a1",
221
+ "/a2",
222
+ "/a3",
223
+ "/a4",
224
+ "/a5",
225
+ "/a6",
226
+ "/a7",
227
+ "/a8",
228
+ "/a9",
229
+ "/a10",
230
+ "/a11",
231
+ "/a12",
232
+ "/a13",
233
+ ];
234
+
235
+ const { lastFrame } = renderWithTheme(
236
+ <Autocomplete
237
+ input="/a"
238
+ options={manyOptions}
239
+ onSelect={onSelect}
240
+ onCancel={onCancel}
241
+ visible={true}
242
+ />
243
+ );
244
+
245
+ const frame = lastFrame() ?? "";
246
+ // Default is 10, so should show "3 more" for 13 options
247
+ expect(frame).toContain("3 more");
248
+ });
249
+ });
250
+
251
+ describe("Selection indicator", () => {
252
+ it("should show selection indicator for first item by default", () => {
253
+ const onSelect = vi.fn();
254
+ const onCancel = vi.fn();
255
+
256
+ const { lastFrame } = renderWithTheme(
257
+ <Autocomplete
258
+ input="/he"
259
+ options={defaultOptions}
260
+ onSelect={onSelect}
261
+ onCancel={onCancel}
262
+ visible={true}
263
+ />
264
+ );
265
+
266
+ const frame = lastFrame() ?? "";
267
+ // Should have the selection indicator (›)
268
+ expect(frame).toContain("›");
269
+ });
270
+
271
+ it("keeps the selected option visible when navigating beyond maxVisible", async () => {
272
+ const onSelect = vi.fn();
273
+ const onCancel = vi.fn();
274
+ const manyOptions = ["/a1", "/a2", "/a3", "/a4", "/a5"];
275
+
276
+ const { lastFrame, stdin } = renderWithTheme(
277
+ <Autocomplete
278
+ input=""
279
+ options={manyOptions}
280
+ onSelect={onSelect}
281
+ onCancel={onCancel}
282
+ visible={true}
283
+ maxVisible={2}
284
+ />
285
+ );
286
+
287
+ // Initially shows first window
288
+ expect(lastFrame()).toContain("a1");
289
+ expect(lastFrame()).toContain("a2");
290
+ expect(lastFrame()).toContain("›");
291
+
292
+ // Move selection down 3 times: should scroll window and keep highlight visible
293
+ await act(async () => {
294
+ stdin.write("\u001b[B");
295
+ await new Promise((r) => setTimeout(r, 0));
296
+ });
297
+ await act(async () => {
298
+ stdin.write("\u001b[B");
299
+ await new Promise((r) => setTimeout(r, 0));
300
+ });
301
+ await act(async () => {
302
+ stdin.write("\u001b[B");
303
+ await new Promise((r) => setTimeout(r, 0));
304
+ });
305
+
306
+ const frame = lastFrame() ?? "";
307
+
308
+ // With maxVisible=2 and selection index=3, the window should include a3 and a4
309
+ expect(frame).toContain("a3");
310
+ expect(frame).toContain("a4");
311
+ // Ensure highlight indicator never disappears
312
+ expect(frame).toContain("›");
313
+ });
314
+ });
315
+
316
+ describe("Props interface", () => {
317
+ it("should accept all required props", () => {
318
+ const onSelect = vi.fn();
319
+ const onCancel = vi.fn();
320
+
321
+ const { lastFrame } = renderWithTheme(
322
+ <Autocomplete
323
+ input="/test"
324
+ options={["/test1", "/test2"]}
325
+ onSelect={onSelect}
326
+ onCancel={onCancel}
327
+ />
328
+ );
329
+
330
+ expect(lastFrame()).toBeTruthy();
331
+ });
332
+
333
+ it("should accept all optional props", () => {
334
+ const onSelect = vi.fn();
335
+ const onCancel = vi.fn();
336
+
337
+ const { lastFrame } = renderWithTheme(
338
+ <Autocomplete
339
+ input="/test"
340
+ options={["/test1", "/test2"]}
341
+ onSelect={onSelect}
342
+ onCancel={onCancel}
343
+ visible={true}
344
+ maxVisible={10}
345
+ />
346
+ );
347
+
348
+ expect(lastFrame()).toBeTruthy();
349
+ });
350
+ });
351
+
352
+ describe("Border styling", () => {
353
+ it("should render with a bordered box", () => {
354
+ const onSelect = vi.fn();
355
+ const onCancel = vi.fn();
356
+
357
+ const { lastFrame } = renderWithTheme(
358
+ <Autocomplete
359
+ input="/he"
360
+ options={defaultOptions}
361
+ onSelect={onSelect}
362
+ onCancel={onCancel}
363
+ visible={true}
364
+ />
365
+ );
366
+
367
+ const frame = lastFrame() ?? "";
368
+ // Ink's Box with borderStyle="single" uses unicode box characters
369
+ // Check that content is displayed (border characters may vary)
370
+ expect(frame.length).toBeGreaterThan(0);
371
+ expect(frame).toContain("help");
372
+ });
373
+ });
374
+ });
@@ -0,0 +1,241 @@
1
+ /**
2
+ * TextInput Component Tests (T009, T013)
3
+ *
4
+ * Tests for the TextInput component with multiline support.
5
+ *
6
+ * Note: ink-testing-library's stdin.write() does not synchronously trigger
7
+ * useInput hooks. Tests focus on:
8
+ * - Rendering behavior (verifiable via lastFrame())
9
+ * - Props contract verification
10
+ * - Visual state assertions
11
+ *
12
+ * Behavioral tests (onChange, onSubmit) are validated via:
13
+ * - Integration tests at the CLI level
14
+ * - Manual testing documentation
15
+ */
16
+
17
+ import { render } from "ink-testing-library";
18
+ import type React from "react";
19
+ import { describe, expect, it, vi } from "vitest";
20
+ import { ThemeProvider } from "../../../theme/index.js";
21
+ import { TextInput } from "../TextInput.js";
22
+
23
+ /**
24
+ * Wrapper to provide theme context for tests
25
+ */
26
+ function renderWithTheme(element: React.ReactElement) {
27
+ return render(<ThemeProvider>{element}</ThemeProvider>);
28
+ }
29
+
30
+ describe("TextInput", () => {
31
+ describe("Rendering", () => {
32
+ it("should render without crashing", () => {
33
+ const onChange = vi.fn();
34
+ const { lastFrame } = renderWithTheme(<TextInput value="" onChange={onChange} />);
35
+ // Empty value with focus shows cursor, which renders as a space with inverse
36
+ expect(lastFrame()).toBeDefined();
37
+ });
38
+
39
+ it("should render placeholder when value is empty", () => {
40
+ const onChange = vi.fn();
41
+ const { lastFrame } = renderWithTheme(
42
+ <TextInput value="" onChange={onChange} placeholder="Type here..." focused={false} />
43
+ );
44
+ expect(lastFrame()).toContain("Type here...");
45
+ });
46
+
47
+ it("should render value when provided", () => {
48
+ const onChange = vi.fn();
49
+ const { lastFrame } = renderWithTheme(
50
+ <TextInput value="Hello World" onChange={onChange} focused={false} />
51
+ );
52
+ expect(lastFrame()).toContain("Hello World");
53
+ });
54
+
55
+ it("should show cursor indicator when focused", () => {
56
+ const onChange = vi.fn();
57
+ const { lastFrame } = renderWithTheme(
58
+ <TextInput value="Test" onChange={onChange} focused={true} />
59
+ );
60
+ // The cursor should be at the end, shown as inverse text
61
+ const frame = lastFrame() ?? "";
62
+ expect(frame).toContain("Test");
63
+ });
64
+ });
65
+
66
+ describe("Multiline mode", () => {
67
+ it("should render multiple lines", () => {
68
+ const onChange = vi.fn();
69
+ const { lastFrame } = renderWithTheme(
70
+ <TextInput value={"Line 1\nLine 2\nLine 3"} onChange={onChange} multiline focused={false} />
71
+ );
72
+ const frame = lastFrame() ?? "";
73
+ expect(frame).toContain("Line 1");
74
+ expect(frame).toContain("Line 2");
75
+ expect(frame).toContain("Line 3");
76
+ });
77
+
78
+ it("should render cursor on correct line in multiline mode", () => {
79
+ const onChange = vi.fn();
80
+ const { lastFrame } = renderWithTheme(
81
+ <TextInput value={"First\nSecond"} onChange={onChange} multiline focused={true} />
82
+ );
83
+ const frame = lastFrame() ?? "";
84
+ expect(frame).toContain("First");
85
+ expect(frame).toContain("Second");
86
+ });
87
+ });
88
+
89
+ describe("Props contract", () => {
90
+ it("should accept all required props", () => {
91
+ const onChange = vi.fn();
92
+ const onSubmit = vi.fn();
93
+
94
+ const { lastFrame } = renderWithTheme(
95
+ <TextInput
96
+ value="test"
97
+ onChange={onChange}
98
+ onSubmit={onSubmit}
99
+ placeholder="placeholder"
100
+ multiline={true}
101
+ disabled={false}
102
+ maxLength={100}
103
+ focused={true}
104
+ />
105
+ );
106
+
107
+ expect(lastFrame()).toBeTruthy();
108
+ });
109
+
110
+ it("should render with disabled state", () => {
111
+ const onChange = vi.fn();
112
+ const { lastFrame } = renderWithTheme(
113
+ <TextInput value="disabled text" onChange={onChange} disabled={true} />
114
+ );
115
+ const frame = lastFrame() ?? "";
116
+ expect(frame).toContain("disabled text");
117
+ });
118
+
119
+ it("should respect maxLength visually (value can be at max)", () => {
120
+ const onChange = vi.fn();
121
+ const { lastFrame } = renderWithTheme(
122
+ <TextInput value="12345" onChange={onChange} maxLength={5} focused={false} />
123
+ );
124
+ expect(lastFrame()).toContain("12345");
125
+ });
126
+ });
127
+
128
+ describe("onChange callback contract", () => {
129
+ it("should accept onChange prop as function", () => {
130
+ const onChange = vi.fn();
131
+ // This verifies the prop type contract
132
+ expect(() => {
133
+ renderWithTheme(<TextInput value="" onChange={onChange} />);
134
+ }).not.toThrow();
135
+ });
136
+
137
+ it("should not throw when onChange is called with new value", () => {
138
+ // Simulates controlled component pattern
139
+ let currentValue = "";
140
+ const onChange = vi.fn((newValue: string) => {
141
+ currentValue = newValue;
142
+ });
143
+
144
+ const { rerender } = renderWithTheme(<TextInput value={currentValue} onChange={onChange} />);
145
+
146
+ // Simulate external value change (as would happen from onChange callback)
147
+ currentValue = "typed text";
148
+ expect(() => {
149
+ rerender(
150
+ <ThemeProvider>
151
+ <TextInput value={currentValue} onChange={onChange} />
152
+ </ThemeProvider>
153
+ );
154
+ }).not.toThrow();
155
+ });
156
+ });
157
+
158
+ describe("onSubmit callback contract", () => {
159
+ it("should accept onSubmit prop as function", () => {
160
+ const onChange = vi.fn();
161
+ const onSubmit = vi.fn();
162
+ expect(() => {
163
+ renderWithTheme(<TextInput value="" onChange={onChange} onSubmit={onSubmit} />);
164
+ }).not.toThrow();
165
+ });
166
+
167
+ it("should work without onSubmit (optional prop)", () => {
168
+ const onChange = vi.fn();
169
+ expect(() => {
170
+ renderWithTheme(<TextInput value="" onChange={onChange} />);
171
+ }).not.toThrow();
172
+ });
173
+ });
174
+
175
+ describe("Disabled state contract", () => {
176
+ it("should accept disabled prop", () => {
177
+ const onChange = vi.fn();
178
+ expect(() => {
179
+ renderWithTheme(<TextInput value="test" onChange={onChange} disabled={true} />);
180
+ }).not.toThrow();
181
+ });
182
+
183
+ it("should render differently when disabled vs enabled", () => {
184
+ const onChange = vi.fn();
185
+
186
+ const { lastFrame: enabledFrame } = renderWithTheme(
187
+ <TextInput value="test" onChange={onChange} disabled={false} focused={true} />
188
+ );
189
+
190
+ const { lastFrame: disabledFrame } = renderWithTheme(
191
+ <TextInput value="test" onChange={onChange} disabled={true} focused={true} />
192
+ );
193
+
194
+ // Both should render, component handles disabled state
195
+ expect(enabledFrame()).toBeTruthy();
196
+ expect(disabledFrame()).toBeTruthy();
197
+ });
198
+ });
199
+
200
+ describe("Multiline mode contract", () => {
201
+ it("should accept multiline prop", () => {
202
+ const onChange = vi.fn();
203
+ expect(() => {
204
+ renderWithTheme(<TextInput value="test" onChange={onChange} multiline={true} />);
205
+ }).not.toThrow();
206
+ });
207
+
208
+ it("should handle newlines in value when multiline", () => {
209
+ const onChange = vi.fn();
210
+ const { lastFrame } = renderWithTheme(
211
+ <TextInput
212
+ value="line1\nline2\nline3"
213
+ onChange={onChange}
214
+ multiline={true}
215
+ focused={false}
216
+ />
217
+ );
218
+ const frame = lastFrame() ?? "";
219
+ // All lines should be present
220
+ expect(frame).toContain("line1");
221
+ expect(frame).toContain("line2");
222
+ expect(frame).toContain("line3");
223
+ });
224
+ });
225
+
226
+ describe("Focus state", () => {
227
+ it("should accept focused prop", () => {
228
+ const onChange = vi.fn();
229
+ expect(() => {
230
+ renderWithTheme(<TextInput value="test" onChange={onChange} focused={true} />);
231
+ }).not.toThrow();
232
+ });
233
+
234
+ it("should default focused to true", () => {
235
+ const onChange = vi.fn();
236
+ // Default prop behavior - should render with cursor
237
+ const { lastFrame } = renderWithTheme(<TextInput value="test" onChange={onChange} />);
238
+ expect(lastFrame()).toBeTruthy();
239
+ });
240
+ });
241
+ });