Skip to content

Commit e757fd9

Browse files
author
Roo
committed
feat: integrate native browser undo stack for enhance prompt feature
- Replace custom undo state management with native browser methods - Use document.execCommand("insertText") for text replacement to preserve undo history - Add setRangeText fallback for browsers without execCommand support - Remove originalPromptBeforeEnhancement state and custom Cmd+Z handling - Add comprehensive tests for native browser method usage - Fix React development mode in test environment for act() support This provides users with the expected VSCode textarea undo experience when using the enhance prompt feature, as requested in PR feedback.
1 parent dd761e2 commit e757fd9

File tree

3 files changed

+113
-35
lines changed

3 files changed

+113
-35
lines changed

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

Lines changed: 23 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,28 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
115115
const message = event.data
116116

117117
if (message.type === "enhancedPrompt") {
118-
if (message.text) {
119-
setInputValue(message.text)
118+
if (message.text && textAreaRef.current) {
119+
// Use native browser methods to preserve undo stack
120+
const textarea = textAreaRef.current
121+
122+
// Focus the textarea to ensure it's the active element
123+
textarea.focus()
124+
125+
// Select all text first
126+
textarea.select()
127+
128+
// Use execCommand to replace text while preserving undo history
129+
if (document.execCommand) {
130+
document.execCommand("insertText", false, message.text)
131+
} else {
132+
// Fallback for browsers that don't support execCommand
133+
// This approach also preserves undo history in modern browsers
134+
textarea.setRangeText(message.text, 0, textarea.value.length, "select")
135+
136+
// Trigger input event to notify React of the change
137+
const inputEvent = new Event("input", { bubbles: true })
138+
textarea.dispatchEvent(inputEvent)
139+
}
120140
}
121141

122142
setIsEnhancingPrompt(false)
@@ -157,7 +177,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
157177
const contextMenuContainerRef = useRef<HTMLDivElement>(null)
158178
const [isEnhancingPrompt, setIsEnhancingPrompt] = useState(false)
159179
const [isFocused, setIsFocused] = useState(false)
160-
const [originalPromptBeforeEnhancement, setOriginalPromptBeforeEnhancement] = useState<string | null>(null)
161180

162181
// Use custom hook for prompt history navigation
163182
const { handleHistoryNavigation, resetHistoryNavigation, resetOnInputChange } = usePromptHistory({
@@ -187,8 +206,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
187206
const trimmedInput = inputValue.trim()
188207

189208
if (trimmedInput) {
190-
// Store the original prompt before enhancement
191-
setOriginalPromptBeforeEnhancement(inputValue)
192209
setIsEnhancingPrompt(true)
193210
vscode.postMessage({ type: "enhancePrompt" as const, text: trimmedInput })
194211
} else {
@@ -379,19 +396,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
379396

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

382-
// Handle undo for enhanced prompt (Cmd+Z/Ctrl+Z)
383-
if (
384-
(event.metaKey || event.ctrlKey) &&
385-
event.key === "z" &&
386-
!event.shiftKey &&
387-
originalPromptBeforeEnhancement
388-
) {
389-
event.preventDefault()
390-
setInputValue(originalPromptBeforeEnhancement)
391-
setOriginalPromptBeforeEnhancement(null)
392-
return
393-
}
394-
395399
// Handle prompt history navigation using custom hook
396400
if (handleHistoryNavigation(event, showContextMenu, isComposing)) {
397401
return
@@ -468,7 +472,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
468472
fileSearchResults,
469473
handleHistoryNavigation,
470474
resetHistoryNavigation,
471-
originalPromptBeforeEnhancement,
472475
],
473476
)
474477

@@ -487,11 +490,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
487490
const newValue = e.target.value
488491
setInputValue(newValue)
489492

490-
// Clear original prompt when user manually changes input
491-
if (originalPromptBeforeEnhancement) {
492-
setOriginalPromptBeforeEnhancement(null)
493-
}
494-
495493
// Reset history navigation when user types
496494
resetOnInputChange()
497495

@@ -549,14 +547,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
549547
setFileSearchResults([]) // Clear file search results.
550548
}
551549
},
552-
[
553-
setInputValue,
554-
setSearchRequestId,
555-
setFileSearchResults,
556-
setSearchLoading,
557-
resetOnInputChange,
558-
originalPromptBeforeEnhancement,
559-
],
550+
[setInputValue, setSearchRequestId, setFileSearchResults, setSearchLoading, resetOnInputChange],
560551
)
561552

562553
useEffect(() => {

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

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -184,10 +184,70 @@ describe("ChatTextArea", () => {
184184
})
185185

186186
describe("enhanced prompt response", () => {
187-
it("should update input value when receiving enhanced prompt", () => {
187+
it("should update input value using native browser methods when receiving enhanced prompt", () => {
188188
const setInputValue = vi.fn()
189189

190-
render(<ChatTextArea {...defaultProps} setInputValue={setInputValue} />)
190+
// Mock document.execCommand
191+
const mockExecCommand = vi.fn().mockReturnValue(true)
192+
Object.defineProperty(document, "execCommand", {
193+
value: mockExecCommand,
194+
writable: true,
195+
})
196+
197+
const { container } = render(
198+
<ChatTextArea {...defaultProps} setInputValue={setInputValue} inputValue="Original prompt" />,
199+
)
200+
201+
const textarea = container.querySelector("textarea")!
202+
203+
// Mock textarea methods
204+
const mockSelect = vi.fn()
205+
const mockFocus = vi.fn()
206+
textarea.select = mockSelect
207+
textarea.focus = mockFocus
208+
209+
// Simulate receiving enhanced prompt message
210+
window.dispatchEvent(
211+
new MessageEvent("message", {
212+
data: {
213+
type: "enhancedPrompt",
214+
text: "Enhanced test prompt",
215+
},
216+
}),
217+
)
218+
219+
// Verify native browser methods were used
220+
expect(mockFocus).toHaveBeenCalled()
221+
expect(mockSelect).toHaveBeenCalled()
222+
expect(mockExecCommand).toHaveBeenCalledWith("insertText", false, "Enhanced test prompt")
223+
})
224+
225+
it("should fallback to setRangeText when execCommand is not available", () => {
226+
const setInputValue = vi.fn()
227+
228+
// Mock document.execCommand to be undefined (not available)
229+
Object.defineProperty(document, "execCommand", {
230+
value: undefined,
231+
writable: true,
232+
})
233+
234+
const { container } = render(
235+
<ChatTextArea {...defaultProps} setInputValue={setInputValue} inputValue="Original prompt" />,
236+
)
237+
238+
const textarea = container.querySelector("textarea")!
239+
240+
// Mock textarea methods
241+
const mockSelect = vi.fn()
242+
const mockFocus = vi.fn()
243+
const mockSetRangeText = vi.fn()
244+
const mockDispatchEvent = vi.fn()
245+
246+
textarea.select = mockSelect
247+
textarea.focus = mockFocus
248+
textarea.setRangeText = mockSetRangeText
249+
textarea.dispatchEvent = mockDispatchEvent
250+
textarea.value = "Original prompt"
191251

192252
// Simulate receiving enhanced prompt message
193253
window.dispatchEvent(
@@ -199,7 +259,28 @@ describe("ChatTextArea", () => {
199259
}),
200260
)
201261

202-
expect(setInputValue).toHaveBeenCalledWith("Enhanced test prompt")
262+
// Verify fallback methods were used
263+
expect(mockFocus).toHaveBeenCalled()
264+
expect(mockSetRangeText).toHaveBeenCalledWith("Enhanced test prompt", 0, 15, "select") // 15 is length of "Original prompt"
265+
expect(mockDispatchEvent).toHaveBeenCalledWith(expect.any(Event))
266+
})
267+
268+
it("should not crash when textarea ref is not available", () => {
269+
const setInputValue = vi.fn()
270+
271+
render(<ChatTextArea {...defaultProps} setInputValue={setInputValue} />)
272+
273+
// Simulate receiving enhanced prompt message when textarea ref might not be ready
274+
expect(() => {
275+
window.dispatchEvent(
276+
new MessageEvent("message", {
277+
data: {
278+
type: "enhancedPrompt",
279+
text: "Enhanced test prompt",
280+
},
281+
}),
282+
)
283+
}).not.toThrow()
203284
})
204285
})
205286

webview-ui/vitest.setup.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import "@testing-library/jest-dom"
22
import "@testing-library/jest-dom/vitest"
33

4+
// Force React into development mode for tests
5+
// This is needed to enable act(...) function in React Testing Library
6+
globalThis.process = globalThis.process || {}
7+
globalThis.process.env = globalThis.process.env || {}
8+
globalThis.process.env.NODE_ENV = "development"
9+
410
class MockResizeObserver {
511
observe() {}
612
unobserve() {}

0 commit comments

Comments
 (0)