@@ -169,6 +169,9 @@ export function WorkflowSearchReplace() {
169169 setActiveMatchId : state . setActiveMatchId ,
170170 } ) )
171171 )
172+ const prevQueryRef = useRef ( query )
173+ const prevIsOpenRef = useRef ( false )
174+ const afterReplaceIndexRef = useRef < number | null > ( null )
172175 const { data : workspaceCredentials } = useWorkspaceCredentials ( { workspaceId, enabled : isOpen } )
173176
174177 useRegisterGlobalCommands ( [
@@ -311,10 +314,7 @@ export function WorkflowSearchReplace() {
311314
312315 return [ ]
313316 } , [ activeMatch , hydratedMatches ] )
314- const eligibleMatchIds = useMemo (
315- ( ) => replaceAllTargetMatches . map ( ( match ) => match . id ) ,
316- [ replaceAllTargetMatches ]
317- )
317+ const eligibleMatchIds = replaceAllTargetMatches . map ( ( match ) => match . id )
318318 const controlTargetMatches = activeMatch ? [ activeMatch ] : [ ]
319319 const usesResourceReplacement = controlTargetMatches . some ( isConstrainedResourceMatch )
320320 const resourceReplacementContextKey =
@@ -324,23 +324,20 @@ export function WorkflowSearchReplace() {
324324 const replacement = resourceReplacementContextKey
325325 ? ( resourceReplacementByContext [ resourceReplacementContextKey ] ?? '' )
326326 : textReplacement
327- const handleReplacementChange = useCallback (
328- ( nextReplacement : string ) => {
329- if ( ! resourceReplacementContextKey ) {
330- setReplacement ( nextReplacement )
331- return
332- }
327+ const handleReplacementChange = ( nextReplacement : string ) => {
328+ if ( ! resourceReplacementContextKey ) {
329+ setReplacement ( nextReplacement )
330+ return
331+ }
333332
334- setResourceReplacementByContext ( ( current ) => ( {
335- ...current ,
336- [ resourceReplacementContextKey ] : nextReplacement ,
337- } ) )
338- } ,
339- [ resourceReplacementContextKey , setReplacement ]
340- )
341- const compatibleResourceOptions = useMemo (
342- ( ) => getCompatibleResourceReplacementOptions ( controlTargetMatches , resourceOptions ) ,
343- [ controlTargetMatches , resourceOptions ]
333+ setResourceReplacementByContext ( ( current ) => ( {
334+ ...current ,
335+ [ resourceReplacementContextKey ] : nextReplacement ,
336+ } ) )
337+ }
338+ const compatibleResourceOptions = getCompatibleResourceReplacementOptions (
339+ controlTargetMatches ,
340+ resourceOptions
344341 )
345342 const hasReplacement = replacement . trim ( ) . length > 0
346343 const activeReplacementIssue = activeMatch
@@ -359,25 +356,31 @@ export function WorkflowSearchReplace() {
359356 } )
360357 : 'No replaceable matches.'
361358
362- const applySubflowUpdate = useCallback (
363- ( update : WorkflowSearchReplaceSubflowUpdate ) => {
364- if ( update . fieldId === WORKFLOW_SEARCH_SUBFLOW_FIELD_IDS . iterations ) {
365- if ( typeof update . nextValue !== 'number' ) return
366- collaborativeUpdateIterationCount ( update . blockId , update . blockType , update . nextValue )
367- return
368- }
359+ const applySubflowUpdate = ( update : WorkflowSearchReplaceSubflowUpdate ) => {
360+ if ( update . fieldId === WORKFLOW_SEARCH_SUBFLOW_FIELD_IDS . iterations ) {
361+ if ( typeof update . nextValue !== 'number' ) return
362+ collaborativeUpdateIterationCount ( update . blockId , update . blockType , update . nextValue )
363+ return
364+ }
369365
370- collaborativeUpdateIterationCollection (
371- update . blockId ,
372- update . blockType ,
373- String ( update . nextValue )
374- )
375- } ,
376- [ collaborativeUpdateIterationCollection , collaborativeUpdateIterationCount ]
377- )
366+ collaborativeUpdateIterationCollection (
367+ update . blockId ,
368+ update . blockType ,
369+ String ( update . nextValue )
370+ )
371+ }
378372
379373 useEffect ( ( ) => {
380- if ( ! isOpen ) return
374+ if ( ! isOpen ) {
375+ prevIsOpenRef . current = false
376+ usePanelEditorSearchStore . getState ( ) . setActiveSearchTarget ( null )
377+ return
378+ }
379+
380+ const justOpened = ! prevIsOpenRef . current
381+ prevIsOpenRef . current = true
382+ const queryChanged = prevQueryRef . current !== query
383+ prevQueryRef . current = query
381384
382385 if ( hydratedMatches . length === 0 ) {
383386 if ( activeMatchId ) setActiveMatchId ( null )
@@ -386,7 +389,22 @@ export function WorkflowSearchReplace() {
386389 }
387390
388391 if ( ! activeMatchId || ! hydratedMatches . some ( ( match ) => match . id === activeMatchId ) ) {
389- handleSelectMatch ( hydratedMatches [ 0 ] . id )
392+ const replaceIndex = afterReplaceIndexRef . current
393+ afterReplaceIndexRef . current = null
394+
395+ if ( queryChanged || justOpened ) {
396+ // Intentional navigation: panel opened or query changed — go to first match.
397+ handleSelectMatch ( hydratedMatches [ 0 ] . id )
398+ } else if ( replaceIndex !== null ) {
399+ // Replace button was clicked: advance to the match now at the same position (clamped),
400+ // which is the next match after the one that was just replaced.
401+ handleSelectMatch ( hydratedMatches [ Math . min ( replaceIndex , hydratedMatches . length - 1 ) ] . id )
402+ } else {
403+ // Content edited manually — don't steal focus or switch panels.
404+ // Deselect so the user can navigate explicitly with the arrow keys.
405+ setActiveMatchId ( null )
406+ usePanelEditorSearchStore . getState ( ) . setActiveSearchTarget ( null )
407+ }
390408 return
391409 }
392410
@@ -401,8 +419,12 @@ export function WorkflowSearchReplace() {
401419
402420 const handleMoveActiveMatch = ( delta : number ) => {
403421 if ( hydratedMatches . length === 0 ) return
404- const currentIndex = activeMatchIndex >= 0 ? activeMatchIndex : 0
405- const nextIndex = ( currentIndex + delta + hydratedMatches . length ) % hydratedMatches . length
422+ if ( activeMatchIndex < 0 ) {
423+ // Nothing selected: ↓ goes to first match, ↑ goes to last.
424+ handleSelectMatch ( hydratedMatches [ delta > 0 ? 0 : hydratedMatches . length - 1 ] . id )
425+ return
426+ }
427+ const nextIndex = ( activeMatchIndex + delta + hydratedMatches . length ) % hydratedMatches . length
406428 handleSelectMatch ( hydratedMatches [ nextIndex ] . id )
407429 }
408430
@@ -512,6 +534,7 @@ export function WorkflowSearchReplace() {
512534
513535 const handleReplaceActive = ( ) => {
514536 if ( ! activeMatch ) return
537+ afterReplaceIndexRef . current = activeMatchIndex
515538 handleApply ( [ activeMatch . id ] )
516539 }
517540
@@ -522,7 +545,9 @@ export function WorkflowSearchReplace() {
522545 const matchCountLabel =
523546 hydratedMatches . length === 0
524547 ? 'No results'
525- : `${ activeMatchIndex >= 0 ? activeMatchIndex + 1 : 1 } of ${ hydratedMatches . length } `
548+ : activeMatchIndex >= 0
549+ ? `${ activeMatchIndex + 1 } of ${ hydratedMatches . length } `
550+ : `0 of ${ hydratedMatches . length } `
526551 return (
527552 < div
528553 role = 'dialog'
@@ -566,7 +591,7 @@ export function WorkflowSearchReplace() {
566591 >
567592 < ChevronRight
568593 className = { cn (
569- 'h-[14px] w -[14px] text-[var(--text-icon)] transition-transform' ,
594+ 'size -[14px] text-[var(--text-icon)] transition-transform' ,
570595 isReplaceExpanded && 'rotate-90'
571596 ) }
572597 />
0 commit comments