diff --git a/src/workspaces/officerdev/src/hooks/useChat.ts b/src/workspaces/officerdev/src/hooks/useChat.ts index 0e60fdde..fcb7fa32 100644 --- a/src/workspaces/officerdev/src/hooks/useChat.ts +++ b/src/workspaces/officerdev/src/hooks/useChat.ts @@ -200,6 +200,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 the turn finishes, because the harness's own `ai-title` may have improved on it by then. + */ + 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 +363,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': @@ -408,6 +430,10 @@ export function useChat(initialSessionId?: string, initialModel?: string | null, setClaudeSessionId(msg.claudeSessionId); if (replaceUrl) writePermalink(msg.claudeSessionId); } + // Again at the end of every turn, and OUTSIDE the id check above: the harness writes its own + // `ai-title` while the turn runs, so the name the list is showing may now be bettered by the one on + // disk — and that is true whether or not this particular `result` happened to carry the id. + announceSession(); commitStreaming(); setMessages((prev) => [ ...prev,