From 1c7619e184a9c26760717de1f7a45fabec4f44e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Sat, 22 Aug 2026 18:44:11 +0000 Subject: [PATCH] the permalink goes through the router, so the open chat learns its own name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Third attempt at this bug, and the first two were fixes to the wrong component. The /chat screen renders ChatDetailPanel, not ChatHeader — so invalidating the sessions list and correcting which id the panel records were both real improvements to a surface that is not the one showing "New chat". The actual cause is one line. The permalink was written with `window.history.replaceState`, which changes the address bar and tells react-router nothing. On that screen BOTH the title and the rename control come from `useParams().sessionId` — see the note in DetailBar about renaming the chain's head rather than the tail — so the router kept reporting no session, the title stayed "New chat", and rename stayed absent. Navigating away and back forced the router to re-read the location, which is why that appeared to be the cure and why the fix looked like it belonged in a list. Now a `navigate(..., { replace: true })`. Replace rather than push, so Back still leaves the conversation instead of stepping through the same one under a succession of names. Worth recording for the next one: three separate "the UI did not update" bugs today were all a URL or an id not reaching the thing that reads it — two of them the officer-key/transcript-id pair, this one the router. `useQueryState` still writes `history.replaceState` directly; same trap, different hook, left alone rather than widened into this change. Co-Authored-By: Claude Opus 5 --- .../officerdev/src/hooks/useChat.ts | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/src/workspaces/officerdev/src/hooks/useChat.ts b/src/workspaces/officerdev/src/hooks/useChat.ts index 631343b7..ee9d8f88 100644 --- a/src/workspaces/officerdev/src/hooks/useChat.ts +++ b/src/workspaces/officerdev/src/hooks/useChat.ts @@ -1,5 +1,6 @@ import { useState, useEffect, useRef, useCallback } from 'react'; import { useQueryClient } from '@tanstack/react-query'; +import { useNavigate } from 'react-router'; import { useChatWebSocket } from 'hooks/useChatWebSocket'; import { useClient } from 'hooks/useClient'; import { useSettings } from 'state/useSettings'; @@ -44,11 +45,23 @@ type UsePiChatOptions = { * contradict it — which is what a hand-edited or stale `?cwd=` used to do. Anything else in the query * string is left alone. */ -function writePermalink(claudeSessionId: string): void { +/** + * The permalink, as a ROUTER navigation rather than a raw history write. + * + * It was `window.history.replaceState`, which changes the address bar and tells react-router nothing. Every + * consumer that reads the URL through the router therefore never saw it: on the /chat screen the title and + * the rename control both come from `useParams().sessionId` (see `ChatDetailPanel`), so a new conversation + * stayed "New chat" and could not be renamed until something else forced the router to re-read the + * location — which is exactly what navigating away and back did. + * + * `replace`, not push, so the browser Back button still leaves the conversation rather than stepping through + * the same one under a different name. + */ +function permalinkTo(claudeSessionId: string): { pathname: string; search: string } { const params = new URLSearchParams(window.location.search); params.delete('cwd'); const search = params.toString(); - window.history.replaceState(null, '', `/chat/${claudeSessionId}${search ? `?${search}` : ''}`); + return { pathname: `/chat/${claudeSessionId}`, search: search ? `?${search}` : '' }; } export function useChat(initialSessionId?: string, initialModel?: string | null, options?: UsePiChatOptions) { @@ -202,6 +215,21 @@ export function useChat(initialSessionId?: string, initialModel?: string | null, const client = useClient(); const queryClient = useQueryClient(); + const navigate = useNavigate(); + + /* + Written through the router so `useParams` updates. A raw `history.replaceState` moves the address bar and + leaves react-router believing the location is unchanged — see `permalinkTo`. + + In a ref because the message handler is re-created per render and closes over whatever it had; a stable + callback keeps the socket's copy valid. + */ + const writePermalink = useCallback( + (claudeSessionId: string) => { + navigate(permalinkTo(claudeSessionId), { replace: true }); + }, + [navigate], + ); /* Tell the session LISTS that this conversation exists.