diff --git a/src/workspaces/officerdev/src/apps/ChatHistory/ChatDetailPanel.tsx b/src/workspaces/officerdev/src/apps/ChatHistory/ChatDetailPanel.tsx index bdb3e522..5227efa9 100644 --- a/src/workspaces/officerdev/src/apps/ChatHistory/ChatDetailPanel.tsx +++ b/src/workspaces/officerdev/src/apps/ChatHistory/ChatDetailPanel.tsx @@ -43,6 +43,12 @@ type ChatLocationState = { type DetailBarProps = { sessionTitle: string | undefined; + /** + * The session's own directory. Not decoration — every `/chat/sessions` call is scoped by it, because + * the server resolves a transcript by searching that cwd's project dir (`findTranscript`). Renaming + * without it searches the default group and 404s for any conversation living in a project. + */ + cwd?: string | null; /** Transcripts behind this conversation; `> 1` when `/clear` parts have been merged into it. */ partCount?: number; isConnected: boolean; @@ -50,7 +56,7 @@ type DetailBarProps = { onDisconnect?: () => void; }; -function DetailBar({ sessionTitle, partCount, isConnected, isGenerating, onDisconnect }: DetailBarProps) { +function DetailBar({ sessionTitle, cwd, partCount, isConnected, isGenerating, onDisconnect }: DetailBarProps) { // The id to rename comes from the URL, not from `resumeSessionId`. They are the same for an ordinary // conversation and not for a merged `/clear` chain, where the resume target is the tail transcript // while the list, the links and the server all address the chain by its head. Renaming the tail would @@ -61,7 +67,7 @@ function DetailBar({ sessionTitle, partCount, isConnected, isGenerating, onDisco // The same rename the list's pencil calls, so both surfaces write one place and the invalidation that // follows refreshes the row too. A failure toasts rather than reverting silently, matching `SessionList`. - const { renameSession } = useClaudeSessions(); + const { renameSession } = useClaudeSessions(cwd); const commitRename = useCallback( async (next: string) => { if (!sessionId) return; @@ -176,6 +182,7 @@ function NewChat(props: NewChatProps) { // comes from the same place the list gets it, which is also what makes the `/clear` numbering // agree in both views instead of only in the row. sessionTitle={props.sessionTitle ?? undefined} + cwd={props.sessionCwd} partCount={props.partCount} isConnected={chat.isConnected} isGenerating={chat.isGenerating} @@ -205,7 +212,16 @@ export const ChatDetailPanel = () => { // the browser tab strip is the only place the name shows, which is exactly what tells two side-by-side // windows apart. Tiled, the same value fills the header's centre. One rule, both states. const { sessionId } = useParams<{ sessionId: string }>(); - usePublishPageTitle(sessionId ? (selected?.title ?? null) : null); + + // The list's copy of the title wins over the one handed down at selection time. `selected.title` rides + // the `chat:selected-session` channel, which is published once when a row is clicked and never updated + // — so a rename reached the server, refreshed the row, and left this pane and the page title showing + // the old name, which reads exactly like the rename having failed. Resolved here, once, and passed + // down: renaming from either surface now retitles both, and the list's own pencil retitles an open pane. + const { sessions } = useClaudeSessions(selected?.cwd); + const title = sessions.find((session) => session.id === sessionId)?.title ?? selected?.title ?? null; + + usePublishPageTitle(sessionId ? title : null); if (!selected) { return ( @@ -224,7 +240,7 @@ export const ChatDetailPanel = () => { total={selected.total} initialOffset={selected.initialOffset} sessionCwd={selected.cwd} - sessionTitle={selected.title} + sessionTitle={title} partCount={selected.partCount} /> );