@oh-my-pi/pi-tui 18.2.3 → 18.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/README.md +43 -0
- package/dist/types/app-keybindings.d.ts +395 -0
- package/dist/types/apps/autoresearch-dashboard.d.ts +43 -0
- package/dist/types/apps/autoresearch-data.d.ts +15 -0
- package/dist/types/apps/cleanse-board.d.ts +66 -0
- package/dist/types/apps/cleanse-picker.d.ts +23 -0
- package/dist/types/apps/debug/log-formatting.d.ts +6 -0
- package/dist/types/apps/debug/log-viewer.d.ts +81 -0
- package/dist/types/apps/debug/protocol-probe.d.ts +42 -0
- package/dist/types/apps/debug/raw-sse-buffer.d.ts +58 -0
- package/dist/types/apps/debug/raw-sse.d.ts +26 -0
- package/dist/types/apps/debug/terminal-info.d.ts +34 -0
- package/dist/types/apps/debug/viewer-frame.d.ts +52 -0
- package/dist/types/apps/git/avatar.d.ts +9 -0
- package/dist/types/apps/git/colors.d.ts +49 -0
- package/dist/types/apps/git/diff-pane.d.ts +188 -0
- package/dist/types/apps/git/git-tui.d.ts +66 -0
- package/dist/types/apps/git/sidebar.d.ts +149 -0
- package/dist/types/apps/git/state.d.ts +117 -0
- package/dist/types/apps/if-bench-board.d.ts +75 -0
- package/dist/types/apps/live-visualizer.d.ts +33 -0
- package/dist/types/apps/ps-data.d.ts +51 -0
- package/dist/types/apps/ps-top.d.ts +33 -0
- package/dist/types/apps/session-picker.d.ts +26 -0
- package/dist/types/apps/setup-model-picker.d.ts +13 -0
- package/dist/types/apps/standalone-picker.d.ts +42 -0
- package/dist/types/autocomplete.d.ts +4 -0
- package/dist/types/chat/advisor-message.d.ts +11 -0
- package/dist/types/chat/assistant-message.d.ts +106 -0
- package/dist/types/chat/background-tan-message.d.ts +9 -0
- package/dist/types/chat/bash-execution.d.ts +47 -0
- package/dist/types/chat/cache-invalidation-marker.d.ts +50 -0
- package/dist/types/chat/chat-transcript-builder.d.ts +50 -0
- package/dist/types/chat/collab-prompt-message.d.ts +10 -0
- package/dist/types/chat/compaction-summary-message.d.ts +54 -0
- package/dist/types/chat/custom-message.d.ts +10 -0
- package/dist/types/chat/display-preferences.d.ts +13 -0
- package/dist/types/chat/eval-execution.d.ts +29 -0
- package/dist/types/chat/execution-shared.d.ts +51 -0
- package/dist/types/chat/extension-types.d.ts +28 -0
- package/dist/types/chat/hook-message.d.ts +10 -0
- package/dist/types/chat/image-loading.d.ts +46 -0
- package/dist/types/chat/late-diagnostics-message.d.ts +21 -0
- package/dist/types/chat/messages.d.ts +189 -0
- package/dist/types/chat/reaction.d.ts +30 -0
- package/dist/types/chat/read-target.d.ts +4 -0
- package/dist/types/chat/read-tool-group.d.ts +67 -0
- package/dist/types/chat/served-model-marker.d.ts +47 -0
- package/dist/types/chat/skill-message.d.ts +33 -0
- package/dist/types/chat/skill-title-input.d.ts +14 -0
- package/dist/types/chat/stripped-tool-calls-placeholder.d.ts +12 -0
- package/dist/types/chat/thinking-display.d.ts +12 -0
- package/dist/types/chat/todo-reminder.d.ts +13 -0
- package/dist/types/chat/tool-execution.d.ts +138 -0
- package/dist/types/chat/transcript-browser.d.ts +93 -0
- package/dist/types/chat/transcript-entry.d.ts +45 -0
- package/dist/types/chat/transcript-outline.d.ts +86 -0
- package/dist/types/chat/transcript-render-helpers.d.ts +92 -0
- package/dist/types/chat/ttsr-notification.d.ts +23 -0
- package/dist/types/chat/user-message.d.ts +52 -0
- package/dist/types/chrome/chat-block.d.ts +64 -0
- package/dist/types/chrome/collab-qrcode.d.ts +28 -0
- package/dist/types/chrome/context-thresholds.d.ts +10 -0
- package/dist/types/chrome/countdown-timer.d.ts +14 -0
- package/dist/types/chrome/diff.d.ts +14 -0
- package/dist/types/chrome/dynamic-border.d.ts +16 -0
- package/dist/types/chrome/error-block.d.ts +17 -0
- package/dist/types/chrome/form-theme.d.ts +3 -0
- package/dist/types/chrome/format.d.ts +12 -0
- package/dist/types/chrome/index.d.ts +20 -0
- package/dist/types/chrome/keybinding-hints.d.ts +40 -0
- package/dist/types/chrome/live-board.d.ts +23 -0
- package/dist/types/chrome/local-date.d.ts +4 -0
- package/dist/types/chrome/message-divider.d.ts +22 -0
- package/dist/types/chrome/message-frame.d.ts +52 -0
- package/dist/types/chrome/message-notice.d.ts +35 -0
- package/dist/types/chrome/overlay-box.d.ts +74 -0
- package/dist/types/chrome/qrcode.d.ts +48 -0
- package/dist/types/chrome/segment-track.d.ts +27 -0
- package/dist/types/chrome/select-list-mouse-routing.d.ts +4 -0
- package/dist/types/chrome/selector-helpers.d.ts +52 -0
- package/dist/types/chrome/shared.d.ts +5 -0
- package/dist/types/chrome/streaming-panel.d.ts +19 -0
- package/dist/types/chrome/tool-activity.d.ts +17 -0
- package/dist/types/chrome/transcript-container.d.ts +129 -0
- package/dist/types/chrome/transcript-status.d.ts +12 -0
- package/dist/types/chrome/visual-truncate.d.ts +19 -0
- package/dist/types/components/disclosure.d.ts +42 -0
- package/dist/types/components/form.d.ts +130 -0
- package/dist/types/components/image.d.ts +18 -16
- package/dist/types/components/key-value-list.d.ts +24 -0
- package/dist/types/components/layout/geometry.d.ts +73 -0
- package/dist/types/components/layout/row.d.ts +55 -0
- package/dist/types/components/layout/split-pane.d.ts +80 -0
- package/dist/types/components/layout/stack.d.ts +50 -0
- package/dist/types/components/menu-selection.d.ts +71 -0
- package/dist/types/components/metric.d.ts +44 -0
- package/dist/types/components/progress-bar.d.ts +36 -0
- package/dist/types/components/scroll-view.d.ts +53 -19
- package/dist/types/components/scroll-viewport.d.ts +46 -0
- package/dist/types/components/section.d.ts +19 -0
- package/dist/types/components/select-list.d.ts +46 -2
- package/dist/types/components/settings-list.d.ts +20 -0
- package/dist/types/components/table.d.ts +32 -0
- package/dist/types/components/tree-view.d.ts +130 -0
- package/dist/types/components/wizard-step.d.ts +60 -0
- package/dist/types/hotkeys-markdown.d.ts +7 -0
- package/dist/types/index.d.ts +17 -0
- package/dist/types/keybinding-matchers.d.ts +35 -0
- package/dist/types/keys.d.ts +2 -2
- package/dist/types/lang-from-path.d.ts +9 -0
- package/dist/types/overlays/advisor-config.d.ts +114 -0
- package/dist/types/overlays/agent-activity.d.ts +41 -0
- package/dist/types/overlays/agent-hub-projection.d.ts +33 -0
- package/dist/types/overlays/agent-hub-renderer.d.ts +53 -0
- package/dist/types/overlays/agent-hub-types.d.ts +99 -0
- package/dist/types/overlays/agent-hub.d.ts +126 -0
- package/dist/types/overlays/agent-transcript-viewer.d.ts +71 -0
- package/dist/types/overlays/agents-hub.d.ts +72 -0
- package/dist/types/overlays/ask-dialog.d.ts +88 -0
- package/dist/types/overlays/bordered-loader.d.ts +11 -0
- package/dist/types/overlays/btw-history-panel.d.ts +29 -0
- package/dist/types/overlays/btw-history.d.ts +17 -0
- package/dist/types/overlays/btw-panel.d.ts +26 -0
- package/dist/types/overlays/cleanse-panel.d.ts +36 -0
- package/dist/types/overlays/codex-reset-fireworks.d.ts +52 -0
- package/dist/types/overlays/composer-shape-preview.d.ts +47 -0
- package/dist/types/overlays/composer-shape-registry.d.ts +50 -0
- package/dist/types/overlays/copy-selector.d.ts +48 -0
- package/dist/types/overlays/copy-targets.d.ts +63 -0
- package/dist/types/overlays/error-banner.d.ts +13 -0
- package/dist/types/overlays/extensions/display-text.d.ts +10 -0
- package/dist/types/overlays/extensions/extension-dashboard.d.ts +63 -0
- package/dist/types/overlays/extensions/extension-list.d.ts +54 -0
- package/dist/types/overlays/extensions/inspector-model.d.ts +109 -0
- package/dist/types/overlays/extensions/inspector-panel.d.ts +17 -0
- package/dist/types/overlays/extensions/live-tool-session.d.ts +28 -0
- package/dist/types/overlays/extensions/mcp-runtime.d.ts +107 -0
- package/dist/types/overlays/extensions/state-manager.d.ts +42 -0
- package/dist/types/overlays/extensions/types.d.ts +165 -0
- package/dist/types/overlays/history-search.d.ts +16 -0
- package/dist/types/overlays/hook-editor.d.ts +44 -0
- package/dist/types/overlays/hook-input.d.ts +20 -0
- package/dist/types/overlays/hook-selector.d.ts +71 -0
- package/dist/types/overlays/hub-frame.d.ts +77 -0
- package/dist/types/overlays/login-dialog.d.ts +43 -0
- package/dist/types/overlays/logout-account-selector.d.ts +15 -0
- package/dist/types/overlays/mcp-add-wizard.d.ts +87 -0
- package/dist/types/overlays/model-browser.d.ts +213 -0
- package/dist/types/overlays/model-hub.d.ts +72 -0
- package/dist/types/overlays/model-picker.d.ts +74 -0
- package/dist/types/overlays/model-selector.d.ts +52 -0
- package/dist/types/overlays/move-overlay.d.ts +39 -0
- package/dist/types/overlays/oauth-selector.d.ts +41 -0
- package/dist/types/overlays/omfg-panel.d.ts +20 -0
- package/dist/types/overlays/pause-screen.d.ts +43 -0
- package/dist/types/overlays/plan-review-overlay.d.ts +89 -0
- package/dist/types/overlays/plan-save-overlay.d.ts +19 -0
- package/dist/types/overlays/plan-toc.d.ts +41 -0
- package/dist/types/overlays/plugin-selector.d.ts +28 -0
- package/dist/types/overlays/plugin-settings.d.ts +180 -0
- package/dist/types/overlays/queue-mode-selector.d.ts +11 -0
- package/dist/types/overlays/reset-usage-selector.d.ts +23 -0
- package/dist/types/overlays/rewind-selector.d.ts +61 -0
- package/dist/types/overlays/running-subagent-badge.d.ts +6 -0
- package/dist/types/overlays/session-account-selector.d.ts +24 -0
- package/dist/types/overlays/session-info-overlay.d.ts +17 -0
- package/dist/types/overlays/session-observer-registry.d.ts +77 -0
- package/dist/types/overlays/session-selector.d.ts +131 -0
- package/dist/types/overlays/settings-defs.d.ts +119 -0
- package/dist/types/overlays/settings-selector.d.ts +73 -0
- package/dist/types/overlays/show-images-selector.d.ts +11 -0
- package/dist/types/overlays/snapcompact-shape-preview.d.ts +31 -0
- package/dist/types/overlays/theme-selector.d.ts +12 -0
- package/dist/types/overlays/thinking-selector.d.ts +12 -0
- package/dist/types/overlays/tiny-title-download-progress.d.ts +21 -0
- package/dist/types/overlays/tree-selector.d.ts +92 -0
- package/dist/types/overlays/usage-dashboard.d.ts +94 -0
- package/dist/types/overlays/usage-display.d.ts +5 -0
- package/dist/types/overlays/usage-row.d.ts +17 -0
- package/dist/types/prompt/attachment-chips.d.ts +19 -0
- package/dist/types/prompt/composer-attachments.d.ts +71 -0
- package/dist/types/prompt/composer-cache.d.ts +37 -0
- package/dist/types/prompt/composer.d.ts +159 -0
- package/dist/types/prompt/custom-editor.d.ts +261 -0
- package/dist/types/prompt/editor-top-gap.d.ts +18 -0
- package/dist/types/prompt/emoji-autocomplete.d.ts +16 -0
- package/dist/types/prompt/github-ref-autocomplete.d.ts +35 -0
- package/dist/types/prompt/gradient-highlight.d.ts +32 -0
- package/dist/types/prompt/image-format.d.ts +4 -0
- package/dist/types/prompt/image-references.d.ts +26 -0
- package/dist/types/prompt/interactive-context-helpers.d.ts +43 -0
- package/dist/types/prompt/internal-url-autocomplete.d.ts +56 -0
- package/dist/types/prompt/macos-spelling.d.ts +44 -0
- package/dist/types/prompt/magic-keyword-boundary.d.ts +9 -0
- package/dist/types/prompt/magic-keywords.d.ts +26 -0
- package/dist/types/prompt/markdown-prose.d.ts +15 -0
- package/dist/types/prompt/model-mention-autocomplete.d.ts +25 -0
- package/dist/types/prompt/model-mention-syntax.d.ts +24 -0
- package/dist/types/prompt/orchestrate.d.ts +13 -0
- package/dist/types/prompt/prompt-action-autocomplete.d.ts +56 -0
- package/dist/types/prompt/queue-input.d.ts +8 -0
- package/dist/types/prompt/skill-tokens.d.ts +13 -0
- package/dist/types/prompt/tools-markdown.d.ts +12 -0
- package/dist/types/prompt/ultrathink.d.ts +13 -0
- package/dist/types/prompt/usage-amounts.d.ts +26 -0
- package/dist/types/prompt/video.d.ts +18 -0
- package/dist/types/prompt/welcome.d.ts +83 -0
- package/dist/types/prompt/workflow.d.ts +13 -0
- package/dist/types/render/code-cell.d.ts +51 -0
- package/dist/types/render/file-list.d.ts +22 -0
- package/dist/types/render/hyperlink.d.ts +80 -0
- package/dist/types/render/index.d.ts +16 -0
- package/dist/types/render/output-block.d.ts +54 -0
- package/dist/types/render/output-pane.d.ts +80 -0
- package/dist/types/render/render-utils.d.ts +312 -0
- package/dist/types/render/sixel.d.ts +21 -0
- package/dist/types/render/status-line.d.ts +23 -0
- package/dist/types/render/tool-card.d.ts +71 -0
- package/dist/types/render/tree-list.d.ts +29 -0
- package/dist/types/render/types.d.ts +17 -0
- package/dist/types/render/utils.d.ts +43 -0
- package/dist/types/render/width-aware-text.d.ts +23 -0
- package/dist/types/setup/lazy.d.ts +3 -0
- package/dist/types/setup/scenes/composer.d.ts +3 -0
- package/dist/types/setup/scenes/glyph.d.ts +3 -0
- package/dist/types/setup/scenes/model.d.ts +3 -0
- package/dist/types/setup/scenes/outro.d.ts +2 -0
- package/dist/types/setup/scenes/providers.d.ts +3 -0
- package/dist/types/setup/scenes/sign-in.d.ts +21 -0
- package/dist/types/setup/scenes/splash.d.ts +11 -0
- package/dist/types/setup/scenes/theme.d.ts +3 -0
- package/dist/types/setup/scenes/types.d.ts +110 -0
- package/dist/types/setup/scenes/web-search.d.ts +22 -0
- package/dist/types/setup/setup-version.d.ts +11 -0
- package/dist/types/setup/startup-splash.d.ts +9 -0
- package/dist/types/setup/wizard-overlay.d.ts +16 -0
- package/dist/types/setup/wizard.d.ts +25 -0
- package/dist/types/status-line/component.d.ts +182 -0
- package/dist/types/status-line/context-usage.d.ts +156 -0
- package/dist/types/status-line/footer.d.ts +30 -0
- package/dist/types/status-line/git-utils.d.ts +22 -0
- package/dist/types/status-line/host.d.ts +129 -0
- package/dist/types/status-line/index.d.ts +6 -0
- package/dist/types/status-line/loop.d.ts +18 -0
- package/dist/types/status-line/metrics.d.ts +21 -0
- package/dist/types/status-line/presets.d.ts +3 -0
- package/dist/types/status-line/schema.d.ts +15 -0
- package/dist/types/status-line/segments.d.ts +5 -0
- package/dist/types/status-line/separators.d.ts +3 -0
- package/dist/types/status-line/types.d.ts +236 -0
- package/dist/types/stdin-buffer.d.ts +4 -0
- package/dist/types/theme/color.d.ts +24 -0
- package/dist/types/theme/defaults/index.d.ts +9489 -0
- package/dist/types/theme/index.d.ts +4 -0
- package/dist/types/theme/loader.d.ts +23 -0
- package/dist/types/theme/mermaid-cache.d.ts +26 -0
- package/dist/types/theme/schema-validation.d.ts +3 -0
- package/dist/types/theme/schema.d.ts +25 -0
- package/dist/types/theme/session-color.d.ts +43 -0
- package/dist/types/theme/shimmer.d.ts +44 -0
- package/dist/types/theme/symbols.d.ts +34 -0
- package/dist/types/theme/theme-class.d.ts +299 -0
- package/dist/types/theme/theme.d.ts +111 -0
- package/dist/types/theme/tui-adapters.d.ts +30 -0
- package/dist/types/thinking.d.ts +165 -0
- package/dist/types/tools/agent-tree.d.ts +36 -0
- package/dist/types/tools/ask.d.ts +67 -0
- package/dist/types/tools/ast-edit.d.ts +53 -0
- package/dist/types/tools/ast-grep.d.ts +52 -0
- package/dist/types/tools/autoresearch.d.ts +156 -0
- package/dist/types/tools/bash-interactive.d.ts +28 -0
- package/dist/types/tools/bash.d.ts +103 -0
- package/dist/types/tools/conflict-detect.d.ts +43 -0
- package/dist/types/tools/debug.d.ts +60 -0
- package/dist/types/tools/default-renderer.d.ts +24 -0
- package/dist/types/tools/edit.d.ts +158 -0
- package/dist/types/tools/eval-format/code-mode-declarations.d.ts +5 -0
- package/dist/types/tools/eval-format/index.d.ts +5 -0
- package/dist/types/tools/eval-format/javascript.d.ts +2 -0
- package/dist/types/tools/eval-format/python.d.ts +2 -0
- package/dist/types/tools/eval.d.ts +94 -0
- package/dist/types/tools/fetch.d.ts +42 -0
- package/dist/types/tools/gh-format.d.ts +8 -0
- package/dist/types/tools/github.d.ts +144 -0
- package/dist/types/tools/glob.d.ts +44 -0
- package/dist/types/tools/goal.d.ts +42 -0
- package/dist/types/tools/grep.d.ts +64 -0
- package/dist/types/tools/grouped-file-output.d.ts +71 -0
- package/dist/types/tools/hashline-format.d.ts +28 -0
- package/dist/types/tools/hub.d.ts +340 -0
- package/dist/types/tools/index.d.ts +4 -0
- package/dist/types/tools/json-tree.d.ts +59 -0
- package/dist/types/tools/line-ranges.d.ts +29 -0
- package/dist/types/tools/list-limit.d.ts +15 -0
- package/dist/types/tools/lsp.d.ts +72 -0
- package/dist/types/tools/match-line-format.d.ts +11 -0
- package/dist/types/tools/mcp.d.ts +81 -0
- package/dist/types/tools/memory.d.ts +67 -0
- package/dist/types/tools/output-meta.d.ts +151 -0
- package/dist/types/tools/read.d.ts +139 -0
- package/dist/types/tools/renderer.d.ts +98 -0
- package/dist/types/tools/report-tool-issue.d.ts +8 -0
- package/dist/types/tools/resolve.d.ts +47 -0
- package/dist/types/tools/streaming-output.d.ts +282 -0
- package/dist/types/tools/subprocess.d.ts +24 -0
- package/dist/types/tools/task-repair-args.d.ts +53 -0
- package/dist/types/tools/task-yield-assembly.d.ts +20 -0
- package/dist/types/tools/task.d.ts +425 -0
- package/dist/types/tools/terminal-output.d.ts +8 -0
- package/dist/types/tools/think.d.ts +13 -0
- package/dist/types/tools/todo.d.ts +118 -0
- package/dist/types/tools/tool-errors.d.ts +7 -0
- package/dist/types/tools/vibe.d.ts +120 -0
- package/dist/types/tools/web-search.d.ts +195 -0
- package/dist/types/tools/write.d.ts +76 -0
- package/dist/types/tools/xd-url.d.ts +13 -0
- package/dist/types/tools/xdev.d.ts +52 -0
- package/dist/types/tui.d.ts +18 -0
- package/dist/types/utils.d.ts +2 -0
- package/package.json +37 -3
- package/src/app-keybindings.ts +760 -0
- package/src/apps/autoresearch-dashboard.ts +500 -0
- package/src/apps/autoresearch-data.ts +64 -0
- package/src/apps/cleanse-board.ts +341 -0
- package/src/apps/cleanse-picker.ts +57 -0
- package/src/apps/debug/log-formatting.ts +53 -0
- package/src/apps/debug/log-viewer.ts +868 -0
- package/src/apps/debug/protocol-probe.ts +267 -0
- package/src/apps/debug/raw-sse-buffer.ts +449 -0
- package/src/apps/debug/raw-sse.ts +282 -0
- package/src/apps/debug/terminal-info.ts +115 -0
- package/src/apps/debug/viewer-frame.ts +174 -0
- package/src/apps/git/avatar.ts +52 -0
- package/src/apps/git/colors.ts +109 -0
- package/src/apps/git/diff-pane.ts +1512 -0
- package/src/apps/git/git-tui.ts +889 -0
- package/src/apps/git/sidebar.ts +1204 -0
- package/src/apps/git/state.ts +113 -0
- package/src/apps/if-bench-board.ts +324 -0
- package/src/apps/live-visualizer.ts +238 -0
- package/src/apps/ps-data.ts +116 -0
- package/src/apps/ps-top.ts +422 -0
- package/src/apps/session-picker.ts +95 -0
- package/src/apps/setup-model-picker.ts +21 -0
- package/src/apps/standalone-picker.ts +111 -0
- package/src/autocomplete.ts +9 -13
- package/src/chat/advisor-message.ts +175 -0
- package/src/chat/assistant-message.ts +1214 -0
- package/src/chat/background-tan-message.ts +35 -0
- package/src/chat/bash-execution.ts +394 -0
- package/src/chat/cache-invalidation-marker.ts +94 -0
- package/src/chat/chat-transcript-builder.ts +593 -0
- package/src/chat/collab-prompt-message.ts +34 -0
- package/src/chat/compaction-summary-message.ts +261 -0
- package/src/chat/custom-message.ts +23 -0
- package/src/chat/display-preferences.ts +24 -0
- package/src/chat/eval-execution.ts +172 -0
- package/src/chat/execution-shared.ts +113 -0
- package/src/chat/extension-types.ts +46 -0
- package/src/chat/hook-message.ts +20 -0
- package/src/chat/image-loading.ts +122 -0
- package/src/chat/late-diagnostics-message.ts +92 -0
- package/src/chat/messages.ts +305 -0
- package/src/chat/reaction.ts +65 -0
- package/src/chat/read-target.ts +7 -0
- package/src/chat/read-tool-group.ts +873 -0
- package/src/chat/served-model-marker.ts +91 -0
- package/src/chat/skill-message.ts +220 -0
- package/src/chat/skill-title-input.ts +41 -0
- package/src/chat/stripped-tool-calls-placeholder.ts +35 -0
- package/src/chat/thinking-display.ts +312 -0
- package/src/chat/todo-reminder.ts +33 -0
- package/src/chat/tool-execution.ts +1395 -0
- package/src/chat/transcript-browser.ts +268 -0
- package/src/chat/transcript-entry.ts +96 -0
- package/src/chat/transcript-outline.ts +260 -0
- package/src/chat/transcript-render-helpers.ts +315 -0
- package/src/chat/ttsr-notification.ts +123 -0
- package/src/chat/user-message.ts +287 -0
- package/src/chrome/chat-block.ts +110 -0
- package/src/chrome/collab-qrcode.ts +74 -0
- package/src/chrome/context-thresholds.ts +85 -0
- package/src/chrome/countdown-timer.ts +74 -0
- package/src/chrome/diff.ts +266 -0
- package/src/chrome/dynamic-border.ts +36 -0
- package/src/chrome/error-block.ts +63 -0
- package/src/chrome/form-theme.ts +10 -0
- package/src/chrome/format.ts +66 -0
- package/src/chrome/index.ts +21 -0
- package/src/chrome/keybinding-hints.ts +55 -0
- package/src/chrome/live-board.ts +123 -0
- package/src/chrome/local-date.ts +20 -0
- package/src/chrome/message-divider.ts +51 -0
- package/src/chrome/message-frame.ts +164 -0
- package/src/chrome/message-notice.ts +103 -0
- package/src/chrome/overlay-box.ts +301 -0
- package/src/chrome/qrcode.ts +539 -0
- package/src/chrome/segment-track.ts +86 -0
- package/src/chrome/select-list-mouse-routing.ts +11 -0
- package/src/chrome/selector-helpers.ts +119 -0
- package/src/chrome/shared.ts +27 -0
- package/src/chrome/streaming-panel.ts +72 -0
- package/src/chrome/tool-activity.ts +44 -0
- package/src/chrome/transcript-container.ts +902 -0
- package/src/chrome/transcript-status.ts +27 -0
- package/src/chrome/visual-truncate.ts +70 -0
- package/src/components/disclosure.ts +235 -0
- package/src/components/editor.ts +10 -12
- package/src/components/form.ts +566 -0
- package/src/components/image.ts +80 -53
- package/src/components/input.ts +6 -27
- package/src/components/key-value-list.ts +88 -0
- package/src/components/layout/geometry.ts +202 -0
- package/src/components/layout/row.ts +283 -0
- package/src/components/layout/split-pane.ts +349 -0
- package/src/components/layout/stack.ts +261 -0
- package/src/components/markdown.ts +16 -12
- package/src/components/menu-selection.ts +248 -0
- package/src/components/metric.ts +132 -0
- package/src/components/progress-bar.ts +140 -0
- package/src/components/scroll-view.ts +372 -70
- package/src/components/scroll-viewport.ts +154 -0
- package/src/components/section.ts +77 -0
- package/src/components/select-list.ts +238 -159
- package/src/components/settings-list.ts +103 -80
- package/src/components/table.ts +127 -0
- package/src/components/tree-view.ts +481 -0
- package/src/components/wizard-step.ts +229 -0
- package/src/deccara.ts +1 -3
- package/src/hotkeys-markdown.ts +100 -0
- package/src/index.ts +18 -0
- package/src/keybinding-matchers.ts +99 -0
- package/src/keys.ts +2 -2
- package/src/lang-from-path.ts +252 -0
- package/src/overlays/advisor-config.ts +761 -0
- package/src/overlays/agent-activity.ts +113 -0
- package/src/overlays/agent-hub-projection.ts +250 -0
- package/src/overlays/agent-hub-renderer.ts +281 -0
- package/src/overlays/agent-hub-types.ts +74 -0
- package/src/overlays/agent-hub.ts +1562 -0
- package/src/overlays/agent-transcript-viewer.ts +669 -0
- package/src/overlays/agents-hub.ts +1203 -0
- package/src/overlays/ask-dialog.ts +1202 -0
- package/src/overlays/bordered-loader.ts +41 -0
- package/src/overlays/btw-history-panel.ts +591 -0
- package/src/overlays/btw-history.ts +33 -0
- package/src/overlays/btw-panel.ts +167 -0
- package/src/overlays/cleanse-panel.ts +167 -0
- package/src/overlays/codex-reset-fireworks.ts +375 -0
- package/src/overlays/composer-shape-preview.ts +137 -0
- package/src/overlays/composer-shape-registry.ts +82 -0
- package/src/overlays/copy-selector.ts +618 -0
- package/src/overlays/copy-targets.ts +210 -0
- package/src/overlays/error-banner.ts +39 -0
- package/src/overlays/extensions/display-text.ts +39 -0
- package/src/overlays/extensions/extension-dashboard.ts +645 -0
- package/src/overlays/extensions/extension-list.ts +667 -0
- package/src/overlays/extensions/inspector-model.ts +484 -0
- package/src/overlays/extensions/inspector-panel.ts +669 -0
- package/src/overlays/extensions/live-tool-session.ts +67 -0
- package/src/overlays/extensions/mcp-runtime.ts +289 -0
- package/src/overlays/extensions/state-manager.ts +337 -0
- package/src/overlays/extensions/types.ts +208 -0
- package/src/overlays/history-search.ts +268 -0
- package/src/overlays/hook-editor.ts +272 -0
- package/src/overlays/hook-input.ts +84 -0
- package/src/overlays/hook-selector.ts +635 -0
- package/src/overlays/hub-frame.ts +262 -0
- package/src/overlays/login-dialog.ts +231 -0
- package/src/overlays/logout-account-selector.ts +115 -0
- package/src/overlays/mcp-add-wizard.ts +1436 -0
- package/src/overlays/model-browser.ts +1176 -0
- package/src/overlays/model-hub.ts +2279 -0
- package/src/overlays/model-picker.ts +285 -0
- package/src/overlays/model-selector.ts +107 -0
- package/src/overlays/move-overlay.ts +197 -0
- package/src/overlays/oauth-selector.ts +446 -0
- package/src/overlays/omfg-panel.ts +136 -0
- package/src/overlays/pause-screen.ts +198 -0
- package/src/overlays/plan-review-overlay.ts +1227 -0
- package/src/overlays/plan-save-overlay.ts +74 -0
- package/src/overlays/plan-toc.ts +139 -0
- package/src/overlays/plugin-selector.ts +97 -0
- package/src/overlays/plugin-settings.ts +839 -0
- package/src/overlays/queue-mode-selector.ts +55 -0
- package/src/overlays/reset-usage-selector.ts +170 -0
- package/src/overlays/rewind-selector.ts +499 -0
- package/src/overlays/running-subagent-badge.ts +19 -0
- package/src/overlays/session-account-selector.ts +68 -0
- package/src/overlays/session-info-overlay.ts +96 -0
- package/src/overlays/session-observer-registry.ts +289 -0
- package/src/overlays/session-selector.ts +1137 -0
- package/src/overlays/settings-defs.ts +315 -0
- package/src/overlays/settings-selector.ts +1415 -0
- package/src/overlays/show-images-selector.ts +44 -0
- package/src/overlays/snapcompact-shape-preview-doc.md +14 -0
- package/src/overlays/snapcompact-shape-preview.ts +192 -0
- package/src/overlays/theme-selector.ts +62 -0
- package/src/overlays/thinking-selector.ts +51 -0
- package/src/overlays/tiny-title-download-progress.ts +102 -0
- package/src/overlays/tree-selector.ts +1121 -0
- package/src/overlays/usage-dashboard.ts +629 -0
- package/src/overlays/usage-display.ts +45 -0
- package/src/overlays/usage-row.ts +118 -0
- package/src/prompt/attachment-chips.ts +201 -0
- package/src/prompt/composer-attachments.ts +280 -0
- package/src/prompt/composer-cache.ts +304 -0
- package/src/prompt/composer.ts +934 -0
- package/src/prompt/custom-editor.ts +1392 -0
- package/src/prompt/data/emojis.json +1 -0
- package/src/prompt/editor-top-gap.ts +28 -0
- package/src/prompt/emoji-autocomplete.ts +285 -0
- package/src/prompt/github-ref-autocomplete.ts +75 -0
- package/src/prompt/gradient-highlight.ts +101 -0
- package/src/prompt/image-format.ts +28 -0
- package/src/prompt/image-references.ts +102 -0
- package/src/prompt/interactive-context-helpers.ts +126 -0
- package/src/prompt/internal-url-autocomplete.ts +155 -0
- package/src/prompt/macos-spelling.ts +456 -0
- package/src/prompt/magic-keyword-boundary.ts +23 -0
- package/src/prompt/magic-keywords.ts +42 -0
- package/src/prompt/markdown-prose.ts +245 -0
- package/src/prompt/model-mention-autocomplete.ts +94 -0
- package/src/prompt/model-mention-syntax.ts +41 -0
- package/src/prompt/orchestrate.ts +26 -0
- package/src/prompt/prompt-action-autocomplete.ts +337 -0
- package/src/prompt/queue-input.ts +132 -0
- package/src/prompt/skill-tokens.ts +36 -0
- package/src/prompt/tips.txt +27 -0
- package/src/prompt/tools-markdown.ts +29 -0
- package/src/prompt/ultrathink.ts +26 -0
- package/src/prompt/usage-amounts.ts +99 -0
- package/src/prompt/video.ts +47 -0
- package/src/prompt/welcome.ts +605 -0
- package/src/prompt/workflow.ts +26 -0
- package/src/render/code-cell.ts +244 -0
- package/src/render/file-list.ts +58 -0
- package/src/render/hyperlink.ts +192 -0
- package/src/render/index.ts +17 -0
- package/src/render/output-block.ts +267 -0
- package/src/render/output-pane.ts +299 -0
- package/src/render/render-utils.ts +1182 -0
- package/src/render/sixel.ts +69 -0
- package/src/render/status-line.ts +55 -0
- package/src/render/tool-card.ts +272 -0
- package/src/render/tree-list.ts +188 -0
- package/src/render/types.ts +19 -0
- package/src/render/utils.ts +109 -0
- package/src/render/width-aware-text.ts +60 -0
- package/src/setup/lazy.ts +17 -0
- package/src/setup/scenes/composer.ts +127 -0
- package/src/setup/scenes/glyph.ts +120 -0
- package/src/setup/scenes/model.ts +138 -0
- package/src/setup/scenes/outro.ts +24 -0
- package/src/setup/scenes/providers.ts +107 -0
- package/src/setup/scenes/sign-in.ts +373 -0
- package/src/setup/scenes/splash.ts +192 -0
- package/src/setup/scenes/theme.ts +343 -0
- package/src/setup/scenes/types.ts +111 -0
- package/src/setup/scenes/web-search.ts +165 -0
- package/src/setup/setup-version.ts +11 -0
- package/src/setup/startup-splash.ts +107 -0
- package/src/setup/wizard-overlay.ts +319 -0
- package/src/setup/wizard.ts +101 -0
- package/src/status-line/component.ts +3003 -0
- package/src/status-line/context-usage.ts +688 -0
- package/src/status-line/footer.ts +323 -0
- package/src/status-line/git-utils.ts +42 -0
- package/src/status-line/host.ts +97 -0
- package/src/status-line/index.ts +6 -0
- package/src/status-line/loop.ts +28 -0
- package/src/status-line/metrics.ts +94 -0
- package/src/status-line/presets.ts +107 -0
- package/src/status-line/schema.ts +57 -0
- package/src/status-line/segments.ts +934 -0
- package/src/status-line/separators.ts +55 -0
- package/src/status-line/types.ts +227 -0
- package/src/stdin-buffer.ts +4 -2
- package/src/theme/color.ts +95 -0
- package/src/theme/dark.json +95 -0
- package/src/theme/defaults/alabaster.json +93 -0
- package/src/theme/defaults/amethyst.json +96 -0
- package/src/theme/defaults/anthracite.json +93 -0
- package/src/theme/defaults/basalt.json +91 -0
- package/src/theme/defaults/birch.json +95 -0
- package/src/theme/defaults/dark-abyss.json +91 -0
- package/src/theme/defaults/dark-arctic.json +104 -0
- package/src/theme/defaults/dark-aurora.json +95 -0
- package/src/theme/defaults/dark-catppuccin.json +107 -0
- package/src/theme/defaults/dark-cavern.json +91 -0
- package/src/theme/defaults/dark-celestial.json +106 -0
- package/src/theme/defaults/dark-copper.json +95 -0
- package/src/theme/defaults/dark-cosmos.json +90 -0
- package/src/theme/defaults/dark-cyberpunk.json +102 -0
- package/src/theme/defaults/dark-dracula.json +98 -0
- package/src/theme/defaults/dark-eclipse.json +91 -0
- package/src/theme/defaults/dark-ember.json +95 -0
- package/src/theme/defaults/dark-equinox.json +90 -0
- package/src/theme/defaults/dark-forest.json +96 -0
- package/src/theme/defaults/dark-github.json +105 -0
- package/src/theme/defaults/dark-gruvbox.json +112 -0
- package/src/theme/defaults/dark-lavender.json +95 -0
- package/src/theme/defaults/dark-lunar.json +89 -0
- package/src/theme/defaults/dark-midnight.json +95 -0
- package/src/theme/defaults/dark-monochrome.json +94 -0
- package/src/theme/defaults/dark-monokai.json +98 -0
- package/src/theme/defaults/dark-nebula.json +90 -0
- package/src/theme/defaults/dark-nord.json +98 -0
- package/src/theme/defaults/dark-ocean.json +101 -0
- package/src/theme/defaults/dark-one.json +100 -0
- package/src/theme/defaults/dark-poimandres.json +115 -0
- package/src/theme/defaults/dark-rainforest.json +91 -0
- package/src/theme/defaults/dark-reef.json +91 -0
- package/src/theme/defaults/dark-retro.json +92 -0
- package/src/theme/defaults/dark-rose-pine.json +96 -0
- package/src/theme/defaults/dark-sakura.json +95 -0
- package/src/theme/defaults/dark-slate.json +95 -0
- package/src/theme/defaults/dark-solarized.json +97 -0
- package/src/theme/defaults/dark-solstice.json +90 -0
- package/src/theme/defaults/dark-starfall.json +91 -0
- package/src/theme/defaults/dark-sunset.json +99 -0
- package/src/theme/defaults/dark-swamp.json +90 -0
- package/src/theme/defaults/dark-synthwave.json +103 -0
- package/src/theme/defaults/dark-taiga.json +91 -0
- package/src/theme/defaults/dark-terminal.json +95 -0
- package/src/theme/defaults/dark-tokyo-night.json +101 -0
- package/src/theme/defaults/dark-tundra.json +91 -0
- package/src/theme/defaults/dark-twilight.json +91 -0
- package/src/theme/defaults/dark-volcanic.json +91 -0
- package/src/theme/defaults/graphite.json +92 -0
- package/src/theme/defaults/index.ts +201 -0
- package/src/theme/defaults/light-arctic.json +107 -0
- package/src/theme/defaults/light-aurora-day.json +91 -0
- package/src/theme/defaults/light-canyon.json +91 -0
- package/src/theme/defaults/light-catppuccin.json +106 -0
- package/src/theme/defaults/light-cirrus.json +90 -0
- package/src/theme/defaults/light-coral.json +95 -0
- package/src/theme/defaults/light-cyberpunk.json +96 -0
- package/src/theme/defaults/light-dawn.json +90 -0
- package/src/theme/defaults/light-dunes.json +91 -0
- package/src/theme/defaults/light-eucalyptus.json +95 -0
- package/src/theme/defaults/light-forest.json +100 -0
- package/src/theme/defaults/light-frost.json +95 -0
- package/src/theme/defaults/light-github.json +115 -0
- package/src/theme/defaults/light-glacier.json +91 -0
- package/src/theme/defaults/light-gruvbox.json +108 -0
- package/src/theme/defaults/light-haze.json +90 -0
- package/src/theme/defaults/light-honeycomb.json +95 -0
- package/src/theme/defaults/light-lagoon.json +91 -0
- package/src/theme/defaults/light-lavender.json +95 -0
- package/src/theme/defaults/light-meadow.json +91 -0
- package/src/theme/defaults/light-mint.json +95 -0
- package/src/theme/defaults/light-monochrome.json +101 -0
- package/src/theme/defaults/light-ocean.json +99 -0
- package/src/theme/defaults/light-one.json +99 -0
- package/src/theme/defaults/light-opal.json +91 -0
- package/src/theme/defaults/light-orchard.json +91 -0
- package/src/theme/defaults/light-paper.json +95 -0
- package/src/theme/defaults/light-poimandres.json +115 -0
- package/src/theme/defaults/light-prism.json +90 -0
- package/src/theme/defaults/light-retro.json +98 -0
- package/src/theme/defaults/light-sand.json +95 -0
- package/src/theme/defaults/light-savanna.json +91 -0
- package/src/theme/defaults/light-solarized.json +102 -0
- package/src/theme/defaults/light-soleil.json +90 -0
- package/src/theme/defaults/light-sunset.json +99 -0
- package/src/theme/defaults/light-synthwave.json +98 -0
- package/src/theme/defaults/light-tokyo-night.json +111 -0
- package/src/theme/defaults/light-wetland.json +91 -0
- package/src/theme/defaults/light-zenith.json +89 -0
- package/src/theme/defaults/limestone.json +94 -0
- package/src/theme/defaults/mahogany.json +97 -0
- package/src/theme/defaults/marble.json +93 -0
- package/src/theme/defaults/obsidian.json +91 -0
- package/src/theme/defaults/onyx.json +91 -0
- package/src/theme/defaults/pearl.json +93 -0
- package/src/theme/defaults/porcelain.json +91 -0
- package/src/theme/defaults/quartz.json +96 -0
- package/src/theme/defaults/sandstone.json +95 -0
- package/src/theme/defaults/titanium.json +90 -0
- package/src/theme/index.ts +5 -0
- package/src/theme/light.json +93 -0
- package/src/theme/loader.ts +200 -0
- package/src/theme/mermaid-cache.ts +92 -0
- package/src/theme/schema-validation.ts +119 -0
- package/src/theme/schema.ts +168 -0
- package/src/theme/session-color.ts +253 -0
- package/src/theme/shimmer.ts +307 -0
- package/src/theme/symbols.ts +1433 -0
- package/src/theme/theme-class.ts +787 -0
- package/src/theme/theme-schema.json +463 -0
- package/src/theme/theme.ts +810 -0
- package/src/theme/tui-adapters.ts +339 -0
- package/src/thinking.ts +380 -0
- package/src/tools/agent-tree.ts +128 -0
- package/src/tools/ask.ts +482 -0
- package/src/tools/ast-edit.ts +210 -0
- package/src/tools/ast-grep.ts +171 -0
- package/src/tools/autoresearch.ts +287 -0
- package/src/tools/bash-interactive.ts +295 -0
- package/src/tools/bash.ts +468 -0
- package/src/tools/conflict-detect.ts +77 -0
- package/src/tools/debug.ts +167 -0
- package/src/tools/default-renderer.ts +172 -0
- package/src/tools/edit.ts +1258 -0
- package/src/tools/eval-format/code-mode-declarations.ts +61 -0
- package/src/tools/eval-format/index.ts +16 -0
- package/src/tools/eval-format/javascript.ts +952 -0
- package/src/tools/eval-format/python.ts +544 -0
- package/src/tools/eval.ts +835 -0
- package/src/tools/fetch.ts +226 -0
- package/src/tools/gh-format.ts +18 -0
- package/src/tools/github.ts +686 -0
- package/src/tools/glob.ts +213 -0
- package/src/tools/goal.ts +153 -0
- package/src/tools/grep.ts +385 -0
- package/src/tools/grouped-file-output.ts +211 -0
- package/src/tools/hashline-format.ts +59 -0
- package/src/tools/hub.ts +1488 -0
- package/src/tools/index.ts +71 -0
- package/src/tools/json-tree.ts +388 -0
- package/src/tools/line-ranges.ts +98 -0
- package/src/tools/list-limit.ts +43 -0
- package/src/tools/lsp.ts +702 -0
- package/src/tools/match-line-format.ts +20 -0
- package/src/tools/mcp.ts +309 -0
- package/src/tools/memory.ts +220 -0
- package/src/tools/output-meta.ts +367 -0
- package/src/tools/read.ts +628 -0
- package/src/tools/renderer.ts +107 -0
- package/src/tools/report-tool-issue.ts +13 -0
- package/src/tools/resolve.ts +139 -0
- package/src/tools/streaming-output.ts +1576 -0
- package/src/tools/subprocess.ts +39 -0
- package/src/tools/task-repair-args.ts +118 -0
- package/src/tools/task-yield-assembly.ts +147 -0
- package/src/tools/task.ts +2110 -0
- package/src/tools/terminal-output.ts +153 -0
- package/src/tools/think.ts +28 -0
- package/src/tools/todo.ts +580 -0
- package/src/tools/tool-errors.ts +15 -0
- package/src/tools/vibe.ts +420 -0
- package/src/tools/web-search.ts +409 -0
- package/src/tools/write.ts +511 -0
- package/src/tools/xd-url.ts +28 -0
- package/src/tools/xdev.ts +201 -0
- package/src/tui.ts +70 -8
- 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
|
+
}
|