| author | |
| committer | |
| log | ca37b19cf8d3882f0b4eee1b9e49050494f30422 |
| tree | 369a1800a5263c9fb10adb8b92bea5be9eb04762 |
| parent | d5465aebf2746b8d5f327be3b2424d9412a29075 |
| signature | Signed by PGP key B5690EEEBB952194 |
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 | 103 | renderProviderTraitsPicker, |
| 104 | 104 | } from "./composerProviderState"; |
| 105 | 105 | import { ContextWindowMeter } from "./ContextWindowMeter"; |
| 106 | import { resolveContextWindowModelDisplayName } from "./ContextWindowMeter.logic"; | |
| 106 | 107 | import { buildExpandedImagePreview, type ExpandedImagePreview } from "./ExpandedImagePreview"; |
| 107 | 108 | import { basenameOfPath } from "../../pierre-icons"; |
| 108 | 109 | import { cn, randomUUID } from "~/lib/utils"; |
| ... | ... | @@ -210,7 +211,7 @@ import { |
| 210 | 211 | XIcon, |
| 211 | 212 | } from "lucide-react"; |
| 212 | 213 | import { proposedPlanTitle } from "../../proposedPlan"; |
| 213 | import { getProviderDisplayName, getProviderInteractionModeToggle } from "../../providerModels"; | |
| 214 | import { getProviderInteractionModeToggle } from "../../providerModels"; | |
| 214 | 215 | import { |
| 215 | 216 | applyProviderInstanceSettings, |
| 216 | 217 | deriveProviderInstanceEntries, |
| ... | ... | @@ -225,10 +226,7 @@ import type { UnifiedSettings } from "@t3tools/contracts/settings"; |
| 225 | 226 | import type { SessionPhase, Thread } from "../../types"; |
| 226 | 227 | import type { PendingUserInputDraftAnswer } from "../../pendingUserInput"; |
| 227 | 228 | import type { PendingApproval, PendingUserInput } from "../../session-logic"; |
| 228 | import { | |
| 229 | deriveLatestContextWindowSnapshot, | |
| 230 | formatProviderDisplayName, | |
| 231 | } from "../../lib/contextWindow"; | |
| 229 | import { deriveLatestContextWindowSnapshot } from "../../lib/contextWindow"; | |
| 232 | 230 | import { formatProviderSkillDisplayName } from "../../providerSkillPresentation"; |
| 233 | 231 | import { searchProviderSkills } from "../../providerSkillSearch"; |
| 234 | 232 | import { useMediaQuery } from "../../hooks/useMediaQuery"; |
| ... | ... | @@ -396,7 +394,7 @@ const ComposerFooterModeControls = memo(function ComposerFooterModeControls(prop |
| 396 | 394 | const ComposerFooterPrimaryActions = memo(function ComposerFooterPrimaryActions(props: { |
| 397 | 395 | compact: boolean; |
| 398 | 396 | activeContextWindow: ReturnType<typeof deriveLatestContextWindowSnapshot>; |
| 399 | activeThreadProviderDisplayName: string | null; | |
| 397 | activeThreadModelDisplayName: string | null; | |
| 400 | 398 | isPreparingWorktree: boolean; |
| 401 | 399 | pendingAction: { |
| 402 | 400 | questionIndex: number; |
| ... | ... | @@ -424,7 +422,7 @@ const ComposerFooterPrimaryActions = memo(function ComposerFooterPrimaryActions( |
| 424 | 422 | {props.activeContextWindow ? ( |
| 425 | 423 | <ContextWindowMeter |
| 426 | 424 | usage={props.activeContextWindow} |
| 427 | providerDisplayName={props.activeThreadProviderDisplayName} | |
| 425 | modelDisplayName={props.activeThreadModelDisplayName} | |
| 428 | 426 | /> |
| 429 | 427 | ) : null} |
| 430 | 428 | {props.isPreparingWorktree ? ( |
| ... | ... | @@ -930,16 +928,10 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) |
| 930 | 928 | () => deriveLatestContextWindowSnapshot(activeThreadActivities ?? []), |
| 931 | 929 | [activeThreadActivities], |
| 932 | 930 | ); |
| 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 | ); | |
| 943 | 935 | |
| 944 | 936 | // ------------------------------------------------------------------ |
| 945 | 937 | // Composer-local state |
| ... | ... | @@ -3222,7 +3214,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) |
| 3222 | 3214 | <ComposerFooterPrimaryActions |
| 3223 | 3215 | compact={isComposerPrimaryActionsCompact} |
| 3224 | 3216 | activeContextWindow={activeContextWindow} |
| 3225 | activeThreadProviderDisplayName={activeThreadProviderDisplayName} | |
| 3217 | activeThreadModelDisplayName={activeThreadModelDisplayName} | |
| 3226 | 3218 | pendingAction={pendingPrimaryAction} |
| 3227 | 3219 | isRunning={phase === "running"} |
| 3228 | 3220 | showPlanFollowUpPrompt={pendingUserInputs.length === 0 && showPlanFollowUpPrompt} |
apps/web/src/components/chat/ContextWindowMeter.logic.test.ts created+58| ... | ... | @@ -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 | }); |
apps/web/src/components/chat/ContextWindowMeter.logic.ts created+25| ... | ... | @@ -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+4-3| ... | ... | @@ -1,6 +1,7 @@ |
| 1 | 1 | import { Button } from "../ui/button"; |
| 2 | 2 | import { type ContextWindowSnapshot, formatContextWindowTokens } from "~/lib/contextWindow"; |
| 3 | 3 | import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover"; |
| 4 | import { formatContextWindowCompactionMessage } from "./ContextWindowMeter.logic"; | |
| 4 | 5 | |
| 5 | 6 | function formatPercentage(value: number | null): string | null { |
| 6 | 7 | if (value === null || !Number.isFinite(value)) { |
| ... | ... | @@ -14,9 +15,9 @@ function formatPercentage(value: number | null): string | null { |
| 14 | 15 | |
| 15 | 16 | export function ContextWindowMeter(props: { |
| 16 | 17 | usage: ContextWindowSnapshot; |
| 17 | providerDisplayName?: string | null; | |
| 18 | modelDisplayName?: string | null; | |
| 18 | 19 | }) { |
| 19 | const { usage, providerDisplayName } = props; | |
| 20 | const { usage, modelDisplayName } = props; | |
| 20 | 21 | const usedPercentage = formatPercentage(usage.usedPercentage); |
| 21 | 22 | const normalizedPercentage = Math.max(0, Math.min(100, usage.usedPercentage ?? 0)); |
| 22 | 23 | const radius = 9.75; |
| ... | ... | @@ -127,7 +128,7 @@ export function ContextWindowMeter(props: { |
| 127 | 128 | ) : null} |
| 128 | 129 | {usage.compactsAutomatically ? ( |
| 129 | 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 | 132 | </div> |
| 132 | 133 | ) : null} |
| 133 | 134 | </div> |