diff --git a/site/src/pages/AgentsPage/AgentChatPage.test.ts b/site/src/pages/AgentsPage/AgentChatPage.test.ts index b0acb53af1..c9ee04ac08 100644 --- a/site/src/pages/AgentsPage/AgentChatPage.test.ts +++ b/site/src/pages/AgentsPage/AgentChatPage.test.ts @@ -324,6 +324,9 @@ describe("useConversationEditingState", () => { ); }); expect(localStorage.getItem(expectedKey)).toBe("work in progress"); + // handleContentChange persists only; it must not advance the seed. + expect(result.current.editorInitialValue).toBe(""); + expect(result.current.initialEditorState).toBeUndefined(); act(() => { // Even though the serialized state is non-empty (Lexical always @@ -336,6 +339,27 @@ describe("useConversationEditingState", () => { unmount(); }); + it("carries a draft typed during loading into the seed for the loaded editor", () => { + const { result, unmount } = renderEditing(); + + // handleContentChange persists but does not advance the seed. + const editorState = + '{"root":{"children":[{"text":"typed while loading"}]}}'; + act(() => { + result.current.handleLoadingDraftChange( + "typed while loading", + editorState, + false, + ); + }); + + expect(localStorage.getItem(expectedKey)).toBe(editorState); + expect(result.current.editorInitialValue).toBe("typed while loading"); + expect(result.current.initialEditorState).toBe(editorState); + + unmount(); + }); + it("loads edit text into the composer and restores the prior draft on cancel without refocusing", () => { const { result, unmount } = renderEditing(); diff --git a/site/src/pages/AgentsPage/AgentChatPage.tsx b/site/src/pages/AgentsPage/AgentChatPage.tsx index 89d55e0816..3c6f85e725 100644 --- a/site/src/pages/AgentsPage/AgentChatPage.tsx +++ b/site/src/pages/AgentsPage/AgentChatPage.tsx @@ -569,6 +569,21 @@ export function useConversationEditingState(deps: { } }; + // Separate from handleContentChange, which avoids setState to prevent + // per-keystroke re-renders. The loading editor is a different instance + // that unmounts on load, so the seed must advance here. + const handleLoadingDraftChange = ( + content: string, + serializedEditorState: string, + hasFileReferences: boolean, + ) => { + handleContentChange(content, serializedEditorState, hasFileReferences); + setDraftState({ + editorInitialValue: content, + initialEditorState: serializedEditorState, + }); + }; + return { inputValueRef, chatInputRef, @@ -584,6 +599,7 @@ export function useConversationEditingState(deps: { handleCancelQueueEdit, handleSendFromInput, handleContentChange, + handleLoadingDraftChange, }; } @@ -1548,6 +1564,11 @@ const AgentChatPage: FC = () => { preferencesQuery.isLoading, )} titleElement={titleElement} + inputRef={editing.chatInputRef} + initialValue={editing.editorInitialValue} + initialEditorState={editing.initialEditorState} + remountKey={editing.remountKey} + onContentChange={editing.handleLoadingDraftChange} isInputDisabled={isInputDisabled} effectiveSelectedModel={effectiveSelectedModel} setSelectedModel={setSelectedModel} diff --git a/site/src/pages/AgentsPage/AgentChatPageView.stories.tsx b/site/src/pages/AgentsPage/AgentChatPageView.stories.tsx index 6d905b634c..bd8bb66bc0 100644 --- a/site/src/pages/AgentsPage/AgentChatPageView.stories.tsx +++ b/site/src/pages/AgentsPage/AgentChatPageView.stories.tsx @@ -748,6 +748,11 @@ export const Loading: Story = { Loading — Agents} + inputRef={{ current: null }} + initialValue="" + initialEditorState={undefined} + remountKey={0} + onContentChange={fn()} isInputDisabled effectiveSelectedModel={defaultModelConfigID} setSelectedModel={fn()} @@ -767,6 +772,11 @@ export const LoadingWithModelOptions: Story = { Loading — Agents} + inputRef={{ current: null }} + initialValue="" + initialEditorState={undefined} + remountKey={0} + onContentChange={fn()} isInputDisabled={false} effectiveSelectedModel={defaultModelConfigID} setSelectedModel={fn()} @@ -785,6 +795,11 @@ export const LoadingWithRightPanel: Story = { Loading — Agents} + inputRef={{ current: null }} + initialValue="" + initialEditorState={undefined} + remountKey={0} + onContentChange={fn()} isInputDisabled effectiveSelectedModel={defaultModelConfigID} setSelectedModel={fn()} @@ -804,6 +819,11 @@ export const LoadingSidebarCollapsed: Story = { Loading — Agents} + inputRef={{ current: null }} + initialValue="" + initialEditorState={undefined} + remountKey={0} + onContentChange={fn()} isInputDisabled effectiveSelectedModel={defaultModelConfigID} setSelectedModel={fn()} diff --git a/site/src/pages/AgentsPage/AgentChatPageView.tsx b/site/src/pages/AgentsPage/AgentChatPageView.tsx index ef85af316c..bf23e0b4fa 100644 --- a/site/src/pages/AgentsPage/AgentChatPageView.tsx +++ b/site/src/pages/AgentsPage/AgentChatPageView.tsx @@ -1016,6 +1016,15 @@ export const AgentChatPageView: FC = ({ interface AgentChatPageLoadingViewProps { sendShortcut: AgentChatSendShortcut; titleElement: React.ReactNode; + inputRef: RefObject; + initialValue: string; + initialEditorState: string | undefined; + remountKey: number; + onContentChange: ( + content: string, + serializedEditorState: string, + hasFileReferences: boolean, + ) => void; isInputDisabled: boolean; effectiveSelectedModel: string; setSelectedModel: (model: string) => void; @@ -1033,6 +1042,11 @@ interface AgentChatPageLoadingViewProps { export const AgentChatPageLoadingView: FC = ({ sendShortcut, titleElement, + inputRef, + initialValue, + initialEditorState, + remountKey, + onContentChange, isInputDisabled, effectiveSelectedModel, setSelectedModel, @@ -1085,7 +1099,11 @@ export const AgentChatPageLoadingView: FC = ({ {}} sendShortcut={sendShortcut} - initialValue="" + inputRef={inputRef} + initialValue={initialValue} + initialEditorState={initialEditorState} + remountKey={remountKey} + onContentChange={onContentChange} isDisabled={isInputDisabled} isLoading={false} selectedModel={effectiveSelectedModel}