fix: sync subagent and answer handoffs

This commit is contained in:
Marco Beretta 2026-08-03 17:08:14 +02:00
parent 43b1be085a
commit 4e3f15c683
No known key found for this signature in database
GPG key ID: D918033D8E74CC11
2 changed files with 58 additions and 7 deletions

View file

@ -1,10 +1,15 @@
const mockSubmitAskAnswer = jest.fn();
const mockResetComposer = jest.fn();
const mockGetComposerText = jest.fn(() => 'answer from A');
const mockSetComposerText = jest.fn();
const mockSetCollapsedIds = jest.fn();
const mockSetSelected = jest.fn();
const mockSetChecked = jest.fn();
const mockSetAnswerDraft = jest.fn();
const mockSetDraft = jest.fn();
let mockSaveDrafts = false;
let mockCollapsedIds: string[] = [];
let mockAnswerDraft = { actionId: null as string | null, text: '' };
jest.mock('~/data-provider', () => ({ useGetMessagesByConvoId: jest.fn() }));
jest.mock('~/components/Chat/Messages/Content/ApprovalContext', () => ({
@ -15,15 +20,20 @@ jest.mock('~/Providers', () => ({
useOptionalChatFormContext: () => ({
reset: mockResetComposer,
getValues: mockGetComposerText,
setValue: mockSetComposerText,
}),
}));
jest.mock('~/utils', () => ({
getAskAnswerDraftId: (id: string) => `draft-${id}`,
morphTransition: (update: () => void) => update(),
setDraft: (...args: unknown[]) => mockSetDraft(...args),
}));
jest.mock('recoil', () => ({
atom: (cfg: unknown) => cfg,
useRecoilState: (state: { key?: string }) => {
if (state.key === 'askAnswerModeCollapsedActions') {
return [mockCollapsedIds, mockSetCollapsedIds];
}
if (state.key === 'askAnswerModeSelection') {
return [null, mockSetSelected];
}
@ -31,7 +41,7 @@ jest.mock('recoil', () => ({
return [[], mockSetChecked];
}
if (state.key === 'askAnswerModeText') {
return [{ actionId: null, text: '' }, mockSetAnswerDraft];
return [mockAnswerDraft, mockSetAnswerDraft];
}
return [[], jest.fn()];
},
@ -55,6 +65,8 @@ describe('useAskAnswerMode', () => {
beforeEach(() => {
jest.clearAllMocks();
mockSaveDrafts = false;
mockCollapsedIds = [];
mockAnswerDraft = { actionId: null, text: '' };
mockGetComposerText.mockReturnValue('answer from A');
});
@ -141,6 +153,34 @@ describe('useAskAnswerMode', () => {
});
});
it('restores the card answer into the composer when drafts are disabled', () => {
mockCollapsedIds = ['a1'];
mockAnswerDraft = { actionId: 'a1', text: 'answer edited in the card' };
mockUseGetMessages.mockReturnValue({ data: liveAsk });
const { result } = renderHook(() => useAskAnswerMode('conversation-1'));
act(() => result.current.expand());
expect(mockSetComposerText).toHaveBeenCalledWith('text', 'answer edited in the card');
});
it('keeps the ask-specific draft current while editing the card', () => {
mockSaveDrafts = true;
mockUseGetMessages.mockReturnValue({ data: liveAsk });
const { result } = renderHook(() => useAskAnswerMode('conversation-1'));
act(() => result.current.setAnswerText('answer edited in the card'));
expect(mockSetAnswerDraft).toHaveBeenCalledWith({
actionId: 'a1',
text: 'answer edited in the card',
});
expect(mockSetDraft).toHaveBeenCalledWith({
id: 'draft-a1',
value: 'answer edited in the card',
});
});
it('does not let a delayed answer success clear the composer or selection after navigation', () => {
let finishAnswer: (() => void) | undefined;
mockUseGetMessages.mockReturnValue({ data: liveAsk });

View file

@ -9,7 +9,7 @@ import {
findLiveAskUserQuestion,
splitOtherOption,
} from '~/utils/approval';
import { getAskAnswerDraftId, morphTransition } from '~/utils';
import { getAskAnswerDraftId, morphTransition, setDraft } from '~/utils';
import { useGetMessagesByConvoId } from '~/data-provider';
import { useOptionalChatFormContext } from '~/Providers';
import store from '~/store';
@ -159,9 +159,15 @@ export default function useAskAnswerMode(conversationId?: string | null) {
(text: string) => {
if (liveAsk) {
setAnswerDraft({ actionId: liveAsk.actionId, text });
/** While the card owns the answer, `useAutoSave` is tracking the
* conversation draft instead. Keep the dormant ask draft current so
* expanding can restore this edit without clobbering that message. */
if (saveDrafts) {
setDraft({ id: getAskAnswerDraftId(liveAsk.actionId), value: text });
}
}
},
[liveAsk, setAnswerDraft],
[liveAsk, saveDrafts, setAnswerDraft],
);
/** Selection state is per-question: a new pause must never inherit a stale
@ -190,11 +196,16 @@ export default function useAskAnswerMode(conversationId?: string | null) {
const expand = useCallback(() => {
if (liveAsk) {
morphTransition(() =>
setCollapsedIds((prev) => prev.filter((id) => id !== liveAsk.actionId)),
);
morphTransition(() => {
/** Autosave restores the ask-specific draft after the key switch. If
* drafts are disabled, perform that handoff directly. */
if (!saveDrafts) {
formContext?.setValue('text', answerText);
}
setCollapsedIds((prev) => prev.filter((id) => id !== liveAsk.actionId));
});
}
}, [liveAsk, setCollapsedIds]);
}, [liveAsk, saveDrafts, formContext, answerText, setCollapsedIds]);
/** Pure check toggle: the keyboard highlight is steered only by the
* composer's digit/arrow shortcuts, so a mouse toggle never leaves a