Skip to content

Commit 0469ab8

Browse files
fix: cache assistant directory separately to handle repo-not-loaded edge case
1 parent 23931e9 commit 0469ab8

5 files changed

Lines changed: 66 additions & 16 deletions

File tree

frontend/src/hooks/useAssistantSessionLauncher.test.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { renderHook, act } from '@testing-library/react'
22
import { describe, it, expect, beforeEach, vi } from 'vitest'
3-
import { useAssistantSessionLauncher } from './useAssistantSessionLauncher'
3+
import { getCachedAssistantDirectory, setCachedAssistantSessionId, useAssistantSessionLauncher } from './useAssistantSessionLauncher'
44
import { OpenCodeClient } from '@/api/opencode'
55

66
const mocks = vi.hoisted(() => ({
@@ -111,6 +111,12 @@ describe('useAssistantSessionLauncher', () => {
111111
expect(mocks.sendPromptAsync).not.toHaveBeenCalled()
112112
})
113113

114+
it('reads the cached assistant directory from the cached session key', () => {
115+
setCachedAssistantSessionId(123, '/assistant', 'cached')
116+
117+
expect(getCachedAssistantDirectory(123)).toBe('/assistant')
118+
})
119+
114120
it('uses the cache-miss callback without querying OpenCode', async () => {
115121
const onNavigate = vi.fn()
116122
const onMissingCachedSession = vi.fn()

frontend/src/hooks/useAssistantSessionLauncher.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ type OpenCodeSession = components['schemas']['Session']
1515
const ASSISTANT_SESSION_LOOKUP_PAGE_SIZE = 25
1616

1717
const LAST_ASSISTANT_SESSION_KEY_PREFIX = 'ocm:assistant:last-session'
18+
const LAST_ASSISTANT_DIRECTORY_KEY_PREFIX = 'ocm:assistant:last-directory'
1819

1920
function getLastAssistantSessionKey(repoId: number, directory: string): string {
2021
return `${LAST_ASSISTANT_SESSION_KEY_PREFIX}:${repoId}:${directory}`
@@ -23,6 +24,7 @@ function getLastAssistantSessionKey(repoId: number, directory: string): string {
2324
export function setCachedAssistantSessionId(repoId: number, directory: string, sessionId: string): void {
2425
try {
2526
localStorage.setItem(getLastAssistantSessionKey(repoId, directory), sessionId)
27+
localStorage.setItem(`${LAST_ASSISTANT_DIRECTORY_KEY_PREFIX}:${repoId}`, directory)
2628
} catch {
2729
return
2830
}
@@ -36,6 +38,25 @@ function getCachedAssistantSessionId(repoId: number, directory: string): string
3638
}
3739
}
3840

41+
export function getCachedAssistantDirectory(repoId: number): string | undefined {
42+
try {
43+
const cachedDirectory = localStorage.getItem(`${LAST_ASSISTANT_DIRECTORY_KEY_PREFIX}:${repoId}`)
44+
if (cachedDirectory) return cachedDirectory
45+
46+
const prefix = `${LAST_ASSISTANT_SESSION_KEY_PREFIX}:${repoId}:`
47+
const storageKeys = [
48+
...Array.from({ length: localStorage.length }, (_, index) => localStorage.key(index)),
49+
...Object.keys(localStorage),
50+
]
51+
for (const key of storageKeys) {
52+
if (key?.startsWith(prefix)) return key.slice(prefix.length)
53+
}
54+
return undefined
55+
} catch {
56+
return undefined
57+
}
58+
}
59+
3960
function isAssistantRootSession(session: OpenCodeSession, assistantDirectory: string): boolean {
4061
return !session.parentID && session.directory === assistantDirectory
4162
}

frontend/src/pages/AssistantRedirect.tsx

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { useNavigate, useLocation } from "react-router-dom"
33
import { useQuery, useQueryClient } from "@tanstack/react-query"
44
import { getRepo } from "@/api/repos"
55
import { OpenCodeClient } from "@/api/opencode"
6-
import { setCachedAssistantSessionId, useAssistantSessionLauncher } from "@/hooks/useAssistantSessionLauncher"
6+
import { getCachedAssistantDirectory, setCachedAssistantSessionId, useAssistantSessionLauncher } from "@/hooks/useAssistantSessionLauncher"
77
import { useCreateSession } from "@/hooks/useOpenCode"
88
import { useDialogParam } from "@/hooks/useDialogParam"
99
import { useSSE } from "@/hooks/useSSE"
@@ -36,6 +36,7 @@ export function AssistantRedirect() {
3636
const [switchConfigOpen, setSwitchConfigOpen] = useState(false)
3737
const [status, setStatus] = useState<"preparing" | "opening" | "creating" | "error">("preparing")
3838
const [errorMessage, setErrorMessage] = useState<string | null>(null)
39+
const cachedAssistantDirectory = getCachedAssistantDirectory(repoId)
3940

4041
const opcodeUrl = OPENCODE_API_ENDPOINT
4142
const { data: repo, isLoading: repoLoading, error: repoError } = useQuery({
@@ -44,19 +45,20 @@ export function AssistantRedirect() {
4445
})
4546

4647
const handleNavigate = useCallback((sessionId: string) => {
47-
if (repo?.fullPath) {
48-
setCachedAssistantSessionId(repoId, repo.fullPath, sessionId)
48+
const directory = repo?.fullPath ?? cachedAssistantDirectory
49+
if (directory) {
50+
setCachedAssistantSessionId(repoId, directory, sessionId)
4951
void queryClient.prefetchQuery({
50-
queryKey: messagesQueryKey(opcodeUrl, sessionId, repo.fullPath),
51-
queryFn: () => new OpenCodeClient(opcodeUrl, repo.fullPath).listMessages(sessionId),
52+
queryKey: messagesQueryKey(opcodeUrl, sessionId, directory),
53+
queryFn: () => new OpenCodeClient(opcodeUrl, directory).listMessages(sessionId),
5254
})
5355
}
5456
setStatus("opening")
5557
if (!showSessionList) {
5658
window.history.replaceState(window.history.state, "", getSessionListPath(repoId, true))
5759
}
58-
navigate(`/repos/${repoId}/sessions/${sessionId}?assistant=1`, { state: { directory: repo?.fullPath } })
59-
}, [navigate, opcodeUrl, queryClient, repo?.fullPath, repoId, showSessionList])
60+
navigate(`/repos/${repoId}/sessions/${sessionId}?assistant=1`, { state: { directory } })
61+
}, [cachedAssistantDirectory, navigate, opcodeUrl, queryClient, repo?.fullPath, repoId, showSessionList])
6062

6163
const handleMissingCachedSession = useCallback(() => {
6264
navigate(getAssistantSessionListPath(), { replace: true })
@@ -70,7 +72,7 @@ export function AssistantRedirect() {
7072
onMissingCachedSession: handleMissingCachedSession,
7173
})
7274

73-
const assistantDirectory = repo?.fullPath
75+
const assistantDirectory = repo?.fullPath ?? cachedAssistantDirectory
7476
const assistantFileBasePath = assistantDirectory?.split('/').filter(Boolean).at(-1)
7577

7678
useSSE(opcodeUrl, assistantDirectory)
@@ -101,8 +103,8 @@ export function AssistantRedirect() {
101103
try {
102104
if (showSessionList) return
103105
setStatus("preparing")
104-
if (repoLoading) return
105-
if (repoError || !repo?.fullPath) throw new Error("Failed to load Assistant workspace")
106+
if (repoLoading && !assistantDirectory) return
107+
if ((repoError && !assistantDirectory) || !assistantDirectory) throw new Error("Failed to load Assistant workspace")
106108
if (cancelled) return
107109
setStatus("creating")
108110
await openAssistant()
@@ -118,7 +120,7 @@ export function AssistantRedirect() {
118120
return () => {
119121
cancelled = true
120122
}
121-
}, [repo?.fullPath, repoError, repoLoading, openAssistant, showSessionList])
123+
}, [assistantDirectory, repoError, repoLoading, openAssistant, showSessionList])
122124

123125
if (showSessionList) {
124126
return (
@@ -140,9 +142,9 @@ export function AssistantRedirect() {
140142
</Header.Actions>
141143
</Header>
142144
<div className="flex-1 flex flex-col min-h-0">
143-
{repoError ? (
145+
{repoError && !assistantDirectory ? (
144146
<div className="p-4 text-sm text-muted-foreground">Failed to load Assistant sessions</div>
145-
) : repoLoading || !repo?.fullPath ? (
147+
) : repoLoading && !assistantDirectory ? (
146148
<div className="p-4 text-sm text-muted-foreground">Loading Assistant sessions...</div>
147149
) : (
148150
<SessionList

frontend/src/pages/SessionDetail.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,7 @@ export function SessionDetail() {
419419
return <Navigate to="/" replace />;
420420
}
421421

422-
if (!repo && !isAssistantSession) {
422+
if (!repo && !isAssistantSession && !repoDirectory) {
423423
return (
424424
<div className="flex items-center justify-center min-h-screen bg-gradient-to-br from-background via-background to-background">
425425
<div className="flex flex-col items-center gap-2">
@@ -493,7 +493,7 @@ export function SessionDetail() {
493493

494494
<div className="relative flex-1 overflow-hidden flex flex-col">
495495
<div key={sessionId} ref={messageContainerRef} className="flex-1 overflow-y-auto overflow-x-hidden overscroll-contain [mask-image:linear-gradient(to_bottom,transparent,black_16px,black)]" style={{ paddingBottom: promptOverlayHeight + inputBottomOffset + PROMPT_OVERLAY_CLEARANCE_PX }}>
496-
{repoLoading || !repoDirectory || sessionLoading || messagesLoading ? (
496+
{(!repoDirectory && repoLoading) || !repoDirectory || sessionLoading || messagesLoading ? (
497497
<MessageSkeleton />
498498
) : opcodeUrl && repoDirectory ? (
499499
<MessageThread

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,14 @@ vi.mock('@/components/file-browser/FileBrowserSheet', () => ({
128128
FileBrowserSheet: vi.fn(() => null),
129129
}))
130130

131+
vi.mock('@/components/message/MessageSkeleton', () => ({
132+
MessageSkeleton: vi.fn(() => <div>Messages loading skeleton</div>),
133+
}))
134+
135+
vi.mock('@/components/message/MessageThread', () => ({
136+
MessageThread: vi.fn(() => <div>Messages rendered</div>),
137+
}))
138+
131139
vi.mock('@/components/repo/RepoMcpDialog', () => ({
132140
RepoMcpDialog: vi.fn(() => null),
133141
}))
@@ -237,4 +245,17 @@ describe('SessionDetail first-load navigation directory', () => {
237245
const sessionCall = mocks.useSession.mock.calls[mocks.useSession.mock.calls.length - 1]
238246
expect(sessionCall?.[2]).toBeUndefined()
239247
})
248+
249+
it('renders messages instead of the skeleton when assistant navigation provides directory while the repo is loading', async () => {
250+
const { queryByText, getByText } = renderSession({
251+
pathname: '/repos/0/sessions/sess-assistant',
252+
state: { directory: '/abs/assistant' },
253+
})
254+
255+
await waitFor(() => {
256+
expect(getByText('Messages rendered')).toBeTruthy()
257+
})
258+
259+
expect(queryByText('Messages loading skeleton')).toBeNull()
260+
})
240261
})

0 commit comments

Comments
 (0)