a chat panel opens on the last conversation, not a blank one
A panel that has never chosen now 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 why you are there. It is also the case that prompted this: the conversation existed before the panel did. ── Three states, because two collapse wrongly ── undefined never chosen -> the newest conversation in this directory null "New chat" pressed -> a fresh one string that conversation `undefined` and `null` were the same value until now. Left that way, pressing New chat would set null, the default would read it as "nothing chosen", and the panel would snap straight back to the newest conversation — the button would look broken. ── Two races, both closed ── The panel does not mount until the session list has answered. Otherwise it starts a NEW conversation, the list arrives, the default applies, and the chat remounts onto something else — discarding anything typed or sent in the gap. And the id recorded on mount is only ever a REAL one. `chat.sessionId` is null for a moment while a resumed session connects, and writing that null would turn "never chosen" into "asked for a new chat", so the NEXT reload would open blank — a bug that only appears one reload later, which is the kind that gets blamed on something else. Asking for a new conversation is something the header says explicitly; this effect only reports what the harness decided. Once the default is applied the harness reports its id and it is recorded, so it stops being derived and becomes this panel's choice — "latest" the first time, "last used" thereafter. tsgo clean, frontend builds, 846 pass / 7 fail unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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 (
|
||||
<ChatPanelInner
|
||||
key={key}
|
||||
|
||||
@@ -16,8 +16,17 @@ import { panelStateKey, type WorkspaceIdentity } from '../../components/Workspac
|
||||
* Persistence comes free: it is dashboard state under `ws-chat-{dashboardId}`, keyed by panel, so it
|
||||
* survives a reload for the same reason the editor's tabs do.
|
||||
*
|
||||
* `null` means "a new conversation" — the body mounts a fresh chat and records the id the harness mints
|
||||
* as soon as it exists, so a reload after the first message comes back to the same transcript.
|
||||
* ── Three states, not two ──
|
||||
*
|
||||
* undefined this panel has never chosen. Pick a sensible default — the newest conversation in the
|
||||
* directory — because a dashboard opened onto a folder that already has history should
|
||||
* continue it rather than ignore it.
|
||||
* null the user asked for a NEW conversation. Not the same as never having chosen, and
|
||||
* collapsing the two would make "New chat" snap straight back to the newest one.
|
||||
* string that conversation.
|
||||
*
|
||||
* The body records the id the harness mints for a new chat as soon as it exists, so a reload after the
|
||||
* first message comes back to the same transcript rather than a third one.
|
||||
*/
|
||||
export type ChatPanelSession = { sessionId: string | null };
|
||||
|
||||
@@ -29,13 +38,14 @@ export function usePanelSession(panelId: string | undefined, workspace: Workspac
|
||||
EMPTY,
|
||||
);
|
||||
|
||||
const sessionId = panelId ? (bag[panelId]?.sessionId ?? null) : null;
|
||||
// `undefined` when this panel has no entry at all — see the three states above.
|
||||
const sessionId = panelId ? bag[panelId]?.sessionId : undefined;
|
||||
|
||||
const setSessionId = useCallback(
|
||||
(next: string | null) => {
|
||||
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 } };
|
||||
});
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user