Skip to content

Commit 0007635

Browse files
fix: pass directory context through schedules navigation, hoist SSE after directory resolve
1 parent 8b08444 commit 0007635

10 files changed

Lines changed: 94 additions & 46 deletions

File tree

frontend/src/components/schedules/RunDetailPanel.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@ import type { ScheduleRun } from '@opencode-manager/shared/types'
77

88
interface RunDetailPanelProps {
99
repoId: number
10+
directory?: string
1011
activeRun: ScheduleRun | null
1112
selectedRunLoading: boolean
1213
onCancelRun: () => void
1314
cancelRunPending: boolean
1415
}
1516

16-
export function RunDetailPanel({ repoId, activeRun, selectedRunLoading, onCancelRun, cancelRunPending }: RunDetailPanelProps) {
17+
export function RunDetailPanel({ repoId, directory, activeRun, selectedRunLoading, onCancelRun, cancelRunPending }: RunDetailPanelProps) {
1718
const navigate = useNavigate()
1819

1920
if (selectedRunLoading && !activeRun) {
@@ -41,7 +42,7 @@ export function RunDetailPanel({ repoId, activeRun, selectedRunLoading, onCancel
4142
<div className="flex items-center justify-between gap-2 px-3 py-2">
4243
<div className="flex items-center gap-2">
4344
{activeRun.sessionId && (
44-
<Button variant="outline" size="sm" onClick={() => navigate(`/repos/${repoId}/sessions/${activeRun.sessionId}`)}>
45+
<Button variant="outline" size="sm" onClick={() => navigate(`/repos/${repoId}/sessions/${activeRun.sessionId}${repoId === 0 ? '?assistant=1' : ''}`, { state: { directory } })}>
4546
Open session
4647
</Button>
4748
)}

frontend/src/components/schedules/RunHistoryCards.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { useRepoScheduleRun } from '@/hooks/useSchedules'
99
interface RunHistoryCardsProps {
1010
runs: ScheduleRun[] | undefined
1111
runsLoading: boolean
12+
directory?: string
1213
onSelectRun: (id: number) => void
1314
onCancelRun: () => void
1415
cancelRunPending: boolean
@@ -17,6 +18,7 @@ interface RunHistoryCardsProps {
1718
export function RunHistoryCards({
1819
runs,
1920
runsLoading,
21+
directory,
2022
onSelectRun,
2123
onCancelRun,
2224
cancelRunPending,
@@ -113,6 +115,7 @@ export function RunHistoryCards({
113115
<div className="flex flex-col min-h-0 flex-1 overflow-hidden">
114116
<RunDetailPanel
115117
repoId={run.repoId}
118+
directory={directory}
116119
activeRun={displayRun}
117120
selectedRunLoading={isExpanded && isLoading}
118121
onCancelRun={onCancelRun}

frontend/src/components/schedules/RunHistoryTab.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { RunHistoryCards, RunDetailPanel } from '@/components/schedules'
55

66
interface RunHistoryTabProps {
77
repoId: number
8+
directory?: string
89
selectedJob: ScheduleJob | undefined
910
runs: ScheduleRun[] | undefined
1011
runsLoading: boolean
@@ -17,6 +18,7 @@ interface RunHistoryTabProps {
1718

1819
export function RunHistoryTab({
1920
repoId,
21+
directory,
2022
selectedJob,
2123
runs,
2224
runsLoading,
@@ -53,6 +55,7 @@ export function RunHistoryTab({
5355
<RunHistoryCards
5456
runs={runs}
5557
runsLoading={runsLoading}
58+
directory={directory}
5659
onSelectRun={onSelectRun}
5760
onCancelRun={onCancelRun}
5861
cancelRunPending={cancelRunPending}
@@ -61,6 +64,7 @@ export function RunHistoryTab({
6164
<div className="hidden xl:flex min-h-0 flex-col overflow-hidden rounded-xl border border-border/60 bg-background/60 p-4">
6265
<RunDetailPanel
6366
repoId={repoId}
67+
directory={directory}
6468
activeRun={activeRun}
6569
selectedRunLoading={selectedRunLoading}
6670
onCancelRun={onCancelRun}

frontend/src/components/session/SessionList.test.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,11 @@ vi.mock('@/hooks/useOpenCode', () => ({
3636

3737
describe('SessionList', () => {
3838
beforeEach(() => {
39+
const now = Date.now()
3940
sessionsData.splice(0, sessionsData.length,
40-
{ id: 'ses_same', title: 'audit: mic-warmup 1/2 #2', directory: '/w/a', workspaceID: 'wrk_a', time: { updated: Date.now() } },
41-
{ id: 'ses_same', title: 'audit: mic-warmup 1/2 #2', directory: '/w/b', workspaceID: 'wrk_b', time: { updated: Date.now() } },
42-
{ id: 'ses_same', title: 'audit: mic-warmup 1/2 #2', directory: '/w/c', workspaceID: 'wrk_c', time: { updated: Date.now() } },
41+
{ id: 'ses_same', title: 'audit: mic-warmup 1/2 #2', directory: '/w/a', workspaceID: 'wrk_a', time: { updated: now } },
42+
{ id: 'ses_same', title: 'audit: mic-warmup 1/2 #2', directory: '/w/b', workspaceID: 'wrk_b', time: { updated: now - 1 } },
43+
{ id: 'ses_same', title: 'audit: mic-warmup 1/2 #2', directory: '/w/c', workspaceID: 'wrk_c', time: { updated: now - 2 } },
4344
)
4445
createSessionMock.mockReset()
4546
createSessionState.directory = undefined

frontend/src/contexts/EventContext.tsx

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,16 @@ export function EventProvider({ children }: { children: React.ReactNode }) {
269269
return repo?.id ?? null
270270
}, [repos, findSessionDirectory])
271271

272+
const navigateToSession = useCallback((sessionID: string) => {
273+
const repoId = getRepoIdForSession(sessionID)
274+
if (!repoId && repoId !== 0) return
275+
const directory = findSessionDirectory(sessionID) ?? undefined
276+
const targetPath = `/repos/${repoId}/sessions/${sessionID}${repoId === 0 ? '?assistant=1' : ''}`
277+
if (`${window.location.pathname}${window.location.search}` !== targetPath) {
278+
navigate(targetPath, { state: { directory } })
279+
}
280+
}, [findSessionDirectory, getRepoIdForSession, navigate])
281+
272282
const getClient = useCallback((sessionID: string): OpenCodeClient | null => {
273283
const result = findSessionInCache(sessionID)
274284
if (!result) return null
@@ -397,25 +407,13 @@ export function EventProvider({ children }: { children: React.ReactNode }) {
397407

398408
const navigateToCurrentQuestion = useCallback(() => {
399409
if (!currentQuestion) return
400-
const repoId = getRepoIdForSession(currentQuestion.sessionID)
401-
if (repoId) {
402-
const targetPath = `/repos/${repoId}/sessions/${currentQuestion.sessionID}`
403-
if (window.location.pathname !== targetPath) {
404-
navigate(targetPath)
405-
}
406-
}
407-
}, [currentQuestion, getRepoIdForSession, navigate])
410+
navigateToSession(currentQuestion.sessionID)
411+
}, [currentQuestion, navigateToSession])
408412

409413
const navigateToCurrentPermission = useCallback(() => {
410414
if (!currentPermission) return
411-
const repoId = getRepoIdForSession(currentPermission.sessionID)
412-
if (repoId) {
413-
const targetPath = `/repos/${repoId}/sessions/${currentPermission.sessionID}`
414-
if (window.location.pathname !== targetPath) {
415-
navigate(targetPath)
416-
}
417-
}
418-
}, [currentPermission, getRepoIdForSession, navigate])
415+
navigateToSession(currentPermission.sessionID)
416+
}, [currentPermission, navigateToSession])
419417

420418
const fetchInitialPendingData = useCallback(async () => {
421419
const reposToUse = reposRef.current

frontend/src/hooks/useCommandHandler.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,9 @@ export function useCommandHandler({
8282
const repoMatch = currentPath.match(/\/repos\/(\d+)\/sessions\//)
8383
if (repoMatch) {
8484
const repoId = repoMatch[1]
85-
const newPath = `/repos/${repoId}/sessions/${newSession.id}`
86-
navigate(newPath)
85+
const assistantSuffix = new URLSearchParams(window.location.search).get('assistant') === '1' ? '?assistant=1' : ''
86+
const newPath = `/repos/${repoId}/sessions/${newSession.id}${assistantSuffix}`
87+
navigate(newPath, { state: { directory } })
8788
} else {
8889
navigate(`/session/${newSession.id}`)
8990
}

frontend/src/pages/AssistantRedirect.tsx

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useQuery, useQueryClient } from "@tanstack/react-query"
44
import { getRepo } from "@/api/repos"
55
import { OpenCodeClient } from "@/api/opencode"
66
import { getCachedAssistantDirectory, setCachedAssistantSessionId, useAssistantSessionLauncher } from "@/hooks/useAssistantSessionLauncher"
7-
import { useCreateSession } from "@/hooks/useOpenCode"
7+
import { useCreateSession, useSessionsAcrossDirectories } from "@/hooks/useOpenCode"
88
import { useDialogParam } from "@/hooks/useDialogParam"
99
import { useSSE } from "@/hooks/useSSE"
1010
import { OPENCODE_API_ENDPOINT } from "@/config"
@@ -66,6 +66,16 @@ export function AssistantRedirect() {
6666

6767
const assistantDirectory = repo?.fullPath ?? cachedAssistantDirectory
6868
const assistantFileBasePath = assistantDirectory?.split('/').filter(Boolean).at(-1)
69+
const assistantSessionDirectories = showSessionList && assistantDirectory ? [assistantDirectory] : []
70+
const { data: assistantSessionsForWarmup } = useSessionsAcrossDirectories(opcodeUrl, assistantSessionDirectories, { limit: 25 })
71+
72+
const prefetchAssistantMessages = useCallback((sessionId: string, directory?: string) => {
73+
if (!directory) return
74+
void queryClient.prefetchQuery({
75+
queryKey: messagesQueryKey(opcodeUrl, sessionId, directory),
76+
queryFn: () => new OpenCodeClient(opcodeUrl, directory).listMessages(sessionId),
77+
})
78+
}, [opcodeUrl, queryClient])
6979

7080
const { openAssistant } = useAssistantSessionLauncher({
7181
repoId,
@@ -81,6 +91,7 @@ export function AssistantRedirect() {
8191
if (assistantDirectory) {
8292
setCachedAssistantSessionId(repoId, assistantDirectory, session.id)
8393
}
94+
prefetchAssistantMessages(session.id, assistantDirectory)
8495
navigate(`/repos/${repoId}/sessions/${session.id}?assistant=1`, { state: { directory: assistantDirectory } })
8596
})
8697

@@ -89,8 +100,17 @@ export function AssistantRedirect() {
89100
if (selectedDirectory) {
90101
setCachedAssistantSessionId(repoId, selectedDirectory, sessionId)
91102
}
103+
prefetchAssistantMessages(sessionId, selectedDirectory)
92104
navigate(`/repos/${repoId}/sessions/${sessionId}?assistant=1`, { state: { directory: selectedDirectory } })
93-
}, [assistantDirectory, navigate, repoId])
105+
}, [assistantDirectory, navigate, prefetchAssistantMessages, repoId])
106+
107+
useEffect(() => {
108+
if (!showSessionList || !assistantDirectory || assistantSessionsForWarmup.length === 0) return
109+
const session = assistantSessionsForWarmup.find((item) => !item.parentID) ?? assistantSessionsForWarmup[0]
110+
if (session?.id) {
111+
prefetchAssistantMessages(session.id, session.directory ?? assistantDirectory)
112+
}
113+
}, [assistantDirectory, assistantSessionsForWarmup, prefetchAssistantMessages, showSessionList])
94114

95115
const handleCreateSession = async () => {
96116
await createSessionMutation.mutateAsync({ agent: undefined })

frontend/src/pages/Schedules.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,7 @@ export function Schedules() {
284284
{repoScheduleTab === 'runs' && (
285285
<RunHistoryTab
286286
repoId={repoId}
287+
directory={scheduleTarget?.fullPath}
287288
selectedJob={selectedJob}
288289
runs={runs}
289290
runsLoading={runsLoading}

frontend/src/pages/SessionDetail.tsx

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ import { SessionTodoDisplay } from "@/components/message/SessionTodoDisplay";
5252
import { useDialogParam } from "@/hooks/useDialogParam";
5353
import { useSidebarAction } from "@/hooks/useSidebarAction";
5454
import { SessionMoreButton } from "@/components/navigation/SessionMoreButton";
55-
import { setCachedAssistantSessionId } from "@/hooks/useAssistantSessionLauncher";
55+
import { getCachedAssistantDirectory, setCachedAssistantSessionId } from "@/hooks/useAssistantSessionLauncher";
5656

5757
const compareMessageIds = (id1: string, id2: string): number => {
5858
const num1 = parseInt(id1, 10)
@@ -120,7 +120,8 @@ export function SessionDetail() {
120120

121121
const opcodeUrl = OPENCODE_API_ENDPOINT;
122122

123-
const repoDirectory = navigationDirectory ?? repo?.fullPath;
123+
const cachedAssistantDirectory = isAssistantSession ? getCachedAssistantDirectory(repoId) : undefined;
124+
const repoDirectory = navigationDirectory ?? repo?.fullPath ?? cachedAssistantDirectory;
124125
const sessionRouteSuffix = isAssistantSession ? '?assistant=1' : '';
125126

126127
useEffect(() => {
@@ -129,13 +130,13 @@ export function SessionDetail() {
129130
}
130131
}, [isAssistantSession, repoDirectory, repoId, sessionId]);
131132

132-
const { isConnected, isReconnecting } = useSSE(opcodeUrl, repoDirectory, sessionId);
133-
134133
const { data: rawMessages, isLoading: messagesLoading } = useMessages(opcodeUrl, sessionId, repoDirectory);
134+
const initialMessagesDirectory = repoDirectory && !messagesLoading ? repoDirectory : undefined;
135+
const { isConnected, isReconnecting } = useSSE(opcodeUrl, initialMessagesDirectory, sessionId);
135136
const { data: session } = useSession(
136137
opcodeUrl,
137138
sessionId,
138-
repoDirectory,
139+
initialMessagesDirectory,
139140
);
140141

141142
const messages = useMemo(() => {
@@ -161,7 +162,7 @@ export function SessionDetail() {
161162
const abortSession = useAbortSession(opcodeUrl, repoDirectory, sessionId);
162163
const updateSession = useUpdateSession(opcodeUrl, repoDirectory);
163164
const createSession = useCreateSession(opcodeUrl, repoDirectory);
164-
const { model, modelString } = useModelSelection(opcodeUrl, repoDirectory);
165+
const { model, modelString } = useModelSelection(opcodeUrl, initialMessagesDirectory);
165166
const isEditingMessage = useUIState((state) => state.isEditingMessage);
166167
const setActivePromptFileBasePath = useUIState((state) => state.setActivePromptFileBasePath);
167168
const { isEnabled: ttsEnabled } = useTTS();
@@ -217,20 +218,20 @@ export function SessionDetail() {
217218
}, [sessionId, minimizedQuestion])
218219

219220
const syncPendingActionsForSession = useCallback(async () => {
220-
if (!repoDirectory || !sessionId) return
221+
if (!initialMessagesDirectory || !sessionId) return
221222
await Promise.all([
222-
syncPermissionsForSession(repoDirectory, sessionId),
223-
syncQuestionsForSession(repoDirectory, sessionId),
223+
syncPermissionsForSession(initialMessagesDirectory, sessionId),
224+
syncQuestionsForSession(initialMessagesDirectory, sessionId),
224225
])
225-
}, [repoDirectory, sessionId, syncPermissionsForSession, syncQuestionsForSession])
226+
}, [initialMessagesDirectory, sessionId, syncPermissionsForSession, syncQuestionsForSession])
226227

227228
useQuery({
228-
queryKey: ['opencode', 'pending-actions', opcodeUrl, sessionId, repoDirectory],
229+
queryKey: ['opencode', 'pending-actions', opcodeUrl, sessionId, initialMessagesDirectory],
229230
queryFn: async () => {
230231
await syncPendingActionsForSession()
231232
return null
232233
},
233-
enabled: !!repoDirectory && !!sessionId,
234+
enabled: !!initialMessagesDirectory && !!sessionId,
234235
refetchOnMount: 'always',
235236
refetchOnReconnect: true,
236237
refetchOnWindowFocus: true,
@@ -475,13 +476,15 @@ export function SessionDetail() {
475476
<div className="flex items-center gap-1">
476477
<PendingActionsGroup />
477478
</div>
478-
<ContextUsageIndicator
479-
opcodeUrl={opcodeUrl}
480-
sessionID={sessionId}
481-
directory={repoDirectory}
482-
isConnected={isConnected}
483-
isReconnecting={isReconnecting}
484-
/>
479+
{initialMessagesDirectory && (
480+
<ContextUsageIndicator
481+
opcodeUrl={opcodeUrl}
482+
sessionID={sessionId}
483+
directory={initialMessagesDirectory}
484+
isConnected={isConnected}
485+
isReconnecting={isReconnecting}
486+
/>
487+
)}
485488
<SessionMoreButton />
486489
</Header.Actions>
487490
</Header>
@@ -508,7 +511,7 @@ export function SessionDetail() {
508511
/>
509512
) : null}
510513
</div>
511-
{opcodeUrl && repoDirectory && !isEditingMessage && (
514+
{opcodeUrl && initialMessagesDirectory && !isEditingMessage && (
512515
<div
513516
ref={promptOverlayRef}
514517
className="absolute left-0 right-0 flex justify-center"
@@ -563,7 +566,7 @@ export function SessionDetail() {
563566
<PromptInput
564567
ref={promptInputRef}
565568
opcodeUrl={opcodeUrl}
566-
directory={repoDirectory}
569+
directory={initialMessagesDirectory}
567570
sessionID={sessionId}
568571
disabled={!isConnected}
569572
showScrollButton={showScrollButton && !hasPromptContent}

frontend/src/pages/__tests__/SessionDetail.first-load-directory.test.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'
22
import { render, waitFor } from '@testing-library/react'
33
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
44
import { MemoryRouter, Route, Routes } from 'react-router-dom'
5+
import { setCachedAssistantSessionId } from '../../hooks/useAssistantSessionLauncher'
56
import { SessionDetail } from '../SessionDetail'
67

78
const mocks = vi.hoisted(() => ({
@@ -171,6 +172,7 @@ vi.mock('@/components/notifications/PendingActionsGroup', () => ({
171172
describe('SessionDetail first-load navigation directory', () => {
172173
beforeEach(() => {
173174
vi.clearAllMocks()
175+
localStorage.clear()
174176

175177
mocks.useSession.mockReturnValue({ data: undefined, isLoading: false })
176178
mocks.useMessages.mockReturnValue({ data: [], isLoading: false })
@@ -246,6 +248,20 @@ describe('SessionDetail first-load navigation directory', () => {
246248
expect(sessionCall?.[2]).toBeUndefined()
247249
})
248250

251+
it('uses the cached assistant directory on direct assistant session load while the repo is loading', async () => {
252+
setCachedAssistantSessionId(0, '/abs/assistant', 'sess-assistant')
253+
254+
renderSession('/repos/0/sessions/sess-assistant?assistant=1')
255+
256+
await waitFor(() => {
257+
const call = mocks.useMessages.mock.calls[mocks.useMessages.mock.calls.length - 1]
258+
expect(call?.[2]).toBe('/abs/assistant')
259+
})
260+
261+
const sessionCall = mocks.useSession.mock.calls[mocks.useSession.mock.calls.length - 1]
262+
expect(sessionCall?.[2]).toBe('/abs/assistant')
263+
})
264+
249265
it('renders messages instead of the skeleton when assistant navigation provides directory while the repo is loading', async () => {
250266
const { queryByText, getByText } = renderSession({
251267
pathname: '/repos/0/sessions/sess-assistant',

0 commit comments

Comments
 (0)