diff --git a/src/workspaces/officerdev/src/apps/Chat/ChatPanelWrapper.tsx b/src/workspaces/officerdev/src/apps/Chat/ChatPanelWrapper.tsx index 222c5924..fa542d1b 100644 --- a/src/workspaces/officerdev/src/apps/Chat/ChatPanelWrapper.tsx +++ b/src/workspaces/officerdev/src/apps/Chat/ChatPanelWrapper.tsx @@ -6,6 +6,8 @@ import { EmbeddableChat } from './EmbeddableChat'; import { AgentIdentityBar } from './AgentIdentityBar'; import { useAgentPanel, type UseAgentPanelType } from './useAgentPanel'; import { usePanelSession } from './use-panel-session'; +import { useChatPwds, useClaudeSessions } from 'state/useClaudeSessions'; +import { cwdToPath } from '../../components/Workspace'; type ChatPanelInnerProps = { scoped: boolean; @@ -40,9 +42,15 @@ const ChatPanelInner = ({ useEffect(() => { setActiveSession(chat.sessionId); - // A new chat starts with no id and is named by the harness on the first turn. Recording it here is - // what makes "New chat, say something, reload" come back to that conversation instead of a third one. - onSessionId?.(chat.sessionId); + // Only a REAL id, never null. + // + // A new chat is nameless until the harness names it on the first turn, and recording that id is what + // makes "New chat, say something, reload" return to that conversation instead of a third one. But + // `chat.sessionId` is also null for a moment while a resumed session connects — and writing that null + // would turn "this panel has never chosen" into "the user asked for a new chat", so the next reload + // would open blank. Asking for a new conversation is something the header says explicitly; this + // effect only ever reports what the harness decided. + if (chat.sessionId) onSessionId?.(chat.sessionId); }, [chat.sessionId]); return ( @@ -106,7 +114,28 @@ export const ChatPanelWrapper = ({ panelId, promptPrefix }: ChatPanelWrapperProp // picker is for the ordinary case where a dashboard simply holds several conversations about the // directory it is rooted in. const [panelSessionId, setPanelSessionId] = usePanelSession(panelId, workspace); - const selectedSessionId = agentPanel.agent?.sessionKey ?? panelSessionId ?? undefined; + + /** + * A panel that has never chosen continues the newest conversation in its directory. + * + * Opening a dashboard onto a folder that already has history and starting a blank chat is the wrong + * default — the history is the reason you are there. This is also the case that prompted it: the + * conversation existed before the panel did. + * + * `undefined` from the hook means "never chosen" and `null` means "the user asked for a new one", which + * is why those are not the same value. Collapsing them would make New chat snap back to the newest. + * + * The absolute path, not the home-relative one — chat's cwd is the directory a turn really ran in, and + * `defaultCwd` is that home as the server sees it. Same join the header does. + */ + const { defaultCwd } = useChatPwds(); + const relativeCwd = cwdToPath(cwd); + const absoluteCwd = relativeCwd === '/' ? null : defaultCwd ? `${defaultCwd.replace(/\/$/, '')}${relativeCwd}` : null; + const { sessions, isLoading: sessionsLoading } = useClaudeSessions(absoluteCwd); + + const neverChose = panelSessionId === undefined; + const selectedSessionId = + agentPanel.agent?.sessionKey ?? (neverChose ? sessions[0]?.id : (panelSessionId ?? undefined)); // The key has to be decided before `useChat` mounts — the hook seeds its state from the first // `initialSessionId` it sees, and the socket's resume handshake sends whatever is in that state on @@ -118,6 +147,11 @@ export const ChatPanelWrapper = ({ panelId, promptPrefix }: ChatPanelWrapperProp // start a throwaway session and stream the answer into a transcript nobody can find again. if (agentPanel.isLoading) return null; + // And until the session list has answered, a panel that has never chosen must not mount either: it + // would start a NEW conversation, then the list would arrive, the default would apply, and the chat + // would remount onto something else — throwing away whatever had been typed or sent in between. + if (neverChose && !agentPanel.agent && sessionsLoading) return null; + return ( { if (!panelId) return; setBag((prev) => { - if ((prev[panelId]?.sessionId ?? null) === next) return prev; + if (panelId in prev && prev[panelId]?.sessionId === next) return prev; return { ...prev, [panelId]: { sessionId: next } }; }); },