diff --git a/docs/web/control-ui.md b/docs/web/control-ui.md index 7b71e26e8f9f..034a389859e2 100644 --- a/docs/web/control-ui.md +++ b/docs/web/control-ui.md @@ -98,7 +98,7 @@ Open **Settings** in Simple view to see the **Gateway Host** card with the Gatew ## Language support -The Control UI localizes itself on first load based on your browser locale. To override it later, open **Overview -> Gateway Access -> Language** (the picker lives in the Gateway Access card, not under Appearance). +The Control UI localizes itself on first load based on your browser locale. To override it later, open **Settings -> General -> Language** (the picker lives in the General quick-settings card, not under Appearance). - Supported locales: `en`, `ar`, `de`, `es`, `fa`, `fr`, `hi`, `id`, `it`, `ja-JP`, `ko`, `nl`, `pl`, `pt-BR`, `ru`, `th`, `tr`, `uk`, `vi`, `zh-CN`, `zh-TW` - Non-English translations are lazy-loaded in the browser. @@ -156,7 +156,7 @@ local-path sources, updates, and advanced plugin configuration. ## Sidebar navigation -The sidebar pins navigation above a scrollable session list. In multi-agent setups every agent appears as a collapsible top-level section; expanding an agent browses its sessions without navigating away from the open chat, and collapsed agents show an unread indicator. Within an agent the list splits into **Pinned**, one built-in section per connected channel (Telegram, Slack, WhatsApp, ...), a built-in **Work** section for sessions bound to a managed worktree or exec node (rows show a `repo ⎇ branch` line plus the node host), custom groups (the session `category`), and **Chats** for the rest. Channel and Work sections classify rows automatically; assigning a session to a custom group always wins. Opening a session moves the selection highlight without reordering the rows. Sessions with new activity since they were last read show an unread dot, and opening one marks it read. Each 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, and Delete; touch layouts keep the direct pin and menu controls visible. Cmd/Ctrl-click toggles 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 session onto a custom group or **Chats** 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 the 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 Chats. The single **+** in the session-list header opens the New session page (see below). The sort control also has a Group by toggle: Grouped (default) or None for one flat list (Pinned stays separate); the choice is stored in the current browser profile. **Overview** is the only destination pinned by default; expand **More** to reach every other destination. Select **Edit pinned items** 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 pins navigation above a scrollable session list. In multi-agent setups every agent appears as a collapsible top-level section; expanding an agent browses its sessions without navigating away from the open chat, and collapsed agents show an unread indicator. Within an agent the list splits into **Pinned**, one built-in section per connected channel (Telegram, Slack, WhatsApp, ...), a built-in **Work** section for sessions bound to a managed worktree or exec node (rows show a `repo ⎇ branch` line plus the node host), custom groups (the session `category`), and **Chats** for the rest. Channel and Work sections classify rows automatically; assigning a session to a custom group always wins. Opening a session moves the selection highlight without reordering the rows. Sessions with new activity since they were last read show an unread dot, and opening one marks it read. Each 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, and Delete; touch layouts keep the direct pin and menu controls visible. Cmd/Ctrl-click toggles 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 session onto a custom group or **Chats** 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 the 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 Chats. The single **+** in the session-list header opens the New session page (see below). The sort control also has a Group by toggle: Grouped (default) or None for one flat list (Pinned stays separate); the choice is stored in the current browser profile. Nothing is pinned by default; expand **More** to reach every destination. Select **Edit pinned items** 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. ## New session page @@ -164,7 +164,7 @@ The **+** in the sidebar session-list header opens a full-page draft at `/new`: Inside **Settings**, the dedicated sidebar starts with a **Search settings** field for quickly finding settings sections. -A **Search** field at the top of the sidebar opens the command palette (⌘K). The compact footer keeps connection status, **Settings**, **Docs**, mobile pairing, and the light/dark/system color-mode toggle together; 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. The sidebar header also holds the collapse toggle (⌘B); collapsing hides the sidebar entirely for a full-width workspace, and a floating expand control (or ⌘B) brings it back; the macOS app hosts that toggle natively in the titlebar instead. 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; in the macOS app that 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 and back/forward buttons next to the window controls, plus trackpad swipe gestures. +A **Search** field at the top of the sidebar opens the command palette (⌘K). Clicking the OpenClaw brand in the sidebar header returns to the chat home. 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 compact footer keeps connection status, **Settings**, **Docs**, mobile pairing, and the light/dark/system color-mode toggle together; 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. The sidebar header also holds the collapse toggle (⌘B); collapsing hides the sidebar entirely for a full-width workspace, and a floating expand control (or ⌘B) brings it back; the macOS app hosts that toggle natively in the titlebar instead. 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; in the macOS app that 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 and back/forward buttons next to the window controls, plus trackpad swipe gestures. ## What it can do (today) @@ -202,6 +202,7 @@ A **Search** field at the top of the sidebar opens the command palette (⌘K). T - Profile: a settings page showing the default agent's identity with all-time usage stats — lifetime tokens, peak day, longest session, activity streaks, a year-long token heatmap, top tools, and channel highlights (`usage.cost`, `sessions.usage`). - MCP has a dedicated settings page with read-only server rows (transport, enablement, OAuth/filter/parallel summaries), common operator commands, and the scoped `mcp` config editor; adding, enabling/disabling, and removing servers happens on the Plugins page. - Model Providers: a settings page listing every configured model provider with its brand icon, auth state (`models.authStatus`), model availability (`models.list`), live plan/quota/billing data where the provider reports it (`usage.status`), and local session spend for the last 30 days (`sessions.usage`). A Refresh action re-reads credential state and provider usage. + - Connection: a settings page (under **Connections**) owning the dashboard's own gateway link — WebSocket URL, gateway token, password, and default session key — plus the latest handshake snapshot (status, uptime, tick interval, last channels refresh). The offline login gate handles the disconnected case; this page edits the connection while connected. - Apply and restart with validation (`config.apply`), then wake the last active session. - Writes include a base-hash guard to prevent clobbering concurrent edits. - Writes (`config.set`/`config.apply`/`config.patch`) preflight active SecretRef resolution for refs in the submitted config payload; unresolved active submitted refs are rejected before write. diff --git a/docs/web/dashboard.md b/docs/web/dashboard.md index 6957954ddf1f..d0489d762a2c 100644 --- a/docs/web/dashboard.md +++ b/docs/web/dashboard.md @@ -82,7 +82,7 @@ Non-goals for v1: - SecretRef-managed token: resolve the external secret provider, or export `OPENCLAW_GATEWAY_TOKEN` in this shell and rerun `openclaw dashboard` - No shared secret configured: `openclaw doctor --generate-gateway-token` - In the dashboard settings, paste the token or password into the auth field, then connect. -- The UI language picker lives in **Overview -> Gateway Access -> Language**, not under Appearance. +- The UI language picker lives in **Settings -> General -> Language**, not under Appearance. ## Related diff --git a/ui/src/api/types.ts b/ui/src/api/types.ts index b738d9fe9b33..72e9f193be34 100644 --- a/ui/src/api/types.ts +++ b/ui/src/api/types.ts @@ -865,16 +865,3 @@ export type ModelAuthStatusProvider = import("../../../src/gateway/server-methods/models-auth-status.js").ModelAuthStatusProvider; export type ModelAuthStatusResult = import("../../../src/gateway/server-methods/models-auth-status.js").ModelAuthStatusResult; - -// ── Attention ─────────────────────────────────────── - -type AttentionSeverity = "error" | "warning" | "info"; - -export type AttentionItem = { - severity: AttentionSeverity; - icon: string; - title: string; - description: string; - href?: string; - external?: boolean; -}; diff --git a/ui/src/app-navigation-groups.test.ts b/ui/src/app-navigation-groups.test.ts index 7cf425ab0ef2..1651979be31d 100644 --- a/ui/src/app-navigation-groups.test.ts +++ b/ui/src/app-navigation-groups.test.ts @@ -10,11 +10,12 @@ import { } from "./app-navigation.ts"; describe("sidebar pinned routes", () => { - it("defaults to a small pinned set drawn from the customizable routes", () => { - expect(DEFAULT_SIDEBAR_PINNED_ROUTES.length).toBeLessThan(SIDEBAR_NAV_ROUTES.length); - for (const routeId of DEFAULT_SIDEBAR_PINNED_ROUTES) { - expect(SIDEBAR_NAV_ROUTES).toContain(routeId); - } + it("defaults to no pinned routes so sessions stay the sidebar's core content", () => { + expect(DEFAULT_SIDEBAR_PINNED_ROUTES).toEqual([]); + }); + + it("drops the retired overview route from persisted pins", () => { + expect(normalizeSidebarPinnedRoutes(["overview", "usage"])).toEqual(["usage"]); }); it("keeps managed worktrees in settings, not the customizable sidebar", () => { @@ -25,7 +26,7 @@ describe("sidebar pinned routes", () => { it("moves activity into system settings and drops stale pinned entries", () => { expect(SIDEBAR_NAV_ROUTES).not.toContain("activity"); expect(SETTINGS_NAVIGATION_ROUTES).toContain("activity"); - expect(normalizeSidebarPinnedRoutes(["activity", "overview"])).toEqual(["overview"]); + expect(normalizeSidebarPinnedRoutes(["activity", "usage"])).toEqual(["usage"]); }); it("keeps channel management and settings slices out of the customizable sidebar", () => { @@ -39,30 +40,30 @@ describe("sidebar pinned routes", () => { it("normalizes persisted pinned routes, dropping unknown and duplicate entries", () => { expect( - normalizeSidebarPinnedRoutes(["usage", "overview", "usage", "worktrees", "instances", 7]), - ).toEqual(["usage", "overview"]); + normalizeSidebarPinnedRoutes(["usage", "sessions", "usage", "worktrees", "instances", 7]), + ).toEqual(["usage", "sessions"]); expect(normalizeSidebarPinnedRoutes([])).toEqual([]); }); it("keeps the plugin manager in the customizable workspace routes", () => { - expect(normalizeSidebarPinnedRoutes(["plugins", "overview", "plugins"])).toEqual([ + expect(normalizeSidebarPinnedRoutes(["plugins", "usage", "plugins"])).toEqual([ "plugins", - "overview", + "usage", ]); - expect(sidebarMoreRoutes(["overview"])).toContain("plugins"); + expect(sidebarMoreRoutes(["usage"])).toContain("plugins"); expect(SETTINGS_NAVIGATION_ROUTES).not.toContain("plugins"); }); it("falls back to null for non-list values so callers use defaults", () => { expect(normalizeSidebarPinnedRoutes(undefined)).toBeNull(); - expect(normalizeSidebarPinnedRoutes({ overview: true })).toBeNull(); - expect(normalizeSidebarPinnedRoutes("overview")).toBeNull(); + expect(normalizeSidebarPinnedRoutes({ usage: true })).toBeNull(); + expect(normalizeSidebarPinnedRoutes("usage")).toBeNull(); }); it("puts every unpinned nav route into the More section", () => { - const pinned = ["overview", "usage"] as const; + const pinned = ["sessions", "usage"] as const; const more = sidebarMoreRoutes(pinned); - expect(more).not.toContain("overview"); + expect(more).not.toContain("sessions"); expect(more).not.toContain("usage"); expect(new Set([...pinned, ...more])).toEqual(new Set(SIDEBAR_NAV_ROUTES)); }); diff --git a/ui/src/app-navigation.test.ts b/ui/src/app-navigation.test.ts index 495a677d0b54..32d60394ad2c 100644 --- a/ui/src/app-navigation.test.ts +++ b/ui/src/app-navigation.test.ts @@ -61,11 +61,11 @@ describe("navigationIconForRoute", () => { Object.fromEntries(ALL_ROUTES.map((routeId) => [routeId, navigationIconForRoute(routeId)])), ).toEqual({ chat: "messageSquare", - overview: "barChart", activity: "activity", workboard: "kanban", worktrees: "folder", channels: "link", + connection: "radio", sessions: "fileText", usage: "coins", cron: "calendarClock", @@ -103,11 +103,11 @@ describe("titleForRoute", () => { Object.fromEntries(ALL_ROUTES.map((routeId) => [routeId, titleForRoute(routeId)])), ).toEqual({ chat: "Chat", - overview: "Overview", activity: "Activity", workboard: "Workboard", worktrees: "Worktrees", channels: "Channels", + connection: "Connection", sessions: "Sessions", usage: "Usage", cron: "Cron Jobs", @@ -139,11 +139,11 @@ describe("subtitleForRoute", () => { Object.fromEntries(ALL_ROUTES.map((routeId) => [routeId, subtitleForRoute(routeId)])), ).toEqual({ chat: "Gateway chat for quick interventions.", - overview: "Status, entry points, health.", activity: "Browser-local tool activity summaries.", workboard: "Agent work queue and session handoff.", worktrees: "Isolated agent task checkouts and recovery snapshots.", channels: "Channels and settings.", + connection: "Gateway endpoint, credentials, and handshake status.", sessions: "Active sessions and defaults.", usage: "API usage and costs.", cron: "Wakeups and recurring runs.", @@ -210,7 +210,7 @@ describe("normalizePath", () => { describe("pathForRoute", () => { it("returns correct path without base", () => { expect(pathForRoute("chat")).toBe("/chat"); - expect(pathForRoute("overview")).toBe("/overview"); + expect(pathForRoute("connection")).toBe("/settings/connection"); expect(pathForRoute("debug")).toBe("/debug"); expect(pathForRoute("logs")).toBe("/logs"); expect(pathForRoute("plugins")).toBe("/settings/plugins"); @@ -226,7 +226,9 @@ describe("routeIdFromPath", () => { it("returns tab for valid path", () => { expect(routeIdFromPath("/chat")).toBe("chat"); expect(routeIdFromPath("/new")).toBe("new-session"); - expect(routeIdFromPath("/overview")).toBe("overview"); + expect(routeIdFromPath("/overview")).toBeNull(); + expect(routeIdFromPath("/settings/connection")).toBe("connection"); + expect(routeIdFromPath("/connection")).toBeNull(); expect(routeIdFromPath("/activity")).toBe("activity"); expect(routeIdFromPath("/sessions")).toBe("sessions"); expect(routeIdFromPath("/debug")).toBe("debug"); @@ -261,7 +263,7 @@ describe("routeIdFromPath", () => { it("matches canonical route casing exactly", () => { expect(routeIdFromPath("/CHAT")).toBeNull(); - expect(routeIdFromPath("/Overview")).toBeNull(); + expect(routeIdFromPath("/Sessions")).toBeNull(); }); }); @@ -300,7 +302,7 @@ describe("inferBasePathFromPathname", () => { it("returns empty string for direct tab path", () => { expect(inferBasePathFromPathname("/chat")).toBe(""); - expect(inferBasePathFromPathname("/overview")).toBe(""); + expect(inferBasePathFromPathname("/settings/connection")).toBe(""); expect(inferBasePathFromPathname("/settings/general")).toBe(""); expect(inferBasePathFromPathname("/settings/appearance")).toBe(""); expect(inferBasePathFromPathname("/appearance")).toBe(""); @@ -363,6 +365,7 @@ describe("SIDEBAR_NAV_ROUTES", () => { "profile", "config", "appearance", + "connection", "channels", "communications", "ai-agents", diff --git a/ui/src/app-navigation.ts b/ui/src/app-navigation.ts index acb855f8fcff..1e75e30a4368 100644 --- a/ui/src/app-navigation.ts +++ b/ui/src/app-navigation.ts @@ -13,7 +13,6 @@ type NavigationItem = { // lives in the collapsed "More" section. Chat is reachable through the session // list and Settings/Docs live in the sidebar footer, so neither is listed here. export const SIDEBAR_NAV_ROUTES = [ - "overview", "workboard", "sessions", "usage", @@ -28,11 +27,9 @@ 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", -] as const satisfies readonly SidebarNavRoute[]; +// Sessions are the sidebar's core content; nothing is pinned by default. Users +// pin pages via the customize menu. +export const DEFAULT_SIDEBAR_PINNED_ROUTES = [] as const satisfies readonly SidebarNavRoute[]; /** * Normalize a persisted pinned-route list. Returns null when the value is not a @@ -69,7 +66,10 @@ type SettingsNavigationGroup = { // Grouping feeds the full-page settings sidebar (settings-sidebar.ts). export const SETTINGS_NAVIGATION_GROUPS = [ { labelKey: null, routes: ["profile", "config", "appearance"] }, - { labelKey: "nav.settingsGroupConnections", routes: ["channels", "communications"] }, + { + labelKey: "nav.settingsGroupConnections", + routes: ["connection", "channels", "communications"], + }, { labelKey: "nav.settingsGroupAgents", routes: ["ai-agents", "model-providers", "automation", "mcp"], @@ -86,10 +86,10 @@ export const SETTINGS_NAVIGATION_ROUTES: readonly NavigationRouteId[] = const NAVIGATION_ICONS: NavigationItem = { agents: "bot", activity: "activity", - overview: "barChart", workboard: "kanban", worktrees: "folder", channels: "link", + connection: "radio", sessions: "fileText", usage: "coins", cron: "calendarClock", @@ -174,10 +174,10 @@ export function cancelRoutePreload( const NAVIGATION_COPY: Record = { agents: { titleKey: "tabs.agents", subtitleKey: "subtitles.agents" }, activity: { titleKey: "tabs.activity", subtitleKey: "subtitles.activity" }, - overview: { titleKey: "tabs.overview", subtitleKey: "subtitles.overview" }, workboard: { titleKey: "tabs.workboard", subtitleKey: "subtitles.workboard" }, worktrees: { titleKey: "tabs.worktrees", subtitleKey: "subtitles.worktrees" }, channels: { titleKey: "tabs.channels", subtitleKey: "subtitles.channels" }, + connection: { titleKey: "tabs.connection", subtitleKey: "subtitles.connection" }, sessions: { titleKey: "tabs.sessions", subtitleKey: "subtitles.sessions" }, usage: { titleKey: "tabs.usage", subtitleKey: "subtitles.usage" }, cron: { titleKey: "tabs.cron", subtitleKey: "subtitles.cron" }, diff --git a/ui/src/app-route-paths.ts b/ui/src/app-route-paths.ts index 163d624736ce..88f9b102c9fa 100644 --- a/ui/src/app-route-paths.ts +++ b/ui/src/app-route-paths.ts @@ -4,10 +4,10 @@ import type { RouteLocation } from "@openclaw/uirouter"; const APP_ROUTE_DEFINITIONS = { chat: { path: "/chat" }, "new-session": { path: "/new" }, - overview: { path: "/overview" }, activity: { path: "/activity" }, agents: { path: "/agents" }, channels: { path: "/settings/channels", aliases: ["/channels"] }, + connection: { path: "/settings/connection" }, config: { path: "/settings/general", aliases: ["/config"] }, profile: { path: "/settings/profile", aliases: ["/profile"] }, communications: { path: "/settings/communications", aliases: ["/communications"] }, diff --git a/ui/src/app-routes.ts b/ui/src/app-routes.ts index 824156984209..6009c0548568 100644 --- a/ui/src/app-routes.ts +++ b/ui/src/app-routes.ts @@ -8,13 +8,13 @@ import { page as agentsPage } from "./pages/agents/route.ts"; import { page as channelsPage } from "./pages/channels/route.ts"; import { page as chatPage } from "./pages/chat/route.ts"; import { pages as configPages } from "./pages/config/route.ts"; +import { page as connectionPage } from "./pages/connection/route.ts"; import { page as cronPage } from "./pages/cron/route.ts"; import { page as debugPage } from "./pages/debug/route.ts"; import { page as logsPage } from "./pages/logs/route.ts"; import { page as modelProvidersPage } from "./pages/model-providers/route.ts"; import { page as newSessionPage } from "./pages/new-session/route.ts"; import { page as nodesPage } from "./pages/nodes/route.ts"; -import { page as overviewPage } from "./pages/overview/route.ts"; import { page as pluginPage } from "./pages/plugin/route.ts"; import { page as pluginsPage } from "./pages/plugins/route.ts"; import { page as profilePage } from "./pages/profile/route.ts"; @@ -41,10 +41,10 @@ type AppRoute = PageDefinition, AppRouteMod const APP_ROUTE_TREE = [ chatPage, newSessionPage, - overviewPage, activityPage, agentsPage, channelsPage, + connectionPage, aboutPage, ...configPages, modelProvidersPage, @@ -79,6 +79,8 @@ export async function startApplicationRouter( context: ApplicationContext, ): Promise { const location = history.location(); + // Unknown paths (including retired routes like /overview) land on chat, so + // removed pages need no legacy aliases for stale bookmarks or history. if (routeIdFromPath(location.pathname, basePath) === null) { history.replace({ ...location, diff --git a/ui/src/app/app-host.test.ts b/ui/src/app/app-host.test.ts index 33693c8874f8..3d5cea80b828 100644 --- a/ui/src/app/app-host.test.ts +++ b/ui/src/app/app-host.test.ts @@ -115,7 +115,7 @@ describe("OpenClaw shell source initialization", () => { const trigger = document.createElement("button"); shell.navDrawerOpen = true; shell.navDrawerTrigger = trigger; - shell.lastWorkspaceLocation = { routeId: "overview", search: "?agent=old" }; + shell.lastWorkspaceLocation = { routeId: "usage", search: "?agent=old" }; shell.activeSessionKey = "agent:old:main"; shell.commandPaletteTarget = {}; shell.agentsListClient = client; @@ -147,7 +147,7 @@ describe("OpenClaw shell source initialization", () => { const shell = document.createElement( "openclaw-app-shell", ) as unknown as ShellInitializationState; - shell.routeState = { routeId: "overview" }; + shell.routeState = { routeId: "usage" }; const client = {} as GatewayBrowserClient; const snapshot = { client, connected: true }; const firstAgents = { diff --git a/ui/src/app/settings.node.test.ts b/ui/src/app/settings.node.test.ts index dea390e1cd9d..d7d08d6dc564 100644 --- a/ui/src/app/settings.node.test.ts +++ b/ui/src/app/settings.node.test.ts @@ -59,7 +59,7 @@ function makeSettings(gatewayUrl: string, overrides: Partial = {}): splitRatio: 0.6, navCollapsed: false, navWidth: 258, - sidebarPinnedRoutes: ["overview"], + sidebarPinnedRoutes: [], sidebarMoreExpanded: false, ...overrides, }; @@ -233,7 +233,7 @@ describe("loadSettings default gateway URL derivation", () => { splitRatio: 0.6, navCollapsed: false, navWidth: 258, - sidebarPinnedRoutes: ["overview"], + sidebarPinnedRoutes: [], sidebarMoreExpanded: false, textScale: 100, }); @@ -265,7 +265,7 @@ describe("loadSettings default gateway URL derivation", () => { splitRatio: 0.6, navCollapsed: false, navWidth: 258, - sidebarPinnedRoutes: ["overview"], + sidebarPinnedRoutes: [], sidebarMoreExpanded: false, }); @@ -282,7 +282,7 @@ describe("loadSettings default gateway URL derivation", () => { splitRatio: 0.6, navCollapsed: false, navWidth: 258, - sidebarPinnedRoutes: ["overview"], + sidebarPinnedRoutes: [], sidebarMoreExpanded: false, }); @@ -311,7 +311,7 @@ describe("loadSettings default gateway URL derivation", () => { splitRatio: 0.6, navCollapsed: false, navWidth: 258, - sidebarPinnedRoutes: ["overview"], + sidebarPinnedRoutes: [], sidebarMoreExpanded: false, }); const settings = loadSettings(); @@ -330,7 +330,7 @@ describe("loadSettings default gateway URL derivation", () => { splitRatio: 0.6, navCollapsed: false, navWidth: 258, - sidebarPinnedRoutes: ["overview"], + sidebarPinnedRoutes: [], sidebarMoreExpanded: false, textScale: 100, sessionsByGateway: { @@ -384,7 +384,7 @@ describe("loadSettings default gateway URL derivation", () => { persisted.navWidth = 220; localStorage.setItem(scopedKey, JSON.stringify(persisted)); - expect(loadSettings().sidebarPinnedRoutes).toEqual(["overview"]); + expect(loadSettings().sidebarPinnedRoutes).toEqual([]); expect(loadSettings().sidebarMoreExpanded).toBe(false); expect(loadSettings().navWidth).toBe(258); }); @@ -503,7 +503,7 @@ describe("loadSettings default gateway URL derivation", () => { splitRatio: 0.6, navCollapsed: false, navWidth: 258, - sidebarPinnedRoutes: ["overview"], + sidebarPinnedRoutes: [], sidebarMoreExpanded: false, }); saveSettings({ @@ -518,7 +518,7 @@ describe("loadSettings default gateway URL derivation", () => { splitRatio: 0.6, navCollapsed: false, navWidth: 258, - sidebarPinnedRoutes: ["overview"], + sidebarPinnedRoutes: [], sidebarMoreExpanded: false, }); @@ -546,7 +546,7 @@ describe("loadSettings default gateway URL derivation", () => { splitRatio: 0.6, navCollapsed: false, navWidth: 320, - sidebarPinnedRoutes: ["overview"], + sidebarPinnedRoutes: [], sidebarMoreExpanded: false, }); @@ -617,7 +617,7 @@ describe("loadSettings default gateway URL derivation", () => { splitRatio: 0.6, navCollapsed: false, navWidth: 258, - sidebarPinnedRoutes: ["overview"], + sidebarPinnedRoutes: [], sidebarMoreExpanded: false, customTheme, }); @@ -647,7 +647,7 @@ describe("loadSettings default gateway URL derivation", () => { splitRatio: 0.6, navCollapsed: false, navWidth: 258, - sidebarPinnedRoutes: ["overview"], + sidebarPinnedRoutes: [], sidebarMoreExpanded: false, customTheme: { sourceUrl: "https://tweakcn.com/themes/broken", @@ -691,7 +691,7 @@ describe("loadSettings default gateway URL derivation", () => { splitRatio: 0.6, navCollapsed: false, navWidth: 258, - sidebarPinnedRoutes: ["overview"], + sidebarPinnedRoutes: [], sidebarMoreExpanded: false, }); @@ -734,7 +734,7 @@ describe("loadSettings default gateway URL derivation", () => { splitRatio: 0.6, navCollapsed: false, navWidth: 258, - sidebarPinnedRoutes: ["overview"], + sidebarPinnedRoutes: [], sidebarMoreExpanded: false, }); diff --git a/ui/src/components/app-sidebar.test.ts b/ui/src/components/app-sidebar.test.ts index 8b02502430eb..4ba7f4fcf276 100644 --- a/ui/src/components/app-sidebar.test.ts +++ b/ui/src/components/app-sidebar.test.ts @@ -25,6 +25,11 @@ import { type LobsterLogoVisitDetail, } from "./lobster-pet.ts"; +// Keep the attention widget inert: it fires its own health RPCs (cron.list, +// models.authStatus) on connect, which would interleave with the nth-call +// assertions on the shared mocked client below. It has its own test file. +vi.mock("./sidebar-attention.ts", () => ({})); + const PROVIDER_ELEMENT_NAME = "test-app-sidebar-context-provider"; class AppSidebarContextProvider extends LitElement { @@ -234,7 +239,7 @@ afterEach(() => { }); describe("AppSidebar update card wiring", () => { - it("renders the update card first in the footer and forwards its action", async () => { + it("renders the update card in the footer after the attention slot and forwards its action", async () => { const gateway = createGateway({} as GatewayBrowserClient); const { sidebar } = await mountSidebar(gateway, createSessions("main", ["agent:main:main"])); const onUpdate = vi.fn(); @@ -247,8 +252,10 @@ describe("AppSidebar update card wiring", () => { await sidebar.updateComplete; const footer = sidebar.querySelector(".sidebar-shell__footer"); - const card = footer?.firstElementChild; - expect(card?.localName).toBe("openclaw-sidebar-update-card"); + // Attention chips (when present) stack above the update card. + expect(footer?.firstElementChild?.localName).toBe("openclaw-sidebar-attention"); + const card = footer?.querySelector("openclaw-sidebar-update-card"); + expect(card).not.toBeNull(); card?.querySelector(".sidebar-update-card__action")?.click(); expect(onUpdate).toHaveBeenCalledOnce(); }); diff --git a/ui/src/components/app-sidebar.ts b/ui/src/components/app-sidebar.ts index a2519c0d763b..d95966a9badb 100644 --- a/ui/src/components/app-sidebar.ts +++ b/ui/src/components/app-sidebar.ts @@ -31,6 +31,7 @@ import { import { controlUiPublicAssetPath } from "../app/public-assets.ts"; import type { ThemeMode } from "../app/theme.ts"; import "./session-menu.ts"; +import "./sidebar-attention.ts"; import "./sidebar-build-chip.ts"; import "./sidebar-update-card.ts"; import "./theme-mode-toggle.ts"; @@ -773,7 +774,18 @@ class AppSidebar extends OpenClawLightDomContentsElement { const collapseLabel = t("nav.collapse"); return html` diff --git a/ui/src/components/connect-command.ts b/ui/src/components/connect-command.ts index 8e9226d752fe..cbcc77fdf574 100644 --- a/ui/src/components/connect-command.ts +++ b/ui/src/components/connect-command.ts @@ -13,14 +13,14 @@ async function copyCommand(command: string) { } export function renderConnectCommand(command: string) { - const copyLabel = t("overview.connection.copyCommand"); + const copyLabel = t("connection.help.copyCommand"); return html`