Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion packages/components/src/components/sessions/session-detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1077,6 +1077,18 @@ const SessionDetail = ({
draftTabs.some((draft) => draft.id === activeTabSessionIdRaw);
return stillExists ? activeTabSessionIdRaw : sessionId;
}, [activeTabSessionIdRaw, visibleChildSessions, draftTabs, sessionId]);
/**
* The active conversation's fork target, as a value a RENDER can read.
*
* `chatRefsMap` is a ref, so `getLastAssistantTurnId()` answers only when
* somebody asks — which is fine for a click and useless for a disabled state.
* `setChatTabRef` mirrors it into state below; this ref is how that callback
* knows which tab is the active one without taking a dependency and changing
* its identity on every tab switch.
*/
const activeTabSessionIdForForkRef = useRef<string>(activeTabSessionId);
activeTabSessionIdForForkRef.current = activeTabSessionId;
const [activeTabAssistantTurnId, setActiveTabAssistantTurnId] = useState<string | null>(null);
const activeSessionTabId = useMemo<SessionId | null>(() => {
if (activeTabSessionId === sessionId) return sessionId;
return visibleChildSessions.find((s) => s.id === activeTabSessionId)?.id ?? null;
Expand Down Expand Up @@ -1689,6 +1701,17 @@ const SessionDetail = ({
const setChatTabRef = useCallback(
(tabId: string, ref: SessionChatInterfaceHandle | DraftSessionChatInterfaceHandle | null) => {
chatRefsMap.current.set(tabId, ref);
// A DETACH IS IGNORED, and that is what keeps this from looping. Every
// render of this page hands each surface a fresh ref arrow, so React
// calls it with `null` and then with the handle inside one commit; taking
// the `null` would queue a state change on every commit for ever.
// `useImperativeHandle` re-attaches whenever the handle's own
// dependencies change — `lastCompletedAssistantMessageId` among them — so
// the attach alone carries every value this needs.
if (ref === null || tabId !== activeTabSessionIdForForkRef.current) return;
setActiveTabAssistantTurnId(
'getLastAssistantTurnId' in ref ? ref.getLastAssistantTurnId() : null
);
},
[]
);
Expand Down Expand Up @@ -3355,10 +3378,17 @@ const SessionDetail = ({
label: t('sessions.detailTabs.sideSession', 'Side Chat'),
kind: 'session',
pending: isCreatingSideSession,
disabled: launcherState === 'disabled' || isCreatingSideSession,
// The third reason there is nothing to launch, beside an offline machine
// and a launch already in flight: a fork needs a completed assistant turn,
// and `forkActiveConversation` refuses with a toast without one.
disabled:
launcherState === 'disabled' ||
isCreatingSideSession ||
activeTabAssistantTurnId === null,
};
}, [
activeDraftTab,
activeTabAssistantTurnId,
activeTabSession,
activeTabSessionMachineOnlineStatus,
canForkSession,
Expand Down