From cbc96475f92762609ced047692b32e9ed482d4b6 Mon Sep 17 00:00:00 2001 From: i-trytoohard <193449657+i-trytoohard@users.noreply.github.com> Date: Mon, 18 May 2026 00:07:25 +0530 Subject: [PATCH] fix(web): simplify sidebar error handling --- packages/web/src/app/sessions/[id]/page.tsx | 8 +-- packages/web/src/components/Dashboard.tsx | 6 +- .../web/src/components/ProjectSidebar.tsx | 52 +++++++++++---- .../web/src/components/PullRequestsPage.tsx | 4 +- .../__tests__/ProjectSidebar.test.tsx | 59 ++++++++++++++++++ .../__tests__/useSessionEvents.mux.test.ts | 2 +- packages/web/src/hooks/useSessionEvents.ts | Bin 12994 -> 13258 bytes 7 files changed, 112 insertions(+), 19 deletions(-) diff --git a/packages/web/src/app/sessions/[id]/page.tsx b/packages/web/src/app/sessions/[id]/page.tsx index a69d80e9c..30416bd7a 100644 --- a/packages/web/src/app/sessions/[id]/page.tsx +++ b/packages/web/src/app/sessions/[id]/page.tsx @@ -204,7 +204,7 @@ function SessionPageShell({ sidebarSessions: DashboardSession[] | null; sidebarOrchestrators?: ProjectSidebarOrchestrator[]; sidebarLoading: boolean; - sidebarError: boolean; + sidebarError: string | null; onRetrySidebar: () => void; activeProjectId?: string; activeSessionId?: string; @@ -410,7 +410,7 @@ export default function SessionPage() { const [loading, setLoading] = useState(cachedSession === null); const [routeError, setRouteError] = useState(null); const [sessionMissing, setSessionMissing] = useState(false); - const [sidebarError, setSidebarError] = useState(false); + const [sidebarError, setSidebarError] = useState(null); const [prefixByProject, setPrefixByProject] = useState>(new Map()); const sessionProjectId = session?.projectId ?? null; const allPrefixes = [...prefixByProject.values()]; @@ -689,7 +689,7 @@ export default function SessionPage() { applyMuxSessionPatches(restSessions, pendingMuxSessionsRef.current ?? []) ?? restSessions; cachedSidebarSessions = nextSessions; setSidebarOrchestrators(body?.orchestrators); - setSidebarError(false); + setSidebarError(null); setSidebarSessions((current) => areSidebarSessionsEqual(current, nextSessions) ? current : nextSessions, ); @@ -698,7 +698,7 @@ export default function SessionPage() { return; } console.error("Failed to fetch sidebar sessions:", err); - setSidebarError(true); + setSidebarError(err instanceof Error ? err.message : String(err)); setSidebarSessions((current) => (current === null ? [] : current)); } finally { fetchingSidebarRef.current = false; diff --git a/packages/web/src/components/Dashboard.tsx b/packages/web/src/components/Dashboard.tsx index c270831f1..dabf1d42f 100644 --- a/packages/web/src/components/Dashboard.tsx +++ b/packages/web/src/components/Dashboard.tsx @@ -203,8 +203,9 @@ function DashboardInner({ : "reconnecting"; const recoveredFromLoadError = Boolean(dashboardLoadError) && liveSessionsResolved; const ssrLoadError = recoveredFromLoadError ? undefined : dashboardLoadError; - // Live WS error takes precedence; fall back to SSR load error when live data hasn't resolved it. - const visibleLoadError = loadError ?? ssrLoadError; + // Live transport errors only block the main dashboard when no session data is visible. + const visibleLoadError = sessions.length === 0 ? (loadError ?? ssrLoadError) : ssrLoadError; + const sidebarError = loadError ?? (ssrLoadError && sessions.length === 0 ? ssrLoadError : null); const searchParams = useSearchParams(); const router = useRouter(); const routerRef = useRef(router); @@ -858,6 +859,7 @@ function DashboardInner({ activeProjectId={projectId} activeSessionId={activeSessionId} loading={!liveSessionsResolved} + error={sidebarError} collapsed={sidebarCollapsed} onToggleCollapsed={() => setSidebarCollapsed((v) => !v)} onMobileClose={() => setMobileSidebarOpen(false)} diff --git a/packages/web/src/components/ProjectSidebar.tsx b/packages/web/src/components/ProjectSidebar.tsx index c6b29a7f6..e5b5ce7a0 100644 --- a/packages/web/src/components/ProjectSidebar.tsx +++ b/packages/web/src/components/ProjectSidebar.tsx @@ -33,7 +33,7 @@ interface ProjectSidebarProps { activeProjectId: string | undefined; activeSessionId: string | undefined; loading?: boolean; - error?: boolean; + error?: string | null; onRetry?: () => void; collapsed?: boolean; onToggleCollapsed?: () => void; @@ -42,6 +42,18 @@ interface ProjectSidebarProps { type SessionDotLevel = "respond" | "review" | "action" | "pending" | "working" | "merge" | "done"; +function isTransientSessionListError(error: string | null | undefined): boolean { + if (!error) return false; + const normalized = error.toLowerCase(); + return ( + normalized.includes("timed out") || + normalized.includes("timeout") || + normalized.includes("failed to fetch") || + normalized.includes("networkerror") || + normalized.includes("network error") + ); +} + const SessionDot = memo(function SessionDot({ level }: { level: SessionDotLevel }) { return (
>( () => @@ -701,9 +714,14 @@ function ProjectSidebarInner({ {error && sessions && sessions.length > 0 ? (
- Failed to refresh · showing cached sessions + + Failed to refresh · showing cached sessions + + {error} + + {onRetry ? (
+
- Retry - + {error} +
+ {onRetry ? ( + + ) : null}
) : (
diff --git a/packages/web/src/components/PullRequestsPage.tsx b/packages/web/src/components/PullRequestsPage.tsx index 8f5bd0363..8cd7ae77e 100644 --- a/packages/web/src/components/PullRequestsPage.tsx +++ b/packages/web/src/components/PullRequestsPage.tsx @@ -63,10 +63,11 @@ export function PullRequestsPage({ } return levels; }, [initialSessions, attentionZones]); - const { sessions, attentionLevels } = useSessionEvents({ + const { sessions, attentionLevels, loadError } = useSessionEvents({ initialSessions, project: projectId, muxSessions: mux?.status === "connected" ? mux.sessions : undefined, + muxLastError: mux?.lastError, initialAttentionLevels, attentionZones, }); @@ -118,6 +119,7 @@ export function PullRequestsPage({ orchestrators={orchestratorLinks} activeProjectId={projectId} activeSessionId={undefined} + error={loadError} collapsed={sidebarCollapsed} onToggleCollapsed={() => setSidebarCollapsed((current) => !current)} onMobileClose={() => setMobileMenuOpen(false)} diff --git a/packages/web/src/components/__tests__/ProjectSidebar.test.tsx b/packages/web/src/components/__tests__/ProjectSidebar.test.tsx index 6f7150c37..5633d4b0f 100644 --- a/packages/web/src/components/__tests__/ProjectSidebar.test.tsx +++ b/packages/web/src/components/__tests__/ProjectSidebar.test.tsx @@ -123,6 +123,65 @@ describe("ProjectSidebar", () => { expect(screen.getByRole("button", { name: /new project/i })).toBeInTheDocument(); }); + it("shows a first-load failure only for the empty session state", () => { + render( + , + ); + + expect(screen.getByText("Failed to load sessions")).toBeInTheDocument(); + expect(screen.getByText("HTTP 500")).toBeInTheDocument(); + expect( + screen.queryByText("Failed to refresh · showing cached sessions"), + ).not.toBeInTheDocument(); + }); + + it("shows transient empty-sidebar failures as temporary instead of hard first-load failures", () => { + render( + , + ); + + expect(screen.getByText("Sessions temporarily unavailable")).toBeInTheDocument(); + expect(screen.queryByText("Failed to load sessions")).not.toBeInTheDocument(); + expect(screen.getByText("Sidebar sessions request timed out after 6000ms")).toBeInTheDocument(); + }); + + it("shows a cached-data refresh banner without replacing visible sessions", () => { + render( + , + ); + + expect(screen.getByText("Failed to refresh · showing cached sessions")).toBeInTheDocument(); + expect(screen.getByText("GitHub API rate limited")).toBeInTheDocument(); + expect(screen.queryByText("Failed to load sessions")).not.toBeInTheDocument(); + expect(screen.getByText("feat/cached")).toBeInTheDocument(); + }); + it("marks the active project row as the current page", () => { render( { afterEach(() => { vi.unstubAllGlobals(); vi.clearAllTimers(); + vi.useRealTimers(); }); it("triggers refresh when mux patch contains unknown id", async () => { @@ -107,6 +108,5 @@ describe("useSessionEvents - mux", () => { "[useSessionEvents] refresh failed:", expect.anything(), ); - vi.useRealTimers(); }); }); diff --git a/packages/web/src/hooks/useSessionEvents.ts b/packages/web/src/hooks/useSessionEvents.ts index afe4aed7cd16b756dc14113917a7612cc103fe26..0cbb0d5f0dca8ffa45e2924161dab68a78385661 100644 GIT binary patch delta 226 zcmX?3@NF0QoFyyTM1 z{5+shDW%D&MH-uraRzb7D5$9c?MW<2)yv6GOo13@Yip~JSDKSESwhHlvJkiM