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
This commit is contained in:
Val Alexander
2026-05-04 02:09:52 -05:00
committed by GitHub
parent 3c971255fa
commit 5fe8cde28f
6 changed files with 99 additions and 2 deletions

View File

@@ -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.

View File

@@ -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;

View File

@@ -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);

View File

@@ -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<AppViewState, "agentsList" | "sessionKey">,
): { 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 }

View File

@@ -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) {
<dashboard-header
.tab=${state.tab}
.basePath=${state.basePath}
.agentLabel=${dashboardHeaderContext.agentLabel}
@navigate=${(event: CustomEvent<Tab>) => {
state.setTab(event.detail);
}}

View File

@@ -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`
<div class="dashboard-header">
@@ -40,6 +42,16 @@ export class DashboardHeader extends LitElement {
>
OpenClaw
</a>
${agentLabel
? html`
<span class="dashboard-header__breadcrumb-segment">
<span class="dashboard-header__breadcrumb-sep"></span>
<span class="dashboard-header__breadcrumb-context" title=${agentLabel}>
${agentLabel}
</span>
</span>
`
: nothing}
<span class="dashboard-header__breadcrumb-sep"></span>
<span class="dashboard-header__breadcrumb-current">${label}</span>
</div>