From 59f600d34db1544e717daa2ed29598b4153a9243 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Sat, 22 Aug 2026 18:26:15 +0000 Subject: [PATCH] =?UTF-8?q?a=20new=20chat=20gets=20its=20name=20without=20?= =?UTF-8?q?a=20refresh=20=E2=80=94=20this=20time=20with=20the=20import?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Re-lands f9a36996, which I reverted because it broke chat rendering outright: `ReferenceError: useQueryClient is not defined`. The cause was mine and it is embarrassing in a specific way. The edit that added the import searched for `import { useCallback, useEffect, useMemo, useRef, useState } from 'react'` and the file's first line is `import { useState, useEffect, useRef, useCallback } from 'react'` — different order, and useMemo is not in it. So the search matched nothing, and I had used replace WITHOUT an assertion, so nothing said so. That is the identical failure I caught and wrote up hours earlier in the CommandHQ layout edit, where two edits silently did nothing for the same reason. Every edit in this commit asserts. Which then caught a second, subtler fault: `commitStreaming()` appears ELEVEN times, so anchoring the turn-end call on it put announceSession inside `assistant:text` rather than `result`, with mangled indentation. An assert proves an anchor EXISTS; it does not prove it is unique. The result handler is now anchored on a three-line sequence that occurs once, and the count is asserted rather than assumed. Verified in the BUILT BUNDLE rather than by typecheck, because a ReferenceError is a bundle fact: the name appears zero times as a free identifier (the bundler resolved and renamed it), and both invalidation keys are present — CLAUDE_SESSIONS went from one occurrence to two, the second being this. Worth recording about the tooling, since I doubted it: tsgo DOES catch this class of error. I probed it three times with a deliberate type error in this exact file and it reported every time. So the check is sound and the miss was mine. Co-Authored-By: Claude Opus 5 --- .../officerdev/src/hooks/useChat.ts | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) 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; }