@oh-my-pi/pi-tui 18.2.3 → 18.2.5

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 (750) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +43 -0
  3. package/dist/types/app-keybindings.d.ts +395 -0
  4. package/dist/types/apps/autoresearch-dashboard.d.ts +43 -0
  5. package/dist/types/apps/autoresearch-data.d.ts +15 -0
  6. package/dist/types/apps/cleanse-board.d.ts +66 -0
  7. package/dist/types/apps/cleanse-picker.d.ts +23 -0
  8. package/dist/types/apps/debug/log-formatting.d.ts +6 -0
  9. package/dist/types/apps/debug/log-viewer.d.ts +81 -0
  10. package/dist/types/apps/debug/protocol-probe.d.ts +42 -0
  11. package/dist/types/apps/debug/raw-sse-buffer.d.ts +58 -0
  12. package/dist/types/apps/debug/raw-sse.d.ts +26 -0
  13. package/dist/types/apps/debug/terminal-info.d.ts +34 -0
  14. package/dist/types/apps/debug/viewer-frame.d.ts +52 -0
  15. package/dist/types/apps/git/avatar.d.ts +9 -0
  16. package/dist/types/apps/git/colors.d.ts +49 -0
  17. package/dist/types/apps/git/diff-pane.d.ts +188 -0
  18. package/dist/types/apps/git/git-tui.d.ts +66 -0
  19. package/dist/types/apps/git/sidebar.d.ts +149 -0
  20. package/dist/types/apps/git/state.d.ts +117 -0
  21. package/dist/types/apps/if-bench-board.d.ts +75 -0
  22. package/dist/types/apps/live-visualizer.d.ts +33 -0
  23. package/dist/types/apps/ps-data.d.ts +51 -0
  24. package/dist/types/apps/ps-top.d.ts +33 -0
  25. package/dist/types/apps/session-picker.d.ts +26 -0
  26. package/dist/types/apps/setup-model-picker.d.ts +13 -0
  27. package/dist/types/apps/standalone-picker.d.ts +42 -0
  28. package/dist/types/autocomplete.d.ts +4 -0
  29. package/dist/types/chat/advisor-message.d.ts +11 -0
  30. package/dist/types/chat/assistant-message.d.ts +106 -0
  31. package/dist/types/chat/background-tan-message.d.ts +9 -0
  32. package/dist/types/chat/bash-execution.d.ts +47 -0
  33. package/dist/types/chat/cache-invalidation-marker.d.ts +50 -0
  34. package/dist/types/chat/chat-transcript-builder.d.ts +50 -0
  35. package/dist/types/chat/collab-prompt-message.d.ts +10 -0
  36. package/dist/types/chat/compaction-summary-message.d.ts +54 -0
  37. package/dist/types/chat/custom-message.d.ts +10 -0
  38. package/dist/types/chat/display-preferences.d.ts +13 -0
  39. package/dist/types/chat/eval-execution.d.ts +29 -0
  40. package/dist/types/chat/execution-shared.d.ts +51 -0
  41. package/dist/types/chat/extension-types.d.ts +28 -0
  42. package/dist/types/chat/hook-message.d.ts +10 -0
  43. package/dist/types/chat/image-loading.d.ts +46 -0
  44. package/dist/types/chat/late-diagnostics-message.d.ts +21 -0
  45. package/dist/types/chat/messages.d.ts +189 -0
  46. package/dist/types/chat/reaction.d.ts +30 -0
  47. package/dist/types/chat/read-target.d.ts +4 -0
  48. package/dist/types/chat/read-tool-group.d.ts +67 -0
  49. package/dist/types/chat/served-model-marker.d.ts +47 -0
  50. package/dist/types/chat/skill-message.d.ts +33 -0
  51. package/dist/types/chat/skill-title-input.d.ts +14 -0
  52. package/dist/types/chat/stripped-tool-calls-placeholder.d.ts +12 -0
  53. package/dist/types/chat/thinking-display.d.ts +12 -0
  54. package/dist/types/chat/todo-reminder.d.ts +13 -0
  55. package/dist/types/chat/tool-execution.d.ts +138 -0
  56. package/dist/types/chat/transcript-browser.d.ts +93 -0
  57. package/dist/types/chat/transcript-entry.d.ts +45 -0
  58. package/dist/types/chat/transcript-outline.d.ts +86 -0
  59. package/dist/types/chat/transcript-render-helpers.d.ts +92 -0
  60. package/dist/types/chat/ttsr-notification.d.ts +23 -0
  61. package/dist/types/chat/user-message.d.ts +52 -0
  62. package/dist/types/chrome/chat-block.d.ts +64 -0
  63. package/dist/types/chrome/collab-qrcode.d.ts +28 -0
  64. package/dist/types/chrome/context-thresholds.d.ts +10 -0
  65. package/dist/types/chrome/countdown-timer.d.ts +14 -0
  66. package/dist/types/chrome/diff.d.ts +14 -0
  67. package/dist/types/chrome/dynamic-border.d.ts +16 -0
  68. package/dist/types/chrome/error-block.d.ts +17 -0
  69. package/dist/types/chrome/form-theme.d.ts +3 -0
  70. package/dist/types/chrome/format.d.ts +12 -0
  71. package/dist/types/chrome/index.d.ts +20 -0
  72. package/dist/types/chrome/keybinding-hints.d.ts +40 -0
  73. package/dist/types/chrome/live-board.d.ts +23 -0
  74. package/dist/types/chrome/local-date.d.ts +4 -0
  75. package/dist/types/chrome/message-divider.d.ts +22 -0
  76. package/dist/types/chrome/message-frame.d.ts +52 -0
  77. package/dist/types/chrome/message-notice.d.ts +35 -0
  78. package/dist/types/chrome/overlay-box.d.ts +74 -0
  79. package/dist/types/chrome/qrcode.d.ts +48 -0
  80. package/dist/types/chrome/segment-track.d.ts +27 -0
  81. package/dist/types/chrome/select-list-mouse-routing.d.ts +4 -0
  82. package/dist/types/chrome/selector-helpers.d.ts +52 -0
  83. package/dist/types/chrome/shared.d.ts +5 -0
  84. package/dist/types/chrome/streaming-panel.d.ts +19 -0
  85. package/dist/types/chrome/tool-activity.d.ts +17 -0
  86. package/dist/types/chrome/transcript-container.d.ts +129 -0
  87. package/dist/types/chrome/transcript-status.d.ts +12 -0
  88. package/dist/types/chrome/visual-truncate.d.ts +19 -0
  89. package/dist/types/components/disclosure.d.ts +42 -0
  90. package/dist/types/components/form.d.ts +130 -0
  91. package/dist/types/components/image.d.ts +18 -16
  92. package/dist/types/components/key-value-list.d.ts +24 -0
  93. package/dist/types/components/layout/geometry.d.ts +73 -0
  94. package/dist/types/components/layout/row.d.ts +55 -0
  95. package/dist/types/components/layout/split-pane.d.ts +80 -0
  96. package/dist/types/components/layout/stack.d.ts +50 -0
  97. package/dist/types/components/menu-selection.d.ts +71 -0
  98. package/dist/types/components/metric.d.ts +44 -0
  99. package/dist/types/components/progress-bar.d.ts +36 -0
  100. package/dist/types/components/scroll-view.d.ts +53 -19
  101. package/dist/types/components/scroll-viewport.d.ts +46 -0
  102. package/dist/types/components/section.d.ts +19 -0
  103. package/dist/types/components/select-list.d.ts +46 -2
  104. package/dist/types/components/settings-list.d.ts +20 -0
  105. package/dist/types/components/table.d.ts +32 -0
  106. package/dist/types/components/tree-view.d.ts +130 -0
  107. package/dist/types/components/wizard-step.d.ts +60 -0
  108. package/dist/types/hotkeys-markdown.d.ts +7 -0
  109. package/dist/types/index.d.ts +17 -0
  110. package/dist/types/keybinding-matchers.d.ts +35 -0
  111. package/dist/types/keys.d.ts +2 -2
  112. package/dist/types/lang-from-path.d.ts +9 -0
  113. package/dist/types/overlays/advisor-config.d.ts +114 -0
  114. package/dist/types/overlays/agent-activity.d.ts +41 -0
  115. package/dist/types/overlays/agent-hub-projection.d.ts +33 -0
  116. package/dist/types/overlays/agent-hub-renderer.d.ts +53 -0
  117. package/dist/types/overlays/agent-hub-types.d.ts +99 -0
  118. package/dist/types/overlays/agent-hub.d.ts +126 -0
  119. package/dist/types/overlays/agent-transcript-viewer.d.ts +71 -0
  120. package/dist/types/overlays/agents-hub.d.ts +72 -0
  121. package/dist/types/overlays/ask-dialog.d.ts +88 -0
  122. package/dist/types/overlays/bordered-loader.d.ts +11 -0
  123. package/dist/types/overlays/btw-history-panel.d.ts +29 -0
  124. package/dist/types/overlays/btw-history.d.ts +17 -0
  125. package/dist/types/overlays/btw-panel.d.ts +26 -0
  126. package/dist/types/overlays/cleanse-panel.d.ts +36 -0
  127. package/dist/types/overlays/codex-reset-fireworks.d.ts +52 -0
  128. package/dist/types/overlays/composer-shape-preview.d.ts +47 -0
  129. package/dist/types/overlays/composer-shape-registry.d.ts +50 -0
  130. package/dist/types/overlays/copy-selector.d.ts +48 -0
  131. package/dist/types/overlays/copy-targets.d.ts +63 -0
  132. package/dist/types/overlays/error-banner.d.ts +13 -0
  133. package/dist/types/overlays/extensions/display-text.d.ts +10 -0
  134. package/dist/types/overlays/extensions/extension-dashboard.d.ts +63 -0
  135. package/dist/types/overlays/extensions/extension-list.d.ts +54 -0
  136. package/dist/types/overlays/extensions/inspector-model.d.ts +109 -0
  137. package/dist/types/overlays/extensions/inspector-panel.d.ts +17 -0
  138. package/dist/types/overlays/extensions/live-tool-session.d.ts +28 -0
  139. package/dist/types/overlays/extensions/mcp-runtime.d.ts +107 -0
  140. package/dist/types/overlays/extensions/state-manager.d.ts +42 -0
  141. package/dist/types/overlays/extensions/types.d.ts +165 -0
  142. package/dist/types/overlays/history-search.d.ts +16 -0
  143. package/dist/types/overlays/hook-editor.d.ts +44 -0
  144. package/dist/types/overlays/hook-input.d.ts +20 -0
  145. package/dist/types/overlays/hook-selector.d.ts +71 -0
  146. package/dist/types/overlays/hub-frame.d.ts +77 -0
  147. package/dist/types/overlays/login-dialog.d.ts +43 -0
  148. package/dist/types/overlays/logout-account-selector.d.ts +15 -0
  149. package/dist/types/overlays/mcp-add-wizard.d.ts +87 -0
  150. package/dist/types/overlays/model-browser.d.ts +213 -0
  151. package/dist/types/overlays/model-hub.d.ts +72 -0
  152. package/dist/types/overlays/model-picker.d.ts +74 -0
  153. package/dist/types/overlays/model-selector.d.ts +52 -0
  154. package/dist/types/overlays/move-overlay.d.ts +39 -0
  155. package/dist/types/overlays/oauth-selector.d.ts +41 -0
  156. package/dist/types/overlays/omfg-panel.d.ts +20 -0
  157. package/dist/types/overlays/pause-screen.d.ts +43 -0
  158. package/dist/types/overlays/plan-review-overlay.d.ts +89 -0
  159. package/dist/types/overlays/plan-save-overlay.d.ts +19 -0
  160. package/dist/types/overlays/plan-toc.d.ts +41 -0
  161. package/dist/types/overlays/plugin-selector.d.ts +28 -0
  162. package/dist/types/overlays/plugin-settings.d.ts +180 -0
  163. package/dist/types/overlays/queue-mode-selector.d.ts +11 -0
  164. package/dist/types/overlays/reset-usage-selector.d.ts +23 -0
  165. package/dist/types/overlays/rewind-selector.d.ts +61 -0
  166. package/dist/types/overlays/running-subagent-badge.d.ts +6 -0
  167. package/dist/types/overlays/session-account-selector.d.ts +24 -0
  168. package/dist/types/overlays/session-info-overlay.d.ts +17 -0
  169. package/dist/types/overlays/session-observer-registry.d.ts +77 -0
  170. package/dist/types/overlays/session-selector.d.ts +131 -0
  171. package/dist/types/overlays/settings-defs.d.ts +119 -0
  172. package/dist/types/overlays/settings-selector.d.ts +73 -0
  173. package/dist/types/overlays/show-images-selector.d.ts +11 -0
  174. package/dist/types/overlays/snapcompact-shape-preview.d.ts +31 -0
  175. package/dist/types/overlays/theme-selector.d.ts +12 -0
  176. package/dist/types/overlays/thinking-selector.d.ts +12 -0
  177. package/dist/types/overlays/tiny-title-download-progress.d.ts +21 -0
  178. package/dist/types/overlays/tree-selector.d.ts +92 -0
  179. package/dist/types/overlays/usage-dashboard.d.ts +94 -0
  180. package/dist/types/overlays/usage-display.d.ts +5 -0
  181. package/dist/types/overlays/usage-row.d.ts +17 -0
  182. package/dist/types/prompt/attachment-chips.d.ts +19 -0
  183. package/dist/types/prompt/composer-attachments.d.ts +71 -0
  184. package/dist/types/prompt/composer-cache.d.ts +37 -0
  185. package/dist/types/prompt/composer.d.ts +159 -0
  186. package/dist/types/prompt/custom-editor.d.ts +261 -0
  187. package/dist/types/prompt/editor-top-gap.d.ts +18 -0
  188. package/dist/types/prompt/emoji-autocomplete.d.ts +16 -0
  189. package/dist/types/prompt/github-ref-autocomplete.d.ts +35 -0
  190. package/dist/types/prompt/gradient-highlight.d.ts +32 -0
  191. package/dist/types/prompt/image-format.d.ts +4 -0
  192. package/dist/types/prompt/image-references.d.ts +26 -0
  193. package/dist/types/prompt/interactive-context-helpers.d.ts +43 -0
  194. package/dist/types/prompt/internal-url-autocomplete.d.ts +56 -0
  195. package/dist/types/prompt/macos-spelling.d.ts +44 -0
  196. package/dist/types/prompt/magic-keyword-boundary.d.ts +9 -0
  197. package/dist/types/prompt/magic-keywords.d.ts +26 -0
  198. package/dist/types/prompt/markdown-prose.d.ts +15 -0
  199. package/dist/types/prompt/model-mention-autocomplete.d.ts +25 -0
  200. package/dist/types/prompt/model-mention-syntax.d.ts +24 -0
  201. package/dist/types/prompt/orchestrate.d.ts +13 -0
  202. package/dist/types/prompt/prompt-action-autocomplete.d.ts +56 -0
  203. package/dist/types/prompt/queue-input.d.ts +8 -0
  204. package/dist/types/prompt/skill-tokens.d.ts +13 -0
  205. package/dist/types/prompt/tools-markdown.d.ts +12 -0
  206. package/dist/types/prompt/ultrathink.d.ts +13 -0
  207. package/dist/types/prompt/usage-amounts.d.ts +26 -0
  208. package/dist/types/prompt/video.d.ts +18 -0
  209. package/dist/types/prompt/welcome.d.ts +83 -0
  210. package/dist/types/prompt/workflow.d.ts +13 -0
  211. package/dist/types/render/code-cell.d.ts +51 -0
  212. package/dist/types/render/file-list.d.ts +22 -0
  213. package/dist/types/render/hyperlink.d.ts +80 -0
  214. package/dist/types/render/index.d.ts +16 -0
  215. package/dist/types/render/output-block.d.ts +54 -0
  216. package/dist/types/render/output-pane.d.ts +80 -0
  217. package/dist/types/render/render-utils.d.ts +312 -0
  218. package/dist/types/render/sixel.d.ts +21 -0
  219. package/dist/types/render/status-line.d.ts +23 -0
  220. package/dist/types/render/tool-card.d.ts +71 -0
  221. package/dist/types/render/tree-list.d.ts +29 -0
  222. package/dist/types/render/types.d.ts +17 -0
  223. package/dist/types/render/utils.d.ts +43 -0
  224. package/dist/types/render/width-aware-text.d.ts +23 -0
  225. package/dist/types/setup/lazy.d.ts +3 -0
  226. package/dist/types/setup/scenes/composer.d.ts +3 -0
  227. package/dist/types/setup/scenes/glyph.d.ts +3 -0
  228. package/dist/types/setup/scenes/model.d.ts +3 -0
  229. package/dist/types/setup/scenes/outro.d.ts +2 -0
  230. package/dist/types/setup/scenes/providers.d.ts +3 -0
  231. package/dist/types/setup/scenes/sign-in.d.ts +21 -0
  232. package/dist/types/setup/scenes/splash.d.ts +11 -0
  233. package/dist/types/setup/scenes/theme.d.ts +3 -0
  234. package/dist/types/setup/scenes/types.d.ts +110 -0
  235. package/dist/types/setup/scenes/web-search.d.ts +22 -0
  236. package/dist/types/setup/setup-version.d.ts +11 -0
  237. package/dist/types/setup/startup-splash.d.ts +9 -0
  238. package/dist/types/setup/wizard-overlay.d.ts +16 -0
  239. package/dist/types/setup/wizard.d.ts +25 -0
  240. package/dist/types/status-line/component.d.ts +182 -0
  241. package/dist/types/status-line/context-usage.d.ts +156 -0
  242. package/dist/types/status-line/footer.d.ts +30 -0
  243. package/dist/types/status-line/git-utils.d.ts +22 -0
  244. package/dist/types/status-line/host.d.ts +129 -0
  245. package/dist/types/status-line/index.d.ts +6 -0
  246. package/dist/types/status-line/loop.d.ts +18 -0
  247. package/dist/types/status-line/metrics.d.ts +21 -0
  248. package/dist/types/status-line/presets.d.ts +3 -0
  249. package/dist/types/status-line/schema.d.ts +15 -0
  250. package/dist/types/status-line/segments.d.ts +5 -0
  251. package/dist/types/status-line/separators.d.ts +3 -0
  252. package/dist/types/status-line/types.d.ts +236 -0
  253. package/dist/types/stdin-buffer.d.ts +4 -0
  254. package/dist/types/theme/color.d.ts +24 -0
  255. package/dist/types/theme/defaults/index.d.ts +9489 -0
  256. package/dist/types/theme/index.d.ts +4 -0
  257. package/dist/types/theme/loader.d.ts +23 -0
  258. package/dist/types/theme/mermaid-cache.d.ts +26 -0
  259. package/dist/types/theme/schema-validation.d.ts +3 -0
  260. package/dist/types/theme/schema.d.ts +25 -0
  261. package/dist/types/theme/session-color.d.ts +43 -0
  262. package/dist/types/theme/shimmer.d.ts +44 -0
  263. package/dist/types/theme/symbols.d.ts +34 -0
  264. package/dist/types/theme/theme-class.d.ts +299 -0
  265. package/dist/types/theme/theme.d.ts +111 -0
  266. package/dist/types/theme/tui-adapters.d.ts +30 -0
  267. package/dist/types/thinking.d.ts +165 -0
  268. package/dist/types/tools/agent-tree.d.ts +36 -0
  269. package/dist/types/tools/ask.d.ts +67 -0
  270. package/dist/types/tools/ast-edit.d.ts +53 -0
  271. package/dist/types/tools/ast-grep.d.ts +52 -0
  272. package/dist/types/tools/autoresearch.d.ts +156 -0
  273. package/dist/types/tools/bash-interactive.d.ts +28 -0
  274. package/dist/types/tools/bash.d.ts +103 -0
  275. package/dist/types/tools/conflict-detect.d.ts +43 -0
  276. package/dist/types/tools/debug.d.ts +60 -0
  277. package/dist/types/tools/default-renderer.d.ts +24 -0
  278. package/dist/types/tools/edit.d.ts +158 -0
  279. package/dist/types/tools/eval-format/code-mode-declarations.d.ts +5 -0
  280. package/dist/types/tools/eval-format/index.d.ts +5 -0
  281. package/dist/types/tools/eval-format/javascript.d.ts +2 -0
  282. package/dist/types/tools/eval-format/python.d.ts +2 -0
  283. package/dist/types/tools/eval.d.ts +94 -0
  284. package/dist/types/tools/fetch.d.ts +42 -0
  285. package/dist/types/tools/gh-format.d.ts +8 -0
  286. package/dist/types/tools/github.d.ts +144 -0
  287. package/dist/types/tools/glob.d.ts +44 -0
  288. package/dist/types/tools/goal.d.ts +42 -0
  289. package/dist/types/tools/grep.d.ts +64 -0
  290. package/dist/types/tools/grouped-file-output.d.ts +71 -0
  291. package/dist/types/tools/hashline-format.d.ts +28 -0
  292. package/dist/types/tools/hub.d.ts +340 -0
  293. package/dist/types/tools/index.d.ts +4 -0
  294. package/dist/types/tools/json-tree.d.ts +59 -0
  295. package/dist/types/tools/line-ranges.d.ts +29 -0
  296. package/dist/types/tools/list-limit.d.ts +15 -0
  297. package/dist/types/tools/lsp.d.ts +72 -0
  298. package/dist/types/tools/match-line-format.d.ts +11 -0
  299. package/dist/types/tools/mcp.d.ts +81 -0
  300. package/dist/types/tools/memory.d.ts +67 -0
  301. package/dist/types/tools/output-meta.d.ts +151 -0
  302. package/dist/types/tools/read.d.ts +139 -0
  303. package/dist/types/tools/renderer.d.ts +98 -0
  304. package/dist/types/tools/report-tool-issue.d.ts +8 -0
  305. package/dist/types/tools/resolve.d.ts +47 -0
  306. package/dist/types/tools/streaming-output.d.ts +282 -0
  307. package/dist/types/tools/subprocess.d.ts +24 -0
  308. package/dist/types/tools/task-repair-args.d.ts +53 -0
  309. package/dist/types/tools/task-yield-assembly.d.ts +20 -0
  310. package/dist/types/tools/task.d.ts +425 -0
  311. package/dist/types/tools/terminal-output.d.ts +8 -0
  312. package/dist/types/tools/think.d.ts +13 -0
  313. package/dist/types/tools/todo.d.ts +118 -0
  314. package/dist/types/tools/tool-errors.d.ts +7 -0
  315. package/dist/types/tools/vibe.d.ts +120 -0
  316. package/dist/types/tools/web-search.d.ts +195 -0
  317. package/dist/types/tools/write.d.ts +76 -0
  318. package/dist/types/tools/xd-url.d.ts +13 -0
  319. package/dist/types/tools/xdev.d.ts +52 -0
  320. package/dist/types/tui.d.ts +18 -0
  321. package/dist/types/utils.d.ts +2 -0
  322. package/package.json +37 -3
  323. package/src/app-keybindings.ts +760 -0
  324. package/src/apps/autoresearch-dashboard.ts +500 -0
  325. package/src/apps/autoresearch-data.ts +64 -0
  326. package/src/apps/cleanse-board.ts +341 -0
  327. package/src/apps/cleanse-picker.ts +57 -0
  328. package/src/apps/debug/log-formatting.ts +53 -0
  329. package/src/apps/debug/log-viewer.ts +868 -0
  330. package/src/apps/debug/protocol-probe.ts +267 -0
  331. package/src/apps/debug/raw-sse-buffer.ts +449 -0
  332. package/src/apps/debug/raw-sse.ts +282 -0
  333. package/src/apps/debug/terminal-info.ts +115 -0
  334. package/src/apps/debug/viewer-frame.ts +174 -0
  335. package/src/apps/git/avatar.ts +52 -0
  336. package/src/apps/git/colors.ts +109 -0
  337. package/src/apps/git/diff-pane.ts +1512 -0
  338. package/src/apps/git/git-tui.ts +889 -0
  339. package/src/apps/git/sidebar.ts +1204 -0
  340. package/src/apps/git/state.ts +113 -0
  341. package/src/apps/if-bench-board.ts +324 -0
  342. package/src/apps/live-visualizer.ts +238 -0
  343. package/src/apps/ps-data.ts +116 -0
  344. package/src/apps/ps-top.ts +422 -0
  345. package/src/apps/session-picker.ts +95 -0
  346. package/src/apps/setup-model-picker.ts +21 -0
  347. package/src/apps/standalone-picker.ts +111 -0
  348. package/src/autocomplete.ts +9 -13
  349. package/src/chat/advisor-message.ts +175 -0
  350. package/src/chat/assistant-message.ts +1214 -0
  351. package/src/chat/background-tan-message.ts +35 -0
  352. package/src/chat/bash-execution.ts +394 -0
  353. package/src/chat/cache-invalidation-marker.ts +94 -0
  354. package/src/chat/chat-transcript-builder.ts +593 -0
  355. package/src/chat/collab-prompt-message.ts +34 -0
  356. package/src/chat/compaction-summary-message.ts +261 -0
  357. package/src/chat/custom-message.ts +23 -0
  358. package/src/chat/display-preferences.ts +24 -0
  359. package/src/chat/eval-execution.ts +172 -0
  360. package/src/chat/execution-shared.ts +113 -0
  361. package/src/chat/extension-types.ts +46 -0
  362. package/src/chat/hook-message.ts +20 -0
  363. package/src/chat/image-loading.ts +122 -0
  364. package/src/chat/late-diagnostics-message.ts +92 -0
  365. package/src/chat/messages.ts +305 -0
  366. package/src/chat/reaction.ts +65 -0
  367. package/src/chat/read-target.ts +7 -0
  368. package/src/chat/read-tool-group.ts +873 -0
  369. package/src/chat/served-model-marker.ts +91 -0
  370. package/src/chat/skill-message.ts +220 -0
  371. package/src/chat/skill-title-input.ts +41 -0
  372. package/src/chat/stripped-tool-calls-placeholder.ts +35 -0
  373. package/src/chat/thinking-display.ts +312 -0
  374. package/src/chat/todo-reminder.ts +33 -0
  375. package/src/chat/tool-execution.ts +1395 -0
  376. package/src/chat/transcript-browser.ts +268 -0
  377. package/src/chat/transcript-entry.ts +96 -0
  378. package/src/chat/transcript-outline.ts +260 -0
  379. package/src/chat/transcript-render-helpers.ts +315 -0
  380. package/src/chat/ttsr-notification.ts +123 -0
  381. package/src/chat/user-message.ts +287 -0
  382. package/src/chrome/chat-block.ts +110 -0
  383. package/src/chrome/collab-qrcode.ts +74 -0
  384. package/src/chrome/context-thresholds.ts +85 -0
  385. package/src/chrome/countdown-timer.ts +74 -0
  386. package/src/chrome/diff.ts +266 -0
  387. package/src/chrome/dynamic-border.ts +36 -0
  388. package/src/chrome/error-block.ts +63 -0
  389. package/src/chrome/form-theme.ts +10 -0
  390. package/src/chrome/format.ts +66 -0
  391. package/src/chrome/index.ts +21 -0
  392. package/src/chrome/keybinding-hints.ts +55 -0
  393. package/src/chrome/live-board.ts +123 -0
  394. package/src/chrome/local-date.ts +20 -0
  395. package/src/chrome/message-divider.ts +51 -0
  396. package/src/chrome/message-frame.ts +164 -0
  397. package/src/chrome/message-notice.ts +103 -0
  398. package/src/chrome/overlay-box.ts +301 -0
  399. package/src/chrome/qrcode.ts +539 -0
  400. package/src/chrome/segment-track.ts +86 -0
  401. package/src/chrome/select-list-mouse-routing.ts +11 -0
  402. package/src/chrome/selector-helpers.ts +119 -0
  403. package/src/chrome/shared.ts +27 -0
  404. package/src/chrome/streaming-panel.ts +72 -0
  405. package/src/chrome/tool-activity.ts +44 -0
  406. package/src/chrome/transcript-container.ts +902 -0
  407. package/src/chrome/transcript-status.ts +27 -0
  408. package/src/chrome/visual-truncate.ts +70 -0
  409. package/src/components/disclosure.ts +235 -0
  410. package/src/components/editor.ts +10 -12
  411. package/src/components/form.ts +566 -0
  412. package/src/components/image.ts +80 -53
  413. package/src/components/input.ts +6 -27
  414. package/src/components/key-value-list.ts +88 -0
  415. package/src/components/layout/geometry.ts +202 -0
  416. package/src/components/layout/row.ts +283 -0
  417. package/src/components/layout/split-pane.ts +349 -0
  418. package/src/components/layout/stack.ts +261 -0
  419. package/src/components/markdown.ts +16 -12
  420. package/src/components/menu-selection.ts +248 -0
  421. package/src/components/metric.ts +132 -0
  422. package/src/components/progress-bar.ts +140 -0
  423. package/src/components/scroll-view.ts +372 -70
  424. package/src/components/scroll-viewport.ts +154 -0
  425. package/src/components/section.ts +77 -0
  426. package/src/components/select-list.ts +238 -159
  427. package/src/components/settings-list.ts +103 -80
  428. package/src/components/table.ts +127 -0
  429. package/src/components/tree-view.ts +481 -0
  430. package/src/components/wizard-step.ts +229 -0
  431. package/src/deccara.ts +1 -3
  432. package/src/hotkeys-markdown.ts +100 -0
  433. package/src/index.ts +18 -0
  434. package/src/keybinding-matchers.ts +99 -0
  435. package/src/keys.ts +2 -2
  436. package/src/lang-from-path.ts +252 -0
  437. package/src/overlays/advisor-config.ts +761 -0
  438. package/src/overlays/agent-activity.ts +113 -0
  439. package/src/overlays/agent-hub-projection.ts +250 -0
  440. package/src/overlays/agent-hub-renderer.ts +281 -0
  441. package/src/overlays/agent-hub-types.ts +74 -0
  442. package/src/overlays/agent-hub.ts +1562 -0
  443. package/src/overlays/agent-transcript-viewer.ts +669 -0
  444. package/src/overlays/agents-hub.ts +1203 -0
  445. package/src/overlays/ask-dialog.ts +1202 -0
  446. package/src/overlays/bordered-loader.ts +41 -0
  447. package/src/overlays/btw-history-panel.ts +591 -0
  448. package/src/overlays/btw-history.ts +33 -0
  449. package/src/overlays/btw-panel.ts +167 -0
  450. package/src/overlays/cleanse-panel.ts +167 -0
  451. package/src/overlays/codex-reset-fireworks.ts +375 -0
  452. package/src/overlays/composer-shape-preview.ts +137 -0
  453. package/src/overlays/composer-shape-registry.ts +82 -0
  454. package/src/overlays/copy-selector.ts +618 -0
  455. package/src/overlays/copy-targets.ts +210 -0
  456. package/src/overlays/error-banner.ts +39 -0
  457. package/src/overlays/extensions/display-text.ts +39 -0
  458. package/src/overlays/extensions/extension-dashboard.ts +645 -0
  459. package/src/overlays/extensions/extension-list.ts +667 -0
  460. package/src/overlays/extensions/inspector-model.ts +484 -0
  461. package/src/overlays/extensions/inspector-panel.ts +669 -0
  462. package/src/overlays/extensions/live-tool-session.ts +67 -0
  463. package/src/overlays/extensions/mcp-runtime.ts +289 -0
  464. package/src/overlays/extensions/state-manager.ts +337 -0
  465. package/src/overlays/extensions/types.ts +208 -0
  466. package/src/overlays/history-search.ts +268 -0
  467. package/src/overlays/hook-editor.ts +272 -0
  468. package/src/overlays/hook-input.ts +84 -0
  469. package/src/overlays/hook-selector.ts +635 -0
  470. package/src/overlays/hub-frame.ts +262 -0
  471. package/src/overlays/login-dialog.ts +231 -0
  472. package/src/overlays/logout-account-selector.ts +115 -0
  473. package/src/overlays/mcp-add-wizard.ts +1436 -0
  474. package/src/overlays/model-browser.ts +1176 -0
  475. package/src/overlays/model-hub.ts +2279 -0
  476. package/src/overlays/model-picker.ts +285 -0
  477. package/src/overlays/model-selector.ts +107 -0
  478. package/src/overlays/move-overlay.ts +197 -0
  479. package/src/overlays/oauth-selector.ts +446 -0
  480. package/src/overlays/omfg-panel.ts +136 -0
  481. package/src/overlays/pause-screen.ts +198 -0
  482. package/src/overlays/plan-review-overlay.ts +1227 -0
  483. package/src/overlays/plan-save-overlay.ts +74 -0
  484. package/src/overlays/plan-toc.ts +139 -0
  485. package/src/overlays/plugin-selector.ts +97 -0
  486. package/src/overlays/plugin-settings.ts +839 -0
  487. package/src/overlays/queue-mode-selector.ts +55 -0
  488. package/src/overlays/reset-usage-selector.ts +170 -0
  489. package/src/overlays/rewind-selector.ts +499 -0
  490. package/src/overlays/running-subagent-badge.ts +19 -0
  491. package/src/overlays/session-account-selector.ts +68 -0
  492. package/src/overlays/session-info-overlay.ts +96 -0
  493. package/src/overlays/session-observer-registry.ts +289 -0
  494. package/src/overlays/session-selector.ts +1137 -0
  495. package/src/overlays/settings-defs.ts +315 -0
  496. package/src/overlays/settings-selector.ts +1415 -0
  497. package/src/overlays/show-images-selector.ts +44 -0
  498. package/src/overlays/snapcompact-shape-preview-doc.md +14 -0
  499. package/src/overlays/snapcompact-shape-preview.ts +192 -0
  500. package/src/overlays/theme-selector.ts +62 -0
  501. package/src/overlays/thinking-selector.ts +51 -0
  502. package/src/overlays/tiny-title-download-progress.ts +102 -0
  503. package/src/overlays/tree-selector.ts +1121 -0
  504. package/src/overlays/usage-dashboard.ts +629 -0
  505. package/src/overlays/usage-display.ts +45 -0
  506. package/src/overlays/usage-row.ts +118 -0
  507. package/src/prompt/attachment-chips.ts +201 -0
  508. package/src/prompt/composer-attachments.ts +280 -0
  509. package/src/prompt/composer-cache.ts +304 -0
  510. package/src/prompt/composer.ts +934 -0
  511. package/src/prompt/custom-editor.ts +1392 -0
  512. package/src/prompt/data/emojis.json +1 -0
  513. package/src/prompt/editor-top-gap.ts +28 -0
  514. package/src/prompt/emoji-autocomplete.ts +285 -0
  515. package/src/prompt/github-ref-autocomplete.ts +75 -0
  516. package/src/prompt/gradient-highlight.ts +101 -0
  517. package/src/prompt/image-format.ts +28 -0
  518. package/src/prompt/image-references.ts +102 -0
  519. package/src/prompt/interactive-context-helpers.ts +126 -0
  520. package/src/prompt/internal-url-autocomplete.ts +155 -0
  521. package/src/prompt/macos-spelling.ts +456 -0
  522. package/src/prompt/magic-keyword-boundary.ts +23 -0
  523. package/src/prompt/magic-keywords.ts +42 -0
  524. package/src/prompt/markdown-prose.ts +245 -0
  525. package/src/prompt/model-mention-autocomplete.ts +94 -0
  526. package/src/prompt/model-mention-syntax.ts +41 -0
  527. package/src/prompt/orchestrate.ts +26 -0
  528. package/src/prompt/prompt-action-autocomplete.ts +337 -0
  529. package/src/prompt/queue-input.ts +132 -0
  530. package/src/prompt/skill-tokens.ts +36 -0
  531. package/src/prompt/tips.txt +27 -0
  532. package/src/prompt/tools-markdown.ts +29 -0
  533. package/src/prompt/ultrathink.ts +26 -0
  534. package/src/prompt/usage-amounts.ts +99 -0
  535. package/src/prompt/video.ts +47 -0
  536. package/src/prompt/welcome.ts +605 -0
  537. package/src/prompt/workflow.ts +26 -0
  538. package/src/render/code-cell.ts +244 -0
  539. package/src/render/file-list.ts +58 -0
  540. package/src/render/hyperlink.ts +192 -0
  541. package/src/render/index.ts +17 -0
  542. package/src/render/output-block.ts +267 -0
  543. package/src/render/output-pane.ts +299 -0
  544. package/src/render/render-utils.ts +1182 -0
  545. package/src/render/sixel.ts +69 -0
  546. package/src/render/status-line.ts +55 -0
  547. package/src/render/tool-card.ts +272 -0
  548. package/src/render/tree-list.ts +188 -0
  549. package/src/render/types.ts +19 -0
  550. package/src/render/utils.ts +109 -0
  551. package/src/render/width-aware-text.ts +60 -0
  552. package/src/setup/lazy.ts +17 -0
  553. package/src/setup/scenes/composer.ts +127 -0
  554. package/src/setup/scenes/glyph.ts +120 -0
  555. package/src/setup/scenes/model.ts +138 -0
  556. package/src/setup/scenes/outro.ts +24 -0
  557. package/src/setup/scenes/providers.ts +107 -0
  558. package/src/setup/scenes/sign-in.ts +373 -0
  559. package/src/setup/scenes/splash.ts +192 -0
  560. package/src/setup/scenes/theme.ts +343 -0
  561. package/src/setup/scenes/types.ts +111 -0
  562. package/src/setup/scenes/web-search.ts +165 -0
  563. package/src/setup/setup-version.ts +11 -0
  564. package/src/setup/startup-splash.ts +107 -0
  565. package/src/setup/wizard-overlay.ts +319 -0
  566. package/src/setup/wizard.ts +101 -0
  567. package/src/status-line/component.ts +3003 -0
  568. package/src/status-line/context-usage.ts +688 -0
  569. package/src/status-line/footer.ts +323 -0
  570. package/src/status-line/git-utils.ts +42 -0
  571. package/src/status-line/host.ts +97 -0
  572. package/src/status-line/index.ts +6 -0
  573. package/src/status-line/loop.ts +28 -0
  574. package/src/status-line/metrics.ts +94 -0
  575. package/src/status-line/presets.ts +107 -0
  576. package/src/status-line/schema.ts +57 -0
  577. package/src/status-line/segments.ts +934 -0
  578. package/src/status-line/separators.ts +55 -0
  579. package/src/status-line/types.ts +227 -0
  580. package/src/stdin-buffer.ts +4 -2
  581. package/src/theme/color.ts +95 -0
  582. package/src/theme/dark.json +95 -0
  583. package/src/theme/defaults/alabaster.json +93 -0
  584. package/src/theme/defaults/amethyst.json +96 -0
  585. package/src/theme/defaults/anthracite.json +93 -0
  586. package/src/theme/defaults/basalt.json +91 -0
  587. package/src/theme/defaults/birch.json +95 -0
  588. package/src/theme/defaults/dark-abyss.json +91 -0
  589. package/src/theme/defaults/dark-arctic.json +104 -0
  590. package/src/theme/defaults/dark-aurora.json +95 -0
  591. package/src/theme/defaults/dark-catppuccin.json +107 -0
  592. package/src/theme/defaults/dark-cavern.json +91 -0
  593. package/src/theme/defaults/dark-celestial.json +106 -0
  594. package/src/theme/defaults/dark-copper.json +95 -0
  595. package/src/theme/defaults/dark-cosmos.json +90 -0
  596. package/src/theme/defaults/dark-cyberpunk.json +102 -0
  597. package/src/theme/defaults/dark-dracula.json +98 -0
  598. package/src/theme/defaults/dark-eclipse.json +91 -0
  599. package/src/theme/defaults/dark-ember.json +95 -0
  600. package/src/theme/defaults/dark-equinox.json +90 -0
  601. package/src/theme/defaults/dark-forest.json +96 -0
  602. package/src/theme/defaults/dark-github.json +105 -0
  603. package/src/theme/defaults/dark-gruvbox.json +112 -0
  604. package/src/theme/defaults/dark-lavender.json +95 -0
  605. package/src/theme/defaults/dark-lunar.json +89 -0
  606. package/src/theme/defaults/dark-midnight.json +95 -0
  607. package/src/theme/defaults/dark-monochrome.json +94 -0
  608. package/src/theme/defaults/dark-monokai.json +98 -0
  609. package/src/theme/defaults/dark-nebula.json +90 -0
  610. package/src/theme/defaults/dark-nord.json +98 -0
  611. package/src/theme/defaults/dark-ocean.json +101 -0
  612. package/src/theme/defaults/dark-one.json +100 -0
  613. package/src/theme/defaults/dark-poimandres.json +115 -0
  614. package/src/theme/defaults/dark-rainforest.json +91 -0
  615. package/src/theme/defaults/dark-reef.json +91 -0
  616. package/src/theme/defaults/dark-retro.json +92 -0
  617. package/src/theme/defaults/dark-rose-pine.json +96 -0
  618. package/src/theme/defaults/dark-sakura.json +95 -0
  619. package/src/theme/defaults/dark-slate.json +95 -0
  620. package/src/theme/defaults/dark-solarized.json +97 -0
  621. package/src/theme/defaults/dark-solstice.json +90 -0
  622. package/src/theme/defaults/dark-starfall.json +91 -0
  623. package/src/theme/defaults/dark-sunset.json +99 -0
  624. package/src/theme/defaults/dark-swamp.json +90 -0
  625. package/src/theme/defaults/dark-synthwave.json +103 -0
  626. package/src/theme/defaults/dark-taiga.json +91 -0
  627. package/src/theme/defaults/dark-terminal.json +95 -0
  628. package/src/theme/defaults/dark-tokyo-night.json +101 -0
  629. package/src/theme/defaults/dark-tundra.json +91 -0
  630. package/src/theme/defaults/dark-twilight.json +91 -0
  631. package/src/theme/defaults/dark-volcanic.json +91 -0
  632. package/src/theme/defaults/graphite.json +92 -0
  633. package/src/theme/defaults/index.ts +201 -0
  634. package/src/theme/defaults/light-arctic.json +107 -0
  635. package/src/theme/defaults/light-aurora-day.json +91 -0
  636. package/src/theme/defaults/light-canyon.json +91 -0
  637. package/src/theme/defaults/light-catppuccin.json +106 -0
  638. package/src/theme/defaults/light-cirrus.json +90 -0
  639. package/src/theme/defaults/light-coral.json +95 -0
  640. package/src/theme/defaults/light-cyberpunk.json +96 -0
  641. package/src/theme/defaults/light-dawn.json +90 -0
  642. package/src/theme/defaults/light-dunes.json +91 -0
  643. package/src/theme/defaults/light-eucalyptus.json +95 -0
  644. package/src/theme/defaults/light-forest.json +100 -0
  645. package/src/theme/defaults/light-frost.json +95 -0
  646. package/src/theme/defaults/light-github.json +115 -0
  647. package/src/theme/defaults/light-glacier.json +91 -0
  648. package/src/theme/defaults/light-gruvbox.json +108 -0
  649. package/src/theme/defaults/light-haze.json +90 -0
  650. package/src/theme/defaults/light-honeycomb.json +95 -0
  651. package/src/theme/defaults/light-lagoon.json +91 -0
  652. package/src/theme/defaults/light-lavender.json +95 -0
  653. package/src/theme/defaults/light-meadow.json +91 -0
  654. package/src/theme/defaults/light-mint.json +95 -0
  655. package/src/theme/defaults/light-monochrome.json +101 -0
  656. package/src/theme/defaults/light-ocean.json +99 -0
  657. package/src/theme/defaults/light-one.json +99 -0
  658. package/src/theme/defaults/light-opal.json +91 -0
  659. package/src/theme/defaults/light-orchard.json +91 -0
  660. package/src/theme/defaults/light-paper.json +95 -0
  661. package/src/theme/defaults/light-poimandres.json +115 -0
  662. package/src/theme/defaults/light-prism.json +90 -0
  663. package/src/theme/defaults/light-retro.json +98 -0
  664. package/src/theme/defaults/light-sand.json +95 -0
  665. package/src/theme/defaults/light-savanna.json +91 -0
  666. package/src/theme/defaults/light-solarized.json +102 -0
  667. package/src/theme/defaults/light-soleil.json +90 -0
  668. package/src/theme/defaults/light-sunset.json +99 -0
  669. package/src/theme/defaults/light-synthwave.json +98 -0
  670. package/src/theme/defaults/light-tokyo-night.json +111 -0
  671. package/src/theme/defaults/light-wetland.json +91 -0
  672. package/src/theme/defaults/light-zenith.json +89 -0
  673. package/src/theme/defaults/limestone.json +94 -0
  674. package/src/theme/defaults/mahogany.json +97 -0
  675. package/src/theme/defaults/marble.json +93 -0
  676. package/src/theme/defaults/obsidian.json +91 -0
  677. package/src/theme/defaults/onyx.json +91 -0
  678. package/src/theme/defaults/pearl.json +93 -0
  679. package/src/theme/defaults/porcelain.json +91 -0
  680. package/src/theme/defaults/quartz.json +96 -0
  681. package/src/theme/defaults/sandstone.json +95 -0
  682. package/src/theme/defaults/titanium.json +90 -0
  683. package/src/theme/index.ts +5 -0
  684. package/src/theme/light.json +93 -0
  685. package/src/theme/loader.ts +200 -0
  686. package/src/theme/mermaid-cache.ts +92 -0
  687. package/src/theme/schema-validation.ts +119 -0
  688. package/src/theme/schema.ts +168 -0
  689. package/src/theme/session-color.ts +253 -0
  690. package/src/theme/shimmer.ts +307 -0
  691. package/src/theme/symbols.ts +1433 -0
  692. package/src/theme/theme-class.ts +787 -0
  693. package/src/theme/theme-schema.json +463 -0
  694. package/src/theme/theme.ts +810 -0
  695. package/src/theme/tui-adapters.ts +339 -0
  696. package/src/thinking.ts +380 -0
  697. package/src/tools/agent-tree.ts +128 -0
  698. package/src/tools/ask.ts +482 -0
  699. package/src/tools/ast-edit.ts +210 -0
  700. package/src/tools/ast-grep.ts +171 -0
  701. package/src/tools/autoresearch.ts +287 -0
  702. package/src/tools/bash-interactive.ts +295 -0
  703. package/src/tools/bash.ts +468 -0
  704. package/src/tools/conflict-detect.ts +77 -0
  705. package/src/tools/debug.ts +167 -0
  706. package/src/tools/default-renderer.ts +172 -0
  707. package/src/tools/edit.ts +1258 -0
  708. package/src/tools/eval-format/code-mode-declarations.ts +61 -0
  709. package/src/tools/eval-format/index.ts +16 -0
  710. package/src/tools/eval-format/javascript.ts +952 -0
  711. package/src/tools/eval-format/python.ts +544 -0
  712. package/src/tools/eval.ts +835 -0
  713. package/src/tools/fetch.ts +226 -0
  714. package/src/tools/gh-format.ts +18 -0
  715. package/src/tools/github.ts +686 -0
  716. package/src/tools/glob.ts +213 -0
  717. package/src/tools/goal.ts +153 -0
  718. package/src/tools/grep.ts +385 -0
  719. package/src/tools/grouped-file-output.ts +211 -0
  720. package/src/tools/hashline-format.ts +59 -0
  721. package/src/tools/hub.ts +1488 -0
  722. package/src/tools/index.ts +71 -0
  723. package/src/tools/json-tree.ts +388 -0
  724. package/src/tools/line-ranges.ts +98 -0
  725. package/src/tools/list-limit.ts +43 -0
  726. package/src/tools/lsp.ts +702 -0
  727. package/src/tools/match-line-format.ts +20 -0
  728. package/src/tools/mcp.ts +309 -0
  729. package/src/tools/memory.ts +220 -0
  730. package/src/tools/output-meta.ts +367 -0
  731. package/src/tools/read.ts +628 -0
  732. package/src/tools/renderer.ts +107 -0
  733. package/src/tools/report-tool-issue.ts +13 -0
  734. package/src/tools/resolve.ts +139 -0
  735. package/src/tools/streaming-output.ts +1576 -0
  736. package/src/tools/subprocess.ts +39 -0
  737. package/src/tools/task-repair-args.ts +118 -0
  738. package/src/tools/task-yield-assembly.ts +147 -0
  739. package/src/tools/task.ts +2110 -0
  740. package/src/tools/terminal-output.ts +153 -0
  741. package/src/tools/think.ts +28 -0
  742. package/src/tools/todo.ts +580 -0
  743. package/src/tools/tool-errors.ts +15 -0
  744. package/src/tools/vibe.ts +420 -0
  745. package/src/tools/web-search.ts +409 -0
  746. package/src/tools/write.ts +511 -0
  747. package/src/tools/xd-url.ts +28 -0
  748. package/src/tools/xdev.ts +201 -0
  749. package/src/tui.ts +70 -8
  750. package/src/utils.ts +8 -0
