Skip to content

Commit ca37b19

Browse files
nqrwhaljuliusmarmingecodex
authored
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>
1 parent d5465ae commit ca37b19

4 files changed

Lines changed: 97 additions & 21 deletions

File tree

apps/web/src/components/chat/ChatComposer.tsx

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ import {
103103
renderProviderTraitsPicker,
104104
} from "./composerProviderState";
105105
import { ContextWindowMeter } from "./ContextWindowMeter";
106+
import { resolveContextWindowModelDisplayName } from "./ContextWindowMeter.logic";
106107
import { buildExpandedImagePreview, type ExpandedImagePreview } from "./ExpandedImagePreview";
107108
import { basenameOfPath } from "../../pierre-icons";
108109
import { cn, randomUUID } from "~/lib/utils";
@@ -210,7 +211,7 @@ import {
210211
XIcon,
211212
} from "lucide-react";
212213
import { proposedPlanTitle } from "../../proposedPlan";
213-
import { getProviderDisplayName, getProviderInteractionModeToggle } from "../../providerModels";
214+
import { getProviderInteractionModeToggle } from "../../providerModels";
214215
import {
215216
applyProviderInstanceSettings,
216217
deriveProviderInstanceEntries,
@@ -225,10 +226,7 @@ import type { UnifiedSettings } from "@t3tools/contracts/settings";
225226
import type { SessionPhase, Thread } from "../../types";
226227
import type { PendingUserInputDraftAnswer } from "../../pendingUserInput";
227228
import type { PendingApproval, PendingUserInput } from "../../session-logic";
228-
import {
229-
deriveLatestContextWindowSnapshot,
230-
formatProviderDisplayName,
231-
} from "../../lib/contextWindow";
229+
import { deriveLatestContextWindowSnapshot } from "../../lib/contextWindow";
232230
import { formatProviderSkillDisplayName } from "../../providerSkillPresentation";
233231
import { searchProviderSkills } from "../../providerSkillSearch";
234232
import { useMediaQuery } from "../../hooks/useMediaQuery";
@@ -396,7 +394,7 @@ const ComposerFooterModeControls = memo(function ComposerFooterModeControls(prop
396394
const 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}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { ProviderInstanceId } from "@t3tools/contracts";
2+
import { describe, expect, it } from "vite-plus/test";
3+
import {
4+
formatContextWindowCompactionMessage,
5+
resolveContextWindowModelDisplayName,
6+
} from "./ContextWindowMeter.logic";
7+
8+
describe("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+
46+
describe("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+
});
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import type { ModelSelection, ProviderInstanceId } from "@t3tools/contracts";
2+
import { getTriggerDisplayModelName, type ModelEsque } from "./providerIconUtils";
3+
4+
export 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+
19+
export 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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Button } from "../ui/button";
22
import { type ContextWindowSnapshot, formatContextWindowTokens } from "~/lib/contextWindow";
33
import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover";
4+
import { formatContextWindowCompactionMessage } from "./ContextWindowMeter.logic";
45

56
function 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

1516
export 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>

0 commit comments

Comments
 (0)