mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
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:
committed by
GitHub
parent
010f662f48
commit
73cf9ee908
@@ -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) &&
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -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
@@ -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. */
|
||||
|
||||
Reference in New Issue
Block a user