diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 8828ca86..8af35bca 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -121,6 +121,7 @@ function boardRoutes() { } /> } /> } /> + } /> } /> } /> } /> @@ -235,6 +236,7 @@ export function App() { } /> } /> } /> + } /> }> {boardRoutes()} diff --git a/ui/src/pages/AgentDetail.tsx b/ui/src/pages/AgentDetail.tsx index 596cb98d..5cd7db7a 100644 --- a/ui/src/pages/AgentDetail.tsx +++ b/ui/src/pages/AgentDetail.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useMemo, useState, useRef } from "react"; -import { useParams, useNavigate, Link, useBeforeUnload } from "@/lib/router"; +import { useParams, useNavigate, Link, Navigate, useBeforeUnload } from "@/lib/router"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { agentsApi, type AgentKey, type ClaudeLoginResult } from "../api/agents"; import { heartbeatsApi } from "../api/heartbeats"; @@ -14,6 +14,7 @@ import { useDialog } from "../context/DialogContext"; import { useBreadcrumbs } from "../context/BreadcrumbContext"; import { queryKeys } from "../lib/queryKeys"; import { AgentConfigForm } from "../components/AgentConfigForm"; +import { PageTabBar } from "../components/PageTabBar"; import { adapterLabels, roleLabels } from "../components/agent-config-primitives"; import { getUIAdapter, buildTranscript } from "../adapters"; import type { TranscriptEntry } from "../adapters"; @@ -28,6 +29,7 @@ import { ScrollToBottom } from "../components/ScrollToBottom"; import { formatCents, formatDate, relativeTime, formatTokens } from "../lib/utils"; import { cn } from "../lib/utils"; import { Button } from "@/components/ui/button"; +import { Tabs } from "@/components/ui/tabs"; import { Popover, PopoverContent, @@ -53,7 +55,6 @@ import { ChevronRight, ChevronDown, ArrowLeft, - Settings, } from "lucide-react"; import { Input } from "@/components/ui/input"; import { AgentIcon, AgentIconPicker } from "../components/AgentIconPicker"; @@ -173,12 +174,12 @@ function scrollToContainerBottom(container: ScrollContainer, behavior: ScrollBeh container.scrollTo({ top: container.scrollHeight, behavior }); } -type AgentDetailView = "overview" | "configure" | "runs"; +type AgentDetailView = "dashboard" | "configuration" | "runs"; function parseAgentDetailView(value: string | null): AgentDetailView { - if (value === "configure" || value === "configuration") return "configure"; + if (value === "configure" || value === "configuration") return "configuration"; if (value === "runs") return value; - return "overview"; + return "dashboard"; } function usageNumber(usage: Record | null, ...keys: string[]) { @@ -304,17 +305,18 @@ export function AgentDetail() { useEffect(() => { if (!agent) return; - if (routeAgentRef === canonicalAgentRef) return; if (urlRunId) { - navigate(`/agents/${canonicalAgentRef}/runs/${urlRunId}`, { replace: true }); + if (routeAgentRef !== canonicalAgentRef) { + navigate(`/agents/${canonicalAgentRef}/runs/${urlRunId}`, { replace: true }); + } return; } - if (urlTab) { - navigate(`/agents/${canonicalAgentRef}/${urlTab}`, { replace: true }); + const canonicalTab = activeView === "configuration" ? "configuration" : "dashboard"; + if (routeAgentRef !== canonicalAgentRef || urlTab !== canonicalTab) { + navigate(`/agents/${canonicalAgentRef}/${canonicalTab}`, { replace: true }); return; } - navigate(`/agents/${canonicalAgentRef}`, { replace: true }); - }, [agent, routeAgentRef, canonicalAgentRef, urlRunId, urlTab, navigate]); + }, [agent, routeAgentRef, canonicalAgentRef, urlRunId, urlTab, activeView, navigate]); useEffect(() => { if (!agent?.companyId || agent.companyId === selectedCompanyId) return; @@ -397,17 +399,19 @@ export function AgentDetail() { { label: "Agents", href: "/agents" }, ]; const agentName = agent?.name ?? routeAgentRef ?? "Agent"; - if (activeView === "overview" && !urlRunId) { + if (activeView === "dashboard" && !urlRunId) { crumbs.push({ label: agentName }); } else { - crumbs.push({ label: agentName, href: `/agents/${canonicalAgentRef}` }); + crumbs.push({ label: agentName, href: `/agents/${canonicalAgentRef}/dashboard` }); if (urlRunId) { crumbs.push({ label: "Runs", href: `/agents/${canonicalAgentRef}/runs` }); crumbs.push({ label: `Run ${urlRunId.slice(0, 8)}` }); - } else if (activeView === "configure") { - crumbs.push({ label: "Configure" }); + } else if (activeView === "configuration") { + crumbs.push({ label: "Configuration" }); } else if (activeView === "runs") { crumbs.push({ label: "Runs" }); + } else { + crumbs.push({ label: "Dashboard" }); } } setBreadcrumbs(crumbs); @@ -416,7 +420,7 @@ export function AgentDetail() { useEffect(() => { closePanel(); return () => closePanel(); - }, []); // eslint-disable-line react-hooks/exhaustive-deps + }, [closePanel]); useBeforeUnload( useCallback((event) => { @@ -429,8 +433,11 @@ export function AgentDetail() { if (isLoading) return ; if (error) return

{error.message}

; if (!agent) return null; + if (!urlRunId && !urlTab) { + return ; + } const isPendingApproval = agent.status === "pending_approval"; - const showConfigActionBar = activeView === "configure" && configDirty; + const showConfigActionBar = activeView === "configuration" && configDirty; return (
@@ -514,16 +521,6 @@ export function AgentDetail() { -
+ {!urlRunId && ( + navigate(`/agents/${canonicalAgentRef}/${value}`)} + > + navigate(`/agents/${canonicalAgentRef}/${value}`)} + /> + + )} + {actionError &&

{actionError}

} {isPendingApproval && (

@@ -623,7 +636,7 @@ export function AgentDetail() { )} {/* View content */} - {activeView === "overview" && ( + {activeView === "dashboard" && ( )} - {activeView === "configure" && ( + {activeView === "configuration" && ( @@ -838,12 +850,10 @@ function AgentOverview({ function ConfigSummary({ agent, - agentRouteId, reportsToAgent, directReports, }: { agent: Agent; - agentRouteId: string; reportsToAgent: Agent | null; directReports: Agent[]; }) { @@ -852,16 +862,7 @@ function ConfigSummary({ return (

-
-

Configuration

- - - Manage → - -
+

Configuration

Agent Details

diff --git a/ui/src/pages/ProjectDetail.tsx b/ui/src/pages/ProjectDetail.tsx index 2a568445..b1dcfd39 100644 --- a/ui/src/pages/ProjectDetail.tsx +++ b/ui/src/pages/ProjectDetail.tsx @@ -16,15 +16,13 @@ import { InlineEditor } from "../components/InlineEditor"; import { StatusBadge } from "../components/StatusBadge"; import { IssuesList } from "../components/IssuesList"; import { PageSkeleton } from "../components/PageSkeleton"; +import { PageTabBar } from "../components/PageTabBar"; import { projectRouteRef, cn } from "../lib/utils"; -import { Button } from "@/components/ui/button"; -import { Sheet, SheetContent, SheetHeader, SheetTitle } from "@/components/ui/sheet"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import { SlidersHorizontal } from "lucide-react"; +import { Tabs } from "@/components/ui/tabs"; /* ── Top-level tab types ── */ -type ProjectTab = "overview" | "list"; +type ProjectTab = "overview" | "list" | "configuration"; function resolveProjectTab(pathname: string, projectId: string): ProjectTab | null { const segments = pathname.split("/").filter(Boolean); @@ -32,6 +30,7 @@ function resolveProjectTab(pathname: string, projectId: string): ProjectTab | nu if (projectsIdx === -1 || segments[projectsIdx + 1] !== projectId) return null; const tab = segments[projectsIdx + 2]; if (tab === "overview") return "overview"; + if (tab === "configuration") return "configuration"; if (tab === "issues") return "list"; return null; } @@ -198,9 +197,8 @@ export function ProjectDetail() { filter?: string; }>(); const { companies, selectedCompanyId, setSelectedCompanyId } = useCompany(); - const { openPanel, closePanel, panelVisible, setPanelVisible } = usePanel(); + const { closePanel } = usePanel(); const { setBreadcrumbs } = useBreadcrumbs(); - const [mobilePropsOpen, setMobilePropsOpen] = useState(false); const queryClient = useQueryClient(); const navigate = useNavigate(); const location = useLocation(); @@ -264,6 +262,10 @@ export function ProjectDetail() { navigate(`/projects/${canonicalProjectRef}/overview`, { replace: true }); return; } + if (activeTab === "configuration") { + navigate(`/projects/${canonicalProjectRef}/configuration`, { replace: true }); + return; + } if (activeTab === "list") { if (filter) { navigate(`/projects/${canonicalProjectRef}/issues/${filter}`, { replace: true }); @@ -276,11 +278,9 @@ export function ProjectDetail() { }, [project, routeProjectRef, canonicalProjectRef, activeTab, filter, navigate]); useEffect(() => { - if (project) { - openPanel( updateProject.mutate(data)} />); - } + closePanel(); return () => closePanel(); - }, [project]); // eslint-disable-line react-hooks/exhaustive-deps + }, [closePanel]); // Redirect bare /projects/:id to /projects/:id/issues if (routeProjectRef && activeTab === null) { @@ -294,6 +294,8 @@ export function ProjectDetail() { const handleTabChange = (tab: ProjectTab) => { if (tab === "overview") { navigate(`/projects/${canonicalProjectRef}/overview`); + } else if (tab === "configuration") { + navigate(`/projects/${canonicalProjectRef}/configuration`); } else { navigate(`/projects/${canonicalProjectRef}/issues`); } @@ -314,54 +316,20 @@ export function ProjectDetail() { as="h2" className="text-xl font-bold" /> - -
- {/* Top-level project tabs */} -
- - -
+ handleTabChange(value as ProjectTab)}> + handleTabChange(value as ProjectTab)} + /> + - {/* Tab content */} {activeTab === "overview" && ( )} - {/* Mobile properties drawer */} - - - - Properties - - -
- updateProject.mutate(data)} /> -
-
-
-
+ {activeTab === "configuration" && ( + updateProject.mutate(data)} /> + )}
); }