From f9a3699603e02a0d09573aad663e8dae7fcf36db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Sat, 22 Aug 2026 18:15:20 +0000 Subject: [PATCH] a new chat gets its name without a refresh, and can be renamed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Start a chat and the header said "New chat" until you navigated away and back, and the rename control was missing entirely. Both halves came from one cause, and it was not the titling. The header resolves its own title by finding the session in the cached LIST — `sessions.find((s) => s.id === sessionId)` — and a newborn conversation is not in that list. It has staleTime 30s and nothing invalidated it, so `current` was undefined: the label fell through to "New chat", and rename was absent because rename is a per-row action on a list the session was not in yet. Navigating away and back refetched it, which is exactly what made this look like a rendering bug. Verified first that the titling itself is fine and needs no change. Claude Code writes an `ai-title` entry early — line 8 in a real transcript, after the user message and BEFORE the first assistant line — and Officer's own `title: summaryTitle || firstUserText || '(untitled)'` means a session is never nameless once its transcript has a user message. The server could always answer; nothing asked it. So the conversation announces itself. On `session:claude` — the START of the turn, when the transcript becomes real on disk — and again at the end of every turn, because the harness may have bettered the name while it ran. The second call sits OUTSIDE the id check deliberately: that is true whether or not a particular `result` happened to carry the id. Cost is one small request per turn per open chat, which is the honest price of a list that is right. 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..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,