diff --git a/packages/ui/src/cloud-ui/components/layout/dashboard-route-page.tsx b/packages/ui/src/cloud-ui/components/layout/dashboard-route-page.tsx index 1a93f9bbc16d4..180e412c6575e 100644 --- a/packages/ui/src/cloud-ui/components/layout/dashboard-route-page.tsx +++ b/packages/ui/src/cloud-ui/components/layout/dashboard-route-page.tsx @@ -3,19 +3,15 @@ /** * A dashboard route page that wires its header into the page-header context. */ -import { - type ComponentPropsWithoutRef, - type DependencyList, - type ReactNode, - useContext, +import type { + ComponentPropsWithoutRef, + DependencyList, + ReactNode, } from "react"; import { cn } from "../../lib/utils"; import { DashboardPageContainer, DashboardPageStack } from "./dashboard-page"; -import { PageHeaderProvider } from "./page-header-context"; -import { - PageHeaderContext, - useSetPageHeader, -} from "./page-header-context.hooks"; +import { EnsurePageHeaderProvider } from "./page-header-context"; +import { useSetPageHeader } from "./page-header-context.hooks"; type DashboardRoutePageBannerTone = "info" | "success" | "warning" | "error"; @@ -71,14 +67,10 @@ function normalizeLayoutProps( * chrome), it defers to that provider so the header stays visible to the chrome. */ export function DashboardRoutePage(props: DashboardRoutePageProps) { - const hasAncestorProvider = useContext(PageHeaderContext) !== undefined; - if (hasAncestorProvider) { - return ; - } return ( - + - + ); } diff --git a/packages/ui/src/cloud-ui/components/layout/index.ts b/packages/ui/src/cloud-ui/components/layout/index.ts index 0fe4e496258f3..2c8d4e911ac82 100644 --- a/packages/ui/src/cloud-ui/components/layout/index.ts +++ b/packages/ui/src/cloud-ui/components/layout/index.ts @@ -41,7 +41,10 @@ export type { DashboardSidebarLinkRenderProps, DashboardSidebarSection, } from "./dashboard-sidebar-types"; -export { PageHeaderProvider } from "./page-header-context"; +export { + EnsurePageHeaderProvider, + PageHeaderProvider, +} from "./page-header-context"; export { usePageHeader, useSetPageHeader, diff --git a/packages/ui/src/cloud-ui/components/layout/page-header-context.tsx b/packages/ui/src/cloud-ui/components/layout/page-header-context.tsx index ec04ab8875e5e..bc7afc040a886 100644 --- a/packages/ui/src/cloud-ui/components/layout/page-header-context.tsx +++ b/packages/ui/src/cloud-ui/components/layout/page-header-context.tsx @@ -7,7 +7,7 @@ "use client"; -import { type ReactNode, useMemo, useState } from "react"; +import { type ReactNode, useContext, useMemo, useState } from "react"; import { PageHeaderContext, type PageHeaderInfo, @@ -47,3 +47,30 @@ export function PageHeaderProvider({ children }: { children: ReactNode }) { ); } + +/** + * Provide a page-header context ONLY when there isn't one already. + * + * Standalone cloud routes need their own {@link PageHeaderProvider} (mounted + * directly by `CloudRouterShell` / natively in the app, they have no ancestor + * provider and `useSetPageHeader` would throw). But the SAME routes also render + * inside `ConsoleShell`, which already provides one and reads it to draw the + * top-bar title. An unconditional inner provider SHADOWS the shell's, so + * `useSetPageHeader` writes to a dead context and the top bar shows no title + * (and any in-page heading then reads as a second, competing title). + * + * Wrapping a route body in this component defers to the shell's provider when + * present, and supplies its own otherwise — so the title always reaches + * whichever header is actually rendered. + */ +export function EnsurePageHeaderProvider({ + children, +}: { + children: ReactNode; +}) { + const hasAncestorProvider = useContext(PageHeaderContext) !== undefined; + if (hasAncestorProvider) { + return <>{children}; + } + return {children}; +} diff --git a/packages/ui/src/cloud/analytics/Page.tsx b/packages/ui/src/cloud/analytics/Page.tsx index d7af68f841bc5..8852cdbb09277 100644 --- a/packages/ui/src/cloud/analytics/Page.tsx +++ b/packages/ui/src/cloud/analytics/Page.tsx @@ -16,7 +16,7 @@ import { useSearchParams } from "react-router-dom"; import { DashboardErrorState, DashboardLoadingState, - PageHeaderProvider, + EnsurePageHeaderProvider, } from "../../cloud-ui"; import { useCloudT } from "../shell/CloudI18nProvider"; import { AnalyticsPageClient } from "./_components/analytics-page-client"; @@ -67,14 +67,17 @@ export default function AnalyticsPage() { ); } - // AnalyticsPageClient sets the page header; this standalone route has no - // ancestor PageHeaderProvider, so supply one here. + // AnalyticsPageClient sets the page header. Inside the ConsoleShell its + // provider already exists and drives the top-bar title; + // EnsurePageHeaderProvider defers to it (supplying one only for the + // standalone/native mount) so the title reaches the shell header rather than + // a shadowed inner provider. return ( - + - + ); } diff --git a/packages/ui/src/cloud/instances/AgentsPage.tsx b/packages/ui/src/cloud/instances/AgentsPage.tsx index f7be2f1c026f2..1ce5eeaf0c11c 100644 --- a/packages/ui/src/cloud/instances/AgentsPage.tsx +++ b/packages/ui/src/cloud/instances/AgentsPage.tsx @@ -74,18 +74,10 @@ export default function AgentsPage() { return ( -
-
- -

- {t("cloud.agents.eyebrow", { defaultValue: "Instances" })} -

-
-

- {t("cloud.agents.title", { defaultValue: "Instances" })} -

-
- + {/* Page title is surfaced in the console top bar by + ElizaAgentsPageWrapper (DashboardRoutePage title="Instances" → + useSetPageHeader). No inline page-level heading here — a second + "Instances" title under the top bar read as a double title. */} + - + ); } diff --git a/packages/ui/src/cloud/instances/components/my-agents.tsx b/packages/ui/src/cloud/instances/components/my-agents.tsx index 841d2db0db422..7bfabe4602555 100644 --- a/packages/ui/src/cloud/instances/components/my-agents.tsx +++ b/packages/ui/src/cloud/instances/components/my-agents.tsx @@ -117,11 +117,15 @@ function AgentConsoleOverview({ })}

