mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
feat(ui): make the sidebar session-first and minimal (#100386)
* feat(ui): bolder sidebar minimalism — pinned/chats session groups, compact icon footer, no brand header * test(ui): align fixtures with single default pinned route; document pinned-cap invariant * fix(ui): preserve sidebar session groups * fix(ui): polish minimal sidebar behavior * fix(ui): avoid duplicate pinned chat fallback * docs(changelog): note sidebar layout refresh * chore(i18n): preserve sidebar locale metadata * fix(ui): reuse localized sessions label * test(ui): follow localized sessions label * chore(ui): trim sidebar landing churn * docs(ui): update minimal sidebar guidance
This commit is contained in:
committed by
GitHub
parent
b170c08e6d
commit
60cf7eaa00
@@ -113,9 +113,9 @@ Appearance also has a browser-local Text size setting, stored with the rest of C
|
||||
|
||||
## Sidebar navigation
|
||||
|
||||
The sidebar keeps sessions first, followed by a small pinned destination set. **Overview**, **Workboard**, and **Agents** are pinned by default; expand **More** to reach every other destination. Select **Customize sidebar** under More, or right-click the navigation area, to pin or unpin destinations and restore the defaults. The pinned set and More expansion state are stored in the current browser profile and survive reloads.
|
||||
The sidebar keeps sessions first, split into **Pinned** and **Sessions** groups. Every pinned session stays visible, while unpinned sessions keep an independent nine-item recent budget. **Overview** is the only destination pinned by default; expand **More** to reach every other destination. Select **Customize sidebar** under More, or right-click the navigation area, to pin or unpin destinations and restore the defaults. The pinned set and More expansion state are stored in the current browser profile and survive reloads.
|
||||
|
||||
**Settings** stays available in the sidebar footer next to **Docs**. On desktop, use the topbar button next to the terminal control to collapse or expand the sidebar. At drawer breakpoints, the hamburger button replaces that control.
|
||||
The compact footer keeps connection status, **Settings**, **Docs**, and mobile pairing together. On desktop, use the topbar button next to the terminal control to collapse or expand the sidebar. At drawer breakpoints, the hamburger button replaces that control.
|
||||
|
||||
## What it can do (today)
|
||||
|
||||
|
||||
@@ -29,10 +29,10 @@ export const SIDEBAR_NAV_ROUTES = [
|
||||
|
||||
export type SidebarNavRoute = (typeof SIDEBAR_NAV_ROUTES)[number];
|
||||
|
||||
// Sessions are the sidebar's core content; Overview is the only page pinned by
|
||||
// default. Users pin more via the customize menu.
|
||||
export const DEFAULT_SIDEBAR_PINNED_ROUTES = [
|
||||
"overview",
|
||||
"workboard",
|
||||
"agents",
|
||||
] as const satisfies readonly SidebarNavRoute[];
|
||||
|
||||
/**
|
||||
|
||||
@@ -306,7 +306,6 @@ class OpenClawShell extends LitElement {
|
||||
@state() private navCollapsed = false;
|
||||
@state() private sidebarPinnedRoutes: readonly SidebarNavRoute[] = [];
|
||||
@state() private sidebarMoreExpanded = false;
|
||||
@state() private recentSessionsCollapsed = false;
|
||||
@state() private navDrawerOpen = false;
|
||||
@state() private gatewayConnected = false;
|
||||
@state() private terminalAvailable = false;
|
||||
@@ -599,7 +598,6 @@ class OpenClawShell extends LitElement {
|
||||
this.navCollapsed = snapshot.navCollapsed;
|
||||
this.sidebarPinnedRoutes = snapshot.sidebarPinnedRoutes;
|
||||
this.sidebarMoreExpanded = snapshot.sidebarMoreExpanded;
|
||||
this.recentSessionsCollapsed = snapshot.recentSessionsCollapsed;
|
||||
};
|
||||
|
||||
override render() {
|
||||
@@ -673,7 +671,6 @@ class OpenClawShell extends LitElement {
|
||||
hasOperatorAdminAccess(context.gateway.snapshot.hello?.auth ?? null)}
|
||||
.sidebarPinnedRoutes=${this.sidebarPinnedRoutes}
|
||||
.sidebarMoreExpanded=${this.sidebarMoreExpanded}
|
||||
.recentSessionsCollapsed=${this.recentSessionsCollapsed}
|
||||
.themeMode=${context.theme.mode}
|
||||
.onToggleMore=${() =>
|
||||
context.navigation.update({
|
||||
@@ -681,10 +678,6 @@ class OpenClawShell extends LitElement {
|
||||
})}
|
||||
.onUpdatePinnedRoutes=${(routes: SidebarNavRoute[]) =>
|
||||
context.navigation.update({ sidebarPinnedRoutes: routes })}
|
||||
.onToggleRecentSessions=${() =>
|
||||
context.navigation.update({
|
||||
recentSessionsCollapsed: !context.navigation.snapshot.recentSessionsCollapsed,
|
||||
})}
|
||||
.onPairMobile=${() => void context.overlays.openDevicePairSetup()}
|
||||
.onNavigate=${(routeId: string, options?: ApplicationNavigationOptions) =>
|
||||
this.navigate(routeId, options)}
|
||||
|
||||
@@ -171,7 +171,6 @@ function createApplicationNavigationPreferences(
|
||||
navCollapsed: settings.navCollapsed,
|
||||
sidebarPinnedRoutes: settings.sidebarPinnedRoutes,
|
||||
sidebarMoreExpanded: settings.sidebarMoreExpanded,
|
||||
recentSessionsCollapsed: settings.recentSessionsCollapsed ?? false,
|
||||
};
|
||||
const listeners = new Set<(next: ApplicationNavigationPreferencesSnapshot) => void>();
|
||||
|
||||
@@ -183,7 +182,6 @@ function createApplicationNavigationPreferences(
|
||||
const nextSnapshot = { ...snapshot, ...patch };
|
||||
if (
|
||||
nextSnapshot.navCollapsed === snapshot.navCollapsed &&
|
||||
nextSnapshot.recentSessionsCollapsed === snapshot.recentSessionsCollapsed &&
|
||||
nextSnapshot.sidebarPinnedRoutes === snapshot.sidebarPinnedRoutes &&
|
||||
nextSnapshot.sidebarMoreExpanded === snapshot.sidebarMoreExpanded
|
||||
) {
|
||||
@@ -193,7 +191,6 @@ function createApplicationNavigationPreferences(
|
||||
navCollapsed: nextSnapshot.navCollapsed,
|
||||
sidebarPinnedRoutes: [...nextSnapshot.sidebarPinnedRoutes],
|
||||
sidebarMoreExpanded: nextSnapshot.sidebarMoreExpanded,
|
||||
recentSessionsCollapsed: nextSnapshot.recentSessionsCollapsed,
|
||||
});
|
||||
snapshot = nextSnapshot;
|
||||
for (const listener of listeners) {
|
||||
|
||||
@@ -34,7 +34,6 @@ export type ApplicationNavigationPreferencesSnapshot = {
|
||||
navCollapsed: boolean;
|
||||
sidebarPinnedRoutes: readonly SidebarNavRoute[];
|
||||
sidebarMoreExpanded: boolean;
|
||||
recentSessionsCollapsed: boolean;
|
||||
};
|
||||
|
||||
export type ApplicationNavigationPreferences = {
|
||||
|
||||
@@ -146,9 +146,8 @@ describe("loadSettings default gateway URL derivation", () => {
|
||||
splitRatio: 0.6,
|
||||
navCollapsed: false,
|
||||
navWidth: 220,
|
||||
sidebarPinnedRoutes: ["overview", "workboard", "agents"],
|
||||
sidebarPinnedRoutes: ["overview"],
|
||||
sidebarMoreExpanded: false,
|
||||
recentSessionsCollapsed: false,
|
||||
borderRadius: 50,
|
||||
textScale: 100,
|
||||
sessionsByGateway: {
|
||||
@@ -182,7 +181,7 @@ describe("loadSettings default gateway URL derivation", () => {
|
||||
splitRatio: 0.6,
|
||||
navCollapsed: false,
|
||||
navWidth: 220,
|
||||
sidebarPinnedRoutes: ["overview", "workboard", "agents"],
|
||||
sidebarPinnedRoutes: ["overview"],
|
||||
sidebarMoreExpanded: false,
|
||||
borderRadius: 50,
|
||||
textScale: 100,
|
||||
@@ -215,7 +214,7 @@ describe("loadSettings default gateway URL derivation", () => {
|
||||
splitRatio: 0.6,
|
||||
navCollapsed: false,
|
||||
navWidth: 220,
|
||||
sidebarPinnedRoutes: ["overview", "workboard", "agents"],
|
||||
sidebarPinnedRoutes: ["overview"],
|
||||
sidebarMoreExpanded: false,
|
||||
borderRadius: 50,
|
||||
});
|
||||
@@ -233,7 +232,7 @@ describe("loadSettings default gateway URL derivation", () => {
|
||||
splitRatio: 0.6,
|
||||
navCollapsed: false,
|
||||
navWidth: 220,
|
||||
sidebarPinnedRoutes: ["overview", "workboard", "agents"],
|
||||
sidebarPinnedRoutes: ["overview"],
|
||||
sidebarMoreExpanded: false,
|
||||
borderRadius: 50,
|
||||
});
|
||||
@@ -263,7 +262,7 @@ describe("loadSettings default gateway URL derivation", () => {
|
||||
splitRatio: 0.6,
|
||||
navCollapsed: false,
|
||||
navWidth: 220,
|
||||
sidebarPinnedRoutes: ["overview", "workboard", "agents"],
|
||||
sidebarPinnedRoutes: ["overview"],
|
||||
sidebarMoreExpanded: false,
|
||||
borderRadius: 50,
|
||||
});
|
||||
@@ -283,9 +282,8 @@ describe("loadSettings default gateway URL derivation", () => {
|
||||
splitRatio: 0.6,
|
||||
navCollapsed: false,
|
||||
navWidth: 220,
|
||||
sidebarPinnedRoutes: ["overview", "workboard", "agents"],
|
||||
sidebarPinnedRoutes: ["overview"],
|
||||
sidebarMoreExpanded: false,
|
||||
recentSessionsCollapsed: false,
|
||||
borderRadius: 50,
|
||||
textScale: 100,
|
||||
sessionsByGateway: {
|
||||
@@ -298,65 +296,6 @@ describe("loadSettings default gateway URL derivation", () => {
|
||||
expect(sessionStorage.length).toBe(1);
|
||||
});
|
||||
|
||||
it("persists recent sessions collapse state across save and load", () => {
|
||||
setTestLocation({
|
||||
protocol: "https:",
|
||||
host: "gateway.example:8443",
|
||||
pathname: "/",
|
||||
});
|
||||
|
||||
const gwUrl = expectedGatewayUrl("");
|
||||
saveSettings({
|
||||
gatewayUrl: gwUrl,
|
||||
token: "",
|
||||
sessionKey: "main",
|
||||
lastActiveSessionKey: "main",
|
||||
theme: "claw",
|
||||
themeMode: "system",
|
||||
chatShowThinking: true,
|
||||
chatShowToolCalls: true,
|
||||
chatAutoScroll: "near-bottom",
|
||||
splitRatio: 0.6,
|
||||
navCollapsed: false,
|
||||
navWidth: 220,
|
||||
sidebarPinnedRoutes: ["overview", "workboard", "agents"],
|
||||
sidebarMoreExpanded: false,
|
||||
recentSessionsCollapsed: true,
|
||||
borderRadius: 50,
|
||||
textScale: 100,
|
||||
});
|
||||
|
||||
expect(loadSettings().recentSessionsCollapsed).toBe(true);
|
||||
|
||||
saveSettings({
|
||||
gatewayUrl: gwUrl,
|
||||
token: "",
|
||||
sessionKey: "main",
|
||||
lastActiveSessionKey: "main",
|
||||
theme: "claw",
|
||||
themeMode: "system",
|
||||
chatShowThinking: true,
|
||||
chatShowToolCalls: true,
|
||||
chatAutoScroll: "near-bottom",
|
||||
splitRatio: 0.6,
|
||||
navCollapsed: false,
|
||||
navWidth: 220,
|
||||
sidebarPinnedRoutes: ["overview", "workboard", "agents"],
|
||||
sidebarMoreExpanded: false,
|
||||
recentSessionsCollapsed: false,
|
||||
borderRadius: 50,
|
||||
textScale: 100,
|
||||
});
|
||||
|
||||
const scopedKey = `openclaw.control.settings.v1:${gwUrl}`;
|
||||
const persisted = JSON.parse(localStorage.getItem(scopedKey) ?? "{}") as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
expect(persisted.recentSessionsCollapsed).toBe(false);
|
||||
expect(loadSettings().recentSessionsCollapsed).toBe(false);
|
||||
});
|
||||
|
||||
it("persists sidebar customization across save and load, normalizing bad values", () => {
|
||||
setTestLocation({
|
||||
protocol: "https:",
|
||||
@@ -380,7 +319,6 @@ describe("loadSettings default gateway URL derivation", () => {
|
||||
navWidth: 220,
|
||||
sidebarPinnedRoutes: ["sessions", "cron"],
|
||||
sidebarMoreExpanded: true,
|
||||
recentSessionsCollapsed: false,
|
||||
borderRadius: 50,
|
||||
textScale: 100,
|
||||
});
|
||||
@@ -398,7 +336,7 @@ describe("loadSettings default gateway URL derivation", () => {
|
||||
persisted.sidebarMoreExpanded = "yes";
|
||||
localStorage.setItem(scopedKey, JSON.stringify(persisted));
|
||||
|
||||
expect(loadSettings().sidebarPinnedRoutes).toEqual(["overview", "workboard", "agents"]);
|
||||
expect(loadSettings().sidebarPinnedRoutes).toEqual(["overview"]);
|
||||
expect(loadSettings().sidebarMoreExpanded).toBe(false);
|
||||
});
|
||||
|
||||
@@ -468,7 +406,7 @@ describe("loadSettings default gateway URL derivation", () => {
|
||||
splitRatio: 0.6,
|
||||
navCollapsed: false,
|
||||
navWidth: 220,
|
||||
sidebarPinnedRoutes: ["overview", "workboard", "agents"],
|
||||
sidebarPinnedRoutes: ["overview"],
|
||||
sidebarMoreExpanded: false,
|
||||
borderRadius: 50,
|
||||
});
|
||||
@@ -484,7 +422,7 @@ describe("loadSettings default gateway URL derivation", () => {
|
||||
splitRatio: 0.6,
|
||||
navCollapsed: false,
|
||||
navWidth: 220,
|
||||
sidebarPinnedRoutes: ["overview", "workboard", "agents"],
|
||||
sidebarPinnedRoutes: ["overview"],
|
||||
sidebarMoreExpanded: false,
|
||||
borderRadius: 50,
|
||||
});
|
||||
@@ -513,7 +451,7 @@ describe("loadSettings default gateway URL derivation", () => {
|
||||
splitRatio: 0.6,
|
||||
navCollapsed: false,
|
||||
navWidth: 320,
|
||||
sidebarPinnedRoutes: ["overview", "workboard", "agents"],
|
||||
sidebarPinnedRoutes: ["overview"],
|
||||
sidebarMoreExpanded: false,
|
||||
borderRadius: 50,
|
||||
});
|
||||
@@ -549,7 +487,7 @@ describe("loadSettings default gateway URL derivation", () => {
|
||||
splitRatio: 0.6,
|
||||
navCollapsed: false,
|
||||
navWidth: 220,
|
||||
sidebarPinnedRoutes: ["overview", "workboard", "agents"],
|
||||
sidebarPinnedRoutes: ["overview"],
|
||||
sidebarMoreExpanded: false,
|
||||
borderRadius: 50,
|
||||
customTheme,
|
||||
@@ -580,7 +518,7 @@ describe("loadSettings default gateway URL derivation", () => {
|
||||
splitRatio: 0.6,
|
||||
navCollapsed: false,
|
||||
navWidth: 220,
|
||||
sidebarPinnedRoutes: ["overview", "workboard", "agents"],
|
||||
sidebarPinnedRoutes: ["overview"],
|
||||
sidebarMoreExpanded: false,
|
||||
borderRadius: 50,
|
||||
customTheme: {
|
||||
@@ -625,7 +563,7 @@ describe("loadSettings default gateway URL derivation", () => {
|
||||
splitRatio: 0.6,
|
||||
navCollapsed: false,
|
||||
navWidth: 220,
|
||||
sidebarPinnedRoutes: ["overview", "workboard", "agents"],
|
||||
sidebarPinnedRoutes: ["overview"],
|
||||
sidebarMoreExpanded: false,
|
||||
borderRadius: 50,
|
||||
});
|
||||
@@ -669,7 +607,7 @@ describe("loadSettings default gateway URL derivation", () => {
|
||||
splitRatio: 0.6,
|
||||
navCollapsed: false,
|
||||
navWidth: 220,
|
||||
sidebarPinnedRoutes: ["overview", "workboard", "agents"],
|
||||
sidebarPinnedRoutes: ["overview"],
|
||||
sidebarMoreExpanded: false,
|
||||
borderRadius: 50,
|
||||
});
|
||||
|
||||
@@ -104,7 +104,6 @@ export type UiSettings = {
|
||||
navWidth: number; // Sidebar width when expanded (240–400px)
|
||||
sidebarPinnedRoutes: SidebarNavRoute[]; // Nav routes shown above the "More" section
|
||||
sidebarMoreExpanded: boolean; // Whether the sidebar "More" section is expanded
|
||||
recentSessionsCollapsed?: boolean; // Collapse recent sessions list in sidebar
|
||||
borderRadius: number; // Corner roundness (0–100, default 50)
|
||||
textScale?: TextScaleStop; // Browser-local text scale percentage
|
||||
customTheme?: ImportedCustomTheme;
|
||||
@@ -467,7 +466,6 @@ export function loadSettings(): UiSettings {
|
||||
navWidth: 220,
|
||||
sidebarPinnedRoutes: [...DEFAULT_SIDEBAR_PINNED_ROUTES],
|
||||
sidebarMoreExpanded: false,
|
||||
recentSessionsCollapsed: false,
|
||||
borderRadius: 50,
|
||||
textScale: 100,
|
||||
};
|
||||
@@ -530,10 +528,6 @@ export function loadSettings(): UiSettings {
|
||||
typeof parsed.sidebarMoreExpanded === "boolean"
|
||||
? parsed.sidebarMoreExpanded
|
||||
: defaults.sidebarMoreExpanded,
|
||||
recentSessionsCollapsed:
|
||||
typeof parsed.recentSessionsCollapsed === "boolean"
|
||||
? parsed.recentSessionsCollapsed
|
||||
: defaults.recentSessionsCollapsed,
|
||||
borderRadius:
|
||||
typeof parsed.borderRadius === "number" &&
|
||||
parsed.borderRadius >= 0 &&
|
||||
@@ -637,7 +631,6 @@ function persistSettings(next: UiSettings) {
|
||||
navWidth: next.navWidth,
|
||||
sidebarPinnedRoutes: next.sidebarPinnedRoutes,
|
||||
sidebarMoreExpanded: next.sidebarMoreExpanded,
|
||||
recentSessionsCollapsed: next.recentSessionsCollapsed ?? false,
|
||||
borderRadius: next.borderRadius,
|
||||
textScale: normalizeTextScale(next.textScale),
|
||||
...(next.customTheme ? { customTheme: next.customTheme } : {}),
|
||||
|
||||
+125
-147
@@ -22,7 +22,6 @@ import {
|
||||
type ApplicationContext,
|
||||
type ApplicationNavigationOptions,
|
||||
} from "../app/context.ts";
|
||||
import { controlUiPublicAssetPath } from "../app/public-assets.ts";
|
||||
import "./theme-mode-toggle.ts";
|
||||
import "./session-picker.ts";
|
||||
import "./tooltip.ts";
|
||||
@@ -32,11 +31,7 @@ import { buildExternalLinkRel, EXTERNAL_LINK_TARGET } from "../lib/external-link
|
||||
import { formatRelativeTimestamp } from "../lib/format.ts";
|
||||
import { startHoverMarquee, stopHoverMarquee } from "../lib/hover-marquee.ts";
|
||||
import { resolveSessionDisplayName } from "../lib/session-display.ts";
|
||||
import {
|
||||
compareSessionRowsByUpdatedAt,
|
||||
resolveSessionNavigation,
|
||||
searchForSession,
|
||||
} from "../lib/sessions/index.ts";
|
||||
import { resolveSessionNavigation, searchForSession } from "../lib/sessions/index.ts";
|
||||
import {
|
||||
buildAgentMainSessionKey,
|
||||
canArchiveSessionRow,
|
||||
@@ -60,7 +55,6 @@ type SidebarRecentSession = {
|
||||
hasActiveRun: boolean;
|
||||
kind?: string;
|
||||
pinned: boolean;
|
||||
pinnedAt?: number | null;
|
||||
};
|
||||
|
||||
function shouldHandleNavigationClick(event: MouseEvent): boolean {
|
||||
@@ -90,11 +84,9 @@ export class AppSidebar extends LitElement {
|
||||
@property({ attribute: false }) sidebarPinnedRoutes: readonly SidebarNavRoute[] =
|
||||
DEFAULT_SIDEBAR_PINNED_ROUTES;
|
||||
@property({ attribute: false }) sidebarMoreExpanded = false;
|
||||
@property({ attribute: false }) recentSessionsCollapsed = false;
|
||||
@property({ attribute: false }) themeMode: ThemeMode = "system";
|
||||
@property({ attribute: false }) onToggleMore?: () => void;
|
||||
@property({ attribute: false }) onUpdatePinnedRoutes?: (routes: SidebarNavRoute[]) => void;
|
||||
@property({ attribute: false }) onToggleRecentSessions?: () => void;
|
||||
@property({ attribute: false }) onPairMobile?: () => void;
|
||||
@property({ attribute: false })
|
||||
onNavigate?: (routeId: NavigationRouteId, options?: ApplicationNavigationOptions) => void;
|
||||
@@ -224,14 +216,12 @@ export class AppSidebar extends LitElement {
|
||||
hasActiveRun: Boolean(row.hasActiveRun),
|
||||
kind: row.kind,
|
||||
pinned: row.pinned === true,
|
||||
pinnedAt: row.pinnedAt,
|
||||
});
|
||||
const activeSession = navigation.selectedSession
|
||||
? toSidebarSession(navigation.selectedSession)
|
||||
: null;
|
||||
const recentSessions = navigation.recentSessions
|
||||
.slice(activeSession ? 1 : 0)
|
||||
.toSorted(compareSessionRowsByUpdatedAt)
|
||||
.map(toSidebarSession);
|
||||
const newSessionDisabled =
|
||||
!this.connected || this.sessionsLoading || Boolean(navigation.selectedSession?.hasActiveRun);
|
||||
@@ -673,6 +663,11 @@ export class AppSidebar extends LitElement {
|
||||
>`}
|
||||
</button>
|
||||
`;
|
||||
// Pinned rows stay separate from the recency-capped chat list; the active
|
||||
// session leads whichever group owns it.
|
||||
const allRows = [...(activeSession ? [activeSession] : []), ...recentSessions];
|
||||
const pinnedRows = allRows.filter((session) => session.pinned);
|
||||
const chatRows = allRows.filter((session) => !session.pinned);
|
||||
return html`
|
||||
<section class="sidebar-sessions ${this.collapsed ? "sidebar-sessions--collapsed" : ""}">
|
||||
${this.collapsed
|
||||
@@ -683,66 +678,64 @@ export class AppSidebar extends LitElement {
|
||||
${this.collapsed
|
||||
? nothing
|
||||
: html`
|
||||
<div
|
||||
class="sidebar-recent-sessions ${this.recentSessionsCollapsed
|
||||
? "sidebar-recent-sessions--collapsed"
|
||||
: ""}"
|
||||
aria-label=${t("overview.cards.recentSessions")}
|
||||
>
|
||||
<div class="sidebar-recent-sessions__head">
|
||||
<button
|
||||
class="sidebar-recent-sessions__label"
|
||||
type="button"
|
||||
aria-expanded=${String(!this.recentSessionsCollapsed)}
|
||||
@click=${() => this.onToggleRecentSessions?.()}
|
||||
>
|
||||
<span class="sidebar-recent-sessions__label-text"
|
||||
>${t("usage.sessions.recentShort")}</span
|
||||
>
|
||||
<span class="sidebar-recent-sessions__chevron"> ${icons.chevronDown} </span>
|
||||
</button>
|
||||
<openclaw-session-picker
|
||||
.sessions=${context?.sessions}
|
||||
.sessionsResult=${this.sessionsResult}
|
||||
.currentSessionKey=${routeSessionKey}
|
||||
.agentId=${selectedAgentId}
|
||||
.defaultAgentId=${defaultAgentId}
|
||||
.mainKey=${resolveUiConfiguredMainKey({
|
||||
agentsList: context?.agents.state.agentsList,
|
||||
hello: context?.gateway.snapshot.hello,
|
||||
})}
|
||||
.connected=${this.connected}
|
||||
.onSelectSession=${this.selectSession}
|
||||
.onReplaceCurrentSession=${this.replaceCurrentSession}
|
||||
></openclaw-session-picker>
|
||||
</div>
|
||||
${this.renderAgentFilter(routeSessionKey, selectedAgentId)}
|
||||
${activeSession
|
||||
? this.renderRecentSession(activeSession)
|
||||
: this.renderChatFallback()}
|
||||
${recentSessions.length === 0
|
||||
<div class="sidebar-recent-sessions" aria-label=${titleForRoute("sessions")}>
|
||||
${pinnedRows.length === 0
|
||||
? nothing
|
||||
: html`
|
||||
<div class="sidebar-recent-sessions__list">
|
||||
${recentSessions.map((session) => this.renderRecentSession(session))}
|
||||
<div class="sidebar-recent-sessions__group">
|
||||
<div class="sidebar-recent-sessions__head">
|
||||
<span class="sidebar-recent-sessions__label-text"
|
||||
>${t("sessionsView.pinned")}</span
|
||||
>
|
||||
</div>
|
||||
<div class="sidebar-recent-sessions__list">
|
||||
${pinnedRows.map((session) => this.renderRecentSession(session))}
|
||||
</div>
|
||||
</div>
|
||||
`}
|
||||
<a
|
||||
href=${pathForRoute("sessions", this.basePath)}
|
||||
class="sidebar-recent-sessions__all"
|
||||
@click=${(event: MouseEvent) => {
|
||||
if (!shouldHandleNavigationClick(event)) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
this.onNavigate?.("sessions");
|
||||
}}
|
||||
>
|
||||
<span>${t("chat.sidebar.allSessions")}</span>
|
||||
<span class="sidebar-recent-sessions__all-icon" aria-hidden="true"
|
||||
>${icons.chevronRight}</span
|
||||
<div class="sidebar-recent-sessions__group">
|
||||
<div class="sidebar-recent-sessions__head">
|
||||
<span class="sidebar-recent-sessions__label-text"
|
||||
>${t("sessionsView.title")}</span
|
||||
>
|
||||
<openclaw-session-picker
|
||||
.sessions=${context?.sessions}
|
||||
.sessionsResult=${this.sessionsResult}
|
||||
.currentSessionKey=${routeSessionKey}
|
||||
.agentId=${selectedAgentId}
|
||||
.defaultAgentId=${defaultAgentId}
|
||||
.mainKey=${resolveUiConfiguredMainKey({
|
||||
agentsList: context?.agents.state.agentsList,
|
||||
hello: context?.gateway.snapshot.hello,
|
||||
})}
|
||||
.connected=${this.connected}
|
||||
.onSelectSession=${this.selectSession}
|
||||
.onReplaceCurrentSession=${this.replaceCurrentSession}
|
||||
></openclaw-session-picker>
|
||||
</div>
|
||||
${this.renderAgentFilter(routeSessionKey, selectedAgentId)}
|
||||
<div class="sidebar-recent-sessions__list">
|
||||
${allRows.length === 0
|
||||
? this.renderChatFallback()
|
||||
: chatRows.map((session) => this.renderRecentSession(session))}
|
||||
</div>
|
||||
<a
|
||||
href=${pathForRoute("sessions", this.basePath)}
|
||||
class="sidebar-recent-sessions__all"
|
||||
@click=${(event: MouseEvent) => {
|
||||
if (!shouldHandleNavigationClick(event)) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
this.onNavigate?.("sessions");
|
||||
}}
|
||||
>
|
||||
</a>
|
||||
<span>${t("chat.sidebar.allSessions")}</span>
|
||||
<span class="sidebar-recent-sessions__all-icon" aria-hidden="true"
|
||||
>${icons.chevronRight}</span
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`}
|
||||
</section>
|
||||
@@ -846,25 +839,11 @@ export class AppSidebar extends LitElement {
|
||||
const gatewayStatus = t("chat.gatewayStatus", {
|
||||
status: this.connected ? t("common.online") : t("common.offline"),
|
||||
});
|
||||
const settingsActive =
|
||||
this.activeRouteId !== undefined && isSettingsNavigationRoute(this.activeRouteId);
|
||||
return html`
|
||||
<aside class="sidebar ${this.collapsed ? "sidebar--collapsed" : ""}">
|
||||
<div class="sidebar-shell">
|
||||
<div class="sidebar-shell__header">
|
||||
<div class="sidebar-brand">
|
||||
<img
|
||||
class="sidebar-brand__logo"
|
||||
src="${controlUiPublicAssetPath("favicon.svg", this.basePath)}"
|
||||
alt="OpenClaw"
|
||||
/>
|
||||
${this.collapsed
|
||||
? nothing
|
||||
: html`
|
||||
<span class="sidebar-brand__copy">
|
||||
<span class="sidebar-brand__title">OpenClaw</span>
|
||||
</span>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
<div class="sidebar-shell__body">
|
||||
${this.renderSessions()}
|
||||
<nav class="sidebar-nav" @contextmenu=${this.openCustomizeMenuFromContext}>
|
||||
@@ -876,72 +855,71 @@ export class AppSidebar extends LitElement {
|
||||
</nav>
|
||||
</div>
|
||||
<div class="sidebar-shell__footer">
|
||||
<div class="sidebar-utility-group">
|
||||
${this.renderRoute("config")}
|
||||
${this.collapsed
|
||||
? html`
|
||||
<openclaw-tooltip
|
||||
.content=${t("chat.docsOpensInNewTab", { label: t("common.docs") })}
|
||||
>
|
||||
<a
|
||||
class="nav-item nav-item--external sidebar-utility-link"
|
||||
href="https://docs.openclaw.ai"
|
||||
target=${EXTERNAL_LINK_TARGET}
|
||||
rel=${buildExternalLinkRel()}
|
||||
>
|
||||
<span class="nav-item__icon" aria-hidden="true">${icons.book}</span>
|
||||
</a>
|
||||
</openclaw-tooltip>
|
||||
`
|
||||
: html`
|
||||
<a
|
||||
class="nav-item nav-item--external sidebar-utility-link"
|
||||
href="https://docs.openclaw.ai"
|
||||
target=${EXTERNAL_LINK_TARGET}
|
||||
rel=${buildExternalLinkRel()}
|
||||
>
|
||||
<span class="nav-item__icon" aria-hidden="true">${icons.book}</span>
|
||||
<span class="nav-item__text">${t("common.docs")}</span>
|
||||
<span class="nav-item__external-icon">${icons.externalLink}</span>
|
||||
</a>
|
||||
`}
|
||||
<div class="sidebar-mode-switch">
|
||||
<openclaw-theme-mode-toggle .mode=${this.themeMode}></openclaw-theme-mode-toggle>
|
||||
</div>
|
||||
<div class="sidebar-footer-row">
|
||||
<div class="sidebar-status">
|
||||
<openclaw-tooltip .content=${gatewayStatus}>
|
||||
<span
|
||||
class="sidebar-status__dot ${this.connected
|
||||
? "sidebar-connection-status--online"
|
||||
: "sidebar-connection-status--offline"}"
|
||||
role="img"
|
||||
aria-live="polite"
|
||||
aria-label=${gatewayStatus}
|
||||
></span>
|
||||
</openclaw-tooltip>
|
||||
${this.collapsed
|
||||
? nothing
|
||||
: html`<span class="sidebar-status__text"
|
||||
>${this.connected ? t("common.online") : t("common.offline")}</span
|
||||
>`}
|
||||
</div>
|
||||
<openclaw-tooltip
|
||||
.content=${this.canPairDevice
|
||||
? t("nodes.pairing.button")
|
||||
: t("nodes.pairing.adminRequired")}
|
||||
<div class="sidebar-footer-bar">
|
||||
<openclaw-tooltip .content=${gatewayStatus}>
|
||||
<span
|
||||
class="sidebar-status__dot ${this.connected
|
||||
? "sidebar-connection-status--online"
|
||||
: "sidebar-connection-status--offline"}"
|
||||
role="img"
|
||||
aria-live="polite"
|
||||
aria-label=${gatewayStatus}
|
||||
></span>
|
||||
</openclaw-tooltip>
|
||||
<span class="sidebar-footer-bar__spacer"></span>
|
||||
<openclaw-tooltip .content=${titleForRoute("config")}>
|
||||
<a
|
||||
href=${pathForRoute("config", this.basePath)}
|
||||
class="sidebar-footer-icon ${settingsActive ? "sidebar-footer-icon--active" : ""}"
|
||||
aria-label=${titleForRoute("config")}
|
||||
aria-current=${settingsActive ? "page" : nothing}
|
||||
@focus=${(event: Event) => this.preloadRoute("config", event)}
|
||||
@blur=${this.cancelPreload}
|
||||
@pointerenter=${(event: Event) => this.preloadRoute("config", event)}
|
||||
@pointerleave=${this.cancelPreload}
|
||||
@touchstart=${(event: TouchEvent) => this.preloadRoute("config", event, true)}
|
||||
@click=${(event: MouseEvent) => {
|
||||
if (!shouldHandleNavigationClick(event)) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
this.onNavigate?.("config");
|
||||
}}
|
||||
>
|
||||
<button
|
||||
class="sidebar-pair-mobile"
|
||||
type="button"
|
||||
aria-label=${t("nodes.pairing.button")}
|
||||
?disabled=${!this.canPairDevice}
|
||||
@click=${() => this.onPairMobile?.()}
|
||||
>
|
||||
<span aria-hidden="true">${icons.smartphone}</span>
|
||||
</button>
|
||||
</openclaw-tooltip>
|
||||
</div>
|
||||
${icons.settings}
|
||||
</a>
|
||||
</openclaw-tooltip>
|
||||
<openclaw-tooltip
|
||||
.content=${t("chat.docsOpensInNewTab", { label: t("common.docs") })}
|
||||
>
|
||||
<a
|
||||
class="sidebar-footer-icon"
|
||||
href="https://docs.openclaw.ai"
|
||||
target=${EXTERNAL_LINK_TARGET}
|
||||
rel=${buildExternalLinkRel()}
|
||||
aria-label=${t("common.docs")}
|
||||
>
|
||||
${icons.book}
|
||||
</a>
|
||||
</openclaw-tooltip>
|
||||
<openclaw-tooltip
|
||||
.content=${this.canPairDevice
|
||||
? t("nodes.pairing.button")
|
||||
: t("nodes.pairing.adminRequired")}
|
||||
>
|
||||
<button
|
||||
class="sidebar-footer-icon sidebar-pair-mobile"
|
||||
type="button"
|
||||
aria-label=${t("nodes.pairing.button")}
|
||||
?disabled=${!this.canPairDevice}
|
||||
@click=${() => this.onPairMobile?.()}
|
||||
>
|
||||
${icons.smartphone}
|
||||
</button>
|
||||
</openclaw-tooltip>
|
||||
<span class="sidebar-mode-switch">
|
||||
<openclaw-theme-mode-toggle .mode=${this.themeMode}></openclaw-theme-mode-toggle>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -92,6 +92,10 @@ function actionOpacity(button: Locator): Promise<string> {
|
||||
return button.evaluate((element) => globalThis.getComputedStyle(element).opacity);
|
||||
}
|
||||
|
||||
async function trimmedTextContents(locator: Locator): Promise<string[]> {
|
||||
return (await locator.allTextContents()).map((text) => text.trim());
|
||||
}
|
||||
|
||||
async function captureUiProof(page: Page, fileName: string) {
|
||||
if (process.env.OPENCLAW_CAPTURE_UI_PROOF !== "1") {
|
||||
return;
|
||||
@@ -151,6 +155,15 @@ describeControlUiE2e("Control UI session management mocked Gateway E2E", () => {
|
||||
const sidebarRows = page.locator(".sidebar-recent-sessions__list .sidebar-recent-session");
|
||||
await sidebarRows.first().waitFor({ state: "visible", timeout: 10_000 });
|
||||
await expect.poll(() => sidebarRows.first().textContent()).toContain("Release planning");
|
||||
const sessionGroups = page.locator(".sidebar-recent-sessions__group");
|
||||
const pinnedGroup = sessionGroups.filter({ hasText: "Pinned" });
|
||||
const chatsGroup = sessionGroups.filter({ hasText: "Sessions" });
|
||||
await expect
|
||||
.poll(() => trimmedTextContents(pinnedGroup.locator(".sidebar-recent-session__name")))
|
||||
.toEqual(["Release planning"]);
|
||||
await expect
|
||||
.poll(() => trimmedTextContents(chatsGroup.locator(".sidebar-recent-session__name")))
|
||||
.toEqual(["Main", "Data migration", "Research notes"]);
|
||||
const sidebarMigration = sidebarRows.filter({ hasText: "Data migration" });
|
||||
await expect
|
||||
.poll(() => sidebarMigration.locator(".session-run-spinner").isVisible())
|
||||
@@ -237,4 +250,38 @@ describeControlUiE2e("Control UI session management mocked Gateway E2E", () => {
|
||||
await context.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("does not duplicate the active chat when its only session is pinned", async () => {
|
||||
const context = await browser.newContext({
|
||||
locale: "en-US",
|
||||
serviceWorkers: "block",
|
||||
viewport: { height: 900, width: 1280 },
|
||||
});
|
||||
const page = await context.newPage();
|
||||
await installMockGateway(page, {
|
||||
methodResponses: {
|
||||
"sessions.list": sessionsListResponse([
|
||||
sessionRow("agent:main:pinned", "Pinned only", Date.parse("2026-07-01T16:00:00.000Z"), {
|
||||
pinned: true,
|
||||
}),
|
||||
]),
|
||||
},
|
||||
sessionKey: "agent:main:pinned",
|
||||
});
|
||||
|
||||
try {
|
||||
await page.goto(`${server.baseUrl}chat`);
|
||||
|
||||
const sessionGroups = page.locator(".sidebar-recent-sessions__group");
|
||||
const pinnedGroup = sessionGroups.filter({ hasText: "Pinned" });
|
||||
const chatsGroup = sessionGroups.filter({ hasText: "Sessions" });
|
||||
await expect
|
||||
.poll(() => trimmedTextContents(pinnedGroup.locator(".sidebar-recent-session__name")))
|
||||
.toEqual(["Pinned only"]);
|
||||
await expect.poll(() => chatsGroup.locator(".sidebar-recent-session").count()).toBe(0);
|
||||
await expect.poll(() => page.locator(".sidebar-recent-session--active").count()).toBe(1);
|
||||
} finally {
|
||||
await context.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -69,9 +69,15 @@ describeControlUiE2e("Control UI sidebar customization mocked Gateway E2E", () =
|
||||
|
||||
const sidebar = page.locator("openclaw-app-sidebar");
|
||||
const pinnedItems = sidebar.locator(".sidebar-nav > .nav-section__items > .nav-item");
|
||||
await expect
|
||||
.poll(() => trimmedTextContents(pinnedItems))
|
||||
.toEqual(["Overview", "Workboard", "Agents"]);
|
||||
await expect.poll(() => trimmedTextContents(pinnedItems)).toEqual(["Overview"]);
|
||||
await expect.poll(() => sidebar.locator(".sidebar-brand").count()).toBe(0);
|
||||
const settingsLink = sidebar.getByRole("link", { name: "Settings" });
|
||||
await expect.poll(() => settingsLink.isVisible()).toBe(true);
|
||||
await settingsLink.click();
|
||||
await expect.poll(() => new URL(page.url()).pathname).toBe("/config");
|
||||
await expect.poll(() => settingsLink.getAttribute("aria-current")).toBe("page");
|
||||
await sidebar.getByRole("link", { name: "Overview" }).click();
|
||||
await expect.poll(() => new URL(page.url()).pathname).toBe("/overview");
|
||||
await captureUiProof(page, "01-default-pinned.png");
|
||||
|
||||
const moreButton = sidebar.getByRole("button", { name: "More" });
|
||||
@@ -98,9 +104,9 @@ describeControlUiE2e("Control UI sidebar customization mocked Gateway E2E", () =
|
||||
await captureUiProof(page, "02-customize-menu.png");
|
||||
|
||||
await overviewItem.click();
|
||||
await expect.poll(() => trimmedTextContents(pinnedItems)).toEqual(["Workboard", "Agents"]);
|
||||
await expect.poll(() => trimmedTextContents(pinnedItems)).toEqual([]);
|
||||
await page.reload();
|
||||
await expect.poll(() => trimmedTextContents(pinnedItems)).toEqual(["Workboard", "Agents"]);
|
||||
await expect.poll(() => trimmedTextContents(pinnedItems)).toEqual([]);
|
||||
await expect.poll(() => moreButton.getAttribute("aria-expanded")).toBe("true");
|
||||
await expect
|
||||
.poll(() =>
|
||||
@@ -113,9 +119,7 @@ describeControlUiE2e("Control UI sidebar customization mocked Gateway E2E", () =
|
||||
|
||||
await customizeButton.click();
|
||||
await menu.getByRole("menuitem", { name: "Reset to defaults" }).click();
|
||||
await expect
|
||||
.poll(() => trimmedTextContents(pinnedItems))
|
||||
.toEqual(["Overview", "Workboard", "Agents"]);
|
||||
await expect.poll(() => trimmedTextContents(pinnedItems)).toEqual(["Overview"]);
|
||||
|
||||
const collapseButton = page.getByRole("button", { name: "Collapse sidebar" });
|
||||
await collapseButton.click();
|
||||
@@ -143,6 +147,11 @@ describeControlUiE2e("Control UI sidebar customization mocked Gateway E2E", () =
|
||||
)
|
||||
.toBe(false);
|
||||
await expect.poll(() => moreButton.isVisible()).toBe(true);
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.locator(".shell-nav").evaluate((element) => element.getBoundingClientRect().left),
|
||||
)
|
||||
.toBe(0);
|
||||
await captureUiProof(page, "05-expanded-tablet-drawer.png");
|
||||
} finally {
|
||||
await context.close();
|
||||
|
||||
Generated
-7
@@ -8,13 +8,6 @@
|
||||
"path": "ui/src/app/app-host.ts",
|
||||
"text": "Close navigation"
|
||||
},
|
||||
{
|
||||
"count": 1,
|
||||
"kind": "html-text",
|
||||
"name": "text",
|
||||
"path": "ui/src/components/app-sidebar.ts",
|
||||
"text": "OpenClaw"
|
||||
},
|
||||
{
|
||||
"count": 1,
|
||||
"kind": "html-text",
|
||||
|
||||
@@ -36,4 +36,50 @@ describe("resolveSessionNavigation", () => {
|
||||
Array.from({ length: 9 }, (_, index) => `agent:main:recent-${index}`),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps every pinned session when pins exceed the recent-session cap", () => {
|
||||
const pinnedSessions = Array.from({ length: 10 }, (_, index) => ({
|
||||
key: `agent:main:pinned-${index}`,
|
||||
kind: "direct" as const,
|
||||
pinned: true,
|
||||
updatedAt: 100 - index,
|
||||
}));
|
||||
const navigation = resolveSessionNavigation({
|
||||
result: sessionsResult([
|
||||
{ key: "agent:main:recent", kind: "direct", updatedAt: 1_000 },
|
||||
...pinnedSessions,
|
||||
]),
|
||||
resultAgentId: "main",
|
||||
sessionKey: "unknown",
|
||||
});
|
||||
|
||||
expect(navigation.recentSessions.map((row) => row.key)).toEqual([
|
||||
...pinnedSessions.map((row) => row.key),
|
||||
"agent:main:recent",
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps nine recent chats in addition to pinned sessions", () => {
|
||||
const pinnedSessions = Array.from({ length: 3 }, (_, index) => ({
|
||||
key: `agent:main:pinned-${index}`,
|
||||
kind: "direct" as const,
|
||||
pinned: true,
|
||||
updatedAt: 100 - index,
|
||||
}));
|
||||
const recentSessions = Array.from({ length: 10 }, (_, index) => ({
|
||||
key: `agent:main:recent-${index}`,
|
||||
kind: "direct" as const,
|
||||
updatedAt: 1_000 - index,
|
||||
}));
|
||||
const navigation = resolveSessionNavigation({
|
||||
result: sessionsResult([...recentSessions, ...pinnedSessions]),
|
||||
resultAgentId: "main",
|
||||
sessionKey: "unknown",
|
||||
});
|
||||
|
||||
expect(navigation.recentSessions.map((row) => row.key)).toEqual([
|
||||
...pinnedSessions.map((row) => row.key),
|
||||
...recentSessions.slice(0, 9).map((row) => row.key),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -214,6 +214,10 @@ export function getVisibleSessionRows(
|
||||
}
|
||||
|
||||
export function compareSessionRowsByUpdatedAt(a: GatewaySessionRow, b: GatewaySessionRow): number {
|
||||
const pinnedStateDiff = Number(b.pinned === true) - Number(a.pinned === true);
|
||||
if (pinnedStateDiff !== 0) {
|
||||
return pinnedStateDiff;
|
||||
}
|
||||
const pinnedDiff = (b.pinnedAt ?? 0) - (a.pinnedAt ?? 0);
|
||||
return pinnedDiff !== 0 ? pinnedDiff : (b.updatedAt ?? 0) - (a.updatedAt ?? 0);
|
||||
}
|
||||
@@ -237,15 +241,21 @@ export function resolveSessionNavigation(input: SessionNavigationInput): Session
|
||||
currentSessionKey && currentSessionKey.toLowerCase() !== "unknown"
|
||||
? { ...(selectedSession ?? { kind: "direct", updatedAt: null }), key: currentSessionKey }
|
||||
: undefined;
|
||||
const recentSessions = getVisibleSessionRows(input.result, {
|
||||
const sortedSessions = getVisibleSessionRows(input.result, {
|
||||
currentSessionKey: currentSessionKey || undefined,
|
||||
agentId: selectedAgentId,
|
||||
defaultAgentId,
|
||||
filterByAgent: shouldFilterByAgent,
|
||||
})
|
||||
.filter((row) => !matchesCurrentSession(row))
|
||||
.toSorted(compareSessionRowsByUpdatedAt)
|
||||
.slice(0, 9);
|
||||
.toSorted(compareSessionRowsByUpdatedAt);
|
||||
// Pinned chats are explicit user choices, so the recent-chat cap only
|
||||
// trims unpinned rows. Otherwise a tenth pin silently vanishes.
|
||||
const pinnedSessions = sortedSessions.filter((row) => row.pinned === true);
|
||||
const recentSessions = [
|
||||
...pinnedSessions,
|
||||
...sortedSessions.filter((row) => row.pinned !== true).slice(0, 9),
|
||||
];
|
||||
return {
|
||||
currentSessionKey,
|
||||
selectedAgentId,
|
||||
|
||||
@@ -422,7 +422,7 @@ function createChatHeaderState(
|
||||
splitRatio: 0.6,
|
||||
navCollapsed: false,
|
||||
navWidth: 280,
|
||||
sidebarPinnedRoutes: ["overview", "workboard", "agents"],
|
||||
sidebarPinnedRoutes: ["overview"],
|
||||
sidebarMoreExpanded: false,
|
||||
borderRadius: 50,
|
||||
chatShowThinking: false,
|
||||
|
||||
@@ -248,7 +248,6 @@ export class OverviewPage extends LitElement {
|
||||
navCollapsed: navigation.navCollapsed,
|
||||
sidebarPinnedRoutes: [...navigation.sidebarPinnedRoutes],
|
||||
sidebarMoreExpanded: navigation.sidebarMoreExpanded,
|
||||
recentSessionsCollapsed: navigation.recentSessionsCollapsed,
|
||||
locale,
|
||||
};
|
||||
this.settings = nextDraft;
|
||||
|
||||
@@ -22,7 +22,7 @@ function createOverviewProps(overrides: Partial<OverviewProps> = {}): OverviewPr
|
||||
splitRatio: 0.6,
|
||||
navCollapsed: false,
|
||||
navWidth: 220,
|
||||
sidebarPinnedRoutes: ["overview", "workboard", "agents"],
|
||||
sidebarPinnedRoutes: ["overview"],
|
||||
sidebarMoreExpanded: false,
|
||||
borderRadius: 50,
|
||||
locale: "en",
|
||||
|
||||
+77
-193
@@ -457,20 +457,10 @@
|
||||
flex: 0 0 var(--shell-nav-rail-width);
|
||||
}
|
||||
|
||||
.sidebar-shell__header,
|
||||
.sidebar-shell__footer {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sidebar-shell__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
min-height: 0;
|
||||
padding: 0 8px 18px;
|
||||
}
|
||||
|
||||
.sidebar-shell__body {
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
@@ -483,39 +473,6 @@
|
||||
border-top: 1px solid color-mix(in srgb, var(--border) 80%, transparent);
|
||||
}
|
||||
|
||||
.sidebar-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sidebar-brand__logo {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
flex-shrink: 0;
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: 0 8px 18px color-mix(in srgb, black 12%, transparent);
|
||||
}
|
||||
|
||||
.sidebar-brand__copy {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sidebar-brand__title {
|
||||
font-size: 15px;
|
||||
line-height: 1.1;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.03em;
|
||||
color: var(--text-strong);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
@@ -588,19 +545,26 @@
|
||||
|
||||
.sidebar-recent-sessions {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
gap: 10px;
|
||||
margin: 0 -8px;
|
||||
}
|
||||
|
||||
.sidebar-recent-sessions__group {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
/* Anchors the session-picker popover to the full header width. */
|
||||
.sidebar-recent-sessions__head {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
min-height: 24px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.sidebar-recent-sessions__head .sidebar-recent-sessions__label {
|
||||
.sidebar-recent-sessions__head .sidebar-recent-sessions__label-text {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
@@ -671,64 +635,15 @@
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
.sidebar-recent-sessions__label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
padding: 0 10px;
|
||||
min-height: 28px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
color: color-mix(in srgb, var(--muted) 72%, var(--text) 28%);
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition:
|
||||
color var(--duration-fast) ease,
|
||||
background var(--duration-fast) ease;
|
||||
}
|
||||
|
||||
.sidebar-recent-sessions__label:hover {
|
||||
color: var(--text);
|
||||
background: color-mix(in srgb, var(--bg-hover) 72%, transparent);
|
||||
}
|
||||
|
||||
.sidebar-recent-sessions__label-text {
|
||||
font-size: 11px;
|
||||
font-weight: 650;
|
||||
letter-spacing: 0.07em;
|
||||
text-transform: uppercase;
|
||||
color: color-mix(in srgb, var(--muted) 72%, var(--text) 28%);
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.sidebar-recent-sessions__chevron {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0.5;
|
||||
transition: transform var(--duration-fast) ease;
|
||||
}
|
||||
|
||||
.sidebar-recent-sessions__chevron svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
stroke: currentColor;
|
||||
fill: none;
|
||||
stroke-width: 1.5px;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
.sidebar-recent-sessions--collapsed .sidebar-recent-sessions__chevron {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.sidebar-recent-sessions--collapsed .sidebar-recent-sessions__list {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar-recent-sessions__list {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
@@ -1126,13 +1041,6 @@
|
||||
padding: 12px 8px 10px;
|
||||
}
|
||||
|
||||
.sidebar--collapsed .sidebar-shell__header {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
padding: 0 2px 16px;
|
||||
}
|
||||
|
||||
.sidebar--collapsed .sidebar-nav {
|
||||
padding: 0;
|
||||
}
|
||||
@@ -1208,44 +1116,67 @@
|
||||
0 10px 20px color-mix(in srgb, black 18%, transparent);
|
||||
}
|
||||
|
||||
.sidebar--collapsed .sidebar-brand__logo {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
/* Single compact icon row: status dot, then utility icons right-aligned.
|
||||
The footer signals connectivity only; gateway version lives in Settings. */
|
||||
.sidebar-footer-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
min-height: 34px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.sidebar-footer-bar__spacer {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.sidebar-footer-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
flex: 0 0 auto;
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow:
|
||||
0 10px 20px color-mix(in srgb, black 20%, transparent),
|
||||
inset 0 1px 0 color-mix(in srgb, white 10%, transparent);
|
||||
}
|
||||
|
||||
.sidebar-utility-group {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.sidebar-utility-link {
|
||||
min-height: 42px;
|
||||
}
|
||||
|
||||
.sidebar-footer-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* Borderless status line: the sidebar footer signals connectivity only;
|
||||
the gateway version moved into Settings (Quick Settings footer). */
|
||||
.sidebar-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 28px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.sidebar-status__text {
|
||||
font-size: 12px;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition:
|
||||
color var(--duration-fast) ease,
|
||||
background var(--duration-fast) ease;
|
||||
}
|
||||
|
||||
.sidebar-footer-icon:hover:not(:disabled),
|
||||
.sidebar-footer-icon:focus-visible {
|
||||
color: var(--text);
|
||||
background: color-mix(in srgb, var(--bg-hover) 84%, transparent);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.sidebar-footer-icon:focus-visible {
|
||||
outline: 2px solid color-mix(in srgb, var(--accent) 48%, transparent);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.sidebar-footer-icon:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.42;
|
||||
}
|
||||
|
||||
.sidebar-footer-icon--active {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.sidebar-footer-icon svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
stroke: currentColor;
|
||||
fill: none;
|
||||
stroke-width: 1.5px;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
.sidebar-status__dot {
|
||||
@@ -1265,48 +1196,6 @@
|
||||
box-shadow: 0 0 0 4px color-mix(in srgb, var(--danger) 14%, transparent);
|
||||
}
|
||||
|
||||
.sidebar-pair-mobile {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
flex: 0 0 auto;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-md);
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
color var(--duration-fast) ease,
|
||||
border-color var(--duration-fast) ease,
|
||||
background var(--duration-fast) ease;
|
||||
}
|
||||
|
||||
.sidebar-pair-mobile:hover:not(:disabled),
|
||||
.sidebar-pair-mobile:focus-visible {
|
||||
color: var(--text);
|
||||
border-color: color-mix(in srgb, var(--border) 76%, transparent);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.sidebar-pair-mobile:focus-visible {
|
||||
outline: 2px solid color-mix(in srgb, var(--accent) 48%, transparent);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.sidebar-pair-mobile:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.42;
|
||||
}
|
||||
|
||||
.sidebar-pair-mobile svg {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
stroke: currentColor;
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.sidebar-pair-mobile svg * {
|
||||
stroke: currentColor;
|
||||
fill: none;
|
||||
@@ -1316,20 +1205,15 @@
|
||||
padding: 8px 0 2px;
|
||||
}
|
||||
|
||||
.sidebar--collapsed .sidebar-utility-group {
|
||||
justify-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.sidebar--collapsed .sidebar-status {
|
||||
width: 44px;
|
||||
min-height: 36px;
|
||||
padding: 0;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.sidebar--collapsed .sidebar-footer-row {
|
||||
/* Rail mode stacks the footer icons vertically. */
|
||||
.sidebar--collapsed .sidebar-footer-bar {
|
||||
flex-direction: column-reverse;
|
||||
gap: 6px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.sidebar--collapsed .sidebar-footer-bar__spacer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Mode switch in sidebar — hidden on desktop, shown on mobile */
|
||||
|
||||
@@ -173,11 +173,6 @@
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.sidebar-shell__header {
|
||||
min-height: 0;
|
||||
padding: 0 4px 16px;
|
||||
}
|
||||
|
||||
.sidebar-nav,
|
||||
.sidebar--collapsed .sidebar-nav {
|
||||
flex: 1 1 auto;
|
||||
@@ -230,10 +225,9 @@
|
||||
padding: 12px 8px 0;
|
||||
}
|
||||
|
||||
.sidebar--collapsed .sidebar-status {
|
||||
width: auto;
|
||||
min-height: 32px;
|
||||
padding: 0 12px;
|
||||
.sidebar--collapsed .sidebar-footer-bar {
|
||||
flex-direction: row;
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user