diff --git a/src/workspaces/officerdev/src/hooks/useChat.ts b/src/workspaces/officerdev/src/hooks/useChat.ts index 0e60fdde..631343b7 100644 --- a/src/workspaces/officerdev/src/hooks/useChat.ts +++ b/src/workspaces/officerdev/src/hooks/useChat.ts @@ -1,4 +1,5 @@ import { useState, useEffect, useRef, useCallback } from 'react'; +import { useQueryClient } from '@tanstack/react-query'; import { useChatWebSocket } from 'hooks/useChatWebSocket'; import { useClient } from 'hooks/useClient'; import { useSettings } from 'state/useSettings'; @@ -200,6 +201,26 @@ export function useChat(initialSessionId?: string, initialModel?: string | null, const [claudeSessionId, setClaudeSessionId] = useState(resumeSessionId ?? null); const client = useClient(); + const queryClient = useQueryClient(); + + /* + Tell the session LISTS that this conversation exists. + + The chat header resolves its own title by finding this session in the cached list — + `sessions.find((s) => s.id === sessionId)`. A newborn conversation is not in that list: it has + `staleTime: 30s` and nothing invalidated it, so for up to half a minute the header read "New chat" and + the rename control was absent, because rename is a per-row action on a list the session was not in yet. + Navigating away and back fixed it, which is what made it look like a rendering bug rather than a stale + cache. + + Fired when the transcript id arrives — the START of the turn, not the end — because that is the moment + the session becomes real on disk and the server can already name it from the first user message. Fired + again when a turn finishes, because the harness writes its own `ai-title` while the turn runs. + */ + const announceSession = useCallback(() => { + void queryClient.invalidateQueries({ queryKey: ['CLAUDE_SESSIONS'] }); + void queryClient.invalidateQueries({ queryKey: ['CHAT_PWDS'] }); + }, [queryClient]); // Fetch the next older window and prepend it. The scroll container restores its position from the // height delta so the view stays put. Guarded against overlap and against running once fully paged in. @@ -343,6 +364,8 @@ export function useChat(initialSessionId?: string, initialModel?: string | null, claudeSessionIdRef.current = msg.claudeSessionId; setClaudeSessionId(msg.claudeSessionId); if (replaceUrl) writePermalink(msg.claudeSessionId); + // The lists do not know this conversation exists yet — see `announceSession`. + announceSession(); break; case 'assistant:delta': @@ -420,6 +443,9 @@ export function useChat(initialSessionId?: string, initialModel?: string | null, const hadTools = toolCallsInTurnRef.current; toolCallsInTurnRef.current = false; onTurnCompleteRef.current?.(hadTools); + // Every turn end: the harness writes its own `ai-title` while a turn runs, so the name the lists are + // showing may now be bettered by the one on disk. + announceSession(); break; }