mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-24 19:35:28 -06:00
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:
committed by
GitHub
parent
95ea82b1e5
commit
0575610f4c
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user