@linxiraos/pi-tui 1.1.27 → 1.2.1
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 +50 -461
- 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 +2 -7
- 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 +19 -42
- 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/splash.d.ts +1 -1
- 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 +46 -31
- 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 +11 -5
- 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-cache.ts +4 -6
- package/src/prompt/composer.ts +6 -37
- 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 +235 -411
- 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/splash.ts +116 -53
- 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
|
@@ -11,10 +11,9 @@ import { ThinkingLevel } from "@linxiraos/pi-agent-core";
|
|
|
11
11
|
import type { Model } from "@linxiraos/pi-ai";
|
|
12
12
|
import { buildModel } from "@linxiraos/pi-catalog/build";
|
|
13
13
|
import { getModelPricingStatus, modelsAreEqual } from "@linxiraos/pi-catalog/models";
|
|
14
|
-
import type
|
|
14
|
+
import { modelKind, type ModelKind, type ModelPricingStatus } from "@linxiraos/pi-catalog/types";
|
|
15
15
|
import type { Component } from "../tui";
|
|
16
|
-
import {
|
|
17
|
-
import { fuzzyRank } from "../fuzzy";
|
|
16
|
+
import { FuzzyCorpus, fuzzyRank } from "../fuzzy";
|
|
18
17
|
import { Input } from "../components/input";
|
|
19
18
|
import { ScrollView } from "../components/scroll-view";
|
|
20
19
|
import { matchesKey } from "../keys";
|
|
@@ -28,7 +27,7 @@ import {
|
|
|
28
27
|
parseConfiguredThinkingLevel,
|
|
29
28
|
} from "../thinking";
|
|
30
29
|
import { thinkingLevelGlyph } from "../render/render-utils";
|
|
31
|
-
import { type ThemeColor, theme } from "../theme/theme";
|
|
30
|
+
import { type Theme, type ThemeColor, theme } from "../theme/theme";
|
|
32
31
|
import {
|
|
33
32
|
matchesSelectCancel,
|
|
34
33
|
matchesSelectDown,
|
|
@@ -123,6 +122,18 @@ export interface ResolvedModelRoleValue {
|
|
|
123
122
|
warning?: string;
|
|
124
123
|
}
|
|
125
124
|
|
|
125
|
+
/** Where auto-compaction triggers for one model, as the model hub's preview shows it (edited from the Roles view). */
|
|
126
|
+
export interface ModelCompactionPoint {
|
|
127
|
+
/** Context tokens that trigger auto-compaction; undefined when auto-compaction is off or the window is unknown. */
|
|
128
|
+
tokens: number | undefined;
|
|
129
|
+
/** Why it triggers there, in a few words: `fixed`, `85% of 400K base`, `80% of window`. */
|
|
130
|
+
basis?: string;
|
|
131
|
+
/** What sets it: the matching `compaction.modelThresholds` key, `global`, or `default`. */
|
|
132
|
+
source: string;
|
|
133
|
+
/** The model's own exact entry as editable text (`90k`, `80%`); absent when it has none. */
|
|
134
|
+
draft?: string;
|
|
135
|
+
}
|
|
136
|
+
|
|
126
137
|
/** Host-provided preferences and model-role resolution for the browser. */
|
|
127
138
|
export interface ModelBrowserSource extends ModelRoleLookup {
|
|
128
139
|
/**
|
|
@@ -135,11 +146,17 @@ export interface ModelBrowserSource extends ModelRoleLookup {
|
|
|
135
146
|
readonly knownRoleIds: readonly string[];
|
|
136
147
|
readonly mruOrder: readonly string[];
|
|
137
148
|
readonly modelPerf: ReadonlyMap<string, ModelBrowserPerf>;
|
|
149
|
+
/**
|
|
150
|
+
* Service tier the host would send for `model`, when it runs one. Rows with a
|
|
151
|
+
* measured tier aggregate (`selector@tier`) then display that tier's numbers
|
|
152
|
+
* instead of the standard aggregate.
|
|
153
|
+
*/
|
|
154
|
+
serviceTierFor?(model: Model): string | undefined;
|
|
138
155
|
getRoleInfo(role: string): ModelBrowserRoleInfo;
|
|
139
|
-
/** True when the provider was injected from the upstream OMP model config. */
|
|
140
|
-
isOmpOriginProvider?(provider: string): boolean;
|
|
141
156
|
defaultRoleChain(role: string): string[];
|
|
142
157
|
resolveRoleValue(value: string | undefined, models: Model[], roleLookup?: ModelRoleLookup): ResolvedModelRoleValue;
|
|
158
|
+
/** Auto-compaction point for `model`; absent hosts show no compaction row in the preview. */
|
|
159
|
+
compactionPointFor?(model: Model): ModelCompactionPoint | undefined;
|
|
143
160
|
}
|
|
144
161
|
|
|
145
162
|
/** Read-only catalog surface consumed by model browsers. */
|
|
@@ -246,6 +263,22 @@ export function resolveRoleAssignments(
|
|
|
246
263
|
return roles;
|
|
247
264
|
}
|
|
248
265
|
|
|
266
|
+
/**
|
|
267
|
+
* Role-resolution pool for a `--models`/`enabledModels` scope: the scoped
|
|
268
|
+
* models plus every available non-chat runner (judge, search, image, …). The
|
|
269
|
+
* scope only filters chat models (it feeds Ctrl+P cycling) and runtime role
|
|
270
|
+
* resolution ignores it for other kinds, so displayed role assignments must too.
|
|
271
|
+
* Shared by the /models hub and the session picker.
|
|
272
|
+
*/
|
|
273
|
+
export function scopedRolePool(scoped: ReadonlyArray<Model>, registry: ModelBrowserRegistry): Model[] {
|
|
274
|
+
const pool = [...scoped];
|
|
275
|
+
for (const model of registry.getAvailable("all")) {
|
|
276
|
+
if (modelKind(model) === "chat" || scoped.some(entry => modelsAreEqual(entry, model))) continue;
|
|
277
|
+
pool.push(model);
|
|
278
|
+
}
|
|
279
|
+
return pool;
|
|
280
|
+
}
|
|
281
|
+
|
|
249
282
|
/** Wrap raw models into browser items. */
|
|
250
283
|
export function buildBrowserItems(models: ReadonlyArray<Model>): ModelBrowserItem[] {
|
|
251
284
|
return models.map(model => ({
|
|
@@ -300,13 +333,17 @@ export interface SortModelItemsOptions {
|
|
|
300
333
|
* then per provider by priority, version, and recency.
|
|
301
334
|
*/
|
|
302
335
|
export function sortModelItems(items: ModelBrowserItem[], options: SortModelItemsOptions = {}): void {
|
|
336
|
+
items.sort(modelItemComparator(options));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function modelItemComparator(options: SortModelItemsOptions): (a: ModelBrowserItem, b: ModelBrowserItem) => number {
|
|
303
340
|
const { roles = {}, mruOrder = [], skipRoleRank = false } = options;
|
|
304
341
|
const mruIndex = new Map(mruOrder.map((key, i) => [key, i]));
|
|
305
342
|
|
|
306
343
|
const dateRe = /-(\d{8})$/;
|
|
307
344
|
const latestRe = /-latest$/;
|
|
308
345
|
|
|
309
|
-
|
|
346
|
+
return (a, b) => {
|
|
310
347
|
if (!skipRoleRank) {
|
|
311
348
|
const aRank = computeModelRank(a.model, roles);
|
|
312
349
|
const bRank = computeModelRank(b.model, roles);
|
|
@@ -351,9 +388,9 @@ export function sortModelItems(items: ModelBrowserItem[], options: SortModelItem
|
|
|
351
388
|
// Both have dates — descending (newest first)
|
|
352
389
|
if (aDate && bDate) return bDate.localeCompare(aDate);
|
|
353
390
|
|
|
354
|
-
//
|
|
355
|
-
return
|
|
356
|
-
}
|
|
391
|
+
// Both -latest — alphabetical (a fixed -1 here made the order depend on input order)
|
|
392
|
+
return a.id.localeCompare(b.id);
|
|
393
|
+
};
|
|
357
394
|
}
|
|
358
395
|
|
|
359
396
|
/** Picker candidates and ordering inputs shared with composer model mentions. */
|
|
@@ -366,8 +403,16 @@ export interface SessionModelScope {
|
|
|
366
403
|
|
|
367
404
|
/** Catalog inputs a {@link SessionModelScope} is derived from. */
|
|
368
405
|
interface SessionModelScopeInputs {
|
|
406
|
+
/** Chat models listed as picker rows. */
|
|
369
407
|
models: ReadonlyArray<Model>;
|
|
408
|
+
/** Catalog that configured role values resolve against. */
|
|
370
409
|
allModels: ReadonlyArray<Model>;
|
|
410
|
+
/**
|
|
411
|
+
* Available models of every kind that unconfigured roles auto-select from,
|
|
412
|
+
* as at runtime; a chat-only pool would pin `judge` on a chat model while
|
|
413
|
+
* the session judges with an available native judge.
|
|
414
|
+
*/
|
|
415
|
+
roleModels: ReadonlyArray<Model>;
|
|
371
416
|
error: string | undefined;
|
|
372
417
|
}
|
|
373
418
|
|
|
@@ -375,21 +420,32 @@ function readSessionModelScopeInputs(
|
|
|
375
420
|
registry: ModelBrowserRegistry,
|
|
376
421
|
scopedModels: ReadonlyArray<Model>,
|
|
377
422
|
): SessionModelScopeInputs {
|
|
378
|
-
if (scopedModels.length > 0)
|
|
423
|
+
if (scopedModels.length > 0) {
|
|
424
|
+
let roleModels: ReadonlyArray<Model>;
|
|
425
|
+
let error: string | undefined;
|
|
426
|
+
try {
|
|
427
|
+
roleModels = scopedRolePool(scopedModels, registry);
|
|
428
|
+
} catch (cause) {
|
|
429
|
+
error = cause instanceof Error ? cause.message : String(cause);
|
|
430
|
+
roleModels = scopedModels;
|
|
431
|
+
}
|
|
432
|
+
return { models: scopedModels, allModels: roleModels, roleModels, error };
|
|
433
|
+
}
|
|
379
434
|
const loadError = registry.getError();
|
|
380
435
|
let error = loadError ? String(loadError) : undefined;
|
|
381
|
-
let
|
|
436
|
+
let roleModels: ReadonlyArray<Model>;
|
|
382
437
|
try {
|
|
383
|
-
|
|
438
|
+
roleModels = registry.getAvailable("all");
|
|
384
439
|
} catch (cause) {
|
|
385
440
|
error = cause instanceof Error ? cause.message : String(cause);
|
|
386
|
-
|
|
441
|
+
roleModels = [];
|
|
387
442
|
}
|
|
388
|
-
|
|
443
|
+
const models = roleModels.filter(model => modelKind(model) === "chat");
|
|
444
|
+
return { models, allModels: registry.getAll("all"), roleModels, error };
|
|
389
445
|
}
|
|
390
446
|
|
|
391
447
|
function scopeFromInputs(settings: ModelBrowserSource, inputs: SessionModelScopeInputs): SessionModelScope {
|
|
392
|
-
const roles = resolveRoleAssignments(settings, inputs.allModels, inputs.
|
|
448
|
+
const roles = resolveRoleAssignments(settings, inputs.allModels, inputs.roleModels);
|
|
393
449
|
const mruOrder = settings.mruOrder;
|
|
394
450
|
const items = buildBrowserItems(inputs.models);
|
|
395
451
|
sortModelItems(items, { roles, mruOrder });
|
|
@@ -434,6 +490,7 @@ export class SessionModelScopeCache {
|
|
|
434
490
|
inputs.error === cachedInputs.error &&
|
|
435
491
|
sameItems(inputs.models, cachedInputs.models) &&
|
|
436
492
|
sameItems(inputs.allModels, cachedInputs.allModels) &&
|
|
493
|
+
sameItems(inputs.roleModels, cachedInputs.roleModels) &&
|
|
437
494
|
sameItems(this.#settings.mruOrder, cached.mruOrder)
|
|
438
495
|
) {
|
|
439
496
|
return cached;
|
|
@@ -533,58 +590,100 @@ function compactModelSearchText(value: string): string {
|
|
|
533
590
|
return value.toLowerCase().replace(/[^\p{Letter}\p{Mark}\p{Number}]+/gu, "");
|
|
534
591
|
}
|
|
535
592
|
|
|
536
|
-
/** Exact id/selector → contiguous literal → fuzzy-only. */
|
|
537
|
-
function modelSearchTier(query: string,
|
|
593
|
+
/** Exact id/selector → contiguous literal → fuzzy-only, against precomputed compact forms. */
|
|
594
|
+
function modelSearchTier(query: string, id: string, selector: string): number {
|
|
538
595
|
if (!query) return 2;
|
|
539
|
-
const id = compactModelSearchText(item.id);
|
|
540
|
-
const selector = compactModelSearchText(item.selector);
|
|
541
596
|
if (query === id || query === selector) return 0;
|
|
542
597
|
if (id.includes(query) || selector.includes(query)) return 1;
|
|
543
598
|
return 2;
|
|
544
599
|
}
|
|
545
600
|
|
|
601
|
+
/** Inputs to {@link rankModelItems} / {@link ModelItemRanker}. */
|
|
602
|
+
export interface ModelRankOptions {
|
|
603
|
+
roles: RoleAssignments;
|
|
604
|
+
mruOrder: ReadonlyArray<string>;
|
|
605
|
+
affinity: SearchAffinity;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/** Query-independent sort keys of one candidate, computed once per ranker. */
|
|
609
|
+
interface ModelRankKeys {
|
|
610
|
+
compactId: string;
|
|
611
|
+
compactSelector: string;
|
|
612
|
+
modelAffinity: number;
|
|
613
|
+
providerAffinity: number;
|
|
614
|
+
/** Position class in the MRU/version order; comparator-equal items share it. */
|
|
615
|
+
orderGroup: number;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* {@link rankModelItems} prepared for one candidate list: the fuzzy index, the
|
|
620
|
+
* MRU/version order, and the affinity ranks depend only on the items and options,
|
|
621
|
+
* so per-keystroke callers (composer mentions, `/switch` arguments, the picker)
|
|
622
|
+
* hold one ranker and pay only the per-query fuzzy scan.
|
|
623
|
+
*/
|
|
624
|
+
export class ModelItemRanker {
|
|
625
|
+
readonly items: ReadonlyArray<ModelBrowserItem>;
|
|
626
|
+
readonly options: ModelRankOptions;
|
|
627
|
+
readonly #corpus: FuzzyCorpus<ModelBrowserItem>;
|
|
628
|
+
readonly #keys = new Map<ModelBrowserItem, ModelRankKeys>();
|
|
629
|
+
|
|
630
|
+
constructor(items: ReadonlyArray<ModelBrowserItem>, options: ModelRankOptions) {
|
|
631
|
+
this.items = items;
|
|
632
|
+
this.options = options;
|
|
633
|
+
this.#corpus = new FuzzyCorpus(items, modelSearchText);
|
|
634
|
+
const compare = modelItemComparator({ roles: options.roles, mruOrder: options.mruOrder, skipRoleRank: true });
|
|
635
|
+
const ordered = [...items].sort(compare);
|
|
636
|
+
let group = 0;
|
|
637
|
+
for (let i = 0; i < ordered.length; i++) {
|
|
638
|
+
const item = ordered[i]!;
|
|
639
|
+
if (i > 0 && compare(ordered[i - 1]!, item) !== 0) group++;
|
|
640
|
+
this.#keys.set(item, {
|
|
641
|
+
compactId: compactModelSearchText(item.id),
|
|
642
|
+
compactSelector: compactModelSearchText(item.selector),
|
|
643
|
+
modelAffinity: options.affinity.models.get(item.selector.toLowerCase()) ?? Number.MAX_SAFE_INTEGER,
|
|
644
|
+
providerAffinity: options.affinity.providers.get(item.provider.toLowerCase()) ?? Number.MAX_SAFE_INTEGER,
|
|
645
|
+
orderGroup: group,
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/** Rank by text relevance, user affinity, and MRU/version order. */
|
|
651
|
+
rank(query: string): ModelBrowserItem[] {
|
|
652
|
+
if (!query.trim()) return [...this.items];
|
|
653
|
+
const queryKey = compactModelSearchText(query);
|
|
654
|
+
// Exact and contiguous matches stay ahead of fuzzy-only candidates; affinity
|
|
655
|
+
// breaks ties before fuzzy quality and the normal MRU/version ordering, and
|
|
656
|
+
// fuzzy order settles items that ordering cannot tell apart.
|
|
657
|
+
const rows = this.#corpus.rank(query).map((result, fuzzyIndex) => {
|
|
658
|
+
const keys = this.#keys.get(result.item)!;
|
|
659
|
+
return {
|
|
660
|
+
item: result.item,
|
|
661
|
+
keys,
|
|
662
|
+
tier: modelSearchTier(queryKey, keys.compactId, keys.compactSelector),
|
|
663
|
+
bucket: Math.round(result.score / 10),
|
|
664
|
+
fuzzyIndex,
|
|
665
|
+
};
|
|
666
|
+
});
|
|
667
|
+
rows.sort(
|
|
668
|
+
(a, b) =>
|
|
669
|
+
a.tier - b.tier ||
|
|
670
|
+
a.keys.modelAffinity - b.keys.modelAffinity ||
|
|
671
|
+
a.keys.providerAffinity - b.keys.providerAffinity ||
|
|
672
|
+
a.bucket - b.bucket ||
|
|
673
|
+
a.keys.orderGroup - b.keys.orderGroup ||
|
|
674
|
+
a.fuzzyIndex - b.fuzzyIndex,
|
|
675
|
+
);
|
|
676
|
+
return rows.map(row => row.item);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
546
680
|
/** Rank picker and mention candidates by text relevance, user affinity, and MRU/version order. */
|
|
547
681
|
export function rankModelItems(
|
|
548
682
|
query: string,
|
|
549
683
|
items: ReadonlyArray<ModelBrowserItem>,
|
|
550
|
-
options:
|
|
684
|
+
options: ModelRankOptions,
|
|
551
685
|
): ModelBrowserItem[] {
|
|
552
|
-
|
|
553
|
-
const ranked = fuzzyRank(items, query, modelSearchText);
|
|
554
|
-
const matches = ranked.map(result => result.item);
|
|
555
|
-
// Exact and contiguous matches stay ahead of fuzzy-only candidates; affinity
|
|
556
|
-
// breaks ties before fuzzy quality and the normal MRU/version ordering.
|
|
557
|
-
sortModelItems(matches, { roles: options.roles, mruOrder: options.mruOrder, skipRoleRank: true });
|
|
558
|
-
const fallbackRanks = new Map(matches.map((item, index) => [item, index]));
|
|
559
|
-
const queryKey = compactModelSearchText(query);
|
|
560
|
-
const searchRanks = new Map<ModelBrowserItem, { tier: number; bucket: number }>();
|
|
561
|
-
for (const result of ranked) {
|
|
562
|
-
searchRanks.set(result.item, {
|
|
563
|
-
tier: modelSearchTier(queryKey, result.item),
|
|
564
|
-
bucket: Math.round(result.score / 10),
|
|
565
|
-
});
|
|
566
|
-
}
|
|
567
|
-
matches.sort((a, b) => {
|
|
568
|
-
const aSearch = searchRanks.get(a);
|
|
569
|
-
const bSearch = searchRanks.get(b);
|
|
570
|
-
const tierCmp = (aSearch?.tier ?? Number.MAX_SAFE_INTEGER) - (bSearch?.tier ?? Number.MAX_SAFE_INTEGER);
|
|
571
|
-
if (tierCmp !== 0) return tierCmp;
|
|
572
|
-
|
|
573
|
-
const modelCmp =
|
|
574
|
-
(options.affinity.models.get(a.selector.toLowerCase()) ?? Number.MAX_SAFE_INTEGER) -
|
|
575
|
-
(options.affinity.models.get(b.selector.toLowerCase()) ?? Number.MAX_SAFE_INTEGER);
|
|
576
|
-
if (modelCmp !== 0) return modelCmp;
|
|
577
|
-
|
|
578
|
-
const providerCmp =
|
|
579
|
-
(options.affinity.providers.get(a.provider.toLowerCase()) ?? Number.MAX_SAFE_INTEGER) -
|
|
580
|
-
(options.affinity.providers.get(b.provider.toLowerCase()) ?? Number.MAX_SAFE_INTEGER);
|
|
581
|
-
if (providerCmp !== 0) return providerCmp;
|
|
582
|
-
|
|
583
|
-
const bucketCmp = (aSearch?.bucket ?? Number.MAX_SAFE_INTEGER) - (bSearch?.bucket ?? Number.MAX_SAFE_INTEGER);
|
|
584
|
-
if (bucketCmp !== 0) return bucketCmp;
|
|
585
|
-
return (fallbackRanks.get(a) ?? Number.MAX_SAFE_INTEGER) - (fallbackRanks.get(b) ?? Number.MAX_SAFE_INTEGER);
|
|
586
|
-
});
|
|
587
|
-
return matches;
|
|
686
|
+
return new ModelItemRanker(items, options).rank(query);
|
|
588
687
|
}
|
|
589
688
|
|
|
590
689
|
/**
|
|
@@ -710,7 +809,7 @@ function previewPrice(model: Model): string {
|
|
|
710
809
|
return withCreditBadge(model, parts.join(" · "));
|
|
711
810
|
}
|
|
712
811
|
|
|
713
|
-
/** The
|
|
812
|
+
/** The omp theme token of a thinking level's dot (`thinkingHigh`); none for inherit and auto. */
|
|
714
813
|
export function thinkingDotToken(level: ConfiguredThinkingLevel): string | undefined {
|
|
715
814
|
if (level === ThinkingLevel.Inherit || level === AUTO_THINKING) return undefined;
|
|
716
815
|
return `thinking${level.charAt(0).toUpperCase()}${level.slice(1)}`;
|
|
@@ -838,6 +937,24 @@ function metaColumnsWidth(widths: readonly number[]): number {
|
|
|
838
937
|
/** What the per-row perf column shows at the current width. */
|
|
839
938
|
type PerfMode = "off" | "tps" | "full";
|
|
840
939
|
|
|
940
|
+
/** A measured perf aggregate and, when it came from one, its service tier. */
|
|
941
|
+
interface MeasuredPerf {
|
|
942
|
+
perf: ModelBrowserPerf;
|
|
943
|
+
tier?: string;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
/** Plain catalog metric cells of one row and their terminal widths, valid for one model and theme. */
|
|
947
|
+
interface MetricCells {
|
|
948
|
+
model: Model;
|
|
949
|
+
theme: Theme;
|
|
950
|
+
intelligence: string;
|
|
951
|
+
intelligenceWidth: number;
|
|
952
|
+
ctx: string;
|
|
953
|
+
ctxWidth: number;
|
|
954
|
+
cost: string;
|
|
955
|
+
costWidth: number;
|
|
956
|
+
}
|
|
957
|
+
|
|
841
958
|
/**
|
|
842
959
|
* The reusable browser component. Renders a fixed-height block
|
|
843
960
|
* (`maxVisible + LIST_ROW_START + DETAIL_ROWS` rows) so host mouse geometry
|
|
@@ -855,7 +972,17 @@ export class ModelBrowser implements Component {
|
|
|
855
972
|
#roles: RoleAssignments = {};
|
|
856
973
|
#mruOrder: ReadonlyArray<string> = [];
|
|
857
974
|
#affinity: SearchAffinity = { models: new Map(), providers: new Map() };
|
|
975
|
+
/** Ranker for the menu's current item array; rebuilt when items or affinity change. */
|
|
976
|
+
#ranker?: { source: readonly ModelBrowserItem[]; ranker: ModelItemRanker };
|
|
858
977
|
#perf: ReadonlyMap<string, ModelBrowserPerf> = new Map();
|
|
978
|
+
/** Fastest tier aggregate per selector in {@link #perf}, built on the first lookup miss. */
|
|
979
|
+
#perfTiers:
|
|
980
|
+
| { perf: ReadonlyMap<string, ModelBrowserPerf>; size: number; best: Map<string, MeasuredPerf> }
|
|
981
|
+
| undefined;
|
|
982
|
+
/** Theme-formatted catalog metric cells per item; rebuilt when the item's model or the theme changes. */
|
|
983
|
+
#metricCells = new WeakMap<ModelBrowserItem, MetricCells>();
|
|
984
|
+
/** Non-separator items of the last ranked visible list, for {@link queryMatches}. */
|
|
985
|
+
#queryMatches: { visible: readonly ModelBrowserItem[]; matches: readonly ModelBrowserItem[] } | undefined;
|
|
859
986
|
#hoveredIndex: number | null = null;
|
|
860
987
|
#maxVisible = 10;
|
|
861
988
|
#showProvider: boolean;
|
|
@@ -1024,6 +1151,22 @@ export class ModelBrowser implements Component {
|
|
|
1024
1151
|
return this.#menu.visibleItems.length;
|
|
1025
1152
|
}
|
|
1026
1153
|
|
|
1154
|
+
/**
|
|
1155
|
+
* Base items matching the live query, best first, without the separator
|
|
1156
|
+
* row; undefined while the query is blank. Reuses the ranking the list
|
|
1157
|
+
* already ran, so hosts can derive match counts without ranking again.
|
|
1158
|
+
*/
|
|
1159
|
+
get queryMatches(): readonly ModelBrowserItem[] | undefined {
|
|
1160
|
+
if (!this.query.trim()) return undefined;
|
|
1161
|
+
const visible = this.#menu.visibleItems;
|
|
1162
|
+
let cached = this.#queryMatches;
|
|
1163
|
+
if (cached?.visible !== visible) {
|
|
1164
|
+
cached = { visible, matches: visible.filter(item => !this.#isDisabled(item)) };
|
|
1165
|
+
this.#queryMatches = cached;
|
|
1166
|
+
}
|
|
1167
|
+
return cached.matches;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1027
1170
|
/** Move selection to `selector`; false when it is not in the current view. */
|
|
1028
1171
|
selectSelector(selector: string): boolean {
|
|
1029
1172
|
if (!this.#menu.setSelectedKey(selector)) return false;
|
|
@@ -1042,17 +1185,26 @@ export class ModelBrowser implements Component {
|
|
|
1042
1185
|
* unfiltered when the query is blank.
|
|
1043
1186
|
*/
|
|
1044
1187
|
#filterItems(items: readonly ModelBrowserItem[], query: string): readonly ModelBrowserItem[] {
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
? fuzzyRank(base, query, modelSearchText).map(result => result.item)
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1188
|
+
if (this.#preserveQueryOrder) {
|
|
1189
|
+
const base = items.filter(item => !this.#isDisabled(item));
|
|
1190
|
+
return this.#insertSeparator(
|
|
1191
|
+
query.trim() ? fuzzyRank(base, query, modelSearchText).map(result => result.item) : base,
|
|
1192
|
+
);
|
|
1193
|
+
}
|
|
1194
|
+
// The menu hands back the same array until `setItems`, and roles/MRU replace the
|
|
1195
|
+
// affinity object, so one ranker serves every keystroke of a search.
|
|
1196
|
+
let cached = this.#ranker;
|
|
1197
|
+
if (cached?.source !== items || cached.ranker.options.affinity !== this.#affinity) {
|
|
1198
|
+
cached = {
|
|
1199
|
+
source: items,
|
|
1200
|
+
ranker: new ModelItemRanker(
|
|
1201
|
+
items.filter(item => !this.#isDisabled(item)),
|
|
1202
|
+
{ roles: this.#roles, mruOrder: this.#mruOrder, affinity: this.#affinity },
|
|
1203
|
+
),
|
|
1204
|
+
};
|
|
1205
|
+
this.#ranker = cached;
|
|
1206
|
+
}
|
|
1207
|
+
return this.#insertSeparator(cached.ranker.rank(query));
|
|
1056
1208
|
}
|
|
1057
1209
|
|
|
1058
1210
|
/** True when `item`'s context window is smaller than the live session token count (grayed row; hosts compact before switching). */
|
|
@@ -1273,14 +1425,76 @@ export class ModelBrowser implements Component {
|
|
|
1273
1425
|
return index;
|
|
1274
1426
|
}
|
|
1275
1427
|
|
|
1428
|
+
/**
|
|
1429
|
+
* Measured perf for a row: the tier the host would send, else the standard
|
|
1430
|
+
* aggregate, else any measured tier (fastest first) so a model only ever run
|
|
1431
|
+
* on a non-default tier still shows its real speed. `tier` is set whenever
|
|
1432
|
+
* the numbers come from a tier aggregate, for the caller to label.
|
|
1433
|
+
*/
|
|
1434
|
+
#perfFor(item: ModelBrowserItem): MeasuredPerf | undefined {
|
|
1435
|
+
if (this.#perf.size === 0) return undefined;
|
|
1436
|
+
const tier = this.#settings.serviceTierFor?.(item.model);
|
|
1437
|
+
if (tier) {
|
|
1438
|
+
const tiered = this.#perf.get(`${item.selector}@${tier}`);
|
|
1439
|
+
if (tiered) return { perf: tiered, tier };
|
|
1440
|
+
}
|
|
1441
|
+
const standard = this.#perf.get(item.selector);
|
|
1442
|
+
if (standard) return { perf: standard };
|
|
1443
|
+
return this.#fastestTierPerf(item.selector);
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
/** Fastest `selector@tier` aggregate (first wins ties), from an index built once per perf map. */
|
|
1447
|
+
#fastestTierPerf(selector: string): MeasuredPerf | undefined {
|
|
1448
|
+
const perfMap = this.#perf;
|
|
1449
|
+
let index = this.#perfTiers;
|
|
1450
|
+
if (index?.perf !== perfMap || index.size !== perfMap.size) {
|
|
1451
|
+
const best = new Map<string, MeasuredPerf>();
|
|
1452
|
+
for (const [key, perf] of perfMap) {
|
|
1453
|
+
// Every `@` may end the selector: `a@b@t` is tier `b@t` of `a` and tier `t` of `a@b`.
|
|
1454
|
+
for (let at = key.indexOf("@"); at >= 0; at = key.indexOf("@", at + 1)) {
|
|
1455
|
+
const owner = key.slice(0, at);
|
|
1456
|
+
const current = best.get(owner);
|
|
1457
|
+
if (!current || perf.tps > current.perf.tps) best.set(owner, { perf, tier: key.slice(at + 1) });
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
index = { perf: perfMap, size: perfMap.size, best };
|
|
1461
|
+
this.#perfTiers = index;
|
|
1462
|
+
}
|
|
1463
|
+
return index.best.get(selector);
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
/** Catalog metric cells of a row, formatted once per model and theme. */
|
|
1467
|
+
#metricCellsFor(item: ModelBrowserItem): MetricCells {
|
|
1468
|
+
const cached = this.#metricCells.get(item);
|
|
1469
|
+
if (cached?.model === item.model && cached.theme === theme) return cached;
|
|
1470
|
+
const intelligence = formatIntelligence(item.model);
|
|
1471
|
+
const ctx = formatContext(item.model);
|
|
1472
|
+
const cost = formatCostPair(item.model);
|
|
1473
|
+
const cells: MetricCells = {
|
|
1474
|
+
model: item.model,
|
|
1475
|
+
theme,
|
|
1476
|
+
intelligence,
|
|
1477
|
+
intelligenceWidth: visibleWidth(intelligence),
|
|
1478
|
+
ctx,
|
|
1479
|
+
ctxWidth: visibleWidth(ctx),
|
|
1480
|
+
cost,
|
|
1481
|
+
costWidth: visibleWidth(cost),
|
|
1482
|
+
};
|
|
1483
|
+
this.#metricCells.set(item, cells);
|
|
1484
|
+
return cells;
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1276
1487
|
/** Measured TPS/TTFT, falling back to the catalog TPS as an estimated `~118t/s`. */
|
|
1277
1488
|
#perfCell(item: ModelBrowserItem, mode: PerfMode): string {
|
|
1278
1489
|
if (mode === "off") return "";
|
|
1279
|
-
const
|
|
1280
|
-
if (
|
|
1281
|
-
const tps = formatTps(perf.tps);
|
|
1282
|
-
|
|
1283
|
-
|
|
1490
|
+
const measured = this.#perfFor(item);
|
|
1491
|
+
if (measured) {
|
|
1492
|
+
const tps = formatTps(measured.perf.tps);
|
|
1493
|
+
const tier = measured.tier ? ` ${measured.tier}` : "";
|
|
1494
|
+
if (mode === "full" && measured.perf.ttftMs !== null) {
|
|
1495
|
+
return `${formatTtft(measured.perf.ttftMs)} ${tps}${tier}`;
|
|
1496
|
+
}
|
|
1497
|
+
return `${tps}${tier}`;
|
|
1284
1498
|
}
|
|
1285
1499
|
const tps = item.model.tps;
|
|
1286
1500
|
return tps != null && Number.isFinite(tps) && tps > 0 ? `~${formatTps(tps)}` : "";
|
|
@@ -1295,7 +1509,8 @@ export class ModelBrowser implements Component {
|
|
|
1295
1509
|
costWidth: number,
|
|
1296
1510
|
intelligenceWidth: number,
|
|
1297
1511
|
perfWidth: number,
|
|
1298
|
-
|
|
1512
|
+
metrics: MetricCells,
|
|
1513
|
+
perfCell: string,
|
|
1299
1514
|
): string {
|
|
1300
1515
|
if (item.id === "separator") {
|
|
1301
1516
|
const dashCount = Math.max(0, width - 4);
|
|
@@ -1312,21 +1527,17 @@ export class ModelBrowser implements Component {
|
|
|
1312
1527
|
: item.id;
|
|
1313
1528
|
const currentMark =
|
|
1314
1529
|
item.selector === this.#currentSelector ? ` ${theme.fg("success", theme.status.enabled)}` : "";
|
|
1315
|
-
const ompBadge = this.#settings.isOmpOriginProvider?.(item.provider)
|
|
1316
|
-
? ` ${theme.fg("dim", tuiText("models.originOmp", "from OMP"))}`
|
|
1317
|
-
: "";
|
|
1318
1530
|
const overLimit = overContext
|
|
1319
1531
|
? ` ${theme.status.disabled} context>${formatNumber(item.model.contextWindow ?? 0).toLowerCase()}`
|
|
1320
1532
|
: "";
|
|
1321
|
-
let left = `${prefix}${providerPrefix}${name}${
|
|
1533
|
+
let left = `${prefix}${providerPrefix}${name}${currentMark}${overLimit}`;
|
|
1322
1534
|
|
|
1323
1535
|
// Metric columns collapse when empty or when the row needs room for its name.
|
|
1324
1536
|
const cols: string[] = [];
|
|
1325
|
-
if (intelligenceWidth > 0)
|
|
1326
|
-
|
|
1327
|
-
if (
|
|
1328
|
-
if (
|
|
1329
|
-
if (costWidth > 0) cols.push(theme.fg("dim", padLeftVisible(formatCostPair(item.model), costWidth)));
|
|
1537
|
+
if (intelligenceWidth > 0) cols.push(theme.fg("dim", padLeftVisible(metrics.intelligence, intelligenceWidth)));
|
|
1538
|
+
if (perfWidth > 0) cols.push(theme.fg("dim", padLeftVisible(perfCell, perfWidth)));
|
|
1539
|
+
if (ctxWidth > 0) cols.push(theme.fg("dim", padLeftVisible(metrics.ctx, ctxWidth)));
|
|
1540
|
+
if (costWidth > 0) cols.push(theme.fg("dim", padLeftVisible(metrics.cost, costWidth)));
|
|
1330
1541
|
const metaWidth = metaColumnsWidth([intelligenceWidth, perfWidth, ctxWidth, costWidth]);
|
|
1331
1542
|
const available = Math.max(1, width - metaWidth - (cols.length > 0 ? 1 : 0));
|
|
1332
1543
|
left = truncateToWidth(left, available);
|
|
@@ -1365,10 +1576,10 @@ export class ModelBrowser implements Component {
|
|
|
1365
1576
|
if (model.input.includes("image")) facts.push("vision");
|
|
1366
1577
|
const intelligence = formatIntelligence(model);
|
|
1367
1578
|
if (intelligence) facts.push(intelligence);
|
|
1368
|
-
const
|
|
1369
|
-
if (
|
|
1370
|
-
facts.push(`~${formatTps(perf.tps)}`);
|
|
1371
|
-
if (perf.ttftMs !== null) facts.push(`${formatTtft(perf.ttftMs)} ttft`);
|
|
1579
|
+
const measured = this.#perfFor(selected);
|
|
1580
|
+
if (measured) {
|
|
1581
|
+
facts.push(`~${formatTps(measured.perf.tps)}${measured.tier ? ` ${measured.tier}` : ""}`);
|
|
1582
|
+
if (measured.perf.ttftMs !== null) facts.push(`${formatTtft(measured.perf.ttftMs)} ttft`);
|
|
1372
1583
|
} else if (model.tps != null && Number.isFinite(model.tps) && model.tps > 0) {
|
|
1373
1584
|
facts.push(`~${formatTps(model.tps)}`);
|
|
1374
1585
|
}
|
|
@@ -1421,10 +1632,7 @@ export class ModelBrowser implements Component {
|
|
|
1421
1632
|
|
|
1422
1633
|
if (total === 0) {
|
|
1423
1634
|
const message =
|
|
1424
|
-
this.#emptyText?.() ??
|
|
1425
|
-
(this.query.trim()
|
|
1426
|
-
? tuiText("mbNoMatchingModels", " No matching models")
|
|
1427
|
-
: tuiText("mbNoModelsInScope", " No models available in this scope"));
|
|
1635
|
+
this.#emptyText?.() ?? (this.query.trim() ? " No matching models" : " No models available in this scope");
|
|
1428
1636
|
lines.push(truncateToWidth(theme.fg("muted", message), width));
|
|
1429
1637
|
for (let i = 1; i < this.#maxVisible; i++) lines.push("");
|
|
1430
1638
|
} else {
|
|
@@ -1435,15 +1643,22 @@ export class ModelBrowser implements Component {
|
|
|
1435
1643
|
const perfMode: PerfMode = width >= PERF_FULL_MIN_WIDTH ? "full" : width >= PERF_TPS_MIN_WIDTH ? "tps" : "off";
|
|
1436
1644
|
let intelligenceWidth = 0;
|
|
1437
1645
|
let perfWidth = 0;
|
|
1646
|
+
const visible = this.#menu.visibleItems;
|
|
1647
|
+
const windowMetrics: MetricCells[] = [];
|
|
1648
|
+
const windowPerf: string[] = [];
|
|
1438
1649
|
for (let i = startIndex; i < endIndex; i++) {
|
|
1439
|
-
const item =
|
|
1650
|
+
const item = visible[i];
|
|
1440
1651
|
if (!item) continue;
|
|
1441
|
-
|
|
1442
|
-
|
|
1652
|
+
const metrics = this.#metricCellsFor(item);
|
|
1653
|
+
const perfCell = this.#perfCell(item, perfMode);
|
|
1654
|
+
windowMetrics[i - startIndex] = metrics;
|
|
1655
|
+
windowPerf[i - startIndex] = perfCell;
|
|
1656
|
+
ctxWidth = Math.max(ctxWidth, metrics.ctxWidth);
|
|
1657
|
+
costWidth = Math.max(costWidth, metrics.costWidth);
|
|
1443
1658
|
if (perfMode !== "off") {
|
|
1444
|
-
intelligenceWidth = Math.max(intelligenceWidth,
|
|
1659
|
+
intelligenceWidth = Math.max(intelligenceWidth, metrics.intelligenceWidth);
|
|
1445
1660
|
}
|
|
1446
|
-
perfWidth = Math.max(perfWidth, visibleWidth(
|
|
1661
|
+
perfWidth = Math.max(perfWidth, visibleWidth(perfCell));
|
|
1447
1662
|
}
|
|
1448
1663
|
// Preserve at least a readable name by dropping cost, then context.
|
|
1449
1664
|
let nameRoom = width - 2 - metaColumnsWidth([intelligenceWidth, perfWidth, ctxWidth, costWidth]);
|
|
@@ -1453,7 +1668,7 @@ export class ModelBrowser implements Component {
|
|
|
1453
1668
|
|
|
1454
1669
|
const rows: string[] = [];
|
|
1455
1670
|
for (let i = startIndex; i < endIndex; i++) {
|
|
1456
|
-
const item =
|
|
1671
|
+
const item = visible[i];
|
|
1457
1672
|
if (!item) continue;
|
|
1458
1673
|
rows.push(
|
|
1459
1674
|
this.#renderRow(
|
|
@@ -1465,7 +1680,8 @@ export class ModelBrowser implements Component {
|
|
|
1465
1680
|
costWidth,
|
|
1466
1681
|
intelligenceWidth,
|
|
1467
1682
|
perfWidth,
|
|
1468
|
-
|
|
1683
|
+
windowMetrics[i - startIndex]!,
|
|
1684
|
+
windowPerf[i - startIndex]!,
|
|
1469
1685
|
),
|
|
1470
1686
|
);
|
|
1471
1687
|
}
|
|
@@ -1636,10 +1852,10 @@ export class ModelBrowser implements Component {
|
|
|
1636
1852
|
if (model.input.includes("image")) facts.push("vision");
|
|
1637
1853
|
const intelligence = formatIntelligence(model);
|
|
1638
1854
|
if (intelligence) facts.push(intelligence);
|
|
1639
|
-
const
|
|
1640
|
-
if (
|
|
1641
|
-
facts.push(`~${formatTps(perf.tps)}`);
|
|
1642
|
-
if (perf.ttftMs !== null) facts.push(`${formatTtft(perf.ttftMs)} ttft`);
|
|
1855
|
+
const measured = this.#perfFor(selected);
|
|
1856
|
+
if (measured) {
|
|
1857
|
+
facts.push(`~${formatTps(measured.perf.tps)}${measured.tier ? ` ${measured.tier}` : ""}`);
|
|
1858
|
+
if (measured.perf.ttftMs !== null) facts.push(`${formatTtft(measured.perf.ttftMs)} ttft`);
|
|
1643
1859
|
} else if (model.tps != null && Number.isFinite(model.tps) && model.tps > 0) {
|
|
1644
1860
|
facts.push(`~${formatTps(model.tps)}`);
|
|
1645
1861
|
}
|
|
@@ -1751,7 +1967,9 @@ export class ModelBrowser implements Component {
|
|
|
1751
1967
|
const facts: Record<string, TspText | number> = {};
|
|
1752
1968
|
const int = model.int != null && Number.isFinite(model.int) ? model.int : undefined;
|
|
1753
1969
|
if (int !== undefined) facts.int = String(Math.round(int));
|
|
1754
|
-
const speed = this.#perfCell(item, "tps")
|
|
1970
|
+
const speed = this.#perfCell(item, "tps")
|
|
1971
|
+
.replace(/\s*t\/s\s*/, " ")
|
|
1972
|
+
.trim();
|
|
1755
1973
|
if (speed) facts.speed = speed;
|
|
1756
1974
|
if (model.contextWindow) facts.ctx = model.contextWindow;
|
|
1757
1975
|
facts.price = pickerPrice(model);
|
|
@@ -1893,7 +2111,7 @@ export class ModelBrowser implements Component {
|
|
|
1893
2111
|
pickerPreview(mode: "full" | "compact", current?: string): readonly NativeChild[] {
|
|
1894
2112
|
const selected = this.getSelected();
|
|
1895
2113
|
const item = selected && !this.#isDisabled(selected) ? selected : undefined;
|
|
1896
|
-
const key = `${mode}\0${current ?? ""}`;
|
|
2114
|
+
const key = `${mode}\0${current ?? ""}\0${this.#settings.revision}`;
|
|
1897
2115
|
const memo = this.#pickerPreview;
|
|
1898
2116
|
if (
|
|
1899
2117
|
memo !== undefined &&
|
|
@@ -1913,7 +2131,7 @@ export class ModelBrowser implements Component {
|
|
|
1913
2131
|
modelPreview(item: ModelBrowserItem, mode: "full" | "compact", current: string | undefined): NativeChild[] {
|
|
1914
2132
|
const model = item.model;
|
|
1915
2133
|
const selector = `${model.provider}/${model.id}`;
|
|
1916
|
-
const
|
|
2134
|
+
const measured = this.#perfFor(item);
|
|
1917
2135
|
const ctx = model.contextWindow ?? 0;
|
|
1918
2136
|
const out = model.maxTokens ?? 0;
|
|
1919
2137
|
const overContext = this.isOverContext(item);
|
|
@@ -1973,10 +2191,12 @@ export class ModelBrowser implements Component {
|
|
|
1973
2191
|
if (badges.length > 0) children.push(row(badges, { gap: "xs", wrap: true }));
|
|
1974
2192
|
|
|
1975
2193
|
const speed: string[] = [];
|
|
1976
|
-
if (
|
|
1977
|
-
speed.push(
|
|
1978
|
-
|
|
1979
|
-
|
|
2194
|
+
if (measured) {
|
|
2195
|
+
speed.push(
|
|
2196
|
+
`${formatTps(measured.perf.tps).replace("t/s", " t/s")}${measured.tier ? ` ${measured.tier}` : ""}`,
|
|
2197
|
+
);
|
|
2198
|
+
if (measured.perf.ttftMs !== null) speed.push(`${formatTtft(measured.perf.ttftMs).replace("s", " s")} TTFT`);
|
|
2199
|
+
speed.push(`${measured.perf.samples} ${measured.perf.samples === 1 ? "sample" : "samples"}`);
|
|
1980
2200
|
} else if (model.tps != null && Number.isFinite(model.tps) && model.tps > 0) {
|
|
1981
2201
|
speed.push(`~${formatTps(model.tps).replace("t/s", " t/s")} (catalog)`);
|
|
1982
2202
|
}
|
|
@@ -1985,6 +2205,17 @@ export class ModelBrowser implements Component {
|
|
|
1985
2205
|
if (v) facts.push({ k: [span(k, "muted")], v: [span(v, "mono")] });
|
|
1986
2206
|
};
|
|
1987
2207
|
fact("Context", ctx > 0 ? ctx.toLocaleString("en-US") : undefined);
|
|
2208
|
+
const compaction = this.#settings.compactionPointFor?.(model);
|
|
2209
|
+
if (compaction) {
|
|
2210
|
+
const value =
|
|
2211
|
+
compaction.tokens === undefined
|
|
2212
|
+
? "off"
|
|
2213
|
+
: `${compaction.tokens.toLocaleString("en-US")}${compaction.basis !== undefined ? ` · ${compaction.basis}` : ""}`;
|
|
2214
|
+
facts.push({
|
|
2215
|
+
k: [span("Compacts at", "muted")],
|
|
2216
|
+
v: [span(value, "mono"), span(` · ${compaction.source}`, "dim")],
|
|
2217
|
+
});
|
|
2218
|
+
}
|
|
1988
2219
|
fact("Max output", out > 0 ? out.toLocaleString("en-US") : undefined);
|
|
1989
2220
|
fact("Price", isFreeModel(model) ? "free" : `${previewPrice(model)} per M`);
|
|
1990
2221
|
fact("Speed", speed.length > 0 ? speed.join(" · ") : undefined);
|