@oh-my-pi/pi-tui 18.2.4 → 18.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/README.md +43 -0
- package/dist/types/app-keybindings.d.ts +395 -0
- package/dist/types/apps/autoresearch-dashboard.d.ts +43 -0
- package/dist/types/apps/autoresearch-data.d.ts +15 -0
- package/dist/types/apps/cleanse-board.d.ts +66 -0
- package/dist/types/apps/cleanse-picker.d.ts +23 -0
- package/dist/types/apps/debug/log-formatting.d.ts +6 -0
- package/dist/types/apps/debug/log-viewer.d.ts +81 -0
- package/dist/types/apps/debug/protocol-probe.d.ts +42 -0
- package/dist/types/apps/debug/raw-sse-buffer.d.ts +58 -0
- package/dist/types/apps/debug/raw-sse.d.ts +26 -0
- package/dist/types/apps/debug/terminal-info.d.ts +34 -0
- package/dist/types/apps/debug/viewer-frame.d.ts +52 -0
- package/dist/types/apps/git/avatar.d.ts +9 -0
- package/dist/types/apps/git/colors.d.ts +49 -0
- package/dist/types/apps/git/diff-pane.d.ts +188 -0
- package/dist/types/apps/git/git-tui.d.ts +66 -0
- package/dist/types/apps/git/sidebar.d.ts +149 -0
- package/dist/types/apps/git/state.d.ts +117 -0
- package/dist/types/apps/if-bench-board.d.ts +75 -0
- package/dist/types/apps/live-visualizer.d.ts +33 -0
- package/dist/types/apps/ps-data.d.ts +51 -0
- package/dist/types/apps/ps-top.d.ts +33 -0
- package/dist/types/apps/session-picker.d.ts +26 -0
- package/dist/types/apps/setup-model-picker.d.ts +13 -0
- package/dist/types/apps/standalone-picker.d.ts +42 -0
- package/dist/types/autocomplete.d.ts +4 -0
- package/dist/types/chat/advisor-message.d.ts +11 -0
- package/dist/types/chat/assistant-message.d.ts +106 -0
- package/dist/types/chat/background-tan-message.d.ts +9 -0
- package/dist/types/chat/bash-execution.d.ts +47 -0
- package/dist/types/chat/cache-invalidation-marker.d.ts +50 -0
- package/dist/types/chat/chat-transcript-builder.d.ts +50 -0
- package/dist/types/chat/collab-prompt-message.d.ts +10 -0
- package/dist/types/chat/compaction-summary-message.d.ts +54 -0
- package/dist/types/chat/custom-message.d.ts +10 -0
- package/dist/types/chat/display-preferences.d.ts +13 -0
- package/dist/types/chat/eval-execution.d.ts +29 -0
- package/dist/types/chat/execution-shared.d.ts +51 -0
- package/dist/types/chat/extension-types.d.ts +28 -0
- package/dist/types/chat/hook-message.d.ts +10 -0
- package/dist/types/chat/image-loading.d.ts +46 -0
- package/dist/types/chat/late-diagnostics-message.d.ts +21 -0
- package/dist/types/chat/messages.d.ts +189 -0
- package/dist/types/chat/reaction.d.ts +30 -0
- package/dist/types/chat/read-target.d.ts +4 -0
- package/dist/types/chat/read-tool-group.d.ts +67 -0
- package/dist/types/chat/served-model-marker.d.ts +47 -0
- package/dist/types/chat/skill-message.d.ts +33 -0
- package/dist/types/chat/skill-title-input.d.ts +14 -0
- package/dist/types/chat/stripped-tool-calls-placeholder.d.ts +12 -0
- package/dist/types/chat/thinking-display.d.ts +12 -0
- package/dist/types/chat/todo-reminder.d.ts +13 -0
- package/dist/types/chat/tool-execution.d.ts +138 -0
- package/dist/types/chat/transcript-browser.d.ts +93 -0
- package/dist/types/chat/transcript-entry.d.ts +45 -0
- package/dist/types/chat/transcript-outline.d.ts +86 -0
- package/dist/types/chat/transcript-render-helpers.d.ts +92 -0
- package/dist/types/chat/ttsr-notification.d.ts +23 -0
- package/dist/types/chat/user-message.d.ts +52 -0
- package/dist/types/chrome/chat-block.d.ts +64 -0
- package/dist/types/chrome/collab-qrcode.d.ts +28 -0
- package/dist/types/chrome/context-thresholds.d.ts +10 -0
- package/dist/types/chrome/countdown-timer.d.ts +14 -0
- package/dist/types/chrome/diff.d.ts +14 -0
- package/dist/types/chrome/dynamic-border.d.ts +16 -0
- package/dist/types/chrome/error-block.d.ts +17 -0
- package/dist/types/chrome/form-theme.d.ts +3 -0
- package/dist/types/chrome/format.d.ts +12 -0
- package/dist/types/chrome/index.d.ts +20 -0
- package/dist/types/chrome/keybinding-hints.d.ts +40 -0
- package/dist/types/chrome/live-board.d.ts +23 -0
- package/dist/types/chrome/local-date.d.ts +4 -0
- package/dist/types/chrome/message-divider.d.ts +22 -0
- package/dist/types/chrome/message-frame.d.ts +52 -0
- package/dist/types/chrome/message-notice.d.ts +35 -0
- package/dist/types/chrome/overlay-box.d.ts +74 -0
- package/dist/types/chrome/qrcode.d.ts +48 -0
- package/dist/types/chrome/segment-track.d.ts +27 -0
- package/dist/types/chrome/select-list-mouse-routing.d.ts +4 -0
- package/dist/types/chrome/selector-helpers.d.ts +52 -0
- package/dist/types/chrome/shared.d.ts +5 -0
- package/dist/types/chrome/streaming-panel.d.ts +19 -0
- package/dist/types/chrome/tool-activity.d.ts +17 -0
- package/dist/types/chrome/transcript-container.d.ts +129 -0
- package/dist/types/chrome/transcript-status.d.ts +12 -0
- package/dist/types/chrome/visual-truncate.d.ts +19 -0
- package/dist/types/components/disclosure.d.ts +42 -0
- package/dist/types/components/form.d.ts +130 -0
- package/dist/types/components/key-value-list.d.ts +24 -0
- package/dist/types/components/layout/geometry.d.ts +73 -0
- package/dist/types/components/layout/row.d.ts +55 -0
- package/dist/types/components/layout/split-pane.d.ts +80 -0
- package/dist/types/components/layout/stack.d.ts +50 -0
- package/dist/types/components/menu-selection.d.ts +71 -0
- package/dist/types/components/metric.d.ts +44 -0
- package/dist/types/components/progress-bar.d.ts +36 -0
- package/dist/types/components/scroll-view.d.ts +53 -19
- package/dist/types/components/scroll-viewport.d.ts +46 -0
- package/dist/types/components/section.d.ts +19 -0
- package/dist/types/components/select-list.d.ts +46 -2
- package/dist/types/components/settings-list.d.ts +20 -0
- package/dist/types/components/table.d.ts +32 -0
- package/dist/types/components/tree-view.d.ts +130 -0
- package/dist/types/components/wizard-step.d.ts +60 -0
- package/dist/types/hotkeys-markdown.d.ts +7 -0
- package/dist/types/index.d.ts +17 -0
- package/dist/types/keybinding-matchers.d.ts +35 -0
- package/dist/types/keys.d.ts +2 -2
- package/dist/types/lang-from-path.d.ts +9 -0
- package/dist/types/overlays/advisor-config.d.ts +114 -0
- package/dist/types/overlays/agent-activity.d.ts +41 -0
- package/dist/types/overlays/agent-hub-projection.d.ts +33 -0
- package/dist/types/overlays/agent-hub-renderer.d.ts +53 -0
- package/dist/types/overlays/agent-hub-types.d.ts +99 -0
- package/dist/types/overlays/agent-hub.d.ts +126 -0
- package/dist/types/overlays/agent-transcript-viewer.d.ts +71 -0
- package/dist/types/overlays/agents-hub.d.ts +72 -0
- package/dist/types/overlays/ask-dialog.d.ts +88 -0
- package/dist/types/overlays/bordered-loader.d.ts +11 -0
- package/dist/types/overlays/btw-history-panel.d.ts +29 -0
- package/dist/types/overlays/btw-history.d.ts +17 -0
- package/dist/types/overlays/btw-panel.d.ts +26 -0
- package/dist/types/overlays/cleanse-panel.d.ts +36 -0
- package/dist/types/overlays/codex-reset-fireworks.d.ts +52 -0
- package/dist/types/overlays/composer-shape-preview.d.ts +47 -0
- package/dist/types/overlays/composer-shape-registry.d.ts +50 -0
- package/dist/types/overlays/copy-selector.d.ts +48 -0
- package/dist/types/overlays/copy-targets.d.ts +63 -0
- package/dist/types/overlays/error-banner.d.ts +13 -0
- package/dist/types/overlays/extensions/display-text.d.ts +10 -0
- package/dist/types/overlays/extensions/extension-dashboard.d.ts +63 -0
- package/dist/types/overlays/extensions/extension-list.d.ts +54 -0
- package/dist/types/overlays/extensions/inspector-model.d.ts +109 -0
- package/dist/types/overlays/extensions/inspector-panel.d.ts +17 -0
- package/dist/types/overlays/extensions/live-tool-session.d.ts +28 -0
- package/dist/types/overlays/extensions/mcp-runtime.d.ts +107 -0
- package/dist/types/overlays/extensions/state-manager.d.ts +42 -0
- package/dist/types/overlays/extensions/types.d.ts +165 -0
- package/dist/types/overlays/history-search.d.ts +16 -0
- package/dist/types/overlays/hook-editor.d.ts +44 -0
- package/dist/types/overlays/hook-input.d.ts +20 -0
- package/dist/types/overlays/hook-selector.d.ts +71 -0
- package/dist/types/overlays/hub-frame.d.ts +77 -0
- package/dist/types/overlays/login-dialog.d.ts +43 -0
- package/dist/types/overlays/logout-account-selector.d.ts +15 -0
- package/dist/types/overlays/mcp-add-wizard.d.ts +87 -0
- package/dist/types/overlays/model-browser.d.ts +213 -0
- package/dist/types/overlays/model-hub.d.ts +72 -0
- package/dist/types/overlays/model-picker.d.ts +74 -0
- package/dist/types/overlays/model-selector.d.ts +52 -0
- package/dist/types/overlays/move-overlay.d.ts +39 -0
- package/dist/types/overlays/oauth-selector.d.ts +41 -0
- package/dist/types/overlays/omfg-panel.d.ts +20 -0
- package/dist/types/overlays/pause-screen.d.ts +43 -0
- package/dist/types/overlays/plan-review-overlay.d.ts +89 -0
- package/dist/types/overlays/plan-save-overlay.d.ts +19 -0
- package/dist/types/overlays/plan-toc.d.ts +41 -0
- package/dist/types/overlays/plugin-selector.d.ts +28 -0
- package/dist/types/overlays/plugin-settings.d.ts +180 -0
- package/dist/types/overlays/queue-mode-selector.d.ts +11 -0
- package/dist/types/overlays/reset-usage-selector.d.ts +23 -0
- package/dist/types/overlays/rewind-selector.d.ts +61 -0
- package/dist/types/overlays/running-subagent-badge.d.ts +6 -0
- package/dist/types/overlays/session-account-selector.d.ts +24 -0
- package/dist/types/overlays/session-info-overlay.d.ts +17 -0
- package/dist/types/overlays/session-observer-registry.d.ts +77 -0
- package/dist/types/overlays/session-selector.d.ts +131 -0
- package/dist/types/overlays/settings-defs.d.ts +119 -0
- package/dist/types/overlays/settings-selector.d.ts +73 -0
- package/dist/types/overlays/show-images-selector.d.ts +11 -0
- package/dist/types/overlays/snapcompact-shape-preview.d.ts +31 -0
- package/dist/types/overlays/theme-selector.d.ts +12 -0
- package/dist/types/overlays/thinking-selector.d.ts +12 -0
- package/dist/types/overlays/tiny-title-download-progress.d.ts +21 -0
- package/dist/types/overlays/tree-selector.d.ts +92 -0
- package/dist/types/overlays/usage-dashboard.d.ts +94 -0
- package/dist/types/overlays/usage-display.d.ts +5 -0
- package/dist/types/overlays/usage-row.d.ts +17 -0
- package/dist/types/prompt/attachment-chips.d.ts +19 -0
- package/dist/types/prompt/composer-attachments.d.ts +71 -0
- package/dist/types/prompt/composer-cache.d.ts +37 -0
- package/dist/types/prompt/composer.d.ts +159 -0
- package/dist/types/prompt/custom-editor.d.ts +261 -0
- package/dist/types/prompt/editor-top-gap.d.ts +18 -0
- package/dist/types/prompt/emoji-autocomplete.d.ts +16 -0
- package/dist/types/prompt/github-ref-autocomplete.d.ts +35 -0
- package/dist/types/prompt/gradient-highlight.d.ts +32 -0
- package/dist/types/prompt/image-format.d.ts +4 -0
- package/dist/types/prompt/image-references.d.ts +26 -0
- package/dist/types/prompt/interactive-context-helpers.d.ts +43 -0
- package/dist/types/prompt/internal-url-autocomplete.d.ts +56 -0
- package/dist/types/prompt/macos-spelling.d.ts +44 -0
- package/dist/types/prompt/magic-keyword-boundary.d.ts +9 -0
- package/dist/types/prompt/magic-keywords.d.ts +26 -0
- package/dist/types/prompt/markdown-prose.d.ts +15 -0
- package/dist/types/prompt/model-mention-autocomplete.d.ts +25 -0
- package/dist/types/prompt/model-mention-syntax.d.ts +24 -0
- package/dist/types/prompt/orchestrate.d.ts +13 -0
- package/dist/types/prompt/prompt-action-autocomplete.d.ts +56 -0
- package/dist/types/prompt/queue-input.d.ts +8 -0
- package/dist/types/prompt/skill-tokens.d.ts +13 -0
- package/dist/types/prompt/tools-markdown.d.ts +12 -0
- package/dist/types/prompt/ultrathink.d.ts +13 -0
- package/dist/types/prompt/usage-amounts.d.ts +26 -0
- package/dist/types/prompt/video.d.ts +18 -0
- package/dist/types/prompt/welcome.d.ts +83 -0
- package/dist/types/prompt/workflow.d.ts +13 -0
- package/dist/types/render/code-cell.d.ts +51 -0
- package/dist/types/render/file-list.d.ts +22 -0
- package/dist/types/render/hyperlink.d.ts +80 -0
- package/dist/types/render/index.d.ts +16 -0
- package/dist/types/render/output-block.d.ts +54 -0
- package/dist/types/render/output-pane.d.ts +80 -0
- package/dist/types/render/render-utils.d.ts +312 -0
- package/dist/types/render/sixel.d.ts +21 -0
- package/dist/types/render/status-line.d.ts +23 -0
- package/dist/types/render/tool-card.d.ts +71 -0
- package/dist/types/render/tree-list.d.ts +29 -0
- package/dist/types/render/types.d.ts +17 -0
- package/dist/types/render/utils.d.ts +43 -0
- package/dist/types/render/width-aware-text.d.ts +23 -0
- package/dist/types/setup/lazy.d.ts +3 -0
- package/dist/types/setup/scenes/composer.d.ts +3 -0
- package/dist/types/setup/scenes/glyph.d.ts +3 -0
- package/dist/types/setup/scenes/model.d.ts +3 -0
- package/dist/types/setup/scenes/outro.d.ts +2 -0
- package/dist/types/setup/scenes/providers.d.ts +3 -0
- package/dist/types/setup/scenes/sign-in.d.ts +21 -0
- package/dist/types/setup/scenes/splash.d.ts +11 -0
- package/dist/types/setup/scenes/theme.d.ts +3 -0
- package/dist/types/setup/scenes/types.d.ts +110 -0
- package/dist/types/setup/scenes/web-search.d.ts +22 -0
- package/dist/types/setup/setup-version.d.ts +11 -0
- package/dist/types/setup/startup-splash.d.ts +9 -0
- package/dist/types/setup/wizard-overlay.d.ts +16 -0
- package/dist/types/setup/wizard.d.ts +25 -0
- package/dist/types/status-line/component.d.ts +182 -0
- package/dist/types/status-line/context-usage.d.ts +156 -0
- package/dist/types/status-line/footer.d.ts +30 -0
- package/dist/types/status-line/git-utils.d.ts +22 -0
- package/dist/types/status-line/host.d.ts +129 -0
- package/dist/types/status-line/index.d.ts +6 -0
- package/dist/types/status-line/loop.d.ts +18 -0
- package/dist/types/status-line/metrics.d.ts +21 -0
- package/dist/types/status-line/presets.d.ts +3 -0
- package/dist/types/status-line/schema.d.ts +15 -0
- package/dist/types/status-line/segments.d.ts +5 -0
- package/dist/types/status-line/separators.d.ts +3 -0
- package/dist/types/status-line/types.d.ts +236 -0
- package/dist/types/stdin-buffer.d.ts +4 -0
- package/dist/types/theme/color.d.ts +24 -0
- package/dist/types/theme/defaults/index.d.ts +9489 -0
- package/dist/types/theme/index.d.ts +4 -0
- package/dist/types/theme/loader.d.ts +23 -0
- package/dist/types/theme/mermaid-cache.d.ts +26 -0
- package/dist/types/theme/schema-validation.d.ts +3 -0
- package/dist/types/theme/schema.d.ts +25 -0
- package/dist/types/theme/session-color.d.ts +43 -0
- package/dist/types/theme/shimmer.d.ts +44 -0
- package/dist/types/theme/symbols.d.ts +34 -0
- package/dist/types/theme/theme-class.d.ts +299 -0
- package/dist/types/theme/theme.d.ts +111 -0
- package/dist/types/theme/tui-adapters.d.ts +30 -0
- package/dist/types/thinking.d.ts +165 -0
- package/dist/types/tools/agent-tree.d.ts +36 -0
- package/dist/types/tools/ask.d.ts +67 -0
- package/dist/types/tools/ast-edit.d.ts +53 -0
- package/dist/types/tools/ast-grep.d.ts +52 -0
- package/dist/types/tools/autoresearch.d.ts +156 -0
- package/dist/types/tools/bash-interactive.d.ts +28 -0
- package/dist/types/tools/bash.d.ts +103 -0
- package/dist/types/tools/conflict-detect.d.ts +43 -0
- package/dist/types/tools/debug.d.ts +60 -0
- package/dist/types/tools/default-renderer.d.ts +24 -0
- package/dist/types/tools/edit.d.ts +158 -0
- package/dist/types/tools/eval-format/code-mode-declarations.d.ts +5 -0
- package/dist/types/tools/eval-format/index.d.ts +5 -0
- package/dist/types/tools/eval-format/javascript.d.ts +2 -0
- package/dist/types/tools/eval-format/python.d.ts +2 -0
- package/dist/types/tools/eval.d.ts +94 -0
- package/dist/types/tools/fetch.d.ts +42 -0
- package/dist/types/tools/gh-format.d.ts +8 -0
- package/dist/types/tools/github.d.ts +144 -0
- package/dist/types/tools/glob.d.ts +44 -0
- package/dist/types/tools/goal.d.ts +42 -0
- package/dist/types/tools/grep.d.ts +64 -0
- package/dist/types/tools/grouped-file-output.d.ts +71 -0
- package/dist/types/tools/hashline-format.d.ts +28 -0
- package/dist/types/tools/hub.d.ts +340 -0
- package/dist/types/tools/index.d.ts +4 -0
- package/dist/types/tools/json-tree.d.ts +59 -0
- package/dist/types/tools/line-ranges.d.ts +29 -0
- package/dist/types/tools/list-limit.d.ts +15 -0
- package/dist/types/tools/lsp.d.ts +72 -0
- package/dist/types/tools/match-line-format.d.ts +11 -0
- package/dist/types/tools/mcp.d.ts +81 -0
- package/dist/types/tools/memory.d.ts +67 -0
- package/dist/types/tools/output-meta.d.ts +151 -0
- package/dist/types/tools/read.d.ts +139 -0
- package/dist/types/tools/renderer.d.ts +98 -0
- package/dist/types/tools/report-tool-issue.d.ts +8 -0
- package/dist/types/tools/resolve.d.ts +47 -0
- package/dist/types/tools/streaming-output.d.ts +282 -0
- package/dist/types/tools/subprocess.d.ts +24 -0
- package/dist/types/tools/task-repair-args.d.ts +53 -0
- package/dist/types/tools/task-yield-assembly.d.ts +20 -0
- package/dist/types/tools/task.d.ts +425 -0
- package/dist/types/tools/terminal-output.d.ts +8 -0
- package/dist/types/tools/think.d.ts +13 -0
- package/dist/types/tools/todo.d.ts +118 -0
- package/dist/types/tools/tool-errors.d.ts +7 -0
- package/dist/types/tools/vibe.d.ts +120 -0
- package/dist/types/tools/web-search.d.ts +195 -0
- package/dist/types/tools/write.d.ts +76 -0
- package/dist/types/tools/xd-url.d.ts +13 -0
- package/dist/types/tools/xdev.d.ts +52 -0
- package/dist/types/tui.d.ts +18 -0
- package/dist/types/utils.d.ts +2 -0
- package/package.json +37 -3
- package/src/app-keybindings.ts +760 -0
- package/src/apps/autoresearch-dashboard.ts +500 -0
- package/src/apps/autoresearch-data.ts +64 -0
- package/src/apps/cleanse-board.ts +341 -0
- package/src/apps/cleanse-picker.ts +57 -0
- package/src/apps/debug/log-formatting.ts +53 -0
- package/src/apps/debug/log-viewer.ts +868 -0
- package/src/apps/debug/protocol-probe.ts +267 -0
- package/src/apps/debug/raw-sse-buffer.ts +449 -0
- package/src/apps/debug/raw-sse.ts +282 -0
- package/src/apps/debug/terminal-info.ts +115 -0
- package/src/apps/debug/viewer-frame.ts +174 -0
- package/src/apps/git/avatar.ts +52 -0
- package/src/apps/git/colors.ts +109 -0
- package/src/apps/git/diff-pane.ts +1512 -0
- package/src/apps/git/git-tui.ts +889 -0
- package/src/apps/git/sidebar.ts +1204 -0
- package/src/apps/git/state.ts +113 -0
- package/src/apps/if-bench-board.ts +324 -0
- package/src/apps/live-visualizer.ts +238 -0
- package/src/apps/ps-data.ts +116 -0
- package/src/apps/ps-top.ts +422 -0
- package/src/apps/session-picker.ts +95 -0
- package/src/apps/setup-model-picker.ts +21 -0
- package/src/apps/standalone-picker.ts +111 -0
- package/src/autocomplete.ts +9 -13
- package/src/chat/advisor-message.ts +175 -0
- package/src/chat/assistant-message.ts +1214 -0
- package/src/chat/background-tan-message.ts +35 -0
- package/src/chat/bash-execution.ts +394 -0
- package/src/chat/cache-invalidation-marker.ts +94 -0
- package/src/chat/chat-transcript-builder.ts +593 -0
- package/src/chat/collab-prompt-message.ts +34 -0
- package/src/chat/compaction-summary-message.ts +261 -0
- package/src/chat/custom-message.ts +23 -0
- package/src/chat/display-preferences.ts +24 -0
- package/src/chat/eval-execution.ts +172 -0
- package/src/chat/execution-shared.ts +113 -0
- package/src/chat/extension-types.ts +46 -0
- package/src/chat/hook-message.ts +20 -0
- package/src/chat/image-loading.ts +122 -0
- package/src/chat/late-diagnostics-message.ts +92 -0
- package/src/chat/messages.ts +305 -0
- package/src/chat/reaction.ts +65 -0
- package/src/chat/read-target.ts +7 -0
- package/src/chat/read-tool-group.ts +873 -0
- package/src/chat/served-model-marker.ts +91 -0
- package/src/chat/skill-message.ts +220 -0
- package/src/chat/skill-title-input.ts +41 -0
- package/src/chat/stripped-tool-calls-placeholder.ts +35 -0
- package/src/chat/thinking-display.ts +312 -0
- package/src/chat/todo-reminder.ts +33 -0
- package/src/chat/tool-execution.ts +1395 -0
- package/src/chat/transcript-browser.ts +268 -0
- package/src/chat/transcript-entry.ts +96 -0
- package/src/chat/transcript-outline.ts +260 -0
- package/src/chat/transcript-render-helpers.ts +315 -0
- package/src/chat/ttsr-notification.ts +123 -0
- package/src/chat/user-message.ts +287 -0
- package/src/chrome/chat-block.ts +110 -0
- package/src/chrome/collab-qrcode.ts +74 -0
- package/src/chrome/context-thresholds.ts +85 -0
- package/src/chrome/countdown-timer.ts +74 -0
- package/src/chrome/diff.ts +266 -0
- package/src/chrome/dynamic-border.ts +36 -0
- package/src/chrome/error-block.ts +63 -0
- package/src/chrome/form-theme.ts +10 -0
- package/src/chrome/format.ts +66 -0
- package/src/chrome/index.ts +21 -0
- package/src/chrome/keybinding-hints.ts +55 -0
- package/src/chrome/live-board.ts +123 -0
- package/src/chrome/local-date.ts +20 -0
- package/src/chrome/message-divider.ts +51 -0
- package/src/chrome/message-frame.ts +164 -0
- package/src/chrome/message-notice.ts +103 -0
- package/src/chrome/overlay-box.ts +301 -0
- package/src/chrome/qrcode.ts +539 -0
- package/src/chrome/segment-track.ts +86 -0
- package/src/chrome/select-list-mouse-routing.ts +11 -0
- package/src/chrome/selector-helpers.ts +119 -0
- package/src/chrome/shared.ts +27 -0
- package/src/chrome/streaming-panel.ts +72 -0
- package/src/chrome/tool-activity.ts +44 -0
- package/src/chrome/transcript-container.ts +902 -0
- package/src/chrome/transcript-status.ts +27 -0
- package/src/chrome/visual-truncate.ts +70 -0
- package/src/components/disclosure.ts +235 -0
- package/src/components/editor.ts +10 -12
- package/src/components/form.ts +566 -0
- package/src/components/input.ts +6 -27
- package/src/components/key-value-list.ts +88 -0
- package/src/components/layout/geometry.ts +202 -0
- package/src/components/layout/row.ts +283 -0
- package/src/components/layout/split-pane.ts +349 -0
- package/src/components/layout/stack.ts +261 -0
- package/src/components/markdown.ts +16 -12
- package/src/components/menu-selection.ts +248 -0
- package/src/components/metric.ts +132 -0
- package/src/components/progress-bar.ts +140 -0
- package/src/components/scroll-view.ts +372 -70
- package/src/components/scroll-viewport.ts +154 -0
- package/src/components/section.ts +77 -0
- package/src/components/select-list.ts +238 -159
- package/src/components/settings-list.ts +103 -80
- package/src/components/table.ts +127 -0
- package/src/components/tree-view.ts +481 -0
- package/src/components/wizard-step.ts +229 -0
- package/src/deccara.ts +1 -3
- package/src/hotkeys-markdown.ts +100 -0
- package/src/index.ts +18 -0
- package/src/keybinding-matchers.ts +99 -0
- package/src/keys.ts +2 -2
- package/src/lang-from-path.ts +252 -0
- package/src/overlays/advisor-config.ts +761 -0
- package/src/overlays/agent-activity.ts +113 -0
- package/src/overlays/agent-hub-projection.ts +250 -0
- package/src/overlays/agent-hub-renderer.ts +281 -0
- package/src/overlays/agent-hub-types.ts +74 -0
- package/src/overlays/agent-hub.ts +1562 -0
- package/src/overlays/agent-transcript-viewer.ts +669 -0
- package/src/overlays/agents-hub.ts +1203 -0
- package/src/overlays/ask-dialog.ts +1202 -0
- package/src/overlays/bordered-loader.ts +41 -0
- package/src/overlays/btw-history-panel.ts +591 -0
- package/src/overlays/btw-history.ts +33 -0
- package/src/overlays/btw-panel.ts +167 -0
- package/src/overlays/cleanse-panel.ts +167 -0
- package/src/overlays/codex-reset-fireworks.ts +375 -0
- package/src/overlays/composer-shape-preview.ts +137 -0
- package/src/overlays/composer-shape-registry.ts +82 -0
- package/src/overlays/copy-selector.ts +618 -0
- package/src/overlays/copy-targets.ts +210 -0
- package/src/overlays/error-banner.ts +39 -0
- package/src/overlays/extensions/display-text.ts +39 -0
- package/src/overlays/extensions/extension-dashboard.ts +645 -0
- package/src/overlays/extensions/extension-list.ts +667 -0
- package/src/overlays/extensions/inspector-model.ts +484 -0
- package/src/overlays/extensions/inspector-panel.ts +669 -0
- package/src/overlays/extensions/live-tool-session.ts +67 -0
- package/src/overlays/extensions/mcp-runtime.ts +289 -0
- package/src/overlays/extensions/state-manager.ts +337 -0
- package/src/overlays/extensions/types.ts +208 -0
- package/src/overlays/history-search.ts +268 -0
- package/src/overlays/hook-editor.ts +272 -0
- package/src/overlays/hook-input.ts +84 -0
- package/src/overlays/hook-selector.ts +635 -0
- package/src/overlays/hub-frame.ts +262 -0
- package/src/overlays/login-dialog.ts +231 -0
- package/src/overlays/logout-account-selector.ts +115 -0
- package/src/overlays/mcp-add-wizard.ts +1436 -0
- package/src/overlays/model-browser.ts +1176 -0
- package/src/overlays/model-hub.ts +2279 -0
- package/src/overlays/model-picker.ts +285 -0
- package/src/overlays/model-selector.ts +107 -0
- package/src/overlays/move-overlay.ts +197 -0
- package/src/overlays/oauth-selector.ts +446 -0
- package/src/overlays/omfg-panel.ts +136 -0
- package/src/overlays/pause-screen.ts +198 -0
- package/src/overlays/plan-review-overlay.ts +1227 -0
- package/src/overlays/plan-save-overlay.ts +74 -0
- package/src/overlays/plan-toc.ts +139 -0
- package/src/overlays/plugin-selector.ts +97 -0
- package/src/overlays/plugin-settings.ts +839 -0
- package/src/overlays/queue-mode-selector.ts +55 -0
- package/src/overlays/reset-usage-selector.ts +170 -0
- package/src/overlays/rewind-selector.ts +499 -0
- package/src/overlays/running-subagent-badge.ts +19 -0
- package/src/overlays/session-account-selector.ts +68 -0
- package/src/overlays/session-info-overlay.ts +96 -0
- package/src/overlays/session-observer-registry.ts +289 -0
- package/src/overlays/session-selector.ts +1137 -0
- package/src/overlays/settings-defs.ts +315 -0
- package/src/overlays/settings-selector.ts +1415 -0
- package/src/overlays/show-images-selector.ts +44 -0
- package/src/overlays/snapcompact-shape-preview-doc.md +14 -0
- package/src/overlays/snapcompact-shape-preview.ts +192 -0
- package/src/overlays/theme-selector.ts +62 -0
- package/src/overlays/thinking-selector.ts +51 -0
- package/src/overlays/tiny-title-download-progress.ts +102 -0
- package/src/overlays/tree-selector.ts +1121 -0
- package/src/overlays/usage-dashboard.ts +629 -0
- package/src/overlays/usage-display.ts +45 -0
- package/src/overlays/usage-row.ts +118 -0
- package/src/prompt/attachment-chips.ts +201 -0
- package/src/prompt/composer-attachments.ts +280 -0
- package/src/prompt/composer-cache.ts +304 -0
- package/src/prompt/composer.ts +934 -0
- package/src/prompt/custom-editor.ts +1392 -0
- package/src/prompt/data/emojis.json +1 -0
- package/src/prompt/editor-top-gap.ts +28 -0
- package/src/prompt/emoji-autocomplete.ts +285 -0
- package/src/prompt/github-ref-autocomplete.ts +75 -0
- package/src/prompt/gradient-highlight.ts +101 -0
- package/src/prompt/image-format.ts +28 -0
- package/src/prompt/image-references.ts +102 -0
- package/src/prompt/interactive-context-helpers.ts +126 -0
- package/src/prompt/internal-url-autocomplete.ts +155 -0
- package/src/prompt/macos-spelling.ts +456 -0
- package/src/prompt/magic-keyword-boundary.ts +23 -0
- package/src/prompt/magic-keywords.ts +42 -0
- package/src/prompt/markdown-prose.ts +245 -0
- package/src/prompt/model-mention-autocomplete.ts +94 -0
- package/src/prompt/model-mention-syntax.ts +41 -0
- package/src/prompt/orchestrate.ts +26 -0
- package/src/prompt/prompt-action-autocomplete.ts +337 -0
- package/src/prompt/queue-input.ts +132 -0
- package/src/prompt/skill-tokens.ts +36 -0
- package/src/prompt/tips.txt +27 -0
- package/src/prompt/tools-markdown.ts +29 -0
- package/src/prompt/ultrathink.ts +26 -0
- package/src/prompt/usage-amounts.ts +99 -0
- package/src/prompt/video.ts +47 -0
- package/src/prompt/welcome.ts +605 -0
- package/src/prompt/workflow.ts +26 -0
- package/src/render/code-cell.ts +244 -0
- package/src/render/file-list.ts +58 -0
- package/src/render/hyperlink.ts +192 -0
- package/src/render/index.ts +17 -0
- package/src/render/output-block.ts +267 -0
- package/src/render/output-pane.ts +299 -0
- package/src/render/render-utils.ts +1182 -0
- package/src/render/sixel.ts +69 -0
- package/src/render/status-line.ts +55 -0
- package/src/render/tool-card.ts +272 -0
- package/src/render/tree-list.ts +188 -0
- package/src/render/types.ts +19 -0
- package/src/render/utils.ts +109 -0
- package/src/render/width-aware-text.ts +60 -0
- package/src/setup/lazy.ts +17 -0
- package/src/setup/scenes/composer.ts +127 -0
- package/src/setup/scenes/glyph.ts +120 -0
- package/src/setup/scenes/model.ts +138 -0
- package/src/setup/scenes/outro.ts +24 -0
- package/src/setup/scenes/providers.ts +107 -0
- package/src/setup/scenes/sign-in.ts +373 -0
- package/src/setup/scenes/splash.ts +192 -0
- package/src/setup/scenes/theme.ts +343 -0
- package/src/setup/scenes/types.ts +111 -0
- package/src/setup/scenes/web-search.ts +165 -0
- package/src/setup/setup-version.ts +11 -0
- package/src/setup/startup-splash.ts +107 -0
- package/src/setup/wizard-overlay.ts +319 -0
- package/src/setup/wizard.ts +101 -0
- package/src/status-line/component.ts +3003 -0
- package/src/status-line/context-usage.ts +688 -0
- package/src/status-line/footer.ts +323 -0
- package/src/status-line/git-utils.ts +42 -0
- package/src/status-line/host.ts +97 -0
- package/src/status-line/index.ts +6 -0
- package/src/status-line/loop.ts +28 -0
- package/src/status-line/metrics.ts +94 -0
- package/src/status-line/presets.ts +107 -0
- package/src/status-line/schema.ts +57 -0
- package/src/status-line/segments.ts +934 -0
- package/src/status-line/separators.ts +55 -0
- package/src/status-line/types.ts +227 -0
- package/src/stdin-buffer.ts +4 -2
- package/src/theme/color.ts +95 -0
- package/src/theme/dark.json +95 -0
- package/src/theme/defaults/alabaster.json +93 -0
- package/src/theme/defaults/amethyst.json +96 -0
- package/src/theme/defaults/anthracite.json +93 -0
- package/src/theme/defaults/basalt.json +91 -0
- package/src/theme/defaults/birch.json +95 -0
- package/src/theme/defaults/dark-abyss.json +91 -0
- package/src/theme/defaults/dark-arctic.json +104 -0
- package/src/theme/defaults/dark-aurora.json +95 -0
- package/src/theme/defaults/dark-catppuccin.json +107 -0
- package/src/theme/defaults/dark-cavern.json +91 -0
- package/src/theme/defaults/dark-celestial.json +106 -0
- package/src/theme/defaults/dark-copper.json +95 -0
- package/src/theme/defaults/dark-cosmos.json +90 -0
- package/src/theme/defaults/dark-cyberpunk.json +102 -0
- package/src/theme/defaults/dark-dracula.json +98 -0
- package/src/theme/defaults/dark-eclipse.json +91 -0
- package/src/theme/defaults/dark-ember.json +95 -0
- package/src/theme/defaults/dark-equinox.json +90 -0
- package/src/theme/defaults/dark-forest.json +96 -0
- package/src/theme/defaults/dark-github.json +105 -0
- package/src/theme/defaults/dark-gruvbox.json +112 -0
- package/src/theme/defaults/dark-lavender.json +95 -0
- package/src/theme/defaults/dark-lunar.json +89 -0
- package/src/theme/defaults/dark-midnight.json +95 -0
- package/src/theme/defaults/dark-monochrome.json +94 -0
- package/src/theme/defaults/dark-monokai.json +98 -0
- package/src/theme/defaults/dark-nebula.json +90 -0
- package/src/theme/defaults/dark-nord.json +98 -0
- package/src/theme/defaults/dark-ocean.json +101 -0
- package/src/theme/defaults/dark-one.json +100 -0
- package/src/theme/defaults/dark-poimandres.json +115 -0
- package/src/theme/defaults/dark-rainforest.json +91 -0
- package/src/theme/defaults/dark-reef.json +91 -0
- package/src/theme/defaults/dark-retro.json +92 -0
- package/src/theme/defaults/dark-rose-pine.json +96 -0
- package/src/theme/defaults/dark-sakura.json +95 -0
- package/src/theme/defaults/dark-slate.json +95 -0
- package/src/theme/defaults/dark-solarized.json +97 -0
- package/src/theme/defaults/dark-solstice.json +90 -0
- package/src/theme/defaults/dark-starfall.json +91 -0
- package/src/theme/defaults/dark-sunset.json +99 -0
- package/src/theme/defaults/dark-swamp.json +90 -0
- package/src/theme/defaults/dark-synthwave.json +103 -0
- package/src/theme/defaults/dark-taiga.json +91 -0
- package/src/theme/defaults/dark-terminal.json +95 -0
- package/src/theme/defaults/dark-tokyo-night.json +101 -0
- package/src/theme/defaults/dark-tundra.json +91 -0
- package/src/theme/defaults/dark-twilight.json +91 -0
- package/src/theme/defaults/dark-volcanic.json +91 -0
- package/src/theme/defaults/graphite.json +92 -0
- package/src/theme/defaults/index.ts +201 -0
- package/src/theme/defaults/light-arctic.json +107 -0
- package/src/theme/defaults/light-aurora-day.json +91 -0
- package/src/theme/defaults/light-canyon.json +91 -0
- package/src/theme/defaults/light-catppuccin.json +106 -0
- package/src/theme/defaults/light-cirrus.json +90 -0
- package/src/theme/defaults/light-coral.json +95 -0
- package/src/theme/defaults/light-cyberpunk.json +96 -0
- package/src/theme/defaults/light-dawn.json +90 -0
- package/src/theme/defaults/light-dunes.json +91 -0
- package/src/theme/defaults/light-eucalyptus.json +95 -0
- package/src/theme/defaults/light-forest.json +100 -0
- package/src/theme/defaults/light-frost.json +95 -0
- package/src/theme/defaults/light-github.json +115 -0
- package/src/theme/defaults/light-glacier.json +91 -0
- package/src/theme/defaults/light-gruvbox.json +108 -0
- package/src/theme/defaults/light-haze.json +90 -0
- package/src/theme/defaults/light-honeycomb.json +95 -0
- package/src/theme/defaults/light-lagoon.json +91 -0
- package/src/theme/defaults/light-lavender.json +95 -0
- package/src/theme/defaults/light-meadow.json +91 -0
- package/src/theme/defaults/light-mint.json +95 -0
- package/src/theme/defaults/light-monochrome.json +101 -0
- package/src/theme/defaults/light-ocean.json +99 -0
- package/src/theme/defaults/light-one.json +99 -0
- package/src/theme/defaults/light-opal.json +91 -0
- package/src/theme/defaults/light-orchard.json +91 -0
- package/src/theme/defaults/light-paper.json +95 -0
- package/src/theme/defaults/light-poimandres.json +115 -0
- package/src/theme/defaults/light-prism.json +90 -0
- package/src/theme/defaults/light-retro.json +98 -0
- package/src/theme/defaults/light-sand.json +95 -0
- package/src/theme/defaults/light-savanna.json +91 -0
- package/src/theme/defaults/light-solarized.json +102 -0
- package/src/theme/defaults/light-soleil.json +90 -0
- package/src/theme/defaults/light-sunset.json +99 -0
- package/src/theme/defaults/light-synthwave.json +98 -0
- package/src/theme/defaults/light-tokyo-night.json +111 -0
- package/src/theme/defaults/light-wetland.json +91 -0
- package/src/theme/defaults/light-zenith.json +89 -0
- package/src/theme/defaults/limestone.json +94 -0
- package/src/theme/defaults/mahogany.json +97 -0
- package/src/theme/defaults/marble.json +93 -0
- package/src/theme/defaults/obsidian.json +91 -0
- package/src/theme/defaults/onyx.json +91 -0
- package/src/theme/defaults/pearl.json +93 -0
- package/src/theme/defaults/porcelain.json +91 -0
- package/src/theme/defaults/quartz.json +96 -0
- package/src/theme/defaults/sandstone.json +95 -0
- package/src/theme/defaults/titanium.json +90 -0
- package/src/theme/index.ts +5 -0
- package/src/theme/light.json +93 -0
- package/src/theme/loader.ts +200 -0
- package/src/theme/mermaid-cache.ts +92 -0
- package/src/theme/schema-validation.ts +119 -0
- package/src/theme/schema.ts +168 -0
- package/src/theme/session-color.ts +253 -0
- package/src/theme/shimmer.ts +307 -0
- package/src/theme/symbols.ts +1433 -0
- package/src/theme/theme-class.ts +787 -0
- package/src/theme/theme-schema.json +463 -0
- package/src/theme/theme.ts +810 -0
- package/src/theme/tui-adapters.ts +339 -0
- package/src/thinking.ts +380 -0
- package/src/tools/agent-tree.ts +128 -0
- package/src/tools/ask.ts +482 -0
- package/src/tools/ast-edit.ts +210 -0
- package/src/tools/ast-grep.ts +171 -0
- package/src/tools/autoresearch.ts +287 -0
- package/src/tools/bash-interactive.ts +295 -0
- package/src/tools/bash.ts +468 -0
- package/src/tools/conflict-detect.ts +77 -0
- package/src/tools/debug.ts +167 -0
- package/src/tools/default-renderer.ts +172 -0
- package/src/tools/edit.ts +1258 -0
- package/src/tools/eval-format/code-mode-declarations.ts +61 -0
- package/src/tools/eval-format/index.ts +16 -0
- package/src/tools/eval-format/javascript.ts +952 -0
- package/src/tools/eval-format/python.ts +544 -0
- package/src/tools/eval.ts +835 -0
- package/src/tools/fetch.ts +226 -0
- package/src/tools/gh-format.ts +18 -0
- package/src/tools/github.ts +686 -0
- package/src/tools/glob.ts +213 -0
- package/src/tools/goal.ts +153 -0
- package/src/tools/grep.ts +385 -0
- package/src/tools/grouped-file-output.ts +211 -0
- package/src/tools/hashline-format.ts +59 -0
- package/src/tools/hub.ts +1488 -0
- package/src/tools/index.ts +71 -0
- package/src/tools/json-tree.ts +388 -0
- package/src/tools/line-ranges.ts +98 -0
- package/src/tools/list-limit.ts +43 -0
- package/src/tools/lsp.ts +702 -0
- package/src/tools/match-line-format.ts +20 -0
- package/src/tools/mcp.ts +309 -0
- package/src/tools/memory.ts +220 -0
- package/src/tools/output-meta.ts +367 -0
- package/src/tools/read.ts +628 -0
- package/src/tools/renderer.ts +107 -0
- package/src/tools/report-tool-issue.ts +13 -0
- package/src/tools/resolve.ts +139 -0
- package/src/tools/streaming-output.ts +1576 -0
- package/src/tools/subprocess.ts +39 -0
- package/src/tools/task-repair-args.ts +118 -0
- package/src/tools/task-yield-assembly.ts +147 -0
- package/src/tools/task.ts +2110 -0
- package/src/tools/terminal-output.ts +153 -0
- package/src/tools/think.ts +28 -0
- package/src/tools/todo.ts +580 -0
- package/src/tools/tool-errors.ts +15 -0
- package/src/tools/vibe.ts +420 -0
- package/src/tools/web-search.ts +409 -0
- package/src/tools/write.ts +511 -0
- package/src/tools/xd-url.ts +28 -0
- package/src/tools/xdev.ts +201 -0
- package/src/tui.ts +57 -4
- package/src/utils.ts +8 -0
|
@@ -0,0 +1,1214 @@
|
|
|
1
|
+
import type { AssistantMessage, ImageContent, TextContent } from "@oh-my-pi/pi-ai";
|
|
2
|
+
import { type Component, Container } from "../tui";
|
|
3
|
+
import { Image, type ImageBudget } from "../components/image";
|
|
4
|
+
import { ImageProtocol, TERMINAL } from "../terminal-capabilities";
|
|
5
|
+
import { Markdown, type MarkdownTheme } from "../components/markdown";
|
|
6
|
+
import { Spacer } from "../components/spacer";
|
|
7
|
+
import { Text } from "../components/text";
|
|
8
|
+
import { formatNumber } from "@oh-my-pi/pi-utils";
|
|
9
|
+
import chalk from "@oh-my-pi/pi-utils/chalk";
|
|
10
|
+
import { LRUCache } from "@oh-my-pi/pi-utils/lru";
|
|
11
|
+
import type { AssistantThinkingRenderer } from "./extension-types";
|
|
12
|
+
import { ensureThemeSync, getMarkdownTheme, theme } from "../theme";
|
|
13
|
+
import { EMPTY_LINK_TARGETS, resolveImageOptions } from "../render/render-utils";
|
|
14
|
+
import { WidthAwareText } from "../render";
|
|
15
|
+
import { convertImageToPng } from "./image-loading";
|
|
16
|
+
import { canonicalizeMessage, formatThinkingForDisplay, hasDisplayableThinking } from "./thinking-display";
|
|
17
|
+
import { resolveAssistantErrorPresentation } from "./transcript-render-helpers";
|
|
18
|
+
import { type CacheInvalidation, CacheInvalidationMarkerComponent } from "./cache-invalidation-marker";
|
|
19
|
+
import { formatErrorBlock } from "../chrome/error-block";
|
|
20
|
+
import { type ServedModelMismatch, ServedModelMarkerComponent } from "./served-model-marker";
|
|
21
|
+
import { isReactionTarget, type ReactionSplit, type ReactionTarget, splitReaction } from "./reaction";
|
|
22
|
+
import { isRowPrefix, type TranscriptStableRow, trimBlankEdges } from "../chrome/transcript-container";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Max wrapped rows of a turn-ending provider error rendered inline in the
|
|
26
|
+
* transcript. Bounds pathological error bodies — e.g. a proxy 502 whose body
|
|
27
|
+
* is a full HTML page — so they can't flood the scrollback, while a long
|
|
28
|
+
* single-line body wraps to the width instead of being cut at a fixed column.
|
|
29
|
+
* Full text is still kept in the persisted session.
|
|
30
|
+
*/
|
|
31
|
+
const MAX_TRANSCRIPT_ERROR_ROWS = 8;
|
|
32
|
+
const EMPTY_STABLE_RENDER: readonly string[] = [];
|
|
33
|
+
|
|
34
|
+
type ThinkingContentBlock = Extract<AssistantMessage["content"][number], { type: "thinking" }>;
|
|
35
|
+
type DisplayThinkingContentBlock = ThinkingContentBlock & { rawThinking?: string };
|
|
36
|
+
type StablePart = { kind: "thinking" | "text"; text: string } | { kind: "spacer" };
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* One published prefix of the block's finished content. Later snapshots extend
|
|
40
|
+
* earlier ones part-wise (only the final part may grow), so rendered stable
|
|
41
|
+
* rows only ever gain a suffix — the append-only transcript contract that lets
|
|
42
|
+
* them retire into native scrollback mid-stream.
|
|
43
|
+
*/
|
|
44
|
+
interface StableSnapshot {
|
|
45
|
+
// Earlier parts are immutable; only the final part needs a historical offset.
|
|
46
|
+
readonly partCount: number;
|
|
47
|
+
readonly lastTextLength: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isSnapshotExtension(previous: readonly StablePart[], current: readonly StablePart[]): boolean {
|
|
51
|
+
if (previous.length > current.length) return false;
|
|
52
|
+
for (let index = 0; index < previous.length; index++) {
|
|
53
|
+
const before = previous[index]!;
|
|
54
|
+
const after = current[index]!;
|
|
55
|
+
if (before.kind !== after.kind) return false;
|
|
56
|
+
if (before.kind === "spacer" || after.kind === "spacer") continue;
|
|
57
|
+
const isLast = index === previous.length - 1;
|
|
58
|
+
if (isLast ? !after.text.startsWith(before.text) : after.text !== before.text) return false;
|
|
59
|
+
}
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function resolveThinkingDisplay(block: ThinkingContentBlock, proseOnly: boolean): { text: string; visible: boolean } {
|
|
64
|
+
const rawThinking = (block as DisplayThinkingContentBlock).rawThinking;
|
|
65
|
+
// When rawThinking is set, `block.thinking` is already the formatted display
|
|
66
|
+
// text that buildDisplayMessage produced (then revealed/sliced by the
|
|
67
|
+
// streaming controller) — re-running the formatter would double-process it,
|
|
68
|
+
// and the growing revealed slice would never hit the per-tick memo. Only
|
|
69
|
+
// format raw (non-display) thinking blocks.
|
|
70
|
+
const formatted = rawThinking !== undefined ? block.thinking : formatThinkingForDisplay(block.thinking, proseOnly);
|
|
71
|
+
return {
|
|
72
|
+
text: formatted.trim(),
|
|
73
|
+
visible: hasDisplayableThinking(rawThinking ?? block.thinking, formatted),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Frames for the streaming "thinking" pulse rendered in place of a hidden
|
|
79
|
+
* thinking block while the model is still producing it. A single fixed-width
|
|
80
|
+
* starburst cycles through facets (✻ ✼ ❉ ❊ ✺ ✹ ✸ ✶) so the indicator animates
|
|
81
|
+
* in place without shifting the line or the trailing speed badge. The dwell per
|
|
82
|
+
* frame eases between {@link THINKING_DOTS_FRAME_MS_MIN} and
|
|
83
|
+
* {@link THINKING_DOTS_FRAME_MS_MAX} across each revolution (see
|
|
84
|
+
* {@link AssistantMessageComponent.thinkingDotsFrameDelay}).
|
|
85
|
+
*/
|
|
86
|
+
const THINKING_DOTS_FRAMES = ["✻", "✼", "❉", "❊", "✺", "✹", "✸", "✶"] as const;
|
|
87
|
+
/**
|
|
88
|
+
* Pulse cadence bounds (ms). Each frame's dwell eases between these on a
|
|
89
|
+
* raised-cosine "breath" — quickest at the cycle start, slowest at its midpoint —
|
|
90
|
+
* so the starburst accelerates and slows instead of ticking at one fixed rate.
|
|
91
|
+
* Mean ≈ 150ms, snappier than the previous flat 320ms.
|
|
92
|
+
*/
|
|
93
|
+
const THINKING_DOTS_FRAME_MS_MIN = 70;
|
|
94
|
+
const THINKING_DOTS_FRAME_MS_MAX = 230;
|
|
95
|
+
|
|
96
|
+
/** Rolling window (ms) over which streaming-rate observations are averaged. */
|
|
97
|
+
const SPEED_WINDOW_MS = 3000;
|
|
98
|
+
/** Color/clamp ceiling: a rate at or above this maps to the full accent color. */
|
|
99
|
+
const SPEED_MAX = 200;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Session-wide streaming-speed gauge. Only one thinking indicator animates at a
|
|
103
|
+
* time, so a single shared instance accumulates instantaneous tok/s observations
|
|
104
|
+
* and reports their windowed average — smoothing the jumpy per-delta numbers.
|
|
105
|
+
* Each thinking block resets the gauge on its first live sample (see
|
|
106
|
+
* {@link AssistantMessageComponent.updateContent}) so the average reflects only
|
|
107
|
+
* the active block, never a previous turn's trailing rate. Components feed it
|
|
108
|
+
* deltas (not cumulative totals), so a fresh turn restarting its token count at
|
|
109
|
+
* zero never produces a spike.
|
|
110
|
+
*/
|
|
111
|
+
class SpeedTracker {
|
|
112
|
+
#observations: Array<{ time: number; rate: number }> = [];
|
|
113
|
+
|
|
114
|
+
#prune(now: number): void {
|
|
115
|
+
const threshold = now - SPEED_WINDOW_MS;
|
|
116
|
+
while (this.#observations.length > 0 && this.#observations[0]!.time < threshold) {
|
|
117
|
+
this.#observations.shift();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Record one instantaneous tok/s reading, clamped to {@link SPEED_MAX} so a
|
|
122
|
+
* single oversized delta (e.g. a buffered reflow tick) can't poison the
|
|
123
|
+
* windowed average. Non-finite/negative rates ignored. */
|
|
124
|
+
observe(rate: number, now = performance.now()): void {
|
|
125
|
+
if (!Number.isFinite(rate) || rate < 0) return;
|
|
126
|
+
this.#observations.push({ time: now, rate: Math.min(rate, SPEED_MAX) });
|
|
127
|
+
this.#prune(now);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Windowed-average tok/s; 0 once observations age out of the window. */
|
|
131
|
+
getSpeed(now = performance.now()): number {
|
|
132
|
+
this.#prune(now);
|
|
133
|
+
if (this.#observations.length === 0) return 0;
|
|
134
|
+
let sum = 0;
|
|
135
|
+
for (const o of this.#observations) sum += o.rate;
|
|
136
|
+
return sum / this.#observations.length;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
reset(): void {
|
|
140
|
+
this.#observations = [];
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** One gauge for the whole session — see {@link SpeedTracker}. */
|
|
145
|
+
const sharedSpeedTracker = new SpeedTracker();
|
|
146
|
+
|
|
147
|
+
/** Test-only: clear the shared gauge so observations don't leak across cases. */
|
|
148
|
+
export function resetThinkingSpeedTracker(): void {
|
|
149
|
+
sharedSpeedTracker.reset();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Linear-interpolate two `#rrggbb` colors in sRGB space. `t` clamps to [0,1]:
|
|
154
|
+
* `t = 0` → `from`, `t = 1` → `to`. Drives the streaming speed badge, fading
|
|
155
|
+
* from a dim gray toward the theme accent as tok/s rises.
|
|
156
|
+
*/
|
|
157
|
+
function lerpHex(from: string, to: string, t: number): string {
|
|
158
|
+
const k = t < 0 ? 0 : t > 1 ? 1 : t;
|
|
159
|
+
const fr = Number.parseInt(from.slice(1, 3), 16);
|
|
160
|
+
const fg = Number.parseInt(from.slice(3, 5), 16);
|
|
161
|
+
const fb = Number.parseInt(from.slice(5, 7), 16);
|
|
162
|
+
const tr = Number.parseInt(to.slice(1, 3), 16);
|
|
163
|
+
const tg = Number.parseInt(to.slice(3, 5), 16);
|
|
164
|
+
const tb = Number.parseInt(to.slice(5, 7), 16);
|
|
165
|
+
const r = Math.round(fr + (tr - fr) * k);
|
|
166
|
+
const g = Math.round(fg + (tg - fg) * k);
|
|
167
|
+
const b = Math.round(fb + (tb - fb) * k);
|
|
168
|
+
return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Renders an assistant message; streaming content remains mutable until the
|
|
173
|
+
* provider finalizes it because later deltas can revise earlier Markdown.
|
|
174
|
+
* The exception is the leading run of visible thinking blocks: raw thinking
|
|
175
|
+
* only ever appends, so its frozen Markdown prefix publishes as append-only
|
|
176
|
+
* stable rows ({@link AppendOnlyTranscriptBlock}) and can retire into native
|
|
177
|
+
* scrollback while the block still streams — a long reasoning trace is no
|
|
178
|
+
* longer clipped to the mutable viewport.
|
|
179
|
+
*/
|
|
180
|
+
export class AssistantMessageComponent extends Container {
|
|
181
|
+
readonly transcriptBlockMode = "appendOnly" as const;
|
|
182
|
+
#contentContainer: Container;
|
|
183
|
+
#markerSlot: Container;
|
|
184
|
+
#cacheMarker?: CacheInvalidationMarkerComponent;
|
|
185
|
+
#servedModelMarker?: ServedModelMarkerComponent;
|
|
186
|
+
#lastMessage?: AssistantMessage;
|
|
187
|
+
#emergencyText?: Markdown;
|
|
188
|
+
#toolImagesByCallId = new Map<string, ImageContent[]>();
|
|
189
|
+
#convertedKittyImages = new Map<string, ImageContent>();
|
|
190
|
+
#showImages = true;
|
|
191
|
+
#showToolResultImages = true;
|
|
192
|
+
#kittyConversionsInFlight = new Set<string>();
|
|
193
|
+
#transcriptBlockFinalized: boolean;
|
|
194
|
+
/** See {@link setMidStreamPublication}; the wire's `stream-revision` axis decides it. */
|
|
195
|
+
#midStreamPublication = true;
|
|
196
|
+
/**
|
|
197
|
+
* When true, the turn-ending `Error: …` line for `stopReason === "error"` is
|
|
198
|
+
* suppressed because the same error is currently shown in the pinned banner
|
|
199
|
+
* above the editor (see `EventController` + `ErrorBannerComponent`). Avoids
|
|
200
|
+
* rendering the identical error twice (inline + banner) at the error moment.
|
|
201
|
+
* Restored to `false` when the banner is cleared at the next turn so the
|
|
202
|
+
* transcript keeps the error in history.
|
|
203
|
+
*/
|
|
204
|
+
#errorPinned = false;
|
|
205
|
+
/**
|
|
206
|
+
* Whether the inline turn-ending error block renders its full body instead of
|
|
207
|
+
* the {@link MAX_TRANSCRIPT_ERROR_ROWS}-capped preview. Toggled by
|
|
208
|
+
* {@link setExpanded} so Ctrl+O (tool-output expansion) reveals a long
|
|
209
|
+
* provider error whose tail would otherwise be unreachable in the live TUI.
|
|
210
|
+
*/
|
|
211
|
+
#errorExpanded = false;
|
|
212
|
+
/**
|
|
213
|
+
* True when the current {@link updateContent} message carries a truncatable
|
|
214
|
+
* inline provider error (the `#appendErrorBlock` path) — set whether or not
|
|
215
|
+
* the inline block was actually drawn, so it stays true even while the error
|
|
216
|
+
* is suppressed under a pinned banner. Gates {@link setExpanded} so toggling
|
|
217
|
+
* expansion only re-renders assistant turns that carry such an error, not
|
|
218
|
+
* every message in the transcript.
|
|
219
|
+
*/
|
|
220
|
+
#hasTruncatableError = false;
|
|
221
|
+
/**
|
|
222
|
+
* Monotonic content version reported to the transcript container via
|
|
223
|
+
* {@link getTranscriptBlockVersion}. Bumped by {@link updateContent} — the
|
|
224
|
+
* choke point every mutator funnels through, including post-finalize changes
|
|
225
|
+
* such as `setErrorPinned(false)` restoring the inline error at the next
|
|
226
|
+
* turn's `agent_start`, late tool-result images, and async Kitty conversions.
|
|
227
|
+
*/
|
|
228
|
+
#blockVersion = 0;
|
|
229
|
+
/** Whether the last updateContent carried an in-flight streaming partial; such
|
|
230
|
+
* renders bypass the markdown module LRU (see Markdown.transientRenderCache). */
|
|
231
|
+
#lastUpdateTransient = false;
|
|
232
|
+
// Fast-path state: reuse Markdown children when message shape is stable during streaming.
|
|
233
|
+
#fastPathKey: string | undefined;
|
|
234
|
+
#fastPathItems:
|
|
235
|
+
| Array<{ md: Markdown; contentIndex: number; blockType: "text" | "thinking"; lastText: string }>
|
|
236
|
+
| undefined;
|
|
237
|
+
/** Live "thinking" pulse shown in place of a hidden thinking block while it
|
|
238
|
+
* streams; undefined when not animating. Driven by {@link #thinkingDotsTimer}. */
|
|
239
|
+
#thinkingDots: Text | undefined;
|
|
240
|
+
#thinkingDotsTimer: NodeJS.Timeout | undefined;
|
|
241
|
+
#thinkingDotsFrame = 0;
|
|
242
|
+
/** Previous cumulative provider token count + timestamp, for deriving this
|
|
243
|
+
* block's instantaneous streaming rate fed into {@link sharedSpeedTracker}.
|
|
244
|
+
* Undefined until the first thinking update of this block. */
|
|
245
|
+
#lastTokenCount: number | undefined;
|
|
246
|
+
#lastTokenTime = 0;
|
|
247
|
+
/** Published width-independent stable prefixes; grows only, never retracts. */
|
|
248
|
+
#stableSnapshots: StableSnapshot[] = [];
|
|
249
|
+
#stableParts: readonly StablePart[] = [];
|
|
250
|
+
#nextStableRowId = 0;
|
|
251
|
+
#transcriptStableRows: TranscriptStableRow[] = [];
|
|
252
|
+
/**
|
|
253
|
+
* Rendered rows per published snapshot index and width. The container asks
|
|
254
|
+
* for several different counts in one frame (emitted, offered end,
|
|
255
|
+
* projected, pressure-loop +1s); a 2-entry LRU thrashes across those and
|
|
256
|
+
* re-renders the whole prefix per miss. Sized generously so the live prefix
|
|
257
|
+
* stays cached; cleared on reset/finalize like before.
|
|
258
|
+
*/
|
|
259
|
+
#stableRenderCache = new LRUCache<string, readonly string[]>({ max: 64 });
|
|
260
|
+
/** Provider-reported tokens in the live thinking block — reasoning tokens when
|
|
261
|
+
* the provider streams them, else total output — shown dimmed beside the
|
|
262
|
+
* speed badge. 0 when no thinking is streaming. */
|
|
263
|
+
#thinkingTokens = 0;
|
|
264
|
+
/** Whether this block has observed a positive provider-token delta — i.e. it is
|
|
265
|
+
* genuinely streaming tokens right now. Gates the numeric speed badge so the
|
|
266
|
+
* session-wide {@link sharedSpeedTracker} can't surface a previous turn's rate
|
|
267
|
+
* on a fresh block that has no live token throughput of its own. */
|
|
268
|
+
#thinkingRateLive = false;
|
|
269
|
+
|
|
270
|
+
#textColorTransform?: (text: string) => string;
|
|
271
|
+
#linkTargets: ReadonlyMap<string, string> = EMPTY_LINK_TARGETS;
|
|
272
|
+
#markdownTheme: MarkdownTheme | undefined;
|
|
273
|
+
/** Block this reply reacts to; undefined when the preceding block takes no reactions. */
|
|
274
|
+
#reactionTarget: ReactionTarget | undefined;
|
|
275
|
+
/** Reaction lifted from the reply's opening emoji, once resolved. */
|
|
276
|
+
#reaction: string | undefined;
|
|
277
|
+
|
|
278
|
+
setTextColorTransform(transform?: (text: string) => string): void {
|
|
279
|
+
this.#textColorTransform = transform;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
#getProseTheme(): MarkdownTheme {
|
|
283
|
+
if (this.#markdownTheme) return this.#markdownTheme;
|
|
284
|
+
const base = getMarkdownTheme();
|
|
285
|
+
const snapshot = this.#linkTargets;
|
|
286
|
+
const markdownTheme = snapshot.size > 0 ? { ...base, resolveLink: (href: string) => snapshot.get(href) } : base;
|
|
287
|
+
this.#markdownTheme = markdownTheme;
|
|
288
|
+
return markdownTheme;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Install resolved destinations for model-authored prose links. A fresh
|
|
293
|
+
* theme object is required whenever the map changes because Markdown's
|
|
294
|
+
* render cache keys themes by object identity.
|
|
295
|
+
*/
|
|
296
|
+
setLinkTargets(targets: ReadonlyMap<string, string>): void {
|
|
297
|
+
if (
|
|
298
|
+
targets === this.#linkTargets ||
|
|
299
|
+
(targets.size === this.#linkTargets.size &&
|
|
300
|
+
[...targets].every(([href, target]) => this.#linkTargets.get(href) === target))
|
|
301
|
+
) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
this.#linkTargets = targets;
|
|
305
|
+
this.#markdownTheme = undefined;
|
|
306
|
+
this.#fastPathKey = undefined;
|
|
307
|
+
this.#fastPathItems = undefined;
|
|
308
|
+
if (this.#lastMessage) {
|
|
309
|
+
this.updateContent(this.#lastMessage, { transient: this.#lastUpdateTransient });
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Choose the block this reply reacts to from the transcript it is about to
|
|
315
|
+
* join: the nearest preceding reaction-capable block (the user's bubble),
|
|
316
|
+
* looking past turn attachments such as file mentions or injected notices
|
|
317
|
+
* but never past an earlier reply — a continuation after tool calls has
|
|
318
|
+
* nothing to react to. Call before adding this component. An
|
|
319
|
+
* already-resolved reaction is re-applied, and a message rendered verbatim
|
|
320
|
+
* for lack of a target is re-rendered with its reaction stripped.
|
|
321
|
+
*/
|
|
322
|
+
pickReactionTarget(transcript: readonly Component[]): void {
|
|
323
|
+
this.#reactionTarget = undefined;
|
|
324
|
+
for (let index = transcript.length - 1; index >= 0; index--) {
|
|
325
|
+
const block = transcript[index]!;
|
|
326
|
+
if (isReactionTarget(block)) {
|
|
327
|
+
this.#reactionTarget = block;
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
if (block instanceof AssistantMessageComponent) break;
|
|
331
|
+
}
|
|
332
|
+
if (this.#reaction !== undefined) this.#reactionTarget?.setReaction(this.#reaction);
|
|
333
|
+
if (this.#lastMessage && this.#openingText(this.#lastMessage)?.split.emoji !== undefined) {
|
|
334
|
+
this.updateContent(this.#lastMessage, { transient: this.#lastUpdateTransient });
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** The reply's first non-empty text block with its reaction split, if any. */
|
|
339
|
+
#openingText(message: AssistantMessage): { index: number; block: TextContent; split: ReactionSplit } | undefined {
|
|
340
|
+
const index = message.content.findIndex(content => content.type === "text" && content.text.length > 0);
|
|
341
|
+
const block = message.content[index];
|
|
342
|
+
if (block?.type !== "text") return undefined;
|
|
343
|
+
return { index, block, split: splitReaction(block.text) };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Display form of `message` with the reaction handled: stripped and
|
|
348
|
+
* forwarded to the target once resolved, withheld entirely while a streaming
|
|
349
|
+
* prefix could still become one, and left verbatim when there is no target.
|
|
350
|
+
*/
|
|
351
|
+
#displayMessage(message: AssistantMessage, transient: boolean): AssistantMessage {
|
|
352
|
+
const opening = this.#openingText(message);
|
|
353
|
+
if (!opening) return message;
|
|
354
|
+
const { index, block, split } = opening;
|
|
355
|
+
let text: string;
|
|
356
|
+
if (split.emoji !== undefined) {
|
|
357
|
+
if (this.#reaction !== split.emoji) {
|
|
358
|
+
this.#reaction = split.emoji;
|
|
359
|
+
this.#reactionTarget?.setReaction(split.emoji);
|
|
360
|
+
}
|
|
361
|
+
if (!this.#reactionTarget) return message;
|
|
362
|
+
text = split.body;
|
|
363
|
+
} else if (split.pending && transient) {
|
|
364
|
+
text = "";
|
|
365
|
+
} else {
|
|
366
|
+
return message;
|
|
367
|
+
}
|
|
368
|
+
const content = message.content.slice();
|
|
369
|
+
content[index] = { ...block, text };
|
|
370
|
+
return { ...message, content };
|
|
371
|
+
}
|
|
372
|
+
#hideThinkingBlock: boolean;
|
|
373
|
+
readonly #onImageUpdate?: () => void;
|
|
374
|
+
readonly #thinkingRenderers: readonly AssistantThinkingRenderer[];
|
|
375
|
+
readonly #imageBudget?: ImageBudget;
|
|
376
|
+
#proseOnlyThinking: boolean;
|
|
377
|
+
|
|
378
|
+
constructor(
|
|
379
|
+
message?: AssistantMessage,
|
|
380
|
+
hideThinkingBlock = false,
|
|
381
|
+
onImageUpdate?: () => void,
|
|
382
|
+
thinkingRenderers: readonly AssistantThinkingRenderer[] = [],
|
|
383
|
+
imageBudget?: ImageBudget,
|
|
384
|
+
proseOnlyThinking = true,
|
|
385
|
+
linkTargets?: ReadonlyMap<string, string>,
|
|
386
|
+
) {
|
|
387
|
+
super();
|
|
388
|
+
this.#hideThinkingBlock = hideThinkingBlock;
|
|
389
|
+
this.#onImageUpdate = onImageUpdate;
|
|
390
|
+
this.#thinkingRenderers = thinkingRenderers;
|
|
391
|
+
this.#imageBudget = imageBudget;
|
|
392
|
+
this.#proseOnlyThinking = proseOnlyThinking;
|
|
393
|
+
|
|
394
|
+
ensureThemeSync();
|
|
395
|
+
this.#transcriptBlockFinalized = message !== undefined;
|
|
396
|
+
if (linkTargets?.size) this.#linkTargets = linkTargets;
|
|
397
|
+
|
|
398
|
+
// Container for text/thinking content.
|
|
399
|
+
this.#contentContainer = new Container();
|
|
400
|
+
this.addChild(this.#contentContainer);
|
|
401
|
+
|
|
402
|
+
// Cache-miss usage arrives only at message end. Keep its divider after
|
|
403
|
+
// streamed content so rows already emitted to native history remain a
|
|
404
|
+
// prefix of this append-only block.
|
|
405
|
+
this.#markerSlot = new Container();
|
|
406
|
+
this.addChild(this.#markerSlot);
|
|
407
|
+
|
|
408
|
+
if (message) {
|
|
409
|
+
this.updateContent(message);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Show or clear the trailing cache-invalidation divider. Set at `message_end`
|
|
415
|
+
* (live) or during rebuild, once the turn's usage is known and compared
|
|
416
|
+
* against the previous turn's cache footprint.
|
|
417
|
+
*/
|
|
418
|
+
setCacheInvalidation(info: CacheInvalidation | undefined): void {
|
|
419
|
+
this.#cacheMarker = info ? new CacheInvalidationMarkerComponent(info) : undefined;
|
|
420
|
+
this.#refreshMarkers();
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Show or clear the trailing served-model divider. Set once the turn's
|
|
425
|
+
* signed thinking block (or router report) has named the model that actually
|
|
426
|
+
* answered, when it differs from the one requested.
|
|
427
|
+
*/
|
|
428
|
+
setServedModelMismatch(info: ServedModelMismatch | undefined): void {
|
|
429
|
+
this.#servedModelMarker = info ? new ServedModelMarkerComponent(info) : undefined;
|
|
430
|
+
this.#refreshMarkers();
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
#refreshMarkers(): void {
|
|
434
|
+
this.#markerSlot.clear();
|
|
435
|
+
if (this.#servedModelMarker) this.#markerSlot.addChild(this.#servedModelMarker);
|
|
436
|
+
if (this.#cacheMarker) this.#markerSlot.addChild(this.#cacheMarker);
|
|
437
|
+
this.#blockVersion++;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
override invalidate(): void {
|
|
441
|
+
super.invalidate();
|
|
442
|
+
// Theme/symbol changes arrive via invalidate(). Fast-path children captured
|
|
443
|
+
// their theme at construction, so drop them and force the teardown path to
|
|
444
|
+
// rebuild with the current theme. Streaming updates call updateContent()
|
|
445
|
+
// directly and keep the fast path.
|
|
446
|
+
this.#markdownTheme = undefined;
|
|
447
|
+
this.#fastPathKey = undefined;
|
|
448
|
+
this.#fastPathItems = undefined;
|
|
449
|
+
if (this.#lastMessage) {
|
|
450
|
+
this.updateContent(this.#lastMessage, { transient: this.#lastUpdateTransient });
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
setHideThinkingBlock(hide: boolean): void {
|
|
455
|
+
this.#hideThinkingBlock = hide;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Allow or withhold retiring finished lines into native scrollback while the
|
|
460
|
+
* turn is still streaming. Wires the `stream-revision` axis marks as able to
|
|
461
|
+
* revise already-streamed text must withhold it: published bytes go to
|
|
462
|
+
* terminal history once and cannot be retracted, so a later revision would
|
|
463
|
+
* leave the reader with a stale copy. Withholding costs only reachability —
|
|
464
|
+
* the block still retires whole when the turn ends.
|
|
465
|
+
*/
|
|
466
|
+
setMidStreamPublication(allowed: boolean): void {
|
|
467
|
+
this.#midStreamPublication = allowed;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
setProseOnlyThinking(proseOnly: boolean): void {
|
|
471
|
+
this.#proseOnlyThinking = proseOnly;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
override dispose(): void {
|
|
475
|
+
this.#stopThinkingAnimation();
|
|
476
|
+
super.dispose();
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Whether to render the animated "thinking" pulse in place of the suppressed
|
|
481
|
+
* reasoning: only while this block is still streaming (not yet finalized — the
|
|
482
|
+
* in-flight message always carries `stopReason: "stop"`, so finalization is the
|
|
483
|
+
* only reliable live signal), thinking is hidden, no tool call has started, and
|
|
484
|
+
* the active tail block is a thinking block (the model is reasoning right now).
|
|
485
|
+
* Once text starts, a tool call streams, or the block is sealed, the pulse ends.
|
|
486
|
+
*/
|
|
487
|
+
#shouldAnimateThinking(message: AssistantMessage): boolean {
|
|
488
|
+
if (!this.#hideThinkingBlock || this.#transcriptBlockFinalized) return false;
|
|
489
|
+
let tail: "text" | "thinking" | undefined;
|
|
490
|
+
for (const content of message.content) {
|
|
491
|
+
if (content.type === "toolCall") return false;
|
|
492
|
+
if (content.type === "text" && canonicalizeMessage(content.text)) tail = "text";
|
|
493
|
+
else if (content.type === "thinking" && canonicalizeMessage(content.thinking)) tail = "thinking";
|
|
494
|
+
}
|
|
495
|
+
return tail === "thinking";
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
#thinkingDotsLabel(): string {
|
|
499
|
+
const glyph = THINKING_DOTS_FRAMES[this.#thinkingDotsFrame % THINKING_DOTS_FRAMES.length] ?? "…";
|
|
500
|
+
const coloredGlyph = theme.fg("thinkingText", glyph);
|
|
501
|
+
const thinkingLabel = theme.fg("muted", " Thinking");
|
|
502
|
+
const rate = Math.min(SPEED_MAX, sharedSpeedTracker.getSpeed());
|
|
503
|
+
// The numeric badge ("<total> · <rate> toks/s") only renders while this block
|
|
504
|
+
// is genuinely streaming provider tokens. A block that has observed no token
|
|
505
|
+
// delta (e.g. a provider that reports usage only at turn end) or whose rate
|
|
506
|
+
// has decayed to zero (a streaming lull) drops it entirely — the persistent
|
|
507
|
+
// text label keeps the pulse descriptive for terminals and screen readers.
|
|
508
|
+
// The liveness flag also stops the session-wide gauge from leaking a previous
|
|
509
|
+
// turn's rate onto a fresh token-less block.
|
|
510
|
+
if (!this.#thinkingRateLive || rate < 0.05) return coloredGlyph + thinkingLabel;
|
|
511
|
+
// Total provider tokens, dimmed, sit next to the pulse.
|
|
512
|
+
const totalSpan = this.#thinkingTokens > 0 ? theme.fg("dim", ` · ${formatNumber(this.#thinkingTokens)}`) : "";
|
|
513
|
+
// Speed badge color: dim gray at rest, brightening toward the theme accent as
|
|
514
|
+
// streaming speed climbs (gray → bright accent). Ease (sqrt) so typical
|
|
515
|
+
// mid-stream rates already read as clearly accent-tinted instead of staying
|
|
516
|
+
// gray until the rarely-hit SPEED_MAX ceiling.
|
|
517
|
+
const ratio = Math.sqrt(rate / SPEED_MAX);
|
|
518
|
+
const hex = lerpHex(theme.getColorHex("dim"), theme.getAccentColorHex(), ratio);
|
|
519
|
+
const rateText = ` · ${rate.toFixed(1)} toks/s`;
|
|
520
|
+
const rateSpan = theme.getColorMode() === "truecolor" ? chalk.hex(hex)(rateText) : theme.fg("muted", rateText);
|
|
521
|
+
return coloredGlyph + thinkingLabel + totalSpan + rateSpan;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
#startThinkingAnimation(): void {
|
|
525
|
+
if (this.#thinkingDotsTimer) return;
|
|
526
|
+
this.#scheduleThinkingFrame();
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/** Eased dwell (ms) for the current pulse frame: a raised cosine over the
|
|
530
|
+
* 8-frame cycle, continuous across the wrap, so the rotation breathes rather
|
|
531
|
+
* than advancing at a fixed interval. */
|
|
532
|
+
#thinkingDotsFrameDelay(): number {
|
|
533
|
+
const phase = (1 - Math.cos((2 * Math.PI * this.#thinkingDotsFrame) / THINKING_DOTS_FRAMES.length)) / 2;
|
|
534
|
+
return THINKING_DOTS_FRAME_MS_MIN + (THINKING_DOTS_FRAME_MS_MAX - THINKING_DOTS_FRAME_MS_MIN) * phase;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/** Self-rescheduling timeout (not a fixed interval) so each frame can pick its
|
|
538
|
+
* own eased dwell. */
|
|
539
|
+
#scheduleThinkingFrame(): void {
|
|
540
|
+
this.#thinkingDotsTimer = setTimeout(() => this.#advanceThinkingDots(), this.#thinkingDotsFrameDelay());
|
|
541
|
+
this.#thinkingDotsTimer.unref?.();
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
#advanceThinkingDots(): void {
|
|
545
|
+
this.#thinkingDotsTimer = undefined;
|
|
546
|
+
if (!this.#thinkingDots) {
|
|
547
|
+
this.#stopThinkingAnimation();
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
this.#thinkingDotsFrame = (this.#thinkingDotsFrame + 1) % THINKING_DOTS_FRAMES.length;
|
|
551
|
+
if (this.#thinkingDots.setText(this.#thinkingDotsLabel())) {
|
|
552
|
+
this.#onImageUpdate?.();
|
|
553
|
+
}
|
|
554
|
+
this.#scheduleThinkingFrame();
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
#stopThinkingAnimation(): void {
|
|
558
|
+
if (this.#thinkingDotsTimer) {
|
|
559
|
+
clearTimeout(this.#thinkingDotsTimer);
|
|
560
|
+
this.#thinkingDotsTimer = undefined;
|
|
561
|
+
}
|
|
562
|
+
this.#thinkingDotsFrame = 0;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Toggle suppression of the inline `Error: …` line while the same error is
|
|
567
|
+
* pinned in the banner above the editor. Re-renders so the change is visible.
|
|
568
|
+
*/
|
|
569
|
+
setErrorPinned(pinned: boolean): void {
|
|
570
|
+
if (this.#errorPinned === pinned) return;
|
|
571
|
+
this.#errorPinned = pinned;
|
|
572
|
+
if (this.#lastMessage) {
|
|
573
|
+
this.updateContent(this.#lastMessage, { transient: this.#lastUpdateTransient });
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Expand or collapse the inline turn-ending error block so Ctrl+O
|
|
579
|
+
* (tool-output expansion) can reveal a long provider error's hidden tail.
|
|
580
|
+
* Only re-renders when the current message carries a truncatable error, so
|
|
581
|
+
* toggling expansion across the transcript skips ordinary turns. Works even
|
|
582
|
+
* while the error is pinned in the banner: the inline block is drawn (in full)
|
|
583
|
+
* when expanded so the complete body is reachable without sending a message.
|
|
584
|
+
*/
|
|
585
|
+
setExpanded(expanded: boolean): void {
|
|
586
|
+
if (this.#errorExpanded === expanded) return;
|
|
587
|
+
this.#errorExpanded = expanded;
|
|
588
|
+
if (this.#hasTruncatableError && this.#lastMessage) {
|
|
589
|
+
this.updateContent(this.#lastMessage, { transient: this.#lastUpdateTransient });
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
isTranscriptBlockFinalized(): boolean {
|
|
594
|
+
return this.#transcriptBlockFinalized;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
override render(width: number): readonly string[] {
|
|
598
|
+
const rows = super.render(width);
|
|
599
|
+
this.#publishStableSnapshot(rows, width);
|
|
600
|
+
return rows;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/** Width-independent stable identities for the streamed leading thinking run. */
|
|
604
|
+
getTranscriptStableRows(): readonly TranscriptStableRow[] {
|
|
605
|
+
return this.#transcriptStableRows;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Drop every published thinking stable row. Called by the transcript
|
|
610
|
+
* container during a visibility-driven destructive replay so the head no
|
|
611
|
+
* longer re-emits reasoning captured while thinking was visible. Safe only
|
|
612
|
+
* because the paired display reset clears the scrollback those rows occupied
|
|
613
|
+
* — see {@link AppendOnlyTranscriptBlock.resetTranscriptStableRows}.
|
|
614
|
+
*/
|
|
615
|
+
resetTranscriptStableRows(): void {
|
|
616
|
+
this.#stableSnapshots = [];
|
|
617
|
+
this.#stableParts = [];
|
|
618
|
+
this.#transcriptStableRows = [];
|
|
619
|
+
this.#stableRenderCache.clear();
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
renderTranscriptStableRows(count: number, width: number): readonly string[] {
|
|
623
|
+
const index = Math.min(Math.trunc(count), this.#stableSnapshots.length);
|
|
624
|
+
if (index <= 0) return EMPTY_STABLE_RENDER;
|
|
625
|
+
const key = `${index}:${width}`;
|
|
626
|
+
const cached = this.#stableRenderCache.get(key);
|
|
627
|
+
if (cached) return cached;
|
|
628
|
+
const snapshot = this.#stableSnapshots[index - 1]!;
|
|
629
|
+
const parts = this.#stableParts.slice(0, snapshot.partCount);
|
|
630
|
+
const last = parts.at(-1);
|
|
631
|
+
if (last && last.kind !== "spacer") {
|
|
632
|
+
parts[parts.length - 1] = { kind: last.kind, text: last.text.slice(0, snapshot.lastTextLength) };
|
|
633
|
+
}
|
|
634
|
+
const rows = this.#renderStableSnapshot(parts, width);
|
|
635
|
+
this.#stableRenderCache.set(key, rows);
|
|
636
|
+
return rows;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Publish the block's finished prefix as stable transcript rows so a long
|
|
641
|
+
* stream can retire into native scrollback mid-turn instead of being clipped
|
|
642
|
+
* to the live viewport until the turn ends. Finished means bytes that can no
|
|
643
|
+
* longer change: closed child blocks, plus the streaming child's frozen
|
|
644
|
+
* Markdown prefix. Published bytes may already sit in terminal history, so
|
|
645
|
+
* every guard skips publication and nothing ever retracts it.
|
|
646
|
+
*/
|
|
647
|
+
#publishStableSnapshot(rendered: readonly string[], width: number): void {
|
|
648
|
+
if (!this.#midStreamPublication) return;
|
|
649
|
+
const parts = this.#currentStableSnapshot();
|
|
650
|
+
if (!parts) return;
|
|
651
|
+
const last = parts.at(-1);
|
|
652
|
+
if (!last || last.kind === "spacer") return;
|
|
653
|
+
const snapshot = { partCount: parts.length, lastTextLength: last.text.length };
|
|
654
|
+
const previous = this.#stableSnapshots.at(-1);
|
|
655
|
+
if (previous && !isSnapshotExtension(this.#stableParts, parts)) return;
|
|
656
|
+
if (previous?.partCount === snapshot.partCount && previous.lastTextLength === snapshot.lastTextLength) return;
|
|
657
|
+
const currentRows = this.#renderStableSnapshot(parts, width);
|
|
658
|
+
// The container verifies stable rows against the blank-trimmed render.
|
|
659
|
+
if (!isRowPrefix(currentRows, trimBlankEdges(rendered))) return;
|
|
660
|
+
const previousRows = previous
|
|
661
|
+
? this.renderTranscriptStableRows(this.#stableSnapshots.length, width)
|
|
662
|
+
: EMPTY_STABLE_RENDER;
|
|
663
|
+
if (!isRowPrefix(previousRows, currentRows)) return;
|
|
664
|
+
// Each stable row must add at least one physical row at every width.
|
|
665
|
+
if (currentRows.length === previousRows.length) return;
|
|
666
|
+
this.#stableParts = parts;
|
|
667
|
+
this.#stableSnapshots.push(snapshot);
|
|
668
|
+
this.#transcriptStableRows.push({ key: `thinking:${this.#nextStableRowId++}` });
|
|
669
|
+
this.#stableRenderCache.set(`${this.#stableSnapshots.length}:${width}`, currentRows);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Width-independent parts eligible for publication right now: the leading
|
|
674
|
+
* run of finished blocks, ending inside the streaming block at Markdown's
|
|
675
|
+
* frozen boundary. Undefined whenever any prefix byte could still change
|
|
676
|
+
* (finalized or non-transient renders, marker rows, extension components,
|
|
677
|
+
* hidden thinking, or no frozen prefix yet).
|
|
678
|
+
*/
|
|
679
|
+
#currentStableSnapshot(): readonly StablePart[] | undefined {
|
|
680
|
+
if (this.#transcriptBlockFinalized || !this.#lastUpdateTransient) return undefined;
|
|
681
|
+
if (this.#markerSlot.children.length > 0) return undefined;
|
|
682
|
+
const items = this.#fastPathItems;
|
|
683
|
+
if (!items || items.length === 0) return undefined;
|
|
684
|
+
const parts: StablePart[] = [];
|
|
685
|
+
let itemIndex = 0;
|
|
686
|
+
for (const child of this.#contentContainer.children) {
|
|
687
|
+
const item = items[itemIndex];
|
|
688
|
+
if (item?.md === child) {
|
|
689
|
+
if (itemIndex === items.length - 1) {
|
|
690
|
+
// Streaming child: publish Markdown's frozen prefix, and only
|
|
691
|
+
// once non-blank content exists past it — the block's last
|
|
692
|
+
// non-blank line is still being written (thinking's prose fold
|
|
693
|
+
// may rewrite it) and must stay out of published bytes.
|
|
694
|
+
const raw = item.md.getLastRenderStableText();
|
|
695
|
+
const frozen = raw.trim();
|
|
696
|
+
if (frozen.length > 0 && /\S/.test(item.lastText.slice(raw.length))) {
|
|
697
|
+
parts.push({ kind: item.blockType, text: frozen });
|
|
698
|
+
}
|
|
699
|
+
break;
|
|
700
|
+
}
|
|
701
|
+
// Closed child: only the streaming tail may mutate in place, so
|
|
702
|
+
// everything before it is final.
|
|
703
|
+
parts.push({ kind: item.blockType, text: item.lastText });
|
|
704
|
+
itemIndex++;
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
if (child instanceof Spacer) {
|
|
708
|
+
parts.push({ kind: "spacer" });
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
711
|
+
// Unknown child (thinking extension, pulse, image, error row): stop.
|
|
712
|
+
break;
|
|
713
|
+
}
|
|
714
|
+
while (parts.at(-1)?.kind === "spacer") parts.pop();
|
|
715
|
+
if (parts.length === 0) return undefined;
|
|
716
|
+
return parts;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
#renderStableSnapshot(parts: readonly StablePart[], width: number): readonly string[] {
|
|
720
|
+
const rows: string[] = [];
|
|
721
|
+
for (const part of parts) {
|
|
722
|
+
if (part.kind === "spacer") {
|
|
723
|
+
rows.push("");
|
|
724
|
+
continue;
|
|
725
|
+
}
|
|
726
|
+
// Constructor args mirror the live child Markdown exactly so these
|
|
727
|
+
// rows are byte-identical to the block render's prefix — including the
|
|
728
|
+
// trim the live children apply, which drops the trailing blank line a
|
|
729
|
+
// frozen prefix still carries.
|
|
730
|
+
const text = part.text.trim();
|
|
731
|
+
const markdown =
|
|
732
|
+
part.kind === "text"
|
|
733
|
+
? new Markdown(
|
|
734
|
+
text,
|
|
735
|
+
1,
|
|
736
|
+
0,
|
|
737
|
+
this.#getProseTheme(),
|
|
738
|
+
this.#textColorTransform ? { color: this.#textColorTransform } : undefined,
|
|
739
|
+
0,
|
|
740
|
+
)
|
|
741
|
+
: new Markdown(text, 1, 0, getMarkdownTheme(), {
|
|
742
|
+
color: (value: string) => theme.fg("thinkingText", value),
|
|
743
|
+
italic: true,
|
|
744
|
+
});
|
|
745
|
+
rows.push(...markdown.render(width));
|
|
746
|
+
}
|
|
747
|
+
return rows;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/** Render completed prose rather than an earlier thinking row under emergency viewport pressure. */
|
|
751
|
+
renderTranscriptBlockEmergencyRow(width: number): string | undefined {
|
|
752
|
+
if (!this.#transcriptBlockFinalized) return undefined;
|
|
753
|
+
return this.#emergencyText?.render(width)[0];
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
getTranscriptBlockVersion(): number {
|
|
757
|
+
return this.#blockVersion;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
markTranscriptBlockFinalized(): void {
|
|
761
|
+
this.#transcriptBlockFinalized = true;
|
|
762
|
+
this.#stableRenderCache.clear();
|
|
763
|
+
this.#stopThinkingAnimation();
|
|
764
|
+
// If the live pulse was on screen when the block sealed, drop the fast path
|
|
765
|
+
// and rebuild so the placeholder is removed — finalized blocks never animate.
|
|
766
|
+
if (this.#thinkingDots) {
|
|
767
|
+
this.#fastPathKey = undefined;
|
|
768
|
+
this.#fastPathItems = undefined;
|
|
769
|
+
if (this.#lastMessage) this.updateContent(this.#lastMessage, { transient: this.#lastUpdateTransient });
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
applyRetryRecovery(retryRecovery: AssistantMessage["retryRecovery"]): void {
|
|
774
|
+
if (!this.#lastMessage || !retryRecovery) return;
|
|
775
|
+
this.setErrorPinned(false);
|
|
776
|
+
this.updateContent({ ...this.#lastMessage, retryRecovery });
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
messagePersistenceKey(): string | undefined {
|
|
780
|
+
if (!this.#lastMessage) return undefined;
|
|
781
|
+
return [
|
|
782
|
+
"assistant",
|
|
783
|
+
this.#lastMessage.timestamp,
|
|
784
|
+
this.#lastMessage.provider,
|
|
785
|
+
this.#lastMessage.model,
|
|
786
|
+
this.#lastMessage.responseId ?? "",
|
|
787
|
+
this.#lastMessage.stopReason,
|
|
788
|
+
].join(":");
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* Render a turn-ending provider error inline, wrapped to the render width.
|
|
793
|
+
* Collapsed (default), the block keeps {@link MAX_TRANSCRIPT_ERROR_ROWS}
|
|
794
|
+
* wrapped rows and ends with a dim `ctrl+o`/expand hint when rows were cut,
|
|
795
|
+
* so a pathological body — e.g. the HTML page a proxy returns on a 502 —
|
|
796
|
+
* can't flood the transcript. Expanded (via {@link setExpanded}), every row
|
|
797
|
+
* is rendered so the complete message is reachable. Mirrors
|
|
798
|
+
* {@link ErrorBannerComponent}. The caller owns the separating Spacer.
|
|
799
|
+
*/
|
|
800
|
+
#appendErrorBlock(message: string): void {
|
|
801
|
+
const maxRows = this.#errorExpanded ? Number.POSITIVE_INFINITY : MAX_TRANSCRIPT_ERROR_ROWS;
|
|
802
|
+
this.#contentContainer.addChild(
|
|
803
|
+
new WidthAwareText(
|
|
804
|
+
contentWidth =>
|
|
805
|
+
formatErrorBlock(message, contentWidth, maxRows, (line, index) =>
|
|
806
|
+
theme.fg("error", index === 0 ? `Error: ${line}` : line),
|
|
807
|
+
),
|
|
808
|
+
1,
|
|
809
|
+
0,
|
|
810
|
+
),
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/** Toggle rendering for assistant-native and tool-result images. */
|
|
815
|
+
setImagesVisible(visible: boolean): void {
|
|
816
|
+
if (this.#showImages === visible) return;
|
|
817
|
+
this.#showImages = visible;
|
|
818
|
+
if (this.#lastMessage) {
|
|
819
|
+
this.updateContent(this.#lastMessage, { transient: this.#lastUpdateTransient });
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/** Toggle only images produced by tool results; assistant-native images remain governed by setImagesVisible. */
|
|
824
|
+
setToolResultImagesVisible(visible: boolean): void {
|
|
825
|
+
if (this.#showToolResultImages === visible) return;
|
|
826
|
+
this.#showToolResultImages = visible;
|
|
827
|
+
if (this.#lastMessage) {
|
|
828
|
+
this.updateContent(this.#lastMessage, { transient: this.#lastUpdateTransient });
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
setToolResultImages(toolCallId: string, images: ImageContent[]): void {
|
|
833
|
+
if (!toolCallId) return;
|
|
834
|
+
const validImages = images.filter(img => img.type === "image" && img.data && img.mimeType);
|
|
835
|
+
for (const key of Array.from(this.#convertedKittyImages.keys())) {
|
|
836
|
+
if (key.startsWith(`${toolCallId}:`)) {
|
|
837
|
+
this.#convertedKittyImages.delete(key);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
for (const key of Array.from(this.#kittyConversionsInFlight)) {
|
|
841
|
+
if (key.startsWith(`${toolCallId}:`)) {
|
|
842
|
+
this.#kittyConversionsInFlight.delete(key);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
if (validImages.length === 0) {
|
|
846
|
+
this.#toolImagesByCallId.delete(toolCallId);
|
|
847
|
+
} else {
|
|
848
|
+
this.#toolImagesByCallId.set(toolCallId, validImages);
|
|
849
|
+
this.#convertImagesForKitty(validImages.map((image, index) => ({ image, key: `${toolCallId}:${index}` })));
|
|
850
|
+
}
|
|
851
|
+
if (this.#lastMessage) {
|
|
852
|
+
this.updateContent(this.#lastMessage, { transient: this.#lastUpdateTransient });
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
#convertImagesForKitty(entries: Array<{ image: ImageContent; key: string }>): void {
|
|
857
|
+
if (TERMINAL.imageProtocol !== ImageProtocol.Kitty) return;
|
|
858
|
+
for (const { image, key } of entries) {
|
|
859
|
+
if (image.mimeType === "image/png") continue;
|
|
860
|
+
if (this.#convertedKittyImages.has(key) || this.#kittyConversionsInFlight.has(key)) continue;
|
|
861
|
+
this.#kittyConversionsInFlight.add(key);
|
|
862
|
+
convertImageToPng(image)
|
|
863
|
+
.then(converted => {
|
|
864
|
+
this.#kittyConversionsInFlight.delete(key);
|
|
865
|
+
this.#convertedKittyImages.set(key, converted);
|
|
866
|
+
if (this.#lastMessage) {
|
|
867
|
+
this.updateContent(this.#lastMessage, { transient: this.#lastUpdateTransient });
|
|
868
|
+
}
|
|
869
|
+
this.#onImageUpdate?.();
|
|
870
|
+
})
|
|
871
|
+
.catch(() => {
|
|
872
|
+
this.#kittyConversionsInFlight.delete(key);
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
#renderImageEntries(entries: Array<{ image: ImageContent; key: string }>, withLeadingSpacer: boolean): void {
|
|
878
|
+
if (!this.#showImages || entries.length === 0) return;
|
|
879
|
+
this.#convertImagesForKitty(entries);
|
|
880
|
+
|
|
881
|
+
if (withLeadingSpacer) this.#contentContainer.addChild(new Spacer(1));
|
|
882
|
+
for (const { image, key } of entries) {
|
|
883
|
+
const displayImage =
|
|
884
|
+
TERMINAL.imageProtocol === ImageProtocol.Kitty && image.mimeType !== "image/png"
|
|
885
|
+
? this.#convertedKittyImages.get(key)
|
|
886
|
+
: image;
|
|
887
|
+
if (TERMINAL.imageProtocol && displayImage) {
|
|
888
|
+
this.#contentContainer.addChild(
|
|
889
|
+
new Image(
|
|
890
|
+
displayImage.data,
|
|
891
|
+
displayImage.mimeType,
|
|
892
|
+
{ fallbackColor: (text: string) => theme.fg("toolOutput", text) },
|
|
893
|
+
{ ...resolveImageOptions(), budget: this.#imageBudget, imageKey: key },
|
|
894
|
+
),
|
|
895
|
+
);
|
|
896
|
+
continue;
|
|
897
|
+
}
|
|
898
|
+
this.#contentContainer.addChild(new Text(theme.fg("toolOutput", `[Image: ${image.mimeType}]`), 1, 0));
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
#renderToolImages(): void {
|
|
903
|
+
if (!this.#showToolResultImages) return;
|
|
904
|
+
const entries = Array.from(this.#toolImagesByCallId.entries()).flatMap(([toolCallId, images]) =>
|
|
905
|
+
images.map((image, index) => ({ image, key: `${toolCallId}:${index}` })),
|
|
906
|
+
);
|
|
907
|
+
this.#renderImageEntries(entries, true);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
#appendThinkingExtensions(contentIndex: number, thinkingIndex: number, text: string): void {
|
|
911
|
+
for (const renderer of this.#thinkingRenderers) {
|
|
912
|
+
try {
|
|
913
|
+
const component = renderer(
|
|
914
|
+
{
|
|
915
|
+
contentIndex,
|
|
916
|
+
thinkingIndex,
|
|
917
|
+
text,
|
|
918
|
+
requestRender: () => this.#onImageUpdate?.(),
|
|
919
|
+
},
|
|
920
|
+
theme,
|
|
921
|
+
);
|
|
922
|
+
if (component) {
|
|
923
|
+
this.#contentContainer.addChild(component);
|
|
924
|
+
}
|
|
925
|
+
} catch {
|
|
926
|
+
// Ignore extension renderer failures and keep the original thinking block visible.
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
#computeShapeKey(message: AssistantMessage): string {
|
|
932
|
+
const parts: string[] = [`htb:${this.#hideThinkingBlock ? 1 : 0}|pot:${this.#proseOnlyThinking ? 1 : 0}`];
|
|
933
|
+
for (const content of message.content) {
|
|
934
|
+
if (content.type === "text") {
|
|
935
|
+
parts.push(canonicalizeMessage(content.text) ? "T1" : "T0");
|
|
936
|
+
} else if (content.type === "thinking") {
|
|
937
|
+
if (this.#hideThinkingBlock) {
|
|
938
|
+
// Match the pulse's empty/nonempty transition without formatting hidden text.
|
|
939
|
+
parts.push(canonicalizeMessage(content.thinking) ? "KH" : "K0");
|
|
940
|
+
} else {
|
|
941
|
+
parts.push(resolveThinkingDisplay(content, this.#proseOnlyThinking).visible ? "KV" : "K0");
|
|
942
|
+
}
|
|
943
|
+
} else {
|
|
944
|
+
// Non-rendered blocks (toolCall, redactedThinking, …) still occupy a
|
|
945
|
+
// content index. Encode their position so an inserted/removed one shifts
|
|
946
|
+
// the key and forces the teardown path instead of mis-indexing children.
|
|
947
|
+
parts.push(`O:${content.type}`);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
return parts.join("|");
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
#canFastPath(message: AssistantMessage): boolean {
|
|
954
|
+
for (const content of message.content) {
|
|
955
|
+
if (content.type === "toolCall" || content.type === "image") return false;
|
|
956
|
+
}
|
|
957
|
+
if (this.#toolImagesByCallId.size > 0) return false;
|
|
958
|
+
const errorPresentation = resolveAssistantErrorPresentation(message);
|
|
959
|
+
if (errorPresentation.kind === "compact-recovered") return false;
|
|
960
|
+
if (
|
|
961
|
+
errorPresentation.kind === "full" &&
|
|
962
|
+
!(message.stopReason === "error" && this.#errorPinned && !this.#errorExpanded)
|
|
963
|
+
) {
|
|
964
|
+
return false;
|
|
965
|
+
}
|
|
966
|
+
// Extension stability: if thinking renderers exist and any tracked thinking
|
|
967
|
+
// block's text changed, extensions may produce a different child count.
|
|
968
|
+
if (!this.#hideThinkingBlock && this.#thinkingRenderers.length > 0 && this.#fastPathItems) {
|
|
969
|
+
for (const item of this.#fastPathItems) {
|
|
970
|
+
if (item.blockType === "thinking") {
|
|
971
|
+
const content = message.content[item.contentIndex];
|
|
972
|
+
if (content?.type === "thinking") {
|
|
973
|
+
const display = resolveThinkingDisplay(content, this.#proseOnlyThinking);
|
|
974
|
+
if (display.text !== item.lastText) return false;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
return true;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
#tryFastPathUpdate(message: AssistantMessage, opts?: { transient?: boolean }): boolean {
|
|
983
|
+
if (!this.#fastPathKey || !this.#fastPathItems) return false;
|
|
984
|
+
if (!this.#canFastPath(message)) {
|
|
985
|
+
this.#fastPathKey = undefined;
|
|
986
|
+
this.#fastPathItems = undefined;
|
|
987
|
+
return false;
|
|
988
|
+
}
|
|
989
|
+
if (this.#computeShapeKey(message) !== this.#fastPathKey) {
|
|
990
|
+
this.#fastPathKey = undefined;
|
|
991
|
+
this.#fastPathItems = undefined;
|
|
992
|
+
return false;
|
|
993
|
+
}
|
|
994
|
+
const transient = opts?.transient === true;
|
|
995
|
+
// Shape is identical — setText only on Markdown children whose source changed.
|
|
996
|
+
this.#applyItemTransience(transient);
|
|
997
|
+
for (let i = 0; i < this.#fastPathItems.length; i++) {
|
|
998
|
+
const item = this.#fastPathItems[i]!;
|
|
999
|
+
const content = message.content[item.contentIndex];
|
|
1000
|
+
if (!content) {
|
|
1001
|
+
this.#fastPathKey = undefined;
|
|
1002
|
+
this.#fastPathItems = undefined;
|
|
1003
|
+
return false;
|
|
1004
|
+
}
|
|
1005
|
+
let newText: string;
|
|
1006
|
+
if (item.blockType === "text" && content.type === "text") {
|
|
1007
|
+
newText = content.text.trim();
|
|
1008
|
+
} else if (item.blockType === "thinking" && content.type === "thinking") {
|
|
1009
|
+
newText = resolveThinkingDisplay(content, this.#proseOnlyThinking).text;
|
|
1010
|
+
} else {
|
|
1011
|
+
this.#fastPathKey = undefined;
|
|
1012
|
+
this.#fastPathItems = undefined;
|
|
1013
|
+
return false;
|
|
1014
|
+
}
|
|
1015
|
+
if (newText !== item.lastText) {
|
|
1016
|
+
// Only the last (actively streaming) block may mutate in place: a
|
|
1017
|
+
// delta into an earlier block would invalidate rows the settled
|
|
1018
|
+
// walk already declared final, so tear down and rebuild instead.
|
|
1019
|
+
if (i < this.#fastPathItems.length - 1) {
|
|
1020
|
+
this.#fastPathKey = undefined;
|
|
1021
|
+
this.#fastPathItems = undefined;
|
|
1022
|
+
return false;
|
|
1023
|
+
}
|
|
1024
|
+
item.md.setText(newText);
|
|
1025
|
+
item.lastText = newText;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
if (this.#thinkingDots) {
|
|
1029
|
+
if (this.#thinkingDots.setText(this.#thinkingDotsLabel())) {
|
|
1030
|
+
this.#onImageUpdate?.();
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
return true;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
updateContent(message: AssistantMessage, opts?: { transient?: boolean }): void {
|
|
1037
|
+
this.#blockVersion++;
|
|
1038
|
+
this.#lastMessage = message;
|
|
1039
|
+
this.#lastUpdateTransient = opts?.transient === true;
|
|
1040
|
+
// Everything below renders the display form; #lastMessage keeps the
|
|
1041
|
+
// verbatim message so re-renders re-derive the reaction deterministically.
|
|
1042
|
+
message = this.#displayMessage(message, this.#lastUpdateTransient);
|
|
1043
|
+
|
|
1044
|
+
// Streaming-speed gauge: only a live, in-flight render of the single
|
|
1045
|
+
// animating hidden-thinking block feeds the shared session tracker. The
|
|
1046
|
+
// token count is the provider's own cumulative output — reasoning tokens when
|
|
1047
|
+
// reported (Gemini's thoughtsTokenCount, OpenAI's reasoning_tokens), else
|
|
1048
|
+
// total output tokens — never a character estimate, which undercounts when
|
|
1049
|
+
// the provider streams a summarized reasoning trace. An instantaneous tok/s
|
|
1050
|
+
// is derived from this block's delta and handed to the windowed averager.
|
|
1051
|
+
// Only transient renders count: the final non-transient render at
|
|
1052
|
+
// message_end carries the turn's end-of-stream usage, whose jump would spike
|
|
1053
|
+
// the gauge and pollute the next block. Providers that report usage only at
|
|
1054
|
+
// turn end leave the live count flat, so the rate stays 0 and the badge
|
|
1055
|
+
// self-suppresses (see #thinkingDotsLabel).
|
|
1056
|
+
const isThinkingNow = this.#lastUpdateTransient && this.#shouldAnimateThinking(message);
|
|
1057
|
+
if (isThinkingNow) {
|
|
1058
|
+
const currentTokens = message.usage.reasoningTokens ?? message.usage.output;
|
|
1059
|
+
this.#thinkingTokens = currentTokens;
|
|
1060
|
+
const now = performance.now();
|
|
1061
|
+
if (this.#lastTokenCount !== undefined) {
|
|
1062
|
+
const tokenDelta = currentTokens - this.#lastTokenCount;
|
|
1063
|
+
const elapsedMs = now - this.#lastTokenTime;
|
|
1064
|
+
if (tokenDelta > 0 && elapsedMs > 0) {
|
|
1065
|
+
// First live sample of this block: drop the session gauge's prior-turn
|
|
1066
|
+
// observations so the windowed average reflects only this block.
|
|
1067
|
+
if (!this.#thinkingRateLive) sharedSpeedTracker.reset();
|
|
1068
|
+
sharedSpeedTracker.observe((tokenDelta / elapsedMs) * 1000, now);
|
|
1069
|
+
this.#thinkingRateLive = true;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
this.#lastTokenCount = currentTokens;
|
|
1073
|
+
this.#lastTokenTime = now;
|
|
1074
|
+
} else {
|
|
1075
|
+
this.#lastTokenCount = undefined;
|
|
1076
|
+
this.#thinkingTokens = 0;
|
|
1077
|
+
this.#thinkingRateLive = false;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// Fast path: reuse Markdown children when shape is stable during streaming
|
|
1081
|
+
if (this.#tryFastPathUpdate(message, opts)) return;
|
|
1082
|
+
|
|
1083
|
+
// Clear content container
|
|
1084
|
+
this.#contentContainer.clear();
|
|
1085
|
+
this.#emergencyText = undefined;
|
|
1086
|
+
this.#thinkingDots = undefined;
|
|
1087
|
+
this.#hasTruncatableError = false;
|
|
1088
|
+
|
|
1089
|
+
// Determine if we should capture Markdown instances for next fast path
|
|
1090
|
+
const shouldCapture = this.#canFastPath(message);
|
|
1091
|
+
const captureItems:
|
|
1092
|
+
| Array<{ md: Markdown; contentIndex: number; blockType: "text" | "thinking"; lastText: string }>
|
|
1093
|
+
| undefined = shouldCapture ? [] : undefined;
|
|
1094
|
+
|
|
1095
|
+
const hasVisibleContent = message.content.some(
|
|
1096
|
+
c =>
|
|
1097
|
+
(c.type === "text" && canonicalizeMessage(c.text)) ||
|
|
1098
|
+
(c.type === "image" && c.data && c.mimeType) ||
|
|
1099
|
+
(!this.#hideThinkingBlock &&
|
|
1100
|
+
c.type === "thinking" &&
|
|
1101
|
+
resolveThinkingDisplay(c, this.#proseOnlyThinking).visible),
|
|
1102
|
+
);
|
|
1103
|
+
|
|
1104
|
+
// Render content in order
|
|
1105
|
+
let thinkingIndex = 0;
|
|
1106
|
+
let hasRenderedContent = false;
|
|
1107
|
+
for (let i = 0; i < message.content.length; i++) {
|
|
1108
|
+
const content = message.content[i];
|
|
1109
|
+
if (content.type === "text" && canonicalizeMessage(content.text)) {
|
|
1110
|
+
// Set paddingY=0 to avoid extra spacing before tool executions
|
|
1111
|
+
const trimmed = content.text.trim();
|
|
1112
|
+
const mdOptions = this.#textColorTransform ? { color: this.#textColorTransform } : undefined;
|
|
1113
|
+
const md = new Markdown(trimmed, 1, 0, this.#getProseTheme(), mdOptions, 0);
|
|
1114
|
+
this.#contentContainer.addChild(md);
|
|
1115
|
+
this.#emergencyText = md;
|
|
1116
|
+
captureItems?.push({ md, contentIndex: i, blockType: "text", lastText: trimmed });
|
|
1117
|
+
hasRenderedContent = true;
|
|
1118
|
+
} else if (content.type === "thinking") {
|
|
1119
|
+
if (this.#hideThinkingBlock) {
|
|
1120
|
+
thinkingIndex += 1;
|
|
1121
|
+
continue;
|
|
1122
|
+
}
|
|
1123
|
+
const display = resolveThinkingDisplay(content, this.#proseOnlyThinking);
|
|
1124
|
+
if (!display.visible) continue;
|
|
1125
|
+
const thinkingText = display.text;
|
|
1126
|
+
// Add spacing only when another visible assistant content block follows.
|
|
1127
|
+
// This avoids a superfluous blank line before separately-rendered tool execution blocks.
|
|
1128
|
+
const hasVisibleContentAfter = message.content
|
|
1129
|
+
.slice(i + 1)
|
|
1130
|
+
.some(
|
|
1131
|
+
c =>
|
|
1132
|
+
(c.type === "text" && canonicalizeMessage(c.text)) ||
|
|
1133
|
+
(c.type === "image" && c.data && c.mimeType) ||
|
|
1134
|
+
(c.type === "thinking" && resolveThinkingDisplay(c, this.#proseOnlyThinking).visible),
|
|
1135
|
+
);
|
|
1136
|
+
|
|
1137
|
+
// Thinking traces in thinkingText color, italic
|
|
1138
|
+
const md = new Markdown(thinkingText, 1, 0, getMarkdownTheme(), {
|
|
1139
|
+
color: (text: string) => theme.fg("thinkingText", text),
|
|
1140
|
+
italic: true,
|
|
1141
|
+
});
|
|
1142
|
+
md.transientRenderCache = this.#lastUpdateTransient;
|
|
1143
|
+
this.#contentContainer.addChild(md);
|
|
1144
|
+
captureItems?.push({ md, contentIndex: i, blockType: "thinking", lastText: thinkingText });
|
|
1145
|
+
this.#appendThinkingExtensions(i, thinkingIndex, thinkingText);
|
|
1146
|
+
hasRenderedContent = true;
|
|
1147
|
+
thinkingIndex += 1;
|
|
1148
|
+
if (hasVisibleContentAfter) {
|
|
1149
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
1150
|
+
}
|
|
1151
|
+
} else if (content.type === "image" && content.data && content.mimeType) {
|
|
1152
|
+
this.#renderImageEntries([{ image: content, key: `native:${i}` }], hasRenderedContent);
|
|
1153
|
+
hasRenderedContent ||= this.#showImages;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
if (this.#shouldAnimateThinking(message)) {
|
|
1158
|
+
if (hasVisibleContent) this.#contentContainer.addChild(new Spacer(1));
|
|
1159
|
+
this.#thinkingDots = new Text(this.#thinkingDotsLabel(), 1, 0);
|
|
1160
|
+
this.#contentContainer.addChild(this.#thinkingDots);
|
|
1161
|
+
this.#startThinkingAnimation();
|
|
1162
|
+
} else {
|
|
1163
|
+
this.#stopThinkingAnimation();
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
this.#renderToolImages();
|
|
1167
|
+
const errorPresentation = resolveAssistantErrorPresentation(message);
|
|
1168
|
+
const hasToolCalls = message.content.some(c => c.type === "toolCall");
|
|
1169
|
+
if (errorPresentation.kind === "compact-recovered") {
|
|
1170
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
1171
|
+
this.#contentContainer.addChild(new Text(theme.fg("dim", errorPresentation.text), 1, 0));
|
|
1172
|
+
} else if (!hasToolCalls && errorPresentation.kind === "full") {
|
|
1173
|
+
if (message.stopReason === "aborted") {
|
|
1174
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
1175
|
+
this.#contentContainer.addChild(new Text(theme.fg("error", errorPresentation.text), 1, 0));
|
|
1176
|
+
} else {
|
|
1177
|
+
// Non-aborted provider error: a truncatable inline block. Mark it so
|
|
1178
|
+
// setExpanded re-renders even while the same error is pinned above.
|
|
1179
|
+
this.#hasTruncatableError = true;
|
|
1180
|
+
// Suppress the inline block only while pinned AND collapsed — the
|
|
1181
|
+
// banner already shows the capped error there. When expanded, draw
|
|
1182
|
+
// the inline block in full so the complete body is reachable without
|
|
1183
|
+
// sending a message; the pinned banner stays a short reminder.
|
|
1184
|
+
if (!(message.stopReason === "error" && this.#errorPinned) || this.#errorExpanded) {
|
|
1185
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
1186
|
+
this.#appendErrorBlock(errorPresentation.text);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
// Store fast-path state for next call
|
|
1191
|
+
if (shouldCapture) {
|
|
1192
|
+
this.#fastPathItems = captureItems;
|
|
1193
|
+
this.#fastPathKey = this.#computeShapeKey(message);
|
|
1194
|
+
this.#applyItemTransience(this.#lastUpdateTransient);
|
|
1195
|
+
} else {
|
|
1196
|
+
this.#fastPathKey = undefined;
|
|
1197
|
+
this.#fastPathItems = undefined;
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
/**
|
|
1202
|
+
* Only the actively streaming (last) markdown renders in transient mode;
|
|
1203
|
+
* completed blocks render final — syntax-highlighted, module-LRU-cached,
|
|
1204
|
+
* byte-stable — so their rows can settle into native scrollback mid-turn
|
|
1205
|
+
* and are byte-identical to the finalize render.
|
|
1206
|
+
*/
|
|
1207
|
+
#applyItemTransience(transient: boolean): void {
|
|
1208
|
+
const items = this.#fastPathItems;
|
|
1209
|
+
if (!items) return;
|
|
1210
|
+
for (let i = 0; i < items.length; i++) {
|
|
1211
|
+
items[i]!.md.transientRenderCache = transient && i === items.length - 1;
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
}
|