diff --git a/packages/ui/src/components/chat/ChatContainer.tsx b/packages/ui/src/components/chat/ChatContainer.tsx index b36c5168..f90762e3 100644 --- a/packages/ui/src/components/chat/ChatContainer.tsx +++ b/packages/ui/src/components/chat/ChatContainer.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { RiArrowLeftLine } from '@remixicon/react'; -import type { Message, Part } from '@opencode-ai/sdk/v2'; +import type { Message, Part, Session } from '@opencode-ai/sdk/v2'; import { ChatInput } from './ChatInput'; import { useUIStore } from '@/stores/useUIStore'; @@ -191,7 +191,8 @@ export const ChatContainer: React.FC = () => { const handleReturnToParentSession = React.useCallback(() => { if (!parentSession) return; - setCurrentSession(parentSession.id); + const parentDirectory = (parentSession as Session & { directory?: string | null }).directory ?? null; + setCurrentSession(parentSession.id, parentDirectory); }, [parentSession, setCurrentSession]); const returnToParentButton = parentSession ? ( diff --git a/packages/ui/src/components/layout/Header.tsx b/packages/ui/src/components/layout/Header.tsx index 6dce9625..11329d24 100644 --- a/packages/ui/src/components/layout/Header.tsx +++ b/packages/ui/src/components/layout/Header.tsx @@ -29,6 +29,7 @@ import { useDirectoryStore } from '@/stores/useDirectoryStore'; import { useFilesViewTabsStore } from '@/stores/useFilesViewTabsStore'; import { useGitStore } from '@/stores/useGitStore'; import { useGitHubPrStatusStore } from '@/stores/useGitHubPrStatusStore'; +import { useGlobalSessionsStore } from '@/stores/useGlobalSessionsStore'; import { useGitHubAuthStore } from '@/stores/useGitHubAuthStore'; import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs'; @@ -65,6 +66,7 @@ import { ProjectActionsButton } from '@/components/layout/ProjectActionsButton'; import { isDesktopShell, isVSCodeRuntime } from '@/lib/desktop'; import { desktopHostsGet, locationMatchesHost, redactSensitiveUrl } from '@/lib/desktopHosts'; import { resolveSessionDiffStats } from '@/components/session/sidebar/utils'; +import type { Session } from '@opencode-ai/sdk/v2/client'; const isSameContextUsage = ( @@ -250,6 +252,7 @@ export const Header: React.FC = ({ const currentSessionMessageRecords = useSessionMessageRecords(currentSessionId ?? ''); const currentSessionMessages = currentSessionId ? (currentSessionMessageRecords.length > 0 ? currentSessionMessageRecords : undefined) : undefined; const sessions = useSessions(); + const globalActiveSessions = useGlobalSessionsStore((state) => state.activeSessions); const activeProject = useProjectsStore((state) => { if (!state.activeProjectId) { return null; @@ -561,16 +564,82 @@ export const Header: React.FC = ({ }); }, [fetchAllQuotas, isUsageRefreshSpinning]); - const currentSession = React.useMemo(() => { + const currentSessionLive = React.useMemo(() => { if (!currentSessionId) return null; - // Try current directory's store first, then fall back to all child stores. - // The sidebar loads sessions globally via SDK, but the header uses - // useSessions() which only has the current directory. This fallback - // ensures the title/directory show when the session lives elsewhere. - return sessions.find((s) => s.id === currentSessionId) + // Resolve from the global sessions snapshot first (same source as sidebar). + // Child-store lists are intentionally partial/truncated during bootstrap. + return globalActiveSessions.find((s) => s.id === currentSessionId) + ?? sessions.find((s) => s.id === currentSessionId) ?? getAllSyncSessions().find((s) => s.id === currentSessionId) ?? null; - }, [currentSessionId, sessions]); + }, [currentSessionId, globalActiveSessions, sessions]); + + const lastResolvedSessionRef = React.useRef<{ + sessionId: string; + session: Session; + expiresAt: number; + } | null>(null); + const [sessionFallbackVersion, setSessionFallbackVersion] = React.useState(0); + + React.useEffect(() => { + if (!currentSessionId) { + if (lastResolvedSessionRef.current) { + lastResolvedSessionRef.current = null; + setSessionFallbackVersion((value) => value + 1); + } + return; + } + + if (currentSessionLive) { + lastResolvedSessionRef.current = { + sessionId: currentSessionId, + session: currentSessionLive, + expiresAt: Date.now() + 2000, + }; + return; + } + + const cached = lastResolvedSessionRef.current; + if (!cached || cached.sessionId !== currentSessionId) { + return; + } + + const remainingMs = cached.expiresAt - Date.now(); + if (remainingMs <= 0) { + lastResolvedSessionRef.current = null; + setSessionFallbackVersion((value) => value + 1); + return; + } + + const timeoutId = window.setTimeout(() => { + if (lastResolvedSessionRef.current?.sessionId === currentSessionId) { + lastResolvedSessionRef.current = null; + } + setSessionFallbackVersion((value) => value + 1); + }, remainingMs); + + return () => { + window.clearTimeout(timeoutId); + }; + }, [currentSessionId, currentSessionLive]); + + void sessionFallbackVersion; + const currentSession = (() => { + if (currentSessionLive) { + return currentSessionLive; + } + + if (!currentSessionId) { + return null; + } + + const cached = lastResolvedSessionRef.current; + if (cached && cached.sessionId === currentSessionId && cached.expiresAt > Date.now()) { + return cached.session; + } + + return null; + })(); const worktreePath = useSessionUIStore((state) => { if (!currentSessionId) return ''; diff --git a/packages/ui/src/components/session/sidebar/hooks/useSessionActions.ts b/packages/ui/src/components/session/sidebar/hooks/useSessionActions.ts index 210ab5a2..054ee129 100644 --- a/packages/ui/src/components/session/sidebar/hooks/useSessionActions.ts +++ b/packages/ui/src/components/session/sidebar/hooks/useSessionActions.ts @@ -24,7 +24,7 @@ type Args = { setDirectory: (directory: string, options?: { showOverlay?: boolean }) => void; setActiveMainTab: (tab: 'chat' | 'plan' | 'git' | 'diff' | 'terminal' | 'files') => void; setSessionSwitcherOpen: (open: boolean) => void; - setCurrentSession: (sessionId: string | null) => void; + setCurrentSession: (sessionId: string | null, directoryHint?: string | null) => void; updateSessionTitle: (id: string, title: string) => Promise; shareSession: (id: string) => Promise; unshareSession: (id: string) => Promise; @@ -93,7 +93,7 @@ export const useSessionActions = (args: Args) => { resetSessionSearch(); return; } - args.setCurrentSession(sessionId); + args.setCurrentSession(sessionId, sessionDirectory ?? null); args.onSessionSelected?.(sessionId); resetSessionSearch(); }, diff --git a/packages/ui/src/components/ui/CommandPalette.tsx b/packages/ui/src/components/ui/CommandPalette.tsx index 2ae01af4..ec3ae82b 100644 --- a/packages/ui/src/components/ui/CommandPalette.tsx +++ b/packages/ui/src/components/ui/CommandPalette.tsx @@ -55,8 +55,8 @@ export const CommandPalette: React.FC = () => { handleClose(); }; - const handleOpenSession = (sessionId: string) => { - setCurrentSession(sessionId); + const handleOpenSession = (sessionId: string, directoryHint?: string | null) => { + setCurrentSession(sessionId, directoryHint ?? null); handleClose(); }; @@ -289,7 +289,7 @@ export const CommandPalette: React.FC = () => { {currentSessions.map((session) => ( handleOpenSession(session.id)} + onSelect={() => handleOpenSession(session.id, currentDirectory ?? null)} >