Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -71,14 +67,10 @@ function normalizeLayoutProps<T extends object>(
* 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 <DashboardRoutePageBody {...props} />;
}
return (
<PageHeaderProvider>
<EnsurePageHeaderProvider>
<DashboardRoutePageBody {...props} />
</PageHeaderProvider>
</EnsurePageHeaderProvider>
);
}

Expand Down
5 changes: 4 additions & 1 deletion packages/ui/src/cloud-ui/components/layout/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -47,3 +47,30 @@ export function PageHeaderProvider({ children }: { children: ReactNode }) {
</PageHeaderContext.Provider>
);
}

/**
* 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 <PageHeaderProvider>{children}</PageHeaderProvider>;
}
13 changes: 8 additions & 5 deletions packages/ui/src/cloud/analytics/Page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 (
<PageHeaderProvider>
<EnsurePageHeaderProvider>
<AnalyticsPageClient
data={breakdown.data}
projectionsData={projections.data}
/>
</PageHeaderProvider>
</EnsurePageHeaderProvider>
);
}
16 changes: 4 additions & 12 deletions packages/ui/src/cloud/instances/AgentsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,18 +74,10 @@ export default function AgentsPage() {
return (
<ElizaAgentsPageWrapper>
<DashboardPageContainer className="space-y-6">
<div className="space-y-1.5">
<div className="flex items-center gap-2">
<span className="inline-block size-2 bg-[var(--accent)]" />
<p className="font-mono text-[11px] uppercase tracking-[0.32em] text-white/60">
{t("cloud.agents.eyebrow", { defaultValue: "Instances" })}
</p>
</div>
<h1 className="text-xl font-semibold text-white md:text-2xl">
{t("cloud.agents.title", { defaultValue: "Instances" })}
</h1>
</div>

{/* 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. */}
<ElizaAgentPricingBanner
runningCount={runningCount}
idleCount={idleCount}
Expand Down
13 changes: 8 additions & 5 deletions packages/ui/src/cloud/instances/MyAgentsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import {
DashboardLoadingState,
PageHeaderProvider,
EnsurePageHeaderProvider,
} from "@elizaos/ui/cloud-ui";
import { useDocumentTitle } from "../lib/use-document-title";
import { useRequireAuth } from "../lib/use-session-auth";
Expand All @@ -28,11 +28,14 @@ export default function MyAgentsPage() {
);
}

// MyAgentsClient sets the page header; this standalone route has no ancestor
// PageHeaderProvider, so supply one here.
// MyAgentsClient sets the page header. When this route renders inside the
// ConsoleShell, its provider already exists and drives the top-bar title;
// EnsurePageHeaderProvider defers to it (and supplies one only for the
// standalone/native mount) so the title reaches the shell header instead of
// a shadowed inner provider.
return (
<PageHeaderProvider>
<EnsurePageHeaderProvider>
<MyAgentsClient />
</PageHeaderProvider>
</EnsurePageHeaderProvider>
);
}
8 changes: 6 additions & 2 deletions packages/ui/src/cloud/instances/components/my-agents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,11 +117,15 @@ function AgentConsoleOverview({
})}
</p>
<div className="space-y-2">
<h1 className="text-2xl font-semibold text-white md:text-3xl">
{/* 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. */}
<h2 className="text-2xl font-semibold text-white md:text-3xl">
{t("cloud.myAgents.heading", {
defaultValue: "Administer and enter your running agent",
})}
</h1>
</h2>
<p className="text-sm leading-6 text-white/60">
{t("cloud.myAgents.subheading", {
defaultValue:
Expand Down
59 changes: 58 additions & 1 deletion packages/ui/src/cloud/shell/ConsoleShell.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand All @@ -48,6 +51,26 @@ function TitledPage() {
return <div data-testid="page-body">body</div>;
}

/**
* 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 (
<EnsurePageHeaderProvider>
<TitledInner />
</EnsurePageHeaderProvider>
);
}

function TitledInner() {
useSetPageHeader({ title: "Standalone QA" });
return <div data-testid="page-body">body</div>;
}

describe("ConsoleShell", () => {
afterEach(cleanup);

Expand All @@ -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(
<MemoryRouter initialEntries={["/dashboard/my-agents"]}>
<ConsoleShell>
<StandaloneTitledPage />
</ConsoleShell>
</MemoryRouter>,
);

// 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(
<MemoryRouter initialEntries={["/dashboard"]}>
<ConsoleShell>
<TitledPage />
</ConsoleShell>
</MemoryRouter>,
);

// 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");
});
});
5 changes: 4 additions & 1 deletion packages/ui/src/cloud/shell/ConsoleShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 0 additions & 2 deletions packages/ui/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 0 additions & 2 deletions packages/ui/src/i18n/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 0 additions & 2 deletions packages/ui/src/i18n/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "集計",
Expand Down
2 changes: 0 additions & 2 deletions packages/ui/src/i18n/locales/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "집계",
Expand Down
2 changes: 0 additions & 2 deletions packages/ui/src/i18n/locales/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 0 additions & 2 deletions packages/ui/src/i18n/locales/tl.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 0 additions & 2 deletions packages/ui/src/i18n/locales/vi.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 0 additions & 2 deletions packages/ui/src/i18n/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "聚合",
Expand Down
Loading