Skip to content

Commit 1a0bb9f

Browse files
committed
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
1 parent 6e4f195 commit 1a0bb9f

File tree

2 files changed

+203
-93
lines changed

2 files changed

+203
-93
lines changed

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

Lines changed: 27 additions & 93 deletions
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
@@ -159,29 +160,24 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
159160
const [isEnhancingPrompt, setIsEnhancingPrompt] = useState(false)
160161
const [isFocused, setIsFocused] = useState(false)
161162

162-
// Prompt history navigation state
163-
const [historyIndex, setHistoryIndex] = useState(-1)
164-
const [tempInput, setTempInput] = useState("")
165-
const [promptHistory, setPromptHistory] = useState<string[]>([])
166-
const [inputValueWithCursor, setInputValueWithCursor] = useState<CursorPositionState>({ value: inputValue })
167-
168-
// Initialize prompt history from task history
169-
useEffect(() => {
170-
if (taskHistory && taskHistory.length > 0 && cwd) {
171-
// Extract user prompts from task history for the current workspace only
172-
const prompts = taskHistory
173-
.filter((item) => {
174-
// Filter by workspace and ensure task is not empty
175-
return item.task && item.task.trim() !== "" && (!item.workspace || item.workspace === cwd)
176-
})
177-
.map((item) => item.task)
178-
// taskHistory is already in chronological order (oldest first)
179-
// We keep it as-is so that navigation works correctly:
180-
// - Arrow up increases index to go back in history (older prompts)
181-
// - Arrow down decreases index to go forward (newer prompts)
182-
setPromptHistory(prompts)
183-
}
184-
}, [taskHistory, cwd])
163+
// Use custom hook for prompt history navigation
164+
const {
165+
historyIndex,
166+
setHistoryIndex,
167+
tempInput,
168+
setTempInput,
169+
promptHistory,
170+
inputValueWithCursor,
171+
setInputValueWithCursor,
172+
handleHistoryNavigation,
173+
resetHistoryNavigation,
174+
resetOnInputChange,
175+
} = usePromptHistory({
176+
taskHistory,
177+
cwd,
178+
inputValue,
179+
setInputValue,
180+
})
185181

186182
// Fetch git commits when Git is selected or when typing a hash.
187183
useEffect(() => {
@@ -390,75 +386,17 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
390386

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

393-
// Handle prompt history navigation
394-
if (!showContextMenu && promptHistory.length > 0 && !isComposing) {
395-
const textarea = event.currentTarget
396-
const { selectionStart, selectionEnd, value } = textarea
397-
const lines = value.substring(0, selectionStart).split("\n")
398-
const currentLineIndex = lines.length - 1
399-
const totalLines = value.split("\n").length
400-
const isAtFirstLine = currentLineIndex === 0
401-
const isAtLastLine = currentLineIndex === totalLines - 1
402-
const hasSelection = selectionStart !== selectionEnd
403-
404-
// Only navigate history if cursor is at first/last line and no text is selected
405-
if (!hasSelection) {
406-
if (event.key === "ArrowUp" && isAtFirstLine) {
407-
event.preventDefault()
408-
409-
// Save current input if starting navigation
410-
if (historyIndex === -1 && inputValue.trim() !== "") {
411-
setTempInput(inputValue)
412-
}
413-
414-
// Navigate to previous prompt
415-
const newIndex = historyIndex + 1
416-
if (newIndex < promptHistory.length) {
417-
setHistoryIndex(newIndex)
418-
const historicalPrompt = promptHistory[newIndex]
419-
setInputValue(historicalPrompt)
420-
setInputValueWithCursor({
421-
value: historicalPrompt,
422-
afterRender: "SET_CURSOR_FIRST_LINE",
423-
})
424-
}
425-
return
426-
}
427-
428-
if (event.key === "ArrowDown" && isAtLastLine) {
429-
event.preventDefault()
430-
431-
// Navigate to next prompt
432-
if (historyIndex > 0) {
433-
const newIndex = historyIndex - 1
434-
setHistoryIndex(newIndex)
435-
const historicalPrompt = promptHistory[newIndex]
436-
setInputValue(historicalPrompt)
437-
setInputValueWithCursor({
438-
value: historicalPrompt,
439-
afterRender: "SET_CURSOR_LAST_LINE",
440-
})
441-
} else if (historyIndex === 0) {
442-
// Return to current input
443-
setHistoryIndex(-1)
444-
setInputValue(tempInput)
445-
setInputValueWithCursor({
446-
value: tempInput,
447-
afterRender: "SET_CURSOR_START",
448-
})
449-
}
450-
return
451-
}
452-
}
389+
// Handle prompt history navigation using custom hook
390+
if (handleHistoryNavigation(event, showContextMenu, isComposing)) {
391+
return
453392
}
454393

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

458397
if (!sendingDisabled) {
459398
// Reset history navigation state when sending
460-
setHistoryIndex(-1)
461-
setTempInput("")
399+
resetHistoryNavigation()
462400
onSend()
463401
}
464402
}
@@ -522,9 +460,8 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
522460
queryItems,
523461
customModes,
524462
fileSearchResults,
525-
historyIndex,
526-
tempInput,
527-
promptHistory,
463+
handleHistoryNavigation,
464+
resetHistoryNavigation,
528465
],
529466
)
530467

