@oh-my-pi/pi-tui 18.4.2 → 18.4.4

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 (463) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/dist/types/apps/cleanse-board.d.ts +17 -0
  3. package/dist/types/apps/git/help.d.ts +16 -0
  4. package/dist/types/apps/git/sidebar.d.ts +18 -0
  5. package/dist/types/apps/live-visualizer.d.ts +10 -0
  6. package/dist/types/apps/ps-top.d.ts +15 -0
  7. package/dist/types/apps/standalone-picker.d.ts +3 -0
  8. package/dist/types/autocomplete.d.ts +8 -0
  9. package/dist/types/chat/advisor-message.d.ts +12 -2
  10. package/dist/types/chat/assistant-message.d.ts +20 -0
  11. package/dist/types/chat/bash-execution.d.ts +9 -0
  12. package/dist/types/chat/chat-transcript-builder.d.ts +6 -1
  13. package/dist/types/chat/collab-prompt-message.d.ts +4 -0
  14. package/dist/types/chat/compaction-summary-message.d.ts +15 -0
  15. package/dist/types/chat/eval-execution.d.ts +8 -0
  16. package/dist/types/chat/execution-shared.d.ts +58 -0
  17. package/dist/types/chat/late-diagnostics-message.d.ts +15 -1
  18. package/dist/types/chat/read-tool-group.d.ts +15 -0
  19. package/dist/types/chat/skill-message.d.ts +9 -0
  20. package/dist/types/chat/stripped-tool-calls-placeholder.d.ts +3 -0
  21. package/dist/types/chat/thinking-display.d.ts +2 -0
  22. package/dist/types/chat/todo-reminder.d.ts +4 -1
  23. package/dist/types/chat/tool-execution.d.ts +25 -1
  24. package/dist/types/chat/transcript-actions.d.ts +37 -0
  25. package/dist/types/chat/transcript-entry.d.ts +3 -1
  26. package/dist/types/chat/user-message.d.ts +25 -0
  27. package/dist/types/chrome/collab-qrcode.d.ts +7 -0
  28. package/dist/types/chrome/context-thresholds.d.ts +11 -0
  29. package/dist/types/chrome/countdown-timer.d.ts +9 -3
  30. package/dist/types/chrome/diff.d.ts +12 -0
  31. package/dist/types/chrome/dynamic-border.d.ts +3 -0
  32. package/dist/types/chrome/format.d.ts +6 -0
  33. package/dist/types/chrome/message-divider.d.ts +13 -0
  34. package/dist/types/chrome/message-frame.d.ts +10 -0
  35. package/dist/types/chrome/message-notice.d.ts +23 -0
  36. package/dist/types/chrome/overlay-box.d.ts +11 -2
  37. package/dist/types/chrome/qrcode.d.ts +7 -0
  38. package/dist/types/chrome/segment-track.d.ts +7 -0
  39. package/dist/types/chrome/selector-helpers.d.ts +0 -6
  40. package/dist/types/chrome/status-notice.d.ts +14 -0
  41. package/dist/types/chrome/streaming-panel.d.ts +3 -0
  42. package/dist/types/chrome/tool-activity.d.ts +3 -0
  43. package/dist/types/chrome/transcript-container.d.ts +13 -0
  44. package/dist/types/chrome/transcript-status.d.ts +15 -3
  45. package/dist/types/components/box.d.ts +7 -0
  46. package/dist/types/components/disclosure.d.ts +9 -0
  47. package/dist/types/components/editor.d.ts +42 -0
  48. package/dist/types/components/form.d.ts +14 -0
  49. package/dist/types/components/image.d.ts +7 -0
  50. package/dist/types/components/input.d.ts +20 -1
  51. package/dist/types/components/key-value-list.d.ts +3 -0
  52. package/dist/types/components/layout/geometry.d.ts +16 -0
  53. package/dist/types/components/layout/row.d.ts +7 -0
  54. package/dist/types/components/layout/split-pane.d.ts +8 -0
  55. package/dist/types/components/layout/stack.d.ts +6 -0
  56. package/dist/types/components/loader.d.ts +48 -0
  57. package/dist/types/components/markdown.d.ts +8 -0
  58. package/dist/types/components/metric.d.ts +7 -0
  59. package/dist/types/components/progress-bar.d.ts +3 -0
  60. package/dist/types/components/scroll-view.d.ts +6 -0
  61. package/dist/types/components/section.d.ts +3 -0
  62. package/dist/types/components/select-list.d.ts +33 -0
  63. package/dist/types/components/settings-list.d.ts +68 -0
  64. package/dist/types/components/spacer.d.ts +4 -0
  65. package/dist/types/components/tab-bar.d.ts +9 -0
  66. package/dist/types/components/table.d.ts +7 -0
  67. package/dist/types/components/text.d.ts +6 -0
  68. package/dist/types/components/tree-view.d.ts +9 -0
  69. package/dist/types/components/truncated-text.d.ts +3 -0
  70. package/dist/types/components/wizard-step.d.ts +6 -0
  71. package/dist/types/hotkeys-markdown.d.ts +24 -0
  72. package/dist/types/index.d.ts +5 -0
  73. package/dist/types/latex-block.d.ts +7 -0
  74. package/dist/types/loop-watchdog.d.ts +9 -0
  75. package/dist/types/native/apply.d.ts +39 -0
  76. package/dist/types/native/backend.d.ts +94 -0
  77. package/dist/types/native/blobs.d.ts +29 -0
  78. package/dist/types/native/describe.d.ts +56 -0
  79. package/dist/types/native/encode.d.ts +50 -0
  80. package/dist/types/native/icons.d.ts +18 -0
  81. package/dist/types/native/memo.d.ts +27 -0
  82. package/dist/types/native/node.d.ts +107 -0
  83. package/dist/types/native/overlay.d.ts +64 -0
  84. package/dist/types/native/picker.d.ts +124 -0
  85. package/dist/types/native/reconcile.d.ts +41 -0
  86. package/dist/types/native/settle.d.ts +11 -0
  87. package/dist/types/native/spans.d.ts +21 -0
  88. package/dist/types/native/state.d.ts +6 -0
  89. package/dist/types/native/tone.d.ts +24 -0
  90. package/dist/types/overlays/advisor-config.d.ts +20 -0
  91. package/dist/types/overlays/agent-hub-renderer.d.ts +27 -11
  92. package/dist/types/overlays/agent-hub.d.ts +10 -0
  93. package/dist/types/overlays/agent-transcript-viewer.d.ts +7 -0
  94. package/dist/types/overlays/agents-hub.d.ts +8 -0
  95. package/dist/types/overlays/annotation-overlay.d.ts +3 -0
  96. package/dist/types/overlays/ask-dialog.d.ts +23 -0
  97. package/dist/types/overlays/bordered-loader.d.ts +3 -0
  98. package/dist/types/overlays/btw-history-panel.d.ts +9 -0
  99. package/dist/types/overlays/btw-panel.d.ts +3 -0
  100. package/dist/types/overlays/cleanse-panel.d.ts +5 -2
  101. package/dist/types/overlays/composer-shape-preview.d.ts +7 -1
  102. package/dist/types/overlays/copy-selector.d.ts +38 -0
  103. package/dist/types/overlays/error-banner.d.ts +9 -0
  104. package/dist/types/overlays/extensions/extension-list.d.ts +1 -0
  105. package/dist/types/overlays/history-search.d.ts +18 -0
  106. package/dist/types/overlays/hook-editor.d.ts +28 -1
  107. package/dist/types/overlays/hook-input.d.ts +3 -0
  108. package/dist/types/overlays/hook-selector.d.ts +10 -0
  109. package/dist/types/overlays/hub-frame.d.ts +18 -0
  110. package/dist/types/overlays/jobs-panel.d.ts +24 -0
  111. package/dist/types/overlays/login-dialog.d.ts +10 -0
  112. package/dist/types/overlays/logout-account-selector.d.ts +4 -0
  113. package/dist/types/overlays/mcp-add-wizard.d.ts +4 -0
  114. package/dist/types/overlays/model-browser.d.ts +69 -0
  115. package/dist/types/overlays/model-hub.d.ts +7 -0
  116. package/dist/types/overlays/model-picker.d.ts +9 -1
  117. package/dist/types/overlays/move-overlay.d.ts +3 -0
  118. package/dist/types/overlays/oauth-selector.d.ts +4 -0
  119. package/dist/types/overlays/omfg-panel.d.ts +3 -0
  120. package/dist/types/overlays/pause-screen.d.ts +12 -0
  121. package/dist/types/overlays/plan-review-overlay.d.ts +5 -0
  122. package/dist/types/overlays/plan-save-overlay.d.ts +10 -0
  123. package/dist/types/overlays/plugin-selector.d.ts +4 -0
  124. package/dist/types/overlays/plugin-settings.d.ts +30 -0
  125. package/dist/types/overlays/queue-mode-selector.d.ts +4 -0
  126. package/dist/types/overlays/reset-usage-selector.d.ts +8 -0
  127. package/dist/types/overlays/rewind-selector.d.ts +5 -0
  128. package/dist/types/overlays/session-info-overlay.d.ts +19 -1
  129. package/dist/types/overlays/session-selector.d.ts +64 -1
  130. package/dist/types/overlays/settings-defs.d.ts +14 -0
  131. package/dist/types/overlays/settings-selector.d.ts +21 -0
  132. package/dist/types/overlays/show-images-selector.d.ts +4 -0
  133. package/dist/types/overlays/snapcompact-shape-preview.d.ts +2 -0
  134. package/dist/types/overlays/stats-notice.d.ts +8 -0
  135. package/dist/types/overlays/theme-selector.d.ts +11 -0
  136. package/dist/types/overlays/thinking-selector.d.ts +4 -0
  137. package/dist/types/overlays/tree-selector.d.ts +60 -2
  138. package/dist/types/overlays/usage-dashboard.d.ts +26 -3
  139. package/dist/types/overlays/usage-row.d.ts +36 -2
  140. package/dist/types/prompt/attachment-chips.d.ts +8 -0
  141. package/dist/types/prompt/composer-attachments.d.ts +15 -3
  142. package/dist/types/prompt/composer.d.ts +18 -1
  143. package/dist/types/prompt/custom-editor.d.ts +48 -2
  144. package/dist/types/prompt/editor-top-gap.d.ts +2 -0
  145. package/dist/types/prompt/macos-spelling.d.ts +6 -0
  146. package/dist/types/prompt/magic-keywords.d.ts +9 -0
  147. package/dist/types/prompt/queued-messages.d.ts +23 -0
  148. package/dist/types/prompt/skill-tokens.d.ts +4 -2
  149. package/dist/types/prompt/welcome.d.ts +16 -0
  150. package/dist/types/render/code-cell.d.ts +10 -0
  151. package/dist/types/render/file-list.d.ts +8 -0
  152. package/dist/types/render/hyperlink.d.ts +14 -0
  153. package/dist/types/render/output-block.d.ts +43 -0
  154. package/dist/types/render/output-pane.d.ts +17 -0
  155. package/dist/types/render/tool-card.d.ts +8 -0
  156. package/dist/types/render/tree-list.d.ts +8 -0
  157. package/dist/types/render/width-aware-text.d.ts +7 -0
  158. package/dist/types/setup/scenes/outro.d.ts +7 -0
  159. package/dist/types/setup/scenes/sign-in.d.ts +16 -9
  160. package/dist/types/setup/scenes/splash.d.ts +8 -0
  161. package/dist/types/setup/scenes/types.d.ts +6 -27
  162. package/dist/types/setup/wizard-overlay.d.ts +9 -0
  163. package/dist/types/status-line/component.d.ts +49 -5
  164. package/dist/types/status-line/context-usage.d.ts +20 -0
  165. package/dist/types/status-line/footer.d.ts +8 -0
  166. package/dist/types/status-line/metrics.d.ts +1 -2
  167. package/dist/types/status-line/segments.d.ts +10 -1
  168. package/dist/types/status-line/types.d.ts +40 -1
  169. package/dist/types/stdin-buffer.d.ts +10 -7
  170. package/dist/types/terminal-capabilities.d.ts +1 -1
  171. package/dist/types/terminal.d.ts +36 -0
  172. package/dist/types/theme/defaults/index.d.ts +100 -0
  173. package/dist/types/theme/schema.d.ts +2 -0
  174. package/dist/types/theme/shimmer.d.ts +8 -0
  175. package/dist/types/theme/theme-class.d.ts +2 -0
  176. package/dist/types/theme/theme.d.ts +33 -1
  177. package/dist/types/tools/ask.d.ts +3 -0
  178. package/dist/types/tools/ast-edit.d.ts +10 -1
  179. package/dist/types/tools/ast-grep.d.ts +10 -1
  180. package/dist/types/tools/autoresearch.d.ts +21 -4
  181. package/dist/types/tools/bash.d.ts +30 -1
  182. package/dist/types/tools/cfg-render.d.ts +5 -1
  183. package/dist/types/tools/debug.d.ts +3 -1
  184. package/dist/types/tools/default-renderer.d.ts +6 -1
  185. package/dist/types/tools/edit.d.ts +14 -1
  186. package/dist/types/tools/eval.d.ts +12 -1
  187. package/dist/types/tools/fetch.d.ts +16 -1
  188. package/dist/types/tools/find.d.ts +15 -1
  189. package/dist/types/tools/github.d.ts +3 -0
  190. package/dist/types/tools/glob.d.ts +14 -1
  191. package/dist/types/tools/goal.d.ts +3 -1
  192. package/dist/types/tools/grep.d.ts +14 -1
  193. package/dist/types/tools/grouped-file-output.d.ts +58 -0
  194. package/dist/types/tools/json-tree.d.ts +20 -0
  195. package/dist/types/tools/lsp.d.ts +3 -10
  196. package/dist/types/tools/mcp.d.ts +16 -1
  197. package/dist/types/tools/memory.d.ts +26 -0
  198. package/dist/types/tools/native-view.d.ts +90 -0
  199. package/dist/types/tools/proc-render.d.ts +7 -1
  200. package/dist/types/tools/read.d.ts +23 -1
  201. package/dist/types/tools/renderer.d.ts +57 -0
  202. package/dist/types/tools/report-tool-issue.d.ts +3 -0
  203. package/dist/types/tools/resolve.d.ts +14 -1
  204. package/dist/types/tools/task.d.ts +3 -0
  205. package/dist/types/tools/think.d.ts +3 -0
  206. package/dist/types/tools/todo.d.ts +12 -0
  207. package/dist/types/tools/vibe.d.ts +3 -1
  208. package/dist/types/tools/wait.d.ts +3 -0
  209. package/dist/types/tools/web-search-types.d.ts +90 -0
  210. package/dist/types/tools/web-search.d.ts +9 -168
  211. package/dist/types/tools/write.d.ts +14 -1
  212. package/dist/types/tools/xdev.d.ts +13 -0
  213. package/dist/types/tui.d.ts +49 -0
  214. package/package.json +9 -9
  215. package/src/apps/autoresearch-dashboard.ts +362 -4
  216. package/src/apps/cleanse-board.ts +219 -1
  217. package/src/apps/cleanse-picker.ts +9 -1
  218. package/src/apps/debug/log-viewer.ts +15 -15
  219. package/src/apps/debug/protocol-probe.ts +2 -20
  220. package/src/apps/git/avatar.ts +2 -22
  221. package/src/apps/git/git-tui.ts +406 -59
  222. package/src/apps/git/help.ts +130 -0
  223. package/src/apps/git/sidebar.ts +491 -0
  224. package/src/apps/live-visualizer.ts +65 -0
  225. package/src/apps/ps-top.ts +448 -17
  226. package/src/apps/session-picker.ts +1 -0
  227. package/src/apps/setup-model-picker.ts +5 -1
  228. package/src/apps/standalone-picker.ts +55 -1
  229. package/src/autocomplete.ts +44 -1
  230. package/src/chat/advisor-message.ts +72 -3
  231. package/src/chat/assistant-message.ts +538 -51
  232. package/src/chat/background-tan-message.ts +4 -4
  233. package/src/chat/bash-execution.ts +81 -0
  234. package/src/chat/cache-invalidation-marker.ts +8 -0
  235. package/src/chat/chat-transcript-builder.ts +37 -6
  236. package/src/chat/collab-prompt-message.ts +15 -3
  237. package/src/chat/compaction-summary-message.ts +74 -1
  238. package/src/chat/custom-message.ts +3 -1
  239. package/src/chat/eval-execution.ts +57 -0
  240. package/src/chat/execution-shared.ts +153 -1
  241. package/src/chat/hook-message.ts +1 -0
  242. package/src/chat/late-diagnostics-message.ts +85 -2
  243. package/src/chat/read-tool-group.ts +341 -1
  244. package/src/chat/served-model-marker.ts +8 -0
  245. package/src/chat/skill-message.ts +63 -0
  246. package/src/chat/stripped-tool-calls-placeholder.ts +18 -10
  247. package/src/chat/thinking-display.ts +73 -11
  248. package/src/chat/todo-reminder.ts +34 -4
  249. package/src/chat/tool-execution.ts +385 -10
  250. package/src/chat/transcript-actions.ts +38 -0
  251. package/src/chat/transcript-browser.ts +2 -9
  252. package/src/chat/transcript-entry.ts +10 -1
  253. package/src/chat/transcript-render-helpers.ts +16 -15
  254. package/src/chat/ttsr-notification.ts +38 -1
  255. package/src/chat/user-message.ts +167 -4
  256. package/src/chrome/collab-qrcode.ts +27 -2
  257. package/src/chrome/context-thresholds.ts +34 -0
  258. package/src/chrome/countdown-timer.ts +24 -1
  259. package/src/chrome/diff.ts +60 -1
  260. package/src/chrome/dynamic-border.ts +7 -0
  261. package/src/chrome/format.ts +12 -0
  262. package/src/chrome/message-divider.ts +33 -1
  263. package/src/chrome/message-frame.ts +53 -10
  264. package/src/chrome/message-notice.ts +71 -0
  265. package/src/chrome/overlay-box.ts +45 -2
  266. package/src/chrome/qrcode.ts +28 -0
  267. package/src/chrome/segment-track.ts +26 -0
  268. package/src/chrome/selector-helpers.ts +1 -17
  269. package/src/chrome/status-notice.ts +46 -0
  270. package/src/chrome/streaming-panel.ts +25 -1
  271. package/src/chrome/tool-activity.ts +20 -0
  272. package/src/chrome/transcript-container.ts +139 -17
  273. package/src/chrome/transcript-status.ts +40 -11
  274. package/src/components/box.ts +51 -0
  275. package/src/components/disclosure.ts +33 -0
  276. package/src/components/editor.ts +239 -36
  277. package/src/components/form.ts +93 -9
  278. package/src/components/image.ts +30 -0
  279. package/src/components/input.ts +77 -26
  280. package/src/components/key-value-list.ts +18 -0
  281. package/src/components/layout/geometry.ts +44 -0
  282. package/src/components/layout/row.ts +30 -0
  283. package/src/components/layout/split-pane.ts +65 -0
  284. package/src/components/layout/stack.ts +27 -0
  285. package/src/components/loader.ts +183 -1
  286. package/src/components/markdown.ts +131 -56
  287. package/src/components/metric.ts +39 -0
  288. package/src/components/progress-bar.ts +28 -0
  289. package/src/components/scroll-view.ts +23 -0
  290. package/src/components/section.ts +16 -0
  291. package/src/components/select-list.ts +174 -25
  292. package/src/components/settings-list.ts +264 -7
  293. package/src/components/spacer.ts +17 -0
  294. package/src/components/tab-bar.ts +42 -0
  295. package/src/components/table.ts +34 -0
  296. package/src/components/text.ts +23 -0
  297. package/src/components/tree-view.ts +65 -0
  298. package/src/components/truncated-text.ts +14 -0
  299. package/src/components/wizard-step.ts +18 -9
  300. package/src/debug-server.ts +14 -7
  301. package/src/hotkeys-markdown.ts +251 -69
  302. package/src/index.ts +6 -0
  303. package/src/latex-block.ts +11 -0
  304. package/src/loop-watchdog.ts +38 -10
  305. package/src/native/apply.ts +275 -0
  306. package/src/native/backend.ts +682 -0
  307. package/src/native/blobs.ts +70 -0
  308. package/src/native/describe.ts +124 -0
  309. package/src/native/encode.ts +233 -0
  310. package/src/native/icons.ts +184 -0
  311. package/src/native/memo.ts +64 -0
  312. package/src/native/node.ts +112 -0
  313. package/src/native/overlay.ts +131 -0
  314. package/src/native/picker.ts +333 -0
  315. package/src/native/reconcile.ts +1063 -0
  316. package/src/native/settle.ts +23 -0
  317. package/src/native/spans.ts +308 -0
  318. package/src/native/state.ts +28 -0
  319. package/src/native/tone.ts +79 -0
  320. package/src/overlays/advisor-config.ts +420 -0
  321. package/src/overlays/agent-hub-renderer.ts +155 -18
  322. package/src/overlays/agent-hub.ts +1037 -44
  323. package/src/overlays/agent-transcript-viewer.ts +129 -0
  324. package/src/overlays/agents-hub.ts +688 -53
  325. package/src/overlays/annotation-overlay.ts +391 -52
  326. package/src/overlays/ask-dialog.ts +435 -24
  327. package/src/overlays/bordered-loader.ts +17 -0
  328. package/src/overlays/btw-history-panel.ts +182 -0
  329. package/src/overlays/btw-panel.ts +68 -2
  330. package/src/overlays/cleanse-panel.ts +105 -15
  331. package/src/overlays/codex-reset-fireworks.ts +65 -5
  332. package/src/overlays/composer-shape-preview.ts +26 -1
  333. package/src/overlays/copy-selector.ts +545 -23
  334. package/src/overlays/error-banner.ts +44 -0
  335. package/src/overlays/extensions/extension-list.ts +23 -17
  336. package/src/overlays/history-search.ts +175 -33
  337. package/src/overlays/hook-editor.ts +113 -6
  338. package/src/overlays/hook-input.ts +22 -1
  339. package/src/overlays/hook-selector.ts +311 -22
  340. package/src/overlays/hub-frame.ts +80 -0
  341. package/src/overlays/jobs-panel.ts +112 -0
  342. package/src/overlays/login-dialog.ts +183 -1
  343. package/src/overlays/logout-account-selector.ts +102 -4
  344. package/src/overlays/mcp-add-wizard.ts +232 -53
  345. package/src/overlays/model-browser.ts +681 -22
  346. package/src/overlays/model-hub.ts +1402 -78
  347. package/src/overlays/model-picker.ts +184 -7
  348. package/src/overlays/move-overlay.ts +45 -0
  349. package/src/overlays/oauth-selector.ts +227 -23
  350. package/src/overlays/omfg-panel.ts +86 -2
  351. package/src/overlays/pause-screen.ts +55 -4
  352. package/src/overlays/plan-review-overlay.ts +352 -0
  353. package/src/overlays/plan-save-overlay.ts +45 -0
  354. package/src/overlays/plugin-selector.ts +21 -1
  355. package/src/overlays/plugin-settings.ts +195 -5
  356. package/src/overlays/queue-mode-selector.ts +19 -1
  357. package/src/overlays/reset-usage-selector.ts +110 -40
  358. package/src/overlays/rewind-selector.ts +458 -44
  359. package/src/overlays/session-account-selector.ts +1 -1
  360. package/src/overlays/session-info-overlay.ts +137 -1
  361. package/src/overlays/session-selector.ts +807 -46
  362. package/src/overlays/settings-defs.ts +41 -0
  363. package/src/overlays/settings-selector.ts +585 -33
  364. package/src/overlays/show-images-selector.ts +18 -1
  365. package/src/overlays/snapcompact-shape-preview.ts +48 -3
  366. package/src/overlays/stats-notice.ts +31 -0
  367. package/src/overlays/theme-selector.ts +31 -1
  368. package/src/overlays/thinking-selector.ts +22 -1
  369. package/src/overlays/tree-selector.ts +570 -110
  370. package/src/overlays/usage-dashboard.ts +587 -12
  371. package/src/overlays/usage-row.ts +161 -25
  372. package/src/prompt/attachment-chips.ts +60 -0
  373. package/src/prompt/composer-attachments.ts +31 -9
  374. package/src/prompt/composer.ts +55 -2
  375. package/src/prompt/custom-editor.ts +302 -4
  376. package/src/prompt/editor-top-gap.ts +8 -0
  377. package/src/prompt/macos-spelling.ts +24 -9
  378. package/src/prompt/magic-keywords.ts +22 -1
  379. package/src/prompt/model-mention-autocomplete.ts +1 -0
  380. package/src/prompt/queued-messages.ts +101 -0
  381. package/src/prompt/skill-tokens.ts +5 -3
  382. package/src/prompt/welcome.ts +213 -6
  383. package/src/render/code-cell.ts +103 -2
  384. package/src/render/file-list.ts +29 -0
  385. package/src/render/hyperlink.ts +32 -0
  386. package/src/render/output-block.ts +110 -0
  387. package/src/render/output-pane.ts +37 -0
  388. package/src/render/tool-card.ts +137 -0
  389. package/src/render/tree-list.ts +39 -0
  390. package/src/render/width-aware-text.ts +20 -0
  391. package/src/setup/scenes/composer.ts +29 -0
  392. package/src/setup/scenes/glyph.ts +15 -0
  393. package/src/setup/scenes/model.ts +36 -8
  394. package/src/setup/scenes/outro.ts +28 -1
  395. package/src/setup/scenes/sign-in.ts +138 -24
  396. package/src/setup/scenes/splash.ts +28 -1
  397. package/src/setup/scenes/theme.ts +88 -3
  398. package/src/setup/scenes/types.ts +7 -28
  399. package/src/setup/startup-splash.ts +46 -9
  400. package/src/setup/wizard-overlay.ts +140 -23
  401. package/src/setup/wizard.ts +1 -1
  402. package/src/status-line/component.ts +414 -18
  403. package/src/status-line/context-usage.ts +279 -50
  404. package/src/status-line/footer.ts +94 -1
  405. package/src/status-line/metrics.ts +3 -6
  406. package/src/status-line/segments.ts +463 -97
  407. package/src/status-line/separators.ts +3 -16
  408. package/src/status-line/types.ts +43 -1
  409. package/src/stdin-buffer.ts +27 -21
  410. package/src/terminal-capabilities.ts +7 -0
  411. package/src/terminal.ts +153 -11
  412. package/src/theme/defaults/dark-neon-noir.json +100 -0
  413. package/src/theme/defaults/index.ts +2 -0
  414. package/src/theme/defaults/light-prism.json +5 -5
  415. package/src/theme/defaults/onyx.json +62 -62
  416. package/src/theme/mermaid-cache.ts +10 -1
  417. package/src/theme/schema.ts +15 -0
  418. package/src/theme/shimmer.ts +33 -0
  419. package/src/theme/theme-class.ts +24 -19
  420. package/src/theme/theme.ts +111 -9
  421. package/src/theme/tui-adapters.ts +14 -1
  422. package/src/tools/ask.ts +157 -1
  423. package/src/tools/ast-edit.ts +174 -1
  424. package/src/tools/ast-grep.ts +82 -2
  425. package/src/tools/autoresearch.ts +149 -1
  426. package/src/tools/bash.ts +233 -62
  427. package/src/tools/cfg-render.ts +66 -1
  428. package/src/tools/debug.ts +78 -1
  429. package/src/tools/default-renderer.ts +35 -1
  430. package/src/tools/edit.ts +357 -31
  431. package/src/tools/eval.ts +257 -1
  432. package/src/tools/fetch.ts +102 -3
  433. package/src/tools/find.ts +127 -1
  434. package/src/tools/github.ts +119 -10
  435. package/src/tools/glob.ts +116 -1
  436. package/src/tools/goal.ts +56 -1
  437. package/src/tools/grep.ts +83 -2
  438. package/src/tools/grouped-file-output.ts +205 -0
  439. package/src/tools/json-tree.ts +116 -1
  440. package/src/tools/lsp.ts +318 -1
  441. package/src/tools/mcp.ts +80 -1
  442. package/src/tools/memory.ts +93 -11
  443. package/src/tools/native-view.ts +292 -0
  444. package/src/tools/proc-render.ts +221 -1
  445. package/src/tools/read.ts +199 -4
  446. package/src/tools/renderer.ts +61 -0
  447. package/src/tools/report-tool-issue.ts +7 -1
  448. package/src/tools/resolve.ts +63 -1
  449. package/src/tools/streaming-output.ts +79 -16
  450. package/src/tools/task.ts +406 -4
  451. package/src/tools/think.ts +19 -1
  452. package/src/tools/todo.ts +89 -0
  453. package/src/tools/vibe.ts +230 -2
  454. package/src/tools/wait.ts +181 -1
  455. package/src/tools/web-search-types.ts +99 -0
  456. package/src/tools/web-search.ts +111 -170
  457. package/src/tools/write.ts +144 -5
  458. package/src/tools/xdev.ts +64 -1
  459. package/src/tui.ts +295 -4
  460. package/dist/types/setup/scenes/providers.d.ts +0 -3
  461. package/dist/types/setup/scenes/web-search.d.ts +0 -22
  462. package/src/setup/scenes/providers.ts +0 -110
  463. package/src/setup/scenes/web-search.ts +0 -165
