Skip to content

Commit 28f59a9

Browse files
authored
fix(ui): align chat header controls
Summary: - Align WebChat desktop header controls to a compact 44px header and 36px control rhythm. - Replace the auto-scroll text dropdown with an icon toggle that keeps tooltip, title, aria-label, and pressed state. - Lay out mobile chat action icons as a five-column full-width grid. Verification: - git diff --check origin/main...HEAD - pnpm changed:lanes --json - pnpm exec oxfmt --check --threads=1 CHANGELOG.md ui/src/ui/app-render.helpers.ts ui/src/ui/app-render.helpers.browser.test.ts ui/src/styles/layout.css ui/src/styles/layout.mobile.css ui/src/styles/chat/layout.css ui/src/styles/chat/layout.test.ts ui/src/styles/layout.mobile.test.ts - pnpm lint:core - pnpm test ui/src/styles/chat/layout.test.ts ui/src/styles/layout.mobile.test.ts ui/src/ui/app-render.helpers.browser.test.ts - pnpm ui:build - Browser proof at desktop 1200x760 and mobile 390x844 - Exact-head GitHub CI green for a25444c
1 parent ec9d566 commit 28f59a9

8 files changed

Lines changed: 162 additions & 108 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ Docs: https://docs.openclaw.ai
3535
### Fixes
3636

3737
- Agents/Azure OpenAI Responses: default unset Azure OpenAI API versions to `preview` so `/openai/v1/responses` calls use Azure's current Responses API route. (#82026) Thanks @leoge007.
38+
- Control UI/WebChat: compact the desktop chat header controls into a single aligned row so the session, model, thinking, and action controls no longer waste vertical space. Thanks @BunsDev.
3839
- Agents: retry empty final turns for generic `anthropic-messages` providers instead of limiting non-visible recovery to Kimi, so custom/proxied Anthropic-compatible routes can recover with a visible answer. Addresses #46080. Thanks @wmgx, @w1tv, and @iFwu.
3940
- Agents/replies: strip workflow `<function_response>` scaffolding from user-visible sanitizer paths so raw tool output does not leak into chat history, transcript mirrors, or channel replies. Fixes #47444. Thanks @5toCode.
4041
- Control UI: rotate browser service-worker caches per build so updated Gateways are less likely to keep serving stale dashboard bundles that trigger protocol mismatch errors.

ui/src/styles/chat/layout.css

Lines changed: 34 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1158,8 +1158,9 @@
11581158
display: flex;
11591159
align-items: center;
11601160
justify-content: flex-start;
1161-
gap: 12px;
1161+
gap: 8px;
11621162
flex-wrap: wrap;
1163+
min-height: 36px;
11631164
}
11641165

11651166
.chat-controls__session {
@@ -1179,8 +1180,9 @@
11791180
minmax(128px, 4fr);
11801181
grid-template-areas: "agent session model thinking";
11811182
align-items: center;
1182-
gap: 8px;
1183+
gap: 6px;
11831184
width: 100%;
1185+
min-height: 36px;
11841186
min-width: 0;
11851187
}
11861188

@@ -1251,51 +1253,46 @@
12511253

12521254
/* Controls separator */
12531255
.chat-controls__separator {
1254-
color: rgba(255, 255, 255, 0.4);
1255-
font-size: 18px;
1256-
margin: 0 8px;
1257-
font-weight: 300;
1258-
}
1259-
1260-
:root[data-theme-mode="light"] .chat-controls__separator {
1261-
color: rgba(16, 24, 40, 0.3);
1262-
}
1263-
1264-
.chat-controls__session select {
1265-
padding: 6px 10px;
1266-
font-size: 13px;
1267-
width: 100%;
1268-
max-width: none;
1256+
align-self: center;
1257+
flex: 0 0 1px;
1258+
width: 1px;
1259+
height: 22px;
1260+
margin: 0 3px;
12691261
overflow: hidden;
1270-
text-overflow: ellipsis;
1262+
background: color-mix(in srgb, var(--border-strong) 72%, transparent);
1263+
color: transparent;
1264+
font-size: 0;
1265+
font-weight: 300;
12711266
}
12721267

