From 7c7ffb56243dc3a973723bd68e87b6c886b0bad4 Mon Sep 17 00:00:00 2001 From: Harshit Singh Bhandari <24b4506@iitb.ac.in> Date: Mon, 4 May 2026 00:09:43 +0530 Subject: [PATCH] fix(web): source sidebar orchestrator from API field, not session list (#1623) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(web): source sidebar orchestrator from API field, not session list PR #1615 merged the initial implementation but missed the follow-up fix. The merged sidebar code looks up the orchestrator inside the `sessions` prop, but /api/sessions/route.ts strips ALL orchestrators from that array before returning (they're exposed via a separate `orchestrators` field on the same response). Result: the new menu entry — and the existing icon button next to the dashboard icon — never render for any project. Replace the broken in-sidebar derivation with a new `orchestrators` prop on ProjectSidebar. Each parent passes the data it already has: - Dashboard.tsx: passes `activeOrchestrators` (already in scope) - PullRequestsPage.tsx: passes `orchestratorLinks` (already in scope) - sessions/[id]/page.tsx: stores the `orchestrators` field already returned by /api/sessions and threads it through SessionPageShell and SessionDetail as `sidebarOrchestrators` Tests refocused: the sidebar now just renders what the prop says, so the live-vs-terminal selection lives in the API (selectPreferredOrchestratorId) and is no longer the sidebar's responsibility. Co-Authored-By: Claude Opus 4.7 * style: fix indentation on sidebarOrchestrators prop in SessionPage Addresses Greptile review comment on PR #1623. Co-Authored-By: Claude Opus 4.7 * refactor(web): use DashboardOrchestratorLink for API response type Addresses non-blocking review feedback on PR #1623. The /api/sessions response actually returns DashboardOrchestratorLink shape (which includes projectName), so use that as the response type instead of the narrower ProjectSidebarOrchestrator. The sidebar prop type stays narrow on purpose — it's the minimum the sidebar needs to render. Also document the "one orchestrator per project" Map invariant. Co-Authored-By: Claude Opus 4.7 --------- Co-authored-by: Claude Opus 4.7 --- packages/web/src/app/sessions/[id]/page.tsx | 40 +++++++++---- packages/web/src/components/Dashboard.tsx | 1 + .../web/src/components/ProjectSidebar.tsx | 52 +++++++++-------- .../web/src/components/PullRequestsPage.tsx | 1 + packages/web/src/components/SessionDetail.tsx | 5 +- .../__tests__/ProjectSidebar.test.tsx | 56 +++---------------- 6 files changed, 73 insertions(+), 82 deletions(-) diff --git a/packages/web/src/app/sessions/[id]/page.tsx b/packages/web/src/app/sessions/[id]/page.tsx index fb32d5a67..cbd7efb63 100644 --- a/packages/web/src/app/sessions/[id]/page.tsx +++ b/packages/web/src/app/sessions/[id]/page.tsx @@ -5,9 +5,17 @@ import { useParams, usePathname, useRouter } from "next/navigation"; import { ACTIVITY_STATE, SESSION_STATUS, isOrchestratorSession } from "@aoagents/ao-core/types"; import { SessionDetail } from "@/components/SessionDetail"; import { ErrorDisplay } from "@/components/ErrorDisplay"; -import { ProjectSidebar } from "@/components/ProjectSidebar"; +import { + ProjectSidebar, + type ProjectSidebarOrchestrator, +} from "@/components/ProjectSidebar"; import { useMediaQuery, MOBILE_BREAKPOINT } from "@/hooks/useMediaQuery"; -import { type DashboardSession, type ActivityState, getAttentionLevel } from "@/lib/types"; +import { + type DashboardSession, + type DashboardOrchestratorLink, + type ActivityState, + getAttentionLevel, +} from "@/lib/types"; import { activityIcon } from "@/lib/activity-icons"; import type { ProjectInfo } from "@/lib/project-name"; import { getSessionTitle } from "@/lib/format"; @@ -170,6 +178,7 @@ function SessionPageShell({ projects, projectsLoading, sidebarSessions, + sidebarOrchestrators, sidebarLoading, sidebarError, onRetrySidebar, @@ -180,6 +189,7 @@ function SessionPageShell({ projects: ProjectInfo[]; projectsLoading: boolean; sidebarSessions: DashboardSession[] | null; + sidebarOrchestrators?: ProjectSidebarOrchestrator[]; sidebarLoading: boolean; sidebarError: boolean; onRetrySidebar: () => void; @@ -251,6 +261,7 @@ function SessionPageShell({ ( () => cachedSidebarSessions, ); + const [sidebarOrchestrators, setSidebarOrchestrators] = useState< + ProjectSidebarOrchestrator[] | undefined + >(undefined); const [loading, setLoading] = useState(cachedSession === null); const [routeError, setRouteError] = useState(null); const [sessionMissing, setSessionMissing] = useState(false); @@ -596,18 +610,19 @@ export default function SessionPage() { const controller = new AbortController(); sidebarFetchControllerRef.current = controller; try { - const body = await fetchJsonWithTimeout<{ sessions?: DashboardSession[] } | null>( - "/api/sessions?fresh=true", - { - signal: controller.signal, - timeoutMs: PROJECT_SIDEBAR_FETCH_TIMEOUT_MS, - timeoutMessage: `Sidebar sessions request timed out after ${PROJECT_SIDEBAR_FETCH_TIMEOUT_MS}ms`, - }, - ); + const body = await fetchJsonWithTimeout<{ + sessions?: DashboardSession[]; + orchestrators?: DashboardOrchestratorLink[]; + } | null>("/api/sessions?fresh=true", { + signal: controller.signal, + timeoutMs: PROJECT_SIDEBAR_FETCH_TIMEOUT_MS, + timeoutMessage: `Sidebar sessions request timed out after ${PROJECT_SIDEBAR_FETCH_TIMEOUT_MS}ms`, + }); const restSessions = body?.sessions ?? []; const nextSessions = applyMuxSessionPatches(restSessions, pendingMuxSessionsRef.current ?? []) ?? restSessions; cachedSidebarSessions = nextSessions; + setSidebarOrchestrators(body?.orchestrators); setSidebarError(false); setSidebarSessions((current) => areSidebarSessionsEqual(current, nextSessions) ? current : nextSessions, @@ -724,6 +739,7 @@ export default function SessionPage() { projects={projects} projectsLoading={projectsLoading} sidebarSessions={sidebarSessions} + sidebarOrchestrators={sidebarOrchestrators} sidebarLoading={sidebarSessions === null} sidebarError={sidebarError} onRetrySidebar={fetchSidebarSessions} @@ -754,6 +770,7 @@ export default function SessionPage() { projects={projects} projectsLoading={projectsLoading} sidebarSessions={sidebarSessions} + sidebarOrchestrators={sidebarOrchestrators} sidebarLoading={sidebarSessions === null} sidebarError={sidebarError} onRetrySidebar={fetchSidebarSessions} @@ -782,6 +799,7 @@ export default function SessionPage() { projects={projects} projectsLoading={projectsLoading} sidebarSessions={sidebarSessions} + sidebarOrchestrators={sidebarOrchestrators} sidebarLoading={sidebarSessions === null} sidebarError={sidebarError} onRetrySidebar={fetchSidebarSessions} @@ -819,6 +837,7 @@ export default function SessionPage() { projects={projects} projectsLoading={projectsLoading} sidebarSessions={sidebarSessions} + sidebarOrchestrators={sidebarOrchestrators} sidebarLoading={sidebarSessions === null} sidebarError={sidebarError} onRetrySidebar={fetchSidebarSessions} @@ -849,6 +868,7 @@ export default function SessionPage() { projectOrchestratorId={projectOrchestratorId} projects={projects} sidebarSessions={sidebarSessions} + sidebarOrchestrators={sidebarOrchestrators} sidebarLoading={sidebarSessions === null} sidebarError={sidebarError} onRetrySidebar={fetchSidebarSessions} diff --git a/packages/web/src/components/Dashboard.tsx b/packages/web/src/components/Dashboard.tsx index a98c75b31..b66f7dc00 100644 --- a/packages/web/src/components/Dashboard.tsx +++ b/packages/web/src/components/Dashboard.tsx @@ -605,6 +605,7 @@ function DashboardInner({ new Map((orchestrators ?? []).map((o) => [o.projectId, o])), + [orchestrators], + ); + const sessionsByProject = useMemo(() => { const map = new Map(); // Build a set of valid project IDs to filter sessions strictly @@ -390,21 +411,7 @@ function ProjectSidebarInner({ const visibleSessions = workerSessions; const hasActiveSessions = visibleSessions.length > 0; - const projectPrefix = prefixByProject.get(project.id); - const canonicalOrchestratorId = projectPrefix - ? getOrchestratorSessionId({ sessionPrefix: projectPrefix }) - : null; - const orchestratorSession = sessions?.find( - (s) => s.projectId === project.id && s.id === canonicalOrchestratorId, - ); - const liveOrchestrator = - orchestratorSession && - !isTerminalSession({ - status: orchestratorSession.status, - activity: orchestratorSession.activity, - }) - ? orchestratorSession - : null; + const orchestratorLink = orchestratorByProject.get(project.id) ?? null; return (
@@ -506,9 +513,9 @@ function ProjectSidebarInner({ ) : null} {/* Orchestrator button */} - {!isDegraded && orchestratorSession && ( + {!isDegraded && orchestratorLink && ( { e.stopPropagation(); onMobileClose?.(); @@ -571,17 +578,14 @@ function ProjectSidebarInner({ role="menu" aria-label={`${project.name} actions`} > - {liveOrchestrator ? ( + {orchestratorLink ? (