diff --git a/apps/web/src/components/chat/ComposerPendingUserInputPanel.navigation.browser.test.tsx b/apps/web/src/components/chat/ComposerPendingUserInputPanel.navigation.browser.test.tsx index 58ad8e8c0900..777238f7635e 100644 --- a/apps/web/src/components/chat/ComposerPendingUserInputPanel.navigation.browser.test.tsx +++ b/apps/web/src/components/chat/ComposerPendingUserInputPanel.navigation.browser.test.tsx @@ -17,7 +17,9 @@ vi.mock(import("../ChatMarkdown"), async (importOriginal) => ({ })); import { act, fakeTimers, render } from "../../../test/render"; +import { useComposerDraftStore } from "../../composerDraftStore"; import { makeThreadProjectionFixture } from "../../test-fixtures"; +import { toastManager } from "../ui/toast"; import { usePendingThreadRequests } from "./usePendingThreadRequests"; const threadRef = { environmentId: EnvironmentId.make("env"), threadId: ThreadId.make("thread") }; @@ -89,7 +91,8 @@ function useProbeRequests({ environmentId, projection, loading = false }: ProbeP serverProjection: loading ? null : projection, supportsQuestionAttachments: false, composerDraftTarget: { environmentId, threadId: projection.thread.id }, - setComposerDraftPrompt: () => {}, + setComposerDraftPrompt: (target, prompt) => + useComposerDraftStore.getState().setPrompt(target, prompt), promptRef, composerRef, setThreadError: () => {}, @@ -141,6 +144,43 @@ it("retains answers, selections and question position after leaving chat, includ expect(requests).toMatchObject(expected); }); +it("tells the user when an option click moves their typed answer into the hidden composer draft", async () => { + using _timers = fakeTimers(); + const environmentId = EnvironmentId.make("displaced-environment"); + const draftTarget = { environmentId, threadId: threadRef.threadId }; + const draftPrompt = () => useComposerDraftStore.getState().getComposerDraft(draftTarget)?.prompt; + using toast = vi.spyOn(toastManager, "add"); + await using _screen = await render( + , + ); + + await act(() => requests.onSelectActivePendingUserInputOption("question-0", "Incremental")); + expect(toast).not.toHaveBeenCalled(); + + await act(() => requests.onChangeActivePendingUserInputCustomAnswer("question-0", "first try")); + await act(() => requests.onSelectActivePendingUserInputOption("question-0", "Incremental")); + expect(draftPrompt()).toBe("first try"); + expect(toast).toHaveBeenCalledTimes(1); + expect(toast).toHaveBeenCalledWith( + expect.objectContaining({ + type: "info", + title: "Your typed text was set aside", + description: "It will be back in the composer once the question is answered.", + data: { threadRef: draftTarget }, + }), + ); + + await act(() => requests.onChangeActivePendingUserInputCustomAnswer("question-0", "second try")); + expect(draftPrompt()).toBe("first try"); + expect(requests.activePendingDraftAnswers["question-0"]).toMatchObject({ + customAnswer: "second try", + }); + expect(toast).toHaveBeenCalledTimes(1); +}); + function QuestionPositionProbe(props: ProbeProps) { const state = useProbeRequests(props); return ( diff --git a/apps/web/src/components/chat/usePendingThreadRequests.ts b/apps/web/src/components/chat/usePendingThreadRequests.ts index 8a07b86b5eec..d31d6e56643d 100644 --- a/apps/web/src/components/chat/usePendingThreadRequests.ts +++ b/apps/web/src/components/chat/usePendingThreadRequests.ts @@ -50,6 +50,8 @@ import { readEnvironmentScope } from "../../state/session"; import { threadEnvironment } from "../../state/threads"; import { useAtomCommand } from "../../state/use-atom-command"; import { useOrchestrationCommand } from "../../state/use-orchestration-command"; +import { toastManager } from "../ui/toast"; + import type { ChatComposerHandle } from "./ChatComposer"; const EMPTY_PENDING_USER_INPUT_ANSWERS: Record = {}; @@ -403,6 +405,12 @@ export function usePendingThreadRequests(input: { const nextPrompt = carryDisplacedCustomAnswerIntoPrompt(currentPrompt, displacedAnswer); if (nextPrompt !== currentPrompt) { setComposerDraftPrompt(composerDraftTarget, nextPrompt); + toastManager.add({ + type: "info", + title: "Your typed text was set aside", + description: "It will be back in the composer once the question is answered.", + data: { threadRef: activeThreadId ? { environmentId, threadId: activeThreadId } : null }, + }); if (activeThreadId) { const store = useComposerDraftStore.getState(); const questionTarget = questionAttachmentDraftId(