mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix: refocus chat input after message send completes (#22666)
## Problem The chat input loses focus after sending a message. Users have to click back into the input field to type their next message. ## Root Cause When a message is sent: 1. `handleSubmit` in `AgentChatInput` calls `onSend(text)` then immediately `focus()` — but this is premature 2. The async send sets `isLoading=true`, which disables the editor via `EditableStatePlugin` 3. When the send resolves, `handleSendFromInput` calls `clear()` and `focus()` — but the editor may still be disabled at this point (React hasn't re-rendered yet) 4. When React re-renders with `isLoading=false`, the editor becomes editable again but nobody restores focus ## Fix Added a `useEffect` in `AgentChatInput` that watches for `isLoading` transitioning from `true` to `false` and calls `focus()` on the editor. This ensures focus is restored *after* React has re-enabled the editor, not prematurely. ## Test Added a test in `AgentDetail.test.ts` verifying that `focus()` is called on the input ref after `handleSendFromInput` resolves.
This commit is contained in:
@@ -263,6 +263,18 @@ export const AgentChatInput = memo<AgentChatInputProps>(
|
||||
[onContentChange],
|
||||
);
|
||||
|
||||
// Re-focus the editor after a send completes (isLoading goes
|
||||
// from true → false) so the user can immediately type again.
|
||||
// Uses the "store previous value in state" pattern recommended
|
||||
// by React for responding to prop changes during render.
|
||||
const [prevIsLoading, setPrevIsLoading] = useState(isLoading);
|
||||
if (prevIsLoading !== isLoading) {
|
||||
setPrevIsLoading(isLoading);
|
||||
if (prevIsLoading && !isLoading) {
|
||||
internalRef.current?.focus();
|
||||
}
|
||||
}
|
||||
|
||||
const canSend = !isDisabled && !isLoading && hasModelOptions && hasContent;
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { act, renderHook } from "@testing-library/react";
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
draftInputStorageKeyPrefix,
|
||||
@@ -86,6 +87,33 @@ describe("useConversationEditingState", () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
it("calls focus on the input ref after a successful send", async () => {
|
||||
const { result, onSend, unmount } = renderEditing();
|
||||
|
||||
// Attach a mock ChatMessageInputRef to the chatInputRef
|
||||
const mockFocus = vi.fn();
|
||||
const mockClear = vi.fn();
|
||||
const mockInputRef = {
|
||||
focus: mockFocus,
|
||||
clear: mockClear,
|
||||
insertText: vi.fn(),
|
||||
getValue: vi.fn().mockReturnValue(""),
|
||||
};
|
||||
// The hook exposes chatInputRef – assign the mock to it.
|
||||
result.current.chatInputRef.current = mockInputRef;
|
||||
|
||||
await act(async () => {
|
||||
result.current.handleSendFromInput("hello");
|
||||
await vi.waitFor(() => {
|
||||
expect(onSend).toHaveBeenCalledWith("hello", undefined);
|
||||
});
|
||||
});
|
||||
|
||||
expect(mockClear).toHaveBeenCalled();
|
||||
expect(mockFocus).toHaveBeenCalled();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it("clears the draft from localStorage on successful send", async () => {
|
||||
localStorage.setItem(expectedKey, "draft to clear");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user