fix(control-ui): use canonical thinkingDefault key in Quick Config (#104523)

* fix(control-ui): use canonical thinkingDefault key in Quick Config

The Control UI Quick Config panel was reading and writing the
non-canonical key agents.defaults.thinkingLevel, which is rejected
by the gateway's strict Zod schema with INVALID_REQUEST errors.

Fix: read from and write to agents.defaults.thinkingDefault instead.
The schema already defines this key and the runtime already reads it
at cfg.agents?.defaults?.thinkingDefault, so this is purely a UI path
correction with zero runtime changes.

The fast-mode control is left unchanged pending a maintainer decision
on its intended scope (per-agent, per-model, or new global default).

Refs #104501

* test(control-ui): prove quick thinking persistence

* docs(changelog): note Quick Config thinking fix

* chore: keep changelog release-owned

---------

Co-authored-by: Erick Kinnee <erick@ekinnee.dev>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
Erick Kinnee
2026-07-11 19:40:11 -05:00
committed by GitHub
parent 82667a66e5
commit 43634f616b
2 changed files with 107 additions and 2 deletions
@@ -0,0 +1,105 @@
// Control UI tests cover Quick Config persistence through the mocked Gateway.
import { chromium, type Browser } from "playwright";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import {
canRunPlaywrightChromium,
installMockGateway,
resolvePlaywrightChromiumExecutablePath,
startControlUiE2eServer,
type ControlUiE2eServer,
type MockGatewayRequest,
} from "../test-helpers/control-ui-e2e.ts";
const chromiumExecutablePath = resolvePlaywrightChromiumExecutablePath(chromium.executablePath());
const chromiumAvailable = canRunPlaywrightChromium(chromiumExecutablePath);
const allowMissingChromium = process.env.OPENCLAW_UI_E2E_ALLOW_MISSING_CHROMIUM === "1";
const describeControlUiE2e = chromiumAvailable || !allowMissingChromium ? describe : describe.skip;
let browser: Browser;
let server: ControlUiE2eServer;
function configResponse(thinkingDefault: "low" | "high", hash: string) {
const config = { agents: { defaults: { model: "openai/gpt-5.5", thinkingDefault } } };
return {
config,
hash,
issues: [],
raw: JSON.stringify(config),
valid: true,
};
}
function requestRaw(request: MockGatewayRequest): Record<string, unknown> {
const params = request.params;
if (!params || typeof params !== "object" || Array.isArray(params)) {
throw new Error("Expected config.set params");
}
return JSON.parse(String((params as Record<string, unknown>).raw)) as Record<string, unknown>;
}
describeControlUiE2e("Control UI Quick Config thinking persistence mocked Gateway E2E", () => {
beforeAll(async () => {
if (!chromiumAvailable) {
throw new Error(
`Playwright Chromium is not installed or cannot start at ${chromiumExecutablePath}. Run \`pnpm --dir ui exec playwright install --with-deps chromium\`, or set OPENCLAW_UI_E2E_ALLOW_MISSING_CHROMIUM=1 only when intentionally skipping this lane.`,
);
}
server = await startControlUiE2eServer();
browser = await chromium.launch({ executablePath: chromiumExecutablePath });
});
afterAll(async () => {
await browser?.close();
await server?.close();
});
it("reads and writes only agents.defaults.thinkingDefault", async () => {
const context = await browser.newContext({
locale: "en-US",
serviceWorkers: "block",
viewport: { height: 900, width: 1280 },
});
const page = await context.newPage();
const initialConfig = configResponse("low", "hash-1");
const savedConfig = configResponse("high", "hash-2");
const gateway = await installMockGateway(page, {
methodResponses: {
"config.get": initialConfig,
"config.set": savedConfig,
},
});
try {
const response = await page.goto(`${server.baseUrl}config`);
expect(response?.status()).toBe(200);
const modelCard = page.locator(".qs-card--model");
const lowButton = modelCard.getByRole("button", { name: "Low", exact: true });
await lowButton.waitFor();
expect(await lowButton.getAttribute("class")).toContain("qs-segmented__btn--active");
await modelCard.getByRole("button", { name: "High", exact: true }).click();
await page.getByRole("button", { name: "Save", exact: true }).click();
const raw = requestRaw(await gateway.waitForRequest("config.set"));
expect(raw).toEqual({
agents: { defaults: { model: "openai/gpt-5.5", thinkingDefault: "high" } },
});
expect(JSON.stringify(raw)).not.toContain("thinkingLevel");
expect(JSON.stringify(raw)).not.toContain("fastMode");
const freshPage = await context.newPage();
await installMockGateway(freshPage, {
methodResponses: { "config.get": savedConfig },
});
await freshPage.goto(`${server.baseUrl}config`);
const highButton = freshPage
.locator(".qs-card--model")
.getByRole("button", { name: "High", exact: true });
await highButton.waitFor();
expect(await highButton.getAttribute("class")).toContain("qs-segmented__btn--active");
} finally {
await context.close();
}
});
});
+2 -2
View File
@@ -816,7 +816,7 @@ export class ConfigPage extends OpenClawLightDomElement {
const agentsDefaults = asConfigRecord(asConfigRecord(configObject.agents)?.defaults);
const model = typeof agentsDefaults?.model === "string" ? agentsDefaults.model : "default";
const thinkingLevel =
typeof agentsDefaults?.thinkingLevel === "string" ? agentsDefaults.thinkingLevel : "off";
typeof agentsDefaults?.thinkingDefault === "string" ? agentsDefaults.thinkingDefault : "off";
const fastMode = agentsDefaults?.fastMode;
const appConfig = this.context.config.current;
return renderQuickSettings({
@@ -879,7 +879,7 @@ export class ConfigPage extends OpenClawLightDomElement {
onSaveConfig: () => void runtimeConfig.save(),
onApplyConfig: () => void runtimeConfig.apply(),
onThinkingChange: (level) =>
runtimeConfig.patchForm(["agents", "defaults", "thinkingLevel"], level),
runtimeConfig.patchForm(["agents", "defaults", "thinkingDefault"], level),
onFastModeChange: (mode: FastMode) =>
runtimeConfig.patchForm(["agents", "defaults", "fastMode"], mode),
onChannelConfigure: () => this.navigate("communications"),