Skip to content

Commit 8a8bdc1

Browse files
fix(session): handle 404 with not-found fallbacks and add SSE-gated message polling (#316)
1 parent 632ce13 commit 8a8bdc1

5 files changed

Lines changed: 57 additions & 4 deletions

File tree

frontend/src/components/session/SessionCard.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export const SessionCard = ({
5757
}`}
5858
>
5959
<button
60+
aria-label="Delete session"
6061
className="h-full w-full flex items-center justify-center text-white hover:bg-red-700"
6162
onClick={handleDeleteClick}
6263
>
@@ -174,6 +175,7 @@ export const SessionCard = ({
174175
)}
175176
{manageMode && (
176177
<button
178+
aria-label="Delete session"
177179
className="h-6 w-6 p-0 text-foreground hover:text-red-600 dark:hover:text-red-400 bg-transparent border-none cursor-pointer"
178180
onClick={(e) => {
179181
e.stopPropagation();

frontend/src/hooks/useOpenCode.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ export const useSession = (opcodeUrl: string | null | undefined, sessionID: stri
138138
refetchOnWindowFocus: true,
139139
refetchOnReconnect: true,
140140
staleTime: 15000,
141+
retry: (failureCount, error) => !(error instanceof FetchError && error.statusCode === 404) && failureCount < 3,
141142
});
142143
};
143144

@@ -158,6 +159,7 @@ export const useMessages = (opcodeUrl: string | null | undefined, sessionID: str
158159
staleTime: 30000,
159160
gcTime: 10 * 60 * 1000,
160161
refetchInterval: opts?.fallbackPoll ? 5000 : undefined,
162+
retry: (failureCount, error) => !(error instanceof FetchError && error.statusCode === 404) && failureCount < 3,
161163
});
162164
};
163165

frontend/src/pages/AssistantRedirect.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export function AssistantRedirect() {
6262
<Plus className="w-4 h-4 mr-2" />
6363
<span>New Session</span>
6464
</Button>
65-
<Button onClick={() => handleCreateSession()} disabled={!opcodeUrl || !assistantDirectory || createSessionMutation.isPending} size="sm" className="sm:hidden h-10 w-10 p-0 bg-blue-600 hover:bg-blue-700 text-white transition-all duration-200 hover:scale-105">
65+
<Button onClick={() => handleCreateSession()} disabled={!opcodeUrl || !assistantDirectory || createSessionMutation.isPending} aria-label="New Session" size="sm" className="sm:hidden h-10 w-10 p-0 bg-blue-600 hover:bg-blue-700 text-white transition-all duration-200 hover:scale-105">
6666
<Plus className="w-5 h-5" />
6767
</Button>
6868
</Header.Actions>

frontend/src/pages/SessionDetail.tsx

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { X, CornerUpLeft } from "lucide-react";
99
import { Header } from "@/components/ui/header";
1010
import { SessionList } from "@/components/session/SessionList";
1111
import { getSessionListPath } from '@/lib/navigation'
12+
import { FetchError } from '@/api/fetchWrapper'
1213

1314
import { FileBrowserSheet } from "@/components/file-browser/FileBrowserSheet";
1415
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
@@ -63,6 +64,18 @@ const compareMessageIds = (id1: string, id2: string): number => {
6364
const PENDING_ACTION_SYNC_INTERVAL_MS = 30000
6465
const PROMPT_OVERLAY_CLEARANCE_PX = 16
6566

67+
function SessionRouteFallback({ message, backTo, backLabel }: { message: string; backTo: string; backLabel: string }) {
68+
const navigate = useNavigate();
69+
return (
70+
<div className="flex items-center justify-center min-h-screen bg-gradient-to-br from-background via-background to-background">
71+
<div className="flex flex-col items-center gap-3 text-center">
72+
<span className="text-muted-foreground">{message}</span>
73+
<Button variant="outline" size="sm" onClick={() => navigate(backTo)}>{backLabel}</Button>
74+
</div>
75+
</div>
76+
);
77+
}
78+
6679
export function SessionDetail() {
6780
const { id, sessionId } = useParams<{ id: string; sessionId: string }>();
6881
const navigate = useNavigate();
@@ -112,6 +125,7 @@ export function SessionDetail() {
112125
queryKey: ["repo", repoId],
113126
queryFn: () => getRepo(repoId),
114127
enabled: id !== undefined,
128+
retry: (failureCount, error) => !(error instanceof FetchError && error.statusCode === 404) && failureCount < 3,
115129
});
116130

117131
useRepoActivity(repoId, Boolean(repo));
@@ -123,8 +137,8 @@ export function SessionDetail() {
123137

124138
const { isConnected, isReconnecting } = useSSE(opcodeUrl, repoDirectory, sessionId);
125139

126-
const { data: rawMessages, isLoading: messagesLoading } = useMessages(opcodeUrl, sessionId, repoDirectory);
127-
const { data: session, isLoading: sessionLoading } = useSession(
140+
const { data: rawMessages, isLoading: messagesLoading } = useMessages(opcodeUrl, sessionId, repoDirectory, { fallbackPoll: !isConnected });
141+
const { data: session, isLoading: sessionLoading, error: sessionQueryError } = useSession(
128142
opcodeUrl,
129143
sessionId,
130144
repoDirectory,
@@ -408,7 +422,7 @@ export function SessionDetail() {
408422
return <Navigate to="/" replace />;
409423
}
410424

411-
if (!repo && !isAssistantSession) {
425+
if (!isAssistantSession && repoLoading) {
412426
return (
413427
<div className="flex items-center justify-center min-h-screen bg-gradient-to-br from-background via-background to-background">
414428
<div className="flex flex-col items-center gap-2">
@@ -419,6 +433,21 @@ export function SessionDetail() {
419433
);
420434
}
421435

436+
if (!isAssistantSession && !repo) {
437+
return <SessionRouteFallback message="Repository not found" backTo="/" backLabel="Back to repositories" />;
438+
}
439+
440+
if (sessionQueryError instanceof FetchError && sessionQueryError.statusCode === 404) {
441+
const listTab = new URLSearchParams(location.search).get('repoTab') ?? undefined;
442+
return (
443+
<SessionRouteFallback
444+
message="Session not found"
445+
backTo={getSessionListPath(repoId, isAssistantSession, listTab)}
446+
backLabel="Back to sessions"
447+
/>
448+
);
449+
}
450+
422451
const workspaceDisplayName = isAssistantSession || !repo
423452
? 'Assistant'
424453
: getRepoDisplayName(repo);

frontend/src/pages/__tests__/SessionDetail.polling.test.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,4 +245,24 @@ describe('SessionDetail pending-actions polling gating', () => {
245245
const query = findPendingActionsQuery(queryClient)
246246
expect((query?.options as { refetchInterval?: unknown }).refetchInterval).toBe(false)
247247
})
248+
249+
it('requests message fallback polling while the SSE stream is disconnected', async () => {
250+
mocks.useSSE.mockReturnValue({ isConnected: false, isReconnecting: true })
251+
252+
const queryClient = createQueryClient()
253+
renderSessionDetail(queryClient)
254+
255+
const calls = mocks.useMessages.mock.calls
256+
expect(calls[calls.length - 1][3]).toEqual({ fallbackPoll: true })
257+
})
258+
259+
it('does not request message fallback polling while the SSE stream is connected', async () => {
260+
mocks.useSSE.mockReturnValue({ isConnected: true, isReconnecting: false })
261+
262+
const queryClient = createQueryClient()
263+
renderSessionDetail(queryClient)
264+
265+
const calls = mocks.useMessages.mock.calls
266+
expect(calls[calls.length - 1][3]).toEqual({ fallbackPoll: false })
267+
})
248268
})

0 commit comments

Comments
 (0)