Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 71 additions & 2 deletions src/segments/claude.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type Claude struct {

// ClaudeData represents the parsed Claude JSON data
type ClaudeData struct {
RateLimits *ClaudeRateLimits `json:"rate_limits"`
Model ClaudeModel `json:"model"`
Workspace ClaudeWorkspace `json:"workspace"`
SessionID string `json:"session_id"`
Expand All @@ -35,10 +36,35 @@ type ClaudeWorkspace struct {
ProjectDir string `json:"project_dir"`
}

// DurationMS is a duration in milliseconds that formats as "Xm Ys".
type DurationMS int64

func (d DurationMS) String() string {
totalSeconds := int64(d) / 1000
minutes := totalSeconds / 60
seconds := totalSeconds % 60
return fmt.Sprintf("%dm %ds", minutes, seconds)
}

// ClaudeCost represents cost and duration information
type ClaudeCost struct {
TotalCostUSD float64 `json:"total_cost_usd"`
TotalDurationMS int64 `json:"total_duration_ms"`
TotalCostUSD float64 `json:"total_cost_usd"`
TotalDurationMS DurationMS `json:"total_duration_ms"`
TotalAPIDurationMS DurationMS `json:"total_api_duration_ms"`
TotalLinesAdded int `json:"total_lines_added"`
TotalLinesRemoved int `json:"total_lines_removed"`
}

// ClaudeRateLimitWindow represents a single rate limit time window.
type ClaudeRateLimitWindow struct {
UsedPercentage *float64 `json:"used_percentage"`
ResetsAt *int64 `json:"resets_at"`
}

// ClaudeRateLimits represents rate limit information across time windows.
type ClaudeRateLimits struct {
FiveHour *ClaudeRateLimitWindow `json:"five_hour"`
SevenDay *ClaudeRateLimitWindow `json:"seven_day"`
}

// ClaudeContextWindow represents token usage information
Expand Down Expand Up @@ -144,6 +170,49 @@ func (c *Claude) FormattedCost() string {
return fmt.Sprintf("$%.2f", c.Cost.TotalCostUSD)
}

// FormattedDuration returns total session duration as "Xm Ys".
func (c *Claude) FormattedDuration() string {
return c.Cost.TotalDurationMS.String()
}

// FormattedAPIDuration returns API wait time as "Xm Ys".
func (c *Claude) FormattedAPIDuration() string {
return c.Cost.TotalAPIDurationMS.String()
}

// rateLimitPercentage extracts a percentage from a rate limit window with nil-safety.
func rateLimitPercentage(limits *ClaudeRateLimits, window func(*ClaudeRateLimits) *ClaudeRateLimitWindow) text.Percentage {
if limits == nil {
return 0
}

w := window(limits)
if w == nil || w.UsedPercentage == nil {
return 0
}

percent := int(*w.UsedPercentage + 0.5)
if percent > 100 {
return 100
}

return text.Percentage(percent)
}

// FiveHourUsage returns the 5-hour rolling window rate limit usage as a Percentage.
func (c *Claude) FiveHourUsage() text.Percentage {
return rateLimitPercentage(c.RateLimits, func(r *ClaudeRateLimits) *ClaudeRateLimitWindow {
return r.FiveHour
})
}

// SevenDayUsage returns the 7-day window rate limit usage as a Percentage.
func (c *Claude) SevenDayUsage() text.Percentage {
return rateLimitPercentage(c.RateLimits, func(r *ClaudeRateLimits) *ClaudeRateLimitWindow {
return r.SevenDay
})
}

// FormattedTokens returns a human-readable string of current context tokens.
// Uses CurrentUsage (which represents actual context and resets on compact/clear)
// with fallback to total tokens for backwards compatibility.
Expand Down
130 changes: 128 additions & 2 deletions src/segments/claude_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,11 @@ func TestClaudeSegment(t *testing.T) {
ProjectDir: "/repo",
},
Cost: ClaudeCost{
TotalCostUSD: 0.01,
TotalDurationMS: 45000,
TotalCostUSD: 0.01,
TotalDurationMS: 45000,
TotalAPIDurationMS: 30000,
TotalLinesAdded: 156,
TotalLinesRemoved: 23,
},
ContextWindow: ClaudeContextWindow{
TotalInputTokens: 15234,
Expand All @@ -49,6 +52,16 @@ func TestClaudeSegment(t *testing.T) {
OutputTokens: 1200,
},
},
RateLimits: &ClaudeRateLimits{
FiveHour: &ClaudeRateLimitWindow{
UsedPercentage: new(24.5),
ResetsAt: new(int64(1711180800)),
},
SevenDay: &ClaudeRateLimitWindow{
UsedPercentage: new(45.0),
ResetsAt: new(int64(1711612800)),
},
},
},
ExpectedEnabled: true,
ExpectedModel: "Opus",
Expand Down Expand Up @@ -268,6 +281,43 @@ func TestClaudeFormattedCost(t *testing.T) {
}
}

