Skip to content

Commit 3aa2fdb

Browse files
authored
fix(dashboard): use registry project display names
Bead: av-4yd
1 parent 6036b68 commit 3aa2fdb

10 files changed

Lines changed: 97 additions & 19 deletions

File tree

apps/dashboard/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
"scripts": {
77
"dev": "vite",
88
"build": "tsc -b && vite build",
9-
"preview": "vite preview"
9+
"preview": "vite preview",
10+
"test": "bun test"
1011
},
1112
"dependencies": {
1213
"@monaco-editor/react": "^4.7.0",

apps/dashboard/src/components/Breadcrumbs.tsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
runPath,
1818
suitePath,
1919
} from '~/lib/navigation';
20+
import { type ProjectDisplayEntry, resolveProjectDisplayName } from '~/lib/project-display-name';
2021

2122
interface BreadcrumbSegment {
2223
label: string;
@@ -33,7 +34,7 @@ function formatRunLabel(runId: string | undefined): string {
3334

3435
function deriveSegments(
3536
matches: ReturnType<typeof useMatches>,
36-
projectNames: ReadonlyMap<string, string> = new Map(),
37+
projects: readonly ProjectDisplayEntry[] = [],
3738
): BreadcrumbSegment[] {
3839
const segments: BreadcrumbSegment[] = [];
3940

@@ -46,7 +47,7 @@ function deriveSegments(
4647
if (routeId === '/' || routeId === '/_layout') continue;
4748

4849
if (routeId.includes('/projects/$projectId') && params.projectId) {
49-
const label = projectNames.get(params.projectId) ?? params.projectId;
50+
const label = resolveProjectDisplayName(params.projectId, projects);
5051
const to = projectHomePath(params.projectId);
5152
if (!segments.some((s) => s.to === to)) {
5253
segments.push({
@@ -169,10 +170,7 @@ function deriveSegments(
169170
export function Breadcrumbs() {
170171
const matches = useMatches();
171172
const { data: projectData } = useProjectList();
172-
const projectNames = new Map(
173-
(projectData?.projects ?? []).map((project) => [project.id, project.name]),
174-
);
175-
const segments = deriveSegments(matches, projectNames);
173+
const segments = deriveSegments(matches, projectData?.projects);
176174

177175
if (segments.length === 0) return null;
178176

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { describe, expect, it } from 'bun:test';
2+
import { renderToStaticMarkup } from 'react-dom/server';
3+
4+
import { ProjectChromeTitle } from './ProjectChromeTitle';
5+
6+
describe('ProjectChromeTitle', () => {
7+
it('renders the registry project name as the primary chrome title', () => {
8+
const html = renderToStaticMarkup(
9+
<ProjectChromeTitle projectId="wtg-ai-prompts" displayName="WTG.AI.Prompts" />,
10+
);
11+
12+
expect(html).toContain('WTG.AI.Prompts');
13+
expect(html).toContain('wtg-ai-prompts');
14+
expect(html.indexOf('WTG.AI.Prompts')).toBeLessThan(html.indexOf('wtg-ai-prompts'));
15+
});
16+
});
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* Project title used by project-scoped Dashboard chrome.
3+
*
4+
* The primary label is the registry display name. The URL-safe ID remains
5+
* visible as secondary context when it differs, but routes still receive the ID.
6+
*/
7+
8+
export function ProjectChromeTitle({
9+
projectId,
10+
displayName,
11+
}: {
12+
projectId: string;
13+
displayName: string;
14+
}) {
15+
return (
16+
<div>
17+
<h1 className="text-2xl font-semibold text-white">{displayName}</h1>
18+
{displayName !== projectId ? (
19+
<p className="mt-0.5 text-sm text-gray-500">{projectId}</p>
20+
) : null}
21+
</div>
22+
);
23+
}

apps/dashboard/src/components/Sidebar.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
useRunList,
3434
useStudioConfig,
3535
} from '~/lib/api';
36+
import { resolveProjectDisplayName } from '~/lib/project-display-name';
3637
import { formatRunLabel, timeAgo } from '~/lib/run-label';
3738
import { useSidebarContext } from '~/lib/sidebar-context';
3839

@@ -89,7 +90,7 @@ function BrandHeader({ projectId }: { projectId?: string }) {
8990

9091
function useProjectDisplayName(projectId: string): string {
9192
const { data } = useProjectList();
92-
return data?.projects.find((project) => project.id === projectId)?.name ?? projectId;
93+
return resolveProjectDisplayName(projectId, data?.projects);
9394
}
9495

9596
export function Sidebar() {

apps/dashboard/src/lib/navigation.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ describe('route path helpers', () => {
6868
expect(experimentPath('prod-baseline', 'demo project')).toBe(
6969
'/projects/demo%20project/experiments/prod-baseline',
7070
);
71+
expect(runsHomePath('wtg-ai-prompts')).toBe('/projects/wtg-ai-prompts?tab=runs');
7172
});
7273

7374
it('keeps unscoped paths for legacy single-project routes', () => {
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { describe, expect, it } from 'bun:test';
2+
3+
import { resolveProjectDisplayName } from './project-display-name';
4+
5+
describe('resolveProjectDisplayName', () => {
6+
it('uses the registry name for project-scoped dashboard chrome', () => {
7+
expect(
8+
resolveProjectDisplayName('wtg-ai-prompts', [
9+
{
10+
id: 'wtg-ai-prompts',
11+
name: 'WTG.AI.Prompts',
12+
},
13+
]),
14+
).toBe('WTG.AI.Prompts');
15+
});
16+
17+
it('falls back to the URL-safe ID when the registry name is unavailable', () => {
18+
expect(resolveProjectDisplayName('wtg-ai-prompts', [])).toBe('wtg-ai-prompts');
19+
expect(
20+
resolveProjectDisplayName('wtg-ai-prompts', [{ id: 'wtg-ai-prompts', name: ' ' }]),
21+
).toBe('wtg-ai-prompts');
22+
});
23+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* Resolve the human project name shown in Dashboard chrome.
3+
*
4+
* Project-scoped URLs use stable registry IDs, while visible chrome should
5+
* show the registry name from `/api/projects`. Callers pass the project list
6+
* they already fetched and this helper falls back to the ID only when the
7+
* registry name is unavailable.
8+
*/
9+
10+
export interface ProjectDisplayEntry {
11+
id: string;
12+
name?: string | null;
13+
}
14+
15+
export function resolveProjectDisplayName(
16+
projectId: string,
17+
projects: readonly ProjectDisplayEntry[] | undefined,
18+
): string {
19+
const name = projects?.find((project) => project.id === projectId)?.name?.trim();
20+
return name || projectId;
21+
}

apps/dashboard/src/routes/projects/$projectId.tsx

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { useState } from 'react';
1010
import { useQuery, useQueryClient } from '@tanstack/react-query';
1111
import { AnalyticsTab } from '~/components/AnalyticsTab';
1212
import { ExperimentsTab } from '~/components/ExperimentsTab';
13+
import { ProjectChromeTitle } from '~/components/ProjectChromeTitle';
1314
import { RunEvalModal } from '~/components/RunEvalModal';
1415
import { RunList } from '~/components/RunList';
1516
import { type RunSourceFilter, RunSourceToolbar } from '~/components/RunSourceToolbar';
@@ -23,6 +24,7 @@ import {
2324
useRemoteStatus,
2425
useStudioConfig,
2526
} from '~/lib/api';
27+
import { resolveProjectDisplayName } from '~/lib/project-display-name';
2628
import { buildProjectSyncFeedback } from '~/lib/project-sync-status';
2729
import { dedupeSyncedRuns } from '~/lib/run-dedupe';
2830

@@ -49,22 +51,14 @@ function ProjectHomePage() {
4951
const { data: config } = useStudioConfig(projectId);
5052
const { data: projectData } = useProjectList();
5153
const isReadOnly = config?.read_only === true;
52-
const projectName =
53-
projectData?.projects.find((project) => project.id === projectId)?.name ??
54-
config?.project_name ??
55-
projectId;
54+
const projectName = resolveProjectDisplayName(projectId, projectData?.projects);
5655

5756
const activeTab: TabId = tabs.some((t) => t.id === tab) ? (tab as TabId) : 'runs';
5857

5958
return (
6059
<div className="space-y-6">
6160
<div className="flex items-center justify-between">
62-
<div>
63-
<h1 className="text-2xl font-semibold text-white">{projectName}</h1>
64-
{projectName !== projectId ? (
65-
<p className="mt-0.5 text-sm text-gray-500">{projectId}</p>
66-
) : null}
67-
</div>
61+
<ProjectChromeTitle projectId={projectId} displayName={projectName} />
6862
{!isReadOnly && (
6963
<button
7064
type="button"

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"lint": "biome check .",
1515
"format": "biome format --write .",
1616
"fix": "biome check --write .",
17-
"test": "bun --filter @agentv/core test && bun --filter @agentv/eval test && bun --filter @agentv/phoenix-adapter test && bun --filter agentv test",
17+
"test": "bun --filter @agentv/core test && bun --filter @agentv/eval test && bun --filter @agentv/phoenix-adapter test && bun --filter agentv test && bun --filter @agentv/dashboard test",
1818
"test:watch": "bun --filter @agentv/core test:watch & bun --filter agentv test:watch",
1919
"agentv": "bun apps/cli/src/cli.ts",
2020
"agentv:buildrun": "bun run build && bun apps/cli/dist/cli.js",

0 commit comments

Comments
 (0)