@@ -54,9 +54,11 @@ import { serverConfigQueryOptions, serverQueryKeys } from "~/lib/serverReactQuer
5454import { isElectron } from "../env" ;
5555import { parseDiffRouteSearch , stripDiffSearchParams } from "../diffRouteSearch" ;
5656import {
57+ clampCollapsedComposerCursor ,
5758 type ComposerSlashCommand ,
5859 type ComposerTrigger ,
5960 type ComposerTriggerKind ,
61+ collapseExpandedComposerCursor ,
6062 detectComposerTrigger ,
6163 expandCollapsedComposerCursor ,
6264 parseStandaloneComposerSlashCommand ,
@@ -663,7 +665,9 @@ export default function ChatView({ threadId }: ChatViewProps) {
663665 const [ attachmentPreviewHandoffByMessageId , setAttachmentPreviewHandoffByMessageId ] = useState <
664666 Record < string , string [ ] >
665667 > ( { } ) ;
666- const [ composerCursor , setComposerCursor ] = useState ( ( ) => prompt . length ) ;
668+ const [ composerCursor , setComposerCursor ] = useState ( ( ) =>
669+ collapseExpandedComposerCursor ( prompt , prompt . length ) ,
670+ ) ;
667671 const [ composerTrigger , setComposerTrigger ] = useState < ComposerTrigger | null > ( ( ) =>
668672 detectComposerTrigger ( prompt , prompt . length ) ,
669673 ) ;
@@ -1035,14 +1039,15 @@ export default function ChatView({ threadId }: ChatViewProps) {
10351039 return ;
10361040 }
10371041 promptRef . current = activePendingProgress . customAnswer ;
1038- setComposerCursor ( activePendingProgress . customAnswer . length ) ;
1042+ const nextCursor = collapseExpandedComposerCursor (
1043+ activePendingProgress . customAnswer ,
1044+ activePendingProgress . customAnswer . length ,
1045+ ) ;
1046+ setComposerCursor ( nextCursor ) ;
10391047 setComposerTrigger (
10401048 detectComposerTrigger (
10411049 activePendingProgress . customAnswer ,
1042- expandCollapsedComposerCursor (
1043- activePendingProgress . customAnswer ,
1044- activePendingProgress . customAnswer . length ,
1045- ) ,
1050+ expandCollapsedComposerCursor ( activePendingProgress . customAnswer , nextCursor ) ,
10461051 ) ,
10471052 ) ;
10481053 setComposerHighlightedItemId ( null ) ;
@@ -2116,7 +2121,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
21162121
21172122 useEffect ( ( ) => {
21182123 promptRef . current = prompt ;
2119- setComposerCursor ( ( existing ) => Math . min ( Math . max ( 0 , existing ) , prompt . length ) ) ;
2124+ setComposerCursor ( ( existing ) => clampCollapsedComposerCursor ( prompt , existing ) ) ;
21202125 } , [ prompt ] ) ;
21212126
21222127 useEffect ( ( ) => {
@@ -2129,7 +2134,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
21292134 setSendPhase ( "idle" ) ;
21302135 setSendStartedAt ( null ) ;
21312136 setComposerHighlightedItemId ( null ) ;
2132- setComposerCursor ( promptRef . current . length ) ;
2137+ setComposerCursor ( collapseExpandedComposerCursor ( promptRef . current , promptRef . current . length ) ) ;
21332138 setComposerTrigger ( detectComposerTrigger ( promptRef . current , promptRef . current . length ) ) ;
21342139 dragDepthRef . current = 0 ;
21352140 setIsDragOverComposer ( false ) ;
@@ -2812,7 +2817,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
28122817 } ) ;
28132818 promptRef . current = trimmed ;
28142819 setPrompt ( trimmed ) ;
2815- setComposerCursor ( trimmed . length ) ;
2820+ setComposerCursor ( collapseExpandedComposerCursor ( trimmed , trimmed . length ) ) ;
28162821 addComposerImagesToDraft ( composerImagesSnapshot . map ( cloneComposerImageForRetry ) ) ;
28172822 setComposerTrigger ( detectComposerTrigger ( trimmed , trimmed . length ) ) ;
28182823 }
@@ -3285,6 +3290,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
32853290 return false ;
32863291 }
32873292 const next = replaceTextRange ( promptRef . current , rangeStart , rangeEnd , replacement ) ;
3293+ const nextCursor = collapseExpandedComposerCursor ( next . text , next . cursor ) ;
32883294 promptRef . current = next . text ;
32893295 const activePendingQuestion = activePendingProgress ?. activeQuestion ;
32903296 if ( activePendingQuestion && activePendingUserInput ) {
@@ -3301,33 +3307,51 @@ export default function ChatView({ threadId }: ChatViewProps) {
33013307 } else {
33023308 setPrompt ( next . text ) ;
33033309 }
3304- setComposerCursor ( next . cursor ) ;
3305- setComposerTrigger ( detectComposerTrigger ( next . text , next . cursor ) ) ;
3310+ setComposerCursor ( nextCursor ) ;
3311+ setComposerTrigger (
3312+ detectComposerTrigger ( next . text , expandCollapsedComposerCursor ( next . text , nextCursor ) ) ,
3313+ ) ;
33063314 window . requestAnimationFrame ( ( ) => {
3307- composerEditorRef . current ?. focusAt ( next . cursor ) ;
3315+ composerEditorRef . current ?. focusAt ( nextCursor ) ;
33083316 } ) ;
33093317 return true ;
33103318 } ,
33113319 [ activePendingProgress ?. activeQuestion , activePendingUserInput , setPrompt ] ,
33123320 ) ;
33133321
3314- const readComposerSnapshot = useCallback ( ( ) : { value : string ; cursor : number } => {
3315- const editorSnapshot = composerEditorRef . current ?. readSnapshot ( ) ;
3316- if ( editorSnapshot ) {
3317- return editorSnapshot ;
3318- }
3319- return { value : promptRef . current , cursor : composerCursor } ;
3320- } , [ composerCursor ] ) ;
3322+ const readComposerSnapshot = useCallback (
3323+ ( ) : { value : string ; cursor : number ; expandedCursor : number } => {
3324+ const editorSnapshot = composerEditorRef . current ?. readSnapshot ( ) ;
3325+ if ( editorSnapshot ) {
3326+ return editorSnapshot ;
3327+ }
3328+ return {
3329+ value : promptRef . current ,
3330+ cursor : composerCursor ,
3331+ expandedCursor : expandCollapsedComposerCursor ( promptRef . current , composerCursor ) ,
3332+ } ;
3333+ } ,
3334+ [ composerCursor ] ,
3335+ ) ;
3336+
3337+ const extendReplacementRangeForTrailingSpace = useCallback (
3338+ ( text : string , rangeEnd : number , replacement : string ) : number => {
3339+ if ( ! replacement . endsWith ( " " ) ) {
3340+ return rangeEnd ;
3341+ }
3342+ return text [ rangeEnd ] === " " ? rangeEnd + 1 : rangeEnd ;
3343+ } ,
3344+ [ ] ,
3345+ ) ;
33213346
33223347 const resolveActiveComposerTrigger = useCallback ( ( ) : {
3323- snapshot : { value : string ; cursor : number } ;
3348+ snapshot : { value : string ; cursor : number ; expandedCursor : number } ;
33243349 trigger : ComposerTrigger | null ;
33253350 } => {
33263351 const snapshot = readComposerSnapshot ( ) ;
3327- const expandedCursor = expandCollapsedComposerCursor ( snapshot . value , snapshot . cursor ) ;
33283352 return {
33293353 snapshot,
3330- trigger : detectComposerTrigger ( snapshot . value , expandedCursor ) ,
3354+ trigger : detectComposerTrigger ( snapshot . value , snapshot . expandedCursor ) ,
33313355 } ;
33323356 } , [ readComposerSnapshot ] ) ;
33333357
@@ -3340,13 +3364,20 @@ export default function ChatView({ threadId }: ChatViewProps) {
33403364 } ) ;
33413365 const { snapshot, trigger } = resolveActiveComposerTrigger ( ) ;
33423366 if ( ! trigger ) return ;
3343- const expectedToken = snapshot . value . slice ( trigger . rangeStart , trigger . rangeEnd ) ;
33443367 if ( item . type === "path" ) {
3368+ const replacement = `@${ item . path } ` ;
3369+ const replacementRangeEnd = extendReplacementRangeForTrailingSpace (
3370+ snapshot . value ,
3371+ trigger . rangeEnd ,
3372+ replacement ,
3373+ ) ;
33453374 const applied = applyPromptReplacement (
33463375 trigger . rangeStart ,
3347- trigger . rangeEnd ,
3348- `@${ item . path } ` ,
3349- { expectedText : expectedToken } ,
3376+ replacementRangeEnd ,
3377+ replacement ,
3378+ {
3379+ expectedText : snapshot . value . slice ( trigger . rangeStart , replacementRangeEnd ) ,
3380+ } ,
33503381 ) ;
33513382 if ( applied ) {
33523383 setComposerHighlightedItemId ( null ) ;
@@ -3355,17 +3386,28 @@ export default function ChatView({ threadId }: ChatViewProps) {
33553386 }
33563387 if ( item . type === "slash-command" ) {
33573388 if ( item . command === "model" ) {
3358- const applied = applyPromptReplacement ( trigger . rangeStart , trigger . rangeEnd , "/model " , {
3359- expectedText : expectedToken ,
3360- } ) ;
3389+ const replacement = "/model " ;
3390+ const replacementRangeEnd = extendReplacementRangeForTrailingSpace (
3391+ snapshot . value ,
3392+ trigger . rangeEnd ,
3393+ replacement ,
3394+ ) ;
3395+ const applied = applyPromptReplacement (
3396+ trigger . rangeStart ,
3397+ replacementRangeEnd ,
3398+ replacement ,
3399+ {
3400+ expectedText : snapshot . value . slice ( trigger . rangeStart , replacementRangeEnd ) ,
3401+ } ,
3402+ ) ;
33613403 if ( applied ) {
33623404 setComposerHighlightedItemId ( null ) ;
33633405 }
33643406 return ;
33653407 }
33663408 void handleInteractionModeChange ( item . command === "plan" ? "plan" : "default" ) ;
33673409 const applied = applyPromptReplacement ( trigger . rangeStart , trigger . rangeEnd , "" , {
3368- expectedText : expectedToken ,
3410+ expectedText : snapshot . value . slice ( trigger . rangeStart , trigger . rangeEnd ) ,
33693411 } ) ;
33703412 if ( applied ) {
33713413 setComposerHighlightedItemId ( null ) ;
@@ -3374,14 +3416,15 @@ export default function ChatView({ threadId }: ChatViewProps) {
33743416 }
33753417 onProviderModelSelect ( item . provider , item . model ) ;
33763418 const applied = applyPromptReplacement ( trigger . rangeStart , trigger . rangeEnd , "" , {
3377- expectedText : expectedToken ,
3419+ expectedText : snapshot . value . slice ( trigger . rangeStart , trigger . rangeEnd ) ,
33783420 } ) ;
33793421 if ( applied ) {
33803422 setComposerHighlightedItemId ( null ) ;
33813423 }
33823424 } ,
33833425 [
33843426 applyPromptReplacement ,
3427+ extendReplacementRangeForTrailingSpace ,
33853428 handleInteractionModeChange ,
33863429 onProviderModelSelect ,
33873430 resolveActiveComposerTrigger ,
@@ -3415,7 +3458,12 @@ export default function ChatView({ threadId }: ChatViewProps) {
34153458 workspaceEntriesQuery . isFetching ) ;
34163459
34173460 const onPromptChange = useCallback (
3418- ( nextPrompt : string , nextCursor : number , cursorAdjacentToMention : boolean ) => {
3461+ (
3462+ nextPrompt : string ,
3463+ nextCursor : number ,
3464+ nextExpandedCursor : number ,
3465+ cursorAdjacentToMention : boolean ,
3466+ ) => {
34193467 if ( activePendingProgress ?. activeQuestion && activePendingUserInput ) {
34203468 onChangeActivePendingUserInputCustomAnswer (
34213469 activePendingProgress . activeQuestion . id ,
@@ -3431,10 +3479,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
34313479 setComposerTrigger (
34323480 cursorAdjacentToMention
34333481 ? null
3434- : detectComposerTrigger (
3435- nextPrompt ,
3436- expandCollapsedComposerCursor ( nextPrompt , nextCursor ) ,
3437- ) ,
3482+ : detectComposerTrigger ( nextPrompt , nextExpandedCursor ) ,
34383483 ) ;
34393484 } ,
34403485 [
0 commit comments