refactor(ui): align sidebar session list on a single lead-slot rail (#120473)

* refactor(ui): align sidebar session list on a single lead-slot rail

* fix(ui): keep sidebar group drag handle interactive

* fix(ui): keep catalog chevron visible on touch devices

Touch contexts have no hover, so branded catalog headers keep the chevron as their resting affordance (ClawSweeper P2 on #120473).
This commit is contained in:
Peter Steinberger
2026-08-08 03:47:26 -07:00
committed by GitHub
parent 95ea82b1e5
commit 0575610f4c
12 changed files with 202 additions and 106 deletions
@@ -138,6 +138,7 @@ export function renderSessionCatalogGroups(params: SessionCatalogGroupsParams) {
});
const hasActiveRun = liveRows.some((row) => row.hasActiveRun === true);
const hasUnread = liveRows.some((row) => row.unread === true);
const hasBrandIcon = hasProviderBrandIcon(catalog.id);
const loadingMore = params.loadingMoreCatalogIds.has(catalog.id);
const hasMore = hosts.some((host) => Boolean(host.nextCursor));
const canCreateSession = catalog.capabilities.createSession !== undefined;
@@ -199,15 +200,22 @@ export function renderSessionCatalogGroups(params: SessionCatalogGroupsParams) {
title=${hasError ? errorHelp : nothing}
@click=${() => params.onToggleSection(sectionId)}
>
${hasProviderBrandIcon(catalog.id)
? renderProviderBrandIcon(catalog.id, {
className: "sidebar-session-catalog-provider-icon",
})
: nothing}
<span class="sidebar-recent-sessions__label-text">${catalog.label}</span>
<span class="sidebar-session-group-toggle__icon" aria-hidden="true"
>${collapsed ? icons.chevronRight : icons.chevronDown}</span
<span
class="sidebar-session-group-toggle__lead ${hasBrandIcon
? "sidebar-session-group-toggle__lead--branded"
: ""}"
aria-hidden="true"
>
${hasBrandIcon
? renderProviderBrandIcon(catalog.id, {
className: "sidebar-session-catalog-provider-icon",
})
: nothing}
<span class="sidebar-session-group-toggle__icon"
>${collapsed ? icons.chevronRight : icons.chevronDown}</span
>
</span>
<span class="sidebar-recent-sessions__label-text">${catalog.label}</span>
${renderCatalogHeaderStatus(hasActiveRun, hasUnread)}
${hasError || (collapsed && rows.length > 0)
? html`<span
@@ -240,7 +248,7 @@ export function renderSessionCatalogGroups(params: SessionCatalogGroupsParams) {
${canCreateSession
? html`<button
type="button"
class="sidebar-session-group-actions sidebar-session-sort sidebar-session-new sidebar-session-catalog-new"
class="sidebar-session-group-actions sidebar-session-new sidebar-session-catalog-new"
title=${params.newSessionDisabledReason ??
`${t("chat.runControls.newSession")}${catalog.label}`}
aria-label=${`${t("chat.runControls.newSession")}${catalog.label}`}
@@ -445,6 +453,7 @@ 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>
@@ -134,10 +134,12 @@ function renderSessionSection(params: {
aria-label=${label}
@click=${() => host.toggleSection(section.id)}
>
<span class="sidebar-session-group-toggle__lead" aria-hidden="true">
<span class="sidebar-session-group-toggle__icon"
>${collapsed ? icons.chevronRight : icons.chevronDown}</span
>
</span>
<span class="sidebar-recent-sessions__label-text">${label}</span>
<span class="sidebar-session-group-toggle__icon" aria-hidden="true"
>${collapsed ? icons.chevronRight : icons.chevronDown}</span
>
${collapsed && totalRowCount > 0
? html`<span class="sidebar-session-group-count">${totalRowCount}</span>`
: nothing}
@@ -248,6 +250,7 @@ function renderDraftSessionRow() {
return html`
<div class="sidebar-recent-session sidebar-recent-session--draft">
<span class="sidebar-recent-session__link">
<span class="sidebar-session-indicator"></span>
<span class="sidebar-recent-session__text">
<span class="sidebar-recent-session__name">${t("newSession.draftRow")}</span>
</span>
@@ -270,11 +270,6 @@ export function renderRecentSession(params: {
@mouseenter=${(event: MouseEvent) => startHoverMarquee(event.currentTarget as HTMLElement)}
@mouseleave=${(event: MouseEvent) => stopHoverMarquee(event.currentTarget as HTMLElement)}
>
${session.visibility === "draft"
? html`<span class="session-row-draft-indicator" title=${t("chat.sessionSharing.draft")}
>👻</span
>`
: nothing}
<a
href=${session.href}
class="sidebar-recent-session__link"
@@ -284,9 +279,14 @@ export function renderRecentSession(params: {
aria-describedby=${[stateId, metaId].filter(Boolean).join(" ") || nothing}
@click=${(event: MouseEvent) => host.handleSessionRowClick(event, session)}
>
${leadingIndicator === nothing
? nothing
: html`<span class="sidebar-session-indicator">${leadingIndicator}</span>`}
<span class="sidebar-session-indicator"
>${leadingIndicator}
${session.visibility === "draft"
? html`<span class="session-row-draft-indicator" title=${t("chat.sessionSharing.draft")}
>👻</span
>`
: nothing}</span
>
<span class="sidebar-recent-session__text">
<span class="sidebar-recent-session__name hover-marquee"
>${session.archived
@@ -154,10 +154,7 @@ export function renderSessionLeadingState(
const sessionState = renderSessionState(session);
return {
running,
leadingIndicator:
sessionState !== nothing
? sessionState
: html`<span class="sidebar-session-indicator__dot" aria-hidden="true"></span>`,
leadingIndicator: sessionState,
trailingIndicator,
};
}
+30 -1
View File
@@ -226,7 +226,10 @@ async function openClaudeCatalogTerminal(page: Page) {
suite.define(() => {
it("groups Claude and Codex sessions by Gateway and paired-node host", async () => {
const page = await suite.browser.newPage({ viewport: { width: 1440, height: 900 } });
const page = await suite.browser.newPage({
hasTouch: true,
viewport: { width: 1440, height: 900 },
});
await installMockGateway(page, {
featureMethods: ["chat.metadata", "chat.startup", "sessions.catalog.list"],
methodResponses: { "sessions.catalog.list": hostGroupedNativeCatalogs() },
@@ -252,6 +255,32 @@ suite.define(() => {
);
}
const touchAffordance = await page
.locator(
'[data-session-section="catalog:claude"] .sidebar-session-group-toggle__lead--branded',
)
.evaluate((lead) => {
const providerIcon = lead.querySelector<HTMLElement>(
".sidebar-session-catalog-provider-icon",
);
const chevron = lead.querySelector<HTMLElement>(".sidebar-session-group-toggle__icon");
if (!providerIcon || !chevron) {
throw new Error("expected branded catalog provider icon and chevron");
}
return {
coarsePointer: matchMedia("(pointer: coarse)").matches,
noHover: matchMedia("(hover: none)").matches,
providerOpacity: getComputedStyle(providerIcon).opacity,
chevronOpacity: getComputedStyle(chevron).opacity,
};
});
expect(touchAffordance).toEqual({
coarsePointer: true,
noHover: true,
providerOpacity: "0",
chevronOpacity: "0.75",
});
const artifactDir = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
if (artifactDir) {
await fs.mkdir(artifactDir, { recursive: true });
+102 -53
View File
@@ -1324,6 +1324,8 @@ html.openclaw-native-macos
}
.sidebar-recent-sessions {
--sidebar-lead: 20px;
display: flex;
flex-direction: column;
gap: 10px;
@@ -1392,13 +1394,12 @@ html.openclaw-native-macos
}
.sidebar-recent-sessions__head {
position: relative;
display: flex;
align-items: center;
gap: 8px;
min-height: 24px;
/* 9px left matches .sidebar-recent-session__link text inset so zone labels
and session titles share one left edge. */
padding: 0 10px 0 9px;
padding: 0 10px 0 0;
color: color-mix(in srgb, var(--muted) 72%, var(--text) 28%);
}
@@ -1420,7 +1421,7 @@ html.openclaw-native-macos
.sidebar-session-group-toggle {
display: flex;
align-items: center;
gap: 5px;
gap: 0;
min-width: 0;
flex: 1 1 auto;
padding: 2px 0;
@@ -1430,22 +1431,44 @@ html.openclaw-native-macos
text-align: left;
}
.sidebar-session-group-toggle__icon {
display: inline-flex;
flex: 0 0 auto;
opacity: 0.75;
.sidebar-session-group-toggle__lead {
position: relative;
display: inline-grid;
width: var(--sidebar-lead);
height: 16px;
flex: 0 0 var(--sidebar-lead);
place-items: center;
}
.sidebar-session-group-toggle__icon,
.sidebar-session-catalog-provider-icon {
grid-area: 1 / 1;
transition: opacity var(--duration-fast) ease;
}
.sidebar-session-group-toggle[aria-expanded="true"] .sidebar-session-group-toggle__icon {
.sidebar-session-group-toggle__icon {
display: inline-flex;
opacity: 0.75;
}
.sidebar-session-group-toggle__lead--branded .sidebar-session-group-toggle__icon {
opacity: 0;
}
.sidebar-recent-sessions__head:hover
.sidebar-session-group-toggle[aria-expanded="true"]
.sidebar-session-group-toggle__lead--branded
.sidebar-session-catalog-provider-icon,
.sidebar-recent-sessions__head:focus-within
.sidebar-session-group-toggle__lead--branded
.sidebar-session-catalog-provider-icon {
opacity: 0;
}
.sidebar-recent-sessions__head:hover
.sidebar-session-group-toggle__lead--branded
.sidebar-session-group-toggle__icon,
.sidebar-recent-sessions__head:focus-within
.sidebar-session-group-toggle[aria-expanded="true"]
.sidebar-session-group-toggle__lead--branded
.sidebar-session-group-toggle__icon {
opacity: 0.75;
}
@@ -1499,9 +1522,9 @@ html.openclaw-native-macos
.sidebar-session-catalog-load-more {
display: block;
width: calc(100% - 20px);
margin: 3px 10px 5px;
padding: 4px 8px;
width: calc(100% - var(--sidebar-lead) - 10px);
margin: 3px 10px 5px var(--sidebar-lead);
padding: 4px 0;
border: none;
border-radius: var(--radius-sm);
background: transparent;
@@ -1522,9 +1545,13 @@ html.openclaw-native-macos
}
.sidebar-session-group-drag-handle {
width: 8px;
position: absolute;
top: 50%;
left: 1px;
z-index: 1;
width: 4px;
height: 18px;
flex: 0 0 8px;
transform: translateY(-50%);
border-radius: 3px;
background-image: radial-gradient(circle, currentColor 1px, transparent 1.2px);
background-position: 0 0;
@@ -1613,7 +1640,8 @@ html.openclaw-native-macos
.sidebar-recent-sessions__head:hover .sidebar-session-group-actions,
.sidebar-recent-sessions__head:focus-within .sidebar-session-group-actions,
.sidebar-session-group-actions[aria-expanded="true"] {
.sidebar-session-group-actions[aria-expanded="true"],
.sidebar-session-group-actions.sidebar-session-sort--filtered {
opacity: 1;
pointer-events: auto;
}
@@ -1635,7 +1663,13 @@ html.openclaw-native-macos
}
@media (hover: none), (pointer: coarse) {
.sidebar-session-group-toggle[aria-expanded="true"] .sidebar-session-group-toggle__icon {
/* Touch has no hover, so the chevron must be the resting state or
catalog collapsibility becomes undiscoverable. */
.sidebar-session-group-toggle__lead--branded .sidebar-session-catalog-provider-icon {
opacity: 0;
}
.sidebar-session-group-toggle__lead--branded .sidebar-session-group-toggle__icon {
opacity: 0.75;
}
@@ -1648,15 +1682,21 @@ html.openclaw-native-macos
/* Agent scope chip: a borderless select in the Sessions group header, sized
as metadata rather than a form control. */
.sidebar-recent-sessions__label-text {
.sidebar-recent-sessions__label-text,
.sidebar-session-catalog-host__label {
font-size: 11px;
font-weight: 650;
letter-spacing: 0.07em;
letter-spacing: 0.08em;
text-transform: uppercase;
color: color-mix(in srgb, var(--muted) 72%, var(--text) 28%);
opacity: 0.85;
}
.sidebar-session-group-toggle
> :not(.sidebar-session-group-toggle__lead):not(.sidebar-recent-sessions__label-text) {
margin-left: 5px;
}
/* Flex, not grid: WebKit does not re-run a grid flex-item's intrinsic sizing
when rows stream in or grow a second subtitle line after first layout, so a
grid list keeps a stale height and the next section paints over its rows. */
@@ -1676,21 +1716,20 @@ html.openclaw-native-macos
}
.sidebar-session-tree__children {
margin-left: 12px;
padding-left: 7px;
border-left: 1px solid color-mix(in srgb, var(--border-strong) 52%, transparent);
margin-left: var(--sidebar-lead);
box-shadow: inset 1px 0 0 color-mix(in srgb, var(--border-strong) 52%, transparent);
}
.sidebar-session-tree__loading {
min-height: 28px;
padding: 6px 8px;
padding: 6px var(--sidebar-lead);
color: var(--muted);
font-size: 11px;
}
.sidebar-session-tree__show-more {
min-height: 28px;
padding: 6px 8px;
padding: 6px var(--sidebar-lead);
border: 0;
background: transparent;
color: var(--muted);
@@ -1719,7 +1758,7 @@ html.openclaw-native-macos
align-items: center;
gap: 8px;
min-height: 19px;
padding: 1px 12px 1px 20px;
padding: 1px 12px 1px var(--sidebar-lead);
color: color-mix(in srgb, var(--muted) 82%, var(--text) 18%);
}
@@ -1729,9 +1768,6 @@ html.openclaw-native-macos
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 10px;
font-weight: 600;
letter-spacing: 0.025em;
}
.sidebar-session-catalog-host__count {
@@ -1769,10 +1805,10 @@ html.openclaw-native-macos
.sidebar-session-catalog-project__head {
display: flex;
align-items: center;
gap: 5px;
gap: 0;
width: 100%;
min-height: 18px;
padding: 2px 12px 0 20px;
padding: 2px 12px 0 0;
border: 0;
border-radius: var(--radius-sm);
background: transparent;
@@ -1792,8 +1828,10 @@ html.openclaw-native-macos
}
.sidebar-session-catalog-project__icon {
display: inline-flex;
flex: 0 0 auto;
display: inline-grid;
width: var(--sidebar-lead);
flex: 0 0 var(--sidebar-lead);
place-items: center;
opacity: 0.75;
}
@@ -1824,15 +1862,11 @@ html.openclaw-native-macos
text-align: center;
}
.sidebar-session-catalog-host__sessions .sidebar-recent-session__link {
padding-left: 18px;
}
.sidebar-session-pagination {
display: flex;
justify-content: center;
justify-content: flex-start;
gap: 14px;
padding: 0 10px 10px;
padding: 0 10px 10px var(--sidebar-lead);
}
.sidebar-session-pagination__button {
@@ -1851,16 +1885,11 @@ html.openclaw-native-macos
.sidebar-session-empty-hint {
display: block;
padding: 4px 10px 10px;
padding: 4px 10px 10px var(--sidebar-lead);
color: var(--muted);
font-size: 11px;
}
.sidebar-session-empty-placeholder {
/* Match the section title: 9px header inset + 8px drag handle + 8px gap. */
padding-left: 25px;
}
.sidebar-session--archived {
opacity: 0.62;
}
@@ -1930,15 +1959,21 @@ html.openclaw-native-macos
.sidebar-recent-session__link {
display: flex;
align-items: center;
gap: 8px;
gap: 0;
flex: 1 1 auto;
min-width: 0;
padding: 3px 4px 3px 9px;
padding: 3px 4px 3px 0;
color: inherit;
cursor: default;
text-decoration: none;
}
.sidebar-recent-sessions
.sidebar-recent-session__link
> :not(.sidebar-session-indicator):not(.sidebar-recent-session__text) {
margin-left: 8px;
}
.sidebar-child-session-toggle {
display: inline-flex;
align-items: center;
@@ -2156,11 +2191,17 @@ html.openclaw-native-macos
}
.sidebar-recent-session--child .sidebar-recent-session__link {
gap: 6px;
padding-top: 3px;
padding-bottom: 3px;
}
.sidebar-recent-sessions
.sidebar-recent-session--child
.sidebar-recent-session__link
> :not(.sidebar-session-indicator):not(.sidebar-recent-session__text) {
margin-left: 6px;
}
.sidebar-recent-session--child .sidebar-recent-session__name {
font-size: 12px;
}
@@ -4091,6 +4132,7 @@ html:not(.openclaw-native-macos):not(.openclaw-native-nav):not(.openclaw-native-
/* Sidebar sessions redesign: work-session subtitles, agent sections, draft row. */
.sidebar-session-indicator {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
@@ -4099,11 +4141,18 @@ html:not(.openclaw-native-macos):not(.openclaw-native-nav):not(.openclaw-native-
flex: 0 0 16px;
}
.sidebar-session-indicator__dot {
width: 7px;
height: 7px;
border-radius: var(--radius-full);
background: color-mix(in srgb, var(--muted) 76%, transparent);
.sidebar-recent-sessions .sidebar-session-indicator {
width: var(--sidebar-lead);
flex-basis: var(--sidebar-lead);
}
.sidebar-session-indicator > .session-row-draft-indicator:not(:only-child) {
position: absolute;
right: 0;
bottom: -1px;
width: 9px;
height: 9px;
font-size: 6px;
}
.sidebar-session-pr-indicator--open {
@@ -164,11 +164,12 @@ describe("AppSidebar session catalog pagination", () => {
});
it.each([
{ id: "claude", label: "Claude Code" },
{ id: "codex", label: "Codex" },
{ id: "opencode", label: "OpenCode" },
{ id: "pi", label: "Pi" },
])("groups $label catalog rows by their owning host", async ({ id, label }) => {
{ id: "claude", label: "Claude Code", branded: true },
{ id: "codex", label: "Codex", branded: true },
{ id: "opencode", label: "OpenCode", branded: true },
{ id: "pi", label: "Pi", branded: true },
{ id: "custom", label: "Custom", branded: false },
])("groups $label catalog rows by their owning host", async ({ id, label, branded }) => {
const gateway = createGateway({} as GatewayBrowserClient);
const { sidebar } = await mountSidebar(gateway, createSessions("main", ["agent:main:main"]));
sidebar.sessionData.sessionCatalogs = [
@@ -227,11 +228,10 @@ describe("AppSidebar session catalog pagination", () => {
await sidebar.updateComplete;
const section = sidebar.querySelector(`[data-session-section="catalog:${id}"]`);
expect(
section
?.querySelector(".sidebar-session-catalog-provider-icon")
?.getAttribute("data-provider-icon"),
).toBe(id);
const lead = section?.querySelector(".sidebar-session-group-toggle__lead");
expect(lead?.querySelector(".sidebar-session-group-toggle__icon")).not.toBeNull();
const providerIcon = lead?.querySelector(".sidebar-session-catalog-provider-icon");
expect(providerIcon?.getAttribute("data-provider-icon")).toBe(branded ? id : undefined);
const hostGroups = section?.querySelectorAll<HTMLElement>("[data-session-catalog-host]");
expect(Array.from(hostGroups ?? []).map((host) => host.dataset.sessionCatalogHost)).toEqual([
"gateway:local",
@@ -53,8 +53,12 @@ describe("AppSidebar project session activity", () => {
expect(active?.querySelector(".session-run-spinner")?.getAttribute("aria-label")).toBe(
"Active run",
);
expect(active?.querySelector(".sidebar-session-indicator")).toBeNull();
expect(idle?.querySelector(".sidebar-session-indicator")).toBeNull();
const activeLead = active?.querySelector(".sidebar-session-indicator");
const idleLead = idle?.querySelector(".sidebar-session-indicator");
expect(activeLead).not.toBeNull();
expect(activeLead?.childElementCount).toBe(0);
expect(idleLead).not.toBeNull();
expect(idleLead?.childElementCount).toBe(0);
expect(idle?.querySelector(".session-row-state")).toBeNull();
});
});
@@ -6,6 +6,12 @@ import { SESSION_PULL_REQUESTS_SUBSCRIBE_METHOD } from "../../lib/session-pull-r
import { createGatewayHarness, createSessionsHarness, mountSidebar } from "../app-sidebar.ts";
import { waitForFast } from "../wait-for.ts";
function expectEmptyLead(row: Element | null) {
const lead = row?.querySelector(".sidebar-session-indicator");
expect(lead).not.toBeNull();
expect(lead?.childElementCount).toBe(0);
}
describe("AppSidebar session indicators", () => {
it("preserves child PR indicators and concurrent pinned run/unread glyph state", async () => {
const parentKey = "agent:main:parent";
@@ -178,11 +184,11 @@ describe("AppSidebar session indicators", () => {
expect(sidebar.querySelector('[data-session-pr-state="merged"]')).not.toBeNull();
});
const plain = sidebar.querySelector(`[data-session-key="${keys.plain}"]`);
expect(plain?.querySelector(".sidebar-session-indicator")).toBeNull();
expectEmptyLead(plain);
expect(plain?.querySelector(".session-row-state")).toBeNull();
const forked = sidebar.querySelector(`[data-session-key="${keys.forked}"]`);
expect(forked?.querySelector(".sidebar-session-indicator")).toBeNull();
expectEmptyLead(forked);
expect(
forked?.querySelector(".session-row-aside > .session-row-state .session-row-fork-indicator"),
).not.toBeNull();
@@ -192,14 +198,14 @@ describe("AppSidebar session indicators", () => {
expect(forked?.querySelector(".session-row-fork-indicator")?.hasAttribute("title")).toBe(false);
const unread = sidebar.querySelector(`[data-session-key="${keys.unread}"]`);
expect(unread?.querySelector(".sidebar-session-indicator")).toBeNull();
expectEmptyLead(unread);
expect(
unread?.querySelector(".session-row-aside > .session-row-state .session-unread-dot"),
).not.toBeNull();
const runningUnread = sidebar.querySelector(`[data-session-key="${keys.runningUnread}"]`);
expect(runningUnread?.classList.contains("session-row-host--running")).toBe(true);
expect(runningUnread?.querySelector(".sidebar-session-indicator")).toBeNull();
expectEmptyLead(runningUnread);
expect(
runningUnread?.querySelector(".session-row-aside > .session-row-state .session-run-spinner"),
).not.toBeNull();
@@ -223,7 +229,7 @@ describe("AppSidebar session indicators", () => {
for (const key of [keys.openPullRequest, keys.mergedPullRequest]) {
const row = sidebar.querySelector(`[data-session-key="${key}"]`);
expect(row?.querySelector(".sidebar-session-indicator")).toBeNull();
expectEmptyLead(row);
expect(row?.querySelector(".session-row-state [data-session-pr-state]")).not.toBeNull();
expect(row?.querySelector("a")?.getAttribute("title")).toContain(
key === keys.openPullRequest ? "Open PR" : "Merged",
@@ -239,11 +245,7 @@ describe("AppSidebar session indicators", () => {
sessions.publishList({ result });
await waitForFast(() => {
expect(sidebar.querySelector('[data-session-pr-state="open"]')).toBeNull();
expect(
sidebar.querySelector(
`[data-session-key="${keys.openPullRequest}"] .sidebar-session-indicator`,
),
).toBeNull();
expectEmptyLead(sidebar.querySelector(`[data-session-key="${keys.openPullRequest}"]`));
});
});
});
@@ -32,9 +32,11 @@ describe("AppSidebar session section visibility", () => {
expect(list?.querySelector(".sidebar-recent-session--draft")?.textContent?.trim()).toBe(
"New thread",
);
expect(
list?.querySelector(".sidebar-recent-session--draft .sidebar-session-indicator"),
).toBeNull();
const draftLead = list?.querySelector(
".sidebar-recent-session--draft .sidebar-session-indicator",
);
expect(draftLead).not.toBeNull();
expect(draftLead?.childElementCount).toBe(0);
expect(
list?.querySelectorAll(".sidebar-recent-session:not(.sidebar-recent-session--draft)"),
).toHaveLength(0);
@@ -494,7 +494,6 @@ describe("AppSidebar session ownership", () => {
expect(row?.querySelector(".session-glyph openclaw-session-owner-chip")).not.toBeNull();
expect(row?.querySelector('.session-glyph__badge[aria-label="Unread"]')).toBeNull();
expect(row?.querySelector(".session-row-state .sidebar-recent-session__unread")).not.toBeNull();
expect(row?.querySelector(".sidebar-session-indicator__dot")).toBeNull();
});
it("keeps owner avatars off child rows", async () => {
@@ -296,8 +296,10 @@ describe("AppSidebar session accessibility", () => {
expect(row?.hasAttribute("aria-label")).toBe(false);
expect(link?.hasAttribute("aria-label")).toBe(false);
expect(link?.getAttribute("aria-current")).toBe("page");
expect(link?.querySelector(".sidebar-session-indicator")).toBeNull();
expect(link?.firstElementChild?.classList.contains("sidebar-recent-session__text")).toBe(true);
const lead = link?.querySelector(".sidebar-session-indicator");
expect(lead).not.toBeNull();
expect(lead?.childElementCount).toBe(0);
expect(link?.querySelector(".sidebar-recent-session__text")).not.toBeNull();
expect(row?.querySelector(".session-row-state .session-unread-dot")).not.toBeNull();
expect(link?.querySelector(".sidebar-recent-session__name")?.textContent).toBe(
"Quarterly launch plan",