mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 12:26:38 -06:00
improve(ui): move session status to second row (#124960)
* improve(ui): move session status to second row * test(ui): align session row layout expectations --------- Co-authored-by: RoboClaw <309084314+roboclaw-bot@users.noreply.github.com>
This commit is contained in:
@@ -485,22 +485,28 @@ function renderCatalogSessionRow(
|
||||
<span class="sidebar-session-indicator"></span>
|
||||
<span class="sidebar-recent-session__text">
|
||||
<span class="sidebar-recent-session__name hover-marquee">${label}</span>
|
||||
<span class="sidebar-recent-session__details">
|
||||
<span class="sidebar-recent-session__details-endcap">
|
||||
${renderSessionRowBadges({
|
||||
hasAutomation: false,
|
||||
pullRequest: session.pullRequest,
|
||||
})}
|
||||
${running
|
||||
? html`<span class="session-row-aside">
|
||||
<span
|
||||
class="session-row-state"
|
||||
id=${stateId}
|
||||
role="img"
|
||||
aria-label=${stateDescription}
|
||||
>${renderSessionRunSpinner(false)}</span
|
||||
>
|
||||
</span>`
|
||||
: nothing}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
${renderSessionRowBadges({
|
||||
hasAutomation: false,
|
||||
pullRequest: session.pullRequest,
|
||||
})}
|
||||
</a>
|
||||
<span class="sidebar-recent-session__aside session-row-aside">
|
||||
${running
|
||||
? html`<span
|
||||
class="session-row-state"
|
||||
id=${stateId}
|
||||
role="img"
|
||||
aria-label=${stateDescription}
|
||||
>${renderSessionRunSpinner(false)}</span
|
||||
>`
|
||||
: nothing}
|
||||
<span class="session-row-actions">
|
||||
<button
|
||||
class="session-action"
|
||||
|
||||
@@ -307,35 +307,64 @@ export function renderRecentSession(params: {
|
||||
>`
|
||||
: nothing}${label}</span
|
||||
>
|
||||
${renderSidebarSessionSubtitle({ subtitle, narration })}
|
||||
<span class="sidebar-recent-session__details">
|
||||
${renderSidebarSessionSubtitle({ subtitle, narration })}
|
||||
<span class="sidebar-recent-session__details-endcap">
|
||||
${!session.isChild && sessionHasBoard(session.key)
|
||||
? html`<span
|
||||
class="sidebar-board-glyph"
|
||||
role="img"
|
||||
aria-label=${t("sessionsView.dashboardAvailable")}
|
||||
title=${t("sessionsView.dashboardAvailable")}
|
||||
>${icons.layoutDashboard}</span
|
||||
>`
|
||||
: nothing}
|
||||
<openclaw-viewer-facepile
|
||||
.presencePayload=${host.sessionData.presencePayload}
|
||||
.selfUserId=${host.sessionDataContext?.gateway.snapshot.selfUser?.id}
|
||||
.selfInstanceId=${host.sessionData.presenceInstanceId}
|
||||
.sessionKey=${session.key}
|
||||
.excludeUserId=${renderedOwnerId}
|
||||
.maxVisible=${3}
|
||||
variant="session"
|
||||
></openclaw-viewer-facepile>
|
||||
${renderSessionRowBadges({
|
||||
...session,
|
||||
hasComposerDraft: session.hasComposerDraft === true && !session.visuallyActive,
|
||||
pullRequest: session.pullRequest ?? display?.pullRequest,
|
||||
hasApproval: sessionHasPendingApproval(
|
||||
host.sessionData.approvalBadgeSnapshot(),
|
||||
session.key,
|
||||
),
|
||||
})}
|
||||
${trailingIndicator === nothing
|
||||
? nothing
|
||||
: html`<span class="session-row-aside">
|
||||
<span
|
||||
class="session-row-state"
|
||||
id=${stateId}
|
||||
role="img"
|
||||
aria-label=${trailingDescription}
|
||||
>${trailingIndicator}</span
|
||||
>
|
||||
</span>`}
|
||||
${hasTrail
|
||||
? html`<span class="session-row-trail" id=${metaId}
|
||||
>${session.runtimeMs != null
|
||||
? session.hasActiveRun
|
||||
? html`<openclaw-elapsed-time
|
||||
.startMs=${session.runtimeSampledAt! - session.runtimeMs}
|
||||
></openclaw-elapsed-time>`
|
||||
: (formatDurationCompact(session.runtimeMs) ?? "0ms")
|
||||
: html`<openclaw-elapsed-time
|
||||
.startMs=${session.startedAt!}
|
||||
.endMs=${session.endedAt ?? null}
|
||||
></openclaw-elapsed-time>`}</span
|
||||
>`
|
||||
: nothing}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
${!session.isChild && sessionHasBoard(session.key)
|
||||
? html`<span
|
||||
class="sidebar-board-glyph"
|
||||
role="img"
|
||||
aria-label=${t("sessionsView.dashboardAvailable")}
|
||||
title=${t("sessionsView.dashboardAvailable")}
|
||||
>${icons.layoutDashboard}</span
|
||||
>`
|
||||
: nothing}
|
||||
<openclaw-viewer-facepile
|
||||
.presencePayload=${host.sessionData.presencePayload}
|
||||
.selfUserId=${host.sessionDataContext?.gateway.snapshot.selfUser?.id}
|
||||
.selfInstanceId=${host.sessionData.presenceInstanceId}
|
||||
.sessionKey=${session.key}
|
||||
.excludeUserId=${renderedOwnerId}
|
||||
.maxVisible=${3}
|
||||
variant="session"
|
||||
></openclaw-viewer-facepile>
|
||||
${renderSessionRowBadges({
|
||||
...session,
|
||||
hasComposerDraft: session.hasComposerDraft === true && !session.visuallyActive,
|
||||
pullRequest: session.pullRequest ?? display?.pullRequest,
|
||||
hasApproval: sessionHasPendingApproval(
|
||||
host.sessionData.approvalBadgeSnapshot(),
|
||||
session.key,
|
||||
),
|
||||
})}
|
||||
</a>
|
||||
${session.childSessionKeys.length > 0
|
||||
? html`<button
|
||||
@@ -365,33 +394,10 @@ export function renderRecentSession(params: {
|
||||
>`}
|
||||
</button>`
|
||||
: nothing}
|
||||
<span class="sidebar-recent-session__aside session-row-aside">
|
||||
${trailingIndicator === nothing
|
||||
? nothing
|
||||
: html`<span
|
||||
class="session-row-state"
|
||||
id=${stateId}
|
||||
role="img"
|
||||
aria-label=${trailingDescription}
|
||||
>${trailingIndicator}</span
|
||||
>`}
|
||||
${hasTrail
|
||||
? html`<span class="session-row-trail" id=${metaId}
|
||||
>${session.runtimeMs != null
|
||||
? session.hasActiveRun
|
||||
? html`<openclaw-elapsed-time
|
||||
.startMs=${session.runtimeSampledAt! - session.runtimeMs}
|
||||
></openclaw-elapsed-time>`
|
||||
: (formatDurationCompact(session.runtimeMs) ?? "0ms")
|
||||
: html`<openclaw-elapsed-time
|
||||
.startMs=${session.startedAt!}
|
||||
.endMs=${session.endedAt ?? null}
|
||||
></openclaw-elapsed-time>`}</span
|
||||
>`
|
||||
: nothing}
|
||||
${session.isChild
|
||||
? nothing
|
||||
: html`<span class="session-row-actions">
|
||||
${session.isChild
|
||||
? nothing
|
||||
: html`<span class="sidebar-recent-session__aside session-row-aside">
|
||||
<span class="session-row-actions">
|
||||
<button
|
||||
class="session-action session-action--pin"
|
||||
data-sidebar-session-pin="true"
|
||||
@@ -419,8 +425,8 @@ export function renderRecentSession(params: {
|
||||
>
|
||||
${icons.moreHorizontal}
|
||||
</button>
|
||||
</span>`}
|
||||
</span>
|
||||
</span>
|
||||
</span>`}
|
||||
</div>
|
||||
`;
|
||||
// Marquee state mutates the row DOM; keying prevents cross-session reuse.
|
||||
|
||||
@@ -17,6 +17,12 @@ const terminalMetadataProofDir = path.join(
|
||||
"control-ui-e2e",
|
||||
"remote-session-sidebar-metadata",
|
||||
);
|
||||
const sessionSecondRowProofDir = path.join(
|
||||
process.cwd(),
|
||||
".artifacts",
|
||||
"control-ui-e2e",
|
||||
"session-status-second-row-implementation",
|
||||
);
|
||||
|
||||
suite.define(() => {
|
||||
it("replaces an intermediate running subtitle with the durable final reply", async () => {
|
||||
@@ -213,6 +219,122 @@ suite.define(() => {
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps session titles on the first line and status on a fixed second line", async () => {
|
||||
if (captureUiProofEnabled) {
|
||||
await mkdir(sessionSecondRowProofDir, { recursive: true });
|
||||
}
|
||||
const context = await suite.newBrowserContext({
|
||||
locale: "en-US",
|
||||
serviceWorkers: "block",
|
||||
viewport: { height: 900, width: 1280 },
|
||||
...(captureUiProofEnabled
|
||||
? { recordVideo: { dir: sessionSecondRowProofDir, size: { height: 900, width: 1280 } } }
|
||||
: {}),
|
||||
});
|
||||
const page = await context.newPage();
|
||||
const busyKey = "agent:main:busy-session";
|
||||
const plainKey = "agent:main:plain-session";
|
||||
await installMockGateway(page, {
|
||||
methodResponses: {
|
||||
"sessions.list": chatSessionListResponse([
|
||||
{
|
||||
key: busyKey,
|
||||
kind: "direct",
|
||||
label: "Terminal tab bar redesign proposal",
|
||||
updatedAt: 2,
|
||||
lastMessagePreview: "Implemented and committed as 094ab2",
|
||||
incognito: true,
|
||||
hasAutomation: true,
|
||||
status: "done",
|
||||
unread: true,
|
||||
},
|
||||
{
|
||||
key: plainKey,
|
||||
kind: "direct",
|
||||
label: "A session without secondary metadata",
|
||||
updatedAt: 1,
|
||||
},
|
||||
]),
|
||||
},
|
||||
sessionKey: busyKey,
|
||||
});
|
||||
|
||||
try {
|
||||
await page.goto(`${suite.server.baseUrl}chat`);
|
||||
const busyRow = page.locator(`.sidebar-recent-session[data-session-key="${busyKey}"]`);
|
||||
const plainRow = page.locator(`.sidebar-recent-session[data-session-key="${plainKey}"]`);
|
||||
await busyRow.locator(".session-row-badges").waitFor();
|
||||
if (captureUiProofEnabled) {
|
||||
await page.screenshot({
|
||||
fullPage: true,
|
||||
path: path.join(sessionSecondRowProofDir, "01-second-row-endcap.png"),
|
||||
});
|
||||
}
|
||||
|
||||
const layout = await busyRow.evaluate((row) => {
|
||||
const rect = (selector: string) => {
|
||||
const element = row.querySelector<HTMLElement>(selector);
|
||||
if (!element) {
|
||||
throw new Error(`Missing session row fixture ${selector}`);
|
||||
}
|
||||
const box = element.getBoundingClientRect();
|
||||
return {
|
||||
bottom: box.bottom,
|
||||
height: box.height,
|
||||
left: box.left,
|
||||
right: box.right,
|
||||
top: box.top,
|
||||
};
|
||||
};
|
||||
return {
|
||||
badges: rect(".session-row-badges"),
|
||||
busyHeight: row.getBoundingClientRect().height,
|
||||
name: rect(".sidebar-recent-session__name"),
|
||||
state: rect(".session-row-state"),
|
||||
subtitle: rect(".sidebar-recent-session__subtitle"),
|
||||
};
|
||||
});
|
||||
const plainHeight = await plainRow.evaluate((row) => row.getBoundingClientRect().height);
|
||||
|
||||
expect(layout.busyHeight).toBeCloseTo(plainHeight, 1);
|
||||
expect(layout.badges.top).toBeGreaterThanOrEqual(layout.name.bottom - 1);
|
||||
expect(layout.name.right).toBeGreaterThan(layout.badges.left);
|
||||
expect((layout.badges.top + layout.badges.bottom) / 2).toBeCloseTo(
|
||||
(layout.subtitle.top + layout.subtitle.bottom) / 2,
|
||||
1,
|
||||
);
|
||||
expect((layout.state.top + layout.state.bottom) / 2).toBeCloseTo(
|
||||
(layout.subtitle.top + layout.subtitle.bottom) / 2,
|
||||
1,
|
||||
);
|
||||
|
||||
await busyRow.hover();
|
||||
await expect
|
||||
.poll(() =>
|
||||
busyRow
|
||||
.locator(".sidebar-recent-session__details-endcap")
|
||||
.evaluate((element) => getComputedStyle(element).opacity),
|
||||
)
|
||||
.toBe("0");
|
||||
await expect
|
||||
.poll(() =>
|
||||
busyRow
|
||||
.locator("[data-session-menu]")
|
||||
.evaluate((element) => getComputedStyle(element).opacity),
|
||||
)
|
||||
.toBe("1");
|
||||
if (captureUiProofEnabled) {
|
||||
await page.screenshot({
|
||||
fullPage: true,
|
||||
path: path.join(sessionSecondRowProofDir, "02-hover-actions.png"),
|
||||
});
|
||||
}
|
||||
await plainRow.waitFor();
|
||||
} finally {
|
||||
await suite.closeBrowserContext(context);
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps the authenticated assistant avatar stable across same-agent switches", async () => {
|
||||
const context = await suite.newBrowserContext({
|
||||
locale: "en-US",
|
||||
|
||||
@@ -495,36 +495,17 @@ suite.define(() => {
|
||||
};
|
||||
}),
|
||||
);
|
||||
expect(threadRowMetrics).toEqual([
|
||||
{
|
||||
height: 30,
|
||||
expect(threadRowMetrics).toHaveLength(4);
|
||||
expect(new Set(threadRowMetrics.map((metric) => metric.height)).size).toBe(1);
|
||||
expect(threadRowMetrics[0]?.height).toBeGreaterThan(30);
|
||||
for (const metric of threadRowMetrics) {
|
||||
expect(metric).toMatchObject({
|
||||
minHeight: "30px",
|
||||
nameFontSize: "13px",
|
||||
paddingBottom: "3px",
|
||||
paddingTop: "3px",
|
||||
},
|
||||
{
|
||||
height: 30,
|
||||
minHeight: "30px",
|
||||
nameFontSize: "13px",
|
||||
paddingBottom: "3px",
|
||||
paddingTop: "3px",
|
||||
},
|
||||
{
|
||||
height: 30,
|
||||
minHeight: "30px",
|
||||
nameFontSize: "13px",
|
||||
paddingBottom: "3px",
|
||||
paddingTop: "3px",
|
||||
},
|
||||
{
|
||||
height: 30,
|
||||
minHeight: "30px",
|
||||
nameFontSize: "13px",
|
||||
paddingBottom: "3px",
|
||||
paddingTop: "3px",
|
||||
},
|
||||
]);
|
||||
});
|
||||
}
|
||||
const projectLabelTone = await openclawProject
|
||||
.locator(".sidebar-session-catalog-project__label")
|
||||
.evaluate((label) => {
|
||||
|
||||
@@ -855,9 +855,10 @@ suite.define(() => {
|
||||
await captureUiProof(page, "sidebar-session-dropped-into-group.png");
|
||||
|
||||
const ungrouped = page.locator('[data-session-section="ungrouped"]');
|
||||
const ungroupedHead = ungrouped.locator(":scope > .sidebar-recent-sessions__head");
|
||||
await gamma
|
||||
.locator('.sidebar-recent-session[data-session-key="agent:main:session-11"]')
|
||||
.dragTo(ungrouped);
|
||||
.dragTo(ungroupedHead, { targetPosition: { x: 4, y: 2 } });
|
||||
const ungroupedPatch = await waitForPatch(
|
||||
gateway,
|
||||
(params) => params.key === "agent:main:session-11" && params.category === null,
|
||||
|
||||
@@ -44,6 +44,7 @@ suite.define(() => {
|
||||
await actionOnlyRow.waitFor({ state: "visible", timeout: 10_000 });
|
||||
const actionOnlyText = actionOnlyRow.locator(".sidebar-recent-session__text");
|
||||
const actionOnlyLink = actionOnlyRow.locator(".sidebar-recent-session__link");
|
||||
const actionOnlyDetails = actionOnlyRow.locator(".sidebar-recent-session__details");
|
||||
const actionOnlyPin = actionOnlyRow.getByRole("button", { name: "Pin session" });
|
||||
await expect
|
||||
.poll(() => actionOnlyLink.evaluate((element) => getComputedStyle(element).paddingRight))
|
||||
@@ -53,7 +54,7 @@ suite.define(() => {
|
||||
await actionOnlyRow.hover();
|
||||
await expect.poll(() => actionOpacity(actionOnlyPin)).toBe("1");
|
||||
await expect
|
||||
.poll(() => actionOnlyLink.evaluate((element) => getComputedStyle(element).paddingRight))
|
||||
.poll(() => actionOnlyDetails.evaluate((element) => getComputedStyle(element).paddingRight))
|
||||
.toBe("52px");
|
||||
const hoveredTextBounds = await actionOnlyText.boundingBox();
|
||||
|
||||
@@ -61,14 +62,14 @@ suite.define(() => {
|
||||
await actionOnlyPin.focus();
|
||||
await expect.poll(() => actionOpacity(actionOnlyPin)).toBe("1");
|
||||
await expect
|
||||
.poll(() => actionOnlyLink.evaluate((element) => getComputedStyle(element).paddingRight))
|
||||
.poll(() => actionOnlyDetails.evaluate((element) => getComputedStyle(element).paddingRight))
|
||||
.toBe("52px");
|
||||
const focusedTextBounds = await actionOnlyText.boundingBox();
|
||||
if (!restingTextBounds || !hoveredTextBounds || !focusedTextBounds) {
|
||||
throw new Error("Expected visible action-only text geometry");
|
||||
}
|
||||
expect(restingTextBounds.width).toBeGreaterThanOrEqual(hoveredTextBounds.width);
|
||||
expect(restingTextBounds.width).toBeGreaterThanOrEqual(focusedTextBounds.width);
|
||||
expect(hoveredTextBounds.width).toBeCloseTo(restingTextBounds.width, 1);
|
||||
expect(focusedTextBounds.width).toBeCloseTo(restingTextBounds.width, 1);
|
||||
|
||||
const row = page.locator('[data-session-key="agent:main:hover-active"]');
|
||||
await row.waitFor({ state: "visible", timeout: 10_000 });
|
||||
@@ -91,7 +92,7 @@ suite.define(() => {
|
||||
if (!nameBounds || !pinBounds || !menuBounds) {
|
||||
throw new Error("Expected visible hovered action geometry");
|
||||
}
|
||||
expect(nameBounds.x + nameBounds.width).toBeLessThanOrEqual(pinBounds.x);
|
||||
expect(nameBounds.y + nameBounds.height / 2).toBeLessThan(pinBounds.y + pinBounds.height / 2);
|
||||
expect(pinBounds.x + pinBounds.width).toBeLessThanOrEqual(menuBounds.x);
|
||||
|
||||
await page.mouse.move(0, 0);
|
||||
@@ -108,7 +109,9 @@ suite.define(() => {
|
||||
if (!focusedNameBounds || !focusedPinBounds || !focusedMenuBounds) {
|
||||
throw new Error("Expected visible focused action geometry");
|
||||
}
|
||||
expect(focusedNameBounds.x + focusedNameBounds.width).toBeLessThanOrEqual(focusedPinBounds.x);
|
||||
expect(focusedNameBounds.y + focusedNameBounds.height / 2).toBeLessThan(
|
||||
focusedPinBounds.y + focusedPinBounds.height / 2,
|
||||
);
|
||||
expect(focusedPinBounds.x + focusedPinBounds.width).toBeLessThanOrEqual(focusedMenuBounds.x);
|
||||
} finally {
|
||||
await context.close();
|
||||
@@ -161,7 +164,9 @@ suite.define(() => {
|
||||
if (!nameBounds || !stateBounds || !pinBounds || !menuBounds) {
|
||||
throw new Error("Expected visible non-running touch state geometry");
|
||||
}
|
||||
expect(nameBounds.x + nameBounds.width).toBeLessThanOrEqual(stateBounds.x);
|
||||
expect(nameBounds.y + nameBounds.height / 2).toBeLessThan(
|
||||
stateBounds.y + stateBounds.height / 2,
|
||||
);
|
||||
expect(stateBounds.x + stateBounds.width).toBeLessThanOrEqual(pinBounds.x);
|
||||
expect(pinBounds.x + pinBounds.width).toBeLessThanOrEqual(menuBounds.x);
|
||||
} finally {
|
||||
@@ -297,11 +302,12 @@ suite.define(() => {
|
||||
await expect.poll(() => state.locator(".session-run-spinner").isVisible()).toBe(true);
|
||||
await expect.poll(() => state.locator(".session-unread-dot").isVisible()).toBe(true);
|
||||
const link = row.locator(".sidebar-recent-session__link");
|
||||
const details = row.locator(".sidebar-recent-session__details");
|
||||
const pin = row.getByRole("button", { name: "Pin session" });
|
||||
const menu = row.getByRole("button", { name: "Open session menu" });
|
||||
await expect
|
||||
.poll(() => link.evaluate((element) => getComputedStyle(element).paddingRight))
|
||||
.toBe("68px");
|
||||
.toBe("4px");
|
||||
|
||||
const [restingTextBounds, restingStateBounds, restingPinBounds, restingMenuBounds] =
|
||||
await Promise.all([
|
||||
@@ -314,30 +320,35 @@ suite.define(() => {
|
||||
throw new Error("Expected visible resting session state geometry");
|
||||
}
|
||||
const actionSurfaceWidth = restingMenuBounds.x + restingMenuBounds.width - restingPinBounds.x;
|
||||
expect(restingTextBounds.x + restingTextBounds.width).toBeLessThanOrEqual(
|
||||
restingStateBounds.x,
|
||||
);
|
||||
expect(restingTextBounds.x + restingTextBounds.width).toBeGreaterThan(
|
||||
restingStateBounds.x - actionSurfaceWidth,
|
||||
);
|
||||
const restingNameBounds = await row.locator(".sidebar-recent-session__name").boundingBox();
|
||||
if (!restingNameBounds) {
|
||||
throw new Error("Expected visible resting session title geometry");
|
||||
}
|
||||
expect(restingNameBounds.y + restingNameBounds.height / 2).toBeLessThan(
|
||||
restingStateBounds.y + restingStateBounds.height / 2,
|
||||
);
|
||||
await row.hover();
|
||||
await expect.poll(() => actionOpacity(state)).toBe("0");
|
||||
await expect.poll(() => actionOpacity(pin)).toBe("1");
|
||||
await expect.poll(() => actionOpacity(menu)).toBe("1");
|
||||
await expect
|
||||
.poll(() => link.evaluate((element) => getComputedStyle(element).paddingRight))
|
||||
.toBe("68px");
|
||||
.poll(() => details.evaluate((element) => getComputedStyle(element).paddingRight))
|
||||
.toBe("52px");
|
||||
|
||||
const [textBounds, pinBounds, menuBounds] = await Promise.all([
|
||||
const [textBounds, nameBounds, pinBounds, menuBounds] = await Promise.all([
|
||||
row.locator(".sidebar-recent-session__text").boundingBox(),
|
||||
row.locator(".sidebar-recent-session__name").boundingBox(),
|
||||
pin.boundingBox(),
|
||||
menu.boundingBox(),
|
||||
]);
|
||||
if (!textBounds || !pinBounds || !menuBounds) {
|
||||
if (!textBounds || !nameBounds || !pinBounds || !menuBounds) {
|
||||
throw new Error("Expected visible combined session action geometry");
|
||||
}
|
||||
expect(restingTextBounds.width).toBeGreaterThanOrEqual(textBounds.width);
|
||||
expect(textBounds.x + textBounds.width).toBeLessThanOrEqual(pinBounds.x);
|
||||
expect(textBounds.width).toBeCloseTo(restingTextBounds.width, 1);
|
||||
expect(nameBounds.y + nameBounds.height / 2).toBeLessThan(pinBounds.y + pinBounds.height / 2);
|
||||
expect(pinBounds.x + pinBounds.width).toBeLessThanOrEqual(menuBounds.x);
|
||||
await page.mouse.move(0, 0);
|
||||
await pin.focus();
|
||||
@@ -345,19 +356,23 @@ suite.define(() => {
|
||||
await expect.poll(() => actionOpacity(pin)).toBe("1");
|
||||
await expect.poll(() => actionOpacity(menu)).toBe("1");
|
||||
await expect
|
||||
.poll(() => link.evaluate((element) => getComputedStyle(element).paddingRight))
|
||||
.toBe("68px");
|
||||
.poll(() => details.evaluate((element) => getComputedStyle(element).paddingRight))
|
||||
.toBe("52px");
|
||||
|
||||
const [focusedTextBounds, focusedPinBounds, focusedMenuBounds] = await Promise.all([
|
||||
row.locator(".sidebar-recent-session__text").boundingBox(),
|
||||
pin.boundingBox(),
|
||||
menu.boundingBox(),
|
||||
]);
|
||||
if (!focusedTextBounds || !focusedPinBounds || !focusedMenuBounds) {
|
||||
const [focusedTextBounds, focusedNameBounds, focusedPinBounds, focusedMenuBounds] =
|
||||
await Promise.all([
|
||||
row.locator(".sidebar-recent-session__text").boundingBox(),
|
||||
row.locator(".sidebar-recent-session__name").boundingBox(),
|
||||
pin.boundingBox(),
|
||||
menu.boundingBox(),
|
||||
]);
|
||||
if (!focusedTextBounds || !focusedNameBounds || !focusedPinBounds || !focusedMenuBounds) {
|
||||
throw new Error("Expected visible focused session action geometry");
|
||||
}
|
||||
expect(restingTextBounds.width).toBeGreaterThanOrEqual(focusedTextBounds.width);
|
||||
expect(focusedTextBounds.x + focusedTextBounds.width).toBeLessThanOrEqual(focusedPinBounds.x);
|
||||
expect(focusedTextBounds.width).toBeCloseTo(restingTextBounds.width, 1);
|
||||
expect(focusedNameBounds.y + focusedNameBounds.height / 2).toBeLessThan(
|
||||
focusedPinBounds.y + focusedPinBounds.height / 2,
|
||||
);
|
||||
expect(focusedPinBounds.x + focusedPinBounds.width).toBeLessThanOrEqual(focusedMenuBounds.x);
|
||||
} finally {
|
||||
await context.close();
|
||||
|
||||
@@ -5687,9 +5687,7 @@ td.data-table-key-col {
|
||||
}
|
||||
|
||||
/* Attribute badges (attached automation, approval, cloud placement):
|
||||
muted metadata that sits after the title inside the row link, outside the
|
||||
trail/action overlap cell, so touch devices (which hide the trail) and
|
||||
hovered rows keep them. */
|
||||
muted metadata grouped in the row's second-line endcap. */
|
||||
.session-row-badges {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
+45
-37
@@ -2254,7 +2254,9 @@ body.update-dialog-open .sidebar-update-card__status {
|
||||
}
|
||||
|
||||
.sidebar-recent-session__name {
|
||||
flex: 1 1 auto;
|
||||
display: block;
|
||||
width: 100%;
|
||||
flex: 0 0 auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -2322,15 +2324,12 @@ body.update-dialog-open .sidebar-update-card__status {
|
||||
padding-right: 2px;
|
||||
}
|
||||
|
||||
/* The trailing aside overlays the row so action-only rows retain the full
|
||||
resting link width. It must not intercept clicks on the link beneath it —
|
||||
the buttons re-enable their own pointer-events on hover/focus. Child rows
|
||||
keep the in-flow aside because their runtime trail is resting content. */
|
||||
/* Actions overlay only the second line, preserving the title's full width.
|
||||
The buttons re-enable their own pointer-events on hover/focus. */
|
||||
.sidebar-recent-session:not(.sidebar-recent-session--child) > .sidebar-recent-session__aside {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
bottom: 0;
|
||||
right: 2px;
|
||||
transform: translateY(-50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -2342,42 +2341,22 @@ body.update-dialog-open .sidebar-update-card__status {
|
||||
right: calc(var(--sidebar-child-session-toggle-width) + 4px);
|
||||
}
|
||||
|
||||
/* State-bearing rows reserve the larger state/action maximum throughout the
|
||||
swap, while action-only rows reserve controls only when they appear. */
|
||||
.sidebar-recent-session:not(.sidebar-recent-session--child):where(
|
||||
:has(> .sidebar-recent-session__aside > .session-row-state)
|
||||
)
|
||||
> .sidebar-recent-session__link {
|
||||
padding-right: 68px;
|
||||
}
|
||||
|
||||
.sidebar-recent-session:not(.sidebar-recent-session--child):not(
|
||||
:has(> .sidebar-recent-session__aside > .session-row-state)
|
||||
):is(:hover, :focus-within)
|
||||
> .sidebar-recent-session__link {
|
||||
/* Hover actions replace the second-row endcap without shortening the title. */
|
||||
.sidebar-recent-session:not(.sidebar-recent-session--child):is(:hover, :focus-within)
|
||||
.sidebar-recent-session__details {
|
||||
padding-right: 52px;
|
||||
}
|
||||
|
||||
/* While the actions are surfaced, the link content fades out beneath them.
|
||||
A mask (not a background scrim) keeps the fade correct over rest, hover,
|
||||
active, and selected row backgrounds alike. */
|
||||
.sidebar-recent-session:not(.sidebar-recent-session--child):hover > .sidebar-recent-session__link,
|
||||
.sidebar-recent-session:not(.sidebar-recent-session--child):focus-within
|
||||
> .sidebar-recent-session__link {
|
||||
mask-image: linear-gradient(to right, black calc(100% - 80px), transparent calc(100% - 52px));
|
||||
.sidebar-recent-session:not(.sidebar-recent-session--child):is(:hover, :focus-within)
|
||||
.sidebar-recent-session__details-endcap {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Touch keeps the actions permanently visible, so the fade is permanent too. */
|
||||
/* Touch keeps the actions permanently visible beside the persistent endcap. */
|
||||
@media (hover: none), (pointer: coarse) {
|
||||
.sidebar-recent-session:not(.sidebar-recent-session--child):has(
|
||||
> .sidebar-recent-session__aside > .session-row-state
|
||||
)
|
||||
> .sidebar-recent-session__link {
|
||||
padding-right: 112px;
|
||||
}
|
||||
|
||||
.sidebar-recent-session:not(.sidebar-recent-session--child) > .sidebar-recent-session__link {
|
||||
mask-image: linear-gradient(to right, black calc(100% - 80px), transparent calc(100% - 52px));
|
||||
.sidebar-recent-session:not(.sidebar-recent-session--child) .sidebar-recent-session__details {
|
||||
padding-right: 52px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4195,14 +4174,43 @@ html:not(.openclaw-native-macos):not(.openclaw-native-nav):not(.openclaw-native-
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sidebar-recent-session__details {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
height: 18px;
|
||||
min-width: 0;
|
||||
transition: padding-right var(--duration-fast) ease;
|
||||
}
|
||||
|
||||
.sidebar-recent-session__subtitle {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: var(--control-ui-text-xs);
|
||||
line-height: 18px;
|
||||
color: color-mix(in srgb, var(--muted) 55%, var(--text) 45%);
|
||||
}
|
||||
|
||||
.sidebar-recent-session__details-endcap {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
max-width: 100%;
|
||||
height: 18px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
flex: 0 1 auto;
|
||||
margin-left: auto;
|
||||
transition: opacity var(--duration-fast) ease;
|
||||
}
|
||||
|
||||
.sidebar-recent-session__details-endcap .session-row-state {
|
||||
min-height: 18px;
|
||||
}
|
||||
|
||||
.sidebar-session-attention__icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in New Issue
Block a user