From 7108d244d10efa9c1eefb4af37ba2ff545647800 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Mon, 16 Feb 2026 13:44:54 +0900 Subject: [PATCH] fix: preserve user-selected variant on first message instead of overriding with fallback chain default First message variant gate was unconditionally overwriting message.variant with the fallback chain value (e.g. 'medium' for Hephaestus), ignoring any variant the user had already selected via OpenCode UI. Now checks message.variant === undefined before applying the resolved variant, matching the behavior already used for subsequent messages. Closes #1861 --- src/plugin/chat-message.test.ts | 118 ++++++++++++++++++++++++++++++++ src/plugin/chat-message.ts | 14 ++-- 2 files changed, 126 insertions(+), 6 deletions(-) create mode 100644 src/plugin/chat-message.test.ts diff --git a/src/plugin/chat-message.test.ts b/src/plugin/chat-message.test.ts new file mode 100644 index 000000000..4b5108add --- /dev/null +++ b/src/plugin/chat-message.test.ts @@ -0,0 +1,118 @@ +import { describe, test, expect } from "bun:test" + +import { createChatMessageHandler } from "./chat-message" + +type ChatMessagePart = { type: string; text?: string; [key: string]: unknown } +type ChatMessageHandlerOutput = { message: Record; parts: ChatMessagePart[] } + +function createMockHandlerArgs(overrides?: { + pluginConfig?: Record + shouldOverride?: boolean +}) { + const appliedSessions: string[] = [] + return { + ctx: { client: { tui: { showToast: async () => {} } } } as any, + pluginConfig: (overrides?.pluginConfig ?? {}) as any, + firstMessageVariantGate: { + shouldOverride: () => overrides?.shouldOverride ?? false, + markApplied: (sessionID: string) => { appliedSessions.push(sessionID) }, + }, + hooks: { + stopContinuationGuard: null, + keywordDetector: null, + claudeCodeHooks: null, + autoSlashCommand: null, + startWork: null, + ralphLoop: null, + } as any, + _appliedSessions: appliedSessions, + } +} + +function createMockInput(agent?: string, model?: { providerID: string; modelID: string }) { + return { + sessionID: "test-session", + agent, + model, + } +} + +function createMockOutput(variant?: string): ChatMessageHandlerOutput { + const message: Record = {} + if (variant !== undefined) { + message["variant"] = variant + } + return { message, parts: [] } +} + +describe("createChatMessageHandler - first message variant", () => { + test("first message: sets variant from fallback chain when user has no selection", async () => { + //#given - first message, no user-selected variant, hephaestus with medium in chain + const args = createMockHandlerArgs({ shouldOverride: true }) + const handler = createChatMessageHandler(args) + const input = createMockInput("hephaestus", { providerID: "openai", modelID: "gpt-5.3-codex" }) + const output = createMockOutput() // no variant set + + //#when + await handler(input, output) + + //#then - should set variant from fallback chain + expect(output.message["variant"]).toBeDefined() + }) + + test("first message: preserves user-selected variant when already set", async () => { + //#given - first message, user already selected "xhigh" variant in OpenCode UI + const args = createMockHandlerArgs({ shouldOverride: true }) + const handler = createChatMessageHandler(args) + const input = createMockInput("hephaestus", { providerID: "openai", modelID: "gpt-5.3-codex" }) + const output = createMockOutput("xhigh") // user selected xhigh + + //#when + await handler(input, output) + + //#then - user's xhigh must be preserved, not overwritten to "medium" + expect(output.message["variant"]).toBe("xhigh") + }) + + test("first message: preserves user-selected 'high' variant", async () => { + //#given - user selected "high" variant + const args = createMockHandlerArgs({ shouldOverride: true }) + const handler = createChatMessageHandler(args) + const input = createMockInput("hephaestus", { providerID: "openai", modelID: "gpt-5.3-codex" }) + const output = createMockOutput("high") + + //#when + await handler(input, output) + + //#then + expect(output.message["variant"]).toBe("high") + }) + + test("subsequent message: does not override existing variant", async () => { + //#given - not first message, variant already set + const args = createMockHandlerArgs({ shouldOverride: false }) + const handler = createChatMessageHandler(args) + const input = createMockInput("hephaestus", { providerID: "openai", modelID: "gpt-5.3-codex" }) + const output = createMockOutput("xhigh") + + //#when + await handler(input, output) + + //#then + expect(output.message["variant"]).toBe("xhigh") + }) + + test("first message: marks gate as applied regardless of variant presence", async () => { + //#given - first message with user-selected variant + const args = createMockHandlerArgs({ shouldOverride: true }) + const handler = createChatMessageHandler(args) + const input = createMockInput("hephaestus", { providerID: "openai", modelID: "gpt-5.3-codex" }) + const output = createMockOutput("xhigh") + + //#when + await handler(input, output) + + //#then - gate should still be marked as applied + expect(args._appliedSessions).toContain("test-session") + }) +}) diff --git a/src/plugin/chat-message.ts b/src/plugin/chat-message.ts index 8cc1b394d..e67203207 100644 --- a/src/plugin/chat-message.ts +++ b/src/plugin/chat-message.ts @@ -56,12 +56,14 @@ export function createChatMessageHandler(args: { const message = output.message if (firstMessageVariantGate.shouldOverride(input.sessionID)) { - const variant = - input.model && input.agent - ? resolveVariantForModel(pluginConfig, input.agent, input.model) - : resolveAgentVariant(pluginConfig, input.agent) - if (variant !== undefined) { - message["variant"] = variant + if (message["variant"] === undefined) { + const variant = + input.model && input.agent + ? resolveVariantForModel(pluginConfig, input.agent, input.model) + : resolveAgentVariant(pluginConfig, input.agent) + if (variant !== undefined) { + message["variant"] = variant + } } firstMessageVariantGate.markApplied(input.sessionID) } else {