From d0e8a5358d4b43fc6ef347120e8239dd3359d677 Mon Sep 17 00:00:00 2001 From: nehaprasad-dev Date: Fri, 22 May 2026 01:05:46 +0530 Subject: [PATCH] fix: stop conversation draft leaking into home chat input --- .../hooks/use-draft-persistence.test.tsx | 46 +++++++++++++++++++ src/hooks/chat/use-chat-input-logic.ts | 4 +- src/hooks/chat/use-draft-persistence.ts | 24 ++++++++++ src/hooks/use-auto-resize.ts | 6 +-- 4 files changed, 75 insertions(+), 5 deletions(-) diff --git a/__tests__/hooks/use-draft-persistence.test.tsx b/__tests__/hooks/use-draft-persistence.test.tsx index 812b235d2..710aff169 100644 --- a/__tests__/hooks/use-draft-persistence.test.tsx +++ b/__tests__/hooks/use-draft-persistence.test.tsx @@ -15,6 +15,16 @@ vi.mock("#/components/features/chat/utils/chat-input.utils", () => ({ getTextContent: vi.fn((el: HTMLDivElement | null) => el?.textContent || ""), })); +const mockSetMessageToSend = vi.fn(); + +vi.mock("#/stores/conversation-store", () => ({ + useConversationStore: { + getState: () => ({ + setMessageToSend: mockSetMessageToSend, + }), + }, +})); + describe("useDraftPersistence", () => { let mockSetDraftMessage: (message: string | null) => void; @@ -28,6 +38,7 @@ describe("useDraftPersistence", () => { beforeEach(() => { vi.clearAllMocks(); + mockSetMessageToSend.mockClear(); vi.useFakeTimers(); localStorage.clear(); @@ -69,6 +80,39 @@ describe("useDraftPersistence", () => { vi.clearAllMocks(); }); + describe("home / zero state (no conversationId)", () => { + it("clears the input and session messageToSend instead of restoring a draft", () => { + const staleDraft = "Draft that must not appear on home"; + const chatInputRef = createMockChatInputRef(staleDraft); + + renderHook(() => + useDraftPersistence(undefined, chatInputRef), + ); + + expect(chatInputRef.current?.textContent).toBe(""); + expect(mockSetMessageToSend).toHaveBeenCalledWith(""); + expect(conversationLocalStorage.getConversationState).not.toHaveBeenCalled(); + }); + + it("saveDraft is a no-op without a conversationId", () => { + const chatInputRef = createMockChatInputRef("typed on home"); + + const { result } = renderHook(() => + useDraftPersistence(undefined, chatInputRef), + ); + + act(() => { + result.current.saveDraft(); + }); + + act(() => { + vi.advanceTimersByTime(500); + }); + + expect(mockSetDraftMessage).not.toHaveBeenCalled(); + }); + }); + describe("draft restoration on mount", () => { it("restores draft from localStorage when mounting with existing draft", () => { // Arrange @@ -504,6 +548,8 @@ describe("useDraftPersistence", () => { // Act - normal conversation switch (not task-to-real) rerender({ conversationId: "conv-B" }); + expect(mockSetMessageToSend).toHaveBeenCalledWith(""); + // Assert - should not use setConversationState directly // (the normal path uses setDraftMessage from the hook) expect(conversationLocalStorage.setConversationState).not.toHaveBeenCalled(); diff --git a/src/hooks/chat/use-chat-input-logic.ts b/src/hooks/chat/use-chat-input-logic.ts index a5f1e78d0..43a329966 100644 --- a/src/hooks/chat/use-chat-input-logic.ts +++ b/src/hooks/chat/use-chat-input-logic.ts @@ -31,6 +31,8 @@ export const useChatInputLogic = () => { chatInputRef, ); + const messageToSendForResize = conversationId ? messageToSend : null; + // Save current input value when drawer state changes useEffect(() => { if (chatInputRef.current) { @@ -59,7 +61,7 @@ export const useChatInputLogic = () => { return { chatInputRef, - messageToSend, + messageToSend: messageToSendForResize, checkIsContentEmpty, clearEmptyContentHandler, getCurrentMessage, diff --git a/src/hooks/chat/use-draft-persistence.ts b/src/hooks/chat/use-draft-persistence.ts index a39fa7005..811324422 100644 --- a/src/hooks/chat/use-draft-persistence.ts +++ b/src/hooks/chat/use-draft-persistence.ts @@ -5,6 +5,7 @@ import { setConversationState, } from "#/utils/conversation-local-storage"; import { getTextContent } from "#/components/features/chat/utils/chat-input.utils"; +import { useConversationStore } from "#/stores/conversation-store"; /** * Check if a conversation ID is a temporary task ID. @@ -41,6 +42,27 @@ export const useDraftPersistence = ( // Track if this is the first mount to handle initial cleanup const isFirstMountRef = useRef(true); + useEffect(() => { + if (conversationId) { + return; + } + + useConversationStore.getState().setMessageToSend(""); + + if (saveTimeoutRef.current) { + clearTimeout(saveTimeoutRef.current); + saveTimeoutRef.current = null; + } + + const element = chatInputRef.current; + if (element) { + element.textContent = ""; + } + + hasRestoredRef.current = false; + setIsRestored(false); + }, [conversationId, chatInputRef]); + // IMPORTANT: This effect must run FIRST when conversation changes. // It handles three concerns: // 1. Cleanup: Cancel pending saves from previous conversation @@ -62,6 +84,8 @@ export const useDraftPersistence = ( saveTimeoutRef.current = null; } + useConversationStore.getState().setMessageToSend(""); + const element = chatInputRef.current; // --- 2. Handle task-to-real ID transition (preserve draft during initialization) --- diff --git a/src/hooks/use-auto-resize.ts b/src/hooks/use-auto-resize.ts index 52546d78e..812eb9b24 100644 --- a/src/hooks/use-auto-resize.ts +++ b/src/hooks/use-auto-resize.ts @@ -325,11 +325,9 @@ export const useAutoResize = ( manualHeightRef.current = finalHeight; } }; - - // Update content and resize when value prop changes - useEffect(() => { +useEffect(() => { const element = elementRef.current; - if (element && value !== undefined) { + if (element && value !== undefined && value.text.length > 0) { element.textContent = value.text; smartResize(); }