diff --git a/commands/cc_statusline.go b/commands/cc_statusline.go index bf1dfa8..9834e24 100644 --- a/commands/cc_statusline.go +++ b/commands/cc_statusline.go @@ -70,7 +70,7 @@ func commandCCStatusline(c *cli.Context) error { } // Format and output - output := formatStatuslineOutput(data.Model.DisplayName, data.Cost.TotalCostUSD, result.Cost, result.SessionSeconds, contextPercent, result.GitBranch, result.GitDirty, result.FiveHourUtilization, result.SevenDayUtilization, result.UserLogin, result.WebEndpoint) + output := formatStatuslineOutput(data.Model.DisplayName, data.Cost.TotalCostUSD, result.Cost, result.SessionSeconds, contextPercent, result.GitBranch, result.GitDirty, result.FiveHourUtilization, result.SevenDayUtilization, result.UserLogin, result.WebEndpoint, data.SessionID) fmt.Println(output) return nil @@ -126,7 +126,7 @@ func calculateContextPercent(cw model.CCStatuslineContextWindow) float64 { return float64(currentTokens) / float64(cw.ContextWindowSize) * 100 } -func formatStatuslineOutput(modelName string, sessionCost, dailyCost float64, sessionSeconds int, contextPercent float64, gitBranch string, gitDirty bool, fiveHourUtil, sevenDayUtil *float64, userLogin, webEndpoint string) string { +func formatStatuslineOutput(modelName string, sessionCost, dailyCost float64, sessionSeconds int, contextPercent float64, gitBranch string, gitDirty bool, fiveHourUtil, sevenDayUtil *float64, userLogin, webEndpoint, sessionID string) string { var parts []string // Git info FIRST (green) @@ -144,8 +144,12 @@ func formatStatuslineOutput(modelName string, sessionCost, dailyCost float64, se modelStr := fmt.Sprintf("🤖 %s", modelName) parts = append(parts, modelStr) - // Session cost (cyan) + // Session cost (cyan) - clickable link to session page when user login and session ID are available sessionStr := color.Cyan.Sprintf("💰 $%.2f", sessionCost) + if userLogin != "" && webEndpoint != "" && sessionID != "" { + url := fmt.Sprintf("%s/users/%s/coding-agent/session/%s", webEndpoint, userLogin, sessionID) + sessionStr = wrapOSC8Link(url, sessionStr) + } parts = append(parts, sessionStr) // Daily cost (yellow) - clickable link to coding agent page when user login is available diff --git a/commands/cc_statusline_test.go b/commands/cc_statusline_test.go index 786be9b..fa52e08 100644 --- a/commands/cc_statusline_test.go +++ b/commands/cc_statusline_test.go @@ -150,7 +150,7 @@ func (s *CCStatuslineTestSuite) TestGetDaemonInfo_UsesDefaultSocketPath() { // formatStatuslineOutput Tests func (s *CCStatuslineTestSuite) TestFormatStatuslineOutput_AllValues() { - output := formatStatuslineOutput("claude-opus-4", 1.23, 4.56, 3661, 75.0, "main", false, nil, nil, "", "") + output := formatStatuslineOutput("claude-opus-4", 1.23, 4.56, 3661, 75.0, "main", false, nil, nil, "", "", "") // Should contain all components assert.Contains(s.T(), output, "🌿 main") @@ -162,7 +162,7 @@ func (s *CCStatuslineTestSuite) TestFormatStatuslineOutput_AllValues() { } func (s *CCStatuslineTestSuite) TestFormatStatuslineOutput_WithDirtyBranch() { - output := formatStatuslineOutput("claude-opus-4", 1.23, 4.56, 3661, 75.0, "feature/test", true, nil, nil, "", "") + output := formatStatuslineOutput("claude-opus-4", 1.23, 4.56, 3661, 75.0, "feature/test", true, nil, nil, "", "", "") // Should contain branch with asterisk for dirty assert.Contains(s.T(), output, "🌿 feature/test*") @@ -170,7 +170,7 @@ func (s *CCStatuslineTestSuite) TestFormatStatuslineOutput_WithDirtyBranch() { } func (s *CCStatuslineTestSuite) TestFormatStatuslineOutput_NoBranch() { - output := formatStatuslineOutput("claude-opus-4", 1.23, 4.56, 3661, 75.0, "", false, nil, nil, "", "") + output := formatStatuslineOutput("claude-opus-4", 1.23, 4.56, 3661, 75.0, "", false, nil, nil, "", "", "") // Should show "-" for no branch assert.Contains(s.T(), output, "🌿 -") @@ -178,7 +178,7 @@ func (s *CCStatuslineTestSuite) TestFormatStatuslineOutput_NoBranch() { } func (s *CCStatuslineTestSuite) TestFormatStatuslineOutput_ZeroDailyCost() { - output := formatStatuslineOutput("claude-sonnet", 0.50, 0, 300, 50.0, "main", false, nil, nil, "", "") + output := formatStatuslineOutput("claude-sonnet", 0.50, 0, 300, 50.0, "main", false, nil, nil, "", "", "") // Should show "-" for zero daily cost assert.Contains(s.T(), output, "📊 -") @@ -186,14 +186,14 @@ func (s *CCStatuslineTestSuite) TestFormatStatuslineOutput_ZeroDailyCost() { } func (s *CCStatuslineTestSuite) TestFormatStatuslineOutput_ZeroSessionSeconds() { - output := formatStatuslineOutput("claude-sonnet", 0.50, 1.0, 0, 50.0, "main", false, nil, nil, "", "") + output := formatStatuslineOutput("claude-sonnet", 0.50, 1.0, 0, 50.0, "main", false, nil, nil, "", "", "") // 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, "main", false, nil, nil, "", "") + output := formatStatuslineOutput("test-model", 1.0, 1.0, 60, 85.0, "main", false, nil, nil, "", "", "") // Should contain the percentage (color codes may vary) assert.Contains(s.T(), output, "85%") @@ -201,7 +201,7 @@ func (s *CCStatuslineTestSuite) TestFormatStatuslineOutput_HighContextPercentage } func (s *CCStatuslineTestSuite) TestFormatStatuslineOutput_LowContextPercentage() { - output := formatStatuslineOutput("test-model", 1.0, 1.0, 45, 25.0, "main", false, nil, nil, "", "") + output := formatStatuslineOutput("test-model", 1.0, 1.0, 45, 25.0, "main", false, nil, nil, "", "", "") // Should contain the percentage assert.Contains(s.T(), output, "25%") @@ -328,10 +328,36 @@ func (s *CCStatuslineTestSuite) TestFormatQuotaPart_ContainsLink() { assert.Contains(s.T(), result, "7d:23%") } +func (s *CCStatuslineTestSuite) TestFormatStatuslineOutput_SessionCostWithLink() { + output := formatStatuslineOutput("claude-opus-4", 1.23, 4.56, 3661, 75.0, "main", false, nil, nil, "testuser", "https://shelltime.xyz", "session-abc123") + + // Should contain OSC8 link wrapping session cost + assert.Contains(s.T(), output, "shelltime.xyz/users/testuser/coding-agent/session/session-abc123") + assert.Contains(s.T(), output, "\033]8;;") + assert.Contains(s.T(), output, "$1.23") +} + +func (s *CCStatuslineTestSuite) TestFormatStatuslineOutput_SessionCostWithoutLink() { + // No userLogin - should not have link + output := formatStatuslineOutput("claude-opus-4", 1.23, 4.56, 3661, 75.0, "main", false, nil, nil, "", "https://shelltime.xyz", "session-abc123") + assert.Contains(s.T(), output, "$1.23") + assert.NotContains(s.T(), output, "coding-agent/session/") + + // No sessionID - should not have link + output = formatStatuslineOutput("claude-opus-4", 1.23, 4.56, 3661, 75.0, "main", false, nil, nil, "testuser", "https://shelltime.xyz", "") + assert.Contains(s.T(), output, "$1.23") + assert.NotContains(s.T(), output, "coding-agent/session/") + + // No webEndpoint - should not have link + output = formatStatuslineOutput("claude-opus-4", 1.23, 4.56, 3661, 75.0, "main", false, nil, nil, "testuser", "", "session-abc123") + assert.Contains(s.T(), output, "$1.23") + assert.NotContains(s.T(), output, "coding-agent/session/") +} + func (s *CCStatuslineTestSuite) TestFormatStatuslineOutput_WithQuota() { fh := 45.0 sd := 23.0 - output := formatStatuslineOutput("claude-opus-4", 1.23, 4.56, 3661, 75.0, "main", false, &fh, &sd, "", "") + output := formatStatuslineOutput("claude-opus-4", 1.23, 4.56, 3661, 75.0, "main", false, &fh, &sd, "", "", "") assert.Contains(s.T(), output, "5h:45%") assert.Contains(s.T(), output, "7d:23%") @@ -339,7 +365,7 @@ func (s *CCStatuslineTestSuite) TestFormatStatuslineOutput_WithQuota() { } func (s *CCStatuslineTestSuite) TestFormatStatuslineOutput_WithoutQuota() { - output := formatStatuslineOutput("claude-opus-4", 1.23, 4.56, 3661, 75.0, "main", false, nil, nil, "", "") + output := formatStatuslineOutput("claude-opus-4", 1.23, 4.56, 3661, 75.0, "main", false, nil, nil, "", "", "") assert.Contains(s.T(), output, "🚦 -") } diff --git a/model/cc_statusline_types.go b/model/cc_statusline_types.go index 0278d47..54fed89 100644 --- a/model/cc_statusline_types.go +++ b/model/cc_statusline_types.go @@ -3,6 +3,7 @@ package model // CCStatuslineInput represents the JSON input from Claude Code statusline type CCStatuslineInput struct { HookEventName string `json:"hook_event_name"` + SessionID string `json:"session_id"` Model CCStatuslineModel `json:"model"` Cost CCStatuslineCost `json:"cost"` ContextWindow CCStatuslineContextWindow `json:"context_window"`