From fadef4b1defb263d1915c66a36931e9dde393d67 Mon Sep 17 00:00:00 2001 From: AnnatarHe Date: Tue, 6 Jan 2026 23:24:54 +0800 Subject: [PATCH 1/2] feat(cc): add git branch and dirty status to statusline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add git branch name and dirty status to the cc statusline output. The git information is displayed first in the statusline, showing the branch name with an asterisk suffix if there are uncommitted changes. - Add WorkingDirectory field to CCStatuslineInput (from Claude Code) - Extend CCInfoRequest/Response with git info fields - Create daemon/git.go using go-git for branch/dirty detection - Update statusline format: 🌿 main* | 🤖 model | 💰 $x | ... 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- commands/cc_statusline.go | 51 ++++++--- daemon/cc_info_handler_test.go | 10 +- daemon/client.go | 7 +- daemon/client_test.go | 8 +- daemon/git.go | 46 ++++++++ daemon/git_test.go | 188 +++++++++++++++++++++++++++++++++ daemon/socket.go | 16 ++- go.mod | 10 ++ go.sum | 17 +++ model/cc_statusline_types.go | 7 +- 10 files changed, 330 insertions(+), 30 deletions(-) create mode 100644 daemon/git.go create mode 100644 daemon/git_test.go diff --git a/commands/cc_statusline.go b/commands/cc_statusline.go index 2871fda..9c551aa 100644 --- a/commands/cc_statusline.go +++ b/commands/cc_statusline.go @@ -22,6 +22,14 @@ var CCStatuslineCommand = &cli.Command{ Action: commandCCStatusline, } +// ccStatuslineResult combines daily stats with git info from daemon +type ccStatuslineResult struct { + Cost float64 + SessionSeconds int + GitBranch string + GitDirty bool +} + func commandCCStatusline(c *cli.Context) error { // Hard timeout for entire operation - statusline must be fast ctx, cancel := context.WithTimeout(c.Context, 100*time.Millisecond) @@ -44,15 +52,15 @@ func commandCCStatusline(c *cli.Context) error { // Calculate context percentage contextPercent := calculateContextPercent(data.ContextWindow) - // Get daily stats - try daemon first, fallback to direct API - var dailyStats model.CCStatuslineDailyStats + // Get daily stats and git info - try daemon first, fallback to direct API + var result ccStatuslineResult config, err := configService.ReadConfigFile(ctx) if err == nil { - dailyStats = getDailyStatsWithDaemonFallback(ctx, config) + result = getDaemonInfoWithFallback(ctx, config, data.WorkingDirectory) } // Format and output - output := formatStatuslineOutput(data.Model.DisplayName, data.Cost.TotalCostUSD, dailyStats.Cost, dailyStats.SessionSeconds, contextPercent) + output := formatStatuslineOutput(data.Model.DisplayName, data.Cost.TotalCostUSD, result.Cost, result.SessionSeconds, contextPercent, result.GitBranch, result.GitDirty) fmt.Println(output) return nil @@ -108,9 +116,20 @@ func calculateContextPercent(cw model.CCStatuslineContextWindow) float64 { return float64(currentTokens) / float64(cw.ContextWindowSize) * 100 } -func formatStatuslineOutput(modelName string, sessionCost, dailyCost float64, sessionSeconds int, contextPercent float64) string { +func formatStatuslineOutput(modelName string, sessionCost, dailyCost float64, sessionSeconds int, contextPercent float64, gitBranch string, gitDirty bool) string { var parts []string + // Git info FIRST (green) + if gitBranch != "" { + gitStr := gitBranch + if gitDirty { + gitStr += "*" + } + parts = append(parts, color.Green.Sprintf("🌿 %s", gitStr)) + } else { + parts = append(parts, color.Gray.Sprint("🌿 -")) + } + // Model name modelStr := fmt.Sprintf("🤖 %s", modelName) parts = append(parts, modelStr) @@ -151,7 +170,7 @@ func formatStatuslineOutput(modelName string, sessionCost, dailyCost float64, se } func outputFallback() { - fmt.Println(color.Gray.Sprint("🤖 - | 💰 - | 📊 - | ⏱️ - | 📈 -%")) + fmt.Println(color.Gray.Sprint("🌿 - | 🤖 - | 💰 - | 📊 - | ⏱️ - | 📈 -%")) } // formatSessionDuration formats seconds into a human-readable duration @@ -169,9 +188,9 @@ func formatSessionDuration(totalSeconds int) string { return fmt.Sprintf("%ds", seconds) } -// getDailyStatsWithDaemonFallback tries to get daily stats from daemon first, -// falls back to direct API if daemon is unavailable -func getDailyStatsWithDaemonFallback(ctx context.Context, config model.ShellTimeConfig) model.CCStatuslineDailyStats { +// getDaemonInfoWithFallback tries to get daily stats and git info from daemon first, +// falls back to direct API for stats if daemon is unavailable (git info only from daemon) +func getDaemonInfoWithFallback(ctx context.Context, config model.ShellTimeConfig, workingDir string) ccStatuslineResult { socketPath := config.SocketPath if socketPath == "" { socketPath = model.DefaultSocketPath @@ -179,15 +198,21 @@ func getDailyStatsWithDaemonFallback(ctx context.Context, config model.ShellTime // Try daemon first (50ms timeout for fast path) if daemon.IsSocketReady(ctx, socketPath) { - resp, err := daemon.RequestCCInfo(socketPath, daemon.CCInfoTimeRangeToday, 50*time.Millisecond) + resp, err := daemon.RequestCCInfo(socketPath, daemon.CCInfoTimeRangeToday, workingDir, 50*time.Millisecond) if err == nil && resp != nil { - return model.CCStatuslineDailyStats{ + return ccStatuslineResult{ Cost: resp.TotalCostUSD, SessionSeconds: resp.TotalSessionSeconds, + GitBranch: resp.GitBranch, + GitDirty: resp.GitDirty, } } } - // Fallback to direct API (existing behavior) - return model.FetchDailyStatsCached(ctx, config) + // Fallback to direct API for stats (no git info available without daemon) + stats := model.FetchDailyStatsCached(ctx, config) + return ccStatuslineResult{ + Cost: stats.Cost, + SessionSeconds: stats.SessionSeconds, + } } diff --git a/daemon/cc_info_handler_test.go b/daemon/cc_info_handler_test.go index 1ac2ad1..a470fab 100644 --- a/daemon/cc_info_handler_test.go +++ b/daemon/cc_info_handler_test.go @@ -232,7 +232,7 @@ func (s *CCInfoClientTestSuite) TestRequestCCInfo_Success() { // Give server time to start time.Sleep(10 * time.Millisecond) - response, err := RequestCCInfo(s.socketPath, CCInfoTimeRangeToday, 1*time.Second) + response, err := RequestCCInfo(s.socketPath, CCInfoTimeRangeToday, "", 1*time.Second) assert.NoError(s.T(), err) assert.NotNil(s.T(), response) @@ -255,14 +255,14 @@ func (s *CCInfoClientTestSuite) TestRequestCCInfo_Timeout() { time.Sleep(10 * time.Millisecond) - response, err := RequestCCInfo(s.socketPath, CCInfoTimeRangeToday, 50*time.Millisecond) + response, err := RequestCCInfo(s.socketPath, CCInfoTimeRangeToday, "", 50*time.Millisecond) assert.Error(s.T(), err) assert.Nil(s.T(), response) } func (s *CCInfoClientTestSuite) TestRequestCCInfo_SocketNotFound() { - response, err := RequestCCInfo("/nonexistent/socket.sock", CCInfoTimeRangeToday, 100*time.Millisecond) + response, err := RequestCCInfo("/nonexistent/socket.sock", CCInfoTimeRangeToday, "", 100*time.Millisecond) assert.Error(s.T(), err) assert.Nil(s.T(), response) @@ -287,7 +287,7 @@ func (s *CCInfoClientTestSuite) TestRequestCCInfo_InvalidResponse() { time.Sleep(10 * time.Millisecond) - response, err := RequestCCInfo(s.socketPath, CCInfoTimeRangeToday, 1*time.Second) + response, err := RequestCCInfo(s.socketPath, CCInfoTimeRangeToday, "", 1*time.Second) assert.Error(s.T(), err) assert.Nil(s.T(), response) @@ -312,7 +312,7 @@ func (s *CCInfoClientTestSuite) TestRequestCCInfo_SendsCorrectMessage() { time.Sleep(10 * time.Millisecond) - RequestCCInfo(s.socketPath, CCInfoTimeRangeWeek, 1*time.Second) + RequestCCInfo(s.socketPath, CCInfoTimeRangeWeek, "", 1*time.Second) assert.Equal(s.T(), SocketMessageTypeCCInfo, receivedMsg.Type) diff --git a/daemon/client.go b/daemon/client.go index c053f1e..0b9d4cc 100644 --- a/daemon/client.go +++ b/daemon/client.go @@ -51,8 +51,8 @@ func SendLocalDataToSocket( return nil } -// RequestCCInfo requests CC info (cost data) from the daemon -func RequestCCInfo(socketPath string, timeRange CCInfoTimeRange, timeout time.Duration) (*CCInfoResponse, error) { +// RequestCCInfo requests CC info (cost data and git info) from the daemon +func RequestCCInfo(socketPath string, timeRange CCInfoTimeRange, workingDir string, timeout time.Duration) (*CCInfoResponse, error) { conn, err := net.DialTimeout("unix", socketPath, timeout) if err != nil { return nil, err @@ -66,7 +66,8 @@ func RequestCCInfo(socketPath string, timeRange CCInfoTimeRange, timeout time.Du msg := SocketMessage{ Type: SocketMessageTypeCCInfo, Payload: CCInfoRequest{ - TimeRange: timeRange, + TimeRange: timeRange, + WorkingDirectory: workingDir, }, } diff --git a/daemon/client_test.go b/daemon/client_test.go index ca3bea4..78c6add 100644 --- a/daemon/client_test.go +++ b/daemon/client_test.go @@ -157,7 +157,7 @@ func TestRequestCCInfo(t *testing.T) { // Give server time to start time.Sleep(50 * time.Millisecond) - response, err := RequestCCInfo(socketPath, CCInfoTimeRangeToday, 5*time.Second) + response, err := RequestCCInfo(socketPath, CCInfoTimeRangeToday, "", 5*time.Second) if err != nil { t.Fatalf("RequestCCInfo failed: %v", err) } @@ -200,14 +200,14 @@ func TestRequestCCInfo_Timeout(t *testing.T) { // Give server time to start time.Sleep(50 * time.Millisecond) - _, err = RequestCCInfo(socketPath, CCInfoTimeRangeToday, 100*time.Millisecond) + _, err = RequestCCInfo(socketPath, CCInfoTimeRangeToday, "", 100*time.Millisecond) if err == nil { t.Error("Expected timeout error") } } func TestRequestCCInfo_SocketNotExists(t *testing.T) { - _, err := RequestCCInfo("/nonexistent/socket.sock", CCInfoTimeRangeToday, 1*time.Second) + _, err := RequestCCInfo("/nonexistent/socket.sock", CCInfoTimeRangeToday, "", 1*time.Second) if err == nil { t.Error("Expected error when socket doesn't exist") } @@ -261,7 +261,7 @@ func TestRequestCCInfo_AllTimeRanges(t *testing.T) { time.Sleep(50 * time.Millisecond) - response, err := RequestCCInfo(socketPath, timeRange, 5*time.Second) + response, err := RequestCCInfo(socketPath, timeRange, "", 5*time.Second) if err != nil { t.Fatalf("RequestCCInfo failed: %v", err) } diff --git a/daemon/git.go b/daemon/git.go new file mode 100644 index 0000000..2e9af40 --- /dev/null +++ b/daemon/git.go @@ -0,0 +1,46 @@ +package daemon + +import ( + "github.com/go-git/go-git/v5" +) + +// GitInfo contains git repository information +type GitInfo struct { + Branch string + Dirty bool + IsRepo bool +} + +// GetGitInfo returns git branch and dirty status for a directory. +// It walks up the directory tree to find the git repository root. +func GetGitInfo(workingDir string) GitInfo { + if workingDir == "" { + return GitInfo{} + } + + repo, err := git.PlainOpenWithOptions(workingDir, &git.PlainOpenOptions{ + DetectDotGit: true, // Walk up to find .git + }) + if err != nil { + return GitInfo{} // Not a git repo + } + + info := GitInfo{IsRepo: true} + + // Get branch name from HEAD + head, err := repo.Head() + if err == nil { + info.Branch = head.Name().Short() + } + + // Check dirty status by examining worktree status + worktree, err := repo.Worktree() + if err == nil { + status, err := worktree.Status() + if err == nil { + info.Dirty = !status.IsClean() + } + } + + return info +} diff --git a/daemon/git_test.go b/daemon/git_test.go new file mode 100644 index 0000000..996c61a --- /dev/null +++ b/daemon/git_test.go @@ -0,0 +1,188 @@ +package daemon + +import ( + "os" + "os/exec" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type GitInfoTestSuite struct { + suite.Suite + tempDir string +} + +func (s *GitInfoTestSuite) SetupTest() { + var err error + s.tempDir, err = os.MkdirTemp("", "git-info-test-*") + assert.NoError(s.T(), err) +} + +func (s *GitInfoTestSuite) TearDownTest() { + os.RemoveAll(s.tempDir) +} + +func (s *GitInfoTestSuite) TestGetGitInfo_EmptyWorkingDir() { + info := GetGitInfo("") + assert.False(s.T(), info.IsRepo) + assert.Empty(s.T(), info.Branch) + assert.False(s.T(), info.Dirty) +} + +func (s *GitInfoTestSuite) TestGetGitInfo_NonGitDirectory() { + info := GetGitInfo(s.tempDir) + assert.False(s.T(), info.IsRepo) + assert.Empty(s.T(), info.Branch) + assert.False(s.T(), info.Dirty) +} + +func (s *GitInfoTestSuite) TestGetGitInfo_NonExistentDirectory() { + info := GetGitInfo("/nonexistent/path/that/does/not/exist") + assert.False(s.T(), info.IsRepo) + assert.Empty(s.T(), info.Branch) + assert.False(s.T(), info.Dirty) +} + +func (s *GitInfoTestSuite) TestGetGitInfo_GitRepo_CleanState() { + // Initialize git repo + cmd := exec.Command("git", "init") + cmd.Dir = s.tempDir + err := cmd.Run() + if err != nil { + s.T().Skip("git not available") + } + + // Configure git user (required for commits) + exec.Command("git", "-C", s.tempDir, "config", "user.email", "test@test.com").Run() + exec.Command("git", "-C", s.tempDir, "config", "user.name", "Test User").Run() + + // Create initial commit to establish HEAD + testFile := filepath.Join(s.tempDir, "test.txt") + os.WriteFile(testFile, []byte("test"), 0644) + exec.Command("git", "-C", s.tempDir, "add", ".").Run() + exec.Command("git", "-C", s.tempDir, "commit", "-m", "initial").Run() + + info := GetGitInfo(s.tempDir) + assert.True(s.T(), info.IsRepo) + assert.NotEmpty(s.T(), info.Branch) + assert.False(s.T(), info.Dirty) +} + +func (s *GitInfoTestSuite) TestGetGitInfo_GitRepo_DirtyState() { + // Initialize git repo + cmd := exec.Command("git", "init") + cmd.Dir = s.tempDir + err := cmd.Run() + if err != nil { + s.T().Skip("git not available") + } + + // Configure git user + exec.Command("git", "-C", s.tempDir, "config", "user.email", "test@test.com").Run() + exec.Command("git", "-C", s.tempDir, "config", "user.name", "Test User").Run() + + // Create initial commit + testFile := filepath.Join(s.tempDir, "test.txt") + os.WriteFile(testFile, []byte("test"), 0644) + exec.Command("git", "-C", s.tempDir, "add", ".").Run() + exec.Command("git", "-C", s.tempDir, "commit", "-m", "initial").Run() + + // Make a change (dirty state) + os.WriteFile(testFile, []byte("modified"), 0644) + + info := GetGitInfo(s.tempDir) + assert.True(s.T(), info.IsRepo) + assert.NotEmpty(s.T(), info.Branch) + assert.True(s.T(), info.Dirty) +} + +func (s *GitInfoTestSuite) TestGetGitInfo_GitRepo_UntrackedFile() { + // Initialize git repo + cmd := exec.Command("git", "init") + cmd.Dir = s.tempDir + err := cmd.Run() + if err != nil { + s.T().Skip("git not available") + } + + // Configure git user + exec.Command("git", "-C", s.tempDir, "config", "user.email", "test@test.com").Run() + exec.Command("git", "-C", s.tempDir, "config", "user.name", "Test User").Run() + + // Create initial commit + testFile := filepath.Join(s.tempDir, "test.txt") + os.WriteFile(testFile, []byte("test"), 0644) + exec.Command("git", "-C", s.tempDir, "add", ".").Run() + exec.Command("git", "-C", s.tempDir, "commit", "-m", "initial").Run() + + // Add untracked file (makes repo dirty) + untrackedFile := filepath.Join(s.tempDir, "untracked.txt") + os.WriteFile(untrackedFile, []byte("untracked"), 0644) + + info := GetGitInfo(s.tempDir) + assert.True(s.T(), info.IsRepo) + assert.True(s.T(), info.Dirty, "untracked files should make repo dirty") +} + +func (s *GitInfoTestSuite) TestGetGitInfo_GitRepo_BranchName() { + // Initialize git repo + cmd := exec.Command("git", "init") + cmd.Dir = s.tempDir + err := cmd.Run() + if err != nil { + s.T().Skip("git not available") + } + + // Configure git user + exec.Command("git", "-C", s.tempDir, "config", "user.email", "test@test.com").Run() + exec.Command("git", "-C", s.tempDir, "config", "user.name", "Test User").Run() + + // Create initial commit on main/master + testFile := filepath.Join(s.tempDir, "test.txt") + os.WriteFile(testFile, []byte("test"), 0644) + exec.Command("git", "-C", s.tempDir, "add", ".").Run() + exec.Command("git", "-C", s.tempDir, "commit", "-m", "initial").Run() + + // Create and checkout a new branch + exec.Command("git", "-C", s.tempDir, "checkout", "-b", "feature/test-branch").Run() + + info := GetGitInfo(s.tempDir) + assert.True(s.T(), info.IsRepo) + assert.Equal(s.T(), "feature/test-branch", info.Branch) +} + +func (s *GitInfoTestSuite) TestGetGitInfo_Subdirectory() { + // Initialize git repo + cmd := exec.Command("git", "init") + cmd.Dir = s.tempDir + err := cmd.Run() + if err != nil { + s.T().Skip("git not available") + } + + // Configure git user + exec.Command("git", "-C", s.tempDir, "config", "user.email", "test@test.com").Run() + exec.Command("git", "-C", s.tempDir, "config", "user.name", "Test User").Run() + + // Create initial commit + testFile := filepath.Join(s.tempDir, "test.txt") + os.WriteFile(testFile, []byte("test"), 0644) + exec.Command("git", "-C", s.tempDir, "add", ".").Run() + exec.Command("git", "-C", s.tempDir, "commit", "-m", "initial").Run() + + // Create subdirectory + subDir := filepath.Join(s.tempDir, "src", "components") + os.MkdirAll(subDir, 0755) + + // GetGitInfo from subdirectory should still work (DetectDotGit: true) + info := GetGitInfo(subDir) + assert.True(s.T(), info.IsRepo) + assert.NotEmpty(s.T(), info.Branch) +} + +func TestGitInfoTestSuite(t *testing.T) { + suite.Run(t, new(GitInfoTestSuite)) +} diff --git a/daemon/socket.go b/daemon/socket.go index c56d2f9..f2f129a 100644 --- a/daemon/socket.go +++ b/daemon/socket.go @@ -32,7 +32,8 @@ const ( ) type CCInfoRequest struct { - TimeRange CCInfoTimeRange `json:"timeRange"` + TimeRange CCInfoTimeRange `json:"timeRange"` + WorkingDirectory string `json:"workingDirectory"` } type CCInfoResponse struct { @@ -40,6 +41,8 @@ type CCInfoResponse struct { TotalSessionSeconds int `json:"totalSessionSeconds"` TimeRange string `json:"timeRange"` CachedAt time.Time `json:"cachedAt"` + GitBranch string `json:"gitBranch"` + GitDirty bool `json:"gitDirty"` } // StatusResponse contains daemon status information @@ -197,23 +200,32 @@ func (p *SocketHandler) handleStatus(conn net.Conn) { func (p *SocketHandler) handleCCInfo(conn net.Conn, msg SocketMessage) { slog.Debug("cc_info socket event received") - // Parse time range from payload, default to "today" + // Parse time range and working directory from payload timeRange := CCInfoTimeRangeToday + var workingDir string if payload, ok := msg.Payload.(map[string]interface{}); ok { if tr, ok := payload["timeRange"].(string); ok { timeRange = CCInfoTimeRange(tr) } + if wd, ok := payload["workingDirectory"].(string); ok { + workingDir = wd + } } // Get cached cost first (marks range as active), then notify activity (starts timer) cache := p.ccInfoTimer.GetCachedCost(timeRange) p.ccInfoTimer.NotifyActivity() + // Get git info (fast, no caching needed since it changes frequently) + gitInfo := GetGitInfo(workingDir) + response := CCInfoResponse{ TotalCostUSD: cache.TotalCostUSD, TotalSessionSeconds: cache.TotalSessionSeconds, TimeRange: string(timeRange), CachedAt: cache.FetchedAt, + GitBranch: gitInfo.Branch, + GitDirty: gitInfo.Dirty, } encoder := json.NewEncoder(conn) diff --git a/go.mod b/go.mod index 5bacf2c..3886a46 100644 --- a/go.mod +++ b/go.mod @@ -31,6 +31,9 @@ require ( atomicgo.dev/cursor v0.2.0 // indirect atomicgo.dev/keyboard v0.2.9 // indirect atomicgo.dev/schedule v0.1.0 // indirect + dario.cat/mergo v1.0.0 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/ProtonMail/go-crypto v1.1.6 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/buger/jsonparser v1.1.1 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect @@ -38,10 +41,12 @@ require ( github.com/clipperhouse/displaywidth v0.6.1 // indirect github.com/clipperhouse/stringish v0.1.1 // indirect github.com/clipperhouse/uax29/v2 v2.3.0 // indirect + github.com/cloudflare/circl v1.6.1 // indirect github.com/containerd/console v1.0.5 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/cyphar/filepath-securejoin v0.6.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/emirpasic/gods v1.18.1 // indirect github.com/fatih/color v1.18.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect @@ -49,8 +54,10 @@ require ( github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-resty/resty/v2 v2.17.0 // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/lithammer/fuzzysearch v1.1.8 // indirect github.com/mailru/easyjson v0.7.7 // indirect @@ -64,8 +71,10 @@ require ( github.com/pjbgf/sha1cd v0.5.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/skeema/knownhosts v1.3.1 // indirect github.com/stretchr/objx v0.5.3 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect @@ -80,6 +89,7 @@ require ( go.opentelemetry.io/otel/sdk v1.39.0 // indirect go.opentelemetry.io/otel/sdk/log v0.15.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.39.0 // indirect + golang.org/x/crypto v0.46.0 // indirect golang.org/x/net v0.48.0 // indirect golang.org/x/sys v0.39.0 // indirect golang.org/x/term v0.38.0 // indirect diff --git a/go.sum b/go.sum index 6c62b77..ba1d4a0 100644 --- a/go.sum +++ b/go.sum @@ -17,6 +17,7 @@ github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/ github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE= github.com/MarvinJWendt/testza v0.5.2 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi+zB4= github.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/PromptPal/go-sdk v0.4.2 h1:onABRhxodJ285s7CQaXTt7cmpPQ54ztTW5FMlXfGoM0= @@ -25,6 +26,10 @@ github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNx github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/ThreeDotsLabs/watermill v1.5.1 h1:t5xMivyf9tpmU3iozPqyrCZXHvoV1XQDfihas4sV0fY= github.com/ThreeDotsLabs/watermill v1.5.1/go.mod h1:Uop10dA3VeJWsSvis9qO3vbVY892LARrKAdki6WtXS4= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= @@ -52,12 +57,16 @@ github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= +github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= +github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/go-billy/v5 v5.7.0 h1:83lBUJhGWhYp0ngzCMSgllhUSuoHP1iEWYjsPl9nwqM= @@ -161,11 +170,13 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -223,6 +234,7 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= @@ -231,6 +243,7 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= @@ -239,13 +252,16 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -261,6 +277,7 @@ golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= diff --git a/model/cc_statusline_types.go b/model/cc_statusline_types.go index 58b9d9f..265fb60 100644 --- a/model/cc_statusline_types.go +++ b/model/cc_statusline_types.go @@ -2,9 +2,10 @@ package model // CCStatuslineInput represents the JSON input from Claude Code statusline type CCStatuslineInput struct { - Model CCStatuslineModel `json:"model"` - Cost CCStatuslineCost `json:"cost"` - ContextWindow CCStatuslineContextWindow `json:"context_window"` + Model CCStatuslineModel `json:"model"` + Cost CCStatuslineCost `json:"cost"` + ContextWindow CCStatuslineContextWindow `json:"context_window"` + WorkingDirectory string `json:"working_directory"` } // CCStatuslineModel represents model information From 5c38a67a686ff24e1d1e92575791eda6c351c661 Mon Sep 17 00:00:00 2001 From: AnnatarHe Date: Tue, 6 Jan 2026 23:32:23 +0800 Subject: [PATCH 2/2] fix(cc): update statusline tests for new git info parameters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update test function signatures to match new formatStatuslineOutput and getDaemonInfoWithFallback APIs. Add tests for git branch display including dirty branch indicator. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- commands/cc_statusline_test.go | 73 +++++++++++++++++++++++----------- 1 file changed, 50 insertions(+), 23 deletions(-) diff --git a/commands/cc_statusline_test.go b/commands/cc_statusline_test.go index 7df2cbd..2dc29c1 100644 --- a/commands/cc_statusline_test.go +++ b/commands/cc_statusline_test.go @@ -40,9 +40,9 @@ func (s *CCStatuslineTestSuite) TearDownTest() { os.Remove(s.socketPath) } -// getDailyCostWithDaemonFallback Tests +// getDaemonInfoWithFallback Tests -func (s *CCStatuslineTestSuite) TestGetDailyStats_UsesDaemonWhenAvailable() { +func (s *CCStatuslineTestSuite) TestGetDaemonInfo_UsesDaemonWhenAvailable() { // Start mock daemon socket listener, err := net.Listen("unix", s.socketPath) assert.NoError(s.T(), err) @@ -50,6 +50,8 @@ func (s *CCStatuslineTestSuite) TestGetDailyStats_UsesDaemonWhenAvailable() { expectedCost := 15.67 expectedSessionSeconds := 3600 + expectedBranch := "main" + expectedDirty := true go func() { conn, _ := listener.Accept() defer conn.Close() @@ -62,6 +64,8 @@ func (s *CCStatuslineTestSuite) TestGetDailyStats_UsesDaemonWhenAvailable() { TotalSessionSeconds: expectedSessionSeconds, TimeRange: "today", CachedAt: time.Now(), + GitBranch: expectedBranch, + GitDirty: expectedDirty, } json.NewEncoder(conn).Encode(response) }() @@ -72,27 +76,31 @@ func (s *CCStatuslineTestSuite) TestGetDailyStats_UsesDaemonWhenAvailable() { SocketPath: s.socketPath, } - stats := getDailyStatsWithDaemonFallback(context.Background(), config) + result := getDaemonInfoWithFallback(context.Background(), config, "/some/path") - assert.Equal(s.T(), expectedCost, stats.Cost) - assert.Equal(s.T(), expectedSessionSeconds, stats.SessionSeconds) + assert.Equal(s.T(), expectedCost, result.Cost) + assert.Equal(s.T(), expectedSessionSeconds, result.SessionSeconds) + assert.Equal(s.T(), expectedBranch, result.GitBranch) + assert.Equal(s.T(), expectedDirty, result.GitDirty) } -func (s *CCStatuslineTestSuite) TestGetDailyStats_FallbackWhenDaemonUnavailable() { +func (s *CCStatuslineTestSuite) TestGetDaemonInfo_FallbackWhenDaemonUnavailable() { // No socket exists, should fall back to cached API config := model.ShellTimeConfig{ SocketPath: "/nonexistent/socket.sock", Token: "", // No token means FetchDailyStatsCached returns zero values } - stats := getDailyStatsWithDaemonFallback(context.Background(), config) + result := getDaemonInfoWithFallback(context.Background(), config, "") // Should return zero values (from cache fallback with no token) - assert.Equal(s.T(), float64(0), stats.Cost) - assert.Equal(s.T(), 0, stats.SessionSeconds) + assert.Equal(s.T(), float64(0), result.Cost) + assert.Equal(s.T(), 0, result.SessionSeconds) + assert.Empty(s.T(), result.GitBranch) + assert.False(s.T(), result.GitDirty) } -func (s *CCStatuslineTestSuite) TestGetDailyStats_FallbackOnDaemonError() { +func (s *CCStatuslineTestSuite) TestGetDaemonInfo_FallbackOnDaemonError() { // Start mock daemon that returns error listener, err := net.Listen("unix", s.socketPath) assert.NoError(s.T(), err) @@ -111,14 +119,16 @@ func (s *CCStatuslineTestSuite) TestGetDailyStats_FallbackOnDaemonError() { Token: "", // No token } - stats := getDailyStatsWithDaemonFallback(context.Background(), config) + result := getDaemonInfoWithFallback(context.Background(), config, "") // Should fall back and return zero values - assert.Equal(s.T(), float64(0), stats.Cost) - assert.Equal(s.T(), 0, stats.SessionSeconds) + assert.Equal(s.T(), float64(0), result.Cost) + assert.Equal(s.T(), 0, result.SessionSeconds) + assert.Empty(s.T(), result.GitBranch) + assert.False(s.T(), result.GitDirty) } -func (s *CCStatuslineTestSuite) TestGetDailyStats_UsesDefaultSocketPath() { +func (s *CCStatuslineTestSuite) TestGetDaemonInfo_UsesDefaultSocketPath() { // Test that default socket path is used when config is empty config := model.ShellTimeConfig{ SocketPath: "", // Empty path - should use model.DefaultSocketPath @@ -127,22 +137,23 @@ func (s *CCStatuslineTestSuite) TestGetDailyStats_UsesDefaultSocketPath() { // This should use model.DefaultSocketPath internally // Since no daemon is running at the default path, it will fall back to cached API - // The function should not panic and should return a valid stats struct - stats := getDailyStatsWithDaemonFallback(context.Background(), config) + // The function should not panic and should return a valid result struct + result := getDaemonInfoWithFallback(context.Background(), config, "") // We can't assert on exact values since the global cache might have data // from previous tests. Just verify the function returns without error // and returns non-negative values - assert.GreaterOrEqual(s.T(), stats.Cost, float64(0)) - assert.GreaterOrEqual(s.T(), stats.SessionSeconds, 0) + assert.GreaterOrEqual(s.T(), result.Cost, float64(0)) + assert.GreaterOrEqual(s.T(), result.SessionSeconds, 0) } // formatStatuslineOutput Tests func (s *CCStatuslineTestSuite) TestFormatStatuslineOutput_AllValues() { - output := formatStatuslineOutput("claude-opus-4", 1.23, 4.56, 3661, 75.0) + output := formatStatuslineOutput("claude-opus-4", 1.23, 4.56, 3661, 75.0, "main", false) // Should contain all components + assert.Contains(s.T(), output, "🌿 main") assert.Contains(s.T(), output, "🤖 claude-opus-4") assert.Contains(s.T(), output, "$1.23") assert.Contains(s.T(), output, "$4.56") @@ -150,8 +161,24 @@ func (s *CCStatuslineTestSuite) TestFormatStatuslineOutput_AllValues() { assert.Contains(s.T(), output, "75%") // Context percentage } +func (s *CCStatuslineTestSuite) TestFormatStatuslineOutput_WithDirtyBranch() { + output := formatStatuslineOutput("claude-opus-4", 1.23, 4.56, 3661, 75.0, "feature/test", true) + + // Should contain branch with asterisk for dirty + assert.Contains(s.T(), output, "🌿 feature/test*") + assert.Contains(s.T(), output, "🤖 claude-opus-4") +} + +func (s *CCStatuslineTestSuite) TestFormatStatuslineOutput_NoBranch() { + output := formatStatuslineOutput("claude-opus-4", 1.23, 4.56, 3661, 75.0, "", false) + + // Should show "-" for no branch + assert.Contains(s.T(), output, "🌿 -") + assert.Contains(s.T(), output, "🤖 claude-opus-4") +} + func (s *CCStatuslineTestSuite) TestFormatStatuslineOutput_ZeroDailyCost() { - output := formatStatuslineOutput("claude-sonnet", 0.50, 0, 300, 50.0) + output := formatStatuslineOutput("claude-sonnet", 0.50, 0, 300, 50.0, "main", false) // Should show "-" for zero daily cost assert.Contains(s.T(), output, "📊 -") @@ -159,14 +186,14 @@ func (s *CCStatuslineTestSuite) TestFormatStatuslineOutput_ZeroDailyCost() { } func (s *CCStatuslineTestSuite) TestFormatStatuslineOutput_ZeroSessionSeconds() { - output := formatStatuslineOutput("claude-sonnet", 0.50, 1.0, 0, 50.0) + output := formatStatuslineOutput("claude-sonnet", 0.50, 1.0, 0, 50.0, "main", false) // Should show "-" for zero session seconds assert.Contains(s.T(), output, "⏱️ -") } func (s *CCStatuslineTestSuite) TestFormatStatuslineOutput_HighContextPercentage() { - output := formatStatuslineOutput("test-model", 1.0, 1.0, 60, 85.0) + output := formatStatuslineOutput("test-model", 1.0, 1.0, 60, 85.0, "main", false) // Should contain the percentage (color codes may vary) assert.Contains(s.T(), output, "85%") @@ -174,7 +201,7 @@ func (s *CCStatuslineTestSuite) TestFormatStatuslineOutput_HighContextPercentage } func (s *CCStatuslineTestSuite) TestFormatStatuslineOutput_LowContextPercentage() { - output := formatStatuslineOutput("test-model", 1.0, 1.0, 45, 25.0) + output := formatStatuslineOutput("test-model", 1.0, 1.0, 45, 25.0, "main", false) // Should contain the percentage assert.Contains(s.T(), output, "25%")