diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index c54b3386166..5fa79868c88 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -71,6 +71,7 @@ import { ComposerPlanFollowUpBanner } from "./ComposerPlanFollowUpBanner"; import { resolveComposerMenuActiveItemId } from "./composerMenuHighlight"; import { searchSlashMenuItems } from "./composerSlashMenuSearch"; import { + getComposerPromptInjectionState, getComposerProviderState, renderProviderTraitsMenuContent, renderProviderTraitsPicker, @@ -747,18 +748,22 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) [selectedProviderEntry], ); + const composerPromptInjectionState = useMemo( + () => getComposerPromptInjectionState(prompt), + [prompt], + ); const composerProviderState = useMemo( () => getComposerProviderState({ provider: selectedProvider, model: selectedModel, models: selectedProviderModels, - prompt, + promptInjectionState: composerPromptInjectionState, modelOptions: composerModelOptions?.[selectedInstanceId], }), [ composerModelOptions, - prompt, + composerPromptInjectionState, selectedInstanceId, selectedModel, selectedProvider, diff --git a/apps/web/src/components/chat/composerProviderState.test.tsx b/apps/web/src/components/chat/composerProviderState.test.tsx index 07eec55de2c..b0bd2d20c99 100644 --- a/apps/web/src/components/chat/composerProviderState.test.tsx +++ b/apps/web/src/components/chat/composerProviderState.test.tsx @@ -6,6 +6,7 @@ import { type ServerProviderModel, } from "@t3tools/contracts"; import { + getComposerPromptInjectionState, getComposerProviderState, renderProviderTraitsMenuContent, renderProviderTraitsPicker, @@ -61,6 +62,16 @@ const ULTRATHINK_FRAME_CLASSES = { } as const; describe("getComposerProviderState", () => { + it("derives a stable prompt injection state for ordinary prompt edits", () => { + expect(getComposerPromptInjectionState("Investigate this failure")).toBe("none"); + expect(getComposerPromptInjectionState("Ultrathink:\nInvestigate this failure")).toBe( + "ultrathink", + ); + expect(getComposerPromptInjectionState("Ultrathink:\nInvestigate this failure deeply")).toBe( + "ultrathink", + ); + }); + it("returns descriptor defaults when no selections are provided", () => { const state = getComposerProviderState({ provider: PROVIDER, @@ -71,7 +82,6 @@ describe("getComposerProviderState", () => { { id: "high", label: "High", isDefault: true }, ]), ]), - prompt: "", modelOptions: undefined, }); @@ -93,7 +103,6 @@ describe("getComposerProviderState", () => { ]), booleanDescriptor("fastMode"), ]), - prompt: "", modelOptions: selections(["effort", "low"], ["fastMode", true]), }); @@ -112,7 +121,6 @@ describe("getComposerProviderState", () => { selectDescriptor("effort", [{ id: "high", label: "High", isDefault: true }]), booleanDescriptor("fastMode"), ]), - prompt: "", modelOptions: selections(["effort", "high"], ["fastMode", false]), }); @@ -126,7 +134,6 @@ describe("getComposerProviderState", () => { provider: PROVIDER, model: MODEL, models: modelWith([booleanDescriptor("thinking")]), - prompt: "", modelOptions: selections(["effort", "max"], ["thinking", false]), }); @@ -152,7 +159,6 @@ describe("getComposerProviderState", () => { { id: "plan", label: "Plan" }, ]), ]), - prompt: "", modelOptions: selections(["agent", "plan"]), }); @@ -167,7 +173,6 @@ describe("getComposerProviderState", () => { provider: PROVIDER, model: MODEL, models: modelWith([]), - prompt: "", modelOptions: selections(["anything", "value"]), }); @@ -193,7 +198,9 @@ describe("getComposerProviderState", () => { ["ultrathink"], ), ]), - prompt: "Ultrathink:\nInvestigate this failure", + promptInjectionState: getComposerPromptInjectionState( + "Ultrathink:\nInvestigate this failure", + ), modelOptions: selections(["effort", "medium"]), }); @@ -212,7 +219,9 @@ describe("getComposerProviderState", () => { models: modelWith([ selectDescriptor("effort", [{ id: "high", label: "High", isDefault: true }]), ]), - prompt: "Ultrathink:\nInvestigate this failure", + promptInjectionState: getComposerPromptInjectionState( + "Ultrathink:\nInvestigate this failure", + ), modelOptions: undefined, }); diff --git a/apps/web/src/components/chat/composerProviderState.tsx b/apps/web/src/components/chat/composerProviderState.tsx index b5cc790538d..ed1c4fef8f1 100644 --- a/apps/web/src/components/chat/composerProviderState.tsx +++ b/apps/web/src/components/chat/composerProviderState.tsx @@ -21,10 +21,13 @@ export type ComposerProviderStateInput = { provider: ProviderDriverKind; model: string; models: ReadonlyArray; - prompt: string; + // Coarse prompt-derived state keeps provider chrome stable across ordinary body edits. + promptInjectionState?: ComposerPromptInjectionState; modelOptions: ReadonlyArray | null | undefined; }; +export type ComposerPromptInjectionState = "none" | "ultrathink"; + export type ComposerProviderState = { provider: ProviderDriverKind; promptEffort: string | null; @@ -46,8 +49,12 @@ type TraitsRenderInput = { onPromptChange: (prompt: string) => void; }; +export function getComposerPromptInjectionState(prompt: string): ComposerPromptInjectionState { + return isClaudeUltrathinkPrompt(prompt) ? "ultrathink" : "none"; +} + export function getComposerProviderState(input: ComposerProviderStateInput): ComposerProviderState { - const { provider, model, models, prompt, modelOptions } = input; + const { provider, model, models, modelOptions, promptInjectionState = "none" } = input; const caps = getProviderModelCapabilities(models, model, provider); const descriptors = getProviderOptionDescriptors({ caps, selections: modelOptions }); const primarySelectDescriptor = descriptors.find( @@ -58,7 +65,7 @@ export function getComposerProviderState(input: ComposerProviderStateInput): Com const promptEffort = typeof primaryValue === "string" ? primaryValue : null; const ultrathinkActive = (primarySelectDescriptor?.promptInjectedValues?.length ?? 0) > 0 && - isClaudeUltrathinkPrompt(prompt); + promptInjectionState === "ultrathink"; return { provider, diff --git a/apps/web/src/index.css b/apps/web/src/index.css index a48a00c9bc7..8ba440e095c 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -707,12 +707,35 @@ label:has(> select#reasoning-effort) select { #ec4899 90%, #ff6b6b 100% ); + position: relative; + background: transparent; +} + +.ultrathink-frame::before { + content: ""; + position: absolute; + inset: 0; + border-radius: inherit; + padding: 2px; background-image: var(--ultrathink-spectrum); background-size: 220% 220%; + pointer-events: none; filter: saturate(0.82) brightness(0.92); + -webkit-mask: + linear-gradient(#000 0 0) content-box, + linear-gradient(#000 0 0); + -webkit-mask-composite: xor; + mask: + linear-gradient(#000 0 0) content-box, + linear-gradient(#000 0 0); + mask-composite: exclude; animation: ultrathink-rainbow 10s linear infinite; } +.ultrathink-frame > * { + position: relative; +} + .ultrathink-chroma { animation: ultrathink-chroma-shift 10s linear infinite; } diff --git a/apps/web/src/modelSelection.ts b/apps/web/src/modelSelection.ts index 0fcf680b732..7ede9665ac9 100644 --- a/apps/web/src/modelSelection.ts +++ b/apps/web/src/modelSelection.ts @@ -303,7 +303,6 @@ export function resolveAppModelSelectionState( provider, model, models: entry.models, - prompt: "", modelOptions: selectedEntry ? selection.options : undefined, }); @@ -321,7 +320,6 @@ export function resolveAppModelSelectionState( provider, model, models: getProviderModels(providers, provider), - prompt: "", modelOptions: keptSelectedProvider ? selection.options : undefined, });