Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
127 changes: 126 additions & 1 deletion webview-ui/src/components/chat/ChatTextArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ interface ChatTextAreaProps {
modeShortcutText: string
}

interface CursorPositionState {
value: string
afterRender?: "SET_CURSOR_FIRST_LINE" | "SET_CURSOR_LAST_LINE" | "SET_CURSOR_START"
}

const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
(
{
Expand Down Expand Up @@ -75,6 +80,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
cwd,
pinnedApiConfigs,
togglePinnedApiConfig,
taskHistory,
} = useExtensionState()

// Find the ID and display text for the currently selected API configuration
Expand Down Expand Up @@ -153,6 +159,30 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
const [isEnhancingPrompt, setIsEnhancingPrompt] = useState(false)
const [isFocused, setIsFocused] = useState(false)

// 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 from task history
useEffect(() => {
if (taskHistory && taskHistory.length > 0 && cwd) {
// Extract user prompts from task history for the current workspace only
const prompts = 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)
// taskHistory is already in chronological order (oldest first)
// We keep it as-is so that navigation works correctly:
// - Arrow up increases index to go back in history (older prompts)
// - Arrow down decreases index to go forward (newer prompts)
setPromptHistory(prompts)
}
}, [taskHistory, cwd])

// Fetch git commits when Git is selected or when typing a hash.
useEffect(() => {
if (selectedType === ContextMenuOptionType.Git || /^[a-f0-9]+$/i.test(searchQuery)) {
Expand Down Expand Up @@ -360,10 +390,75 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(

const isComposing = event.nativeEvent?.isComposing ?? false

// 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]
setInputValue(historicalPrompt)
setInputValueWithCursor({
value: historicalPrompt,
afterRender: "SET_CURSOR_FIRST_LINE",
})
}
return
}

if (event.key === "ArrowDown" && isAtLastLine) {
event.preventDefault()

// Navigate to next prompt
if (historyIndex > 0) {
const newIndex = historyIndex - 1
setHistoryIndex(newIndex)
const historicalPrompt = promptHistory[newIndex]
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
}
}
}

if (event.key === "Enter" && !event.shiftKey && !isComposing) {
event.preventDefault()

if (!sendingDisabled) {
// Reset history navigation state when sending
setHistoryIndex(-1)
setTempInput("")
onSend()
}
}
Expand Down Expand Up @@ -427,6 +522,9 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
queryItems,
customModes,
fileSearchResults,
historyIndex,
tempInput,
promptHistory,
],
)

Expand All @@ -437,6 +535,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])

// Ref to store the search timeout.
const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null)

Expand All @@ -445,6 +564,12 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
const newValue = e.target.value
setInputValue(newValue)

// Reset history navigation when user types
if (historyIndex !== -1) {
setHistoryIndex(-1)
setTempInput("")
}

const newCursorPosition = e.target.selectionStart
setCursorPosition(newCursorPosition)

Expand Down Expand Up @@ -499,7 +624,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
setFileSearchResults([]) // Clear file search results.
}
},
[setInputValue, setSearchRequestId, setFileSearchResults, setSearchLoading],
[setInputValue, setSearchRequestId, setFileSearchResults, setSearchLoading, historyIndex],
)

useEffect(() => {
Expand Down
Loading
Loading