fix: keep selected session header details stable

- Prevent header flicker to Untitled during session refresh
- Resolve header session data from global sessions first
- Pass session directory hints on more session-switch paths
This commit is contained in:
Bohdan Triapitsyn
2026-04-03 12:38:03 +03:00
parent dd0587501f
commit 9d78c48949
4 changed files with 84 additions and 14 deletions
@@ -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 ? (
+76 -7
View File
@@ -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<HeaderProps> = ({
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<HeaderProps> = ({
});
}, [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 '';
@@ -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<void>;
shareSession: (id: string) => Promise<Session | null>;
unshareSession: (id: string) => Promise<Session | null>;
@@ -93,7 +93,7 @@ export const useSessionActions = (args: Args) => {
resetSessionSearch();
return;
}
args.setCurrentSession(sessionId);
args.setCurrentSession(sessionId, sessionDirectory ?? null);
args.onSessionSelected?.(sessionId);
resetSessionSearch();
},
@@ -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) => (
<CommandItem
key={session.id}
onSelect={() => handleOpenSession(session.id)}
onSelect={() => handleOpenSession(session.id, currentDirectory ?? null)}
>
<RiChatAi3Line className="mr-2 h-4 w-4" />
<span className="truncate">