authorgravatar for 81386789+nqrwhal@users.noreply.github.comnqrwhal <81386789+nqrwhal@users.noreply.github.com> 2026-08-15 05:07:53-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-15 12:07:53+00:00
logca37b19cf8d3882f0b4eee1b9e49050494f30422
tree369a1800a5263c9fb10adb8b92bea5be9eb04762
parentd5465aebf2746b8d5f327be3b2424d9412a29075
signature Signed by PGP key B5690EEEBB952194

fix(web): show selected model in context window tooltip (#4772)

Co-authored-by: Julius Marminge <julius0216@outlook.com> Co-authored-by: codex <codex@users.noreply.github.com>

4 files changed, 97 insertions(+), 21 deletions(-)

apps/web/src/components/chat/ChatComposer.tsx+10-18
...@@ -103,6 +103,7 @@ import {...@@ -103,6 +103,7 @@ import {
103 renderProviderTraitsPicker,103 renderProviderTraitsPicker,
104} from "./composerProviderState";104} from "./composerProviderState";
105import { ContextWindowMeter } from "./ContextWindowMeter";105import { ContextWindowMeter } from "./ContextWindowMeter";
106import { resolveContextWindowModelDisplayName } from "./ContextWindowMeter.logic";
106import { buildExpandedImagePreview, type ExpandedImagePreview } from "./ExpandedImagePreview";107import { buildExpandedImagePreview, type ExpandedImagePreview } from "./ExpandedImagePreview";
107import { basenameOfPath } from "../../pierre-icons";108import { basenameOfPath } from "../../pierre-icons";
108import { cn, randomUUID } from "~/lib/utils";109import { cn, randomUUID } from "~/lib/utils";
...@@ -210,7 +211,7 @@ import {...@@ -210,7 +211,7 @@ import {
210 XIcon,211 XIcon,
211} from "lucide-react";212} from "lucide-react";
212import { proposedPlanTitle } from "../../proposedPlan";213import { proposedPlanTitle } from "../../proposedPlan";
213import { getProviderDisplayName, getProviderInteractionModeToggle } from "../../providerModels";214import { getProviderInteractionModeToggle } from "../../providerModels";
214import {215import {
215 applyProviderInstanceSettings,216 applyProviderInstanceSettings,
216 deriveProviderInstanceEntries,217 deriveProviderInstanceEntries,
...@@ -225,10 +226,7 @@ import type { UnifiedSettings } from "@t3tools/contracts/settings";...@@ -225,10 +226,7 @@ import type { UnifiedSettings } from "@t3tools/contracts/settings";
225import type { SessionPhase, Thread } from "../../types";226import type { SessionPhase, Thread } from "../../types";
226import type { PendingUserInputDraftAnswer } from "../../pendingUserInput";227import type { PendingUserInputDraftAnswer } from "../../pendingUserInput";
227import type { PendingApproval, PendingUserInput } from "../../session-logic";228import type { PendingApproval, PendingUserInput } from "../../session-logic";
228import {229import { deriveLatestContextWindowSnapshot } from "../../lib/contextWindow";
229 deriveLatestContextWindowSnapshot,
230 formatProviderDisplayName,
231} from "../../lib/contextWindow";
232import { formatProviderSkillDisplayName } from "../../providerSkillPresentation";230import { formatProviderSkillDisplayName } from "../../providerSkillPresentation";
233import { searchProviderSkills } from "../../providerSkillSearch";231import { searchProviderSkills } from "../../providerSkillSearch";
234import { useMediaQuery } from "../../hooks/useMediaQuery";232import { useMediaQuery } from "../../hooks/useMediaQuery";
...@@ -396,7 +394,7 @@ const ComposerFooterModeControls = memo(function ComposerFooterModeControls(prop...@@ -396,7 +394,7 @@ const ComposerFooterModeControls = memo(function ComposerFooterModeControls(prop
396const ComposerFooterPrimaryActions = memo(function ComposerFooterPrimaryActions(props: {394const ComposerFooterPrimaryActions = memo(function ComposerFooterPrimaryActions(props: {
397 compact: boolean;395 compact: boolean;
398 activeContextWindow: ReturnType<typeof deriveLatestContextWindowSnapshot>;396 activeContextWindow: ReturnType<typeof deriveLatestContextWindowSnapshot>;
399 activeThreadProviderDisplayName: string | null;397 activeThreadModelDisplayName: string | null;
400 isPreparingWorktree: boolean;398 isPreparingWorktree: boolean;
401 pendingAction: {399 pendingAction: {
402 questionIndex: number;400 questionIndex: number;
...@@ -424,7 +422,7 @@ const ComposerFooterPrimaryActions = memo(function ComposerFooterPrimaryActions(...@@ -424,7 +422,7 @@ const ComposerFooterPrimaryActions = memo(function ComposerFooterPrimaryActions(
424 {props.activeContextWindow ? (422 {props.activeContextWindow ? (
425 <ContextWindowMeter423 <ContextWindowMeter
426 usage={props.activeContextWindow}424 usage={props.activeContextWindow}
427 providerDisplayName={props.activeThreadProviderDisplayName}425 modelDisplayName={props.activeThreadModelDisplayName}
428 />426 />
429 ) : null}427 ) : null}
430 {props.isPreparingWorktree ? (428 {props.isPreparingWorktree ? (
...@@ -930,16 +928,10 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -930,16 +928,10 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
930 () => deriveLatestContextWindowSnapshot(activeThreadActivities ?? []),928 () => deriveLatestContextWindowSnapshot(activeThreadActivities ?? []),
931 [activeThreadActivities],929 [activeThreadActivities],
932 );930 );
933 const activeThreadProviderDisplayName = useMemo(() => {931 const activeThreadModelDisplayName = useMemo(
934 if (!activeThreadModelSelection) return null;932 () => resolveContextWindowModelDisplayName(activeThreadModelSelection, modelOptionsByInstance),
935 const entry = providerStatuses.find(933 [activeThreadModelSelection, modelOptionsByInstance],
936 (p) => p.instanceId === activeThreadModelSelection.instanceId,934 );
937 );
938 if (entry) {
939 return getProviderDisplayName(providerStatuses, entry.driver);
940 }
941 return formatProviderDisplayName(activeThreadModelSelection.instanceId);
942 }, [providerStatuses, activeThreadModelSelection]);
943935
944 // ------------------------------------------------------------------936 // ------------------------------------------------------------------
945 // Composer-local state937 // Composer-local state
...@@ -3222,7 +3214,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -3222,7 +3214,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
3222 <ComposerFooterPrimaryActions3214 <ComposerFooterPrimaryActions
3223 compact={isComposerPrimaryActionsCompact}3215 compact={isComposerPrimaryActionsCompact}
3224 activeContextWindow={activeContextWindow}3216 activeContextWindow={activeContextWindow}
3225 activeThreadProviderDisplayName={activeThreadProviderDisplayName}3217 activeThreadModelDisplayName={activeThreadModelDisplayName}
3226 pendingAction={pendingPrimaryAction}3218 pendingAction={pendingPrimaryAction}
3227 isRunning={phase === "running"}3219 isRunning={phase === "running"}
3228 showPlanFollowUpPrompt={pendingUserInputs.length === 0 && showPlanFollowUpPrompt}3220 showPlanFollowUpPrompt={pendingUserInputs.length === 0 && showPlanFollowUpPrompt}
apps/web/src/components/chat/ContextWindowMeter.logic.test.ts created+58
...@@ -0,0 +1,58 @@
1import { ProviderInstanceId } from "@t3tools/contracts";
2import { describe, expect, it } from "vite-plus/test";
3import {
4 formatContextWindowCompactionMessage,
5 resolveContextWindowModelDisplayName,
6} from "./ContextWindowMeter.logic";
7
8describe("resolveContextWindowModelDisplayName", () => {
9 it("uses the selected model from the exact provider instance", () => {
10 const primaryInstanceId = ProviderInstanceId.make("codex");
11 const selectedInstanceId = ProviderInstanceId.make("codex-work");
12 const modelOptionsByInstance = new Map([
13 [
14 primaryInstanceId,
15 [{ slug: "gpt-5.6-sol", name: "Primary profile model", shortName: "Primary" }],
16 ],
17 [selectedInstanceId, [{ slug: "gpt-5.6-sol", name: "GPT-5.6 Sol", shortName: "5.6 Sol" }]],
18 ]);
19
20 expect(
21 resolveContextWindowModelDisplayName(
22 {
23 instanceId: selectedInstanceId,
24 model: "gpt-5.6-sol",
25 },
26 modelOptionsByInstance,
27 ),
28 ).toBe("5.6 Sol");
29 });
30
31 it("falls back to the selected model slug when model metadata is unavailable", () => {
32 const selectedInstanceId = ProviderInstanceId.make("codex-work");
33
34 expect(
35 resolveContextWindowModelDisplayName(
36 {
37 instanceId: selectedInstanceId,
38 model: "custom-model",
39 },
40 new Map(),
41 ),
42 ).toBe("custom-model");
43 });
44});
45
46describe("formatContextWindowCompactionMessage", () => {
47 it("describes compaction in terms of the selected model", () => {
48 expect(formatContextWindowCompactionMessage("GPT-5.6 Sol")).toBe(
49 "Context for GPT-5.6 Sol compacts automatically when needed.",
50 );
51 });
52
53 it("uses neutral copy when the model is unavailable", () => {
54 expect(formatContextWindowCompactionMessage(null)).toBe(
55 "Context compacts automatically when needed.",
56 );
57 });
58});
apps/web/src/components/chat/ContextWindowMeter.logic.ts created+25
...@@ -0,0 +1,25 @@
1import type { ModelSelection, ProviderInstanceId } from "@t3tools/contracts";
2import { getTriggerDisplayModelName, type ModelEsque } from "./providerIconUtils";
3
4export function resolveContextWindowModelDisplayName(
5 selection: ModelSelection | null | undefined,
6 modelOptionsByInstance: ReadonlyMap<ProviderInstanceId, ReadonlyArray<ModelEsque>>,
7): string | null {
8 if (!selection) {
9 return null;
10 }
11
12 const selectedModel = modelOptionsByInstance
13 .get(selection.instanceId)
14 ?.find((model) => model.slug === selection.model);
15
16 return selectedModel ? getTriggerDisplayModelName(selectedModel) : selection.model;
17}
18
19export function formatContextWindowCompactionMessage(
20 modelDisplayName: string | null | undefined,
21): string {
22 return modelDisplayName
23 ? `Context for ${modelDisplayName} compacts automatically when needed.`
24 : "Context compacts automatically when needed.";
25}
apps/web/src/components/chat/ContextWindowMeter.tsx+4-3
...@@ -1,6 +1,7 @@...@@ -1,6 +1,7 @@
1import { Button } from "../ui/button";1import { Button } from "../ui/button";
2import { type ContextWindowSnapshot, formatContextWindowTokens } from "~/lib/contextWindow";2import { type ContextWindowSnapshot, formatContextWindowTokens } from "~/lib/contextWindow";
3import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover";3import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover";
4import { formatContextWindowCompactionMessage } from "./ContextWindowMeter.logic";
45
5function formatPercentage(value: number | null): string | null {6function formatPercentage(value: number | null): string | null {
6 if (value === null || !Number.isFinite(value)) {7 if (value === null || !Number.isFinite(value)) {
...@@ -14,9 +15,9 @@ function formatPercentage(value: number | null): string | null {...@@ -14,9 +15,9 @@ function formatPercentage(value: number | null): string | null {
1415
15export function ContextWindowMeter(props: {16export function ContextWindowMeter(props: {
16 usage: ContextWindowSnapshot;17 usage: ContextWindowSnapshot;
17 providerDisplayName?: string | null;18 modelDisplayName?: string | null;
18}) {19}) {
19 const { usage, providerDisplayName } = props;20 const { usage, modelDisplayName } = props;
20 const usedPercentage = formatPercentage(usage.usedPercentage);21 const usedPercentage = formatPercentage(usage.usedPercentage);
21 const normalizedPercentage = Math.max(0, Math.min(100, usage.usedPercentage ?? 0));22 const normalizedPercentage = Math.max(0, Math.min(100, usage.usedPercentage ?? 0));
22 const radius = 9.75;23 const radius = 9.75;
...@@ -127,7 +128,7 @@ export function ContextWindowMeter(props: {...@@ -127,7 +128,7 @@ export function ContextWindowMeter(props: {
127 ) : null}128 ) : null}
128 {usage.compactsAutomatically ? (129 {usage.compactsAutomatically ? (
129 <div className="mt-1 text-pretty text-secondary-label text-[11px] font-medium">130 <div className="mt-1 text-pretty text-secondary-label text-[11px] font-medium">
130 {providerDisplayName ?? "It"} automatically compacts its context when needed.131 {formatContextWindowCompactionMessage(modelDisplayName)}
131 </div>132 </div>
132 ) : null}133 ) : null}
133 </div>134 </div>