@@ -24,6 +24,7 @@ func NewModel(root string) TuiModel {
2424 memHistory : []int {},
2525 gitChangesMap : make (map [string ]GitChanges ),
2626 updatesMap : make (map [string ]UpdateInfo ),
27+ localGitDirs : make (map [string ]bool ),
2728 settingsSel : 0 ,
2829 termWidth : 160 ,
2930 termHeight : 40 ,
@@ -156,10 +157,12 @@ func (m *TuiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
156157 if m .tickCount % 4 == 0 {
157158 m .blink = ! m .blink
158159 }
159-
160- var statusCmd tea.Cmd
160+
161+ var cmds []tea.Cmd
162+ cmds = append (cmds , tickCmd ())
163+
164+ // Every ~1.5s: sample memory + check server status
161165 if m .tickCount % 10 == 0 {
162- // Sample Nuxt process memory stats
163166 var mb int
164167 if m .ctx != nil {
165168 pidPath := filepath .Join (m .ctx .Paths .MetaDir , "dev.pid" )
@@ -175,22 +178,22 @@ func (m *TuiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
175178 m .memHistory = m .memHistory [1 :]
176179 }
177180 if m .ctx != nil {
178- statusCmd = m .checkServerStatusCmd ()
181+ cmds = append ( cmds , m .checkServerStatusCmd () )
179182 }
180183 }
181-
184+
185+ // Every ~15s: fast local git status check (no network)
182186 if m .tickCount % 100 == 0 && ! m .loading && m .activeTask == TaskNone && ! m .checkingUpdates {
183187 m .checkingUpdates = true
184- if statusCmd != nil {
185- return m , tea .Batch (tickCmd (), statusCmd , m .checkForUpdatesCmd ())
186- }
187- return m , tea .Batch (tickCmd (), m .checkForUpdatesCmd ())
188+ cmds = append (cmds , m .checkLocalChangesCmd ())
188189 }
189-
190- if statusCmd != nil {
191- return m , tea .Batch (tickCmd (), statusCmd )
190+
191+ // Every ~5min: slow remote check (git behind without fetch + npm versions)
192+ if m .tickCount % 2000 == 0 && ! m .loading && m .activeTask == TaskNone {
193+ cmds = append (cmds , m .checkRemoteUpdatesCmd ())
192194 }
193- return m , tickCmd ()
195+
196+ return m , tea .Batch (cmds ... )
194197
195198 case tea.KeyMsg :
196199 promptToShow := m .activePrompt
@@ -373,28 +376,28 @@ func (m *TuiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
373376 m .catalog = msg .Cat
374377 m .statusMsg = fmt .Sprintf ("Catalog loaded (%s)." , msg .Cat .CacheAge )
375378
379+ // Run fast local check immediately; schedule remote check too
376380 m .checkingUpdates = true
377- return m , m . checkForUpdatesCmd ( )
381+ return m , tea . Batch ( m . checkLocalChangesCmd (), m . checkRemoteUpdatesCmd () )
378382 }
379383
380- case updatesLoadedMsg :
384+ case localChangesMsg :
381385 m .gitChangesMap = msg .GitChanges
382- m .updatesMap = msg .Updates
386+ m .localGitDirs = msg .LocalGitDirs
383387 m .checkingUpdates = false
384388 m .loading = false
385- if msg .Err != nil {
386- m .statusMsg = fmt .Sprintf ("⚠️ Updates check failed: %v" , msg .Err )
387- } else {
389+
390+ case remoteUpdatesMsg :
391+ m .updatesMap = msg .Updates
392+ if msg .Err == nil {
388393 count := 0
389394 for _ , up := range msg .Updates {
390395 if up .LocalGit || up .Npm {
391396 count ++
392397 }
393398 }
394399 if count > 0 {
395- m .statusMsg = fmt .Sprintf ("Updates check completed: %d update(s) available." , count )
396- } else {
397- m .statusMsg = "All packages are up to date."
400+ m .statusMsg = fmt .Sprintf ("%d update(s) available." , count )
398401 }
399402 }
400403
@@ -488,24 +491,80 @@ func (m *TuiModel) addLog(line string) {
488491}
489492
490493
491- func (m * TuiModel ) checkForUpdatesCmd () tea.Cmd {
494+ // checkLocalChangesCmd runs a fast local-only git status check for every catalog entry.
495+ // No network activity — only reads the local working tree.
496+ // Runs every ~15s. Also detects which packages have their own .git folder.
497+ func (m * TuiModel ) checkLocalChangesCmd () tea.Cmd {
492498 return func () tea.Msg {
493499 gitChangesMap := make (map [string ]GitChanges )
500+ localGitDirs := make (map [string ]bool )
501+
502+ if m .catalog == nil {
503+ return localChangesMsg {GitChanges : gitChangesMap , LocalGitDirs : localGitDirs }
504+ }
505+
506+ for _ , entry := range m .catalog .Entries {
507+ short := entry .ShortName
508+ var kindDir string
509+ switch entry .Kind {
510+ case "app" :
511+ kindDir = "apps"
512+ case "module" :
513+ kindDir = "packages"
514+ case "theme" :
515+ kindDir = "themes"
516+ }
517+ if kindDir == "" {
518+ continue
519+ }
520+
521+ pkgPath := filepath .Join (m .workspaceRoot , kindDir , short )
522+ gitDir := filepath .Join (pkgPath , ".git" )
523+
524+ if _ , err := os .Stat (gitDir ); err == nil {
525+ // Package has its own .git repo
526+ localGitDirs [short ] = true
527+ added , modified , deleted , err := runGitStatus (pkgPath )
528+ if err == nil && (added > 0 || modified > 0 || deleted > 0 ) {
529+ gitChangesMap [short ] = GitChanges {Added : added , Modified : modified , Deleted : deleted }
530+ }
531+ } else {
532+ // Fallback: check monorepo git for this subdirectory
533+ parentGitDir := filepath .Join (m .workspaceRoot , ".git" )
534+ if _ , err := os .Stat (parentGitDir ); err == nil {
535+ relPath := filepath .Join (kindDir , short )
536+ added , modified , deleted , err := runGitStatusForSubdir (m .workspaceRoot , relPath )
537+ if err == nil && (added > 0 || modified > 0 || deleted > 0 ) {
538+ gitChangesMap [short ] = GitChanges {Added : added , Modified : modified , Deleted : deleted }
539+ }
540+ }
541+ }
542+ }
543+
544+ return localChangesMsg {GitChanges : gitChangesMap , LocalGitDirs : localGitDirs }
545+ }
546+ }
547+
548+ // checkRemoteUpdatesCmd runs the slow network-dependent checks:
549+ // - git behind count (uses cached remote refs, NO git fetch)
550+ // - npm latest version check
551+ // Runs every ~5 minutes.
552+ func (m * TuiModel ) checkRemoteUpdatesCmd () tea.Cmd {
553+ return func () tea.Msg {
494554 updatesMap := make (map [string ]UpdateInfo )
495555
496556 if m .catalog == nil {
497- return updatesLoadedMsg { GitChanges : gitChangesMap , Updates : updatesMap }
557+ return remoteUpdatesMsg { Updates : updatesMap }
498558 }
499559
500560 type result struct {
501561 shortName string
502- changes * GitChanges
503562 update * UpdateInfo
504563 err error
505564 }
506565
507566 resultsChan := make (chan result , len (m .catalog .Entries ))
508- sem := make (chan struct {}, 10 ) // Limit concurrency
567+ sem := make (chan struct {}, 5 ) // modest concurrency — these are network ops
509568
510569 for _ , e := range m .catalog .Entries {
511570 sem <- struct {}{}
@@ -514,9 +573,8 @@ func (m *TuiModel) checkForUpdatesCmd() tea.Cmd {
514573
515574 res := result {shortName : entry .ShortName }
516575
517- // 1. Check local changes & upstream behind counts
518576 short := entry .ShortName
519- kindDir := ""
577+ var kindDir string
520578 switch entry .Kind {
521579 case "app" :
522580 kindDir = "apps"
@@ -526,33 +584,19 @@ func (m *TuiModel) checkForUpdatesCmd() tea.Cmd {
526584 kindDir = "themes"
527585 }
528586
587+ // 1. Git behind check (NO fetch — uses cached remote refs)
529588 if kindDir != "" {
530589 pkgPath := filepath .Join (m .workspaceRoot , kindDir , short )
531590 gitDir := filepath .Join (pkgPath , ".git" )
532591 if _ , err := os .Stat (gitDir ); err == nil {
533- added , modified , deleted , err := runGitStatus (pkgPath )
534- if err == nil && (added > 0 || modified > 0 || deleted > 0 ) {
535- res .changes = & GitChanges {Added : added , Modified : modified , Deleted : deleted }
536- }
537-
538- behind , err := runGitBehindCheck (pkgPath )
592+ behind , err := runGitBehindCheckNoFetch (pkgPath )
539593 if err == nil && behind > 0 {
540594 res .update = & UpdateInfo {LocalGit : true , BehindCount : behind }
541595 }
542- } else {
543- // Fallback: Check if the monorepo itself tracks changes for this directory
544- parentGitDir := filepath .Join (m .workspaceRoot , ".git" )
545- if _ , err := os .Stat (parentGitDir ); err == nil {
546- relPath := filepath .Join (kindDir , short )
547- added , modified , deleted , err := runGitStatusForSubdir (m .workspaceRoot , relPath )
548- if err == nil && (added > 0 || modified > 0 || deleted > 0 ) {
549- res .changes = & GitChanges {Added : added , Modified : modified , Deleted : deleted }
550- }
551- }
552596 }
553597 }
554598
555- // 2. Check NPM updates
599+ // 2. NPM latest version check
556600 if entry .Installed && ! entry .LocalSource {
557601 localVer := getLocalVersion (m .workspaceRoot , entry )
558602 if localVer != "" {
@@ -572,9 +616,6 @@ func (m *TuiModel) checkForUpdatesCmd() tea.Cmd {
572616 var checkErr error
573617 for i := 0 ; i < len (m .catalog .Entries ); i ++ {
574618 res := <- resultsChan
575- if res .changes != nil {
576- gitChangesMap [res .shortName ] = * res .changes
577- }
578619 if res .update != nil {
579620 updatesMap [res .shortName ] = * res .update
580621 }
@@ -583,7 +624,7 @@ func (m *TuiModel) checkForUpdatesCmd() tea.Cmd {
583624 }
584625 }
585626
586- return updatesLoadedMsg { GitChanges : gitChangesMap , Updates : updatesMap , Err : checkErr }
627+ return remoteUpdatesMsg { Updates : updatesMap , Err : checkErr }
587628 }
588629}
589630
0 commit comments