@@ -17,8 +17,8 @@ import { pullRequestComposerContext, threadComposerContext } from "../../lib/com
1717import { uuidv4 } from "../../lib/uuid" ;
1818import {
1919 getComposerDraftSnapshot ,
20- readComposerDraftSelection ,
2120 setComposerDraftContext ,
21+ useComposerDraftSelection ,
2222} from "../../state/use-composer-drafts" ;
2323import { USAGE_LIMITS_COMMAND } from "@t3tools/shared/usageLimits" ;
2424import {
@@ -43,7 +43,7 @@ import {
4343} from "@t3tools/client-runtime/providerSkills" ;
4444import { useAtomRefresh } from "@effect/atom-react" ;
4545import { Atom } from "effect/reactivity" ;
46- import { useEffect , useRef , useState } from "react" ;
46+ import { useEffect , useState } from "react" ;
4747
4848import type { ComposerEditorSelection } from "../../components/ComposerEditor" ;
4949import { serverEnvironment } from "../../state/server" ;
@@ -68,6 +68,27 @@ function composerSelectionAtEnd(draftMessage: string): ComposerEditorSelection {
6868 return { start : draftMessage . length , end : draftMessage . length } ;
6969}
7070
71+ /**
72+ * A caret placed by an insertion (chips, pasted context) wins while the draft
73+ * still has the text it was placed in. Otherwise the composer's own caret is
74+ * clamped to the draft, and a caret from another owner starts at the end.
75+ */
76+ export function resolveComposerMenuSelection ( input : {
77+ readonly draftMessage : string ;
78+ readonly ownerKey : string | null ;
79+ readonly inserted : ComposerEditorSelection | null ;
80+ readonly local : {
81+ readonly ownerKey : string | null ;
82+ readonly start : number ;
83+ readonly end : number ;
84+ } ;
85+ } ) : ComposerEditorSelection {
86+ if ( input . inserted ) return input . inserted ;
87+ if ( input . local . ownerKey !== input . ownerKey ) return composerSelectionAtEnd ( input . draftMessage ) ;
88+ const length = input . draftMessage . length ;
89+ return { start : Math . min ( input . local . start , length ) , end : Math . min ( input . local . end , length ) } ;
90+ }
91+
7192export function buildComposerSlashCommandItems ( input : {
7293 readonly query : string ;
7394 readonly atMessageStart : boolean ;
@@ -212,34 +233,21 @@ export function useComposerCommandMenu({
212233 readonly onUpdateInteractionMode ?: ( mode : ProviderInteractionMode ) => void ;
213234 readonly onUsageLimits ?: ( ) => void ;
214235} ) {
215- const [ selection , setSelection ] = useState ( ( ) => composerSelectionAtEnd ( draftMessage ) ) ;
236+ const [ localSelection , setLocalSelection ] = useState ( ( ) => ( {
237+ ownerKey,
238+ ...composerSelectionAtEnd ( draftMessage ) ,
239+ } ) ) ;
216240 // Thread mentions rank by recency as of when the composer mounted.
217241 const [ now ] = useState ( ( ) => Date . now ( ) ) ;
218- const previousOwnerKeyRef = useRef ( ownerKey ) ;
219- useEffect ( ( ) => {
220- const inserted = ownerKey ? readComposerDraftSelection ( ownerKey , draftMessage ) : null ;
221- if ( inserted ) {
222- // oxlint-disable-next-line react/set-state-in-effect -- Applies a selection inserted into the draft store by another surface.
223- setSelection ( ( current ) =>
224- current . start === inserted . start && current . end === inserted . end ? current : inserted ,
225- ) ;
226- return ;
227- }
228- const end = draftMessage . length ;
229- setSelection ( ( current ) => {
230- const start = Math . min ( current . start , end ) ;
231- const selectionEnd = Math . min ( current . end , end ) ;
232- if ( start === current . start && selectionEnd === current . end ) {
233- return current ;
234- }
235- return { start, end : selectionEnd } ;
236- } ) ;
237- } , [ draftMessage , ownerKey ] ) ;
238- useEffect ( ( ) => {
239- if ( previousOwnerKeyRef . current === ownerKey ) return ;
240- previousOwnerKeyRef . current = ownerKey ;
241- setSelection ( composerSelectionAtEnd ( draftMessage ) ) ;
242- } , [ draftMessage , ownerKey ] ) ;
242+ const insertedSelection = useComposerDraftSelection ( ownerKey , draftMessage ) ;
243+ const selection = resolveComposerMenuSelection ( {
244+ draftMessage,
245+ ownerKey,
246+ inserted : insertedSelection ,
247+ local : localSelection ,
248+ } ) ;
249+ const setSelection = ( next : ComposerEditorSelection ) =>
250+ setLocalSelection ( { ownerKey, start : next . start , end : next . end } ) ;
243251
244252 const skills = selectedProviderStatus
245253 ? resolveProviderSkillsForCwd ( selectedProviderStatus , projectCwd )
0 commit comments