fix: show streaming chat messages after startup

Read chat state from the active session directory
Align status rows with the active session
This commit is contained in:
Bohdan Triapitsyn
2026-06-03 22:38:15 +03:00
parent 04b1425c2e
commit 04c9aa57dd
4 changed files with 49 additions and 23 deletions
@@ -355,6 +355,7 @@ export const ChatContainer: React.FC<ChatContainerProps> = ({ autoOpenDraft = tr
const { t } = useI18n(); const { t } = useI18n();
// Session UI state // Session UI state
const currentSessionId = useSessionUIStore((s) => s.currentSessionId); const currentSessionId = useSessionUIStore((s) => s.currentSessionId);
const currentSessionDirectory = useSessionUIStore((s) => s.currentSessionDirectory);
const openNewSessionDraft = useSessionUIStore((s) => s.openNewSessionDraft); const openNewSessionDraft = useSessionUIStore((s) => s.openNewSessionDraft);
const setCurrentSession = useSessionUIStore((s) => s.setCurrentSession); const setCurrentSession = useSessionUIStore((s) => s.setCurrentSession);
const newSessionDraft = useSessionUIStore((s) => s.newSessionDraft); const newSessionDraft = useSessionUIStore((s) => s.newSessionDraft);
@@ -364,6 +365,7 @@ export const ChatContainer: React.FC<ChatContainerProps> = ({ autoOpenDraft = tr
// Sync actions // Sync actions
const sync = useSync(); const sync = useSync();
const syncDirectory = useSyncDirectory(); const syncDirectory = useSyncDirectory();
const effectiveSessionDirectory = currentSessionDirectory ?? syncDirectory;
const ensureSessionRenderable = React.useCallback( const ensureSessionRenderable = React.useCallback(
(sessionId: string) => sync.ensureSessionRenderable(sessionId), (sessionId: string) => sync.ensureSessionRenderable(sessionId),
[sync], [sync],
@@ -396,45 +398,48 @@ export const ChatContainer: React.FC<ChatContainerProps> = ({ autoOpenDraft = tr
[streamingMessageId], [streamingMessageId],
), ),
); );
const sessionMessageCount = useSessionMessageCount(currentSessionId ?? ''); const sessionMessageCount = useSessionMessageCount(currentSessionId ?? '', effectiveSessionDirectory);
const hasRenderableSessionSnapshot = useDirectorySync( const hasRenderableSessionSnapshot = useDirectorySync(
React.useCallback( React.useCallback(
(state) => (currentSessionId ? getSessionMaterializationStatus(state, currentSessionId).renderable : false), (state) => (currentSessionId ? getSessionMaterializationStatus(state, currentSessionId).renderable : false),
[currentSessionId], [currentSessionId],
), ),
effectiveSessionDirectory,
); );
// Messages from sync system // Messages from sync system
const sessionMessageRecords = useSessionMessageRecords(currentSessionId ?? ''); const sessionMessageRecords = useSessionMessageRecords(currentSessionId ?? '', effectiveSessionDirectory);
const sessionMessages = currentSessionId ? sessionMessageRecords : EMPTY_MESSAGES; const sessionMessages = currentSessionId ? sessionMessageRecords : EMPTY_MESSAGES;
const sessionPrefetchInfo = React.useSyncExternalStore( const sessionPrefetchInfo = React.useSyncExternalStore(
React.useCallback( React.useCallback(
(notify) => currentSessionId (notify) => currentSessionId
? subscribeSessionPrefetch(syncDirectory, currentSessionId, notify) ? subscribeSessionPrefetch(effectiveSessionDirectory, currentSessionId, notify)
: () => undefined, : () => undefined,
[currentSessionId, syncDirectory], [currentSessionId, effectiveSessionDirectory],
), ),
React.useCallback( React.useCallback(
() => currentSessionId ? getSessionPrefetch(syncDirectory, currentSessionId) : undefined, () => currentSessionId ? getSessionPrefetch(effectiveSessionDirectory, currentSessionId) : undefined,
[currentSessionId, syncDirectory], [currentSessionId, effectiveSessionDirectory],
), ),
React.useCallback(() => undefined, []), React.useCallback(() => undefined, []),
); );
// Sessions from sync system // Sessions from sync system
const sessions = useSessions(); const sessions = useSessions(effectiveSessionDirectory);
// Plan detection - watches messages for plan creation and signals store // Plan detection - watches messages for plan creation and signals store
usePlanDetection(currentSessionId ?? '', sessionMessages); usePlanDetection(currentSessionId ?? '', sessionMessages);
// Session status from sync system // Session status from sync system
const sessionStatusForCurrent = useSessionStatus(currentSessionId ?? '') ?? IDLE_SESSION_STATUS; const sessionStatusForCurrent = useSessionStatus(currentSessionId ?? '', effectiveSessionDirectory) ?? IDLE_SESSION_STATUS;
// Permissions & questions from sync system // Permissions & questions from sync system
const allPermissions = useDirectorySync( const allPermissions = useDirectorySync(
React.useCallback((s) => s.permission ?? {}, []), React.useCallback((s) => s.permission ?? {}, []),
effectiveSessionDirectory,
); );
const allQuestions = useDirectorySync( const allQuestions = useDirectorySync(
React.useCallback((s) => s.question ?? {}, []), React.useCallback((s) => s.question ?? {}, []),
effectiveSessionDirectory,
); );
// Convert Record → Map for blockingRequests helpers // Convert Record → Map for blockingRequests helpers
@@ -782,8 +787,9 @@ export const ChatContainer: React.FC<ChatContainerProps> = ({ autoOpenDraft = tr
React.useEffect(() => { React.useEffect(() => {
if (!currentSessionId) return; if (!currentSessionId) return;
if (hasRenderableSessionSnapshot) return; if (hasRenderableSessionSnapshot) return;
if (effectiveSessionDirectory !== syncDirectory) return;
void ensureSessionRenderable(currentSessionId); void ensureSessionRenderable(currentSessionId);
}, [currentSessionId, ensureSessionRenderable, hasRenderableSessionSnapshot]); }, [currentSessionId, effectiveSessionDirectory, ensureSessionRenderable, hasRenderableSessionSnapshot, syncDirectory]);
if (!currentSessionId && !draftOpen) { if (!currentSessionId && !draftOpen) {
// With auto-open, the draft welcome opens on the next tick (effect below), // With auto-open, the draft welcome opens on the next tick (effect below),
+8 -5
View File
@@ -249,6 +249,7 @@ const getToolDisplayName = (part: ToolPart): string => {
export function useAssistantStatus(): AssistantStatusSnapshot { export function useAssistantStatus(): AssistantStatusSnapshot {
const currentSessionId = useSessionUIStore((state) => state.currentSessionId); const currentSessionId = useSessionUIStore((state) => state.currentSessionId);
const currentSessionDirectory = useSessionUIStore((state) => state.currentSessionDirectory);
const rawSessionMessages = useDirectorySync( const rawSessionMessages = useDirectorySync(
React.useCallback((state) => { React.useCallback((state) => {
@@ -256,7 +257,8 @@ export function useAssistantStatus(): AssistantStatusSnapshot {
return EMPTY_MESSAGES; return EMPTY_MESSAGES;
} }
return state.message[currentSessionId] ?? EMPTY_MESSAGES; return state.message[currentSessionId] ?? EMPTY_MESSAGES;
}, [currentSessionId]) }, [currentSessionId]),
currentSessionDirectory ?? undefined,
); );
// Only subscribe to parts for the last assistant message — avoids re-render // Only subscribe to parts for the last assistant message — avoids re-render
@@ -273,11 +275,12 @@ export function useAssistantStatus(): AssistantStatusSnapshot {
const genericKey = `${currentSessionId ?? ''}:${lastAssistantId ?? ''}`; const genericKey = `${currentSessionId ?? ''}:${lastAssistantId ?? ''}`;
const parts = lastAssistantId ? (state.part[lastAssistantId] ?? EMPTY_PARTS) : EMPTY_PARTS; const parts = lastAssistantId ? (state.part[lastAssistantId] ?? EMPTY_PARTS) : EMPTY_PARTS;
return encodeParsedStatus(createParsedStatus(parts, genericKey)); return encodeParsedStatus(createParsedStatus(parts, genericKey));
}, [currentSessionId, lastAssistantId]) }, [currentSessionId, lastAssistantId]),
currentSessionDirectory ?? undefined,
); );
const sessionPermissionRequests = useSessionPermissions(currentSessionId ?? ''); const sessionPermissionRequests = useSessionPermissions(currentSessionId ?? '', currentSessionDirectory ?? undefined);
const sessionQuestionRequests = useSessionQuestions(currentSessionId ?? ''); const sessionQuestionRequests = useSessionQuestions(currentSessionId ?? '', currentSessionDirectory ?? undefined);
const sessionAbortRecord = useSessionUIStore( const sessionAbortRecord = useSessionUIStore(
React.useCallback((state) => { React.useCallback((state) => {
@@ -290,7 +293,7 @@ export function useAssistantStatus(): AssistantStatusSnapshot {
const { phase: activityPhase, isWorking: isPhaseWorking } = useCurrentSessionActivity(); const { phase: activityPhase, isWorking: isPhaseWorking } = useCurrentSessionActivity();
const currentSessionStatus = useSessionStatus(currentSessionId ?? ''); const currentSessionStatus = useSessionStatus(currentSessionId ?? '', currentSessionDirectory ?? undefined);
const sessionRetryAttempt = currentSessionStatus?.type === 'retry' const sessionRetryAttempt = currentSessionStatus?.type === 'retry'
? (currentSessionStatus as { type: 'retry'; attempt?: number }).attempt ? (currentSessionStatus as { type: 'retry'; attempt?: number }).attempt
+2 -1
View File
@@ -66,5 +66,6 @@ export function useSessionActivity(sessionId: string | null | undefined, directo
export function useCurrentSessionActivity(): SessionActivityResult { export function useCurrentSessionActivity(): SessionActivityResult {
const currentSessionId = useSessionUIStore((state) => state.currentSessionId); const currentSessionId = useSessionUIStore((state) => state.currentSessionId);
return useSessionActivity(currentSessionId); const currentSessionDirectory = useSessionUIStore((state) => state.currentSessionDirectory);
return useSessionActivity(currentSessionId, currentSessionDirectory ?? undefined);
} }
+24 -8
View File
@@ -199,6 +199,7 @@ export type SessionHistoryMeta = {
export type SessionUIState = { export type SessionUIState = {
currentSessionId: string | null currentSessionId: string | null
currentSessionDirectory: string | null
newSessionDraft: NewSessionDraftState newSessionDraft: NewSessionDraftState
abortPromptSessionId: string | null abortPromptSessionId: string | null
abortPromptExpiresAt: number | null abortPromptExpiresAt: number | null
@@ -344,6 +345,10 @@ const resolveSessionDirectory = (
if (attachmentDirectory) return attachmentDirectory if (attachmentDirectory) return attachmentDirectory
const metaPath = getWtMeta(sessionId)?.path const metaPath = getWtMeta(sessionId)?.path
if (typeof metaPath === "string" && metaPath.trim().length > 0) return normalizePath(metaPath) if (typeof metaPath === "string" && metaPath.trim().length > 0) return normalizePath(metaPath)
const runtimeMemory = runtimeSessionMemory.get(runtimeMemoryKey())
if (runtimeMemory?.sessionId === sessionId && runtimeMemory.directory) {
return normalizePath(runtimeMemory.directory)
}
const sessions = getAllSyncSessions() const sessions = getAllSyncSessions()
const target = sessions.find((s) => s.id === sessionId) const target = sessions.find((s) => s.id === sessionId)
if (!target) return null if (!target) return null
@@ -391,6 +396,7 @@ const writeRuntimeSessionMemory = (key: string, patch: Partial<RuntimeSessionMem
export const useSessionUIStore = create<SessionUIState>()((set, get) => ({ export const useSessionUIStore = create<SessionUIState>()((set, get) => ({
currentSessionId: null, currentSessionId: null,
currentSessionDirectory: null,
newSessionDraft: { ...DEFAULT_DRAFT }, newSessionDraft: { ...DEFAULT_DRAFT },
abortPromptSessionId: null, abortPromptSessionId: null,
abortPromptExpiresAt: null, abortPromptExpiresAt: null,
@@ -418,10 +424,6 @@ export const useSessionUIStore = create<SessionUIState>()((set, get) => ({
activeSessionByRuntime.set(key, id) activeSessionByRuntime.set(key, id)
const previousSessionId = get().currentSessionId const previousSessionId = get().currentSessionId
// Set currentSessionId immediately so the skeleton renders without delay.
set({ currentSessionId: id })
const directoryState = useDirectoryStore.getState() const directoryState = useDirectoryStore.getState()
const sessionDir = resolveSessionDirectory( const sessionDir = resolveSessionDirectory(
@@ -430,6 +432,10 @@ export const useSessionUIStore = create<SessionUIState>()((set, get) => ({
) )
const fallbackDir = opencodeClient.getDirectory() ?? directoryState.currentDirectory ?? null const fallbackDir = opencodeClient.getDirectory() ?? directoryState.currentDirectory ?? null
const resolvedDir = (directoryHint ? normalizePath(directoryHint) : null) ?? sessionDir ?? fallbackDir const resolvedDir = (directoryHint ? normalizePath(directoryHint) : null) ?? sessionDir ?? fallbackDir
// Set the directory together with the session id so chat hooks read the
// same child store that send/SSE events will update during startup races.
set({ currentSessionId: id, currentSessionDirectory: id ? resolvedDir ?? null : null })
writeRuntimeSessionMemory(key, { sessionId: id, directory: resolvedDir ?? null }) writeRuntimeSessionMemory(key, { sessionId: id, directory: resolvedDir ?? null })
try { try {
@@ -493,6 +499,7 @@ export const useSessionUIStore = create<SessionUIState>()((set, get) => ({
} }
set({ set({
currentSessionId: restoredSessionId, currentSessionId: restoredSessionId,
currentSessionDirectory: restoredSessionId ? restoredDirectory : null,
newSessionDraft: restoredSessionId ? { ...DEFAULT_DRAFT } : restoredDraft, newSessionDraft: restoredSessionId ? { ...DEFAULT_DRAFT } : restoredDraft,
abortPromptSessionId: null, abortPromptSessionId: null,
abortPromptExpiresAt: null, abortPromptExpiresAt: null,
@@ -501,7 +508,7 @@ export const useSessionUIStore = create<SessionUIState>()((set, get) => ({
pendingChangesBarDismissed: new Map(), pendingChangesBarDismissed: new Map(),
}) })
if (restoredSessionId) { if (restoredSessionId) {
setActiveSession(opencodeClient.getDirectory() ?? "", restoredSessionId) setActiveSession(restoredDirectory ?? opencodeClient.getDirectory() ?? "", restoredSessionId)
} else { } else {
setActiveSession("", "") setActiveSession("", "")
} }
@@ -575,6 +582,7 @@ export const useSessionUIStore = create<SessionUIState>()((set, get) => ({
...nextDraft, ...nextDraft,
}, },
currentSessionId: null, currentSessionId: null,
currentSessionDirectory: null,
error: null, error: null,
}) })
@@ -1225,6 +1233,11 @@ export const useSessionUIStore = create<SessionUIState>()((set, get) => ({
}, },
getDirectoryForSession: (sessionId) => { getDirectoryForSession: (sessionId) => {
if (sessionId === get().currentSessionId && get().currentSessionDirectory) {
return get().currentSessionDirectory
}
const resolved = resolveSessionDirectory(sessionId, (sid) => get().worktreeMetadata.get(sid))
if (resolved) return resolved
const attachmentDirectory = getAttachedSessionDirectory(getAttachmentForSession(sessionId)) const attachmentDirectory = getAttachedSessionDirectory(getAttachmentForSession(sessionId))
if (attachmentDirectory) return attachmentDirectory if (attachmentDirectory) return attachmentDirectory
const sessions = getAllSyncSessions() const sessions = getAllSyncSessions()
@@ -1292,9 +1305,12 @@ export const useSessionUIStore = create<SessionUIState>()((set, get) => ({
// Handled by sync system's SSE stream // Handled by sync system's SSE stream
}, },
setSessionDirectory: () => { setSessionDirectory: (sessionId, directory) => {
// Session directory is owned by sync child stores via SSE events. const normalized = normalizePath(directory)
// This is now a no-op — kept for interface compatibility during migration. if (sessionId === get().currentSessionId) {
set({ currentSessionDirectory: normalized })
writeRuntimeSessionMemory(runtimeMemoryKey(), { sessionId, directory: normalized })
}
}, },
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------