@oh-my-pi/pi-tui 18.2.4 → 18.2.6

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 (748) hide show
  1. package/CHANGELOG.md +23 -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/key-value-list.d.ts +24 -0
  92. package/dist/types/components/layout/geometry.d.ts +73 -0
  93. package/dist/types/components/layout/row.d.ts +55 -0
  94. package/dist/types/components/layout/split-pane.d.ts +80 -0
  95. package/dist/types/components/layout/stack.d.ts +50 -0
  96. package/dist/types/components/menu-selection.d.ts +71 -0
  97. package/dist/types/components/metric.d.ts +44 -0
  98. package/dist/types/components/progress-bar.d.ts +36 -0
  99. package/dist/types/components/scroll-view.d.ts +53 -19
  100. package/dist/types/components/scroll-viewport.d.ts +46 -0
  101. package/dist/types/components/section.d.ts +19 -0
  102. package/dist/types/components/select-list.d.ts +46 -2
  103. package/dist/types/components/settings-list.d.ts +20 -0
  104. package/dist/types/components/table.d.ts +32 -0
  105. package/dist/types/components/tree-view.d.ts +130 -0
  106. package/dist/types/components/wizard-step.d.ts +60 -0
  107. package/dist/types/hotkeys-markdown.d.ts +7 -0
  108. package/dist/types/index.d.ts +17 -0
  109. package/dist/types/keybinding-matchers.d.ts +35 -0
  110. package/dist/types/keys.d.ts +2 -2
  111. package/dist/types/lang-from-path.d.ts +9 -0
  112. package/dist/types/overlays/advisor-config.d.ts +114 -0
  113. package/dist/types/overlays/agent-activity.d.ts +41 -0
  114. package/dist/types/overlays/agent-hub-projection.d.ts +33 -0
  115. package/dist/types/overlays/agent-hub-renderer.d.ts +53 -0
  116. package/dist/types/overlays/agent-hub-types.d.ts +99 -0
  117. package/dist/types/overlays/agent-hub.d.ts +126 -0
  118. package/dist/types/overlays/agent-transcript-viewer.d.ts +71 -0
  119. package/dist/types/overlays/agents-hub.d.ts +72 -0
  120. package/dist/types/overlays/ask-dialog.d.ts +88 -0
  121. package/dist/types/overlays/bordered-loader.d.ts +11 -0
  122. package/dist/types/overlays/btw-history-panel.d.ts +29 -0
  123. package/dist/types/overlays/btw-history.d.ts +17 -0
  124. package/dist/types/overlays/btw-panel.d.ts +26 -0
  125. package/dist/types/overlays/cleanse-panel.d.ts +36 -0
  126. package/dist/types/overlays/codex-reset-fireworks.d.ts +52 -0
  127. package/dist/types/overlays/composer-shape-preview.d.ts +47 -0
  128. package/dist/types/overlays/composer-shape-registry.d.ts +50 -0
  129. package/dist/types/overlays/copy-selector.d.ts +48 -0
  130. package/dist/types/overlays/copy-targets.d.ts +63 -0
  131. package/dist/types/overlays/error-banner.d.ts +13 -0
  132. package/dist/types/overlays/extensions/display-text.d.ts +10 -0
  133. package/dist/types/overlays/extensions/extension-dashboard.d.ts +63 -0
  134. package/dist/types/overlays/extensions/extension-list.d.ts +54 -0
  135. package/dist/types/overlays/extensions/inspector-model.d.ts +109 -0
  136. package/dist/types/overlays/extensions/inspector-panel.d.ts +17 -0
  137. package/dist/types/overlays/extensions/live-tool-session.d.ts +28 -0
  138. package/dist/types/overlays/extensions/mcp-runtime.d.ts +107 -0
  139. package/dist/types/overlays/extensions/state-manager.d.ts +42 -0
  140. package/dist/types/overlays/extensions/types.d.ts +165 -0
  141. package/dist/types/overlays/history-search.d.ts +16 -0
  142. package/dist/types/overlays/hook-editor.d.ts +44 -0
  143. package/dist/types/overlays/hook-input.d.ts +20 -0
  144. package/dist/types/overlays/hook-selector.d.ts +71 -0
  145. package/dist/types/overlays/hub-frame.d.ts +77 -0
  146. package/dist/types/overlays/login-dialog.d.ts +43 -0
  147. package/dist/types/overlays/logout-account-selector.d.ts +15 -0
  148. package/dist/types/overlays/mcp-add-wizard.d.ts +87 -0
  149. package/dist/types/overlays/model-browser.d.ts +213 -0
  150. package/dist/types/overlays/model-hub.d.ts +72 -0
  151. package/dist/types/overlays/model-picker.d.ts +74 -0
  152. package/dist/types/overlays/model-selector.d.ts +52 -0
  153. package/dist/types/overlays/move-overlay.d.ts +39 -0
  154. package/dist/types/overlays/oauth-selector.d.ts +41 -0
  155. package/dist/types/overlays/omfg-panel.d.ts +20 -0
  156. package/dist/types/overlays/pause-screen.d.ts +43 -0
  157. package/dist/types/overlays/plan-review-overlay.d.ts +89 -0
  158. package/dist/types/overlays/plan-save-overlay.d.ts +19 -0
  159. package/dist/types/overlays/plan-toc.d.ts +41 -0
  160. package/dist/types/overlays/plugin-selector.d.ts +28 -0
  161. package/dist/types/overlays/plugin-settings.d.ts +180 -0
  162. package/dist/types/overlays/queue-mode-selector.d.ts +11 -0
  163. package/dist/types/overlays/reset-usage-selector.d.ts +23 -0
  164. package/dist/types/overlays/rewind-selector.d.ts +61 -0
  165. package/dist/types/overlays/running-subagent-badge.d.ts +6 -0
  166. package/dist/types/overlays/session-account-selector.d.ts +24 -0
  167. package/dist/types/overlays/session-info-overlay.d.ts +17 -0
  168. package/dist/types/overlays/session-observer-registry.d.ts +77 -0
  169. package/dist/types/overlays/session-selector.d.ts +131 -0
  170. package/dist/types/overlays/settings-defs.d.ts +119 -0
  171. package/dist/types/overlays/settings-selector.d.ts +73 -0
  172. package/dist/types/overlays/show-images-selector.d.ts +11 -0
  173. package/dist/types/overlays/snapcompact-shape-preview.d.ts +31 -0
  174. package/dist/types/overlays/theme-selector.d.ts +12 -0
  175. package/dist/types/overlays/thinking-selector.d.ts +12 -0
  176. package/dist/types/overlays/tiny-title-download-progress.d.ts +21 -0
  177. package/dist/types/overlays/tree-selector.d.ts +92 -0
  178. package/dist/types/overlays/usage-dashboard.d.ts +94 -0
  179. package/dist/types/overlays/usage-display.d.ts +5 -0
  180. package/dist/types/overlays/usage-row.d.ts +17 -0
  181. package/dist/types/prompt/attachment-chips.d.ts +19 -0
  182. package/dist/types/prompt/composer-attachments.d.ts +71 -0
  183. package/dist/types/prompt/composer-cache.d.ts +37 -0
  184. package/dist/types/prompt/composer.d.ts +159 -0
  185. package/dist/types/prompt/custom-editor.d.ts +261 -0
  186. package/dist/types/prompt/editor-top-gap.d.ts +18 -0
  187. package/dist/types/prompt/emoji-autocomplete.d.ts +16 -0
  188. package/dist/types/prompt/github-ref-autocomplete.d.ts +35 -0
  189. package/dist/types/prompt/gradient-highlight.d.ts +32 -0
  190. package/dist/types/prompt/image-format.d.ts +4 -0
  191. package/dist/types/prompt/image-references.d.ts +26 -0
  192. package/dist/types/prompt/interactive-context-helpers.d.ts +43 -0
  193. package/dist/types/prompt/internal-url-autocomplete.d.ts +56 -0
  194. package/dist/types/prompt/macos-spelling.d.ts +44 -0
  195. package/dist/types/prompt/magic-keyword-boundary.d.ts +9 -0
  196. package/dist/types/prompt/magic-keywords.d.ts +26 -0
  197. package/dist/types/prompt/markdown-prose.d.ts +15 -0
  198. package/dist/types/prompt/model-mention-autocomplete.d.ts +25 -0
  199. package/dist/types/prompt/model-mention-syntax.d.ts +24 -0
  200. package/dist/types/prompt/orchestrate.d.ts +13 -0
  201. package/dist/types/prompt/prompt-action-autocomplete.d.ts +56 -0
  202. package/dist/types/prompt/queue-input.d.ts +8 -0
  203. package/dist/types/prompt/skill-tokens.d.ts +13 -0
  204. package/dist/types/prompt/tools-markdown.d.ts +12 -0
  205. package/dist/types/prompt/ultrathink.d.ts +13 -0
  206. package/dist/types/prompt/usage-amounts.d.ts +26 -0
  207. package/dist/types/prompt/video.d.ts +18 -0
  208. package/dist/types/prompt/welcome.d.ts +83 -0
  209. package/dist/types/prompt/workflow.d.ts +13 -0
  210. package/dist/types/render/code-cell.d.ts +51 -0
  211. package/dist/types/render/file-list.d.ts +22 -0
  212. package/dist/types/render/hyperlink.d.ts +80 -0
  213. package/dist/types/render/index.d.ts +16 -0
  214. package/dist/types/render/output-block.d.ts +54 -0
  215. package/dist/types/render/output-pane.d.ts +80 -0
  216. package/dist/types/render/render-utils.d.ts +312 -0
  217. package/dist/types/render/sixel.d.ts +21 -0
  218. package/dist/types/render/status-line.d.ts +23 -0
  219. package/dist/types/render/tool-card.d.ts +71 -0
  220. package/dist/types/render/tree-list.d.ts +29 -0
  221. package/dist/types/render/types.d.ts +17 -0
  222. package/dist/types/render/utils.d.ts +43 -0
  223. package/dist/types/render/width-aware-text.d.ts +23 -0
  224. package/dist/types/setup/lazy.d.ts +3 -0
  225. package/dist/types/setup/scenes/composer.d.ts +3 -0
  226. package/dist/types/setup/scenes/glyph.d.ts +3 -0
  227. package/dist/types/setup/scenes/model.d.ts +3 -0
  228. package/dist/types/setup/scenes/outro.d.ts +2 -0
  229. package/dist/types/setup/scenes/providers.d.ts +3 -0
  230. package/dist/types/setup/scenes/sign-in.d.ts +21 -0
  231. package/dist/types/setup/scenes/splash.d.ts +11 -0
  232. package/dist/types/setup/scenes/theme.d.ts +3 -0
  233. package/dist/types/setup/scenes/types.d.ts +110 -0
  234. package/dist/types/setup/scenes/web-search.d.ts +22 -0
  235. package/dist/types/setup/setup-version.d.ts +11 -0
  236. package/dist/types/setup/startup-splash.d.ts +9 -0
  237. package/dist/types/setup/wizard-overlay.d.ts +16 -0
  238. package/dist/types/setup/wizard.d.ts +25 -0
  239. package/dist/types/status-line/component.d.ts +182 -0
  240. package/dist/types/status-line/context-usage.d.ts +156 -0
  241. package/dist/types/status-line/footer.d.ts +30 -0
  242. package/dist/types/status-line/git-utils.d.ts +22 -0
  243. package/dist/types/status-line/host.d.ts +129 -0
  244. package/dist/types/status-line/index.d.ts +6 -0
  245. package/dist/types/status-line/loop.d.ts +18 -0
  246. package/dist/types/status-line/metrics.d.ts +21 -0
  247. package/dist/types/status-line/presets.d.ts +3 -0
  248. package/dist/types/status-line/schema.d.ts +15 -0
  249. package/dist/types/status-line/segments.d.ts +5 -0
  250. package/dist/types/status-line/separators.d.ts +3 -0
  251. package/dist/types/status-line/types.d.ts +236 -0
  252. package/dist/types/stdin-buffer.d.ts +4 -0
  253. package/dist/types/theme/color.d.ts +24 -0
  254. package/dist/types/theme/defaults/index.d.ts +9489 -0
  255. package/dist/types/theme/index.d.ts +4 -0
  256. package/dist/types/theme/loader.d.ts +23 -0
  257. package/dist/types/theme/mermaid-cache.d.ts +26 -0
  258. package/dist/types/theme/schema-validation.d.ts +3 -0
  259. package/dist/types/theme/schema.d.ts +25 -0
  260. package/dist/types/theme/session-color.d.ts +43 -0
  261. package/dist/types/theme/shimmer.d.ts +44 -0
  262. package/dist/types/theme/symbols.d.ts +34 -0
  263. package/dist/types/theme/theme-class.d.ts +299 -0
  264. package/dist/types/theme/theme.d.ts +111 -0
  265. package/dist/types/theme/tui-adapters.d.ts +30 -0
  266. package/dist/types/thinking.d.ts +165 -0
  267. package/dist/types/tools/agent-tree.d.ts +36 -0
  268. package/dist/types/tools/ask.d.ts +67 -0
  269. package/dist/types/tools/ast-edit.d.ts +53 -0
  270. package/dist/types/tools/ast-grep.d.ts +52 -0
  271. package/dist/types/tools/autoresearch.d.ts +156 -0
  272. package/dist/types/tools/bash-interactive.d.ts +28 -0
  273. package/dist/types/tools/bash.d.ts +103 -0
  274. package/dist/types/tools/conflict-detect.d.ts +43 -0
  275. package/dist/types/tools/debug.d.ts +60 -0
  276. package/dist/types/tools/default-renderer.d.ts +24 -0
  277. package/dist/types/tools/edit.d.ts +158 -0
  278. package/dist/types/tools/eval-format/code-mode-declarations.d.ts +5 -0
  279. package/dist/types/tools/eval-format/index.d.ts +5 -0
  280. package/dist/types/tools/eval-format/javascript.d.ts +2 -0
  281. package/dist/types/tools/eval-format/python.d.ts +2 -0
  282. package/dist/types/tools/eval.d.ts +94 -0
  283. package/dist/types/tools/fetch.d.ts +42 -0
  284. package/dist/types/tools/gh-format.d.ts +8 -0
  285. package/dist/types/tools/github.d.ts +144 -0
  286. package/dist/types/tools/glob.d.ts +44 -0
  287. package/dist/types/tools/goal.d.ts +42 -0
  288. package/dist/types/tools/grep.d.ts +64 -0
  289. package/dist/types/tools/grouped-file-output.d.ts +71 -0
  290. package/dist/types/tools/hashline-format.d.ts +28 -0
  291. package/dist/types/tools/hub.d.ts +340 -0
  292. package/dist/types/tools/index.d.ts +4 -0
  293. package/dist/types/tools/json-tree.d.ts +59 -0
  294. package/dist/types/tools/line-ranges.d.ts +29 -0
  295. package/dist/types/tools/list-limit.d.ts +15 -0
  296. package/dist/types/tools/lsp.d.ts +72 -0
  297. package/dist/types/tools/match-line-format.d.ts +11 -0
  298. package/dist/types/tools/mcp.d.ts +81 -0
  299. package/dist/types/tools/memory.d.ts +67 -0
  300. package/dist/types/tools/output-meta.d.ts +151 -0
  301. package/dist/types/tools/read.d.ts +139 -0
  302. package/dist/types/tools/renderer.d.ts +98 -0
  303. package/dist/types/tools/report-tool-issue.d.ts +8 -0
  304. package/dist/types/tools/resolve.d.ts +47 -0
  305. package/dist/types/tools/streaming-output.d.ts +282 -0
  306. package/dist/types/tools/subprocess.d.ts +24 -0
  307. package/dist/types/tools/task-repair-args.d.ts +53 -0
  308. package/dist/types/tools/task-yield-assembly.d.ts +20 -0
  309. package/dist/types/tools/task.d.ts +425 -0
  310. package/dist/types/tools/terminal-output.d.ts +8 -0
  311. package/dist/types/tools/think.d.ts +13 -0
  312. package/dist/types/tools/todo.d.ts +118 -0
  313. package/dist/types/tools/tool-errors.d.ts +7 -0
  314. package/dist/types/tools/vibe.d.ts +120 -0
  315. package/dist/types/tools/web-search.d.ts +195 -0
  316. package/dist/types/tools/write.d.ts +76 -0
  317. package/dist/types/tools/xd-url.d.ts +13 -0
  318. package/dist/types/tools/xdev.d.ts +52 -0
  319. package/dist/types/tui.d.ts +18 -0
  320. package/dist/types/utils.d.ts +2 -0
  321. package/package.json +37 -3
  322. package/src/app-keybindings.ts +760 -0
  323. package/src/apps/autoresearch-dashboard.ts +500 -0
  324. package/src/apps/autoresearch-data.ts +64 -0
  325. package/src/apps/cleanse-board.ts +341 -0
  326. package/src/apps/cleanse-picker.ts +57 -0
  327. package/src/apps/debug/log-formatting.ts +53 -0
  328. package/src/apps/debug/log-viewer.ts +868 -0
  329. package/src/apps/debug/protocol-probe.ts +267 -0
  330. package/src/apps/debug/raw-sse-buffer.ts +449 -0
  331. package/src/apps/debug/raw-sse.ts +282 -0
  332. package/src/apps/debug/terminal-info.ts +115 -0
  333. package/src/apps/debug/viewer-frame.ts +174 -0
  334. package/src/apps/git/avatar.ts +52 -0
  335. package/src/apps/git/colors.ts +109 -0
  336. package/src/apps/git/diff-pane.ts +1512 -0
  337. package/src/apps/git/git-tui.ts +889 -0
  338. package/src/apps/git/sidebar.ts +1204 -0
  339. package/src/apps/git/state.ts +113 -0
  340. package/src/apps/if-bench-board.ts +324 -0
  341. package/src/apps/live-visualizer.ts +238 -0
  342. package/src/apps/ps-data.ts +116 -0
  343. package/src/apps/ps-top.ts +422 -0
  344. package/src/apps/session-picker.ts +95 -0
  345. package/src/apps/setup-model-picker.ts +21 -0
  346. package/src/apps/standalone-picker.ts +111 -0
  347. package/src/autocomplete.ts +9 -13
  348. package/src/chat/advisor-message.ts +175 -0
  349. package/src/chat/assistant-message.ts +1214 -0
  350. package/src/chat/background-tan-message.ts +35 -0
  351. package/src/chat/bash-execution.ts +394 -0
  352. package/src/chat/cache-invalidation-marker.ts +94 -0
  353. package/src/chat/chat-transcript-builder.ts +593 -0
  354. package/src/chat/collab-prompt-message.ts +34 -0
  355. package/src/chat/compaction-summary-message.ts +261 -0
  356. package/src/chat/custom-message.ts +23 -0
  357. package/src/chat/display-preferences.ts +24 -0
  358. package/src/chat/eval-execution.ts +172 -0
  359. package/src/chat/execution-shared.ts +113 -0
  360. package/src/chat/extension-types.ts +46 -0
  361. package/src/chat/hook-message.ts +20 -0
  362. package/src/chat/image-loading.ts +122 -0
  363. package/src/chat/late-diagnostics-message.ts +92 -0
  364. package/src/chat/messages.ts +305 -0
  365. package/src/chat/reaction.ts +65 -0
  366. package/src/chat/read-target.ts +7 -0
  367. package/src/chat/read-tool-group.ts +873 -0
  368. package/src/chat/served-model-marker.ts +91 -0
  369. package/src/chat/skill-message.ts +220 -0
  370. package/src/chat/skill-title-input.ts +41 -0
  371. package/src/chat/stripped-tool-calls-placeholder.ts +35 -0
  372. package/src/chat/thinking-display.ts +312 -0
  373. package/src/chat/todo-reminder.ts +33 -0
  374. package/src/chat/tool-execution.ts +1395 -0
  375. package/src/chat/transcript-browser.ts +268 -0
  376. package/src/chat/transcript-entry.ts +96 -0
  377. package/src/chat/transcript-outline.ts +260 -0
  378. package/src/chat/transcript-render-helpers.ts +315 -0
  379. package/src/chat/ttsr-notification.ts +123 -0
  380. package/src/chat/user-message.ts +287 -0
  381. package/src/chrome/chat-block.ts +110 -0
  382. package/src/chrome/collab-qrcode.ts +74 -0
  383. package/src/chrome/context-thresholds.ts +85 -0
  384. package/src/chrome/countdown-timer.ts +74 -0
  385. package/src/chrome/diff.ts +266 -0
  386. package/src/chrome/dynamic-border.ts +36 -0
  387. package/src/chrome/error-block.ts +63 -0
  388. package/src/chrome/form-theme.ts +10 -0
  389. package/src/chrome/format.ts +66 -0
  390. package/src/chrome/index.ts +21 -0
  391. package/src/chrome/keybinding-hints.ts +55 -0
  392. package/src/chrome/live-board.ts +123 -0
  393. package/src/chrome/local-date.ts +20 -0
  394. package/src/chrome/message-divider.ts +51 -0
  395. package/src/chrome/message-frame.ts +164 -0
  396. package/src/chrome/message-notice.ts +103 -0
  397. package/src/chrome/overlay-box.ts +301 -0
  398. package/src/chrome/qrcode.ts +539 -0
  399. package/src/chrome/segment-track.ts +86 -0
  400. package/src/chrome/select-list-mouse-routing.ts +11 -0
  401. package/src/chrome/selector-helpers.ts +119 -0
  402. package/src/chrome/shared.ts +27 -0
  403. package/src/chrome/streaming-panel.ts +72 -0
  404. package/src/chrome/tool-activity.ts +44 -0
  405. package/src/chrome/transcript-container.ts +902 -0
  406. package/src/chrome/transcript-status.ts +27 -0
  407. package/src/chrome/visual-truncate.ts +70 -0
  408. package/src/components/disclosure.ts +235 -0
  409. package/src/components/editor.ts +10 -12
  410. package/src/components/form.ts +566 -0
  411. package/src/components/input.ts +6 -27
  412. package/src/components/key-value-list.ts +88 -0
  413. package/src/components/layout/geometry.ts +202 -0
  414. package/src/components/layout/row.ts +283 -0
  415. package/src/components/layout/split-pane.ts +349 -0
  416. package/src/components/layout/stack.ts +261 -0
  417. package/src/components/markdown.ts +16 -12
  418. package/src/components/menu-selection.ts +248 -0
  419. package/src/components/metric.ts +132 -0
  420. package/src/components/progress-bar.ts +140 -0
  421. package/src/components/scroll-view.ts +372 -70
  422. package/src/components/scroll-viewport.ts +154 -0
  423. package/src/components/section.ts +77 -0
  424. package/src/components/select-list.ts +238 -159
  425. package/src/components/settings-list.ts +103 -80
  426. package/src/components/table.ts +127 -0
  427. package/src/components/tree-view.ts +481 -0
  428. package/src/components/wizard-step.ts +229 -0
  429. package/src/deccara.ts +1 -3
  430. package/src/hotkeys-markdown.ts +100 -0
  431. package/src/index.ts +18 -0
  432. package/src/keybinding-matchers.ts +99 -0
  433. package/src/keys.ts +2 -2
  434. package/src/lang-from-path.ts +252 -0
  435. package/src/overlays/advisor-config.ts +761 -0
  436. package/src/overlays/agent-activity.ts +113 -0
  437. package/src/overlays/agent-hub-projection.ts +250 -0
  438. package/src/overlays/agent-hub-renderer.ts +281 -0
  439. package/src/overlays/agent-hub-types.ts +74 -0
  440. package/src/overlays/agent-hub.ts +1562 -0
  441. package/src/overlays/agent-transcript-viewer.ts +669 -0
  442. package/src/overlays/agents-hub.ts +1203 -0
  443. package/src/overlays/ask-dialog.ts +1202 -0
  444. package/src/overlays/bordered-loader.ts +41 -0
  445. package/src/overlays/btw-history-panel.ts +591 -0
  446. package/src/overlays/btw-history.ts +33 -0
  447. package/src/overlays/btw-panel.ts +167 -0
  448. package/src/overlays/cleanse-panel.ts +167 -0
  449. package/src/overlays/codex-reset-fireworks.ts +375 -0
  450. package/src/overlays/composer-shape-preview.ts +137 -0
  451. package/src/overlays/composer-shape-registry.ts +82 -0
  452. package/src/overlays/copy-selector.ts +618 -0
  453. package/src/overlays/copy-targets.ts +210 -0
  454. package/src/overlays/error-banner.ts +39 -0
  455. package/src/overlays/extensions/display-text.ts +39 -0
  456. package/src/overlays/extensions/extension-dashboard.ts +645 -0
  457. package/src/overlays/extensions/extension-list.ts +667 -0
  458. package/src/overlays/extensions/inspector-model.ts +484 -0
  459. package/src/overlays/extensions/inspector-panel.ts +669 -0
  460. package/src/overlays/extensions/live-tool-session.ts +67 -0
  461. package/src/overlays/extensions/mcp-runtime.ts +289 -0
  462. package/src/overlays/extensions/state-manager.ts +337 -0
  463. package/src/overlays/extensions/types.ts +208 -0
  464. package/src/overlays/history-search.ts +268 -0
  465. package/src/overlays/hook-editor.ts +272 -0
  466. package/src/overlays/hook-input.ts +84 -0
  467. package/src/overlays/hook-selector.ts +635 -0
  468. package/src/overlays/hub-frame.ts +262 -0
  469. package/src/overlays/login-dialog.ts +231 -0
  470. package/src/overlays/logout-account-selector.ts +115 -0
  471. package/src/overlays/mcp-add-wizard.ts +1436 -0
  472. package/src/overlays/model-browser.ts +1176 -0
  473. package/src/overlays/model-hub.ts +2279 -0
  474. package/src/overlays/model-picker.ts +285 -0
  475. package/src/overlays/model-selector.ts +107 -0
  476. package/src/overlays/move-overlay.ts +197 -0
  477. package/src/overlays/oauth-selector.ts +446 -0
  478. package/src/overlays/omfg-panel.ts +136 -0
  479. package/src/overlays/pause-screen.ts +198 -0
  480. package/src/overlays/plan-review-overlay.ts +1227 -0
  481. package/src/overlays/plan-save-overlay.ts +74 -0
  482. package/src/overlays/plan-toc.ts +139 -0
  483. package/src/overlays/plugin-selector.ts +97 -0
  484. package/src/overlays/plugin-settings.ts +839 -0
  485. package/src/overlays/queue-mode-selector.ts +55 -0
  486. package/src/overlays/reset-usage-selector.ts +170 -0
  487. package/src/overlays/rewind-selector.ts +499 -0
  488. package/src/overlays/running-subagent-badge.ts +19 -0
  489. package/src/overlays/session-account-selector.ts +68 -0
  490. package/src/overlays/session-info-overlay.ts +96 -0
  491. package/src/overlays/session-observer-registry.ts +289 -0
  492. package/src/overlays/session-selector.ts +1137 -0
  493. package/src/overlays/settings-defs.ts +315 -0
  494. package/src/overlays/settings-selector.ts +1415 -0
  495. package/src/overlays/show-images-selector.ts +44 -0
  496. package/src/overlays/snapcompact-shape-preview-doc.md +14 -0
  497. package/src/overlays/snapcompact-shape-preview.ts +192 -0
  498. package/src/overlays/theme-selector.ts +62 -0
  499. package/src/overlays/thinking-selector.ts +51 -0
  500. package/src/overlays/tiny-title-download-progress.ts +102 -0
  501. package/src/overlays/tree-selector.ts +1121 -0
  502. package/src/overlays/usage-dashboard.ts +629 -0
  503. package/src/overlays/usage-display.ts +45 -0
  504. package/src/overlays/usage-row.ts +118 -0
  505. package/src/prompt/attachment-chips.ts +201 -0
  506. package/src/prompt/composer-attachments.ts +280 -0
  507. package/src/prompt/composer-cache.ts +304 -0
  508. package/src/prompt/composer.ts +934 -0
  509. package/src/prompt/custom-editor.ts +1392 -0
  510. package/src/prompt/data/emojis.json +1 -0
  511. package/src/prompt/editor-top-gap.ts +28 -0
  512. package/src/prompt/emoji-autocomplete.ts +285 -0
  513. package/src/prompt/github-ref-autocomplete.ts +75 -0
  514. package/src/prompt/gradient-highlight.ts +101 -0
  515. package/src/prompt/image-format.ts +28 -0
  516. package/src/prompt/image-references.ts +102 -0
  517. package/src/prompt/interactive-context-helpers.ts +126 -0
  518. package/src/prompt/internal-url-autocomplete.ts +155 -0
  519. package/src/prompt/macos-spelling.ts +456 -0
  520. package/src/prompt/magic-keyword-boundary.ts +23 -0
  521. package/src/prompt/magic-keywords.ts +42 -0
  522. package/src/prompt/markdown-prose.ts +245 -0
  523. package/src/prompt/model-mention-autocomplete.ts +94 -0
  524. package/src/prompt/model-mention-syntax.ts +41 -0
  525. package/src/prompt/orchestrate.ts +26 -0
  526. package/src/prompt/prompt-action-autocomplete.ts +337 -0
  527. package/src/prompt/queue-input.ts +132 -0
  528. package/src/prompt/skill-tokens.ts +36 -0
  529. package/src/prompt/tips.txt +27 -0
  530. package/src/prompt/tools-markdown.ts +29 -0
  531. package/src/prompt/ultrathink.ts +26 -0
  532. package/src/prompt/usage-amounts.ts +99 -0
  533. package/src/prompt/video.ts +47 -0
  534. package/src/prompt/welcome.ts +605 -0
  535. package/src/prompt/workflow.ts +26 -0
  536. package/src/render/code-cell.ts +244 -0
  537. package/src/render/file-list.ts +58 -0
  538. package/src/render/hyperlink.ts +192 -0
  539. package/src/render/index.ts +17 -0
  540. package/src/render/output-block.ts +267 -0
  541. package/src/render/output-pane.ts +299 -0
  542. package/src/render/render-utils.ts +1182 -0
  543. package/src/render/sixel.ts +69 -0
  544. package/src/render/status-line.ts +55 -0
  545. package/src/render/tool-card.ts +272 -0
  546. package/src/render/tree-list.ts +188 -0
  547. package/src/render/types.ts +19 -0
  548. package/src/render/utils.ts +109 -0
  549. package/src/render/width-aware-text.ts +60 -0
  550. package/src/setup/lazy.ts +17 -0
  551. package/src/setup/scenes/composer.ts +127 -0
  552. package/src/setup/scenes/glyph.ts +120 -0
  553. package/src/setup/scenes/model.ts +138 -0
  554. package/src/setup/scenes/outro.ts +24 -0
  555. package/src/setup/scenes/providers.ts +107 -0
  556. package/src/setup/scenes/sign-in.ts +373 -0
  557. package/src/setup/scenes/splash.ts +192 -0
  558. package/src/setup/scenes/theme.ts +343 -0
  559. package/src/setup/scenes/types.ts +111 -0
  560. package/src/setup/scenes/web-search.ts +165 -0
  561. package/src/setup/setup-version.ts +11 -0
  562. package/src/setup/startup-splash.ts +107 -0
  563. package/src/setup/wizard-overlay.ts +319 -0
  564. package/src/setup/wizard.ts +101 -0
  565. package/src/status-line/component.ts +3003 -0
  566. package/src/status-line/context-usage.ts +688 -0
  567. package/src/status-line/footer.ts +323 -0
  568. package/src/status-line/git-utils.ts +42 -0
  569. package/src/status-line/host.ts +97 -0
  570. package/src/status-line/index.ts +6 -0
  571. package/src/status-line/loop.ts +28 -0
  572. package/src/status-line/metrics.ts +94 -0
  573. package/src/status-line/presets.ts +107 -0
  574. package/src/status-line/schema.ts +57 -0
  575. package/src/status-line/segments.ts +934 -0
  576. package/src/status-line/separators.ts +55 -0
  577. package/src/status-line/types.ts +227 -0
  578. package/src/stdin-buffer.ts +4 -2
  579. package/src/theme/color.ts +95 -0
  580. package/src/theme/dark.json +95 -0
  581. package/src/theme/defaults/alabaster.json +93 -0
  582. package/src/theme/defaults/amethyst.json +96 -0
  583. package/src/theme/defaults/anthracite.json +93 -0
  584. package/src/theme/defaults/basalt.json +91 -0
  585. package/src/theme/defaults/birch.json +95 -0
  586. package/src/theme/defaults/dark-abyss.json +91 -0
  587. package/src/theme/defaults/dark-arctic.json +104 -0
  588. package/src/theme/defaults/dark-aurora.json +95 -0
  589. package/src/theme/defaults/dark-catppuccin.json +107 -0
  590. package/src/theme/defaults/dark-cavern.json +91 -0
  591. package/src/theme/defaults/dark-celestial.json +106 -0
  592. package/src/theme/defaults/dark-copper.json +95 -0
  593. package/src/theme/defaults/dark-cosmos.json +90 -0
  594. package/src/theme/defaults/dark-cyberpunk.json +102 -0
  595. package/src/theme/defaults/dark-dracula.json +98 -0
  596. package/src/theme/defaults/dark-eclipse.json +91 -0
  597. package/src/theme/defaults/dark-ember.json +95 -0
  598. package/src/theme/defaults/dark-equinox.json +90 -0
  599. package/src/theme/defaults/dark-forest.json +96 -0
  600. package/src/theme/defaults/dark-github.json +105 -0
  601. package/src/theme/defaults/dark-gruvbox.json +112 -0
  602. package/src/theme/defaults/dark-lavender.json +95 -0
  603. package/src/theme/defaults/dark-lunar.json +89 -0
  604. package/src/theme/defaults/dark-midnight.json +95 -0
  605. package/src/theme/defaults/dark-monochrome.json +94 -0
  606. package/src/theme/defaults/dark-monokai.json +98 -0
  607. package/src/theme/defaults/dark-nebula.json +90 -0
  608. package/src/theme/defaults/dark-nord.json +98 -0
  609. package/src/theme/defaults/dark-ocean.json +101 -0
  610. package/src/theme/defaults/dark-one.json +100 -0
  611. package/src/theme/defaults/dark-poimandres.json +115 -0
  612. package/src/theme/defaults/dark-rainforest.json +91 -0
  613. package/src/theme/defaults/dark-reef.json +91 -0
  614. package/src/theme/defaults/dark-retro.json +92 -0
  615. package/src/theme/defaults/dark-rose-pine.json +96 -0
  616. package/src/theme/defaults/dark-sakura.json +95 -0
  617. package/src/theme/defaults/dark-slate.json +95 -0
  618. package/src/theme/defaults/dark-solarized.json +97 -0
  619. package/src/theme/defaults/dark-solstice.json +90 -0
  620. package/src/theme/defaults/dark-starfall.json +91 -0
  621. package/src/theme/defaults/dark-sunset.json +99 -0
  622. package/src/theme/defaults/dark-swamp.json +90 -0
  623. package/src/theme/defaults/dark-synthwave.json +103 -0
  624. package/src/theme/defaults/dark-taiga.json +91 -0
  625. package/src/theme/defaults/dark-terminal.json +95 -0
  626. package/src/theme/defaults/dark-tokyo-night.json +101 -0
  627. package/src/theme/defaults/dark-tundra.json +91 -0
  628. package/src/theme/defaults/dark-twilight.json +91 -0
  629. package/src/theme/defaults/dark-volcanic.json +91 -0
  630. package/src/theme/defaults/graphite.json +92 -0
  631. package/src/theme/defaults/index.ts +201 -0
  632. package/src/theme/defaults/light-arctic.json +107 -0
  633. package/src/theme/defaults/light-aurora-day.json +91 -0
  634. package/src/theme/defaults/light-canyon.json +91 -0
  635. package/src/theme/defaults/light-catppuccin.json +106 -0
  636. package/src/theme/defaults/light-cirrus.json +90 -0
  637. package/src/theme/defaults/light-coral.json +95 -0
  638. package/src/theme/defaults/light-cyberpunk.json +96 -0
  639. package/src/theme/defaults/light-dawn.json +90 -0
  640. package/src/theme/defaults/light-dunes.json +91 -0
  641. package/src/theme/defaults/light-eucalyptus.json +95 -0
  642. package/src/theme/defaults/light-forest.json +100 -0
  643. package/src/theme/defaults/light-frost.json +95 -0
  644. package/src/theme/defaults/light-github.json +115 -0
  645. package/src/theme/defaults/light-glacier.json +91 -0
  646. package/src/theme/defaults/light-gruvbox.json +108 -0
  647. package/src/theme/defaults/light-haze.json +90 -0
  648. package/src/theme/defaults/light-honeycomb.json +95 -0
  649. package/src/theme/defaults/light-lagoon.json +91 -0
  650. package/src/theme/defaults/light-lavender.json +95 -0
  651. package/src/theme/defaults/light-meadow.json +91 -0
  652. package/src/theme/defaults/light-mint.json +95 -0
  653. package/src/theme/defaults/light-monochrome.json +101 -0
  654. package/src/theme/defaults/light-ocean.json +99 -0
  655. package/src/theme/defaults/light-one.json +99 -0
  656. package/src/theme/defaults/light-opal.json +91 -0
  657. package/src/theme/defaults/light-orchard.json +91 -0
  658. package/src/theme/defaults/light-paper.json +95 -0
  659. package/src/theme/defaults/light-poimandres.json +115 -0
  660. package/src/theme/defaults/light-prism.json +90 -0
  661. package/src/theme/defaults/light-retro.json +98 -0
  662. package/src/theme/defaults/light-sand.json +95 -0
  663. package/src/theme/defaults/light-savanna.json +91 -0
  664. package/src/theme/defaults/light-solarized.json +102 -0
  665. package/src/theme/defaults/light-soleil.json +90 -0
  666. package/src/theme/defaults/light-sunset.json +99 -0
  667. package/src/theme/defaults/light-synthwave.json +98 -0
  668. package/src/theme/defaults/light-tokyo-night.json +111 -0
  669. package/src/theme/defaults/light-wetland.json +91 -0
  670. package/src/theme/defaults/light-zenith.json +89 -0
  671. package/src/theme/defaults/limestone.json +94 -0
  672. package/src/theme/defaults/mahogany.json +97 -0
  673. package/src/theme/defaults/marble.json +93 -0
  674. package/src/theme/defaults/obsidian.json +91 -0
  675. package/src/theme/defaults/onyx.json +91 -0
  676. package/src/theme/defaults/pearl.json +93 -0
  677. package/src/theme/defaults/porcelain.json +91 -0
  678. package/src/theme/defaults/quartz.json +96 -0
  679. package/src/theme/defaults/sandstone.json +95 -0
  680. package/src/theme/defaults/titanium.json +90 -0
  681. package/src/theme/index.ts +5 -0
  682. package/src/theme/light.json +93 -0
  683. package/src/theme/loader.ts +200 -0
  684. package/src/theme/mermaid-cache.ts +92 -0
  685. package/src/theme/schema-validation.ts +119 -0
  686. package/src/theme/schema.ts +168 -0
  687. package/src/theme/session-color.ts +253 -0
  688. package/src/theme/shimmer.ts +307 -0
  689. package/src/theme/symbols.ts +1433 -0
  690. package/src/theme/theme-class.ts +787 -0
  691. package/src/theme/theme-schema.json +463 -0
  692. package/src/theme/theme.ts +810 -0
  693. package/src/theme/tui-adapters.ts +339 -0
  694. package/src/thinking.ts +380 -0
  695. package/src/tools/agent-tree.ts +128 -0
  696. package/src/tools/ask.ts +482 -0
  697. package/src/tools/ast-edit.ts +210 -0
  698. package/src/tools/ast-grep.ts +171 -0
  699. package/src/tools/autoresearch.ts +287 -0
  700. package/src/tools/bash-interactive.ts +295 -0
  701. package/src/tools/bash.ts +468 -0
  702. package/src/tools/conflict-detect.ts +77 -0
  703. package/src/tools/debug.ts +167 -0
  704. package/src/tools/default-renderer.ts +172 -0
  705. package/src/tools/edit.ts +1258 -0
  706. package/src/tools/eval-format/code-mode-declarations.ts +61 -0
  707. package/src/tools/eval-format/index.ts +16 -0
  708. package/src/tools/eval-format/javascript.ts +952 -0
  709. package/src/tools/eval-format/python.ts +544 -0
  710. package/src/tools/eval.ts +835 -0
  711. package/src/tools/fetch.ts +226 -0
  712. package/src/tools/gh-format.ts +18 -0
  713. package/src/tools/github.ts +686 -0
  714. package/src/tools/glob.ts +213 -0
  715. package/src/tools/goal.ts +153 -0
  716. package/src/tools/grep.ts +385 -0
  717. package/src/tools/grouped-file-output.ts +211 -0
  718. package/src/tools/hashline-format.ts +59 -0
  719. package/src/tools/hub.ts +1488 -0
  720. package/src/tools/index.ts +71 -0
  721. package/src/tools/json-tree.ts +388 -0
  722. package/src/tools/line-ranges.ts +98 -0
  723. package/src/tools/list-limit.ts +43 -0
  724. package/src/tools/lsp.ts +702 -0
  725. package/src/tools/match-line-format.ts +20 -0
  726. package/src/tools/mcp.ts +309 -0
  727. package/src/tools/memory.ts +220 -0
  728. package/src/tools/output-meta.ts +367 -0
  729. package/src/tools/read.ts +628 -0
  730. package/src/tools/renderer.ts +107 -0
  731. package/src/tools/report-tool-issue.ts +13 -0
  732. package/src/tools/resolve.ts +139 -0
  733. package/src/tools/streaming-output.ts +1576 -0
  734. package/src/tools/subprocess.ts +39 -0
  735. package/src/tools/task-repair-args.ts +118 -0
  736. package/src/tools/task-yield-assembly.ts +147 -0
  737. package/src/tools/task.ts +2110 -0
  738. package/src/tools/terminal-output.ts +153 -0
  739. package/src/tools/think.ts +28 -0
  740. package/src/tools/todo.ts +580 -0
  741. package/src/tools/tool-errors.ts +15 -0
  742. package/src/tools/vibe.ts +420 -0
  743. package/src/tools/web-search.ts +409 -0
  744. package/src/tools/write.ts +511 -0
  745. package/src/tools/xd-url.ts +28 -0
  746. package/src/tools/xdev.ts +201 -0
  747. package/src/tui.ts +57 -4
  748. package/src/utils.ts +8 -0
