fix(ui): compact the macOS gateway picker menu (#115977)

* fix(ui): compact native gateway menu

* chore: leave changelog to release automation

* test: stabilize UI and CLI process checks
This commit is contained in:
Peter Steinberger
2026-07-29 14:42:18 -04:00
committed by GitHub
parent 6ef0beaa60
commit b240703cfc
5 changed files with 78 additions and 21 deletions
+8 -19
View File
@@ -498,26 +498,12 @@ describe("JSON console style process output", () => {
expect(() => parseJsonLines(result.stdout)).toThrow();
});
it.each([
{
name: "missing container value",
args: ["--container"],
message: "--container requires a value",
},
{
name: "missing profile value",
args: ["--profile"],
message: "--profile requires a value",
},
{
name: "container/profile conflict",
args: ["--container", "demo", "--profile", "work", "status"],
message: "--container cannot be combined with --profile/--dev",
},
])("structures entry validation for $name", async ({ args, message }) => {
it("structures entry validation errors", async () => {
let failure: CliProcessFailure | undefined;
try {
await runCliProcess({ args, config: loggingConfig });
// One cold process proves entry-level JSON formatting. Profile parsing and
// container/profile conflicts have dedicated unit and process coverage below.
await runCliProcess({ args: ["--container"], config: loggingConfig });
} catch (error) {
failure = error as CliProcessFailure;
}
@@ -526,7 +512,10 @@ describe("JSON console style process output", () => {
expect(failure?.stdout ?? "").toBe("");
expect(parseJsonLines(failure?.stderr ?? "")).toEqual(
expect.arrayContaining([
expect.objectContaining({ level: "error", message: expect.stringContaining(message) }),
expect.objectContaining({
level: "error",
message: expect.stringContaining("--container requires a value"),
}),
]),
);
});
@@ -586,6 +586,53 @@ describeBrowserLayout.concurrent("chat responsive browser layout", () => {
}
});
it("keeps the native gateway picker as compact as sidebar menus", async () => {
const page = await openBrowserPage(800, 600);
try {
const splitViewCss = readStyleSheet("ui/src/styles/chat/split-view.css");
await page.setContent(
`<!doctype html><html><head><style>${readUiCss()}\n${splitViewCss}</style></head><body>
<wa-dropdown class="chat-pane__gateway-menu">
<template shadowrootmode="open"><div part="menu">Gateways</div></template>
<wa-dropdown-item class="chat-pane__gateway-menu-item">Local Gateway</wa-dropdown-item>
</wa-dropdown>
</body></html>`,
);
const styles = await page.evaluate(() => {
const dropdown = document.querySelector<HTMLElement>(".chat-pane__gateway-menu")!;
const menu = dropdown.shadowRoot!.querySelector<HTMLElement>('[part="menu"]')!;
const item = dropdown.querySelector<HTMLElement>(".chat-pane__gateway-menu-item")!;
const menuStyle = getComputedStyle(menu);
const itemStyle = getComputedStyle(item);
return {
menu: {
borderRadius: menuStyle.borderRadius,
padding: menuStyle.padding,
},
item: {
borderRadius: itemStyle.borderRadius,
fontSize: itemStyle.fontSize,
minHeight: itemStyle.minHeight,
padding: itemStyle.padding,
},
};
});
expect(styles).toEqual({
menu: { borderRadius: "8px", padding: "6px" },
item: {
borderRadius: "8px",
fontSize: "13px",
minHeight: "30px",
padding: "0px 8px",
},
});
} finally {
await closeBrowserPage(page);
}
});
it("pins the collapsed session rail to the pane header edge", async () => {
const page = await openBrowserPage(922, 282);
try {
@@ -120,6 +120,7 @@ describe("chat pane header", () => {
const { container } = mount({ nativeGateways: nativeGateways(gatewaySnapshot) });
const rows = container.querySelectorAll(".chat-pane__gateway-item");
expect(rows).toHaveLength(2);
expect(container.querySelectorAll(".chat-pane__gateway-menu-item")).toHaveLength(4);
expect(rows[0]?.textContent).toContain("Local Gateway");
expect(rows[0]?.textContent).toContain("primary");
expect(rows[0]?.querySelector(".chat-pane__gateway-check")).not.toBeNull();
@@ -177,7 +177,7 @@ function renderGatewayPicker(props: ChatPaneHeaderProps) {
${snapshot.gateways.map((gateway) => {
const selected = gateway.id === snapshot.currentId;
return html`<wa-dropdown-item
class="chat-pane__gateway-item"
class="chat-pane__gateway-menu-item chat-pane__gateway-item"
type="checkbox"
role="menuitemradio"
aria-checked=${String(selected)}
@@ -212,11 +212,14 @@ function renderGatewayPicker(props: ChatPaneHeaderProps) {
})}
<div class="chat-pane__gateway-divider" role="separator"></div>
<wa-dropdown-item
class="chat-pane__gateway-menu-item"
?disabled=${setPrimaryDisabled}
@click=${() => !setPrimaryDisabled && capability.setPrimary(current.id)}
>${t("chat.sessionHeader.gatewayPicker.setPrimary")}</wa-dropdown-item
>
<wa-dropdown-item @click=${() => capability.openSettings()}
<wa-dropdown-item
class="chat-pane__gateway-menu-item"
@click=${() => capability.openSettings()}
>${t("chat.sessionHeader.gatewayPicker.openSettings")}</wa-dropdown-item
>
</wa-dropdown>
+17
View File
@@ -221,6 +221,23 @@ openclaw-chat-pane {
min-width: 0;
}
.chat-pane__gateway-menu::part(menu) {
padding: 6px;
border: 1px solid color-mix(in srgb, var(--border-strong) 78%, transparent);
border-radius: 8px;
background: var(--bg-elevated);
box-shadow: var(--shadow-lg);
}
.chat-pane__gateway-menu-item {
min-height: 30px;
padding: 0 8px;
border-radius: 8px;
color: var(--text);
font: inherit;
font-size: 13px;
}
.chat-pane__gateway-chip {
display: inline-flex;
max-width: 170px;