Skip to content
Closed
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
64 changes: 64 additions & 0 deletions webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,70 @@ describe("ChatTextArea", () => {
expect(setInputValue).toHaveBeenCalledWith("New input")
})

it("should allow navigating back to original input after typing in history mode", () => {
const setInputValue = vi.fn()
const { container } = render(
<ChatTextArea {...defaultProps} setInputValue={setInputValue} inputValue="My original input" />,
)

const textarea = container.querySelector("textarea")!
// Ensure cursor is at the beginning
textarea.setSelectionRange(0, 0)

// Navigate to history (saves "My original input" as tempInput)
fireEvent.keyDown(textarea, { key: "ArrowUp" })
expect(setInputValue).toHaveBeenCalledWith("Third prompt")
setInputValue.mockClear()

// Type something to modify the historical prompt
fireEvent.change(textarea, { target: { value: "Modified third prompt", selectionStart: 21 } })
expect(setInputValue).toHaveBeenCalledWith("Modified third prompt")
setInputValue.mockClear()

// Now press down arrow - should go back to the first history item
textarea.setSelectionRange(0, 0) // Cursor at beginning
fireEvent.keyDown(textarea, { key: "ArrowDown" })

// After typing, pressing down goes back to the first history item
expect(setInputValue).toHaveBeenCalledWith("Third prompt")
setInputValue.mockClear()

// Navigate down to return to original input
fireEvent.keyDown(textarea, { key: "ArrowDown" })
expect(setInputValue).toHaveBeenCalledWith("My original input")
})

it("should preserve original input when modifying history entry and navigating back", () => {
const setInputValue = vi.fn()
const { container } = render(
<ChatTextArea {...defaultProps} setInputValue={setInputValue} inputValue="test" />,
)

const textarea = container.querySelector("textarea")!
// Ensure cursor is at the beginning
textarea.setSelectionRange(0, 0)

// Navigate to history (saves "test" as tempInput)
fireEvent.keyDown(textarea, { key: "ArrowUp" })
expect(setInputValue).toHaveBeenCalledWith("Third prompt")
setInputValue.mockClear()

// Modify the historical prompt
fireEvent.change(textarea, { target: { value: "1Third prompt", selectionStart: 13 } })
expect(setInputValue).toHaveBeenCalledWith("1Third prompt")
setInputValue.mockClear()

// Press down arrow - should go back to unmodified history entry (first history item)
textarea.setSelectionRange(0, 0) // Cursor at beginning
fireEvent.keyDown(textarea, { key: "ArrowDown" })
expect(setInputValue).toHaveBeenCalledWith("Third prompt")
setInputValue.mockClear()

// Press down arrow again - should return to original input "test"
fireEvent.keyDown(textarea, { key: "ArrowDown" })
expect(setInputValue).toHaveBeenCalledWith("test")
})

it("should reset history navigation when sending message", () => {
const onSend = vi.fn()
const setInputValue = vi.fn()
Expand Down
36 changes: 27 additions & 9 deletions webview-ui/src/components/chat/hooks/usePromptHistory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,16 @@ export const usePromptHistory = ({
setPromptHistory(filteredPromptHistory)
// Reset navigation state when switching between history sources
setHistoryIndex(-1)
// Clear tempInput when history source changes (e.g., switching tasks)
setTempInput("")
}, [filteredPromptHistory])

// Reset history navigation when user types (but not when we're setting it programmatically)
const resetOnInputChange = useCallback(() => {
if (historyIndex !== -1) {
// Don't clear tempInput - preserve it so user can navigate back to their original input
setHistoryIndex(-1)
setTempInput("")
// Keep tempInput as is, don't clear it
}
}, [historyIndex])

Expand Down Expand Up @@ -151,27 +153,43 @@ export const usePromptHistory = ({
return navigateToHistory(historyIndex + 1, textarea, "start")
}

// Handle DOWN arrow - only in history navigation mode
if (event.key === "ArrowDown" && historyIndex >= 0 && (isAtBeginning || isAtEnd)) {
// Handle DOWN arrow - allow navigation back even after typing
if (event.key === "ArrowDown" && (isAtBeginning || isAtEnd)) {
event.preventDefault()

if (historyIndex > 0) {
// Keep cursor position consistent with where we started
return navigateToHistory(historyIndex - 1, textarea, isAtBeginning ? "start" : "end")
} else if (historyIndex === 0) {
returnToCurrentInput(textarea, isAtBeginning ? "start" : "end")
// If we're not in history mode but have tempInput, it means user typed after navigating
// Allow them to go back to history
if (historyIndex === -1 && tempInput !== "") {
// User typed something after navigating history, but wants to go back
// Don't overwrite tempInput - it contains the original input we want to preserve
// Go to the most recent history item (index 0)
return navigateToHistory(0, textarea, isAtBeginning ? "start" : "end")
} else if (historyIndex >= 0) {
if (historyIndex > 0) {
// Keep cursor position consistent with where we started
return navigateToHistory(historyIndex - 1, textarea, isAtBeginning ? "start" : "end")
} else if (historyIndex === 0) {
// At the most recent history item, go back to original input
returnToCurrentInput(textarea, isAtBeginning ? "start" : "end")
return true
}
} else if (historyIndex === -1 && inputValue !== "") {
// User is at their original input (or any input) and wants to clear it
setInputValue("")
setCursorPosition(textarea, isAtBeginning ? "start" : "end", 0)
return true
}
}
}
}
return false
},
[promptHistory, historyIndex, inputValue, navigateToHistory, returnToCurrentInput],
[promptHistory, historyIndex, inputValue, tempInput, navigateToHistory, returnToCurrentInput],
)

const resetHistoryNavigation = useCallback(() => {
setHistoryIndex(-1)
// Clear tempInput when explicitly resetting (e.g., when sending a message)
setTempInput("")
}, [])

Expand Down
Loading