From e6dd38fbaaaa835d326a63e616b99e8b75172610 Mon Sep 17 00:00:00 2001 From: AnnatarHe Date: Wed, 7 Jan 2026 00:32:01 +0800 Subject: [PATCH 1/2] fix(daemon): cache git info to avoid cc statusline timeout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The git branch was always showing "-" because GetGitInfo() calls worktree.Status() which can take 100ms+ on large repos, exceeding the 50ms client timeout. Added git info caching with 20s TTL to CCInfoTimerService. After the first request caches the git info, subsequent requests return instantly. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- daemon/cc_info_timer.go | 36 ++++++++++++++++++++++++++++++++++++ daemon/socket.go | 4 ++-- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/daemon/cc_info_timer.go b/daemon/cc_info_timer.go index 633383c..90e8144 100644 --- a/daemon/cc_info_timer.go +++ b/daemon/cc_info_timer.go @@ -12,6 +12,7 @@ import ( var ( CCInfoFetchInterval = 3 * time.Second CCInfoInactivityTimeout = 3 * time.Minute + GitInfoCacheTTL = 20 * time.Second ) // CCInfoCache holds the cached cost data for a time range @@ -21,6 +22,12 @@ type CCInfoCache struct { FetchedAt time.Time } +// cachedGitInfo holds cached git information for a directory +type cachedGitInfo struct { + info GitInfo + fetchedAt time.Time +} + // CCInfoTimerService manages lazy-fetching of CC info data type CCInfoTimerService struct { config *model.ShellTimeConfig @@ -35,6 +42,10 @@ type CCInfoTimerService struct { ticker *time.Ticker stopChan chan struct{} wg sync.WaitGroup + + // Git info cache (per working directory) + gitCacheMu sync.RWMutex + gitCache map[string]cachedGitInfo } // NewCCInfoTimerService creates a new CC info timer service @@ -44,6 +55,7 @@ func NewCCInfoTimerService(config *model.ShellTimeConfig) *CCInfoTimerService { cache: make(map[CCInfoTimeRange]CCInfoCache), activeRanges: make(map[CCInfoTimeRange]bool), stopChan: make(chan struct{}), + gitCache: make(map[string]cachedGitInfo), } } @@ -262,3 +274,27 @@ func (s *CCInfoTimerService) fetchCCInfo(ctx context.Context, timeRange CCInfoTi TotalSessionSeconds: analytics.TotalSessionSeconds, }, nil } + +// GetCachedGitInfo returns cached git info, fetching fresh if cache expired +func (s *CCInfoTimerService) GetCachedGitInfo(workingDir string) GitInfo { + if workingDir == "" { + return GitInfo{} + } + + s.gitCacheMu.RLock() + cached, exists := s.gitCache[workingDir] + s.gitCacheMu.RUnlock() + + if exists && time.Since(cached.fetchedAt) < GitInfoCacheTTL { + return cached.info + } + + // Fetch fresh (this is the slow part) + info := GetGitInfo(workingDir) + + s.gitCacheMu.Lock() + s.gitCache[workingDir] = cachedGitInfo{info: info, fetchedAt: time.Now()} + s.gitCacheMu.Unlock() + + return info +} diff --git a/daemon/socket.go b/daemon/socket.go index f2f129a..0f85745 100644 --- a/daemon/socket.go +++ b/daemon/socket.go @@ -216,8 +216,8 @@ func (p *SocketHandler) handleCCInfo(conn net.Conn, msg SocketMessage) { cache := p.ccInfoTimer.GetCachedCost(timeRange) p.ccInfoTimer.NotifyActivity() - // Get git info (fast, no caching needed since it changes frequently) - gitInfo := GetGitInfo(workingDir) + // Get git info (cached to avoid slow worktree.Status() on large repos) + gitInfo := p.ccInfoTimer.GetCachedGitInfo(workingDir) response := CCInfoResponse{ TotalCostUSD: cache.TotalCostUSD, From 0a4c29b57c80cdd2bbd703732162033f4dfa1503 Mon Sep 17 00:00:00 2001 From: AnnatarHe Date: Wed, 7 Jan 2026 00:46:33 +0800 Subject: [PATCH 2/2] refactor(daemon): fetch git info in timer loop instead of on-demand MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous on-demand caching still had cold-cache issues where the first request could timeout. This refactors to fetch git info in the background timer (every 3s) like cost data, ensuring: - First request returns immediately (may be empty briefly) - Timer fetches git info async, no timeout risk - Consistent architecture with cost data fetching - Simpler code with single active directory tracking 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- daemon/cc_info_timer.go | 54 ++++++++++++++++++++++++----------------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/daemon/cc_info_timer.go b/daemon/cc_info_timer.go index 90e8144..43d230d 100644 --- a/daemon/cc_info_timer.go +++ b/daemon/cc_info_timer.go @@ -12,7 +12,6 @@ import ( var ( CCInfoFetchInterval = 3 * time.Second CCInfoInactivityTimeout = 3 * time.Minute - GitInfoCacheTTL = 20 * time.Second ) // CCInfoCache holds the cached cost data for a time range @@ -22,11 +21,6 @@ type CCInfoCache struct { FetchedAt time.Time } -// cachedGitInfo holds cached git information for a directory -type cachedGitInfo struct { - info GitInfo - fetchedAt time.Time -} // CCInfoTimerService manages lazy-fetching of CC info data type CCInfoTimerService struct { @@ -43,9 +37,9 @@ type CCInfoTimerService struct { stopChan chan struct{} wg sync.WaitGroup - // Git info cache (per working directory) - gitCacheMu sync.RWMutex - gitCache map[string]cachedGitInfo + // Git info (single active working directory, fetched by timer) + activeWorkingDir string + gitInfo GitInfo } // NewCCInfoTimerService creates a new CC info timer service @@ -55,7 +49,6 @@ func NewCCInfoTimerService(config *model.ShellTimeConfig) *CCInfoTimerService { cache: make(map[CCInfoTimeRange]CCInfoCache), activeRanges: make(map[CCInfoTimeRange]bool), stopChan: make(chan struct{}), - gitCache: make(map[string]cachedGitInfo), } } @@ -129,9 +122,11 @@ func (s *CCInfoTimerService) stopTimer() { s.ticker.Stop() s.timerRunning = false - // Clear active ranges when stopping + // Clear active ranges and git info when stopping s.mu.Lock() s.activeRanges = make(map[CCInfoTimeRange]bool) + s.activeWorkingDir = "" + s.gitInfo = GitInfo{} s.mu.Unlock() slog.Info("CC info timer stopped due to inactivity") @@ -143,6 +138,7 @@ func (s *CCInfoTimerService) timerLoop() { // Fetch immediately on start s.fetchActiveRanges(context.Background()) + s.fetchGitInfo() for { select { @@ -155,6 +151,7 @@ func (s *CCInfoTimerService) timerLoop() { return } s.fetchActiveRanges(context.Background()) + s.fetchGitInfo() case <-s.stopChan: return @@ -275,26 +272,39 @@ func (s *CCInfoTimerService) fetchCCInfo(ctx context.Context, timeRange CCInfoTi }, nil } -// GetCachedGitInfo returns cached git info, fetching fresh if cache expired +// GetCachedGitInfo marks the working directory as active and returns cached git info. +// Git info is fetched by the background timer, so first call may return empty. func (s *CCInfoTimerService) GetCachedGitInfo(workingDir string) GitInfo { if workingDir == "" { return GitInfo{} } - s.gitCacheMu.RLock() - cached, exists := s.gitCache[workingDir] - s.gitCacheMu.RUnlock() + s.mu.Lock() + s.activeWorkingDir = workingDir + info := s.gitInfo + s.mu.Unlock() + + return info +} + +// fetchGitInfo fetches git info for the active working directory +func (s *CCInfoTimerService) fetchGitInfo() { + s.mu.RLock() + workingDir := s.activeWorkingDir + s.mu.RUnlock() - if exists && time.Since(cached.fetchedAt) < GitInfoCacheTTL { - return cached.info + if workingDir == "" { + return } - // Fetch fresh (this is the slow part) info := GetGitInfo(workingDir) - s.gitCacheMu.Lock() - s.gitCache[workingDir] = cachedGitInfo{info: info, fetchedAt: time.Now()} - s.gitCacheMu.Unlock() + s.mu.Lock() + s.gitInfo = info + s.mu.Unlock() - return info + slog.Debug("Git info updated", + slog.String("workingDir", workingDir), + slog.String("branch", info.Branch), + slog.Bool("dirty", info.Dirty)) }