feat(ui): Home page and identity-row IA for the five-zone sidebar (#110295)

* feat(ui): Home page, identity row as agent menu, Pages header, Threads-hosted actions

* fix(ui): rename Home badge local shadowing the lit state import
This commit is contained in:
Peter Steinberger
2026-07-18 03:48:32 +01:00
committed by GitHub
parent 010f662f48
commit 73cf9ee908
8 changed files with 258 additions and 199 deletions
+33 -15
View File
@@ -377,6 +377,39 @@ export abstract class AppSidebarSessionListElement extends AppSidebarMenusElemen
></span>`
: nothing}
</button>
${section.id === "ungrouped"
? html`
<button
type="button"
class="sidebar-session-group-actions sidebar-session-sort"
title=${t("chat.sidebar.sortSessions")}
aria-label=${t("chat.sidebar.sortSessions")}
aria-haspopup="menu"
aria-expanded=${String(this.sessionSortMenuPosition !== null)}
@click=${(event: MouseEvent) => {
event.stopPropagation();
this.toggleSessionSortMenu(event.currentTarget as HTMLElement);
}}
>
${icons.listFilter}
</button>
<button
type="button"
class="sidebar-session-group-actions sidebar-new-session"
title=${this.connected
? t("chat.runControls.newSession")
: t("chat.runControls.newSessionDisconnected")}
aria-label=${t("chat.runControls.newSession")}
?disabled=${!this.connected}
@click=${(event: MouseEvent) => {
event.stopPropagation();
this.onOpenNewSession?.(this.expandedAgentId());
}}
>
${icons.plus}
</button>
`
: nothing}
${group
? html`
<button
@@ -520,21 +553,6 @@ export abstract class AppSidebarSessionListElement extends AppSidebarMenusElemen
`
: nothing}
<div class="sidebar-recent-sessions" aria-label=${titleForRoute("sessions")}>
<div class="sidebar-recent-sessions__head sidebar-recent-sessions__head--root">
<span class="sidebar-recent-sessions__label-text">${t("sessionsView.title")}</span>
<button
type="button"
class="sidebar-session-sort"
title=${t("chat.sidebar.sortSessions")}
aria-label=${t("chat.sidebar.sortSessions")}
aria-haspopup="menu"
aria-expanded=${String(this.sessionSortMenuPosition !== null)}
@click=${(event: MouseEvent) =>
this.toggleSessionSortMenu(event.currentTarget as HTMLElement)}
>
${icons.listFilter}
</button>
</div>
${this.renderSessionListBody(visibleSessions, {
showDraft:
Boolean(this.draftSessionAgentId) &&
+46 -23
View File
@@ -336,7 +336,7 @@ describe("AppSidebar update card wiring", () => {
});
describe("AppSidebar brand actions", () => {
it("starts a session for the active agent and leaves the sessions header sort-only", async () => {
it("starts a session for the active agent from the Threads header", async () => {
const gateway = createGateway({} as GatewayBrowserClient);
const agentsList = {
defaultId: "main",
@@ -355,12 +355,14 @@ describe("AppSidebar brand actions", () => {
sidebar.onOpenNewSession = onOpenNewSession;
await sidebar.updateComplete;
const button = sidebar.querySelector<HTMLButtonElement>(".sidebar-brand .sidebar-new-session");
// The new-session affordance moved off the brand row onto Threads.
expect(sidebar.querySelector(".sidebar-brand .sidebar-new-session")).toBeNull();
expect(sidebar.querySelector(".sidebar-recent-sessions__head--root")).toBeNull();
const button = sidebar.querySelector<HTMLButtonElement>(
'[data-session-section="ungrouped"] .sidebar-new-session',
);
expect(button?.getAttribute("aria-label")).toBe("New session");
expect(button?.disabled).toBe(false);
expect(
sidebar.querySelector(".sidebar-recent-sessions__head--root .sidebar-session-new"),
).toBeNull();
button?.click();
expect(onOpenNewSession).toHaveBeenCalledExactlyOnceWith("research");
@@ -391,7 +393,7 @@ describe("AppSidebar agent chip", () => {
sidebar.onNavigate = onNavigate;
await sidebar.updateComplete;
sidebar.querySelector<HTMLButtonElement>(".sidebar-agent-card__switcher")?.click();
sidebar.querySelector<HTMLButtonElement>(".sidebar-agent-card__main")?.click();
await sidebar.updateComplete;
const rows = [
...sidebar.querySelectorAll<HTMLElement>(
@@ -426,7 +428,7 @@ describe("AppSidebar agent chip", () => {
sidebar.onNavigate = onNavigate;
await sidebar.updateComplete;
sidebar.querySelector<HTMLButtonElement>(".sidebar-agent-card__switcher")?.click();
sidebar.querySelector<HTMLButtonElement>(".sidebar-agent-card__main")?.click();
await sidebar.updateComplete;
const menu = sidebar.querySelector<HTMLElement>(".sidebar-agent-menu");
const settingsAgent = [
@@ -517,7 +519,7 @@ describe("AppSidebar agent chip", () => {
// the main session lives behind the identity card instead of the list.
expect(sidebar.querySelector(".sidebar-agent-section")).toBeNull();
expect(sidebar.querySelectorAll(".sidebar-recent-session")).toHaveLength(0);
expect(sidebar.querySelector(".sidebar-agent-card__switcher-unread")).not.toBeNull();
expect(sidebar.querySelector(".sidebar-agent-card__menu-unread")).not.toBeNull();
// Mid-switch (selected agent != loaded result agent) the list renders the
// target agent's cached rows instead of flashing empty until refresh.
@@ -530,6 +532,27 @@ describe("AppSidebar agent chip", () => {
expect(rows[0]?.textContent).toContain("Research task");
});
it("routes Home to the main session and marks it active there", async () => {
const gateway = createGateway({} as GatewayBrowserClient);
const setSessionKey = vi.fn();
(gateway as { setSessionKey: (key: string) => void }).setSessionKey = setSessionKey;
const { sidebar } = await mountSidebar(gateway, createSessions("main", ["agent:main:main"]));
const navigate = vi.fn();
sidebar.onNavigate = navigate;
sidebar.connected = true;
(sidebar as unknown as { activeRouteId: string }).activeRouteId = "chat";
sidebar.sessionKey = "agent:main:main";
await sidebar.updateComplete;
const home = sidebar.querySelector<HTMLAnchorElement>(".nav-item--home");
expect(home?.textContent).toContain("Home");
expect(home?.getAttribute("aria-current")).toBe("page");
home?.click();
expect(setSessionKey).toHaveBeenCalledWith("agent:main:main");
expect(navigate).toHaveBeenCalledWith("chat", { search: "?session=agent%3Amain%3Amain" });
});
it("treats the global key as the main session under global scope", async () => {
const gateway = createGateway({} as GatewayBrowserClient);
const harness = createSessionsHarness("main", ["global"]);
@@ -554,12 +577,12 @@ describe("AppSidebar agent chip", () => {
});
await sidebar.updateComplete;
// The advertised global main hides behind the identity card instead of
// leaking into Threads; ordinary sessions still list, and the card
// surfaces the global row's unread state.
// The advertised global main hides behind the Home row instead of
// leaking into Threads; ordinary sessions still list, and Home surfaces
// the global row's unread state.
expect(sidebar.querySelector('[data-session-key="global"]')).toBeNull();
expect(sidebar.querySelector('[data-session-key="agent:main:side-quest"]')).not.toBeNull();
expect(sidebar.querySelector(".sidebar-agent-card__main .session-unread-dot")).not.toBeNull();
expect(sidebar.querySelector(".nav-item--home .session-unread-dot")).not.toBeNull();
});
it("promotes main-session children to top-level threads, including alias parent keys", async () => {
@@ -1207,7 +1230,7 @@ describe("AppSidebar agent chip", () => {
await sidebar.updateComplete;
expect(sidebar.querySelector(".sidebar-agent-card__name")?.textContent?.trim()).toBe("Molty");
sidebar.querySelector<HTMLButtonElement>(".sidebar-agent-card__switcher")?.click();
sidebar.querySelector<HTMLButtonElement>(".sidebar-agent-card__main")?.click();
await sidebar.updateComplete;
const menu = sidebar.querySelector(".sidebar-agent-menu");
@@ -1244,7 +1267,7 @@ describe("AppSidebar agent chip", () => {
openExternal.mockRestore();
await sidebar.updateComplete;
sidebar.querySelector<HTMLButtonElement>(".sidebar-agent-card__switcher")?.click();
sidebar.querySelector<HTMLButtonElement>(".sidebar-agent-card__main")?.click();
await sidebar.updateComplete;
const reopenedMenu = sidebar.querySelector(".sidebar-agent-menu");
@@ -1284,7 +1307,7 @@ describe("AppSidebar agent chip", () => {
sidebar.onNavigate = onNavigate;
await sidebar.updateComplete;
sidebar.querySelector<HTMLButtonElement>(".sidebar-agent-card__switcher")?.click();
sidebar.querySelector<HTMLButtonElement>(".sidebar-agent-card__main")?.click();
await sidebar.updateComplete;
const settingsRow = [
...sidebar.querySelectorAll<HTMLElement>(".sidebar-agent-menu wa-dropdown-item"),
@@ -1315,7 +1338,7 @@ describe("AppSidebar agent chip", () => {
sidebar.connected = true;
await sidebar.updateComplete;
sidebar.querySelector<HTMLButtonElement>(".sidebar-agent-card__switcher")?.click();
sidebar.querySelector<HTMLButtonElement>(".sidebar-agent-card__main")?.click();
await sidebar.updateComplete;
expect(sidebar.querySelector(".sidebar-agent-menu__filter")).toBeNull();
expect(
@@ -1339,7 +1362,7 @@ describe("AppSidebar agent chip", () => {
context.agentSelection.state.selectedId = "agent-1";
await sidebar.updateComplete;
sidebar.querySelector<HTMLButtonElement>(".sidebar-agent-card__switcher")?.click();
sidebar.querySelector<HTMLButtonElement>(".sidebar-agent-card__main")?.click();
await sidebar.updateComplete;
const input = sidebar.querySelector<HTMLInputElement>(".sidebar-agent-menu__filter input");
expect(input).not.toBeNull();
@@ -1390,7 +1413,7 @@ describe("AppSidebar agent chip", () => {
sidebar.connected = true;
await sidebar.updateComplete;
sidebar.querySelector<HTMLButtonElement>(".sidebar-agent-card__switcher")?.click();
sidebar.querySelector<HTMLButtonElement>(".sidebar-agent-card__main")?.click();
await sidebar.updateComplete;
expect(sidebar.querySelector(".sidebar-agent-menu__filter")).not.toBeNull();
expect(
@@ -1412,7 +1435,7 @@ describe("AppSidebar agent chip", () => {
sidebar.pinnedAgentIds = ["deleted-agent"];
await sidebar.updateComplete;
sidebar.querySelector<HTMLButtonElement>(".sidebar-agent-card__switcher")?.click();
sidebar.querySelector<HTMLButtonElement>(".sidebar-agent-card__main")?.click();
await sidebar.updateComplete;
expect(
sidebar.querySelectorAll(
@@ -1435,7 +1458,7 @@ describe("AppSidebar agent chip", () => {
sidebar.connected = true;
await sidebar.updateComplete;
sidebar.querySelector<HTMLButtonElement>(".sidebar-agent-card__switcher")?.click();
sidebar.querySelector<HTMLButtonElement>(".sidebar-agent-card__main")?.click();
await sidebar.updateComplete;
const rows = [
...sidebar.querySelectorAll(
@@ -3953,7 +3976,7 @@ describe("AppSidebar transient menus", () => {
it("ignores a stale agent-menu hide after opening its replacement", async () => {
const gateway = createGateway({} as GatewayBrowserClient);
const { sidebar } = await mountSidebar(gateway, createSessions("main", ["agent:main:main"]));
const trigger = sidebar.querySelector<HTMLButtonElement>(".sidebar-agent-card__switcher");
const trigger = sidebar.querySelector<HTMLButtonElement>(".sidebar-agent-card__main");
if (!trigger) {
throw new Error("expected agent menu trigger");
}
@@ -3987,9 +4010,9 @@ describe("AppSidebar transient menus", () => {
it("ignores a stale More-menu hide after opening its replacement", async () => {
const gateway = createGateway({} as GatewayBrowserClient);
const { sidebar } = await mountSidebar(gateway, createSessions("main", ["agent:main:main"]));
const trigger = sidebar.querySelector<HTMLButtonElement>("button.nav-item--action");
const trigger = sidebar.querySelector<HTMLButtonElement>(".sidebar-nav__head-action");
if (!trigger) {
throw new Error("expected More menu trigger");
throw new Error("expected Pages menu trigger");
}
trigger.click();
+88 -51
View File
@@ -1,11 +1,11 @@
import { html, nothing } from "lit";
import { state } from "lit/decorators.js";
import type { NavigationRouteId } from "../app-navigation.ts";
import { pathForRoute } from "../app-route-paths.ts";
import { beginNativeWindowDragFromTopInset } from "../app/native-window-drag.ts";
import { controlUiPublicAssetPath } from "../app/public-assets.ts";
import { t } from "../i18n/index.ts";
import { normalizeAgentLabel, resolveAgentTextAvatar } from "../lib/agents/display.ts";
import { resolveAgentAvatarUrl } from "../lib/avatar.ts";
import "./menu-surface.ts";
import "./session-menu.ts";
import "./sidebar-agent-card.ts";
@@ -14,7 +14,10 @@ import "./sidebar-build-chip.ts";
import "./sidebar-update-card.ts";
import "./theme-mode-toggle.ts";
import "./tooltip.ts";
import { resolveAgentAvatarUrl } from "../lib/avatar.ts";
import { searchForSession } from "../lib/sessions/index.ts";
import { areUiSessionKeysEquivalent, normalizeAgentId } from "../lib/sessions/session-key.ts";
import { shouldHandleNavigationClick } from "./app-sidebar-nav-menus.ts";
import { AppSidebarSessionListElement } from "./app-sidebar-session-list.ts";
import { icons } from "./icons.ts";
import {
@@ -76,25 +79,34 @@ class AppSidebar extends AppSidebarSessionListElement {
private renderBrand() {
const collapseLabel = t("nav.collapse");
const chipAgentId = this.activeChipAgent().activeId;
const newSessionTitle = this.connected
? t("chat.runControls.newSession")
: t("chat.runControls.newSessionDisconnected");
const gatewayStatus = t("chat.gatewayStatus", {
status: this.connected ? t("common.online") : t("common.offline"),
});
const { activeId: cardAgentId, agent: cardAgent, agents: cardAgents } = this.activeChipAgent();
const menuUnread = cardAgents.some((entry) => {
const agentId = normalizeAgentId(entry.id);
return agentId !== cardAgentId && this.agentUnreadCount(agentId) > 0;
});
const cardName = cardAgent ? normalizeAgentLabel(cardAgent) : cardAgentId;
const cardAvatarText =
(cardAgent ? resolveAgentTextAvatar(cardAgent) : null) ??
(cardName || cardAgentId).slice(0, 1).toUpperCase();
return html`
<div class="sidebar-brand">
<openclaw-sidebar-agent-card
.agentName=${cardName}
.avatarUrl=${cardAgent ? resolveAgentAvatarUrl(cardAgent) : null}
.avatarText=${cardAvatarText}
.connected=${this.connected}
.statusLabel=${gatewayStatus}
.subtitle=${this.agentChipSubtitle(cardAgentId)}
.menuOpen=${this.agentMenuPosition !== null}
.menuUnread=${menuUnread}
.switcherAvailable=${cardAgents.length > 1}
.onToggleMenu=${(trigger: HTMLElement) => this.toggleAgentMenu(trigger)}
></openclaw-sidebar-agent-card>
<div class="sidebar-brand__actions">
${this.renderSearch()}
<openclaw-tooltip .content=${newSessionTitle}>
<button
class="sidebar-brand__icon sidebar-new-session"
type="button"
?disabled=${!this.connected}
@click=${() => this.onOpenNewSession?.(chipAgentId)}
aria-label=${t("chat.runControls.newSession")}
>
${icons.plus}
</button>
</openclaw-tooltip>
<openclaw-tooltip .content=${`${collapseLabel} (⌘B)`}>
<button
class="sidebar-brand__icon sidebar-brand__collapse"
@@ -111,42 +123,66 @@ class AppSidebar extends AppSidebarSessionListElement {
`;
}
/** Zone 1: the agent identity card is the main session's entry point. */
private renderAgentCard() {
const gatewayStatus = t("chat.gatewayStatus", {
status: this.connected ? t("common.online") : t("common.offline"),
});
const { activeId: cardAgentId, agent: cardAgent, agents: cardAgents } = this.activeChipAgent();
const menuUnread = cardAgents.some((entry) => {
const agentId = normalizeAgentId(entry.id);
return agentId !== cardAgentId && this.agentUnreadCount(agentId) > 0;
});
const cardName = cardAgent ? normalizeAgentLabel(cardAgent) : cardAgentId;
const cardAvatarText =
(cardAgent ? resolveAgentTextAvatar(cardAgent) : null) ??
(cardName || cardAgentId).slice(0, 1).toUpperCase();
const mainRow = this.mainSessionRow(cardAgentId);
const mainKey = this.selectedAgentMainSessionKey(cardAgentId);
const mainSessionActive =
/** Home: the first page. Opens the agent's rolling main session and carries
its unread/running state; later grows into the docked dashboard surface. */
private renderHomeRow() {
const agentId = this.activeChipAgent().activeId;
const mainKey = this.selectedAgentMainSessionKey(agentId);
const mainRow = this.mainSessionRow(agentId);
const active =
this.activeRouteId === "chat" &&
areUiSessionKeysEquivalent(this.getRouteSessionKey(), mainKey);
const stateBadge = mainRow?.hasActiveRun
? html`<span
class="session-run-spinner nav-item__state"
role="img"
aria-label=${t("sessionsView.activeRun")}
title=${t("sessionsView.activeRun")}
></span>`
: mainRow?.unread === true && !active
? html`<span
class="session-unread-dot nav-item__state"
role="img"
aria-label=${t("sessionsView.unread")}
></span>`
: nothing;
return html`
<openclaw-sidebar-agent-card
.agentName=${cardName}
.avatarUrl=${cardAgent ? resolveAgentAvatarUrl(cardAgent) : null}
.avatarText=${cardAvatarText}
.connected=${this.connected}
.statusLabel=${gatewayStatus}
.subtitle=${this.agentChipSubtitle(cardAgentId)}
.activeSession=${mainSessionActive}
.running=${Boolean(mainRow?.hasActiveRun)}
.unread=${mainRow?.unread === true && !mainSessionActive}
.menuOpen=${this.agentMenuPosition !== null}
.menuUnread=${menuUnread}
.switcherAvailable=${cardAgents.length > 1}
.onOpenMain=${() => this.openMainSession(cardAgentId)}
.onToggleMenu=${(trigger: HTMLElement) => this.toggleAgentMenu(trigger)}
></openclaw-sidebar-agent-card>
<a
href=${`${pathForRoute("chat", this.basePath)}${searchForSession(mainKey)}`}
class="nav-item nav-item--home ${active ? "nav-item--active" : ""}"
aria-current=${active ? "page" : nothing}
@click=${(event: MouseEvent) => {
if (!shouldHandleNavigationClick(event)) {
return;
}
event.preventDefault();
this.openMainSession(agentId);
}}
>
<span class="nav-item__icon" aria-hidden="true">${icons.home}</span>
<span class="nav-item__text">${t("nav.home")}</span>
${stateBadge}
</a>
`;
}
/** "Pages" header: the customize affordance opens the pages menu (all
routes navigable, pin editor behind it) that used to hide behind More. */
private renderPagesHead() {
return html`
<div class="sidebar-nav__head">
<span class="sidebar-recent-sessions__label-text">${t("nav.pages")}</span>
<button
type="button"
class="sidebar-nav__head-action"
aria-haspopup="menu"
aria-expanded=${String(this.moreMenuPosition !== null)}
aria-label=${t("nav.customize")}
@click=${(event: MouseEvent) => this.toggleMoreMenu(event.currentTarget as HTMLElement)}
>
${icons.penLine}
</button>
</div>
`;
}
@@ -215,16 +251,17 @@ class AppSidebar extends AppSidebarSessionListElement {
return html`
<aside class="sidebar">
<div class="sidebar-shell" @mousedown=${beginNativeWindowDragFromTopInset}>
${this.renderBrand()} ${this.renderAgentCard()}
${this.renderBrand()}
<div
class="sidebar-shell__body sidebar-shell__body--scroll-${this.sessionsScrollState}"
@scroll=${(event: Event) =>
this.updateSessionsScrollState(event.currentTarget as HTMLElement)}
>
<nav class="sidebar-nav" @contextmenu=${this.openCustomizeMenuFromContext}>
${this.renderPagesHead()}
<div class="nav-section__items">
${this.renderHomeRow()}
${this.sidebarPinnedRoutes.map((routeId) => this.renderRoute(routeId))}
${this.renderMoreRow()}
</div>
</nav>
${this.renderSessions()}
+6
View File
@@ -357,6 +357,12 @@ export const icons = {
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
</svg>
`,
home: html`
<svg viewBox="0 0 24 24">
<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
<polyline points="9 22 9 12 15 12 15 22" />
</svg>
`,
penLine: html`
<svg viewBox="0 0 24 24">
<path d="M12 20h9" />
+26 -64
View File
@@ -5,9 +5,9 @@ import { OpenClawLightDomContentsElement } from "../lit/openclaw-element.ts";
import { icons } from "./icons.ts";
import "./tooltip.ts";
/** Sidebar identity card: the agent IS the main session. The body opens the
agent's rolling main conversation; the trailing button opens the agent
menu owned by app-sidebar (labeled as a switcher with 2+ agents). */
/** Sidebar identity row: who you're talking to. The whole body opens the
agent menu (switcher + utilities) — the conversation itself lives on the
Home page row, so this row carries profile semantics only. */
class SidebarAgentCard extends OpenClawLightDomContentsElement {
@property({ attribute: false }) agentName = "";
@property({ attribute: false }) avatarUrl: string | null = null;
@@ -15,50 +15,26 @@ class SidebarAgentCard extends OpenClawLightDomContentsElement {
@property({ attribute: false }) connected = false;
@property({ attribute: false }) statusLabel = "";
@property({ attribute: false }) subtitle = "";
/** The main session is the currently open chat route. */
@property({ attribute: false }) activeSession = false;
/** Main session has an active run; shows the working spinner. */
@property({ attribute: false }) running = false;
/** Main session has unread activity. */
@property({ attribute: false }) unread = false;
@property({ attribute: false }) menuOpen = false;
/** Unread sessions exist on non-active agents; surfaces on the switcher. */
/** Unread sessions exist on non-active agents; surfaces next to the name. */
@property({ attribute: false }) menuUnread = false;
/** More than one agent is configured; single-agent setups hide the switcher. */
/** More than one agent is configured; labels the menu as a switcher. */
@property({ attribute: false }) switcherAvailable = false;
@property({ attribute: false }) onOpenMain?: () => void;
@property({ attribute: false }) onToggleMenu?: (trigger: HTMLElement) => void;
private renderState() {
if (this.running) {
return html`<span
class="session-run-spinner sidebar-agent-card__state"
role="img"
aria-label=${t("sessionsView.activeRun")}
title=${t("sessionsView.activeRun")}
></span>`;
}
if (this.unread) {
return html`<span
class="session-unread-dot sidebar-agent-card__state"
role="img"
aria-label=${t("sessionsView.unread")}
></span>`;
}
return nothing;
}
override render() {
const menuLabel = this.switcherAvailable
? t("agentChip.switchAgent")
: t("agentChip.menuLabel");
return html`
<div class="sidebar-agent-card ${this.activeSession ? "sidebar-agent-card--active" : ""}">
<div class="sidebar-agent-card ${this.menuOpen ? "sidebar-agent-card--open" : ""}">
<button
type="button"
class="sidebar-agent-card__main"
aria-current=${this.activeSession ? "page" : nothing}
aria-label=${this.connected
? t("agentChip.openChat", { name: this.agentName })
: t("agentChip.offlineOpenSettings")}
@click=${() => this.onOpenMain?.()}
aria-haspopup="menu"
aria-expanded=${String(this.menuOpen)}
aria-label="${this.agentName} · ${menuLabel}"
@click=${(event: MouseEvent) => this.onToggleMenu?.(event.currentTarget as HTMLElement)}
>
<span class="sidebar-agent-card__avatar">
${this.avatarUrl
@@ -83,38 +59,24 @@ class SidebarAgentCard extends OpenClawLightDomContentsElement {
></span>
</span>
<span class="sidebar-agent-card__text">
<span class="sidebar-agent-card__name">${this.agentName}</span>
<span class="sidebar-agent-card__name">
${this.agentName}
<span class="sidebar-agent-card__chevron" aria-hidden="true"
>${icons.chevronDown}</span
>
</span>
${this.subtitle
? html`<span class="sidebar-agent-card__subtitle">${this.subtitle}</span>`
: nothing}
</span>
${this.renderState()}
${this.menuUnread && !this.menuOpen
? html`<span
class="session-unread-dot sidebar-agent-card__menu-unread"
role="img"
aria-label=${t("sessionsView.unread")}
></span>`
: nothing}
</button>
<openclaw-tooltip
.content=${this.switcherAvailable ? t("agentChip.switchAgent") : t("agentChip.menuLabel")}
>
<button
type="button"
class="sidebar-agent-card__switcher ${this.menuOpen
? "sidebar-agent-card__switcher--open"
: ""}"
aria-haspopup="menu"
aria-expanded=${String(this.menuOpen)}
aria-label=${this.switcherAvailable
? t("agentChip.switchAgent")
: t("agentChip.menuLabel")}
@click=${(event: MouseEvent) => this.onToggleMenu?.(event.currentTarget as HTMLElement)}
>
${icons.chevronDown}
${this.menuUnread && !this.menuOpen
? html`<span
class="session-unread-dot sidebar-agent-card__switcher-unread"
role="img"
aria-label=${t("sessionsView.unread")}
></span>`
: nothing}
</button>
</openclaw-tooltip>
</div>
`;
}
+2
View File
@@ -1563,6 +1563,8 @@ export const en: TranslationMap = {
collapse: "Collapse sidebar",
resize: "Resize sidebar",
more: "More",
home: "Home",
pages: "Pages",
customize: "Edit pinned items",
customizeReset: "Reset pinned items",
},
+6 -9
View File
@@ -35,18 +35,15 @@ export type SidebarSessionSection<Row> = {
};
/**
* Sections that render a header (and therefore can collapse). Threads
* ("ungrouped") only shows its header in category grouping; the flat mode
* keeps it headerless. Shared by the renderer and keyboard-order walker so
* collapse behavior cannot drift between them.
* Sections that render a header (and therefore can collapse). Every zone
* shows one — Threads hosts the sort and new-session actions on its header.
* Shared by the renderer and keyboard-order walker so collapse behavior
* cannot drift between them.
*/
export function sidebarSectionHasHeader(
sectionId: string,
grouping: SidebarSessionsGrouping,
_sectionId: string,
_grouping: SidebarSessionsGrouping,
): boolean {
if (sectionId === "ungrouped") {
return grouping === "category";
}
return true;
}
+51 -37
View File
@@ -864,8 +864,7 @@ html.openclaw-native-web-chrome .shell:not(.shell--mobile-nav) .sidebar-brand .s
display: flex;
align-items: center;
gap: 4px;
flex: 1 1 auto;
justify-content: flex-end;
flex: 0 0 auto;
}
.sidebar-brand__icon {
@@ -2266,17 +2265,8 @@ wa-dropdown.sidebar-session-sort-menu::part(menu) {
.sidebar-agent-card {
display: flex;
align-items: center;
gap: 2px;
margin: 0 8px 10px;
padding: 4px;
border: 1px solid var(--border);
border-radius: var(--radius-lg, 12px);
background: color-mix(in srgb, var(--bg-hover) 40%, transparent);
}
.sidebar-agent-card--active {
border-color: color-mix(in srgb, var(--accent) 55%, var(--border));
background: color-mix(in srgb, var(--accent-subtle, var(--bg-hover)) 55%, transparent);
min-width: 0;
flex: 1 1 auto;
}
.sidebar-agent-card__main {
@@ -2285,7 +2275,7 @@ wa-dropdown.sidebar-session-sort-menu::part(menu) {
gap: 10px;
flex: 1 1 auto;
min-width: 0;
padding: 5px 6px;
padding: 4px 6px;
border: none;
border-radius: var(--radius-md);
background: transparent;
@@ -2295,7 +2285,8 @@ wa-dropdown.sidebar-session-sort-menu::part(menu) {
}
.sidebar-agent-card__main:hover,
.sidebar-agent-card__main:focus-visible {
.sidebar-agent-card__main:focus-visible,
.sidebar-agent-card--open .sidebar-agent-card__main {
background: color-mix(in srgb, var(--bg-hover) 84%, transparent);
}
@@ -2353,6 +2344,9 @@ wa-dropdown.sidebar-session-sort-menu::part(menu) {
}
.sidebar-agent-card__name {
display: flex;
align-items: center;
gap: 4px;
overflow: hidden;
color: var(--text-strong);
font-size: 14px;
@@ -2362,6 +2356,22 @@ wa-dropdown.sidebar-session-sort-menu::part(menu) {
white-space: nowrap;
}
.sidebar-agent-card__chevron {
display: inline-flex;
flex: none;
color: var(--muted);
}
.sidebar-agent-card__chevron svg {
width: 13px;
height: 13px;
stroke: currentColor;
fill: none;
stroke-width: 1.5px;
stroke-linecap: round;
stroke-linejoin: round;
}
.sidebar-agent-card__subtitle {
overflow: hidden;
color: var(--muted);
@@ -2371,18 +2381,30 @@ wa-dropdown.sidebar-session-sort-menu::part(menu) {
white-space: nowrap;
}
.sidebar-agent-card__state {
.sidebar-agent-card__menu-unread {
flex: none;
margin-left: auto;
width: 6px;
height: 6px;
}
.sidebar-agent-card__switcher {
position: relative;
/* "Pages" section head: zone-style label plus the pin-editor affordance. */
.sidebar-nav__head {
display: flex;
align-items: center;
padding: 2px 8px 2px 12px;
}
.sidebar-nav__head .sidebar-recent-sessions__label-text {
flex: 1 1 auto;
}
.sidebar-nav__head-action {
display: inline-flex;
align-items: center;
justify-content: center;
width: 26px;
height: 26px;
width: 22px;
height: 22px;
flex: none;
border: none;
border-radius: var(--radius-md);
@@ -2394,21 +2416,15 @@ wa-dropdown.sidebar-session-sort-menu::part(menu) {
background var(--duration-fast) ease;
}
.sidebar-agent-card__switcher:hover,
.sidebar-agent-card__switcher:focus-visible,
.sidebar-agent-card__switcher--open {
.sidebar-nav__head-action:hover,
.sidebar-nav__head-action:focus-visible {
color: var(--text);
background: color-mix(in srgb, var(--bg-hover) 84%, transparent);
}
.sidebar-agent-card__switcher:focus-visible {
outline: 2px solid color-mix(in srgb, var(--accent) 48%, transparent);
outline-offset: 1px;
}
.sidebar-agent-card__switcher svg {
width: 15px;
height: 15px;
.sidebar-nav__head-action svg {
width: 13px;
height: 13px;
stroke: currentColor;
fill: none;
stroke-width: 1.5px;
@@ -2416,12 +2432,10 @@ wa-dropdown.sidebar-session-sort-menu::part(menu) {
stroke-linejoin: round;
}
.sidebar-agent-card__switcher-unread {
position: absolute;
top: 2px;
right: 2px;
width: 6px;
height: 6px;
/* Home carries the main session's unread/running state at the row's edge. */
.nav-item__state {
flex: none;
margin-left: auto;
}
/* Zone 5: product chrome in one slim footer bar. */