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:
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { RiArrowLeftLine } from '@remixicon/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 { ChatInput } from './ChatInput';
|
||||||
import { useUIStore } from '@/stores/useUIStore';
|
import { useUIStore } from '@/stores/useUIStore';
|
||||||
@@ -191,7 +191,8 @@ export const ChatContainer: React.FC = () => {
|
|||||||
|
|
||||||
const handleReturnToParentSession = React.useCallback(() => {
|
const handleReturnToParentSession = React.useCallback(() => {
|
||||||
if (!parentSession) return;
|
if (!parentSession) return;
|
||||||
setCurrentSession(parentSession.id);
|
const parentDirectory = (parentSession as Session & { directory?: string | null }).directory ?? null;
|
||||||
|
setCurrentSession(parentSession.id, parentDirectory);
|
||||||
}, [parentSession, setCurrentSession]);
|
}, [parentSession, setCurrentSession]);
|
||||||
|
|
||||||
const returnToParentButton = parentSession ? (
|
const returnToParentButton = parentSession ? (
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
|||||||
import { useFilesViewTabsStore } from '@/stores/useFilesViewTabsStore';
|
import { useFilesViewTabsStore } from '@/stores/useFilesViewTabsStore';
|
||||||
import { useGitStore } from '@/stores/useGitStore';
|
import { useGitStore } from '@/stores/useGitStore';
|
||||||
import { useGitHubPrStatusStore } from '@/stores/useGitHubPrStatusStore';
|
import { useGitHubPrStatusStore } from '@/stores/useGitHubPrStatusStore';
|
||||||
|
import { useGlobalSessionsStore } from '@/stores/useGlobalSessionsStore';
|
||||||
|
|
||||||
import { useGitHubAuthStore } from '@/stores/useGitHubAuthStore';
|
import { useGitHubAuthStore } from '@/stores/useGitHubAuthStore';
|
||||||
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
|
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
|
||||||
@@ -65,6 +66,7 @@ import { ProjectActionsButton } from '@/components/layout/ProjectActionsButton';
|
|||||||
import { isDesktopShell, isVSCodeRuntime } from '@/lib/desktop';
|
import { isDesktopShell, isVSCodeRuntime } from '@/lib/desktop';
|
||||||
import { desktopHostsGet, locationMatchesHost, redactSensitiveUrl } from '@/lib/desktopHosts';
|
import { desktopHostsGet, locationMatchesHost, redactSensitiveUrl } from '@/lib/desktopHosts';
|
||||||
import { resolveSessionDiffStats } from '@/components/session/sidebar/utils';
|
import { resolveSessionDiffStats } from '@/components/session/sidebar/utils';
|
||||||
|
import type { Session } from '@opencode-ai/sdk/v2/client';
|
||||||
|
|
||||||
|
|
||||||
const isSameContextUsage = (
|
const isSameContextUsage = (
|
||||||
@@ -250,6 +252,7 @@ export const Header: React.FC<HeaderProps> = ({
|
|||||||
const currentSessionMessageRecords = useSessionMessageRecords(currentSessionId ?? '');
|
const currentSessionMessageRecords = useSessionMessageRecords(currentSessionId ?? '');
|
||||||
const currentSessionMessages = currentSessionId ? (currentSessionMessageRecords.length > 0 ? currentSessionMessageRecords : undefined) : undefined;
|
const currentSessionMessages = currentSessionId ? (currentSessionMessageRecords.length > 0 ? currentSessionMessageRecords : undefined) : undefined;
|
||||||
const sessions = useSessions();
|
const sessions = useSessions();
|
||||||
|
const globalActiveSessions = useGlobalSessionsStore((state) => state.activeSessions);
|
||||||
const activeProject = useProjectsStore((state) => {
|
const activeProject = useProjectsStore((state) => {
|
||||||
if (!state.activeProjectId) {
|
if (!state.activeProjectId) {
|
||||||
return null;
|
return null;
|
||||||
@@ -561,16 +564,82 @@ export const Header: React.FC<HeaderProps> = ({
|
|||||||
});
|
});
|
||||||
}, [fetchAllQuotas, isUsageRefreshSpinning]);
|
}, [fetchAllQuotas, isUsageRefreshSpinning]);
|
||||||
|
|
||||||
const currentSession = React.useMemo(() => {
|
const currentSessionLive = React.useMemo(() => {
|
||||||
if (!currentSessionId) return null;
|
if (!currentSessionId) return null;
|
||||||
// Try current directory's store first, then fall back to all child stores.
|
// Resolve from the global sessions snapshot first (same source as sidebar).
|
||||||
// The sidebar loads sessions globally via SDK, but the header uses
|
// Child-store lists are intentionally partial/truncated during bootstrap.
|
||||||
// useSessions() which only has the current directory. This fallback
|
return globalActiveSessions.find((s) => s.id === currentSessionId)
|
||||||
// ensures the title/directory show when the session lives elsewhere.
|
?? sessions.find((s) => s.id === currentSessionId)
|
||||||
return sessions.find((s) => s.id === currentSessionId)
|
|
||||||
?? getAllSyncSessions().find((s) => s.id === currentSessionId)
|
?? getAllSyncSessions().find((s) => s.id === currentSessionId)
|
||||||
?? null;
|
?? 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) => {
|
const worktreePath = useSessionUIStore((state) => {
|
||||||
if (!currentSessionId) return '';
|
if (!currentSessionId) return '';
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ type Args = {
|
|||||||
setDirectory: (directory: string, options?: { showOverlay?: boolean }) => void;
|
setDirectory: (directory: string, options?: { showOverlay?: boolean }) => void;
|
||||||
setActiveMainTab: (tab: 'chat' | 'plan' | 'git' | 'diff' | 'terminal' | 'files') => void;
|
setActiveMainTab: (tab: 'chat' | 'plan' | 'git' | 'diff' | 'terminal' | 'files') => void;
|
||||||
setSessionSwitcherOpen: (open: boolean) => 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>;
|
updateSessionTitle: (id: string, title: string) => Promise<void>;
|
||||||
shareSession: (id: string) => Promise<Session | null>;
|
shareSession: (id: string) => Promise<Session | null>;
|
||||||
unshareSession: (id: string) => Promise<Session | null>;
|
unshareSession: (id: string) => Promise<Session | null>;
|
||||||
@@ -93,7 +93,7 @@ export const useSessionActions = (args: Args) => {
|
|||||||
resetSessionSearch();
|
resetSessionSearch();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
args.setCurrentSession(sessionId);
|
args.setCurrentSession(sessionId, sessionDirectory ?? null);
|
||||||
args.onSessionSelected?.(sessionId);
|
args.onSessionSelected?.(sessionId);
|
||||||
resetSessionSearch();
|
resetSessionSearch();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -55,8 +55,8 @@ export const CommandPalette: React.FC = () => {
|
|||||||
handleClose();
|
handleClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOpenSession = (sessionId: string) => {
|
const handleOpenSession = (sessionId: string, directoryHint?: string | null) => {
|
||||||
setCurrentSession(sessionId);
|
setCurrentSession(sessionId, directoryHint ?? null);
|
||||||
handleClose();
|
handleClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -289,7 +289,7 @@ export const CommandPalette: React.FC = () => {
|
|||||||
{currentSessions.map((session) => (
|
{currentSessions.map((session) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={session.id}
|
key={session.id}
|
||||||
onSelect={() => handleOpenSession(session.id)}
|
onSelect={() => handleOpenSession(session.id, currentDirectory ?? null)}
|
||||||
>
|
>
|
||||||
<RiChatAi3Line className="mr-2 h-4 w-4" />
|
<RiChatAi3Line className="mr-2 h-4 w-4" />
|
||||||
<span className="truncate">
|
<span className="truncate">
|
||||||
|
|||||||
Reference in New Issue
Block a user