@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,252 @@
1
+ /**
2
+ * SessionListPanel Component (T056)
3
+ *
4
+ * Displays a scrollable list of sessions with keyboard navigation.
5
+ *
6
+ * @module tui/components/session/SessionListPanel
7
+ */
8
+
9
+ import { Box, Text, useInput } from "ink";
10
+ import type React from "react";
11
+ import { useCallback, useMemo, useState } from "react";
12
+ import { useTheme } from "../../theme/index.js";
13
+ import { SessionItem } from "./SessionItem.js";
14
+ import type { SessionListPanelProps } from "./types.js";
15
+
16
+ // =============================================================================
17
+ // Constants
18
+ // =============================================================================
19
+
20
+ /** Default maximum height for the session list */
21
+ const DEFAULT_MAX_HEIGHT = 10;
22
+
23
+ /** Number of sessions to skip when using page up/down */
24
+ const PAGE_SIZE = 5;
25
+
26
+ // =============================================================================
27
+ // Helper Functions
28
+ // =============================================================================
29
+
30
+ /**
31
+ * Calculate visible sessions based on scroll position and max height.
32
+ */
33
+ function getVisibleSessions<T>(
34
+ sessions: readonly T[],
35
+ scrollOffset: number,
36
+ maxVisible: number
37
+ ): readonly T[] {
38
+ return sessions.slice(scrollOffset, scrollOffset + maxVisible);
39
+ }
40
+
41
+ // =============================================================================
42
+ // Main Component
43
+ // =============================================================================
44
+
45
+ /**
46
+ * SessionListPanel displays a scrollable list of sessions.
47
+ *
48
+ * Features:
49
+ * - j/k or arrow keys for navigation
50
+ * - Page up/down support
51
+ * - Home/End to jump to first/last
52
+ * - Visual scroll indicators
53
+ * - Highlights selected and active sessions
54
+ *
55
+ * @example
56
+ * ```tsx
57
+ * <SessionListPanel
58
+ * sessions={sessionList}
59
+ * selectedSessionId="sess-1"
60
+ * activeSessionId="sess-2"
61
+ * onSelectSession={(id) => handleSelect(id)}
62
+ * maxHeight={10}
63
+ * isFocused={true}
64
+ * />
65
+ * ```
66
+ */
67
+ export function SessionListPanel({
68
+ sessions,
69
+ selectedSessionId,
70
+ activeSessionId,
71
+ onSelectSession,
72
+ maxHeight = DEFAULT_MAX_HEIGHT,
73
+ isFocused = true,
74
+ }: SessionListPanelProps): React.JSX.Element {
75
+ const { theme } = useTheme();
76
+
77
+ // Track the index of the selected session
78
+ const [selectedIndex, setSelectedIndex] = useState(() => {
79
+ if (selectedSessionId) {
80
+ const index = sessions.findIndex((s) => s.id === selectedSessionId);
81
+ return index >= 0 ? index : 0;
82
+ }
83
+ return 0;
84
+ });
85
+
86
+ // Track scroll offset for virtualization
87
+ const [scrollOffset, setScrollOffset] = useState(0);
88
+
89
+ // Calculate visible items (account for header)
90
+ const maxVisible = Math.max(1, maxHeight - 2);
91
+
92
+ // Get visible sessions based on scroll
93
+ const visibleSessions = useMemo(
94
+ () => getVisibleSessions(sessions, scrollOffset, maxVisible),
95
+ [sessions, scrollOffset, maxVisible]
96
+ );
97
+
98
+ // Check if we can scroll
99
+ const canScrollUp = scrollOffset > 0;
100
+ const canScrollDown = scrollOffset + maxVisible < sessions.length;
101
+
102
+ /**
103
+ * Navigate to a specific index, adjusting scroll as needed.
104
+ */
105
+ const navigateToIndex = useCallback(
106
+ (newIndex: number) => {
107
+ // Clamp index to valid range
108
+ const clampedIndex = Math.max(0, Math.min(newIndex, sessions.length - 1));
109
+ setSelectedIndex(clampedIndex);
110
+
111
+ // Adjust scroll to keep selection visible
112
+ if (clampedIndex < scrollOffset) {
113
+ setScrollOffset(clampedIndex);
114
+ } else if (clampedIndex >= scrollOffset + maxVisible) {
115
+ setScrollOffset(clampedIndex - maxVisible + 1);
116
+ }
117
+
118
+ // Notify parent of selection change
119
+ if (onSelectSession && sessions[clampedIndex]) {
120
+ onSelectSession(sessions[clampedIndex].id);
121
+ }
122
+ },
123
+ [sessions, scrollOffset, maxVisible, onSelectSession]
124
+ );
125
+
126
+ /**
127
+ * Check if key matches navigation down.
128
+ */
129
+ const isNavigateDown = (input: string, key: { downArrow: boolean }) =>
130
+ input === "j" || key.downArrow;
131
+
132
+ /**
133
+ * Check if key matches navigation up.
134
+ */
135
+ const isNavigateUp = (input: string, key: { upArrow: boolean }) => input === "k" || key.upArrow;
136
+
137
+ /**
138
+ * Check if key matches page navigation.
139
+ */
140
+ const isPageNav = (
141
+ input: string,
142
+ key: { pageDown?: boolean; pageUp?: boolean; ctrl?: boolean }
143
+ ): "down" | "up" | null => {
144
+ if (key.pageDown || (key.ctrl && input === "d")) return "down";
145
+ if (key.pageUp || (key.ctrl && input === "u")) return "up";
146
+ return null;
147
+ };
148
+
149
+ // Handle keyboard input
150
+ useInput(
151
+ (input, key) => {
152
+ if (isNavigateDown(input, key)) {
153
+ navigateToIndex(selectedIndex + 1);
154
+ return;
155
+ }
156
+ if (isNavigateUp(input, key)) {
157
+ navigateToIndex(selectedIndex - 1);
158
+ return;
159
+ }
160
+ const pageDir = isPageNav(input, key);
161
+ if (pageDir === "down") {
162
+ navigateToIndex(selectedIndex + PAGE_SIZE);
163
+ return;
164
+ }
165
+ if (pageDir === "up") {
166
+ navigateToIndex(selectedIndex - PAGE_SIZE);
167
+ return;
168
+ }
169
+ // g/G: vim-style jump to first/last
170
+ if (input === "g") {
171
+ navigateToIndex(0);
172
+ return;
173
+ }
174
+ if (input === "G") {
175
+ navigateToIndex(sessions.length - 1);
176
+ }
177
+ },
178
+ { isActive: isFocused }
179
+ );
180
+
181
+ const textColor = theme.semantic.text.primary;
182
+ const mutedColor = theme.semantic.text.muted;
183
+ const borderColor = theme.semantic.border.default;
184
+
185
+ // Empty state
186
+ if (sessions.length === 0) {
187
+ return (
188
+ <Box
189
+ flexDirection="column"
190
+ borderStyle="single"
191
+ borderColor={borderColor}
192
+ paddingX={1}
193
+ height={maxHeight}
194
+ >
195
+ <Text color={textColor} bold>
196
+ Sessions
197
+ </Text>
198
+ <Box flexGrow={1} justifyContent="center" alignItems="center">
199
+ <Text color={mutedColor} italic>
200
+ No sessions found
201
+ </Text>
202
+ </Box>
203
+ </Box>
204
+ );
205
+ }
206
+
207
+ return (
208
+ <Box
209
+ flexDirection="column"
210
+ borderStyle="single"
211
+ borderColor={borderColor}
212
+ paddingX={1}
213
+ height={maxHeight}
214
+ >
215
+ {/* Header with scroll indicators */}
216
+ <Box flexDirection="row" justifyContent="space-between">
217
+ <Text color={textColor} bold>
218
+ Sessions ({sessions.length})
219
+ </Text>
220
+ <Box flexDirection="row" gap={1}>
221
+ {canScrollUp && <Text color={mutedColor}>↑</Text>}
222
+ {canScrollDown && <Text color={mutedColor}>↓</Text>}
223
+ </Box>
224
+ </Box>
225
+
226
+ {/* Session list */}
227
+ <Box flexDirection="column" flexGrow={1} overflow="hidden">
228
+ {visibleSessions.map((session, visibleIndex) => {
229
+ const actualIndex = scrollOffset + visibleIndex;
230
+ return (
231
+ <SessionItem
232
+ key={session.id}
233
+ session={session}
234
+ isSelected={actualIndex === selectedIndex}
235
+ isActive={session.id === activeSessionId}
236
+ onSelect={onSelectSession}
237
+ />
238
+ );
239
+ })}
240
+ </Box>
241
+
242
+ {/* Footer with navigation hints */}
243
+ <Box marginTop={0}>
244
+ <Text color={mutedColor} dimColor>
245
+ j/k: navigate • Enter: select • g/G: first/last
246
+ </Text>
247
+ </Box>
248
+ </Box>
249
+ );
250
+ }
251
+
252
+ export default SessionListPanel;
@@ -0,0 +1,449 @@
1
+ /**
2
+ * SessionPicker Component (T056)
3
+ *
4
+ * Modal dialog for selecting a session from the session list.
5
+ * Triggered by Ctrl+S keybinding.
6
+ *
7
+ * @module tui/components/session/SessionPicker
8
+ */
9
+
10
+ import { getIcons } from "@vellum/shared";
11
+ import { Box, Text, useInput } from "ink";
12
+ import type React from "react";
13
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
14
+ import { useTheme } from "../../theme/index.js";
15
+ import { truncateToDisplayWidth } from "../../utils/index.js";
16
+ import { SessionPreview } from "./SessionPreview.js";
17
+ import type { SessionMetadata, SessionPickerProps, SessionPreviewMessage } from "./types.js";
18
+
19
+ // =============================================================================
20
+ // Constants
21
+ // =============================================================================
22
+
23
+ /** Maximum height for the session list */
24
+ const LIST_MAX_HEIGHT = 12;
25
+
26
+ /** Maximum height for the preview panel */
27
+ const PREVIEW_MAX_HEIGHT = 6;
28
+
29
+ /** Page size for navigation */
30
+ const PAGE_SIZE = 5;
31
+
32
+ // =============================================================================
33
+ // Helper Functions
34
+ // =============================================================================
35
+
36
+ /**
37
+ * Format timestamp for session item display.
38
+ */
39
+ function formatTimestamp(date: Date): string {
40
+ const now = new Date();
41
+ const isToday =
42
+ date.getDate() === now.getDate() &&
43
+ date.getMonth() === now.getMonth() &&
44
+ date.getFullYear() === now.getFullYear();
45
+
46
+ if (isToday) {
47
+ return date.toLocaleTimeString(undefined, {
48
+ hour: "2-digit",
49
+ minute: "2-digit",
50
+ });
51
+ }
52
+
53
+ return date.toLocaleDateString(undefined, {
54
+ month: "short",
55
+ day: "numeric",
56
+ });
57
+ }
58
+
59
+ /**
60
+ * Truncate text with ellipsis.
61
+ * Uses string-width for accurate CJK/Emoji handling.
62
+ */
63
+ function truncateText(text: string, maxLength: number): string {
64
+ return truncateToDisplayWidth(text, maxLength);
65
+ }
66
+
67
+ /**
68
+ * Build a minimal preview from session metadata.
69
+ * Used as a graceful fallback when message preview data is unavailable.
70
+ */
71
+ function buildFallbackPreviewMessages(session: SessionMetadata): SessionPreviewMessage[] {
72
+ const messages: SessionPreviewMessage[] = [];
73
+
74
+ messages.push({
75
+ id: `${session.id}-preview-fallback-title`,
76
+ role: "user",
77
+ content: session.title,
78
+ timestamp: new Date(Math.max(0, session.timestamp.getTime() - 60000)),
79
+ });
80
+
81
+ if (session.lastMessage) {
82
+ messages.push({
83
+ id: `${session.id}-preview-fallback-last`,
84
+ role: "assistant",
85
+ content: session.lastMessage,
86
+ timestamp: session.timestamp,
87
+ });
88
+ }
89
+
90
+ return messages;
91
+ }
92
+
93
+ // =============================================================================
94
+ // Sub-Components
95
+ // =============================================================================
96
+
97
+ /**
98
+ * Session list item in the picker.
99
+ */
100
+ interface PickerItemProps {
101
+ readonly session: SessionMetadata;
102
+ readonly isSelected: boolean;
103
+ readonly isActive: boolean;
104
+ readonly primaryColor: string;
105
+ readonly textColor: string;
106
+ readonly mutedColor: string;
107
+ readonly successColor: string;
108
+ }
109
+
110
+ function PickerItem({
111
+ session,
112
+ isSelected,
113
+ isActive,
114
+ primaryColor,
115
+ textColor,
116
+ mutedColor,
117
+ successColor,
118
+ }: PickerItemProps): React.JSX.Element {
119
+ const indicator = isSelected ? "▶" : isActive ? "●" : " ";
120
+ const indicatorColor = isSelected ? primaryColor : isActive ? successColor : mutedColor;
121
+ const displayTitle = truncateText(session.title, 35);
122
+
123
+ return (
124
+ <Box flexDirection="row" paddingX={1}>
125
+ <Text color={indicatorColor}>{indicator} </Text>
126
+ <Box flexDirection="row" justifyContent="space-between" flexGrow={1}>
127
+ <Text color={isSelected ? primaryColor : textColor} bold={isSelected}>
128
+ {displayTitle}
129
+ </Text>
130
+ <Box flexDirection="row" gap={1}>
131
+ <Text dimColor>({session.messageCount})</Text>
132
+ <Text color={mutedColor}>{formatTimestamp(session.timestamp)}</Text>
133
+ </Box>
134
+ </Box>
135
+ </Box>
136
+ );
137
+ }
138
+
139
+ // =============================================================================
140
+ // Main Component
141
+ // =============================================================================
142
+
143
+ /**
144
+ * SessionPicker is a modal dialog for selecting sessions.
145
+ *
146
+ * Features:
147
+ * - j/k or arrow keys for navigation
148
+ * - Enter to select session
149
+ * - Escape or q to close
150
+ * - Preview panel shows selected session messages
151
+ * - Page up/down for faster navigation
152
+ * - g/G to jump to first/last
153
+ *
154
+ * @example
155
+ * ```tsx
156
+ * <SessionPicker
157
+ * sessions={sessionList}
158
+ * activeSessionId="current-session"
159
+ * onSelect={(id) => switchToSession(id)}
160
+ * onClose={() => setPickerOpen(false)}
161
+ * isOpen={pickerOpen}
162
+ * />
163
+ * ```
164
+ */
165
+ export function SessionPicker({
166
+ sessions,
167
+ activeSessionId,
168
+ loadPreviewMessages,
169
+ onSelect,
170
+ onClose,
171
+ isOpen,
172
+ }: SessionPickerProps): React.JSX.Element | null {
173
+ const { theme } = useTheme();
174
+
175
+ // Track selection index
176
+ const [selectedIndex, setSelectedIndex] = useState(0);
177
+
178
+ // Track scroll offset
179
+ const [scrollOffset, setScrollOffset] = useState(0);
180
+
181
+ // Prevent double-handling of input
182
+ const handledRef = useRef(false);
183
+
184
+ // Calculate visible sessions
185
+ const maxVisible = LIST_MAX_HEIGHT - 4; // Account for header, footer, borders
186
+
187
+ const visibleSessions = useMemo(
188
+ () => sessions.slice(scrollOffset, scrollOffset + maxVisible),
189
+ [sessions, scrollOffset, maxVisible]
190
+ );
191
+
192
+ // Get selected session
193
+ const selectedSession = sessions[selectedIndex];
194
+
195
+ // Preview messages cache and state (performance: cache per session id)
196
+ const previewCacheRef = useRef<Map<string, readonly SessionPreviewMessage[]>>(new Map());
197
+ const [previewMessages, setPreviewMessages] = useState<readonly SessionPreviewMessage[]>([]);
198
+ const previewRequestIdRef = useRef(0);
199
+
200
+ // Keep preview messages in sync with selected session.
201
+ // Uses cache when available; otherwise loads via loadPreviewMessages and stores per id.
202
+ useEffect(() => {
203
+ if (!isOpen) {
204
+ return;
205
+ }
206
+
207
+ if (!selectedSession) {
208
+ setPreviewMessages([]);
209
+ return;
210
+ }
211
+
212
+ const cached = previewCacheRef.current.get(selectedSession.id);
213
+ if (cached) {
214
+ setPreviewMessages(cached);
215
+ return;
216
+ }
217
+
218
+ previewRequestIdRef.current += 1;
219
+ const requestId = previewRequestIdRef.current;
220
+
221
+ const load = async () => {
222
+ if (!loadPreviewMessages) {
223
+ const fallback = buildFallbackPreviewMessages(selectedSession);
224
+ previewCacheRef.current.set(selectedSession.id, fallback);
225
+ if (previewRequestIdRef.current === requestId) {
226
+ setPreviewMessages(fallback);
227
+ }
228
+ return;
229
+ }
230
+
231
+ try {
232
+ const loaded = await loadPreviewMessages(selectedSession.id);
233
+ const messages = loaded ?? buildFallbackPreviewMessages(selectedSession);
234
+ previewCacheRef.current.set(selectedSession.id, messages);
235
+ if (previewRequestIdRef.current === requestId) {
236
+ setPreviewMessages(messages);
237
+ }
238
+ } catch {
239
+ const fallback = buildFallbackPreviewMessages(selectedSession);
240
+ previewCacheRef.current.set(selectedSession.id, fallback);
241
+ if (previewRequestIdRef.current === requestId) {
242
+ setPreviewMessages(fallback);
243
+ }
244
+ }
245
+ };
246
+
247
+ void load();
248
+ }, [isOpen, selectedSession, loadPreviewMessages]);
249
+
250
+ // Scroll indicators
251
+ const canScrollUp = scrollOffset > 0;
252
+ const canScrollDown = scrollOffset + maxVisible < sessions.length;
253
+
254
+ /**
255
+ * Navigate to a specific index.
256
+ */
257
+ const navigateToIndex = useCallback(
258
+ (newIndex: number) => {
259
+ const clampedIndex = Math.max(0, Math.min(newIndex, sessions.length - 1));
260
+ setSelectedIndex(clampedIndex);
261
+
262
+ // Adjust scroll to keep selection visible
263
+ if (clampedIndex < scrollOffset) {
264
+ setScrollOffset(clampedIndex);
265
+ } else if (clampedIndex >= scrollOffset + maxVisible) {
266
+ setScrollOffset(clampedIndex - maxVisible + 1);
267
+ }
268
+ },
269
+ [sessions.length, scrollOffset, maxVisible]
270
+ );
271
+
272
+ /**
273
+ * Handle close action with double-handling prevention.
274
+ */
275
+ const handleClose = useCallback(() => {
276
+ if (handledRef.current) return false;
277
+ handledRef.current = true;
278
+ onClose();
279
+ setTimeout(() => {
280
+ handledRef.current = false;
281
+ }, 0);
282
+ return true;
283
+ }, [onClose]);
284
+
285
+ /**
286
+ * Handle select action with double-handling prevention.
287
+ */
288
+ const handleSelect = useCallback(() => {
289
+ if (handledRef.current || !selectedSession) return false;
290
+ handledRef.current = true;
291
+ onSelect(selectedSession.id);
292
+ setTimeout(() => {
293
+ handledRef.current = false;
294
+ }, 0);
295
+ return true;
296
+ }, [onSelect, selectedSession]);
297
+
298
+ /**
299
+ * Check navigation direction from key input.
300
+ */
301
+ const getNavDirection = (
302
+ input: string,
303
+ key: {
304
+ downArrow: boolean;
305
+ upArrow: boolean;
306
+ pageDown?: boolean;
307
+ pageUp?: boolean;
308
+ ctrl?: boolean;
309
+ }
310
+ ): number | null => {
311
+ if (input === "j" || key.downArrow) return 1;
312
+ if (input === "k" || key.upArrow) return -1;
313
+ if (key.pageDown || (key.ctrl && input === "d")) return PAGE_SIZE;
314
+ if (key.pageUp || (key.ctrl && input === "u")) return -PAGE_SIZE;
315
+ if (input === "g") return -selectedIndex; // Jump to 0
316
+ if (input === "G") return sessions.length - 1 - selectedIndex; // Jump to end
317
+ return null;
318
+ };
319
+
320
+ // Handle keyboard input
321
+ useInput(
322
+ (input, key) => {
323
+ if (handledRef.current) return;
324
+
325
+ // Close: Escape or q
326
+ if (key.escape || input.toLowerCase() === "q") {
327
+ handleClose();
328
+ return;
329
+ }
330
+
331
+ // Select: Enter
332
+ if (key.return) {
333
+ handleSelect();
334
+ return;
335
+ }
336
+
337
+ // Navigation
338
+ const delta = getNavDirection(input, key);
339
+ if (delta !== null) {
340
+ navigateToIndex(selectedIndex + delta);
341
+ }
342
+ },
343
+ { isActive: isOpen }
344
+ );
345
+
346
+ // Don't render if not open
347
+ if (!isOpen) {
348
+ return null;
349
+ }
350
+
351
+ const textColor = theme.semantic.text.primary;
352
+ const mutedColor = theme.semantic.text.muted;
353
+ const primaryColor = theme.colors.primary;
354
+ const successColor = theme.colors.success;
355
+
356
+ // Empty state
357
+ if (sessions.length === 0) {
358
+ return (
359
+ <Box
360
+ flexDirection="column"
361
+ borderStyle="double"
362
+ borderColor={primaryColor}
363
+ paddingX={2}
364
+ paddingY={1}
365
+ >
366
+ <Text color={textColor} bold>
367
+ {getIcons().plan} Select Session
368
+ </Text>
369
+ <Box marginY={1}>
370
+ <Text color={mutedColor} italic>
371
+ No sessions available
372
+ </Text>
373
+ </Box>
374
+ <Text color={mutedColor} dimColor>
375
+ Press <Text bold>Esc</Text> or <Text bold>q</Text> to close
376
+ </Text>
377
+ </Box>
378
+ );
379
+ }
380
+
381
+ return (
382
+ <Box flexDirection="column">
383
+ {/* Main modal */}
384
+ <Box
385
+ flexDirection="column"
386
+ borderStyle="double"
387
+ borderColor={primaryColor}
388
+ paddingX={1}
389
+ paddingY={0}
390
+ >
391
+ {/* Header */}
392
+ <Box flexDirection="row" justifyContent="space-between" marginBottom={1}>
393
+ <Text color={textColor} bold>
394
+ {getIcons().plan} Select Session ({sessions.length})
395
+ </Text>
396
+ <Box flexDirection="row" gap={1}>
397
+ {canScrollUp && <Text color={mutedColor}>↑</Text>}
398
+ {canScrollDown && <Text color={mutedColor}>↓</Text>}
399
+ </Box>
400
+ </Box>
401
+
402
+ {/* Session list */}
403
+ <Box flexDirection="column">
404
+ {visibleSessions.map((session, visibleIndex) => {
405
+ const actualIndex = scrollOffset + visibleIndex;
406
+ return (
407
+ <PickerItem
408
+ key={session.id}
409
+ session={session}
410
+ isSelected={actualIndex === selectedIndex}
411
+ isActive={session.id === activeSessionId}
412
+ primaryColor={primaryColor}
413
+ textColor={textColor}
414
+ mutedColor={mutedColor}
415
+ successColor={successColor}
416
+ />
417
+ );
418
+ })}
419
+ </Box>
420
+
421
+ {/* Footer with keybindings */}
422
+ <Box marginTop={1} flexDirection="row" gap={2}>
423
+ <Text color={mutedColor} dimColor>
424
+ <Text bold>j/k</Text> navigate
425
+ </Text>
426
+ <Text color={mutedColor} dimColor>
427
+ <Text bold>Enter</Text> select
428
+ </Text>
429
+ <Text color={mutedColor} dimColor>
430
+ <Text bold>Esc/q</Text> close
431
+ </Text>
432
+ </Box>
433
+ </Box>
434
+
435
+ {/* Preview panel */}
436
+ {selectedSession && (
437
+ <Box marginTop={1}>
438
+ <SessionPreview
439
+ messages={previewMessages}
440
+ title={selectedSession.title}
441
+ maxHeight={PREVIEW_MAX_HEIGHT}
442
+ />
443
+ </Box>
444
+ )}
445
+ </Box>
446
+ );
447
+ }
448
+
449
+ export default SessionPicker;