@@ -0,0 +1,1227 @@
1
+ /**
2
+ * Fullscreen plan-review overlay. The overlay owns its entire content: the plan
3
+ * is split into sections (preamble + one per heading), each rendered through its
4
+ * own {@link Markdown} and windowed by a {@link ScrollView}, while the approval
5
+ * options (plus the optional model-tier slider) sit beneath inside the same
6
+ * outlined box — one self-contained surface in the spirit of the `/copy` picker.
7
+ *
8
+ * When the terminal is wide enough and the plan has ≥2 headings, a Contents
9
+ * sidebar appears: it tracks the scrolled section with an accent "glow", and —
10
+ * when focused — lets the operator jump between sections, delete a section
11
+ * (with undo), and annotate sections with feedback that feeds the Refine loop.
12
+ *
13
+ * Focus regions (`toc`/`body`/`actions`) cycle with Tab/Shift+Tab; arrows move
14
+ * within the focused region and step left into the sidebar. The default focus is
15
+ * `actions`, so the muscle memory of the old single-target overlay carries over:
16
+ * ↑/↓ select options, Enter confirms, ←/→ drives the slider when there is no
17
+ * sidebar, g/G + PgUp/PgDn scroll, and the external-editor key opens the plan.
18
+ */
19
+ import {
20
+ type Component,
21
+ Ellipsis,
22
+ Input,
23
+ Markdown,
24
+ type MarkdownTheme,
25
+ matchesKey,
26
+ replaceTabs,
27
+ routeSgrMouseInput,
28
+ ScrollView,
29
+ truncateToWidth,
30
+ visibleWidth,
31
+ } from "../index";
32
+ import { sanitizeText } from "@oh-my-pi/pi-utils";
33
+ import { sanitizeStatusText } from "../chrome/shared";
34
+ import { getMarkdownTheme, theme } from "../theme/theme";
35
+ import {
36
+ matchesAppExternalEditor,
37
+ matchesSelectCancel,
38
+ matchesSelectDown,
39
+ matchesSelectUp,
40
+ } from "../keybinding-matchers";
41
+ import type { HookSelectorSlider } from "./hook-selector";
42
+ import {
43
+ bottomBorder,
44
+ divider,
45
+ dividerSplit,
46
+ row,
47
+ splitBodyWidth,
48
+ splitRow,
49
+ topBorder,
50
+ topBorderSplit,
51
+ } from "../chrome/overlay-box";
52
+ import { joinPlanSections, parsePlanSections, sectionDeletionSpan } from "./plan-toc";
53
+ import { padToWidth } from "../render/utils";
54
+ import { renderSegmentTrack } from "../chrome/segment-track";
55
+
56
+ /** Title shown in the overlay's top border. */
57
+ const OVERLAY_TITLE = "Plan Review";
58
+ /** Minimum plan-body rows kept visible even on short terminals. */
59
+ const MIN_BODY_ROWS = 3;
60
+ /** Sidebar gates: enough headings, a wide terminal, and a usable body column. */
61
+ const SIDEBAR_MIN_HEADINGS = 2;
62
+ const SIDEBAR_MIN_TOTAL_WIDTH = 64;
63
+ const SIDEBAR_MIN_BODY_WIDTH = 40;
64
+ /** Persisted line-context cap; render-time captions clamp again to the viewport. */
65
+ const MAX_ANNOTATION_CONTEXT_WIDTH = 120;
66
+
67
+ type Focus = "toc" | "body" | "actions";
68
+
69
+ type AnnotationTarget = { kind: "section" } | { kind: "line"; row: number; context: string; contextTruncated: boolean };
70
+
71
+ interface OverlayAnnotation {
72
+ note: string;
73
+ target: AnnotationTarget;
74
+ }
75
+
76
+ interface OverlaySection {
77
+ level: number;
78
+ title: string;
79
+ raw: string;
80
+ md: Markdown;
81
+ annotations: OverlayAnnotation[];
82
+ }
83
+
84
+ interface LineAnchorContext {
85
+ text: string;
86
+ truncated: boolean;
87
+ }
88
+
89
+ interface BodyRowAnchor {
90
+ sectionIndex: number;
91
+ row: number;
92
+ context: string;
93
+ contextTruncated: boolean;
94
+ }
95
+
96
+ /** Serializable annotations retained by the plan-review owner between overlays. */
97
+ export interface PlanReviewAnnotationState {
98
+ annotations: Array<{
99
+ section: {
100
+ index: number;
101
+ title: string;
102
+ /** Heading ancestry from the document root, when emitted by this overlay. */
103
+ path?: string[];
104
+ /** Hash of the section source, used to reject ambiguous moved headings. */
105
+ contentHash?: string;
106
+ };
107
+ target: { kind: "section" } | { kind: "line"; row: number; context: string; contextTruncated?: boolean };
108
+ note: string;
109
+ }>;
110
+ }
111
+
112
+ /** Undo snapshot: joined plan text, annotations aligned by section, and the
113
+ * accumulated deleted-section feedback at the time of the snapshot. */
114
+ interface UndoEntry {
115
+ text: string;
116
+ annotations: OverlayAnnotation[][];
117
+ deleted: string[];
118
+ }
119
+
120
+ export interface PlanReviewOverlayCallbacks {
121
+ /** Invoked with the chosen option label (never a disabled one). */
122
+ onPick: (label: string) => void;
123
+ /** Invoked on Esc / cancel. */
124
+ onCancel: () => void;
125
+ /** Invoked with the current full plan text when the copy hotkey is pressed. */
126
+ onCopyPlan?: (content: string) => void | Promise<void>;
127
+ /** Invoked when the external-editor key is pressed (overlay stays open). */
128
+ onExternalEditor?: () => void;
129
+ /** Invoked when the external-editor key edits the active annotation draft. */
130
+ onAnnotationExternalEditor?: (draft: string, commit: (text: string | null) => void) => void;
131
+ /** Invoked with the new full plan text after an in-overlay delete/undo. */
132
+ onPlanEdited?: (content: string) => void;
133
+ /** Invoked with the Refine feedback markdown whenever annotations change. */
134
+ onFeedbackChange?: (feedback: string) => void;
135
+ /** Invoked with a serializable annotation snapshot whenever annotations change. */
136
+ onAnnotationStateChange?: (state: PlanReviewAnnotationState) => void;
137
+ }
138
+
139
+ export interface PlanReviewOverlayOptions {
140
+ /** Prompt rendered above the options (e.g. "Plan mode - next step"). */
141
+ promptTitle?: string;
142
+ options: string[];
143
+ /** Indices into `options` that render dimmed and cannot be selected. */
144
+ disabledIndices?: number[];
145
+ /** Trailing footer hint (cancel hint); the overlay prepends dynamic help. */
146
+ helpText?: string;
147
+ /** Initially highlighted option index. */
148
+ initialIndex?: number;
149
+ /** Optional model-tier slider rendered between the plan body and options. */
150
+ slider?: HookSelectorSlider;
151
+ /** Display label for the external-editor key, surfaced in the footer help. */
152
+ externalEditorLabel?: string;
153
+ /** Serializable annotations restored into this overlay instance. */
154
+ annotationState?: PlanReviewAnnotationState;
155
+ }
156
+
157
+ /** Default trailing footer hint when the caller supplies none. */
158
+ const DEFAULT_HELP_SUFFIX = "esc cancel";
159
+
160
+ export class PlanReviewOverlay implements Component {
161
+ #mdTheme: MarkdownTheme;
162
+ #scrollView: ScrollView;
163
+ #sections: OverlaySection[] = [];
164
+ #toc: number[] = [];
165
+ /** Shallowest level among ToC entries, used to flatten indentation. */
166
+ #tocBaseLevel = 1;
167
+ #sectionOffsets: number[] = [];
168
+ /** Rendered body row to underlying plan row; callouts retain their owner's anchor. */
169
+ #bodyRowAnchors: BodyRowAnchor[] = [];
170
+ #undo: UndoEntry[] = [];
171
+ /** Titles of sections deleted in the overlay, surfaced as Refine feedback. */
172
+ #deleted: string[] = [];
173
+
174
+ #options: string[];
175
+ #disabled: Set<number>;
176
+ #helpSuffix: string;
177
+ #externalEditorLabel: string | undefined;
178
+ #promptTitle: string | undefined;
179
+ #selectedIndex: number;
180
+ #slider: HookSelectorSlider | undefined;
181
+ #sliderIndex: number;
182
+
183
+ #focus: Focus = "actions";
184
+ #tocCursor = 0;
185
+ #sidebarShown = false;
186
+ #pendingScrollToToc = false;
187
+ /** Last meaningful relative body position, retained while a frame cannot scroll. */
188
+ #scrollProgress = 0;
189
+
190
+ // Click hit-testing, rebuilt every render. Keys are 0-based rendered-line
191
+ // indices (== screen rows, since the fullscreen overlay paints from row 0).
192
+ #optionClickRows = new Map<number, number>();
193
+ #tocClickRows = new Map<number, number>();
194
+ #bodyClickRows = new Set<number>();
195
+ /** Exclusive 0-based column bound below which a region-row click targets the sidebar. */
196
+ #sidebarClickMaxCol = 0;
197
+ /** Option index the pointer is currently hovering, or undefined. Updated from
198
+ * motion mouse reports and cleared when the pointer leaves the option rows. */
199
+ #hoveredOption: number | undefined;
200
+
201
+ // Once a choice fires, the promise-based caller resolves but keeps this
202
+ // overlay mounted while it runs slow async approval work (e.g. context
203
+ // compaction). Lock input and switch to a "submitting" indicator so the
204
+ // overlay stops looking interactive and repeat Enter/Esc are not silently
205
+ // swallowed with zero feedback (#5926).
206
+ #committed = false;
207
+ /** Label of the committed choice, shown while the async approval settles. */
208
+ #committedLabel: string | undefined;
209
+ #annotating = false;
210
+ #input: Input;
211
+ #annotationTarget: BodyRowAnchor | { sectionIndex: number; row: null; context: null } | undefined;
212
+
213
+ constructor(
214
+ planContent: string,
215
+ options: PlanReviewOverlayOptions,
216
+ private readonly callbacks: PlanReviewOverlayCallbacks,
217
+ ) {
218
+ this.#mdTheme = getMarkdownTheme();
219
+ this.#scrollView = new ScrollView([], {
220
+ height: MIN_BODY_ROWS,
221
+ scrollbar: "auto",
222
+ ellipsis: Ellipsis.Omit,
223
+ theme: { track: t => theme.fg("dim", t), thumb: t => theme.fg("accent", t) },
224
+ });
225
+ this.#options = options.options;
226
+ this.#disabled = new Set(
227
+ (options.disabledIndices ?? []).filter(i => Number.isInteger(i) && i >= 0 && i < this.#options.length),
228
+ );
229
+ this.#helpSuffix = options.helpText ?? DEFAULT_HELP_SUFFIX;
230
+ this.#externalEditorLabel = options.externalEditorLabel;
231
+ this.#promptTitle = options.promptTitle;
232
+ this.#selectedIndex = this.#coerceIndex(options.initialIndex ?? 0);
233
+ if (options.slider && options.slider.segments.length > 0) {
234
+ this.#slider = options.slider;
235
+ this.#sliderIndex = Math.max(0, Math.min(options.slider.index, options.slider.segments.length - 1));
236
+ } else {
237
+ this.#sliderIndex = 0;
238
+ }
239
+ this.#input = new Input();
240
+ this.#input.setUseTerminalCursor(false);
241
+ this.#input.onSubmit = value => this.#submitAnnotation(value);
242
+ this.#input.onEscape = () => this.#exitAnnotate();
243
+ this.#setSections(planContent);
244
+ this.#restoreAnnotationState(options.annotationState);
245
+ if (Array.isArray(options.annotationState?.annotations) && options.annotationState.annotations.length > 0) {
246
+ this.#recomputeFeedback();
247
+ }
248
+ }
249
+
250
+ invalidate(): void {
251
+ for (const section of this.#sections) section.md.invalidate();
252
+ }
253
+
254
+ /** Swap the displayed plan (e.g. after an external-editor round-trip) and
255
+ * reset scroll/focus so the operator starts at the top. Does not emit
256
+ * `onPlanEdited` (the editor round-trip already persisted the file). */
257
+ setPlanContent(planContent: string): void {
258
+ const annotations = this.#annotationState();
259
+ this.#setSections(planContent);
260
+ this.#restoreAnnotationState(annotations);
261
+ this.#scrollView.scrollToTop();
262
+ this.#scrollProgress = 0;
263
+ this.#tocCursor = 0;
264
+ // A wholesale external-editor swap supersedes prior in-overlay deletions.
265
+ this.#deleted = [];
266
+ this.#undo = [];
267
+ this.#recomputeFeedback();
268
+ }
269
+
270
+ #setSections(planContent: string): void {
271
+ this.#sections = parsePlanSections(planContent).map(section => ({
272
+ level: section.level,
273
+ title: section.title,
274
+ raw: section.raw,
275
+ md: new Markdown(section.raw, 1, 0, this.#mdTheme),
276
+ annotations: [],
277
+ }));
278
+ this.#rebuildToc();
279
+ this.#tocCursor = Math.min(this.#tocCursor, Math.max(0, this.#toc.length - 1));
280
+ }
281
+ #cloneAnnotation(annotation: OverlayAnnotation): OverlayAnnotation {
282
+ return {
283
+ note: annotation.note,
284
+ target:
285
+ annotation.target.kind === "section"
286
+ ? { kind: "section" }
287
+ : {
288
+ kind: "line",
289
+ row: annotation.target.row,
290
+ context: annotation.target.context,
291
+ contextTruncated: annotation.target.contextTruncated,
292
+ },
293
+ };
294
+ }
295
+
296
+ #sectionPaths(): string[][] {
297
+ const stack: Array<{ level: number; title: string }> = [];
298
+ return this.#sections.map(section => {
299
+ if (section.level < 1) return [];
300
+ while (stack.length > 0 && stack[stack.length - 1]!.level >= section.level) stack.pop();
301
+ stack.push({ level: section.level, title: section.title });
302
+ return stack.map(entry => entry.title);
303
+ });
304
+ }
305
+
306
+ #sectionContentHash(section: OverlaySection): string {
307
+ return `${section.raw.length}:${Bun.hash(section.raw).toString(16)}`;
308
+ }
309
+
310
+ #annotationState(): PlanReviewAnnotationState {
311
+ const annotations: PlanReviewAnnotationState["annotations"] = [];
312
+ const sectionPaths = this.#sectionPaths();
313
+ for (let sectionIndex = 0; sectionIndex < this.#sections.length; sectionIndex++) {
314
+ const section = this.#sections[sectionIndex]!;
315
+ for (const annotation of section.annotations) {
316
+ annotations.push({
317
+ section: {
318
+ index: sectionIndex,
319
+ title: section.title,
320
+ path: sectionPaths[sectionIndex]!,
321
+ contentHash: this.#sectionContentHash(section),
322
+ },
323
+ target:
324
+ annotation.target.kind === "section"
325
+ ? { kind: "section" }
326
+ : {
327
+ kind: "line",
328
+ row: annotation.target.row,
329
+ context: annotation.target.context,
330
+ contextTruncated: annotation.target.contextTruncated,
331
+ },
332
+ note: annotation.note,
333
+ });
334
+ }
335
+ }
336
+ return { annotations };
337
+ }
338
+
339
+ #restoreAnnotationState(state: PlanReviewAnnotationState | undefined): void {
340
+ if (!state || !Array.isArray(state.annotations)) return;
341
+ const sectionPaths = this.#sectionPaths();
342
+ const contentHashes = this.#sections.map(section => this.#sectionContentHash(section));
343
+ for (const entry of state.annotations) {
344
+ if (
345
+ !entry ||
346
+ typeof entry.note !== "string" ||
347
+ !entry.section ||
348
+ !entry.target ||
349
+ typeof entry.section.title !== "string"
350
+ ) {
351
+ continue;
352
+ }
353
+ const note = entry.note.trim();
354
+ if (!note) continue;
355
+ const storedIndex = Number.isInteger(entry.section.index) ? entry.section.index : -1;
356
+ const storedPath = entry.section.path;
357
+ let matchingSections: number[];
358
+ if (
359
+ Array.isArray(storedPath) &&
360
+ storedPath.every(segment => typeof segment === "string") &&
361
+ typeof entry.section.contentHash === "string"
362
+ ) {
363
+ matchingSections = [];
364
+ for (let i = 0; i < this.#sections.length; i++) {
365
+ const path = sectionPaths[i]!;
366
+ if (
367
+ this.#sections[i]!.title === entry.section.title &&
368
+ contentHashes[i] === entry.section.contentHash &&
369
+ path.length === storedPath.length &&
370
+ path.every((segment, pathIndex) => segment === storedPath[pathIndex])
371
+ ) {
372
+ matchingSections.push(i);
373
+ }
374
+ }
375
+ } else {
376
+ matchingSections =
377
+ storedIndex >= 0 &&
378
+ storedIndex < this.#sections.length &&
379
+ this.#sections[storedIndex]!.title === entry.section.title
380
+ ? [storedIndex]
381
+ : [];
382
+ }
383
+ if (matchingSections.length === 0) continue;
384
+ const sectionIndex = matchingSections.reduce((best, candidate) =>
385
+ Math.abs(candidate - storedIndex) < Math.abs(best - storedIndex) ? candidate : best,
386
+ );
387
+ const section = this.#sections[sectionIndex]!;
388
+ if (entry.target.kind === "section") {
389
+ if (section.level >= 1) section.annotations.push({ note, target: { kind: "section" } });
390
+ continue;
391
+ }
392
+ if (
393
+ entry.target.kind !== "line" ||
394
+ !Number.isFinite(entry.target.row) ||
395
+ typeof entry.target.context !== "string"
396
+ ) {
397
+ continue;
398
+ }
399
+ const contexts = section.md.render(MAX_ANNOTATION_CONTEXT_WIDTH).map(line => this.#lineContext(line));
400
+ const row = this.#resolveLineRow(
401
+ entry.target.row,
402
+ { text: entry.target.context, truncated: entry.target.contextTruncated === true },
403
+ contexts,
404
+ );
405
+ if (row < 0) continue;
406
+ const context = contexts[row]!;
407
+ section.annotations.push({
408
+ note,
409
+ target: {
410
+ kind: "line",
411
+ row,
412
+ context: context.text,
413
+ contextTruncated: context.truncated,
414
+ },
415
+ });
416
+ }
417
+ }
418
+
419
+ #rebuildToc(): void {
420
+ const headings: number[] = [];
421
+ for (let i = 0; i < this.#sections.length; i++) {
422
+ if (this.#sections[i]!.level >= 1) headings.push(i);
423
+ }
424
+ // Drop the plan's title from the ToC: a single shallowest heading at the
425
+ // top of the document is the plan name itself ("we know it's the plan"),
426
+ // so listing it adds noise. Plans with several top-level sections keep
427
+ // them all.
428
+ let minLevel = Number.POSITIVE_INFINITY;
429
+ for (const i of headings) minLevel = Math.min(minLevel, this.#sections[i]!.level);
430
+ const topLevel = headings.filter(i => this.#sections[i]!.level === minLevel);
431
+ const titleIndex = topLevel.length === 1 && headings[0] === topLevel[0] ? topLevel[0] : -1;
432
+ this.#toc = headings.filter(i => i !== titleIndex);
433
+ this.#tocBaseLevel = this.#toc.length > 0 ? Math.min(...this.#toc.map(i => this.#sections[i]!.level)) : 1;
434
+ }
435
+
436
+ /** Clamp `index` to range, then walk to the nearest enabled option so the
437
+ * cursor never rests on a disabled row. */
438
+ #coerceIndex(index: number): number {
439
+ const max = this.#options.length - 1;
440
+ if (max < 0) return -1;
441
+ const clamped = Math.max(0, Math.min(index, max));
442
+ if (!this.#disabled.has(clamped)) return clamped;
443
+ for (let i = clamped + 1; i <= max; i++) if (!this.#disabled.has(i)) return i;
444
+ for (let i = clamped - 1; i >= 0; i--) if (!this.#disabled.has(i)) return i;
445
+ return clamped;
446
+ }
447
+
448
+ /** First enabled option index (or -1 when none), used to detect the "top". */
449
+ #firstEnabledIndex(): number {
450
+ for (let i = 0; i < this.#options.length; i++) if (!this.#disabled.has(i)) return i;
451
+ return -1;
452
+ }
453
+
454
+ /** Move the option cursor by `delta`, skipping disabled rows, stopping at the
455
+ * list edge. */
456
+ #moveSelection(delta: number): void {
457
+ const max = this.#options.length - 1;
458
+ if (max < 0) return;
459
+ let index = this.#selectedIndex;
460
+ while (true) {
461
+ const next = Math.max(0, Math.min(index + delta, max));
462
+ if (next === index) return;
463
+ index = next;
464
+ if (!this.#disabled.has(index)) {
465
+ this.#selectedIndex = index;
466
+ return;
467
+ }
468
+ }
469
+ }
470
+
471
+ /** Step the slider by `delta`, clamped to its edges (narrow-terminal mode). */
472
+ #moveSlider(delta: number): void {
473
+ const slider = this.#slider;
474
+ if (!slider) return;
475
+ const next = Math.max(0, Math.min(slider.segments.length - 1, this.#sliderIndex + delta));
476
+ if (next === this.#sliderIndex) return;
477
+ this.#sliderIndex = next;
478
+ slider.onChange?.(next);
479
+ }
480
+
481
+ #confirmSelection(): void {
482
+ const index = this.#selectedIndex;
483
+ if (index >= 0 && index < this.#options.length && !this.#disabled.has(index)) {
484
+ this.#committed = true;
485
+ this.#committedLabel = this.#options[index]!;
486
+ this.callbacks.onPick(this.#options[index]!);
487
+ }
488
+ }
489
+
490
+ handleInput(keyData: string): void {
491
+ if (this.#committed) return;
492
+ if (keyData.startsWith("\x1b[<") && this.#handleMouse(keyData)) return;
493
+ if (this.#annotating) {
494
+ if (this.callbacks.onAnnotationExternalEditor && matchesAppExternalEditor(keyData)) {
495
+ this.callbacks.onAnnotationExternalEditor(this.#input.getValue(), text => {
496
+ if (text !== null) this.#submitAnnotation(text);
497
+ });
498
+ return;
499
+ }
500
+ this.#input.handleInput(keyData);
501
+ return;
502
+ }
503
+ if (matchesSelectCancel(keyData)) {
504
+ this.#committed = true;
505
+ this.callbacks.onCancel();
506
+ return;
507
+ }
508
+ if (this.callbacks.onExternalEditor && matchesAppExternalEditor(keyData)) {
509
+ this.callbacks.onExternalEditor();
510
+ return;
511
+ }
512
+ if (this.callbacks.onCopyPlan && keyData === "c") {
513
+ void this.callbacks.onCopyPlan(joinPlanSections(this.#sections));
514
+ return;
515
+ }
516
+ if (matchesKey(keyData, "tab") || keyData === "\t") {
517
+ this.#cycleRegion(1);
518
+ return;
519
+ }
520
+ if (matchesKey(keyData, "shift+tab") || keyData === "\x1b[Z") {
521
+ this.#cycleRegion(-1);
522
+ return;
523
+ }
524
+ switch (this.#focus) {
525
+ case "actions":
526
+ this.#handleActions(keyData);
527
+ return;
528
+ case "body":
529
+ this.#handleBody(keyData);
530
+ return;
531
+ case "toc":
532
+ this.#handleToc(keyData);
533
+ return;
534
+ }
535
+ }
536
+
537
+ /**
538
+ * Hit-test an SGR mouse report (`\x1b[<b;x;yM/m`) against the click maps the
539
+ * last render recorded. Returns true when consumed. The fullscreen overlay
540
+ * paints from screen row 0, so a 1-based mouse row maps directly to the
541
+ * rendered-line index. Wheel scrolls the body; pointer motion lights up the
542
+ * hovered option row; a left click on an option activates it (select +
543
+ * confirm), on a ToC row jumps to that section, and on the body column focuses
544
+ * the body.
545
+ */
546
+ #handleMouse(data: string): boolean {
547
+ return routeSgrMouseInput(data, event => {
548
+ if (event.wheel !== null) {
549
+ // Scroll wheel: three rows per notch.
550
+ this.#scrollView.scroll(event.wheel * 3);
551
+ this.#captureScrollProgress();
552
+ return true;
553
+ }
554
+ if (event.release) return true;
555
+ if (event.motion) {
556
+ // Motion (hover or drag): light up the option row under the pointer so a
557
+ // mouse user gets the same affordance the keyboard cursor gives. Any
558
+ // non-option row clears the highlight.
559
+ this.#setHoveredOption(this.#optionClickRows.get(event.row));
560
+ return true;
561
+ }
562
+ if (!event.leftClick) return true;
563
+ const optionIndex = this.#optionClickRows.get(event.row);
564
+ if (optionIndex !== undefined) {
565
+ if (!this.#disabled.has(optionIndex)) {
566
+ this.#focus = "actions";
567
+ this.#selectedIndex = optionIndex;
568
+ this.#confirmSelection();
569
+ }
570
+ return true;
571
+ }
572
+ const tocPos = this.#tocClickRows.get(event.row);
573
+ if (tocPos !== undefined && event.col < this.#sidebarClickMaxCol) {
574
+ this.#focus = "toc";
575
+ this.#tocCursor = tocPos;
576
+ this.#scrubBodyToToc();
577
+ return true;
578
+ }
579
+ if (this.#bodyClickRows.has(event.row)) {
580
+ this.#setFocus("body");
581
+ }
582
+ return true;
583
+ });
584
+ }
585
+
586
+ /** Set the hovered option from a hit-tested row, ignoring disabled rows and
587
+ * non-option rows (both clear the highlight). */
588
+ #setHoveredOption(index: number | undefined): void {
589
+ this.#hoveredOption = index !== undefined && !this.#disabled.has(index) ? index : undefined;
590
+ }
591
+
592
+ #cycleRegion(direction: number): void {
593
+ // Sidebar is skipped from the cycle when it is not shown.
594
+ const regions: Focus[] = this.#sidebarShown ? ["toc", "body", "actions"] : ["body", "actions"];
595
+ const current = regions.indexOf(this.#focus);
596
+ const base = current < 0 ? regions.length - 1 : current;
597
+ this.#setFocus(regions[(base + direction + regions.length) % regions.length]!);
598
+ }
599
+
600
+ #setFocus(focus: Focus): void {
601
+ this.#focus = focus;
602
+ if (focus === "toc") this.#tocCursor = this.#deriveTocCursorFromScroll();
603
+ }
604
+
605
+ #handleActions(data: string): void {
606
+ // Left/right always drive the slider. The sidebar sits beside the body
607
+ // (above this row), not the slider, so stealing left for it would strand
608
+ // the operator unable to step the model tier back — reach the ToC via Tab.
609
+ const isLeft = matchesKey(data, "left") || (this.#slider !== undefined && matchesKey(data, "h"));
610
+ const isRight = matchesKey(data, "right") || (this.#slider !== undefined && matchesKey(data, "l"));
611
+ if (isLeft) {
612
+ this.#moveSlider(-1);
613
+ return;
614
+ }
615
+ if (isRight) {
616
+ this.#moveSlider(1);
617
+ return;
618
+ }
619
+ if (matchesSelectUp(data) || matchesKey(data, "k")) {
620
+ if (this.#selectedIndex === this.#firstEnabledIndex()) this.#setFocus("body");
621
+ else this.#moveSelection(-1);
622
+ return;
623
+ }
624
+ if (matchesSelectDown(data) || matchesKey(data, "j")) {
625
+ this.#moveSelection(1);
626
+ return;
627
+ }
628
+ if (matchesKey(data, "enter") || matchesKey(data, "return") || data === "\n") {
629
+ this.#confirmSelection();
630
+ return;
631
+ }
632
+ this.#handleBodyScroll(data);
633
+ }
634
+
635
+ #handleBody(data: string): void {
636
+ if (data === "a") {
637
+ this.#startBodyAnnotate();
638
+ return;
639
+ }
640
+ if (matchesKey(data, "left") || matchesKey(data, "h")) {
641
+ if (this.#sidebarShown) this.#setFocus("toc");
642
+ return;
643
+ }
644
+ if (
645
+ matchesKey(data, "right") ||
646
+ matchesKey(data, "l") ||
647
+ matchesKey(data, "enter") ||
648
+ matchesKey(data, "return") ||
649
+ data === "\n"
650
+ ) {
651
+ this.#setFocus("actions");
652
+ return;
653
+ }
654
+ // Vertical nav flows between regions at the edges: scrolling off the bottom
655
+ // drops into the actions ("next step"); scrolling off the top steps back up
656
+ // to the ToC.
657
+ if (matchesSelectUp(data) || matchesKey(data, "k")) {
658
+ if (this.#scrollView.getScrollOffset() <= 0 && this.#sidebarShown) {
659
+ this.#setFocus("toc");
660
+ } else {
661
+ this.#scrollView.scroll(-1);
662
+ this.#captureScrollProgress();
663
+ }
664
+ return;
665
+ }
666
+ if (matchesSelectDown(data) || matchesKey(data, "j")) {
667
+ if (this.#scrollView.getScrollOffset() >= this.#scrollView.getMaxScrollOffset()) {
668
+ this.#setFocus("actions");
669
+ } else {
670
+ this.#scrollView.scroll(1);
671
+ this.#captureScrollProgress();
672
+ }
673
+ return;
674
+ }
675
+ this.#handleBodyScroll(data);
676
+ }
677
+
678
+ /**
679
+ * Shared scroll dispatch for body + actions focus. Delegates standard keys
680
+ * (Arrows, Shift+Arrow fast-scroll, PgUp/PgDn, Home/End) to the ScrollView,
681
+ * then adds the vim g/G jumps. Plain Arrow/k/j are consumed by the callers
682
+ * before this runs, so here it only ever sees the paging/fast keys.
683
+ */
684
+ #handleBodyScroll(data: string): void {
685
+ if (this.#scrollView.handleScrollKey(data)) {
686
+ if (matchesKey(data, "home")) this.#scrollProgress = 0;
687
+ else if (matchesKey(data, "end")) this.#scrollProgress = 1;
688
+ else this.#captureScrollProgress();
689
+ return;
690
+ }
691
+ if (data === "g") {
692
+ this.#scrollView.scrollToTop();
693
+ this.#scrollProgress = 0;
694
+ } else if (data === "G") {
695
+ this.#scrollView.scrollToBottom();
696
+ this.#scrollProgress = 1;
697
+ }
698
+ }
699
+
700
+ #handleToc(data: string): void {
701
+ if (matchesSelectUp(data) || matchesKey(data, "k")) {
702
+ this.#moveTocCursor(-1);
703
+ return;
704
+ }
705
+ if (matchesSelectDown(data) || matchesKey(data, "j")) {
706
+ // Past the last section, fall through to the actions ("next step").
707
+ if (this.#tocCursor >= this.#toc.length - 1) this.#setFocus("actions");
708
+ else this.#moveTocCursor(1);
709
+ return;
710
+ }
711
+ if (
712
+ matchesKey(data, "right") ||
713
+ matchesKey(data, "l") ||
714
+ matchesKey(data, "enter") ||
715
+ matchesKey(data, "return") ||
716
+ data === "\n"
717
+ ) {
718
+ this.#setFocus("body");
719
+ return;
720
+ }
721
+ if (data === "d" || matchesKey(data, "delete")) {
722
+ this.#deleteSelectedSection();
723
+ return;
724
+ }
725
+ if (data === "a") {
726
+ this.#startSectionAnnotate();
727
+ return;
728
+ }
729
+ if (data === "u") {
730
+ this.#undoLast();
731
+ return;
732
+ }
733
+ }
734
+
735
+ #moveTocCursor(delta: number): void {
736
+ if (this.#toc.length === 0) return;
737
+ const next = Math.max(0, Math.min(this.#toc.length - 1, this.#tocCursor + delta));
738
+ if (next === this.#tocCursor) return;
739
+ this.#tocCursor = next;
740
+ this.#scrubBodyToToc();
741
+ }
742
+
743
+ /** Scroll the body so the selected ToC section's heading sits at the top. */
744
+ #scrubBodyToToc(): void {
745
+ const sectionIndex = this.#toc[this.#tocCursor];
746
+ if (sectionIndex === undefined) return;
747
+ const offset = this.#sectionOffsets[sectionIndex];
748
+ if (offset !== undefined) {
749
+ this.#scrollView.setScrollOffset(offset);
750
+ this.#captureScrollProgress();
751
+ }
752
+ }
753
+
754
+ /** Greatest ToC position whose section starts at or above the scroll offset. */
755
+ #deriveTocCursorFromScroll(): number {
756
+ if (this.#toc.length === 0) return 0;
757
+ const scrollOffset = this.#scrollView.getScrollOffset();
758
+ let current = 0;
759
+ for (let i = 0; i < this.#sections.length; i++) {
760
+ if ((this.#sectionOffsets[i] ?? 0) <= scrollOffset) current = i;
761
+ else break;
762
+ }
763
+ let pos = 0;
764
+ for (let p = 0; p < this.#toc.length; p++) {
765
+ if (this.#toc[p]! <= current) pos = p;
766
+ else break;
767
+ }
768
+ return pos;
769
+ }
770
+
771
+ #pushUndo(): void {
772
+ this.#undo.push({
773
+ text: joinPlanSections(this.#sections),
774
+ annotations: this.#sections.map(section =>
775
+ section.annotations.map(annotation => this.#cloneAnnotation(annotation)),
776
+ ),
777
+ deleted: [...this.#deleted],
778
+ });
779
+ }
780
+
781
+ #deleteSelectedSection(): void {
782
+ const sectionIndex = this.#toc[this.#tocCursor];
783
+ if (sectionIndex === undefined) return;
784
+ const span = sectionDeletionSpan(this.#sections, sectionIndex);
785
+ if (span.length === 0) return;
786
+ this.#pushUndo();
787
+ // Record the removed headings so the Refine feedback can ask the model to
788
+ // drop them, then splice from the bottom up so earlier indices stay valid.
789
+ for (const i of span) {
790
+ const section = this.#sections[i]!;
791
+ if (section.level >= 1 && section.title) this.#deleted.push(section.title);
792
+ }
793
+ for (let i = span.length - 1; i >= 0; i--) this.#sections.splice(span[i]!, 1);
794
+ this.#rebuildToc();
795
+ this.#tocCursor = Math.min(this.#tocCursor, Math.max(0, this.#toc.length - 1));
796
+ this.#pendingScrollToToc = true;
797
+ this.callbacks.onPlanEdited?.(joinPlanSections(this.#sections));
798
+ this.#recomputeFeedback();
799
+ }
800
+
801
+ #undoLast(): void {
802
+ const entry = this.#undo.pop();
803
+ if (!entry) return;
804
+ this.#setSections(entry.text);
805
+ for (let i = 0; i < this.#sections.length; i++) {
806
+ this.#sections[i]!.annotations =
807
+ entry.annotations[i]?.map(annotation => this.#cloneAnnotation(annotation)) ?? [];
808
+ }
809
+ this.#deleted = [...entry.deleted];
810
+ this.#tocCursor = Math.min(this.#tocCursor, Math.max(0, this.#toc.length - 1));
811
+ this.#pendingScrollToToc = true;
812
+ this.callbacks.onPlanEdited?.(joinPlanSections(this.#sections));
813
+ this.#recomputeFeedback();
814
+ }
815
+
816
+ #startSectionAnnotate(): void {
817
+ const sectionIndex = this.#toc[this.#tocCursor];
818
+ if (sectionIndex === undefined) return;
819
+ this.#startAnnotate({ sectionIndex, row: null, context: null });
820
+ }
821
+
822
+ #startBodyAnnotate(): void {
823
+ const maxRow = this.#bodyRowAnchors.length - 1;
824
+ if (maxRow < 0) return;
825
+ const topRow = Math.max(0, Math.min(maxRow, Math.floor(this.#scrollView.getScrollOffset())));
826
+ this.#startAnnotate(this.#bodyRowAnchors[topRow]!);
827
+ }
828
+
829
+ #startAnnotate(target: BodyRowAnchor | { sectionIndex: number; row: null; context: null }): void {
830
+ this.#annotationTarget = target;
831
+ this.#annotating = true;
832
+ this.#input.setValue("");
833
+ }
834
+
835
+ #submitAnnotation(value: string): void {
836
+ this.#annotating = false;
837
+ const note = value.trim();
838
+ const target = this.#annotationTarget;
839
+ this.#annotationTarget = undefined;
840
+ const section = target ? this.#sections[target.sectionIndex] : undefined;
841
+ if (note && section && target) {
842
+ this.#pushUndo();
843
+ section.annotations.push({
844
+ note,
845
+ target:
846
+ target.row === null
847
+ ? { kind: "section" }
848
+ : {
849
+ kind: "line",
850
+ row: target.row,
851
+ context: target.context,
852
+ contextTruncated: target.contextTruncated,
853
+ },
854
+ });
855
+ this.#recomputeFeedback();
856
+ }
857
+ this.#input.setValue("");
858
+ }
859
+
860
+ #exitAnnotate(): void {
861
+ this.#annotating = false;
862
+ this.#annotationTarget = undefined;
863
+ this.#input.setValue("");
864
+ }
865
+
866
+ #recomputeFeedback(): void {
867
+ this.callbacks.onAnnotationStateChange?.(this.#annotationState());
868
+ const annotated = this.#sections.filter(section => section.annotations.length > 0);
869
+ if (annotated.length === 0 && this.#deleted.length === 0) {
870
+ this.callbacks.onFeedbackChange?.("");
871
+ return;
872
+ }
873
+ let feedback = "Refinement feedback on the plan:\n";
874
+ if (this.#deleted.length > 0) {
875
+ feedback += "\nRemove these sections:\n";
876
+ for (const title of this.#deleted) feedback += `- ${title}\n`;
877
+ }
878
+ for (const section of annotated) {
879
+ feedback += `\n## ${section.title || "Plan preamble"}\n`;
880
+ for (const annotation of section.annotations) {
881
+ if (annotation.target.kind === "line") feedback += `> Line: ${annotation.target.context}\n`;
882
+ feedback += this.#formatAnnotationFeedback(annotation.note);
883
+ }
884
+ }
885
+ this.callbacks.onFeedbackChange?.(feedback);
886
+ }
887
+
888
+ #formatAnnotationFeedback(note: string): string {
889
+ if (!note.includes("\n")) return `- ${note}\n`;
890
+ const fence = this.#markdownFenceFor(note);
891
+ return `${fence}md\n${note}\n${fence}\n`;
892
+ }
893
+
894
+ #markdownFenceFor(text: string): string {
895
+ let fence = "```";
896
+ while (text.includes(fence)) fence += "`";
897
+ return fence;
898
+ }
899
+
900
+ #renderSliderLines(): string[] {
901
+ const slider = this.#slider;
902
+ if (!slider) return [];
903
+ const active = this.#sliderIndex;
904
+ const track = renderSegmentTrack(slider.segments, active);
905
+ const leftArrow = theme.fg(active > 0 ? "accent" : "dim", "◂");
906
+ const rightArrow = theme.fg(active < slider.segments.length - 1 ? "accent" : "dim", "▸");
907
+ const caption = slider.caption ? `${theme.fg("dim", slider.caption)} ` : "";
908
+ const trackLine = `${caption}${leftArrow} ${track} ${rightArrow}`;
909
+ const detail = slider.segments[active]?.detail;
910
+ if (!detail) return [trackLine];
911
+ return [trackLine, ` ${theme.fg("dim", "↳")} ${theme.fg("muted", detail)}`];
912
+ }
913
+
914
+ #renderOptionLines(): string[] {
915
+ const active = this.#focus === "actions";
916
+ return this.#options.map((label, i) => {
917
+ const selected = i === this.#selectedIndex;
918
+ const isDisabled = this.#disabled.has(i);
919
+ const hovered = !isDisabled && i === this.#hoveredOption;
920
+ // The cursor marks the selected option; it dims when actions are not the
921
+ // focused region so the active region's highlight stays unambiguous.
922
+ const cursor = selected ? theme.fg(active ? "accent" : "dim", `${theme.nav.cursor} `) : " ";
923
+ let text = isDisabled
924
+ ? theme.fg("dim", label)
925
+ : selected && active
926
+ ? theme.bold(theme.fg("accent", label))
927
+ : theme.fg("text", label);
928
+ // A pointer hovering an option paints a highlight band behind its label,
929
+ // distinct from the keyboard selection (cursor glyph + bold accent) which
930
+ // stays where it is. One space of padding gives the band a button shape.
931
+ if (hovered) text = theme.bg("selectedBg", ` ${text} `);
932
+ return cursor + text;
933
+ });
934
+ }
935
+
936
+ #buildHelp(): string {
937
+ const sep = " · ";
938
+ const parts: string[] = [];
939
+ switch (this.#focus) {
940
+ case "actions":
941
+ parts.push("↑↓ select", "⏎ confirm");
942
+ if (this.#slider) parts.push("◂▸ model");
943
+ break;
944
+ case "toc":
945
+ parts.push("↑↓ section", "⏎ open", "a annotate", "d delete", "u undo");
946
+ break;
947
+ case "body":
948
+ parts.push("↑↓ scroll", "⇧ faster", "pgup/pgdn", "g/G ends", "a annotate");
949
+ break;
950
+ }
951
+ if (this.callbacks.onCopyPlan) parts.push("c copy");
952
+ parts.push("tab regions");
953
+ if (this.#externalEditorLabel && this.#focus !== "toc") parts.push(`${this.#externalEditorLabel} editor`);
954
+ parts.push(this.#helpSuffix);
955
+ return parts.join(sep);
956
+ }
957
+
958
+ /**
959
+ * Retain relative progress across reflow frames. A non-scrollable intermediate
960
+ * frame has no meaningful offset, so it must not erase the last scroll position.
961
+ */
962
+ #captureScrollProgress(): void {
963
+ const maxOffset = this.#scrollView.getMaxScrollOffset();
964
+ if (maxOffset > 0) this.#scrollProgress = this.#scrollView.getScrollOffset() / maxOffset;
965
+ }
966
+
967
+ #layoutBody(lines: readonly string[], height: number): void {
968
+ this.#captureScrollProgress();
969
+ this.#scrollView.setLines(lines);
970
+ this.#scrollView.setHeight(height);
971
+ const maxOffset = this.#scrollView.getMaxScrollOffset();
972
+ if (maxOffset > 0) this.#scrollView.setScrollOffset(Math.round(this.#scrollProgress * maxOffset));
973
+ }
974
+
975
+ /** Build the concatenated body lines and map each rendered row back to a plan anchor. */
976
+ #buildBody(bodyContentWidth: number): string[] {
977
+ const lines: string[] = [];
978
+ const anchors: BodyRowAnchor[] = [];
979
+ // oxlint-disable-next-line unicorn/no-new-array -- length preallocation
980
+ const offsets: number[] = new Array(this.#sections.length);
981
+ for (let sectionIndex = 0; sectionIndex < this.#sections.length; sectionIndex++) {
982
+ const section = this.#sections[sectionIndex]!;
983
+ offsets[sectionIndex] = lines.length;
984
+ const rendered = section.md.render(bodyContentWidth);
985
+ const contexts = rendered.map(line => this.#lineContext(line));
986
+ for (let row = 0; row < rendered.length; row++) {
987
+ const context = contexts[row]!;
988
+ const anchor = {
989
+ sectionIndex,
990
+ row,
991
+ context: context.text,
992
+ contextTruncated: context.truncated,
993
+ };
994
+ lines.push(rendered[row]!);
995
+ anchors.push(anchor);
996
+ for (const annotation of section.annotations) {
997
+ const annotationRow =
998
+ annotation.target.kind === "section"
999
+ ? 0
1000
+ : this.#resolveLineRow(
1001
+ annotation.target.row,
1002
+ {
1003
+ text: annotation.target.context,
1004
+ truncated: annotation.target.contextTruncated,
1005
+ },
1006
+ contexts,
1007
+ );
1008
+ if (annotationRow === row) {
1009
+ this.#appendAnnotationCallout(lines, anchors, annotation.note, anchor, bodyContentWidth);
1010
+ }
1011
+ }
1012
+ }
1013
+ }
1014
+ this.#sectionOffsets = offsets;
1015
+ this.#bodyRowAnchors = anchors;
1016
+ return lines;
1017
+ }
1018
+
1019
+ #lineContext(line: string): LineAnchorContext {
1020
+ const sanitized = sanitizeStatusText(line);
1021
+ const truncated = visibleWidth(sanitized) > MAX_ANNOTATION_CONTEXT_WIDTH;
1022
+ const text = truncateToWidth(sanitized, MAX_ANNOTATION_CONTEXT_WIDTH, Ellipsis.Unicode);
1023
+ return { text: text || "(blank line)", truncated };
1024
+ }
1025
+
1026
+ #resolveLineRow(
1027
+ storedRow: number,
1028
+ storedContext: LineAnchorContext,
1029
+ contexts: readonly LineAnchorContext[],
1030
+ ): number {
1031
+ if (contexts.length === 0) return -1;
1032
+ const targetRow = Math.max(0, Math.floor(storedRow));
1033
+ const normalize = (context: LineAnchorContext): string => {
1034
+ const normalized = sanitizeStatusText(context.text).replace(/\s+/g, " ").trim();
1035
+ return context.truncated && normalized.endsWith("…") ? normalized.slice(0, -1) : normalized;
1036
+ };
1037
+ const normalizedStoredContext = normalize(storedContext);
1038
+ if (!normalizedStoredContext) return -1;
1039
+ let best = -1;
1040
+ let bestDistance = Number.POSITIVE_INFINITY;
1041
+ for (let row = 0; row < contexts.length; row++) {
1042
+ const normalizedContext = normalize(contexts[row]!);
1043
+ if (
1044
+ normalizedContext !== normalizedStoredContext &&
1045
+ !normalizedContext.includes(normalizedStoredContext) &&
1046
+ !normalizedStoredContext.includes(normalizedContext)
1047
+ ) {
1048
+ continue;
1049
+ }
1050
+ const distance = Math.abs(row - targetRow);
1051
+ if (distance < bestDistance) {
1052
+ best = row;
1053
+ bestDistance = distance;
1054
+ }
1055
+ }
1056
+ return best;
1057
+ }
1058
+
1059
+ #appendAnnotationCallout(
1060
+ lines: string[],
1061
+ anchors: BodyRowAnchor[],
1062
+ note: string,
1063
+ anchor: BodyRowAnchor,
1064
+ bodyContentWidth: number,
1065
+ ): void {
1066
+ const noteLines = note.split(/\r?\n/);
1067
+ for (let i = 0; i < noteLines.length; i++) {
1068
+ const prefix =
1069
+ i === 0
1070
+ ? `${theme.fg("warning", "▎ ")}${theme.fg("dim", "note: ")}`
1071
+ : `${theme.fg("warning", "▎ ")}${theme.fg("dim", " ")}`;
1072
+ const available = Math.max(0, bodyContentWidth - visibleWidth(prefix));
1073
+ const displayLine = truncateToWidth(
1074
+ replaceTabs(sanitizeText(noteLines[i] ?? "")),
1075
+ available,
1076
+ Ellipsis.Unicode,
1077
+ );
1078
+ lines.push(truncateToWidth(`${prefix}${theme.fg("accent", displayLine)}`, bodyContentWidth));
1079
+ anchors.push(anchor);
1080
+ }
1081
+ }
1082
+
1083
+ #sidebarWidthFor(width: number): number {
1084
+ return Math.max(18, Math.min(30, Math.round(width * 0.24)));
1085
+ }
1086
+
1087
+ #sidebarVisible(width: number): boolean {
1088
+ if (this.#toc.length < SIDEBAR_MIN_HEADINGS) return false;
1089
+ if (width < SIDEBAR_MIN_TOTAL_WIDTH) return false;
1090
+ return splitBodyWidth(width, this.#sidebarWidthFor(width)) >= SIDEBAR_MIN_BODY_WIDTH;
1091
+ }
1092
+
1093
+ /** Sidebar lines plus, per row, the ToC position shown there (for clicks). */
1094
+ #renderSidebarLines(
1095
+ regionRows: number,
1096
+ sidebarWidth: number,
1097
+ ): { lines: string[]; posForRow: (number | undefined)[] } {
1098
+ // No "Contents" label and no plan-title entry: the box title already says
1099
+ // "Plan Review", so the sidebar is just the bare list of sections, VS
1100
+ // Code-style. Window the entries around the cursor.
1101
+ const lines: string[] = [];
1102
+ const posForRow: (number | undefined)[] = [];
1103
+ const slots = Math.max(0, regionRows);
1104
+ const total = this.#toc.length;
1105
+ let start = 0;
1106
+ if (total > slots) {
1107
+ start = Math.max(0, Math.min(this.#tocCursor - Math.floor(slots / 2), total - slots));
1108
+ }
1109
+ for (let r = 0; r < slots; r++) {
1110
+ const p = start + r;
1111
+ lines.push(p < total ? this.#renderTocEntry(p, sidebarWidth) : "");
1112
+ posForRow.push(p < total ? p : undefined);
1113
+ }
1114
+ return { lines, posForRow };
1115
+ }
1116
+
1117
+ #renderTocEntry(p: number, width: number): string {
1118
+ const section = this.#sections[this.#toc[p]!]!;
1119
+ const highlighted = p === this.#tocCursor;
1120
+ const selected = highlighted && this.#focus === "toc";
1121
+ const glow = highlighted && this.#focus !== "toc";
1122
+ // Compact, VS Code-like rows: a single-column gutter, one space of indent
1123
+ // per nesting level, then the title and an annotation marker.
1124
+ const indent = " ".repeat(Math.max(0, section.level - this.#tocBaseLevel));
1125
+ const ann = section.annotations.length > 0 ? " ✎" : "";
1126
+ const avail = Math.max(0, width - 1 - indent.length - visibleWidth(ann));
1127
+ const title = truncateToWidth(section.title || "(untitled)", avail, Ellipsis.Unicode);
1128
+ const body = indent + title + ann;
1129
+ // Single-column gutter glyph: a cursor `›` on the focused selection, an
1130
+ // accent bar `▎` on the current scrolled section, otherwise blank. The
1131
+ // glyph keeps the cursor legible even where the selection background is
1132
+ // subtle; the focused row also gets the full-row highlight.
1133
+ const gutter = selected ? "›" : glow ? "▎" : " ";
1134
+ const line = gutter + body;
1135
+ if (selected) return theme.bg("selectedBg", theme.bold(width > 0 ? padToWidth(line, width) : ""));
1136
+ if (glow) return theme.fg("accent", line);
1137
+ return theme.fg("muted", line);
1138
+ }
1139
+
1140
+ #renderFooterLines(innerWidth: number): string[] {
1141
+ if (this.#annotating) {
1142
+ const target = this.#annotationTarget;
1143
+ const section = target ? this.#sections[target.sectionIndex] : undefined;
1144
+ const title = sanitizeStatusText(section?.title || "Plan preamble");
1145
+ const location =
1146
+ target?.row === null
1147
+ ? `‹${title}›`
1148
+ : `‹${title}› · ${truncateToWidth(target?.context ?? "", Math.max(1, innerWidth - 16), Ellipsis.Unicode)}`;
1149
+ const caption = truncateToWidth(
1150
+ `${theme.fg("dim", "Annotate")} ${theme.fg("accent", location)}`,
1151
+ innerWidth,
1152
+ Ellipsis.Unicode,
1153
+ );
1154
+ const hintParts = ["enter save", "esc cancel"];
1155
+ if (this.#externalEditorLabel) hintParts.push(`${this.#externalEditorLabel} editor`);
1156
+ return [caption, this.#input.render(innerWidth)[0] ?? "", theme.fg("dim", hintParts.join(" · "))];
1157
+ }
1158
+ return [theme.fg("dim", this.#buildHelp())];
1159
+ }
1160
+
1161
+ render(width: number): readonly string[] {
1162
+ const termHeight = process.stdout.rows || 40;
1163
+ const sidebarShown = this.#sidebarVisible(width);
1164
+ this.#sidebarShown = sidebarShown;
1165
+ const sidebarWidth = sidebarShown ? this.#sidebarWidthFor(width) : 0;
1166
+ const innerWidth = Math.max(1, width - 4);
1167
+ const bodyContentWidth = sidebarShown ? splitBodyWidth(width, sidebarWidth) : innerWidth;
1168
+
1169
+ const committed = this.#committed;
1170
+ const sliderLines = committed ? [] : this.#renderSliderLines();
1171
+ const submittingLabel = this.#committedLabel ? `${this.#committedLabel} — submitting…` : "Submitting…";
1172
+ const optionLines = committed ? [theme.bold(theme.fg("accent", submittingLabel))] : this.#renderOptionLines();
1173
+ const promptLines = this.#promptTitle ? [theme.bold(theme.fg("accent", this.#promptTitle))] : [];
1174
+ const footerLines = committed
1175
+ ? [theme.fg("dim", "Applying your selection — this can take a moment while context is prepared.")]
1176
+ : this.#renderFooterLines(innerWidth);
1177
+
1178
+ // Chrome rows: top border, two dividers, bottom border, plus the
1179
+ // prompt/slider/option/footer rows between them.
1180
+ const chrome = 4 + promptLines.length + sliderLines.length + optionLines.length + footerLines.length;
1181
+ const regionRows = Math.max(MIN_BODY_ROWS, termHeight - chrome);
1182
+
1183
+ const bodyLines = this.#buildBody(bodyContentWidth);
1184
+ this.#layoutBody(bodyLines, regionRows);
1185
+ if (this.#pendingScrollToToc) {
1186
+ this.#pendingScrollToToc = false;
1187
+ this.#scrubBodyToToc();
1188
+ }
1189
+ if (this.#focus !== "toc") this.#tocCursor = this.#deriveTocCursorFromScroll();
1190
+ const body = this.#scrollView.render(bodyContentWidth);
1191
+
1192
+ this.#optionClickRows.clear();
1193
+ this.#tocClickRows.clear();
1194
+ this.#bodyClickRows.clear();
1195
+ this.#sidebarClickMaxCol = sidebarShown ? sidebarWidth + 3 : 0;
1196
+
1197
+ const out: string[] = [];
1198
+ if (sidebarShown) {
1199
+ const { lines: sidebar, posForRow } = this.#renderSidebarLines(regionRows, sidebarWidth);
1200
+ out.push(topBorderSplit(width, OVERLAY_TITLE, sidebarWidth));
1201
+ for (let i = 0; i < regionRows; i++) {
1202
+ const pos = posForRow[i];
1203
+ if (pos !== undefined) this.#tocClickRows.set(out.length, pos);
1204
+ this.#bodyClickRows.add(out.length);
1205
+ out.push(splitRow(sidebar[i] ?? "", body[i] ?? "", width, sidebarWidth));
1206
+ }
1207
+ out.push(dividerSplit(width, sidebarWidth));
1208
+ } else {
1209
+ out.push(topBorder(width, OVERLAY_TITLE));
1210
+ for (const line of body) {
1211
+ this.#bodyClickRows.add(out.length);
1212
+ out.push(row(line, width));
1213
+ }
1214
+ out.push(divider(width));
1215
+ }
1216
+ for (const line of promptLines) out.push(row(line, width));
1217
+ for (const line of sliderLines) out.push(row(line, width));
1218
+ for (let i = 0; i < optionLines.length; i++) {
1219
+ if (!committed) this.#optionClickRows.set(out.length, i);
1220
+ out.push(row(optionLines[i]!, width));
1221
+ }
1222
+ out.push(divider(width));
1223
+ for (const line of footerLines) out.push(row(line, width));
1224
+ out.push(bottomBorder(width));
1225
+ return out;
1226
+ }
1227
+ }