-

+ {/* Console hero heading. Demoted from h1 to h2: the page-level + title ("My Agent") is owned by the console top bar via + useSetPageHeader below, so a second h1 here read as a double + title. This stays the in-page section heading. */} +

{t("cloud.myAgents.heading", { defaultValue: "Administer and enter your running agent", })} -

+

{t("cloud.myAgents.subheading", { defaultValue: diff --git a/packages/ui/src/cloud/shell/ConsoleShell.test.tsx b/packages/ui/src/cloud/shell/ConsoleShell.test.tsx index 6d94d30da0c65..952e0e777e094 100644 --- a/packages/ui/src/cloud/shell/ConsoleShell.test.tsx +++ b/packages/ui/src/cloud/shell/ConsoleShell.test.tsx @@ -23,7 +23,10 @@ vi.mock("../lib/use-session-auth", () => ({ }), })); -import { useSetPageHeader } from "../../cloud-ui/components/layout"; +import { + EnsurePageHeaderProvider, + useSetPageHeader, +} from "../../cloud-ui/components/layout"; import { ConsoleShell } from "./ConsoleShell"; const NAV_HREFS = [ @@ -48,6 +51,26 @@ function TitledPage() { return

body
; } +/** + * A standalone-route body: it publishes its own header inside + * EnsurePageHeaderProvider (the pattern MyAgentsPage / AnalyticsPage use so + * they also work mounted directly by CloudRouterShell). Inside ConsoleShell the + * provider must DEFER to the shell so the title reaches the top bar rather than + * a shadowed inner provider. + */ +function StandaloneTitledPage() { + return ( + + + + ); +} + +function TitledInner() { + useSetPageHeader({ title: "Standalone QA" }); + return
body
; +} + describe("ConsoleShell", () => { afterEach(cleanup); @@ -73,4 +96,38 @@ describe("ConsoleShell", () => { expect(hrefs.has(href), `missing sidebar link ${href}`).toBe(true); } }); + + it("surfaces a standalone route's title in the top bar (EnsurePageHeaderProvider defers to the shell, no shadowed provider)", () => { + render( + + + + + , + ); + + // The page's own EnsurePageHeaderProvider defers to the shell provider, so + // useSetPageHeader writes to the context the top bar reads. Exactly one + // page-level heading with the title — no shadowed/dead inner provider. + expect(screen.getByRole("heading", { name: "Standalone QA" })).toBeTruthy(); + }); + + it("names the account-plumbing section 'Workspace', not 'Account' (no section title duplicating an item label)", () => { + render( + + + + + , + ); + + // The section that holds Connectors/Account/Security/Organization is + // titled "Workspace" so it doesn't repeat the "Account" item label below + // it (the sidebar half of the double-title fix). + expect(screen.getByText("Workspace")).toBeTruthy(); + // The "Account" nav item itself is unchanged. + expect( + screen.getByRole("link", { name: /Account/i }).getAttribute("href"), + ).toBe("/dashboard/account"); + }); }); diff --git a/packages/ui/src/cloud/shell/ConsoleShell.tsx b/packages/ui/src/cloud/shell/ConsoleShell.tsx index 9dac155334943..0f540deeb5163 100644 --- a/packages/ui/src/cloud/shell/ConsoleShell.tsx +++ b/packages/ui/src/cloud/shell/ConsoleShell.tsx @@ -111,7 +111,10 @@ const CONSOLE_NAV_SECTIONS: DashboardSidebarSection[] = [ ], }, { - title: "Account", + // Section title was "Account", which duplicated the "Account" item label + // right below it (double-title feel in the sidebar). "Workspace" covers + // the whole group — connectors, account, security, and organization. + title: "Workspace", items: [ { id: "connectors", diff --git a/packages/ui/src/i18n/locales/en.json b/packages/ui/src/i18n/locales/en.json index 4d5a5017b8a2c..c10d196c4b645 100644 --- a/packages/ui/src/i18n/locales/en.json +++ b/packages/ui/src/i18n/locales/en.json @@ -785,11 +785,9 @@ "cloud.agents.detail.vpnIp": "VPN IP", "cloud.agents.detail.webUi": "Web UI", "cloud.agents.detail.webUiPort": "Web UI Port", - "cloud.agents.eyebrow": "Instances", "cloud.agents.loading": "Loading instances", "cloud.agents.metaDescription": "View, launch, and manage your instances backed by Eliza Cloud.", "cloud.agents.metaTitle": "Instances", - "cloud.agents.title": "Instances", "cloud.analytics.custom": "Custom", "cloud.analytics.dataPointsCount": "{{n}} data points", "cloud.analytics.filters.aggregation": "Aggregation", diff --git a/packages/ui/src/i18n/locales/es.json b/packages/ui/src/i18n/locales/es.json index a73ca902b8829..9ed8f5cabfb0b 100644 --- a/packages/ui/src/i18n/locales/es.json +++ b/packages/ui/src/i18n/locales/es.json @@ -778,11 +778,9 @@ "cloud.agents.detail.vpnIp": "IP VPN", "cloud.agents.detail.webUi": "Web UI", "cloud.agents.detail.webUiPort": "Puerto Web UI", - "cloud.agents.eyebrow": "Instancias", "cloud.agents.loading": "Cargando instancias", "cloud.agents.metaDescription": "Mira, lanza y gestiona tus instancias respaldadas por Eliza Cloud.", "cloud.agents.metaTitle": "Instancias", - "cloud.agents.title": "Instancias", "cloud.analytics.custom": "Personalizado", "cloud.analytics.dataPointsCount": "{{n}} puntos de datos", "cloud.analytics.filters.aggregation": "Agregación", diff --git a/packages/ui/src/i18n/locales/ja.json b/packages/ui/src/i18n/locales/ja.json index 6d10dba82ffa1..95e0fc546ce98 100644 --- a/packages/ui/src/i18n/locales/ja.json +++ b/packages/ui/src/i18n/locales/ja.json @@ -778,11 +778,9 @@ "cloud.agents.detail.vpnIp": "VPN IP", "cloud.agents.detail.webUi": "Web UI", "cloud.agents.detail.webUiPort": "Web UIポート", - "cloud.agents.eyebrow": "インスタンス", "cloud.agents.loading": "インスタンスを読み込み中", "cloud.agents.metaDescription": "Eliza Cloudで動くインスタンスの確認・起動・管理。", "cloud.agents.metaTitle": "インスタンス", - "cloud.agents.title": "インスタンス", "cloud.analytics.custom": "カスタム", "cloud.analytics.dataPointsCount": "{{n}}件のデータポイント", "cloud.analytics.filters.aggregation": "集計", diff --git a/packages/ui/src/i18n/locales/ko.json b/packages/ui/src/i18n/locales/ko.json index b061d8751b63f..249cffbc65721 100644 --- a/packages/ui/src/i18n/locales/ko.json +++ b/packages/ui/src/i18n/locales/ko.json @@ -778,11 +778,9 @@ "cloud.agents.detail.vpnIp": "VPN IP", "cloud.agents.detail.webUi": "웹 UI", "cloud.agents.detail.webUiPort": "Web UI 포트", - "cloud.agents.eyebrow": "인스턴스", "cloud.agents.loading": "인스턴스 불러오는 중", "cloud.agents.metaDescription": "Eliza Cloud로 구동되는 인스턴스를 보고, 실행하고, 관리하세요.", "cloud.agents.metaTitle": "인스턴스", - "cloud.agents.title": "인스턴스", "cloud.analytics.custom": "사용자 정의", "cloud.analytics.dataPointsCount": "데이터 포인트 {{n}}개", "cloud.analytics.filters.aggregation": "집계", diff --git a/packages/ui/src/i18n/locales/pt.json b/packages/ui/src/i18n/locales/pt.json index 6401d561ca86b..57792494183d3 100644 --- a/packages/ui/src/i18n/locales/pt.json +++ b/packages/ui/src/i18n/locales/pt.json @@ -778,11 +778,9 @@ "cloud.agents.detail.vpnIp": "IP VPN", "cloud.agents.detail.webUi": "Interface Web", "cloud.agents.detail.webUiPort": "Porta da Interface Web", - "cloud.agents.eyebrow": "Instâncias", "cloud.agents.loading": "Carregando instâncias", "cloud.agents.metaDescription": "Visualize, inicie e gerencie suas instâncias com suporte do Eliza Cloud.", "cloud.agents.metaTitle": "Instâncias", - "cloud.agents.title": "Instâncias", "cloud.analytics.custom": "Personalizado", "cloud.analytics.dataPointsCount": "{{n}} pontos de dados", "cloud.analytics.filters.aggregation": "Agregação", diff --git a/packages/ui/src/i18n/locales/tl.json b/packages/ui/src/i18n/locales/tl.json index e1eaa8b35bb9c..239d57ee3efde 100644 --- a/packages/ui/src/i18n/locales/tl.json +++ b/packages/ui/src/i18n/locales/tl.json @@ -778,11 +778,9 @@ "cloud.agents.detail.vpnIp": "VPN IP", "cloud.agents.detail.webUi": "Web UI", "cloud.agents.detail.webUiPort": "Port ng Web UI", - "cloud.agents.eyebrow": "Mga instance", "cloud.agents.loading": "Naglo-load ng instances", "cloud.agents.metaDescription": "Tingnan, ilunsad, at i-manage ang instances mo sa Eliza Cloud.", "cloud.agents.metaTitle": "Mga instance", - "cloud.agents.title": "Mga instance", "cloud.analytics.custom": "Pasadya", "cloud.analytics.dataPointsCount": "{{n}} mga punto ng datos", "cloud.analytics.filters.aggregation": "Aggregasyon", diff --git a/packages/ui/src/i18n/locales/vi.json b/packages/ui/src/i18n/locales/vi.json index f31b955c6b055..54f6b1c917a49 100644 --- a/packages/ui/src/i18n/locales/vi.json +++ b/packages/ui/src/i18n/locales/vi.json @@ -778,11 +778,9 @@ "cloud.agents.detail.vpnIp": "IP VPN", "cloud.agents.detail.webUi": "Giao diện web", "cloud.agents.detail.webUiPort": "Cổng giao diện web", - "cloud.agents.eyebrow": "Phiên bản", "cloud.agents.loading": "Đang tải instances", "cloud.agents.metaDescription": "Xem, khởi chạy và quản lý instances của bạn trên Eliza Cloud.", "cloud.agents.metaTitle": "Phiên bản", - "cloud.agents.title": "Phiên bản", "cloud.analytics.custom": "Tùy chỉnh", "cloud.analytics.dataPointsCount": "{{n}} điểm dữ liệu", "cloud.analytics.filters.aggregation": "Tổng hợp", diff --git a/packages/ui/src/i18n/locales/zh-CN.json b/packages/ui/src/i18n/locales/zh-CN.json index 63bf7faa00abf..e67df7133625b 100644 --- a/packages/ui/src/i18n/locales/zh-CN.json +++ b/packages/ui/src/i18n/locales/zh-CN.json @@ -778,11 +778,9 @@ "cloud.agents.detail.vpnIp": "VPN IP", "cloud.agents.detail.webUi": "Web UI", "cloud.agents.detail.webUiPort": "Web UI 端口", - "cloud.agents.eyebrow": "实例", "cloud.agents.loading": "正在加载实例", "cloud.agents.metaDescription": "查看、启动和管理由 Eliza Cloud 支持的实例。", "cloud.agents.metaTitle": "实例", - "cloud.agents.title": "实例", "cloud.analytics.custom": "自定义", "cloud.analytics.dataPointsCount": "{{n}} 个数据点", "cloud.analytics.filters.aggregation": "聚合",