@@ -148,6 +148,25 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
148148 } , [ ] ) ;
149149 const inputRef = useRef < HTMLTextAreaElement > ( null ) ;
150150 const modelSelectorRef = useRef < ModelSelectorRef > ( null ) ;
151+
152+ // Draft state combines text input and image attachments
153+ // Use these helpers to avoid accidentally losing images when modifying text
154+ interface DraftState {
155+ text : string ;
156+ images : ImageAttachment [ ] ;
157+ }
158+ const getDraft = useCallback (
159+ ( ) : DraftState => ( { text : input , images : imageAttachments } ) ,
160+ [ input , imageAttachments ]
161+ ) ;
162+ const setDraft = useCallback (
163+ ( draft : DraftState ) => {
164+ setInput ( draft . text ) ;
165+ setImageAttachments ( draft . images ) ;
166+ } ,
167+ [ setInput ]
168+ ) ;
169+ const preEditDraftRef = useRef < DraftState > ( { text : "" , images : [ ] } ) ;
151170 const [ mode , setMode ] = useMode ( ) ;
152171 const { recentModels, addModel, defaultModel, setDefaultModel } = useModelLRU ( ) ;
153172 const commandListId = useId ( ) ;
@@ -346,10 +365,11 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
346365 } ;
347366 } , [ focusMessageInput ] ) ;
348367
349- // When entering editing mode, populate input with message content
368+ // When entering editing mode, save current draft and populate with message content
350369 useEffect ( ( ) => {
351370 if ( editingMessage ) {
352- setInput ( editingMessage . content ) ;
371+ preEditDraftRef . current = getDraft ( ) ;
372+ setDraft ( { text : editingMessage . content , images : [ ] } ) ;
353373 // Auto-resize textarea and focus
354374 setTimeout ( ( ) => {
355375 if ( inputRef . current ) {
@@ -360,7 +380,8 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
360380 }
361381 } , 0 ) ;
362382 }
363- } , [ editingMessage , setInput ] ) ;
383+ // eslint-disable-next-line react-hooks/exhaustive-deps -- only run when editingMessage changes
384+ } , [ editingMessage ] ) ;
364385
365386 // Watch input for slash commands
366387 useEffect ( ( ) => {
@@ -826,6 +847,15 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
826847 }
827848 } ;
828849
850+ // Handler for Escape in vim normal mode - cancels edit if editing
851+ const handleEscapeInNormalMode = ( ) => {
852+ if ( variant === "workspace" && editingMessage && props . onCancelEdit ) {
853+ setDraft ( preEditDraftRef . current ) ;
854+ props . onCancelEdit ( ) ;
855+ inputRef . current ?. blur ( ) ;
856+ }
857+ } ;
858+
829859 const handleKeyDown = ( e : React . KeyboardEvent ) => {
830860 // Handle cancel for creation variant
831861 if ( variant === "creation" && matchesKeybind ( e , KEYBINDS . CANCEL ) && props . onCancel ) {
@@ -870,10 +900,13 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
870900 return ;
871901 }
872902
873- // Handle cancel edit (Ctrl+Q) - workspace only
903+ // Handle cancel edit (Escape) - workspace only
904+ // In vim mode, escape first goes to normal mode; escapeInNormalMode callback handles cancel
905+ // In non-vim mode, escape directly cancels edit
874906 if ( matchesKeybind ( e , KEYBINDS . CANCEL_EDIT ) ) {
875- if ( variant === "workspace" && editingMessage && props . onCancelEdit ) {
907+ if ( variant === "workspace" && editingMessage && props . onCancelEdit && ! vimEnabled ) {
876908 e . preventDefault ( ) ;
909+ setDraft ( preEditDraftRef . current ) ;
877910 props . onCancelEdit ( ) ;
878911 const isFocused = document . activeElement === inputRef . current ;
879912 if ( isFocused ) {
@@ -897,7 +930,6 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
897930 }
898931
899932 // Note: ESC handled by VimTextArea (for mode transitions) and CommandSuggestions (for dismissal)
900- // Edit canceling is Ctrl+Q, stream interruption is Ctrl+C (vim) or Esc (normal)
901933
902934 // Don't handle keys if command suggestions are visible
903935 if (
@@ -924,7 +956,10 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
924956
925957 // Workspace variant placeholders
926958 if ( editingMessage ) {
927- return `Edit your message... (${ formatKeybind ( KEYBINDS . CANCEL_EDIT ) } to cancel, ${ formatKeybind ( KEYBINDS . SEND_MESSAGE ) } to send)` ;
959+ const cancelHint = vimEnabled
960+ ? `${ formatKeybind ( KEYBINDS . CANCEL_EDIT ) } ×2 to cancel`
961+ : `${ formatKeybind ( KEYBINDS . CANCEL_EDIT ) } to cancel` ;
962+ return `Edit your message... (${ cancelHint } , ${ formatKeybind ( KEYBINDS . SEND_MESSAGE ) } to send)` ;
928963 }
929964 if ( isCompacting ) {
930965 const interruptKeybind = vimEnabled
@@ -1040,6 +1075,7 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
10401075 onPaste = { handlePaste }
10411076 onDragOver = { handleDragOver }
10421077 onDrop = { handleDrop }
1078+ onEscapeInNormalMode = { handleEscapeInNormalMode }
10431079 suppressKeys = { showCommandSuggestions ? COMMAND_SUGGESTION_KEYS : undefined }
10441080 placeholder = { placeholder }
10451081 disabled = { ! editingMessage && ( disabled || isSending ) }
@@ -1074,7 +1110,8 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
10741110 { /* Editing indicator - workspace only */ }
10751111 { variant === "workspace" && editingMessage && (
10761112 < div className = "text-edit-mode text-[11px] font-medium" >
1077- Editing message ({ formatKeybind ( KEYBINDS . CANCEL_EDIT ) } to cancel)
1113+ Editing message ({ formatKeybind ( KEYBINDS . CANCEL_EDIT ) }
1114+ { vimEnabled ? "×2" : "" } to cancel)
10781115 </ div >
10791116 ) }
10801117
0 commit comments