From c591a8a7719ab9f635e34106157c5fff79b1a29b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Tue, 4 Aug 2026 02:15:30 +0000 Subject: [PATCH] make the /chat url a permalink again MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit reloading /chat/?cwd= landed on an empty general_chat_sessions instead of the conversation. one line did both halves of it: if (replaceUrl) window.history.replaceState(null, '', `/chat/${msg.sessionId}`) that ran on session:init, and session:init's sessionId is officer's own per-connection key — websocket.ts mints it as `msg.sessionId || randomUUID()`. /chat/sessions/:id resolves a CLAUDE TRANSCRIPT uuid, so the address bar ended up naming something no lookup could find; the detail fetch 404'd and the catch dropped you into a blank chat. the template also had no location.search, so ?cwd= — added later, for agent grounds — was thrown away every time the socket connected, which is why the pwd picker fell back to the default group. the transcript uuid is only known once the turn reports it, and it only started crossing the wire in 7b6ca5f, so move the rewrite to `result`, use claudeSessionId, and carry the query string through untouched. new chats gain a working permalink too — /chat/new used to become an unresolvable id the same way. mobile back had the same query-string hole; it now keeps the search. Co-Authored-By: Claude Opus 5 --- .../Screens/Dashboard/ChatHistory/index.tsx | 4 +++- src/workspaces/officerdev/src/apps/Chat/types.ts | 5 ++++- src/workspaces/officerdev/src/hooks/useChat.ts | 10 +++++++++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/apps/officer-web/Screens/Dashboard/ChatHistory/index.tsx b/src/apps/officer-web/Screens/Dashboard/ChatHistory/index.tsx index 32de66a4..4148fc03 100644 --- a/src/apps/officer-web/Screens/Dashboard/ChatHistory/index.tsx +++ b/src/apps/officer-web/Screens/Dashboard/ChatHistory/index.tsx @@ -113,7 +113,9 @@ export const SessionListPage = ({ isNew }: SessionListPageProps) => { locked mobilePanelId={mobilePanelId} onMobilePanelChange={(id) => { - if (!id) navigate('/chat', { replace: true }); + // Keep the query string: ?cwd= names the project group the list is showing, so dropping it on + // mobile back sends you to an empty general_chat_sessions instead of the list you came from. + if (!id) navigate({ pathname: '/chat', search: window.location.search }, { replace: true }); }} /> diff --git a/src/workspaces/officerdev/src/apps/Chat/types.ts b/src/workspaces/officerdev/src/apps/Chat/types.ts index 1bc8431c..60d63d2f 100644 --- a/src/workspaces/officerdev/src/apps/Chat/types.ts +++ b/src/workspaces/officerdev/src/apps/Chat/types.ts @@ -68,7 +68,10 @@ export type ServerMessage = parentToolUseId?: string; } | { type: 'tool:result'; toolCallId: string; output: string; isError: boolean; parentToolUseId?: string } - | { type: 'result'; sessionId: string; cost: MessageCost } + // `claudeSessionId` is Claude's OWN transcript uuid, and the only id `/chat/sessions/:id` can resolve. + // `sessionId` is officer's per-connection key (websocket.ts mints it with `msg.sessionId || randomUUID()`), + // which addresses nothing after the socket closes. + | { type: 'result'; sessionId: string; cost: MessageCost; claudeSessionId?: string } | { type: 'sync:messages'; sessionId: string; messages: Message[]; isGenerating: boolean; streamingText: string } | { type: 'error'; message: string; errorCode?: string } | { type: 'stopped' } diff --git a/src/workspaces/officerdev/src/hooks/useChat.ts b/src/workspaces/officerdev/src/hooks/useChat.ts index 58e5bf46..5fa398b8 100644 --- a/src/workspaces/officerdev/src/hooks/useChat.ts +++ b/src/workspaces/officerdev/src/hooks/useChat.ts @@ -219,7 +219,6 @@ export function useChat(initialSessionId?: string, initialModel?: string | null, setSessionId(msg.sessionId); setModel(msg.model); setCwd(msg.cwd); - if (replaceUrl) window.history.replaceState(null, '', `/chat/${msg.sessionId}`); break; case 'assistant:delta': @@ -276,6 +275,15 @@ export function useChat(initialSessionId?: string, initialModel?: string | null, } case 'result': { + // Make the address bar a permalink. This used to run on `session:init` writing `/chat/`, + // which was wrong twice over: that id is officer's per-connection key (`msg.sessionId || + // randomUUID()`), which `/chat/sessions/:id` cannot resolve, and the template dropped + // `location.search` — so a reload lost both the conversation AND the `?cwd=` naming its project + // group, landing you in an empty general_chat_sessions. The transcript uuid is only known once the + // turn reports it, so wait for it, and carry the query string through untouched. + if (replaceUrl && msg.claudeSessionId) { + window.history.replaceState(null, '', `/chat/${msg.claudeSessionId}${window.location.search}`); + } commitStreaming(); setMessages((prev) => [ ...prev,