@@ -565,10 +502,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
565502
setInputValue(newValue)
566503

567504
// Reset history navigation when user types
568-
if (historyIndex !== -1) {
569-
setHistoryIndex(-1)
570-
setTempInput("")
571-
}
505+
resetOnInputChange(newValue)
572506

573507
const newCursorPosition = e.target.selectionStart
574508
setCursorPosition(newCursorPosition)
@@ -624,7 +558,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
624558
setFileSearchResults([]) // Clear file search results.
625559
}
626560
},
627-
[setInputValue, setSearchRequestId, setFileSearchResults, setSearchLoading, historyIndex],
561+
[setInputValue, setSearchRequestId, setFileSearchResults, setSearchLoading, resetOnInputChange],
628562
)
629563

630564
useEffect(() => {
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import { useCallback, useEffect, useMemo, useState } from "react"
2+
3+
interface TaskHistoryItem {
4+
task: string
5+
workspace?: string
6+
}
7+
8+
interface UsePromptHistoryProps {
9+
taskHistory: TaskHistoryItem[] | undefined
10+
cwd: string | undefined
11+
inputValue: string
12+
setInputValue: (value: string) => void
13+
}
14+
15+
interface CursorPositionState {
16+
value: string
17+
afterRender?: "SET_CURSOR_FIRST_LINE" | "SET_CURSOR_LAST_LINE" | "SET_CURSOR_START"
18+
}
19+
20+
export interface UsePromptHistoryReturn {
21+
historyIndex: number
22+
setHistoryIndex: (index: number) => void
23+
tempInput: string
24+
setTempInput: (input: string) => void
25+
promptHistory: string[]
26+
inputValueWithCursor: CursorPositionState
27+
setInputValueWithCursor: (state: CursorPositionState) => void
28+
handleHistoryNavigation: (
29+
event: React.KeyboardEvent<HTMLTextAreaElement>,
30+
showContextMenu: boolean,
31+
isComposing: boolean,
32+
) => boolean
33+
resetHistoryNavigation: () => void
34+
resetOnInputChange: (newValue: string) => void
35+
}
36+
37+
export const usePromptHistory = ({
38+
taskHistory,
39+
cwd,
40+
inputValue,
41+
setInputValue,
42+
}: UsePromptHistoryProps): UsePromptHistoryReturn => {
43+
// Maximum number of prompts to keep in history for memory management
44+
const MAX_PROMPT_HISTORY_SIZE = 100
45+
46+
// Prompt history navigation state
47+
const [historyIndex, setHistoryIndex] = useState(-1)
48+
const [tempInput, setTempInput] = useState("")
49+
const [promptHistory, setPromptHistory] = useState<string[]>([])
50+
const [inputValueWithCursor, setInputValueWithCursor] = useState<CursorPositionState>({ value: inputValue })
51+
52+
// Initialize prompt history from task history with performance optimization
53+
const filteredPromptHistory = useMemo(() => {
54+
if (!taskHistory || taskHistory.length === 0 || !cwd) {
55+
return []
56+
}
57+
58+
// Extract user prompts from task history for the current workspace only
59+
const prompts = taskHistory
60+
.filter((item) => {
61+
// Filter by workspace and ensure task is not empty
62+
return item.task && item.task.trim() !== "" && (!item.workspace || item.workspace === cwd)
63+
})
64+
.map((item) => item.task)
65+
// Limit history size to prevent memory issues
66+
.slice(-MAX_PROMPT_HISTORY_SIZE)
67+
68+
// taskHistory is already in chronological order (oldest first)
69+
// We keep it as-is so that navigation works correctly:
70+
// - Arrow up increases index to go back in history (older prompts)
71+
// - Arrow down decreases index to go forward (newer prompts)
72+
return prompts
73+
}, [taskHistory, cwd])
74+
75+
// Update prompt history when filtered history changes
76+
useEffect(() => {
77+
setPromptHistory(filteredPromptHistory)
78+
}, [filteredPromptHistory])
79+
80+
// Reset history navigation when user types (but not when we're setting it programmatically)
81+
const resetOnInputChange = useCallback(
82+
(newValue: string) => {
83+
if (historyIndex !== -1) {
84+
setHistoryIndex(-1)
85+
setTempInput("")
86+
}
87+
},
88+
[historyIndex],
89+
)
90+
91+
const handleHistoryNavigation = useCallback(
92+
(event: React.KeyboardEvent<HTMLTextAreaElement>, showContextMenu: boolean, isComposing: boolean): boolean => {
93+
// Handle prompt history navigation
94+
if (!showContextMenu && promptHistory.length > 0 && !isComposing) {
95+
const textarea = event.currentTarget
96+
const { selectionStart, selectionEnd, value } = textarea
97+
const lines = value.substring(0, selectionStart).split("\n")
98+
const currentLineIndex = lines.length - 1
99+
const totalLines = value.split("\n").length
100+
const isAtFirstLine = currentLineIndex === 0
101+
const isAtLastLine = currentLineIndex === totalLines - 1
102+
const hasSelection = selectionStart !== selectionEnd
103+
104+
// Only navigate history if cursor is at first/last line and no text is selected
105+
if (!hasSelection) {
106+
if (event.key === "ArrowUp" && isAtFirstLine) {
107+
event.preventDefault()
108+
109+
// Save current input if starting navigation
110+
if (historyIndex === -1 && inputValue.trim() !== "") {
111+
setTempInput(inputValue)
112+
}
113+
114+
// Navigate to previous prompt
115+
const newIndex = historyIndex + 1
116+
if (newIndex < promptHistory.length) {
117+
setHistoryIndex(newIndex)
118+
const historicalPrompt = promptHistory[newIndex]
119+
setInputValue(historicalPrompt)
120+
setInputValueWithCursor({
121+
value: historicalPrompt,
122+
afterRender: "SET_CURSOR_FIRST_LINE",
123+
})
124+
}
125+
return true
126+
}
127+
128+
if (event.key === "ArrowDown" && isAtLastLine) {
129+
event.preventDefault()
130+
131+
// Navigate to next prompt
132+
if (historyIndex > 0) {
133+
const newIndex = historyIndex - 1
134+
setHistoryIndex(newIndex)
135+
const historicalPrompt = promptHistory[newIndex]
136+
setInputValue(historicalPrompt)
137+
setInputValueWithCursor({
138+
value: historicalPrompt,
139+
afterRender: "SET_CURSOR_LAST_LINE",
140+
})
141+
} else if (historyIndex === 0) {
142+
// Return to current input
143+
setHistoryIndex(-1)
144+
setInputValue(tempInput)
145+
setInputValueWithCursor({
146+
value: tempInput,
147+
afterRender: "SET_CURSOR_START",
148+
})
149+
}
150+
return true
151+
}
152+
}
153+
}
154+
return false
155+
},
156+
[promptHistory, historyIndex, inputValue, tempInput, setInputValue],
157+
)
158+
159+
const resetHistoryNavigation = useCallback(() => {
160+
setHistoryIndex(-1)
161+
setTempInput("")
162+
}, [])
163+
164+
return {
165+
historyIndex,
166+
setHistoryIndex,
167+
tempInput,
168+
setTempInput,
169+
promptHistory,
170+
inputValueWithCursor,
171+
setInputValueWithCursor,
172+
handleHistoryNavigation,
173+
resetHistoryNavigation,
174+
resetOnInputChange,
175+
}
176+
}

0 commit comments

Comments
 (0)