From 5fe8cde28f842cc9a6ffc4dc7581f02ce2ac512b Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Mon, 4 May 2026 02:09:52 -0500 Subject: [PATCH] feat(ui): show active agent in dashboard header Show the active agent name in the Control UI dashboard breadcrumb without adding the current session key/name. Verification: - pnpm test ui/src/ui/app-render.helpers.node.test.ts - node scripts/run-oxlint.mjs --tsconfig config/tsconfig/oxlint.core.json ui/src/ui/components/dashboard-header.ts ui/src/ui/app-render.helpers.ts ui/src/ui/app-render.ts ui/src/ui/app-render.helpers.node.test.ts - git diff --check - Testbox pnpm check:changed --- CHANGELOG.md | 1 + ui/src/styles/layout.css | 22 +++++++++++ ui/src/ui/app-render.helpers.node.test.ts | 45 +++++++++++++++++++++++ ui/src/ui/app-render.helpers.ts | 16 +++++++- ui/src/ui/app-render.ts | 3 ++ ui/src/ui/components/dashboard-header.ts | 14 ++++++- 6 files changed, 99 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index afd8ba329b8..d85b4908cf1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Docs: https://docs.openclaw.ai ### Changes +- Control UI/header: show the active agent name in dashboard breadcrumbs without adding the current session key, keeping non-chat views oriented without crowding the topbar. - Gateway/startup: keep model-catalog test helpers, run-session lookup code, QR pairing helpers, and TypeBox memory-tool schema construction out of hot startup import paths, reducing default gateway benchmark plugin-load and memory pressure. - Control UI/performance: record browser long animation frame or long task entries in the debug event log when supported, making slow dashboard renders easier to attribute from the UI. - Channels/streaming: add unified `streaming.mode: "progress"` drafts with auto single-word status labels and shared progress configuration across Discord, Telegram, Matrix, Slack, and Microsoft Teams. diff --git a/ui/src/styles/layout.css b/ui/src/styles/layout.css index 069c72a578c..af70a5bd88d 100644 --- a/ui/src/styles/layout.css +++ b/ui/src/styles/layout.css @@ -196,6 +196,13 @@ color: var(--muted); } +.topnav-shell .dashboard-header__breadcrumb-segment { + display: inline-flex; + align-items: center; + gap: 8px; + min-width: 0; +} + .topnav-shell .dashboard-header__breadcrumb-link { display: inline-flex; align-items: center; @@ -221,6 +228,21 @@ text-overflow: ellipsis; } +.topnav-shell .dashboard-header__breadcrumb-context { + max-width: 18ch; + overflow: hidden; + color: var(--muted); + font-weight: 550; + text-overflow: ellipsis; + white-space: nowrap; +} + +@media (max-width: 720px) { + .topnav-shell .dashboard-header__breadcrumb-context { + max-width: 10ch; + } +} + .topbar-status { display: flex; align-items: center; diff --git a/ui/src/ui/app-render.helpers.node.test.ts b/ui/src/ui/app-render.helpers.node.test.ts index 3771efb71d9..e4c6334f39c 100644 --- a/ui/src/ui/app-render.helpers.node.test.ts +++ b/ui/src/ui/app-render.helpers.node.test.ts @@ -39,6 +39,7 @@ import { isCronSessionKey, parseSessionKey, resolveAssistantAttachmentAuthToken, + resolveDashboardHeaderContext, resolveSessionOptionGroups, resolveSessionDisplayName, switchChatSession, @@ -452,6 +453,50 @@ describe("resolveSessionDisplayName", () => { }); }); +describe("resolveDashboardHeaderContext", () => { + it("uses the active agent identity name", () => { + expect( + resolveDashboardHeaderContext({ + sessionKey: "agent:deep-chat:imessage:sample-thread", + agentsList: { + defaultId: "deep-chat", + mainKey: "main", + scope: "user", + agents: [{ id: "deep-chat", identity: { name: "Deep Chat" } }], + }, + } as unknown as AppViewState), + ).toEqual({ agentLabel: "Deep Chat" }); + }); + + it("falls back to the configured agent name", () => { + expect( + resolveDashboardHeaderContext({ + sessionKey: "agent:beta:main", + agentsList: { + defaultId: "beta", + mainKey: "main", + scope: "user", + agents: [{ id: "beta", name: "Coding" }], + }, + } as unknown as AppViewState), + ).toEqual({ agentLabel: "Coding" }); + }); + + it("falls back to the agent id", () => { + expect( + resolveDashboardHeaderContext({ + sessionKey: "agent:beta:subagent:maintainer-v2", + agentsList: { + defaultId: "main", + mainKey: "main", + scope: "user", + agents: [], + }, + } as unknown as AppViewState), + ).toEqual({ agentLabel: "beta" }); + }); +}); + describe("isCronSessionKey", () => { it("returns true for cron: prefixed keys", () => { expect(isCronSessionKey("cron:abc-123")).toBe(true); diff --git a/ui/src/ui/app-render.helpers.ts b/ui/src/ui/app-render.helpers.ts index 9c6eb4d1ca2..0178ea06866 100644 --- a/ui/src/ui/app-render.helpers.ts +++ b/ui/src/ui/app-render.helpers.ts @@ -18,7 +18,7 @@ import { createSessionAndRefresh, loadSessions } from "./controllers/sessions.ts import { icons } from "./icons.ts"; import { iconForTab, pathForTab, titleForTab, type Tab } from "./navigation.ts"; import { parseAgentSessionKey, resolveAgentIdFromSessionKey } from "./session-key.ts"; -import { normalizeOptionalString } from "./string-coerce.ts"; +import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "./string-coerce.ts"; import type { ThemeMode } from "./theme.ts"; import type { SessionsListResult } from "./types.ts"; import type { ChatQueueItem } from "./ui-types.ts"; @@ -52,6 +52,20 @@ export function resolveAssistantAttachmentAuthToken( return resolveControlUiAuthToken(state); } +export function resolveDashboardHeaderContext( + state: Pick, +): { agentLabel: string } { + const agentId = resolveAgentIdFromSessionKey(state.sessionKey); + const agent = state.agentsList?.agents.find( + (entry) => normalizeLowercaseStringOrEmpty(entry.id) === agentId, + ); + const agentLabel = + normalizeOptionalString(agent?.identity?.name) ?? + normalizeOptionalString(agent?.name) ?? + agentId; + return { agentLabel }; +} + function resolveSidebarChatSessionKey(state: AppViewState): string { const snapshot = state.hello?.snapshot as | { sessionDefaults?: SessionDefaultsSnapshot } diff --git a/ui/src/ui/app-render.ts b/ui/src/ui/app-render.ts index ec00df6e5b4..08fe9d26e06 100644 --- a/ui/src/ui/app-render.ts +++ b/ui/src/ui/app-render.ts @@ -11,6 +11,7 @@ import { renderChatSessionSelect, renderTab, resolveAssistantAttachmentAuthToken, + resolveDashboardHeaderContext, renderSidebarConnectionStatus, renderTopbarThemeModeToggle, createChatSession, @@ -645,6 +646,7 @@ export function renderApp(state: AppViewState) { const chatFocus = isChat && (state.settings.chatFocusMode || state.onboarding); const navDrawerOpen = state.navDrawerOpen && !chatFocus && !state.onboarding; const navCollapsed = state.settings.navCollapsed && !navDrawerOpen; + const dashboardHeaderContext = resolveDashboardHeaderContext(state); const showThinking = state.onboarding ? false : state.settings.chatShowThinking; const showToolCalls = state.onboarding ? true : state.settings.chatShowToolCalls; const localAssistantAvatarOverride = @@ -1362,6 +1364,7 @@ export function renderApp(state: AppViewState) { ) => { state.setTab(event.detail); }} diff --git a/ui/src/ui/components/dashboard-header.ts b/ui/src/ui/components/dashboard-header.ts index bbbc05f8bf6..130cbf65252 100644 --- a/ui/src/ui/components/dashboard-header.ts +++ b/ui/src/ui/components/dashboard-header.ts @@ -1,4 +1,4 @@ -import { LitElement, html } from "lit"; +import { LitElement, html, nothing } from "lit"; import { property } from "lit/decorators.js"; import { pathForTab, titleForTab, type Tab } from "../navigation.js"; @@ -9,6 +9,7 @@ export class DashboardHeader extends LitElement { @property() tab: Tab = "overview"; @property() basePath = ""; + @property() agentLabel = ""; private readonly handleOverviewClick = (event: MouseEvent) => { if ( @@ -29,6 +30,7 @@ export class DashboardHeader extends LitElement { override render() { const label = titleForTab(this.tab); + const agentLabel = this.agentLabel.trim(); return html`
@@ -40,6 +42,16 @@ export class DashboardHeader extends LitElement { > OpenClaw + ${agentLabel + ? html` + + + + ${agentLabel} + + + ` + : nothing} ${label}