func TestClaudeFormattedDuration(t *testing.T) {
cases := []struct {
Case string
Expected string
MS DurationMS
}{
{Case: "Zero", MS: 0, Expected: "0m 0s"},
{Case: "Seconds only", MS: 45000, Expected: "0m 45s"},
{Case: "Minutes and seconds", MS: 125000, Expected: "2m 5s"},
{Case: "Exact minute", MS: 60000, Expected: "1m 0s"},
}

for _, tc := range cases {
claude := &Claude{}
claude.Cost.TotalDurationMS = tc.MS
assert.Equal(t, tc.Expected, claude.FormattedDuration(), tc.Case)
}
}

func TestClaudeFormattedAPIDuration(t *testing.T) {
cases := []struct {
Case string
Expected string
MS DurationMS
}{
{Case: "Zero", MS: 0, Expected: "0m 0s"},
{Case: "Seconds only", MS: 30000, Expected: "0m 30s"},
{Case: "Minutes and seconds", MS: 90000, Expected: "1m 30s"},
}

for _, tc := range cases {
claude := &Claude{}
claude.Cost.TotalAPIDurationMS = tc.MS
assert.Equal(t, tc.Expected, claude.FormattedAPIDuration(), tc.Case)
}
}

func TestClaudeFormattedTokens(t *testing.T) {
cases := []struct {
Case string
Expand Down Expand Up @@ -366,3 +416,79 @@ func TestClaudeFormattedTokens(t *testing.T) {
assert.Equal(t, tc.ExpectedFormat, formatted, tc.Case)
}
}

func TestClaudeRateLimitUsage(t *testing.T) {
cases := []struct {
RateLimits *ClaudeRateLimits
Case string
ExpectedFive text.Percentage
ExpectedSeven text.Percentage
}{
{
Case: "Nil RateLimits",
RateLimits: nil,
ExpectedFive: 0,
ExpectedSeven: 0,
},
{
Case: "Nil FiveHour window",
RateLimits: &ClaudeRateLimits{
SevenDay: &ClaudeRateLimitWindow{UsedPercentage: new(50.0)},
},
ExpectedFive: 0,
ExpectedSeven: 50,
},
{
Case: "Nil SevenDay window",
RateLimits: &ClaudeRateLimits{
FiveHour: &ClaudeRateLimitWindow{UsedPercentage: new(25.0)},
},
ExpectedFive: 25,
ExpectedSeven: 0,
},
{
Case: "Nil UsedPercentage",
RateLimits: &ClaudeRateLimits{
FiveHour: &ClaudeRateLimitWindow{UsedPercentage: nil},
SevenDay: &ClaudeRateLimitWindow{UsedPercentage: nil},
},
ExpectedFive: 0,
ExpectedSeven: 0,
},
{
Case: "Valid percentages",
RateLimits: &ClaudeRateLimits{
FiveHour: &ClaudeRateLimitWindow{UsedPercentage: new(42.7)},
SevenDay: &ClaudeRateLimitWindow{UsedPercentage: new(75.3)},
},
ExpectedFive: 43,
ExpectedSeven: 75,
},
{
Case: "Value over 100 capped",
RateLimits: &ClaudeRateLimits{
FiveHour: &ClaudeRateLimitWindow{UsedPercentage: new(150.0)},
SevenDay: &ClaudeRateLimitWindow{UsedPercentage: new(200.0)},
},
ExpectedFive: 100,
ExpectedSeven: 100,
},
{
Case: "Zero percentages",
RateLimits: &ClaudeRateLimits{
FiveHour: &ClaudeRateLimitWindow{UsedPercentage: new(0.0)},
SevenDay: &ClaudeRateLimitWindow{UsedPercentage: new(0.0)},
},
ExpectedFive: 0,
ExpectedSeven: 0,
},
}

for _, tc := range cases {
claude := &Claude{}
claude.RateLimits = tc.RateLimits

assert.Equal(t, tc.ExpectedFive, claude.FiveHourUsage(), tc.Case+" (FiveHour)")
assert.Equal(t, tc.ExpectedSeven, claude.SevenDayUsage(), tc.Case+" (SevenDay)")
}
}
27 changes: 25 additions & 2 deletions website/docs/segments/cli/claude.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ import Config from "@site/src/components/Config.js";
| `.TokenUsagePercent` | `Percentage` | Percentage of context window used (0-100) |
| `.FormattedCost` | `string` | Formatted cost string (e.g., "$0.15" or "$0.0012") |
| `.FormattedTokens` | `string` | Human-readable token count (e.g., "1.2K", "15.3M") |
| `.FormattedDuration` | `string` | Total session duration (e.g., "2m 5s") |
| `.FormattedAPIDuration` | `string` | API wait time (e.g., "0m 45s") |
| `.FiveHourUsage` | `Percentage` | 5-hour rolling rate limit usage (0-100) |
| `.SevenDayUsage` | `Percentage` | 7-day rate limit usage (0-100) |

#### Model Properties

Expand All @@ -70,8 +74,11 @@ import Config from "@site/src/components/Config.js";

| Name | Type | Description |
| ------------------ | --------- | -------------------------------------- |
| `.TotalCostUSD` | `float64` | Total cost in USD |
| `.TotalDurationMS` | `int64` | Total session duration in milliseconds |
| `.TotalCostUSD` | `float64` | Total cost in USD |
| `.TotalDurationMS` | `DurationMS` | Total session duration in milliseconds (formats as "Xm Ys") |
| `.TotalAPIDurationMS` | `DurationMS` | Time spent waiting for API responses (formats as "Xm Ys") |
| `.TotalLinesAdded` | `int` | Lines of code added in the session |
| `.TotalLinesRemoved` | `int` | Lines of code removed in the session |

#### ContextWindow Properties

Expand All @@ -89,6 +96,22 @@ import Config from "@site/src/components/Config.js";
| `.InputTokens` | `int` | Input tokens for the current message |
| `.OutputTokens` | `int` | Output tokens for the current message |

#### RateLimits Properties

Available when Claude Code provides rate limit data (Pro/Max subscribers). Access via `.RateLimits`.

| Name | Type | Description |
| ----------- | ----------------- | ------------------------ |
| `.FiveHour` | `RateLimitWindow` | 5-hour rolling window |
| `.SevenDay` | `RateLimitWindow` | 7-day rolling window |

#### RateLimitWindow Properties

| Name | Type | Description |
| ----------------- | ---------- | ---------------------------------------- |
| `.UsedPercentage` | `*float64` | Usage percentage (0-100), nil if unknown |
| `.ResetsAt` | `*int64` | Unix epoch seconds when window resets |

### Percentage Methods

The `TokenUsagePercent` property is a `Percentage` type that provides additional functionality:
Expand Down