mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-03 13:34:06 +00:00
fix(ui): keep selected chat model visible after session switch
Fixes #86597. Thanks @brokemac79.
This commit is contained in:
@@ -109,6 +109,17 @@ export async function startControlUiE2eServer(): Promise<ControlUiE2eServer> {
|
||||
publicDir: path.join(uiRoot, "public"),
|
||||
resolve: {
|
||||
alias: {
|
||||
"@openclaw/net-policy/ip": path.join(repoRoot, "packages/net-policy/src/ip.ts"),
|
||||
"@openclaw/net-policy/ipv4": path.join(repoRoot, "packages/net-policy/src/ipv4.ts"),
|
||||
"@openclaw/net-policy/redact-sensitive-url": path.join(
|
||||
repoRoot,
|
||||
"packages/net-policy/src/redact-sensitive-url.ts",
|
||||
),
|
||||
"@openclaw/net-policy/url-userinfo": path.join(
|
||||
repoRoot,
|
||||
"packages/net-policy/src/url-userinfo.ts",
|
||||
),
|
||||
"@openclaw/net-policy": path.join(repoRoot, "packages/net-policy/src/index.ts"),
|
||||
json5: json5EsmPath,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { html } from "lit";
|
||||
import { live } from "lit/directives/live.js";
|
||||
import { repeat } from "lit/directives/repeat.js";
|
||||
import { t } from "../../i18n/index.ts";
|
||||
import { createChatSessionsLoadOverrides } from "../app-chat.ts";
|
||||
@@ -810,6 +811,7 @@ function renderChatModelSelect(state: AppViewState) {
|
||||
data-chat-model-select="true"
|
||||
aria-label=${t("chat.selectors.model")}
|
||||
title=${selectedLabel}
|
||||
.value=${live(currentOverride)}
|
||||
?disabled=${disabled}
|
||||
@change=${async (e: Event) => {
|
||||
const next = (e.target as HTMLSelectElement).value.trim();
|
||||
|
||||
@@ -46,6 +46,33 @@ async function waitForRequests(
|
||||
throw new Error(`Timed out waiting for ${count} ${method} requests`);
|
||||
}
|
||||
|
||||
function chatSessionListResponse() {
|
||||
return {
|
||||
count: 2,
|
||||
defaults: {
|
||||
contextTokens: null,
|
||||
model: "gpt-5.5",
|
||||
modelProvider: "openai",
|
||||
},
|
||||
path: "",
|
||||
sessions: [
|
||||
{
|
||||
key: "agent:main:session-a",
|
||||
kind: "direct",
|
||||
label: "Session A",
|
||||
updatedAt: 2,
|
||||
},
|
||||
{
|
||||
key: "agent:main:session-b",
|
||||
kind: "direct",
|
||||
label: "Session B",
|
||||
updatedAt: 1,
|
||||
},
|
||||
],
|
||||
ts: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
describeControlUiE2e("Control UI mocked Gateway E2E", () => {
|
||||
beforeAll(async () => {
|
||||
if (!chromiumAvailable) {
|
||||
@@ -201,6 +228,63 @@ describeControlUiE2e("Control UI mocked Gateway E2E", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps a session model override selected after switching away and back", async () => {
|
||||
const context = await browser.newContext({
|
||||
locale: "en-US",
|
||||
serviceWorkers: "block",
|
||||
viewport: { height: 900, width: 1280 },
|
||||
});
|
||||
const page = await context.newPage();
|
||||
const gateway = await installMockGateway(page, {
|
||||
methodResponses: {
|
||||
"sessions.list": chatSessionListResponse(),
|
||||
},
|
||||
models: [
|
||||
{ id: "gpt-5.5", name: "GPT-5.5", provider: "openai" },
|
||||
{ id: "claude-opus-4.5", name: "Claude Opus 4.5", provider: "bedrock" },
|
||||
],
|
||||
sessionKey: "agent:main:session-a",
|
||||
});
|
||||
|
||||
try {
|
||||
await page.goto(`${server.baseUrl}chat`);
|
||||
|
||||
const main = page.getByRole("main");
|
||||
const modelSelect = main.locator('select[data-chat-model-select="true"]');
|
||||
await modelSelect.waitFor({ state: "visible", timeout: 10_000 });
|
||||
expect(await modelSelect.inputValue()).toBe("");
|
||||
|
||||
await modelSelect.selectOption("bedrock/claude-opus-4.5");
|
||||
const patchRequest = await gateway.waitForRequest("sessions.patch");
|
||||
expect(requireRecord(patchRequest.params)).toMatchObject({
|
||||
key: "agent:main:session-a",
|
||||
model: "bedrock/claude-opus-4.5",
|
||||
});
|
||||
expect(await modelSelect.inputValue()).toBe("bedrock/claude-opus-4.5");
|
||||
|
||||
await main.getByRole("button", { name: "Chat session" }).click();
|
||||
await page
|
||||
.locator('button[data-chat-session-picker-option="true"][data-session-key="agent:main:session-b"]')
|
||||
.click();
|
||||
await main.getByRole("button", { name: "Chat session" }).getByText("Session B").waitFor({
|
||||
timeout: 10_000,
|
||||
});
|
||||
expect(await modelSelect.inputValue()).toBe("");
|
||||
|
||||
await main.getByRole("button", { name: "Chat session" }).click();
|
||||
await page
|
||||
.locator('button[data-chat-session-picker-option="true"][data-session-key="agent:main:session-a"]')
|
||||
.click();
|
||||
await main.getByRole("button", { name: "Chat session" }).getByText("Session A").waitFor({
|
||||
timeout: 10_000,
|
||||
});
|
||||
|
||||
expect(await modelSelect.inputValue()).toBe("bedrock/claude-opus-4.5");
|
||||
} finally {
|
||||
await context.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("refreshes history after a tool-call window disconnects and reconnects", async () => {
|
||||
const context = await browser.newContext({
|
||||
locale: "en-US",
|
||||
|
||||
@@ -2340,6 +2340,81 @@ describe("chat session controls", () => {
|
||||
expect(rerendered.value).toBe("openai/gpt-5-mini");
|
||||
});
|
||||
|
||||
it("keeps the selected model visible after switching away and back to a session", async () => {
|
||||
const sessionA = "agent:main:session-a";
|
||||
const sessionB = "agent:main:session-b";
|
||||
const catalog = createModelCatalog(...DEFAULT_CHAT_MODEL_CATALOG, {
|
||||
id: "claude-opus-4.5",
|
||||
name: "Claude Opus 4.5",
|
||||
provider: "bedrock",
|
||||
});
|
||||
const { state } = createChatHeaderState({ models: catalog });
|
||||
let rows: GatewaySessionRow[] = [
|
||||
{ key: sessionA, kind: "direct", label: "Session A", updatedAt: 2 },
|
||||
{ key: sessionB, kind: "direct", label: "Session B", updatedAt: 1 },
|
||||
];
|
||||
const request = vi.fn(async (method: string, params: Record<string, unknown> = {}) => {
|
||||
if (method === "sessions.patch") {
|
||||
const key = typeof params.key === "string" ? params.key : "";
|
||||
const nextModel = typeof params.model === "string" ? params.model.trim() : "";
|
||||
rows = rows.map((row) => {
|
||||
if (row.key !== key) {
|
||||
return row;
|
||||
}
|
||||
const nextRow: GatewaySessionRow = { ...row };
|
||||
if (!nextModel) {
|
||||
delete nextRow.model;
|
||||
delete nextRow.modelProvider;
|
||||
return nextRow;
|
||||
}
|
||||
const slashIndex = nextModel.indexOf("/");
|
||||
if (slashIndex > 0) {
|
||||
nextRow.modelProvider = nextModel.slice(0, slashIndex);
|
||||
} else {
|
||||
delete nextRow.modelProvider;
|
||||
}
|
||||
nextRow.model = slashIndex > 0 ? nextModel.slice(slashIndex + 1) : nextModel;
|
||||
return nextRow;
|
||||
});
|
||||
return { ok: true, key };
|
||||
}
|
||||
if (method === "sessions.list") {
|
||||
return createSessionsResultFromRows(rows);
|
||||
}
|
||||
if (method === "chat.history") {
|
||||
return { messages: [] };
|
||||
}
|
||||
if (method === "tools.effective") {
|
||||
return { agentId: "main", profile: "coding", groups: [] };
|
||||
}
|
||||
throw new Error(`Unexpected request: ${method}`);
|
||||
});
|
||||
state.client = { request } as unknown as GatewayBrowserClient;
|
||||
state.sessionKey = sessionA;
|
||||
state.settings.sessionKey = sessionA;
|
||||
state.sessionsResult = createSessionsResultFromRows(rows);
|
||||
const container = document.createElement("div");
|
||||
render(renderChatSessionSelect(state), container);
|
||||
|
||||
const modelSelect = getChatModelSelect(container);
|
||||
expect(modelSelect.value).toBe("");
|
||||
|
||||
modelSelect.value = "bedrock/claude-opus-4.5";
|
||||
modelSelect.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
await flushTasks();
|
||||
|
||||
state.sessionKey = sessionB;
|
||||
state.settings.sessionKey = sessionB;
|
||||
render(renderChatSessionSelect(state), container);
|
||||
expect(getChatModelSelect(container).value).toBe("");
|
||||
|
||||
state.sessionKey = sessionA;
|
||||
state.settings.sessionKey = sessionA;
|
||||
render(renderChatSessionSelect(state), container);
|
||||
|
||||
expect(getChatModelSelect(container).value).toBe("bedrock/claude-opus-4.5");
|
||||
});
|
||||
|
||||
it("uses default thinking options when the active session is absent", () => {
|
||||
const { state } = createChatHeaderState({ omitSessionFromList: true });
|
||||
state.sessionsResult = createSessionsListResult({
|
||||
|
||||
Reference in New Issue
Block a user