@oh-my-pi/pi-tui 18.2.4 → 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 +23 -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/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/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 +57 -4
- package/src/utils.ts +8 -0
|
@@ -0,0 +1,1202 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Component,
|
|
3
|
+
Ellipsis,
|
|
4
|
+
Markdown,
|
|
5
|
+
type MarkdownTheme,
|
|
6
|
+
matchesKey,
|
|
7
|
+
padding,
|
|
8
|
+
renderInlineMarkdown,
|
|
9
|
+
replaceTabs,
|
|
10
|
+
ScrollView,
|
|
11
|
+
type Tab,
|
|
12
|
+
TabBar,
|
|
13
|
+
Text,
|
|
14
|
+
type TUI,
|
|
15
|
+
truncateToWidth,
|
|
16
|
+
visibleWidth,
|
|
17
|
+
wrapTextWithAnsi,
|
|
18
|
+
} from "../index";
|
|
19
|
+
/** One selectable answer in an ask dialog. */
|
|
20
|
+
export interface ExtensionAskDialogOption {
|
|
21
|
+
label: string;
|
|
22
|
+
description?: string;
|
|
23
|
+
preview?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Question and answer choices displayed by the ask dialog. */
|
|
27
|
+
export interface ExtensionAskDialogQuestion {
|
|
28
|
+
id: string;
|
|
29
|
+
question: string;
|
|
30
|
+
header?: string;
|
|
31
|
+
options: ExtensionAskDialogOption[];
|
|
32
|
+
multi?: boolean;
|
|
33
|
+
recommended?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Submitted answer to one dialog question. */
|
|
37
|
+
export interface ExtensionAskDialogResultItem {
|
|
38
|
+
id: string;
|
|
39
|
+
question: string;
|
|
40
|
+
options: string[];
|
|
41
|
+
multi: boolean;
|
|
42
|
+
selectedOptions: string[];
|
|
43
|
+
customInput?: string;
|
|
44
|
+
note?: string;
|
|
45
|
+
timedOut?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Answers submitted from the dialog. */
|
|
49
|
+
export interface ExtensionAskDialogSubmitResult {
|
|
50
|
+
kind: "submit";
|
|
51
|
+
results: ExtensionAskDialogResultItem[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Request to discuss the question instead of answering it. */
|
|
55
|
+
export interface ExtensionAskDialogChatResult {
|
|
56
|
+
kind: "chat";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Completed dialog action, distinct from cancellation. */
|
|
60
|
+
export type ExtensionAskDialogResult = ExtensionAskDialogSubmitResult | ExtensionAskDialogChatResult;
|
|
61
|
+
import { disambiguateDisplayLabels, expandKeyHint, sanitizeCarriageReturns } from "../render/render-utils";
|
|
62
|
+
import { getTabBarTheme } from "../chrome/shared";
|
|
63
|
+
import { getMarkdownTheme, highlightCode, theme } from "../theme/theme";
|
|
64
|
+
import {
|
|
65
|
+
matchesAppToolsExpand,
|
|
66
|
+
matchesSelectCancel,
|
|
67
|
+
matchesSelectDown,
|
|
68
|
+
matchesSelectPageDown,
|
|
69
|
+
matchesSelectPageUp,
|
|
70
|
+
matchesSelectUp,
|
|
71
|
+
} from "../keybinding-matchers";
|
|
72
|
+
import { optionMarker } from "../tools/ask";
|
|
73
|
+
import { CountdownTimer } from "../chrome/countdown-timer";
|
|
74
|
+
import { editorKey } from "../chrome/keybinding-hints";
|
|
75
|
+
import { OverlayPanel, PanelDivider, PanelRows } from "../chrome/overlay-box";
|
|
76
|
+
import { handleTabSwitchKey } from "../chrome/selector-helpers";
|
|
77
|
+
|
|
78
|
+
const OTHER_OPTION = "Other (type your own)";
|
|
79
|
+
const SUBMIT_OPTION = "Submit";
|
|
80
|
+
|
|
81
|
+
// Action rows appended by the guest race participant. An option sanitizing
|
|
82
|
+
// to one of these must disambiguate identically on both sides, or the same
|
|
83
|
+
// question renders different rows depending on who answers.
|
|
84
|
+
const GUEST_ACTION_LABELS = ["Chat about this", "Next →"];
|
|
85
|
+
|
|
86
|
+
/** Fraction of the terminal the dialog may occupy. The box height is fixed
|
|
87
|
+
* at spawn from the tallest tab's content (re-measured only on viewport
|
|
88
|
+
* resize) and clamped to this ratio; it rises from the bottom as a stable
|
|
89
|
+
* panel that never resizes on tab switches or cursor moves. */
|
|
90
|
+
const DIALOG_HEIGHT_RATIO = 0.7;
|
|
91
|
+
const MIN_DIALOG_ROWS = 12;
|
|
92
|
+
const MIN_BODY_ROWS = 5;
|
|
93
|
+
const MAX_HEADER_CHIP_WIDTH = 16;
|
|
94
|
+
/** Maximum number of title lines shown in the prompt editor overlay, so a
|
|
95
|
+
* long or multi-line question cannot push the input row off-screen. Mirrors
|
|
96
|
+
* the bounded-title pattern from the legacy ask path without its option-window
|
|
97
|
+
* coupling. */
|
|
98
|
+
const MAX_PROMPT_TITLE_ROWS = 3;
|
|
99
|
+
/** Border (2) + padX (2) columns consumed by the HookEditor chrome. */
|
|
100
|
+
const PROMPT_TITLE_CHROME_COLUMNS = 4;
|
|
101
|
+
/** Maximum number of wrapped lines for an in-body question header, so a long
|
|
102
|
+
* or multi-line question cannot push the option list off-screen. Mirrors the
|
|
103
|
+
* row-cap pattern used by boundPromptTitle for the prompt editor overlay. */
|
|
104
|
+
const MAX_HEADER_ROWS = 4;
|
|
105
|
+
/** Maximum number of wrapped lines shown for an option description before
|
|
106
|
+
* Ctrl+O expansion. Mirrors the header row-cap above so long descriptions
|
|
107
|
+
* cannot push later options off-screen. */
|
|
108
|
+
const MAX_DESC_ROWS = 2;
|
|
109
|
+
|
|
110
|
+
function promptTitleContentWidth(): number {
|
|
111
|
+
const cols = process.stdout.columns ?? 80;
|
|
112
|
+
return Math.max(1, cols - PROMPT_TITLE_CHROME_COLUMNS);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Bound a prompt editor title to a fixed row/width budget so long or
|
|
116
|
+
* multi-line questions stay usable inside the small prompt overlay. */
|
|
117
|
+
export function boundPromptTitle(prefix: string, question: string): string {
|
|
118
|
+
const width = promptTitleContentWidth();
|
|
119
|
+
const flat = normalizedInlineInput(`${prefix}${question}`);
|
|
120
|
+
const wrapped = wrapTextWithAnsi(flat, width);
|
|
121
|
+
if (wrapped.length <= MAX_PROMPT_TITLE_ROWS) return wrapped.join("\n");
|
|
122
|
+
const kept = wrapped.slice(0, MAX_PROMPT_TITLE_ROWS - 1);
|
|
123
|
+
const last = truncateToWidth(wrapped[MAX_PROMPT_TITLE_ROWS - 1] ?? "", width, Ellipsis.Unicode);
|
|
124
|
+
return [...kept, last].join("\n");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
interface AskDialogCallbacks {
|
|
128
|
+
onSubmit(result: ExtensionAskDialogSubmitResult): void;
|
|
129
|
+
onCancel(): void;
|
|
130
|
+
onPrompt(title: string, prefill?: string): Promise<string | undefined>;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
interface AskDialogInputGuard {
|
|
134
|
+
isBlocked(): boolean;
|
|
135
|
+
handleInput(keyData: string): void;
|
|
136
|
+
hint: string;
|
|
137
|
+
/** Mirror the guard's blocked state onto the proxied draft surface each
|
|
138
|
+
* render, so a draft that owns input shows a visible insertion cursor even
|
|
139
|
+
* though this dialog holds TUI focus. */
|
|
140
|
+
syncPresentation?(): void;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
interface AskDialogOptions {
|
|
144
|
+
timeout?: number;
|
|
145
|
+
onTimeout?: () => void;
|
|
146
|
+
tui?: TUI;
|
|
147
|
+
inputGuard?: AskDialogInputGuard;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
interface QuestionState {
|
|
151
|
+
selectedOptions: Set<string>;
|
|
152
|
+
customInput: string | undefined;
|
|
153
|
+
note: string | undefined;
|
|
154
|
+
noteRowKey: string | undefined;
|
|
155
|
+
cursorIndex: number;
|
|
156
|
+
scrollOffset: number;
|
|
157
|
+
manualScroll: boolean;
|
|
158
|
+
timedOut: boolean;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
type QuestionRowKind = "option" | "other";
|
|
162
|
+
|
|
163
|
+
interface QuestionRow {
|
|
164
|
+
kind: QuestionRowKind;
|
|
165
|
+
key: string;
|
|
166
|
+
label: string;
|
|
167
|
+
optionIndex: number | undefined;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
interface RenderedList {
|
|
171
|
+
lines: string[];
|
|
172
|
+
scrollOffset: number;
|
|
173
|
+
indicator: string;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
interface PreviewSegment {
|
|
177
|
+
kind: "markdown" | "code";
|
|
178
|
+
text: string;
|
|
179
|
+
language: string | undefined;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
type PreviewRenderCache = Map<string, Map<number, readonly string[]>>;
|
|
183
|
+
|
|
184
|
+
function clamp(value: number, min: number, max: number): number {
|
|
185
|
+
return Math.max(min, Math.min(value, max));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function questionTabLabel(question: ExtensionAskDialogQuestion, index: number): string {
|
|
189
|
+
const base = question.header?.trim() || sanitizeCarriageReturns(question.id) || `Q${index + 1}`;
|
|
190
|
+
return truncateToWidth(replaceTabs(base), MAX_HEADER_CHIP_WIDTH, Ellipsis.Unicode);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function wrapQuestionTitle(question: ExtensionAskDialogQuestion, width: number): string[] {
|
|
194
|
+
const mdTheme = getMarkdownTheme();
|
|
195
|
+
const questionText = renderInlineMarkdown(replaceTabs(sanitizeCarriageReturns(question.question)), mdTheme, t =>
|
|
196
|
+
theme.fg("text", t),
|
|
197
|
+
);
|
|
198
|
+
return wrapTextWithAnsi(questionText, Math.max(1, width));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function renderQuestionTitle(question: ExtensionAskDialogQuestion, width: number, maxRows = MAX_HEADER_ROWS): string[] {
|
|
202
|
+
const wrapped = wrapQuestionTitle(question, width);
|
|
203
|
+
if (wrapped.length <= maxRows) return wrapped;
|
|
204
|
+
return [
|
|
205
|
+
...wrapped.slice(0, maxRows - 1),
|
|
206
|
+
truncateToWidth(wrapped.slice(maxRows - 1).join(" "), Math.max(1, width), Ellipsis.Unicode),
|
|
207
|
+
];
|
|
208
|
+
}
|
|
209
|
+
/** Wrap an option description exactly as the option list renders it, so
|
|
210
|
+
* overflow detection and expanded rendering stay in sync with the row. */
|
|
211
|
+
function wrapOptionDescription(description: string, contentWidth: number): string[] {
|
|
212
|
+
const mdTheme = getMarkdownTheme();
|
|
213
|
+
const rendered = renderInlineMarkdown(description.trim(), mdTheme, t => theme.fg("muted", t));
|
|
214
|
+
return wrapTextWithAnsi(rendered, Math.max(1, contentWidth - 6));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** True when any option description of the question wraps past the collapsed
|
|
218
|
+
* row-cap at the given content width. */
|
|
219
|
+
function questionDescriptionsOverflow(question: ExtensionAskDialogQuestion, contentWidth: number): boolean {
|
|
220
|
+
return question.options.some(option => {
|
|
221
|
+
if (!option?.description?.trim()) return false;
|
|
222
|
+
return wrapOptionDescription(option.description, contentWidth).length > MAX_DESC_ROWS;
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function splitPreviewSegments(preview: string): PreviewSegment[] {
|
|
227
|
+
const segments: PreviewSegment[] = [];
|
|
228
|
+
const markdownBuffer: string[] = [];
|
|
229
|
+
let fenceChar: string | undefined;
|
|
230
|
+
let fenceLength = 0;
|
|
231
|
+
let fenceLanguage: string | undefined;
|
|
232
|
+
let codeBuffer: string[] = [];
|
|
233
|
+
|
|
234
|
+
const flushMarkdown = (): void => {
|
|
235
|
+
if (markdownBuffer.length === 0) return;
|
|
236
|
+
segments.push({ kind: "markdown", text: markdownBuffer.join("\n"), language: undefined });
|
|
237
|
+
markdownBuffer.length = 0;
|
|
238
|
+
};
|
|
239
|
+
const flushCode = (): void => {
|
|
240
|
+
segments.push({ kind: "code", text: codeBuffer.join("\n"), language: fenceLanguage });
|
|
241
|
+
codeBuffer = [];
|
|
242
|
+
fenceChar = undefined;
|
|
243
|
+
fenceLength = 0;
|
|
244
|
+
fenceLanguage = undefined;
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
for (const line of replaceTabs(preview).split("\n")) {
|
|
248
|
+
const fenceMatch = /^(\s{0,3})(`{3,}|~{3,})(.*)$/.exec(line);
|
|
249
|
+
if (fenceChar !== undefined) {
|
|
250
|
+
if (fenceMatch) {
|
|
251
|
+
const marker = fenceMatch[2] ?? "";
|
|
252
|
+
const info = fenceMatch[3]?.trim() ?? "";
|
|
253
|
+
if (marker.startsWith(fenceChar) && marker.length >= fenceLength && info === "") {
|
|
254
|
+
flushCode();
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
codeBuffer.push(line);
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (fenceMatch) {
|
|
262
|
+
flushMarkdown();
|
|
263
|
+
const marker = fenceMatch[2] ?? "";
|
|
264
|
+
fenceChar = marker[0];
|
|
265
|
+
fenceLength = marker.length;
|
|
266
|
+
fenceLanguage = fenceMatch[3]?.trim().split(/\s+/, 1)[0] || undefined;
|
|
267
|
+
codeBuffer = [];
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
markdownBuffer.push(line);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (fenceChar !== undefined) {
|
|
274
|
+
segments.push({ kind: "code", text: codeBuffer.join("\n"), language: fenceLanguage });
|
|
275
|
+
} else {
|
|
276
|
+
flushMarkdown();
|
|
277
|
+
}
|
|
278
|
+
return segments;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function renderPreviewContent(preview: string, width: number): string[] {
|
|
282
|
+
const out: string[] = [];
|
|
283
|
+
const mdTheme = getMarkdownTheme();
|
|
284
|
+
const accentStyle = { color: (text: string) => theme.fg("muted", text) };
|
|
285
|
+
for (const segment of splitPreviewSegments(preview)) {
|
|
286
|
+
if (segment.kind === "code") {
|
|
287
|
+
const highlighted = highlightCode(segment.text, segment.language);
|
|
288
|
+
const text = new Text(highlighted.join("\n"), 0, 0);
|
|
289
|
+
out.push(...text.render(Math.max(1, width)));
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
const markdown = new Markdown(segment.text, 0, 0, mdTheme, accentStyle);
|
|
293
|
+
out.push(...markdown.render(Math.max(1, width)));
|
|
294
|
+
}
|
|
295
|
+
return out;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function renderCachedPreview(cache: PreviewRenderCache, preview: string, width: number): readonly string[] {
|
|
299
|
+
let byWidth = cache.get(preview);
|
|
300
|
+
if (!byWidth) {
|
|
301
|
+
byWidth = new Map();
|
|
302
|
+
cache.set(preview, byWidth);
|
|
303
|
+
}
|
|
304
|
+
let rendered = byWidth.get(width);
|
|
305
|
+
if (!rendered) {
|
|
306
|
+
rendered = renderPreviewContent(preview, width).map(line => ` ${theme.fg("border", "│")} ${line}`);
|
|
307
|
+
byWidth.set(width, rendered);
|
|
308
|
+
}
|
|
309
|
+
return rendered;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function pageKeysLabel(): string {
|
|
313
|
+
const pageUp = editorKey("tui.select.pageUp");
|
|
314
|
+
const pageDown = editorKey("tui.select.pageDown");
|
|
315
|
+
return `${pageUp === "pageup" ? "PgUp" : pageUp}/${pageDown === "pagedown" ? "PgDn" : pageDown}`;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function cancelKeyLabel(): string {
|
|
319
|
+
const [key = ""] = editorKey("tui.select.cancel").split("/");
|
|
320
|
+
return key === "escape" ? "Esc" : key;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function normalizedInlineInput(input: string): string {
|
|
324
|
+
return replaceTabs(input).replace(/\s+/g, " ").trim();
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Final display labels for a question's options: sanitized, badged, unique,
|
|
329
|
+
* and sentinel-safe. The recommendation badge goes on BEFORE collision
|
|
330
|
+
* disambiguation — badging itself can collide two rows (`Retry\rnow`
|
|
331
|
+
* recommended vs a literal `Retry now (Recommended)`). Mirrors the guest
|
|
332
|
+
* selector (`#runGuestAskQuestion`): same inputs, same rows, whichever
|
|
333
|
+
* participant answers. State and results keep originals.
|
|
334
|
+
*/
|
|
335
|
+
function displayOptionLabels(question: ExtensionAskDialogQuestion): string[] {
|
|
336
|
+
const recommendedSuffix = " (Recommended)";
|
|
337
|
+
const badged = question.options.map((option, index) => {
|
|
338
|
+
const base = sanitizeCarriageReturns(option.label);
|
|
339
|
+
return question.recommended === index && !base.endsWith(recommendedSuffix) ? `${base}${recommendedSuffix}` : base;
|
|
340
|
+
});
|
|
341
|
+
return disambiguateDisplayLabels(badged, [OTHER_OPTION, ...GUEST_ACTION_LABELS]);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function renderAnswerSummary(question: ExtensionAskDialogQuestion, state: QuestionState): string {
|
|
345
|
+
const display = displayOptionLabels(question);
|
|
346
|
+
const selected = question.options
|
|
347
|
+
.map((option, index) => ({
|
|
348
|
+
raw: option.label,
|
|
349
|
+
display: display[index] ?? sanitizeCarriageReturns(option.label),
|
|
350
|
+
}))
|
|
351
|
+
.filter(entry => state.selectedOptions.has(entry.raw))
|
|
352
|
+
.map(entry => entry.display);
|
|
353
|
+
if (question.multi) {
|
|
354
|
+
const answers = [...selected];
|
|
355
|
+
if (state.customInput !== undefined) answers.push(`Other: “${normalizedInlineInput(state.customInput)}”`);
|
|
356
|
+
return answers.length > 0 ? answers.join(", ") : theme.fg("warning", "unanswered");
|
|
357
|
+
}
|
|
358
|
+
if (state.customInput !== undefined) return `“${normalizedInlineInput(state.customInput)}”`;
|
|
359
|
+
if (selected.length === 0) return theme.fg("warning", "unanswered");
|
|
360
|
+
return selected[0] ?? theme.fg("warning", "unanswered");
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function clearNote(state: QuestionState): void {
|
|
364
|
+
state.note = undefined;
|
|
365
|
+
state.noteRowKey = undefined;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function clearNoteIfRow(state: QuestionState, rowKey: string): void {
|
|
369
|
+
if (state.noteRowKey === rowKey) clearNote(state);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function clearNoteUnlessRow(state: QuestionState, rowKey: string): void {
|
|
373
|
+
if (state.noteRowKey !== undefined && state.noteRowKey !== rowKey) clearNote(state);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function noteForSubmittedAnswer(question: ExtensionAskDialogQuestion, state: QuestionState): string | undefined {
|
|
377
|
+
if (state.note === undefined || state.noteRowKey === undefined) return undefined;
|
|
378
|
+
if (state.noteRowKey === "other") return state.customInput !== undefined ? state.note : undefined;
|
|
379
|
+
const match = /^option:(\d+)$/.exec(state.noteRowKey);
|
|
380
|
+
const optionIndex = match?.[1] === undefined ? Number.NaN : Number.parseInt(match[1], 10);
|
|
381
|
+
const option = Number.isInteger(optionIndex) ? question.options[optionIndex] : undefined;
|
|
382
|
+
return option && state.selectedOptions.has(option.label) ? state.note : undefined;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function renderRowLabel(
|
|
386
|
+
rowItem: QuestionRow,
|
|
387
|
+
question: ExtensionAskDialogQuestion,
|
|
388
|
+
state: QuestionState,
|
|
389
|
+
selected: boolean,
|
|
390
|
+
mdTheme: MarkdownTheme,
|
|
391
|
+
previewCache: PreviewRenderCache,
|
|
392
|
+
width: number,
|
|
393
|
+
expanded = false,
|
|
394
|
+
): string[] {
|
|
395
|
+
const isOption = rowItem.kind === "option";
|
|
396
|
+
const isOther = rowItem.kind === "other";
|
|
397
|
+
const option = isOption ? question.options[rowItem.optionIndex ?? -1] : undefined;
|
|
398
|
+
const checked =
|
|
399
|
+
option !== undefined ? state.selectedOptions.has(option.label) : isOther && state.customInput !== undefined;
|
|
400
|
+
const color = selected ? "accent" : checked ? "toolOutput" : "text";
|
|
401
|
+
const marker = `${theme.fg(checked ? "success" : "dim", optionMarker(theme, question.multi, checked))} `;
|
|
402
|
+
const cursor = selected ? theme.fg("accent", `${theme.nav.cursor} `) : " ";
|
|
403
|
+
const label = renderInlineMarkdown(rowItem.label, mdTheme, t => theme.fg(color, t));
|
|
404
|
+
const noteMarker = state.note && state.noteRowKey === rowItem.key ? theme.fg("success", " ✎ note") : "";
|
|
405
|
+
// `width` is already the inner content width supplied by OverlayPanel; when
|
|
406
|
+
// a scrollbar is needed, renderRows() calls this again with one less column.
|
|
407
|
+
// Keep the cursor, option marker, first wrapped label line, and optional
|
|
408
|
+
// note marker within that budget so the outer fit() never truncates them.
|
|
409
|
+
const noteWidth = noteMarker ? visibleWidth(noteMarker) : 0;
|
|
410
|
+
const labelWidth = Math.max(1, width - visibleWidth(cursor) - visibleWidth(marker) - noteWidth);
|
|
411
|
+
const wrappedLabel = wrapTextWithAnsi(label, labelWidth);
|
|
412
|
+
const indent = padding(visibleWidth(cursor) + visibleWidth(marker));
|
|
413
|
+
const lines = [`${cursor}${marker}${wrappedLabel[0] ?? ""}${noteMarker}`];
|
|
414
|
+
for (let i = 1; i < wrappedLabel.length; i++) {
|
|
415
|
+
lines.push(`${indent}${wrappedLabel[i] ?? ""}`);
|
|
416
|
+
}
|
|
417
|
+
if (rowItem.kind === "option") {
|
|
418
|
+
const option = question.options[rowItem.optionIndex ?? -1];
|
|
419
|
+
if (option?.description?.trim()) {
|
|
420
|
+
const wrapped = wrapOptionDescription(option.description, width);
|
|
421
|
+
const shown = expanded ? wrapped : wrapped.slice(0, MAX_DESC_ROWS);
|
|
422
|
+
for (const line of shown) {
|
|
423
|
+
lines.push(` ${truncateToWidth(line, Math.max(1, width - 6), Ellipsis.Unicode)}`);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
if (option?.preview?.trim()) {
|
|
427
|
+
const previewWidth = Math.max(1, width - 8);
|
|
428
|
+
lines.push(...renderCachedPreview(previewCache, option.preview, previewWidth));
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
if (isOther && state.customInput !== undefined) {
|
|
432
|
+
const preview = replaceTabs(state.customInput).replace(/\s+/g, " ").trim();
|
|
433
|
+
lines.push(theme.fg("muted", ` ${truncateToWidth(preview, Math.max(1, width - 6), Ellipsis.Unicode)}`));
|
|
434
|
+
}
|
|
435
|
+
return lines;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Coerce untrusted dialog questions into a render-safe shape. The live ask
|
|
440
|
+
* dialog is reached from the public `askDialog` extension surface and from
|
|
441
|
+
* streamed tool args, where a question entry can arrive with a missing or
|
|
442
|
+
* non-string `question` field. The render helpers (`replaceTabs`,
|
|
443
|
+
* `renderQuestionTitle`, `questionTabLabel`) assume strings, so a malformed
|
|
444
|
+
* entry throws and takes down the whole TUI render loop. Mirrors
|
|
445
|
+
* `normalizeRenderQuestions` on the transcript path.
|
|
446
|
+
*/
|
|
447
|
+
export function normalizeDialogQuestions(questions: ExtensionAskDialogQuestion[]): ExtensionAskDialogQuestion[] {
|
|
448
|
+
if (!Array.isArray(questions)) return [];
|
|
449
|
+
const out: ExtensionAskDialogQuestion[] = [];
|
|
450
|
+
for (const entry of questions) {
|
|
451
|
+
if (!entry || typeof entry !== "object") continue;
|
|
452
|
+
const q = entry as Partial<ExtensionAskDialogQuestion>;
|
|
453
|
+
const options: ExtensionAskDialogOption[] = [];
|
|
454
|
+
if (Array.isArray(q.options)) {
|
|
455
|
+
for (const opt of q.options) {
|
|
456
|
+
if (!opt || typeof opt !== "object") continue;
|
|
457
|
+
const o = opt as Partial<ExtensionAskDialogOption>;
|
|
458
|
+
options.push({
|
|
459
|
+
// The label is a caller-supplied correlation key echoed verbatim
|
|
460
|
+
// in results (matching the guest path) — sanitize only the
|
|
461
|
+
// display copy (`displayOptionLabels`).
|
|
462
|
+
label: typeof o.label === "string" ? o.label : "",
|
|
463
|
+
...(typeof o.description === "string" ? { description: sanitizeCarriageReturns(o.description) } : {}),
|
|
464
|
+
...(typeof o.preview === "string" ? { preview: sanitizeCarriageReturns(o.preview) } : {}),
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
out.push({
|
|
469
|
+
// The id is a caller-supplied correlation key echoed verbatim in
|
|
470
|
+
// results — sanitize only the display copy (`questionTabLabel`).
|
|
471
|
+
id: typeof q.id === "string" ? q.id : "?",
|
|
472
|
+
// The question is echoed verbatim in results (matching the guest
|
|
473
|
+
// path) — sanitize only the display copy (`wrapQuestionTitle`).
|
|
474
|
+
question: typeof q.question === "string" ? q.question : "",
|
|
475
|
+
...(typeof q.header === "string" ? { header: sanitizeCarriageReturns(q.header) } : {}),
|
|
476
|
+
options,
|
|
477
|
+
...(typeof q.multi === "boolean" ? { multi: q.multi } : {}),
|
|
478
|
+
...(Number.isInteger(q.recommended) ? { recommended: q.recommended } : {}),
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
return out;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
export class AskDialogComponent implements Component {
|
|
485
|
+
#states: QuestionState[];
|
|
486
|
+
#activeTabIndex = 0;
|
|
487
|
+
#submitScrollOffset = 0;
|
|
488
|
+
#bodyRows = MIN_BODY_ROWS;
|
|
489
|
+
#questionCanPage = false;
|
|
490
|
+
#remainingSeconds: number | undefined;
|
|
491
|
+
#countdown: CountdownTimer | undefined;
|
|
492
|
+
#promptActive = false;
|
|
493
|
+
#timeoutExpired = false;
|
|
494
|
+
#closed = false;
|
|
495
|
+
#tabBar: TabBar | undefined;
|
|
496
|
+
#stableHeight: { key: string; total: number } | undefined;
|
|
497
|
+
#previewCache: PreviewRenderCache = new Map();
|
|
498
|
+
#overflowLayouts = new WeakMap<ExtensionAskDialogQuestion, Set<string>>();
|
|
499
|
+
#expanded = false;
|
|
500
|
+
#contentWidth = 76;
|
|
501
|
+
#headerExpandable = false;
|
|
502
|
+
#descExpandable = false;
|
|
503
|
+
readonly #panel: OverlayPanel;
|
|
504
|
+
readonly #headerRegion: PanelRows;
|
|
505
|
+
readonly #bodyRegion: PanelRows;
|
|
506
|
+
readonly #footerRegion: PanelRows;
|
|
507
|
+
readonly #questions: ExtensionAskDialogQuestion[];
|
|
508
|
+
|
|
509
|
+
constructor(
|
|
510
|
+
questions: ExtensionAskDialogQuestion[],
|
|
511
|
+
private readonly callbacks: AskDialogCallbacks,
|
|
512
|
+
private readonly options: AskDialogOptions = {},
|
|
513
|
+
) {
|
|
514
|
+
this.#questions = normalizeDialogQuestions(questions);
|
|
515
|
+
this.#states = this.#questions.map(question => {
|
|
516
|
+
const recommended = Number.isInteger(question.recommended) ? question.recommended : 0;
|
|
517
|
+
const maxIndex = Math.max(0, question.options.length - 1);
|
|
518
|
+
return {
|
|
519
|
+
selectedOptions: new Set<string>(),
|
|
520
|
+
customInput: undefined,
|
|
521
|
+
note: undefined,
|
|
522
|
+
noteRowKey: undefined,
|
|
523
|
+
cursorIndex: clamp(recommended ?? 0, 0, maxIndex),
|
|
524
|
+
scrollOffset: 0,
|
|
525
|
+
manualScroll: false,
|
|
526
|
+
timedOut: false,
|
|
527
|
+
};
|
|
528
|
+
});
|
|
529
|
+
if (options.timeout && options.timeout > 0) {
|
|
530
|
+
this.#countdown = new CountdownTimer(
|
|
531
|
+
options.timeout,
|
|
532
|
+
options.tui,
|
|
533
|
+
seconds => {
|
|
534
|
+
this.#remainingSeconds = seconds;
|
|
535
|
+
},
|
|
536
|
+
() => this.#handleTimeout(),
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
this.#panel = new OverlayPanel("Ask");
|
|
540
|
+
this.#headerRegion = new PanelRows();
|
|
541
|
+
this.#bodyRegion = new PanelRows();
|
|
542
|
+
this.#footerRegion = new PanelRows();
|
|
543
|
+
this.#footerRegion.setHeight(1);
|
|
544
|
+
this.#panel.addChild(this.#headerRegion);
|
|
545
|
+
this.#panel.addChild(new PanelDivider());
|
|
546
|
+
this.#panel.addChild(this.#bodyRegion);
|
|
547
|
+
this.#panel.addChild(new PanelDivider());
|
|
548
|
+
this.#panel.addChild(this.#footerRegion);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
invalidate(): void {
|
|
552
|
+
this.#stableHeight = undefined;
|
|
553
|
+
this.#previewCache.clear();
|
|
554
|
+
this.#overflowLayouts = new WeakMap();
|
|
555
|
+
this.#tabBar?.invalidate();
|
|
556
|
+
this.#panel.invalidate();
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
dispose(): void {
|
|
560
|
+
this.#closed = true;
|
|
561
|
+
this.#countdown?.dispose();
|
|
562
|
+
this.#panel.dispose();
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Toggle truncated question headers and option descriptions. Returns false
|
|
566
|
+
* when there is nothing to expand so the global Ctrl+O listener can still
|
|
567
|
+
* expand transcript tools.
|
|
568
|
+
*/
|
|
569
|
+
toggleQuestionExpansion(): boolean {
|
|
570
|
+
if (this.#closed || this.#isSubmitTab()) return false;
|
|
571
|
+
const question = this.#questions[this.#currentQuestionIndex()];
|
|
572
|
+
if (!question) return false;
|
|
573
|
+
const headerOverflows = wrapQuestionTitle(question, this.#contentWidth).length > MAX_HEADER_ROWS;
|
|
574
|
+
const descOverflows = questionDescriptionsOverflow(question, this.#contentWidth);
|
|
575
|
+
if (!headerOverflows && !descOverflows && !this.#expanded) return false;
|
|
576
|
+
this.#expanded = !this.#expanded;
|
|
577
|
+
this.invalidate();
|
|
578
|
+
this.#requestRender();
|
|
579
|
+
return true;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
handleInput(keyData: string): void {
|
|
583
|
+
if (this.#closed || this.#promptActive) return;
|
|
584
|
+
// Reset the inactivity countdown on any key that reaches past the
|
|
585
|
+
// closed/prompt guards, matching HookSelector/HookInput semantics.
|
|
586
|
+
this.#countdown?.reset();
|
|
587
|
+
if (matchesSelectCancel(keyData)) {
|
|
588
|
+
this.#finishCancel();
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
// Expand before the draft-input guard so Ctrl+O can reveal a truncated
|
|
592
|
+
// question even while a pending prompt still owns typing.
|
|
593
|
+
if (matchesAppToolsExpand(keyData)) {
|
|
594
|
+
this.toggleQuestionExpansion();
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
const inputGuard = this.options.inputGuard;
|
|
598
|
+
if (inputGuard?.isBlocked()) {
|
|
599
|
+
inputGuard.handleInput(keyData);
|
|
600
|
+
this.#requestRender();
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
if (this.#hasSubmitTab() && handleTabSwitchKey(keyData, direction => this.#switchTab(direction))) {
|
|
604
|
+
this.#requestRender();
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
if (this.#isSubmitTab()) {
|
|
608
|
+
this.#handleSubmitTabInput(keyData);
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
this.#handleQuestionInput(keyData);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
render(width: number): readonly string[] {
|
|
615
|
+
// Keep the proxied draft's cursor visible while it owns input (the editor
|
|
616
|
+
// renders as the next sibling in the same container, so this lands in the
|
|
617
|
+
// same frame).
|
|
618
|
+
this.options.inputGuard?.syncPresentation?.();
|
|
619
|
+
const innerWidth = Math.max(1, width - 4);
|
|
620
|
+
this.#contentWidth = innerWidth;
|
|
621
|
+
// Fixed panel height: measured from the tallest tab at spawn and
|
|
622
|
+
// re-measured only when the viewport changes. Tab switches, cursor
|
|
623
|
+
// moves, and later answers never resize the box; content that
|
|
624
|
+
// outgrows it scrolls. Expanding a truncated question uses the space
|
|
625
|
+
// available within the existing height cap.
|
|
626
|
+
const totalRows = this.#dialogHeight(innerWidth, process.stdout.rows || 40);
|
|
627
|
+
const tabBarRows = this.#hasSubmitTab() ? 1 : 0;
|
|
628
|
+
const maxTitleRows = Math.max(1, totalRows - 5 - MIN_BODY_ROWS - tabBarRows);
|
|
629
|
+
const headerLines = this.#renderHeader(innerWidth, maxTitleRows);
|
|
630
|
+
// top border (1) + header(N) + two dividers + footer(1) + bottom
|
|
631
|
+
// border (1) = N + 5 fixed rows outside the body. Without the bottom
|
|
632
|
+
// border term the dialog overflowed the viewport by one row
|
|
633
|
+
// (PRRT_kwDOQxs0bc6OFbDY).
|
|
634
|
+
const fixedRows = 1 + headerLines.length + 1 + 1 + 1 + 1;
|
|
635
|
+
const bodyRows = Math.max(MIN_BODY_ROWS, totalRows - fixedRows);
|
|
636
|
+
this.#bodyRows = bodyRows;
|
|
637
|
+
const bodyLines = this.#isSubmitTab()
|
|
638
|
+
? this.#renderSubmitBody(innerWidth, bodyRows)
|
|
639
|
+
: this.#renderQuestionBody(innerWidth, bodyRows);
|
|
640
|
+
const footer = this.#footerHintText(bodyLines.indicator);
|
|
641
|
+
this.#panel.title = this.#titleText();
|
|
642
|
+
this.#headerRegion.setLines(headerLines);
|
|
643
|
+
this.#bodyRegion.setLines(bodyLines.lines);
|
|
644
|
+
this.#bodyRegion.setHeight(bodyRows);
|
|
645
|
+
this.#footerRegion.setLines([theme.fg("dim", footer)]);
|
|
646
|
+
return this.#panel.render(width);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
#dialogHeight(width: number, termRows: number): number {
|
|
650
|
+
const key = `${width}:${termRows}:${this.#expanded ? 1 : 0}`;
|
|
651
|
+
if (this.#stableHeight?.key === key) return this.#stableHeight.total;
|
|
652
|
+
const total = this.#measureHeight(width, termRows);
|
|
653
|
+
this.#stableHeight = { key, total };
|
|
654
|
+
return total;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/** Measure the tallest tab's natural content height, clamped to
|
|
658
|
+
* DIALOG_HEIGHT_RATIO of the terminal. Derived from questions and
|
|
659
|
+
* viewport only — never from cursor, tab, or answer state — so the box
|
|
660
|
+
* size is stable for the dialog's lifetime at a given terminal size. */
|
|
661
|
+
#measureHeight(width: number, termRows: number): number {
|
|
662
|
+
const maxHeight = Math.max(MIN_DIALOG_ROWS, Math.floor(termRows * DIALOG_HEIGHT_RATIO));
|
|
663
|
+
const chrome = 5; // topBorder + divider + divider + footer + bottomBorder
|
|
664
|
+
const tabBarRows = this.#hasSubmitTab() ? 1 : 0;
|
|
665
|
+
const mdTheme = getMarkdownTheme();
|
|
666
|
+
let needed = MIN_DIALOG_ROWS;
|
|
667
|
+
for (let index = 0; index < this.#questions.length; index++) {
|
|
668
|
+
const question = this.#questions[index];
|
|
669
|
+
const state = this.#states[index];
|
|
670
|
+
if (!question || !state) continue;
|
|
671
|
+
const titleRows = this.#expanded ? Number.POSITIVE_INFINITY : MAX_HEADER_ROWS;
|
|
672
|
+
const headerRows = tabBarRows + renderQuestionTitle(question, width, titleRows).length;
|
|
673
|
+
const rowItems = this.#questionRows(question);
|
|
674
|
+
const listRows = (listWidth: number): number => {
|
|
675
|
+
let total = 0;
|
|
676
|
+
for (const rowItem of rowItems) {
|
|
677
|
+
total += renderRowLabel(
|
|
678
|
+
rowItem,
|
|
679
|
+
question,
|
|
680
|
+
state,
|
|
681
|
+
false,
|
|
682
|
+
mdTheme,
|
|
683
|
+
this.#previewCache,
|
|
684
|
+
listWidth,
|
|
685
|
+
this.#expanded,
|
|
686
|
+
).length;
|
|
687
|
+
}
|
|
688
|
+
return total;
|
|
689
|
+
};
|
|
690
|
+
const body = listRows(width);
|
|
691
|
+
needed = Math.max(needed, chrome + headerRows + Math.max(MIN_BODY_ROWS, body));
|
|
692
|
+
}
|
|
693
|
+
if (this.#hasSubmitTab()) {
|
|
694
|
+
// Warning line + blank, one summary line per question, blank, and
|
|
695
|
+
// the Submit row; note lines added later scroll within the body.
|
|
696
|
+
const body = 2 + this.#questions.length + 2;
|
|
697
|
+
needed = Math.max(needed, chrome + tabBarRows + 1 + Math.max(MIN_BODY_ROWS, body));
|
|
698
|
+
}
|
|
699
|
+
return Math.min(needed, maxHeight);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
#titleText(): string {
|
|
703
|
+
return this.#remainingSeconds === undefined ? "Ask" : `Ask (${this.#remainingSeconds}s)`;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
#hasSubmitTab(): boolean {
|
|
707
|
+
// Multi questions confirm on the Submit tab (Enter toggles, never
|
|
708
|
+
// submits), so any multi question forces the tab even when there is
|
|
709
|
+
// only one question.
|
|
710
|
+
return this.#questions.length > 1 || this.#questions.some(question => question.multi);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
#submitTabIndex(): number {
|
|
714
|
+
return this.#questions.length;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
#isSubmitTab(): boolean {
|
|
718
|
+
return this.#hasSubmitTab() && this.#activeTabIndex === this.#submitTabIndex();
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
#currentQuestionIndex(): number {
|
|
722
|
+
return clamp(this.#activeTabIndex, 0, Math.max(0, this.#questions.length - 1));
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
#requestRender(): void {
|
|
726
|
+
this.options.tui?.requestRender();
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
#renderHeader(width: number, maxTitleRows: number): string[] {
|
|
730
|
+
const lines: string[] = [];
|
|
731
|
+
if (this.#hasSubmitTab()) {
|
|
732
|
+
const tabs: Tab[] = [
|
|
733
|
+
...this.#questions.map((question, index) => ({
|
|
734
|
+
id: String(index),
|
|
735
|
+
label: questionTabLabel(question, index),
|
|
736
|
+
})),
|
|
737
|
+
{ id: "submit", label: "Submit" },
|
|
738
|
+
];
|
|
739
|
+
this.#tabBar = new TabBar("", tabs, getTabBarTheme(), this.#activeTabIndex);
|
|
740
|
+
this.#tabBar.showHint = false;
|
|
741
|
+
lines.push(...this.#tabBar.render(width));
|
|
742
|
+
}
|
|
743
|
+
if (this.#isSubmitTab()) {
|
|
744
|
+
this.#headerExpandable = false;
|
|
745
|
+
this.#descExpandable = false;
|
|
746
|
+
lines.push(theme.bold(theme.fg("accent", "Review answers")));
|
|
747
|
+
return lines;
|
|
748
|
+
}
|
|
749
|
+
const questionIndex = this.#currentQuestionIndex();
|
|
750
|
+
const question = this.#questions[questionIndex];
|
|
751
|
+
if (!question) {
|
|
752
|
+
this.#headerExpandable = false;
|
|
753
|
+
this.#descExpandable = false;
|
|
754
|
+
return lines;
|
|
755
|
+
}
|
|
756
|
+
const wrapped = wrapQuestionTitle(question, width);
|
|
757
|
+
this.#headerExpandable = wrapped.length > MAX_HEADER_ROWS;
|
|
758
|
+
this.#descExpandable = questionDescriptionsOverflow(question, width);
|
|
759
|
+
const maxRows = this.#expanded ? maxTitleRows : MAX_HEADER_ROWS;
|
|
760
|
+
lines.push(...renderQuestionTitle(question, width, maxRows));
|
|
761
|
+
return lines;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
#expandHint(): string {
|
|
765
|
+
if (!this.#headerExpandable && !this.#descExpandable) return "";
|
|
766
|
+
return ` · ${expandKeyHint()} ${this.#expanded ? "collapse" : "expand"}`;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
#footerHintText(indicator: string): string {
|
|
770
|
+
const cancel = `${cancelKeyLabel()} cancel`;
|
|
771
|
+
const inputGuard = this.options.inputGuard;
|
|
772
|
+
if (inputGuard?.isBlocked()) return `${inputGuard.hint}${this.#expandHint()} · ${cancel}`;
|
|
773
|
+
if (this.#isSubmitTab()) {
|
|
774
|
+
const scroll = indicator ? ` ${indicator} scroll ·` : "";
|
|
775
|
+
return `Enter submit · ↑/↓ scroll ·${scroll} ${cancel}`;
|
|
776
|
+
}
|
|
777
|
+
const question = this.#questions[this.#currentQuestionIndex()];
|
|
778
|
+
// Enter advances in multi-question dialogs and submits single-question ones.
|
|
779
|
+
const enterAction = this.#questions.length > 1 ? "next" : "submit";
|
|
780
|
+
const action = question?.multi ? `Space toggle · Enter ${enterAction}` : "Enter select · n note";
|
|
781
|
+
const tabs = this.#hasSubmitTab() ? " · Tab/←/→" : "";
|
|
782
|
+
const expand = this.#expandHint();
|
|
783
|
+
if (this.#questionCanPage && indicator) {
|
|
784
|
+
return `${action} · ↑/↓${tabs} · ${cancel}${expand} · ${pageKeysLabel()} ${indicator}`;
|
|
785
|
+
}
|
|
786
|
+
const scroll = indicator ? ` ${indicator} scroll ·` : "";
|
|
787
|
+
return `${action} · ↑/↓ move${tabs} ·${scroll} ${cancel}${expand}`;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
#questionRows(question: ExtensionAskDialogQuestion): QuestionRow[] {
|
|
791
|
+
const display = displayOptionLabels(question);
|
|
792
|
+
const rows: QuestionRow[] = question.options.map((option, index) => ({
|
|
793
|
+
kind: "option",
|
|
794
|
+
key: `option:${index}`,
|
|
795
|
+
label: display[index] ?? sanitizeCarriageReturns(option.label),
|
|
796
|
+
optionIndex: index,
|
|
797
|
+
}));
|
|
798
|
+
rows.push({ kind: "other", key: "other", label: OTHER_OPTION, optionIndex: undefined });
|
|
799
|
+
return rows;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
#activeQuestionState(): { question: ExtensionAskDialogQuestion; state: QuestionState } | undefined {
|
|
803
|
+
const question = this.#questions[this.#currentQuestionIndex()];
|
|
804
|
+
const state = this.#states[this.#currentQuestionIndex()];
|
|
805
|
+
if (!question || !state) return undefined;
|
|
806
|
+
return { question, state };
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
#handleQuestionInput(keyData: string): void {
|
|
810
|
+
const active = this.#activeQuestionState();
|
|
811
|
+
if (!active) return;
|
|
812
|
+
const { question, state } = active;
|
|
813
|
+
const rows = this.#questionRows(question);
|
|
814
|
+
if (matchesSelectPageUp(keyData)) {
|
|
815
|
+
state.scrollOffset = Math.max(0, state.scrollOffset - Math.max(1, this.#bodyRows - 1));
|
|
816
|
+
state.manualScroll = true;
|
|
817
|
+
this.#requestRender();
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
if (matchesSelectPageDown(keyData)) {
|
|
821
|
+
state.scrollOffset += Math.max(1, this.#bodyRows - 1);
|
|
822
|
+
state.manualScroll = true;
|
|
823
|
+
this.#requestRender();
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
if (matchesSelectUp(keyData)) {
|
|
827
|
+
state.cursorIndex = clamp(state.cursorIndex - 1, 0, Math.max(0, rows.length - 1));
|
|
828
|
+
state.manualScroll = false;
|
|
829
|
+
this.#requestRender();
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
if (matchesSelectDown(keyData)) {
|
|
833
|
+
state.cursorIndex = clamp(state.cursorIndex + 1, 0, Math.max(0, rows.length - 1));
|
|
834
|
+
state.manualScroll = false;
|
|
835
|
+
this.#requestRender();
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
const rowItem = rows[state.cursorIndex];
|
|
839
|
+
if (!rowItem) return;
|
|
840
|
+
if (keyData === "n" || keyData === "N") {
|
|
841
|
+
if (rowItem.kind === "option" || rowItem.kind === "other") {
|
|
842
|
+
void this.#promptForNote(question, state, rowItem);
|
|
843
|
+
}
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
const isEnter = matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n";
|
|
847
|
+
const isSpace = matchesKey(keyData, "space") || keyData === " ";
|
|
848
|
+
if (!isEnter && !(question.multi && isSpace)) return;
|
|
849
|
+
if (rowItem.kind === "other") {
|
|
850
|
+
void this.#promptForCustomInput(question, state, rowItem);
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
const option = question.options[rowItem.optionIndex ?? -1];
|
|
854
|
+
if (!option) return;
|
|
855
|
+
if (question.multi) {
|
|
856
|
+
if (isEnter) {
|
|
857
|
+
// Enter confirms the current selection without toggling the
|
|
858
|
+
// focused option; Space toggles. Advances to the next question
|
|
859
|
+
// (submitting only for a single-question dialog), matching
|
|
860
|
+
// single-select Enter (#8252).
|
|
861
|
+
this.#advanceAfterQuestion();
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
if (state.selectedOptions.has(option.label)) {
|
|
865
|
+
state.selectedOptions.delete(option.label);
|
|
866
|
+
clearNoteIfRow(state, rowItem.key);
|
|
867
|
+
} else {
|
|
868
|
+
state.selectedOptions.add(option.label);
|
|
869
|
+
}
|
|
870
|
+
this.#requestRender();
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
state.selectedOptions = new Set([option.label]);
|
|
874
|
+
state.customInput = undefined;
|
|
875
|
+
clearNoteUnlessRow(state, rowItem.key);
|
|
876
|
+
this.#advanceAfterQuestion();
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
#handleSubmitTabInput(keyData: string): void {
|
|
880
|
+
if (matchesSelectUp(keyData)) {
|
|
881
|
+
this.#submitScrollOffset = Math.max(0, this.#submitScrollOffset - 1);
|
|
882
|
+
this.#requestRender();
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
if (matchesSelectDown(keyData)) {
|
|
886
|
+
// Clamped against the rendered line count in #renderSubmitBody.
|
|
887
|
+
this.#submitScrollOffset += 1;
|
|
888
|
+
this.#requestRender();
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
const isEnter = matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n";
|
|
892
|
+
if (isEnter) this.#finishSubmit();
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
#switchTab(direction: 1 | -1): void {
|
|
896
|
+
const tabCount = this.#questions.length + 1;
|
|
897
|
+
this.#activeTabIndex = (this.#activeTabIndex + direction + tabCount) % tabCount;
|
|
898
|
+
this.#submitScrollOffset = 0;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
#advanceAfterQuestion(): void {
|
|
902
|
+
const current = this.#currentQuestionIndex();
|
|
903
|
+
if (this.#questions.length === 1) {
|
|
904
|
+
this.#finishSubmit();
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
this.#activeTabIndex = current + 1 < this.#questions.length ? current + 1 : this.#submitTabIndex();
|
|
908
|
+
this.#submitScrollOffset = 0;
|
|
909
|
+
this.#requestRender();
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
async #promptForCustomInput(
|
|
913
|
+
question: ExtensionAskDialogQuestion,
|
|
914
|
+
state: QuestionState,
|
|
915
|
+
rowItem: QuestionRow,
|
|
916
|
+
): Promise<void> {
|
|
917
|
+
this.#promptActive = true;
|
|
918
|
+
try {
|
|
919
|
+
const input = await this.callbacks.onPrompt(
|
|
920
|
+
boundPromptTitle("Custom answer: ", question.question),
|
|
921
|
+
state.customInput,
|
|
922
|
+
);
|
|
923
|
+
if (input === undefined || this.#closed) return;
|
|
924
|
+
if (input.trim() === "") {
|
|
925
|
+
// Submitting an empty value unselects the custom answer.
|
|
926
|
+
state.customInput = undefined;
|
|
927
|
+
clearNoteIfRow(state, rowItem.key);
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
state.customInput = input;
|
|
931
|
+
if (!question.multi) {
|
|
932
|
+
state.selectedOptions.clear();
|
|
933
|
+
clearNoteUnlessRow(state, rowItem.key);
|
|
934
|
+
}
|
|
935
|
+
if (question.multi && this.#questions.length === 1) {
|
|
936
|
+
this.#activeTabIndex = this.#submitTabIndex();
|
|
937
|
+
this.#submitScrollOffset = 0;
|
|
938
|
+
} else {
|
|
939
|
+
this.#advanceAfterQuestion();
|
|
940
|
+
}
|
|
941
|
+
} finally {
|
|
942
|
+
this.#promptActive = false;
|
|
943
|
+
this.#runDeferredTimeout();
|
|
944
|
+
this.#requestRender();
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
async #promptForNote(
|
|
949
|
+
question: ExtensionAskDialogQuestion,
|
|
950
|
+
state: QuestionState,
|
|
951
|
+
rowItem: QuestionRow,
|
|
952
|
+
): Promise<void> {
|
|
953
|
+
this.#promptActive = true;
|
|
954
|
+
try {
|
|
955
|
+
const input = await this.callbacks.onPrompt(
|
|
956
|
+
boundPromptTitle(`Note for ${rowItem.label}: `, question.question),
|
|
957
|
+
state.noteRowKey === rowItem.key ? state.note : undefined,
|
|
958
|
+
);
|
|
959
|
+
if (input === undefined || this.#closed) return;
|
|
960
|
+
state.note = input;
|
|
961
|
+
state.noteRowKey = rowItem.key;
|
|
962
|
+
} finally {
|
|
963
|
+
this.#promptActive = false;
|
|
964
|
+
this.#runDeferredTimeout();
|
|
965
|
+
this.#requestRender();
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
#renderQuestionBody(width: number, maxRows: number): RenderedList {
|
|
970
|
+
const active = this.#activeQuestionState();
|
|
971
|
+
if (!active) return { lines: [], scrollOffset: 0, indicator: "" };
|
|
972
|
+
const { question, state } = active;
|
|
973
|
+
const rowItems = this.#questionRows(question);
|
|
974
|
+
state.cursorIndex = clamp(state.cursorIndex, 0, Math.max(0, rowItems.length - 1));
|
|
975
|
+
return this.#renderQuestionList(question, state, rowItems, width, maxRows);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
#renderQuestionList(
|
|
979
|
+
question: ExtensionAskDialogQuestion,
|
|
980
|
+
state: QuestionState,
|
|
981
|
+
rowItems: QuestionRow[],
|
|
982
|
+
width: number,
|
|
983
|
+
rows: number,
|
|
984
|
+
): RenderedList {
|
|
985
|
+
const mdTheme = getMarkdownTheme();
|
|
986
|
+
const renderRows = (contentWidth: number): { allLines: string[]; lineStartByRow: number[] } => {
|
|
987
|
+
const allLines: string[] = [];
|
|
988
|
+
const lineStartByRow: number[] = [];
|
|
989
|
+
for (let index = 0; index < rowItems.length; index++) {
|
|
990
|
+
lineStartByRow.push(allLines.length);
|
|
991
|
+
const rowItem = rowItems[index];
|
|
992
|
+
if (!rowItem) continue;
|
|
993
|
+
allLines.push(
|
|
994
|
+
...renderRowLabel(
|
|
995
|
+
rowItem,
|
|
996
|
+
question,
|
|
997
|
+
state,
|
|
998
|
+
index === state.cursorIndex,
|
|
999
|
+
mdTheme,
|
|
1000
|
+
this.#previewCache,
|
|
1001
|
+
contentWidth,
|
|
1002
|
+
this.#expanded,
|
|
1003
|
+
),
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
return { allLines, lineStartByRow };
|
|
1007
|
+
};
|
|
1008
|
+
const layoutKey = `${width}:${rows}:${state.customInput === undefined ? 0 : 1}`;
|
|
1009
|
+
let overflowLayouts = this.#overflowLayouts.get(question);
|
|
1010
|
+
const knownOverflow = overflowLayouts?.has(layoutKey) ?? false;
|
|
1011
|
+
let renderedRows = renderRows(knownOverflow && width > 1 ? width - 1 : width);
|
|
1012
|
+
if (!knownOverflow && width > 1 && renderedRows.allLines.length > rows) {
|
|
1013
|
+
if (!overflowLayouts) {
|
|
1014
|
+
overflowLayouts = new Set();
|
|
1015
|
+
this.#overflowLayouts.set(question, overflowLayouts);
|
|
1016
|
+
}
|
|
1017
|
+
overflowLayouts.add(layoutKey);
|
|
1018
|
+
renderedRows = renderRows(width - 1);
|
|
1019
|
+
}
|
|
1020
|
+
const { allLines, lineStartByRow } = renderedRows;
|
|
1021
|
+
const cursorStart = lineStartByRow[state.cursorIndex] ?? 0;
|
|
1022
|
+
const cursorEnd = lineStartByRow[state.cursorIndex + 1] ?? allLines.length;
|
|
1023
|
+
this.#questionCanPage = cursorEnd - cursorStart > rows;
|
|
1024
|
+
state.scrollOffset = this.#scrollOffsetForCursor(
|
|
1025
|
+
state.scrollOffset,
|
|
1026
|
+
cursorStart,
|
|
1027
|
+
cursorEnd,
|
|
1028
|
+
rows,
|
|
1029
|
+
allLines.length,
|
|
1030
|
+
state.manualScroll,
|
|
1031
|
+
);
|
|
1032
|
+
const scrollView = new ScrollView(allLines, {
|
|
1033
|
+
height: rows,
|
|
1034
|
+
scrollbar: "auto",
|
|
1035
|
+
theme: { track: t => theme.fg("muted", t), thumb: t => theme.fg("accent", t) },
|
|
1036
|
+
});
|
|
1037
|
+
scrollView.setScrollOffset(state.scrollOffset);
|
|
1038
|
+
const lines = [...scrollView.render(width)];
|
|
1039
|
+
while (lines.length < rows) lines.push("");
|
|
1040
|
+
return {
|
|
1041
|
+
lines: lines.slice(0, rows),
|
|
1042
|
+
scrollOffset: state.scrollOffset,
|
|
1043
|
+
indicator: this.#clipIndicator(state.scrollOffset, rows, allLines.length),
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
#renderSubmitBody(width: number, rows: number): RenderedList {
|
|
1048
|
+
const allLines: string[] = [];
|
|
1049
|
+
const unanswered = this.#unansweredCount();
|
|
1050
|
+
if (unanswered > 0) {
|
|
1051
|
+
allLines.push(
|
|
1052
|
+
theme.fg(
|
|
1053
|
+
"warning",
|
|
1054
|
+
`${unanswered} unanswered question${unanswered === 1 ? "" : "s"}; Enter still submits.`,
|
|
1055
|
+
),
|
|
1056
|
+
);
|
|
1057
|
+
allLines.push("");
|
|
1058
|
+
}
|
|
1059
|
+
for (let index = 0; index < this.#questions.length; index++) {
|
|
1060
|
+
const question = this.#questions[index];
|
|
1061
|
+
const state = this.#states[index];
|
|
1062
|
+
if (!question || !state) continue;
|
|
1063
|
+
const label = questionTabLabel(question, index);
|
|
1064
|
+
const answer = renderAnswerSummary(question, state);
|
|
1065
|
+
allLines.push(`${theme.fg("dim", `${index + 1}. ${label}:`)} ${answer}`);
|
|
1066
|
+
const submittedNote = noteForSubmittedAnswer(question, state);
|
|
1067
|
+
if (submittedNote?.trim()) {
|
|
1068
|
+
const note = normalizedInlineInput(submittedNote);
|
|
1069
|
+
allLines.push(
|
|
1070
|
+
theme.fg("muted", ` Note: ${truncateToWidth(note, Math.max(1, width - 9), Ellipsis.Unicode)}`),
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
allLines.push("");
|
|
1075
|
+
allLines.push(theme.fg("accent", `${theme.nav.cursor} ${SUBMIT_OPTION}`));
|
|
1076
|
+
this.#submitScrollOffset = clamp(this.#submitScrollOffset, 0, Math.max(0, allLines.length - rows));
|
|
1077
|
+
const scrollView = new ScrollView(allLines, {
|
|
1078
|
+
height: rows,
|
|
1079
|
+
scrollbar: "auto",
|
|
1080
|
+
theme: { track: t => theme.fg("muted", t), thumb: t => theme.fg("accent", t) },
|
|
1081
|
+
});
|
|
1082
|
+
scrollView.setScrollOffset(this.#submitScrollOffset);
|
|
1083
|
+
const rendered = scrollView.render(width);
|
|
1084
|
+
const lines = [...rendered];
|
|
1085
|
+
while (lines.length < rows) lines.push("");
|
|
1086
|
+
return {
|
|
1087
|
+
lines: lines.slice(0, rows),
|
|
1088
|
+
scrollOffset: this.#submitScrollOffset,
|
|
1089
|
+
indicator: this.#clipIndicator(this.#submitScrollOffset, rows, allLines.length),
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
#scrollOffsetForCursor(
|
|
1094
|
+
currentOffset: number,
|
|
1095
|
+
cursorStart: number,
|
|
1096
|
+
cursorEnd: number,
|
|
1097
|
+
rows: number,
|
|
1098
|
+
totalRows: number,
|
|
1099
|
+
manualScroll: boolean,
|
|
1100
|
+
): number {
|
|
1101
|
+
const maxOffset = Math.max(0, totalRows - rows);
|
|
1102
|
+
if (maxOffset === 0) return 0;
|
|
1103
|
+
let nextOffset = clamp(currentOffset, 0, maxOffset);
|
|
1104
|
+
const cursorRows = cursorEnd - cursorStart;
|
|
1105
|
+
if (manualScroll && cursorRows > rows) {
|
|
1106
|
+
// A page must not expose another option while Enter still targets this one.
|
|
1107
|
+
nextOffset = clamp(nextOffset, cursorStart, cursorEnd - rows);
|
|
1108
|
+
} else if (cursorStart < nextOffset || cursorEnd > nextOffset + rows) {
|
|
1109
|
+
nextOffset = cursorRows <= rows ? cursorEnd - rows : cursorStart;
|
|
1110
|
+
}
|
|
1111
|
+
return clamp(nextOffset, 0, maxOffset);
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
#clipIndicator(offset: number, rows: number, totalRows: number): string {
|
|
1115
|
+
const above = offset > 0;
|
|
1116
|
+
const below = offset + rows < totalRows;
|
|
1117
|
+
if (above && below) return "↕";
|
|
1118
|
+
if (above) return "↑";
|
|
1119
|
+
if (below) return "↓";
|
|
1120
|
+
return "";
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
#unansweredCount(): number {
|
|
1124
|
+
let count = 0;
|
|
1125
|
+
for (let index = 0; index < this.#questions.length; index++) {
|
|
1126
|
+
const question = this.#questions[index];
|
|
1127
|
+
const state = this.#states[index];
|
|
1128
|
+
if (!question || !state) continue;
|
|
1129
|
+
if (state.selectedOptions.size === 0 && state.customInput === undefined) count += 1;
|
|
1130
|
+
}
|
|
1131
|
+
return count;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
#handleTimeout(): void {
|
|
1135
|
+
if (this.#closed) return;
|
|
1136
|
+
if (this.#promptActive) {
|
|
1137
|
+
this.#timeoutExpired = true;
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
this.options.onTimeout?.();
|
|
1141
|
+
for (let index = 0; index < this.#questions.length; index++) {
|
|
1142
|
+
const question = this.#questions[index];
|
|
1143
|
+
const state = this.#states[index];
|
|
1144
|
+
if (!question || !state) continue;
|
|
1145
|
+
if (state.selectedOptions.size === 0 && state.customInput === undefined) {
|
|
1146
|
+
const noteMatch = /^option:(\d+)$/.exec(state.noteRowKey ?? "");
|
|
1147
|
+
const notedIndex = noteMatch ? Number.parseInt(noteMatch[1], 10) : Number.NaN;
|
|
1148
|
+
const fallbackIndex =
|
|
1149
|
+
Number.isInteger(notedIndex) && question.options[notedIndex]
|
|
1150
|
+
? notedIndex
|
|
1151
|
+
: clamp(question.recommended ?? 0, 0, Math.max(0, question.options.length - 1));
|
|
1152
|
+
const fallback = question.options[fallbackIndex];
|
|
1153
|
+
if (fallback) state.selectedOptions.add(fallback.label);
|
|
1154
|
+
state.timedOut = true;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
this.#finishSubmit();
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
#runDeferredTimeout(): void {
|
|
1161
|
+
if (!this.#timeoutExpired) return;
|
|
1162
|
+
this.#timeoutExpired = false;
|
|
1163
|
+
this.#handleTimeout();
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
#finishSubmit(): void {
|
|
1167
|
+
if (this.#closed) return;
|
|
1168
|
+
this.#closed = true;
|
|
1169
|
+
this.#countdown?.dispose();
|
|
1170
|
+
this.callbacks.onSubmit({ kind: "submit", results: this.#buildResults() });
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
#finishCancel(): void {
|
|
1174
|
+
if (this.#closed) return;
|
|
1175
|
+
this.#closed = true;
|
|
1176
|
+
this.#countdown?.dispose();
|
|
1177
|
+
this.callbacks.onCancel();
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
#buildResults(): ExtensionAskDialogResultItem[] {
|
|
1181
|
+
const results: ExtensionAskDialogResultItem[] = [];
|
|
1182
|
+
for (let index = 0; index < this.#questions.length; index++) {
|
|
1183
|
+
const question = this.#questions[index];
|
|
1184
|
+
const state = this.#states[index];
|
|
1185
|
+
if (!question || !state) continue;
|
|
1186
|
+
const selectedOptions = question.options
|
|
1187
|
+
.map(option => option.label)
|
|
1188
|
+
.filter(label => state.selectedOptions.has(label));
|
|
1189
|
+
results.push({
|
|
1190
|
+
id: question.id,
|
|
1191
|
+
question: question.question,
|
|
1192
|
+
options: question.options.map(option => option.label),
|
|
1193
|
+
multi: question.multi ?? false,
|
|
1194
|
+
selectedOptions,
|
|
1195
|
+
customInput: state.customInput,
|
|
1196
|
+
note: noteForSubmittedAnswer(question, state),
|
|
1197
|
+
timedOut: state.timedOut || undefined,
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
return results;
|
|
1201
|
+
}
|
|
1202
|
+
}
|