@@ -21,6 +21,7 @@ import { DialogSelectModelUnpaid } from "@/components/dialog-select-model-unpaid
2121import { useProviders } from "@/hooks/use-providers"
2222import { useCommand , formatKeybind } from "@/context/command"
2323import { persisted } from "@/utils/persist"
24+ import { Identifier } from "@opencode-ai/util/identifier"
2425
2526const ACCEPTED_IMAGE_TYPES = [ "image/png" , "image/jpeg" , "image/gif" , "image/webp" ]
2627const ACCEPTED_FILE_TYPES = [ ...ACCEPTED_IMAGE_TYPES , "application/pdf" ]
@@ -100,6 +101,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
100101 dragging : boolean
101102 imageAttachments : ImageAttachmentPart [ ]
102103 mode : "normal" | "shell"
104+ applyingHistory : boolean
103105 } > ( {
104106 popover : null ,
105107 historyIndex : - 1 ,
@@ -108,6 +110,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
108110 dragging : false ,
109111 imageAttachments : [ ] ,
110112 mode : "normal" ,
113+ applyingHistory : false ,
111114 } )
112115
113116 const MAX_HISTORY = 100
@@ -135,10 +138,12 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
135138
136139 const applyHistoryPrompt = ( p : Prompt , position : "start" | "end" ) => {
137140 const length = position === "start" ? 0 : promptLength ( p )
141+ setStore ( "applyingHistory" , true )
138142 prompt . set ( p , length )
139143 requestAnimationFrame ( ( ) => {
140144 editorRef . focus ( )
141145 setCursorPosition ( editorRef , length )
146+ setStore ( "applyingHistory" , false )
142147 } )
143148 }
144149
@@ -429,21 +434,42 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
429434 const rawParts = parseFromDOM ( )
430435 const cursorPosition = getCursorPosition ( editorRef )
431436 const rawText = rawParts . map ( ( p ) => ( "content" in p ? p . content : "" ) ) . join ( "" )
437+ const trimmed = rawText . replace ( / \u200B / g, "" ) . trim ( )
438+ const hasNonText = rawParts . some ( ( part ) => part . type !== "text" )
439+ const shouldReset = trimmed . length === 0 && ! hasNonText
432440
433- const atMatch = rawText . substring ( 0 , cursorPosition ) . match ( / @ ( \S * ) $ / )
434- const slashMatch = rawText . match ( / ^ \/ ( \S * ) $ / )
441+ if ( shouldReset ) {
442+ setStore ( "popover" , null )
443+ if ( store . historyIndex >= 0 && ! store . applyingHistory ) {
444+ setStore ( "historyIndex" , - 1 )
445+ setStore ( "savedPrompt" , null )
446+ }
447+ if ( prompt . dirty ( ) ) {
448+ prompt . set ( DEFAULT_PROMPT , 0 )
449+ }
450+ return
451+ }
435452
436- if ( atMatch ) {
437- onInput ( atMatch [ 1 ] )
438- setStore ( "popover" , "file" )
439- } else if ( slashMatch ) {
440- slashOnInput ( slashMatch [ 1 ] )
441- setStore ( "popover" , "slash" )
453+ const shellMode = store . mode === "shell"
454+
455+ if ( ! shellMode ) {
456+ const atMatch = rawText . substring ( 0 , cursorPosition ) . match ( / @ ( \S * ) $ / )
457+ const slashMatch = rawText . match ( / ^ \/ ( \S * ) $ / )
458+
459+ if ( atMatch ) {
460+ onInput ( atMatch [ 1 ] )
461+ setStore ( "popover" , "file" )
462+ } else if ( slashMatch ) {
463+ slashOnInput ( slashMatch [ 1 ] )
464+ setStore ( "popover" , "slash" )
465+ } else {
466+ setStore ( "popover" , null )
467+ }
442468 } else {
443469 setStore ( "popover" , null )
444470 }
445471
446- if ( store . historyIndex >= 0 ) {
472+ if ( store . historyIndex >= 0 && ! store . applyingHistory ) {
447473 setStore ( "historyIndex" , - 1 )
448474 setStore ( "savedPrompt" , null )
449475 }
@@ -591,8 +617,13 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
591617 }
592618 }
593619 if ( store . mode === "shell" ) {
594- const cursorPosition = getCursorPosition ( editorRef )
595- if ( ( event . key === "Backspace" && cursorPosition === 0 ) || event . key === "Escape" ) {
620+ const { collapsed, cursorPosition, textLength } = getCaretState ( )
621+ if ( event . key === "Escape" ) {
622+ setStore ( "mode" , "normal" )
623+ event . preventDefault ( )
624+ return
625+ }
626+ if ( event . key === "Backspace" && collapsed && cursorPosition === 0 && textLength === 0 ) {
596627 setStore ( "mode" , "normal" )
597628 event . preventDefault ( )
598629 return
@@ -685,6 +716,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
685716 ? `?start=${ attachment . selection . startLine } &end=${ attachment . selection . endLine } `
686717 : ""
687718 return {
719+ id : Identifier . ascending ( "part" ) ,
688720 type : "file" as const ,
689721 mime : "text/plain" ,
690722 url : `file://${ absolute } ${ query } ` ,
@@ -702,6 +734,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
702734 } )
703735
704736 const imageAttachmentParts = store . imageAttachments . map ( ( attachment ) => ( {
737+ id : Identifier . ascending ( "part" ) ,
705738 type : "file" as const ,
706739 mime : attachment . mime ,
707740 url : attachment . dataUrl ,
@@ -747,14 +780,23 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
747780 }
748781 }
749782
783+ const messageID = Identifier . ascending ( "message" )
784+ const textPart = {
785+ id : Identifier . ascending ( "part" ) ,
786+ type : "text" as const ,
787+ text,
788+ }
789+ const requestParts = [ textPart , ...fileAttachmentParts , ...imageAttachmentParts ]
790+ const optimisticParts = requestParts . map ( ( part ) => ( {
791+ ...part ,
792+ sessionID : existing . id ,
793+ messageID,
794+ } ) )
795+
750796 sync . session . addOptimisticMessage ( {
751797 sessionID : existing . id ,
752- text,
753- parts : [
754- { type : "text" , text } as import ( "@opencode-ai/sdk/v2/client" ) . Part ,
755- ...( fileAttachmentParts as import ( "@opencode-ai/sdk/v2/client" ) . Part [ ] ) ,
756- ...( imageAttachmentParts as import ( "@opencode-ai/sdk/v2/client" ) . Part [ ] ) ,
757- ] ,
798+ messageID,
799+ parts : optimisticParts ,
758800 agent,
759801 model,
760802 } )
@@ -763,14 +805,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
763805 sessionID : existing . id ,
764806 agent,
765807 model,
766- parts : [
767- {
768- type : "text" ,
769- text,
770- } ,
771- ...fileAttachmentParts ,
772- ...imageAttachmentParts ,
773- ] ,
808+ messageID,
809+ parts : requestParts ,
774810 } )
775811 }
776812
@@ -911,6 +947,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
911947 classList = { {
912948 "w-full px-5 py-3 text-14-regular text-text-strong focus:outline-none whitespace-pre-wrap" : true ,
913949 "[&>[data-type=file]]:text-icon-info-active" : true ,
950+ "font-mono!" : store . mode === "shell" ,
914951 } }
915952 />
916953 < Show when = { ! prompt . dirty ( ) && store . imageAttachments . length === 0 } >
0 commit comments