the permalink goes through the router, so the open chat learns its own name

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 <noreply@anthropic.com>
This commit is contained in:
2026-08-22 18:44:11 +00:00
co-authored by Claude Opus 5
parent 3f1ba38f19
commit 1c7619e184
+30 -2
View File
@@ -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.