@linxiraos/pi-tui 1.1.27 → 1.2.0
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 +47 -463
- package/README.md +7 -5
- package/dist/types/app-keybindings.d.ts +1 -1
- package/dist/types/apps/cleanse-board.d.ts +1 -1
- package/dist/types/apps/debug/log-viewer.d.ts +4 -0
- package/dist/types/apps/debug/raw-sse-buffer.d.ts +2 -0
- package/dist/types/apps/git/git-tui.d.ts +2 -2
- package/dist/types/apps/git/help.d.ts +3 -1
- package/dist/types/apps/if-bench-board.d.ts +2 -2
- package/dist/types/apps/setup-model-picker.d.ts +1 -1
- package/dist/types/autocomplete.d.ts +5 -2
- package/dist/types/charts/chart-plan.d.ts +674 -0
- package/dist/types/charts/chart-svg.d.ts +59 -0
- package/dist/types/charts/table-data.d.ts +175 -0
- package/dist/types/chat/assistant-message.d.ts +5 -0
- package/dist/types/chat/chat-transcript-builder.d.ts +2 -0
- package/dist/types/chat/compaction-summary-message.d.ts +1 -0
- package/dist/types/chat/execution-shared.d.ts +6 -5
- package/dist/types/chat/fence-figure.d.ts +42 -0
- package/dist/types/chat/figure-markdown.d.ts +42 -0
- package/dist/types/chat/messages.d.ts +1 -1
- package/dist/types/chat/svg-figure.d.ts +33 -0
- package/dist/types/chat/svg-source.d.ts +49 -0
- package/dist/types/chat/table-chart.d.ts +89 -0
- package/dist/types/chat/tool-execution.d.ts +2 -0
- package/dist/types/chat/user-message.d.ts +15 -2
- package/dist/types/chrome/shared.d.ts +9 -0
- package/dist/types/chrome/transcript-container.d.ts +6 -0
- package/dist/types/components/box.d.ts +1 -0
- package/dist/types/components/disclosure.d.ts +1 -0
- package/dist/types/components/editor.d.ts +15 -4
- package/dist/types/components/form.d.ts +1 -0
- package/dist/types/components/image.d.ts +12 -44
- package/dist/types/components/key-value-list.d.ts +1 -0
- package/dist/types/components/layout/row.d.ts +1 -0
- package/dist/types/components/layout/stack.d.ts +1 -0
- package/dist/types/components/markdown.d.ts +36 -3
- package/dist/types/components/scroll-view.d.ts +1 -0
- package/dist/types/components/section.d.ts +1 -0
- package/dist/types/components/table.d.ts +1 -0
- package/dist/types/components/text.d.ts +1 -0
- package/dist/types/components/tree-view.d.ts +7 -0
- package/dist/types/components/truncated-text.d.ts +1 -0
- package/dist/types/fuzzy.d.ts +29 -1
- package/dist/types/key-hint-format.d.ts +1 -1
- package/dist/types/keybinding-matchers.d.ts +3 -0
- package/dist/types/keybindings.d.ts +1 -1
- package/dist/types/kitty-graphics.d.ts +2 -2
- package/dist/types/loop-watchdog.d.ts +8 -7
- package/dist/types/multiplexers/cmux.d.ts +16 -0
- package/dist/types/multiplexers/herdr.d.ts +26 -0
- package/dist/types/multiplexers/notify.d.ts +13 -0
- package/dist/types/multiplexers/orca.d.ts +12 -0
- package/dist/types/multiplexers/screen.d.ts +9 -0
- package/dist/types/multiplexers/tmux.d.ts +29 -0
- package/dist/types/multiplexers/types.d.ts +100 -0
- package/dist/types/multiplexers/wmux.d.ts +12 -0
- package/dist/types/multiplexers/zellij.d.ts +12 -0
- package/dist/types/native/backend.d.ts +3 -2
- package/dist/types/native/blobs.d.ts +7 -3
- package/dist/types/native/encode.d.ts +3 -4
- package/dist/types/native/overlay.d.ts +5 -0
- package/dist/types/overlays/agent-hub-projection.d.ts +6 -5
- package/dist/types/overlays/agent-hub-types.d.ts +9 -0
- package/dist/types/overlays/agent-hub.d.ts +2 -0
- package/dist/types/overlays/agent-transcript-viewer.d.ts +11 -2
- package/dist/types/overlays/agents-hub.d.ts +9 -0
- package/dist/types/overlays/annotation-overlay.d.ts +19 -2
- package/dist/types/overlays/annotation-types.d.ts +2 -0
- package/dist/types/overlays/ask-dialog.d.ts +5 -2
- package/dist/types/overlays/history-search.d.ts +10 -0
- package/dist/types/overlays/hook-selector.d.ts +9 -4
- package/dist/types/overlays/jobs-panel.d.ts +1 -7
- package/dist/types/overlays/model-browser.d.ts +54 -7
- package/dist/types/overlays/model-hub.d.ts +23 -0
- package/dist/types/overlays/oauth-selector.d.ts +0 -1
- package/dist/types/overlays/plan-review-overlay.d.ts +3 -2
- package/dist/types/overlays/session-observer-registry.d.ts +2 -0
- package/dist/types/overlays/session-selector.d.ts +6 -2
- package/dist/types/overlays/settings-defs.d.ts +1 -0
- package/dist/types/overlays/usage-dashboard.d.ts +8 -4
- package/dist/types/overlays/usage-display.d.ts +3 -1
- package/dist/types/prompt/composer.d.ts +1 -1
- package/dist/types/prompt/custom-editor.d.ts +11 -8
- package/dist/types/prompt/image-source.d.ts +2 -1
- package/dist/types/prompt/interactive-context-helpers.d.ts +4 -1
- package/dist/types/prompt/model-mention-autocomplete.d.ts +1 -1
- package/dist/types/prompt/welcome.d.ts +6 -1
- package/dist/types/render/composite.d.ts +2 -0
- package/dist/types/render/output-pane.d.ts +1 -0
- package/dist/types/render/render-utils.d.ts +1 -0
- package/dist/types/render/tool-card.d.ts +6 -0
- package/dist/types/render/width-aware-text.d.ts +2 -0
- package/dist/types/setup/scenes/types.d.ts +1 -0
- package/dist/types/status-line/component.d.ts +3 -0
- package/dist/types/status-line/host.d.ts +4 -1
- package/dist/types/status-line/segments.d.ts +2 -0
- package/dist/types/status-line/types.d.ts +14 -3
- package/dist/types/terminal-capabilities.d.ts +27 -35
- package/dist/types/terminal-multiplexer.d.ts +127 -14
- package/dist/types/terminal.d.ts +22 -6
- package/dist/types/theme/active-symbols.d.ts +1 -1
- package/dist/types/theme/schema.d.ts +11 -2
- package/dist/types/theme/symbols.d.ts +1 -1
- package/dist/types/theme/theme-class.d.ts +7 -0
- package/dist/types/theme/theme.d.ts +1 -1
- package/dist/types/theme/tui-adapters.d.ts +3 -0
- package/dist/types/tmux.d.ts +11 -4
- package/dist/types/tools/daemon.d.ts +1 -1
- package/dist/types/tools/eval.d.ts +22 -3
- package/dist/types/tools/glob.d.ts +4 -0
- package/dist/types/tools/native-view.d.ts +47 -0
- package/dist/types/tools/output-meta.d.ts +11 -1
- package/dist/types/tools/renderer.d.ts +37 -2
- package/dist/types/tools/streaming-output.d.ts +20 -1
- package/dist/types/tools/task.d.ts +0 -4
- package/dist/types/tools/write.d.ts +9 -2
- package/dist/types/tools/yield.d.ts +22 -0
- package/dist/types/tui.d.ts +31 -0
- package/dist/types/utils.d.ts +6 -0
- package/dist/types/vim.d.ts +19 -5
- package/package.json +13 -9
- package/src/app-keybindings.d.ts +1 -1
- package/src/app-keybindings.ts +1 -1
- package/src/apps/autoresearch-dashboard.ts +7 -3
- package/src/apps/cleanse-board.ts +2 -2
- package/src/apps/cleanse-picker.ts +1 -1
- package/src/apps/debug/log-viewer.ts +188 -72
- package/src/apps/debug/raw-sse-buffer.ts +8 -0
- package/src/apps/debug/raw-sse.ts +39 -5
- package/src/apps/git/diff-pane.ts +26 -5
- package/src/apps/git/git-tui.ts +2 -2
- package/src/apps/git/help.ts +29 -20
- package/src/apps/if-bench-board.ts +2 -2
- package/src/apps/ps-top.ts +2 -2
- package/src/apps/setup-model-picker.ts +1 -1
- package/src/autocomplete.d.ts +5 -2
- package/src/autocomplete.ts +67 -43
- package/src/charts/chart-plan.d.ts +674 -0
- package/src/charts/chart-plan.ts +5081 -0
- package/src/charts/chart-svg.d.ts +59 -0
- package/src/charts/chart-svg.ts +4657 -0
- package/src/charts/table-data.d.ts +175 -0
- package/src/charts/table-data.ts +1478 -0
- package/src/chat/advisor-message.ts +11 -0
- package/src/chat/assistant-message.d.ts +5 -0
- package/src/chat/assistant-message.ts +219 -50
- package/src/chat/chat-transcript-builder.d.ts +2 -0
- package/src/chat/chat-transcript-builder.ts +5 -3
- package/src/chat/compaction-summary-message.d.ts +1 -0
- package/src/chat/compaction-summary-message.ts +8 -0
- package/src/chat/execution-shared.d.ts +6 -5
- package/src/chat/execution-shared.ts +42 -25
- package/src/chat/fence-figure.d.ts +42 -0
- package/src/chat/fence-figure.ts +80 -0
- package/src/chat/figure-markdown.d.ts +42 -0
- package/src/chat/figure-markdown.ts +174 -0
- package/src/chat/messages.d.ts +1 -1
- package/src/chat/messages.ts +1 -1
- package/src/chat/read-tool-group.ts +6 -4
- package/src/chat/skill-message.ts +6 -0
- package/src/chat/svg-figure.d.ts +33 -0
- package/src/chat/svg-figure.ts +399 -0
- package/src/chat/svg-source.d.ts +49 -0
- package/src/chat/svg-source.ts +188 -0
- package/src/chat/table-chart.d.ts +89 -0
- package/src/chat/table-chart.ts +294 -0
- package/src/chat/tool-execution.d.ts +2 -0
- package/src/chat/tool-execution.ts +80 -6
- package/src/chat/user-message.d.ts +15 -2
- package/src/chat/user-message.ts +55 -3
- package/src/chrome/shared.ts +53 -0
- package/src/chrome/transcript-container.d.ts +6 -0
- package/src/chrome/transcript-container.ts +93 -13
- package/src/components/box.d.ts +1 -0
- package/src/components/box.ts +5 -0
- package/src/components/disclosure.d.ts +1 -0
- package/src/components/disclosure.ts +8 -0
- package/src/components/editor.d.ts +15 -4
- package/src/components/editor.ts +294 -46
- package/src/components/form.d.ts +1 -0
- package/src/components/form.ts +8 -0
- package/src/components/image.d.ts +12 -44
- package/src/components/image.ts +47 -151
- package/src/components/key-value-list.d.ts +1 -0
- package/src/components/key-value-list.ts +4 -0
- package/src/components/layout/row.d.ts +1 -0
- package/src/components/layout/row.ts +7 -0
- package/src/components/layout/stack.d.ts +1 -0
- package/src/components/layout/stack.ts +7 -0
- package/src/components/markdown.d.ts +36 -3
- package/src/components/markdown.ts +433 -318
- package/src/components/scroll-view.d.ts +1 -0
- package/src/components/scroll-view.ts +5 -0
- package/src/components/section.d.ts +1 -0
- package/src/components/section.ts +5 -0
- package/src/components/settings-list.ts +2 -0
- package/src/components/table.d.ts +1 -0
- package/src/components/table.ts +4 -0
- package/src/components/text.d.ts +1 -0
- package/src/components/text.ts +7 -0
- package/src/components/tree-view.d.ts +7 -0
- package/src/components/tree-view.ts +38 -8
- package/src/components/truncated-text.d.ts +1 -0
- package/src/components/truncated-text.ts +5 -0
- package/src/desktop-notify.ts +1 -1
- package/src/fuzzy.d.ts +29 -1
- package/src/fuzzy.ts +115 -30
- package/src/key-hint-format.d.ts +1 -1
- package/src/key-hint-format.ts +1 -1
- package/src/keybinding-matchers.d.ts +3 -0
- package/src/keybinding-matchers.ts +6 -2
- package/src/keybindings.d.ts +1 -1
- package/src/keybindings.ts +1 -1
- package/src/kitty-graphics.d.ts +2 -2
- package/src/kitty-graphics.ts +4 -4
- package/src/loop-watchdog.d.ts +8 -7
- package/src/loop-watchdog.ts +18 -13
- package/src/multiplexers/cmux.d.ts +16 -0
- package/src/multiplexers/cmux.ts +29 -0
- package/src/multiplexers/herdr.d.ts +26 -0
- package/src/multiplexers/herdr.ts +64 -0
- package/src/multiplexers/notify.d.ts +13 -0
- package/src/multiplexers/notify.ts +24 -0
- package/src/multiplexers/orca.d.ts +12 -0
- package/src/multiplexers/orca.ts +16 -0
- package/src/multiplexers/screen.d.ts +9 -0
- package/src/multiplexers/screen.ts +15 -0
- package/src/multiplexers/tmux.d.ts +29 -0
- package/src/multiplexers/tmux.ts +40 -0
- package/src/multiplexers/types.d.ts +100 -0
- package/src/multiplexers/types.ts +110 -0
- package/src/multiplexers/wmux.d.ts +12 -0
- package/src/multiplexers/wmux.ts +16 -0
- package/src/multiplexers/zellij.d.ts +12 -0
- package/src/multiplexers/zellij.ts +24 -0
- package/src/native/backend.d.ts +3 -2
- package/src/native/backend.ts +174 -18
- package/src/native/blobs.d.ts +7 -3
- package/src/native/blobs.ts +47 -12
- package/src/native/encode.d.ts +3 -4
- package/src/native/encode.ts +58 -31
- package/src/native/overlay.d.ts +5 -0
- package/src/native/overlay.ts +13 -0
- package/src/native/reconcile.ts +4 -0
- package/src/overlays/agent-hub-projection.d.ts +6 -5
- package/src/overlays/agent-hub-projection.ts +124 -13
- package/src/overlays/agent-hub-types.d.ts +9 -0
- package/src/overlays/agent-hub-types.ts +6 -0
- package/src/overlays/agent-hub.ts +44 -29
- package/src/overlays/agent-transcript-viewer.ts +91 -23
- package/src/overlays/agents-hub.ts +28 -8
- package/src/overlays/annotation-overlay.ts +293 -90
- package/src/overlays/annotation-types.ts +2 -0
- package/src/overlays/ask-dialog.ts +7 -10
- package/src/overlays/btw-history-panel.ts +12 -1
- package/src/overlays/copy-selector.ts +30 -8
- package/src/overlays/extensions/state-manager.ts +52 -12
- package/src/overlays/history-search.ts +100 -7
- package/src/overlays/hook-selector.ts +12 -5
- package/src/overlays/jobs-panel.ts +28 -46
- package/src/overlays/model-browser.ts +341 -110
- package/src/overlays/model-hub.ts +300 -49
- package/src/overlays/move-overlay.ts +28 -14
- package/src/overlays/oauth-selector.ts +54 -70
- package/src/overlays/plan-review-overlay.ts +194 -70
- package/src/overlays/plugin-settings.ts +3 -3
- package/src/overlays/report-panel.ts +2 -2
- package/src/overlays/rewind-selector.ts +86 -17
- package/src/overlays/session-observer-registry.d.ts +2 -0
- package/src/overlays/session-observer-registry.ts +10 -2
- package/src/overlays/session-selector.ts +64 -26
- package/src/overlays/settings-defs.ts +4 -3
- package/src/overlays/settings-selector.ts +33 -8
- package/src/overlays/snapcompact-shape-preview.ts +2 -3
- package/src/overlays/stats-notice.ts +1 -1
- package/src/overlays/tree-selector.ts +41 -20
- package/src/overlays/usage-dashboard.ts +100 -50
- package/src/overlays/usage-display.ts +10 -4
- package/src/prompt/attachment-chips.ts +15 -11
- package/src/prompt/composer-attachments.ts +6 -4
- package/src/prompt/composer.ts +4 -15
- package/src/prompt/custom-editor.ts +54 -26
- package/src/prompt/image-source.ts +2 -1
- package/src/prompt/interactive-context-helpers.ts +30 -18
- package/src/prompt/macos-spelling.ts +7 -0
- package/src/prompt/magic-keywords.ts +11 -2
- package/src/prompt/model-mention-autocomplete.ts +14 -14
- package/src/prompt/tips.txt +6 -5
- package/src/prompt/welcome.ts +55 -12
- package/src/prompt/word-completion.ts +38 -5
- package/src/render/composite.d.ts +2 -0
- package/src/render/composite.ts +64 -0
- package/src/render/output-pane.d.ts +1 -0
- package/src/render/output-pane.ts +6 -0
- package/src/render/render-utils.d.ts +1 -0
- package/src/render/render-utils.ts +11 -0
- package/src/render/tool-card.d.ts +6 -0
- package/src/render/tool-card.ts +15 -0
- package/src/render/width-aware-text.d.ts +2 -0
- package/src/render/width-aware-text.ts +5 -0
- package/src/setup/scenes/model.ts +2 -1
- package/src/setup/scenes/types.ts +1 -0
- package/src/status-line/component.ts +292 -78
- package/src/status-line/host.ts +4 -1
- package/src/status-line/segments.ts +40 -22
- package/src/status-line/types.ts +15 -3
- package/src/terminal-capabilities.d.ts +27 -35
- package/src/terminal-capabilities.ts +75 -189
- package/src/terminal-multiplexer.d.ts +127 -14
- package/src/terminal-multiplexer.ts +112 -33
- package/src/terminal.d.ts +22 -6
- package/src/terminal.ts +139 -37
- package/src/theme/active-symbols.d.ts +1 -1
- package/src/theme/active-symbols.ts +1 -1
- package/src/theme/schema-validation.ts +1 -0
- package/src/theme/schema.d.ts +11 -2
- package/src/theme/schema.ts +14 -1
- package/src/theme/session-color.ts +1 -7
- package/src/theme/symbols.d.ts +1 -1
- package/src/theme/symbols.ts +6 -1
- package/src/theme/theme-class.d.ts +7 -0
- package/src/theme/theme-class.ts +26 -1
- package/src/theme/theme-schema.json +5 -1
- package/src/theme/theme.d.ts +1 -1
- package/src/theme/theme.ts +8 -4
- package/src/theme/tui-adapters.d.ts +3 -0
- package/src/theme/tui-adapters.ts +68 -0
- package/src/tmux.d.ts +11 -4
- package/src/tmux.ts +47 -11
- package/src/tools/ast-edit.ts +59 -35
- package/src/tools/ast-grep.ts +35 -16
- package/src/tools/bash.ts +92 -42
- package/src/tools/daemon.d.ts +1 -1
- package/src/tools/daemon.ts +1 -1
- package/src/tools/default-renderer.ts +35 -19
- package/src/tools/edit.ts +142 -120
- package/src/tools/eval-format/index.ts +28 -2
- package/src/tools/eval-format/javascript.ts +7 -5
- package/src/tools/eval.d.ts +22 -3
- package/src/tools/eval.ts +364 -208
- package/src/tools/fetch.ts +5 -6
- package/src/tools/glob.d.ts +4 -0
- package/src/tools/glob.ts +33 -65
- package/src/tools/grep.ts +81 -48
- package/src/tools/index.ts +2 -0
- package/src/tools/lsp.ts +81 -59
- package/src/tools/mcp.ts +111 -68
- package/src/tools/native-view.d.ts +47 -0
- package/src/tools/native-view.ts +102 -1
- package/src/tools/output-meta.d.ts +11 -1
- package/src/tools/output-meta.ts +23 -3
- package/src/tools/read.ts +7 -5
- package/src/tools/renderer.d.ts +37 -2
- package/src/tools/renderer.ts +42 -2
- package/src/tools/streaming-output.d.ts +20 -1
- package/src/tools/streaming-output.ts +149 -50
- package/src/tools/task.d.ts +0 -4
- package/src/tools/task.ts +112 -59
- package/src/tools/todo.ts +119 -30
- package/src/tools/wait.ts +34 -24
- package/src/tools/web-search.ts +1 -1
- package/src/tools/write.d.ts +9 -2
- package/src/tools/write.ts +147 -95
- package/src/tools/yield.d.ts +22 -0
- package/src/tools/yield.ts +225 -0
- package/src/tui.d.ts +31 -0
- package/src/tui.ts +309 -160
- package/src/utils.d.ts +6 -0
- package/src/utils.ts +19 -0
- package/src/vim.d.ts +19 -5
- package/src/vim.ts +333 -18
- package/src/windows-input-mode.ts +8 -0
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* to the session's `local://` root, and generated video contact-sheet previews.
|
|
5
5
|
* Symbol metadata stays out of serialized/model-bound image data while traveling
|
|
6
6
|
* with the draft object, until AgentSession creates the hidden companion message
|
|
7
|
-
* that tells the model the path
|
|
7
|
+
* that tells the model the path. The model keeps `local://` references across
|
|
8
|
+
* `/move`; chips for internal URLs instead use a stable blob copy.
|
|
8
9
|
*/
|
|
9
10
|
import type { ImageContent } from "@linxiraos/pi-ai";
|
|
10
11
|
import { isRecord } from "@linxiraos/pi-utils";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Shared assistant transcript construction and model-authored link caching. */
|
|
2
|
-
import type { AssistantMessage, Model } from "@linxiraos/pi-ai";
|
|
2
|
+
import type { AssistantMessage, Model, TextContent } from "@linxiraos/pi-ai";
|
|
3
3
|
import { getMarkdownLinkUrls } from "../index";
|
|
4
4
|
import { EMPTY_LINK_TARGETS } from "../render/render-utils";
|
|
5
5
|
import type { ImageBudget } from "../components/image";
|
|
@@ -18,10 +18,13 @@ export interface AssistantMessageHost {
|
|
|
18
18
|
readonly proseOnlyThinking: boolean;
|
|
19
19
|
readonly expandThinkingBlocks: boolean;
|
|
20
20
|
readonly assistantImagesVisible: boolean;
|
|
21
|
+
/** Whether the viewed session's tables get charts: the main session's do, a focused subagent's do not. */
|
|
22
|
+
readonly tableChartsVisible: boolean;
|
|
21
23
|
readonly hideToolActivity: boolean;
|
|
22
24
|
readonly toolOutputExpanded: boolean;
|
|
23
25
|
readonly ui: { requestRender(): void; readonly imageBudget: ImageBudget };
|
|
24
|
-
|
|
26
|
+
/** Resolve already-extracted Markdown link destinations to local targets. */
|
|
27
|
+
resolveAssistantMessageLinkHrefs(hrefs: readonly string[]): Promise<ReadonlyMap<string, string>>;
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
const kMarkdownLinkTargets = Symbol("markdownLinkTargets");
|
|
@@ -29,14 +32,26 @@ type SessionWithMarkdownLinkTargets = AssistantMessageSession & {
|
|
|
29
32
|
[kMarkdownLinkTargets]?: ReadonlyMap<string, string>;
|
|
30
33
|
};
|
|
31
34
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
/** Link destinations per text block, revalidated against the block's text (streaming blocks grow in place). */
|
|
36
|
+
const textBlockHrefs = new WeakMap<TextContent, { text: string; hrefs: readonly string[] }>();
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Distinct Markdown link destinations authored in `message`'s text blocks.
|
|
40
|
+
* Each block is lexed once per text revision; transcript rebuilds and the
|
|
41
|
+
* message_end refresh reuse the result.
|
|
42
|
+
*/
|
|
43
|
+
function assistantMessageLinkHrefs(message: AssistantMessage): string[] {
|
|
44
|
+
const hrefs = new Set<string>();
|
|
45
|
+
for (const content of message.content) {
|
|
46
|
+
if (content.type !== "text") continue;
|
|
47
|
+
let memo = textBlockHrefs.get(content);
|
|
48
|
+
if (memo?.text !== content.text) {
|
|
49
|
+
memo = { text: content.text, hrefs: getMarkdownLinkUrls(content.text) };
|
|
50
|
+
textBlockHrefs.set(content, memo);
|
|
37
51
|
}
|
|
52
|
+
for (const href of memo.hrefs) hrefs.add(href);
|
|
38
53
|
}
|
|
39
|
-
return
|
|
54
|
+
return [...hrefs];
|
|
40
55
|
}
|
|
41
56
|
|
|
42
57
|
/**
|
|
@@ -51,13 +66,12 @@ export async function refreshAssistantMessageLinkTargets(
|
|
|
51
66
|
): Promise<ReadonlyMap<string, string>> {
|
|
52
67
|
const session: SessionWithMarkdownLinkTargets = ctx.viewSession;
|
|
53
68
|
const previous = session[kMarkdownLinkTargets] ?? EMPTY_LINK_TARGETS;
|
|
54
|
-
const texts = assistantTextBlocks(messages);
|
|
55
69
|
const hrefs = new Set<string>();
|
|
56
|
-
for (const
|
|
57
|
-
for (const href of
|
|
70
|
+
for (const message of messages) {
|
|
71
|
+
for (const href of assistantMessageLinkHrefs(message)) hrefs.add(href);
|
|
58
72
|
}
|
|
59
73
|
if (hrefs.size === 0) return previous;
|
|
60
|
-
const resolved = await ctx.
|
|
74
|
+
const resolved = await ctx.resolveAssistantMessageLinkHrefs([...hrefs]);
|
|
61
75
|
let changed = false;
|
|
62
76
|
for (const href of hrefs) {
|
|
63
77
|
if (previous.get(href) !== resolved.get(href)) {
|
|
@@ -85,12 +99,9 @@ export function assistantMessageLinkTargets(
|
|
|
85
99
|
targets: ReadonlyMap<string, string>,
|
|
86
100
|
): ReadonlyMap<string, string> {
|
|
87
101
|
const selected = new Map<string, string>();
|
|
88
|
-
for (const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const target = targets.get(href);
|
|
92
|
-
if (target) selected.set(href, target);
|
|
93
|
-
}
|
|
102
|
+
for (const href of assistantMessageLinkHrefs(message)) {
|
|
103
|
+
const target = targets.get(href);
|
|
104
|
+
if (target) selected.set(href, target);
|
|
94
105
|
}
|
|
95
106
|
return selected;
|
|
96
107
|
}
|
|
@@ -116,6 +127,7 @@ export function createAssistantMessageComponent(
|
|
|
116
127
|
ctx.expandThinkingBlocks,
|
|
117
128
|
);
|
|
118
129
|
component.setImagesVisible(ctx.assistantImagesVisible);
|
|
130
|
+
component.setTableChartsVisible(ctx.tableChartsVisible);
|
|
119
131
|
component.setToolResultImagesVisible(!ctx.hideToolActivity);
|
|
120
132
|
component.setExpanded(ctx.toolOutputExpanded);
|
|
121
133
|
// A wire the `stream-revision` axis marks `possible` can rewrite text it has
|
|
@@ -56,6 +56,8 @@ export class MacOSSpellingProvider implements EditorTextAssistProvider {
|
|
|
56
56
|
#availabilityChecked = false;
|
|
57
57
|
#cacheGeneration = 0;
|
|
58
58
|
#typoCache = new Map<string, readonly native.SpellingRange[]>();
|
|
59
|
+
/** {@link #projectTypoRanges} answers for the current {@link #typoCache}; segments re-ask every frame. */
|
|
60
|
+
#projectionMemo = new Map<string, readonly native.SpellingRange[] | undefined>();
|
|
59
61
|
#typoInFlight = new Map<string, Promise<readonly native.SpellingRange[]>>();
|
|
60
62
|
#automaticTypoActive = false;
|
|
61
63
|
#automaticTypoQueue = new Map<string, string>();
|
|
@@ -237,6 +239,7 @@ export class MacOSSpellingProvider implements EditorTextAssistProvider {
|
|
|
237
239
|
return request;
|
|
238
240
|
}
|
|
239
241
|
#projectTypoRanges(text: string): readonly native.SpellingRange[] | undefined {
|
|
242
|
+
if (this.#projectionMemo.has(text)) return this.#projectionMemo.get(text);
|
|
240
243
|
let projected: native.SpellingRange[] | undefined;
|
|
241
244
|
let matchLength = -1;
|
|
242
245
|
for (const [previous, ranges] of this.#typoCache) {
|
|
@@ -265,6 +268,8 @@ export class MacOSSpellingProvider implements EditorTextAssistProvider {
|
|
|
265
268
|
});
|
|
266
269
|
matchLength = prefix + suffix;
|
|
267
270
|
}
|
|
271
|
+
if (this.#projectionMemo.size >= CACHE_LIMIT) this.#projectionMemo.clear();
|
|
272
|
+
this.#projectionMemo.set(text, projected);
|
|
268
273
|
return projected;
|
|
269
274
|
}
|
|
270
275
|
|
|
@@ -316,6 +321,7 @@ export class MacOSSpellingProvider implements EditorTextAssistProvider {
|
|
|
316
321
|
const hadProjectedRanges = (this.#projectTypoRanges(text)?.length ?? 0) > 0;
|
|
317
322
|
if (this.#typoCache.size >= CACHE_LIMIT) this.#typoCache.clear();
|
|
318
323
|
this.#typoCache.set(text, ranges);
|
|
324
|
+
this.#projectionMemo.clear();
|
|
319
325
|
if (ranges.length > 0 || hadProjectedRanges) this.onUpdate?.();
|
|
320
326
|
return ranges;
|
|
321
327
|
}
|
|
@@ -323,6 +329,7 @@ export class MacOSSpellingProvider implements EditorTextAssistProvider {
|
|
|
323
329
|
#clearCaches(): void {
|
|
324
330
|
this.#cacheGeneration++;
|
|
325
331
|
this.#typoCache.clear();
|
|
332
|
+
this.#projectionMemo.clear();
|
|
326
333
|
this.#typoInFlight.clear();
|
|
327
334
|
this.#automaticTypoQueue.clear();
|
|
328
335
|
}
|
|
@@ -148,6 +148,9 @@ export function magicKeywordRanges(text: string): { from: number; to: number }[]
|
|
|
148
148
|
return ranges.sort((a, b) => a.from - b.from);
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
/** Last {@link hasMagicKeyword} answer: the live editor asks once per layout segment with the same buffer. */
|
|
152
|
+
let lastProbe: { text: string; registry: readonly RegisteredKeyword[]; found: boolean } | undefined;
|
|
153
|
+
|
|
151
154
|
/**
|
|
152
155
|
* Cheap test for "does this text contain any registered keyword as standalone
|
|
153
156
|
* prose?". Short-circuits on a substring probe before paying for the
|
|
@@ -155,8 +158,14 @@ export function magicKeywordRanges(text: string): { from: number; to: number }[]
|
|
|
155
158
|
* `String#includes` per word. Used by the live editor to gate the shimmer timer.
|
|
156
159
|
*/
|
|
157
160
|
export function hasMagicKeyword(text: string): boolean {
|
|
161
|
+
if (lastProbe?.text === text && lastProbe.registry === registry) return lastProbe.found;
|
|
162
|
+
let found = false;
|
|
158
163
|
for (const keyword of registry) {
|
|
159
|
-
if (text.includes(keyword.word) && containsMagicKeyword(text, keyword.word))
|
|
164
|
+
if (text.includes(keyword.word) && containsMagicKeyword(text, keyword.word)) {
|
|
165
|
+
found = true;
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
160
168
|
}
|
|
161
|
-
|
|
169
|
+
lastProbe = { text, registry, found };
|
|
170
|
+
return found;
|
|
162
171
|
}
|
|
@@ -5,9 +5,7 @@ import { modelMentionDisplayName } from "./model-mention-syntax";
|
|
|
5
5
|
import type {
|
|
6
6
|
buildSearchAffinity as BuildSearchAffinity,
|
|
7
7
|
ModelBrowserItem,
|
|
8
|
-
|
|
9
|
-
SearchAffinity,
|
|
10
|
-
SessionModelScope,
|
|
8
|
+
ModelItemRanker as ModelItemRankerClass,
|
|
11
9
|
SessionModelScopeCache as SessionModelScopeCacheClass,
|
|
12
10
|
} from "../overlays/model-browser";
|
|
13
11
|
import { theme } from "../theme/theme";
|
|
@@ -21,7 +19,7 @@ export type ModelMentionCandidateSource = (query: string) => ReadonlyArray<Model
|
|
|
21
19
|
interface ModelBrowserModules {
|
|
22
20
|
SessionModelScopeCache: typeof SessionModelScopeCacheClass;
|
|
23
21
|
buildSearchAffinity: typeof BuildSearchAffinity;
|
|
24
|
-
|
|
22
|
+
ModelItemRanker: typeof ModelItemRankerClass;
|
|
25
23
|
}
|
|
26
24
|
|
|
27
25
|
/** Synchronous first-use boundary for the interactive model browser implementation. */
|
|
@@ -80,7 +78,7 @@ export function applyModelMentionCompletion(
|
|
|
80
78
|
|
|
81
79
|
/**
|
|
82
80
|
* Create a session-scoped model candidate lookup using picker ordering. The
|
|
83
|
-
* scope and
|
|
81
|
+
* scope and its prepared ranker are reused across queries until their inputs change.
|
|
84
82
|
*/
|
|
85
83
|
export function createModelMentionSource(host: {
|
|
86
84
|
source: ModelBrowserSource;
|
|
@@ -88,20 +86,22 @@ export function createModelMentionSource(host: {
|
|
|
88
86
|
scopedModels: () => ReadonlyArray<Model>;
|
|
89
87
|
}): ModelMentionCandidateSource {
|
|
90
88
|
let scopeCache: SessionModelScopeCacheClass | undefined;
|
|
91
|
-
let
|
|
92
|
-
let
|
|
93
|
-
let affinity: SearchAffinity | undefined;
|
|
89
|
+
let ranker: ModelItemRankerClass | undefined;
|
|
90
|
+
let rankerProviderOrder: readonly string[] | undefined;
|
|
94
91
|
return query => {
|
|
95
|
-
const { SessionModelScopeCache, buildSearchAffinity,
|
|
92
|
+
const { SessionModelScopeCache, buildSearchAffinity, ModelItemRanker } = loadModelBrowser();
|
|
96
93
|
scopeCache ??= new SessionModelScopeCache(host.source, host.registry);
|
|
97
94
|
const scope = scopeCache.get(host.scopedModels());
|
|
98
95
|
if (!query.trim()) return scope.items;
|
|
99
96
|
const providerOrder = host.source.modelProviderOrder;
|
|
100
|
-
if (
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
97
|
+
if (ranker?.items !== scope.items || rankerProviderOrder !== providerOrder) {
|
|
98
|
+
ranker = new ModelItemRanker(scope.items, {
|
|
99
|
+
roles: scope.roles,
|
|
100
|
+
mruOrder: scope.mruOrder,
|
|
101
|
+
affinity: buildSearchAffinity(providerOrder, scope.roles, scope.mruOrder),
|
|
102
|
+
});
|
|
103
|
+
rankerProviderOrder = providerOrder;
|
|
104
104
|
}
|
|
105
|
-
return
|
|
105
|
+
return ranker.rank(query);
|
|
106
106
|
};
|
|
107
107
|
}
|
package/src/prompt/tips.txt
CHANGED
|
@@ -6,17 +6,17 @@ Start your prompt with `$ ` to run Python — `$$ ` runs it without adding the o
|
|
|
6
6
|
You can /btw to ask a side question
|
|
7
7
|
Use /tan to fork the current conversation into a background agent
|
|
8
8
|
With default keybindings, {key:ctrl+d} deletes the character under the cursor instead of quitting your draft — see /hotkeys for your exit key
|
|
9
|
-
Find out which model you emotionally abuse the most with `
|
|
9
|
+
Find out which model you emotionally abuse the most with `zetacode stats`
|
|
10
10
|
Try task isolation to create CoW worktrees
|
|
11
|
-
Need a cheap nested model call? Use `completion(x...)
|
|
11
|
+
Need a cheap nested model call? Use `completion(x...)`
|
|
12
12
|
Spaghetti code? Try complaining with /omfg
|
|
13
|
-
Did you know? Each kitty/tmux/cmux/zellij/wezterm split keeps its own session — `
|
|
13
|
+
Did you know? Each kitty/tmux/cmux/zellij/wezterm split keeps its own session — `zetacode -c` resumes the right one
|
|
14
14
|
Drop the word `ultrathink` in your message for harder multi-step reasoning — watch it glow rainbow as you type
|
|
15
15
|
Say `orchestrate` in your message to drive a multi-phase task with parallel subagents — watch it glow as you type
|
|
16
16
|
Say `workflowz` in your message to drive the task with parallel subagents in eval — watch it glow as you type
|
|
17
17
|
Bulk data to classify or triage? Say `jevify` in your message — the eval kernel's `judge()` processes the batch and you read only what it flags — watch it glow as you type
|
|
18
18
|
Log in to several accounts of the same provider — `/login` again — and zeta load-balances across them automatically
|
|
19
|
-
Run `
|
|
19
|
+
Run `zetacode auth-broker serve` once and every machine pulls live tokens over the wire — refresh keys never leave the host; `zetacode auth-gateway` fronts it as a drop-in proxy any OpenAI-compatible client can hit
|
|
20
20
|
Press {key:alt+p} (or /switch) to switch provider, and {key:ctrl+p} to cycle role models smol -> slow -> etc
|
|
21
21
|
Press {key:ctrl+r} to search your prompt history and reuse a past message
|
|
22
22
|
`/force read` pins the next turn to one specific tool when the model keeps reaching for the wrong one
|
|
@@ -29,4 +29,5 @@ Turn on `/advisor` to attach a second model that reviews every turn and quietly
|
|
|
29
29
|
Try starting your prompt with a ->, and writing a list (1. Do X, 2. Do Y)
|
|
30
30
|
Press {key:shift+tab} to cycle through reasoning effort levels
|
|
31
31
|
zeta can reconfigure and extend itself — ask any questions you have!
|
|
32
|
-
Lint/type errors piling up? `
|
|
32
|
+
Lint/type errors piling up? `zetacode cleanse` (or /cleanse right here) hunts project diagnostics and fixes them with parallel subagents — {key:escape} cancels
|
|
33
|
+
[NO-TERN] Psst, zeta has a natural habitat 🐦 [tern](https://scl.so/tern)
|
package/src/prompt/welcome.ts
CHANGED
|
@@ -6,17 +6,23 @@ import { getKeybindings, type Keybinding } from "../keybindings";
|
|
|
6
6
|
import { card, col, kbd, keyed, node, row, span, text } from "../native/describe";
|
|
7
7
|
import type { DescribeContext, NativeChild, NativeNode, NativeUiEvent } from "../native/node";
|
|
8
8
|
import { runTranscriptAction } from "../chat/transcript-actions";
|
|
9
|
-
import { plainLine } from "../native/spans";
|
|
9
|
+
import { compactText, plainLine, plainText } from "../native/spans";
|
|
10
|
+
|
|
10
11
|
import { isNativeRendering } from "../native/state";
|
|
12
|
+
import { isHyperlinkEnabled, urlHyperlink, urlLinkSpan } from "../render/hyperlink";
|
|
11
13
|
import { TERMINAL } from "../terminal-capabilities";
|
|
12
14
|
import { theme } from "../theme/theme";
|
|
13
15
|
import type { Component } from "../tui";
|
|
14
16
|
import { padding, replaceTabs, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "../utils";
|
|
15
17
|
import tipsText from "./tips.txt" with { type: "text" };
|
|
16
18
|
|
|
17
|
-
/**
|
|
19
|
+
/** Leading marker for a tip that pitches Tern: shown only while no TSP surface
|
|
20
|
+
* is live, since a live one means the user is already in Tern. */
|
|
21
|
+
const NO_TERN_TIP_MARKER = /^\[NO-TERN\]\s*/;
|
|
22
|
+
|
|
23
|
+
/** tips.txt lines embedded at build time; blanks dropped. Key placeholders
|
|
18
24
|
* (see {@link expandTipKeys}) stay raw until render time. */
|
|
19
|
-
const
|
|
25
|
+
const TIP_LINES: readonly string[] = tipsText
|
|
20
26
|
.split("\n")
|
|
21
27
|
.map(line => line.trim())
|
|
22
28
|
.filter(line => line.length > 0);
|
|
@@ -33,6 +39,12 @@ export const WELCOME_SESSION_SLOTS = 4;
|
|
|
33
39
|
*/
|
|
34
40
|
export const WELCOME_LSP_SLOTS = 4;
|
|
35
41
|
|
|
42
|
+
/** Tips for the classic renderer: every tip, {@link NO_TERN_TIP_MARKER} stripped. */
|
|
43
|
+
const TIPS: readonly string[] = TIP_LINES.map(line => line.replace(NO_TERN_TIP_MARKER, ""));
|
|
44
|
+
|
|
45
|
+
/** Tips for a live TSP surface: those marked {@link NO_TERN_TIP_MARKER} dropped. */
|
|
46
|
+
const TSP_TIPS: readonly string[] = TIP_LINES.filter(line => !NO_TERN_TIP_MARKER.test(line));
|
|
47
|
+
|
|
36
48
|
/** Trailing marker that flags a tip as a "what's new" callout. Stripped before
|
|
37
49
|
* wrapping (with any preceding whitespace) and replaced by {@link NEW_TAG_TEXT}
|
|
38
50
|
* painted as a shimmering rainbow. Non-global so `.test` stays stateless. */
|
|
@@ -120,14 +132,44 @@ function expandTipKeys(tip: string): string {
|
|
|
120
132
|
});
|
|
121
133
|
}
|
|
122
134
|
|
|
123
|
-
|
|
135
|
+
/** Markdown-style links in tips.txt: `[Tern](https://scl.so/tern)` shows the label, linked to the URL. */
|
|
136
|
+
const TIP_LINK = /\[([^\]]+)\]\(([^\s)]+)\)/g;
|
|
137
|
+
|
|
138
|
+
/** Tip links as underlined OSC 8 labels, or `label (url)` where the terminal cannot link. */
|
|
139
|
+
function linkTipText(tip: string): string {
|
|
140
|
+
const linkable = isHyperlinkEnabled();
|
|
141
|
+
return tip.replace(TIP_LINK, (_link, label: string, url: string) =>
|
|
142
|
+
linkable ? urlHyperlink(url, theme.underline(label)) : `${label} (${url})`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** The tip as native text: escapes dropped, links as `href` spans. */
|
|
147
|
+
function tipSpans(tip: string): TspSpan[] {
|
|
148
|
+
const spans: TspSpan[] = [];
|
|
149
|
+
let cursor = 0;
|
|
150
|
+
for (const match of tip.matchAll(TIP_LINK)) {
|
|
151
|
+
const [link, label, url] = match;
|
|
152
|
+
if (match.index > cursor) spans.push(span(plainText(tip.slice(cursor, match.index))));
|
|
153
|
+
spans.push(urlLinkSpan(url, label));
|
|
154
|
+
cursor = match.index + link.length;
|
|
155
|
+
}
|
|
156
|
+
if (cursor < tip.length) spans.push(span(plainText(tip.slice(cursor))));
|
|
157
|
+
return spans;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The welcome tip as lines of at most `width` columns: `Tip:` and the body
|
|
162
|
+
* wrapped together, with no indent, so the banner can center each line.
|
|
163
|
+
* `[]` when `width` leaves no room for a useful line.
|
|
164
|
+
*/
|
|
165
|
+
export function renderWelcomeTip(tip: string, width: number, phase = 0): string[] {
|
|
124
166
|
const label = "Tip: ";
|
|
125
167
|
const labelWidth = visibleWidth(label);
|
|
126
|
-
const bodyBudget =
|
|
168
|
+
const bodyBudget = width - labelWidth;
|
|
127
169
|
if (bodyBudget < 8) return [];
|
|
128
170
|
|
|
129
171
|
const isNew = NEW_TIP_MARKER.test(tip);
|
|
130
|
-
const body = expandTipKeys(isNew ? tip.replace(NEW_TIP_MARKER, "") : tip);
|
|
172
|
+
const body = linkTipText(expandTipKeys(isNew ? tip.replace(NEW_TIP_MARKER, "") : tip));
|
|
131
173
|
|
|
132
174
|
const wrappedBody = wrapTextWithAnsi(replaceTabs(body), bodyBudget);
|
|
133
175
|
if (wrappedBody.length === 0) return [];
|
|
@@ -152,7 +194,7 @@ export function renderWelcomeTip(tip: string, boxWidth: number, phase = 0): stri
|
|
|
152
194
|
const tag = renderNewTag(phase, encoding);
|
|
153
195
|
const tagWidth = 1 + visibleWidth(NEW_TAG_TEXT); // 1 = space separator
|
|
154
196
|
const lastLine = lines[lines.length - 1];
|
|
155
|
-
if (lastLine !== undefined && visibleWidth(lastLine) + tagWidth <=
|
|
197
|
+
if (lastLine !== undefined && visibleWidth(lastLine) + tagWidth <= width) {
|
|
156
198
|
lines[lines.length - 1] = `${lastLine} ${tag}`;
|
|
157
199
|
} else {
|
|
158
200
|
lines.push(continuationIndent + tag);
|
|
@@ -183,9 +225,10 @@ export class WelcomeComponent implements Component {
|
|
|
183
225
|
#animTimer: Timer | null = null;
|
|
184
226
|
#requestRender: (() => void) | null = null;
|
|
185
227
|
// Tip randomness is latched once so the tip is stable across renders, but
|
|
186
|
-
// the nerdfont-nag gate re-reads the live preset
|
|
187
|
-
// run under the default "unicode"
|
|
188
|
-
//
|
|
228
|
+
// the nerdfont-nag gate re-reads the live preset and the pool re-reads the
|
|
229
|
+
// live TSP state: the startup prepaint can run under the default "unicode"
|
|
230
|
+
// preset before settings resolve the real one, and an optimistic Tern start
|
|
231
|
+
// falls back to the classic renderer when `hello` never confirms.
|
|
189
232
|
#nagRoll: number | undefined;
|
|
190
233
|
#tipRoll: number | undefined;
|
|
191
234
|
// Render cache: the welcome box is the first transcript-area component, so
|
|
@@ -209,7 +252,7 @@ export class WelcomeComponent implements Component {
|
|
|
209
252
|
if (theme.getSymbolPreset() === "unicode" && this.#nagRoll < 0.1) {
|
|
210
253
|
return "Please use nerdfont 😭.";
|
|
211
254
|
}
|
|
212
|
-
return pickWeightedTip(TIPS, this.#tipRoll) || undefined;
|
|
255
|
+
return pickWeightedTip(isNativeRendering() ? TSP_TIPS : TIPS, this.#tipRoll) || undefined;
|
|
213
256
|
}
|
|
214
257
|
|
|
215
258
|
invalidate(): void {
|
|
@@ -338,7 +381,7 @@ export class WelcomeComponent implements Component {
|
|
|
338
381
|
];
|
|
339
382
|
if (tip) {
|
|
340
383
|
const isNew = NEW_TIP_MARKER.test(tip);
|
|
341
|
-
const tipText =
|
|
384
|
+
const tipText = compactText(tipSpans(expandTipKeys(isNew ? tip.replace(NEW_TIP_MARKER, "") : tip)));
|
|
342
385
|
const tipRow: NativeChild[] = [
|
|
343
386
|
node("icon", { name: "lightbulb", role: "zeta.welcome.tip-icon" }),
|
|
344
387
|
text(tipText, { wrap: "word", role: "zeta.welcome.tip-text" }),
|
|
@@ -56,9 +56,20 @@ interface WordQuery extends WordCompletionQuery {
|
|
|
56
56
|
|
|
57
57
|
/** Editor text before the word starting at `start`, truncated to its last {@link BEFORE_LIMIT} code units. */
|
|
58
58
|
function textBefore(lines: readonly string[], cursorLine: number, start: number): string {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
const head = (lines[cursorLine] ?? "").slice(0, start);
|
|
60
|
+
// Find the first line the capped tail reaches, then join once.
|
|
61
|
+
let length = head.length;
|
|
62
|
+
let first = cursorLine;
|
|
63
|
+
while (first > 0 && length < BEFORE_LIMIT) {
|
|
64
|
+
first--;
|
|
65
|
+
length += (lines[first] ?? "").length + 1;
|
|
66
|
+
}
|
|
67
|
+
let text = head;
|
|
68
|
+
if (first < cursorLine) {
|
|
69
|
+
const parts: string[] = [];
|
|
70
|
+
for (let line = first; line < cursorLine; line++) parts.push(lines[line] ?? "");
|
|
71
|
+
parts.push(head);
|
|
72
|
+
text = parts.join("\n");
|
|
62
73
|
}
|
|
63
74
|
if (text.length <= BEFORE_LIMIT) return text;
|
|
64
75
|
let cut = text.length - BEFORE_LIMIT;
|
|
@@ -111,6 +122,16 @@ export class WordCompletionProvider implements EditorTextAssistProvider {
|
|
|
111
122
|
#lastShown: { before: string; word: string } | undefined;
|
|
112
123
|
/** What the last `getWordCompletion` returned, so a fetch repaints only when it changes that. */
|
|
113
124
|
#displayed: { key: string; suffix: string | null } | undefined;
|
|
125
|
+
/** Last {@link #query} input snapshot (lines are copied: the editor mutates its array in place). */
|
|
126
|
+
#queryMemo:
|
|
127
|
+
| {
|
|
128
|
+
method: WordCompletionMethod;
|
|
129
|
+
lines: readonly string[];
|
|
130
|
+
cursorLine: number;
|
|
131
|
+
cursorCol: number;
|
|
132
|
+
query: WordQuery | undefined;
|
|
133
|
+
}
|
|
134
|
+
| undefined;
|
|
114
135
|
|
|
115
136
|
/** Invoked when an asynchronous answer changes the rendered ghost text. */
|
|
116
137
|
onUpdate: (() => void) | undefined;
|
|
@@ -171,11 +192,23 @@ export class WordCompletionProvider implements EditorTextAssistProvider {
|
|
|
171
192
|
|
|
172
193
|
#query(lines: readonly string[], cursorLine: number, cursorCol: number): WordQuery | undefined {
|
|
173
194
|
if (this.#method === "off") return undefined;
|
|
195
|
+
// Rendering asks every frame; reuse the answer until the buffer, caret, or engine changes.
|
|
196
|
+
const memo = this.#queryMemo;
|
|
197
|
+
if (
|
|
198
|
+
memo?.method === this.#method &&
|
|
199
|
+
memo.cursorLine === cursorLine &&
|
|
200
|
+
memo.cursorCol === cursorCol &&
|
|
201
|
+
memo.lines.length === lines.length &&
|
|
202
|
+
memo.lines.every((line, index) => line === lines[index])
|
|
203
|
+
) {
|
|
204
|
+
return memo.query;
|
|
205
|
+
}
|
|
174
206
|
const query = wordCompletionQuery(lines, cursorLine, cursorCol, this.#prose);
|
|
175
|
-
if (!query) return undefined;
|
|
176
207
|
// The method keeps a request still in flight for the previous engine from
|
|
177
208
|
// shadowing the same word state on the new one.
|
|
178
|
-
|
|
209
|
+
const result = query && { key: `${this.#method}\u0000${query.prefix}\u0000${query.before}`, ...query };
|
|
210
|
+
this.#queryMemo = { method: this.#method, lines: lines.slice(), cursorLine, cursorCol, query: result };
|
|
211
|
+
return result;
|
|
179
212
|
}
|
|
180
213
|
|
|
181
214
|
#project(query: WordQuery): string | null {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { TERMINAL } from "../terminal-capabilities";
|
|
2
|
+
import { extractSegments, sliceByColumn, sliceWithWidth, visibleWidth } from "../utils";
|
|
3
|
+
|
|
4
|
+
const SEGMENT_RESET = "\x1b[0m";
|
|
5
|
+
|
|
6
|
+
/** Splice overlay content into a base line at a specific column. Single-pass optimized. */
|
|
7
|
+
export function compositeLineAt(
|
|
8
|
+
baseLine: string,
|
|
9
|
+
overlayLine: string,
|
|
10
|
+
startCol: number,
|
|
11
|
+
overlayWidth: number,
|
|
12
|
+
totalWidth: number,
|
|
13
|
+
): string {
|
|
14
|
+
// A full-width image row owns its terminal row; the writer must see its exact placement shape.
|
|
15
|
+
if (startCol === 0 && overlayWidth >= totalWidth && TERMINAL.isImageLine(overlayLine)) return overlayLine;
|
|
16
|
+
if (TERMINAL.isImageLine(baseLine)) {
|
|
17
|
+
// Full-width overlays such as /switch are opaque: replace the
|
|
18
|
+
// Unicode placeholder cells so the image cannot cover the modal.
|
|
19
|
+
// Partial overlays cannot safely splice placement control sequences.
|
|
20
|
+
if (startCol !== 0 || overlayWidth < totalWidth) return baseLine;
|
|
21
|
+
const overlay = sliceWithWidth(overlayLine, 0, totalWidth, true);
|
|
22
|
+
return SEGMENT_RESET + overlay.text + " ".repeat(Math.max(0, totalWidth - overlay.width));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Single pass through baseLine extracts both before and after segments
|
|
26
|
+
const afterStart = startCol + overlayWidth;
|
|
27
|
+
const base = extractSegments(baseLine, startCol, afterStart, totalWidth - afterStart, true);
|
|
28
|
+
|
|
29
|
+
// Extract overlay with width tracking (strict=true to exclude wide chars at boundary)
|
|
30
|
+
const overlay = sliceWithWidth(overlayLine, 0, overlayWidth, true);
|
|
31
|
+
|
|
32
|
+
// Pad segments to target widths
|
|
33
|
+
const beforePad = Math.max(0, startCol - base.beforeWidth);
|
|
34
|
+
const overlayPad = Math.max(0, overlayWidth - overlay.width);
|
|
35
|
+
const actualBeforeWidth = Math.max(startCol, base.beforeWidth);
|
|
36
|
+
const actualOverlayWidth = Math.max(overlayWidth, overlay.width);
|
|
37
|
+
const afterTarget = Math.max(0, totalWidth - actualBeforeWidth - actualOverlayWidth);
|
|
38
|
+
const afterPad = Math.max(0, afterTarget - base.afterWidth);
|
|
39
|
+
|
|
40
|
+
// Compose result
|
|
41
|
+
const r = SEGMENT_RESET;
|
|
42
|
+
const result =
|
|
43
|
+
base.before +
|
|
44
|
+
" ".repeat(beforePad) +
|
|
45
|
+
r +
|
|
46
|
+
overlay.text +
|
|
47
|
+
" ".repeat(overlayPad) +
|
|
48
|
+
r +
|
|
49
|
+
base.after +
|
|
50
|
+
" ".repeat(afterPad);
|
|
51
|
+
|
|
52
|
+
// CRITICAL: Always verify and truncate to terminal width.
|
|
53
|
+
// This is the final safeguard against width overflow which would crash the TUI.
|
|
54
|
+
// Width tracking can drift from actual visible width due to:
|
|
55
|
+
// - Complex ANSI/OSC sequences (hyperlinks, colors)
|
|
56
|
+
// - Wide characters at segment boundaries
|
|
57
|
+
// - Edge cases in segment extraction
|
|
58
|
+
const resultWidth = visibleWidth(result);
|
|
59
|
+
if (resultWidth <= totalWidth) {
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
// Truncate with strict=true to ensure we don't exceed totalWidth
|
|
63
|
+
return sliceByColumn(result, 0, totalWidth, true);
|
|
64
|
+
}
|
|
@@ -92,6 +92,7 @@ export declare class OutputPane implements Component {
|
|
|
92
92
|
describe(_cx: DescribeContext): NativeNode;
|
|
93
93
|
render(width: number): readonly string[];
|
|
94
94
|
invalidate(): void;
|
|
95
|
+
releaseRenderCaches(): void;
|
|
95
96
|
}
|
|
96
97
|
/** Default styling for plain tool output while preserving existing ANSI. */
|
|
97
98
|
export declare function styleToolOutputLine(line: string, theme: Theme): string;
|
|
@@ -302,6 +302,12 @@ export class OutputPane implements Component {
|
|
|
302
302
|
this.#text.invalidate();
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
+
releaseRenderCaches(): void {
|
|
306
|
+
this.#renderKey = "";
|
|
307
|
+
this.#text.setText("");
|
|
308
|
+
this.#text.releaseRenderCaches();
|
|
309
|
+
}
|
|
310
|
+
|
|
305
311
|
#appendToTail(text: string): void {
|
|
306
312
|
if (!text) return;
|
|
307
313
|
if (this.#lines.length === 0) this.#lines.push("");
|
|
@@ -298,6 +298,7 @@ export interface RenderedStringCache {
|
|
|
298
298
|
export declare function createRenderedStringCache(): RenderedStringCache;
|
|
299
299
|
/** Drop the memo so the next lookup re-renders (e.g. the render function identity changed). */
|
|
300
300
|
export declare function invalidateRenderedStringCache(cache: RenderedStringCache): void;
|
|
301
|
+
export declare function releaseRenderedStringCache(cache: RenderedStringCache): void;
|
|
301
302
|
/** Reuse a rendered string while its theme, expansion, content, and salt match. */
|
|
302
303
|
export declare function cachedRenderedString(cache: RenderedStringCache | undefined, theme: Theme, expanded: boolean, salt: string, content: string, render: () => string): string;
|
|
303
304
|
/**
|
|
@@ -1128,6 +1128,9 @@ export function createCachedComponent(
|
|
|
1128
1128
|
invalidate() {
|
|
1129
1129
|
cached = undefined;
|
|
1130
1130
|
},
|
|
1131
|
+
releaseRenderCaches() {
|
|
1132
|
+
cached = undefined;
|
|
1133
|
+
},
|
|
1131
1134
|
};
|
|
1132
1135
|
}
|
|
1133
1136
|
|
|
@@ -1160,6 +1163,14 @@ export function invalidateRenderedStringCache(cache: RenderedStringCache): void
|
|
|
1160
1163
|
cache.theme = null;
|
|
1161
1164
|
}
|
|
1162
1165
|
|
|
1166
|
+
export function releaseRenderedStringCache(cache: RenderedStringCache): void {
|
|
1167
|
+
cache.theme = null;
|
|
1168
|
+
cache.expanded = false;
|
|
1169
|
+
cache.salt = "";
|
|
1170
|
+
cache.content = "";
|
|
1171
|
+
cache.value = "";
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1163
1174
|
/** Reuse a rendered string while its theme, expansion, content, and salt match. */
|
|
1164
1175
|
export function cachedRenderedString(
|
|
1165
1176
|
cache: RenderedStringCache | undefined,
|
|
@@ -46,6 +46,11 @@ export interface ToolCardOptions {
|
|
|
46
46
|
paddingY?: number;
|
|
47
47
|
ignoreTight?: boolean;
|
|
48
48
|
onInvalidate?: () => void;
|
|
49
|
+
/**
|
|
50
|
+
* Drop builder-owned memos the next build recomputes. Called by
|
|
51
|
+
* {@link ToolCard.releaseRenderCaches}, which never calls `onInvalidate`.
|
|
52
|
+
*/
|
|
53
|
+
onReleaseRenderCaches?: () => void;
|
|
49
54
|
onDispose?: () => void;
|
|
50
55
|
}
|
|
51
56
|
/** Map transcript lifecycle terminology onto the output frame's visual states. */
|
|
@@ -71,6 +76,7 @@ export declare class ToolCard implements Component {
|
|
|
71
76
|
handleInput(data: string): void;
|
|
72
77
|
setIgnoreTight(ignore: boolean): void;
|
|
73
78
|
invalidate(): void;
|
|
79
|
+
releaseRenderCaches(): void;
|
|
74
80
|
dispose(): void;
|
|
75
81
|
}
|
|
76
82
|
/** Construct a lazy framed tool card. */
|
package/src/render/tool-card.ts
CHANGED
|
@@ -69,6 +69,11 @@ export interface ToolCardOptions {
|
|
|
69
69
|
paddingY?: number;
|
|
70
70
|
ignoreTight?: boolean;
|
|
71
71
|
onInvalidate?: () => void;
|
|
72
|
+
/**
|
|
73
|
+
* Drop builder-owned memos the next build recomputes. Called by
|
|
74
|
+
* {@link ToolCard.releaseRenderCaches}, which never calls `onInvalidate`.
|
|
75
|
+
*/
|
|
76
|
+
onReleaseRenderCaches?: () => void;
|
|
72
77
|
onDispose?: () => void;
|
|
73
78
|
}
|
|
74
79
|
|
|
@@ -381,6 +386,16 @@ export class ToolCard implements Component {
|
|
|
381
386
|
this.#options.onInvalidate?.();
|
|
382
387
|
}
|
|
383
388
|
|
|
389
|
+
releaseRenderCaches(): void {
|
|
390
|
+
this.#block.invalidate();
|
|
391
|
+
this.#lastBlockOptions = undefined;
|
|
392
|
+
this.#plainKey = "";
|
|
393
|
+
this.#plainText.setText("");
|
|
394
|
+
this.#plainText.releaseRenderCaches();
|
|
395
|
+
for (const child of this.#renderedChildren) child.releaseRenderCaches?.();
|
|
396
|
+
this.#options.onReleaseRenderCaches?.();
|
|
397
|
+
}
|
|
398
|
+
|
|
384
399
|
dispose(): void {
|
|
385
400
|
if (this.#disposed) return;
|
|
386
401
|
this.#disposed = true;
|
|
@@ -20,6 +20,8 @@ export declare class WidthAwareText implements Component {
|
|
|
20
20
|
setCustomBgFn(customBgFn?: (text: string) => string): void;
|
|
21
21
|
setIgnoreTight(ignore: boolean): this;
|
|
22
22
|
invalidate(): void;
|
|
23
|
+
/** The formatted text stays: the inner `Text` renders from it, so dropping it would free nothing. */
|
|
24
|
+
releaseRenderCaches(): void;
|
|
23
25
|
/**
|
|
24
26
|
* Re-run the formatter on the next render because its inputs changed.
|
|
25
27
|
* Unlike {@link invalidate}, the inner `Text` keeps its wrap cache, so a
|