From bb4d88e55747b023dc655b506a670c5ef266a8fe Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 22 May 2026 17:35:33 +0100 Subject: [PATCH] fix(ui): hide thinking options for non-reasoning models (#85406) * fix(ui): hide thinking options for non-reasoning models * test(ui): satisfy thinking selector lint * chore(deps): refresh generated shrinkwraps * test(ui): remove redundant thinking selector assertion --- CHANGELOG.md | 1 + extensions/acpx/npm-shrinkwrap.json | 2 + .../amazon-bedrock-mantle/npm-shrinkwrap.json | 2 + extensions/amazon-bedrock/npm-shrinkwrap.json | 2 + .../anthropic-vertex/npm-shrinkwrap.json | 2 + extensions/codex/npm-shrinkwrap.json | 2 + .../diagnostics-otel/npm-shrinkwrap.json | 2 + extensions/discord/npm-shrinkwrap.json | 3 +- extensions/memory-lancedb/npm-shrinkwrap.json | 1 + extensions/twitch/npm-shrinkwrap.json | 2 + extensions/whatsapp/npm-shrinkwrap.json | 3 ++ npm-shrinkwrap.json | 6 +++ ui/src/ui/chat/session-controls.ts | 49 +++++++++++++++---- ui/src/ui/views/chat.test.ts | 42 ++++++++++++++++ 14 files changed, 108 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 187e5206c04c..ad9878e67257 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ Docs: https://docs.openclaw.ai - Gateway/LaunchAgent: wait for launchd reload bootout to finish and fall back to kickstart when bootstrap races, so reload handoff does not leave the service deregistered. Fixes #84630. (#84641) Thanks @NianJiuZst. - Gateway/LaunchAgent: treat a concurrent launchd bootstrap as a successful restart when the service is already loaded, avoiding false macOS Gateway restart failures. Fixes #84721. (#84722) Thanks @googlerest. - Gateway/service: include the active `openclaw` command bin directory in managed service PATH generation and doctor audit expectations for npm-global macOS installs. Fixes #84201. (#84475) Thanks @jbetala7. +- Control UI/chat: disable the thinking selector for known non-reasoning models instead of showing duplicate Off choices. Fixes #84069. Thanks @DrippingMellow. - CLI/update: preserve managed Gateway service environment during package cutovers so macOS LaunchAgent repair/restart reads the pre-update service state instead of caller shell state. (#83026) - Agents/providers: honor per-model `api` and `baseUrl` overrides in custom provider auth hooks and transport selection. Fixes #80487. (#80488) Thanks @huveewomg. - Gateway/restart: eager-load the lifecycle runtime before in-place upgrade signal handling so package replacement does not deadlock restart imports. (#84890) Thanks @myps6415. diff --git a/extensions/acpx/npm-shrinkwrap.json b/extensions/acpx/npm-shrinkwrap.json index 70fa68b081a8..d83935499341 100644 --- a/extensions/acpx/npm-shrinkwrap.json +++ b/extensions/acpx/npm-shrinkwrap.json @@ -903,6 +903,7 @@ "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.3.tgz", "integrity": "sha512-HdUm8EMQBLaJvGUdidNNbqpA1kYkwNcb+MYxkxCLAPJGQzlv9J0C24h8V65Z4c5GLd/JEALDvpFCQgpLJqc0zw==", "license": "Apache-2.0", + "peer": true, "peerDependencies": { "bare-abort-controller": "*" }, @@ -2238,6 +2239,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/extensions/amazon-bedrock-mantle/npm-shrinkwrap.json b/extensions/amazon-bedrock-mantle/npm-shrinkwrap.json index f2c30ea5afcb..339737cceaaa 100644 --- a/extensions/amazon-bedrock-mantle/npm-shrinkwrap.json +++ b/extensions/amazon-bedrock-mantle/npm-shrinkwrap.json @@ -1314,6 +1314,7 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", "license": "MIT", + "peer": true, "engines": { "node": ">=10.0.0" }, @@ -1350,6 +1351,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/extensions/amazon-bedrock/npm-shrinkwrap.json b/extensions/amazon-bedrock/npm-shrinkwrap.json index 66fdca19db82..76491ddaeec2 100644 --- a/extensions/amazon-bedrock/npm-shrinkwrap.json +++ b/extensions/amazon-bedrock/npm-shrinkwrap.json @@ -1188,6 +1188,7 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", "license": "MIT", + "peer": true, "engines": { "node": ">=10.0.0" }, @@ -1224,6 +1225,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/extensions/anthropic-vertex/npm-shrinkwrap.json b/extensions/anthropic-vertex/npm-shrinkwrap.json index dcfdb16e2c53..87118a470789 100644 --- a/extensions/anthropic-vertex/npm-shrinkwrap.json +++ b/extensions/anthropic-vertex/npm-shrinkwrap.json @@ -1321,6 +1321,7 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", "license": "MIT", + "peer": true, "engines": { "node": ">=10.0.0" }, @@ -1372,6 +1373,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/extensions/codex/npm-shrinkwrap.json b/extensions/codex/npm-shrinkwrap.json index 1dff52b7ca81..b4d69435251d 100644 --- a/extensions/codex/npm-shrinkwrap.json +++ b/extensions/codex/npm-shrinkwrap.json @@ -1866,6 +1866,7 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", "license": "MIT", + "peer": true, "engines": { "node": ">=10.0.0" }, @@ -1917,6 +1918,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/extensions/diagnostics-otel/npm-shrinkwrap.json b/extensions/diagnostics-otel/npm-shrinkwrap.json index e0e5bd240931..fbf215bf1762 100644 --- a/extensions/diagnostics-otel/npm-shrinkwrap.json +++ b/extensions/diagnostics-otel/npm-shrinkwrap.json @@ -67,6 +67,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=8.0.0" } @@ -580,6 +581,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, diff --git a/extensions/discord/npm-shrinkwrap.json b/extensions/discord/npm-shrinkwrap.json index 923991b3a03a..ac3c203eb372 100644 --- a/extensions/discord/npm-shrinkwrap.json +++ b/extensions/discord/npm-shrinkwrap.json @@ -432,7 +432,8 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/opusscript/-/opusscript-0.1.1.tgz", "integrity": "sha512-mL0fZZOUnXdZ78woRXp18lApwpp0lF5tozJOD1Wut0dgrA9WuQTgSels/CSmFleaAZrJi/nci5KOVtbuxeWoQA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/prism-media": { "version": "1.3.5", diff --git a/extensions/memory-lancedb/npm-shrinkwrap.json b/extensions/memory-lancedb/npm-shrinkwrap.json index 5c4cf3a1d5f2..c96fd1bb54a8 100644 --- a/extensions/memory-lancedb/npm-shrinkwrap.json +++ b/extensions/memory-lancedb/npm-shrinkwrap.json @@ -209,6 +209,7 @@ "resolved": "https://registry.npmjs.org/apache-arrow/-/apache-arrow-18.1.0.tgz", "integrity": "sha512-v/ShMp57iBnBp4lDgV8Jx3d3Q5/Hac25FWmQ98eMahUiHPXcvwIMKJD0hBIgclm/FCG+LwPkAKtkRO1O/W0YGg==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@swc/helpers": "^0.5.11", "@types/command-line-args": "^5.2.3", diff --git a/extensions/twitch/npm-shrinkwrap.json b/extensions/twitch/npm-shrinkwrap.json index 942b33ef499c..19dfcd82d38d 100644 --- a/extensions/twitch/npm-shrinkwrap.json +++ b/extensions/twitch/npm-shrinkwrap.json @@ -168,6 +168,7 @@ "resolved": "https://registry.npmjs.org/@twurple/auth/-/auth-8.1.4.tgz", "integrity": "sha512-ylsJoPInCw9BwOqxKcx+1k2ce9QG3vJpKFzPdIyHh49HvM/ulQZ0CAGysydugDYXF0iO/TGryh7PluSwx5fIwA==", "license": "MIT", + "peer": true, "dependencies": { "@d-fischer/logger": "^4.2.1", "@d-fischer/shared-utils": "^3.6.1", @@ -288,6 +289,7 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", "license": "MIT", + "peer": true, "engines": { "node": ">=10.0.0" }, diff --git a/extensions/whatsapp/npm-shrinkwrap.json b/extensions/whatsapp/npm-shrinkwrap.json index 9568a120d0cf..b009acf8e363 100644 --- a/extensions/whatsapp/npm-shrinkwrap.json +++ b/extensions/whatsapp/npm-shrinkwrap.json @@ -1178,6 +1178,7 @@ "resolved": "https://registry.npmjs.org/audio-decode/-/audio-decode-2.2.3.tgz", "integrity": "sha512-Z0lHvMayR/Pad9+O9ddzaBJE0DrhZkQlStrC1RwcAHF3AhQAsdwKHeLGK8fYKyp2DDU6xHxzGb4CLMui12yVrg==", "license": "MIT", + "peer": true, "dependencies": { "@wasm-audio-decoders/flac": "^0.2.4", "@wasm-audio-decoders/ogg-vorbis": "^0.1.15", @@ -1417,6 +1418,7 @@ "resolved": "https://registry.npmjs.org/jimp/-/jimp-1.6.1.tgz", "integrity": "sha512-hNQh6rZtWfSVWSNVmvq87N5BPJsNH7k7I7qyrXf9DOma9xATQk3fsyHazCQe51nCjdkoWdTmh0vD7bjVSLoxxw==", "license": "MIT", + "peer": true, "dependencies": { "@jimp/core": "1.6.1", "@jimp/diff": "1.6.1", @@ -1461,6 +1463,7 @@ "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", "license": "MIT", + "peer": true, "dependencies": { "@keyv/serialize": "^1.1.1" } diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 9b240637fcb5..9c5d4bfced4c 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1567,6 +1567,7 @@ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", "license": "MIT", + "peer": true, "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", @@ -2813,6 +2814,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -3230,6 +3232,7 @@ "resolved": "https://registry.npmjs.org/grammy/-/grammy-1.43.0.tgz", "integrity": "sha512-7dYm06A945mXuIk/5HUlSjeyIYChW8vCEiU2dkOKKqJJzwAWxTkCc91Eqbz7TgODh2rtFFKWI/fekowWHOkmjQ==", "license": "MIT", + "peer": true, "dependencies": { "@grammyjs/types": "3.27.3", "abort-controller": "^3.0.0", @@ -3298,6 +3301,7 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.18.tgz", "integrity": "sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=16.9.0" } @@ -5064,6 +5068,7 @@ "resolved": "https://registry.npmjs.org/undici/-/undici-8.3.0.tgz", "integrity": "sha512-TkUDgb6tl7KOGZ+7e8E3d2FYgUQgF6z5YypqjWmixVQSQERFcVrVg0ySADm2LVLRh5ljAaHTCR5Fmz3Q34rB7Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=22.19.0" } @@ -5287,6 +5292,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/ui/src/ui/chat/session-controls.ts b/ui/src/ui/chat/session-controls.ts index a4cc0d9b7e23..0c17f3348915 100644 --- a/ui/src/ui/chat/session-controls.ts +++ b/ui/src/ui/chat/session-controls.ts @@ -29,6 +29,7 @@ import { normalizeThinkingOptionValue, } from "../thinking-labels.ts"; import { + type ThinkingCatalogEntry, listThinkingLevelLabels, normalizeThinkLevel, resolveThinkingDefaultForModel, @@ -673,26 +674,51 @@ function buildThinkingOptions( return options; } +function isOffThinkingOption(value: string | null | undefined): boolean { + return normalizeThinkingOptionValue(value ?? "") === "off"; +} + +function isOffOnlyThinkingLevels(levels: readonly GatewayThinkingLevelOption[]): boolean { + return levels.every((level) => isOffThinkingOption(level.id || level.label)); +} + function resolveThinkingLevelOptions( activeRow: SessionsListResult["sessions"][number] | undefined, defaults: SessionsListResult["defaults"] | undefined, provider: string | null, model: string | null, + catalog: readonly ThinkingCatalogEntry[], ): GatewayThinkingLevelOption[] { - if (activeRow?.thinkingLevels?.length) { - return activeRow.thinkingLevels; - } const sessionModelMatchesDefaults = (!activeRow?.modelProvider || activeRow.modelProvider === defaults?.modelProvider) && (!activeRow?.model || activeRow.model === defaults?.model); - if (sessionModelMatchesDefaults && defaults?.thinkingLevels?.length) { - return defaults.thinkingLevels; + const catalogEntry = + provider && model + ? catalog.find((entry) => entry.provider === provider && entry.id === model) + : undefined; + const explicitLevels = + (activeRow?.thinkingLevels?.length ? activeRow.thinkingLevels : null) ?? + (sessionModelMatchesDefaults && defaults?.thinkingLevels?.length + ? defaults.thinkingLevels + : null); + if (explicitLevels) { + if (catalogEntry?.reasoning === false && isOffOnlyThinkingLevels(explicitLevels)) { + return []; + } + return explicitLevels; } - const labels = + const explicitLabels = (activeRow?.thinkingOptions?.length ? activeRow.thinkingOptions : null) ?? (sessionModelMatchesDefaults && defaults?.thinkingOptions?.length ? defaults.thinkingOptions - : null) ?? + : null); + if (catalogEntry?.reasoning === false) { + if (!explicitLabels || explicitLabels.every(isOffThinkingOption)) { + return []; + } + } + const labels = + explicitLabels ?? (provider && model ? listThinkingLevelLabels(provider, model) : listThinkingLevelLabels()); return labels.map((label) => ({ id: normalizeThinkLevel(label) ?? normalizeLowercaseStringOrEmpty(label), @@ -713,6 +739,7 @@ export function resolveChatThinkingSelectState(state: AppViewState): ChatThinkin state.sessionsResult?.defaults, provider, model, + state.chatModelCatalog ?? [], ); const defaultLevel = activeRow?.thinkingDefault ?? @@ -724,10 +751,11 @@ export function resolveChatThinkingSelectState(state: AppViewState): ChatThinkin catalog: state.chatModelCatalog ?? [], }) : "off"); + const effectiveOverride = levels.length === 0 && currentOverride === "off" ? "" : currentOverride; return { - currentOverride, + currentOverride: effectiveOverride, defaultLabel: formatInheritedThinkingLabel(defaultLevel), - options: buildThinkingOptions(levels, currentOverride), + options: buildThinkingOptions(levels, effectiveOverride), }; } @@ -735,7 +763,8 @@ export function renderChatThinkingSelect(state: AppViewState) { const { currentOverride, defaultLabel, options } = resolveChatThinkingSelectState(state); const busy = state.chatLoading || state.chatSending || Boolean(state.chatRunId) || state.chatStream !== null; - const disabled = !state.connected || busy || !state.client; + const disabled = + !state.connected || busy || !state.client || (options.length === 0 && currentOverride === ""); const selectedLabel = currentOverride === "" ? defaultLabel diff --git a/ui/src/ui/views/chat.test.ts b/ui/src/ui/views/chat.test.ts index 0e07306d681b..0be14778703e 100644 --- a/ui/src/ui/views/chat.test.ts +++ b/ui/src/ui/views/chat.test.ts @@ -1675,6 +1675,48 @@ describe("chat session controls", () => { expect(thinkingSelect?.title).toBe("Inherited: Adaptive"); }); + it("disables thinking for known non-reasoning models without duplicate off options", () => { + const { state } = createChatHeaderState({ + model: "mistral:v0.3", + modelProvider: "ollama", + models: [ + { + id: "mistral:v0.3", + name: "Mistral", + provider: "ollama", + reasoning: false, + }, + ], + }); + const session = state.sessionsResult!.sessions[0]; + state.sessionsResult = { + ...state.sessionsResult!, + defaults: { + ...state.sessionsResult!.defaults, + thinkingLevels: [{ id: "off", label: "off" }], + }, + sessions: [ + { + ...session, + thinkingLevel: "off", + thinkingLevels: [{ id: "off", label: "off" }], + }, + ], + }; + const container = document.createElement("div"); + render(renderChatSessionSelect(state), container); + + const thinkingSelect = container.querySelector( + 'select[data-chat-thinking-select="true"]', + ); + + expect(thinkingSelect?.disabled).toBe(true); + expect([...(thinkingSelect?.options ?? [])].map((option) => option.value)).toEqual([""]); + expect( + [...(thinkingSelect?.options ?? [])].map((option) => option.textContent?.trim()), + ).toEqual(["Inherited: Off"]); + }); + it("always renders full thinking labels", () => { const { state } = createChatHeaderState({ model: "gpt-5.5",