diff --git a/docs/web/control-ui.md b/docs/web/control-ui.md index 2605337e9196..b7e5fb3c9f3d 100644 --- a/docs/web/control-ui.md +++ b/docs/web/control-ui.md @@ -217,7 +217,7 @@ and [Linux](/platforms/linux) desktop apps, the ## Sidebar navigation -The sidebar organizes everything around the agent. The identity row at the top is the active agent; below it, the **Pages** section starts with **Home** — the agent's rolling main session, badged with its unread or running state — followed by the pinned destinations (**Usage**, **Automations**, and **Plugins** by default). The customize control on the Pages header opens a menu with every other destination, including plugin-provided tabs, plus **Edit pinned items**; right-clicking the navigation area opens the pin editor directly. The session list below splits into zones: **Threads** for the agent's chat sessions (the main session stays behind Home; sessions it spawned appear here as top-level threads, and named threads show without a type prefix), **Groups** for group and room conversations, and **Coding** for sessions bound to a managed worktree or exec node (rows show a `repo ⎇ branch` line plus the node host), ACP-backed harness sessions, and the Codex/Claude CLI catalogs. Coding starts collapsed on first run and remembers your choice; its collapsed header keeps the true count and shows a running indicator while contained sessions work. Custom groups (the session `category`) and **Pinned** rows sit above Threads, and assigning a session to a custom group always wins over the automatic zone classification. The Threads header holds the sort control (Created or Last updated, Group by, and a persisted **Status** filter for Active, Archived, or All) and the **+** that opens the New session page. Archived rows stay inline, dimmed with an archive glyph; they do not contribute unread or attention state and stay outside lineage promotion. Opening a session moves the selection highlight without reordering rows. Parent sessions with recent child runs show a disclosure and child count; expand it to inspect nested child sessions, live or terminal status, and runtime without leaving the sidebar. Selecting a child opens its chat and automatically reveals its ancestor path. Child rows stay outside root grouping, pinning, dragging, multi-select, and pagination; collapsed zones do not consume the visible page budget. Sessions with new activity since they were last read show an unread dot, and opening one marks it read. An agent can also publish a short expiring status line and optionally request attention with a curated amber icon; that declaration clears when you open the session, send the next message, clear it explicitly, or its TTL expires. Cloud-worker lifecycle states use a globe badge; local and reclaimed sessions omit a placement badge because local execution is the default. Each root session row has a context menu (kebab button or right-click) with Pin/Unpin, Mark as unread/read, Rename, Fork, Move to group (including New group and Remove from group), Archive or Unarchive, and Delete; touch layouts keep the direct pin and menu controls visible. Cmd/Ctrl-click toggles root rows into a multi-select and Shift-click extends it across the visible order; opening the menu on a selected row then offers batch actions (Mark N as unread/read, Move N to group, Archive N, Delete N) that apply to every selected session, with a single confirmation for batch delete. Drag a root session onto **Pinned** to pin it, or onto a custom group to move it. Custom group headers can be collapsed, expanded, or dragged to reorder them; group names and their order live in the gateway (`sessions.groups.*`), so they follow you across browsers, while collapsed state stays in the browser profile. Group headers also have a menu (kebab button or right-click) with Rename group, New group, and Delete group; renaming or deleting a group updates every member session server-side, including archived ones, and deleting a group keeps its sessions and moves them back to Threads. +The sidebar organizes everything around the agent. The identity row at the top is the active agent; below it, the **Pages** section starts with **Home** — the agent's rolling main session, badged with its unread or running state — followed by the pinned destinations (**Automations** and **Plugins** by default). The customize control on the Pages header opens a menu with every other destination, including **Usage** and plugin-provided tabs, plus **Edit pinned items**; right-clicking the navigation area opens the pin editor directly. The session list below splits into zones: **Threads** for the agent's chat sessions (the main session stays behind Home; sessions it spawned appear here as top-level threads, and named threads show without a type prefix), **Groups** for group and room conversations, and **Coding** for sessions bound to a managed worktree or exec node (rows show a `repo ⎇ branch` line plus the node host), ACP-backed harness sessions, and the Codex/Claude CLI catalogs. Coding starts collapsed on first run and remembers your choice; its collapsed header keeps the true count and shows a running indicator while contained sessions work. Custom groups (the session `category`) and **Pinned** rows sit above Threads, and assigning a session to a custom group always wins over the automatic zone classification. The Threads header holds the sort control (Created or Last updated, Group by, and a persisted **Status** filter for Active, Archived, or All) and the **+** that opens the New session page. Archived rows stay inline, dimmed with an archive glyph; they do not contribute unread or attention state and stay outside lineage promotion. Opening a session moves the selection highlight without reordering rows. Parent sessions with recent child runs show a disclosure and child count; expand it to inspect nested child sessions, live or terminal status, and runtime without leaving the sidebar. Selecting a child opens its chat and automatically reveals its ancestor path. Child rows stay outside root grouping, pinning, dragging, multi-select, and pagination; collapsed zones do not consume the visible page budget. Sessions with new activity since they were last read show an unread dot, and opening one marks it read. An agent can also publish a short expiring status line and optionally request attention with a curated amber icon; that declaration clears when you open the session, send the next message, clear it explicitly, or its TTL expires. Cloud-worker lifecycle states use a globe badge; local and reclaimed sessions omit a placement badge because local execution is the default. Each root session row has a context menu (kebab button or right-click) with Pin/Unpin, Mark as unread/read, Rename, Fork, Move to group (including New group and Remove from group), Archive or Unarchive, and Delete; touch layouts keep the direct pin and menu controls visible. Cmd/Ctrl-click toggles root rows into a multi-select and Shift-click extends it across the visible order; opening the menu on a selected row then offers batch actions (Mark N as unread/read, Move N to group, Archive N, Delete N) that apply to every selected session, with a single confirmation for batch delete. Drag a root session onto **Pinned** to pin it, or onto a custom group to move it. Custom group headers can be collapsed, expanded, or dragged to reorder them; group names and their order live in the gateway (`sessions.groups.*`), so they follow you across browsers, while collapsed state stays in the browser profile. Group headers also have a menu (kebab button or right-click) with Rename group, New group, and Delete group; renaming or deleting a group updates every member session server-side, including archived ones, and deleting a group keeps its sessions and moves them back to Threads. ## New session page @@ -229,7 +229,7 @@ On multi-user gateways, only admin-scope connections can create or view incognit Inside **Settings**, the dedicated sidebar includes **Ask OpenClaw** and starts with a **Search settings** field for quickly finding settings sections. -On desktop web, a fixed control cluster at the top-left of the content area — the web counterpart of the macOS titlebar strip — holds the sidebar collapse toggle (⌘B) and the command-palette search button (⌘K). Clicking the agent identity row at the top of the sidebar opens the agent menu; **Home** opens the main session. When something needs action — failed or overdue cron jobs, expiring or expired model auth — compact attention chips appear above the sidebar footer and click through to the owning page. The identity row shows the agent's avatar (identity image or emoji), name, connection dot, and a live subtitle. Its agent-scoped menu contains the inline agent switcher (multi-agent setups), **New agent**, "What can this agent do?", and **Agent settings**. Rosters above ten agents get a filter field and list pinned agents first; pin or unpin agents from the Agents settings page, with the pinned set stored in the browser profile. Choosing an agent scopes Chat plus Usage, Automations, Tasks, Workboard, and Sessions to that agent. Each scoped page exposes an **Agent** control with **All agents** as an escape; this widens the shared page scope without changing the concrete chat agent, while direct session links still open their target. The Agents settings page keeps its own `?agent=` selection and does not follow the shared page scope. The footer bar holds your identity (avatar and name) and a one-click Settings shortcut; clicking your identity opens the app/account menu with **Profile**, mobile pairing, **Get the apps**, **Help** (help, Discord, Docs, and the changelog), the version/build chip, and the color-mode toggle. A connection status pill appears only while the gateway is offline. The build chip opens the About page. When the gateway runs from a source checkout on a branch other than `main`, the footer also shows that branch name in red so a non-release gateway is obvious at a glance (release installs never show it). Shift-Command-Comma opens **Settings** without overriding the browser's Command-Comma shortcut. Collapsing the sidebar (⌘B or the cluster's toggle) hides it entirely for a full-width workspace; while collapsed, the top-left cluster keeps the expand toggle and search and gains a new-thread button — mirroring what the macOS app hosts natively in its titlebar. The sidebar is the only navigation chrome on desktop, with no top bar. Narrow viewports swap the sidebar for a slide-over drawer behind a compact header row holding the drawer toggle, brand, and command-palette search; on phones, Chat absorbs that navigation row into its title bar, with the menu and search controls beside the session title. In the macOS app the separate header row folds the titlebar clearance into a single compact strip beside the window controls. Navigation uses regular browser history, so the browser's back/forward buttons traverse it; the macOS app adds a native sidebar toggle next to the window controls plus trackpad swipe gestures, with back/forward buttons at the sidebar's right edge while it is expanded and native search (command palette) and new-session buttons while it is collapsed. +On desktop web, a fixed control cluster at the top-left of the content area — the web counterpart of the macOS titlebar strip — holds the sidebar collapse toggle (⌘B) and the command-palette search button (⌘K). Clicking the agent identity row at the top of the sidebar opens the agent menu; **Home** opens the main session. When something needs action — failed or overdue cron jobs, expiring or expired model auth — compact attention chips appear above the sidebar footer and click through to the owning page. The identity row shows the agent's avatar (identity image or emoji), name, connection dot, and a live subtitle. Its agent-scoped menu contains the inline agent switcher (multi-agent setups), **New agent**, "What can this agent do?", and **Agent settings**. Rosters above ten agents get a filter field and list pinned agents first; pin or unpin agents from the Agents settings page, with the pinned set stored in the browser profile. Choosing an agent scopes Chat plus Usage, Automations, Tasks, Workboard, and Sessions to that agent. Each scoped page exposes an **Agent** control with **All agents** as an escape; this widens the shared page scope without changing the concrete chat agent, while direct session links still open their target. The Agents settings page keeps its own `?agent=` selection and does not follow the shared page scope. The footer is one full-width identity card that remains available offline and shows **Reconnecting…** beneath the last-known account name. It opens the app/account menu, whose profile identity header is followed by **Settings**, **Usage**, mobile pairing, **Get the apps**, **Help** (help, Discord, Docs, and the changelog), an offline retry action when needed, the version/build chip, and the color-mode toggle. The build chip opens the About page. When the gateway runs from a source checkout on a branch other than `main`, the footer also shows that branch name in red so a non-release gateway is obvious at a glance (release installs never show it). Shift-Command-Comma on Apple platforms or Ctrl-Shift-Comma elsewhere opens **Settings** without overriding the browser's plain Command-Comma shortcut. Collapsing the sidebar (⌘B or the cluster's toggle) hides it entirely for a full-width workspace; while collapsed, the top-left cluster keeps the expand toggle and search and gains a new-thread button — mirroring what the macOS app hosts natively in its titlebar. The sidebar is the only navigation chrome on desktop, with no top bar. Narrow viewports swap the sidebar for a slide-over drawer behind a compact header row holding the drawer toggle, brand, and command-palette search; on phones, Chat absorbs that navigation row into its title bar, with the menu and search controls beside the session title. In the macOS app the separate header row folds the titlebar clearance into a single compact strip beside the window controls. Navigation uses regular browser history, so the browser's back/forward buttons traverse it; the macOS app adds a native sidebar toggle next to the window controls plus trackpad swipe gestures, with back/forward buttons at the sidebar's right edge while it is expanded and native search (command palette) and new-session buttons while it is collapsed. Pending approvals also contribute an attention chip above the sidebar footer; select it to open the owning Approvals page. diff --git a/ui/src/app-navigation-groups.test.ts b/ui/src/app-navigation-groups.test.ts index e4bf594d7c56..ecdc6ba6b019 100644 --- a/ui/src/app-navigation-groups.test.ts +++ b/ui/src/app-navigation-groups.test.ts @@ -16,7 +16,8 @@ const settingsRoutes = SETTINGS_NAVIGATION_GROUPS.flatMap((group) => group.route describe("sidebar entries", () => { it("keeps operational destinations visible by default", () => { - expect(DEFAULT_SIDEBAR_ENTRIES).toEqual(["route:usage", "route:cron", "route:plugins"]); + expect(DEFAULT_SIDEBAR_ENTRIES).toEqual(["route:cron", "route:plugins"]); + expect(DEFAULT_SIDEBAR_ENTRIES).not.toContain("route:usage"); }); it("drops retired routes from persisted entries", () => { diff --git a/ui/src/app-navigation.ts b/ui/src/app-navigation.ts index 49aac0328c24..83dd89fee5dc 100644 --- a/ui/src/app-navigation.ts +++ b/ui/src/app-navigation.ts @@ -56,7 +56,7 @@ export type SidebarZoneEntry = // Keep the highest-value operational destinations visible on first use. Users // can still replace this route set through the customize menu. -export const DEFAULT_SIDEBAR_ENTRIES = ["usage", "cron", "plugins"].map((route) => +export const DEFAULT_SIDEBAR_ENTRIES = ["cron", "plugins"].map((route) => serializeSidebarEntry({ type: "route", route: route as SidebarNavRoute }), ); diff --git a/ui/src/app/app-host.test.ts b/ui/src/app/app-host.test.ts index 8ba80b4857ae..03ef19dfbd94 100644 --- a/ui/src/app/app-host.test.ts +++ b/ui/src/app/app-host.test.ts @@ -848,6 +848,28 @@ describe("OpenClaw shell keyboard shortcuts", () => { expect(navigate).toHaveBeenCalledWith("config", undefined); }); + it("opens Settings with Ctrl-Shift-Comma", () => { + const navigate = vi.fn(); + const shell = document.createElement("openclaw-app-shell") as unknown as ShellKeyboardState; + shell.runtime = { + context: { + navigate, + } as unknown as ApplicationContext, + }; + const event = new KeyboardEvent("keydown", { + key: "<", + code: "Comma", + ctrlKey: true, + shiftKey: true, + cancelable: true, + }); + + shell.handleDocumentKeydown(event); + + expect(event.defaultPrevented).toBe(true); + expect(navigate).toHaveBeenCalledWith("config", undefined); + }); + it("toggles the navigation sidebar when the native macOS titlebar button fires", () => { const snapshot = { navCollapsed: false }; const update = vi.fn((next: { navCollapsed: boolean }) => { diff --git a/ui/src/app/app-host.ts b/ui/src/app/app-host.ts index 6dc855d65908..5c49bc271283 100644 --- a/ui/src/app/app-host.ts +++ b/ui/src/app/app-host.ts @@ -1161,12 +1161,13 @@ class OpenClawShell extends OpenClawLightDomElement { this.exitSettings(); return; } - const commandKey = event.metaKey && !event.ctrlKey && !event.altKey; - if (commandKey && event.shiftKey && event.code === "Comma") { + const settingsModifier = event.metaKey !== event.ctrlKey && !event.altKey; + if (settingsModifier && event.shiftKey && event.code === "Comma") { event.preventDefault(); this.navigate("config"); return; } + const commandKey = event.metaKey && !event.ctrlKey && !event.altKey; if (!commandKey || event.shiftKey || event.key.toLowerCase() !== "b") { return; } @@ -1818,8 +1819,6 @@ class OpenClawShell extends OpenClawLightDomElement { .connected=${gatewayConnected} .offline=${gatewaySnapshot.offlineStable} .outboxCountForSession=${outboxCountForSession} - .queuedOutboxCount=${storedOutboxes?.total ?? 0} - .lastError=${gatewaySnapshot.lastError} .terminalAvailable=${terminalAvailable} .catalogOpenTarget=${normalizeCatalogOpenTarget(uiSettings.catalogOpenTarget)} .canPairDevice=${gatewayConnected && diff --git a/ui/src/app/settings.node.test.ts b/ui/src/app/settings.node.test.ts index 6902474bd35e..2a8a2fa135f4 100644 --- a/ui/src/app/settings.node.test.ts +++ b/ui/src/app/settings.node.test.ts @@ -402,7 +402,7 @@ describe("loadSettings default gateway URL derivation", () => { persisted.navWidth = 220; localStorage.setItem(scopedKey, JSON.stringify(persisted)); - expect(loadSettings().sidebarEntries).toEqual(["route:usage", "route:cron", "route:plugins"]); + expect(loadSettings().sidebarEntries).toEqual(["route:cron", "route:plugins"]); expect(loadSettings().navWidth).toBe(258); }); diff --git a/ui/src/components/app-sidebar-agent-menu.ts b/ui/src/components/app-sidebar-agent-menu.ts index 20a222d6afe1..eb76cb32685f 100644 --- a/ui/src/components/app-sidebar-agent-menu.ts +++ b/ui/src/components/app-sidebar-agent-menu.ts @@ -71,17 +71,26 @@ type SidebarAgentMenuParams = { }; type SidebarIdentityMenuParams = { - position: { x: number; bottom: number } | null; + position: { x: number; bottom: number; width: number } | null; canPairDevice: boolean; basePath: string; gatewayVersion: string | null; + selfName?: string; + selfEmail?: string; + offline: boolean; themeMode: ThemeMode; + triggerWidth: number; onTabAway: () => void; onClose: (restoreFocus?: boolean) => void; onNavigate: (routeId: NavigationRouteId, options?: ApplicationNavigationOptions) => void; onPairMobile: () => void; + onRetryConnect?: () => void; }; +function isApplePlatform(): boolean { + return /Mac|iPhone|iPad|iPod/u.test(globalThis.navigator?.platform ?? ""); +} + /** Rows for the chip switcher. Small rosters list everything; past QUICK_SWITCH_AGENT_LIMIT the menu shows pinned agents (plus the active one) and the filter searches the full roster. */ @@ -356,10 +365,12 @@ export function renderSidebarIdentityMenu(params: SidebarIdentityMenuParams) { if (!position) { return nothing; } + const profileLabel = params.selfEmail ?? params.selfName; return html` @@ -405,9 +425,22 @@ export function renderSidebarIdentityMenu(params: SidebarIdentityMenuParams) { aria-label=${t("profilePage.identity.menuLabel")} style="position: fixed; left: ${position.x}px; bottom: ${position.bottom}px; width: 1px; height: 1px; opacity: 0; pointer-events: none;" > - - - ${titleForRoute("profile")} + ${profileLabel + ? html` + ${profileLabel} + + ` + : nothing} + + + ${t("nav.settings")} + + + + + ${titleForRoute("usage")} ${t("agentChip.help")} ${renderIdentityMenuHelpSubmenu()} + ${params.offline + ? html` + + ${t("connection.retryNow")} + ` + : nothing}