@oh-my-pi/pi-tui 18.2.3 → 18.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/README.md +43 -0
- package/dist/types/app-keybindings.d.ts +395 -0
- package/dist/types/apps/autoresearch-dashboard.d.ts +43 -0
- package/dist/types/apps/autoresearch-data.d.ts +15 -0
- package/dist/types/apps/cleanse-board.d.ts +66 -0
- package/dist/types/apps/cleanse-picker.d.ts +23 -0
- package/dist/types/apps/debug/log-formatting.d.ts +6 -0
- package/dist/types/apps/debug/log-viewer.d.ts +81 -0
- package/dist/types/apps/debug/protocol-probe.d.ts +42 -0
- package/dist/types/apps/debug/raw-sse-buffer.d.ts +58 -0
- package/dist/types/apps/debug/raw-sse.d.ts +26 -0
- package/dist/types/apps/debug/terminal-info.d.ts +34 -0
- package/dist/types/apps/debug/viewer-frame.d.ts +52 -0
- package/dist/types/apps/git/avatar.d.ts +9 -0
- package/dist/types/apps/git/colors.d.ts +49 -0
- package/dist/types/apps/git/diff-pane.d.ts +188 -0
- package/dist/types/apps/git/git-tui.d.ts +66 -0
- package/dist/types/apps/git/sidebar.d.ts +149 -0
- package/dist/types/apps/git/state.d.ts +117 -0
- package/dist/types/apps/if-bench-board.d.ts +75 -0
- package/dist/types/apps/live-visualizer.d.ts +33 -0
- package/dist/types/apps/ps-data.d.ts +51 -0
- package/dist/types/apps/ps-top.d.ts +33 -0
- package/dist/types/apps/session-picker.d.ts +26 -0
- package/dist/types/apps/setup-model-picker.d.ts +13 -0
- package/dist/types/apps/standalone-picker.d.ts +42 -0
- package/dist/types/autocomplete.d.ts +4 -0
- package/dist/types/chat/advisor-message.d.ts +11 -0
- package/dist/types/chat/assistant-message.d.ts +106 -0
- package/dist/types/chat/background-tan-message.d.ts +9 -0
- package/dist/types/chat/bash-execution.d.ts +47 -0
- package/dist/types/chat/cache-invalidation-marker.d.ts +50 -0
- package/dist/types/chat/chat-transcript-builder.d.ts +50 -0
- package/dist/types/chat/collab-prompt-message.d.ts +10 -0
- package/dist/types/chat/compaction-summary-message.d.ts +54 -0
- package/dist/types/chat/custom-message.d.ts +10 -0
- package/dist/types/chat/display-preferences.d.ts +13 -0
- package/dist/types/chat/eval-execution.d.ts +29 -0
- package/dist/types/chat/execution-shared.d.ts +51 -0
- package/dist/types/chat/extension-types.d.ts +28 -0
- package/dist/types/chat/hook-message.d.ts +10 -0
- package/dist/types/chat/image-loading.d.ts +46 -0
- package/dist/types/chat/late-diagnostics-message.d.ts +21 -0
- package/dist/types/chat/messages.d.ts +189 -0
- package/dist/types/chat/reaction.d.ts +30 -0
- package/dist/types/chat/read-target.d.ts +4 -0
- package/dist/types/chat/read-tool-group.d.ts +67 -0
- package/dist/types/chat/served-model-marker.d.ts +47 -0
- package/dist/types/chat/skill-message.d.ts +33 -0
- package/dist/types/chat/skill-title-input.d.ts +14 -0
- package/dist/types/chat/stripped-tool-calls-placeholder.d.ts +12 -0
- package/dist/types/chat/thinking-display.d.ts +12 -0
- package/dist/types/chat/todo-reminder.d.ts +13 -0
- package/dist/types/chat/tool-execution.d.ts +138 -0
- package/dist/types/chat/transcript-browser.d.ts +93 -0
- package/dist/types/chat/transcript-entry.d.ts +45 -0
- package/dist/types/chat/transcript-outline.d.ts +86 -0
- package/dist/types/chat/transcript-render-helpers.d.ts +92 -0
- package/dist/types/chat/ttsr-notification.d.ts +23 -0
- package/dist/types/chat/user-message.d.ts +52 -0
- package/dist/types/chrome/chat-block.d.ts +64 -0
- package/dist/types/chrome/collab-qrcode.d.ts +28 -0
- package/dist/types/chrome/context-thresholds.d.ts +10 -0
- package/dist/types/chrome/countdown-timer.d.ts +14 -0
- package/dist/types/chrome/diff.d.ts +14 -0
- package/dist/types/chrome/dynamic-border.d.ts +16 -0
- package/dist/types/chrome/error-block.d.ts +17 -0
- package/dist/types/chrome/form-theme.d.ts +3 -0
- package/dist/types/chrome/format.d.ts +12 -0
- package/dist/types/chrome/index.d.ts +20 -0
- package/dist/types/chrome/keybinding-hints.d.ts +40 -0
- package/dist/types/chrome/live-board.d.ts +23 -0
- package/dist/types/chrome/local-date.d.ts +4 -0
- package/dist/types/chrome/message-divider.d.ts +22 -0
- package/dist/types/chrome/message-frame.d.ts +52 -0
- package/dist/types/chrome/message-notice.d.ts +35 -0
- package/dist/types/chrome/overlay-box.d.ts +74 -0
- package/dist/types/chrome/qrcode.d.ts +48 -0
- package/dist/types/chrome/segment-track.d.ts +27 -0
- package/dist/types/chrome/select-list-mouse-routing.d.ts +4 -0
- package/dist/types/chrome/selector-helpers.d.ts +52 -0
- package/dist/types/chrome/shared.d.ts +5 -0
- package/dist/types/chrome/streaming-panel.d.ts +19 -0
- package/dist/types/chrome/tool-activity.d.ts +17 -0
- package/dist/types/chrome/transcript-container.d.ts +129 -0
- package/dist/types/chrome/transcript-status.d.ts +12 -0
- package/dist/types/chrome/visual-truncate.d.ts +19 -0
- package/dist/types/components/disclosure.d.ts +42 -0
- package/dist/types/components/form.d.ts +130 -0
- package/dist/types/components/image.d.ts +18 -16
- package/dist/types/components/key-value-list.d.ts +24 -0
- package/dist/types/components/layout/geometry.d.ts +73 -0
- package/dist/types/components/layout/row.d.ts +55 -0
- package/dist/types/components/layout/split-pane.d.ts +80 -0
- package/dist/types/components/layout/stack.d.ts +50 -0
- package/dist/types/components/menu-selection.d.ts +71 -0
- package/dist/types/components/metric.d.ts +44 -0
- package/dist/types/components/progress-bar.d.ts +36 -0
- package/dist/types/components/scroll-view.d.ts +53 -19
- package/dist/types/components/scroll-viewport.d.ts +46 -0
- package/dist/types/components/section.d.ts +19 -0
- package/dist/types/components/select-list.d.ts +46 -2
- package/dist/types/components/settings-list.d.ts +20 -0
- package/dist/types/components/table.d.ts +32 -0
- package/dist/types/components/tree-view.d.ts +130 -0
- package/dist/types/components/wizard-step.d.ts +60 -0
- package/dist/types/hotkeys-markdown.d.ts +7 -0
- package/dist/types/index.d.ts +17 -0
- package/dist/types/keybinding-matchers.d.ts +35 -0
- package/dist/types/keys.d.ts +2 -2
- package/dist/types/lang-from-path.d.ts +9 -0
- package/dist/types/overlays/advisor-config.d.ts +114 -0
- package/dist/types/overlays/agent-activity.d.ts +41 -0
- package/dist/types/overlays/agent-hub-projection.d.ts +33 -0
- package/dist/types/overlays/agent-hub-renderer.d.ts +53 -0
- package/dist/types/overlays/agent-hub-types.d.ts +99 -0
- package/dist/types/overlays/agent-hub.d.ts +126 -0
- package/dist/types/overlays/agent-transcript-viewer.d.ts +71 -0
- package/dist/types/overlays/agents-hub.d.ts +72 -0
- package/dist/types/overlays/ask-dialog.d.ts +88 -0
- package/dist/types/overlays/bordered-loader.d.ts +11 -0
- package/dist/types/overlays/btw-history-panel.d.ts +29 -0
- package/dist/types/overlays/btw-history.d.ts +17 -0
- package/dist/types/overlays/btw-panel.d.ts +26 -0
- package/dist/types/overlays/cleanse-panel.d.ts +36 -0
- package/dist/types/overlays/codex-reset-fireworks.d.ts +52 -0
- package/dist/types/overlays/composer-shape-preview.d.ts +47 -0
- package/dist/types/overlays/composer-shape-registry.d.ts +50 -0
- package/dist/types/overlays/copy-selector.d.ts +48 -0
- package/dist/types/overlays/copy-targets.d.ts +63 -0
- package/dist/types/overlays/error-banner.d.ts +13 -0
- package/dist/types/overlays/extensions/display-text.d.ts +10 -0
- package/dist/types/overlays/extensions/extension-dashboard.d.ts +63 -0
- package/dist/types/overlays/extensions/extension-list.d.ts +54 -0
- package/dist/types/overlays/extensions/inspector-model.d.ts +109 -0
- package/dist/types/overlays/extensions/inspector-panel.d.ts +17 -0
- package/dist/types/overlays/extensions/live-tool-session.d.ts +28 -0
- package/dist/types/overlays/extensions/mcp-runtime.d.ts +107 -0
- package/dist/types/overlays/extensions/state-manager.d.ts +42 -0
- package/dist/types/overlays/extensions/types.d.ts +165 -0
- package/dist/types/overlays/history-search.d.ts +16 -0
- package/dist/types/overlays/hook-editor.d.ts +44 -0
- package/dist/types/overlays/hook-input.d.ts +20 -0
- package/dist/types/overlays/hook-selector.d.ts +71 -0
- package/dist/types/overlays/hub-frame.d.ts +77 -0
- package/dist/types/overlays/login-dialog.d.ts +43 -0
- package/dist/types/overlays/logout-account-selector.d.ts +15 -0
- package/dist/types/overlays/mcp-add-wizard.d.ts +87 -0
- package/dist/types/overlays/model-browser.d.ts +213 -0
- package/dist/types/overlays/model-hub.d.ts +72 -0
- package/dist/types/overlays/model-picker.d.ts +74 -0
- package/dist/types/overlays/model-selector.d.ts +52 -0
- package/dist/types/overlays/move-overlay.d.ts +39 -0
- package/dist/types/overlays/oauth-selector.d.ts +41 -0
- package/dist/types/overlays/omfg-panel.d.ts +20 -0
- package/dist/types/overlays/pause-screen.d.ts +43 -0
- package/dist/types/overlays/plan-review-overlay.d.ts +89 -0
- package/dist/types/overlays/plan-save-overlay.d.ts +19 -0
- package/dist/types/overlays/plan-toc.d.ts +41 -0
- package/dist/types/overlays/plugin-selector.d.ts +28 -0
- package/dist/types/overlays/plugin-settings.d.ts +180 -0
- package/dist/types/overlays/queue-mode-selector.d.ts +11 -0
- package/dist/types/overlays/reset-usage-selector.d.ts +23 -0
- package/dist/types/overlays/rewind-selector.d.ts +61 -0
- package/dist/types/overlays/running-subagent-badge.d.ts +6 -0
- package/dist/types/overlays/session-account-selector.d.ts +24 -0
- package/dist/types/overlays/session-info-overlay.d.ts +17 -0
- package/dist/types/overlays/session-observer-registry.d.ts +77 -0
- package/dist/types/overlays/session-selector.d.ts +131 -0
- package/dist/types/overlays/settings-defs.d.ts +119 -0
- package/dist/types/overlays/settings-selector.d.ts +73 -0
- package/dist/types/overlays/show-images-selector.d.ts +11 -0
- package/dist/types/overlays/snapcompact-shape-preview.d.ts +31 -0
- package/dist/types/overlays/theme-selector.d.ts +12 -0
- package/dist/types/overlays/thinking-selector.d.ts +12 -0
- package/dist/types/overlays/tiny-title-download-progress.d.ts +21 -0
- package/dist/types/overlays/tree-selector.d.ts +92 -0
- package/dist/types/overlays/usage-dashboard.d.ts +94 -0
- package/dist/types/overlays/usage-display.d.ts +5 -0
- package/dist/types/overlays/usage-row.d.ts +17 -0
- package/dist/types/prompt/attachment-chips.d.ts +19 -0
- package/dist/types/prompt/composer-attachments.d.ts +71 -0
- package/dist/types/prompt/composer-cache.d.ts +37 -0
- package/dist/types/prompt/composer.d.ts +159 -0
- package/dist/types/prompt/custom-editor.d.ts +261 -0
- package/dist/types/prompt/editor-top-gap.d.ts +18 -0
- package/dist/types/prompt/emoji-autocomplete.d.ts +16 -0
- package/dist/types/prompt/github-ref-autocomplete.d.ts +35 -0
- package/dist/types/prompt/gradient-highlight.d.ts +32 -0
- package/dist/types/prompt/image-format.d.ts +4 -0
- package/dist/types/prompt/image-references.d.ts +26 -0
- package/dist/types/prompt/interactive-context-helpers.d.ts +43 -0
- package/dist/types/prompt/internal-url-autocomplete.d.ts +56 -0
- package/dist/types/prompt/macos-spelling.d.ts +44 -0
- package/dist/types/prompt/magic-keyword-boundary.d.ts +9 -0
- package/dist/types/prompt/magic-keywords.d.ts +26 -0
- package/dist/types/prompt/markdown-prose.d.ts +15 -0
- package/dist/types/prompt/model-mention-autocomplete.d.ts +25 -0
- package/dist/types/prompt/model-mention-syntax.d.ts +24 -0
- package/dist/types/prompt/orchestrate.d.ts +13 -0
- package/dist/types/prompt/prompt-action-autocomplete.d.ts +56 -0
- package/dist/types/prompt/queue-input.d.ts +8 -0
- package/dist/types/prompt/skill-tokens.d.ts +13 -0
- package/dist/types/prompt/tools-markdown.d.ts +12 -0
- package/dist/types/prompt/ultrathink.d.ts +13 -0
- package/dist/types/prompt/usage-amounts.d.ts +26 -0
- package/dist/types/prompt/video.d.ts +18 -0
- package/dist/types/prompt/welcome.d.ts +83 -0
- package/dist/types/prompt/workflow.d.ts +13 -0
- package/dist/types/render/code-cell.d.ts +51 -0
- package/dist/types/render/file-list.d.ts +22 -0
- package/dist/types/render/hyperlink.d.ts +80 -0
- package/dist/types/render/index.d.ts +16 -0
- package/dist/types/render/output-block.d.ts +54 -0
- package/dist/types/render/output-pane.d.ts +80 -0
- package/dist/types/render/render-utils.d.ts +312 -0
- package/dist/types/render/sixel.d.ts +21 -0
- package/dist/types/render/status-line.d.ts +23 -0
- package/dist/types/render/tool-card.d.ts +71 -0
- package/dist/types/render/tree-list.d.ts +29 -0
- package/dist/types/render/types.d.ts +17 -0
- package/dist/types/render/utils.d.ts +43 -0
- package/dist/types/render/width-aware-text.d.ts +23 -0
- package/dist/types/setup/lazy.d.ts +3 -0
- package/dist/types/setup/scenes/composer.d.ts +3 -0
- package/dist/types/setup/scenes/glyph.d.ts +3 -0
- package/dist/types/setup/scenes/model.d.ts +3 -0
- package/dist/types/setup/scenes/outro.d.ts +2 -0
- package/dist/types/setup/scenes/providers.d.ts +3 -0
- package/dist/types/setup/scenes/sign-in.d.ts +21 -0
- package/dist/types/setup/scenes/splash.d.ts +11 -0
- package/dist/types/setup/scenes/theme.d.ts +3 -0
- package/dist/types/setup/scenes/types.d.ts +110 -0
- package/dist/types/setup/scenes/web-search.d.ts +22 -0
- package/dist/types/setup/setup-version.d.ts +11 -0
- package/dist/types/setup/startup-splash.d.ts +9 -0
- package/dist/types/setup/wizard-overlay.d.ts +16 -0
- package/dist/types/setup/wizard.d.ts +25 -0
- package/dist/types/status-line/component.d.ts +182 -0
- package/dist/types/status-line/context-usage.d.ts +156 -0
- package/dist/types/status-line/footer.d.ts +30 -0
- package/dist/types/status-line/git-utils.d.ts +22 -0
- package/dist/types/status-line/host.d.ts +129 -0
- package/dist/types/status-line/index.d.ts +6 -0
- package/dist/types/status-line/loop.d.ts +18 -0
- package/dist/types/status-line/metrics.d.ts +21 -0
- package/dist/types/status-line/presets.d.ts +3 -0
- package/dist/types/status-line/schema.d.ts +15 -0
- package/dist/types/status-line/segments.d.ts +5 -0
- package/dist/types/status-line/separators.d.ts +3 -0
- package/dist/types/status-line/types.d.ts +236 -0
- package/dist/types/stdin-buffer.d.ts +4 -0
- package/dist/types/theme/color.d.ts +24 -0
- package/dist/types/theme/defaults/index.d.ts +9489 -0
- package/dist/types/theme/index.d.ts +4 -0
- package/dist/types/theme/loader.d.ts +23 -0
- package/dist/types/theme/mermaid-cache.d.ts +26 -0
- package/dist/types/theme/schema-validation.d.ts +3 -0
- package/dist/types/theme/schema.d.ts +25 -0
- package/dist/types/theme/session-color.d.ts +43 -0
- package/dist/types/theme/shimmer.d.ts +44 -0
- package/dist/types/theme/symbols.d.ts +34 -0
- package/dist/types/theme/theme-class.d.ts +299 -0
- package/dist/types/theme/theme.d.ts +111 -0
- package/dist/types/theme/tui-adapters.d.ts +30 -0
- package/dist/types/thinking.d.ts +165 -0
- package/dist/types/tools/agent-tree.d.ts +36 -0
- package/dist/types/tools/ask.d.ts +67 -0
- package/dist/types/tools/ast-edit.d.ts +53 -0
- package/dist/types/tools/ast-grep.d.ts +52 -0
- package/dist/types/tools/autoresearch.d.ts +156 -0
- package/dist/types/tools/bash-interactive.d.ts +28 -0
- package/dist/types/tools/bash.d.ts +103 -0
- package/dist/types/tools/conflict-detect.d.ts +43 -0
- package/dist/types/tools/debug.d.ts +60 -0
- package/dist/types/tools/default-renderer.d.ts +24 -0
- package/dist/types/tools/edit.d.ts +158 -0
- package/dist/types/tools/eval-format/code-mode-declarations.d.ts +5 -0
- package/dist/types/tools/eval-format/index.d.ts +5 -0
- package/dist/types/tools/eval-format/javascript.d.ts +2 -0
- package/dist/types/tools/eval-format/python.d.ts +2 -0
- package/dist/types/tools/eval.d.ts +94 -0
- package/dist/types/tools/fetch.d.ts +42 -0
- package/dist/types/tools/gh-format.d.ts +8 -0
- package/dist/types/tools/github.d.ts +144 -0
- package/dist/types/tools/glob.d.ts +44 -0
- package/dist/types/tools/goal.d.ts +42 -0
- package/dist/types/tools/grep.d.ts +64 -0
- package/dist/types/tools/grouped-file-output.d.ts +71 -0
- package/dist/types/tools/hashline-format.d.ts +28 -0
- package/dist/types/tools/hub.d.ts +340 -0
- package/dist/types/tools/index.d.ts +4 -0
- package/dist/types/tools/json-tree.d.ts +59 -0
- package/dist/types/tools/line-ranges.d.ts +29 -0
- package/dist/types/tools/list-limit.d.ts +15 -0
- package/dist/types/tools/lsp.d.ts +72 -0
- package/dist/types/tools/match-line-format.d.ts +11 -0
- package/dist/types/tools/mcp.d.ts +81 -0
- package/dist/types/tools/memory.d.ts +67 -0
- package/dist/types/tools/output-meta.d.ts +151 -0
- package/dist/types/tools/read.d.ts +139 -0
- package/dist/types/tools/renderer.d.ts +98 -0
- package/dist/types/tools/report-tool-issue.d.ts +8 -0
- package/dist/types/tools/resolve.d.ts +47 -0
- package/dist/types/tools/streaming-output.d.ts +282 -0
- package/dist/types/tools/subprocess.d.ts +24 -0
- package/dist/types/tools/task-repair-args.d.ts +53 -0
- package/dist/types/tools/task-yield-assembly.d.ts +20 -0
- package/dist/types/tools/task.d.ts +425 -0
- package/dist/types/tools/terminal-output.d.ts +8 -0
- package/dist/types/tools/think.d.ts +13 -0
- package/dist/types/tools/todo.d.ts +118 -0
- package/dist/types/tools/tool-errors.d.ts +7 -0
- package/dist/types/tools/vibe.d.ts +120 -0
- package/dist/types/tools/web-search.d.ts +195 -0
- package/dist/types/tools/write.d.ts +76 -0
- package/dist/types/tools/xd-url.d.ts +13 -0
- package/dist/types/tools/xdev.d.ts +52 -0
- package/dist/types/tui.d.ts +18 -0
- package/dist/types/utils.d.ts +2 -0
- package/package.json +37 -3
- package/src/app-keybindings.ts +760 -0
- package/src/apps/autoresearch-dashboard.ts +500 -0
- package/src/apps/autoresearch-data.ts +64 -0
- package/src/apps/cleanse-board.ts +341 -0
- package/src/apps/cleanse-picker.ts +57 -0
- package/src/apps/debug/log-formatting.ts +53 -0
- package/src/apps/debug/log-viewer.ts +868 -0
- package/src/apps/debug/protocol-probe.ts +267 -0
- package/src/apps/debug/raw-sse-buffer.ts +449 -0
- package/src/apps/debug/raw-sse.ts +282 -0
- package/src/apps/debug/terminal-info.ts +115 -0
- package/src/apps/debug/viewer-frame.ts +174 -0
- package/src/apps/git/avatar.ts +52 -0
- package/src/apps/git/colors.ts +109 -0
- package/src/apps/git/diff-pane.ts +1512 -0
- package/src/apps/git/git-tui.ts +889 -0
- package/src/apps/git/sidebar.ts +1204 -0
- package/src/apps/git/state.ts +113 -0
- package/src/apps/if-bench-board.ts +324 -0
- package/src/apps/live-visualizer.ts +238 -0
- package/src/apps/ps-data.ts +116 -0
- package/src/apps/ps-top.ts +422 -0
- package/src/apps/session-picker.ts +95 -0
- package/src/apps/setup-model-picker.ts +21 -0
- package/src/apps/standalone-picker.ts +111 -0
- package/src/autocomplete.ts +9 -13
- package/src/chat/advisor-message.ts +175 -0
- package/src/chat/assistant-message.ts +1214 -0
- package/src/chat/background-tan-message.ts +35 -0
- package/src/chat/bash-execution.ts +394 -0
- package/src/chat/cache-invalidation-marker.ts +94 -0
- package/src/chat/chat-transcript-builder.ts +593 -0
- package/src/chat/collab-prompt-message.ts +34 -0
- package/src/chat/compaction-summary-message.ts +261 -0
- package/src/chat/custom-message.ts +23 -0
- package/src/chat/display-preferences.ts +24 -0
- package/src/chat/eval-execution.ts +172 -0
- package/src/chat/execution-shared.ts +113 -0
- package/src/chat/extension-types.ts +46 -0
- package/src/chat/hook-message.ts +20 -0
- package/src/chat/image-loading.ts +122 -0
- package/src/chat/late-diagnostics-message.ts +92 -0
- package/src/chat/messages.ts +305 -0
- package/src/chat/reaction.ts +65 -0
- package/src/chat/read-target.ts +7 -0
- package/src/chat/read-tool-group.ts +873 -0
- package/src/chat/served-model-marker.ts +91 -0
- package/src/chat/skill-message.ts +220 -0
- package/src/chat/skill-title-input.ts +41 -0
- package/src/chat/stripped-tool-calls-placeholder.ts +35 -0
- package/src/chat/thinking-display.ts +312 -0
- package/src/chat/todo-reminder.ts +33 -0
- package/src/chat/tool-execution.ts +1395 -0
- package/src/chat/transcript-browser.ts +268 -0
- package/src/chat/transcript-entry.ts +96 -0
- package/src/chat/transcript-outline.ts +260 -0
- package/src/chat/transcript-render-helpers.ts +315 -0
- package/src/chat/ttsr-notification.ts +123 -0
- package/src/chat/user-message.ts +287 -0
- package/src/chrome/chat-block.ts +110 -0
- package/src/chrome/collab-qrcode.ts +74 -0
- package/src/chrome/context-thresholds.ts +85 -0
- package/src/chrome/countdown-timer.ts +74 -0
- package/src/chrome/diff.ts +266 -0
- package/src/chrome/dynamic-border.ts +36 -0
- package/src/chrome/error-block.ts +63 -0
- package/src/chrome/form-theme.ts +10 -0
- package/src/chrome/format.ts +66 -0
- package/src/chrome/index.ts +21 -0
- package/src/chrome/keybinding-hints.ts +55 -0
- package/src/chrome/live-board.ts +123 -0
- package/src/chrome/local-date.ts +20 -0
- package/src/chrome/message-divider.ts +51 -0
- package/src/chrome/message-frame.ts +164 -0
- package/src/chrome/message-notice.ts +103 -0
- package/src/chrome/overlay-box.ts +301 -0
- package/src/chrome/qrcode.ts +539 -0
- package/src/chrome/segment-track.ts +86 -0
- package/src/chrome/select-list-mouse-routing.ts +11 -0
- package/src/chrome/selector-helpers.ts +119 -0
- package/src/chrome/shared.ts +27 -0
- package/src/chrome/streaming-panel.ts +72 -0
- package/src/chrome/tool-activity.ts +44 -0
- package/src/chrome/transcript-container.ts +902 -0
- package/src/chrome/transcript-status.ts +27 -0
- package/src/chrome/visual-truncate.ts +70 -0
- package/src/components/disclosure.ts +235 -0
- package/src/components/editor.ts +10 -12
- package/src/components/form.ts +566 -0
- package/src/components/image.ts +80 -53
- package/src/components/input.ts +6 -27
- package/src/components/key-value-list.ts +88 -0
- package/src/components/layout/geometry.ts +202 -0
- package/src/components/layout/row.ts +283 -0
- package/src/components/layout/split-pane.ts +349 -0
- package/src/components/layout/stack.ts +261 -0
- package/src/components/markdown.ts +16 -12
- package/src/components/menu-selection.ts +248 -0
- package/src/components/metric.ts +132 -0
- package/src/components/progress-bar.ts +140 -0
- package/src/components/scroll-view.ts +372 -70
- package/src/components/scroll-viewport.ts +154 -0
- package/src/components/section.ts +77 -0
- package/src/components/select-list.ts +238 -159
- package/src/components/settings-list.ts +103 -80
- package/src/components/table.ts +127 -0
- package/src/components/tree-view.ts +481 -0
- package/src/components/wizard-step.ts +229 -0
- package/src/deccara.ts +1 -3
- package/src/hotkeys-markdown.ts +100 -0
- package/src/index.ts +18 -0
- package/src/keybinding-matchers.ts +99 -0
- package/src/keys.ts +2 -2
- package/src/lang-from-path.ts +252 -0
- package/src/overlays/advisor-config.ts +761 -0
- package/src/overlays/agent-activity.ts +113 -0
- package/src/overlays/agent-hub-projection.ts +250 -0
- package/src/overlays/agent-hub-renderer.ts +281 -0
- package/src/overlays/agent-hub-types.ts +74 -0
- package/src/overlays/agent-hub.ts +1562 -0
- package/src/overlays/agent-transcript-viewer.ts +669 -0
- package/src/overlays/agents-hub.ts +1203 -0
- package/src/overlays/ask-dialog.ts +1202 -0
- package/src/overlays/bordered-loader.ts +41 -0
- package/src/overlays/btw-history-panel.ts +591 -0
- package/src/overlays/btw-history.ts +33 -0
- package/src/overlays/btw-panel.ts +167 -0
- package/src/overlays/cleanse-panel.ts +167 -0
- package/src/overlays/codex-reset-fireworks.ts +375 -0
- package/src/overlays/composer-shape-preview.ts +137 -0
- package/src/overlays/composer-shape-registry.ts +82 -0
- package/src/overlays/copy-selector.ts +618 -0
- package/src/overlays/copy-targets.ts +210 -0
- package/src/overlays/error-banner.ts +39 -0
- package/src/overlays/extensions/display-text.ts +39 -0
- package/src/overlays/extensions/extension-dashboard.ts +645 -0
- package/src/overlays/extensions/extension-list.ts +667 -0
- package/src/overlays/extensions/inspector-model.ts +484 -0
- package/src/overlays/extensions/inspector-panel.ts +669 -0
- package/src/overlays/extensions/live-tool-session.ts +67 -0
- package/src/overlays/extensions/mcp-runtime.ts +289 -0
- package/src/overlays/extensions/state-manager.ts +337 -0
- package/src/overlays/extensions/types.ts +208 -0
- package/src/overlays/history-search.ts +268 -0
- package/src/overlays/hook-editor.ts +272 -0
- package/src/overlays/hook-input.ts +84 -0
- package/src/overlays/hook-selector.ts +635 -0
- package/src/overlays/hub-frame.ts +262 -0
- package/src/overlays/login-dialog.ts +231 -0
- package/src/overlays/logout-account-selector.ts +115 -0
- package/src/overlays/mcp-add-wizard.ts +1436 -0
- package/src/overlays/model-browser.ts +1176 -0
- package/src/overlays/model-hub.ts +2279 -0
- package/src/overlays/model-picker.ts +285 -0
- package/src/overlays/model-selector.ts +107 -0
- package/src/overlays/move-overlay.ts +197 -0
- package/src/overlays/oauth-selector.ts +446 -0
- package/src/overlays/omfg-panel.ts +136 -0
- package/src/overlays/pause-screen.ts +198 -0
- package/src/overlays/plan-review-overlay.ts +1227 -0
- package/src/overlays/plan-save-overlay.ts +74 -0
- package/src/overlays/plan-toc.ts +139 -0
- package/src/overlays/plugin-selector.ts +97 -0
- package/src/overlays/plugin-settings.ts +839 -0
- package/src/overlays/queue-mode-selector.ts +55 -0
- package/src/overlays/reset-usage-selector.ts +170 -0
- package/src/overlays/rewind-selector.ts +499 -0
- package/src/overlays/running-subagent-badge.ts +19 -0
- package/src/overlays/session-account-selector.ts +68 -0
- package/src/overlays/session-info-overlay.ts +96 -0
- package/src/overlays/session-observer-registry.ts +289 -0
- package/src/overlays/session-selector.ts +1137 -0
- package/src/overlays/settings-defs.ts +315 -0
- package/src/overlays/settings-selector.ts +1415 -0
- package/src/overlays/show-images-selector.ts +44 -0
- package/src/overlays/snapcompact-shape-preview-doc.md +14 -0
- package/src/overlays/snapcompact-shape-preview.ts +192 -0
- package/src/overlays/theme-selector.ts +62 -0
- package/src/overlays/thinking-selector.ts +51 -0
- package/src/overlays/tiny-title-download-progress.ts +102 -0
- package/src/overlays/tree-selector.ts +1121 -0
- package/src/overlays/usage-dashboard.ts +629 -0
- package/src/overlays/usage-display.ts +45 -0
- package/src/overlays/usage-row.ts +118 -0
- package/src/prompt/attachment-chips.ts +201 -0
- package/src/prompt/composer-attachments.ts +280 -0
- package/src/prompt/composer-cache.ts +304 -0
- package/src/prompt/composer.ts +934 -0
- package/src/prompt/custom-editor.ts +1392 -0
- package/src/prompt/data/emojis.json +1 -0
- package/src/prompt/editor-top-gap.ts +28 -0
- package/src/prompt/emoji-autocomplete.ts +285 -0
- package/src/prompt/github-ref-autocomplete.ts +75 -0
- package/src/prompt/gradient-highlight.ts +101 -0
- package/src/prompt/image-format.ts +28 -0
- package/src/prompt/image-references.ts +102 -0
- package/src/prompt/interactive-context-helpers.ts +126 -0
- package/src/prompt/internal-url-autocomplete.ts +155 -0
- package/src/prompt/macos-spelling.ts +456 -0
- package/src/prompt/magic-keyword-boundary.ts +23 -0
- package/src/prompt/magic-keywords.ts +42 -0
- package/src/prompt/markdown-prose.ts +245 -0
- package/src/prompt/model-mention-autocomplete.ts +94 -0
- package/src/prompt/model-mention-syntax.ts +41 -0
- package/src/prompt/orchestrate.ts +26 -0
- package/src/prompt/prompt-action-autocomplete.ts +337 -0
- package/src/prompt/queue-input.ts +132 -0
- package/src/prompt/skill-tokens.ts +36 -0
- package/src/prompt/tips.txt +27 -0
- package/src/prompt/tools-markdown.ts +29 -0
- package/src/prompt/ultrathink.ts +26 -0
- package/src/prompt/usage-amounts.ts +99 -0
- package/src/prompt/video.ts +47 -0
- package/src/prompt/welcome.ts +605 -0
- package/src/prompt/workflow.ts +26 -0
- package/src/render/code-cell.ts +244 -0
- package/src/render/file-list.ts +58 -0
- package/src/render/hyperlink.ts +192 -0
- package/src/render/index.ts +17 -0
- package/src/render/output-block.ts +267 -0
- package/src/render/output-pane.ts +299 -0
- package/src/render/render-utils.ts +1182 -0
- package/src/render/sixel.ts +69 -0
- package/src/render/status-line.ts +55 -0
- package/src/render/tool-card.ts +272 -0
- package/src/render/tree-list.ts +188 -0
- package/src/render/types.ts +19 -0
- package/src/render/utils.ts +109 -0
- package/src/render/width-aware-text.ts +60 -0
- package/src/setup/lazy.ts +17 -0
- package/src/setup/scenes/composer.ts +127 -0
- package/src/setup/scenes/glyph.ts +120 -0
- package/src/setup/scenes/model.ts +138 -0
- package/src/setup/scenes/outro.ts +24 -0
- package/src/setup/scenes/providers.ts +107 -0
- package/src/setup/scenes/sign-in.ts +373 -0
- package/src/setup/scenes/splash.ts +192 -0
- package/src/setup/scenes/theme.ts +343 -0
- package/src/setup/scenes/types.ts +111 -0
- package/src/setup/scenes/web-search.ts +165 -0
- package/src/setup/setup-version.ts +11 -0
- package/src/setup/startup-splash.ts +107 -0
- package/src/setup/wizard-overlay.ts +319 -0
- package/src/setup/wizard.ts +101 -0
- package/src/status-line/component.ts +3003 -0
- package/src/status-line/context-usage.ts +688 -0
- package/src/status-line/footer.ts +323 -0
- package/src/status-line/git-utils.ts +42 -0
- package/src/status-line/host.ts +97 -0
- package/src/status-line/index.ts +6 -0
- package/src/status-line/loop.ts +28 -0
- package/src/status-line/metrics.ts +94 -0
- package/src/status-line/presets.ts +107 -0
- package/src/status-line/schema.ts +57 -0
- package/src/status-line/segments.ts +934 -0
- package/src/status-line/separators.ts +55 -0
- package/src/status-line/types.ts +227 -0
- package/src/stdin-buffer.ts +4 -2
- package/src/theme/color.ts +95 -0
- package/src/theme/dark.json +95 -0
- package/src/theme/defaults/alabaster.json +93 -0
- package/src/theme/defaults/amethyst.json +96 -0
- package/src/theme/defaults/anthracite.json +93 -0
- package/src/theme/defaults/basalt.json +91 -0
- package/src/theme/defaults/birch.json +95 -0
- package/src/theme/defaults/dark-abyss.json +91 -0
- package/src/theme/defaults/dark-arctic.json +104 -0
- package/src/theme/defaults/dark-aurora.json +95 -0
- package/src/theme/defaults/dark-catppuccin.json +107 -0
- package/src/theme/defaults/dark-cavern.json +91 -0
- package/src/theme/defaults/dark-celestial.json +106 -0
- package/src/theme/defaults/dark-copper.json +95 -0
- package/src/theme/defaults/dark-cosmos.json +90 -0
- package/src/theme/defaults/dark-cyberpunk.json +102 -0
- package/src/theme/defaults/dark-dracula.json +98 -0
- package/src/theme/defaults/dark-eclipse.json +91 -0
- package/src/theme/defaults/dark-ember.json +95 -0
- package/src/theme/defaults/dark-equinox.json +90 -0
- package/src/theme/defaults/dark-forest.json +96 -0
- package/src/theme/defaults/dark-github.json +105 -0
- package/src/theme/defaults/dark-gruvbox.json +112 -0
- package/src/theme/defaults/dark-lavender.json +95 -0
- package/src/theme/defaults/dark-lunar.json +89 -0
- package/src/theme/defaults/dark-midnight.json +95 -0
- package/src/theme/defaults/dark-monochrome.json +94 -0
- package/src/theme/defaults/dark-monokai.json +98 -0
- package/src/theme/defaults/dark-nebula.json +90 -0
- package/src/theme/defaults/dark-nord.json +98 -0
- package/src/theme/defaults/dark-ocean.json +101 -0
- package/src/theme/defaults/dark-one.json +100 -0
- package/src/theme/defaults/dark-poimandres.json +115 -0
- package/src/theme/defaults/dark-rainforest.json +91 -0
- package/src/theme/defaults/dark-reef.json +91 -0
- package/src/theme/defaults/dark-retro.json +92 -0
- package/src/theme/defaults/dark-rose-pine.json +96 -0
- package/src/theme/defaults/dark-sakura.json +95 -0
- package/src/theme/defaults/dark-slate.json +95 -0
- package/src/theme/defaults/dark-solarized.json +97 -0
- package/src/theme/defaults/dark-solstice.json +90 -0
- package/src/theme/defaults/dark-starfall.json +91 -0
- package/src/theme/defaults/dark-sunset.json +99 -0
- package/src/theme/defaults/dark-swamp.json +90 -0
- package/src/theme/defaults/dark-synthwave.json +103 -0
- package/src/theme/defaults/dark-taiga.json +91 -0
- package/src/theme/defaults/dark-terminal.json +95 -0
- package/src/theme/defaults/dark-tokyo-night.json +101 -0
- package/src/theme/defaults/dark-tundra.json +91 -0
- package/src/theme/defaults/dark-twilight.json +91 -0
- package/src/theme/defaults/dark-volcanic.json +91 -0
- package/src/theme/defaults/graphite.json +92 -0
- package/src/theme/defaults/index.ts +201 -0
- package/src/theme/defaults/light-arctic.json +107 -0
- package/src/theme/defaults/light-aurora-day.json +91 -0
- package/src/theme/defaults/light-canyon.json +91 -0
- package/src/theme/defaults/light-catppuccin.json +106 -0
- package/src/theme/defaults/light-cirrus.json +90 -0
- package/src/theme/defaults/light-coral.json +95 -0
- package/src/theme/defaults/light-cyberpunk.json +96 -0
- package/src/theme/defaults/light-dawn.json +90 -0
- package/src/theme/defaults/light-dunes.json +91 -0
- package/src/theme/defaults/light-eucalyptus.json +95 -0
- package/src/theme/defaults/light-forest.json +100 -0
- package/src/theme/defaults/light-frost.json +95 -0
- package/src/theme/defaults/light-github.json +115 -0
- package/src/theme/defaults/light-glacier.json +91 -0
- package/src/theme/defaults/light-gruvbox.json +108 -0
- package/src/theme/defaults/light-haze.json +90 -0
- package/src/theme/defaults/light-honeycomb.json +95 -0
- package/src/theme/defaults/light-lagoon.json +91 -0
- package/src/theme/defaults/light-lavender.json +95 -0
- package/src/theme/defaults/light-meadow.json +91 -0
- package/src/theme/defaults/light-mint.json +95 -0
- package/src/theme/defaults/light-monochrome.json +101 -0
- package/src/theme/defaults/light-ocean.json +99 -0
- package/src/theme/defaults/light-one.json +99 -0
- package/src/theme/defaults/light-opal.json +91 -0
- package/src/theme/defaults/light-orchard.json +91 -0
- package/src/theme/defaults/light-paper.json +95 -0
- package/src/theme/defaults/light-poimandres.json +115 -0
- package/src/theme/defaults/light-prism.json +90 -0
- package/src/theme/defaults/light-retro.json +98 -0
- package/src/theme/defaults/light-sand.json +95 -0
- package/src/theme/defaults/light-savanna.json +91 -0
- package/src/theme/defaults/light-solarized.json +102 -0
- package/src/theme/defaults/light-soleil.json +90 -0
- package/src/theme/defaults/light-sunset.json +99 -0
- package/src/theme/defaults/light-synthwave.json +98 -0
- package/src/theme/defaults/light-tokyo-night.json +111 -0
- package/src/theme/defaults/light-wetland.json +91 -0
- package/src/theme/defaults/light-zenith.json +89 -0
- package/src/theme/defaults/limestone.json +94 -0
- package/src/theme/defaults/mahogany.json +97 -0
- package/src/theme/defaults/marble.json +93 -0
- package/src/theme/defaults/obsidian.json +91 -0
- package/src/theme/defaults/onyx.json +91 -0
- package/src/theme/defaults/pearl.json +93 -0
- package/src/theme/defaults/porcelain.json +91 -0
- package/src/theme/defaults/quartz.json +96 -0
- package/src/theme/defaults/sandstone.json +95 -0
- package/src/theme/defaults/titanium.json +90 -0
- package/src/theme/index.ts +5 -0
- package/src/theme/light.json +93 -0
- package/src/theme/loader.ts +200 -0
- package/src/theme/mermaid-cache.ts +92 -0
- package/src/theme/schema-validation.ts +119 -0
- package/src/theme/schema.ts +168 -0
- package/src/theme/session-color.ts +253 -0
- package/src/theme/shimmer.ts +307 -0
- package/src/theme/symbols.ts +1433 -0
- package/src/theme/theme-class.ts +787 -0
- package/src/theme/theme-schema.json +463 -0
- package/src/theme/theme.ts +810 -0
- package/src/theme/tui-adapters.ts +339 -0
- package/src/thinking.ts +380 -0
- package/src/tools/agent-tree.ts +128 -0
- package/src/tools/ask.ts +482 -0
- package/src/tools/ast-edit.ts +210 -0
- package/src/tools/ast-grep.ts +171 -0
- package/src/tools/autoresearch.ts +287 -0
- package/src/tools/bash-interactive.ts +295 -0
- package/src/tools/bash.ts +468 -0
- package/src/tools/conflict-detect.ts +77 -0
- package/src/tools/debug.ts +167 -0
- package/src/tools/default-renderer.ts +172 -0
- package/src/tools/edit.ts +1258 -0
- package/src/tools/eval-format/code-mode-declarations.ts +61 -0
- package/src/tools/eval-format/index.ts +16 -0
- package/src/tools/eval-format/javascript.ts +952 -0
- package/src/tools/eval-format/python.ts +544 -0
- package/src/tools/eval.ts +835 -0
- package/src/tools/fetch.ts +226 -0
- package/src/tools/gh-format.ts +18 -0
- package/src/tools/github.ts +686 -0
- package/src/tools/glob.ts +213 -0
- package/src/tools/goal.ts +153 -0
- package/src/tools/grep.ts +385 -0
- package/src/tools/grouped-file-output.ts +211 -0
- package/src/tools/hashline-format.ts +59 -0
- package/src/tools/hub.ts +1488 -0
- package/src/tools/index.ts +71 -0
- package/src/tools/json-tree.ts +388 -0
- package/src/tools/line-ranges.ts +98 -0
- package/src/tools/list-limit.ts +43 -0
- package/src/tools/lsp.ts +702 -0
- package/src/tools/match-line-format.ts +20 -0
- package/src/tools/mcp.ts +309 -0
- package/src/tools/memory.ts +220 -0
- package/src/tools/output-meta.ts +367 -0
- package/src/tools/read.ts +628 -0
- package/src/tools/renderer.ts +107 -0
- package/src/tools/report-tool-issue.ts +13 -0
- package/src/tools/resolve.ts +139 -0
- package/src/tools/streaming-output.ts +1576 -0
- package/src/tools/subprocess.ts +39 -0
- package/src/tools/task-repair-args.ts +118 -0
- package/src/tools/task-yield-assembly.ts +147 -0
- package/src/tools/task.ts +2110 -0
- package/src/tools/terminal-output.ts +153 -0
- package/src/tools/think.ts +28 -0
- package/src/tools/todo.ts +580 -0
- package/src/tools/tool-errors.ts +15 -0
- package/src/tools/vibe.ts +420 -0
- package/src/tools/web-search.ts +409 -0
- package/src/tools/write.ts +511 -0
- package/src/tools/xd-url.ts +28 -0
- package/src/tools/xdev.ts +201 -0
- package/src/tui.ts +70 -8
- package/src/utils.ts +8 -0
|
@@ -0,0 +1,1395 @@
|
|
|
1
|
+
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
2
|
+
import { Box } from "../components/box";
|
|
3
|
+
import { SPINNER_ADVANCE_MS } from "../components/loader";
|
|
4
|
+
import { Image } from "../components/image";
|
|
5
|
+
import { Spacer } from "../components/spacer";
|
|
6
|
+
import { Text } from "../components/text";
|
|
7
|
+
import { getImageDimensions, ImageProtocol, imageFallback, TERMINAL } from "../terminal-capabilities";
|
|
8
|
+
import { type Component, Container, type TUI } from "../tui";
|
|
9
|
+
import { truncateToWidth } from "../utils";
|
|
10
|
+
import { getProjectDir, isRecord, logger, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
11
|
+
import type { Theme } from "../theme/theme";
|
|
12
|
+
import { ensureThemeSync, getThemeEpoch, theme } from "../theme/theme";
|
|
13
|
+
import {
|
|
14
|
+
type FirstResultViewportRepaint,
|
|
15
|
+
type ToolActivitySummary,
|
|
16
|
+
type ToolRenderer,
|
|
17
|
+
toolRenderers,
|
|
18
|
+
} from "../tools/index";
|
|
19
|
+
import { BASH_DEFAULT_PREVIEW_LINES } from "../tools/bash";
|
|
20
|
+
import { formatDefaultToolExecution } from "../tools/default-renderer";
|
|
21
|
+
import { type EditMode, type PerFileDiffPreview, renderStreamingFallback } from "../tools/edit";
|
|
22
|
+
import { EVAL_DEFAULT_PREVIEW_LINES } from "../tools/eval";
|
|
23
|
+
import { taskCardAgentIds } from "../tools/task";
|
|
24
|
+
import { TODO_STRIKE_TOTAL_FRAMES, type TodoToolDetails } from "../tools/todo";
|
|
25
|
+
import { isWaitingPollDetails } from "../tools/hub";
|
|
26
|
+
import { formatStatusIcon, replaceTabs, resolveImageOptions } from "../render/render-utils";
|
|
27
|
+
import type { XdevMountedState } from "../tools/xdev";
|
|
28
|
+
import { isFramedBlockComponent, markFramedBlockComponent, renderStatusLine, WidthAwareText } from "../render/index";
|
|
29
|
+
import { convertImageToPng } from "./image-loading";
|
|
30
|
+
import { sanitizeWithOptionalSixelPassthrough } from "../render/sixel";
|
|
31
|
+
import { renderDiff } from "../chrome/diff";
|
|
32
|
+
import { type AnimationFrame, trimBlankEdges } from "../chrome/transcript-container";
|
|
33
|
+
|
|
34
|
+
/** Resolves the canonical renderer key while retaining the provider's wire name in message history. */
|
|
35
|
+
export function toolRenderName(wireName: string, tool: AgentTool | undefined): string {
|
|
36
|
+
return tool?.name ?? wireName;
|
|
37
|
+
}
|
|
38
|
+
type DisplaceableToolName = "hub" | "todo";
|
|
39
|
+
|
|
40
|
+
function isTodoToolDetails(details: unknown): details is TodoToolDetails {
|
|
41
|
+
return (
|
|
42
|
+
typeof details === "object" &&
|
|
43
|
+
details !== null &&
|
|
44
|
+
"phases" in details &&
|
|
45
|
+
Array.isArray((details as { phases?: unknown }).phases)
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface ToolImageBlock {
|
|
50
|
+
data?: string;
|
|
51
|
+
mimeType?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function imageBlocksFromDetails(details: unknown): ToolImageBlock[] {
|
|
55
|
+
if (!isRecord(details) || !Array.isArray(details.images)) return [];
|
|
56
|
+
return details.images.filter(
|
|
57
|
+
(image): image is ToolImageBlock =>
|
|
58
|
+
isRecord(image) &&
|
|
59
|
+
(image.data === undefined || typeof image.data === "string") &&
|
|
60
|
+
(image.mimeType === undefined || typeof image.mimeType === "string"),
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function displaceableToolName(
|
|
65
|
+
toolName: string,
|
|
66
|
+
result: { details?: unknown; isError?: boolean },
|
|
67
|
+
isPartial: boolean,
|
|
68
|
+
): DisplaceableToolName | undefined {
|
|
69
|
+
if (result.isError === true) return undefined;
|
|
70
|
+
if (toolName === "hub" && isWaitingPollDetails(result.details)) return "hub";
|
|
71
|
+
if (toolName === "todo" && !isPartial && isTodoToolDetails(result.details)) return "todo";
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isEditLikeToolName(toolName: string): boolean {
|
|
76
|
+
return toolName === "edit" || toolName === "apply_patch";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function resolveEditModeForTool(toolName: string, tool: AgentTool | undefined): EditMode | undefined {
|
|
80
|
+
if (toolName === "apply_patch") return "apply_patch";
|
|
81
|
+
if (toolName !== "edit") return undefined;
|
|
82
|
+
return (tool as { mode?: EditMode } | undefined)?.mode;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
type ToolRendererStage = "call" | "result";
|
|
86
|
+
|
|
87
|
+
class SafeToolRendererComponent implements Component {
|
|
88
|
+
#toolName: string;
|
|
89
|
+
#stage: ToolRendererStage;
|
|
90
|
+
#component: Component;
|
|
91
|
+
#fallback: () => Component | undefined;
|
|
92
|
+
#warned = false;
|
|
93
|
+
readonly wantsKeyRelease: boolean | undefined;
|
|
94
|
+
|
|
95
|
+
constructor(
|
|
96
|
+
toolName: string,
|
|
97
|
+
stage: ToolRendererStage,
|
|
98
|
+
component: Component,
|
|
99
|
+
fallback: () => Component | undefined,
|
|
100
|
+
) {
|
|
101
|
+
this.#toolName = toolName;
|
|
102
|
+
this.#stage = stage;
|
|
103
|
+
this.#component = component;
|
|
104
|
+
this.#fallback = fallback;
|
|
105
|
+
this.wantsKeyRelease = component.wantsKeyRelease;
|
|
106
|
+
if (isFramedBlockComponent(component)) {
|
|
107
|
+
markFramedBlockComponent(this);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
render(width: number): readonly string[] {
|
|
112
|
+
try {
|
|
113
|
+
return this.#component.render(width);
|
|
114
|
+
} catch (err) {
|
|
115
|
+
if (!this.#warned) {
|
|
116
|
+
this.#warned = true;
|
|
117
|
+
logger.warn("Tool renderer failed", { tool: this.#toolName, stage: this.#stage, error: String(err) });
|
|
118
|
+
}
|
|
119
|
+
return this.#fallback()?.render(width) ?? [];
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
handleInput(data: string): void {
|
|
124
|
+
const handleInput = this.#component.handleInput;
|
|
125
|
+
if (handleInput === undefined) return;
|
|
126
|
+
handleInput.call(this.#component, data);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
invalidate(): void {
|
|
130
|
+
const invalidate = this.#component.invalidate;
|
|
131
|
+
if (invalidate === undefined) return;
|
|
132
|
+
invalidate.call(this.#component);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
setIgnoreTight(ignore: boolean): void {
|
|
136
|
+
const setIgnoreTight = this.#component.setIgnoreTight;
|
|
137
|
+
if (setIgnoreTight === undefined) return;
|
|
138
|
+
setIgnoreTight.call(this.#component, ignore);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
dispose(): void {
|
|
142
|
+
const dispose = this.#component.dispose;
|
|
143
|
+
if (dispose === undefined) return;
|
|
144
|
+
dispose.call(this.#component);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Minimal TUI surface ToolExecutionComponent uses to schedule repaints and share image budget. */
|
|
149
|
+
export interface ToolExecutionUi {
|
|
150
|
+
requestRender(): void;
|
|
151
|
+
requestComponentRender(component: Component): void;
|
|
152
|
+
resetDisplay(): void;
|
|
153
|
+
imageBudget?: TUI["imageBudget"];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface ToolExecutionOptions {
|
|
157
|
+
showImages?: boolean; // default: true (only used if terminal supports images)
|
|
158
|
+
/** Allow the name-keyed renderer registry only when the active tool is the built-in implementation. */
|
|
159
|
+
useBuiltInRenderer?: boolean;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export interface ToolExecutionHandle extends Component {
|
|
163
|
+
updateArgs(args: unknown, toolCallId?: string): void;
|
|
164
|
+
updateStreamPreview?(update: unknown): void;
|
|
165
|
+
updateResult(
|
|
166
|
+
result: {
|
|
167
|
+
content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;
|
|
168
|
+
details?: unknown;
|
|
169
|
+
isError?: boolean;
|
|
170
|
+
},
|
|
171
|
+
isPartial?: boolean,
|
|
172
|
+
toolCallId?: string,
|
|
173
|
+
): void;
|
|
174
|
+
setArgsComplete(toolCallId?: string): void;
|
|
175
|
+
setExecutionStarted(toolCallId?: string): void;
|
|
176
|
+
setExpanded(expanded: boolean): void;
|
|
177
|
+
setToolActivityVisible(visible: boolean): void;
|
|
178
|
+
/** Mark the call parked: it returned, but stays tracked for async job frames. */
|
|
179
|
+
parkAsBackground(): void;
|
|
180
|
+
/** Seal the block as final history and stop its animations. */
|
|
181
|
+
seal(): void;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Phase-locked spinner glyph index shared by every live tool block so parallel
|
|
185
|
+
* spinners advance in lockstep instead of each tracking its own start time. */
|
|
186
|
+
export function sharedSpinnerFrame(frameCount: number, now: number = performance.now()): number {
|
|
187
|
+
return frameCount > 0 ? Math.floor(now / SPINNER_ADVANCE_MS) % frameCount : 0;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Live tool blocks currently driving a spinner. A single shared ticker (below)
|
|
191
|
+
* advances and repaints every registered block per glyph step, so N concurrent
|
|
192
|
+
* live/streaming blocks — e.g. parallel `task` subagents — cost one 80ms timer
|
|
193
|
+
* and one coalesced render frame per tick instead of N unsynchronized timers
|
|
194
|
+
* each independently waking the render scheduler (issue #8731). */
|
|
195
|
+
const liveSpinnerBlocks = new Set<ToolExecutionComponent>();
|
|
196
|
+
let sharedSpinnerTimer: NodeJS.Timeout | undefined;
|
|
197
|
+
|
|
198
|
+
/** Arm the shared spinner ticker if it is not already running. */
|
|
199
|
+
function ensureSharedSpinnerTicker(): void {
|
|
200
|
+
if (sharedSpinnerTimer) return;
|
|
201
|
+
sharedSpinnerTimer = setInterval(() => {
|
|
202
|
+
const frame = sharedSpinnerFrame(theme.spinnerFrames.length);
|
|
203
|
+
// Removing the current block mid-iteration is safe on a Set.
|
|
204
|
+
for (const block of liveSpinnerBlocks) block.tickSpinner(frame);
|
|
205
|
+
}, SPINNER_ADVANCE_MS);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Register a live block with the shared ticker, starting it on first use. */
|
|
209
|
+
function registerSpinnerBlock(block: ToolExecutionComponent): void {
|
|
210
|
+
liveSpinnerBlocks.add(block);
|
|
211
|
+
ensureSharedSpinnerTicker();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Drop a block; stop the ticker once no live block remains. */
|
|
215
|
+
function unregisterSpinnerBlock(block: ToolExecutionComponent): void {
|
|
216
|
+
if (!liveSpinnerBlocks.delete(block)) return;
|
|
217
|
+
if (liveSpinnerBlocks.size === 0 && sharedSpinnerTimer) {
|
|
218
|
+
clearInterval(sharedSpinnerTimer);
|
|
219
|
+
sharedSpinnerTimer = undefined;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Stop the shared spinner ticker and drop every registered live block.
|
|
224
|
+
* Called on interactive-mode teardown so a stray live block cannot keep the
|
|
225
|
+
* process-wide 80ms interval alive past shutdown (lingering event-loop
|
|
226
|
+
* handles pin the process; cf. `postmortem.quit`). Test files that assert on
|
|
227
|
+
* ticker arming also use this to start from a clean slate. */
|
|
228
|
+
export function stopSharedSpinnerTicker(): void {
|
|
229
|
+
liveSpinnerBlocks.clear();
|
|
230
|
+
if (sharedSpinnerTimer) {
|
|
231
|
+
clearInterval(sharedSpinnerTimer);
|
|
232
|
+
sharedSpinnerTimer = undefined;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Stable per-instance counter so each tool execution's inline images get a
|
|
237
|
+
// graphics id that survives child re-creation (the image budget keys off it).
|
|
238
|
+
let toolExecutionInstanceSeq = 0;
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Component that renders a tool call with its result (updateable)
|
|
242
|
+
*/
|
|
243
|
+
export class ToolExecutionComponent extends Container {
|
|
244
|
+
#contentBox: Box; // Used for custom tools and bash visual truncation
|
|
245
|
+
#contentText: WidthAwareText; // Generic fallback (no custom/built-in renderer)
|
|
246
|
+
// Which container the constructor mounted: bespoke/built-in renderers use
|
|
247
|
+
// #contentBox, everything else the generic #contentText fallback.
|
|
248
|
+
#usesContentBox = false;
|
|
249
|
+
#multiFileBoxes: (Box | Spacer)[] = []; // Extra boxes for multi-file edit results
|
|
250
|
+
#imageComponents: Image[] = [];
|
|
251
|
+
#imageSpacers: Spacer[] = [];
|
|
252
|
+
readonly #instanceId = ++toolExecutionInstanceSeq;
|
|
253
|
+
#toolName: string;
|
|
254
|
+
#toolLabel: string;
|
|
255
|
+
#args: unknown;
|
|
256
|
+
#expanded = false;
|
|
257
|
+
#allocation = Number.POSITIVE_INFINITY;
|
|
258
|
+
#presentationFrame: AnimationFrame = { tick: 0, now: 0 };
|
|
259
|
+
#toolActivityVisible = true;
|
|
260
|
+
#showImages: boolean;
|
|
261
|
+
#isPartial = true;
|
|
262
|
+
// A background task whose call already returned; later async job frames are
|
|
263
|
+
// partial updates, but the block is ready to retire as history.
|
|
264
|
+
#parkedBackground = false;
|
|
265
|
+
#resultVersion = 0;
|
|
266
|
+
// Post-finalize mutation counter (see FinalizableBlock.getTranscriptBlockVersion):
|
|
267
|
+
// a tool block can keep changing after isTranscriptBlockFinalized() first
|
|
268
|
+
// returns true — an async task's terminal result settlement, seal(), or an
|
|
269
|
+
// expansion toggle — and the transcript's width-epoch resolution and
|
|
270
|
+
// committed-render bypass must observe those mutations.
|
|
271
|
+
#blockVersion = 0;
|
|
272
|
+
#lastDisplayKey: string | undefined;
|
|
273
|
+
// Bumped whenever a render input that #rebuildDisplay consumes but the memo
|
|
274
|
+
// key cannot cheaply hash changes: streamed call args, native edit-diff
|
|
275
|
+
// previews, and Kitty PNG conversions. Folded into the dirty key so those
|
|
276
|
+
// updates are not swallowed by the memo (see #updateDisplay).
|
|
277
|
+
#displayInputVersion = 0;
|
|
278
|
+
// Set once #rebuildDisplay has populated the display. Replaces a
|
|
279
|
+
// #contentBox.children.length probe so the memo fast-path also covers the
|
|
280
|
+
// #contentText fallback path (which leaves #contentBox empty).
|
|
281
|
+
#displayBuilt = false;
|
|
282
|
+
// Number of Image children the last rebuild emitted. Only when this is > 0 does
|
|
283
|
+
// the memo key fold in viewport-dependent image sizing (resolveImageOptions),
|
|
284
|
+
// so a terminal resize re-shapes image-bearing results to rescale them without
|
|
285
|
+
// forcing the common image-free result to re-shape on every resize tick.
|
|
286
|
+
#renderedImageCount = 0;
|
|
287
|
+
#tool?: AgentTool;
|
|
288
|
+
#renderer?: ToolRenderer;
|
|
289
|
+
#ui: ToolExecutionUi;
|
|
290
|
+
#result?: {
|
|
291
|
+
content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;
|
|
292
|
+
isError?: boolean;
|
|
293
|
+
details?: unknown;
|
|
294
|
+
};
|
|
295
|
+
// Edit preview state
|
|
296
|
+
#editMode?: EditMode;
|
|
297
|
+
#editDiffPreview?: PerFileDiffPreview[];
|
|
298
|
+
#previewReady?: PromiseWithResolvers<void>;
|
|
299
|
+
// Cached converted images for Kitty protocol (which requires PNG), keyed by index
|
|
300
|
+
#convertedImages: Map<number, { data: string; mimeType: string }> = new Map();
|
|
301
|
+
// Spinner animation for partial task results
|
|
302
|
+
#spinnerFrame?: number;
|
|
303
|
+
#spinnerActive = false;
|
|
304
|
+
// Todo write completion strikethrough reveal animation
|
|
305
|
+
#todoStrikeInterval?: NodeJS.Timeout;
|
|
306
|
+
// Track if args are still being streamed (for edit/write spinner)
|
|
307
|
+
#argsComplete = false;
|
|
308
|
+
#executionStarted = false;
|
|
309
|
+
// Sealed once the tool reaches a terminal state (result delivered, or the
|
|
310
|
+
// turn abandoned it without one). Until then the block remains active so a
|
|
311
|
+
// late result can update its streaming preview.
|
|
312
|
+
#sealed = false;
|
|
313
|
+
// Tool result snapshots that may be superseded by a later same-tool call
|
|
314
|
+
// while still in the mutable viewport. `hub` uses this for repeated all-running polls; `todo` uses
|
|
315
|
+
// it for per-turn state snapshots so only the latest list remains visible.
|
|
316
|
+
#displaceableByToolName: DisplaceableToolName | undefined;
|
|
317
|
+
// Execution start on the presentation clock (performance.now domain, the
|
|
318
|
+
// same domain as AnimationFrame.now supplied by the transcript allocator).
|
|
319
|
+
#executionStartedAtNow: number | undefined;
|
|
320
|
+
// Wall clock captured whenever a task card is rebuilt.
|
|
321
|
+
#taskRenderNowMs = Date.now();
|
|
322
|
+
// Set on each `render()` when the last painted pending shape must be
|
|
323
|
+
// replayed wholesale when the first result arrives. Reset gates key off
|
|
324
|
+
// these so a topology-changing update that lands before the shape reaches
|
|
325
|
+
// the terminal never triggers an unnecessary full-viewport replay.
|
|
326
|
+
#firstResultViewportRepaintShapePainted = false;
|
|
327
|
+
#partialResultShapePainted = false;
|
|
328
|
+
#renderState: {
|
|
329
|
+
spinnerFrame?: number;
|
|
330
|
+
expanded: boolean;
|
|
331
|
+
isPartial: boolean;
|
|
332
|
+
argsComplete?: boolean;
|
|
333
|
+
executionStarted?: boolean;
|
|
334
|
+
renderContext?: Record<string, unknown>;
|
|
335
|
+
} = {
|
|
336
|
+
expanded: false,
|
|
337
|
+
isPartial: true,
|
|
338
|
+
argsComplete: false,
|
|
339
|
+
executionStarted: false,
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
constructor(
|
|
343
|
+
toolName: string,
|
|
344
|
+
args: unknown,
|
|
345
|
+
options: ToolExecutionOptions = {},
|
|
346
|
+
tool: AgentTool | undefined,
|
|
347
|
+
ui: ToolExecutionUi,
|
|
348
|
+
_cwd: string = getProjectDir(),
|
|
349
|
+
_toolCallId?: string,
|
|
350
|
+
) {
|
|
351
|
+
super();
|
|
352
|
+
ensureThemeSync();
|
|
353
|
+
this.#toolName = toolName;
|
|
354
|
+
this.#toolLabel = tool?.label ?? toolName;
|
|
355
|
+
this.#renderer = options.useBuiltInRenderer === false ? undefined : toolRenderers[toolName];
|
|
356
|
+
this.#showImages = options.showImages ?? true;
|
|
357
|
+
this.#tool = tool;
|
|
358
|
+
this.#ui = ui;
|
|
359
|
+
this.#args = args;
|
|
360
|
+
this.#editMode = resolveEditModeForTool(toolName, tool);
|
|
361
|
+
if (this.#editMode) this.#previewReady = Promise.withResolvers<void>();
|
|
362
|
+
|
|
363
|
+
// Always create both - contentBox for custom tools/bash/tools with renderers, contentText for other built-ins.
|
|
364
|
+
// paddingY is 1 so background-tinted blocks (custom/extension tools and the
|
|
365
|
+
// generic fallback) get top/bottom breathing room. TranscriptContainer
|
|
366
|
+
// strips PLAIN-blank edges, so framed/minimal blocks (no bg set) drop these
|
|
367
|
+
// lines and keep their tight spacing — only tinted lines survive.
|
|
368
|
+
this.#contentBox = new Box(0, 1);
|
|
369
|
+
this.#contentText = new WidthAwareText(contentWidth => this.#renderDefaultCard(contentWidth), 1, 1);
|
|
370
|
+
|
|
371
|
+
// Use Box for custom tools or built-in tools with rich renderers.
|
|
372
|
+
const hasCustomRenderer = !!(tool?.renderCall || tool?.renderResult);
|
|
373
|
+
this.#usesContentBox = hasCustomRenderer || this.#renderer !== undefined;
|
|
374
|
+
if (this.#usesContentBox) {
|
|
375
|
+
this.addChild(this.#contentBox);
|
|
376
|
+
} else {
|
|
377
|
+
this.addChild(this.#contentText);
|
|
378
|
+
}
|
|
379
|
+
// Tool blocks are visually distinct cards (background-tinted or framed),
|
|
380
|
+
// so keep their horizontal padding even when the user enables tight layout.
|
|
381
|
+
this.setIgnoreTight(true);
|
|
382
|
+
|
|
383
|
+
this.#updateSpinnerAnimation();
|
|
384
|
+
this.#updateDisplay();
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
updateArgs(args: unknown, _toolCallId?: string): void {
|
|
388
|
+
// Reference-equality short-circuit before any further work. Callers
|
|
389
|
+
// always allocate a new arg object on each streamed delta (see
|
|
390
|
+
// event-controller.ts and ui-helpers.ts), so a same-reference assignment
|
|
391
|
+
// signals "nothing meaningful changed" and the renderer can skip.
|
|
392
|
+
if (args === this.#args) return;
|
|
393
|
+
this.#args = args;
|
|
394
|
+
this.#displayInputVersion++;
|
|
395
|
+
this.#updateSpinnerAnimation();
|
|
396
|
+
this.#updateDisplay();
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Signal that args are complete (tool is about to execute).
|
|
401
|
+
* This triggers an immediate final diff computation for edit-like tools.
|
|
402
|
+
*/
|
|
403
|
+
setArgsComplete(_toolCallId?: string): void {
|
|
404
|
+
const alreadyComplete = this.#argsComplete;
|
|
405
|
+
this.#argsComplete = true;
|
|
406
|
+
this.#updateSpinnerAnimation();
|
|
407
|
+
if (alreadyComplete) return;
|
|
408
|
+
this.#displayInputVersion++;
|
|
409
|
+
this.#updateDisplay();
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Signal that this specific call has begun executing (`tool_execution_start`).
|
|
414
|
+
* Distinct from {@link setArgsComplete}: exclusive writes are marked complete
|
|
415
|
+
* at `message_end` but stay queued until this fires for that call.
|
|
416
|
+
*/
|
|
417
|
+
setExecutionStarted(_toolCallId?: string): void {
|
|
418
|
+
if (this.#executionStarted) return;
|
|
419
|
+
this.#executionStarted = true;
|
|
420
|
+
this.#executionStartedAtNow = performance.now();
|
|
421
|
+
this.#argsComplete = true;
|
|
422
|
+
this.#updateSpinnerAnimation();
|
|
423
|
+
this.#displayInputVersion++;
|
|
424
|
+
this.#updateDisplay();
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Resolve once the final (`streaming: false`) preview batch lands or the
|
|
429
|
+
* result settles. Callers without a live batch source (gallery snapshots)
|
|
430
|
+
* pass a budget so a component that will never receive a batch cannot
|
|
431
|
+
* hang them; live approval gates omit it and wait indefinitely.
|
|
432
|
+
*/
|
|
433
|
+
async whenPreviewSettled(timeoutMs?: number): Promise<void> {
|
|
434
|
+
if (this.#previewReady === undefined) return;
|
|
435
|
+
if (timeoutMs === undefined) {
|
|
436
|
+
await this.#previewReady.promise;
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
await Promise.race([this.#previewReady.promise, Bun.sleep(timeoutMs)]);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
updateStreamPreview(update: unknown): void {
|
|
443
|
+
if (
|
|
444
|
+
this.#previewDiffSettled() ||
|
|
445
|
+
update === null ||
|
|
446
|
+
typeof update !== "object" ||
|
|
447
|
+
!("files" in update) ||
|
|
448
|
+
!Array.isArray(update.files)
|
|
449
|
+
) {
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
if ("streaming" in update && update.streaming === false) this.#previewReady?.resolve();
|
|
453
|
+
if (update.files.length === 0) return;
|
|
454
|
+
const rawFiles: unknown[] = update.files;
|
|
455
|
+
const files = rawFiles.filter(
|
|
456
|
+
(
|
|
457
|
+
file,
|
|
458
|
+
): file is {
|
|
459
|
+
path: string;
|
|
460
|
+
diff?: string | null;
|
|
461
|
+
firstChangedLine?: number | null;
|
|
462
|
+
error?: string | null;
|
|
463
|
+
} => file !== null && typeof file === "object" && "path" in file && typeof file.path === "string",
|
|
464
|
+
);
|
|
465
|
+
if (files.length === 0) return;
|
|
466
|
+
this.#editDiffPreview = files.map(file => ({
|
|
467
|
+
path: file.path,
|
|
468
|
+
diff: file.diff ?? undefined,
|
|
469
|
+
firstChangedLine: file.firstChangedLine ?? undefined,
|
|
470
|
+
error: file.error ?? undefined,
|
|
471
|
+
}));
|
|
472
|
+
this.#displayInputVersion++;
|
|
473
|
+
this.#updateDisplay();
|
|
474
|
+
this.#ui.requestRender();
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* True once a terminal result makes the streaming preview moot: renderResult
|
|
479
|
+
* prefers `details.diff` and renders errors from the result text, consulting
|
|
480
|
+
* the computed preview only for a non-error result that carries no details.
|
|
481
|
+
*/
|
|
482
|
+
#previewDiffSettled(): boolean {
|
|
483
|
+
const result = this.#result;
|
|
484
|
+
return result !== undefined && !this.#isPartial && (result.isError === true || result.details != null);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
updateResult(
|
|
488
|
+
result: {
|
|
489
|
+
content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;
|
|
490
|
+
details?: unknown;
|
|
491
|
+
isError?: boolean;
|
|
492
|
+
},
|
|
493
|
+
isPartial = false,
|
|
494
|
+
_toolCallId?: string,
|
|
495
|
+
): void {
|
|
496
|
+
const hadNoResult = this.#result === undefined;
|
|
497
|
+
const wasPartialResult = this.#result !== undefined && this.#isPartial;
|
|
498
|
+
const firstResultRepaintShapePainted = this.#firstResultViewportRepaintShapePainted;
|
|
499
|
+
const partialResultPainted = this.#partialResultShapePainted;
|
|
500
|
+
this.#firstResultViewportRepaintShapePainted = false;
|
|
501
|
+
this.#partialResultShapePainted = false;
|
|
502
|
+
this.#result = result;
|
|
503
|
+
this.#resultVersion++;
|
|
504
|
+
this.#blockVersion++;
|
|
505
|
+
this.#isPartial = isPartial;
|
|
506
|
+
this.#displaceableByToolName = displaceableToolName(this.#toolName, result, isPartial);
|
|
507
|
+
// When tool is complete, ensure args are marked complete so spinner stops
|
|
508
|
+
if (!isPartial) {
|
|
509
|
+
this.#argsComplete = true;
|
|
510
|
+
this.#previewReady?.resolve();
|
|
511
|
+
}
|
|
512
|
+
this.#updateSpinnerAnimation();
|
|
513
|
+
this.#updateTodoStrikeAnimation();
|
|
514
|
+
this.#updateDisplay();
|
|
515
|
+
this.#resetDisplayForResultTopologyChange(
|
|
516
|
+
hadNoResult && firstResultRepaintShapePainted,
|
|
517
|
+
wasPartialResult && partialResultPainted,
|
|
518
|
+
isPartial,
|
|
519
|
+
);
|
|
520
|
+
// Convert non-PNG images to PNG for Kitty protocol (async)
|
|
521
|
+
this.#maybeConvertImagesForKitty();
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Get all image blocks from result content and details.
|
|
526
|
+
* Some tools (like generate_image) store images in details to avoid bloating
|
|
527
|
+
* model context. Xdev-dispatched tools preserve those details under
|
|
528
|
+
* details.xdev.inner.
|
|
529
|
+
*/
|
|
530
|
+
#getAllImageBlocks(): ToolImageBlock[] {
|
|
531
|
+
if (!this.#result) return [];
|
|
532
|
+
const contentImages = this.#result.content.filter(block => block.type === "image");
|
|
533
|
+
const details = this.#result.details;
|
|
534
|
+
const detailImages = imageBlocksFromDetails(details);
|
|
535
|
+
const xdevImages = isRecord(details) && isRecord(details.xdev) ? imageBlocksFromDetails(details.xdev.inner) : [];
|
|
536
|
+
return [...contentImages, ...detailImages, ...xdevImages];
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Convert non-PNG images to PNG for Kitty graphics protocol.
|
|
541
|
+
* Kitty requires PNG format (f=100), so JPEG/GIF/WebP won't display.
|
|
542
|
+
*/
|
|
543
|
+
#maybeConvertImagesForKitty(): void {
|
|
544
|
+
// Only needed for Kitty protocol
|
|
545
|
+
if (TERMINAL.imageProtocol !== ImageProtocol.Kitty) return;
|
|
546
|
+
if (!this.#result) return;
|
|
547
|
+
|
|
548
|
+
const imageBlocks = this.#getAllImageBlocks();
|
|
549
|
+
|
|
550
|
+
for (let i = 0; i < imageBlocks.length; i++) {
|
|
551
|
+
const img = imageBlocks[i];
|
|
552
|
+
if (!img.data || !img.mimeType) continue;
|
|
553
|
+
// Skip if already PNG or already converted
|
|
554
|
+
if (img.mimeType === "image/png") continue;
|
|
555
|
+
if (this.#convertedImages.has(i)) continue;
|
|
556
|
+
|
|
557
|
+
// Convert async - catch errors from processing
|
|
558
|
+
const index = i;
|
|
559
|
+
convertImageToPng({ type: "image", data: img.data, mimeType: img.mimeType })
|
|
560
|
+
.then(converted => {
|
|
561
|
+
this.#convertedImages.set(index, converted);
|
|
562
|
+
this.#displayInputVersion++;
|
|
563
|
+
this.#updateDisplay();
|
|
564
|
+
this.#ui.requestRender();
|
|
565
|
+
})
|
|
566
|
+
.catch(() => {
|
|
567
|
+
// Ignore conversion failures - display will use original image format
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Start or stop spinner animation for live states that visibly tick.
|
|
574
|
+
*/
|
|
575
|
+
#updateSpinnerAnimation(): void {
|
|
576
|
+
// Live partial tool blocks stay repaintable until a terminal result seals
|
|
577
|
+
// them. Todo snapshots and detached background tool progress are deliberate
|
|
578
|
+
// static exceptions because their rows can be superseded or committed to
|
|
579
|
+
// scrollback while later updates continue elsewhere.
|
|
580
|
+
const isStreamingArgs = !this.#argsComplete && (isEditLikeToolName(this.#toolName) || this.#toolName === "write");
|
|
581
|
+
const isBackgroundAsyncRunning =
|
|
582
|
+
(this.#result?.details as { async?: { state?: string } } | undefined)?.async?.state === "running";
|
|
583
|
+
const renderer = this.#renderer;
|
|
584
|
+
const pendingAnimation = renderer?.animatedPendingPreview;
|
|
585
|
+
const partialAnimation = renderer?.animatedPartialResult;
|
|
586
|
+
const pendingCallConsumesSpinner =
|
|
587
|
+
this.#result === undefined &&
|
|
588
|
+
(renderer === undefined
|
|
589
|
+
? // Only the generic #formatToolExecution fallback consumes the frame;
|
|
590
|
+
// a custom renderCall/renderResult pair routes through the custom
|
|
591
|
+
// branch whose pending label is a static tool-name Text.
|
|
592
|
+
!this.#tool?.renderCall && !this.#tool?.renderResult
|
|
593
|
+
: typeof pendingAnimation === "function"
|
|
594
|
+
? pendingAnimation(this.#args)
|
|
595
|
+
: pendingAnimation === true);
|
|
596
|
+
const partialResultConsumesSpinner =
|
|
597
|
+
this.#result !== undefined &&
|
|
598
|
+
(renderer === undefined
|
|
599
|
+
? !this.#tool?.renderCall && !this.#tool?.renderResult
|
|
600
|
+
: typeof partialAnimation === "function"
|
|
601
|
+
? partialAnimation(this.#args)
|
|
602
|
+
: partialAnimation === true);
|
|
603
|
+
const isLivePartialTool =
|
|
604
|
+
this.#isPartial &&
|
|
605
|
+
this.#toolName !== "todo" &&
|
|
606
|
+
!isBackgroundAsyncRunning &&
|
|
607
|
+
(pendingCallConsumesSpinner || partialResultConsumesSpinner);
|
|
608
|
+
const needsSpinner = isStreamingArgs || isLivePartialTool || this.#displaceableByToolName === "hub";
|
|
609
|
+
if (needsSpinner && !this.#spinnerActive) {
|
|
610
|
+
const frameCount = theme.spinnerFrames.length;
|
|
611
|
+
const frame = sharedSpinnerFrame(frameCount);
|
|
612
|
+
this.#spinnerFrame = frame;
|
|
613
|
+
this.#renderState.spinnerFrame = frame;
|
|
614
|
+
this.#spinnerActive = true;
|
|
615
|
+
registerSpinnerBlock(this);
|
|
616
|
+
} else if (!needsSpinner && this.#spinnerActive) {
|
|
617
|
+
this.#spinnerActive = false;
|
|
618
|
+
unregisterSpinnerBlock(this);
|
|
619
|
+
// Clear the last drawn frame so a non-live renderCall (e.g. a write whose
|
|
620
|
+
// args just completed) stops showing a frozen spinner glyph. Skip when a
|
|
621
|
+
// todo strike owns the frame — it sets its own value right after this.
|
|
622
|
+
if (!this.#todoStrikeInterval) {
|
|
623
|
+
this.#spinnerFrame = undefined;
|
|
624
|
+
this.#renderState.spinnerFrame = undefined;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Advance to the shared spinner glyph and repaint just this block. Driven by
|
|
631
|
+
* the single shared spinner ticker (see `registerSpinnerBlock`); the tick is
|
|
632
|
+
* component-scoped so the TUI reuses every other root subtree (issue #4377).
|
|
633
|
+
*/
|
|
634
|
+
tickSpinner(frame: number): void {
|
|
635
|
+
this.#spinnerFrame = frame;
|
|
636
|
+
this.#renderState.spinnerFrame = frame;
|
|
637
|
+
this.#ui.requestComponentRender(this);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
#updateTodoStrikeAnimation(): void {
|
|
641
|
+
if (this.#toolName !== "todo" || this.#isPartial || this.#result?.isError) {
|
|
642
|
+
this.#stopTodoStrikeAnimation();
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
const completedTasks = (this.#result?.details as { completedTasks?: unknown[] } | undefined)?.completedTasks;
|
|
646
|
+
if (!completedTasks || completedTasks.length === 0) {
|
|
647
|
+
this.#stopTodoStrikeAnimation();
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
if (this.#todoStrikeInterval) return;
|
|
651
|
+
|
|
652
|
+
this.#spinnerFrame = 0;
|
|
653
|
+
this.#renderState.spinnerFrame = 0;
|
|
654
|
+
this.#todoStrikeInterval = setInterval(() => {
|
|
655
|
+
const nextFrame = (this.#spinnerFrame ?? 0) + 1;
|
|
656
|
+
if (nextFrame > TODO_STRIKE_TOTAL_FRAMES) {
|
|
657
|
+
this.#stopTodoStrikeAnimation();
|
|
658
|
+
} else {
|
|
659
|
+
this.#spinnerFrame = nextFrame;
|
|
660
|
+
this.#renderState.spinnerFrame = nextFrame;
|
|
661
|
+
}
|
|
662
|
+
// Component-scoped: strike animation only mutates this tool block's
|
|
663
|
+
// glyph, so the TUI reuses every other root subtree (issue #4377).
|
|
664
|
+
this.#ui.requestComponentRender(this);
|
|
665
|
+
}, 65);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
#stopTodoStrikeAnimation(): void {
|
|
669
|
+
if (this.#todoStrikeInterval) {
|
|
670
|
+
clearInterval(this.#todoStrikeInterval);
|
|
671
|
+
this.#todoStrikeInterval = undefined;
|
|
672
|
+
}
|
|
673
|
+
if (!this.#spinnerActive) {
|
|
674
|
+
this.#spinnerFrame = undefined;
|
|
675
|
+
this.#renderState.spinnerFrame = undefined;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Whether this block is ready to retire as immutable history. Partial
|
|
681
|
+
* results, including detached background tasks, remain active and mutable
|
|
682
|
+
* until they settle. Hidden blocks render no rows and cannot gate history.
|
|
683
|
+
*/
|
|
684
|
+
isTranscriptBlockFinalized(): boolean {
|
|
685
|
+
if (!this.#toolActivityVisible) return true;
|
|
686
|
+
if (this.#sealed) return true;
|
|
687
|
+
if (this.#result === undefined) return false;
|
|
688
|
+
// A parked background task's call already returned; job frames that land
|
|
689
|
+
// while it is still live keep updating it, but it must not gate history.
|
|
690
|
+
if (this.#parkedBackground) return true;
|
|
691
|
+
return !this.#isPartial;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Subagent ids visible on this card for click-to-focus hit-testing. Empty
|
|
696
|
+
* unless this is a task card whose details already name spawned agents.
|
|
697
|
+
* Callers intersect with the live registry, which decides focusability.
|
|
698
|
+
*/
|
|
699
|
+
getClickFocusAgentIds(): string[] {
|
|
700
|
+
if (this.#toolName !== "task") return [];
|
|
701
|
+
return taskCardAgentIds(this.#result?.details);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
getTranscriptBlockVersion(): number {
|
|
705
|
+
return this.#blockVersion;
|
|
706
|
+
}
|
|
707
|
+
/** Mark the call parked: it returned, but stays tracked for async job frames. */
|
|
708
|
+
parkAsBackground(): void {
|
|
709
|
+
this.#parkedBackground = true;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Mark the tool terminal even though no result arrived (the turn aborted or
|
|
714
|
+
* abandoned it) and stop animating so the container can retire it.
|
|
715
|
+
*/
|
|
716
|
+
seal(): void {
|
|
717
|
+
if (this.#sealed) return;
|
|
718
|
+
this.#sealed = true;
|
|
719
|
+
this.#blockVersion++;
|
|
720
|
+
this.#displaceableByToolName = undefined;
|
|
721
|
+
this.stopAnimation();
|
|
722
|
+
this.#updateDisplay();
|
|
723
|
+
this.#ui.requestRender();
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Whether this block is a supersedable result snapshot that has not been
|
|
728
|
+
* sealed. Displacement is best-effort: the snapshot finalizes like any other
|
|
729
|
+
* block, so under capacity pressure it may retire to native scrollback
|
|
730
|
+
* first — then the follow-up call appends a fresh snapshot instead of
|
|
731
|
+
* removing this one (see {@link TranscriptContainer.canRemoveBlock}).
|
|
732
|
+
*/
|
|
733
|
+
isDisplaceableBlock(): boolean {
|
|
734
|
+
return this.#displaceableByToolName !== undefined && !this.#sealed;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
canBeDisplacedBy(nextToolName: string | undefined): boolean {
|
|
738
|
+
return (
|
|
739
|
+
this.#displaceableByToolName !== undefined && this.#displaceableByToolName === nextToolName && !this.#sealed
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* A live block torn down through the generic Container path (transcript
|
|
745
|
+
* clear, session switch mid-run) must not leak its shared-ticker
|
|
746
|
+
* registration: the dead component would keep the process-wide 80ms
|
|
747
|
+
* interval alive and keep being repainted.
|
|
748
|
+
*/
|
|
749
|
+
override dispose(): void {
|
|
750
|
+
this.stopAnimation();
|
|
751
|
+
super.dispose();
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Stop spinner animation and cleanup resources.
|
|
756
|
+
*/
|
|
757
|
+
stopAnimation(): void {
|
|
758
|
+
if (this.#spinnerActive) {
|
|
759
|
+
this.#spinnerActive = false;
|
|
760
|
+
unregisterSpinnerBlock(this);
|
|
761
|
+
this.#spinnerFrame = undefined;
|
|
762
|
+
this.#renderState.spinnerFrame = undefined;
|
|
763
|
+
}
|
|
764
|
+
this.#stopTodoStrikeAnimation();
|
|
765
|
+
this.#previewReady?.resolve();
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
setExpanded(expanded: boolean): void {
|
|
769
|
+
if (this.#expanded !== expanded) this.#blockVersion++;
|
|
770
|
+
this.#expanded = expanded;
|
|
771
|
+
this.#updateDisplay();
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/** Apply the transcript allocator's current viewport reservation. */
|
|
775
|
+
setTranscriptAllocation(rows: number, frame: AnimationFrame): void {
|
|
776
|
+
this.#allocation = Math.max(0, Math.trunc(rows));
|
|
777
|
+
this.#presentationFrame = frame;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
setToolActivityVisible(visible: boolean): void {
|
|
781
|
+
this.#toolActivityVisible = visible;
|
|
782
|
+
super.invalidate();
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
setShowImages(show: boolean): void {
|
|
786
|
+
this.#showImages = show;
|
|
787
|
+
this.#updateDisplay();
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
override invalidate(): void {
|
|
791
|
+
super.invalidate();
|
|
792
|
+
this.#updateDisplay();
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
#updateDisplay(): void {
|
|
796
|
+
// `TERMINAL.imageProtocol` is resolved by an async capability probe during
|
|
797
|
+
// TUI startup, so a result rendered before it lands must re-shape once it
|
|
798
|
+
// does (it gates Image children vs text fallback in #rebuildDisplay); keyed
|
|
799
|
+
// here for the same reason markdown.ts keys its render cache on it.
|
|
800
|
+
const key = `${this.#resultVersion}|${this.#expanded}|${this.#isPartial}|${this.#argsComplete ? "1" : "0"}|${this.#executionStarted ? "1" : "0"}|${this.#spinnerFrame ?? "-"}|${this.#showImages}|${getThemeEpoch()}|${this.#displayInputVersion}|${TERMINAL.imageProtocol ?? "-"}|${this.#imageSizeKey()}`;
|
|
801
|
+
if (key === this.#lastDisplayKey && this.#displayBuilt) return;
|
|
802
|
+
this.#lastDisplayKey = key;
|
|
803
|
+
|
|
804
|
+
this.#rebuildDisplay();
|
|
805
|
+
this.#displayBuilt = true;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
#rendererFlag(name: "forceResultViewportRepaintOnSettle"): boolean {
|
|
809
|
+
const toolValue = (this.#tool as Record<string, unknown> | undefined)?.[name];
|
|
810
|
+
const rendererValue = this.#renderer?.[name];
|
|
811
|
+
return toolValue === true || (toolValue === undefined && rendererValue === true);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* True while the last painted pending-call shape opted into a full viewport
|
|
816
|
+
* repaint at the first result (`forceFirstResultViewportRepaint`) — e.g. a
|
|
817
|
+
* collapsed write tail window, which the first result render re-anchors
|
|
818
|
+
* instead of preserving. Kept as a per-paint fact so a topology-changing update that
|
|
819
|
+
* lands before the pending rows reach the terminal skips the reset.
|
|
820
|
+
*/
|
|
821
|
+
#needsFirstResultViewportRepaintAtRender(): boolean {
|
|
822
|
+
if (this.#result !== undefined) return false;
|
|
823
|
+
const toolValue = (this.#tool as { forceFirstResultViewportRepaint?: FirstResultViewportRepaint } | undefined)
|
|
824
|
+
?.forceFirstResultViewportRepaint;
|
|
825
|
+
const value = toolValue !== undefined ? toolValue : this.#renderer?.forceFirstResultViewportRepaint;
|
|
826
|
+
if (typeof value === "function") return value(this.#args, this.#renderState);
|
|
827
|
+
return value === true;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
#resetDisplayForResultTopologyChange(
|
|
831
|
+
firstResultAfterRepaintShapePaint: boolean,
|
|
832
|
+
partialResultPaintedBeforeSettle: boolean,
|
|
833
|
+
isPartial: boolean,
|
|
834
|
+
): void {
|
|
835
|
+
const provisionalResultSettled =
|
|
836
|
+
partialResultPaintedBeforeSettle && !isPartial && this.#rendererFlag("forceResultViewportRepaintOnSettle");
|
|
837
|
+
if (firstResultAfterRepaintShapePaint || provisionalResultSettled) {
|
|
838
|
+
this.#ui.requestRender();
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
override render(width: number): readonly string[] {
|
|
843
|
+
if (!this.#toolActivityVisible || this.#allocation === 0) return [];
|
|
844
|
+
let lines = super.render(width);
|
|
845
|
+
if (this.#allocation < 3) {
|
|
846
|
+
// A squeezed allocation degrades only blocks that genuinely overflow it.
|
|
847
|
+
// The allocator measures blocks by trimmed height and never squeezes one
|
|
848
|
+
// below that, so inline tools whose real content is 1-2 rows (hub
|
|
849
|
+
// receipts, one-line results) keep that content instead of an equally
|
|
850
|
+
// tall but contentless frame.
|
|
851
|
+
const trimmed = trimBlankEdges(lines);
|
|
852
|
+
if (trimmed.length > this.#allocation) return this.#renderCompact(width);
|
|
853
|
+
lines = trimmed;
|
|
854
|
+
}
|
|
855
|
+
this.#firstResultViewportRepaintShapePainted = this.#needsFirstResultViewportRepaintAtRender();
|
|
856
|
+
this.#partialResultShapePainted = this.#result !== undefined && this.#isPartial;
|
|
857
|
+
return lines;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
#renderCompact(width: number): readonly string[] {
|
|
861
|
+
const summary = this.#activitySummary();
|
|
862
|
+
const detail = summary.detail ? theme.fg("muted", ` · ${summary.detail.replace(/\s+/g, " ")}`) : "";
|
|
863
|
+
// Elapsed ticks only while the call is genuinely running; a settled
|
|
864
|
+
// placeholder row must not read as live ("Todo · running 0s").
|
|
865
|
+
const elapsed =
|
|
866
|
+
this.#isRunning() && this.#executionStartedAtNow !== undefined
|
|
867
|
+
? theme.fg(
|
|
868
|
+
"dim",
|
|
869
|
+
` ${Math.max(0, Math.floor((this.#presentationFrame.now - this.#executionStartedAtNow) / 1000))}s`,
|
|
870
|
+
)
|
|
871
|
+
: "";
|
|
872
|
+
const text = truncateToWidth(
|
|
873
|
+
`${theme.fg("toolTitle", theme.bold(summary.label))}${detail}${elapsed}`,
|
|
874
|
+
Math.max(1, width - 4),
|
|
875
|
+
);
|
|
876
|
+
if (this.#allocation === 1) {
|
|
877
|
+
const glyph = this.#spinnerFrame === undefined ? "•" : (theme.spinnerFrames[this.#spinnerFrame] ?? "•");
|
|
878
|
+
const styledGlyph = theme.fg(this.#spinnerFrame === undefined ? "dim" : "muted", glyph);
|
|
879
|
+
return [truncateToWidth(`${styledGlyph} ${text}`, width)];
|
|
880
|
+
}
|
|
881
|
+
return [truncateToWidth(`${theme.fg("dim", "╭─")} ${text}`, width), theme.fg("dim", "╰")];
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
#activitySummary(): ToolActivitySummary {
|
|
885
|
+
this.#renderState.renderContext ??= this.#buildRenderContext();
|
|
886
|
+
const summary = this.#renderer?.activitySummary?.(this.#args, {
|
|
887
|
+
expanded: this.#expanded,
|
|
888
|
+
isPartial: this.#isPartial,
|
|
889
|
+
spinnerFrame: this.#spinnerFrame,
|
|
890
|
+
renderContext: this.#renderState.renderContext,
|
|
891
|
+
});
|
|
892
|
+
if (summary !== undefined) {
|
|
893
|
+
if (summary.detail) return summary;
|
|
894
|
+
// A detail-less custom summary (e.g. hub before its streamed args
|
|
895
|
+
// parse) must not fold to a bare `╭─ Label` frame under viewport
|
|
896
|
+
// pressure — keep the generic liveness hint for in-flight calls.
|
|
897
|
+
return this.#isRunning() ? { ...summary, detail: "running" } : summary;
|
|
898
|
+
}
|
|
899
|
+
if (isRecord(this.#args)) {
|
|
900
|
+
for (const key of ["command", "path", "input"] as const) {
|
|
901
|
+
const value = this.#args[key];
|
|
902
|
+
if (typeof value === "string" && value.length > 0) {
|
|
903
|
+
return { label: this.#toolLabel, detail: value.split("\n", 1)[0] };
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
return { label: this.#toolLabel, detail: this.#isRunning() ? "running" : undefined };
|
|
908
|
+
}
|
|
909
|
+
/** Still executing: no settled result yet and the turn has not sealed it. */
|
|
910
|
+
#isRunning(): boolean {
|
|
911
|
+
return !this.#sealed && (this.#result === undefined || this.#isPartial);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// Viewport-/settings-dependent image sizing folded into the memo key only when
|
|
915
|
+
// the last rebuild actually emitted images, so a terminal resize re-shapes an
|
|
916
|
+
// image-bearing result (to rescale it) without re-shaping every image-free
|
|
917
|
+
// result on each resize tick.
|
|
918
|
+
#imageSizeKey(): string {
|
|
919
|
+
if (this.#renderedImageCount === 0) return "-";
|
|
920
|
+
const o = resolveImageOptions();
|
|
921
|
+
return `${o.maxWidthCells}:${o.maxHeightCells ?? "-"}`;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
#rebuildDisplay(): void {
|
|
925
|
+
// Sync shared mutable render state for component closures
|
|
926
|
+
this.#renderState.expanded = this.#expanded;
|
|
927
|
+
this.#renderState.isPartial = this.#isPartial;
|
|
928
|
+
this.#renderState.argsComplete = this.#argsComplete;
|
|
929
|
+
this.#renderState.executionStarted = this.#executionStarted;
|
|
930
|
+
this.#renderState.spinnerFrame = this.#spinnerFrame;
|
|
931
|
+
|
|
932
|
+
// Non-self-framing tools (custom/extension renderers and the generic
|
|
933
|
+
// fallback) get a padded, state-tinted block — built-ins that draw their
|
|
934
|
+
// own frame opt out below via the framed-component mark. A benign skip
|
|
935
|
+
// (steering/peer interrupt aborted a still-pending call) never ran, so it
|
|
936
|
+
// gets the neutral pending tint rather than the error tint (#7199).
|
|
937
|
+
const benignSkip = this.#isBenignSkip();
|
|
938
|
+
const stateBgKey =
|
|
939
|
+
this.#isPartial || benignSkip ? "toolPendingBg" : this.#result?.isError ? "toolErrorBg" : "toolSuccessBg";
|
|
940
|
+
const stateBgFn = (t: string) => theme.bg(stateBgKey, t);
|
|
941
|
+
|
|
942
|
+
// A benign skip is a synthetic placeholder for a call that never executed,
|
|
943
|
+
// so bypass any bespoke error frame and draw the neutral generic card —
|
|
944
|
+
// the per-tool ✘/red-border would misread normal mid-turn steering as a
|
|
945
|
+
// failure (#7199).
|
|
946
|
+
if (benignSkip) {
|
|
947
|
+
this.#renderBenignSkipCard(stateBgFn);
|
|
948
|
+
} else if (this.#tool && (this.#tool.renderCall || this.#tool.renderResult)) {
|
|
949
|
+
const tool = this.#tool;
|
|
950
|
+
const mergeCallAndResult = Boolean((tool as { mergeCallAndResult?: boolean }).mergeCallAndResult);
|
|
951
|
+
// Custom tools use Box for flexible component rendering
|
|
952
|
+
this.#contentBox.setBgFn(undefined);
|
|
953
|
+
this.#contentBox.clear();
|
|
954
|
+
// Mirror the built-in renderer branch so custom renderers (notably the
|
|
955
|
+
// task tool, whose live instance routes through here) receive the same
|
|
956
|
+
// render context — e.g. the `hasResult` flag that suppresses the task
|
|
957
|
+
// call preview once result lines exist.
|
|
958
|
+
this.#renderState.renderContext = this.#buildRenderContext();
|
|
959
|
+
|
|
960
|
+
// Render call component. The fallback label only stands in for a
|
|
961
|
+
// missing `renderCall`; when the call is intentionally suppressed
|
|
962
|
+
// (mergeCallAndResult once a result exists) we render nothing here so
|
|
963
|
+
// the result component isn't preceded by a redundant tool-name line.
|
|
964
|
+
const shouldRenderCall = !this.#result || !mergeCallAndResult;
|
|
965
|
+
if (shouldRenderCall) {
|
|
966
|
+
if (tool.renderCall) {
|
|
967
|
+
try {
|
|
968
|
+
const callArgs = this.#getCallArgsForRender();
|
|
969
|
+
const callComponent = tool.renderCall(callArgs, this.#renderState, theme) as Component | undefined;
|
|
970
|
+
if (callComponent) {
|
|
971
|
+
this.#contentBox.addChild(
|
|
972
|
+
new SafeToolRendererComponent(
|
|
973
|
+
this.#toolName,
|
|
974
|
+
"call",
|
|
975
|
+
callComponent,
|
|
976
|
+
() => new Text(theme.fg("toolTitle", theme.bold(this.#toolLabel)), 0, 0),
|
|
977
|
+
),
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
} catch (err) {
|
|
981
|
+
logger.warn("Tool renderer failed", { tool: this.#toolName, error: String(err) });
|
|
982
|
+
// Fall back to default on error
|
|
983
|
+
this.#contentBox.addChild(new Text(theme.fg("toolTitle", theme.bold(this.#toolLabel)), 0, 0));
|
|
984
|
+
}
|
|
985
|
+
} else {
|
|
986
|
+
// No custom renderCall, show tool name
|
|
987
|
+
this.#contentBox.addChild(new Text(theme.fg("toolTitle", theme.bold(this.#toolLabel)), 0, 0));
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// Render result component if we have a result
|
|
992
|
+
if (this.#result && tool.renderResult) {
|
|
993
|
+
try {
|
|
994
|
+
const renderResult = tool.renderResult as (
|
|
995
|
+
result: { content: Array<{ type: string; text?: string }>; details?: unknown; isError?: boolean },
|
|
996
|
+
options: { expanded: boolean; isPartial: boolean; spinnerFrame?: number },
|
|
997
|
+
theme: Theme,
|
|
998
|
+
args?: unknown,
|
|
999
|
+
) => Component;
|
|
1000
|
+
const resultComponent = renderResult(
|
|
1001
|
+
{
|
|
1002
|
+
content: this.#result.content,
|
|
1003
|
+
details: this.#result.details,
|
|
1004
|
+
isError: this.#result.isError,
|
|
1005
|
+
},
|
|
1006
|
+
this.#renderState,
|
|
1007
|
+
theme,
|
|
1008
|
+
this.#args,
|
|
1009
|
+
);
|
|
1010
|
+
if (resultComponent) {
|
|
1011
|
+
this.#contentBox.addChild(
|
|
1012
|
+
new SafeToolRendererComponent(this.#toolName, "result", resultComponent, () => {
|
|
1013
|
+
const output = this.#getTextOutput();
|
|
1014
|
+
if (!output) return undefined;
|
|
1015
|
+
return new Text(theme.fg("toolOutput", replaceTabs(output)), 0, 0);
|
|
1016
|
+
}),
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
} catch (err) {
|
|
1020
|
+
logger.warn("Tool renderer failed", { tool: this.#toolName, error: String(err) });
|
|
1021
|
+
// Fall back to showing raw output on error
|
|
1022
|
+
const output = this.#getTextOutput();
|
|
1023
|
+
if (output) {
|
|
1024
|
+
this.#contentBox.addChild(new Text(theme.fg("toolOutput", replaceTabs(output)), 0, 0));
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
} else if (this.#result) {
|
|
1028
|
+
// Has result but no custom renderResult
|
|
1029
|
+
const output = this.#getTextOutput();
|
|
1030
|
+
if (output) {
|
|
1031
|
+
this.#contentBox.addChild(new Text(theme.fg("toolOutput", replaceTabs(output)), 0, 0));
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
// Custom tools that draw their own frame (task) render flush; plain
|
|
1035
|
+
// extension renderers get the padded, state-tinted block back.
|
|
1036
|
+
const customFramed = this.#contentBox.children.some(isFramedBlockComponent);
|
|
1037
|
+
this.#contentBox.setPaddingX(customFramed ? 0 : 1);
|
|
1038
|
+
this.#contentBox.setBgFn(customFramed ? undefined : stateBgFn);
|
|
1039
|
+
} else if (this.#renderer) {
|
|
1040
|
+
// The active registry entry is a built-in tool with a rich renderer.
|
|
1041
|
+
const renderer = this.#renderer;
|
|
1042
|
+
|
|
1043
|
+
// Clean up previous multi-file boxes
|
|
1044
|
+
for (const box of this.#multiFileBoxes) {
|
|
1045
|
+
this.removeChild(box);
|
|
1046
|
+
}
|
|
1047
|
+
this.#multiFileBoxes = [];
|
|
1048
|
+
|
|
1049
|
+
// Check for multi-file edit results
|
|
1050
|
+
const details = this.#result?.details;
|
|
1051
|
+
const perFileResults = (isRecord(details) ? details.perFileResults : undefined) as
|
|
1052
|
+
| Array<{ path: string; isError?: boolean }>
|
|
1053
|
+
| undefined;
|
|
1054
|
+
if (perFileResults && perFileResults.length > 1) {
|
|
1055
|
+
// Multi-file: render each file as its own Box (identical to separate tool calls)
|
|
1056
|
+
this.#contentBox.setBgFn(undefined);
|
|
1057
|
+
this.#contentBox.clear();
|
|
1058
|
+
|
|
1059
|
+
const renderContext = this.#buildRenderContext();
|
|
1060
|
+
this.#renderState.renderContext = renderContext;
|
|
1061
|
+
|
|
1062
|
+
for (let i = 0; i < perFileResults.length; i++) {
|
|
1063
|
+
const fileResult = perFileResults[i];
|
|
1064
|
+
if (i > 0) {
|
|
1065
|
+
const spacer = new Spacer(1);
|
|
1066
|
+
this.#multiFileBoxes.push(spacer);
|
|
1067
|
+
this.addChild(spacer);
|
|
1068
|
+
}
|
|
1069
|
+
const fileBox = new Box(0, 0);
|
|
1070
|
+
try {
|
|
1071
|
+
const resultComponent = renderer.renderResult(
|
|
1072
|
+
{ content: [], details: fileResult, isError: fileResult.isError },
|
|
1073
|
+
this.#renderState,
|
|
1074
|
+
theme,
|
|
1075
|
+
);
|
|
1076
|
+
if (resultComponent) {
|
|
1077
|
+
fileBox.addChild(
|
|
1078
|
+
new SafeToolRendererComponent(this.#toolName, "result", resultComponent, () => undefined),
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
1081
|
+
} catch (err) {
|
|
1082
|
+
logger.warn("Tool renderer failed", { tool: this.#toolName, error: String(err) });
|
|
1083
|
+
}
|
|
1084
|
+
this.#multiFileBoxes.push(fileBox);
|
|
1085
|
+
this.addChild(fileBox);
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// Show pending indicator for remaining files
|
|
1089
|
+
const edits = isRecord(this.#args) ? this.#args.edits : undefined;
|
|
1090
|
+
const totalFiles = Array.isArray(edits)
|
|
1091
|
+
? new Set(edits.map((entry: unknown) => (isRecord(entry) ? entry.path : undefined)).filter(Boolean)).size
|
|
1092
|
+
: 0;
|
|
1093
|
+
const remaining = Math.max(0, totalFiles - perFileResults.length);
|
|
1094
|
+
if (remaining > 0 && this.#isPartial) {
|
|
1095
|
+
const pendingSpacer = new Spacer(1);
|
|
1096
|
+
this.#multiFileBoxes.push(pendingSpacer);
|
|
1097
|
+
this.addChild(pendingSpacer);
|
|
1098
|
+
const pendingBox = new Box(0, 0);
|
|
1099
|
+
const spinner =
|
|
1100
|
+
this.#spinnerFrame !== undefined ? formatStatusIcon("running", theme, this.#spinnerFrame) : "";
|
|
1101
|
+
const pendingText = renderStatusLine(
|
|
1102
|
+
{
|
|
1103
|
+
iconOverride: spinner,
|
|
1104
|
+
title: "Edit",
|
|
1105
|
+
description: theme.fg("dim", `${remaining} more file${remaining > 1 ? "s" : ""} pending…`),
|
|
1106
|
+
},
|
|
1107
|
+
theme,
|
|
1108
|
+
);
|
|
1109
|
+
pendingBox.addChild(new Text(pendingText, 0, 0));
|
|
1110
|
+
this.#multiFileBoxes.push(pendingBox);
|
|
1111
|
+
this.addChild(pendingBox);
|
|
1112
|
+
}
|
|
1113
|
+
} else {
|
|
1114
|
+
// Single-file or no result: standard rendering
|
|
1115
|
+
// Inline renderers skip background styling
|
|
1116
|
+
this.#contentBox.setBgFn(undefined);
|
|
1117
|
+
this.#contentBox.clear();
|
|
1118
|
+
|
|
1119
|
+
const renderContext = this.#buildRenderContext();
|
|
1120
|
+
this.#renderState.renderContext = renderContext;
|
|
1121
|
+
|
|
1122
|
+
const shouldRenderCall = !this.#result || !renderer.mergeCallAndResult;
|
|
1123
|
+
if (shouldRenderCall) {
|
|
1124
|
+
// Render call component
|
|
1125
|
+
try {
|
|
1126
|
+
const callArgs = this.#getCallArgsForRender();
|
|
1127
|
+
const callComponent = renderer.renderCall(callArgs, this.#renderState, theme);
|
|
1128
|
+
if (callComponent) {
|
|
1129
|
+
this.#contentBox.addChild(
|
|
1130
|
+
new SafeToolRendererComponent(
|
|
1131
|
+
this.#toolName,
|
|
1132
|
+
"call",
|
|
1133
|
+
callComponent,
|
|
1134
|
+
() => new Text(theme.fg("toolTitle", theme.bold(this.#toolLabel)), 0, 0),
|
|
1135
|
+
),
|
|
1136
|
+
);
|
|
1137
|
+
}
|
|
1138
|
+
} catch (err) {
|
|
1139
|
+
logger.warn("Tool renderer failed", { tool: this.#toolName, error: String(err) });
|
|
1140
|
+
// Fall back to default on error
|
|
1141
|
+
this.#contentBox.addChild(new Text(theme.fg("toolTitle", theme.bold(this.#toolLabel)), 0, 0));
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// Render result component if we have a result
|
|
1146
|
+
if (this.#result) {
|
|
1147
|
+
try {
|
|
1148
|
+
const resultComponent = renderer.renderResult(
|
|
1149
|
+
{
|
|
1150
|
+
content: this.#result.content,
|
|
1151
|
+
details: this.#result.details,
|
|
1152
|
+
isError: this.#result.isError,
|
|
1153
|
+
},
|
|
1154
|
+
this.#renderState,
|
|
1155
|
+
theme,
|
|
1156
|
+
this.#getCallArgsForRender(),
|
|
1157
|
+
);
|
|
1158
|
+
if (resultComponent) {
|
|
1159
|
+
this.#contentBox.addChild(
|
|
1160
|
+
new SafeToolRendererComponent(this.#toolName, "result", resultComponent, () => {
|
|
1161
|
+
const output = this.#getTextOutput();
|
|
1162
|
+
if (!output) return undefined;
|
|
1163
|
+
return new Text(theme.fg("toolOutput", replaceTabs(output)), 0, 0);
|
|
1164
|
+
}),
|
|
1165
|
+
);
|
|
1166
|
+
}
|
|
1167
|
+
} catch (err) {
|
|
1168
|
+
logger.warn("Tool renderer failed", { tool: this.#toolName, error: String(err) });
|
|
1169
|
+
// Fall back to showing raw output on error
|
|
1170
|
+
const output = this.#getTextOutput();
|
|
1171
|
+
if (output) {
|
|
1172
|
+
this.#contentBox.addChild(new Text(theme.fg("toolOutput", replaceTabs(output)), 0, 0));
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
} else {
|
|
1178
|
+
// Generic fallback (no custom/built-in renderer). WidthAwareText
|
|
1179
|
+
// reformats at render time so output fills the actual terminal width
|
|
1180
|
+
// instead of a fixed column cap.
|
|
1181
|
+
this.#contentText.setCustomBgFn(stateBgFn);
|
|
1182
|
+
this.#contentText.invalidate();
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
// Handle images (same for both custom and built-in)
|
|
1186
|
+
for (const img of this.#imageComponents) {
|
|
1187
|
+
this.removeChild(img);
|
|
1188
|
+
}
|
|
1189
|
+
this.#imageComponents = [];
|
|
1190
|
+
for (const spacer of this.#imageSpacers) {
|
|
1191
|
+
this.removeChild(spacer);
|
|
1192
|
+
}
|
|
1193
|
+
this.#imageSpacers = [];
|
|
1194
|
+
|
|
1195
|
+
if (this.#result) {
|
|
1196
|
+
const imageBlocks = this.#getAllImageBlocks();
|
|
1197
|
+
|
|
1198
|
+
for (let i = 0; i < imageBlocks.length; i++) {
|
|
1199
|
+
const img = imageBlocks[i];
|
|
1200
|
+
if (TERMINAL.imageProtocol && this.#showImages && img.data && img.mimeType) {
|
|
1201
|
+
// Use converted PNG for Kitty protocol if available
|
|
1202
|
+
const converted = this.#convertedImages.get(i);
|
|
1203
|
+
const imageData = converted?.data ?? img.data;
|
|
1204
|
+
const imageMimeType = converted?.mimeType ?? img.mimeType;
|
|
1205
|
+
|
|
1206
|
+
// For Kitty, skip non-PNG images that haven't been converted yet
|
|
1207
|
+
if (TERMINAL.imageProtocol === ImageProtocol.Kitty && imageMimeType !== "image/png") {
|
|
1208
|
+
continue;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
const spacer = new Spacer(1);
|
|
1212
|
+
this.addChild(spacer);
|
|
1213
|
+
this.#imageSpacers.push(spacer);
|
|
1214
|
+
const imageComponent = new Image(
|
|
1215
|
+
imageData,
|
|
1216
|
+
imageMimeType,
|
|
1217
|
+
{ fallbackColor: (s: string) => theme.fg("toolOutput", s) },
|
|
1218
|
+
{ ...resolveImageOptions(), budget: this.#ui.imageBudget, imageKey: `te${this.#instanceId}:${i}` },
|
|
1219
|
+
);
|
|
1220
|
+
this.#imageComponents.push(imageComponent);
|
|
1221
|
+
this.addChild(imageComponent);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
this.#renderedImageCount = this.#imageComponents.length;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
#getCallArgsForRender(): unknown {
|
|
1229
|
+
const renderArgs = this.#args;
|
|
1230
|
+
if (!isEditLikeToolName(this.#toolName)) {
|
|
1231
|
+
return renderArgs;
|
|
1232
|
+
}
|
|
1233
|
+
const previews = this.#editDiffPreview;
|
|
1234
|
+
if (!previews || previews.length === 0) {
|
|
1235
|
+
return renderArgs;
|
|
1236
|
+
}
|
|
1237
|
+
// Single-file previews feed the existing `previewDiff` channel consumed
|
|
1238
|
+
// by `formatStreamingDiff` in the renderer.
|
|
1239
|
+
const first = previews[0];
|
|
1240
|
+
if (!first?.diff) {
|
|
1241
|
+
return renderArgs;
|
|
1242
|
+
}
|
|
1243
|
+
return { ...(renderArgs as Record<string, unknown>), previewDiff: first.diff };
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
/**
|
|
1247
|
+
* Build render context for tools that need extra state (bash, python, edit)
|
|
1248
|
+
*/
|
|
1249
|
+
#buildRenderContext(): Record<string, unknown> {
|
|
1250
|
+
const context: Record<string, unknown> = {};
|
|
1251
|
+
const normalizeTimeoutSeconds = (value: unknown, maxSeconds: number): number | undefined => {
|
|
1252
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
|
|
1253
|
+
return Math.max(1, Math.min(maxSeconds, value));
|
|
1254
|
+
};
|
|
1255
|
+
|
|
1256
|
+
if (this.#toolName === "bash") {
|
|
1257
|
+
// Bash needs render context even before a result exists. The renderer uses the pending-call args
|
|
1258
|
+
// plus this context to keep the inline command preview visible while tool-call JSON is still streaming.
|
|
1259
|
+
if (this.#result) {
|
|
1260
|
+
// Pass raw output and expanded state - renderer handles width-aware truncation
|
|
1261
|
+
const output = this.#getTextOutput().trimEnd();
|
|
1262
|
+
context.output = output;
|
|
1263
|
+
}
|
|
1264
|
+
context.expanded = this.#expanded;
|
|
1265
|
+
context.previewLines = BASH_DEFAULT_PREVIEW_LINES;
|
|
1266
|
+
context.timeout = normalizeTimeoutSeconds(isRecord(this.#args) ? this.#args.timeout : undefined, 3600);
|
|
1267
|
+
} else if (this.#toolName === "eval" && this.#result) {
|
|
1268
|
+
const output = this.#getTextOutput().trimEnd();
|
|
1269
|
+
context.output = output;
|
|
1270
|
+
context.expanded = this.#expanded;
|
|
1271
|
+
context.previewLines = EVAL_DEFAULT_PREVIEW_LINES;
|
|
1272
|
+
} else if (this.#toolName === "task") {
|
|
1273
|
+
// Once a result snapshot exists the task renderer's `renderResult`
|
|
1274
|
+
// draws every dispatched agent as a progress/result line, so tell
|
|
1275
|
+
// `renderCall` to drop its duplicate streaming preview list.
|
|
1276
|
+
context.hasResult = Boolean(this.#result);
|
|
1277
|
+
this.#taskRenderNowMs = Date.now();
|
|
1278
|
+
context.nowMs = this.#taskRenderNowMs;
|
|
1279
|
+
} else if (isEditLikeToolName(this.#toolName)) {
|
|
1280
|
+
context.editMode = this.#editMode;
|
|
1281
|
+
const previews = this.#editDiffPreview;
|
|
1282
|
+
if (previews && previews.length > 0) {
|
|
1283
|
+
const first = previews[0];
|
|
1284
|
+
if (first?.diff || first?.error) {
|
|
1285
|
+
context.editDiffPreview = first.error
|
|
1286
|
+
? { error: first.error }
|
|
1287
|
+
: { diff: first.diff ?? "", firstChangedLine: first.firstChangedLine };
|
|
1288
|
+
}
|
|
1289
|
+
if (previews.length > 1) {
|
|
1290
|
+
context.perFileDiffPreview = previews;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
if (!previews?.some(preview => preview.diff)) {
|
|
1294
|
+
const editMode = this.#editMode;
|
|
1295
|
+
const fallback = editMode ? renderStreamingFallback(editMode, this.#args, theme) : "";
|
|
1296
|
+
if (fallback) context.editStreamingFallback = fallback;
|
|
1297
|
+
}
|
|
1298
|
+
context.renderDiff = renderDiff;
|
|
1299
|
+
} else if (this.#toolName === "write") {
|
|
1300
|
+
// Device-dispatch previews resolve renderers from the canonical tool map.
|
|
1301
|
+
const writeTool = this.#tool as { session?: { xdev?: XdevMountedState } } | undefined;
|
|
1302
|
+
const xdev = writeTool?.session?.xdev;
|
|
1303
|
+
if (xdev) {
|
|
1304
|
+
context.resolveXdevMounted = (name: string) =>
|
|
1305
|
+
xdev.mountedNames.has(name) ? xdev.tools.get(name) : undefined;
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
return context;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
#getTextOutput(): string {
|
|
1313
|
+
if (!this.#result) return "";
|
|
1314
|
+
|
|
1315
|
+
const textBlocks = this.#result.content.filter(c => c.type === "text");
|
|
1316
|
+
const imageBlocks = this.#getAllImageBlocks();
|
|
1317
|
+
|
|
1318
|
+
let output = textBlocks
|
|
1319
|
+
.map(c => {
|
|
1320
|
+
return sanitizeWithOptionalSixelPassthrough(c.text || "", sanitizeText);
|
|
1321
|
+
})
|
|
1322
|
+
.join("\n");
|
|
1323
|
+
|
|
1324
|
+
if (imageBlocks.length > 0 && (!TERMINAL.imageProtocol || !this.#showImages)) {
|
|
1325
|
+
const imageIndicators = imageBlocks
|
|
1326
|
+
.map(img => {
|
|
1327
|
+
const mimeType = String(img.mimeType);
|
|
1328
|
+
const dims = img.data ? (getImageDimensions(img.data, mimeType) ?? undefined) : undefined;
|
|
1329
|
+
return imageFallback(mimeType, dims);
|
|
1330
|
+
})
|
|
1331
|
+
.join("\n");
|
|
1332
|
+
output = output ? `${output}\n${imageIndicators}` : imageIndicators;
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
return output;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
/**
|
|
1339
|
+
* Format the generic call/result card at `contentWidth`. Shared by the
|
|
1340
|
+
* #contentText fallback and the benign-skip path so both render identically.
|
|
1341
|
+
*/
|
|
1342
|
+
#renderDefaultCard(contentWidth: number): string {
|
|
1343
|
+
return formatDefaultToolExecution(
|
|
1344
|
+
{
|
|
1345
|
+
label: this.#toolLabel,
|
|
1346
|
+
args: this.#args,
|
|
1347
|
+
result: this.#result
|
|
1348
|
+
? { output: this.#getTextOutput(), isError: this.#result.isError, skipped: this.#isBenignSkip() }
|
|
1349
|
+
: undefined,
|
|
1350
|
+
options: this.#renderState,
|
|
1351
|
+
},
|
|
1352
|
+
contentWidth,
|
|
1353
|
+
theme,
|
|
1354
|
+
);
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
/**
|
|
1358
|
+
* True for a steering/peer-interrupt placeholder. A synthetic placeholder
|
|
1359
|
+
* identifies a call that never entered `tool.execute`; an interrupted
|
|
1360
|
+
* placeholder identifies one that started but threw before returning usable
|
|
1361
|
+
* output. Both are normal steering control flow and render neutrally (#7199).
|
|
1362
|
+
*/
|
|
1363
|
+
#isBenignSkip(): boolean {
|
|
1364
|
+
if (this.#isPartial || !this.#result) return false;
|
|
1365
|
+
const details = this.#result.details as
|
|
1366
|
+
| { __synthetic?: boolean; __interrupted?: boolean; source?: string; execution?: string }
|
|
1367
|
+
| undefined;
|
|
1368
|
+
if (details?.source !== "interrupt_skipped") return false;
|
|
1369
|
+
return details.__synthetic === true || (details.__interrupted === true && details.execution === "started");
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
/**
|
|
1373
|
+
* Render a benign skip as the neutral generic card, replacing any bespoke
|
|
1374
|
+
* renderer's error frame. Generic-fallback tools already route through
|
|
1375
|
+
* {@link #renderDefaultCard} (which emits the info card for a skip); they
|
|
1376
|
+
* only need the neutral tint. Bespoke-renderer tools get their content box
|
|
1377
|
+
* swapped for the same neutral card.
|
|
1378
|
+
*/
|
|
1379
|
+
#renderBenignSkipCard(stateBgFn: (text: string) => string): void {
|
|
1380
|
+
if (!this.#usesContentBox) {
|
|
1381
|
+
this.#contentText.setCustomBgFn(stateBgFn);
|
|
1382
|
+
this.#contentText.invalidate();
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1385
|
+
for (const box of this.#multiFileBoxes) {
|
|
1386
|
+
this.removeChild(box);
|
|
1387
|
+
}
|
|
1388
|
+
this.#multiFileBoxes = [];
|
|
1389
|
+
this.#contentBox.setBgFn(undefined);
|
|
1390
|
+
this.#contentBox.clear();
|
|
1391
|
+
this.#contentBox.setPaddingX(1);
|
|
1392
|
+
this.#contentBox.setBgFn(stateBgFn);
|
|
1393
|
+
this.#contentBox.addChild(new WidthAwareText(contentWidth => this.#renderDefaultCard(contentWidth), 0, 0));
|
|
1394
|
+
}
|
|
1395
|
+
}
|