@oh-my-pi/pi-tui 18.2.4 → 18.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/README.md +43 -0
- package/dist/types/app-keybindings.d.ts +395 -0
- package/dist/types/apps/autoresearch-dashboard.d.ts +43 -0
- package/dist/types/apps/autoresearch-data.d.ts +15 -0
- package/dist/types/apps/cleanse-board.d.ts +66 -0
- package/dist/types/apps/cleanse-picker.d.ts +23 -0
- package/dist/types/apps/debug/log-formatting.d.ts +6 -0
- package/dist/types/apps/debug/log-viewer.d.ts +81 -0
- package/dist/types/apps/debug/protocol-probe.d.ts +42 -0
- package/dist/types/apps/debug/raw-sse-buffer.d.ts +58 -0
- package/dist/types/apps/debug/raw-sse.d.ts +26 -0
- package/dist/types/apps/debug/terminal-info.d.ts +34 -0
- package/dist/types/apps/debug/viewer-frame.d.ts +52 -0
- package/dist/types/apps/git/avatar.d.ts +9 -0
- package/dist/types/apps/git/colors.d.ts +49 -0
- package/dist/types/apps/git/diff-pane.d.ts +188 -0
- package/dist/types/apps/git/git-tui.d.ts +66 -0
- package/dist/types/apps/git/sidebar.d.ts +149 -0
- package/dist/types/apps/git/state.d.ts +117 -0
- package/dist/types/apps/if-bench-board.d.ts +75 -0
- package/dist/types/apps/live-visualizer.d.ts +33 -0
- package/dist/types/apps/ps-data.d.ts +51 -0
- package/dist/types/apps/ps-top.d.ts +33 -0
- package/dist/types/apps/session-picker.d.ts +26 -0
- package/dist/types/apps/setup-model-picker.d.ts +13 -0
- package/dist/types/apps/standalone-picker.d.ts +42 -0
- package/dist/types/autocomplete.d.ts +4 -0
- package/dist/types/chat/advisor-message.d.ts +11 -0
- package/dist/types/chat/assistant-message.d.ts +106 -0
- package/dist/types/chat/background-tan-message.d.ts +9 -0
- package/dist/types/chat/bash-execution.d.ts +47 -0
- package/dist/types/chat/cache-invalidation-marker.d.ts +50 -0
- package/dist/types/chat/chat-transcript-builder.d.ts +50 -0
- package/dist/types/chat/collab-prompt-message.d.ts +10 -0
- package/dist/types/chat/compaction-summary-message.d.ts +54 -0
- package/dist/types/chat/custom-message.d.ts +10 -0
- package/dist/types/chat/display-preferences.d.ts +13 -0
- package/dist/types/chat/eval-execution.d.ts +29 -0
- package/dist/types/chat/execution-shared.d.ts +51 -0
- package/dist/types/chat/extension-types.d.ts +28 -0
- package/dist/types/chat/hook-message.d.ts +10 -0
- package/dist/types/chat/image-loading.d.ts +46 -0
- package/dist/types/chat/late-diagnostics-message.d.ts +21 -0
- package/dist/types/chat/messages.d.ts +189 -0
- package/dist/types/chat/reaction.d.ts +30 -0
- package/dist/types/chat/read-target.d.ts +4 -0
- package/dist/types/chat/read-tool-group.d.ts +67 -0
- package/dist/types/chat/served-model-marker.d.ts +47 -0
- package/dist/types/chat/skill-message.d.ts +33 -0
- package/dist/types/chat/skill-title-input.d.ts +14 -0
- package/dist/types/chat/stripped-tool-calls-placeholder.d.ts +12 -0
- package/dist/types/chat/thinking-display.d.ts +12 -0
- package/dist/types/chat/todo-reminder.d.ts +13 -0
- package/dist/types/chat/tool-execution.d.ts +138 -0
- package/dist/types/chat/transcript-browser.d.ts +93 -0
- package/dist/types/chat/transcript-entry.d.ts +45 -0
- package/dist/types/chat/transcript-outline.d.ts +86 -0
- package/dist/types/chat/transcript-render-helpers.d.ts +92 -0
- package/dist/types/chat/ttsr-notification.d.ts +23 -0
- package/dist/types/chat/user-message.d.ts +52 -0
- package/dist/types/chrome/chat-block.d.ts +64 -0
- package/dist/types/chrome/collab-qrcode.d.ts +28 -0
- package/dist/types/chrome/context-thresholds.d.ts +10 -0
- package/dist/types/chrome/countdown-timer.d.ts +14 -0
- package/dist/types/chrome/diff.d.ts +14 -0
- package/dist/types/chrome/dynamic-border.d.ts +16 -0
- package/dist/types/chrome/error-block.d.ts +17 -0
- package/dist/types/chrome/form-theme.d.ts +3 -0
- package/dist/types/chrome/format.d.ts +12 -0
- package/dist/types/chrome/index.d.ts +20 -0
- package/dist/types/chrome/keybinding-hints.d.ts +40 -0
- package/dist/types/chrome/live-board.d.ts +23 -0
- package/dist/types/chrome/local-date.d.ts +4 -0
- package/dist/types/chrome/message-divider.d.ts +22 -0
- package/dist/types/chrome/message-frame.d.ts +52 -0
- package/dist/types/chrome/message-notice.d.ts +35 -0
- package/dist/types/chrome/overlay-box.d.ts +74 -0
- package/dist/types/chrome/qrcode.d.ts +48 -0
- package/dist/types/chrome/segment-track.d.ts +27 -0
- package/dist/types/chrome/select-list-mouse-routing.d.ts +4 -0
- package/dist/types/chrome/selector-helpers.d.ts +52 -0
- package/dist/types/chrome/shared.d.ts +5 -0
- package/dist/types/chrome/streaming-panel.d.ts +19 -0
- package/dist/types/chrome/tool-activity.d.ts +17 -0
- package/dist/types/chrome/transcript-container.d.ts +129 -0
- package/dist/types/chrome/transcript-status.d.ts +12 -0
- package/dist/types/chrome/visual-truncate.d.ts +19 -0
- package/dist/types/components/disclosure.d.ts +42 -0
- package/dist/types/components/form.d.ts +130 -0
- package/dist/types/components/key-value-list.d.ts +24 -0
- package/dist/types/components/layout/geometry.d.ts +73 -0
- package/dist/types/components/layout/row.d.ts +55 -0
- package/dist/types/components/layout/split-pane.d.ts +80 -0
- package/dist/types/components/layout/stack.d.ts +50 -0
- package/dist/types/components/menu-selection.d.ts +71 -0
- package/dist/types/components/metric.d.ts +44 -0
- package/dist/types/components/progress-bar.d.ts +36 -0
- package/dist/types/components/scroll-view.d.ts +53 -19
- package/dist/types/components/scroll-viewport.d.ts +46 -0
- package/dist/types/components/section.d.ts +19 -0
- package/dist/types/components/select-list.d.ts +46 -2
- package/dist/types/components/settings-list.d.ts +20 -0
- package/dist/types/components/table.d.ts +32 -0
- package/dist/types/components/tree-view.d.ts +130 -0
- package/dist/types/components/wizard-step.d.ts +60 -0
- package/dist/types/hotkeys-markdown.d.ts +7 -0
- package/dist/types/index.d.ts +17 -0
- package/dist/types/keybinding-matchers.d.ts +35 -0
- package/dist/types/keys.d.ts +2 -2
- package/dist/types/lang-from-path.d.ts +9 -0
- package/dist/types/overlays/advisor-config.d.ts +114 -0
- package/dist/types/overlays/agent-activity.d.ts +41 -0
- package/dist/types/overlays/agent-hub-projection.d.ts +33 -0
- package/dist/types/overlays/agent-hub-renderer.d.ts +53 -0
- package/dist/types/overlays/agent-hub-types.d.ts +99 -0
- package/dist/types/overlays/agent-hub.d.ts +126 -0
- package/dist/types/overlays/agent-transcript-viewer.d.ts +71 -0
- package/dist/types/overlays/agents-hub.d.ts +72 -0
- package/dist/types/overlays/ask-dialog.d.ts +88 -0
- package/dist/types/overlays/bordered-loader.d.ts +11 -0
- package/dist/types/overlays/btw-history-panel.d.ts +29 -0
- package/dist/types/overlays/btw-history.d.ts +17 -0
- package/dist/types/overlays/btw-panel.d.ts +26 -0
- package/dist/types/overlays/cleanse-panel.d.ts +36 -0
- package/dist/types/overlays/codex-reset-fireworks.d.ts +52 -0
- package/dist/types/overlays/composer-shape-preview.d.ts +47 -0
- package/dist/types/overlays/composer-shape-registry.d.ts +50 -0
- package/dist/types/overlays/copy-selector.d.ts +48 -0
- package/dist/types/overlays/copy-targets.d.ts +63 -0
- package/dist/types/overlays/error-banner.d.ts +13 -0
- package/dist/types/overlays/extensions/display-text.d.ts +10 -0
- package/dist/types/overlays/extensions/extension-dashboard.d.ts +63 -0
- package/dist/types/overlays/extensions/extension-list.d.ts +54 -0
- package/dist/types/overlays/extensions/inspector-model.d.ts +109 -0
- package/dist/types/overlays/extensions/inspector-panel.d.ts +17 -0
- package/dist/types/overlays/extensions/live-tool-session.d.ts +28 -0
- package/dist/types/overlays/extensions/mcp-runtime.d.ts +107 -0
- package/dist/types/overlays/extensions/state-manager.d.ts +42 -0
- package/dist/types/overlays/extensions/types.d.ts +165 -0
- package/dist/types/overlays/history-search.d.ts +16 -0
- package/dist/types/overlays/hook-editor.d.ts +44 -0
- package/dist/types/overlays/hook-input.d.ts +20 -0
- package/dist/types/overlays/hook-selector.d.ts +71 -0
- package/dist/types/overlays/hub-frame.d.ts +77 -0
- package/dist/types/overlays/login-dialog.d.ts +43 -0
- package/dist/types/overlays/logout-account-selector.d.ts +15 -0
- package/dist/types/overlays/mcp-add-wizard.d.ts +87 -0
- package/dist/types/overlays/model-browser.d.ts +213 -0
- package/dist/types/overlays/model-hub.d.ts +72 -0
- package/dist/types/overlays/model-picker.d.ts +74 -0
- package/dist/types/overlays/model-selector.d.ts +52 -0
- package/dist/types/overlays/move-overlay.d.ts +39 -0
- package/dist/types/overlays/oauth-selector.d.ts +41 -0
- package/dist/types/overlays/omfg-panel.d.ts +20 -0
- package/dist/types/overlays/pause-screen.d.ts +43 -0
- package/dist/types/overlays/plan-review-overlay.d.ts +89 -0
- package/dist/types/overlays/plan-save-overlay.d.ts +19 -0
- package/dist/types/overlays/plan-toc.d.ts +41 -0
- package/dist/types/overlays/plugin-selector.d.ts +28 -0
- package/dist/types/overlays/plugin-settings.d.ts +180 -0
- package/dist/types/overlays/queue-mode-selector.d.ts +11 -0
- package/dist/types/overlays/reset-usage-selector.d.ts +23 -0
- package/dist/types/overlays/rewind-selector.d.ts +61 -0
- package/dist/types/overlays/running-subagent-badge.d.ts +6 -0
- package/dist/types/overlays/session-account-selector.d.ts +24 -0
- package/dist/types/overlays/session-info-overlay.d.ts +17 -0
- package/dist/types/overlays/session-observer-registry.d.ts +77 -0
- package/dist/types/overlays/session-selector.d.ts +131 -0
- package/dist/types/overlays/settings-defs.d.ts +119 -0
- package/dist/types/overlays/settings-selector.d.ts +73 -0
- package/dist/types/overlays/show-images-selector.d.ts +11 -0
- package/dist/types/overlays/snapcompact-shape-preview.d.ts +31 -0
- package/dist/types/overlays/theme-selector.d.ts +12 -0
- package/dist/types/overlays/thinking-selector.d.ts +12 -0
- package/dist/types/overlays/tiny-title-download-progress.d.ts +21 -0
- package/dist/types/overlays/tree-selector.d.ts +92 -0
- package/dist/types/overlays/usage-dashboard.d.ts +94 -0
- package/dist/types/overlays/usage-display.d.ts +5 -0
- package/dist/types/overlays/usage-row.d.ts +17 -0
- package/dist/types/prompt/attachment-chips.d.ts +19 -0
- package/dist/types/prompt/composer-attachments.d.ts +71 -0
- package/dist/types/prompt/composer-cache.d.ts +37 -0
- package/dist/types/prompt/composer.d.ts +159 -0
- package/dist/types/prompt/custom-editor.d.ts +261 -0
- package/dist/types/prompt/editor-top-gap.d.ts +18 -0
- package/dist/types/prompt/emoji-autocomplete.d.ts +16 -0
- package/dist/types/prompt/github-ref-autocomplete.d.ts +35 -0
- package/dist/types/prompt/gradient-highlight.d.ts +32 -0
- package/dist/types/prompt/image-format.d.ts +4 -0
- package/dist/types/prompt/image-references.d.ts +26 -0
- package/dist/types/prompt/interactive-context-helpers.d.ts +43 -0
- package/dist/types/prompt/internal-url-autocomplete.d.ts +56 -0
- package/dist/types/prompt/macos-spelling.d.ts +44 -0
- package/dist/types/prompt/magic-keyword-boundary.d.ts +9 -0
- package/dist/types/prompt/magic-keywords.d.ts +26 -0
- package/dist/types/prompt/markdown-prose.d.ts +15 -0
- package/dist/types/prompt/model-mention-autocomplete.d.ts +25 -0
- package/dist/types/prompt/model-mention-syntax.d.ts +24 -0
- package/dist/types/prompt/orchestrate.d.ts +13 -0
- package/dist/types/prompt/prompt-action-autocomplete.d.ts +56 -0
- package/dist/types/prompt/queue-input.d.ts +8 -0
- package/dist/types/prompt/skill-tokens.d.ts +13 -0
- package/dist/types/prompt/tools-markdown.d.ts +12 -0
- package/dist/types/prompt/ultrathink.d.ts +13 -0
- package/dist/types/prompt/usage-amounts.d.ts +26 -0
- package/dist/types/prompt/video.d.ts +18 -0
- package/dist/types/prompt/welcome.d.ts +83 -0
- package/dist/types/prompt/workflow.d.ts +13 -0
- package/dist/types/render/code-cell.d.ts +51 -0
- package/dist/types/render/file-list.d.ts +22 -0
- package/dist/types/render/hyperlink.d.ts +80 -0
- package/dist/types/render/index.d.ts +16 -0
- package/dist/types/render/output-block.d.ts +54 -0
- package/dist/types/render/output-pane.d.ts +80 -0
- package/dist/types/render/render-utils.d.ts +312 -0
- package/dist/types/render/sixel.d.ts +21 -0
- package/dist/types/render/status-line.d.ts +23 -0
- package/dist/types/render/tool-card.d.ts +71 -0
- package/dist/types/render/tree-list.d.ts +29 -0
- package/dist/types/render/types.d.ts +17 -0
- package/dist/types/render/utils.d.ts +43 -0
- package/dist/types/render/width-aware-text.d.ts +23 -0
- package/dist/types/setup/lazy.d.ts +3 -0
- package/dist/types/setup/scenes/composer.d.ts +3 -0
- package/dist/types/setup/scenes/glyph.d.ts +3 -0
- package/dist/types/setup/scenes/model.d.ts +3 -0
- package/dist/types/setup/scenes/outro.d.ts +2 -0
- package/dist/types/setup/scenes/providers.d.ts +3 -0
- package/dist/types/setup/scenes/sign-in.d.ts +21 -0
- package/dist/types/setup/scenes/splash.d.ts +11 -0
- package/dist/types/setup/scenes/theme.d.ts +3 -0
- package/dist/types/setup/scenes/types.d.ts +110 -0
- package/dist/types/setup/scenes/web-search.d.ts +22 -0
- package/dist/types/setup/setup-version.d.ts +11 -0
- package/dist/types/setup/startup-splash.d.ts +9 -0
- package/dist/types/setup/wizard-overlay.d.ts +16 -0
- package/dist/types/setup/wizard.d.ts +25 -0
- package/dist/types/status-line/component.d.ts +182 -0
- package/dist/types/status-line/context-usage.d.ts +156 -0
- package/dist/types/status-line/footer.d.ts +30 -0
- package/dist/types/status-line/git-utils.d.ts +22 -0
- package/dist/types/status-line/host.d.ts +129 -0
- package/dist/types/status-line/index.d.ts +6 -0
- package/dist/types/status-line/loop.d.ts +18 -0
- package/dist/types/status-line/metrics.d.ts +21 -0
- package/dist/types/status-line/presets.d.ts +3 -0
- package/dist/types/status-line/schema.d.ts +15 -0
- package/dist/types/status-line/segments.d.ts +5 -0
- package/dist/types/status-line/separators.d.ts +3 -0
- package/dist/types/status-line/types.d.ts +236 -0
- package/dist/types/stdin-buffer.d.ts +4 -0
- package/dist/types/theme/color.d.ts +24 -0
- package/dist/types/theme/defaults/index.d.ts +9489 -0
- package/dist/types/theme/index.d.ts +4 -0
- package/dist/types/theme/loader.d.ts +23 -0
- package/dist/types/theme/mermaid-cache.d.ts +26 -0
- package/dist/types/theme/schema-validation.d.ts +3 -0
- package/dist/types/theme/schema.d.ts +25 -0
- package/dist/types/theme/session-color.d.ts +43 -0
- package/dist/types/theme/shimmer.d.ts +44 -0
- package/dist/types/theme/symbols.d.ts +34 -0
- package/dist/types/theme/theme-class.d.ts +299 -0
- package/dist/types/theme/theme.d.ts +111 -0
- package/dist/types/theme/tui-adapters.d.ts +30 -0
- package/dist/types/thinking.d.ts +165 -0
- package/dist/types/tools/agent-tree.d.ts +36 -0
- package/dist/types/tools/ask.d.ts +67 -0
- package/dist/types/tools/ast-edit.d.ts +53 -0
- package/dist/types/tools/ast-grep.d.ts +52 -0
- package/dist/types/tools/autoresearch.d.ts +156 -0
- package/dist/types/tools/bash-interactive.d.ts +28 -0
- package/dist/types/tools/bash.d.ts +103 -0
- package/dist/types/tools/conflict-detect.d.ts +43 -0
- package/dist/types/tools/debug.d.ts +60 -0
- package/dist/types/tools/default-renderer.d.ts +24 -0
- package/dist/types/tools/edit.d.ts +158 -0
- package/dist/types/tools/eval-format/code-mode-declarations.d.ts +5 -0
- package/dist/types/tools/eval-format/index.d.ts +5 -0
- package/dist/types/tools/eval-format/javascript.d.ts +2 -0
- package/dist/types/tools/eval-format/python.d.ts +2 -0
- package/dist/types/tools/eval.d.ts +94 -0
- package/dist/types/tools/fetch.d.ts +42 -0
- package/dist/types/tools/gh-format.d.ts +8 -0
- package/dist/types/tools/github.d.ts +144 -0
- package/dist/types/tools/glob.d.ts +44 -0
- package/dist/types/tools/goal.d.ts +42 -0
- package/dist/types/tools/grep.d.ts +64 -0
- package/dist/types/tools/grouped-file-output.d.ts +71 -0
- package/dist/types/tools/hashline-format.d.ts +28 -0
- package/dist/types/tools/hub.d.ts +340 -0
- package/dist/types/tools/index.d.ts +4 -0
- package/dist/types/tools/json-tree.d.ts +59 -0
- package/dist/types/tools/line-ranges.d.ts +29 -0
- package/dist/types/tools/list-limit.d.ts +15 -0
- package/dist/types/tools/lsp.d.ts +72 -0
- package/dist/types/tools/match-line-format.d.ts +11 -0
- package/dist/types/tools/mcp.d.ts +81 -0
- package/dist/types/tools/memory.d.ts +67 -0
- package/dist/types/tools/output-meta.d.ts +151 -0
- package/dist/types/tools/read.d.ts +139 -0
- package/dist/types/tools/renderer.d.ts +98 -0
- package/dist/types/tools/report-tool-issue.d.ts +8 -0
- package/dist/types/tools/resolve.d.ts +47 -0
- package/dist/types/tools/streaming-output.d.ts +282 -0
- package/dist/types/tools/subprocess.d.ts +24 -0
- package/dist/types/tools/task-repair-args.d.ts +53 -0
- package/dist/types/tools/task-yield-assembly.d.ts +20 -0
- package/dist/types/tools/task.d.ts +425 -0
- package/dist/types/tools/terminal-output.d.ts +8 -0
- package/dist/types/tools/think.d.ts +13 -0
- package/dist/types/tools/todo.d.ts +118 -0
- package/dist/types/tools/tool-errors.d.ts +7 -0
- package/dist/types/tools/vibe.d.ts +120 -0
- package/dist/types/tools/web-search.d.ts +195 -0
- package/dist/types/tools/write.d.ts +76 -0
- package/dist/types/tools/xd-url.d.ts +13 -0
- package/dist/types/tools/xdev.d.ts +52 -0
- package/dist/types/tui.d.ts +18 -0
- package/dist/types/utils.d.ts +2 -0
- package/package.json +37 -3
- package/src/app-keybindings.ts +760 -0
- package/src/apps/autoresearch-dashboard.ts +500 -0
- package/src/apps/autoresearch-data.ts +64 -0
- package/src/apps/cleanse-board.ts +341 -0
- package/src/apps/cleanse-picker.ts +57 -0
- package/src/apps/debug/log-formatting.ts +53 -0
- package/src/apps/debug/log-viewer.ts +868 -0
- package/src/apps/debug/protocol-probe.ts +267 -0
- package/src/apps/debug/raw-sse-buffer.ts +449 -0
- package/src/apps/debug/raw-sse.ts +282 -0
- package/src/apps/debug/terminal-info.ts +115 -0
- package/src/apps/debug/viewer-frame.ts +174 -0
- package/src/apps/git/avatar.ts +52 -0
- package/src/apps/git/colors.ts +109 -0
- package/src/apps/git/diff-pane.ts +1512 -0
- package/src/apps/git/git-tui.ts +889 -0
- package/src/apps/git/sidebar.ts +1204 -0
- package/src/apps/git/state.ts +113 -0
- package/src/apps/if-bench-board.ts +324 -0
- package/src/apps/live-visualizer.ts +238 -0
- package/src/apps/ps-data.ts +116 -0
- package/src/apps/ps-top.ts +422 -0
- package/src/apps/session-picker.ts +95 -0
- package/src/apps/setup-model-picker.ts +21 -0
- package/src/apps/standalone-picker.ts +111 -0
- package/src/autocomplete.ts +9 -13
- package/src/chat/advisor-message.ts +175 -0
- package/src/chat/assistant-message.ts +1214 -0
- package/src/chat/background-tan-message.ts +35 -0
- package/src/chat/bash-execution.ts +394 -0
- package/src/chat/cache-invalidation-marker.ts +94 -0
- package/src/chat/chat-transcript-builder.ts +593 -0
- package/src/chat/collab-prompt-message.ts +34 -0
- package/src/chat/compaction-summary-message.ts +261 -0
- package/src/chat/custom-message.ts +23 -0
- package/src/chat/display-preferences.ts +24 -0
- package/src/chat/eval-execution.ts +172 -0
- package/src/chat/execution-shared.ts +113 -0
- package/src/chat/extension-types.ts +46 -0
- package/src/chat/hook-message.ts +20 -0
- package/src/chat/image-loading.ts +122 -0
- package/src/chat/late-diagnostics-message.ts +92 -0
- package/src/chat/messages.ts +305 -0
- package/src/chat/reaction.ts +65 -0
- package/src/chat/read-target.ts +7 -0
- package/src/chat/read-tool-group.ts +873 -0
- package/src/chat/served-model-marker.ts +91 -0
- package/src/chat/skill-message.ts +220 -0
- package/src/chat/skill-title-input.ts +41 -0
- package/src/chat/stripped-tool-calls-placeholder.ts +35 -0
- package/src/chat/thinking-display.ts +312 -0
- package/src/chat/todo-reminder.ts +33 -0
- package/src/chat/tool-execution.ts +1395 -0
- package/src/chat/transcript-browser.ts +268 -0
- package/src/chat/transcript-entry.ts +96 -0
- package/src/chat/transcript-outline.ts +260 -0
- package/src/chat/transcript-render-helpers.ts +315 -0
- package/src/chat/ttsr-notification.ts +123 -0
- package/src/chat/user-message.ts +287 -0
- package/src/chrome/chat-block.ts +110 -0
- package/src/chrome/collab-qrcode.ts +74 -0
- package/src/chrome/context-thresholds.ts +85 -0
- package/src/chrome/countdown-timer.ts +74 -0
- package/src/chrome/diff.ts +266 -0
- package/src/chrome/dynamic-border.ts +36 -0
- package/src/chrome/error-block.ts +63 -0
- package/src/chrome/form-theme.ts +10 -0
- package/src/chrome/format.ts +66 -0
- package/src/chrome/index.ts +21 -0
- package/src/chrome/keybinding-hints.ts +55 -0
- package/src/chrome/live-board.ts +123 -0
- package/src/chrome/local-date.ts +20 -0
- package/src/chrome/message-divider.ts +51 -0
- package/src/chrome/message-frame.ts +164 -0
- package/src/chrome/message-notice.ts +103 -0
- package/src/chrome/overlay-box.ts +301 -0
- package/src/chrome/qrcode.ts +539 -0
- package/src/chrome/segment-track.ts +86 -0
- package/src/chrome/select-list-mouse-routing.ts +11 -0
- package/src/chrome/selector-helpers.ts +119 -0
- package/src/chrome/shared.ts +27 -0
- package/src/chrome/streaming-panel.ts +72 -0
- package/src/chrome/tool-activity.ts +44 -0
- package/src/chrome/transcript-container.ts +902 -0
- package/src/chrome/transcript-status.ts +27 -0
- package/src/chrome/visual-truncate.ts +70 -0
- package/src/components/disclosure.ts +235 -0
- package/src/components/editor.ts +10 -12
- package/src/components/form.ts +566 -0
- package/src/components/input.ts +6 -27
- package/src/components/key-value-list.ts +88 -0
- package/src/components/layout/geometry.ts +202 -0
- package/src/components/layout/row.ts +283 -0
- package/src/components/layout/split-pane.ts +349 -0
- package/src/components/layout/stack.ts +261 -0
- package/src/components/markdown.ts +16 -12
- package/src/components/menu-selection.ts +248 -0
- package/src/components/metric.ts +132 -0
- package/src/components/progress-bar.ts +140 -0
- package/src/components/scroll-view.ts +372 -70
- package/src/components/scroll-viewport.ts +154 -0
- package/src/components/section.ts +77 -0
- package/src/components/select-list.ts +238 -159
- package/src/components/settings-list.ts +103 -80
- package/src/components/table.ts +127 -0
- package/src/components/tree-view.ts +481 -0
- package/src/components/wizard-step.ts +229 -0
- package/src/deccara.ts +1 -3
- package/src/hotkeys-markdown.ts +100 -0
- package/src/index.ts +18 -0
- package/src/keybinding-matchers.ts +99 -0
- package/src/keys.ts +2 -2
- package/src/lang-from-path.ts +252 -0
- package/src/overlays/advisor-config.ts +761 -0
- package/src/overlays/agent-activity.ts +113 -0
- package/src/overlays/agent-hub-projection.ts +250 -0
- package/src/overlays/agent-hub-renderer.ts +281 -0
- package/src/overlays/agent-hub-types.ts +74 -0
- package/src/overlays/agent-hub.ts +1562 -0
- package/src/overlays/agent-transcript-viewer.ts +669 -0
- package/src/overlays/agents-hub.ts +1203 -0
- package/src/overlays/ask-dialog.ts +1202 -0
- package/src/overlays/bordered-loader.ts +41 -0
- package/src/overlays/btw-history-panel.ts +591 -0
- package/src/overlays/btw-history.ts +33 -0
- package/src/overlays/btw-panel.ts +167 -0
- package/src/overlays/cleanse-panel.ts +167 -0
- package/src/overlays/codex-reset-fireworks.ts +375 -0
- package/src/overlays/composer-shape-preview.ts +137 -0
- package/src/overlays/composer-shape-registry.ts +82 -0
- package/src/overlays/copy-selector.ts +618 -0
- package/src/overlays/copy-targets.ts +210 -0
- package/src/overlays/error-banner.ts +39 -0
- package/src/overlays/extensions/display-text.ts +39 -0
- package/src/overlays/extensions/extension-dashboard.ts +645 -0
- package/src/overlays/extensions/extension-list.ts +667 -0
- package/src/overlays/extensions/inspector-model.ts +484 -0
- package/src/overlays/extensions/inspector-panel.ts +669 -0
- package/src/overlays/extensions/live-tool-session.ts +67 -0
- package/src/overlays/extensions/mcp-runtime.ts +289 -0
- package/src/overlays/extensions/state-manager.ts +337 -0
- package/src/overlays/extensions/types.ts +208 -0
- package/src/overlays/history-search.ts +268 -0
- package/src/overlays/hook-editor.ts +272 -0
- package/src/overlays/hook-input.ts +84 -0
- package/src/overlays/hook-selector.ts +635 -0
- package/src/overlays/hub-frame.ts +262 -0
- package/src/overlays/login-dialog.ts +231 -0
- package/src/overlays/logout-account-selector.ts +115 -0
- package/src/overlays/mcp-add-wizard.ts +1436 -0
- package/src/overlays/model-browser.ts +1176 -0
- package/src/overlays/model-hub.ts +2279 -0
- package/src/overlays/model-picker.ts +285 -0
- package/src/overlays/model-selector.ts +107 -0
- package/src/overlays/move-overlay.ts +197 -0
- package/src/overlays/oauth-selector.ts +446 -0
- package/src/overlays/omfg-panel.ts +136 -0
- package/src/overlays/pause-screen.ts +198 -0
- package/src/overlays/plan-review-overlay.ts +1227 -0
- package/src/overlays/plan-save-overlay.ts +74 -0
- package/src/overlays/plan-toc.ts +139 -0
- package/src/overlays/plugin-selector.ts +97 -0
- package/src/overlays/plugin-settings.ts +839 -0
- package/src/overlays/queue-mode-selector.ts +55 -0
- package/src/overlays/reset-usage-selector.ts +170 -0
- package/src/overlays/rewind-selector.ts +499 -0
- package/src/overlays/running-subagent-badge.ts +19 -0
- package/src/overlays/session-account-selector.ts +68 -0
- package/src/overlays/session-info-overlay.ts +96 -0
- package/src/overlays/session-observer-registry.ts +289 -0
- package/src/overlays/session-selector.ts +1137 -0
- package/src/overlays/settings-defs.ts +315 -0
- package/src/overlays/settings-selector.ts +1415 -0
- package/src/overlays/show-images-selector.ts +44 -0
- package/src/overlays/snapcompact-shape-preview-doc.md +14 -0
- package/src/overlays/snapcompact-shape-preview.ts +192 -0
- package/src/overlays/theme-selector.ts +62 -0
- package/src/overlays/thinking-selector.ts +51 -0
- package/src/overlays/tiny-title-download-progress.ts +102 -0
- package/src/overlays/tree-selector.ts +1121 -0
- package/src/overlays/usage-dashboard.ts +629 -0
- package/src/overlays/usage-display.ts +45 -0
- package/src/overlays/usage-row.ts +118 -0
- package/src/prompt/attachment-chips.ts +201 -0
- package/src/prompt/composer-attachments.ts +280 -0
- package/src/prompt/composer-cache.ts +304 -0
- package/src/prompt/composer.ts +934 -0
- package/src/prompt/custom-editor.ts +1392 -0
- package/src/prompt/data/emojis.json +1 -0
- package/src/prompt/editor-top-gap.ts +28 -0
- package/src/prompt/emoji-autocomplete.ts +285 -0
- package/src/prompt/github-ref-autocomplete.ts +75 -0
- package/src/prompt/gradient-highlight.ts +101 -0
- package/src/prompt/image-format.ts +28 -0
- package/src/prompt/image-references.ts +102 -0
- package/src/prompt/interactive-context-helpers.ts +126 -0
- package/src/prompt/internal-url-autocomplete.ts +155 -0
- package/src/prompt/macos-spelling.ts +456 -0
- package/src/prompt/magic-keyword-boundary.ts +23 -0
- package/src/prompt/magic-keywords.ts +42 -0
- package/src/prompt/markdown-prose.ts +245 -0
- package/src/prompt/model-mention-autocomplete.ts +94 -0
- package/src/prompt/model-mention-syntax.ts +41 -0
- package/src/prompt/orchestrate.ts +26 -0
- package/src/prompt/prompt-action-autocomplete.ts +337 -0
- package/src/prompt/queue-input.ts +132 -0
- package/src/prompt/skill-tokens.ts +36 -0
- package/src/prompt/tips.txt +27 -0
- package/src/prompt/tools-markdown.ts +29 -0
- package/src/prompt/ultrathink.ts +26 -0
- package/src/prompt/usage-amounts.ts +99 -0
- package/src/prompt/video.ts +47 -0
- package/src/prompt/welcome.ts +605 -0
- package/src/prompt/workflow.ts +26 -0
- package/src/render/code-cell.ts +244 -0
- package/src/render/file-list.ts +58 -0
- package/src/render/hyperlink.ts +192 -0
- package/src/render/index.ts +17 -0
- package/src/render/output-block.ts +267 -0
- package/src/render/output-pane.ts +299 -0
- package/src/render/render-utils.ts +1182 -0
- package/src/render/sixel.ts +69 -0
- package/src/render/status-line.ts +55 -0
- package/src/render/tool-card.ts +272 -0
- package/src/render/tree-list.ts +188 -0
- package/src/render/types.ts +19 -0
- package/src/render/utils.ts +109 -0
- package/src/render/width-aware-text.ts +60 -0
- package/src/setup/lazy.ts +17 -0
- package/src/setup/scenes/composer.ts +127 -0
- package/src/setup/scenes/glyph.ts +120 -0
- package/src/setup/scenes/model.ts +138 -0
- package/src/setup/scenes/outro.ts +24 -0
- package/src/setup/scenes/providers.ts +107 -0
- package/src/setup/scenes/sign-in.ts +373 -0
- package/src/setup/scenes/splash.ts +192 -0
- package/src/setup/scenes/theme.ts +343 -0
- package/src/setup/scenes/types.ts +111 -0
- package/src/setup/scenes/web-search.ts +165 -0
- package/src/setup/setup-version.ts +11 -0
- package/src/setup/startup-splash.ts +107 -0
- package/src/setup/wizard-overlay.ts +319 -0
- package/src/setup/wizard.ts +101 -0
- package/src/status-line/component.ts +3003 -0
- package/src/status-line/context-usage.ts +688 -0
- package/src/status-line/footer.ts +323 -0
- package/src/status-line/git-utils.ts +42 -0
- package/src/status-line/host.ts +97 -0
- package/src/status-line/index.ts +6 -0
- package/src/status-line/loop.ts +28 -0
- package/src/status-line/metrics.ts +94 -0
- package/src/status-line/presets.ts +107 -0
- package/src/status-line/schema.ts +57 -0
- package/src/status-line/segments.ts +934 -0
- package/src/status-line/separators.ts +55 -0
- package/src/status-line/types.ts +227 -0
- package/src/stdin-buffer.ts +4 -2
- package/src/theme/color.ts +95 -0
- package/src/theme/dark.json +95 -0
- package/src/theme/defaults/alabaster.json +93 -0
- package/src/theme/defaults/amethyst.json +96 -0
- package/src/theme/defaults/anthracite.json +93 -0
- package/src/theme/defaults/basalt.json +91 -0
- package/src/theme/defaults/birch.json +95 -0
- package/src/theme/defaults/dark-abyss.json +91 -0
- package/src/theme/defaults/dark-arctic.json +104 -0
- package/src/theme/defaults/dark-aurora.json +95 -0
- package/src/theme/defaults/dark-catppuccin.json +107 -0
- package/src/theme/defaults/dark-cavern.json +91 -0
- package/src/theme/defaults/dark-celestial.json +106 -0
- package/src/theme/defaults/dark-copper.json +95 -0
- package/src/theme/defaults/dark-cosmos.json +90 -0
- package/src/theme/defaults/dark-cyberpunk.json +102 -0
- package/src/theme/defaults/dark-dracula.json +98 -0
- package/src/theme/defaults/dark-eclipse.json +91 -0
- package/src/theme/defaults/dark-ember.json +95 -0
- package/src/theme/defaults/dark-equinox.json +90 -0
- package/src/theme/defaults/dark-forest.json +96 -0
- package/src/theme/defaults/dark-github.json +105 -0
- package/src/theme/defaults/dark-gruvbox.json +112 -0
- package/src/theme/defaults/dark-lavender.json +95 -0
- package/src/theme/defaults/dark-lunar.json +89 -0
- package/src/theme/defaults/dark-midnight.json +95 -0
- package/src/theme/defaults/dark-monochrome.json +94 -0
- package/src/theme/defaults/dark-monokai.json +98 -0
- package/src/theme/defaults/dark-nebula.json +90 -0
- package/src/theme/defaults/dark-nord.json +98 -0
- package/src/theme/defaults/dark-ocean.json +101 -0
- package/src/theme/defaults/dark-one.json +100 -0
- package/src/theme/defaults/dark-poimandres.json +115 -0
- package/src/theme/defaults/dark-rainforest.json +91 -0
- package/src/theme/defaults/dark-reef.json +91 -0
- package/src/theme/defaults/dark-retro.json +92 -0
- package/src/theme/defaults/dark-rose-pine.json +96 -0
- package/src/theme/defaults/dark-sakura.json +95 -0
- package/src/theme/defaults/dark-slate.json +95 -0
- package/src/theme/defaults/dark-solarized.json +97 -0
- package/src/theme/defaults/dark-solstice.json +90 -0
- package/src/theme/defaults/dark-starfall.json +91 -0
- package/src/theme/defaults/dark-sunset.json +99 -0
- package/src/theme/defaults/dark-swamp.json +90 -0
- package/src/theme/defaults/dark-synthwave.json +103 -0
- package/src/theme/defaults/dark-taiga.json +91 -0
- package/src/theme/defaults/dark-terminal.json +95 -0
- package/src/theme/defaults/dark-tokyo-night.json +101 -0
- package/src/theme/defaults/dark-tundra.json +91 -0
- package/src/theme/defaults/dark-twilight.json +91 -0
- package/src/theme/defaults/dark-volcanic.json +91 -0
- package/src/theme/defaults/graphite.json +92 -0
- package/src/theme/defaults/index.ts +201 -0
- package/src/theme/defaults/light-arctic.json +107 -0
- package/src/theme/defaults/light-aurora-day.json +91 -0
- package/src/theme/defaults/light-canyon.json +91 -0
- package/src/theme/defaults/light-catppuccin.json +106 -0
- package/src/theme/defaults/light-cirrus.json +90 -0
- package/src/theme/defaults/light-coral.json +95 -0
- package/src/theme/defaults/light-cyberpunk.json +96 -0
- package/src/theme/defaults/light-dawn.json +90 -0
- package/src/theme/defaults/light-dunes.json +91 -0
- package/src/theme/defaults/light-eucalyptus.json +95 -0
- package/src/theme/defaults/light-forest.json +100 -0
- package/src/theme/defaults/light-frost.json +95 -0
- package/src/theme/defaults/light-github.json +115 -0
- package/src/theme/defaults/light-glacier.json +91 -0
- package/src/theme/defaults/light-gruvbox.json +108 -0
- package/src/theme/defaults/light-haze.json +90 -0
- package/src/theme/defaults/light-honeycomb.json +95 -0
- package/src/theme/defaults/light-lagoon.json +91 -0
- package/src/theme/defaults/light-lavender.json +95 -0
- package/src/theme/defaults/light-meadow.json +91 -0
- package/src/theme/defaults/light-mint.json +95 -0
- package/src/theme/defaults/light-monochrome.json +101 -0
- package/src/theme/defaults/light-ocean.json +99 -0
- package/src/theme/defaults/light-one.json +99 -0
- package/src/theme/defaults/light-opal.json +91 -0
- package/src/theme/defaults/light-orchard.json +91 -0
- package/src/theme/defaults/light-paper.json +95 -0
- package/src/theme/defaults/light-poimandres.json +115 -0
- package/src/theme/defaults/light-prism.json +90 -0
- package/src/theme/defaults/light-retro.json +98 -0
- package/src/theme/defaults/light-sand.json +95 -0
- package/src/theme/defaults/light-savanna.json +91 -0
- package/src/theme/defaults/light-solarized.json +102 -0
- package/src/theme/defaults/light-soleil.json +90 -0
- package/src/theme/defaults/light-sunset.json +99 -0
- package/src/theme/defaults/light-synthwave.json +98 -0
- package/src/theme/defaults/light-tokyo-night.json +111 -0
- package/src/theme/defaults/light-wetland.json +91 -0
- package/src/theme/defaults/light-zenith.json +89 -0
- package/src/theme/defaults/limestone.json +94 -0
- package/src/theme/defaults/mahogany.json +97 -0
- package/src/theme/defaults/marble.json +93 -0
- package/src/theme/defaults/obsidian.json +91 -0
- package/src/theme/defaults/onyx.json +91 -0
- package/src/theme/defaults/pearl.json +93 -0
- package/src/theme/defaults/porcelain.json +91 -0
- package/src/theme/defaults/quartz.json +96 -0
- package/src/theme/defaults/sandstone.json +95 -0
- package/src/theme/defaults/titanium.json +90 -0
- package/src/theme/index.ts +5 -0
- package/src/theme/light.json +93 -0
- package/src/theme/loader.ts +200 -0
- package/src/theme/mermaid-cache.ts +92 -0
- package/src/theme/schema-validation.ts +119 -0
- package/src/theme/schema.ts +168 -0
- package/src/theme/session-color.ts +253 -0
- package/src/theme/shimmer.ts +307 -0
- package/src/theme/symbols.ts +1433 -0
- package/src/theme/theme-class.ts +787 -0
- package/src/theme/theme-schema.json +463 -0
- package/src/theme/theme.ts +810 -0
- package/src/theme/tui-adapters.ts +339 -0
- package/src/thinking.ts +380 -0
- package/src/tools/agent-tree.ts +128 -0
- package/src/tools/ask.ts +482 -0
- package/src/tools/ast-edit.ts +210 -0
- package/src/tools/ast-grep.ts +171 -0
- package/src/tools/autoresearch.ts +287 -0
- package/src/tools/bash-interactive.ts +295 -0
- package/src/tools/bash.ts +468 -0
- package/src/tools/conflict-detect.ts +77 -0
- package/src/tools/debug.ts +167 -0
- package/src/tools/default-renderer.ts +172 -0
- package/src/tools/edit.ts +1258 -0
- package/src/tools/eval-format/code-mode-declarations.ts +61 -0
- package/src/tools/eval-format/index.ts +16 -0
- package/src/tools/eval-format/javascript.ts +952 -0
- package/src/tools/eval-format/python.ts +544 -0
- package/src/tools/eval.ts +835 -0
- package/src/tools/fetch.ts +226 -0
- package/src/tools/gh-format.ts +18 -0
- package/src/tools/github.ts +686 -0
- package/src/tools/glob.ts +213 -0
- package/src/tools/goal.ts +153 -0
- package/src/tools/grep.ts +385 -0
- package/src/tools/grouped-file-output.ts +211 -0
- package/src/tools/hashline-format.ts +59 -0
- package/src/tools/hub.ts +1488 -0
- package/src/tools/index.ts +71 -0
- package/src/tools/json-tree.ts +388 -0
- package/src/tools/line-ranges.ts +98 -0
- package/src/tools/list-limit.ts +43 -0
- package/src/tools/lsp.ts +702 -0
- package/src/tools/match-line-format.ts +20 -0
- package/src/tools/mcp.ts +309 -0
- package/src/tools/memory.ts +220 -0
- package/src/tools/output-meta.ts +367 -0
- package/src/tools/read.ts +628 -0
- package/src/tools/renderer.ts +107 -0
- package/src/tools/report-tool-issue.ts +13 -0
- package/src/tools/resolve.ts +139 -0
- package/src/tools/streaming-output.ts +1576 -0
- package/src/tools/subprocess.ts +39 -0
- package/src/tools/task-repair-args.ts +118 -0
- package/src/tools/task-yield-assembly.ts +147 -0
- package/src/tools/task.ts +2110 -0
- package/src/tools/terminal-output.ts +153 -0
- package/src/tools/think.ts +28 -0
- package/src/tools/todo.ts +580 -0
- package/src/tools/tool-errors.ts +15 -0
- package/src/tools/vibe.ts +420 -0
- package/src/tools/web-search.ts +409 -0
- package/src/tools/write.ts +511 -0
- package/src/tools/xd-url.ts +28 -0
- package/src/tools/xdev.ts +201 -0
- package/src/tui.ts +57 -4
- package/src/utils.ts +8 -0
|
@@ -0,0 +1,1182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities and constants for tool renderers.
|
|
3
|
+
*
|
|
4
|
+
* Provides consistent formatting, truncation, and display patterns across all
|
|
5
|
+
* tool renderers to ensure a unified TUI experience.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as os from "node:os";
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
import { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
11
|
+
import type { Ellipsis } from "@oh-my-pi/pi-natives";
|
|
12
|
+
import { pluralize, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
13
|
+
import { formatKeyHints, type KeyId } from "../app-keybindings";
|
|
14
|
+
import { getKeybindings } from "../keybindings";
|
|
15
|
+
import type { Theme } from "../theme/theme";
|
|
16
|
+
import type { Component } from "../tui";
|
|
17
|
+
import { replaceTabs, sliceByColumn, truncateToWidth, visibleWidth } from "../utils";
|
|
18
|
+
import { Hasher } from "./utils";
|
|
19
|
+
|
|
20
|
+
export { Ellipsis } from "@oh-my-pi/pi-natives";
|
|
21
|
+
export { replaceTabs, truncateToWidth, wrapTextWithAnsi } from "../utils";
|
|
22
|
+
|
|
23
|
+
/** A thinking selector configured as a concrete level or automatic selection. */
|
|
24
|
+
export type ConfiguredThinkingLevel = ThinkingLevel | "auto";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Normalize stray carriage returns in model-authored display text. Some models
|
|
28
|
+
* (observed with GLM via OpenRouter) degenerate into injecting `\r` runs between
|
|
29
|
+
* words inside JSON string values; CommonMark treats a lone `\r` as a line
|
|
30
|
+
* ending, which splatters the text one word per row. CRLF becomes LF, CR runs
|
|
31
|
+
* collapse to a single space — word separators in prose, one indent unit in
|
|
32
|
+
* mangled code previews.
|
|
33
|
+
*/
|
|
34
|
+
export function sanitizeCarriageReturns(text: string): string {
|
|
35
|
+
if (!text.includes("\r")) return text;
|
|
36
|
+
return text.replaceAll("\r\n", "\n").replace(/\r+/g, " ");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Sanitize raw ask option labels into unique, action-safe display copies.
|
|
41
|
+
* Degenerate input can sanitize alike (`Retry\rnow`/`Retry now`) or match a
|
|
42
|
+
* runtime action row (`Other (type your own)`); both would answer the wrong
|
|
43
|
+
* row, so colliding entries take a numeric suffix. Order and length are
|
|
44
|
+
* preserved, so indices still align with the original labels for mapping
|
|
45
|
+
* answers and dialog state back. Every ask race participant (local dialog,
|
|
46
|
+
* guest selector) must call this with the same `reservedLabels` so a
|
|
47
|
+
* question renders identically wherever it is answered.
|
|
48
|
+
*/
|
|
49
|
+
export function disambiguateDisplayLabels(rawLabels: string[], reservedLabels: readonly string[]): string[] {
|
|
50
|
+
const taken = new Set<string>(reservedLabels);
|
|
51
|
+
return rawLabels.map(raw => {
|
|
52
|
+
const base = sanitizeCarriageReturns(raw);
|
|
53
|
+
let candidate = base;
|
|
54
|
+
for (let suffix = 2; taken.has(candidate); suffix++) candidate = `${base} (${suffix})`;
|
|
55
|
+
taken.add(candidate);
|
|
56
|
+
return candidate;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// =============================================================================
|
|
61
|
+
// Standardized Display Constants
|
|
62
|
+
// =============================================================================
|
|
63
|
+
|
|
64
|
+
/** Defaults for the host's tui.maxInlineImageColumns and tui.maxInlineImageRows settings. */
|
|
65
|
+
let inlineImageMaxColumns = 100;
|
|
66
|
+
let inlineImageMaxRows = 20;
|
|
67
|
+
|
|
68
|
+
/** Set the maximum inline image width in terminal columns; zero leaves it uncapped. */
|
|
69
|
+
export function setInlineImageMaxColumns(maxColumns: number): void {
|
|
70
|
+
inlineImageMaxColumns = maxColumns;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Set the maximum inline image height in terminal rows; zero uses the viewport limit. */
|
|
74
|
+
export function setInlineImageMaxRows(maxRows: number): void {
|
|
75
|
+
inlineImageMaxRows = maxRows;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Resolve inline image dimension caps from the configured limits and viewport. */
|
|
79
|
+
export function resolveImageOptions(): { maxWidthCells: number; maxHeightCells?: number } {
|
|
80
|
+
const maxWidthCells = inlineImageMaxColumns;
|
|
81
|
+
const rowSetting = Math.max(0, inlineImageMaxRows);
|
|
82
|
+
const viewportRows = process.stdout.rows;
|
|
83
|
+
const viewportFraction = viewportRows ? Math.floor(viewportRows * 0.6) : 0;
|
|
84
|
+
let maxHeightCells: number | undefined;
|
|
85
|
+
if (rowSetting === 0) {
|
|
86
|
+
// No explicit cap — use viewport fraction as safety bound
|
|
87
|
+
maxHeightCells = viewportFraction || undefined;
|
|
88
|
+
} else if (viewportFraction > 0) {
|
|
89
|
+
maxHeightCells = Math.min(rowSetting, viewportFraction);
|
|
90
|
+
} else {
|
|
91
|
+
// Viewport size unknown (transitional state) — honor explicit setting
|
|
92
|
+
maxHeightCells = rowSetting;
|
|
93
|
+
}
|
|
94
|
+
return { maxWidthCells, maxHeightCells };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Preview limits for collapsed/expanded views */
|
|
98
|
+
export const PREVIEW_LIMITS = {
|
|
99
|
+
/** Lines shown in collapsed view */
|
|
100
|
+
COLLAPSED_LINES: 3,
|
|
101
|
+
/** Lines shown in expanded view */
|
|
102
|
+
EXPANDED_LINES: 12,
|
|
103
|
+
/** Items (files, results) shown in collapsed view */
|
|
104
|
+
COLLAPSED_ITEMS: 8,
|
|
105
|
+
/** Output preview lines in collapsed view */
|
|
106
|
+
OUTPUT_COLLAPSED: 3,
|
|
107
|
+
/** Output preview lines in expanded view */
|
|
108
|
+
OUTPUT_EXPANDED: 10,
|
|
109
|
+
/** Computer script lines shown in collapsed view */
|
|
110
|
+
COMPUTER_CODE_COLLAPSED: 10,
|
|
111
|
+
/** Max hunks shown when collapsed (edit tool) */
|
|
112
|
+
DIFF_COLLAPSED_HUNKS: 8,
|
|
113
|
+
/** Max diff lines shown when collapsed (edit tool) */
|
|
114
|
+
DIFF_COLLAPSED_LINES: 40,
|
|
115
|
+
} as const;
|
|
116
|
+
|
|
117
|
+
/** Default number of terminal output rows shown before expansion. */
|
|
118
|
+
export const DEFAULT_TERMINAL_PREVIEW_LINES = 10;
|
|
119
|
+
|
|
120
|
+
/** Match a Markdown fenced-code opener and capture its indentation and marker. */
|
|
121
|
+
export const FENCE_RE = /^( {0,3})([`~]{3,})/;
|
|
122
|
+
|
|
123
|
+
/** Shared empty link-target lookup for renderers without interactive links. */
|
|
124
|
+
export const EMPTY_LINK_TARGETS: ReadonlyMap<string, string> = new Map();
|
|
125
|
+
|
|
126
|
+
/** Default display width reserved for a resolved model badge. */
|
|
127
|
+
export const FEED_MODEL_BADGE_WIDTH = 30;
|
|
128
|
+
|
|
129
|
+
let feedModelBadgeEnabled = false;
|
|
130
|
+
|
|
131
|
+
/** Set whether feed rows display their resolved model badge. */
|
|
132
|
+
export function setFeedModelBadgeEnabled(enabled: boolean): void {
|
|
133
|
+
feedModelBadgeEnabled = enabled;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Return whether resolved model badges are enabled. */
|
|
137
|
+
export function isFeedModelBadgeEnabled(): boolean {
|
|
138
|
+
return feedModelBadgeEnabled;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Compact glyph for a configured thinking level; empty for `inherit` (nothing to show). */
|
|
142
|
+
export function thinkingLevelGlyph(level: ConfiguredThinkingLevel, uiTheme: Theme): string {
|
|
143
|
+
if (level === ThinkingLevel.Inherit) return "";
|
|
144
|
+
if (level === ThinkingLevel.Off) return uiTheme.status.disabled;
|
|
145
|
+
const symbol = uiTheme.thinking[level === "auto" ? "autoPending" : level];
|
|
146
|
+
if (typeof symbol !== "string") return "";
|
|
147
|
+
const space = symbol.indexOf(" ");
|
|
148
|
+
return space < 0 ? symbol : symbol.slice(0, space);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Compact feed-row prefix: explicit thinking glyph, sanitized model identity,
|
|
153
|
+
* then advisor eye. Keep fitting icons if no model fits; preserve literal identity suffixes.
|
|
154
|
+
*/
|
|
155
|
+
export function formatFeedModelBadge(
|
|
156
|
+
modelIdentity: string | undefined,
|
|
157
|
+
thinkingLevel: ConfiguredThinkingLevel | undefined,
|
|
158
|
+
advisor: boolean | undefined,
|
|
159
|
+
uiTheme: Theme,
|
|
160
|
+
maxWidth = FEED_MODEL_BADGE_WIDTH,
|
|
161
|
+
): string {
|
|
162
|
+
if (!modelIdentity) return "";
|
|
163
|
+
const width = Math.max(0, Math.floor(maxWidth));
|
|
164
|
+
const clean = sanitizeText(modelIdentity).replace(/\s+/g, " ").trim();
|
|
165
|
+
if (!clean || !(width > 0)) return "";
|
|
166
|
+
const glyph = thinkingLevel !== undefined ? thinkingLevelGlyph(thinkingLevel, uiTheme) : "";
|
|
167
|
+
const advisorIcon = advisor === true ? uiTheme.icon.advisor : "";
|
|
168
|
+
const prefix = glyph ? `${glyph} ` : "";
|
|
169
|
+
const suffix = advisorIcon ? ` ${advisorIcon}` : "";
|
|
170
|
+
const modelWidth = width - visibleWidth(prefix) - visibleWidth(suffix);
|
|
171
|
+
if (modelWidth < 1) {
|
|
172
|
+
const advisorWidth = visibleWidth(advisorIcon);
|
|
173
|
+
if (advisorIcon && advisorWidth <= width) {
|
|
174
|
+
const thinkingPrefix = glyph && visibleWidth(prefix) + advisorWidth <= width ? prefix : "";
|
|
175
|
+
return uiTheme.fg("accent", thinkingPrefix) + uiTheme.fg("dim", advisorIcon);
|
|
176
|
+
}
|
|
177
|
+
return glyph && visibleWidth(glyph) <= width ? uiTheme.fg("accent", glyph) : "";
|
|
178
|
+
}
|
|
179
|
+
const model = truncateMiddleToWidth(clean, modelWidth);
|
|
180
|
+
return uiTheme.fg("accent", prefix) + uiTheme.fg("dim", `${model}${suffix}`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Truncation lengths for different content types */
|
|
184
|
+
export const TRUNCATE_LENGTHS = {
|
|
185
|
+
/** Short titles, labels */
|
|
186
|
+
TITLE: 60,
|
|
187
|
+
/** Medium-length content (messages, previews) */
|
|
188
|
+
CONTENT: 80,
|
|
189
|
+
/** Longer content (code, explanations) */
|
|
190
|
+
LONG: 100,
|
|
191
|
+
/** Full line content */
|
|
192
|
+
LINE: 110,
|
|
193
|
+
/** Very short (task previews, badges) */
|
|
194
|
+
SHORT: 40,
|
|
195
|
+
/** Idle recap status line (~40-word LLM reply) */
|
|
196
|
+
RECAP: 280,
|
|
197
|
+
} as const;
|
|
198
|
+
|
|
199
|
+
/** Keybinding action that toggles tool-output expansion. */
|
|
200
|
+
const EXPAND_ACTION = "app.tools.expand";
|
|
201
|
+
/** Fallback key when no binding is resolvable (e.g. outside an interactive session). */
|
|
202
|
+
const DEFAULT_EXPAND_KEY: KeyId = "ctrl+o";
|
|
203
|
+
|
|
204
|
+
/** Human-readable key currently bound to tool-output expansion, e.g. `Ctrl+O`. */
|
|
205
|
+
export function expandKeyHint(): string {
|
|
206
|
+
const keys = getKeybindings().getKeys(EXPAND_ACTION);
|
|
207
|
+
return formatKeyHints(keys.length > 0 ? keys : [DEFAULT_EXPAND_KEY]);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// =============================================================================
|
|
211
|
+
// Text Truncation Utilities
|
|
212
|
+
// =============================================================================
|
|
213
|
+
/** Keep both ends of a single-line label without splitting wide characters. */
|
|
214
|
+
function truncateMiddleToWidth(text: string, maxWidth: number): string {
|
|
215
|
+
const width = visibleWidth(text);
|
|
216
|
+
if (width <= maxWidth) return text;
|
|
217
|
+
if (maxWidth <= 1) return maxWidth === 1 ? "…" : "";
|
|
218
|
+
const tailWidth = Math.ceil((maxWidth - 1) / 2);
|
|
219
|
+
const headWidth = maxWidth - 1 - tailWidth;
|
|
220
|
+
return `${sliceByColumn(text, 0, headWidth, true)}…${sliceByColumn(text, width - tailWidth, tailWidth, true)}`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Select a leading line window and count the lines hidden after it. */
|
|
224
|
+
export function cappedHeadLines(lines: readonly string[], max: number): { lines: readonly string[]; hidden: number } {
|
|
225
|
+
const count = Math.max(0, Math.min(lines.length, Math.floor(max)));
|
|
226
|
+
return { lines: count === lines.length ? lines : lines.slice(0, count), hidden: lines.length - count };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Get the first nonblank preview lines, trimmed and width-capped. */
|
|
230
|
+
export function getPreviewLines(text: string, maxLines: number, maxLineLen: number, ellipsis?: Ellipsis): string[] {
|
|
231
|
+
const lines = text.split("\n").filter(l => l.trim());
|
|
232
|
+
return lines.slice(0, maxLines).map(l => truncateToWidth(l.trim(), maxLineLen, ellipsis));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Collapse a possibly multi-line string into a single line, then truncate it to
|
|
237
|
+
* `maxWidth` display cells. {@link truncateToWidth} alone caps width but
|
|
238
|
+
* newlines are zero-width, so multi-line content (markdown briefs, tool args,
|
|
239
|
+
* provider errors) would otherwise spill a single status row across several
|
|
240
|
+
* visual lines. Whitespace runs collapse to one space, so tabs are handled too.
|
|
241
|
+
*/
|
|
242
|
+
export function previewLine(text: string, maxWidth: number, ellipsis?: Ellipsis): string {
|
|
243
|
+
return truncateToWidth(text.replace(/\s+/g, " ").trim(), maxWidth, ellipsis);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// =============================================================================
|
|
247
|
+
// URL Utilities
|
|
248
|
+
// =============================================================================
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Extract domain from URL, stripping www. prefix.
|
|
252
|
+
*/
|
|
253
|
+
export function getDomain(url: string): string {
|
|
254
|
+
try {
|
|
255
|
+
const u = new URL(url);
|
|
256
|
+
return u.hostname.replace(/^www\./, "");
|
|
257
|
+
} catch {
|
|
258
|
+
return url;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// =============================================================================
|
|
263
|
+
// Formatting Utilities
|
|
264
|
+
// =============================================================================
|
|
265
|
+
|
|
266
|
+
export { formatAge, formatBytes, formatCount, formatDuration, formatNumber, pluralize } from "@oh-my-pi/pi-utils";
|
|
267
|
+
|
|
268
|
+
// =============================================================================
|
|
269
|
+
// Theme Helper Utilities
|
|
270
|
+
// =============================================================================
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Get the appropriate status icon with color for a given state.
|
|
274
|
+
* Standardizes status icon usage across all renderers.
|
|
275
|
+
*/
|
|
276
|
+
export function formatStatusIcon(status: ToolUIStatus, theme: Theme, spinnerFrame?: number): string {
|
|
277
|
+
switch (status) {
|
|
278
|
+
case "success":
|
|
279
|
+
return theme.styledSymbol("status.success", "success");
|
|
280
|
+
case "done":
|
|
281
|
+
return theme.styledSymbol("status.done", "success");
|
|
282
|
+
case "error":
|
|
283
|
+
return theme.styledSymbol("status.error", "error");
|
|
284
|
+
case "warning":
|
|
285
|
+
return theme.styledSymbol("status.warning", "warning");
|
|
286
|
+
case "info":
|
|
287
|
+
return theme.styledSymbol("status.info", "accent");
|
|
288
|
+
case "pending":
|
|
289
|
+
return theme.styledSymbol("status.pending", "muted");
|
|
290
|
+
case "running":
|
|
291
|
+
if (spinnerFrame !== undefined) {
|
|
292
|
+
const frames = theme.spinnerFrames;
|
|
293
|
+
return frames[spinnerFrame % frames.length];
|
|
294
|
+
}
|
|
295
|
+
return theme.styledSymbol("status.running", "accent");
|
|
296
|
+
case "aborted":
|
|
297
|
+
return theme.styledSymbol("status.aborted", "error");
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Format the expand hint with proper theming.
|
|
303
|
+
* Returns empty string if already expanded or there is nothing more to show.
|
|
304
|
+
*/
|
|
305
|
+
export function formatExpandHint(theme: Theme, expanded?: boolean, hasMore?: boolean): string {
|
|
306
|
+
if (expanded) return "";
|
|
307
|
+
if (hasMore === false) return "";
|
|
308
|
+
return theme.fg("dim", wrapBrackets(`${expandKeyHint()}: Expand`, theme));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Format a badge like [done] or [failed] with brackets and color.
|
|
313
|
+
*/
|
|
314
|
+
export function formatBadge(label: string, color: ToolUIColor, theme: Theme): string {
|
|
315
|
+
const left = theme.format.bracketLeft;
|
|
316
|
+
const right = theme.format.bracketRight;
|
|
317
|
+
return theme.fg(color, `${left}${label}${right}`);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Build a "more items" suffix line for truncated lists.
|
|
322
|
+
* Uses consistent wording pattern.
|
|
323
|
+
*/
|
|
324
|
+
export function formatMoreItems(remaining: number, itemType: string): string {
|
|
325
|
+
const safeRemaining = Number.isFinite(remaining) ? remaining : 0;
|
|
326
|
+
return `… ${safeRemaining} more ${pluralize(itemType, safeRemaining)}`;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Collapsed command/code previews render a tail window sized from the live
|
|
331
|
+
* viewport: terminal rows minus a reserve for the rest of the block (frame,
|
|
332
|
+
* Output section, stats line) and the editor/status area below the
|
|
333
|
+
* transcript. This keeps a volatile streaming block from growing past the
|
|
334
|
+
* viewport and stranding its top, while letting tall terminals show more.
|
|
335
|
+
*/
|
|
336
|
+
const PREVIEW_WINDOW_RESERVED_ROWS = 20;
|
|
337
|
+
/** Floor so tiny or unknown viewports still show a useful window. */
|
|
338
|
+
const PREVIEW_WINDOW_MIN_LINES = 6;
|
|
339
|
+
/** Assumed viewport when rows are unknown (non-TTY, tests). */
|
|
340
|
+
const PREVIEW_WINDOW_FALLBACK_ROWS = 30;
|
|
341
|
+
|
|
342
|
+
/** Tail-window height for collapsed command/code previews. */
|
|
343
|
+
export function previewWindowRows(): number {
|
|
344
|
+
const rows = process.stdout.rows || PREVIEW_WINDOW_FALLBACK_ROWS;
|
|
345
|
+
return Math.max(PREVIEW_WINDOW_MIN_LINES, rows - PREVIEW_WINDOW_RESERVED_ROWS);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Cap a pre-rendered command preview to a viewport-sized tail window: the end
|
|
350
|
+
* of the command stays visible (it is the live edge while args stream) behind
|
|
351
|
+
* an "… N earlier lines" marker on top. The same window applies while
|
|
352
|
+
* streaming and after completion so the block never jumps; only `expanded`
|
|
353
|
+
* (ctrl+o) uncaps it.
|
|
354
|
+
*
|
|
355
|
+
* `prefix` (raw, e.g. a dim tree gutter) is prepended to the marker line so
|
|
356
|
+
* nested previews stay aligned. `expandHint: false` drops the "ctrl+o: Expand"
|
|
357
|
+
* suffix for callers that cap even inside the expanded view (task recent
|
|
358
|
+
* output), where the hint would point the wrong way.
|
|
359
|
+
*/
|
|
360
|
+
export function capPreviewLines(
|
|
361
|
+
lines: string[],
|
|
362
|
+
theme: Theme,
|
|
363
|
+
options: { max?: number; expanded?: boolean; prefix?: string; expandHint?: boolean } = {},
|
|
364
|
+
): string[] {
|
|
365
|
+
if (options.expanded) return lines;
|
|
366
|
+
const max = options.max ?? previewWindowRows();
|
|
367
|
+
if (lines.length <= max) return lines;
|
|
368
|
+
const visible = max <= 1 ? [] : lines.slice(lines.length - (max - 1));
|
|
369
|
+
const hidden = lines.length - visible.length;
|
|
370
|
+
const hint = options.expandHint === false ? "" : formatExpandHint(theme, false, true);
|
|
371
|
+
const marker = `… ${hidden} earlier ${pluralize("line", hidden)}${hint ? ` ${hint}` : ""}`;
|
|
372
|
+
return [`${options.prefix ?? ""}${theme.fg("dim", marker)}`, ...visible];
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** Join metadata with the theme separator and muted styling. */
|
|
376
|
+
export function formatMeta(meta: string[], theme: Theme): string {
|
|
377
|
+
return meta.length > 0 ? ` ${theme.fg("muted", meta.join(theme.sep.dot))}` : "";
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function sanitizeErrorText(message: string | undefined): string {
|
|
381
|
+
const clean = (message ?? "").replace(/^Error:\s*/, "").trim();
|
|
382
|
+
return clean ? replaceTabs(truncateToWidth(clean, TRUNCATE_LENGTHS.LINE)) : "Unknown error";
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Split multi-line tool text into TUI-safe display lines.
|
|
387
|
+
* Splits CRLF/LF, collapses stray `\r` progress overwrites to the final
|
|
388
|
+
* segment (mirroring terminal rendering), and expands tabs — so raw
|
|
389
|
+
* subprocess or fetched output (e.g. Windows ssh emitting CRLF, tab-indented
|
|
390
|
+
* web content) can't corrupt the framed block layout with cursor-moving
|
|
391
|
+
* control characters or tab-stop width mismatches.
|
|
392
|
+
*/
|
|
393
|
+
export function sanitizeDisplayLines(text: string): string[] {
|
|
394
|
+
return text.split(/\r?\n/).map(line => {
|
|
395
|
+
const idx = line.lastIndexOf("\r");
|
|
396
|
+
return replaceTabs(sanitizeText(idx < 0 ? line : line.slice(idx + 1)));
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** Render a sanitized error message with its status icon. */
|
|
401
|
+
export function formatErrorMessage(message: string | undefined, theme: Theme): string {
|
|
402
|
+
return `${theme.styledSymbol("status.error", "error")} ${theme.fg("error", `Error: ${sanitizeErrorText(message)}`)}`;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Error message rendered as a subordinate detail line beneath a status header
|
|
407
|
+
* that already carries the error icon (e.g. `✘ Write: <path>`). The header's
|
|
408
|
+
* icon already signals failure, so this omits the redundant error symbol and
|
|
409
|
+
* "Error:" prefix that `formatErrorMessage` adds for standalone single-line
|
|
410
|
+
* errors, indenting two columns to sit under the header title instead.
|
|
411
|
+
*/
|
|
412
|
+
export function formatErrorDetail(message: string | undefined, theme: Theme): string {
|
|
413
|
+
return ` ${theme.fg("error", sanitizeErrorText(message))}`;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** Render a muted empty-state message with a warning icon. */
|
|
417
|
+
export function formatEmptyMessage(message: string, theme: Theme): string {
|
|
418
|
+
return `${theme.styledSymbol("status.warning", "warning")} ${theme.fg("muted", message)}`;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// =============================================================================
|
|
422
|
+
// Code Frame Formatting
|
|
423
|
+
// =============================================================================
|
|
424
|
+
|
|
425
|
+
/** Marker displayed in the gutter of a code frame. */
|
|
426
|
+
export type CodeFrameMarker = "" | " " | "*" | "+" | "-" | ">";
|
|
427
|
+
|
|
428
|
+
/** Format a code-frame row with a padded line-number gutter. */
|
|
429
|
+
export function formatCodeFrameLine(
|
|
430
|
+
marker: CodeFrameMarker,
|
|
431
|
+
lineNumber: string | number,
|
|
432
|
+
content: string,
|
|
433
|
+
lineNumberWidth: number,
|
|
434
|
+
): string {
|
|
435
|
+
const markerText = marker.trim();
|
|
436
|
+
const lineNumberText = String(lineNumber).trim();
|
|
437
|
+
const gutterText = markerText && lineNumberText ? `${markerText}${lineNumberText}` : lineNumberText || markerText;
|
|
438
|
+
return `${gutterText.padStart(lineNumberWidth + 1, " ")}│${content}`;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// =============================================================================
|
|
442
|
+
// Tool UI Helpers
|
|
443
|
+
// =============================================================================
|
|
444
|
+
|
|
445
|
+
/** Status states supported by shared tool renderer icons. */
|
|
446
|
+
export type ToolUIStatus = "success" | "done" | "error" | "warning" | "info" | "pending" | "running" | "aborted";
|
|
447
|
+
/** Semantic foreground colors used by tool renderer labels. */
|
|
448
|
+
export type ToolUIColor = "success" | "error" | "warning" | "accent" | "muted";
|
|
449
|
+
|
|
450
|
+
/** Presentation options for a tool title. */
|
|
451
|
+
export interface ToolUITitleOptions {
|
|
452
|
+
bold?: boolean;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/** Render a themed tool title with optional bold emphasis. */
|
|
456
|
+
export function formatTitle(label: string, theme: Theme, options?: ToolUITitleOptions): string {
|
|
457
|
+
const content = options?.bold === false ? label : theme.bold(label);
|
|
458
|
+
return theme.fg("toolTitle", content);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// =============================================================================
|
|
462
|
+
// Diagnostic Formatting
|
|
463
|
+
// =============================================================================
|
|
464
|
+
|
|
465
|
+
/** Parsed diagnostic location, severity, and optional source and code metadata. */
|
|
466
|
+
export interface ParsedDiagnostic {
|
|
467
|
+
filePath: string;
|
|
468
|
+
line: number;
|
|
469
|
+
col: number;
|
|
470
|
+
severity: "error" | "warning" | "info" | "hint";
|
|
471
|
+
source?: string;
|
|
472
|
+
message: string;
|
|
473
|
+
code?: string;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/** Expand tabs in diagnostic text for terminal display. */
|
|
477
|
+
export function sanitizeDiagnosticDisplayText(text: string): string {
|
|
478
|
+
return replaceTabs(text);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function getSeverityRank(severity: ParsedDiagnostic["severity"]): number {
|
|
482
|
+
switch (severity) {
|
|
483
|
+
case "error":
|
|
484
|
+
return 0;
|
|
485
|
+
case "warning":
|
|
486
|
+
return 1;
|
|
487
|
+
case "info":
|
|
488
|
+
return 2;
|
|
489
|
+
case "hint":
|
|
490
|
+
return 3;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/** Parse a diagnostic location and message, including optional source and code. */
|
|
495
|
+
export function parseDiagnosticMessage(msg: string): ParsedDiagnostic | null {
|
|
496
|
+
const match = msg.match(/^(.+?):(\d+):(\d+)\s+\[(\w+)\]\s+(?:\[([^\]]+)\]\s+)?(.+?)(?:\s+\(([^)]+)\))?$/);
|
|
497
|
+
if (!match) return null;
|
|
498
|
+
return {
|
|
499
|
+
filePath: sanitizeDiagnosticDisplayText(match[1]),
|
|
500
|
+
line: parseInt(match[2], 10),
|
|
501
|
+
col: parseInt(match[3], 10),
|
|
502
|
+
severity: match[4] as ParsedDiagnostic["severity"],
|
|
503
|
+
source: match[5] ? sanitizeDiagnosticDisplayText(match[5]) : undefined,
|
|
504
|
+
message: sanitizeDiagnosticDisplayText(match[6]),
|
|
505
|
+
code: match[7] ? sanitizeDiagnosticDisplayText(match[7]) : undefined,
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/** Render diagnostic summaries and optionally expanded messages. */
|
|
510
|
+
export function formatDiagnostics(
|
|
511
|
+
diag: { errored: boolean; summary: string; messages: string[] },
|
|
512
|
+
expanded: boolean,
|
|
513
|
+
theme: Theme,
|
|
514
|
+
getLangIcon: (filePath: string) => string,
|
|
515
|
+
options?: { title?: string },
|
|
516
|
+
): string {
|
|
517
|
+
if (diag.messages.length === 0) return "";
|
|
518
|
+
|
|
519
|
+
const byFile = new Map<string, ParsedDiagnostic[]>();
|
|
520
|
+
const unparsed: string[] = [];
|
|
521
|
+
|
|
522
|
+
for (const msg of diag.messages) {
|
|
523
|
+
const parsed = parseDiagnosticMessage(msg);
|
|
524
|
+
if (parsed) {
|
|
525
|
+
const existing = byFile.get(parsed.filePath) ?? [];
|
|
526
|
+
existing.push(parsed);
|
|
527
|
+
byFile.set(parsed.filePath, existing);
|
|
528
|
+
} else {
|
|
529
|
+
unparsed.push(sanitizeDiagnosticDisplayText(msg));
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
for (const diagnostics of byFile.values()) {
|
|
534
|
+
diagnostics.sort((a, b) => {
|
|
535
|
+
const severityCompare = getSeverityRank(a.severity) - getSeverityRank(b.severity);
|
|
536
|
+
if (severityCompare !== 0) return severityCompare;
|
|
537
|
+
if (a.line !== b.line) return a.line - b.line;
|
|
538
|
+
if (a.col !== b.col) return a.col - b.col;
|
|
539
|
+
return a.message.localeCompare(b.message);
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const headerIcon = diag.errored
|
|
544
|
+
? theme.styledSymbol("status.error", "error")
|
|
545
|
+
: theme.styledSymbol("status.warning", "warning");
|
|
546
|
+
const summary = sanitizeDiagnosticDisplayText(diag.summary);
|
|
547
|
+
const summaryTag = summary ? ` ${theme.fg("dim", `(${summary})`)}` : "";
|
|
548
|
+
let output = `\n\n${headerIcon} ${theme.fg("toolTitle", options?.title ?? "Diagnostics")}${summaryTag}`;
|
|
549
|
+
|
|
550
|
+
const maxDiags = expanded ? diag.messages.length : 5;
|
|
551
|
+
let diagsShown = 0;
|
|
552
|
+
|
|
553
|
+
const files = Array.from(byFile.entries());
|
|
554
|
+
|
|
555
|
+
// Count total diagnostics for "... X more" calculation
|
|
556
|
+
const totalParsedDiags = files.reduce((sum, [, diags]) => sum + diags.length, 0);
|
|
557
|
+
const totalDiags = totalParsedDiags + unparsed.length;
|
|
558
|
+
|
|
559
|
+
// Helper to check if this is the very last item in the tree
|
|
560
|
+
const isTreeEnd = (fileIdx: number, diagIdx: number | null, unparsedIdx: number | null): boolean => {
|
|
561
|
+
const willShowMore = totalDiags > diagsShown + 1;
|
|
562
|
+
if (willShowMore) return false;
|
|
563
|
+
|
|
564
|
+
if (unparsedIdx !== null) {
|
|
565
|
+
return unparsedIdx === unparsed.length - 1;
|
|
566
|
+
}
|
|
567
|
+
if (diagIdx !== null) {
|
|
568
|
+
const isLastDiagInFile = diagIdx === files[fileIdx][1].length - 1;
|
|
569
|
+
const isLastFile = fileIdx === files.length - 1;
|
|
570
|
+
return isLastDiagInFile && isLastFile && unparsed.length === 0;
|
|
571
|
+
}
|
|
572
|
+
// File node - never the tree end if it has diagnostics
|
|
573
|
+
return false;
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
for (let fi = 0; fi < files.length && diagsShown < maxDiags; fi++) {
|
|
577
|
+
const [filePath, diagnostics] = files[fi];
|
|
578
|
+
// File is "last" only if no more files AND no unparsed AND we'll show all diags AND no "... X more"
|
|
579
|
+
const remainingDiagsInFile = diagnostics.length;
|
|
580
|
+
const remainingDiagsAfter = files.slice(fi + 1).reduce((sum, [, d]) => sum + d.length, 0) + unparsed.length;
|
|
581
|
+
const willShowAllRemaining = diagsShown + remainingDiagsInFile + remainingDiagsAfter <= maxDiags;
|
|
582
|
+
const isLastFileNode = fi === files.length - 1 && unparsed.length === 0 && willShowAllRemaining;
|
|
583
|
+
const fileBranch = isLastFileNode ? theme.tree.last : theme.tree.branch;
|
|
584
|
+
|
|
585
|
+
const fileIcon = theme.fg("muted", getLangIcon(filePath));
|
|
586
|
+
output += `\n ${theme.fg("dim", fileBranch)} ${fileIcon} ${theme.fg("accent", filePath)}`;
|
|
587
|
+
|
|
588
|
+
for (let di = 0; di < diagnostics.length && diagsShown < maxDiags; di++) {
|
|
589
|
+
const d = diagnostics[di];
|
|
590
|
+
const isLastDiagInFile = di === diagnostics.length - 1;
|
|
591
|
+
// This is the last visible diag in file if it's actually last OR we're about to hit the limit
|
|
592
|
+
const atDisplayLimit = diagsShown + 1 >= maxDiags;
|
|
593
|
+
const isLastVisibleInFile = isLastDiagInFile || atDisplayLimit;
|
|
594
|
+
// Check if this is the last visible item in the entire tree
|
|
595
|
+
const isVeryLast = isTreeEnd(fi, di, null);
|
|
596
|
+
const diagBranch = isLastFileNode
|
|
597
|
+
? isLastVisibleInFile || isVeryLast
|
|
598
|
+
? ` ${theme.tree.last}`
|
|
599
|
+
: ` ${theme.tree.branch}`
|
|
600
|
+
: isLastVisibleInFile || isVeryLast
|
|
601
|
+
? `${theme.tree.vertical} ${theme.tree.last}`
|
|
602
|
+
: `${theme.tree.vertical} ${theme.tree.branch}`;
|
|
603
|
+
|
|
604
|
+
const sevIcon =
|
|
605
|
+
d.severity === "error"
|
|
606
|
+
? theme.styledSymbol("status.error", "error")
|
|
607
|
+
: d.severity === "warning"
|
|
608
|
+
? theme.styledSymbol("status.warning", "warning")
|
|
609
|
+
: theme.styledSymbol("status.info", "muted");
|
|
610
|
+
const location = theme.fg("dim", `:${d.line}:${d.col}`);
|
|
611
|
+
const codeTag = d.code ? theme.fg("dim", ` (${d.code})`) : "";
|
|
612
|
+
const msgColor = d.severity === "error" ? "error" : d.severity === "warning" ? "warning" : "toolOutput";
|
|
613
|
+
|
|
614
|
+
output += `\n ${theme.fg("dim", diagBranch)} ${sevIcon}${location} ${theme.fg(msgColor, d.message)}${codeTag}`;
|
|
615
|
+
diagsShown++;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
for (let ui = 0; ui < unparsed.length && diagsShown < maxDiags; ui++) {
|
|
620
|
+
const msg = unparsed[ui];
|
|
621
|
+
const isVeryLast = isTreeEnd(-1, null, ui);
|
|
622
|
+
const branch = isVeryLast ? theme.tree.last : theme.tree.branch;
|
|
623
|
+
const color = msg.includes("[error]") ? "error" : msg.includes("[warning]") ? "warning" : "dim";
|
|
624
|
+
output += `\n ${theme.fg("dim", branch)} ${theme.fg(color, msg)}`;
|
|
625
|
+
diagsShown++;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
if (totalDiags > diagsShown) {
|
|
629
|
+
const remaining = totalDiags - diagsShown;
|
|
630
|
+
output += `\n ${theme.fg("dim", theme.tree.last)} ${theme.fg(
|
|
631
|
+
"muted",
|
|
632
|
+
`… ${remaining} more`,
|
|
633
|
+
)} ${formatExpandHint(theme)}`;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
return output;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// =============================================================================
|
|
640
|
+
// Diff Utilities
|
|
641
|
+
// =============================================================================
|
|
642
|
+
|
|
643
|
+
/** Added and removed line counts and hunk metadata for a diff. */
|
|
644
|
+
export interface DiffStats {
|
|
645
|
+
added: number;
|
|
646
|
+
removed: number;
|
|
647
|
+
hunks: number;
|
|
648
|
+
lines: number;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/** Count added lines, removed lines, and hunks in a unified diff. */
|
|
652
|
+
export function getDiffStats(diffText: string): DiffStats {
|
|
653
|
+
const lines = diffText ? diffText.split("\n") : [];
|
|
654
|
+
let added = 0;
|
|
655
|
+
let removed = 0;
|
|
656
|
+
let hunks = 0;
|
|
657
|
+
let inHunk = false;
|
|
658
|
+
|
|
659
|
+
for (const line of lines) {
|
|
660
|
+
const isAdded = line.startsWith("+");
|
|
661
|
+
const isRemoved = line.startsWith("-");
|
|
662
|
+
const isChange = isAdded || isRemoved;
|
|
663
|
+
|
|
664
|
+
if (isAdded) added++;
|
|
665
|
+
if (isRemoved) removed++;
|
|
666
|
+
|
|
667
|
+
if (isChange && !inHunk) {
|
|
668
|
+
hunks++;
|
|
669
|
+
inHunk = true;
|
|
670
|
+
} else if (!isChange) {
|
|
671
|
+
inHunk = false;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
return { added, removed, hunks, lines: lines.length };
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/** Render a compact themed summary of diff counts. */
|
|
679
|
+
export function formatDiffStats(added: number, removed: number, hunks: number, theme: Theme): string {
|
|
680
|
+
const parts: string[] = [];
|
|
681
|
+
if (added > 0) parts.push(theme.fg("toolDiffAdded", `+${added}`));
|
|
682
|
+
if (removed > 0) parts.push(theme.fg("toolDiffRemoved", `-${removed}`));
|
|
683
|
+
if (hunks > 0) parts.push(theme.fg("dim", `${hunks} hunk${hunks !== 1 ? "s" : ""}`));
|
|
684
|
+
return parts.join(theme.fg("dim", " / "));
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
interface DiffSegment {
|
|
688
|
+
lines: string[];
|
|
689
|
+
isChange: boolean;
|
|
690
|
+
isEllipsis: boolean;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function parseDiffSegments(lines: string[]): DiffSegment[] {
|
|
694
|
+
const segments: DiffSegment[] = [];
|
|
695
|
+
let current: DiffSegment | null = null;
|
|
696
|
+
|
|
697
|
+
for (const line of lines) {
|
|
698
|
+
const isChange = line.startsWith("+") || line.startsWith("-");
|
|
699
|
+
const isEllipsis = line.trimStart().startsWith("...") || line.trim().length === 0;
|
|
700
|
+
|
|
701
|
+
if (isEllipsis) {
|
|
702
|
+
if (current) segments.push(current);
|
|
703
|
+
segments.push({ lines: [line], isChange: false, isEllipsis: true });
|
|
704
|
+
current = null;
|
|
705
|
+
} else if (!current || current.isChange !== isChange) {
|
|
706
|
+
if (current) segments.push(current);
|
|
707
|
+
current = { lines: [line], isChange, isEllipsis: false };
|
|
708
|
+
} else {
|
|
709
|
+
current.lines.push(line);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
if (current) segments.push(current);
|
|
714
|
+
return segments;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/** Truncate a unified diff at hunk boundaries within the supplied preview limits. */
|
|
718
|
+
export function truncateDiffByHunk(
|
|
719
|
+
diffText: string,
|
|
720
|
+
maxHunks: number,
|
|
721
|
+
maxLines: number,
|
|
722
|
+
options?: { fromTail?: boolean },
|
|
723
|
+
): { text: string; hiddenHunks: number; hiddenLines: number } {
|
|
724
|
+
if (options?.fromTail) {
|
|
725
|
+
// Streaming previews want to track the tail of the diff as new hunks
|
|
726
|
+
// arrive. Reversing the line buffer reuses the head-mode logic without
|
|
727
|
+
// duplicating the segment-budget bookkeeping: hunk runs survive
|
|
728
|
+
// reversal (a continuous `+`/`-` block stays contiguous) and so do the
|
|
729
|
+
// per-line `+`/`-` markers, so getDiffStats yields identical counts.
|
|
730
|
+
const reversed = (diffText ?? "").split("\n").reverse().join("\n");
|
|
731
|
+
const result = truncateDiffByHunk(reversed, maxHunks, maxLines);
|
|
732
|
+
return {
|
|
733
|
+
text: result.text.split("\n").reverse().join("\n"),
|
|
734
|
+
hiddenHunks: result.hiddenHunks,
|
|
735
|
+
hiddenLines: result.hiddenLines,
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
const lines = diffText ? diffText.split("\n") : [];
|
|
739
|
+
const totalStats = getDiffStats(diffText);
|
|
740
|
+
|
|
741
|
+
if (lines.length <= maxLines && totalStats.hunks <= maxHunks) {
|
|
742
|
+
return { text: diffText, hiddenHunks: 0, hiddenLines: 0 };
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
const segments = parseDiffSegments(lines);
|
|
746
|
+
|
|
747
|
+
const changeSegments = segments.filter(s => s.isChange);
|
|
748
|
+
const changeLineCount = changeSegments.reduce((sum, s) => sum + s.lines.length, 0);
|
|
749
|
+
|
|
750
|
+
if (changeLineCount > maxLines) {
|
|
751
|
+
const kept: string[] = [];
|
|
752
|
+
let keptHunks = 0;
|
|
753
|
+
|
|
754
|
+
for (const seg of segments) {
|
|
755
|
+
if (kept.length >= maxLines) break;
|
|
756
|
+
if (seg.isChange) {
|
|
757
|
+
if (keptHunks >= maxHunks) break;
|
|
758
|
+
keptHunks++;
|
|
759
|
+
}
|
|
760
|
+
const take = Math.min(seg.lines.length, maxLines - kept.length);
|
|
761
|
+
for (let i = 0; i < take; i++) {
|
|
762
|
+
kept.push(seg.lines[i]!);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
return {
|
|
767
|
+
text: kept.join("\n"),
|
|
768
|
+
hiddenHunks: Math.max(0, totalStats.hunks - keptHunks),
|
|
769
|
+
hiddenLines: Math.max(0, lines.length - kept.length),
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const contextBudget = maxLines - changeLineCount;
|
|
774
|
+
const contextSegments = segments.filter(s => !s.isChange);
|
|
775
|
+
const totalContextLines = contextSegments.reduce((sum, s) => sum + s.lines.length, 0);
|
|
776
|
+
|
|
777
|
+
const kept: string[] = [];
|
|
778
|
+
let keptHunks = 0;
|
|
779
|
+
let keptSourceLines = 0;
|
|
780
|
+
|
|
781
|
+
if (totalContextLines <= contextBudget) {
|
|
782
|
+
for (const seg of segments) {
|
|
783
|
+
if (seg.isChange) {
|
|
784
|
+
if (keptHunks >= maxHunks) break;
|
|
785
|
+
keptHunks++;
|
|
786
|
+
}
|
|
787
|
+
kept.push(...seg.lines);
|
|
788
|
+
keptSourceLines += seg.lines.length;
|
|
789
|
+
}
|
|
790
|
+
} else {
|
|
791
|
+
const contextRatio = totalContextLines > 0 ? contextBudget / totalContextLines : 0;
|
|
792
|
+
let remainingContextBudget = contextBudget;
|
|
793
|
+
|
|
794
|
+
for (let i = 0; i < segments.length; i++) {
|
|
795
|
+
const seg = segments[i];
|
|
796
|
+
|
|
797
|
+
if (seg.isChange) {
|
|
798
|
+
if (keptHunks >= maxHunks) break;
|
|
799
|
+
keptHunks++;
|
|
800
|
+
kept.push(...seg.lines);
|
|
801
|
+
keptSourceLines += seg.lines.length;
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
if (remainingContextBudget <= 0) continue;
|
|
805
|
+
|
|
806
|
+
const allowedLines = Math.min(
|
|
807
|
+
remainingContextBudget,
|
|
808
|
+
Math.max(1, Math.floor(seg.lines.length * contextRatio)),
|
|
809
|
+
);
|
|
810
|
+
const outputStart = kept.length;
|
|
811
|
+
let sourceLinesAdded = 0;
|
|
812
|
+
|
|
813
|
+
if (seg.isEllipsis || seg.lines.length <= allowedLines) {
|
|
814
|
+
for (let j = 0; j < allowedLines; j++) {
|
|
815
|
+
kept.push(seg.lines[j]!);
|
|
816
|
+
}
|
|
817
|
+
sourceLinesAdded = allowedLines;
|
|
818
|
+
} else {
|
|
819
|
+
const isBeforeChange = segments[i + 1]?.isChange;
|
|
820
|
+
const isAfterChange = segments[i - 1]?.isChange;
|
|
821
|
+
|
|
822
|
+
if (isBeforeChange && isAfterChange) {
|
|
823
|
+
if (allowedLines >= 3) {
|
|
824
|
+
const sourceBudget = allowedLines - 1;
|
|
825
|
+
const firstCount = Math.ceil(sourceBudget / 2);
|
|
826
|
+
const lastCount = sourceBudget - firstCount;
|
|
827
|
+
kept.push(...seg.lines.slice(0, firstCount));
|
|
828
|
+
kept.push("");
|
|
829
|
+
if (lastCount > 0) kept.push(...seg.lines.slice(-lastCount));
|
|
830
|
+
sourceLinesAdded = sourceBudget;
|
|
831
|
+
} else {
|
|
832
|
+
const firstCount = Math.ceil(allowedLines / 2);
|
|
833
|
+
const lastCount = allowedLines - firstCount;
|
|
834
|
+
kept.push(...seg.lines.slice(0, firstCount));
|
|
835
|
+
if (lastCount > 0) kept.push(...seg.lines.slice(-lastCount));
|
|
836
|
+
sourceLinesAdded = allowedLines;
|
|
837
|
+
}
|
|
838
|
+
} else if (isBeforeChange) {
|
|
839
|
+
kept.push(...seg.lines.slice(-allowedLines));
|
|
840
|
+
sourceLinesAdded = allowedLines;
|
|
841
|
+
} else if (isAfterChange) {
|
|
842
|
+
kept.push(...seg.lines.slice(0, allowedLines));
|
|
843
|
+
sourceLinesAdded = allowedLines;
|
|
844
|
+
} else {
|
|
845
|
+
const take = Math.min(allowedLines, 2);
|
|
846
|
+
kept.push(...seg.lines.slice(0, take));
|
|
847
|
+
sourceLinesAdded = take;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
keptSourceLines += sourceLinesAdded;
|
|
852
|
+
remainingContextBudget -= kept.length - outputStart;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
return {
|
|
857
|
+
text: kept.join("\n"),
|
|
858
|
+
hiddenHunks: Math.max(0, totalStats.hunks - keptHunks),
|
|
859
|
+
hiddenLines: Math.max(0, lines.length - keptSourceLines),
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// =============================================================================
|
|
864
|
+
// Path Utilities
|
|
865
|
+
// =============================================================================
|
|
866
|
+
|
|
867
|
+
let cachedHomeDir: string | undefined;
|
|
868
|
+
let cachedHomedir: typeof os.homedir | undefined;
|
|
869
|
+
|
|
870
|
+
function defaultHomeDir(): string {
|
|
871
|
+
const homedir = os.homedir;
|
|
872
|
+
if (cachedHomeDir === undefined || cachedHomedir !== homedir) {
|
|
873
|
+
cachedHomedir = homedir;
|
|
874
|
+
cachedHomeDir = homedir();
|
|
875
|
+
}
|
|
876
|
+
return cachedHomeDir;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
const homePatternCache = new Map<string, RegExp>();
|
|
880
|
+
function homePatternFor(homeDir: string, windowsStyle: boolean): RegExp {
|
|
881
|
+
const key = `${windowsStyle ? 1 : 0} ${homeDir}`;
|
|
882
|
+
let pattern = homePatternCache.get(key);
|
|
883
|
+
if (pattern === undefined) {
|
|
884
|
+
const escapedHome = windowsStyle
|
|
885
|
+
? homeDir
|
|
886
|
+
.replaceAll("/", "\\")
|
|
887
|
+
.split("\\")
|
|
888
|
+
.map(part => RegExp.escape(part))
|
|
889
|
+
.join("[\\\\/]")
|
|
890
|
+
: RegExp.escape(homeDir);
|
|
891
|
+
pattern = new RegExp(
|
|
892
|
+
`[a-zA-Z][a-zA-Z0-9+.-]*://[^\\s"'<>]+|(^|[\\s"'\\x60([{=,:])(${escapedHome})(?=$|[\\\\/\\s"'\\x60)\\]},;:])`,
|
|
893
|
+
windowsStyle ? "gi" : "g",
|
|
894
|
+
);
|
|
895
|
+
if (homePatternCache.size >= 16) homePatternCache.clear();
|
|
896
|
+
homePatternCache.set(key, pattern);
|
|
897
|
+
}
|
|
898
|
+
return pattern;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
/** Replace a leading home directory with a portable tilde prefix. */
|
|
902
|
+
export function shortenPath(filePath: unknown, homeDir?: string): string {
|
|
903
|
+
if (typeof filePath !== "string") {
|
|
904
|
+
return "";
|
|
905
|
+
}
|
|
906
|
+
const home = homeDir ?? defaultHomeDir();
|
|
907
|
+
const windowsStyle = /^[A-Za-z]:[\\/]/.test(home) || home.startsWith("\\\\");
|
|
908
|
+
const hasHomePrefix = windowsStyle
|
|
909
|
+
? filePath.toLowerCase().startsWith(home.toLowerCase())
|
|
910
|
+
: filePath.startsWith(home);
|
|
911
|
+
if (home && hasHomePrefix) {
|
|
912
|
+
const suffix = filePath.slice(home.length);
|
|
913
|
+
if (suffix === "" || suffix.startsWith(path.posix.sep) || suffix.startsWith(path.win32.sep)) {
|
|
914
|
+
return `~${suffix.replaceAll(path.win32.sep, path.posix.sep)}`;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
return filePath;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/** Shorten embedded home paths; normalize Windows separators unless the caller preserves native error text. */
|
|
921
|
+
export function shortenEmbeddedPaths(text: string, homeDir?: string, preserveSeparators = false): string {
|
|
922
|
+
const resolvedHome = homeDir ?? defaultHomeDir();
|
|
923
|
+
if (!resolvedHome || resolvedHome.length <= 1) return text;
|
|
924
|
+
const windowsStyle = /^[A-Za-z]:[\\/]/.test(resolvedHome) || resolvedHome.startsWith("\\\\");
|
|
925
|
+
const homePattern = homePatternFor(resolvedHome, windowsStyle);
|
|
926
|
+
const textWithShortenedHome = text.replace(
|
|
927
|
+
homePattern,
|
|
928
|
+
(match, boundary: string | undefined, candidate: string | undefined) =>
|
|
929
|
+
candidate === undefined ? match : `${boundary}~`,
|
|
930
|
+
);
|
|
931
|
+
if (preserveSeparators) return textWithShortenedHome;
|
|
932
|
+
return textWithShortenedHome
|
|
933
|
+
.split(" ")
|
|
934
|
+
.map(segment => {
|
|
935
|
+
const leading = segment.match(/^[("'`[]*/)?.[0] ?? "";
|
|
936
|
+
const trailing = segment.match(/[)"'`,.;:\]]*$/)?.[0] ?? "";
|
|
937
|
+
const end = segment.length - trailing.length;
|
|
938
|
+
if (leading.length >= end) return segment;
|
|
939
|
+
const shortened = shortenPath(segment.slice(leading.length, end), resolvedHome);
|
|
940
|
+
const normalized = shortened.startsWith("~")
|
|
941
|
+
? shortened.replaceAll(path.win32.sep, path.posix.sep)
|
|
942
|
+
: shortened;
|
|
943
|
+
return `${leading}${normalized}${trailing}`;
|
|
944
|
+
})
|
|
945
|
+
.join(" ");
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
/** Sanitize warning text before showing it in TUI, including embedded home paths. */
|
|
949
|
+
export function sanitizeDisplayWarning(text: string): string {
|
|
950
|
+
return shortenEmbeddedPaths(
|
|
951
|
+
replaceTabs(sanitizeText(text))
|
|
952
|
+
.replace(/[\r\n]+/g, " ")
|
|
953
|
+
.trim(),
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
/** Sanitize and bound warning text before showing it in TUI. */
|
|
958
|
+
export function sanitizeDisplayWarnings(warnings: readonly string[]): string[] {
|
|
959
|
+
const visible = warnings
|
|
960
|
+
.slice(0, PREVIEW_LIMITS.COLLAPSED_ITEMS)
|
|
961
|
+
.map(warning => truncateToWidth(sanitizeDisplayWarning(warning), TRUNCATE_LENGTHS.LONG));
|
|
962
|
+
const hidden = warnings.length - visible.length;
|
|
963
|
+
if (hidden > 0) visible.push(`… ${hidden} more ${pluralize("warning", hidden)}`);
|
|
964
|
+
return visible;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
/** Format a tool working directory when it differs from the project directory. */
|
|
968
|
+
export function formatToolWorkingDirectory(workdir: string | undefined, projectDir: string): string | undefined {
|
|
969
|
+
if (!workdir) return undefined;
|
|
970
|
+
const resolvedProjectDir = path.resolve(projectDir);
|
|
971
|
+
const resolvedWorkdir = path.resolve(projectDir, workdir);
|
|
972
|
+
if (resolvedWorkdir === resolvedProjectDir) {
|
|
973
|
+
return undefined;
|
|
974
|
+
}
|
|
975
|
+
const relativePath = path.relative(resolvedProjectDir, resolvedWorkdir);
|
|
976
|
+
const isWithinProject =
|
|
977
|
+
relativePath.length > 0 && !relativePath.startsWith("..") && !relativePath.startsWith(`..${path.sep}`);
|
|
978
|
+
const displayWorkdir = isWithinProject ? relativePath : shortenPath(resolvedWorkdir);
|
|
979
|
+
return replaceTabs(displayWorkdir);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
/** Wrap text in the theme-specific bracket pair. */
|
|
983
|
+
export function wrapBrackets(text: string, theme: Theme): string {
|
|
984
|
+
return `${theme.format.bracketLeft}${text}${theme.format.bracketRight}`;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
/** Maximum number of parse errors displayed in expanded details. */
|
|
988
|
+
export const PARSE_ERRORS_LIMIT = 20;
|
|
989
|
+
|
|
990
|
+
/** Deduplicate parse errors while preserving their original order. */
|
|
991
|
+
export function dedupeParseErrors(errors: string[] | undefined): string[] {
|
|
992
|
+
if (!errors || errors.length === 0) return [];
|
|
993
|
+
const seen = new Set<string>();
|
|
994
|
+
const deduped: string[] = [];
|
|
995
|
+
for (const error of errors) {
|
|
996
|
+
if (seen.has(error)) continue;
|
|
997
|
+
seen.add(error);
|
|
998
|
+
deduped.push(error);
|
|
999
|
+
}
|
|
1000
|
+
return deduped;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/** Format deduplicated parse errors with a bounded detail list. */
|
|
1004
|
+
export function formatParseErrors(errors: string[], total?: number): string[] {
|
|
1005
|
+
const deduped = dedupeParseErrors(errors);
|
|
1006
|
+
if (deduped.length === 0) return [];
|
|
1007
|
+
const fullCount = total ?? deduped.length;
|
|
1008
|
+
const capped = deduped.slice(0, PARSE_ERRORS_LIMIT);
|
|
1009
|
+
const header = fullCount > capped.length ? `Parse issues (${capped.length} / ${fullCount}):` : "Parse issues:";
|
|
1010
|
+
return [header, ...capped.map(err => `- ${err}`)];
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
/**
|
|
1014
|
+
* Cap an upstream parse-error list to {@link PARSE_ERRORS_LIMIT} unique entries,
|
|
1015
|
+
* preserving the original deduplicated total. Use this at the source so tool
|
|
1016
|
+
* details never carry thousands of per-file parse errors into traces or
|
|
1017
|
+
* renderers.
|
|
1018
|
+
*/
|
|
1019
|
+
export function capParseErrors(
|
|
1020
|
+
errors: string[] | undefined,
|
|
1021
|
+
limit: number = PARSE_ERRORS_LIMIT,
|
|
1022
|
+
): { errors: string[]; total: number } {
|
|
1023
|
+
const deduped = dedupeParseErrors(errors);
|
|
1024
|
+
return { errors: deduped.slice(0, limit), total: deduped.length };
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// =============================================================================
|
|
1028
|
+
// Renderer helpers shared by search / find / ast tools
|
|
1029
|
+
// =============================================================================
|
|
1030
|
+
|
|
1031
|
+
/**
|
|
1032
|
+
* Standard width+expand keyed render cache used by every search-style tool
|
|
1033
|
+
* renderer. `compute` re-runs only when the cache key changes; the returned
|
|
1034
|
+
* Component is the canonical `{ render, invalidate }` pair.
|
|
1035
|
+
*/
|
|
1036
|
+
export function createCachedComponent(
|
|
1037
|
+
getExpanded: () => boolean,
|
|
1038
|
+
compute: (width: number, expanded: boolean) => string[],
|
|
1039
|
+
options: { paddingX?: number } = {},
|
|
1040
|
+
): Component {
|
|
1041
|
+
let cached: { key: bigint; lines: string[] } | undefined;
|
|
1042
|
+
return {
|
|
1043
|
+
render(width: number): readonly string[] {
|
|
1044
|
+
const expanded = getExpanded();
|
|
1045
|
+
const key = new Hasher().bool(expanded).u32(width).digest();
|
|
1046
|
+
if (cached?.key === key) return cached.lines;
|
|
1047
|
+
const paddingX = Math.max(0, options.paddingX ?? 0);
|
|
1048
|
+
const innerWidth = Math.max(1, width - paddingX * 2);
|
|
1049
|
+
const lines = compute(innerWidth, expanded);
|
|
1050
|
+
const pad = paddingX === 0 ? "" : " ".repeat(paddingX);
|
|
1051
|
+
const paddedLines = paddingX === 0 ? lines : lines.map(line => `${pad}${line}${pad}`);
|
|
1052
|
+
cached = { key, lines: paddedLines };
|
|
1053
|
+
return paddedLines;
|
|
1054
|
+
},
|
|
1055
|
+
invalidate() {
|
|
1056
|
+
cached = undefined;
|
|
1057
|
+
},
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
/**
|
|
1062
|
+
* Single-slot memo for an expensive rendered string (syntax highlighting, diff
|
|
1063
|
+
* coloring) keyed by the exact inputs that shape the bytes: theme instance,
|
|
1064
|
+
* expanded state, a caller-chosen salt (path/language), and the source content.
|
|
1065
|
+
* Field-wise comparison instead of a concatenated key string: a cache hit costs
|
|
1066
|
+
* one string value-compare (engines short-circuit on length) and a miss never
|
|
1067
|
+
* allocates a key. Comparing the {@link Theme} by reference is sound because
|
|
1068
|
+
* theme switches replace the instance wholesale (`setTheme`/`previewTheme`/
|
|
1069
|
+
* `setSymbolPreset` in pi-tui theme/theme.ts) — themes are never mutated in
|
|
1070
|
+
* place.
|
|
1071
|
+
*/
|
|
1072
|
+
export interface RenderedStringCache {
|
|
1073
|
+
theme: Theme | null;
|
|
1074
|
+
expanded: boolean;
|
|
1075
|
+
salt: string;
|
|
1076
|
+
content: string;
|
|
1077
|
+
value: string;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
/** Create an empty memo for rendered tool strings. */
|
|
1081
|
+
export function createRenderedStringCache(): RenderedStringCache {
|
|
1082
|
+
return { theme: null, expanded: false, salt: "", content: "", value: "" };
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
/** Drop the memo so the next lookup re-renders (e.g. the render function identity changed). */
|
|
1086
|
+
export function invalidateRenderedStringCache(cache: RenderedStringCache): void {
|
|
1087
|
+
cache.theme = null;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
/** Reuse a rendered string while its theme, expansion, content, and salt match. */
|
|
1091
|
+
export function cachedRenderedString(
|
|
1092
|
+
cache: RenderedStringCache | undefined,
|
|
1093
|
+
theme: Theme,
|
|
1094
|
+
expanded: boolean,
|
|
1095
|
+
salt: string,
|
|
1096
|
+
content: string,
|
|
1097
|
+
render: () => string,
|
|
1098
|
+
): string {
|
|
1099
|
+
if (
|
|
1100
|
+
cache !== undefined &&
|
|
1101
|
+
cache.theme === theme &&
|
|
1102
|
+
cache.expanded === expanded &&
|
|
1103
|
+
cache.salt === salt &&
|
|
1104
|
+
cache.content === content
|
|
1105
|
+
) {
|
|
1106
|
+
return cache.value;
|
|
1107
|
+
}
|
|
1108
|
+
const value = render();
|
|
1109
|
+
if (cache !== undefined) {
|
|
1110
|
+
cache.theme = theme;
|
|
1111
|
+
cache.expanded = expanded;
|
|
1112
|
+
cache.salt = salt;
|
|
1113
|
+
cache.content = content;
|
|
1114
|
+
cache.value = value;
|
|
1115
|
+
}
|
|
1116
|
+
return value;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
/**
|
|
1120
|
+
* Append the indented bullet list of parse errors (capped at
|
|
1121
|
+
* {@link PARSE_ERRORS_LIMIT}) to `lines`, with an overflow summary line if the
|
|
1122
|
+
* total exceeds the cap. No-op when `parseErrors` is empty.
|
|
1123
|
+
*/
|
|
1124
|
+
export function appendParseErrorsBulletList(
|
|
1125
|
+
lines: string[],
|
|
1126
|
+
parseErrors: readonly string[] | undefined,
|
|
1127
|
+
theme: Theme,
|
|
1128
|
+
total?: number,
|
|
1129
|
+
): void {
|
|
1130
|
+
if (!parseErrors || parseErrors.length === 0) return;
|
|
1131
|
+
const fullCount = total ?? parseErrors.length;
|
|
1132
|
+
const capped = parseErrors.slice(0, PARSE_ERRORS_LIMIT);
|
|
1133
|
+
for (const err of capped) {
|
|
1134
|
+
lines.push(theme.fg("warning", ` - ${err}`));
|
|
1135
|
+
}
|
|
1136
|
+
if (fullCount > capped.length) {
|
|
1137
|
+
lines.push(theme.fg("dim", ` … ${fullCount - capped.length} more`));
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
/**
|
|
1142
|
+
* Human-readable summary string for the parse-issues count, capped by
|
|
1143
|
+
* {@link PARSE_ERRORS_LIMIT}.
|
|
1144
|
+
*/
|
|
1145
|
+
export function formatParseErrorsCountLabel(parseErrors: readonly string[], total?: number): string {
|
|
1146
|
+
const fullCount = total ?? parseErrors.length;
|
|
1147
|
+
return fullCount > PARSE_ERRORS_LIMIT
|
|
1148
|
+
? `${PARSE_ERRORS_LIMIT} / ${fullCount} parse issues`
|
|
1149
|
+
: `${fullCount} parse issue${fullCount !== 1 ? "s" : ""}`;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
/**
|
|
1153
|
+
* Parse a JSON-encoded array of path strings (e.g. `'["a.ts","b.ts"]'`).
|
|
1154
|
+
* Returns `null` when the input is not a bracketed JSON string array, so the
|
|
1155
|
+
* caller can fall back to treating the input as a single literal path.
|
|
1156
|
+
*/
|
|
1157
|
+
function parseStringEncodedPathArray(input: string): string[] | null {
|
|
1158
|
+
const trimmed = input.trim();
|
|
1159
|
+
if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) return null;
|
|
1160
|
+
|
|
1161
|
+
let parsed: unknown;
|
|
1162
|
+
try {
|
|
1163
|
+
parsed = JSON.parse(trimmed);
|
|
1164
|
+
} catch {
|
|
1165
|
+
return null;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
if (!Array.isArray(parsed) || parsed.some(entry => typeof entry !== "string")) {
|
|
1169
|
+
return null;
|
|
1170
|
+
}
|
|
1171
|
+
return parsed;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
/**
|
|
1175
|
+
* Normalize a path argument that may arrive as a single string, a JSON-encoded
|
|
1176
|
+
* string array (`'["a.ts"]'`), or an actual array into a flat `string[]`.
|
|
1177
|
+
* Delimited single strings (`"a.ts b.ts"`) remain literal.
|
|
1178
|
+
*/
|
|
1179
|
+
export function toPathList(input: string | string[] | undefined): string[] {
|
|
1180
|
+
if (typeof input === "string") return parseStringEncodedPathArray(input) ?? [input];
|
|
1181
|
+
return input ?? [];
|
|
1182
|
+
}
|