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:
ClawSweeper
2026-08-16 18:59:33 -07:00
committed by GitHub
parent 07da6f6d6d
commit 16ecf00983
8 changed files with 301 additions and 164 deletions
@@ -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",
+7 -26
View File
@@ -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();
+1 -3
View File
@@ -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
View File
@@ -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;