Skip to content

Commit 68c0191

Browse files
hannesrudolphdaniel-lxsclaude
authored andcommitted
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 <[email protected]> --------- Co-authored-by: Daniel Riccio <[email protected]> Co-authored-by: Claude <[email protected]>
1 parent 9e4bcdd commit 68c0191

File tree

3 files changed

+594
-1
lines changed

3 files changed

+594
-1
lines changed

webview-ui/src/components/chat/ChatTextArea.tsx

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import ContextMenu from "./ContextMenu"
2727
import { VolumeX, Pin, Check } from "lucide-react"
2828
import { IconButton } from "./IconButton"
2929
import { cn } from "@/lib/utils"
30+
import { usePromptHistory } from "./hooks/usePromptHistory"
3031

3132
interface ChatTextAreaProps {
3233
inputValue: string
@@ -75,6 +76,8 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
7576
cwd,
7677
pinnedApiConfigs,
7778
togglePinnedApiConfig,
79+
taskHistory,
80+
clineMessages,
7881
} = useExtensionState()
7982

8083
// Find the ID and display text for the currently selected API configuration
@@ -153,6 +156,21 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
153156
const [isEnhancingPrompt, setIsEnhancingPrompt] = useState(false)
154157
const [isFocused, setIsFocused] = useState(false)
155158

159+
// Use custom hook for prompt history navigation
160+
const {
161+
inputValueWithCursor,
162+
setInputValueWithCursor,
163+
handleHistoryNavigation,
164+
resetHistoryNavigation,
165+
resetOnInputChange,
166+
} = usePromptHistory({
167+
clineMessages,
168+
taskHistory,
169+
cwd,
170+
inputValue,
171+
setInputValue,
172+
})
173+
156174
// Fetch git commits when Git is selected or when typing a hash.
157175
useEffect(() => {
158176
if (selectedType === ContextMenuOptionType.Git || /^[a-f0-9]+$/i.test(searchQuery)) {
@@ -360,10 +378,17 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
360378

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

381+
// Handle prompt history navigation using custom hook
382+
if (handleHistoryNavigation(event, showContextMenu, isComposing)) {
383+
return
384+
}
385+
363386
if (event.key === "Enter" && !event.shiftKey && !isComposing) {
364387
event.preventDefault()
365388

366389
if (!sendingDisabled) {
390+
// Reset history navigation state when sending
391+
resetHistoryNavigation()
367392
onSend()
368393
}
369394
}
@@ -427,6 +452,8 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
427452
queryItems,
428453
customModes,
429454
fileSearchResults,
455+
handleHistoryNavigation,
456+
resetHistoryNavigation,
430457
],
431458
)
432459

@@ -437,6 +464,27 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
437464
}
438465
}, [inputValue, intendedCursorPosition])
439466

467+
// Handle cursor positioning after history navigation
468+
useLayoutEffect(() => {
469+
if (!inputValueWithCursor.afterRender || !textAreaRef.current) return
470+
471+
if (inputValueWithCursor.afterRender === "SET_CURSOR_FIRST_LINE") {
472+
const firstLineEnd =
473+
inputValueWithCursor.value.indexOf("\n") === -1
474+
? inputValueWithCursor.value.length
475+
: inputValueWithCursor.value.indexOf("\n")
476+
textAreaRef.current.setSelectionRange(firstLineEnd, firstLineEnd)
477+
} else if (inputValueWithCursor.afterRender === "SET_CURSOR_LAST_LINE") {
478+
const lines = inputValueWithCursor.value.split("\n")
479+
const lastLineStart = inputValueWithCursor.value.length - lines[lines.length - 1].length
480+
textAreaRef.current.setSelectionRange(lastLineStart, lastLineStart)
481+
} else if (inputValueWithCursor.afterRender === "SET_CURSOR_START") {
482+
textAreaRef.current.setSelectionRange(0, 0)
483+
}
484+
485+
setInputValueWithCursor({ value: inputValueWithCursor.value })
486+
}, [inputValueWithCursor, setInputValueWithCursor])
487+
440488
// Ref to store the search timeout.
441489
const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null)
442490

@@ -445,6 +493,9 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
445493
const newValue = e.target.value
446494
setInputValue(newValue)
447495

496+
// Reset history navigation when user types
497+
resetOnInputChange()
498+
448499
const newCursorPosition = e.target.selectionStart
449500
setCursorPosition(newCursorPosition)
450501

@@ -499,7 +550,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
499550
setFileSearchResults([]) // Clear file search results.
500551
}
501552
},
502-
[setInputValue, setSearchRequestId, setFileSearchResults, setSearchLoading],
553+
[setInputValue, setSearchRequestId, setFileSearchResults, setSearchLoading, resetOnInputChange],
503554
)
504555

505556
useEffect(() => {

0 commit comments

Comments
 (0)