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
This commit is contained in:
Peter Steinberger
2026-05-22 17:35:33 +01:00
committed by GitHub
parent a03a8d91f6
commit bb4d88e557
14 changed files with 108 additions and 11 deletions

View File

@@ -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.

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
},

View File

@@ -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",

View File

@@ -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",

View File

@@ -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"
},

View File

@@ -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"
}

6
npm-shrinkwrap.json generated
View File

@@ -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"
}

View File

@@ -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

View File

@@ -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<HTMLSelectElement>(
'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",