From 1a2ddab65a63cd3f478c8080231247aeea6fe394 Mon Sep 17 00:00:00 2001 From: Amp Date: Wed, 16 Sep 2026 12:25:35 +0000 Subject: [PATCH 1/4] fix(webview): preserve prompt history draft while streaming Amp-Thread-ID: https://ampcode.com/threads/T-01a0aa02-b6de-7763-97b8-1650ea4b46da --- .../chat/__tests__/ChatTextArea.spec.tsx | 32 +++++++++++++++++++ .../components/chat/hooks/usePromptHistory.ts | 8 ++++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx b/webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx index aae9ac3cc6..ce22d4b07c 100644 --- a/webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx @@ -583,6 +583,38 @@ describe("ChatTextArea", () => { expect(setInputValue).toHaveBeenCalledWith("Current input") }) + it("should preserve current input while assistant messages stream", () => { + const setInputValue = vi.fn() + const { container, rerender } = render( + , + ) + const textarea = container.querySelector("textarea")! + + textarea.setSelectionRange(0, 0) + fireEvent.keyDown(textarea, { key: "ArrowUp" }) + expect(setInputValue).toHaveBeenCalledWith("Third prompt") + ;(useExtensionState as ReturnType).mockReturnValue({ + filePaths: [], + openedTabs: [], + apiConfiguration: { + apiProvider: providerIdentifiers.anthropic, + }, + taskHistory: [], + clineMessages: [ + ...mockClineMessages, + { type: "say", say: "text", text: "Streaming assistant output", ts: 4000 }, + ], + cwd: "/test/workspace", + }) + setInputValue.mockClear() + rerender() + textarea.setSelectionRange(textarea.value.length, textarea.value.length) + + fireEvent.keyDown(textarea, { key: "ArrowDown" }) + + expect(setInputValue).toHaveBeenCalledWith("Current input") + }) + it("should reset history navigation when user types", () => { const setInputValue = vi.fn() const { container } = render( diff --git a/webview-ui/src/components/chat/hooks/usePromptHistory.ts b/webview-ui/src/components/chat/hooks/usePromptHistory.ts index 402538182a..ae3b27893d 100644 --- a/webview-ui/src/components/chat/hooks/usePromptHistory.ts +++ b/webview-ui/src/components/chat/hooks/usePromptHistory.ts @@ -71,11 +71,17 @@ export const usePromptHistory = ({ // Update prompt history when filtered history changes and reset navigation useEffect(() => { + const historyChanged = + promptHistory.length !== filteredPromptHistory.length || + promptHistory.some((prompt, index) => prompt !== filteredPromptHistory[index]) + + if (!historyChanged) return + setPromptHistory(filteredPromptHistory) // Reset navigation state when switching between history sources setHistoryIndex(-1) setTempInput("") - }, [filteredPromptHistory]) + }, [filteredPromptHistory, promptHistory]) // Reset history navigation when user types (but not when we're setting it programmatically) const resetOnInputChange = useCallback(() => { From 19b6a793f5e8708154eacf0766b00e0bc28308ff Mon Sep 17 00:00:00 2001 From: Amp Date: Wed, 16 Sep 2026 15:57:59 +0000 Subject: [PATCH 2/4] fix(webview): reset prompt navigation on source change Amp-Thread-ID: https://ampcode.com/threads/T-01a0aac1-c3f8-7220-a896-ac2dafbf72e1 --- .../hooks/__tests__/usePromptHistory.spec.ts | 53 +++++++++++++++++++ .../components/chat/hooks/usePromptHistory.ts | 14 +++-- 2 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 webview-ui/src/components/chat/hooks/__tests__/usePromptHistory.spec.ts diff --git a/webview-ui/src/components/chat/hooks/__tests__/usePromptHistory.spec.ts b/webview-ui/src/components/chat/hooks/__tests__/usePromptHistory.spec.ts new file mode 100644 index 0000000000..89cbbb5e22 --- /dev/null +++ b/webview-ui/src/components/chat/hooks/__tests__/usePromptHistory.spec.ts @@ -0,0 +1,53 @@ +import { ClineMessage, HistoryItem } from "@roo-code/types" +import { act, renderHook } from "@testing-library/react" + +import { usePromptHistory } from "../usePromptHistory" + +describe("usePromptHistory", () => { + it("resets navigation when switching to conversation history with identical prompts", () => { + const prompt = "Explain this code" + const taskHistory: HistoryItem[] = [ + { + id: "task-1", + number: 1, + ts: 1, + task: prompt, + tokensIn: 0, + tokensOut: 0, + totalCost: 0, + workspace: "/workspace", + }, + ] + const conversationHistory: ClineMessage[] = [ + { ts: 2, type: "say", say: "user_feedback", text: prompt }, + ] + const setInputValue = vi.fn() + + const { result, rerender } = renderHook( + ({ clineMessages }: { clineMessages: ClineMessage[] | undefined }) => + usePromptHistory({ + clineMessages, + taskHistory, + cwd: "/workspace", + inputValue: "draft", + setInputValue, + }), + { initialProps: { clineMessages: undefined } }, + ) + + act(() => { + result.current.setHistoryIndex(0) + result.current.setTempInput("draft") + }) + + expect(result.current.promptHistory).toEqual([prompt]) + expect(result.current.historyIndex).toBe(0) + expect(result.current.tempInput).toBe("draft") + + rerender({ clineMessages: conversationHistory }) + + expect(result.current.promptHistory).toEqual([prompt]) + expect(result.current.historyIndex).toBe(-1) + expect(result.current.tempInput).toBe("") + }) +}) diff --git a/webview-ui/src/components/chat/hooks/usePromptHistory.ts b/webview-ui/src/components/chat/hooks/usePromptHistory.ts index ae3b27893d..fef127953a 100644 --- a/webview-ui/src/components/chat/hooks/usePromptHistory.ts +++ b/webview-ui/src/components/chat/hooks/usePromptHistory.ts @@ -1,5 +1,5 @@ import { ClineMessage, HistoryItem } from "@roo-code/types" -import { useCallback, useEffect, useMemo, useState } from "react" +import { useCallback, useEffect, useMemo, useRef, useState } from "react" interface UsePromptHistoryProps { clineMessages: ClineMessage[] | undefined @@ -38,6 +38,8 @@ export const usePromptHistory = ({ const [historyIndex, setHistoryIndex] = useState(-1) const [tempInput, setTempInput] = useState("") const [promptHistory, setPromptHistory] = useState([]) + const historySource = clineMessages?.length ? "conversation" : "task" + const previousHistorySource = useRef(historySource) // Initialize prompt history with hybrid approach: conversation messages if in task, otherwise task history const filteredPromptHistory = useMemo(() => { @@ -74,14 +76,18 @@ export const usePromptHistory = ({ const historyChanged = promptHistory.length !== filteredPromptHistory.length || promptHistory.some((prompt, index) => prompt !== filteredPromptHistory[index]) + const historySourceChanged = previousHistorySource.current !== historySource + previousHistorySource.current = historySource - if (!historyChanged) return + if (!historyChanged && !historySourceChanged) return - setPromptHistory(filteredPromptHistory) + if (historyChanged) { + setPromptHistory(filteredPromptHistory) + } // Reset navigation state when switching between history sources setHistoryIndex(-1) setTempInput("") - }, [filteredPromptHistory, promptHistory]) + }, [filteredPromptHistory, historySource, promptHistory]) // Reset history navigation when user types (but not when we're setting it programmatically) const resetOnInputChange = useCallback(() => { From af788e7c84eb82900b82fe51c49cca70524a0e9d Mon Sep 17 00:00:00 2001 From: Amp Date: Wed, 16 Sep 2026 16:17:02 +0000 Subject: [PATCH 3/4] test(webview): type prompt history rerender props Amp-Thread-ID: https://ampcode.com/threads/T-01a0aafa-4924-751a-9a0b-dce899b72a16 --- .../chat/hooks/__tests__/usePromptHistory.spec.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/webview-ui/src/components/chat/hooks/__tests__/usePromptHistory.spec.ts b/webview-ui/src/components/chat/hooks/__tests__/usePromptHistory.spec.ts index 89cbbb5e22..78b9cc435e 100644 --- a/webview-ui/src/components/chat/hooks/__tests__/usePromptHistory.spec.ts +++ b/webview-ui/src/components/chat/hooks/__tests__/usePromptHistory.spec.ts @@ -18,13 +18,14 @@ describe("usePromptHistory", () => { workspace: "/workspace", }, ] - const conversationHistory: ClineMessage[] = [ - { ts: 2, type: "say", say: "user_feedback", text: prompt }, - ] + const conversationHistory: ClineMessage[] = [{ ts: 2, type: "say", say: "user_feedback", text: prompt }] const setInputValue = vi.fn() - const { result, rerender } = renderHook( - ({ clineMessages }: { clineMessages: ClineMessage[] | undefined }) => + const { result, rerender } = renderHook< + ReturnType, + { clineMessages: ClineMessage[] | undefined } + >( + ({ clineMessages }) => usePromptHistory({ clineMessages, taskHistory, From 74f8e5bddf6129ad1b5a3afd636687cf474bcc35 Mon Sep 17 00:00:00 2001 From: PierrunoYT Date: Fri, 18 Sep 2026 15:30:28 +0200 Subject: [PATCH 4/4] test(webview): cover prompt history content updates --- .../hooks/__tests__/usePromptHistory.spec.ts | 40 ++++++++++++++++--- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/webview-ui/src/components/chat/hooks/__tests__/usePromptHistory.spec.ts b/webview-ui/src/components/chat/hooks/__tests__/usePromptHistory.spec.ts index 78b9cc435e..ee51191429 100644 --- a/webview-ui/src/components/chat/hooks/__tests__/usePromptHistory.spec.ts +++ b/webview-ui/src/components/chat/hooks/__tests__/usePromptHistory.spec.ts @@ -1,7 +1,7 @@ import { ClineMessage, HistoryItem } from "@roo-code/types" import { act, renderHook } from "@testing-library/react" -import { usePromptHistory } from "../usePromptHistory" +import { usePromptHistory, type UsePromptHistoryReturn } from "../usePromptHistory" describe("usePromptHistory", () => { it("resets navigation when switching to conversation history with identical prompts", () => { @@ -21,10 +21,7 @@ describe("usePromptHistory", () => { const conversationHistory: ClineMessage[] = [{ ts: 2, type: "say", say: "user_feedback", text: prompt }] const setInputValue = vi.fn() - const { result, rerender } = renderHook< - ReturnType, - { clineMessages: ClineMessage[] | undefined } - >( + const { result, rerender } = renderHook( ({ clineMessages }) => usePromptHistory({ clineMessages, @@ -51,4 +48,37 @@ describe("usePromptHistory", () => { expect(result.current.historyIndex).toBe(-1) expect(result.current.tempInput).toBe("") }) + + it("resets navigation when the current history source gains a prompt", () => { + const firstPrompt = "Explain this code" + const secondPrompt = "Now simplify it" + const initialHistory: ClineMessage[] = [{ ts: 1, type: "say", say: "user_feedback", text: firstPrompt }] + const updatedHistory: ClineMessage[] = [ + ...initialHistory, + { ts: 2, type: "say", say: "user_feedback", text: secondPrompt }, + ] + + const { result, rerender } = renderHook( + ({ clineMessages }) => + usePromptHistory({ + clineMessages, + taskHistory: undefined, + cwd: "/workspace", + inputValue: "draft", + setInputValue: vi.fn(), + }), + { initialProps: { clineMessages: initialHistory } }, + ) + + act(() => { + result.current.setHistoryIndex(0) + result.current.setTempInput("draft") + }) + + rerender({ clineMessages: updatedHistory }) + + expect(result.current.promptHistory).toEqual([secondPrompt, firstPrompt]) + expect(result.current.historyIndex).toBe(-1) + expect(result.current.tempInput).toBe("") + }) })