feat: Navigate prompt history in prompt field via arrow up/down (#4139) (#4450)

* feat: add prompt history navigation with arrow keys (#4139)

- Navigate through prompt history using arrow up/down keys
- Only triggers when cursor is at first line (up) or last line (down)
- Preserves current input when starting navigation
- Resets navigation state when typing or sending messages
- Follows VSCode's standard UX patterns for history navigation

* fix: correct prompt history order and add workspace filtering (#4139)

- Remove reverse() to maintain chronological order in history array
- Add workspace filtering to only show prompts from current workspace
- Ensure arrow up navigates to older prompts (as expected)
- Filter history items by workspace field matching current cwd

* test: Fix Windows unit test failures for prompt history navigation

- Add missing taskHistory and cwd properties to all useExtensionState mocks
- Add comprehensive test coverage for prompt history navigation feature
- Ensure all 25 tests pass including new prompt history functionality

Fixes failing Windows CI test in PR #4450

* refactor: Improve cursor positioning with useLayoutEffect

- Replace setTimeout(..., 0) with useLayoutEffect for more reliable cursor positioning
- Implement state-based cursor positioning pattern suggested by @mochiya98
- Add CursorPositionState interface for better type safety
- Maintain all existing functionality while improving timing reliability

This addresses the technical suggestion in PR #4450 comment about using
useLayoutEffect instead of setTimeout for DOM manipulation timing.

* feat: optimize prompt history with performance improvements and memory management

- Add useMemo for prompt history filtering to prevent unnecessary re-computations
- Implement MAX_PROMPT_HISTORY_SIZE = 100 limit for memory management
- Extract logic into usePromptHistory custom hook for better code organization
- Simplify ChatTextArea component by delegating history logic to custom hook

Addresses review feedback on PR #4450 for issue #4139

* refactor: clean up unused code and fix linting issues in prompt history

- Remove unused CursorPositionState interface from ChatTextArea
- Remove unused destructured variables from usePromptHistory hook
- Fix missing dependency in useEffect dependency array
- Rename unused parameter with underscore prefix

Related to #4139

* feat: implement hybrid prompt history with position reset

- In chat: Use conversation messages (user_feedback), newest first
- Out of chat: Use task history, oldest first
- Reset navigation position when switching between history sources
- Switch from taskHistory to clineMessages for active conversations
- Maintain backward compatibility with task history fallback
- Add comprehensive tests for hybrid behavior and position reset

This provides intuitive UX where:
- Users navigate recent conversation messages during tasks (newest first)
- Users access initial task prompts when starting fresh (oldest first)
- Navigation always starts fresh when switching contexts

* fix: correct task history slicing order for prompt navigation

Task history was using .slice(-100) which gets the newest 100 tasks,
but we want to show oldest tasks first when navigating. Changed to
.slice(0, 100) to get the oldest 100 tasks instead.

This ensures that when starting fresh (no conversation), up arrow
shows the oldest task prompts first, which is the intended behavior.

* refactor: remove comment on task history size limitation and clarify order preservation

* refactor: replace local ClineMessage and TaskHistoryItem interfaces with imported types

* fix: prevent prompt history fallback to task list during active conversation

When an active task has only an initial prompt with no follow-up user messages,
the prompt history should return empty instead of falling back to task history.
This fixes the "Starting Fresh" behavior appearing inappropriately.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Daniel Riccio <ricciodaniel98@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Hannes Rudolph 2025-06-12 09:43:54 -06:00 committed by GitHub
parent befbebb8a4
commit 69472099ea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 594 additions and 1 deletions

View file

@ -27,6 +27,7 @@ import ContextMenu from "./ContextMenu"
import { VolumeX, Pin, Check } from "lucide-react"
import { IconButton } from "./IconButton"
import { cn } from "@/lib/utils"
import { usePromptHistory } from "./hooks/usePromptHistory"
interface ChatTextAreaProps {
inputValue: string
@ -75,6 +76,8 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
cwd,
pinnedApiConfigs,
togglePinnedApiConfig,
taskHistory,
clineMessages,
} = useExtensionState()
// Find the ID and display text for the currently selected API configuration
@ -153,6 +156,21 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
const [isEnhancingPrompt, setIsEnhancingPrompt] = useState(false)
const [isFocused, setIsFocused] = useState(false)
// Use custom hook for prompt history navigation
const {
inputValueWithCursor,
setInputValueWithCursor,
handleHistoryNavigation,
resetHistoryNavigation,
resetOnInputChange,
} = usePromptHistory({
clineMessages,
taskHistory,
cwd,
inputValue,
setInputValue,
})
// Fetch git commits when Git is selected or when typing a hash.
useEffect(() => {
if (selectedType === ContextMenuOptionType.Git || /^[a-f0-9]+$/i.test(searchQuery)) {
@ -360,10 +378,17 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
const isComposing = event.nativeEvent?.isComposing ?? false
// Handle prompt history navigation using custom hook
if (handleHistoryNavigation(event, showContextMenu, isComposing)) {
return
}
if (event.key === "Enter" && !event.shiftKey && !isComposing) {
event.preventDefault()
if (!sendingDisabled) {
// Reset history navigation state when sending
resetHistoryNavigation()
onSend()
}
}
@ -427,6 +452,8 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
queryItems,
customModes,
fileSearchResults,
handleHistoryNavigation,
resetHistoryNavigation,
],
)
@ -437,6 +464,27 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
}
}, [inputValue, intendedCursorPosition])
// Handle cursor positioning after history navigation
useLayoutEffect(() => {
if (!inputValueWithCursor.afterRender || !textAreaRef.current) return
if (inputValueWithCursor.afterRender === "SET_CURSOR_FIRST_LINE") {
const firstLineEnd =
inputValueWithCursor.value.indexOf("\n") === -1
? inputValueWithCursor.value.length
: inputValueWithCursor.value.indexOf("\n")
textAreaRef.current.setSelectionRange(firstLineEnd, firstLineEnd)
} else if (inputValueWithCursor.afterRender === "SET_CURSOR_LAST_LINE") {
const lines = inputValueWithCursor.value.split("\n")
const lastLineStart = inputValueWithCursor.value.length - lines[lines.length - 1].length
textAreaRef.current.setSelectionRange(lastLineStart, lastLineStart)
} else if (inputValueWithCursor.afterRender === "SET_CURSOR_START") {
textAreaRef.current.setSelectionRange(0, 0)
}
setInputValueWithCursor({ value: inputValueWithCursor.value })
}, [inputValueWithCursor, setInputValueWithCursor])
// Ref to store the search timeout.
const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null)
@ -445,6 +493,9 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
const newValue = e.target.value
setInputValue(newValue)
// Reset history navigation when user types
resetOnInputChange()
const newCursorPosition = e.target.selectionStart
setCursorPosition(newCursorPosition)
@ -499,7 +550,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
setFileSearchResults([]) // Clear file search results.
}
},
[setInputValue, setSearchRequestId, setFileSearchResults, setSearchLoading],
[setInputValue, setSearchRequestId, setFileSearchResults, setSearchLoading, resetOnInputChange],
)
useEffect(() => {

View file

@ -68,6 +68,8 @@ describe("ChatTextArea", () => {
apiConfiguration: {
apiProvider: "anthropic",
},
taskHistory: [],
cwd: "/test/workspace",
})
})
@ -76,6 +78,8 @@ describe("ChatTextArea", () => {
;(useExtensionState as jest.Mock).mockReturnValue({
filePaths: [],
openedTabs: [],
taskHistory: [],
cwd: "/test/workspace",
})
render(<ChatTextArea {...defaultProps} sendingDisabled={true} />)
const enhanceButton = getEnhancePromptButton()
@ -94,6 +98,8 @@ describe("ChatTextArea", () => {
filePaths: [],
openedTabs: [],
apiConfiguration,
taskHistory: [],
cwd: "/test/workspace",
})
render(<ChatTextArea {...defaultProps} inputValue="Test prompt" />)
@ -114,6 +120,8 @@ describe("ChatTextArea", () => {
apiConfiguration: {
apiProvider: "openrouter",
},
taskHistory: [],
cwd: "/test/workspace",
})
render(<ChatTextArea {...defaultProps} inputValue="" />)
@ -131,6 +139,8 @@ describe("ChatTextArea", () => {
apiConfiguration: {
apiProvider: "openrouter",
},
taskHistory: [],
cwd: "/test/workspace",
})
render(<ChatTextArea {...defaultProps} inputValue="Test prompt" />)
@ -155,6 +165,8 @@ describe("ChatTextArea", () => {
apiProvider: "openrouter",
newSetting: "test",
},
taskHistory: [],
cwd: "/test/workspace",
})
rerender(<ChatTextArea {...defaultProps} />)
@ -408,6 +420,338 @@ describe("ChatTextArea", () => {
// Verify setInputValue was not called
expect(setInputValue).not.toHaveBeenCalled()
})
describe("prompt history navigation", () => {
const mockClineMessages = [
{ type: "say", say: "user_feedback", text: "First prompt", ts: 1000 },
{ type: "say", say: "user_feedback", text: "Second prompt", ts: 2000 },
{ type: "say", say: "user_feedback", text: "Third prompt", ts: 3000 },
]
beforeEach(() => {
;(useExtensionState as jest.Mock).mockReturnValue({
filePaths: [],
openedTabs: [],
apiConfiguration: {
apiProvider: "anthropic",
},
taskHistory: [],
clineMessages: mockClineMessages,
cwd: "/test/workspace",
})
})
it("should navigate to previous prompt on arrow up", () => {
const setInputValue = jest.fn()
const { container } = render(
<ChatTextArea {...defaultProps} setInputValue={setInputValue} inputValue="" />,
)
const textarea = container.querySelector("textarea")!
// Simulate arrow up key press
fireEvent.keyDown(textarea, { key: "ArrowUp" })
// Should set the newest conversation message (first in reversed array)
expect(setInputValue).toHaveBeenCalledWith("Third prompt")
})
it("should navigate through history with multiple arrow up presses", () => {
const setInputValue = jest.fn()
const { container } = render(
<ChatTextArea {...defaultProps} setInputValue={setInputValue} inputValue="" />,
)
const textarea = container.querySelector("textarea")!
// First arrow up - newest conversation message
fireEvent.keyDown(textarea, { key: "ArrowUp" })
expect(setInputValue).toHaveBeenCalledWith("Third prompt")
// Update input value to simulate the state change
setInputValue.mockClear()
// Second arrow up - previous conversation message
fireEvent.keyDown(textarea, { key: "ArrowUp" })
expect(setInputValue).toHaveBeenCalledWith("Second prompt")
})
it("should navigate forward with arrow down", () => {
const setInputValue = jest.fn()
const { container } = render(
<ChatTextArea {...defaultProps} setInputValue={setInputValue} inputValue="" />,
)
const textarea = container.querySelector("textarea")!
// Go back in history first (index 0 -> "Third prompt", then index 1 -> "Second prompt")
fireEvent.keyDown(textarea, { key: "ArrowUp" })
fireEvent.keyDown(textarea, { key: "ArrowUp" })
setInputValue.mockClear()
// Navigate forward (from index 1 back to index 0)
fireEvent.keyDown(textarea, { key: "ArrowDown" })
expect(setInputValue).toHaveBeenCalledWith("Third prompt")
})
it("should preserve current input when starting navigation", () => {
const setInputValue = jest.fn()
const { container } = render(
<ChatTextArea {...defaultProps} setInputValue={setInputValue} inputValue="Current input" />,
)
const textarea = container.querySelector("textarea")!
// Navigate to history
fireEvent.keyDown(textarea, { key: "ArrowUp" })
expect(setInputValue).toHaveBeenCalledWith("Third prompt")
setInputValue.mockClear()
// Navigate back to current input
fireEvent.keyDown(textarea, { key: "ArrowDown" })
expect(setInputValue).toHaveBeenCalledWith("Current input")
})
it("should reset history navigation when user types", () => {
const setInputValue = jest.fn()
const { container } = render(
<ChatTextArea {...defaultProps} setInputValue={setInputValue} inputValue="" />,
)
const textarea = container.querySelector("textarea")!
// Navigate to history
fireEvent.keyDown(textarea, { key: "ArrowUp" })
setInputValue.mockClear()
// Type something
fireEvent.change(textarea, { target: { value: "New input", selectionStart: 9 } })
// Should reset history navigation
expect(setInputValue).toHaveBeenCalledWith("New input")
})
it("should reset history navigation when sending message", () => {
const onSend = jest.fn()
const setInputValue = jest.fn()
const { container } = render(
<ChatTextArea
{...defaultProps}
onSend={onSend}
setInputValue={setInputValue}
inputValue="Test message"
/>,
)
const textarea = container.querySelector("textarea")!
// Navigate to history first
fireEvent.keyDown(textarea, { key: "ArrowUp" })
setInputValue.mockClear()
// Send message
fireEvent.keyDown(textarea, { key: "Enter" })
expect(onSend).toHaveBeenCalled()
})
it("should navigate history when cursor is at first line", () => {
const setInputValue = jest.fn()
const { container } = render(
<ChatTextArea {...defaultProps} setInputValue={setInputValue} inputValue="" />,
)
const textarea = container.querySelector("textarea")!
// Clear any calls from initial render
setInputValue.mockClear()
// With empty input, cursor is at first line by default
// Arrow up should navigate history
fireEvent.keyDown(textarea, { key: "ArrowUp" })
expect(setInputValue).toHaveBeenCalledWith("Third prompt")
})
it("should filter history by current workspace", () => {
const mixedClineMessages = [
{ type: "say", say: "user_feedback", text: "Workspace 1 prompt", ts: 1000 },
{ type: "say", say: "user_feedback", text: "Other workspace prompt", ts: 2000 },
{ type: "say", say: "user_feedback", text: "Workspace 1 prompt 2", ts: 3000 },
]
;(useExtensionState as jest.Mock).mockReturnValue({
filePaths: [],
openedTabs: [],
apiConfiguration: {
apiProvider: "anthropic",
},
taskHistory: [],
clineMessages: mixedClineMessages,
cwd: "/test/workspace",
})
const setInputValue = jest.fn()
const { container } = render(
<ChatTextArea {...defaultProps} setInputValue={setInputValue} inputValue="" />,
)
const textarea = container.querySelector("textarea")!
// Should show conversation messages newest first (after reverse)
fireEvent.keyDown(textarea, { key: "ArrowUp" })
expect(setInputValue).toHaveBeenCalledWith("Workspace 1 prompt 2")
setInputValue.mockClear()
fireEvent.keyDown(textarea, { key: "ArrowUp" })
expect(setInputValue).toHaveBeenCalledWith("Other workspace prompt")
})
it("should handle empty conversation history gracefully", () => {
;(useExtensionState as jest.Mock).mockReturnValue({
filePaths: [],
openedTabs: [],
apiConfiguration: {
apiProvider: "anthropic",
},
taskHistory: [],
clineMessages: [],
cwd: "/test/workspace",
})
const setInputValue = jest.fn()
const { container } = render(
<ChatTextArea {...defaultProps} setInputValue={setInputValue} inputValue="" />,
)
const textarea = container.querySelector("textarea")!
// Should not crash or call setInputValue
fireEvent.keyDown(textarea, { key: "ArrowUp" })
expect(setInputValue).not.toHaveBeenCalled()
})
it("should ignore empty or whitespace-only messages", () => {
const clineMessagesWithEmpty = [
{ type: "say", say: "user_feedback", text: "Valid prompt", ts: 1000 },
{ type: "say", say: "user_feedback", text: "", ts: 2000 },
{ type: "say", say: "user_feedback", text: " ", ts: 3000 },
{ type: "say", say: "user_feedback", text: "Another valid prompt", ts: 4000 },
]
;(useExtensionState as jest.Mock).mockReturnValue({
filePaths: [],
openedTabs: [],
apiConfiguration: {
apiProvider: "anthropic",
},
taskHistory: [],
clineMessages: clineMessagesWithEmpty,
cwd: "/test/workspace",
})
const setInputValue = jest.fn()
const { container } = render(
<ChatTextArea {...defaultProps} setInputValue={setInputValue} inputValue="" />,
)
const textarea = container.querySelector("textarea")!
// Should skip empty messages, newest first for conversation
fireEvent.keyDown(textarea, { key: "ArrowUp" })
expect(setInputValue).toHaveBeenCalledWith("Another valid prompt")
setInputValue.mockClear()
fireEvent.keyDown(textarea, { key: "ArrowUp" })
expect(setInputValue).toHaveBeenCalledWith("Valid prompt")
})
it("should use task history (oldest first) when no conversation messages exist", () => {
const mockTaskHistory = [
{ task: "First task", workspace: "/test/workspace" },
{ task: "Second task", workspace: "/test/workspace" },
{ task: "Third task", workspace: "/test/workspace" },
]
;(useExtensionState as jest.Mock).mockReturnValue({
filePaths: [],
openedTabs: [],
apiConfiguration: {
apiProvider: "anthropic",
},
taskHistory: mockTaskHistory,
clineMessages: [], // No conversation messages
cwd: "/test/workspace",
})
const setInputValue = jest.fn()
const { container } = render(
<ChatTextArea {...defaultProps} setInputValue={setInputValue} inputValue="" />,
)
const textarea = container.querySelector("textarea")!
// Should show task history oldest first (chronological order)
fireEvent.keyDown(textarea, { key: "ArrowUp" })
expect(setInputValue).toHaveBeenCalledWith("First task")
setInputValue.mockClear()
fireEvent.keyDown(textarea, { key: "ArrowUp" })
expect(setInputValue).toHaveBeenCalledWith("Second task")
})
it("should reset navigation position when switching between history sources", () => {
const setInputValue = jest.fn()
const { rerender } = render(
<ChatTextArea {...defaultProps} setInputValue={setInputValue} inputValue="" />,
)
// Start with task history
;(useExtensionState as jest.Mock).mockReturnValue({
filePaths: [],
openedTabs: [],
apiConfiguration: {
apiProvider: "anthropic",
},
taskHistory: [
{ task: "Task 1", workspace: "/test/workspace" },
{ task: "Task 2", workspace: "/test/workspace" },
],
clineMessages: [],
cwd: "/test/workspace",
})
rerender(<ChatTextArea {...defaultProps} setInputValue={setInputValue} inputValue="" />)
const textarea = document.querySelector("textarea")!
// Navigate in task history
fireEvent.keyDown(textarea, { key: "ArrowUp" })
expect(setInputValue).toHaveBeenCalledWith("Task 1")
// Switch to conversation messages
;(useExtensionState as jest.Mock).mockReturnValue({
filePaths: [],
openedTabs: [],
apiConfiguration: {
apiProvider: "anthropic",
},
taskHistory: [],
clineMessages: [
{ type: "say", say: "user_feedback", text: "Message 1", ts: 1000 },
{ type: "say", say: "user_feedback", text: "Message 2", ts: 2000 },
],
cwd: "/test/workspace",
})
setInputValue.mockClear()
rerender(<ChatTextArea {...defaultProps} setInputValue={setInputValue} inputValue="" />)
// Should start from beginning of conversation history (newest first)
fireEvent.keyDown(textarea, { key: "ArrowUp" })
expect(setInputValue).toHaveBeenCalledWith("Message 2")
})
})
})
describe("selectApiConfig", () => {

View file

@ -0,0 +1,198 @@
import { ClineMessage, HistoryItem } from "@roo-code/types"
import { useCallback, useEffect, useMemo, useState } from "react"
interface UsePromptHistoryProps {
clineMessages: ClineMessage[] | undefined
taskHistory: HistoryItem[] | undefined
cwd: string | undefined
inputValue: string
setInputValue: (value: string) => void
}
interface CursorPositionState {
value: string
afterRender?: "SET_CURSOR_FIRST_LINE" | "SET_CURSOR_LAST_LINE" | "SET_CURSOR_START"
}
export interface UsePromptHistoryReturn {
historyIndex: number
setHistoryIndex: (index: number) => void
tempInput: string
setTempInput: (input: string) => void
promptHistory: string[]
inputValueWithCursor: CursorPositionState
setInputValueWithCursor: (state: CursorPositionState) => void
handleHistoryNavigation: (
event: React.KeyboardEvent<HTMLTextAreaElement>,
showContextMenu: boolean,
isComposing: boolean,
) => boolean
resetHistoryNavigation: () => void
resetOnInputChange: () => void
}
export const usePromptHistory = ({
clineMessages,
taskHistory,
cwd,
inputValue,
setInputValue,
}: UsePromptHistoryProps): UsePromptHistoryReturn => {
// Maximum number of prompts to keep in history for memory management
const MAX_PROMPT_HISTORY_SIZE = 100
// Prompt history navigation state
const [historyIndex, setHistoryIndex] = useState(-1)
const [tempInput, setTempInput] = useState("")
const [promptHistory, setPromptHistory] = useState<string[]>([])
const [inputValueWithCursor, setInputValueWithCursor] = useState<CursorPositionState>({ value: inputValue })
// Initialize prompt history with hybrid approach: conversation messages if in task, otherwise task history
const filteredPromptHistory = useMemo(() => {
// First try to get conversation messages (user_feedback from clineMessages)
const conversationPrompts = clineMessages
?.filter((message) => {
// Filter for user_feedback messages that have text content
return (
message.type === "say" &&
message.say === "user_feedback" &&
message.text &&
message.text.trim() !== ""
)
})
.map((message) => message.text!)
// If we have conversation messages, use those (newest first when navigating up)
if (conversationPrompts && conversationPrompts.length > 0) {
return conversationPrompts.slice(-MAX_PROMPT_HISTORY_SIZE).reverse() // newest first for conversation messages
}
// If we have clineMessages array (meaning we're in an active task), don't fall back to task history
// Only use task history when starting fresh (no active conversation)
if (clineMessages && clineMessages.length > 0) {
return []
}
// Fall back to task history only when starting fresh (no active conversation)
if (!taskHistory || taskHistory.length === 0 || !cwd) {
return []
}
// Extract user prompts from task history for the current workspace only
const taskPrompts = taskHistory
.filter((item) => {
// Filter by workspace and ensure task is not empty
return item.task && item.task.trim() !== "" && (!item.workspace || item.workspace === cwd)
})
.map((item) => item.task)
.slice(0, MAX_PROMPT_HISTORY_SIZE)
return taskPrompts
}, [clineMessages, taskHistory, cwd])
// Update prompt history when filtered history changes and reset navigation
useEffect(() => {
setPromptHistory(filteredPromptHistory)
// Reset navigation state when switching between history sources
setHistoryIndex(-1)
setTempInput("")
}, [filteredPromptHistory])
// Reset history navigation when user types (but not when we're setting it programmatically)
const resetOnInputChange = useCallback(() => {
if (historyIndex !== -1) {
setHistoryIndex(-1)
setTempInput("")
}
}, [historyIndex])
const handleHistoryNavigation = useCallback(
(event: React.KeyboardEvent<HTMLTextAreaElement>, showContextMenu: boolean, isComposing: boolean): boolean => {
// Handle prompt history navigation
if (!showContextMenu && promptHistory.length > 0 && !isComposing) {
const textarea = event.currentTarget
const { selectionStart, selectionEnd, value } = textarea
const lines = value.substring(0, selectionStart).split("\n")
const currentLineIndex = lines.length - 1
const totalLines = value.split("\n").length
const isAtFirstLine = currentLineIndex === 0
const isAtLastLine = currentLineIndex === totalLines - 1
const hasSelection = selectionStart !== selectionEnd
// Only navigate history if cursor is at first/last line and no text is selected
if (!hasSelection) {
if (event.key === "ArrowUp" && isAtFirstLine) {
event.preventDefault()
// Save current input if starting navigation
if (historyIndex === -1 && inputValue.trim() !== "") {
setTempInput(inputValue)
}
// Navigate to previous prompt
const newIndex = historyIndex + 1
if (newIndex < promptHistory.length) {
setHistoryIndex(newIndex)
const historicalPrompt = promptHistory[newIndex]
if (historicalPrompt) {
setInputValue(historicalPrompt)
setInputValueWithCursor({
value: historicalPrompt,
afterRender: "SET_CURSOR_FIRST_LINE",
})
}
}
return true
}
if (event.key === "ArrowDown" && isAtLastLine) {
event.preventDefault()
// Navigate to next prompt
if (historyIndex > 0) {
const newIndex = historyIndex - 1
setHistoryIndex(newIndex)
const historicalPrompt = promptHistory[newIndex]
if (historicalPrompt) {
setInputValue(historicalPrompt)
setInputValueWithCursor({
value: historicalPrompt,
afterRender: "SET_CURSOR_LAST_LINE",
})
}
} else if (historyIndex === 0) {
// Return to current input
setHistoryIndex(-1)
setInputValue(tempInput)
setInputValueWithCursor({
value: tempInput,
afterRender: "SET_CURSOR_START",
})
}
return true
}
}
}
return false
},
[promptHistory, historyIndex, inputValue, tempInput, setInputValue],
)
const resetHistoryNavigation = useCallback(() => {
setHistoryIndex(-1)
setTempInput("")
}, [])
return {
historyIndex,
setHistoryIndex,
tempInput,
setTempInput,
promptHistory,
inputValueWithCursor,
setInputValueWithCursor,
handleHistoryNavigation,
resetHistoryNavigation,
resetOnInputChange,
}
}