From ab7bdcd68aa004d3be98a05adea16c984c76903a Mon Sep 17 00:00:00 2001 From: Cascade Bot Date: Sat, 14 Mar 2026 21:46:46 +0000 Subject: [PATCH 1/4] feat(dashboard): refactor project tabs into URL-backed routes with expandable sidebar tree --- tests/unit/web/project-navigation.test.ts | 122 +++++++++++++++ web/src/components/layout/sidebar.tsx | 72 ++++++++- .../components/projects/projects-table.tsx | 5 +- web/src/lib/project-sections.ts | 33 ++++ .../projects/$projectId.agent-configs.tsx | 14 ++ .../routes/projects/$projectId.general.tsx | 26 ++++ .../routes/projects/$projectId.harness.tsx | 26 ++++ .../projects/$projectId.integrations.tsx | 14 ++ web/src/routes/projects/$projectId.tsx | 146 +++--------------- web/src/routes/projects/$projectId.work.tsx | 90 +++++++++++ web/src/routes/route-tree.ts | 13 +- 11 files changed, 427 insertions(+), 134 deletions(-) create mode 100644 tests/unit/web/project-navigation.test.ts create mode 100644 web/src/lib/project-sections.ts create mode 100644 web/src/routes/projects/$projectId.agent-configs.tsx create mode 100644 web/src/routes/projects/$projectId.general.tsx create mode 100644 web/src/routes/projects/$projectId.harness.tsx create mode 100644 web/src/routes/projects/$projectId.integrations.tsx create mode 100644 web/src/routes/projects/$projectId.work.tsx diff --git a/tests/unit/web/project-navigation.test.ts b/tests/unit/web/project-navigation.test.ts new file mode 100644 index 00000000..785a2869 --- /dev/null +++ b/tests/unit/web/project-navigation.test.ts @@ -0,0 +1,122 @@ +import { describe, expect, it } from 'vitest'; +import { + DEFAULT_PROJECT_SECTION, + PROJECT_SECTIONS, + isProjectActive, + isSectionActive, + resolveDefaultProjectPath, +} from '../../../web/src/lib/project-sections.js'; + +describe('PROJECT_SECTIONS', () => { + it('contains exactly the expected sections in order', () => { + expect(PROJECT_SECTIONS.map((s) => s.id)).toEqual([ + 'general', + 'harness', + 'work', + 'integrations', + 'agent-configs', + ]); + }); + + it('each section has a non-empty label and path', () => { + for (const section of PROJECT_SECTIONS) { + expect(section.label.length).toBeGreaterThan(0); + expect(section.path.length).toBeGreaterThan(0); + } + }); + + it('has unique ids', () => { + const ids = PROJECT_SECTIONS.map((s) => s.id); + expect(new Set(ids).size).toBe(ids.length); + }); + + it('has unique paths', () => { + const paths = PROJECT_SECTIONS.map((s) => s.path); + expect(new Set(paths).size).toBe(paths.length); + }); +}); + +describe('DEFAULT_PROJECT_SECTION', () => { + it('is "general"', () => { + expect(DEFAULT_PROJECT_SECTION).toBe('general'); + }); + + it('exists in PROJECT_SECTIONS', () => { + const ids = PROJECT_SECTIONS.map((s) => s.id); + expect(ids).toContain(DEFAULT_PROJECT_SECTION); + }); +}); + +describe('section path mapping', () => { + it('maps general section to /general path', () => { + const generalSection = PROJECT_SECTIONS.find((s) => s.id === 'general'); + expect(generalSection?.path).toBe('general'); + }); + + it('maps agent-configs section to /agent-configs path', () => { + const agentConfigsSection = PROJECT_SECTIONS.find((s) => s.id === 'agent-configs'); + expect(agentConfigsSection?.path).toBe('agent-configs'); + }); + + it('maps work section to /work path', () => { + const workSection = PROJECT_SECTIONS.find((s) => s.id === 'work'); + expect(workSection?.path).toBe('work'); + }); + + it('maps integrations section to /integrations path', () => { + const integrationsSection = PROJECT_SECTIONS.find((s) => s.id === 'integrations'); + expect(integrationsSection?.path).toBe('integrations'); + }); +}); + +describe('isProjectActive', () => { + it('detects active project from section path', () => { + expect(isProjectActive('/projects/my-project/general', 'my-project')).toBe(true); + expect(isProjectActive('/projects/my-project/work', 'my-project')).toBe(true); + expect(isProjectActive('/projects/my-project/agent-configs', 'my-project')).toBe(true); + }); + + it('detects active project at root path', () => { + expect(isProjectActive('/projects/my-project', 'my-project')).toBe(true); + }); + + it('does not falsely match other projects', () => { + expect(isProjectActive('/projects/other-project/general', 'my-project')).toBe(false); + expect(isProjectActive('/projects', 'my-project')).toBe(false); + }); +}); + +describe('isSectionActive', () => { + it('returns true for matching section path', () => { + expect(isSectionActive('/projects/proj1/general', 'proj1', 'general')).toBe(true); + expect(isSectionActive('/projects/proj1/work', 'proj1', 'work')).toBe(true); + expect(isSectionActive('/projects/proj1/agent-configs', 'proj1', 'agent-configs')).toBe(true); + }); + + it('returns false for non-matching section', () => { + expect(isSectionActive('/projects/proj1/general', 'proj1', 'work')).toBe(false); + expect(isSectionActive('/projects/proj1/integrations', 'proj1', 'general')).toBe(false); + }); + + it('returns false for different project', () => { + expect(isSectionActive('/projects/proj2/general', 'proj1', 'general')).toBe(false); + }); + + it('returns true for sub-paths of a section', () => { + expect(isSectionActive('/projects/proj1/work/details', 'proj1', 'work')).toBe(true); + }); +}); + +describe('resolveDefaultProjectPath', () => { + it('resolves to /general for any project id', () => { + expect(resolveDefaultProjectPath('abc123')).toBe('/projects/abc123/general'); + expect(resolveDefaultProjectPath('my-project')).toBe('/projects/my-project/general'); + }); + + it('always uses the DEFAULT_PROJECT_SECTION', () => { + const projectId = 'test-proj'; + expect(resolveDefaultProjectPath(projectId)).toBe( + `/projects/${projectId}/${DEFAULT_PROJECT_SECTION}`, + ); + }); +}); diff --git a/web/src/components/layout/sidebar.tsx b/web/src/components/layout/sidebar.tsx index 1954834c..8c21a07b 100644 --- a/web/src/components/layout/sidebar.tsx +++ b/web/src/components/layout/sidebar.tsx @@ -1,4 +1,5 @@ import { Separator } from '@/components/ui/separator.js'; +import { PROJECT_SECTIONS } from '@/lib/project-sections.js'; import { trpc } from '@/lib/trpc.js'; import { cn } from '@/lib/utils.js'; import { useQuery } from '@tanstack/react-query'; @@ -7,6 +8,8 @@ import { Activity, BookOpen, Building, + ChevronDown, + ChevronRight, FolderGit2, KeyRound, LayoutDashboard, @@ -14,6 +17,7 @@ import { Users, Zap, } from 'lucide-react'; +import { useState } from 'react'; interface SidebarProps { user: { name: string; email: string; role: string } | undefined; @@ -66,6 +70,64 @@ function NavLink({ ); } +interface ProjectNavItemProps { + project: { id: string; name: string }; + currentPath: string; +} + +function ProjectNavItem({ project, currentPath }: ProjectNavItemProps) { + const projectBasePath = `/projects/${project.id}`; + const isActiveProject = currentPath.startsWith(projectBasePath); + const [isExpanded, setIsExpanded] = useState(isActiveProject); + + return ( +
+ + + {isExpanded && ( +
+ {PROJECT_SECTIONS.map((section) => { + const sectionPath = `${projectBasePath}/${section.path}`; + const isSectionActive = + currentPath === sectionPath || currentPath.startsWith(`${sectionPath}/`); + return ( + + {section.label} + + ); + })} +
+ )} +
+ ); +} + export function Sidebar({ user }: SidebarProps) { const routerState = useRouterState(); const currentPath = routerState.location.pathname; @@ -89,16 +151,10 @@ export function Sidebar({ user }: SidebarProps) {
Projects
-
+
{projects && projects.length > 0 ? ( projects.map((project) => ( - + )) ) : ( - navigate({ to: '/projects/$projectId', params: { projectId: project.id } }) + navigate({ + to: '/projects/$projectId/general', + params: { projectId: project.id }, + }) } > {project.name} diff --git a/web/src/lib/project-sections.ts b/web/src/lib/project-sections.ts new file mode 100644 index 00000000..bdb604b7 --- /dev/null +++ b/web/src/lib/project-sections.ts @@ -0,0 +1,33 @@ +export type ProjectSection = 'general' | 'harness' | 'work' | 'integrations' | 'agent-configs'; + +export const PROJECT_SECTIONS: { id: ProjectSection; label: string; path: string }[] = [ + { id: 'general', label: 'General', path: 'general' }, + { id: 'harness', label: 'Harness', path: 'harness' }, + { id: 'work', label: 'Work', path: 'work' }, + { id: 'integrations', label: 'Integrations', path: 'integrations' }, + { id: 'agent-configs', label: 'Agent Configs', path: 'agent-configs' }, +]; + +export const DEFAULT_PROJECT_SECTION: ProjectSection = 'general'; + +/** + * Returns true if the given pathname is within the given project. + */ +export function isProjectActive(pathname: string, projectId: string): boolean { + return pathname.startsWith(`/projects/${projectId}/`) || pathname === `/projects/${projectId}`; +} + +/** + * Returns true if the given pathname matches a specific section of a project. + */ +export function isSectionActive(pathname: string, projectId: string, sectionPath: string): boolean { + const fullPath = `/projects/${projectId}/${sectionPath}`; + return pathname === fullPath || pathname.startsWith(`${fullPath}/`); +} + +/** + * Resolves the default section URL for a given project. + */ +export function resolveDefaultProjectPath(projectId: string): string { + return `/projects/${projectId}/${DEFAULT_PROJECT_SECTION}`; +} diff --git a/web/src/routes/projects/$projectId.agent-configs.tsx b/web/src/routes/projects/$projectId.agent-configs.tsx new file mode 100644 index 00000000..33555a60 --- /dev/null +++ b/web/src/routes/projects/$projectId.agent-configs.tsx @@ -0,0 +1,14 @@ +import { ProjectAgentConfigs } from '@/components/projects/project-agent-configs.js'; +import { createRoute } from '@tanstack/react-router'; +import { projectDetailRoute } from './$projectId.js'; + +function ProjectAgentConfigsPage() { + const { projectId } = projectAgentConfigsRoute.useParams(); + return ; +} + +export const projectAgentConfigsRoute = createRoute({ + getParentRoute: () => projectDetailRoute, + path: '/agent-configs', + component: ProjectAgentConfigsPage, +}); diff --git a/web/src/routes/projects/$projectId.general.tsx b/web/src/routes/projects/$projectId.general.tsx new file mode 100644 index 00000000..eb95efae --- /dev/null +++ b/web/src/routes/projects/$projectId.general.tsx @@ -0,0 +1,26 @@ +import { ProjectGeneralForm } from '@/components/projects/project-general-form.js'; +import { trpc } from '@/lib/trpc.js'; +import { useQuery } from '@tanstack/react-query'; +import { createRoute } from '@tanstack/react-router'; +import { projectDetailRoute } from './$projectId.js'; + +function ProjectGeneralPage() { + const { projectId } = projectGeneralRoute.useParams(); + const projectQuery = useQuery(trpc.projects.getById.queryOptions({ id: projectId })); + + if (projectQuery.isLoading) { + return
Loading...
; + } + + if (projectQuery.isError || !projectQuery.data) { + return
Project not found
; + } + + return ; +} + +export const projectGeneralRoute = createRoute({ + getParentRoute: () => projectDetailRoute, + path: '/general', + component: ProjectGeneralPage, +}); diff --git a/web/src/routes/projects/$projectId.harness.tsx b/web/src/routes/projects/$projectId.harness.tsx new file mode 100644 index 00000000..9a86e4eb --- /dev/null +++ b/web/src/routes/projects/$projectId.harness.tsx @@ -0,0 +1,26 @@ +import { ProjectHarnessForm } from '@/components/projects/project-harness-form.js'; +import { trpc } from '@/lib/trpc.js'; +import { useQuery } from '@tanstack/react-query'; +import { createRoute } from '@tanstack/react-router'; +import { projectDetailRoute } from './$projectId.js'; + +function ProjectHarnessPage() { + const { projectId } = projectHarnessRoute.useParams(); + const projectQuery = useQuery(trpc.projects.getById.queryOptions({ id: projectId })); + + if (projectQuery.isLoading) { + return
Loading...
; + } + + if (projectQuery.isError || !projectQuery.data) { + return
Project not found
; + } + + return ; +} + +export const projectHarnessRoute = createRoute({ + getParentRoute: () => projectDetailRoute, + path: '/harness', + component: ProjectHarnessPage, +}); diff --git a/web/src/routes/projects/$projectId.integrations.tsx b/web/src/routes/projects/$projectId.integrations.tsx new file mode 100644 index 00000000..91f82611 --- /dev/null +++ b/web/src/routes/projects/$projectId.integrations.tsx @@ -0,0 +1,14 @@ +import { IntegrationForm } from '@/components/projects/integration-form.js'; +import { createRoute } from '@tanstack/react-router'; +import { projectDetailRoute } from './$projectId.js'; + +function ProjectIntegrationsPage() { + const { projectId } = projectIntegrationsRoute.useParams(); + return ; +} + +export const projectIntegrationsRoute = createRoute({ + getParentRoute: () => projectDetailRoute, + path: '/integrations', + component: ProjectIntegrationsPage, +}); diff --git a/web/src/routes/projects/$projectId.tsx b/web/src/routes/projects/$projectId.tsx index 4f55c3d8..1d60d44c 100644 --- a/web/src/routes/projects/$projectId.tsx +++ b/web/src/routes/projects/$projectId.tsx @@ -1,36 +1,22 @@ -import { IntegrationForm } from '@/components/projects/integration-form.js'; -import { ProjectAgentConfigs } from '@/components/projects/project-agent-configs.js'; -import { ProjectGeneralForm } from '@/components/projects/project-general-form.js'; -import { ProjectHarnessForm } from '@/components/projects/project-harness-form.js'; -import { ProjectWorkTable } from '@/components/projects/project-work-table.js'; -import { ProjectWorkDurationChart } from '@/components/runs/project-work-duration-chart.js'; -import { WorkItemCostChart } from '@/components/runs/work-item-cost-chart.js'; +import { + DEFAULT_PROJECT_SECTION, + PROJECT_SECTIONS, + type ProjectSection, +} from '@/lib/project-sections.js'; import { trpc } from '@/lib/trpc.js'; -import { cn, formatCost } from '@/lib/utils.js'; import { useQuery } from '@tanstack/react-query'; -import { Link, createRoute } from '@tanstack/react-router'; +import { Link, Outlet, createRoute, redirect } from '@tanstack/react-router'; import { ArrowLeft } from 'lucide-react'; -import { useState } from 'react'; import { rootRoute } from '../__root.js'; -type Tab = 'general' | 'harness' | 'work' | 'integrations' | 'agent-configs'; +// Re-export for backward compatibility +export type { ProjectSection }; +export { DEFAULT_PROJECT_SECTION, PROJECT_SECTIONS }; -const WORK_PAGE_SIZE = 50; - -function ProjectDetailPage() { +function ProjectShellPage() { const { projectId } = projectDetailRoute.useParams(); - const [activeTab, setActiveTab] = useState('general'); - const [workOffset, setWorkOffset] = useState(0); const projectQuery = useQuery(trpc.projects.getById.queryOptions({ id: projectId })); - const workQuery = useQuery({ - ...trpc.prs.listUnified.queryOptions({ projectId }), - enabled: activeTab === 'work', - }); - const workStatsQuery = useQuery({ - ...trpc.prs.workStats.queryOptions({ projectId }), - enabled: activeTab === 'work', - }); if (projectQuery.isLoading) { return
Loading project...
; @@ -42,14 +28,6 @@ function ProjectDetailPage() { const project = projectQuery.data; - const tabs: { id: Tab; label: string }[] = [ - { id: 'general', label: 'General' }, - { id: 'harness', label: 'Harness' }, - { id: 'work', label: 'Work' }, - { id: 'integrations', label: 'Integrations' }, - { id: 'agent-configs', label: 'Agent Configs' }, - ]; - return (
@@ -64,97 +42,7 @@ function ProjectDetailPage() {

{project.name}

-
- -
- - {activeTab === 'general' && } - - {activeTab === 'harness' && } - - {activeTab === 'work' && ( -
-
-

Work

- {workQuery.data && ( - {workQuery.data.length} total - )} -
- - {workStatsQuery.data && workStatsQuery.data.length > 0 && ( - <> -
- ({ ...r, id: String(i) }))} - /> - -
-
- - - {workStatsQuery.data.length >= 500 ? '500+' : workStatsQuery.data.length} - {' '} - {workStatsQuery.data.length >= 500 - ? 'latest runs (showing most recent 500)' - : 'total runs'} - - - - {formatCost( - workStatsQuery.data - .reduce( - (sum, r) => sum + (r.costUsd != null ? Number.parseFloat(r.costUsd) : 0), - 0, - ) - .toFixed(4), - )} - {' '} - total cost - -
- - )} - - {workQuery.isLoading && ( -
Loading work items...
- )} - - {workQuery.isError && ( -
- Failed to load work: {workQuery.error.message} -
- )} - - {workQuery.data && ( - - )} -
- )} - - {activeTab === 'integrations' && } - {activeTab === 'agent-configs' && } +
); } @@ -162,5 +50,15 @@ function ProjectDetailPage() { export const projectDetailRoute = createRoute({ getParentRoute: () => rootRoute, path: '/projects/$projectId', - component: ProjectDetailPage, + component: ProjectShellPage, + beforeLoad: ({ location, params }) => { + // If navigating exactly to /projects/$projectId, redirect to /general subsection + const path = location.pathname; + if (path.match(/^\/projects\/[^/]+\/?$/)) { + throw redirect({ + to: '/projects/$projectId/general', + params: { projectId: params.projectId }, + }); + } + }, }); diff --git a/web/src/routes/projects/$projectId.work.tsx b/web/src/routes/projects/$projectId.work.tsx new file mode 100644 index 00000000..f722d25f --- /dev/null +++ b/web/src/routes/projects/$projectId.work.tsx @@ -0,0 +1,90 @@ +import { ProjectWorkTable } from '@/components/projects/project-work-table.js'; +import { ProjectWorkDurationChart } from '@/components/runs/project-work-duration-chart.js'; +import { WorkItemCostChart } from '@/components/runs/work-item-cost-chart.js'; +import { trpc } from '@/lib/trpc.js'; +import { formatCost } from '@/lib/utils.js'; +import { useQuery } from '@tanstack/react-query'; +import { createRoute } from '@tanstack/react-router'; +import { useState } from 'react'; +import { projectDetailRoute } from './$projectId.js'; + +const WORK_PAGE_SIZE = 50; + +function ProjectWorkPage() { + const { projectId } = projectWorkRoute.useParams(); + const [workOffset, setWorkOffset] = useState(0); + + const workQuery = useQuery(trpc.prs.listUnified.queryOptions({ projectId })); + const workStatsQuery = useQuery(trpc.prs.workStats.queryOptions({ projectId })); + + return ( +
+
+

Work

+ {workQuery.data && ( + {workQuery.data.length} total + )} +
+ + {workStatsQuery.data && workStatsQuery.data.length > 0 && ( + <> +
+ ({ ...r, id: String(i) }))} + /> + +
+
+ + + {workStatsQuery.data.length >= 500 ? '500+' : workStatsQuery.data.length} + {' '} + {workStatsQuery.data.length >= 500 + ? 'latest runs (showing most recent 500)' + : 'total runs'} + + + + {formatCost( + workStatsQuery.data + .reduce( + (sum, r) => sum + (r.costUsd != null ? Number.parseFloat(r.costUsd) : 0), + 0, + ) + .toFixed(4), + )} + {' '} + total cost + +
+ + )} + + {workQuery.isLoading && ( +
Loading work items...
+ )} + + {workQuery.isError && ( +
+ Failed to load work: {workQuery.error.message} +
+ )} + + {workQuery.data && ( + + )} +
+ ); +} + +export const projectWorkRoute = createRoute({ + getParentRoute: () => projectDetailRoute, + path: '/work', + component: ProjectWorkPage, +}); diff --git a/web/src/routes/route-tree.ts b/web/src/routes/route-tree.ts index c98077ed..93b697f1 100644 --- a/web/src/routes/route-tree.ts +++ b/web/src/routes/route-tree.ts @@ -5,7 +5,12 @@ import { globalRunsRoute } from './global/runs.js'; import { globalWebhookLogsRoute } from './global/webhook-logs.js'; import { indexRoute } from './index.js'; import { loginRoute } from './login.js'; +import { projectAgentConfigsRoute } from './projects/$projectId.agent-configs.js'; +import { projectGeneralRoute } from './projects/$projectId.general.js'; +import { projectHarnessRoute } from './projects/$projectId.harness.js'; +import { projectIntegrationsRoute } from './projects/$projectId.integrations.js'; import { projectDetailRoute } from './projects/$projectId.js'; +import { projectWorkRoute } from './projects/$projectId.work.js'; import { projectsIndexRoute } from './projects/index.js'; import { prRunsRoute } from './prs/$projectId.$prNumber.js'; import { runDetailRoute } from './runs/$runId.js'; @@ -19,7 +24,13 @@ export const routeTree = rootRoute.addChildren([ indexRoute, runDetailRoute, projectsIndexRoute, - projectDetailRoute, + projectDetailRoute.addChildren([ + projectGeneralRoute, + projectHarnessRoute, + projectWorkRoute, + projectIntegrationsRoute, + projectAgentConfigsRoute, + ]), settingsGeneralRoute, settingsCredentialsRoute, settingsUsersRoute, From 56082f9bb547d0b8ec53b1172a2a861912450305 Mon Sep 17 00:00:00 2001 From: Cascade Bot Date: Sat, 14 Mar 2026 22:00:43 +0000 Subject: [PATCH 2/4] fix(sidebar): address review feedback on navigation helpers and back-links - Fix prefix-matching bug in ProjectNavItem by importing and using isProjectActive/isSectionActive helpers from project-sections.ts instead of inline startsWith checks (prevents false matches like /projects/proj matching /projects/project-2/general) - Update back-links in prs/$projectId.$prNumber.tsx and work-items/$projectId.$workItemId.tsx to point directly to /projects/$projectId/general, avoiding an unnecessary client-side redirect on every click - Remove dead re-exports from $projectId.tsx (ProjectSection, DEFAULT_PROJECT_SECTION, PROJECT_SECTIONS) which had zero consumers - Restore overflow-y-auto max-h-48 on the projects container to prevent expandable tree items from pushing settings/global nav off-screen Co-Authored-By: Claude Opus 4.6 --- web/src/components/layout/sidebar.tsx | 19 ++++++++----------- web/src/routes/projects/$projectId.tsx | 9 --------- web/src/routes/prs/$projectId.$prNumber.tsx | 2 +- .../work-items/$projectId.$workItemId.tsx | 2 +- 4 files changed, 10 insertions(+), 22 deletions(-) diff --git a/web/src/components/layout/sidebar.tsx b/web/src/components/layout/sidebar.tsx index 8c21a07b..eb8b252a 100644 --- a/web/src/components/layout/sidebar.tsx +++ b/web/src/components/layout/sidebar.tsx @@ -1,5 +1,5 @@ import { Separator } from '@/components/ui/separator.js'; -import { PROJECT_SECTIONS } from '@/lib/project-sections.js'; +import { PROJECT_SECTIONS, isProjectActive, isSectionActive } from '@/lib/project-sections.js'; import { trpc } from '@/lib/trpc.js'; import { cn } from '@/lib/utils.js'; import { useQuery } from '@tanstack/react-query'; @@ -76,9 +76,8 @@ interface ProjectNavItemProps { } function ProjectNavItem({ project, currentPath }: ProjectNavItemProps) { - const projectBasePath = `/projects/${project.id}`; - const isActiveProject = currentPath.startsWith(projectBasePath); - const [isExpanded, setIsExpanded] = useState(isActiveProject); + const activeProject = isProjectActive(currentPath, project.id); + const [isExpanded, setIsExpanded] = useState(activeProject); return (
@@ -87,7 +86,7 @@ function ProjectNavItem({ project, currentPath }: ProjectNavItemProps) { onClick={() => setIsExpanded((prev) => !prev)} className={cn( 'flex w-full items-center gap-2 rounded-md px-3 py-2 text-sm font-medium transition-colors', - isActiveProject + activeProject ? 'bg-sidebar-accent text-sidebar-accent-foreground' : 'text-sidebar-foreground hover:bg-sidebar-accent/50', )} @@ -104,16 +103,14 @@ function ProjectNavItem({ project, currentPath }: ProjectNavItemProps) { {isExpanded && (
{PROJECT_SECTIONS.map((section) => { - const sectionPath = `${projectBasePath}/${section.path}`; - const isSectionActive = - currentPath === sectionPath || currentPath.startsWith(`${sectionPath}/`); + const sectionActive = isSectionActive(currentPath, project.id, section.path); return ( Projects
-
+
{projects && projects.length > 0 ? ( projects.map((project) => ( diff --git a/web/src/routes/projects/$projectId.tsx b/web/src/routes/projects/$projectId.tsx index 1d60d44c..5aea411a 100644 --- a/web/src/routes/projects/$projectId.tsx +++ b/web/src/routes/projects/$projectId.tsx @@ -1,18 +1,9 @@ -import { - DEFAULT_PROJECT_SECTION, - PROJECT_SECTIONS, - type ProjectSection, -} from '@/lib/project-sections.js'; import { trpc } from '@/lib/trpc.js'; import { useQuery } from '@tanstack/react-query'; import { Link, Outlet, createRoute, redirect } from '@tanstack/react-router'; import { ArrowLeft } from 'lucide-react'; import { rootRoute } from '../__root.js'; -// Re-export for backward compatibility -export type { ProjectSection }; -export { DEFAULT_PROJECT_SECTION, PROJECT_SECTIONS }; - function ProjectShellPage() { const { projectId } = projectDetailRoute.useParams(); diff --git a/web/src/routes/prs/$projectId.$prNumber.tsx b/web/src/routes/prs/$projectId.$prNumber.tsx index c9199b92..61d1109a 100644 --- a/web/src/routes/prs/$projectId.$prNumber.tsx +++ b/web/src/routes/prs/$projectId.$prNumber.tsx @@ -33,7 +33,7 @@ function PRRunsPage() {
diff --git a/web/src/routes/work-items/$projectId.$workItemId.tsx b/web/src/routes/work-items/$projectId.$workItemId.tsx index 62ed5e42..3580c99f 100644 --- a/web/src/routes/work-items/$projectId.$workItemId.tsx +++ b/web/src/routes/work-items/$projectId.$workItemId.tsx @@ -32,7 +32,7 @@ function WorkItemRunsPage() {
From e77fe7d7f2380dc674845a000c0dd3b0659b1da3 Mon Sep 17 00:00:00 2001 From: Cascade Bot Date: Sat, 14 Mar 2026 22:10:14 +0000 Subject: [PATCH 3/4] fix(build): use typed route paths for project section links in sidebar Add `route` property to PROJECT_SECTIONS with typed TanStack Router paths (`/projects/$projectId/
`) and update the sidebar Link component to use `to={section.route}` with `params={{ projectId: project.id }}` instead of a template literal that violated TanStack Router's type-safe route requirements, fixing the frontend build failure. Co-Authored-By: Claude Opus 4.6 --- web/src/components/layout/sidebar.tsx | 3 ++- web/src/lib/project-sections.ts | 34 ++++++++++++++++++++++----- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/web/src/components/layout/sidebar.tsx b/web/src/components/layout/sidebar.tsx index eb8b252a..07a86499 100644 --- a/web/src/components/layout/sidebar.tsx +++ b/web/src/components/layout/sidebar.tsx @@ -107,7 +107,8 @@ function ProjectNavItem({ project, currentPath }: ProjectNavItemProps) { return ( Date: Sat, 14 Mar 2026 22:21:54 +0000 Subject: [PATCH 4/4] fix(sidebar): sync expansion state with URL navigation and fix back-link destinations - Use useEffect to keep sidebar project expansion in sync when activeProject changes due to URL navigation (not just initial mount) - Update back-navigation links in PR runs and work item runs pages to point to /work section instead of /general, since users reach these pages from Work Co-Authored-By: Claude Opus 4.6 --- web/src/components/layout/sidebar.tsx | 9 ++++++++- web/src/routes/prs/$projectId.$prNumber.tsx | 4 ++-- web/src/routes/work-items/$projectId.$workItemId.tsx | 4 ++-- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/web/src/components/layout/sidebar.tsx b/web/src/components/layout/sidebar.tsx index 07a86499..2b89ebcc 100644 --- a/web/src/components/layout/sidebar.tsx +++ b/web/src/components/layout/sidebar.tsx @@ -17,7 +17,7 @@ import { Users, Zap, } from 'lucide-react'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; interface SidebarProps { user: { name: string; email: string; role: string } | undefined; @@ -79,6 +79,13 @@ function ProjectNavItem({ project, currentPath }: ProjectNavItemProps) { const activeProject = isProjectActive(currentPath, project.id); const [isExpanded, setIsExpanded] = useState(activeProject); + // Sync expansion state when the active project changes due to URL navigation + useEffect(() => { + if (activeProject) { + setIsExpanded(true); + } + }, [activeProject]); + return (