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 {
103103 renderProviderTraitsPicker,
104104} from "./composerProviderState";
105105import { ContextWindowMeter } from "./ContextWindowMeter";
106import { resolveContextWindowModelDisplayName } from "./ContextWindowMeter.logic";
106107import { buildExpandedImagePreview, type ExpandedImagePreview } from "./ExpandedImagePreview";
107108import { basenameOfPath } from "../../pierre-icons";
108109import { cn, randomUUID } from "~/lib/utils";
......@@ -210,7 +211,7 @@ import {
210211 XIcon,
211212} from "lucide-react";
212213import { proposedPlanTitle } from "../../proposedPlan";
213import { getProviderDisplayName, getProviderInteractionModeToggle } from "../../providerModels";
214import { getProviderInteractionModeToggle } from "../../providerModels";
214215import {
215216 applyProviderInstanceSettings,
216217 deriveProviderInstanceEntries,
......@@ -225,10 +226,7 @@ import type { UnifiedSettings } from "@t3tools/contracts/settings";
225226import type { SessionPhase, Thread } from "../../types";
226227import type { PendingUserInputDraftAnswer } from "../../pendingUserInput";
227228import type { PendingApproval, PendingUserInput } from "../../session-logic";
228import {
229 deriveLatestContextWindowSnapshot,
230 formatProviderDisplayName,
231} from "../../lib/contextWindow";
229import { deriveLatestContextWindowSnapshot } from "../../lib/contextWindow";
232230import { formatProviderSkillDisplayName } from "../../providerSkillPresentation";
233231import { searchProviderSkills } from "../../providerSkillSearch";
234232import { useMediaQuery } from "../../hooks/useMediaQuery";
......@@ -396,7 +394,7 @@ const ComposerFooterModeControls = memo(function ComposerFooterModeControls(prop
396394const ComposerFooterPrimaryActions = memo(function ComposerFooterPrimaryActions(props: {
397395 compact: boolean;
398396 activeContextWindow: ReturnType<typeof deriveLatestContextWindowSnapshot>;
399 activeThreadProviderDisplayName: string | null;
397 activeThreadModelDisplayName: string | null;
400398 isPreparingWorktree: boolean;
401399 pendingAction: {
402400 questionIndex: number;
......@@ -424,7 +422,7 @@ const ComposerFooterPrimaryActions = memo(function ComposerFooterPrimaryActions(
424422 {props.activeContextWindow ? (
425423 <ContextWindowMeter
426424 usage={props.activeContextWindow}
427 providerDisplayName={props.activeThreadProviderDisplayName}
425 modelDisplayName={props.activeThreadModelDisplayName}
428426 />
429427 ) : null}
430428 {props.isPreparingWorktree ? (
......@@ -930,16 +928,10 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
930928 () => deriveLatestContextWindowSnapshot(activeThreadActivities ?? []),
931929 [activeThreadActivities],
932930 );
933 const activeThreadProviderDisplayName = useMemo(() => {
934 if (!activeThreadModelSelection) return null;
935 const entry = providerStatuses.find(
936 (p) => p.instanceId === activeThreadModelSelection.instanceId,
937 );
938 if (entry) {
939 return getProviderDisplayName(providerStatuses, entry.driver);
940 }
941 return formatProviderDisplayName(activeThreadModelSelection.instanceId);
942 }, [providerStatuses, activeThreadModelSelection]);
931 const activeThreadModelDisplayName = useMemo(
932 () => resolveContextWindowModelDisplayName(activeThreadModelSelection, modelOptionsByInstance),
933 [activeThreadModelSelection, modelOptionsByInstance],
934 );
943935
944936 // ------------------------------------------------------------------
945937 // Composer-local state
......@@ -3222,7 +3214,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
32223214 <ComposerFooterPrimaryActions
32233215 compact={isComposerPrimaryActionsCompact}
32243216 activeContextWindow={activeContextWindow}
3225 activeThreadProviderDisplayName={activeThreadProviderDisplayName}
3217 activeThreadModelDisplayName={activeThreadModelDisplayName}
32263218 pendingAction={pendingPrimaryAction}
32273219 isRunning={phase === "running"}
32283220 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 @@
11import { Button } from "../ui/button";
22import { type ContextWindowSnapshot, formatContextWindowTokens } from "~/lib/contextWindow";
33import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover";
4import { formatContextWindowCompactionMessage } from "./ContextWindowMeter.logic";
45
56function formatPercentage(value: number | null): string | null {
67 if (value === null || !Number.isFinite(value)) {
......@@ -14,9 +15,9 @@ function formatPercentage(value: number | null): string | null {
1415
1516export function ContextWindowMeter(props: {
1617 usage: ContextWindowSnapshot;
17 providerDisplayName?: string | null;
18 modelDisplayName?: string | null;
1819}) {
19 const { usage, providerDisplayName } = props;
20 const { usage, modelDisplayName } = props;
2021 const usedPercentage = formatPercentage(usage.usedPercentage);
2122 const normalizedPercentage = Math.max(0, Math.min(100, usage.usedPercentage ?? 0));
2223 const radius = 9.75;
......@@ -127,7 +128,7 @@ export function ContextWindowMeter(props: {
127128 ) : null}
128129 {usage.compactsAutomatically ? (
129130 <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)}
131132 </div>
132133 ) : null}
133134 </div>