Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions __tests__/hooks/use-draft-persistence.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -28,6 +38,7 @@ describe("useDraftPersistence", () => {

beforeEach(() => {
vi.clearAllMocks();
mockSetMessageToSend.mockClear();
vi.useFakeTimers();
localStorage.clear();

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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();
Expand Down
4 changes: 3 additions & 1 deletion src/hooks/chat/use-chat-input-logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -59,7 +61,7 @@ export const useChatInputLogic = () => {

return {
chatInputRef,
messageToSend,
messageToSend: messageToSendForResize,
checkIsContentEmpty,
clearEmptyContentHandler,
getCurrentMessage,
Expand Down
24 changes: 24 additions & 0 deletions src/hooks/chat/use-draft-persistence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
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.
Expand Down Expand Up @@ -41,6 +42,27 @@
// 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
Expand All @@ -62,6 +84,8 @@
saveTimeoutRef.current = null;
}

useConversationStore.getState().setMessageToSend("");

Check failure on line 87 in src/hooks/chat/use-draft-persistence.ts

View workflow job for this annotation

GitHub Actions / test-and-build (ubuntu)

Delete `·`

const element = chatInputRef.current;

// --- 2. Handle task-to-real ID transition (preserve draft during initialization) ---
Expand Down
6 changes: 2 additions & 4 deletions src/hooks/use-auto-resize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,11 +325,9 @@
manualHeightRef.current = finalHeight;
}
};

// Update content and resize when value prop changes
useEffect(() => {
useEffect(() => {

Check failure on line 328 in src/hooks/use-auto-resize.ts

View workflow job for this annotation

GitHub Actions / test-and-build (ubuntu)

Insert `··`
const element = elementRef.current;
if (element && value !== undefined) {
if (element && value !== undefined && value.text.length > 0) {
element.textContent = value.text;
smartResize();
}
Expand Down
Loading