@@ -53,6 +53,8 @@ export function createCopyMode(input: {
5353 toBottom : ( ) => void
5454} ) {
5555 const [ state , setState ] = createSignal < CopyState > ( { ...empty } )
56+ let skipSyncOnce = false
57+ let lastIdx = - 1
5658
5759 // --- row building ---
5860
@@ -282,7 +284,18 @@ export function createCopyMode(input: {
282284
283285 // --- navigation ---
284286
285- function sync ( next : number ) {
287+ function sync ( next : number , scrollToVisible = true ) {
288+ if ( skipSyncOnce ) {
289+ skipSyncOnce = false
290+ return
291+ }
292+ const s = state ( )
293+ if ( s . idx === next && ! s . active ) {
294+ return
295+ }
296+ if ( s . idx === next && s . active ) {
297+ return
298+ }
286299 const scroll = input . scroll ( )
287300 const list = rows ( )
288301 if ( ! list . length ) {
@@ -292,10 +305,10 @@ export function createCopyMode(input: {
292305 const idx = Math . max ( 0 , Math . min ( next , list . length - 1 ) )
293306 setState ( ( s ) => ( { ...s , active : true , idx } ) )
294307 const row = list [ idx ]
295- if ( ! row ) return
308+ if ( ! row || ! scrollToVisible ) return
296309 const y = row . y
297310 const top = scroll . y
298- const bottom = scroll . y + scroll . height - 1
311+ const bottom = scroll . y + scroll . height + 1
299312 if ( y < top ) {
300313 scroll . scrollBy ( y - top )
301314 return
@@ -307,13 +320,33 @@ export function createCopyMode(input: {
307320
308321 function enter ( ) {
309322 const init = ( ) => {
323+ const scroll = input . scroll ( )
310324 const list = rows ( )
311325 if ( ! list . length ) return false
312- const idx = list . findLastIndex ( ( x ) => x . role === "assistant" )
313- const target = idx >= 0 ? idx : list . length - 1
314- const row = list [ target ]
315- setState ( ( s ) => ( { ...s , col : copyMin ( row ) , stick : "first" as const } ) )
316- sync ( target )
326+
327+ const top = scroll . y
328+ const bottom = scroll . y + scroll . height - 1
329+ const visibleRows = list . filter ( ( x ) => x . role === "assistant" && x . y >= top && x . y <= bottom )
330+
331+ const savedIdx = lastIdx >= 0 ? lastIdx : state ( ) . idx
332+ const savedRow = savedIdx >= 0 && savedIdx < list . length ? list [ savedIdx ] : undefined
333+ const savedVisible = savedRow && savedRow . y >= top && savedRow . y <= bottom
334+
335+ const visibleIdx = list . findLastIndex ( ( x ) => x . role === "assistant" && x . y >= top && x . y <= bottom )
336+ const lastAssistantIdx = list . findLastIndex ( ( x ) => x . role === "assistant" )
337+
338+ let target : number
339+ if ( savedIdx >= 0 ) {
340+ target = savedIdx
341+ } else if ( visibleIdx >= 0 ) {
342+ target = visibleIdx
343+ } else {
344+ target = lastAssistantIdx
345+ }
346+ const finalTarget = target >= 0 ? target : 0
347+ const row = list [ finalTarget ]
348+ setState ( ( s ) => ( { ...s , active : true , idx : finalTarget , col : copyMin ( row ) , stick : "first" as const } ) )
349+ skipSyncOnce = true
317350 return true
318351 }
319352 if ( init ( ) ) return
@@ -322,15 +355,23 @@ export function createCopyMode(input: {
322355 } , 0 )
323356 }
324357
325- function exit ( ) {
326- setState ( { ...empty } )
327- input . toBottom ( )
358+ function exit ( scrollToBottom ?: boolean ) {
359+ if ( scrollToBottom ) {
360+ lastIdx = - 1
361+ input . toBottom ( )
362+ setState ( { ...empty } )
363+ } else {
364+ lastIdx = state ( ) . idx
365+ setState ( s => ( { ...s , active : false , visual : undefined , anchor : undefined } ) )
366+ }
328367 }
329368
330369 function move ( action : "up" | "down" | "left" | "right" ) {
331370 const scroll = input . scroll ( )
332371 const s = state ( )
333372 if ( ! s . active ) return
373+ // want to detect if we just entered copy mode and put cursor into a message
374+ // that is currently visible in the chat
334375 if ( action === "up" || action === "down" ) {
335376 sync ( s . idx + ( action === "up" ? - 1 : 1 ) )
336377 const row = rows ( ) [ state ( ) . idx ]
@@ -525,21 +566,28 @@ export function createCopyMode(input: {
525566
526567 createEffect ( ( prev : string | undefined ) => {
527568 const id = input . session ( )
528- if ( prev !== undefined && prev !== id ) exit ( )
569+ if ( prev !== undefined && prev !== id ) {
570+ exit ( )
571+ }
529572 return id
530573 } )
531574
532- createEffect ( ( ) => {
575+
576+ createEffect ( ( prev : { active : boolean ; idx : number } | undefined ) => {
533577 const s = state ( )
578+
579+ if ( prev && prev . active === s . active ) return prev
580+
534581 const list = rows ( )
535- if ( ! s . active ) return
582+ if ( ! s . active ) return { active : s . active , idx : s . idx }
536583 if ( ! list . length ) {
537584 exit ( )
538- return
585+ return { active : s . active , idx : s . idx }
539586 }
540587 if ( s . idx >= list . length ) {
541588 sync ( list . length - 1 )
542589 }
590+ return { active : s . active , idx : s . idx }
543591 } )
544592
545593 // --- derived ---
@@ -552,7 +600,7 @@ export function createCopyMode(input: {
552600
553601 const highlights = createMemo ( ( ) => {
554602 const s = state ( )
555- if ( ! s . visual || ! s . anchor ) return new Map < string , CopyHighlight [ ] > ( )
603+ if ( ! s . active || ! s . visual || ! s . anchor ) return new Map < string , CopyHighlight [ ] > ( )
556604 const list = rows ( )
557605 const cache = new Map (
558606 input
@@ -590,7 +638,7 @@ export function createCopyMode(input: {
590638 return {
591639 prompt : {
592640 enter,
593- exit,
641+ exit : ( scrollToBottom = true ) => exit ( scrollToBottom ) ,
594642 visual,
595643 yank,
596644 copy,
0 commit comments