@@ -13,7 +13,22 @@ import {
13
13
  truncateToWidth,
14
14
  visibleWidth,
15
15
  } from "../index";
16
- import { formatBytes } from "@oh-my-pi/pi-utils";
16
+ import * as path from "node:path";
17
+ import { formatBytes, getProjectDir } from "@oh-my-pi/pi-utils";
18
+ import type { TspPickerGroup, TspPickerItem, TspSpan, TspText, TspTone } from "@oh-my-pi/pi-wire";
19
+ import { compact, kv, md, node, span } from "../native/describe";
20
+ import {
21
+ picker,
22
+ pickerAction,
23
+ pickerAge,
24
+ pickerDate,
25
+ type PickerEvent,
26
+ pickerEvent,
27
+ pickerHits,
28
+ } from "../native/picker";
29
+ import type { DescribeContext, NativeChild, NativeNode, NativeUiEvent } from "../native/node";
30
+ import { hintsRow, type NativeHint, overlayCard } from "../native/overlay";
31
+ import { plainText } from "../native/spans";
17
32
  import { theme } from "../theme/theme";
18
33
  import { contentRowWidth } from "../chrome/selector-helpers";
19
34
  import { matchesAppInterrupt, matchesSelectDown, matchesSelectUp } from "../keybinding-matchers";