1273-
.chat-controls__agent select {
1274-
width: 100%;
1275-
max-width: none;
1268+
.chat-controls .btn--icon {
1269+
width: 36px;
1270+
min-width: 36px;
1271+
height: 36px;
1272+
padding: 0;
1273+
border-radius: var(--radius-lg);
12761274
}
12771275

1278-
.chat-controls__model select {
1279-
width: 100%;
1280-
max-width: none;
1276+
:root[data-theme-mode="light"] .chat-controls__separator {
1277+
background: rgba(16, 24, 40, 0.18);
12811278
}
12821279

1280+
.chat-controls__session select,
1281+
.chat-controls__agent select,
1282+
.chat-controls__model select,
12831283
.chat-controls__thinking-select select {
1284+
box-sizing: border-box;
1285+
height: 36px;
1286+
min-height: 36px;
1287+
padding: 0 34px 0 12px;
12841288
width: 100%;
12851289
max-width: none;
1286-
}
1287-
1288-
.chat-controls__autoscroll {
1289-
min-width: 112px;
1290-
max-width: 118px;
1291-
}
1292-
1293-
.chat-controls__autoscroll-select {
1294-
width: 100%;
1295-
height: 32px;
1296-
min-width: 0;
1297-
padding: 4px 26px 4px 8px;
1298-
font-size: 12px;
1290+
border-color: color-mix(in srgb, var(--input) 88%, transparent);
1291+
border-radius: var(--radius-lg);
1292+
background-color: color-mix(in srgb, var(--card) 92%, var(--bg-elevated) 8%);
1293+
font-size: 13px;
1294+
overflow: hidden;
1295+
text-overflow: ellipsis;
12991296
}
13001297

13011298
.chat-controls__thinking {

ui/src/styles/chat/layout.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,15 @@ describe("chat layout styles", () => {
4646
expect(css).toContain("@media (display-mode: standalone) and (max-width: 768px)");
4747
expect(css).toContain("margin-bottom: calc(14px + max(var(--safe-area-bottom), 34px));");
4848
});
49+
50+
it("keeps desktop chat header controls on a compact aligned rhythm", () => {
51+
const css = readLayoutCss();
52+
53+
expect(css).toContain("min-height: 36px;");
54+
expect(css).toContain("height: 36px;");
55+
expect(css).toContain(".chat-controls .btn--icon {");
56+
expect(css).toContain("width: 36px;");
57+
expect(css).toContain(".chat-controls__separator {");
58+
expect(css).toContain("height: 22px;");
59+
});
4960
});

ui/src/styles/layout.css

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -983,6 +983,7 @@
983983
.content--chat .content-header.content-header--chat-hidden {
984984
opacity: 0;
985985
transform: translateY(-10px);
986+
min-height: 0;
986987
max-height: 0px;
987988
padding-top: 0;
988989
padding-bottom: 0;
@@ -1108,26 +1109,50 @@
11081109
grid-template-columns: minmax(0, 1fr) max-content;
11091110
align-items: center;
11101111
justify-content: space-between;
1111-
gap: 12px;
1112-
padding-bottom: 0;
1112+
gap: 10px;
1113+
min-height: 44px;
1114+
padding: 4px 8px;
11131115
overflow: visible;
1114-
max-height: 56px;
1116+
max-height: 44px;
11151117
}
11161118

11171119
.content--chat .content-header > div:first-child {
1120+
display: grid;
1121+
align-items: center;
1122+
position: relative;
11181123
text-align: left;
11191124
min-width: 0;
11201125
}
11211126

1127+
.shell--chat-focus .content--chat .content-header {
1128+
min-height: 0;
1129+
}
1130+
11221131
.content--chat .page-meta {
1132+
align-self: center;
11231133
justify-content: flex-end;
1134+
height: 36px;
1135+
align-items: center;
11241136
min-width: max-content;
11251137
overflow: visible;
11261138
}
11271139

11281140
.content--chat .chat-controls {
11291141
flex-shrink: 0;
11301142
flex-wrap: nowrap;
1143+
gap: 6px;
1144+
}
1145+
1146+
.content--chat .content-header .chat-controls__session-notice {
1147+
position: absolute;
1148+
top: calc(100% + 2px);
1149+
left: 0;
1150+
width: min(100%, 680px);
1151+
min-height: 0;
1152+
overflow: hidden;
1153+
pointer-events: none;
1154+
text-overflow: ellipsis;
1155+
white-space: nowrap;
11311156
}
11321157

11331158
/* ===========================================

ui/src/styles/layout.mobile.css

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@
44

55
@media (max-width: 1320px) {
66
.content--chat .content-header {
7-
align-items: stretch;
7+
align-items: center;
88
gap: 8px;
99
row-gap: 0;
10-
max-height: 56px;
10+
min-height: 44px;
11+
max-height: 44px;
1112
overflow: visible;
1213
}
1314

@@ -16,6 +17,9 @@
1617
}
1718

1819
.content--chat .page-meta {
20+
align-self: center;
21+
height: 36px;
22+
align-items: center;
1923
min-width: 0;
2024
justify-content: flex-end;
2125
flex-wrap: nowrap;
@@ -505,28 +509,18 @@
505509
}
506510

507511
.chat-mobile-controls-wrapper .chat-controls-dropdown .chat-controls__thinking {
508-
display: flex;
509-
flex-wrap: wrap;
510-
gap: 6px;
511-
justify-content: flex-end;
512+
display: grid;
513+
grid-template-columns: repeat(5, minmax(0, 1fr));
514+
align-items: center;
515+
justify-content: stretch;
516+
gap: 8px;
512517
width: 100%;
513518
padding: 0;
514519
}
515520

516-
.chat-mobile-controls-wrapper .chat-controls-dropdown .chat-controls__autoscroll {
517-
flex: 1 1 100%;
518-
max-width: none;
519-
min-width: 0;
520-
}
521-
522-
.chat-mobile-controls-wrapper .chat-controls-dropdown .chat-controls__autoscroll-select {
523-
width: 100%;
524-
height: 40px;
525-
}
526-
527521
.chat-mobile-controls-wrapper .chat-controls-dropdown .btn--icon {
528-
width: 44px;
529-
min-width: 44px;
522+
width: 100%;
523+
min-width: 0;
530524
height: 44px;
531525
}
532526

ui/src/styles/layout.mobile.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,29 @@ function readGroupedChatCss(): string {
1616
describe("chat header responsive mobile styles", () => {
1717
it("keeps the chat header and session controls from clipping on narrow widths", () => {
1818
const css = readMobileCss();
19+
const layoutCss = readLayoutCss();
1920

2021
expect(css).toContain("@media (max-width: 1320px)");
2122
expect(css).toContain(".content--chat .content-header");
23+
expect(css).toContain("max-height: 44px;");
24+
expect(layoutCss).toContain(".content--chat .content-header .chat-controls__session-notice");
25+
expect(layoutCss).toContain("position: absolute;");
2226
expect(css).toContain(".chat-controls__session-row");
2327
expect(css).toContain(".chat-controls__thinking-select");
2428
});
29+
30+
it("lays out mobile chat header action icons as an even full-width grid", () => {
31+
const css = readMobileCss();
32+
33+
expect(css).toContain(
34+
".chat-mobile-controls-wrapper .chat-controls-dropdown .chat-controls__thinking",
35+
);
36+
expect(css).toContain("grid-template-columns: repeat(5, minmax(0, 1fr));");
37+
expect(css).toContain(
38+
".chat-mobile-controls-wrapper .chat-controls-dropdown .btn--icon {\n width: 100%;",
39+
);
40+
expect(css).toContain("height: 44px;");
41+
});
2542
});
2643

2744
describe("sidebar menu trigger styles", () => {

ui/src/ui/app-render.helpers.browser.test.ts

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -100,11 +100,12 @@ describe("chat header controls (browser)", () => {
100100
container.querySelectorAll<HTMLButtonElement>(".chat-controls .btn--icon[data-tooltip]"),
101101
);
102102

103-
expect(buttons).toHaveLength(5);
103+
expect(buttons).toHaveLength(6);
104104

105105
const labels = buttons.map((button) => button.getAttribute("data-tooltip"));
106106
expect(labels).toEqual([
107107
t("chat.refreshTitle"),
108+
`${t("chat.autoScrollMode")}: ${t("chat.autoScrollNearBottom")}`,
108109
t("chat.thinkingToggle"),
109110
t("chat.toolCallsToggle"),
110111
t("chat.focusToggle"),
@@ -162,7 +163,9 @@ describe("chat header controls (browser)", () => {
162163
container.querySelectorAll<HTMLButtonElement>(".chat-controls__thinking .btn--icon"),
163164
);
164165

165-
expect(buttons).toHaveLength(4);
166+
expect(buttons).toHaveLength(5);
167+
const autoScrollButton = requireButton(buttons.at(0), "auto-scroll mode");
168+
expect(autoScrollButton.dataset.chatAutoScrollMode).toBe("near-bottom");
166169
const cronButton = requireButton(buttons.at(-1), "cron sessions");
167170
expect([...cronButton.classList]).toEqual(["btn", "btn--sm", "btn--icon", "active"]);
168171
expect(cronButton.getAttribute("aria-pressed")).toBe("true");
@@ -173,26 +176,29 @@ describe("chat header controls (browser)", () => {
173176
expect(state.sessionsHideCron).toBe(false);
174177
});
175178

176-
it("renders and applies the chat auto-scroll mode selector", async () => {
179+
it("renders and applies the chat auto-scroll mode toggle", async () => {
177180
const applySettings = vi.fn();
178181
const state = createState({ applySettings });
179182
const container = document.createElement("div");
180183
render(renderChatControls(state), container);
181184
await Promise.resolve();
182185

183-
const select = requireElement(
184-
container.querySelector<HTMLSelectElement>('[data-chat-auto-scroll-select="true"]'),
185-
"auto-scroll select",
186+
const toggle = requireButton(
187+
container.querySelector<HTMLButtonElement>('[data-chat-auto-scroll-toggle="true"]'),
188+
"auto-scroll toggle",
186189
);
187-
expect(select.getAttribute("aria-label")).toBe(t("chat.autoScrollMode"));
188-
expect(select.value).toBe("near-bottom");
190+
expect(toggle.getAttribute("aria-label")).toBe(
191+
`${t("chat.autoScrollMode")}: ${t("chat.autoScrollNearBottom")}`,
192+
);
193+
expect(toggle.getAttribute("data-tooltip")).toBe(toggle.getAttribute("aria-label"));
194+
expect(toggle.dataset.chatAutoScrollMode).toBe("near-bottom");
195+
expect(toggle.getAttribute("aria-pressed")).toBe("true");
189196

190-
select.value = "off";
191-
select.dispatchEvent(new Event("change"));
197+
toggle.click();
192198

193199
expect(applySettings).toHaveBeenCalledWith({
194200
...state.settings,
195-
chatAutoScroll: "off",
201+
chatAutoScroll: "always",
196202
});
197203
});
198204

@@ -229,12 +235,16 @@ describe("chat header controls (browser)", () => {
229235
const selectDatasets = Array.from(container.querySelectorAll("select")).map(
230236
(select) => select.dataset,
231237
);
232-
expect(selectDatasets).toHaveLength(5);
238+
expect(selectDatasets).toHaveLength(4);
233239
expect(selectDatasets[0]?.chatAgentFilter).toBe("true");
234240
expect(selectDatasets[1]?.chatSessionSelect).toBe("true");
235241
expect(selectDatasets[2]?.chatModelSelect).toBe("true");
236242
expect(selectDatasets[3]?.chatThinkingSelect).toBe("true");
237-
expect(selectDatasets[4]?.chatAutoScrollSelect).toBe("true");
243+
const autoScrollToggle = requireButton(
244+
container.querySelector<HTMLButtonElement>('[data-chat-auto-scroll-toggle="true"]'),
245+
"auto-scroll toggle",
246+
);
247+
expect(autoScrollToggle.dataset.chatAutoScrollMode).toBe("near-bottom");
238248
});
239249

240250
it("renders the mobile dropdown from state instead of mutating DOM classes", async () => {

0 commit comments

Comments
 (0)