@oh-my-pi/pi-tui 18.2.4 → 18.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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,1137 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Component,
|
|
3
|
+
Container,
|
|
4
|
+
FuzzyText,
|
|
5
|
+
Input,
|
|
6
|
+
matchesKey,
|
|
7
|
+
padding,
|
|
8
|
+
replaceTabs,
|
|
9
|
+
routeSgrMouseInput,
|
|
10
|
+
ScrollView,
|
|
11
|
+
Spacer,
|
|
12
|
+
Text,
|
|
13
|
+
truncateToWidth,
|
|
14
|
+
visibleWidth,
|
|
15
|
+
} from "../index";
|
|
16
|
+
import { formatBytes } from "@oh-my-pi/pi-utils";
|
|
17
|
+
import { theme } from "../theme/theme";
|
|
18
|
+
import { contentRowWidth } from "../chrome/selector-helpers";
|
|
19
|
+
import { matchesAppInterrupt, matchesSelectDown, matchesSelectUp } from "../keybinding-matchers";
|
|
20
|
+
/** Session lifecycle status presented by the picker. */
|
|
21
|
+
export type SessionSelectorStatus = "complete" | "interrupted" | "aborted" | "error" | "pending" | "unknown";
|
|
22
|
+
|
|
23
|
+
/** Session listing fields consumed by the picker. */
|
|
24
|
+
export interface SessionSelectorEntry {
|
|
25
|
+
path: string;
|
|
26
|
+
id: string;
|
|
27
|
+
cwd: string;
|
|
28
|
+
title?: string;
|
|
29
|
+
modified: Date;
|
|
30
|
+
size: number;
|
|
31
|
+
firstMessage: string;
|
|
32
|
+
allMessagesText: string;
|
|
33
|
+
status?: SessionSelectorStatus;
|
|
34
|
+
/** Path to the parent session when this one was forked. */
|
|
35
|
+
parentSessionPath?: string;
|
|
36
|
+
}
|
|
37
|
+
import { shortenPath } from "../render/render-utils";
|
|
38
|
+
import { HookSelectorComponent } from "./hook-selector";
|
|
39
|
+
import { bottomBorder, OverlayPanel, row, topBorder } from "../chrome/overlay-box";
|
|
40
|
+
import { MenuSelection, getMenuWindow } from "../components/menu-selection";
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Themed glyph + colored label for a session's lifecycle status, or `undefined`
|
|
44
|
+
* when there is nothing useful to show (`unknown`/unset) so the metadata line
|
|
45
|
+
* stays uncluttered. The glyph resolves through the active symbol preset
|
|
46
|
+
* (nerdfont / unicode / ascii) via `theme.status.*`.
|
|
47
|
+
*/
|
|
48
|
+
function formatSessionStatus(status: SessionSelectorStatus | undefined): string | undefined {
|
|
49
|
+
switch (status) {
|
|
50
|
+
case "complete":
|
|
51
|
+
return theme.fg("success", `${theme.status.success} done`);
|
|
52
|
+
case "interrupted":
|
|
53
|
+
return theme.fg("warning", `${theme.status.warning} interrupted`);
|
|
54
|
+
case "aborted":
|
|
55
|
+
return theme.fg("muted", `${theme.status.aborted} aborted`);
|
|
56
|
+
case "error":
|
|
57
|
+
return theme.fg("error", `${theme.status.error} error`);
|
|
58
|
+
case "pending":
|
|
59
|
+
return theme.fg("accent", `${theme.status.pending} pending`);
|
|
60
|
+
default:
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Returns the IDs of sessions whose recorded prompts match a query, best first. */
|
|
66
|
+
export type SessionHistoryMatcher = (query: string) => string[];
|
|
67
|
+
|
|
68
|
+
function sessionSearchText(session: SessionSelectorEntry): string {
|
|
69
|
+
const parts = [
|
|
70
|
+
session.id,
|
|
71
|
+
session.title ?? "",
|
|
72
|
+
session.cwd ?? "",
|
|
73
|
+
session.firstMessage ?? "",
|
|
74
|
+
session.allMessagesText,
|
|
75
|
+
session.path,
|
|
76
|
+
];
|
|
77
|
+
return parts.filter(Boolean).join(" ");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Lowercased per-session search haystack, built once and cached on the
|
|
82
|
+
* {@link SessionSelectorEntry} itself (so it dies with the listing that produced it).
|
|
83
|
+
* Rebuilding it per keystroke — a ~4KB string join plus `toLowerCase` per
|
|
84
|
+
* session — was one of the costs that made resume search visibly lag.
|
|
85
|
+
*
|
|
86
|
+
* Only the string is cached. A prebuilt fuzzy index (~60KB per 4KB session)
|
|
87
|
+
* would cost hundreds of MB on multi-thousand-session listings, so fuzzy
|
|
88
|
+
* indexes are built transiently per scan visit instead (see
|
|
89
|
+
* {@link scoreFuzzySession} callers).
|
|
90
|
+
*/
|
|
91
|
+
const kSearchTextLower = Symbol("session.searchTextLower");
|
|
92
|
+
|
|
93
|
+
interface SearchableSessionInfo extends SessionSelectorEntry {
|
|
94
|
+
[kSearchTextLower]?: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function sessionTextLower(session: SessionSelectorEntry): string {
|
|
98
|
+
const tagged = session as SearchableSessionInfo;
|
|
99
|
+
let textLower = tagged[kSearchTextLower];
|
|
100
|
+
if (textLower === undefined) {
|
|
101
|
+
textLower = sessionSearchText(session).toLowerCase();
|
|
102
|
+
tagged[kSearchTextLower] = textLower;
|
|
103
|
+
}
|
|
104
|
+
return textLower;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function tokenizeSessionQuery(query: string): string[] {
|
|
108
|
+
const trimmed = query.trim().toLowerCase();
|
|
109
|
+
return trimmed ? trimmed.split(/\s+/) : [];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function compareSessionRecency(a: SessionSelectorEntry, b: SessionSelectorEntry): number {
|
|
113
|
+
return b.modified.getTime() - a.modified.getTime();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const MIN_PURE_FUZZY_TOKEN_SCORE = -20;
|
|
117
|
+
|
|
118
|
+
/** One ranked search hit; `index` is the session's position in the unfiltered list (recency order). */
|
|
119
|
+
interface RankedSessionMatch<T extends SessionSelectorEntry = SessionSelectorEntry> {
|
|
120
|
+
session: T;
|
|
121
|
+
score: number;
|
|
122
|
+
index: number;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* True when every query token appears verbatim in the haystack. Literal
|
|
127
|
+
* matches rank purely by recency, so they skip fuzzy scoring entirely — a pure
|
|
128
|
+
* fast path, not a semantic change: a contiguous substring of the lowercased
|
|
129
|
+
* text always lies within one normalized word per query sub-token, so every
|
|
130
|
+
* literal token also fuzzy-matches.
|
|
131
|
+
*/
|
|
132
|
+
function isLiteralMatch(textLower: string, tokens: string[]): boolean {
|
|
133
|
+
for (const token of tokens) {
|
|
134
|
+
if (!textLower.includes(token)) return false;
|
|
135
|
+
}
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Fuzzy-score one non-literal session against every query token. Returns
|
|
141
|
+
* undefined when a token fails to match or the weakest token is pure-fuzzy
|
|
142
|
+
* noise. The caller builds `fuzzy` once per session visit so multi-token
|
|
143
|
+
* queries share a single index.
|
|
144
|
+
*/
|
|
145
|
+
function scoreFuzzySession<T extends SessionSelectorEntry>(
|
|
146
|
+
session: T,
|
|
147
|
+
index: number,
|
|
148
|
+
tokens: string[],
|
|
149
|
+
fuzzy: FuzzyText,
|
|
150
|
+
): RankedSessionMatch<T> | undefined {
|
|
151
|
+
let score = 0;
|
|
152
|
+
let worstTokenScore = Number.NEGATIVE_INFINITY;
|
|
153
|
+
for (const token of tokens) {
|
|
154
|
+
const match = fuzzy.match(token);
|
|
155
|
+
if (!match.matches) return undefined;
|
|
156
|
+
score += match.score;
|
|
157
|
+
worstTokenScore = Math.max(worstTokenScore, match.score);
|
|
158
|
+
}
|
|
159
|
+
if (worstTokenScore >= MIN_PURE_FUZZY_TOKEN_SCORE) return undefined;
|
|
160
|
+
return { session, score, index };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function compareLiteralRank(a: RankedSessionMatch, b: RankedSessionMatch): number {
|
|
164
|
+
return compareSessionRecency(a.session, b.session) || a.index - b.index;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function compareFuzzyRank(a: RankedSessionMatch, b: RankedSessionMatch): number {
|
|
168
|
+
return a.score - b.score || compareSessionRecency(a.session, b.session) || a.index - b.index;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Exact titles lead partial titles; other matches retain their existing order. */
|
|
172
|
+
function prioritizeTitleMatches<T extends SessionSelectorEntry>(
|
|
173
|
+
sessions: T[],
|
|
174
|
+
tokens: string[],
|
|
175
|
+
literal: RankedSessionMatch<T>[],
|
|
176
|
+
): T[] {
|
|
177
|
+
const query = tokens.join(" ");
|
|
178
|
+
const exact: T[] = [];
|
|
179
|
+
const partial: T[] = [];
|
|
180
|
+
const titleMatches = new Set<T>();
|
|
181
|
+
// Title hits are literal matches, already ranked by recency and source index.
|
|
182
|
+
for (const { session } of literal) {
|
|
183
|
+
const title = session.title?.trim().toLowerCase().replace(/\s+/g, " ");
|
|
184
|
+
if (title === query) exact.push(session);
|
|
185
|
+
else if (title && isLiteralMatch(title, tokens)) partial.push(session);
|
|
186
|
+
else continue;
|
|
187
|
+
titleMatches.add(session);
|
|
188
|
+
}
|
|
189
|
+
if (titleMatches.size === 0) return sessions;
|
|
190
|
+
return [...exact, ...partial, ...sessions.filter(session => !titleMatches.has(session))];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Filter and rank session picker search results.
|
|
195
|
+
*
|
|
196
|
+
* Exact and partial title matches lead. Other literal matches rank by recency
|
|
197
|
+
* rather than a slightly better fuzzy position match. Pure fuzzy/acronym
|
|
198
|
+
* matches still sort by fuzzy score after literal matches, but weak pure
|
|
199
|
+
* fuzzy tokens are dropped as noise.
|
|
200
|
+
*
|
|
201
|
+
* This is the synchronous reference implementation; {@link SessionList} runs
|
|
202
|
+
* the same primitives incrementally so huge listings never block a keystroke.
|
|
203
|
+
*/
|
|
204
|
+
export function rankSessionSearchMatches<T extends SessionSelectorEntry>(allSessions: T[], query: string): T[] {
|
|
205
|
+
const tokens = tokenizeSessionQuery(query);
|
|
206
|
+
if (tokens.length === 0) return allSessions;
|
|
207
|
+
|
|
208
|
+
const literal: RankedSessionMatch<T>[] = [];
|
|
209
|
+
const fuzzyMatches: RankedSessionMatch<T>[] = [];
|
|
210
|
+
for (let index = 0; index < allSessions.length; index++) {
|
|
211
|
+
const session = allSessions[index]!;
|
|
212
|
+
const textLower = sessionTextLower(session);
|
|
213
|
+
if (isLiteralMatch(textLower, tokens)) {
|
|
214
|
+
literal.push({ session, score: 0, index });
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const match = scoreFuzzySession(session, index, tokens, new FuzzyText(textLower));
|
|
218
|
+
if (match) fuzzyMatches.push(match);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
literal.sort(compareLiteralRank);
|
|
222
|
+
fuzzyMatches.sort(compareFuzzyRank);
|
|
223
|
+
const out: T[] = [];
|
|
224
|
+
for (const match of literal) out.push(match.session);
|
|
225
|
+
for (const match of fuzzyMatches) out.push(match.session);
|
|
226
|
+
return prioritizeTitleMatches(out, tokens, literal);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Combine metadata matches with prompt-history matches for ranking, using both
|
|
231
|
+
* signals rather than replacing one with the other.
|
|
232
|
+
*
|
|
233
|
+
* - `fuzzy` is the ordered metadata/session-text result.
|
|
234
|
+
* - `historyIds` are session IDs whose recorded prompts matched the query,
|
|
235
|
+
* ordered by prompt-history rank (typically newest matching prompt first); duplicates are tolerated.
|
|
236
|
+
*
|
|
237
|
+
* Ranking: prompt-history matches lead in history order, then remaining
|
|
238
|
+
* metadata matches keep their existing order. A metadata match is never dropped,
|
|
239
|
+
* and history matches not present in `allSessions` (e.g. deleted or out-of-scope
|
|
240
|
+
* sessions) are ignored since they cannot be resumed from here.
|
|
241
|
+
*/
|
|
242
|
+
export function mergeSessionRanking<T extends SessionSelectorEntry>(
|
|
243
|
+
allSessions: T[],
|
|
244
|
+
fuzzy: T[],
|
|
245
|
+
historyIds: string[],
|
|
246
|
+
): T[] {
|
|
247
|
+
if (historyIds.length === 0) return fuzzy;
|
|
248
|
+
|
|
249
|
+
const sessionsById = new Map<string, T>();
|
|
250
|
+
for (const session of allSessions) {
|
|
251
|
+
if (!sessionsById.has(session.id)) sessionsById.set(session.id, session);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const historyMatches: T[] = [];
|
|
255
|
+
const historyPaths = new Set<string>();
|
|
256
|
+
for (const id of historyIds) {
|
|
257
|
+
const session = sessionsById.get(id);
|
|
258
|
+
if (!session || historyPaths.has(session.path)) continue;
|
|
259
|
+
historyMatches.push(session);
|
|
260
|
+
historyPaths.add(session.path);
|
|
261
|
+
}
|
|
262
|
+
if (historyMatches.length === 0) return fuzzy;
|
|
263
|
+
|
|
264
|
+
const metadataOnly = fuzzy.filter(session => !historyPaths.has(session.path));
|
|
265
|
+
return [...historyMatches, ...metadataOnly];
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Delay before the prompt-history DB is consulted for the current query.
|
|
270
|
+
* History matching hits SQLite synchronously (an FTS lookup plus a LIKE scan
|
|
271
|
+
* over every stored prompt — tens to hundreds of ms on a year-old database),
|
|
272
|
+
* so it must never run per keystroke: fuzzy results render immediately and
|
|
273
|
+
* the history merge lands once typing pauses.
|
|
274
|
+
*/
|
|
275
|
+
const HISTORY_MERGE_DEBOUNCE_MS = 150;
|
|
276
|
+
/**
|
|
277
|
+
* Minimum query length for history augmentation. A single character matches
|
|
278
|
+
* essentially every stored prompt — the most expensive FTS prefix to expand —
|
|
279
|
+
* and only reorders the recency-ranked list by noise.
|
|
280
|
+
*/
|
|
281
|
+
const HISTORY_MERGE_MIN_QUERY = 2;
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Sessions fuzzy-scored synchronously inside the keystroke itself. Small
|
|
285
|
+
* listings finish within it, keeping the complete-in-one-frame behavior;
|
|
286
|
+
* anything left spills into async chunks. A fuzzy visit costs ~100µs (index
|
|
287
|
+
* build over the ≤4KB per-session corpus dominates), so 100 visits ≈ 10ms —
|
|
288
|
+
* about one frame. Counts rather than a deadline keep chunk boundaries
|
|
289
|
+
* deterministic (and testable under fake timers).
|
|
290
|
+
*/
|
|
291
|
+
const FUZZY_SCAN_INLINE_COUNT = 100;
|
|
292
|
+
/**
|
|
293
|
+
* Sessions fuzzy-scored per async chunk (~15ms). Each chunk yields back to
|
|
294
|
+
* the event loop so the next keystroke is never blocked behind a long scan; a
|
|
295
|
+
* new query bumps the scan generation and orphans pending chunks.
|
|
296
|
+
*/
|
|
297
|
+
const FUZZY_SCAN_CHUNK_COUNT = 150;
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Custom session list component with multi-line items and search
|
|
301
|
+
*/
|
|
302
|
+
class SessionList<T extends SessionSelectorEntry> implements Component {
|
|
303
|
+
#menu: MenuSelection<T>;
|
|
304
|
+
// Maps a 0-based line within this list's own render to a filtered-session
|
|
305
|
+
// index, or undefined for chrome rows (search line, blanks, scrollbar gap).
|
|
306
|
+
// Rebuilt every render so the picker's mouse hit-testing tracks the live
|
|
307
|
+
// scroll window. Only consulted while the picker holds the alternate screen
|
|
308
|
+
// (where the overlay enables mouse tracking and paints from screen row 0).
|
|
309
|
+
#hitRows: (number | undefined)[] = [];
|
|
310
|
+
readonly #searchInput: Input;
|
|
311
|
+
onSelect?: (session: T) => void;
|
|
312
|
+
onCancel?: () => void;
|
|
313
|
+
onExit: () => void = () => {};
|
|
314
|
+
onToggleScope?: () => void;
|
|
315
|
+
// Snapshot of the live terminal-row getter; the visible window is derived
|
|
316
|
+
// from it per render so the picker fits the viewport (and adapts to resize).
|
|
317
|
+
readonly #getTerminalRows: () => number;
|
|
318
|
+
|
|
319
|
+
onDeleteRequest?: (session: T) => void;
|
|
320
|
+
|
|
321
|
+
#allSessions: T[];
|
|
322
|
+
#showCwd: boolean;
|
|
323
|
+
#pinnedIds: ReadonlySet<string>;
|
|
324
|
+
readonly #getCurrentSessionPath: () => string | undefined;
|
|
325
|
+
readonly #historyMatcher?: SessionHistoryMatcher;
|
|
326
|
+
#historyMergeTimer: NodeJS.Timeout | undefined;
|
|
327
|
+
/** Re-render hook for async list updates (fuzzy scan chunks, history merge). */
|
|
328
|
+
onRequestRender?: () => void;
|
|
329
|
+
|
|
330
|
+
// ── Incremental search state ──────────────────────────────────────────
|
|
331
|
+
// The menu's visible list is always composed from these three inputs (see
|
|
332
|
+
// #composeFiltered), so late-arriving fuzzy chunks and the debounced
|
|
333
|
+
// history merge can land in any order without clobbering each other.
|
|
334
|
+
/** Recency-ranked sessions whose text contains every query token verbatim. */
|
|
335
|
+
#literalRanked: RankedSessionMatch<T>[] = [];
|
|
336
|
+
/** Score-ranked fuzzy-only matches, appended by scan chunks. */
|
|
337
|
+
#fuzzyRanked: RankedSessionMatch<T>[] = [];
|
|
338
|
+
/** Prompt-history session IDs for the current query, once the merge landed. */
|
|
339
|
+
#historyIds: string[] = [];
|
|
340
|
+
/** Invalidates in-flight scan chunks when the query or dataset changes. */
|
|
341
|
+
#scanGeneration = 0;
|
|
342
|
+
#scanTimer: NodeJS.Timeout | undefined;
|
|
343
|
+
/**
|
|
344
|
+
* True once the user moved the selection for the current query; blocks the
|
|
345
|
+
* history merge from reordering the list under their cursor. (Fuzzy chunks
|
|
346
|
+
* only append below the literal group, which never shifts existing rows.)
|
|
347
|
+
*/
|
|
348
|
+
#selectionMoved = false;
|
|
349
|
+
/** True after a nonempty query; empty refilter restores current only then. */
|
|
350
|
+
#hadFilterQuery = false;
|
|
351
|
+
/** Last query passed to {@link #filterSessions}; same-query refilter keeps the index. */
|
|
352
|
+
#lastFilterQuery = "";
|
|
353
|
+
|
|
354
|
+
constructor(
|
|
355
|
+
sessions: T[],
|
|
356
|
+
showCwd = false,
|
|
357
|
+
historyMatcher?: SessionHistoryMatcher,
|
|
358
|
+
getTerminalRows: () => number = () => 24,
|
|
359
|
+
pinnedIds: ReadonlySet<string> = new Set(),
|
|
360
|
+
currentSessionPath?: string | (() => string | undefined),
|
|
361
|
+
) {
|
|
362
|
+
this.#getTerminalRows = getTerminalRows;
|
|
363
|
+
this.#allSessions = sessions;
|
|
364
|
+
this.#showCwd = showCwd;
|
|
365
|
+
this.#pinnedIds = pinnedIds;
|
|
366
|
+
this.#getCurrentSessionPath =
|
|
367
|
+
typeof currentSessionPath === "function" ? currentSessionPath : () => currentSessionPath;
|
|
368
|
+
this.#historyMatcher = historyMatcher;
|
|
369
|
+
this.#menu = new MenuSelection<T>(sessions, {
|
|
370
|
+
getKey: session => session.path,
|
|
371
|
+
getSearchText: sessionSearchText,
|
|
372
|
+
});
|
|
373
|
+
this.#selectCurrentSession();
|
|
374
|
+
this.#searchInput = new Input();
|
|
375
|
+
|
|
376
|
+
// Handle Enter in search input - select current item
|
|
377
|
+
this.#searchInput.onSubmit = () => {
|
|
378
|
+
const selected = this.#menu.selectedItem;
|
|
379
|
+
if (selected) {
|
|
380
|
+
this.onSelect?.(selected);
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Session-row line budget for one render, sized so the whole picker fits
|
|
387
|
+
* the current viewport instead of pushing its header/search off the top.
|
|
388
|
+
*
|
|
389
|
+
* Chrome (7) is the panel's top border, one spacer, the list's search line
|
|
390
|
+
* and its blank, and the pinned footer minus its leading blank (hint,
|
|
391
|
+
* blank, bottom border) — the last visible session's separator blank is
|
|
392
|
+
* never rendered, so the footer's own blank stands in for it. The reserve
|
|
393
|
+
* covers below-editor hook widgets / cursor. The floor of 8 always admits
|
|
394
|
+
* two titled sessions (the tallest item at 4 lines: title + preview +
|
|
395
|
+
* metadata + separator).
|
|
396
|
+
*/
|
|
397
|
+
#lineBudget(): number {
|
|
398
|
+
const CHROME = 7;
|
|
399
|
+
const RESERVE = 1;
|
|
400
|
+
return Math.max(8, this.#getTerminalRows() - CHROME - RESERVE);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** PageUp/PageDown jump, approximated from the worst-case session height. */
|
|
404
|
+
#pageSize(): number {
|
|
405
|
+
return Math.max(2, Math.floor(this.#lineBudget() / 4));
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** Focus the live session when it is in the visible list. */
|
|
409
|
+
#selectCurrentSession(): void {
|
|
410
|
+
const currentPath = this.#getCurrentSessionPath();
|
|
411
|
+
if (!currentPath) return;
|
|
412
|
+
const index = this.#menu.visibleItems.findIndex(s => s.path === currentPath);
|
|
413
|
+
if (index >= 0) this.#menu.setSelectedIndex(index);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** Replace the visible dataset, e.g. when toggling folder/all-projects scope. */
|
|
417
|
+
setSessions(sessions: T[], showCwd: boolean, pinnedIds?: ReadonlySet<string>): void {
|
|
418
|
+
this.#allSessions = sessions;
|
|
419
|
+
this.#showCwd = showCwd;
|
|
420
|
+
if (pinnedIds !== undefined) this.#pinnedIds = pinnedIds;
|
|
421
|
+
this.#menu.setSelectedIndex(0);
|
|
422
|
+
this.#filterSessions(this.#searchInput.getValue());
|
|
423
|
+
this.#selectCurrentSession();
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
#filterSessions(query: string): void {
|
|
427
|
+
this.#scanGeneration++;
|
|
428
|
+
if (this.#scanTimer !== undefined) {
|
|
429
|
+
clearTimeout(this.#scanTimer);
|
|
430
|
+
this.#scanTimer = undefined;
|
|
431
|
+
}
|
|
432
|
+
this.#selectionMoved = false;
|
|
433
|
+
this.#historyIds = [];
|
|
434
|
+
this.#literalRanked = [];
|
|
435
|
+
this.#fuzzyRanked = [];
|
|
436
|
+
|
|
437
|
+
const tokens = tokenizeSessionQuery(query);
|
|
438
|
+
const hadQuery = this.#hadFilterQuery;
|
|
439
|
+
const queryChanged = query !== this.#lastFilterQuery;
|
|
440
|
+
this.#hadFilterQuery = tokens.length > 0;
|
|
441
|
+
this.#lastFilterQuery = query;
|
|
442
|
+
if (tokens.length === 0) {
|
|
443
|
+
const keepIndex = Math.min(this.#menu.selectedIndex, Math.max(0, this.#allSessions.length - 1));
|
|
444
|
+
this.#menu.setItems(this.#allSessions, keepIndex >= 0 ? this.#allSessions[keepIndex]?.path : undefined);
|
|
445
|
+
if (hadQuery) this.#selectCurrentSession();
|
|
446
|
+
this.#scheduleHistoryMerge(query);
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Literal pass: one substring scan per token per session, synchronous so
|
|
451
|
+
// every keystroke gets immediate recency-ranked feedback regardless of
|
|
452
|
+
// listing size.
|
|
453
|
+
const literal: RankedSessionMatch<T>[] = [];
|
|
454
|
+
const rest: number[] = [];
|
|
455
|
+
const all = this.#allSessions;
|
|
456
|
+
for (let index = 0; index < all.length; index++) {
|
|
457
|
+
if (isLiteralMatch(sessionTextLower(all[index]!), tokens)) {
|
|
458
|
+
literal.push({ session: all[index]!, score: 0, index });
|
|
459
|
+
} else {
|
|
460
|
+
rest.push(index);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
literal.sort(compareLiteralRank);
|
|
464
|
+
this.#literalRanked = literal;
|
|
465
|
+
|
|
466
|
+
// Fuzzy pass: building a fuzzy index per session is too expensive to run
|
|
467
|
+
// across a huge listing inside one keystroke, so scan a bounded slice now
|
|
468
|
+
// and spill the remainder into async chunks.
|
|
469
|
+
this.#scanFuzzySlice(this.#scanGeneration, tokens, rest, 0, FUZZY_SCAN_INLINE_COUNT);
|
|
470
|
+
this.#composeFiltered();
|
|
471
|
+
// New query rebuilds ranking from scratch. Same-query refilter (delete)
|
|
472
|
+
// and async compose (fuzzy chunks / history merge) only clamp so an
|
|
473
|
+
// arrow selection survives. A live-session index > 0 would otherwise
|
|
474
|
+
// land on a lower-ranked match after the first keystroke.
|
|
475
|
+
if (queryChanged) this.#menu.setSelectedIndex(0);
|
|
476
|
+
this.#scheduleHistoryMerge(query);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Score up to `budget` sessions from `rest[start..]` (indexes into the
|
|
481
|
+
* unfiltered list), then schedule the remainder on a macrotask so pending
|
|
482
|
+
* input events run first. Chunks that added matches recompose the visible
|
|
483
|
+
* list and request a render; a stale generation aborts silently.
|
|
484
|
+
*/
|
|
485
|
+
#scanFuzzySlice(generation: number, tokens: string[], rest: number[], start: number, budget: number): void {
|
|
486
|
+
const all = this.#allSessions;
|
|
487
|
+
const end = Math.min(rest.length, start + budget);
|
|
488
|
+
for (let i = start; i < end; i++) {
|
|
489
|
+
const index = rest[i]!;
|
|
490
|
+
const session = all[index]!;
|
|
491
|
+
const match = scoreFuzzySession(session, index, tokens, new FuzzyText(sessionTextLower(session)));
|
|
492
|
+
if (match) this.#fuzzyRanked.push(match);
|
|
493
|
+
}
|
|
494
|
+
if (end >= rest.length) return;
|
|
495
|
+
this.#scanTimer = setTimeout(() => {
|
|
496
|
+
this.#scanTimer = undefined;
|
|
497
|
+
if (generation !== this.#scanGeneration) return;
|
|
498
|
+
const before = this.#fuzzyRanked.length;
|
|
499
|
+
this.#scanFuzzySlice(generation, tokens, rest, end, FUZZY_SCAN_CHUNK_COUNT);
|
|
500
|
+
if (this.#fuzzyRanked.length > before) {
|
|
501
|
+
this.#composeFiltered();
|
|
502
|
+
this.onRequestRender?.();
|
|
503
|
+
}
|
|
504
|
+
}, 0);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Rebuild the menu's visible list with title matches first, then other
|
|
509
|
+
* prompt-history hits, literal matches (recency), and fuzzy-only hits (score).
|
|
510
|
+
*/
|
|
511
|
+
#composeFiltered(): void {
|
|
512
|
+
this.#fuzzyRanked.sort(compareFuzzyRank);
|
|
513
|
+
const base: T[] = [];
|
|
514
|
+
for (const match of this.#literalRanked) base.push(match.session);
|
|
515
|
+
for (const match of this.#fuzzyRanked) base.push(match.session);
|
|
516
|
+
const composed = prioritizeTitleMatches(
|
|
517
|
+
this.#historyIds.length > 0 ? mergeSessionRanking(this.#allSessions, base, this.#historyIds) : base,
|
|
518
|
+
tokenizeSessionQuery(this.#searchInput.getValue()),
|
|
519
|
+
this.#literalRanked,
|
|
520
|
+
);
|
|
521
|
+
// Async chunks and the history merge only clamp the cursor so an arrow
|
|
522
|
+
// selection survives recomposition; the query path resets explicitly.
|
|
523
|
+
const keepIndex = Math.min(this.#menu.selectedIndex, Math.max(0, composed.length - 1));
|
|
524
|
+
this.#menu.setItems(composed, keepIndex >= 0 ? composed[keepIndex]?.path : undefined);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Augment ranked results with prompt-history matches without replacing them.
|
|
529
|
+
* The session-list corpus only sees the first 4KB of each session, so a prompt
|
|
530
|
+
* typed deep into a long session is invisible to text search; `historyMatcher`
|
|
531
|
+
* recovers those via `history.db`. The lookup hits SQLite synchronously, so it
|
|
532
|
+
* is debounced off the keystroke path ({@link HISTORY_MERGE_DEBOUNCE_MS}) and
|
|
533
|
+
* composed in when it lands, discarded if the query changed meanwhile.
|
|
534
|
+
*/
|
|
535
|
+
#scheduleHistoryMerge(query: string): void {
|
|
536
|
+
if (this.#historyMergeTimer !== undefined) {
|
|
537
|
+
clearTimeout(this.#historyMergeTimer);
|
|
538
|
+
this.#historyMergeTimer = undefined;
|
|
539
|
+
}
|
|
540
|
+
const matcher = this.#historyMatcher;
|
|
541
|
+
const trimmed = query.trim();
|
|
542
|
+
if (!matcher || trimmed.length < HISTORY_MERGE_MIN_QUERY) return;
|
|
543
|
+
this.#historyMergeTimer = setTimeout(() => {
|
|
544
|
+
this.#historyMergeTimer = undefined;
|
|
545
|
+
if (this.#searchInput.getValue() !== query) return;
|
|
546
|
+
if (this.#selectionMoved) return;
|
|
547
|
+
const historyIds = matcher(trimmed);
|
|
548
|
+
if (historyIds.length === 0) return;
|
|
549
|
+
this.#historyIds = historyIds;
|
|
550
|
+
this.#composeFiltered();
|
|
551
|
+
this.onRequestRender?.();
|
|
552
|
+
}, HISTORY_MERGE_DEBOUNCE_MS);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/** Cancel pending async search work; idempotent, called on every picker exit path. */
|
|
556
|
+
dispose(): void {
|
|
557
|
+
this.#scanGeneration++;
|
|
558
|
+
if (this.#scanTimer !== undefined) {
|
|
559
|
+
clearTimeout(this.#scanTimer);
|
|
560
|
+
this.#scanTimer = undefined;
|
|
561
|
+
}
|
|
562
|
+
if (this.#historyMergeTimer !== undefined) {
|
|
563
|
+
clearTimeout(this.#historyMergeTimer);
|
|
564
|
+
this.#historyMergeTimer = undefined;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
removeSession(sessionPath: string): void {
|
|
569
|
+
const index = this.#allSessions.findIndex(s => s.path === sessionPath);
|
|
570
|
+
if (index === -1) return;
|
|
571
|
+
this.#allSessions.splice(index, 1);
|
|
572
|
+
// Re-filter to update the composed result set
|
|
573
|
+
this.#filterSessions(this.#searchInput.getValue());
|
|
574
|
+
// Adjust selection if we deleted the last item or beyond
|
|
575
|
+
if (this.#menu.selectedIndex >= this.#menu.visibleItems.length) {
|
|
576
|
+
this.#menu.setSelectedIndex(Math.max(0, this.#menu.visibleItems.length - 1));
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/** Resolve a list-local rendered-line index to a filtered-session index. */
|
|
581
|
+
hitTestSession(line: number): number | undefined {
|
|
582
|
+
return this.#hitRows[line];
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/** Wheel notch: move the selection one step (clamped, no wrap). */
|
|
586
|
+
handleWheel(delta: -1 | 1): void {
|
|
587
|
+
if (this.#menu.visibleItems.length === 0) return;
|
|
588
|
+
this.#selectionMoved = true;
|
|
589
|
+
this.#menu.move(delta, false);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/** Mouse click: select the session under the pointer and resume it. */
|
|
593
|
+
selectAndConfirm(index: number): void {
|
|
594
|
+
const session = this.#menu.visibleItems[index];
|
|
595
|
+
if (!session) return;
|
|
596
|
+
this.#menu.setSelectedIndex(index);
|
|
597
|
+
this.onSelect?.(session);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
invalidate(): void {
|
|
601
|
+
// No cached state to invalidate currently
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
render(width: number): readonly string[] {
|
|
605
|
+
const lines: string[] = [];
|
|
606
|
+
this.#hitRows = [];
|
|
607
|
+
|
|
608
|
+
// Render search input
|
|
609
|
+
lines.push(...this.#searchInput.render(width));
|
|
610
|
+
lines.push(""); // Blank line after search
|
|
611
|
+
|
|
612
|
+
if (this.#menu.visibleItems.length === 0) {
|
|
613
|
+
if (this.#showCwd) {
|
|
614
|
+
lines.push(truncateToWidth(theme.fg("muted", "No sessions found"), width));
|
|
615
|
+
} else {
|
|
616
|
+
// "Current folder" scope - hint to try "all"
|
|
617
|
+
lines.push(
|
|
618
|
+
truncateToWidth(theme.fg("muted", "No sessions in current folder. Press Tab to view all."), width),
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
return lines;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Format dates
|
|
625
|
+
const formatDate = (date: Date): string => {
|
|
626
|
+
const now = new Date();
|
|
627
|
+
const diffMs = now.getTime() - date.getTime();
|
|
628
|
+
const diffMins = Math.floor(diffMs / 60000);
|
|
629
|
+
const diffHours = Math.floor(diffMs / 3600000);
|
|
630
|
+
const diffDays = Math.floor(diffMs / 86400000);
|
|
631
|
+
|
|
632
|
+
if (diffMins < 1) return "just now";
|
|
633
|
+
if (diffMins < 60) return `${diffMins} minute${diffMins !== 1 ? "s" : ""} ago`;
|
|
634
|
+
if (diffHours < 24) return `${diffHours} hour${diffHours !== 1 ? "s" : ""} ago`;
|
|
635
|
+
if (diffDays === 1) return "1 day ago";
|
|
636
|
+
if (diffDays < 7) return `${diffDays} days ago`;
|
|
637
|
+
|
|
638
|
+
return date.toLocaleDateString();
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
// Window the list around the selection by actual line height (3 lines
|
|
642
|
+
// per session, 4 when a title adds a preview line) until the viewport
|
|
643
|
+
// budget is spent, so short sessions never strand blank rows a
|
|
644
|
+
// worst-case count-based window would leave (then padded by
|
|
645
|
+
// fill-height).
|
|
646
|
+
const filtered = this.#menu.visibleItems;
|
|
647
|
+
const itemHeight = (session: T): number => (session.title ? 4 : 3);
|
|
648
|
+
const budget = this.#lineBudget();
|
|
649
|
+
const {
|
|
650
|
+
startIndex,
|
|
651
|
+
endIndex,
|
|
652
|
+
rowOffset: offsetRows,
|
|
653
|
+
totalRows: rawTotalRows,
|
|
654
|
+
} = getMenuWindow(filtered.map(itemHeight), this.#menu.selectedIndex, budget);
|
|
655
|
+
|
|
656
|
+
// Each session block is built into sessionLines, then wrapped by ScrollView
|
|
657
|
+
// so the right-edge scrollbar is proportional at the physical-line level.
|
|
658
|
+
const sessionLines: string[] = [];
|
|
659
|
+
const sessionRowIndex: number[] = [];
|
|
660
|
+
const rowWidth = contentRowWidth(width, rawTotalRows, budget);
|
|
661
|
+
const currentPath = this.#getCurrentSessionPath();
|
|
662
|
+
for (let i = startIndex; i < endIndex; i++) {
|
|
663
|
+
const blockStart = sessionLines.length;
|
|
664
|
+
const session = filtered[i];
|
|
665
|
+
if (!session) continue;
|
|
666
|
+
const isSelected = i === this.#menu.selectedIndex;
|
|
667
|
+
|
|
668
|
+
// Normalize first message to single line
|
|
669
|
+
const normalizedMessage = session.firstMessage.replace(/\n/g, " ").trim();
|
|
670
|
+
|
|
671
|
+
// First line: cursor + optional pin icon + title (or first message if no title)
|
|
672
|
+
const cursorSymbol = `${theme.nav.cursor} `;
|
|
673
|
+
const cursorWidth = visibleWidth(cursorSymbol);
|
|
674
|
+
const cursor = isSelected ? theme.fg("accent", cursorSymbol) : padding(cursorWidth);
|
|
675
|
+
const maxWidth = rowWidth - cursorWidth; // Account for cursor width
|
|
676
|
+
|
|
677
|
+
const isPinned = this.#pinnedIds.has(session.id);
|
|
678
|
+
const pinPrefix = isPinned ? `${theme.fg("accent", theme.icon.pin)} ` : "";
|
|
679
|
+
const pinPrefixWidth = isPinned ? visibleWidth(`${theme.icon.pin} `) : 0;
|
|
680
|
+
const maxTextWidth = Math.max(0, maxWidth - pinPrefixWidth);
|
|
681
|
+
|
|
682
|
+
if (session.title) {
|
|
683
|
+
// Has title: show title on first line, dimmed first message on second line
|
|
684
|
+
const truncatedTitle = truncateToWidth(session.title, maxTextWidth);
|
|
685
|
+
const titleLine = `${cursor}${pinPrefix}${isSelected ? theme.bold(truncatedTitle) : truncatedTitle}`;
|
|
686
|
+
sessionLines.push(titleLine);
|
|
687
|
+
|
|
688
|
+
// Second line: dimmed first message preview
|
|
689
|
+
const truncatedPreview = truncateToWidth(normalizedMessage, maxWidth);
|
|
690
|
+
sessionLines.push(` ${theme.fg("dim", truncatedPreview)}`);
|
|
691
|
+
} else {
|
|
692
|
+
// No title: show first message as main line
|
|
693
|
+
const truncatedMsg = truncateToWidth(normalizedMessage, maxTextWidth);
|
|
694
|
+
const messageLine = `${cursor}${pinPrefix}${isSelected ? theme.bold(truncatedMsg) : truncatedMsg}`;
|
|
695
|
+
sessionLines.push(messageLine);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Metadata line: date + file size + current marker + lifecycle status
|
|
699
|
+
// (+ project dir in all-projects scope). The status segment carries
|
|
700
|
+
// its own color, so each segment is dimmed individually rather than
|
|
701
|
+
// wrapping the whole line.
|
|
702
|
+
const dim = (s: string) => theme.fg("dim", s);
|
|
703
|
+
const dot = dim(theme.sep.dot);
|
|
704
|
+
const modified = formatDate(session.modified);
|
|
705
|
+
let metadata = ` ${dim(modified)} ${dot} ${dim(formatBytes(session.size))}`;
|
|
706
|
+
if (currentPath !== undefined && session.path === currentPath) {
|
|
707
|
+
metadata += ` ${dot} ${theme.fg("accent", "current")}`;
|
|
708
|
+
}
|
|
709
|
+
const status = formatSessionStatus(session.status);
|
|
710
|
+
if (status) {
|
|
711
|
+
metadata += ` ${dot} ${status}`;
|
|
712
|
+
}
|
|
713
|
+
if (session.parentSessionPath) {
|
|
714
|
+
metadata += ` ${dot} ${dim(`${theme.icon.branch} fork`)}`;
|
|
715
|
+
}
|
|
716
|
+
if (this.#showCwd && session.cwd) {
|
|
717
|
+
metadata += ` ${dot} ${dim(shortenPath(session.cwd))}`;
|
|
718
|
+
}
|
|
719
|
+
const metadataLine = truncateToWidth(metadata, rowWidth);
|
|
720
|
+
|
|
721
|
+
sessionLines.push(metadataLine);
|
|
722
|
+
// Blank separator between sessions; the last block ends flush against
|
|
723
|
+
// the footer, whose leading blank provides the same gap.
|
|
724
|
+
if (i < endIndex - 1) sessionLines.push("");
|
|
725
|
+
for (let k = blockStart; k < sessionLines.length; k++) sessionRowIndex[k] = i;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// Wrap the rendered window in a ScrollView for a proportional right-edge
|
|
729
|
+
// bar, with exact physical-line totals from the per-session heights.
|
|
730
|
+
// The last session's separator blank is never rendered (see the block
|
|
731
|
+
// loop above), so exclude it or a fully visible list would still show a
|
|
732
|
+
// scrollbar.
|
|
733
|
+
const totalRows = rawTotalRows - 1;
|
|
734
|
+
const sv = new ScrollView(sessionLines, {
|
|
735
|
+
height: sessionLines.length,
|
|
736
|
+
scrollbar: "auto",
|
|
737
|
+
totalRows,
|
|
738
|
+
theme: { track: t => theme.fg("muted", t), thumb: t => theme.fg("accent", t) },
|
|
739
|
+
});
|
|
740
|
+
sv.setScrollOffset(offsetRows);
|
|
741
|
+
const sessionRegionStart = lines.length;
|
|
742
|
+
const svLines = sv.render(width);
|
|
743
|
+
for (let k = 0; k < svLines.length; k++) this.#hitRows[sessionRegionStart + k] = sessionRowIndex[k];
|
|
744
|
+
lines.push(...svLines);
|
|
745
|
+
|
|
746
|
+
return lines;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
handleInput(keyData: string): void {
|
|
750
|
+
// Delete key — or Backspace on an empty search query — request delete
|
|
751
|
+
// confirmation from the parent. macOS laptops have no dedicated Forward
|
|
752
|
+
// Delete key: Fn+Backspace is the only way to send \e[3~, and many macOS
|
|
753
|
+
// terminals (Terminal.app, some iTerm2 profiles) deliver \x7f for that
|
|
754
|
+
// combo instead. Regular Backspace on an empty query means "delete
|
|
755
|
+
// session"; with a typed query it stays bound to the search Input so users
|
|
756
|
+
// can still edit their filter text.
|
|
757
|
+
if (
|
|
758
|
+
matchesKey(keyData, "delete") ||
|
|
759
|
+
(matchesKey(keyData, "backspace") && this.#searchInput.getValue().length === 0)
|
|
760
|
+
) {
|
|
761
|
+
const selected = this.#menu.selectedItem;
|
|
762
|
+
if (selected && this.onDeleteRequest) {
|
|
763
|
+
this.onDeleteRequest(selected);
|
|
764
|
+
}
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
// Up arrow
|
|
768
|
+
if (matchesSelectUp(keyData)) {
|
|
769
|
+
this.#selectionMoved = true;
|
|
770
|
+
this.#menu.move(-1, false);
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
// Down arrow
|
|
774
|
+
if (matchesSelectDown(keyData)) {
|
|
775
|
+
this.#selectionMoved = true;
|
|
776
|
+
this.#menu.move(1, false);
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
// Page up - jump up by maxVisible items
|
|
780
|
+
if (matchesKey(keyData, "pageUp")) {
|
|
781
|
+
this.#selectionMoved = true;
|
|
782
|
+
this.#menu.move(-this.#pageSize(), false);
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
// Page down - jump down by maxVisible items
|
|
786
|
+
if (matchesKey(keyData, "pageDown")) {
|
|
787
|
+
this.#selectionMoved = true;
|
|
788
|
+
this.#menu.move(this.#pageSize(), false);
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
// Enter
|
|
792
|
+
if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
|
|
793
|
+
const selected = this.#menu.selectedItem;
|
|
794
|
+
if (selected && this.onSelect) {
|
|
795
|
+
this.onSelect(selected);
|
|
796
|
+
}
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
// Escape - cancel
|
|
800
|
+
if (matchesAppInterrupt(keyData)) {
|
|
801
|
+
if (this.onCancel) {
|
|
802
|
+
this.onCancel();
|
|
803
|
+
}
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
// Ctrl+C - exit
|
|
807
|
+
if (matchesKey(keyData, "ctrl+c")) {
|
|
808
|
+
this.onExit();
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
// Tab - toggle folder / all-projects scope
|
|
812
|
+
if (matchesKey(keyData, "tab")) {
|
|
813
|
+
this.onToggleScope?.();
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
// Pass everything else to search input
|
|
817
|
+
this.#searchInput.handleInput(keyData);
|
|
818
|
+
this.#filterSessions(this.#searchInput.getValue());
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
export interface SessionSelectorOptions<T extends SessionSelectorEntry = SessionSelectorEntry> {
|
|
823
|
+
onDelete?: (session: T) => Promise<boolean>;
|
|
824
|
+
historyMatcher?: SessionHistoryMatcher;
|
|
825
|
+
/** Loads sessions across all projects for the all-projects scope toggle (Tab). */
|
|
826
|
+
loadAllSessions?: () => Promise<T[]>;
|
|
827
|
+
/** Preloaded all-projects list; cached so the first Tab toggle is instant. */
|
|
828
|
+
allSessions?: T[];
|
|
829
|
+
/** Picker heading; defaults to "Resume Session". */
|
|
830
|
+
title?: string;
|
|
831
|
+
/** Fixed scope label, or false to omit the scope suffix. */
|
|
832
|
+
scopeLabel?: string | false;
|
|
833
|
+
/** Show each session's working directory in the list. */
|
|
834
|
+
showCwd?: boolean;
|
|
835
|
+
/**
|
|
836
|
+
* Reads the live terminal height so the visible window fits the viewport.
|
|
837
|
+
* Omitted only in tests; defaults to a conservative 24 rows.
|
|
838
|
+
*/
|
|
839
|
+
getTerminalRows?: () => number;
|
|
840
|
+
/**
|
|
841
|
+
* Fill the whole viewport and pin the footer (hint + bottom border) to the
|
|
842
|
+
* last rows, so the footer stops drifting as the list window changes height.
|
|
843
|
+
* Set by the standalone `--resume` picker (fullscreen alternate screen); the
|
|
844
|
+
* in-editor selector leaves it off and renders compactly.
|
|
845
|
+
*/
|
|
846
|
+
fillHeight?: boolean;
|
|
847
|
+
/** Set of pinned session ids to display with a pin indicator. */
|
|
848
|
+
pinnedIds?: ReadonlySet<string>;
|
|
849
|
+
/** Path of the live session, or a getter so detach/newSession stays accurate. */
|
|
850
|
+
currentSessionPath?: string | (() => string | undefined);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/**
|
|
854
|
+
* Component that renders a session selector with optional confirmation dialog
|
|
855
|
+
*/
|
|
856
|
+
export class SessionSelectorComponent<T extends SessionSelectorEntry = SessionSelectorEntry> extends OverlayPanel {
|
|
857
|
+
#sessionList: SessionList<T>;
|
|
858
|
+
#confirmationDialog: HookSelectorComponent | null = null;
|
|
859
|
+
// Hosts whichever of `#sessionList` / `#confirmationDialog` is live this
|
|
860
|
+
// frame. The delete dialog REPLACES the list in this slot rather than being
|
|
861
|
+
// appended below the picker chrome, so the picker is always
|
|
862
|
+
// `chrome + max(list, dialog) + chrome` and never overflows the viewport
|
|
863
|
+
// (issue #3283: an overflowing dialog frame committed the header into
|
|
864
|
+
// scrollback, stranding it above the viewport once the dialog closed).
|
|
865
|
+
#contentSlot: Container;
|
|
866
|
+
#messageContainer: Container;
|
|
867
|
+
#onDelete?: (session: T) => Promise<boolean>;
|
|
868
|
+
#onRequestRender?: () => void;
|
|
869
|
+
readonly #loadAllSessions?: () => Promise<T[]>;
|
|
870
|
+
#folderSessions: T[];
|
|
871
|
+
#globalSessions: T[] | null = null;
|
|
872
|
+
#scope: "folder" | "all" = "folder";
|
|
873
|
+
#toggling = false;
|
|
874
|
+
#inputLocked = false;
|
|
875
|
+
// 0-based line where the session list begins within this component's own
|
|
876
|
+
// render, captured each frame. The fullscreen picker overlay paints from
|
|
877
|
+
// screen row 0, so a mouse row maps to `row - #listLineOffset` inside the
|
|
878
|
+
// list. Only meaningful while the picker holds the alternate screen.
|
|
879
|
+
#listLineOffset = 0;
|
|
880
|
+
// 0-based line where the pinned footer begins; clicks at or below it never
|
|
881
|
+
// hit-test the list, so a footer click on a cramped (trimmed) frame can't
|
|
882
|
+
// resume a session scrolled off-screen.
|
|
883
|
+
#footerStart = 0;
|
|
884
|
+
readonly #getTerminalRows: () => number;
|
|
885
|
+
readonly #fillHeight: boolean;
|
|
886
|
+
readonly #title: string;
|
|
887
|
+
readonly #scopeLabel: string | false | undefined;
|
|
888
|
+
|
|
889
|
+
constructor(
|
|
890
|
+
sessions: T[],
|
|
891
|
+
onSelect: (session: T) => void,
|
|
892
|
+
onCancel: () => void,
|
|
893
|
+
onExit: () => void,
|
|
894
|
+
options: SessionSelectorOptions<T> = {},
|
|
895
|
+
) {
|
|
896
|
+
super(options.title ?? "Resume Session");
|
|
897
|
+
|
|
898
|
+
this.#messageContainer = new Container();
|
|
899
|
+
this.#onDelete = options.onDelete;
|
|
900
|
+
this.#loadAllSessions = options.loadAllSessions;
|
|
901
|
+
this.#folderSessions = sessions;
|
|
902
|
+
this.#globalSessions = options.allSessions ?? null;
|
|
903
|
+
this.#getTerminalRows = options.getTerminalRows ?? (() => 24);
|
|
904
|
+
this.#fillHeight = options.fillHeight ?? false;
|
|
905
|
+
this.#title = options.title ?? "Resume Session";
|
|
906
|
+
this.#scopeLabel = options.scopeLabel;
|
|
907
|
+
this.title = this.#headerLabel();
|
|
908
|
+
// One spacer of breathing room; OverlayPanel supplies the two outer
|
|
909
|
+
// border rows and the horizontal inset.
|
|
910
|
+
this.addChild(new Spacer(1));
|
|
911
|
+
this.addChild(this.#messageContainer);
|
|
912
|
+
// Create session list in folder scope; the empty-state hint invites the
|
|
913
|
+
// user to Tab into all-projects rather than silently surfacing other
|
|
914
|
+
// projects' history (issue #3099).
|
|
915
|
+
this.#sessionList = new SessionList(
|
|
916
|
+
sessions,
|
|
917
|
+
options.showCwd ?? false,
|
|
918
|
+
options.historyMatcher,
|
|
919
|
+
options.getTerminalRows,
|
|
920
|
+
options.pinnedIds,
|
|
921
|
+
options.currentSessionPath,
|
|
922
|
+
);
|
|
923
|
+
// Every exit path cancels the list's pending history merge, so a stale
|
|
924
|
+
// debounce timer can never run its SQLite lookup after the picker closed.
|
|
925
|
+
this.#sessionList.onSelect = session => {
|
|
926
|
+
this.#sessionList.dispose();
|
|
927
|
+
onSelect(session);
|
|
928
|
+
};
|
|
929
|
+
this.#sessionList.onCancel = () => {
|
|
930
|
+
this.#sessionList.dispose();
|
|
931
|
+
onCancel();
|
|
932
|
+
};
|
|
933
|
+
this.#sessionList.onExit = () => {
|
|
934
|
+
this.#sessionList.dispose();
|
|
935
|
+
onExit();
|
|
936
|
+
};
|
|
937
|
+
this.#sessionList.onRequestRender = () => this.#onRequestRender?.();
|
|
938
|
+
this.#sessionList.onDeleteRequest = (session: T) => {
|
|
939
|
+
this.#showDeleteConfirmation(session);
|
|
940
|
+
};
|
|
941
|
+
if (this.#loadAllSessions || this.#globalSessions) {
|
|
942
|
+
this.#sessionList.onToggleScope = () => {
|
|
943
|
+
void this.#toggleScope();
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
this.#contentSlot = new Container();
|
|
947
|
+
this.#contentSlot.addChild(this.#sessionList);
|
|
948
|
+
this.addChild(this.#contentSlot);
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
#headerLabel(): string {
|
|
952
|
+
if (this.#scopeLabel === false) return this.#title;
|
|
953
|
+
const scopeLabel = this.#scopeLabel ?? (this.#scope === "all" ? "all projects" : "current folder");
|
|
954
|
+
return `${this.#title} (${scopeLabel})`;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
/**
|
|
958
|
+
* Toggle between current-folder and all-projects scope. The global list is
|
|
959
|
+
* loaded lazily on first switch and cached, so the common folder-scope path
|
|
960
|
+
* never pays for the cross-project scan.
|
|
961
|
+
*/
|
|
962
|
+
async #toggleScope(): Promise<void> {
|
|
963
|
+
if (this.#toggling || this.#confirmationDialog) return;
|
|
964
|
+
if (this.#scope === "folder") {
|
|
965
|
+
let global = this.#globalSessions;
|
|
966
|
+
if (!global) {
|
|
967
|
+
if (!this.#loadAllSessions) return;
|
|
968
|
+
this.#toggling = true;
|
|
969
|
+
this.#messageContainer.clear();
|
|
970
|
+
this.#messageContainer.addChild(new Text(theme.fg("muted", "Loading all projects…"), 0, 0));
|
|
971
|
+
this.#onRequestRender?.();
|
|
972
|
+
try {
|
|
973
|
+
global = await this.#loadAllSessions();
|
|
974
|
+
} catch (err) {
|
|
975
|
+
this.#showError(err instanceof Error ? err.message : String(err));
|
|
976
|
+
this.#toggling = false;
|
|
977
|
+
this.#onRequestRender?.();
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
this.#globalSessions = global;
|
|
981
|
+
this.#messageContainer.clear();
|
|
982
|
+
this.#toggling = false;
|
|
983
|
+
}
|
|
984
|
+
this.#scope = "all";
|
|
985
|
+
this.#sessionList.setSessions(global, true);
|
|
986
|
+
} else {
|
|
987
|
+
this.#scope = "folder";
|
|
988
|
+
this.#sessionList.setSessions(this.#folderSessions, false);
|
|
989
|
+
}
|
|
990
|
+
this.title = this.#headerLabel();
|
|
991
|
+
this.#onRequestRender?.();
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
setOnRequestRender(callback: () => void): void {
|
|
995
|
+
this.#onRequestRender = callback;
|
|
996
|
+
}
|
|
997
|
+
/** Ignore input after selection while the host resumes the session. */
|
|
998
|
+
lockInput(): void {
|
|
999
|
+
this.#inputLocked = true;
|
|
1000
|
+
}
|
|
1001
|
+
/** Re-enable input after a failed resume so the user can pick again. */
|
|
1002
|
+
unlockInput(): void {
|
|
1003
|
+
this.#inputLocked = false;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
/**
|
|
1007
|
+
* Dispose the session list explicitly: while the delete-confirmation dialog
|
|
1008
|
+
* is mounted the list is detached from the child tree, so Container's
|
|
1009
|
+
* child-walking dispose would miss its pending history-merge timer.
|
|
1010
|
+
*/
|
|
1011
|
+
override dispose(): void {
|
|
1012
|
+
this.#sessionList.dispose();
|
|
1013
|
+
super.dispose();
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
#clearError(): void {
|
|
1017
|
+
this.#messageContainer.clear();
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
#showError(message: string): void {
|
|
1021
|
+
this.#messageContainer.clear();
|
|
1022
|
+
this.#messageContainer.addChild(new Text(theme.fg("error", `Error: ${replaceTabs(message)}`), 0, 0));
|
|
1023
|
+
this.#messageContainer.addChild(new Spacer(1));
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
#showDeleteConfirmation(session: T): void {
|
|
1027
|
+
const displayName = session.title || session.firstMessage.slice(0, 40) || session.id;
|
|
1028
|
+
const closeDialog = () => {
|
|
1029
|
+
this.#confirmationDialog = null;
|
|
1030
|
+
// Restore the SessionList into the content slot so the picker is back
|
|
1031
|
+
// to its normal layout on the very next render — the same frame the
|
|
1032
|
+
// dialog disappears.
|
|
1033
|
+
this.#contentSlot.clear();
|
|
1034
|
+
this.#contentSlot.addChild(this.#sessionList);
|
|
1035
|
+
this.#onRequestRender?.();
|
|
1036
|
+
};
|
|
1037
|
+
this.#confirmationDialog = new HookSelectorComponent(
|
|
1038
|
+
`Delete session?\n${displayName}`,
|
|
1039
|
+
["Yes", "No"],
|
|
1040
|
+
async (option: string) => {
|
|
1041
|
+
if (option === "Yes" && this.#onDelete) {
|
|
1042
|
+
this.#clearError();
|
|
1043
|
+
try {
|
|
1044
|
+
const deleted = await this.#onDelete(session);
|
|
1045
|
+
if (deleted) {
|
|
1046
|
+
this.#sessionList.removeSession(session.path);
|
|
1047
|
+
this.#folderSessions = this.#folderSessions.filter(s => s.path !== session.path);
|
|
1048
|
+
if (this.#globalSessions) {
|
|
1049
|
+
this.#globalSessions = this.#globalSessions.filter(s => s.path !== session.path);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
} catch (err) {
|
|
1053
|
+
this.#showError(err instanceof Error ? err.message : String(err));
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
closeDialog();
|
|
1057
|
+
},
|
|
1058
|
+
closeDialog,
|
|
1059
|
+
);
|
|
1060
|
+
// Swap the SessionList out of the content slot and mount the dialog in its
|
|
1061
|
+
// place: the dialog competes only with the SessionList's rendered budget,
|
|
1062
|
+
// never the SessionList AND the picker chrome, so the picker frame stays
|
|
1063
|
+
// inside the terminal viewport and the TUI never commits the header into
|
|
1064
|
+
// scrollback (issue #3283).
|
|
1065
|
+
this.#contentSlot.clear();
|
|
1066
|
+
this.#contentSlot.addChild(this.#confirmationDialog);
|
|
1067
|
+
this.#onRequestRender?.();
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
/**
|
|
1071
|
+
* Render the panel directly so fill-height mode can keep its footer pinned
|
|
1072
|
+
* while sharing OverlayPanel's exact rounded-box chrome. Children receive
|
|
1073
|
+
* the panel's inner width before their rows are wrapped.
|
|
1074
|
+
*/
|
|
1075
|
+
override render(width: number): readonly string[] {
|
|
1076
|
+
const innerWidth = Math.max(1, width - 4);
|
|
1077
|
+
const lines: string[] = [topBorder(width, this.title)];
|
|
1078
|
+
for (const child of this.children) {
|
|
1079
|
+
const childLines = child.render(innerWidth);
|
|
1080
|
+
if (child === this.#contentSlot) this.#listLineOffset = lines.length;
|
|
1081
|
+
for (const line of childLines) lines.push(row(line, width));
|
|
1082
|
+
}
|
|
1083
|
+
const footer = this.#footerLines(width);
|
|
1084
|
+
if (this.#fillHeight) {
|
|
1085
|
+
const target = Math.max(0, this.#getTerminalRows() - footer.length);
|
|
1086
|
+
if (lines.length > target) lines.length = target;
|
|
1087
|
+
else for (let i = lines.length; i < target; i++) lines.push(row("", width));
|
|
1088
|
+
}
|
|
1089
|
+
this.#footerStart = lines.length;
|
|
1090
|
+
for (const line of footer) lines.push(line);
|
|
1091
|
+
return lines;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
/** Blank · keybinding hint · bottom border. Rendered by {@link render}. */
|
|
1095
|
+
#footerLines(width: number): string[] {
|
|
1096
|
+
const scopeHint = this.#scope === "all" ? "current folder" : "all projects";
|
|
1097
|
+
const hint = theme.fg("muted", `[Del/⌫ delete · Enter select · Tab ${scopeHint} · Esc cancel]`);
|
|
1098
|
+
return [row("", width), row(hint, width), row("", width), bottomBorder(width)];
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
handleInput(keyData: string): void {
|
|
1102
|
+
if (this.#inputLocked) return;
|
|
1103
|
+
if (keyData.startsWith("\x1b[<")) {
|
|
1104
|
+
this.#handleMouse(keyData);
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
if (this.#confirmationDialog) {
|
|
1108
|
+
this.#confirmationDialog.handleInput(keyData);
|
|
1109
|
+
} else {
|
|
1110
|
+
this.#sessionList.handleInput(keyData);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
/**
|
|
1115
|
+
* SGR mouse reports, delivered only while the picker holds the alternate
|
|
1116
|
+
* screen (the fullscreen overlay enables tracking and paints from screen row
|
|
1117
|
+
* 0). Wheel scrolls the list; a left click resumes the session under the
|
|
1118
|
+
* pointer. Mouse is inert while the delete-confirmation dialog is open.
|
|
1119
|
+
*/
|
|
1120
|
+
#handleMouse(data: string): void {
|
|
1121
|
+
if (this.#confirmationDialog) return;
|
|
1122
|
+
routeSgrMouseInput(data, event => {
|
|
1123
|
+
if (event.wheel !== null) {
|
|
1124
|
+
this.#sessionList.handleWheel(event.wheel);
|
|
1125
|
+
return true;
|
|
1126
|
+
}
|
|
1127
|
+
if (!event.leftClick || event.row >= this.#footerStart) return true;
|
|
1128
|
+
const index = this.#sessionList.hitTestSession(event.row - this.#listLineOffset);
|
|
1129
|
+
if (index !== undefined) this.#sessionList.selectAndConfirm(index);
|
|
1130
|
+
return true;
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
getSessionList(): SessionList<T> {
|
|
1135
|
+
return this.#sessionList;
|
|
1136
|
+
}
|
|
1137
|
+
}
|