@@ -75,7 +75,7 @@ export function App(props: AppProps) {
7575 filteredLogs,
7676 searchQuery,
7777 setSearchQuery,
78- searchMatches ,
78+ getSearchMatches ,
7979 currentMatchIndex,
8080 setCurrentMatchIndex,
8181 nextMatch,
@@ -122,6 +122,28 @@ export function App(props: AppProps) {
122122 return filteredLogs ( ) ;
123123 } ;
124124
125+ // Compute search matches based on visible logs
126+ const searchMatches = ( ) => getSearchMatches ( visibleLogs ( ) ) ;
127+
128+ // Clear search when switching services or view mode
129+ const [ lastSelectedName , setLastSelectedName ] = createSignal < string | null > ( null ) ;
130+ const [ lastViewMode , setLastViewMode ] = createSignal < "single" | "all" > ( "single" ) ;
131+
132+ createEffect ( ( ) => {
133+ const currentName = selectedName ( ) ;
134+ const currentViewMode = viewMode ( ) ;
135+ const prevName = lastSelectedName ( ) ;
136+ const prevViewMode = lastViewMode ( ) ;
137+
138+ // Clear search if service or view mode changed
139+ if ( ( currentName !== prevName || currentViewMode !== prevViewMode ) && isSearchActive ( ) ) {
140+ clearSearch ( ) ;
141+ }
142+
143+ setLastSelectedName ( currentName ) ;
144+ setLastViewMode ( currentViewMode ) ;
145+ } ) ;
146+
125147 // Update scroll info from scrollbox
126148 const updateScrollInfo = ( ) => {
127149 if ( scrollboxRef ) {
@@ -219,12 +241,14 @@ export function App(props: AppProps) {
219241 if ( query ) {
220242 setSearchQuery ( query ) ;
221243 setCurrentMatchIndex ( 0 ) ;
222- // Scroll to first match
223- const matches = searchMatches ( ) ;
224- if ( matches . length > 0 && scrollboxRef ) {
225- scrollboxRef . scrollTo ( matches [ 0 ] ! . logIndex ) ;
226- setFollowing ( false ) ;
227- }
244+ // Scroll to first match after query is set
245+ setTimeout ( ( ) => {
246+ const matches = searchMatches ( ) ;
247+ if ( matches . length > 0 && scrollboxRef ) {
248+ scrollboxRef . scrollTo ( matches [ 0 ] ! . logIndex ) ;
249+ setFollowing ( false ) ;
250+ }
251+ } , 0 ) ;
228252 }
229253 setSearchMode ( false ) ;
230254 setSearchInput ( "" ) ;
@@ -487,13 +511,16 @@ export function App(props: AppProps) {
487511 case "n" :
488512 // Next search match
489513 if ( isSearchActive ( ) ) {
490- nextMatch ( ) ;
491514 const matches = searchMatches ( ) ;
492- const idx = currentMatchIndex ( ) ;
493- if ( matches . length > 0 && scrollboxRef ) {
494- scrollboxRef . scrollTo ( matches [ idx ] ! . logIndex ) ;
495- setFollowing ( false ) ;
496- }
515+ nextMatch ( matches . length ) ;
516+ // Get updated index after nextMatch
517+ setTimeout ( ( ) => {
518+ const idx = currentMatchIndex ( ) ;
519+ if ( matches . length > 0 && scrollboxRef && matches [ idx ] ) {
520+ scrollboxRef . scrollTo ( matches [ idx ] ! . logIndex ) ;
521+ setFollowing ( false ) ;
522+ }
523+ } , 0 ) ;
497524 }
498525 event . preventDefault ( ) ;
499526 break ;
@@ -502,13 +529,16 @@ export function App(props: AppProps) {
502529 if ( event . shift ) {
503530 // Previous match (N in vim)
504531 if ( isSearchActive ( ) ) {
505- prevMatch ( ) ;
506532 const matches = searchMatches ( ) ;
507- const idx = currentMatchIndex ( ) ;
508- if ( matches . length > 0 && scrollboxRef ) {
509- scrollboxRef . scrollTo ( matches [ idx ] ! . logIndex ) ;
510- setFollowing ( false ) ;
511- }
533+ prevMatch ( matches . length ) ;
534+ // Get updated index after prevMatch
535+ setTimeout ( ( ) => {
536+ const idx = currentMatchIndex ( ) ;
537+ if ( matches . length > 0 && scrollboxRef && matches [ idx ] ) {
538+ scrollboxRef . scrollTo ( matches [ idx ] ! . logIndex ) ;
539+ setFollowing ( false ) ;
540+ }
541+ } , 0 ) ;
512542 }
513543 event . preventDefault ( ) ;
514544 }
@@ -667,10 +697,48 @@ export function App(props: AppProps) {
667697 < box flexDirection = "column" >
668698 < For each = { visibleLogs ( ) } >
669699 { ( log , logIdx ) => {
670- // Check if this log line has matches
671- const matches = searchMatches ( ) . filter ( ( m ) => m . logIndex === logIdx ( ) ) ;
672- const currentMatch = searchMatches ( ) [ currentMatchIndex ( ) ] ;
673- const isCurrentMatchLine = currentMatch ?. logIndex === logIdx ( ) ;
700+ // Reactive accessors for search matches - must be functions for SolidJS reactivity
701+ const matches = ( ) => searchMatches ( ) . filter ( ( m ) => m . logIndex === logIdx ( ) ) ;
702+ const currentMatch = ( ) => searchMatches ( ) [ currentMatchIndex ( ) ] ;
703+ const isCurrentMatchLine = ( ) => currentMatch ( ) ?. logIndex === logIdx ( ) ;
704+
705+ // Compute highlighted parts reactively
706+ const highlightedParts = ( ) => {
707+ const m = matches ( ) ;
708+ if ( m . length === 0 ) return null ;
709+
710+ const content = log . content ;
711+ const parts : { text : string ; highlight : boolean ; isCurrent : boolean } [ ] = [ ] ;
712+ let lastEnd = 0 ;
713+ const current = currentMatch ( ) ;
714+ const isCurrentLine = isCurrentMatchLine ( ) ;
715+
716+ m . forEach ( ( match ) => {
717+ if ( match . startIndex > lastEnd ) {
718+ parts . push ( {
719+ text : content . slice ( lastEnd , match . startIndex ) ,
720+ highlight : false ,
721+ isCurrent : false ,
722+ } ) ;
723+ }
724+ parts . push ( {
725+ text : content . slice ( match . startIndex , match . endIndex ) ,
726+ highlight : true ,
727+ isCurrent : isCurrentLine && match === current ,
728+ } ) ;
729+ lastEnd = match . endIndex ;
730+ } ) ;
731+
732+ if ( lastEnd < content . length ) {
733+ parts . push ( {
734+ text : content . slice ( lastEnd ) ,
735+ highlight : false ,
736+ isCurrent : false ,
737+ } ) ;
738+ }
739+
740+ return parts ;
741+ } ;
674742
675743 return (
676744 < box height = { 1 } paddingLeft = { 1 } flexDirection = "row" >
@@ -680,60 +748,36 @@ export function App(props: AppProps) {
680748 < text fg = "cyan" > { log . service } </ text >
681749 < text fg = "gray" > | </ text >
682750 </ Show >
683- < Show when = { matches . length > 0 } fallback = {
684- < text fg = { log . stream === "stderr" ? "red" : undefined } > { log . content } </ text >
685- } >
686- { /* Render with highlighted matches */ }
687- { ( ( ) => {
688- const content = log . content ;
689- const parts : { text : string ; highlight : boolean ; isCurrent : boolean } [ ] = [ ] ;
690- let lastEnd = 0 ;
691-
692- matches . forEach ( ( match ) => {
693- if ( match . startIndex > lastEnd ) {
694- parts . push ( {
695- text : content . slice ( lastEnd , match . startIndex ) ,
696- highlight : false ,
697- isCurrent : false ,
698- } ) ;
699- }
700- parts . push ( {
701- text : content . slice ( match . startIndex , match . endIndex ) ,
702- highlight : true ,
703- isCurrent : isCurrentMatchLine && match === currentMatch ,
704- } ) ;
705- lastEnd = match . endIndex ;
706- } ) ;
707-
708- if ( lastEnd < content . length ) {
709- parts . push ( {
710- text : content . slice ( lastEnd ) ,
711- highlight : false ,
712- isCurrent : false ,
713- } ) ;
714- }
715-
716- return (
717- < box flexDirection = "row" >
718- < For each = { parts } >
719- { ( part ) => (
720- < Show
721- when = { part . highlight }
722- fallback = {
723- < text fg = { log . stream === "stderr" ? "red" : undefined } >
724- { part . text }
725- </ text >
726- }
727- >
728- < box backgroundColor = { part . isCurrent ? "#ffff00" : "#555500" } >
729- < text fg = "black" > { part . text } </ text >
730- </ box >
731- </ Show >
732- ) }
733- </ For >
734- </ box >
735- ) ;
736- } ) ( ) }
751+ < Show
752+ when = { highlightedParts ( ) }
753+ fallback = {
754+ < text fg = { log . stream === "stderr" ? "red" : undefined } >
755+ { log . content }
756+ </ text >
757+ }
758+ >
759+ { (
760+ parts : ( ) => { text : string ; highlight : boolean ; isCurrent : boolean } [ ] ,
761+ ) => (
762+ < box flexDirection = "row" >
763+ < For each = { parts ( ) } >
764+ { ( part ) => (
765+ < Show
766+ when = { part . highlight }
767+ fallback = {
768+ < text fg = { log . stream === "stderr" ? "red" : undefined } >
769+ { part . text }
770+ </ text >
771+ }
772+ >
773+ < box backgroundColor = { part . isCurrent ? "#ffff00" : "#555500" } >
774+ < text fg = "black" > { part . text } </ text >
775+ </ box >
776+ </ Show >
777+ ) }
778+ </ For >
779+ </ box >
780+ ) }
737781 </ Show >
738782 </ box >
739783 ) ;
0 commit comments