@@ -27,6 +42,7 @@ export interface SessionSelectorEntry {
27
42
  cwd: string;
28
43
  title?: string;
29
44
  modified: Date;
45
+ created?: Date;
30
46
  size: number;
31
47
  firstMessage: string;
32
48
  allMessagesText: string;
@@ -39,7 +55,7 @@ import { HookSelectorComponent } from "./hook-selector";
39
55
  import { bottomBorder, OverlayPanel, row, topBorder } from "../chrome/overlay-box";
40
56
  import { MenuSelection, getMenuWindow } from "../components/menu-selection";
41
57
  import { formatKeyHint, formatKeyHints } from "../app-keybindings";
42
- import { interruptKey } from "../chrome/keybinding-hints";
58
+ import { boundKeys, interruptKey } from "../chrome/keybinding-hints";
43
59
 
44
60
  /**
45
61
  * Themed glyph + colored label for a session's lifecycle status, or `undefined`
@@ -64,6 +80,181 @@ function formatSessionStatus(status: SessionSelectorStatus | undefined): string
64
80
  }
65
81
  }
66
82
 
83
+ /** Lifecycle status as one theme-colored span (native picker), mirroring {@link formatSessionStatus}. */
84
+ function sessionStatusSpan(status: SessionSelectorStatus | undefined): TspSpan | undefined {
85
+ switch (status) {
86
+ case "complete":
87
+ return span("done", "success");
88
+ case "interrupted":
89
+ return span("interrupted", "warning");
90
+ case "aborted":
91
+ return span("aborted", "muted");
92
+ case "error":
93
+ return span("error", "error");
94
+ case "pending":
95
+ return span("pending", "accent");
96
+ default:
97
+ return undefined;
98
+ }
99
+ }
100
+
101
+ /** Status dot tone of a session row in the native picker; none when the status is unknown. */
102
+ function sessionStatusTone(status: SessionSelectorStatus | undefined): TspTone | undefined {
103
+ switch (status) {
104
+ case "complete":
105
+ return "success";
106
+ case "interrupted":
107
+ return "warning";
108
+ case "aborted":
109
+ case "error":
110
+ return "error";
111
+ case "pending":
112
+ return "pending";
113
+ default:
114
+ return undefined;
115
+ }
116
+ }
117
+
118
+ /** The name a session goes by: its title, else its first message on one line, else its id. */
119
+ function sessionLabel(session: SessionSelectorEntry): string {
120
+ const title = session.title ? plainText(session.title).trim() : "";
121
+ return title || plainText(session.firstMessage).replace(/\s+/g, " ").trim() || session.id;
122
+ }
123
+
124
+ /** Longest prompt or answer excerpt the native preview shows. */
125
+ const PREVIEW_EXCERPT_CHARS = 1200;
126
+ /** The preview follows the selection once it rests this long (holding ↓ doesn't flood the wire). */
127
+ const PREVIEW_SETTLE_MS = 60;
128
+ const DAY_MS = 86_400_000;
129
+ const PINNED_GROUP = { id: "pinned", label: "Pinned" };
130
+
131
+ /** Day group of a session's last modification, relative to local midnight today. */
132
+ function sessionDayGroup(modified: Date, startOfToday: number): { id: string; label: string } {
133
+ const at = modified.getTime();
134
+ if (at >= startOfToday) return { id: "today", label: "Today" };
135
+ if (at >= startOfToday - DAY_MS) return { id: "yesterday", label: "Yesterday" };
136
+ if (at >= startOfToday - 6 * DAY_MS) return { id: "week", label: "This week" };
137
+ return { id: "earlier", label: "Earlier" };
138
+ }
139
+
140
+ /** A cwd as dim-prefix / strong-tail path spans. */
141
+ function cwdSpans(cwd: string): TspSpan[] {
142
+ const short = shortenPath(cwd);
143
+ const cut = short.lastIndexOf("/") + 1;
144
+ return cut > 0 && cut < short.length
145
+ ? [span(short.slice(0, cut), "path dim"), span(short.slice(cut), "path")]
146
+ : [span(short, "path")];
147
+ }
148
+
149
+ /**
150
+ * The native preview of a session: its title, a fact grid, and the
151
+ * conversation's first prompt (user tint) plus the tail of the rest as an
152
+ * excerpt of the latest answer, both clipped to {@link PREVIEW_EXCERPT_CHARS}.
153
+ */
154
+ function sessionPreview(session: SessionSelectorEntry, forkedFrom: string | undefined): NativeChild[] {
155
+ const status = sessionStatusSpan(session.status);
156
+ const created = session.created && !Number.isNaN(session.created.getTime()) ? session.created : undefined;
157
+ const first = session.firstMessage.trim();
158
+ const rest = session.allMessagesText.startsWith(session.firstMessage)
159
+ ? session.allMessagesText.slice(session.firstMessage.length).trim()
160
+ : "";
161
+ const conversation = compact([
162
+ first.length > 0 &&
163
+ md(first.length > PREVIEW_EXCERPT_CHARS ? `${first.slice(0, PREVIEW_EXCERPT_CHARS)}…` : first, {
164
+ tone: "user",
165
+ }),
166
+ rest.length > 0 && md(rest.length > PREVIEW_EXCERPT_CHARS ? `…${rest.slice(-PREVIEW_EXCERPT_CHARS)}` : rest),
167
+ ]);
168
+ return compact([
169
+ node("text", { text: sessionLabel(session), role: "omp.picker.title" }),
170
+ kv([
171
+ ["Folder", session.cwd ? cwdSpans(session.cwd) : undefined],
172
+ ["Created", created && pickerDate(created)],
173
+ ["Modified", pickerDate(session.modified)],
174
+ ["Size", formatBytes(session.size)],
175
+ ["Status", status && [status]],
176
+ ["Forked from", forkedFrom],
177
+ ]),
178
+ conversation.length > 0 && node("section", { head: "Conversation" }, conversation),
179
+ ]);
180
+ }
181
+
182
+ /** Relative age of a session's last modification (`"3 hours ago"`), falling back to the date after a week. */
183
+ function formatSessionDate(date: Date): string {
184
+ const diffMs = Date.now() - date.getTime();
185
+ const diffMins = Math.floor(diffMs / 60000);
186
+ const diffHours = Math.floor(diffMs / 3600000);
187
+ const diffDays = Math.floor(diffMs / 86400000);
188
+
189
+ if (diffMins < 1) return "just now";
190
+ if (diffMins < 60) return `${diffMins} minute${diffMins !== 1 ? "s" : ""} ago`;
191
+ if (diffHours < 24) return `${diffHours} hour${diffHours !== 1 ? "s" : ""} ago`;
192
+ if (diffDays === 1) return "1 day ago";
193
+ if (diffDays < 7) return `${diffDays} days ago`;
194
+
195
+ return date.toLocaleDateString();
196
+ }
197
+
198
+ /** A cached native session item and the inputs it was built from. */
199
+ interface SessionItemMemo {
200
+ node: NativeNode;
201
+ date: string;
202
+ pinned: boolean;
203
+ current: boolean;
204
+ showCwd: boolean;
205
+ }
206
+
207
+ /** Cached native item node on the session entry, rebuilt when its inputs change. */
208
+ const kNativeItem = Symbol("session.nativeItem");
209
+
210
+ interface NativeSessionInfo extends SessionSelectorEntry {
211
+ [kNativeItem]?: SessionItemMemo;
212
+ }
213
+
214
+ /** The session picker's catalogue: one item per session in scope, and the inputs it was built from. */
215
+ interface SessionCatalogue {
216
+ version: number;
217
+ showCwd: boolean;
218
+ currentPath: string | undefined;
219
+ pinnedIds: ReadonlySet<string>;
220
+ minute: number;
221
+ items: readonly TspPickerItem[];
222
+ byPath: ReadonlyMap<string, TspPickerItem>;
223
+ }
224
+
225
+ /** The session list's part of the picker props, plus the memo keys it was built from. */
226
+ interface SessionPickerView {
227
+ items: readonly TspPickerItem[];
228
+ itemsAdd?: readonly TspPickerItem[];
229
+ version: number;
230
+ query: string;
231
+ /** Search caret (UTF-16 offset into `query`). */
232
+ cursor: number;
233
+ order: readonly (string | TspPickerGroup)[];
234
+ hits?: Readonly<Record<string, readonly (readonly [number, number])[]>>;
235
+ selected: string | null;
236
+ current: readonly string[];
237
+ total: number;
238
+ }
239
+
240
+ /** An open delete confirmation: the session and the dialog's two answers. */
241
+ interface DeleteChoice<T extends SessionSelectorEntry> {
242
+ session: T;
243
+ confirm(): void;
244
+ cancel(): void;
245
+ }
246
+
247
+ /** Transient status line above the list: scope loading or an error. */
248
+ interface SessionPickerMessage {
249
+ kind: "loading" | "error";
250
+ text: string;
251
+ }
252
+
253
+ const SCOPE_TABS: readonly { id: "folder" | "all"; label: string }[] = [
254
+ { id: "folder", label: "Current folder" },
255
+ { id: "all", label: "All projects" },
256
+ ];
257
+
67
258
  /** Returns the IDs of sessions whose recorded prompts match a query, best first. */
68
259
  export type SessionHistoryMatcher = (query: string) => string[];
69
260
 
@@ -352,6 +543,22 @@ class SessionList<T extends SessionSelectorEntry> implements Component {
352
543
  #hadFilterQuery = false;
353
544
  /** Last query passed to {@link #filterSessions}; same-query refilter keeps the index. */
354
545
  #lastFilterQuery = "";
546
+ /** Bumped whenever the visible session set may have changed (native memo key). */
547
+ #itemsVersion = 0;
548
+ #itemsNative:
549
+ | { version: number; showCwd: boolean; currentPath: string | undefined; items: NativeNode[] }
550
+ | undefined;
551
+ #listNative:
552
+ | { items: readonly NativeNode[]; selected: string | undefined; query: string; node: NativeNode }
553
+ | undefined;
554
+ /** Bumped when the session set itself changes (scope switch, delete): the picker catalogue's memo key. */
555
+ #datasetVersion = 0;
556
+ /** The picker catalogue (every session in scope), rebuilt only when the set, markers or minute change. */
557
+ #pickerCatalogue: SessionCatalogue | undefined;
558
+ /** Paths upserted through `itemsAdd` (history badges) since the catalogue was last sent. */
559
+ #pickerPatched = new Set<string>();
560
+ #pickerPatch: { catalogue: readonly TspPickerItem[]; flagged: string; add: readonly TspPickerItem[] } | undefined;
561
+ #pickerView: SessionPickerView | undefined;
355
562
 
356
563
  constructor(
357
564
  sessions: T[],
@@ -417,6 +624,7 @@ class SessionList<T extends SessionSelectorEntry> implements Component {
417
624
 
418
625
  /** Replace the visible dataset, e.g. when toggling folder/all-projects scope. */
419
626
  setSessions(sessions: T[], showCwd: boolean, pinnedIds?: ReadonlySet<string>): void {
627
+ this.#datasetVersion++;
420
628
  this.#allSessions = sessions;
421
629
  this.#showCwd = showCwd;
422
630
  if (pinnedIds !== undefined) this.#pinnedIds = pinnedIds;
@@ -427,6 +635,7 @@ class SessionList<T extends SessionSelectorEntry> implements Component {
427
635
 
428
636
  #filterSessions(query: string): void {
429
637
  this.#scanGeneration++;
638
+ this.#itemsVersion++;
430
639
  if (this.#scanTimer !== undefined) {
431
640
  clearTimeout(this.#scanTimer);
432
641
  this.#scanTimer = undefined;
@@ -511,6 +720,7 @@ class SessionList<T extends SessionSelectorEntry> implements Component {
511
720
  * prompt-history hits, literal matches (recency), and fuzzy-only hits (score).
512
721
  */
513
722
  #composeFiltered(): void {
723
+ this.#itemsVersion++;
514
724
  this.#fuzzyRanked.sort(compareFuzzyRank);
515
725
  const base: T[] = [];
516
726
  for (const match of this.#literalRanked) base.push(match.session);
@@ -570,6 +780,7 @@ class SessionList<T extends SessionSelectorEntry> implements Component {
570
780
  removeSession(sessionPath: string): void {
571
781
  const index = this.#allSessions.findIndex(s => s.path === sessionPath);
572
782
  if (index === -1) return;
783
+ this.#datasetVersion++;
573
784
  this.#allSessions.splice(index, 1);
574
785
  // Re-filter to update the composed result set
575
786
  this.#filterSessions(this.#searchInput.getValue());
@@ -599,6 +810,293 @@ class SessionList<T extends SessionSelectorEntry> implements Component {
599
810
  this.onSelect?.(session);
600
811
  }
601
812
 
813
+ /** Native click on a session item: the same outcome as a mouse click (select + resume). */
814
+ confirmSession(path: string): void {
815
+ const index = this.#menu.visibleItems.findIndex(s => s.path === path);
816
+ if (index >= 0) this.selectAndConfirm(index);
817
+ }
818
+
819
+ /** Native row click: move the selection there, as the arrow keys would. */
820
+ selectSession(path: string): void {
821
+ if (this.#menu.setSelectedKey(path)) this.#selectionMoved = true;
822
+ }
823
+
824
+ /** Enter: resume the selected session. */
825
+ resumeSelected(): void {
826
+ const selected = this.#menu.selectedItem;
827
+ if (selected) this.onSelect?.(selected);
828
+ }
829
+
830
+ /** Delete / Backspace on an empty query: ask the parent to confirm deleting the selected session. */
831
+ requestDelete(): void {
832
+ const selected = this.#menu.selectedItem;
833
+ if (selected) this.onDeleteRequest?.(selected);
834
+ }
835
+
836
+ /** Empty the search query (the empty state's "Clear search"). */
837
+ clearSearch(): void {
838
+ this.#searchInput.setValue("");
839
+ this.#filterSessions("");
840
+ }
841
+
842
+ get selectedSession(): T | undefined {
843
+ return this.#menu.selectedItem;
844
+ }
845
+
846
+ /** Label of the session at `sessionPath` in this listing, else its file name. */
847
+ parentLabel(sessionPath: string): string {
848
+ const parent = this.#allSessions.find(s => s.path === sessionPath);
849
+ return parent ? sessionLabel(parent) : path.basename(sessionPath, ".jsonl");
850
+ }
851
+
852
+ /**
853
+ * The session list's picker data. `items` is the whole scope and keeps its
854
+ * identity while typing (rebuilt when the set, pins, the live session or
855
+ * the minute change); a query changes only `order`, `hits` and
856
+ * `selected`, plus `itemsAdd` upserts for prompt-history badges. Without
857
+ * a query `order` groups pinned sessions, then by day (Today, Yesterday,
858
+ * This week, Earlier); with one it is the flat ranking.
859
+ */
860
+ pickerView(): SessionPickerView {
861
+ const catalogue = this.#catalogue();
862
+ const query = this.#searchInput.getValue();
863
+ const cursor = this.#searchInput.getCursor();
864
+ const selected = this.#menu.selectedKey ?? null;
865
+ const flagged = this.#historyFlagged();
866
+ const itemsAdd = this.#historyPatch(catalogue.items, catalogue.byPath, flagged);
867
+ const view = this.#pickerView;
868
+ if (
869
+ view?.items === catalogue.items &&
870
+ view.version === this.#itemsVersion &&
871
+ view.query === query &&
872
+ view.cursor === cursor &&
873
+ view.selected === selected &&
874
+ view.itemsAdd === itemsAdd
875
+ ) {
876
+ return view;
877
+ }
878
+ const visible = this.#menu.visibleItems;
879
+ const tokens = tokenizeSessionQuery(query);
880
+ const order: (string | TspPickerGroup)[] = [];
881
+ let hits: Record<string, (readonly [number, number])[]> | undefined;
882
+ if (tokens.length === 0) {
883
+ const now = new Date();
884
+ const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
885
+ // Pins lead the listing (`sortPinnedFirst`), so they get their own group ahead of the days.
886
+ const groups = visible.map(session =>
887
+ this.#pinnedIds.has(session.id) ? PINNED_GROUP : sessionDayGroup(session.modified, startOfToday),
888
+ );
889
+ for (let i = 0; i < visible.length; i++) {
890
+ const group = groups[i]!;
891
+ if (i === 0 || groups[i - 1]!.id !== group.id) {
892
+ let count = 1;
893
+ while (i + count < visible.length && groups[i + count]!.id === group.id) count++;
894
+ order.push({ group: `${group.id}-${i}`, label: group.label, count });
895
+ }
896
+ order.push(visible[i]!.path);
897
+ }
898
+ } else {
899
+ for (const session of visible) {
900
+ order.push(session.path);
901
+ const label = catalogue.byPath.get(session.path)?.label;
902
+ if (typeof label !== "string") continue;
903
+ const ranges = pickerHits(label, tokens);
904
+ if (ranges.length > 0) (hits ??= {})[session.path] = ranges;
905
+ }
906
+ }
907
+ const next: SessionPickerView = {
908
+ items: catalogue.items,
909
+ ...(itemsAdd.length > 0 ? { itemsAdd } : {}),
910
+ version: this.#itemsVersion,
911
+ query,
912
+ cursor,
913
+ order,
914
+ ...(hits ? { hits } : {}),
915
+ selected,
916
+ current: catalogue.currentPath !== undefined ? [catalogue.currentPath] : [],
917
+ total: this.#allSessions.length,
918
+ };
919
+ this.#pickerView = next;
920
+ return next;
921
+ }
922
+
923
+ #catalogue(): SessionCatalogue {
924
+ const currentPath = this.#getCurrentSessionPath();
925
+ const minute = Math.floor(Date.now() / 60_000);
926
+ const cached = this.#pickerCatalogue;
927
+ if (
928
+ cached?.version === this.#datasetVersion &&
929
+ cached.showCwd === this.#showCwd &&
930
+ cached.currentPath === currentPath &&
931
+ cached.pinnedIds === this.#pinnedIds &&
932
+ cached.minute === minute
933
+ ) {
934
+ return cached;
935
+ }
936
+ const now = Date.now();
937
+ const byPath = new Map<string, TspPickerItem>();
938
+ const items = this.#allSessions.map(session => {
939
+ const item = this.#pickerItem(session, currentPath, now);
940
+ byPath.set(session.path, item);
941
+ return item;
942
+ });
943
+ this.#pickerPatched.clear();
944
+ const catalogue: SessionCatalogue = {
945
+ version: this.#datasetVersion,
946
+ showCwd: this.#showCwd,
947
+ currentPath,
948
+ pinnedIds: this.#pinnedIds,
949
+ minute,
950
+ items,
951
+ byPath,
952
+ };
953
+ this.#pickerCatalogue = catalogue;
954
+ return catalogue;
955
+ }
956
+
957
+ #pickerItem(session: T, currentPath: string | undefined, now: number): TspPickerItem {
958
+ const message = plainText(session.firstMessage).replace(/\s+/g, " ").trim();
959
+ const title = session.title ? plainText(session.title).trim() : "";
960
+ const badges: { text: string; tone?: TspTone; title?: string }[] = [];
961
+ if (session.parentSessionPath) {
962
+ badges.push({ text: "fork", title: `Forked from “${this.parentLabel(session.parentSessionPath)}”` });
963
+ }
964
+ if (session.path === currentPath) badges.push({ text: "current", tone: "accent" });
965
+ const when = pickerDate(session.modified);
966
+ const detail = this.#showCwd ? (session.cwd ? cwdSpans(session.cwd) : undefined) : title ? message : undefined;
967
+ const dot = sessionStatusTone(session.status);
968
+ return {
969
+ id: session.path,
970
+ label: title || message || session.id,
971
+ ...(detail ? { detail } : {}),
972
+ facts: { when: pickerAge(session.modified, now), size: formatBytes(session.size) },
973
+ ...(dot ? { dot } : {}),
974
+ ...(this.#pinnedIds.has(session.id) ? { icon: "pin" } : {}),
975
+ ...(badges.length > 0 ? { badges } : {}),
976
+ title: this.#showCwd && message ? `${message}\n${when}` : when,
977
+ };
978
+ }
979
+
980
+ /** Paths of the visible sessions the prompt-history merge surfaced for the current query. */
981
+ #historyFlagged(): string {
982
+ if (this.#historyIds.length === 0 || tokenizeSessionQuery(this.#searchInput.getValue()).length === 0) return "";
983
+ const ids = new Set(this.#historyIds);
984
+ const paths: string[] = [];
985
+ for (const session of this.#menu.visibleItems) if (ids.has(session.id)) paths.push(session.path);
986
+ return paths.join("\n");
987
+ }
988
+
989
+ /**
990
+ * `itemsAdd` for the history badges: every session flagged now gets the
991
+ * badge, and every one flagged since the catalogue was sent but not now
992
+ * gets its plain item back (upserts stick until `items` is replaced).
993
+ */
994
+ #historyPatch(
995
+ catalogue: readonly TspPickerItem[],
996
+ byPath: ReadonlyMap<string, TspPickerItem>,
997
+ flagged: string,
998
+ ): readonly TspPickerItem[] {
999
+ const memo = this.#pickerPatch;
1000
+ if (memo?.catalogue === catalogue && memo.flagged === flagged) return memo.add;
1001
+ const now = new Set(flagged ? flagged.split("\n") : []);
1002
+ for (const key of now) this.#pickerPatched.add(key);
1003
+ const add: TspPickerItem[] = [];
1004
+ for (const key of this.#pickerPatched) {
1005
+ const base = byPath.get(key);
1006
+ if (!base) continue;
1007
+ add.push(
1008
+ now.has(key)
1009
+ ? { ...base, badges: [...(base.badges ?? []), { text: "history", title: "Matched in prompt history" }] }
1010
+ : base,
1011
+ );
1012
+ }
1013
+ this.#pickerPatch = { catalogue, flagged, add };
1014
+ return add;
1015
+ }
1016
+
1017
+ get searchInput(): Input {
1018
+ return this.#searchInput;
1019
+ }
1020
+
1021
+ /**
1022
+ * The session list as a native `list` keyed `"list"`, items keyed by
1023
+ * session path. The terminal owns scrolling and virtualization; item nodes
1024
+ * are cached per session and the list node while nothing visible changed.
1025
+ */
1026
+ describeList(): NativeNode {
1027
+ const currentPath = this.#getCurrentSessionPath();
1028
+ let items = this.#itemsNative;
1029
+ if (
1030
+ items?.version !== this.#itemsVersion ||
1031
+ items.showCwd !== this.#showCwd ||
1032
+ items.currentPath !== currentPath
1033
+ ) {
1034
+ items = {
1035
+ version: this.#itemsVersion,
1036
+ showCwd: this.#showCwd,
1037
+ currentPath,
1038
+ items: this.#menu.visibleItems.map(session => this.#describeSession(session, currentPath)),
1039
+ };
1040
+ this.#itemsNative = items;
1041
+ }
1042
+ const selected = this.#menu.selectedKey;
1043
+ const query = this.#searchInput.getValue();
1044
+ const memo = this.#listNative;
1045
+ if (memo?.items === items.items && memo.selected === selected && memo.query === query) return memo.node;
1046
+ const empty: TspText = this.#showCwd
1047
+ ? [span("No sessions found", "muted")]
1048
+ : [span("No sessions in current folder. Press ", "muted"), span("tab", "key"), span(" to view all.", "muted")];
1049
+ const list = node(
1050
+ "list",
1051
+ { selected: selected ?? null, filter: query.trim() || undefined, empty, virtual: true },
1052
+ items.items,
1053
+ "list",
1054
+ );
1055
+ this.#listNative = { items: items.items, selected, query, node: list };
1056
+ return list;
1057
+ }
1058
+
1059
+ #describeSession(session: T, currentPath: string | undefined): NativeNode {
1060
+ const date = formatSessionDate(session.modified);
1061
+ const pinned = this.#pinnedIds.has(session.id);
1062
+ const current = currentPath !== undefined && session.path === currentPath;
1063
+ const tagged = session as NativeSessionInfo;
1064
+ const cached = tagged[kNativeItem];
1065
+ if (
1066
+ cached?.date === date &&
1067
+ cached.pinned === pinned &&
1068
+ cached.current === current &&
1069
+ cached.showCwd === this.#showCwd
1070
+ ) {
1071
+ return cached.node;
1072
+ }
1073
+ const message = plainText(session.firstMessage).replace(/\s+/g, " ").trim();
1074
+ const detail: TspSpan[] = [];
1075
+ const push = (part: TspSpan): void => {
1076
+ if (detail.length > 0) detail.push(span(` ${theme.sep.dot} `, "dim"));
1077
+ detail.push(part);
1078
+ };
1079
+ if (session.title && message) push(span(message, "dim"));
1080
+ if (current) push(span("current", "accent"));
1081
+ const status = sessionStatusSpan(session.status);
1082
+ if (status) push(status);
1083
+ if (session.parentSessionPath) push(span("fork", "dim"));
1084
+ if (this.#showCwd && session.cwd) push(span(shortenPath(session.cwd), "path"));
1085
+ const item = node(
1086
+ "item",
1087
+ {
1088
+ label: (session.title && plainText(session.title)) || message || session.id,
1089
+ detail: detail.length > 0 ? detail : undefined,
1090
+ value: [span(`${date} ${theme.sep.dot} ${formatBytes(session.size)}`, "dim")],
1091
+ icon: pinned ? "pin" : undefined,
1092
+ },
1093
+ undefined,
1094
+ session.path,
1095
+ );
1096
+ tagged[kNativeItem] = { node: item, date, pinned, current, showCwd: this.#showCwd };
1097
+ return item;
1098
+ }
1099
+
602
1100
  invalidate(): void {
603
1101
  // No cached state to invalidate currently
604
1102
  }
@@ -626,23 +1124,6 @@ class SessionList<T extends SessionSelectorEntry> implements Component {
626
1124
  return lines;
627
1125
  }
628
1126
 
629
- // Format dates
630
- const formatDate = (date: Date): string => {
631
- const now = new Date();
632
- const diffMs = now.getTime() - date.getTime();
633
- const diffMins = Math.floor(diffMs / 60000);
634
- const diffHours = Math.floor(diffMs / 3600000);
635
- const diffDays = Math.floor(diffMs / 86400000);
636
-
637
- if (diffMins < 1) return "just now";
638
- if (diffMins < 60) return `${diffMins} minute${diffMins !== 1 ? "s" : ""} ago`;
639
- if (diffHours < 24) return `${diffHours} hour${diffHours !== 1 ? "s" : ""} ago`;
640
- if (diffDays === 1) return "1 day ago";
641
- if (diffDays < 7) return `${diffDays} days ago`;
642
-
643
- return date.toLocaleDateString();
644
- };
645
-
646
1127
  // Window the list around the selection by actual line height (3 lines
647
1128
  // per session, 4 when a title adds a preview line) until the viewport
648
1129
  // budget is spent, so short sessions never strand blank rows a
@@ -706,7 +1187,7 @@ class SessionList<T extends SessionSelectorEntry> implements Component {
706
1187
  // wrapping the whole line.
707
1188
  const dim = (s: string) => theme.fg("dim", s);
708
1189
  const dot = dim(theme.sep.dot);
709
- const modified = formatDate(session.modified);
1190
+ const modified = formatSessionDate(session.modified);
710
1191
  let metadata = ` ${dim(modified)} ${dot} ${dim(formatBytes(session.size))}`;
711
1192
  if (currentPath !== undefined && session.path === currentPath) {
712
1193
  metadata += ` ${dot} ${theme.fg("accent", "current")}`;
@@ -763,10 +1244,7 @@ class SessionList<T extends SessionSelectorEntry> implements Component {
763
1244
  matchesKey(keyData, "delete") ||
764
1245
  (matchesKey(keyData, "backspace") && this.#searchInput.getValue().length === 0)
765
1246
  ) {
766
- const selected = this.#menu.selectedItem;
767
- if (selected && this.onDeleteRequest) {
768
- this.onDeleteRequest(selected);
769
- }
1247
+ this.requestDelete();
770
1248
  return;
771
1249
  }
772
1250
  // Up arrow
@@ -795,10 +1273,7 @@ class SessionList<T extends SessionSelectorEntry> implements Component {
795
1273
  }
796
1274
  // Enter
797
1275
  if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
798
- const selected = this.#menu.selectedItem;
799
- if (selected && this.onSelect) {
800
- this.onSelect(selected);
801
- }
1276
+ this.resumeSelected();
802
1277
  return;
803
1278
  }
804
1279
  // Escape - cancel
@@ -853,6 +1328,12 @@ export interface SessionSelectorOptions<T extends SessionSelectorEntry = Session
853
1328
  pinnedIds?: ReadonlySet<string>;
854
1329
  /** Path of the live session, or a getter so detach/newSession stays accurate. */
855
1330
  currentSessionPath?: string | (() => string | undefined);
1331
+ /**
1332
+ * The picker is the whole program (`omp --resume`): the native picker
1333
+ * fills the screen surface (`size:"screen"`) instead of floating as a
1334
+ * sheet over the transcript.
1335
+ */
1336
+ standalone?: boolean;
856
1337
  }
857
1338
 
858
1339
  /**
@@ -890,6 +1371,40 @@ export class SessionSelectorComponent<T extends SessionSelectorEntry = SessionSe
890
1371
  readonly #fillHeight: boolean;
891
1372
  readonly #title: string;
892
1373
  readonly #scopeLabel: string | false | undefined;
1374
+ /** What `#messageContainer` shows, for the native description. */
1375
+ #message: SessionPickerMessage | undefined;
1376
+ /** The native picker shows `#message`'s error until the next key or pointer event. */
1377
+ #pickerErrorOpen = false;
1378
+ readonly #standalone: boolean;
1379
+ readonly #pickerTitle: string;
1380
+ /** The open delete confirmation's two answers, for the picker's confirm strip. */
1381
+ #deleteChoice: DeleteChoice<T> | null = null;
1382
+ /** The preview pane's content and the session it shows; follows the selection once it settles. */
1383
+ #preview: { session: T | undefined; nodes: readonly NativeChild[] } | undefined;
1384
+ #previewPending: T | undefined;
1385
+ #previewSettled: T | undefined;
1386
+ #previewTimer: NodeJS.Timeout | undefined;
1387
+ #pickerMemo:
1388
+ | {
1389
+ view: SessionPickerView;
1390
+ preview: readonly NativeChild[];
1391
+ scope: "folder" | "all";
1392
+ message: SessionPickerMessage | undefined;
1393
+ errorOpen: boolean;
1394
+ choice: DeleteChoice<T> | null;
1395
+ node: NativeNode;
1396
+ }
1397
+ | undefined;
1398
+ #nativeMemo:
1399
+ | {
1400
+ title: string;
1401
+ scope: "folder" | "all";
1402
+ message: SessionPickerMessage | undefined;
1403
+ dialog: HookSelectorComponent | null;
1404
+ list: NativeNode;
1405
+ node: NativeNode;
1406
+ }
1407
+ | undefined;
893
1408
 
894
1409
  constructor(
895
1410
  sessions: T[],
@@ -908,6 +1423,8 @@ export class SessionSelectorComponent<T extends SessionSelectorEntry = SessionSe
908
1423
  this.#getTerminalRows = options.getTerminalRows ?? (() => 24);
909
1424
  this.#fillHeight = options.fillHeight ?? false;
910
1425
  this.#title = options.title ?? "Resume Session";
1426
+ this.#pickerTitle = options.title ?? "Resume session";
1427
+ this.#standalone = options.standalone ?? false;
911
1428
  this.#scopeLabel = options.scopeLabel;
912
1429
  this.title = this.#headerLabel();
913
1430
  // One spacer of breathing room; OverlayPanel supplies the two outer
@@ -973,6 +1490,7 @@ export class SessionSelectorComponent<T extends SessionSelectorEntry = SessionSe
973
1490
  this.#toggling = true;
974
1491
  this.#messageContainer.clear();
975
1492
  this.#messageContainer.addChild(new Text(theme.fg("muted", "Loading all projects…"), 0, 0));
1493
+ this.#message = { kind: "loading", text: "Loading all projects…" };
976
1494
  this.#onRequestRender?.();
977
1495
  try {
978
1496
  global = await this.#loadAllSessions();
@@ -984,6 +1502,7 @@ export class SessionSelectorComponent<T extends SessionSelectorEntry = SessionSe
984
1502
  }
985
1503
  this.#globalSessions = global;
986
1504
  this.#messageContainer.clear();
1505
+ this.#message = undefined;
987
1506
  this.#toggling = false;
988
1507
  }
989
1508
  this.#scope = "all";
@@ -1015,15 +1534,19 @@ export class SessionSelectorComponent<T extends SessionSelectorEntry = SessionSe
1015
1534
  */
1016
1535
  override dispose(): void {
1017
1536
  this.#sessionList.dispose();
1537
+ clearTimeout(this.#previewTimer);
1018
1538
  super.dispose();
1019
1539
  }
1020
1540
 
1021
1541
  #clearError(): void {
1022
1542
  this.#messageContainer.clear();
1543
+ this.#message = undefined;
1023
1544
  }
1024
1545
 
1025
1546
  #showError(message: string): void {
1026
1547
  this.#messageContainer.clear();
1548
+ this.#message = { kind: "error", text: `Error: ${plainText(message)}` };
1549
+ this.#pickerErrorOpen = true;
1027
1550
  this.#messageContainer.addChild(new Text(theme.fg("error", `Error: ${replaceTabs(message)}`), 0, 0));
1028
1551
  this.#messageContainer.addChild(new Spacer(1));
1029
1552
  }
@@ -1032,6 +1555,7 @@ export class SessionSelectorComponent<T extends SessionSelectorEntry = SessionSe
1032
1555
  const displayName = session.title || session.firstMessage.slice(0, 40) || session.id;
1033
1556
  const closeDialog = () => {
1034
1557
  this.#confirmationDialog = null;
1558
+ this.#deleteChoice = null;
1035
1559
  // Restore the SessionList into the content slot so the picker is back
1036
1560
  // to its normal layout on the very next render — the same frame the
1037
1561
  // dialog disappears.
@@ -1039,29 +1563,39 @@ export class SessionSelectorComponent<T extends SessionSelectorEntry = SessionSe
1039
1563
  this.#contentSlot.addChild(this.#sessionList);
1040
1564
  this.#onRequestRender?.();
1041
1565
  };
1042
- this.#confirmationDialog = new HookSelectorComponent(
1043
- `Delete session?\n${displayName}`,
1044
- ["Yes", "No"],
1045
- async (option: string) => {
1046
- if (option === "Yes" && this.#onDelete) {
1047
- this.#clearError();
1048
- try {
1049
- const deleted = await this.#onDelete(session);
1050
- if (deleted) {
1051
- this.#sessionList.removeSession(session.path);
1052
- this.#folderSessions = this.#folderSessions.filter(s => s.path !== session.path);
1053
- if (this.#globalSessions) {
1054
- this.#globalSessions = this.#globalSessions.filter(s => s.path !== session.path);
1055
- }
1566
+ const answer = async (option: string) => {
1567
+ if (option === "Yes" && this.#onDelete) {
1568
+ this.#clearError();
1569
+ try {
1570
+ const deleted = await this.#onDelete(session);
1571
+ if (deleted) {
1572
+ this.#sessionList.removeSession(session.path);
1573
+ this.#folderSessions = this.#folderSessions.filter(s => s.path !== session.path);
1574
+ if (this.#globalSessions) {
1575
+ this.#globalSessions = this.#globalSessions.filter(s => s.path !== session.path);
1056
1576
  }
1057
- } catch (err) {
1058
- this.#showError(err instanceof Error ? err.message : String(err));
1059
1577
  }
1578
+ } catch (err) {
1579
+ this.#showError(err instanceof Error ? err.message : String(err));
1060
1580
  }
1061
- closeDialog();
1062
- },
1581
+ }
1582
+ closeDialog();
1583
+ };
1584
+ this.#confirmationDialog = new HookSelectorComponent(
1585
+ `Delete session?\n${displayName}`,
1586
+ ["Yes", "No"],
1587
+ answer,
1063
1588
  closeDialog,
1064
1589
  );
1590
+ this.#deleteChoice = {
1591
+ session,
1592
+ confirm: () => {
1593
+ // The strip goes at once; the dialog stays until the delete settles, as after Enter on "Yes".
1594
+ this.#deleteChoice = null;
1595
+ void answer("Yes");
1596
+ },
1597
+ cancel: closeDialog,
1598
+ };
1065
1599
  // Swap the SessionList out of the content slot and mount the dialog in its
1066
1600
  // place: the dialog competes only with the SessionList's rendered budget,
1067
1601
  // never the SessionList AND the picker chrome, so the picker frame stays
@@ -1108,8 +1642,235 @@ export class SessionSelectorComponent<T extends SessionSelectorEntry = SessionSe
1108
1642
  return [row("", width), row(hint, width), row("", width), bottomBorder(width)];
1109
1643
  }
1110
1644
 
1645
+ /** Scope tabs replace the "(current folder)" title suffix when the scope can toggle. */
1646
+ #hasScopeTabs(): boolean {
1647
+ return this.#scopeLabel === undefined && this.#sessionList.onToggleScope !== undefined;
1648
+ }
1649
+
1650
+ /** Over the transcript the picker is its own sheet (`layer`); the standalone app fills the screen surface. */
1651
+ nativeSheet(cx: DescribeContext): boolean {
1652
+ return cx.supports("picker") && !this.#standalone;
1653
+ }
1654
+
1655
+ /**
1656
+ * With the `picker` kind: the data-first session picker (§4.7). Otherwise
1657
+ * scope `tabs`, loading/error message, then the search `input` and session
1658
+ * `list` — or the delete-confirmation dialog in their place, as on the ANSI
1659
+ * path — and the key-hint footer.
1660
+ */
1661
+ override describe(cx: DescribeContext): NativeNode {
1662
+ if (cx.supports("picker")) return this.#describePicker();
1663
+ const list = this.#sessionList.describeList();
1664
+ const memo = this.#nativeMemo;
1665
+ if (
1666
+ memo?.title === this.title &&
1667
+ memo.scope === this.#scope &&
1668
+ memo.message === this.#message &&
1669
+ memo.dialog === this.#confirmationDialog &&
1670
+ memo.list === list
1671
+ ) {
1672
+ return memo.node;
1673
+ }
1674
+ const tabs = this.#hasScopeTabs();
1675
+ const children: NativeChild[] = [];
1676
+ if (tabs) children.push(node("tabs", { items: SCOPE_TABS, active: this.#scope }, undefined, "scope"));
1677
+ const message = this.#message;
1678
+ if (message?.kind === "loading") {
1679
+ children.push(node("spinner", { label: [span(message.text, "muted")] }, undefined, "message"));
1680
+ } else if (message) {
1681
+ children.push(node("text", { spans: [span(message.text, "error")] }, undefined, "message"));
1682
+ }
1683
+ if (this.#confirmationDialog) {
1684
+ children.push(this.#confirmationDialog);
1685
+ } else {
1686
+ children.push(this.#sessionList.searchInput, list);
1687
+ }
1688
+ const hints: (NativeHint | undefined)[] = [
1689
+ { keys: ["delete", "backspace"], label: "delete" },
1690
+ { keys: ["enter"], label: "select" },
1691
+ ];
1692
+ if (this.#sessionList.onToggleScope) {
1693
+ hints.push({ keys: ["tab"], label: this.#scope === "all" ? "current folder" : "all projects" });
1694
+ }
1695
+ hints.push({ keys: [boundKeys("app.interrupt", ["escape"])[0] ?? "escape"], label: "cancel" });
1696
+ const result = overlayCard("omp.overlay.sessions", tabs ? this.#title : this.title, [
1697
+ ...children,
1698
+ hintsRow(hints),
1699
+ ]);
1700
+ this.#nativeMemo = {
1701
+ title: this.title,
1702
+ scope: this.#scope,
1703
+ message,
1704
+ dialog: this.#confirmationDialog,
1705
+ list,
1706
+ node: result,
1707
+ };
1708
+ return result;
1709
+ }
1710
+
1711
+ /**
1712
+ * `lg` cards sheet (`screen` for the standalone app): This folder / All
1713
+ * projects tabs, sessions grouped by day (ranked flat while searching),
1714
+ * the selected session's preview, the delete confirm strip, and the
1715
+ * actions of the keys Enter, Delete/Backspace, Tab and Esc.
1716
+ */
1717
+ #describePicker(): NativeNode {
1718
+ const list = this.#sessionList;
1719
+ const view = list.pickerView();
1720
+ const preview = this.#previewFor(list.selectedSession);
1721
+ const message = this.#message;
1722
+ const errorOpen = this.#pickerErrorOpen && message?.kind === "error";
1723
+ const choice = this.#deleteChoice;
1724
+ const memo = this.#pickerMemo;
1725
+ if (
1726
+ memo?.view === view &&
1727
+ memo.preview === preview &&
1728
+ memo.scope === this.#scope &&
1729
+ memo.message === message &&
1730
+ memo.errorOpen === errorOpen &&
1731
+ memo.choice === choice
1732
+ ) {
1733
+ return memo.node;
1734
+ }
1735
+ const loading = message?.kind === "loading";
1736
+ const toggle = list.onToggleScope !== undefined;
1737
+ const actions = [pickerAction("resume", "Resume", "enter", { primary: true })];
1738
+ if (this.#onDelete) actions.push(pickerAction("delete", "Delete", "backspace"));
1739
+ if (toggle) {
1740
+ actions.push(pickerAction("scope", this.#scope === "all" ? "This folder" : "All projects", "tab"));
1741
+ }
1742
+ actions.push(
1743
+ pickerAction("close", "Close", boundKeys("app.interrupt", ["escape"])[0] ?? "escape", { end: true }),
1744
+ );
1745
+ const subtitle = this.#scopeLabel === false ? undefined : (this.#scopeLabel ?? path.basename(getProjectDir()));
1746
+ const result = picker(
1747
+ {
1748
+ title: this.#pickerTitle,
1749
+ ...(subtitle ? { subtitle } : {}),
1750
+ icon: "history",
1751
+ noun: "sessions",
1752
+ size: this.#standalone ? "screen" : "lg",
1753
+ layout: "cards",
1754
+ preview: "side",
1755
+ query: view.query,
1756
+ cursor: view.cursor,
1757
+ placeholder: "Search sessions…",
1758
+ ...(this.#hasScopeTabs()
1759
+ ? {
1760
+ tabs: [
1761
+ { id: "folder", label: "This folder" },
1762
+ { id: "all", label: "All projects" },
1763
+ ],
1764
+ tab: loading ? "all" : this.#scope,
1765
+ }
1766
+ : {}),
1767
+ columns: [
1768
+ { id: "when", format: "time" },
1769
+ { id: "size", format: "dim" },
1770
+ ],
1771
+ items: view.items,
1772
+ ...(view.itemsAdd ? { itemsAdd: view.itemsAdd } : {}),
1773
+ order: view.order,
1774
+ ...(view.hits ? { hits: view.hits } : {}),
1775
+ selected: view.selected,
1776
+ current: view.current,
1777
+ total: view.total,
1778
+ actions,
1779
+ state: loading ? "loading" : errorOpen ? "error" : "ready",
1780
+ ...(loading || errorOpen ? { message: message!.text } : {}),
1781
+ empty: this.#scope === "all" || !toggle ? "No sessions yet" : "No sessions in this folder yet",
1782
+ confirm: choice
1783
+ ? {
1784
+ text: `Delete “${sessionLabel(choice.session)}”? This removes the session file.`,
1785
+ act: "delete-confirm",
1786
+ label: "Delete",
1787
+ }
1788
+ : null,
1789
+ },
1790
+ preview,
1791
+ );
1792
+ this.#pickerMemo = { view, preview, scope: this.#scope, message, errorOpen, choice, node: result };
1793
+ return result;
1794
+ }
1795
+
1796
+ /**
1797
+ * The preview children for `target`: built at once for the first
1798
+ * selection, then only after the selection has rested
1799
+ * {@link PREVIEW_SETTLE_MS} (the old preview stays meanwhile).
1800
+ */
1801
+ #previewFor(target: T | undefined): readonly NativeChild[] {
1802
+ const shown = this.#preview;
1803
+ if (shown && shown.session === target) return shown.nodes;
1804
+ if (shown && target !== this.#previewSettled) {
1805
+ if (this.#previewPending !== target || this.#previewTimer === undefined) {
1806
+ clearTimeout(this.#previewTimer);
1807
+ this.#previewPending = target;
1808
+ this.#previewTimer = setTimeout(() => {
1809
+ this.#previewTimer = undefined;
1810
+ this.#previewSettled = this.#previewPending;
1811
+ this.#onRequestRender?.();
1812
+ }, PREVIEW_SETTLE_MS);
1813
+ }
1814
+ return shown.nodes;
1815
+ }
1816
+ const nodes = target
1817
+ ? sessionPreview(
1818
+ target,
1819
+ target.parentSessionPath ? this.#sessionList.parentLabel(target.parentSessionPath) : undefined,
1820
+ )
1821
+ : [];
1822
+ this.#preview = { session: target, nodes };
1823
+ return nodes;
1824
+ }
1825
+
1826
+ /**
1827
+ * Picker pointer events run the keys' paths: a row click selects (arrows),
1828
+ * a second click or `Resume` resumes (Enter), `Delete` asks (Delete), the
1829
+ * tab or `All projects` toggles scope (Tab), `Close` cancels (Esc), and
1830
+ * the confirm strip answers the delete dialog (Yes / Esc).
1831
+ */
1832
+ #handlePickerEvent(ev: PickerEvent): void {
1833
+ const list = this.#sessionList;
1834
+ const choice = this.#deleteChoice;
1835
+ if (ev.kind !== "action") {
1836
+ if (choice) return;
1837
+ if (ev.kind === "select") list.selectSession(ev.item);
1838
+ else list.confirmSession(ev.item);
1839
+ return;
1840
+ }
1841
+ if (ev.act === "delete-confirm") choice?.confirm();
1842
+ else if (ev.act === "cancel") choice?.cancel();
1843
+ else if (ev.act === "close") {
1844
+ if (choice) choice.cancel();
1845
+ else list.onCancel?.();
1846
+ } else if (choice) return;
1847
+ else if (ev.act === "resume") list.resumeSelected();
1848
+ else if (ev.act === "delete") list.requestDelete();
1849
+ else if (ev.act === "scope" || (ev.act === "tab" && ev.value !== undefined && ev.value !== this.#scope)) {
1850
+ list.onToggleScope?.();
1851
+ } else if (ev.act === "clear") list.clearSearch();
1852
+ }
1853
+
1854
+ /** Picker events (root keypath); fallback: scope tab → toggle scope, click on a session → resume it, exactly like a mouse click. */
1855
+ handleNativeEvent(event: NativeUiEvent): void {
1856
+ if (this.#inputLocked) return;
1857
+ const ev = pickerEvent(event);
1858
+ if (ev) {
1859
+ this.#pickerErrorOpen = false;
1860
+ this.#handlePickerEvent(ev);
1861
+ return;
1862
+ }
1863
+ if (event.type !== "select" && event.type !== "activate") return;
1864
+ if (event.key === "scope") {
1865
+ if (event.type === "select" && event.item !== this.#scope) void this.#toggleScope();
1866
+ return;
1867
+ }
1868
+ if (event.key === "list" && !this.#confirmationDialog) this.#sessionList.confirmSession(event.item);
1869
+ }
1870
+
1111
1871
  handleInput(keyData: string): void {
1112
1872
  if (this.#inputLocked) return;
1873
+ this.#pickerErrorOpen = false;
1113
1874
  if (keyData.startsWith("\x1b[<")) {
1114
1875
  this.#handleMouse(keyData);
1115
1876
  return;