diff --git a/ui/src/e2e/chat-flow.models-reasoning.e2e.test.ts b/ui/src/e2e/chat-flow.models-reasoning.e2e.test.ts index f24c32a14de7..f14a2fba672b 100644 --- a/ui/src/e2e/chat-flow.models-reasoning.e2e.test.ts +++ b/ui/src/e2e/chat-flow.models-reasoning.e2e.test.ts @@ -130,22 +130,13 @@ suite.define(() => { await page.goto(`${suite.server.baseUrl}chat`); const main = page.getByRole("main"); - const openModelSelect = async () => { - const trigger = main.locator( - 'openclaw-chat-pane[aria-hidden="false"] wa-select.chat-controls__model-picker', - ); - await trigger.waitFor({ state: "visible", timeout: 10_000 }); - return trigger; - }; + const activePane = () => main.locator('openclaw-chat-pane[aria-hidden="false"]'); const selectModel = async (value: string) => { - const activePane = main.locator('openclaw-chat-pane[aria-hidden="false"]'); - await selectChatModel(activePane, value); + await selectChatModel(activePane(), value); }; - let modelSelect = await openModelSelect(); - expect(await chatModelValue(main.locator('openclaw-chat-pane[aria-hidden="false"]'))).toBe( - "", - ); + await chatModelPicker(activePane()).waitFor({ state: "visible", timeout: 10_000 }); + expect(await chatModelValue(activePane())).toBe(""); await selectModel("bedrock/claude-opus-4.5"); const patchRequest = await gateway.waitForRequest("sessions.patch"); @@ -153,9 +144,7 @@ suite.define(() => { key: "agent:main:session-a", model: "bedrock/claude-opus-4.5", }); - expect(await chatModelValue(main.locator('openclaw-chat-pane[aria-hidden="false"]'))).toBe( - "bedrock/claude-opus-4.5", - ); + expect(await chatModelValue(activePane())).toBe("bedrock/claude-opus-4.5"); await page .locator( @@ -165,10 +154,8 @@ suite.define(() => { await page.locator(".sidebar-recent-session--active").getByText("Session B").waitFor({ timeout: 10_000, }); - modelSelect = await openModelSelect(); - expect(await chatModelValue(main.locator('openclaw-chat-pane[aria-hidden="false"]'))).toBe( - "", - ); + await chatModelPicker(activePane()).waitFor({ state: "visible", timeout: 10_000 }); + expect(await chatModelValue(activePane())).toBe(""); await page .locator( @@ -179,10 +166,8 @@ suite.define(() => { timeout: 10_000, }); - modelSelect = await openModelSelect(); - expect(await chatModelValue(main.locator('openclaw-chat-pane[aria-hidden="false"]'))).toBe( - "bedrock/claude-opus-4.5", - ); + await chatModelPicker(activePane()).waitFor({ state: "visible", timeout: 10_000 }); + expect(await chatModelValue(activePane())).toBe("bedrock/claude-opus-4.5"); } finally { await suite.closeBrowserContext(context); } diff --git a/ui/src/e2e/new-session-page.workspace-memory.e2e.test.ts b/ui/src/e2e/new-session-page.workspace-memory.e2e.test.ts index d626e47d8829..69873830e449 100644 --- a/ui/src/e2e/new-session-page.workspace-memory.e2e.test.ts +++ b/ui/src/e2e/new-session-page.workspace-memory.e2e.test.ts @@ -317,7 +317,6 @@ suite.define(() => { }); await expect.poll(() => effortSelect.getAttribute("data-chat-thinking-value")).toBe("xhigh"); - const modelSelect = newSessionModelPicker(page); await selectNewSessionModel(page, "openai/gpt-5.6-sol"); await effortSelect.click(); @@ -366,7 +365,6 @@ suite.define(() => { await page.getByRole("button", { name: "Worktree" }).click(); await page.keyboard.press("Escape"); - const modelSelect = newSessionModelPicker(page); await selectNewSessionModel(page, "anthropic/claude-sonnet-4-6"); const effortSelect = page.locator('[data-chat-thinking-select="true"]'); await effortSelect.click(); @@ -582,7 +580,6 @@ suite.define(() => { .toBe(1); await gateway.deferNext("users.prefs.set"); - const modelSelect = newSessionModelPicker(page); await selectNewSessionModel(page, ""); await expect .poll(async () => (await gateway.getRequests("users.prefs.set")).length) @@ -665,7 +662,6 @@ suite.define(() => { await placeTrigger.click(); await page.getByRole("button", { name: "Worktree" }).click(); await page.keyboard.press("Escape"); - const modelSelect = newSessionModelPicker(page); await selectNewSessionModel(page, "anthropic/claude-sonnet-4-6"); await navigateInApp(page, "chat"); @@ -747,7 +743,6 @@ suite.define(() => { "openclaw-next", ); - const modelSelect = newSessionModelPicker(page); await selectNewSessionModel(page, "anthropic/claude-sonnet-4-6"); const storedPreference = await readMainPreference(page); expect(storedPreference).toMatchObject({ diff --git a/ui/src/pages/config/memory-dreaming.ts b/ui/src/pages/config/memory-dreaming.ts index 530b9e5bf52b..55b278fdc17d 100644 --- a/ui/src/pages/config/memory-dreaming.ts +++ b/ui/src/pages/config/memory-dreaming.ts @@ -378,7 +378,7 @@ function renderField(props: DreamingSettingsProps, spec: DreamingFieldSpec) { placeholder: spec.placeholderKey ? t(spec.placeholderKey) : "", commit: "change", }, - onChange: (value) => props.onPatch(spec.path, value.trim() || undefined), + onChange: (model) => props.onPatch(spec.path, model.trim() || undefined), })} `, }); diff --git a/ui/src/pages/config/session-observer-settings.ts b/ui/src/pages/config/session-observer-settings.ts index b2abe8f6dd20..793306eda758 100644 --- a/ui/src/pages/config/session-observer-settings.ts +++ b/ui/src/pages/config/session-observer-settings.ts @@ -117,7 +117,12 @@ export function renderSessionObserverSettings(props: { }, ] : []), - ...options.map((option) => ({ ...option, disabled: props.modelsUnavailable })), + ...options.map(({ value, label, provider }) => ({ + value, + label, + provider, + disabled: props.modelsUnavailable, + })), ], disabled: props.disabled, onChange: (value) => diff --git a/ui/src/pages/config/talk.ts b/ui/src/pages/config/talk.ts index f25e7cc293be..9bd6887d63a2 100644 --- a/ui/src/pages/config/talk.ts +++ b/ui/src/pages/config/talk.ts @@ -249,7 +249,7 @@ function renderModelRow(props: TalkViewProps) { control: renderModelPicker({ label: t("talkPage.model.title"), value: model ?? TALK_PICKER_UNSET, - options: options.map((option) => ({ ...option, provider: provider.id })), + options: options.map(({ value, label }) => ({ value, label, provider: provider.id })), disabled: props.configBusy, onChange: (value) => props.onModelChange(value || null), }), diff --git a/ui/src/pages/cron/view.test.ts b/ui/src/pages/cron/view.test.ts index 5b6c9c4a83cd..094f712c88a5 100644 --- a/ui/src/pages/cron/view.test.ts +++ b/ui/src/pages/cron/view.test.ts @@ -534,7 +534,7 @@ describe("cron view editor", () => { prompt.dispatchEvent(new Event("input", { bubbles: true })); expect(onFormChange).toHaveBeenCalledWith({ payloadText: "do the thing" }); - for (const field of ["name", "sessionKey", "deliveryAccountId"] as const) { + for (const field of ["name", "sessionKey", "deliveryAccountId", "payloadModel"] as const) { const id = `cron-${field.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}`; const input = getElement(container, `#${id}`, HTMLInputElement); if (field === "sessionKey" || field === "deliveryAccountId") { @@ -545,26 +545,6 @@ describe("cron view editor", () => { expect(onFormChange).toHaveBeenLastCalledWith({ [field]: field }); } - const modelPicker = getElement( - container, - "#cron-payload-model-picker", - HTMLElement, - ) as HTMLElement & { value: string }; - const customModel = Array.from(modelPicker.querySelectorAll("wa-option")).find( - (option) => option.textContent?.trim() === "Custom model…", - ); - Object.defineProperty(modelPicker, "value", { - configurable: true, - value: customModel?.getAttribute("value"), - }); - modelPicker.dispatchEvent(new Event("change", { bubbles: true })); - Reflect.deleteProperty(modelPicker, "value"); - const modelInput = getElement(container, "#cron-payload-model", HTMLInputElement); - expect(modelInput.hidden).toBe(false); - modelInput.value = "vendor/model with spaces"; - modelInput.dispatchEvent(new Event("input", { bubbles: true })); - expect(onFormChange).toHaveBeenLastCalledWith({ payloadModel: "vendor/model with spaces" }); - const channel = getElement( container, "#cron-failure-alert-channel", @@ -575,7 +555,6 @@ describe("cron view editor", () => { expect( Array.from(channel.querySelectorAll("wa-option"), (option) => option.getAttribute("value")), ).toContain("retired-channel"); - expect(channel.localName).toBe("wa-select"); expect(channel.querySelector('wa-option[value="telegram"] img')).not.toBeNull(); expect( (channel.querySelector('wa-option[value="telegram"]') as HTMLElement & { label?: string }) diff --git a/ui/src/pages/cron/view.ts b/ui/src/pages/cron/view.ts index 01eefe3457e5..b10bdf5f1302 100644 --- a/ui/src/pages/cron/view.ts +++ b/ui/src/pages/cron/view.ts @@ -1242,7 +1242,7 @@ function renderPromptSection( const modelError = props.fieldErrors.payloadModel; const modelOptions = uniqueStrings(props.modelSuggestions).map((value) => { const provider = providerIdFromModelRef(value); - return { value, label: value, ...(provider ? { provider } : {}) }; + return { value, label: value, provider: provider ?? undefined }; }); const agentTurnRows = ctx.isAgentTurn ? html`