Skip to content

Commit e06dad9

Browse files
committed
fix(flow-chat): preserve side-question composer target
Validate target cleanup against live session and pane state so a stale composer snapshot does not clear a newly selected side conversation. Add regression coverage for selection before the session snapshot catches up, including guards against sending to the main session.
1 parent 745f747 commit e06dad9

2 files changed

Lines changed: 32 additions & 13 deletions

File tree

‎src/web-ui/src/flow_chat/hooks/useChatInputTargets.test.tsx‎

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ describe('composer target lifecycle through production stores', () => {
1515
let root: Root;
1616
let container: HTMLDivElement;
1717
let target: ReturnType<typeof useChatInputTargets>;
18-
function Probe() {
18+
function Probe({ sessions }: { sessions?: ReadonlyMap<string, Session> }) {
1919
const state = flowChatStore.getState();
2020
target = useChatInputTargets({ currentSessionId: state.activeSessionId,
21-
sessions: state.sessions, auxiliaryEnabled: true,
21+
sessions: sessions ?? state.sessions, auxiliaryEnabled: true,
2222
activeChild: selectActiveBtwSessionTab(useAgentCanvasStore.getState())?.content.data as BtwSessionPanelData | undefined });
2323
return null;
2424
}
@@ -61,6 +61,24 @@ describe('composer target lifecycle through production stores', () => {
6161
expect(target.isTargetCurrent('a')).toBe(false);
6262
});
6363

64+
it('keeps an explicit side-question target while the composer session snapshot catches up', () => {
65+
render();
66+
const previousSessions = flowChatStore.getState().sessions;
67+
setSession('draft', { sessionKind: 'btw', parentSessionId: 'main',
68+
isTransient: true, agentBackedTransient: true });
69+
act(() => {
70+
useAgentCanvasStore.getState().addTab(buildBtwSessionPanelContent('draft', 'main', {}), 'active');
71+
target.selectTargetSession('draft');
72+
root.render(<Probe sessions={previousSessions} />);
73+
});
74+
expect(target.isTargetCurrent('draft')).toBe(true);
75+
expect(target.isTargetCurrent('main')).toBe(false);
76+
77+
render();
78+
expect(target.effectiveSessionId).toBe('draft');
79+
expect(target.inputTarget).toBe('btw');
80+
});
81+
6482
it('does not transfer selection to a newly opened sibling or revive old selection', () => {
6583
openChild('a');
6684
act(() => target.selectTargetSession('a'));

‎src/web-ui/src/flow_chat/hooks/useChatInputTargets.ts‎

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -66,17 +66,6 @@ export function useChatInputTargets(options: {
6666
candidates: candidatesFor(options.currentSessionId, options.activeChild, options.sessions),
6767
});
6868

69-
// Resolve synchronously in render; cleanup only prevents a removed selection
70-
// from reviving when a closed pane or an unavailable child reappears later.
71-
useLayoutEffect(() => {
72-
if (requested && (requested.surfaceId !== surfaceId
73-
|| requested.parentSessionId !== options.currentSessionId
74-
|| requested.sessionId !== resolution.effectiveSessionId)) {
75-
requestedRef.current = null;
76-
setRequested(null);
77-
}
78-
}, [options.currentSessionId, requested, resolution.effectiveSessionId, surfaceId]);
79-
8069
const resolveLive = useCallback(() => {
8170
const current = optionsRef.current;
8271
const state = FlowChatStore.getInstance().getState();
@@ -91,6 +80,18 @@ export function useChatInputTargets(options: {
9180
});
9281
}, []);
9382

83+
// A new side-question pane can render before ChatInput's session snapshot
84+
// catches up. Only authoritative state may discard an explicit selection.
85+
// Closed panes and unavailable children must still release their selection.
86+
useLayoutEffect(() => {
87+
if (requested && (requested.surfaceId !== surfaceId
88+
|| requested.parentSessionId !== options.currentSessionId
89+
|| requested.sessionId !== resolveLive().effectiveSessionId)) {
90+
requestedRef.current = null;
91+
setRequested(null);
92+
}
93+
}, [options.currentSessionId, requested, resolution.effectiveSessionId, resolveLive, surfaceId]);
94+
9495
const selectTargetSession = useCallback((sessionId: string) => {
9596
const current = optionsRef.current;
9697
if (!resolveLive().targets.some(target => target.sessionId === sessionId)) return;

0 commit comments

Comments
 (0)