From 8295a12a837de1dfbafe4075878e393937eb35e2 Mon Sep 17 00:00:00 2001 From: vyctorbrzezowski Date: Mon, 6 Jul 2026 13:39:23 -0300 Subject: [PATCH] improve(ui): move shell controls into side rails --- ui/src/app/app-host.ts | 99 +++--- ui/src/components/app-sidebar.ts | 54 ++- ui/src/components/app-topbar.ts | 123 ++----- ui/src/components/dashboard-header.ts | 76 ----- ui/src/components/terminal/terminal-panel.ts | 31 +- ui/src/components/theme-mode-toggle.ts | 64 ++-- ui/src/e2e/sidebar-customization.e2e.test.ts | 14 +- ui/src/i18n/.i18n/ar.meta.json | 10 +- ui/src/i18n/.i18n/de.meta.json | 10 +- ui/src/i18n/.i18n/es.meta.json | 10 +- ui/src/i18n/.i18n/fa.meta.json | 10 +- ui/src/i18n/.i18n/fr.meta.json | 10 +- ui/src/i18n/.i18n/hi.meta.json | 10 +- ui/src/i18n/.i18n/id.meta.json | 10 +- ui/src/i18n/.i18n/it.meta.json | 10 +- ui/src/i18n/.i18n/ja-JP.meta.json | 10 +- ui/src/i18n/.i18n/ko.meta.json | 10 +- ui/src/i18n/.i18n/nl.meta.json | 10 +- ui/src/i18n/.i18n/pl.meta.json | 10 +- ui/src/i18n/.i18n/pt-BR.meta.json | 10 +- ui/src/i18n/.i18n/raw-copy-baseline.json | 28 +- ui/src/i18n/.i18n/ru.meta.json | 10 +- ui/src/i18n/.i18n/th.meta.json | 10 +- ui/src/i18n/.i18n/tr.meta.json | 10 +- ui/src/i18n/.i18n/uk.meta.json | 10 +- ui/src/i18n/.i18n/vi.meta.json | 10 +- ui/src/i18n/.i18n/zh-CN.meta.json | 10 +- ui/src/i18n/.i18n/zh-TW.meta.json | 10 +- ui/src/i18n/locales/ar.ts | 2 +- ui/src/i18n/locales/de.ts | 2 +- ui/src/i18n/locales/en.ts | 6 +- ui/src/i18n/locales/es.ts | 2 +- ui/src/i18n/locales/fa.ts | 2 +- ui/src/i18n/locales/fr.ts | 2 +- ui/src/i18n/locales/hi.ts | 2 +- ui/src/i18n/locales/id.ts | 2 +- ui/src/i18n/locales/it.ts | 2 +- ui/src/i18n/locales/ja-JP.ts | 2 +- ui/src/i18n/locales/ko.ts | 2 +- ui/src/i18n/locales/nl.ts | 2 +- ui/src/i18n/locales/pl.ts | 2 +- ui/src/i18n/locales/pt-BR.ts | 2 +- ui/src/i18n/locales/ru.ts | 2 +- ui/src/i18n/locales/th.ts | 2 +- ui/src/i18n/locales/tr.ts | 2 +- ui/src/i18n/locales/uk.ts | 2 +- ui/src/i18n/locales/vi.ts | 2 +- ui/src/i18n/locales/zh-CN.ts | 2 +- ui/src/i18n/locales/zh-TW.ts | 2 +- ui/src/pages/chat/chat-pane.ts | 13 + ui/src/pages/chat/chat-state.ts | 1 + .../chat/components/chat-session-workspace.ts | 60 ++-- ui/src/styles/chat/sidebar.css | 65 ++-- ui/src/styles/layout.css | 320 ++++++++++++------ ui/src/styles/layout.mobile.css | 109 +++++- 55 files changed, 755 insertions(+), 548 deletions(-) delete mode 100644 ui/src/components/dashboard-header.ts diff --git a/ui/src/app/app-host.ts b/ui/src/app/app-host.ts index 48b25737bf1..a47419a4466 100644 --- a/ui/src/app/app-host.ts +++ b/ui/src/app/app-host.ts @@ -15,7 +15,7 @@ import "../components/terminal/terminal-panel.ts"; import "../components/tooltip.ts"; import "../components/update-banner.ts"; import type { SidebarNavRoute } from "../app-navigation.ts"; -import { APP_ROUTE_IDS, isRouteId, pathForRoute, type RouteId } from "../app-routes.ts"; +import { APP_ROUTE_IDS, isRouteId, type RouteId } from "../app-routes.ts"; import { COMMAND_PALETTE_TARGET_EVENT, type CommandPalette, @@ -27,8 +27,6 @@ import { copyToClipboard } from "../lib/clipboard.ts"; import { isGatewayMethodAdvertised } from "../lib/gateway-methods.ts"; import { isWorkboardEnabledInConfigSnapshot } from "../lib/plugin-activation.ts"; import { searchForSession } from "../lib/sessions/index.ts"; -import { resolveAgentIdFromSessionKey } from "../lib/sessions/session-key.ts"; -import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "../lib/string-coerce.ts"; import { renderDevicePairSetup } from "../pages/nodes/view-pairing.ts"; import { pluginTabKey, pluginTabRefFromSearch } from "../pages/plugin/route.ts"; import { bootstrapApplication, type ApplicationRuntime } from "./bootstrap.ts"; @@ -70,18 +68,6 @@ function equalShellRouteState(previous: ShellRouteState, next: ShellRouteState): ); } -function resolveAgentLabel(sessionKey: string, agentsList: AgentsListResult | null): string { - const agentId = resolveAgentIdFromSessionKey(sessionKey); - const agent = agentsList?.agents.find( - (entry) => normalizeLowercaseStringOrEmpty(entry.id) === agentId, - ); - return ( - normalizeOptionalString(agent?.identity?.name) ?? - normalizeOptionalString(agent?.name) ?? - agentId - ); -} - function resolveOnboardingMode(): boolean { const raw = new URLSearchParams(globalThis.location?.search ?? "").get("onboarding"); return raw !== null && /^(?:1|true|yes|on)$/iu.test(raw.trim()); @@ -123,10 +109,14 @@ function isTerminalAvailable( } return ( hasOperatorAdminAccess(snapshot.hello?.auth ?? null) && - isGatewayMethodAdvertised(snapshot, "terminal.open") === true + isGatewayMethodAdvertised(snapshot, "terminal.open") ); } +function isMobileNavLayout(): boolean { + return globalThis.matchMedia?.("(max-width: 1100px)").matches ?? false; +} + class OpenClawApp extends LitElement { @state() private gatewayConnected = false; @state() private gatewayReconnecting = false; @@ -376,7 +366,6 @@ class OpenClawShell extends LitElement { @state() private terminalAvailable = false; @state() private terminalClient: GatewayBrowserClient | null = null; @state() private activeSessionKey = ""; - @state() private agentLabel = ""; @state() private routeState: ShellRouteState = {}; @state() private overlaySnapshot: ApplicationOverlaySnapshot = { updateAvailable: null, @@ -396,7 +385,6 @@ class OpenClawShell extends LitElement { private navDrawerTrigger: HTMLElement | null = null; private agentsListClient: GatewayBrowserClient | null = null; private sessionKeyClient: GatewayBrowserClient | null = null; - private stopAgentsSubscription: (() => void) | undefined; private stopConfigSubscription: (() => void) | undefined; private stopGatewaySubscription: (() => void) | undefined; private stopNavigationSubscription: (() => void) | undefined; @@ -413,6 +401,7 @@ class OpenClawShell extends LitElement { super.connectedCallback(); this.startSubscriptions(); this.addEventListener(COMMAND_PALETTE_TARGET_EVENT, this.handleCommandPaletteTarget); + document.addEventListener("keydown", this.handleDocumentKeydown); } override updated() { @@ -425,7 +414,6 @@ class OpenClawShell extends LitElement { if ( !runtime || !context || - this.stopAgentsSubscription || this.stopConfigSubscription || this.stopGatewaySubscription || this.stopNavigationSubscription || @@ -443,13 +431,11 @@ class OpenClawShell extends LitElement { this.updateGatewaySessionKey(context.gateway.snapshot); this.updateGatewayStatus(context.gateway.snapshot); this.updateTerminalSurface(context.gateway.snapshot); - this.updateAgentLabel(); this.ensureRuntimeConfig(context.gateway.snapshot); this.stopGatewaySubscription = context.gateway.subscribe((snapshot) => { this.updateGatewaySessionKey(snapshot); this.updateGatewayStatus(snapshot); this.updateTerminalSurface(snapshot); - this.updateAgentLabel(); this.ensureAgentsList(snapshot); this.ensureRuntimeConfig(snapshot); }); @@ -457,9 +443,6 @@ class OpenClawShell extends LitElement { this.updateTerminalSurface(context.gateway.snapshot); }); this.stopThemeSubscription = context.theme.subscribe(() => this.requestUpdate()); - this.stopAgentsSubscription = context.agents.subscribe(() => { - this.updateAgentLabel(); - }); this.updateRouteState(selectShellRouteState(runtime.router.getState())); this.stopRouteSubscription = runtime.router.subscribeSelector( selectShellRouteState, @@ -480,8 +463,7 @@ class OpenClawShell extends LitElement { override disconnectedCallback() { this.removeEventListener(COMMAND_PALETTE_TARGET_EVENT, this.handleCommandPaletteTarget); - this.stopAgentsSubscription?.(); - this.stopAgentsSubscription = undefined; + document.removeEventListener("keydown", this.handleDocumentKeydown); this.stopConfigSubscription?.(); this.stopConfigSubscription = undefined; this.stopGatewaySubscription?.(); @@ -533,13 +515,23 @@ class OpenClawShell extends LitElement { this.context?.replace("chat", this.chatNavigationOptions()); } - private toggleNavDrawer(trigger: HTMLElement) { - if (this.navDrawerOpen) { - this.closeNavDrawer({ restoreFocus: true }); + private toggleNavigationSurface(trigger?: HTMLElement) { + const context = this.context; + if (!context || this.onboarding) { return; } - this.navDrawerTrigger = trigger; - this.navDrawerOpen = true; + if (isMobileNavLayout()) { + if (this.navDrawerOpen) { + this.closeNavDrawer({ restoreFocus: Boolean(trigger) }); + return; + } + this.navDrawerTrigger = trigger ?? null; + this.navDrawerOpen = true; + return; + } + context.navigation.update({ + navCollapsed: !this.navCollapsed, + }); } private closeNavDrawer(options: { restoreFocus?: boolean } = {}) { @@ -564,6 +556,21 @@ class OpenClawShell extends LitElement { this.closeNavDrawer({ restoreFocus: true }); }; + private readonly handleDocumentKeydown = (event: KeyboardEvent) => { + if ( + event.defaultPrevented || + event.altKey || + event.shiftKey || + !event.metaKey || + event.ctrlKey || + event.key.toLowerCase() !== "b" + ) { + return; + } + event.preventDefault(); + this.toggleNavigationSurface(); + }; + private readonly openPalette = () => { this.commandPalette?.openPalette(); }; @@ -676,21 +683,9 @@ class OpenClawShell extends LitElement { const sessionKey = new URLSearchParams(routeState.location?.search).get("session")?.trim(); if (sessionKey) { this.activeSessionKey = sessionKey; - this.updateAgentLabel(); } } - private updateAgentLabel() { - const context = this.context; - if (!context) { - return; - } - this.agentLabel = resolveAgentLabel( - this.activeSessionKey || context.gateway.snapshot.sessionKey, - context.agents.state.agentsList, - ); - } - private readonly updateNavigationPreferences = ( snapshot: ApplicationRuntime["context"]["navigation"]["snapshot"], ) => { @@ -740,23 +735,12 @@ class OpenClawShell extends LitElement { @click=${() => this.closeNavDrawer({ restoreFocus: true })} > - window.dispatchEvent(new CustomEvent("openclaw:terminal-toggle"))} - .onToggleDrawer=${(trigger: HTMLElement) => this.toggleNavDrawer(trigger)} - .onToggleCollapse=${() => - context.navigation.update({ - navCollapsed: !navCollapsed, - })} - .onNavigate=${(routeId: string, options?: ApplicationNavigationOptions) => - this.navigate(routeId, options)} + .onOpenPalette=${this.openPalette} + .onToggleDrawer=${(trigger: HTMLElement) => this.toggleNavigationSurface(trigger)} >
this.toggleNavigationSurface()} .onToggleMore=${() => context.navigation.update({ sidebarMoreExpanded: !context.navigation.snapshot.sidebarMoreExpanded, diff --git a/ui/src/components/app-sidebar.ts b/ui/src/components/app-sidebar.ts index 380a72e9081..c6104c41a39 100644 --- a/ui/src/components/app-sidebar.ts +++ b/ui/src/components/app-sidebar.ts @@ -70,6 +70,7 @@ type SidebarRecentSession = { meta: string; href: string; active: boolean; + visuallyActive: boolean; hasActiveRun: boolean; kind?: string; pinned: boolean; @@ -113,6 +114,14 @@ const SIDEBAR_SESSION_SORT_OPTIONS = [ labelKey: "chat.sidebar.sortCreated" | "chat.sidebar.sortUpdated"; }>; +function formatSidebarTimestamp(timestampMs: number | null | undefined): string { + const value = formatRelativeTimestamp(timestampMs, { fallback: "" }); + if (value === "just now") { + return "now"; + } + return value.endsWith(" ago") ? value.slice(0, -" ago".length) : value; +} + function shouldHandleNavigationClick(event: MouseEvent): boolean { return ( !event.defaultPrevented && @@ -142,6 +151,7 @@ class AppSidebar extends LitElement { @property({ attribute: false }) sidebarMoreExpanded = false; @property({ attribute: false }) themeMode: ThemeMode = "system"; @property({ attribute: false }) onOpenPalette?: () => void; + @property({ attribute: false }) onToggleSidebar?: () => void; @property({ attribute: false }) onToggleMore?: () => void; @property({ attribute: false }) onUpdatePinnedRoutes?: (routes: SidebarNavRoute[]) => void; @property({ attribute: false }) onPairMobile?: () => void; @@ -278,6 +288,37 @@ class AppSidebar extends LitElement { this.gatewayClient = client; } + private renderBrand() { + const collapseLabel = this.collapsed ? t("nav.expand") : t("nav.collapse"); + const collapseTooltip = `${collapseLabel} (⌘B)`; + return html` + + `; + } + private getRouteSessionKey(): string { return this.sessionKey.trim() || this.context?.gateway.snapshot.sessionKey.trim() || ""; } @@ -321,12 +362,14 @@ class AppSidebar extends LitElement { hello: context?.gateway.snapshot.hello, compareSessions: this.compareSidebarSessionRows, }); + const highlightCurrentSession = this.activeRouteId === "chat"; const toSidebarSession = (row: SessionsListResult["sessions"][number]) => ({ key: row.key, label: resolveSessionDisplayName(row.key, row), - meta: row.updatedAt ? formatRelativeTimestamp(row.updatedAt) : "", + meta: formatSidebarTimestamp(row.updatedAt), href: `${pathForRoute("chat", context?.basePath ?? "")}${searchForSession(row.key)}`, active: row.key === navigation.activeRowKey, + visuallyActive: highlightCurrentSession && row.key === navigation.currentSessionKey, hasActiveRun: Boolean(row.hasActiveRun), kind: row.kind, pinned: row.pinned === true, @@ -1211,7 +1254,7 @@ class AppSidebar extends LitElement { const rowClass = [ "sidebar-recent-session", "session-row-host", - session.active ? "sidebar-recent-session--active" : "", + session.visuallyActive ? "sidebar-recent-session--active" : "", session.pinned ? "session-row-host--pinned" : "", session.hasActiveRun ? "session-row-host--running" : "", this.draggingSessionKey === session.key ? "sidebar-recent-session--dragging" : "", @@ -1454,7 +1497,7 @@ class AppSidebar extends LitElement { @click=${(event: MouseEvent) => { const trigger = event.currentTarget as HTMLElement; const rect = trigger.getBoundingClientRect(); - this.openSessionSortMenu(rect.right - 180, rect.bottom + 4, trigger); + this.openSessionSortMenu(rect.right, rect.bottom + 4, trigger); }} > ${icons.listFilter} @@ -1621,6 +1664,7 @@ class AppSidebar extends LitElement { alt="OpenClaw" />