@@ -0,0 +1,1392 @@
1
+ import * as url from "node:url";
2
+ import type { ImageContent } from "@oh-my-pi/pi-ai";
3
+ import { BracketedPasteHandler } from "../bracketed-paste";
4
+ import { BRACKETED_PASTE_END, BRACKETED_PASTE_START } from "../stdin-buffer";
5
+ import { Editor, type EditorTextDecorationContext, type EditorTheme } from "../components/editor";
6
+ import { addKeyAliases, canonicalKeyId, getKeybindings } from "../keybindings";
7
+ import { type KeyId, parseKey, parseKittySequence } from "../keys";
8
+ import { TUI } from "../tui";
9
+ import type { AppKeybinding } from "../app-keybindings";
10
+ import { allowsModelMentions, allowsSkillTokens, SKILL_TOKEN_RE } from "./skill-tokens";
11
+ import { expandModelMentionTags, MODEL_MENTION_RE, modelMentionToken } from "./model-mention-syntax";
12
+ import { isVideoPath, videoPreviewSource } from "./video";
13
+ import {
14
+ attachmentSgr,
15
+ COMPOSER_TOKEN_REGEX,
16
+ chipLabel,
17
+ collapseImageMarkers,
18
+ collapseModelMentions,
19
+ collapseSkillTokens,
20
+ composerTokenRegex,
21
+ modelChipStyle,
22
+ renderPlaceholders,
23
+ skillChipLabel,
24
+ skillChipStyle,
25
+ skillToken,
26
+ } from "./composer-attachments";
27
+ import { MacOSSpellingProvider, type SpellingFeatures } from "./macos-spelling";
28
+ import { hasMagicKeyword, highlightMagicKeywords } from "./magic-keywords";
29
+ import { isQueuedMessageList, parseQueueShorthand, QUEUE_LIST_MARKER_RE } from "./queue-input";
30
+ import { fgOrPlain, theme } from "../theme/theme";
31
+
32
+ type ConfigurableEditorAction = Extract<
33
+ AppKeybinding,
34
+ | "app.interrupt"
35
+ | "app.clear"
36
+ | "app.exit"
37
+ | "app.suspend"
38
+ | "app.display.reset"
39
+ | "app.thinking.cycle"
40
+ | "app.model.cycleForward"
41
+ | "app.model.cycleBackward"
42
+ | "app.model.select"
43
+ | "app.model.selectTemporary"
44
+ | "app.message.dequeue"
45
+ | "app.retry"
46
+ | "app.clipboard.pasteImage"
47
+ | "app.clipboard.pasteTextRaw"
48
+ | "app.clipboard.copyPrompt"
49
+ >;
50
+
51
+ const DEFAULT_ACTION_KEYS: Record<ConfigurableEditorAction, KeyId[]> = {
52
+ "app.interrupt": ["escape"],
53
+ "app.clear": ["ctrl+c"],
54
+ "app.exit": ["ctrl+d"],
55
+ "app.suspend": ["ctrl+z"],
56
+ "app.display.reset": ["alt+l"],
57
+ "app.thinking.cycle": ["shift+tab"],
58
+ "app.model.cycleForward": ["ctrl+p"],
59
+ "app.model.cycleBackward": ["shift+ctrl+p"],
60
+ "app.model.select": ["alt+m"],
61
+ "app.model.selectTemporary": ["alt+p"],
62
+ "app.message.dequeue": ["alt+up", "shift+up"],
63
+ "app.retry": ["f5", "alt+r"],
64
+ "app.clipboard.pasteImage": ["ctrl+v"],
65
+ "app.clipboard.pasteTextRaw": ["ctrl+shift+v", "alt+shift+v"],
66
+ "app.clipboard.copyPrompt": ["alt+shift+c"],
67
+ };
68
+
69
+ function buildMatchKeys(keys: readonly KeyId[]): Set<string> {
70
+ const matchKeys = new Set<string>();
71
+ for (const key of keys) {
72
+ addKeyAliases(matchKeys, key);
73
+ }
74
+ return matchKeys;
75
+ }
76
+
77
+ function unionOfMatchKeys(matchKeys: ReadonlyMap<ConfigurableEditorAction, ReadonlySet<string>>): Set<string> {
78
+ const union = new Set<string>();
79
+ for (const keys of matchKeys.values()) {
80
+ for (const key of keys) union.add(key);
81
+ }
82
+ return union;
83
+ }
84
+
85
+ const BRACKETED_IMAGE_PATH_REGEX = /\.(?:png|jpe?g|gif|webp)$/i;
86
+ const SHELL_ESCAPED_PATH_CHAR_REGEX = /\\([\\\s'"()[\]{}&;<>|?*!$`])/g;
87
+ const URI_SCHEME_REGEX = /^[a-z][a-z0-9+.-]*:/i;
88
+ const FILE_URI_REGEX = /^file:\/\//i;
89
+ /**
90
+ * Alternation of the filesystem prefixes that make a path unambiguously
91
+ * absolute (POSIX root, home, `file://`, UNC, Windows drive). Shared by
92
+ * {@link ABSOLUTE_PATH_PREFIX_REGEX} and {@link INTERIOR_PATH_ANCHOR_REGEX} so
93
+ * the leading-anchor test and the second-anchor test can never disagree about
94
+ * what counts as the start of a path.
95
+ */
96
+ const ABSOLUTE_PATH_PREFIX_SOURCE = String.raw`(?:\/|~\/|file:\/\/|\\\\|[A-Za-z]:[\\/])`;
97
+ /**
98
+ * Whole-string anchor for paths that are unambiguously absolute. Restricts the
99
+ * "treat the entire text as one path" pass of {@link extractWholeTextAttachmentPath}
100
+ * to inputs that start with a clearly-anchored filesystem prefix, so prose
101
+ * containing a path-shaped fragment (e.g. "see /tmp/x.png") never hijacks the
102
+ * smart fallback.
103
+ */
104
+ const ABSOLUTE_PATH_PREFIX_REGEX = new RegExp(`^${ABSOLUTE_PATH_PREFIX_SOURCE}`);
105
+ /**
106
+ * A second path anchor after *unescaped* whitespace — the signature of a
107
+ * multi-path payload (`/tmp/a.png /tmp/b shot.png`, `/tmp/a.png ./b shot.png`)
108
+ * rather than of one path whose name merely contains spaces. Anchors are the
109
+ * absolute prefixes plus dot-relative starts (`./`, `../`, `.\`), which never
110
+ * begin a component of a single sane path. Bare relatives (`dir/b shot.png`)
111
+ * are deliberately NOT anchors: an interior `token/` after a space is exactly
112
+ * the shape of a single path with a spaced directory name
113
+ * (`/Users/me/My Photos/shot 1.png`), which this fallback exists to recover.
114
+ * Escaped whitespace (`/tmp/My\ Photos/x.png`) is exempt: the escape is the
115
+ * terminal asserting the space belongs to the path.
116
+ */
117
+ const INTERIOR_PATH_ANCHOR_REGEX = new RegExp(String.raw`(?<!\\)\s(?:${ABSOLUTE_PATH_PREFIX_SOURCE}|\.\.?[\\/])`);
118
+
119
+ /** Max gap (ms) between two spaces for the later one to count as OS key auto-repeat rather than a
120
+ * deliberate press. OS auto-repeat is fast; a deliberate tap (even a fast one) is slower. */
121
+ export const SPACE_REPEAT_MAX_GAP_MS = 120;
122
+ /** Two consecutive inter-space gaps are "mechanical" (machine-driven auto-repeat) when both are
123
+ * within {@link SPACE_REPEAT_MAX_GAP_MS} and differ by no more than this — an absolute jitter floor
124
+ * or, for slower repeat rates, {@link SPACE_REPEAT_JITTER_RATIO} of the smaller gap. OS key-repeat
125
+ * is metronomic; a human smashing the bar is fast but irregular, so its deltas never stay this
126
+ * steady. */
127
+ export const SPACE_REPEAT_JITTER_MS = 18;
128
+ export const SPACE_REPEAT_JITTER_RATIO = 0.35;
129
+ /** Consecutive mechanical (fast + steady) deltas that confirm the space bar is held and start
130
+ * recording. Needs a sustained metronomic cadence, so jittery smashing and deliberate taps never
131
+ * reach it. */
132
+ export const SPACE_HOLD_MECHANICAL_RUN = 2;
133
+ /** Idle gap (ms) after the last repeated space that counts as the space bar being released, ending
134
+ * the push-to-talk recording. Must comfortably exceed the OS key-repeat interval. */
135
+ export const SPACE_HOLD_RELEASE_MS = 250;
136
+
137
+ /** Whether two consecutive inter-space gaps look machine-driven: both within the auto-repeat band
138
+ * and steady enough (small absolute or proportional difference). OS key-repeat is metronomic, so
139
+ * its successive deltas match closely; human smashing is fast but irregular and deliberate taps are
140
+ * too slow, so neither passes. */
141
+ function gapsAreMechanical(gap: number, prevGap: number): boolean {
142
+ if (gap > SPACE_REPEAT_MAX_GAP_MS || prevGap > SPACE_REPEAT_MAX_GAP_MS) return false;
143
+ const tolerance = Math.max(SPACE_REPEAT_JITTER_MS, Math.min(gap, prevGap) * SPACE_REPEAT_JITTER_RATIO);
144
+ return Math.abs(gap - prevGap) <= tolerance;
145
+ }
146
+
147
+ function isPastedPathSeparator(char: string | undefined): boolean {
148
+ return char === undefined || char === " " || char === "\t" || char === "\r" || char === "\n";
149
+ }
150
+
151
+ function normalizePastedPath(path: string): string {
152
+ const trimmed = path.trim();
153
+ const first = trimmed[0];
154
+ const last = trimmed[trimmed.length - 1];
155
+ const unquoted =
156
+ trimmed.length > 1 && (first === '"' || first === "'") && last === first ? trimmed.slice(1, -1) : trimmed;
157
+ // `file://` URL → local filesystem path. Mirrors Codex's
158
+ // `normalize_pasted_path` (codex-rs/tui/src/clipboard_paste.rs) so a
159
+ // pasteboard whose text representation is a `file:///Users/…/img.png`
160
+ // URL — common when terminals forward the macOS pasteboard's
161
+ // `public.file-url` representation — loads as the file itself rather
162
+ // than failing in `loadImageInput` with a literal-`file://` path.
163
+ if (FILE_URI_REGEX.test(unquoted)) {
164
+ try {
165
+ return url.fileURLToPath(unquoted);
166
+ } catch {
167
+ // Malformed file URL: drop through to the shell-unescape branch
168
+ // so the caller can still reject it as a non-explicit path.
169
+ }
170
+ }
171
+ return unquoted.replace(SHELL_ESCAPED_PATH_CHAR_REGEX, "$1");
172
+ }
173
+
174
+ function isExplicitPastedPath(path: string): boolean {
175
+ if (ABSOLUTE_PATH_PREFIX_REGEX.test(path) || /^\.\.?[\\/]/.test(path)) return true;
176
+ if (URI_SCHEME_REGEX.test(path)) return false;
177
+ return path.includes("\\");
178
+ }
179
+
180
+ /** A pasted local image or video can become a vision-ready image attachment. */
181
+ function isPreviewableAttachmentPath(path: string): boolean {
182
+ return BRACKETED_IMAGE_PATH_REGEX.test(path) || isVideoPath(path);
183
+ }
184
+
185
+ function splitPastedPathSegments(payload: string): string[] | undefined {
186
+ const segments: string[] = [];
187
+ let segment = "";
188
+ let quote: string | undefined;
189
+ let escaped = false;
190
+
191
+ for (let i = 0; i < payload.length; i++) {
192
+ const char = payload[i];
193
+ if (escaped) {
194
+ segment += char;
195
+ escaped = false;
196
+ continue;
197
+ }
198
+ if (char === "\\") {
199
+ segment += char;
200
+ escaped = true;
201
+ continue;
202
+ }
203
+ if (quote) {
204
+ segment += char;
205
+ if (char === quote) quote = undefined;
206
+ continue;
207
+ }
208
+ if (char === '"' || char === "'") {
209
+ segment += char;
210
+ quote = char;
211
+ continue;
212
+ }
213
+ if (isPastedPathSeparator(char)) {
214
+ if (segment) {
215
+ segments.push(segment);
216
+ segment = "";
217
+ }
218
+ continue;
219
+ }
220
+ segment += char;
221
+ }
222
+
223
+ if (escaped || quote) return undefined;
224
+ if (segment) segments.push(segment);
225
+ return segments.length > 0 ? segments : undefined;
226
+ }
227
+
228
+ /**
229
+ * Extract whitespace/quoted-separated path-like segments from `payload`.
230
+ * Shared backend of {@link extractBracketedPastePaths} and {@link extractPastePathsFromText}.
231
+ * Returns the segments only when EVERY segment looks like an anchored local
232
+ * path or uses Windows separators; otherwise undefined so ambiguous relative
233
+ * URL/path text falls back to a plain text paste.
234
+ */
235
+ function extractExplicitPathSegments(payload: string): string[] | undefined {
236
+ const pasted = payload.trim();
237
+ if (!pasted) return undefined;
238
+
239
+ const segments = splitPastedPathSegments(pasted);
240
+ if (!segments) return undefined;
241
+
242
+ const paths: string[] = [];
243
+ for (const segment of segments) {
244
+ const path = normalizePastedPath(segment);
245
+ if (!path || !isExplicitPastedPath(path)) return undefined;
246
+ paths.push(path);
247
+ }
248
+ return paths;
249
+ }
250
+
251
+ /**
252
+ * Extract image-or-other file paths from plain (un-bracketed) clipboard text.
253
+ * Mirrors {@link extractBracketedPastePaths} for terminals/handlers that
254
+ * already stripped the `\x1b[200~`…`\x1b[201~` markers (e.g. clipboard text
255
+ * read directly via `pbpaste`/PowerShell).
256
+ */
257
+ export function extractPastePathsFromText(text: string): string[] | undefined {
258
+ return extractExplicitPathSegments(text);
259
+ }
260
+
261
+ /**
262
+ * Whole-text-as-path pass shared by {@link extractImagePastePathsFromText}
263
+ * and {@link extractImagePathFromText}: treat the entire text as one path
264
+ * when it is anchored by {@link ABSOLUTE_PATH_PREFIX_REGEX}, contains no
265
+ * newlines, and points at a vision-previewable image or video extension. Recovers single paths
266
+ * whose unescaped spaces defeat the segment splitter (macOS screenshot names).
267
+ *
268
+ * Refuses payloads carrying a second {@link INTERIOR_PATH_ANCHOR_REGEX} anchor.
269
+ * Dragging two files at once emits `/tmp/a.png /tmp/b shot.png`, which the
270
+ * splitter also refuses (`shot.png` is not explicit); swallowing it as one path
271
+ * attaches nothing, and `handleImagePathPaste`'s ENOENT branch only surfaces a
272
+ * status — unlike its other failure branches it never re-pastes the text — so
273
+ * both paths would vanish. Genuinely ambiguous input lands here too (a
274
+ * directory whose name ends in a space, as in `/tmp/odd dir /sub/x.png`); a
275
+ * plain text paste is the losing-nothing outcome, so ambiguity resolves that way.
276
+ */
277
+ function extractWholeTextAttachmentPath(text: string): string | undefined {
278
+ const trimmed = text.trim();
279
+ if (!trimmed || /[\r\n]/.test(trimmed) || !ABSOLUTE_PATH_PREFIX_REGEX.test(trimmed)) return undefined;
280
+ if (INTERIOR_PATH_ANCHOR_REGEX.test(trimmed)) return undefined;
281
+ const wholePath = normalizePastedPath(trimmed);
282
+ return wholePath && isExplicitPastedPath(wholePath) && isPreviewableAttachmentPath(wholePath)
283
+ ? wholePath
284
+ : undefined;
285
+ }
286
+
287
+ /**
288
+ * Same shape as {@link extractBracketedImagePastePaths} but operates on a
289
+ * payload that has already been stripped of the `\x1b[200~` / `\x1b[201~`
290
+ * markers — used by the assembled-paste router in {@link CustomEditor.handleInput}
291
+ * so split bracketed pastes get the same attachment-path detection as single-chunk ones.
292
+ *
293
+ * When the segment splitter fails (an unescaped space in a real path breaks
294
+ * its every-segment-is-a-path invariant), falls back to
295
+ * {@link extractWholeTextAttachmentPath}, so a dropped macOS screenshot
296
+ * (`Screenshot 2026-06-25 at 1.23.45 PM.png`) attaches as an image instead of
297
+ * degrading to literal text (#6578).
298
+ */
299
+ export function extractImagePastePathsFromText(text: string): string[] | undefined {
300
+ const paths = extractPastePathsFromText(text);
301
+ if (paths !== undefined) return paths.every(isPreviewableAttachmentPath) ? paths : undefined;
302
+ const wholePath = extractWholeTextAttachmentPath(text);
303
+ return wholePath ? [wholePath] : undefined;
304
+ }
305
+
306
+ function bracketedPastePayload(data: string): string | undefined {
307
+ if (!data.startsWith(BRACKETED_PASTE_START)) return undefined;
308
+ const endIndex = data.indexOf(BRACKETED_PASTE_END, BRACKETED_PASTE_START.length);
309
+ if (endIndex === -1 || endIndex + BRACKETED_PASTE_END.length !== data.length) return undefined;
310
+ return data.slice(BRACKETED_PASTE_START.length, endIndex);
311
+ }
312
+
313
+ export function extractBracketedPastePaths(data: string): string[] | undefined {
314
+ const payload = bracketedPastePayload(data);
315
+ return payload === undefined ? undefined : extractExplicitPathSegments(payload);
316
+ }
317
+
318
+ export function extractBracketedImagePastePaths(data: string): string[] | undefined {
319
+ const payload = bracketedPastePayload(data);
320
+ return payload === undefined ? undefined : extractImagePastePathsFromText(payload);
321
+ }
322
+
323
+ export function extractBracketedImagePastePath(data: string): string | undefined {
324
+ const paths = extractBracketedImagePastePaths(data);
325
+ return paths?.length === 1 ? paths[0] : undefined;
326
+ }
327
+
328
+ /**
329
+ * Return a single previewable file path when `text` is exactly one explicit
330
+ * image (`.png`, `.jpg`/`.jpeg`, `.gif`, `.webp`) or video path. Used by the
331
+ * keybind-driven clipboard image paste path so a
332
+ * clipboard whose only payload is an image file (e.g. Finder `Cmd+C` on
333
+ * macOS) attaches the image instead of pasting the path as literal text.
334
+ *
335
+ * Two-stage detection:
336
+ *
337
+ * 1. Splitter pass (shared with the bracketed-paste handler) — handles
338
+ * quoted paths, shell-escaped spaces, and unambiguous single tokens.
339
+ * Returns the single previewable path when it parses cleanly; explicitly
340
+ * returns `undefined` when the splitter found multiple segments (so
341
+ * ambiguous multi-path clipboard text like `/tmp/a.png /tmp/b.png`
342
+ * still falls through to the text fallback instead of being mis-loaded
343
+ * as one giant path).
344
+ * 2. {@link extractWholeTextAttachmentPath} — only reached when the splitter
345
+ * failed (every segment must look like an explicit path; an unescaped
346
+ * space in a real path breaks that). This is what recovers macOS
347
+ * screenshot filenames like
348
+ * `/Users/me/Desktop/Screenshot 2026-06-25 at 1.23.45 PM.png`.
349
+ */
350
+ export function extractImagePathFromText(text: string): string | undefined {
351
+ const paths = extractPastePathsFromText(text);
352
+ if (paths?.length === 1 && isPreviewableAttachmentPath(paths[0])) return paths[0];
353
+ if (paths !== undefined) return undefined;
354
+ return extractWholeTextAttachmentPath(text);
355
+ }
356
+
357
+ /**
358
+ * Resolve the {@link EditorTheme} from a `CustomEditor`/`Editor` constructor
359
+ * argument list, tolerating both the omp `(theme)` and upstream-pi
360
+ * `(tui, theme, keybindings)` conventions (see {@link CustomEditor}'s
361
+ * constructor). A real `EditorTheme` is identified structurally — it exposes a
362
+ * `borderColor` function and a `symbols` object — so a `TUI` passed in the first
363
+ * slot is skipped rather than mistaken for the theme.
364
+ */
365
+ function pickEditorTheme(args: readonly unknown[]): EditorTheme {
366
+ for (const arg of args) {
367
+ if (isEditorTheme(arg)) return arg;
368
+ }
369
+ // Fall back to the first argument so a caller passing a bare theme that
370
+ // somehow fails the shape probe still reaches the base constructor.
371
+ return args[0] as EditorTheme;
372
+ }
373
+
374
+ function isEditorTheme(value: unknown): value is EditorTheme {
375
+ if (typeof value !== "object" || value === null) return false;
376
+ const candidate = value as Partial<EditorTheme>;
377
+ return (
378
+ typeof candidate.borderColor === "function" && typeof candidate.symbols === "object" && candidate.symbols !== null
379
+ );
380
+ }
381
+
382
+ /** A large text paste staged as a composer chip. `content` feeds the band card's snippet and
383
+ * captions; the submit-time expansion (verbatim content or a wrapped block) lives in the
384
+ * editor's atom table under `label`. */
385
+ export interface TextAttachment {
386
+ n: number;
387
+ label: string;
388
+ content: string;
389
+ lineCount: number;
390
+ charCount: number;
391
+ }
392
+
393
+ /** One visible composer attachment, in band order (vision attachments first, then text pastes). */
394
+ export type ComposerChipDescriptor =
395
+ | { kind: "image" | "video"; n: number; image: ImageContent; link: string | undefined }
396
+ | { kind: "paste"; n: number; text: TextAttachment };
397
+
398
+ /**
399
+ * Custom editor that handles configurable app-level shortcuts for coding-agent.
400
+ */
401
+ export class CustomEditor extends Editor {
402
+ #spelling = new MacOSSpellingProvider();
403
+ imageLinks?: readonly (string | undefined)[];
404
+
405
+ /** Draft images pasted into the composer, consumed on submit. Co-located with
406
+ * {@link imageLinks} so every piece of draft-image state lives on the editor. */
407
+ pendingImages: ImageContent[] = [];
408
+ /** Per-image source links (file:// targets) parallel to {@link pendingImages};
409
+ * `undefined` entries are images without a backing reference yet. */
410
+ pendingImageLinks: (string | undefined)[] = [];
411
+ /** Large text pastes staged as compact chip tokens; expansion lives in the atom table.
412
+ * Numbered by a per-draft monotonic counter so a deleted chip never recycles its number
413
+ * (labels key the atom table). */
414
+ pendingTexts: TextAttachment[] = [];
415
+ #textAttachmentCounter = 0;
416
+ #composerChipsCache:
417
+ | {
418
+ textRevision: number;
419
+ images: ImageContent[];
420
+ imageCount: number;
421
+ imageLinks: (string | undefined)[];
422
+ imageLinkCount: number;
423
+ texts: TextAttachment[];
424
+ textCount: number;
425
+ chips: ComposerChipDescriptor[];
426
+ }
427
+ | undefined;
428
+ /** Host-wired producer of per-image `file://` links (session blob store); drives clickable
429
+ * chip tokens for restored drafts (esc-esc, `/tree`, branch). */
430
+ draftImageLinkMaterializer?: (images: readonly ImageContent[]) => Promise<(string | undefined)[] | undefined>;
431
+
432
+ /**
433
+ * The host {@link TUI}, captured when a plugin constructs this editor through
434
+ * the upstream-pi `(tui, theme, keybindings)` convention. Undefined for omp's
435
+ * own `new CustomEditor(theme)` callers (they drive repaints through the
436
+ * interactive-mode wiring instead). Plugins that call `this.tui.requestRender()`
437
+ * in their overrides read it here (issue #4766).
438
+ */
439
+ tui?: TUI;
440
+
441
+ /**
442
+ * Accept both the omp constructor convention — `new CustomEditor(theme)` —
443
+ * and the upstream-pi `Editor` convention — `new Editor(tui, theme, keybindings)`
444
+ * — that {@link ExtensionUIContext.setEditorComponent}'s factory contract
445
+ * advertises `(tui, theme, keybindings)`. Plugins written against upstream pi
446
+ * subclass `CustomEditor`/`Editor` and forward `super(tui, theme, keybindings)`;
447
+ * without this shim the `TUI` lands in the `theme` slot and every render throws
448
+ * `undefined is not an object (evaluating 'this.#theme.symbols.boxRound')`
449
+ * (issue #4766). We locate the real {@link EditorTheme} among the args by shape
450
+ * (it carries `symbols`/`borderColor`) rather than by position, and capture a
451
+ * leading {@link TUI} so plugin overrides calling `this.tui.requestRender()`
452
+ * keep working.
453
+ */
454
+ constructor(...args: readonly unknown[]) {
455
+ super(pickEditorTheme(args));
456
+ const requestTextAssistRepaint = (): void => {
457
+ this.invalidate();
458
+ this.#requestShimmerRepaint?.();
459
+ };
460
+ this.#spelling.onUpdate = requestTextAssistRepaint;
461
+ this.onTextAssistApplied = requestTextAssistRepaint;
462
+ this.setTextAssistProvider(this.#spelling);
463
+ if (args[0] instanceof TUI) this.tui = args[0];
464
+ }
465
+
466
+ /** Independently configure typo detection, word autocomplete, and autocorrect. */
467
+ setSpellingFeatures(features: SpellingFeatures): void {
468
+ this.#spelling.setFeatures(features);
469
+ }
470
+
471
+ /** Clear the composer draft: optionally commit `historyText` to history, then
472
+ * reset the editor text and all pending draft-image state. The shared tail of
473
+ * every "message submitted" path; pass no argument for a plain discard. */
474
+ clearDraft(historyText?: string): void {
475
+ if (historyText !== undefined) this.addToHistory(historyText);
476
+ this.setText("");
477
+ this.clearPasteState();
478
+ this.imageLinks = undefined;
479
+ this.pendingImages = [];
480
+ this.pendingImageLinks = [];
481
+ this.pendingTexts = [];
482
+ this.#textAttachmentCounter = 0;
483
+ }
484
+
485
+ /** Preserve a canceled draft in local navigation, then clear the composer. */
486
+ clearDraftForRecall(): void {
487
+ if (!this.getText().trim()) {
488
+ this.clearDraft();
489
+ return;
490
+ }
491
+ const images = [...this.pendingImages];
492
+ const links = [...this.pendingImageLinks];
493
+ const imageLinks = this.imageLinks;
494
+ const texts = [...this.pendingTexts];
495
+ const counter = this.#textAttachmentCounter;
496
+ this.rememberDraft(() => {
497
+ this.pendingImages = [...images];
498
+ this.pendingImageLinks = [...links];
499
+ this.imageLinks = imageLinks;
500
+ this.pendingTexts = [...texts];
501
+ this.#textAttachmentCounter = counter;
502
+ if (this.pendingImages.length > 0 && this.pendingImageLinks.some(link => link === undefined)) {
503
+ void this.#materializeDraftLinks();
504
+ }
505
+ });
506
+ this.clearDraft();
507
+ }
508
+
509
+ override restoreHistoryState(restore?: () => void): void {
510
+ this.imageLinks = undefined;
511
+ this.pendingImages = [];
512
+ this.pendingImageLinks = [];
513
+ this.pendingTexts = [];
514
+ this.#textAttachmentCounter = 0;
515
+ super.restoreHistoryState(restore);
516
+ }
517
+
518
+ /** Replace the composer draft with a restored historical prompt: re-attaches the message's
519
+ * images, collapses stored `[Image #N, WxH]` markers back into compact chip tokens (so the
520
+ * chips band and atomic deletion return), and re-materializes `file://` links so the tokens
521
+ * are clickable again instead of degrading to dead text (esc-esc branch, `/tree`). */
522
+ setDraft(text: string, images?: readonly ImageContent[]): void {
523
+ this.clearPasteState();
524
+ this.pendingTexts = [];
525
+ this.#textAttachmentCounter = 0;
526
+ this.imageLinks = undefined;
527
+ this.pendingImages = images ? [...images] : [];
528
+ this.pendingImageLinks = images ? images.map(() => undefined) : [];
529
+ this.setCollapsedText(text);
530
+ void this.#materializeDraftLinks();
531
+ }
532
+
533
+ /** Set restored text with image, skill, and model references collapsed into atomic chips.
534
+ * Leaves pending image/text state untouched — callers own that. */
535
+ setCollapsedText(text: string): void {
536
+ const register = (label: string, expansion: string) => this.registerAtom(label, expansion);
537
+ this.setText(
538
+ collapseModelMentions(
539
+ collapseSkillTokens(
540
+ collapseImageMarkers(
541
+ expandModelMentionTags(text, this.modelMentionSelector),
542
+ this.pendingImages.length,
543
+ register,
544
+ ),
545
+ name => this.skillFilePath(name) !== undefined,
546
+ register,
547
+ ),
548
+ selector => this.#mentionLabelFor(selector),
549
+ register,
550
+ ),
551
+ );
552
+ this.#syncComposerTokenPattern();
553
+ }
554
+
555
+ /**
556
+ * Host-owned skill registry probe: the SKILL.md path for a registered skill, else
557
+ * `undefined`. Only registered skills collapse into chips — an unknown `/skill:<name>`
558
+ * stays literal text — and the path makes the chip a clickable link. Startup defaults
559
+ * to "none known".
560
+ */
561
+ skillFilePath: (name: string) => string | undefined = () => undefined;
562
+
563
+ /** Host-owned model probe: maps a mentionable selector to its display chip label. */
564
+ modelMentionLabel: (selector: string) => string | undefined = () => undefined;
565
+
566
+ /** Host-owned session probe: maps a persisted model pseudonym back to its selector. */
567
+ modelMentionSelector: (agent: string) => string | undefined = () => undefined;
568
+
569
+ #mentionLabelFor(selector: string): string | undefined {
570
+ const label = this.modelMentionLabel(selector);
571
+ if (label === undefined) return undefined;
572
+ const expansion = modelMentionToken(selector);
573
+ const registered = this.atoms.get(label);
574
+ return registered !== undefined && registered !== expansion ? undefined : label;
575
+ }
576
+
577
+ /**
578
+ * Late-bound OSC 8 file link renderer. Startup stays plain until the full
579
+ * interactive graph supplies the settings-aware implementation.
580
+ */
581
+ fileHyperlink: (filePath: string, text: string) => string = (_filePath, text) => text;
582
+
583
+ /** Collapse every completed `/skill:<name>` token for a known skill into an atomic chip.
584
+ * A token is complete once whitespace follows it (autocomplete appends one; so does the
585
+ * user moving on), so a half-typed name never snaps early. */
586
+ #collapseSkillTokens(): void {
587
+ // Scan lines (no buffer join) so plain typing stays O(1) allocations per keystroke.
588
+ const lines = this.getLines();
589
+ if (!lines.some(line => line.includes("/skill:")) || !allowsSkillTokens(this.getText())) return;
590
+ for (let i = 0; i < lines.length; i++) {
591
+ let line = lines[i];
592
+ if (!line.includes("/skill:")) continue;
593
+ for (;;) {
594
+ SKILL_TOKEN_RE.lastIndex = 0;
595
+ let collapsed = false;
596
+ for (let match = SKILL_TOKEN_RE.exec(line); match !== null; match = SKILL_TOKEN_RE.exec(line)) {
597
+ const name = match[2];
598
+ const start = match.index + match[1].length;
599
+ const end = match.index + match[0].length;
600
+ if (end === line.length && i === lines.length - 1) break;
601
+ if (this.skillFilePath(name) === undefined) continue;
602
+ this.collapseToAtom(i, start, end, skillChipLabel(name), skillToken(name));
603
+ collapsed = true;
604
+ break;
605
+ }
606
+ if (!collapsed) break;
607
+ line = this.getLines()[i];
608
+ }
609
+ }
610
+ }
611
+
612
+ /** Collapse every completed mentionable `^provider/id` selector into an atomic model chip. */
613
+ #collapseModelMentions(): void {
614
+ const lines = this.getLines();
615
+ if (!lines.some(line => line.includes("^")) || !allowsModelMentions(this.getText())) return;
616
+ let anyCollapsed = false;
617
+ for (let i = 0; i < lines.length; i++) {
618
+ let line = lines[i];
619
+ if (!line.includes("^")) continue;
620
+ for (;;) {
621
+ MODEL_MENTION_RE.lastIndex = 0;
622
+ let collapsed = false;
623
+ for (let match = MODEL_MENTION_RE.exec(line); match !== null; match = MODEL_MENTION_RE.exec(line)) {
624
+ const selector = match[2];
625
+ const start = match.index + match[1].length;
626
+ const end = match.index + match[0].length;
627
+ if (end === line.length && i === lines.length - 1) break;
628
+ const label = this.#mentionLabelFor(selector);
629
+ if (label === undefined) continue;
630
+ this.collapseToAtom(i, start, end, label, modelMentionToken(selector));
631
+ collapsed = true;
632
+ anyCollapsed = true;
633
+ break;
634
+ }
635
+ if (!collapsed) break;
636
+ line = this.getLines()[i];
637
+ }
638
+ }
639
+ if (anyCollapsed) this.#syncComposerTokenPattern();
640
+ }
641
+
642
+ /** Stage `content` as a text-attachment chip: inserts the compact token at the cursor and
643
+ * registers `expansion` (default: the content itself) in the atom table for submit. */
644
+ insertTextAttachment(content: string, expansion: string = content): void {
645
+ this.#textAttachmentCounter++;
646
+ const n = this.#textAttachmentCounter;
647
+ const label = chipLabel("paste", n);
648
+ this.pendingTexts.push({
649
+ n,
650
+ label,
651
+ content,
652
+ lineCount: content.split("\n").length,
653
+ charCount: content.length,
654
+ });
655
+ this.insertAtom(label, expansion);
656
+ }
657
+
658
+ /** Cached read-only attachments whose chip token remains in the buffer.
659
+ * Deleting a token hides its chip and drops the attachment from submission. */
660
+ composerChips(): readonly ComposerChipDescriptor[] {
661
+ const cached = this.#composerChipsCache;
662
+ if (
663
+ cached?.textRevision === this.textRevision &&
664
+ cached.images === this.pendingImages &&
665
+ cached.imageCount === this.pendingImages.length &&
666
+ cached.imageLinks === this.pendingImageLinks &&
667
+ cached.imageLinkCount === this.pendingImageLinks.length &&
668
+ cached.texts === this.pendingTexts &&
669
+ cached.textCount === this.pendingTexts.length
670
+ ) {
671
+ return cached.chips;
672
+ }
673
+ const text = this.getText();
674
+ const chips: ComposerChipDescriptor[] = [];
675
+ for (let i = 0; i < this.pendingImages.length; i++) {
676
+ const n = i + 1;
677
+ const video =
678
+ text.includes(chipLabel("video", n)) || text.includes(`[Video #${n}]`) || text.includes(`[Video #${n},`);
679
+ const image =
680
+ text.includes(chipLabel("image", n)) || text.includes(`[Image #${n}]`) || text.includes(`[Image #${n},`);
681
+ if (!video && !image) continue;
682
+ chips.push({
683
+ kind: video ? "video" : "image",
684
+ n,
685
+ image: this.pendingImages[i],
686
+ link: this.pendingImageLinks[i],
687
+ });
688
+ }
689
+ for (const entry of this.pendingTexts) {
690
+ if (!text.includes(entry.label)) continue;
691
+ chips.push({ kind: "paste", n: entry.n, text: entry });
692
+ }
693
+ this.#composerChipsCache = {
694
+ textRevision: this.textRevision,
695
+ images: this.pendingImages,
696
+ imageCount: this.pendingImages.length,
697
+ imageLinks: this.pendingImageLinks,
698
+ imageLinkCount: this.pendingImageLinks.length,
699
+ texts: this.pendingTexts,
700
+ textCount: this.pendingTexts.length,
701
+ chips,
702
+ };
703
+ return chips;
704
+ }
705
+
706
+ /** Resolve draft-image links off the render path and repaint when they land; guarded against
707
+ * the draft being replaced while the blob writes were in flight. */
708
+ async #materializeDraftLinks(): Promise<void> {
709
+ const materialize = this.draftImageLinkMaterializer;
710
+ const images = this.pendingImages;
711
+ if (!materialize || images.length === 0) return;
712
+ const links = await materialize(images);
713
+ if (!links || this.pendingImages !== images) return;
714
+ this.pendingImageLinks = images.map((image, index) => videoPreviewSource(image) ?? links[index]);
715
+ this.imageLinks = this.pendingImageLinks;
716
+ this.#requestShimmerRepaint?.();
717
+ }
718
+
719
+ /** Treat image/paste references — compact chip tokens and bracketed markers alike — as
720
+ * indivisible: a stray backspace deletes the whole token instead of corrupting it. */
721
+ override atomicTokenPattern = COMPOSER_TOKEN_REGEX;
722
+
723
+ #syncComposerTokenPattern(): void {
724
+ const labels = [...this.atoms].filter(([, expansion]) => expansion.startsWith("^")).map(([label]) => label);
725
+ const next = composerTokenRegex(labels);
726
+ if (next.source !== this.atomicTokenPattern.source) this.atomicTokenPattern = next;
727
+ }
728
+
729
+ /** Magic-keyword shimmer cadence — drives one editor repaint every 70 ms while
730
+ * a keyword is on screen and the prompt is focused. ~14 frames/s is smooth
731
+ * without flooding the renderer. */
732
+ static readonly SHIMMER_FRAME_MS = 70;
733
+ /** Time for the gradient to sweep one full cycle across each keyword. */
734
+ static readonly SHIMMER_PERIOD_MS = 1800;
735
+
736
+ /** Per-render scratch flag: did any layout line in this render contain a magic
737
+ * keyword that should shimmer? Reset by {@link #scheduleShimmerIfNeeded} each
738
+ * time a frame is queued. */
739
+ #shimmerTimer: Timer | undefined;
740
+ /** Repaint hook the host wires once at construction. Called from the shimmer
741
+ * timer to request the next animation frame. Undefined when nobody is
742
+ * listening (tests, headless callers); the timer chain still self-cleans. */
743
+ #requestShimmerRepaint: (() => void) | undefined;
744
+ #queueDecorationText: string | undefined;
745
+ #decorationLines: readonly string[] = [""];
746
+ #queueShorthandActive = false;
747
+ #queueListActive = false;
748
+
749
+ /** Decorate magic keywords, attachments, and the queue-composer header/list markers.
750
+ * Queue shorthand reserves its first logical line as a dim `Queueing` label; sequential
751
+ * item markers use the accent color so separate follow-ups remain visible while composing. */
752
+ override decorateText = (text: string, context: EditorTextDecorationContext): string => {
753
+ this.#syncComposerTokenPattern();
754
+ const editorText = this.getText();
755
+ const animated = this.focused && this.#shimmerEnabled() && hasMagicKeyword(editorText);
756
+ const phase = animated ? (Date.now() % CustomEditor.SHIMMER_PERIOD_MS) / CustomEditor.SHIMMER_PERIOD_MS : 0;
757
+ if (animated) this.#scheduleShimmerFrame();
758
+ if (this.#queueDecorationText !== editorText) {
759
+ this.#queueDecorationText = editorText;
760
+ this.#decorationLines = this.getLines();
761
+ const queueBody = parseQueueShorthand(editorText);
762
+ this.#queueShorthandActive = queueBody !== undefined;
763
+ this.#queueListActive = queueBody !== undefined && isQueuedMessageList(queueBody);
764
+ }
765
+ let sourceSearchOffset = 0;
766
+ const locateSource = (value: string): number => {
767
+ const offset = text.indexOf(value, sourceSearchOffset);
768
+ if (offset === -1) return sourceSearchOffset;
769
+ sourceSearchOffset = offset + value.length;
770
+ return offset;
771
+ };
772
+ return renderPlaceholders(
773
+ text,
774
+ {
775
+ renderText: value => {
776
+ const sourceOffset = locateSource(value);
777
+ const highlighted = this.#spelling.decorateTypos(
778
+ value,
779
+ {
780
+ editorText,
781
+ lines: this.#decorationLines,
782
+ line: context.line,
783
+ startCol: context.startCol + sourceOffset,
784
+ },
785
+ span => highlightMagicKeywords(span, undefined, phase),
786
+ );
787
+ if (this.#queueShorthandActive && (value.startsWith("->") || value.startsWith("=>"))) {
788
+ const icon = typeof theme === "undefined" ? "➤" : theme.nav.selected;
789
+ return `${fgOrPlain("dim", `Queueing ${icon}`)}${highlighted.slice(2)}`;
790
+ }
791
+ if (this.#queueListActive) {
792
+ const markerMatch = QUEUE_LIST_MARKER_RE.exec(value);
793
+ if (markerMatch) {
794
+ const indent = markerMatch[1] ?? "";
795
+ const markerEnd = markerMatch[0].length;
796
+ return `${indent}${fgOrPlain("accent", value.slice(indent.length, markerEnd))}${highlighted.slice(markerEnd)}`;
797
+ }
798
+ }
799
+ return highlighted;
800
+ },
801
+ renderSkill: (label, name) => {
802
+ locateSource(label);
803
+ const styled = skillChipStyle(label);
804
+ const filePath = this.skillFilePath(name);
805
+ return filePath === undefined ? styled : this.fileHyperlink(filePath, styled);
806
+ },
807
+ renderMention: label => {
808
+ locateSource(label);
809
+ return modelChipStyle(label);
810
+ },
811
+ renderReference: (value, kind, index, form) => {
812
+ locateSource(value);
813
+ if (form === "chip") {
814
+ // Chip tokens carry their attachment identity color (matches the band card).
815
+ const styled = `${attachmentSgr(kind, index)}\x1b[1m${value}\x1b[22m\x1b[39m`;
816
+ return kind === "image" || kind === "video"
817
+ ? this.imageReferenceHyperlink(value, index, this.imageLinks, () => styled)
818
+ : styled;
819
+ }
820
+ return kind === "image" || kind === "video"
821
+ ? this.imageReferenceHyperlink(value, index, this.imageLinks, label =>
822
+ fgOrPlain("accent", label, `\x1b[1m\x1b[4m${label}\x1b[24m\x1b[22m`),
823
+ )
824
+ : fgOrPlain("accent", value, `\x1b[1m${value}\x1b[22m`);
825
+ },
826
+ },
827
+ this.atomicTokenPattern,
828
+ );
829
+ };
830
+
831
+ /** Optional test override for the magic-keyword shimmer gate. */
832
+ magicKeywordsEnabledOverride: boolean | undefined;
833
+
834
+ /**
835
+ * Host-owned setting reader. Startup defaults to enabled without loading the
836
+ * settings graph; InteractiveMode replaces this with the live session setting.
837
+ */
838
+ magicKeywordsEnabled: () => boolean = () => true;
839
+
840
+ /**
841
+ * Late-bound OSC hyperlink renderer. Startup stays plain until the full
842
+ * interactive graph supplies the settings-aware implementation.
843
+ */
844
+ imageReferenceHyperlink: (
845
+ label: string,
846
+ index: number,
847
+ imageLinks: readonly (string | undefined)[] | undefined,
848
+ renderLabel: (text: string) => string,
849
+ ) => string = (label, _index, _imageLinks, renderLabel) => renderLabel(label);
850
+
851
+ #shimmerEnabled(): boolean {
852
+ return this.magicKeywordsEnabledOverride ?? this.magicKeywordsEnabled();
853
+ }
854
+
855
+ /** Bind the host's render request callback. Idempotent — the host wires this
856
+ * once after construction (and again after `setEditorComponent` swaps the
857
+ * editor). Passing `undefined` clears any pending frame. */
858
+ setShimmerRepaintHandler(handler: (() => void) | undefined): void {
859
+ this.#requestShimmerRepaint = handler;
860
+ if (!handler && this.#shimmerTimer) {
861
+ clearTimeout(this.#shimmerTimer);
862
+ this.#shimmerTimer = undefined;
863
+ }
864
+ }
865
+
866
+ /** Schedule one shimmer frame if none is already pending. The next render
867
+ * decides whether to schedule another, so the chain stops by itself when
868
+ * `focused` flips off or the keyword leaves the buffer. */
869
+ #scheduleShimmerFrame(): void {
870
+ if (this.#shimmerTimer || !this.#requestShimmerRepaint) return;
871
+ this.#shimmerTimer = setTimeout(() => {
872
+ this.#shimmerTimer = undefined;
873
+ this.#requestShimmerRepaint?.();
874
+ }, CustomEditor.SHIMMER_FRAME_MS);
875
+ this.#shimmerTimer.unref?.();
876
+ }
877
+ onEscape?: () => void;
878
+ onClear?: () => void;
879
+ onExit?: () => void;
880
+ onDisplayReset?: () => void;
881
+ onCycleThinkingLevel?: () => void;
882
+ onCycleModelForward?: () => void;
883
+ onCycleModelBackward?: () => void;
884
+ onSelectModel?: () => void;
885
+ onSuspend?: () => void;
886
+ onSelectModelTemporary?: () => void;
887
+ /** Called when the configured copy-prompt shortcut is pressed. */
888
+ onCopyPrompt?: () => void;
889
+ /** Called when the configured image-paste shortcut is pressed. */
890
+ onPasteImage?: () => Promise<boolean>;
891
+ /** Called when a bracketed paste contains one or more image or video file paths. */
892
+ onPasteImagePath?: (path: string) => void | Promise<void>;
893
+ /** Called when the configured raw text-paste shortcut is pressed. */
894
+ onPasteTextRaw?: () => void;
895
+ /** Called when the configured dequeue shortcut is pressed. */
896
+ onDequeue?: () => void;
897
+ /** Called when the configured retry shortcut is pressed. */
898
+ onRetry?: () => void;
899
+ /** Called when Caps Lock is pressed. */
900
+ onCapsLock?: () => void;
901
+ /** Called when left-arrow is pressed while the editor is empty (cursor necessarily at start). */
902
+ onLeftAtStart?: () => void;
903
+
904
+ /** Fired when a sustained space-bar hold is recognized — the push-to-talk STT start. The
905
+ * optimistically-typed spaces have already been deleted by the time this runs. */
906
+ onSpaceHoldStart?: () => void;
907
+ /** Fired when the held space bar is released (detected as an idle gap with no further repeated
908
+ * spaces) — the push-to-talk STT stop. */
909
+ onSpaceHoldEnd?: () => void;
910
+ /** Gate for the space-hold gesture. Returns false to keep the space bar inserting spaces
911
+ * normally; wired to `stt.enabled` so disabling STT restores plain space behavior. */
912
+ sttHoldEnabled?: () => boolean;
913
+
914
+ /** Custom key handlers from extensions and non-built-in app actions. */
915
+ #customKeyHandlers = new Map<KeyId, () => void>();
916
+ #customMatchKeys = new Map<string, () => void>();
917
+ /** Bracketed-paste assembler that runs ahead of the inherited handler so terminals which
918
+ * deliver `\x1b[200~` and `\x1b[201~` in separate stdin chunks still resolve to a single
919
+ * assembled payload here; the empty-paste / image-path branches must see the full content,
920
+ * not the raw single-chunk byte sequence. */
921
+ #pasteHandler = new BracketedPasteHandler();
922
+ /** Number of async pastes (clipboard-image reads / image-path attachments) currently in flight.
923
+ * While > 0, `handleInput` queues subsequent keystrokes into {@link #pendingInput} instead of
924
+ * dispatching them so a trailing `Enter` after `Cmd+V` can't submit before the image lands on
925
+ * `pendingImages` (Codex PR #3602 review). */
926
+ #pasteInFlight = 0;
927
+ /** Input chunks deferred behind an in-flight paste, drained in FIFO order once the paste
928
+ * count returns to zero. */
929
+ #pendingInput: string[] = [];
930
+ /** Spaces actually inserted in the current run; tracked back out when a hold is recognized. */
931
+ #spaceRunInserted = 0;
932
+ /** Consecutive "mechanical" deltas (fast + steady); a sustained run of these confirms a held bar. */
933
+ #mechanicalRun = 0;
934
+ /** Inter-space gap (ms) of the previous space pair, compared against the next to judge steadiness. */
935
+ #prevSpaceGap: number | undefined;
936
+ /** Monotonic timestamp (ms) of the last space, to measure the gap to the next one. */
937
+ #lastSpaceAt = Number.NEGATIVE_INFINITY;
938
+ /** True while a recognized space-hold push-to-talk recording is in progress. */
939
+ #spaceHoldActive = false;
940
+ /** Idle timer that fires `onSpaceHoldEnd` once repeated spaces stop arriving. */
941
+ #spaceHoldTimer: NodeJS.Timeout | undefined;
942
+ #actionKeys = new Map<ConfigurableEditorAction, KeyId[]>(
943
+ Object.entries(DEFAULT_ACTION_KEYS).map(([action, keys]) => [action as ConfigurableEditorAction, [...keys]]),
944
+ );
945
+ #actionMatchKeys = new Map<ConfigurableEditorAction, Set<string>>(
946
+ Object.entries(DEFAULT_ACTION_KEYS).map(([action, keys]) => [
947
+ action as ConfigurableEditorAction,
948
+ buildMatchKeys(keys),
949
+ ]),
950
+ );
951
+ /** Union of every action's match keys: one probe in `handleInput` decides
952
+ * whether the per-action interception chain can match at all. */
953
+ #actionMatchKeyUnion = unionOfMatchKeys(this.#actionMatchKeys);
954
+
955
+ setActionKeys(action: ConfigurableEditorAction, keys: KeyId[]): void {
956
+ this.#actionKeys.set(action, [...keys]);
957
+ this.#actionMatchKeys.set(action, buildMatchKeys(keys));
958
+ this.#actionMatchKeyUnion = unionOfMatchKeys(this.#actionMatchKeys);
959
+ }
960
+
961
+ #rebuildCustomMatchKeys(): void {
962
+ this.#customMatchKeys.clear();
963
+ for (const [keyId, handler] of this.#customKeyHandlers) {
964
+ for (const alias of buildMatchKeys([keyId])) {
965
+ // Preserve current iteration behavior: the first registered handler for colliding aliases wins.
966
+ if (!this.#customMatchKeys.has(alias)) this.#customMatchKeys.set(alias, handler);
967
+ }
968
+ }
969
+ }
970
+
971
+ #matchesAction(canonical: string | undefined, action: ConfigurableEditorAction): boolean {
972
+ return canonical !== undefined && (this.#actionMatchKeys.get(action)?.has(canonical) ?? false);
973
+ }
974
+
975
+ /** Whether `data` is exactly one keypress the base editor would treat as submit. */
976
+ #isSubmitKey(data: string): boolean {
977
+ if (data === "\n") return true;
978
+ const key = parseKey(data);
979
+ return key !== undefined && getKeybindings().matchesCanonical(canonicalKeyId(key), "tui.input.submit");
980
+ }
981
+
982
+ /**
983
+ * Register a custom key handler. Extensions use this for shortcuts.
984
+ */
985
+ setCustomKeyHandler(key: KeyId, handler: () => void): void {
986
+ this.#customKeyHandlers.set(key, handler);
987
+ this.#rebuildCustomMatchKeys();
988
+ }
989
+
990
+ /**
991
+ * Remove a custom key handler.
992
+ */
993
+ removeCustomKeyHandler(key: KeyId): void {
994
+ this.#customKeyHandlers.delete(key);
995
+ this.#rebuildCustomMatchKeys();
996
+ }
997
+
998
+ /**
999
+ * Clear all custom key handlers.
1000
+ */
1001
+ clearCustomKeyHandlers(): void {
1002
+ this.#customKeyHandlers.clear();
1003
+ this.#rebuildCustomMatchKeys();
1004
+ }
1005
+
1006
+ #spaceHoldGestureEnabled(): boolean {
1007
+ // Push-to-talk is a text-composition gesture, so it stays out of Vim's Normal/Visual modes
1008
+ // where the space bar is the `l` motion.
1009
+ if (this.vimMode !== "insert") return false;
1010
+ return this.onSpaceHoldStart !== undefined && (this.sttHoldEnabled?.() ?? false) && !this.isShowingAutocomplete();
1011
+ }
1012
+
1013
+ /** Drive the space-hold push-to-talk state machine. Returns true when the gesture consumed the
1014
+ * input so it must not reach normal editing. A held space bar emits OS auto-repeat: a *steady*
1015
+ * stream of spaces at a fixed fast interval. We watch the inter-space deltas and only recognize a
1016
+ * hold once {@link SPACE_HOLD_MECHANICAL_RUN} consecutive deltas are "mechanical" — both
1017
+ * auto-repeat-fast and near-identical (see {@link gapsAreMechanical}). Smashing the bar is fast
1018
+ * but jittery and deliberate taps are too slow, so neither escalates and both keep typing real
1019
+ * spaces; the few spaces typed before a real hold is recognized are tracked back out. */
1020
+ #handleSpaceHold(data: string, canonical: string | undefined): boolean {
1021
+ const isSpace = canonical === "space";
1022
+ if (this.#spaceHoldActive) {
1023
+ if (isSpace) {
1024
+ // Auto-repeat while held: swallow it and keep the release timer alive.
1025
+ this.#armSpaceHoldReleaseTimer();
1026
+ return true;
1027
+ }
1028
+ // Any non-space means the bar was released — stop recording, then let the key through.
1029
+ this.#endSpaceHold();
1030
+ return false;
1031
+ }
1032
+ if (!isSpace) {
1033
+ this.#resetSpaceRun();
1034
+ return false;
1035
+ }
1036
+ if (!this.#spaceHoldGestureEnabled()) return false;
1037
+ const now = performance.now();
1038
+ const gap = now - this.#lastSpaceAt;
1039
+ const prevGap = this.#prevSpaceGap;
1040
+ this.#lastSpaceAt = now;
1041
+ this.#prevSpaceGap = gap;
1042
+ if (prevGap === undefined || !gapsAreMechanical(gap, prevGap)) {
1043
+ // First space, a deliberate tap, or jittery smashing: not a steady machine cadence yet, so
1044
+ // type a real space and reset the mechanical run.
1045
+ this.#mechanicalRun = 0;
1046
+ this.#forwardInput(data);
1047
+ this.#spaceRunInserted++;
1048
+ return true;
1049
+ }
1050
+ // Steady fast repeat: swallow it. Once the cadence has held for SPACE_HOLD_MECHANICAL_RUN
1051
+ // deltas it's a held bar — track back the few pre-burst spaces already typed and start.
1052
+ if (++this.#mechanicalRun >= SPACE_HOLD_MECHANICAL_RUN) {
1053
+ this.deleteBeforeCursor(this.#spaceRunInserted);
1054
+ this.#resetSpaceRun();
1055
+ this.#beginSpaceHold();
1056
+ }
1057
+ return true;
1058
+ }
1059
+
1060
+ #resetSpaceRun(): void {
1061
+ this.#spaceRunInserted = 0;
1062
+ this.#mechanicalRun = 0;
1063
+ this.#prevSpaceGap = undefined;
1064
+ this.#lastSpaceAt = Number.NEGATIVE_INFINITY;
1065
+ }
1066
+
1067
+ #beginSpaceHold(): void {
1068
+ this.#spaceHoldActive = true;
1069
+ this.#armSpaceHoldReleaseTimer();
1070
+ this.onSpaceHoldStart?.();
1071
+ }
1072
+
1073
+ #armSpaceHoldReleaseTimer(): void {
1074
+ if (this.#spaceHoldTimer) clearTimeout(this.#spaceHoldTimer);
1075
+ this.#spaceHoldTimer = setTimeout(() => {
1076
+ this.#spaceHoldTimer = undefined;
1077
+ this.#endSpaceHold();
1078
+ }, SPACE_HOLD_RELEASE_MS);
1079
+ this.#spaceHoldTimer.unref?.();
1080
+ }
1081
+
1082
+ #endSpaceHold(): void {
1083
+ if (!this.#spaceHoldActive) return;
1084
+ this.#spaceHoldActive = false;
1085
+ this.#resetSpaceRun();
1086
+ if (this.#spaceHoldTimer) {
1087
+ clearTimeout(this.#spaceHoldTimer);
1088
+ this.#spaceHoldTimer = undefined;
1089
+ }
1090
+ this.onSpaceHoldEnd?.();
1091
+ }
1092
+
1093
+ /** Decrement {@link #pasteInFlight} once an async paste settles and, when the count returns
1094
+ * to zero, drain {@link #pendingInput} through `handleInput` so requeueing still works if a
1095
+ * drained chunk triggers another async paste. Bound member so it can be passed straight to
1096
+ * `Promise.then(callback, callback)`. */
1097
+ #onPasteSettled = (): void => {
1098
+ this.#pasteInFlight--;
1099
+ if (this.#pasteInFlight > 0) return;
1100
+ const drained = this.#pendingInput.splice(0);
1101
+ for (const chunk of drained) this.handleInput(chunk);
1102
+ };
1103
+
1104
+ /** Track `promise` as an in-flight paste so subsequent `handleInput` calls queue behind it,
1105
+ * then drain the queue once it settles. Codex PR #3602 review: without this, a trailing
1106
+ * keystroke (Enter most painfully) in the same stdin read processes synchronously while the
1107
+ * clipboard read is still pending — submit fires with the text but `pendingImages` is still
1108
+ * empty and the image lands on the *next* draft instead. */
1109
+ #trackAsyncPaste(promise: Promise<unknown>): void {
1110
+ this.#pasteInFlight++;
1111
+ void promise.then(this.#onPasteSettled, this.#onPasteSettled);
1112
+ }
1113
+
1114
+ override handleInput(data: string): void {
1115
+ // Serialize behind any in-flight async paste so a trailing Enter / follow-up key can't
1116
+ // submit before the clipboard image reaches `pendingImages` (Codex PR #3602 review).
1117
+ if (this.#pasteInFlight > 0) {
1118
+ this.#pendingInput.push(data);
1119
+ return;
1120
+ }
1121
+ // textEquals avoids getText()'s O(buffer) join on every keystroke; kitty
1122
+ // sequences always start with ESC, so plain bytes skip the native parse.
1123
+ const hadBareQueuePrefix = this.textEquals("->") || this.textEquals("=>");
1124
+ const kittyParsed = data.charCodeAt(0) === 0x1b ? parseKittySequence(data) : null;
1125
+ if (kittyParsed && (kittyParsed.modifier & 64) !== 0 && this.onCapsLock) {
1126
+ // Caps Lock is modifier bit 64
1127
+ this.onCapsLock();
1128
+ return;
1129
+ }
1130
+
1131
+ // Bracketed-paste assembly. Some terminals fragment the start marker,
1132
+ // the payload, and the end marker across separate stdin chunks
1133
+ // (Windows Terminal under heavy load, certain SSH muxes, …); the
1134
+ // inherited handler then sees a zero-length payload and silently
1135
+ // drops it through the normal text-insert path. Running our own
1136
+ // `BracketedPasteHandler` ahead of `super.handleInput` lets us route
1137
+ // the assembled content regardless of chunk boundaries:
1138
+ // - empty payload → `onPasteImage` (#3601: `Cmd+V`/`Ctrl+V` on an
1139
+ // image-only macOS pasteboard the terminal stripped to `""` first);
1140
+ // - explicit image-file paths → `onPasteImagePath` (#3506);
1141
+ // - anything else → the base editor's `pasteText` so `[Paste #N]`
1142
+ // markers, autocomplete, and undo state stay intact.
1143
+ const paste = this.#pasteHandler.process(data);
1144
+ if (paste.handled) {
1145
+ if (paste.pasteContent === undefined) return; // still buffering — wait for end marker
1146
+ const content = paste.pasteContent;
1147
+ const remaining = paste.remaining;
1148
+ // Queue any trailing bytes from the same read (typically a follow-up keystroke such as
1149
+ // Enter that the user pressed right after Cmd+V) so they only fire *after* the paste
1150
+ // completes — fixes the race where submit runs against an empty `pendingImages`.
1151
+ if (remaining.length > 0) this.#pendingInput.push(remaining);
1152
+ if (content.length === 0 && this.onPasteImage) {
1153
+ this.#trackAsyncPaste(Promise.resolve(this.onPasteImage()));
1154
+ return;
1155
+ }
1156
+ const attachmentPaths = extractImagePastePathsFromText(content);
1157
+ if (attachmentPaths && this.onPasteImagePath) {
1158
+ this.#trackAsyncPaste(
1159
+ (async () => {
1160
+ for (const p of attachmentPaths) await this.onPasteImagePath?.(p);
1161
+ })(),
1162
+ );
1163
+ return;
1164
+ }
1165
+ // A submit key that shared the read (see `StdinBuffer`'s paste event) is
1166
+ // about to be drained below, so a large-paste host must stage the paste
1167
+ // synchronously instead of opening a menu the submit would land in.
1168
+ if (this.#isSubmitKey(remaining)) this.pasteText(content, { submitAfterPaste: true });
1169
+ else this.pasteText(content);
1170
+ this.#collapseSkillTokens();
1171
+ this.#collapseModelMentions();
1172
+ // No async paste was started; drain the queued trailing bytes ourselves.
1173
+ const drained = this.#pendingInput.splice(0);
1174
+ for (const chunk of drained) this.handleInput(chunk);
1175
+ return;
1176
+ }
1177
+
1178
+ const parsedKey = parseKey(data);
1179
+ const canonical = parsedKey !== undefined ? canonicalKeyId(parsedKey) : undefined;
1180
+
1181
+ // Left-arrow on an empty editor: surface for the agent-hub double-tap
1182
+ // gesture. Plain "left" only — modified arrows and any in-text cursor
1183
+ // movement fall through to normal handling.
1184
+ if (canonical === "left" && this.onLeftAtStart && this.getText().trim() === "") {
1185
+ this.onLeftAtStart();
1186
+ return;
1187
+ }
1188
+
1189
+ // Space-hold push-to-talk: a sustained space bar starts/stops STT instead of typing spaces.
1190
+ if (this.#handleSpaceHold(data, canonical)) return;
1191
+
1192
+ // One union probe decides whether any per-action interception below can
1193
+ // match — plain typing then skips the ~20 per-action set lookups per key.
1194
+ if (
1195
+ canonical !== undefined &&
1196
+ (this.#actionMatchKeyUnion.has(canonical) || this.#customMatchKeys.has(canonical))
1197
+ ) {
1198
+ // Intercept configured image paste (async - fires and handles result)
1199
+ if (this.#matchesAction(canonical, "app.clipboard.pasteImage") && this.onPasteImage) {
1200
+ void this.onPasteImage();
1201
+ return;
1202
+ }
1203
+
1204
+ // Intercept configured raw text paste (fires and handles result)
1205
+ if (this.#matchesAction(canonical, "app.clipboard.pasteTextRaw") && this.onPasteTextRaw) {
1206
+ this.onPasteTextRaw();
1207
+ return;
1208
+ }
1209
+
1210
+ // Intercept configured temporary model selector shortcut
1211
+ if (this.#matchesAction(canonical, "app.model.selectTemporary") && this.onSelectModelTemporary) {
1212
+ this.onSelectModelTemporary();
1213
+ return;
1214
+ }
1215
+
1216
+ // Intercept configured display reset shortcut
1217
+ if (this.#matchesAction(canonical, "app.display.reset") && this.onDisplayReset) {
1218
+ this.onDisplayReset();
1219
+ return;
1220
+ }
1221
+
1222
+ // Intercept configured suspend shortcut
1223
+ if (this.#matchesAction(canonical, "app.suspend") && this.onSuspend) {
1224
+ this.onSuspend();
1225
+ return;
1226
+ }
1227
+
1228
+ // Intercept configured model selector shortcut
1229
+ if (this.#matchesAction(canonical, "app.model.select") && this.onSelectModel) {
1230
+ this.onSelectModel();
1231
+ return;
1232
+ }
1233
+
1234
+ // Intercept configured backward model cycling (check before forward cycling)
1235
+ if (this.#matchesAction(canonical, "app.model.cycleBackward") && this.onCycleModelBackward) {
1236
+ this.onCycleModelBackward();
1237
+ return;
1238
+ }
1239
+
1240
+ // Intercept configured forward model cycling
1241
+ if (this.#matchesAction(canonical, "app.model.cycleForward") && this.onCycleModelForward) {
1242
+ this.onCycleModelForward();
1243
+ return;
1244
+ }
1245
+
1246
+ // Intercept configured thinking level cycling
1247
+ if (this.#matchesAction(canonical, "app.thinking.cycle") && this.onCycleThinkingLevel) {
1248
+ this.onCycleThinkingLevel();
1249
+ return;
1250
+ }
1251
+
1252
+ // Intercept configured interrupt shortcut.
1253
+ // When the autocomplete popup is visible, ESC's first job is to dismiss
1254
+ // the popup — let super.handleInput() route it to #cancelAutocomplete().
1255
+ // The user can press ESC again afterward to fire the global interrupt
1256
+ // handler. This matches the standard TUI/IDE pattern and prevents a
1257
+ // single ESC from both closing an @ completion and aborting an active
1258
+ // agent run (#1655).
1259
+ // Vim mode claims Escape ahead of the interrupt: it has to mean "leave Insert mode" and
1260
+ // "cancel a half-typed operator" first. Only a quiet Normal mode gives it back here, so
1261
+ // the familiar single-ESC-to-abort still works once the user is out of Insert mode.
1262
+ if (
1263
+ this.#matchesAction(canonical, "app.interrupt") &&
1264
+ this.onEscape &&
1265
+ !this.isShowingAutocomplete() &&
1266
+ !this.vimConsumesEscape()
1267
+ ) {
1268
+ this.onEscape();
1269
+ return;
1270
+ }
1271
+
1272
+ // Intercept configured clear shortcut
1273
+ if (this.#matchesAction(canonical, "app.clear") && this.onClear) {
1274
+ this.onClear();
1275
+ return;
1276
+ }
1277
+
1278
+ // Intercept configured exit shortcut. When the key doubles as
1279
+ // forward-delete (readline ^D: the default app.exit binding overlaps
1280
+ // tui.editor.deleteCharForward) and the buffer is non-empty, perform
1281
+ // the delete here instead of quitting. Invoking the operation directly
1282
+ // — not falling through, not redispatching the raw key — keeps the
1283
+ // exit chord's precedence slot on both sides: a later app action or
1284
+ // extension handler bound to the same chord cannot steal it, and
1285
+ // neither can an earlier base-editor action (e.g. a user-bound
1286
+ // tui.input.submit, which Editor.handleInput checks before
1287
+ // deleteCharForward). Only an empty buffer exits; firing onExit is
1288
+ // the controller's chance to snapshot the current text as a draft
1289
+ // before shutting down. Exit keys with no forward-delete role always
1290
+ // exit. Draft presence is read off the buffer alone: attachments live
1291
+ // as inline chip tokens, while `pendingImages` / `pendingTexts`
1292
+ // intentionally retain deleted records so numbering isn't recycled
1293
+ // (see composerChips) — trusting them would make Ctrl+D a permanent
1294
+ // no-op after the last chip is deleted.
1295
+ if (this.#matchesAction(canonical, "app.exit")) {
1296
+ const doublesAsForwardDelete =
1297
+ canonical !== undefined && getKeybindings().matchesCanonical(canonical, "tui.editor.deleteCharForward");
1298
+ if (doublesAsForwardDelete && !this.textEquals("")) {
1299
+ this.deleteCharForward();
1300
+ // Same post-edit normalization the parent dispatch runs below: an edit that
1301
+ // leaves a bare "->"/"=>" turns it into a reserved queue header, or later
1302
+ // typing lands on the Queueing label instead of the queue body.
1303
+ this.#normalizeQueuePrefix(hadBareQueuePrefix);
1304
+ return;
1305
+ }
1306
+ this.onExit?.();
1307
+ return;
1308
+ }
1309
+
1310
+ // Intercept configured dequeue shortcut (restore queued message to editor)
1311
+ if (this.#matchesAction(canonical, "app.message.dequeue") && this.onDequeue) {
1312
+ this.onDequeue();
1313
+ return;
1314
+ }
1315
+
1316
+ // Intercept configured copy-prompt shortcut
1317
+ if (this.#matchesAction(canonical, "app.clipboard.copyPrompt") && this.onCopyPrompt) {
1318
+ this.onCopyPrompt();
1319
+ return;
1320
+ }
1321
+
1322
+ // Intercept configured retry shortcut. Later user/custom handlers keep
1323
+ // precedence so adding the default Alt+R binding does not steal existing
1324
+ // shortcuts such as app.plan.toggle or extension commands; copy-prompt is
1325
+ // checked above for the same reason.
1326
+ if (this.#matchesAction(canonical, "app.retry") && this.onRetry) {
1327
+ const customHandler = this.#customMatchKeys.get(canonical);
1328
+ if (customHandler) {
1329
+ customHandler();
1330
+ return;
1331
+ }
1332
+ this.onRetry();
1333
+ return;
1334
+ }
1335
+
1336
+ // Check custom key handlers (extensions)
1337
+ const handler = this.#customMatchKeys.get(canonical);
1338
+ if (handler) {
1339
+ handler();
1340
+ return;
1341
+ }
1342
+ }
1343
+
1344
+ // Pass to parent for normal handling
1345
+ this.#forwardInput(data);
1346
+ this.#normalizeQueuePrefix(hadBareQueuePrefix);
1347
+ }
1348
+
1349
+ /** Promote a newly formed bare `->` / `=>` prefix to a reserved header line by opening the
1350
+ * queue body beneath it. `hadBareQueuePrefix` is the pre-edit state: a prompt that was
1351
+ * already just the prefix is left alone so the user can keep editing it. */
1352
+ #normalizeQueuePrefix(hadBareQueuePrefix: boolean): void {
1353
+ if (hadBareQueuePrefix || !(this.textEquals("->") || this.textEquals("=>"))) return;
1354
+ const cursor = this.getCursor();
1355
+ if (cursor.line === 0 && cursor.col === 2) {
1356
+ this.insertText("\n");
1357
+ }
1358
+ }
1359
+
1360
+ /**
1361
+ * Route a keystroke through the base text-editor pipeline only, skipping the
1362
+ * editor-scoped shortcut interception in {@link handleInput}. Used when the
1363
+ * editor is mounted for draft editing beneath another focused surface — e.g.
1364
+ * an Ask dialog opened over a non-empty prompt — so finishing or submitting
1365
+ * the draft cannot fire an editor-slot shortcut that clears
1366
+ * `editorContainer` and orphans the overlay. Only text editing, cursor
1367
+ * movement, submission, and the clear action reach the buffer.
1368
+ */
1369
+ handleDraftEdit(data: string): void {
1370
+ // The base editor reserves Ctrl+C for parent handling and returns without
1371
+ // touching the buffer, so the configured clear action must be dispatched
1372
+ // explicitly here — otherwise the guard's "finish or clear the prompt"
1373
+ // instruction has no working clear key. onClear (Ctrl+C → handleCtrlC)
1374
+ // clears the draft on first press without swapping the editor slot; a
1375
+ // standalone editor with no callback clears its own text.
1376
+ const parsed = parseKey(data);
1377
+ const canonical = parsed !== undefined ? canonicalKeyId(parsed) : undefined;
1378
+ if (canonical !== undefined && this.#matchesAction(canonical, "app.clear")) {
1379
+ if (this.onClear) this.onClear();
1380
+ else this.setText("");
1381
+ return;
1382
+ }
1383
+ this.#forwardInput(data);
1384
+ }
1385
+
1386
+ /** Base text-editing pipeline, then snap any skill or model token the keystroke just completed. */
1387
+ #forwardInput(data: string): void {
1388
+ super.handleInput(data);
1389
+ this.#collapseSkillTokens();
1390
+ this.#collapseModelMentions();
1391
+ }
1392
+ }