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/__tests__/usePromptHistory.spec.ts b/webview-ui/src/components/chat/hooks/__tests__/usePromptHistory.spec.ts
new file mode 100644
index 0000000000..ee51191429
--- /dev/null
+++ b/webview-ui/src/components/chat/hooks/__tests__/usePromptHistory.spec.ts
@@ -0,0 +1,84 @@
+import { ClineMessage, HistoryItem } from "@roo-code/types"
+import { act, renderHook } from "@testing-library/react"
+
+import { usePromptHistory, type UsePromptHistoryReturn } 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 }) =>
+ 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("")
+ })
+
+ 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("")
+ })
+})
diff --git a/webview-ui/src/components/chat/hooks/usePromptHistory.ts b/webview-ui/src/components/chat/hooks/usePromptHistory.ts
index 402538182a..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(() => {
@@ -71,11 +73,21 @@ export const usePromptHistory = ({
// Update prompt history when filtered history changes and reset navigation
useEffect(() => {
- setPromptHistory(filteredPromptHistory)
+ const historyChanged =
+ promptHistory.length !== filteredPromptHistory.length ||
+ promptHistory.some((prompt, index) => prompt !== filteredPromptHistory[index])
+ const historySourceChanged = previousHistorySource.current !== historySource
+ previousHistorySource.current = historySource
+
+ if (!historyChanged && !historySourceChanged) return
+
+ if (historyChanged) {
+ setPromptHistory(filteredPromptHistory)
+ }
// Reset navigation state when switching between history sources
setHistoryIndex(-1)
setTempInput("")
- }, [filteredPromptHistory])
+ }, [filteredPromptHistory, historySource, promptHistory])
// Reset history navigation when user types (but not when we're setting it programmatically)
const resetOnInputChange = useCallback(() => {