improve(ui): make chat disclosures compact (#124832)

Co-authored-by: RoboClaw <309084314+roboclaw-bot@users.noreply.github.com>
This commit is contained in:
ClawSweeper
2026-08-16 14:04:47 -07:00
committed by GitHub
parent d39a29fcbe
commit da85382686
9 changed files with 126 additions and 162 deletions
@@ -108,9 +108,16 @@ suite.define(() => {
});
it("pairs a canonical parallel batch and renders per-file patch sections", async () => {
const artifactDir = process.env.OPENCLAW_CONTROL_UI_E2E_ARTIFACT_DIR?.trim();
if (artifactDir) {
await fs.mkdir(artifactDir, { recursive: true });
}
const context = await suite.browser.newContext({
locale: "en-US",
viewport: { height: 900, width: 1200 },
...(artifactDir
? { recordVideo: { dir: artifactDir, size: { height: 900, width: 1200 } } }
: {}),
});
const page = await context.newPage();
await installMockGateway(page, {
@@ -165,12 +172,36 @@ suite.define(() => {
const activity = page.locator(".chat-group--activity .chat-activity-group__summary");
await activity.waitFor();
expect(await activity.textContent()).toContain("Read a file, edited a file, created a file");
const activityGeometry = await activity.evaluate((node) => {
const container = node.closest<HTMLElement>(".chat-activity-group");
const label = node.querySelector<HTMLElement>(".chat-activity-group__label");
const chevron = node.querySelector<HTMLElement>(".chat-inline-disclosure__chevron");
if (!container || !label || !chevron) {
throw new Error("Expected compact activity disclosure parts");
}
const containerRect = container.getBoundingClientRect();
const summaryRect = node.getBoundingClientRect();
const labelRect = label.getBoundingClientRect();
const chevronRect = chevron.getBoundingClientRect();
return {
containerWidth: containerRect.width,
summaryWidth: summaryRect.width,
chevronGap: chevronRect.left - labelRect.right,
};
});
expect(activityGeometry.summaryWidth).toBeLessThan(activityGeometry.containerWidth);
expect(activityGeometry.chevronGap).toBeLessThanOrEqual(8);
await activity.hover();
expect(await activity.evaluate((node) => getComputedStyle(node).backgroundColor)).toBe(
"rgba(0, 0, 0, 0)",
);
if ((await activity.getAttribute("aria-expanded")) !== "true") {
await activity.click();
}
const rows = page.locator(".chat-activity-group__body .chat-tool-msg-summary");
expect(await rows.count()).toBe(2);
expect(await rows.locator(".chat-inline-disclosure__chevron").count()).toBe(2);
expect(await page.locator(".chat-tool-msg-summary__label", { hasText: "Tool" }).count()).toBe(
0,
);
@@ -206,28 +206,31 @@ function activityAlignmentHtml() {
<div class="chat-avatar tool">A</div>
<div class="chat-group-messages">
<div class="chat-activity-group is-open">
<button class="chat-activity-group__summary" type="button">
<button class="chat-inline-disclosure chat-activity-group__summary" type="button" aria-expanded="true">
<span class="chat-activity-group__icon">${iconSvg()}</span>
<span class="chat-activity-group__label">Activity: 2 tools</span>
<span class="chat-inline-disclosure__chevron">${iconSvg()}</span>
</button>
<div class="chat-activity-group__body">
<div class="chat-bubble chat-bubble--tool-shell" data-activity-call-row>
<div class="chat-tools-inline">
<div class="chat-tool-msg-collapse">
<button class="chat-tool-msg-summary" type="button">
<button class="chat-inline-disclosure chat-tool-msg-summary" type="button" aria-expanded="false">
<span class="chat-tool-msg-summary__icon">${iconSvg()}</span>
<span class="chat-tool-msg-summary__label">Bash</span>
<span class="chat-tool-msg-summary__names">search a deliberately long workspace path without extra card chrome</span>
<span class="chat-inline-disclosure__chevron">${iconSvg()}</span>
</button>
</div>
</div>
</div>
<div class="chat-bubble chat-bubble--tool-shell">
<div class="chat-tool-msg-collapse">
<button class="chat-tool-msg-summary" data-failed-call-row type="button">
<button class="chat-inline-disclosure chat-tool-msg-summary" data-failed-call-row type="button" aria-expanded="false">
<span class="chat-tool-msg-summary__icon">${iconSvg()}</span>
<span class="chat-tool-msg-summary__label">Bash</span>
<span class="chat-tool-msg-summary__names">Bash</span>
<span class="chat-inline-disclosure__chevron">${iconSvg()}</span>
<span class="chat-tool-row__badge">failed</span>
</button>
</div>
@@ -1171,7 +1174,7 @@ describeBrowserLayout.concurrent("chat responsive browser layout", () => {
it.each([
[430, 720],
[1366, 900],
] as const)("right-aligns activity rows with call bubbles at %sx%s", async (width, height) => {
] as const)("keeps activity disclosures compact at %sx%s", async (width, height) => {
const page = await openBrowserPage(width, height);
try {
await page.setContent(
@@ -1179,24 +1182,27 @@ describeBrowserLayout.concurrent("chat responsive browser layout", () => {
);
await expectNoHorizontalOverflow(page);
const callRow = await getRect(page, "[data-activity-call-row]");
const activityGroup = await getRect(page, ".chat-activity-group");
const activitySummary = await getRect(page, ".chat-activity-group__summary");
const failedSummary = await getRect(page, "[data-failed-call-row]");
expect(Math.abs(callRow.right - failedSummary.right)).toBeLessThanOrEqual(1);
expect(Math.abs(callRow.height - failedSummary.height)).toBeLessThanOrEqual(1);
expect(activitySummary.width).toBeLessThan(activityGroup.width);
expect(failedSummary.width).toBeLessThan(activityGroup.width);
const styles = await page.evaluate(() => {
const call = document.querySelector<HTMLElement>("[data-activity-call-row]")!;
const activity = document.querySelector<HTMLElement>(".chat-activity-group__summary")!;
const label = activity.querySelector<HTMLElement>(".chat-activity-group__label")!;
const chevron = activity.querySelector<HTMLElement>(".chat-inline-disclosure__chevron")!;
return {
activity: getComputedStyle(
document.querySelector<HTMLElement>(".chat-activity-group__summary")!,
).userSelect,
callBackground: getComputedStyle(call).backgroundColor,
activity: getComputedStyle(activity).userSelect,
activityBackground: getComputedStyle(activity).backgroundColor,
chevronGap: chevron.getBoundingClientRect().left - label.getBoundingClientRect().right,
tool: getComputedStyle(document.querySelector<HTMLElement>(".chat-tool-msg-summary")!)
.userSelect,
};
});
expect(styles).toEqual({
activity: "text",
callBackground: "rgba(0, 0, 0, 0)",
activityBackground: "rgba(0, 0, 0, 0)",
chevronGap: 8,
tool: "text",
});
} finally {
@@ -463,7 +463,7 @@ export function renderGroupedMessage(
: ""}"
>
<button
class="chat-tool-msg-summary"
class="chat-inline-disclosure chat-tool-msg-summary"
type="button"
aria-expanded=${String(toolMessageExpanded)}
@click=${(event: MouseEvent) => {
@@ -479,6 +479,9 @@ export function renderGroupedMessage(
: toolPreview
? html`<span class="chat-tool-msg-summary__preview">${toolPreview}</span>`
: nothing}
<span class="chat-inline-disclosure__chevron" aria-hidden="true"
>${icons.chevronDown}</span
>
${toolMessageHasError
? html`<span class="chat-tool-row__badge">${t("chat.toolCards.failed")}</span>`
: nothing}
@@ -280,7 +280,7 @@ export function renderActivityGroup(
<div class="chat-group-messages">
<div class="chat-activity-group ${activityExpanded ? "is-open" : ""}">
<button
class="chat-activity-group__summary"
class="chat-inline-disclosure chat-activity-group__summary"
type="button"
aria-expanded=${String(activityExpanded)}
aria-controls=${activityBodyId}
@@ -294,9 +294,7 @@ export function renderActivityGroup(
<span class="chat-activity-group__label" title=${groupSummaryLabel}
>${groupSummaryLabel}</span
>
<span
class="collapse-chevron ${activityExpanded ? "" : "collapse-chevron--collapsed"}"
aria-hidden="true"
<span class="chat-inline-disclosure__chevron" aria-hidden="true"
>${icons.chevronDown}</span
>
</button>
@@ -169,7 +169,7 @@ export function renderWorkGroupSummary(
<div class="chat-group-messages">
<div class="chat-activity-group chat-work-group ${opts.expanded ? "is-open" : ""}">
<button
class="chat-activity-group__summary"
class="chat-inline-disclosure chat-activity-group__summary"
type="button"
aria-expanded=${String(opts.expanded)}
@click=${(event: MouseEvent) => {
@@ -180,9 +180,7 @@ export function renderWorkGroupSummary(
>
<span class="chat-activity-group__icon">${icons.check}</span>
<span class="chat-activity-group__label" title=${label}>${label}</span>
<span
class="collapse-chevron ${opts.expanded ? "" : "collapse-chevron--collapsed"}"
aria-hidden="true"
<span class="chat-inline-disclosure__chevron" aria-hidden="true"
>${icons.chevronDown}</span
>
</button>
@@ -692,7 +692,8 @@ describe("tool-cards", () => {
expect(container.querySelector(".chat-tool-card__preview-frame")).toBeNull();
expect(rawToggle).toBeInstanceOf(HTMLButtonElement);
expect(rawBody).toBeInstanceOf(HTMLElement);
expect([...rawToggle!.classList]).toEqual(["chat-tool-card__raw-toggle"]);
expect(rawToggle!.classList).toContain("chat-inline-disclosure");
expect(rawToggle!.classList).toContain("chat-tool-card__raw-toggle");
expect(rawToggle!.textContent?.trim()).toBe("Raw details");
expect(rawToggle!.getAttribute("aria-expanded")).toBe("false");
expect(rawBody!.hidden).toBe(true);
@@ -192,13 +192,13 @@ export function renderRawOutputToggle(text: string) {
return html`
<div class="chat-tool-card__raw">
<button
class="chat-tool-card__raw-toggle"
class="chat-inline-disclosure chat-tool-card__raw-toggle"
type="button"
aria-expanded="false"
@click=${handleRawDetailsToggle}
>
<span>${t("chat.toolCards.rawDetails")}</span>
<span class="chat-tool-card__raw-toggle-icon">${icons.chevronDown}</span>
<span class="chat-inline-disclosure__chevron" aria-hidden="true">${icons.chevronDown}</span>
</button>
<div class="chat-tool-card__raw-body" hidden>
${renderToolDataBlock({ label: t("chat.toolCards.toolOutput"), text })}
@@ -630,7 +630,9 @@ export function renderToolCard(
: ""}"
>
<button
class="chat-tool-msg-summary chat-tool-row ${isRunning ? "chat-tool-row--running" : ""}"
class="chat-inline-disclosure chat-tool-msg-summary chat-tool-row ${isRunning
? "chat-tool-row--running"
: ""}"
type="button"
aria-expanded=${String(opts.expanded)}
@click=${(event: MouseEvent) => {
@@ -641,6 +643,7 @@ export function renderToolCard(
>
<span class="chat-tool-msg-summary__icon">${renderToolIcon(icon)}</span>
${renderToolRowContent(card, view, outcome)}
<span class="chat-inline-disclosure__chevron" aria-hidden="true">${icons.chevronDown}</span>
${isError
? html`<span class="chat-tool-row__badge">${t("chat.toolCards.failed")}</span>`
: nothing}
+60 -113
View File
@@ -2,6 +2,61 @@
Tool rows deliberately carry no card chrome (border/background/shadow) so
dense agent activity stays scannable; detail surfaces appear only on expand. */
/* Compact text disclosure shared by activity rollups, tool rows, and raw
details. The button hugs its label while the expanded body keeps its own
full-width layout. */
.chat-inline-disclosure {
display: inline-flex;
align-items: center;
gap: 6px;
width: fit-content;
max-width: 100%;
min-width: 0;
box-sizing: border-box;
padding: 4px 0;
border: 0;
border-radius: var(--radius-sm);
background: transparent;
color: var(--muted);
font: inherit;
text-align: left;
user-select: text;
cursor: var(--cursor-action);
transition: color 150ms ease-out;
}
.chat-inline-disclosure:hover {
color: var(--text);
}
.chat-inline-disclosure:focus-visible {
color: var(--text);
outline: 2px solid var(--focus-ring);
outline-offset: 2px;
}
.chat-inline-disclosure__chevron {
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: transform 150ms ease-out;
}
.chat-inline-disclosure__chevron svg {
width: 12px;
height: 12px;
stroke: currentColor;
fill: none;
stroke-width: 1.5px;
stroke-linecap: round;
stroke-linejoin: round;
}
.chat-inline-disclosure[aria-expanded="false"] .chat-inline-disclosure__chevron {
transform: rotate(-90deg);
}
/* ── Collapsed tool row ── */
.chat-tool-msg-collapse {
width: 100%;
@@ -15,69 +70,19 @@
margin-top: 0;
}
/* Flat top-level tool rows align their content with the assistant text edge:
the row's 8px inline inset bleeds left into the avatar gutter (never right,
which could overflow the thread on narrow panes) so the hover pill keeps
its inset without indenting the icon/label. Child combinator keeps shells
nested in .chat-activity-group__body on the group's indented rail. */
.chat-group-messages > .chat-bubble--tool-shell .chat-tool-msg-summary {
width: calc(100% + 8px);
margin-left: -8px;
}
.chat-tool-msg-summary {
display: flex;
align-items: center;
gap: 7px;
min-width: 0;
width: 100%;
box-sizing: border-box;
padding: 4px 8px;
font-size: var(--control-ui-text-sm);
line-height: 1.5;
color: var(--muted);
user-select: text;
list-style: none;
border: 0;
border-radius: var(--radius-sm);
background: transparent;
text-align: left;
appearance: none;
-webkit-appearance: none;
font-family: inherit;
transition:
color 150ms ease,
background 150ms ease;
}
.chat-tool-msg-summary:hover,
.chat-tool-msg-summary:focus-visible {
background: color-mix(in srgb, var(--bg-hover) 60%, transparent);
color: var(--text);
}
.chat-tool-msg-summary::-webkit-details-marker {
display: none;
}
.chat-tool-msg-summary::before {
content: "▸";
display: inline-flex;
align-items: center;
justify-content: center;
width: 9px;
font-size: 12px;
line-height: 1;
color: var(--muted);
opacity: 0.7;
flex-shrink: 0;
transition: transform 150ms ease;
}
.chat-tool-msg-summary[aria-expanded="true"]::before {
transform: rotate(90deg);
}
.chat-tool-msg-summary__icon {
display: inline-flex;
align-items: center;
@@ -111,7 +116,7 @@
.chat-tool-msg-summary__names,
.chat-tool-msg-summary__preview {
flex: 1 1 0;
flex: 0 1 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
@@ -152,7 +157,7 @@
}
.chat-tool-row__cmd--secondary {
flex: 0 100000 auto;
flex: 0 1 auto;
color: color-mix(in srgb, var(--muted) 85%, var(--text) 15%);
}
@@ -175,7 +180,7 @@
}
.chat-tool-row__detail {
flex: 0 100000 auto;
flex: 0 1 auto;
min-width: 24px;
overflow: hidden;
text-overflow: ellipsis;
@@ -239,7 +244,6 @@
.chat-tool-row__spinner {
flex-shrink: 0;
margin-left: auto;
width: 7px;
height: 7px;
border-radius: var(--radius-full, 999px);
@@ -269,7 +273,6 @@
.chat-tool-row__badge {
flex-shrink: 0;
margin-left: auto;
padding: 1px 6px;
border-radius: var(--radius-full, 999px);
background: color-mix(in srgb, var(--destructive) 14%, transparent);
@@ -866,42 +869,7 @@
}
.chat-tool-card__raw-toggle {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 0;
border: 0;
background: transparent;
color: var(--muted);
font: inherit;
font-size: 12px;
transition: color 150ms ease-out;
}
.chat-tool-card__raw-toggle:hover,
.chat-tool-card__raw-toggle:focus-visible {
color: var(--text);
}
.chat-tool-card__raw-toggle[aria-expanded="true"] .chat-tool-card__raw-toggle-icon {
transform: rotate(180deg);
}
.chat-tool-card__raw-toggle-icon {
display: inline-flex;
align-items: center;
justify-content: center;
transition: transform 150ms ease-out;
}
.chat-tool-card__raw-toggle-icon svg {
width: 14px;
height: 14px;
stroke: currentColor;
fill: none;
stroke-width: 1.6px;
stroke-linecap: round;
stroke-linejoin: round;
}
.chat-tool-card__raw-body {
@@ -1005,26 +973,9 @@
}
.chat-activity-group__summary {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
min-width: 0;
box-sizing: border-box;
padding: 5px 8px;
border: 0;
border-radius: var(--radius-sm);
background: transparent;
padding-block: 5px;
color: var(--text);
font: inherit;
text-align: left;
user-select: text;
transition: background 150ms ease;
}
.chat-activity-group__summary:hover,
.chat-activity-group__summary:focus-visible {
background: color-mix(in srgb, var(--bg-hover) 60%, transparent);
}
.chat-activity-group__icon {
@@ -1046,7 +997,7 @@
}
.chat-activity-group__label {
flex: 1 1 auto;
flex: 0 1 auto;
min-width: 0;
overflow: hidden;
font-size: var(--control-ui-text-sm);
@@ -1056,10 +1007,6 @@
white-space: nowrap;
}
.chat-activity-group__summary .collapse-chevron {
flex-shrink: 0;
}
/* Rows form a flat indented list below the summary. The rail marks the group
extent while shared row hover pills remain free of per-row chrome. */
.chat-activity-group__body {
-23
View File
@@ -5144,29 +5144,6 @@ td.data-table-key-col {
}
}
/* ── Shared collapse chevron ── */
.collapse-chevron {
display: inline-flex;
align-items: center;
justify-content: center;
transition: transform var(--duration-fast) ease;
}
.collapse-chevron svg {
width: 12px;
height: 12px;
stroke: currentColor;
fill: none;
stroke-width: 1.5px;
stroke-linecap: round;
stroke-linejoin: round;
}
.collapse-chevron--collapsed {
transform: rotate(-90deg);
}
/* Mobile pairing */
.device-pair-setup {
max-height: calc(100dvh - 48px);