From 7e15aa750ca32498da01f3e2df0bf5bfb9cf1fbf Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+berry-13@users.noreply.github.com> Date: Sat, 29 Aug 2026 00:20:14 +0200 Subject: [PATCH] fix(client): restore the released text for non-text answers too Only the free-text path sets consumedComposerText, so gating the restore on it discarded the stashed ordinary message whenever the user answered by clicking an option or skipping, while the deletion below ran regardless. The stash is now restored whenever one exists, independently of the reset condition, and it outranks whatever the composer holds since that is the answer draft the submission just consumed. --- .../src/hooks/Input/useAskAnswerMode.spec.ts | 33 +++++++++++++++++++ client/src/hooks/Input/useAskAnswerMode.ts | 23 +++++++------ 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/client/src/hooks/Input/useAskAnswerMode.spec.ts b/client/src/hooks/Input/useAskAnswerMode.spec.ts index 7122122946..3b19074403 100644 --- a/client/src/hooks/Input/useAskAnswerMode.spec.ts +++ b/client/src/hooks/Input/useAskAnswerMode.spec.ts @@ -266,6 +266,39 @@ describe('useAskAnswerMode', () => { expect(mockReleasedComposerText).toEqual({}); }); + it('hands the released text back for an option answer too', () => { + /** Only the free-text path sets `consumedComposerText`, so gating the + * restore on it discarded the stash whenever the user answered by + * clicking an option (or skipping) instead of typing. */ + mockReleasedComposerText = { a1: 'an ordinary unsent message' }; + const askWithOptions = { + actionId: 'a1', + question: { + question: 'Pick one', + options: [ + { label: 'Blue', value: 'blue' }, + { label: 'Green', value: 'green' }, + ], + multiSelect: false, + }, + } as unknown as typeof liveAsk; + mockUseGetMessages.mockReturnValue({ data: askWithOptions }); + mockSubmitAskAnswer.mockImplementation( + (_actionId: string, _answer: string, options?: { onSuccess?: () => void }) => { + options?.onSuccess?.(); + }, + ); + const { result } = renderHook(() => useAskAnswerMode('conversation-1')); + + act(() => { + result.current.submitOption(0); + }); + + expect(mockSubmitAskAnswer).toHaveBeenCalledWith('a1', 'blue', expect.anything()); + expect(mockSetComposerText).toHaveBeenLastCalledWith('text', 'an ordinary unsent message'); + expect(mockReleasedComposerText).toEqual({}); + }); + it('does not let a delayed answer success clear the composer or selection after navigation', () => { let finishAnswer: (() => void) | undefined; mockUseGetMessages.mockReturnValue({ data: liveAsk }); diff --git a/client/src/hooks/Input/useAskAnswerMode.ts b/client/src/hooks/Input/useAskAnswerMode.ts index 182012c2b7..e9c0fce4ad 100644 --- a/client/src/hooks/Input/useAskAnswerMode.ts +++ b/client/src/hooks/Input/useAskAnswerMode.ts @@ -369,20 +369,23 @@ export default function useAskAnswerMode(conversationId?: string | null) { return next; }); /** A successful answer never goes through `collapse`, so the released - * text has to be handed back here too or the user's ordinary - * message dies with the question. Restored in place of the reset - * when one is stashed; the entry is dropped either way so the map - * stays bounded. */ + * text has to be handed back here too or the user's ordinary message + * dies with the question. + * + * Restored whenever one is stashed, independently of the reset + * condition below: only the free-text path sets + * `consumedComposerText`, so gating on it discarded the stash on an + * option click or Skip. The stash also outranks whatever the + * composer currently holds, which is the answer draft this + * submission just consumed. */ const released = releasedComposerTextRef.current[submittedActionId]; - if ( + if (released) { + currentScope.formContext?.setValue('text', released); + } else if ( (consumedComposerText || (wasActive && saveDrafts)) && currentScope.formContext?.getValues('text') === submittedComposerText ) { - if (released) { - currentScope.formContext.setValue('text', released); - } else { - currentScope.formContext.reset(); - } + currentScope.formContext.reset(); } setReleasedComposerText((current) => { if (current[submittedActionId] == null) {