Skip to content

Add Claude Code usage tracking - #1

Merged
MartianGreed merged 2 commits into
mainfrom
feat/usage
Jan 20, 2026
Merged

MartianGreed merged 2 commits into
mainfrom
feat/usage

Conversation

@MartianGreed

Copy link
Copy Markdown
Owner

Summary

  • Parse local Claude Code JSONL session files (~/.claude/projects/<project>/*.jsonl) to display usage metrics
  • Show token counts (input/output) and estimated costs in the TUI
  • Support pricing for Opus, Sonnet, and Haiku models
  • Display usage in both preview status line and session list

Changes

  • New internal/usage/ package with types, parser, pricing, and watcher
  • Modified internal/daemon/monitor.go to track usage per session
  • Modified internal/tui/view.go to render usage info

Token Pricing (per 1M tokens)

Model Input Output Cache Write Cache Read
Opus 4 $15 $75 $18.75 $1.50
Sonnet 4 $3 $15 $3.75 $0.30
Haiku 3.5 $0.80 $4 $1 $0.08

Test plan

  • Run ccmanager with active Claude Code session
  • Send messages in Claude Code
  • Verify tokens/cost update in ccmanager TUI
  • Test with multiple sessions

🤖 Generated with Claude Code

@MartianGreed

Copy link
Copy Markdown
Owner Author

@greptile

@greptile-apps

greptile-apps Bot commented Jan 20, 2026 •

Copy link
Copy Markdown

Greptile Summary

  • Adds Claude Code usage tracking by parsing local JSONL session files to display token counts and cost estimates in the TUI
  • Introduces GitHub Actions CI/CD workflows for automated testing and releases with multi-platform binary building
  • Integrates usage monitoring into existing session management system with concurrent file watching and real-time TUI updates

Important Files Changed

Filename Overview
.github/workflows/ci.yml New CI workflow with critical Go version issue (1.25.0 doesn't exist) causing all jobs to fail
.github/workflows/release.yml New release workflow with same Go version issue that will prevent successful builds
internal/usage/parser.go New usage parser with tail parsing optimization but has potential memory and error handling issues
internal/daemon/monitor.go Integrates usage tracking into session monitoring with proper mutex protection and lifecycle management
internal/tui/view.go Adds cost display to session list and preview panels with proper formatting functions

Confidence score: 2/5

  • This PR has critical CI/CD issues that will prevent successful builds and releases due to invalid Go version specification
  • Score lowered due to Go1.25.0 references in CI workflows (non-existent version), memory management concerns in parser tail logic, and potential race conditions in usage file watching
  • Pay close attention to CI workflow files and the usage parser implementation, particularly the tail parsing buffer management and concurrent file access patterns

Sequence Diagram

sequenceDiagram
    participant User
    participant Monitor
    participant UsageWatcher
    participant TmuxClient
    participant ClaudeDetector
    participant UsageParser
    participant FileSystem

    User->>Monitor: "Start monitoring"
    Monitor->>UsageWatcher: "Start()"
    Monitor->>Monitor: "Start poll loop"
    
    loop Every poll interval
        Monitor->>TmuxClient: "ListSessions()"
        TmuxClient-->>Monitor: "tmux sessions"
        
        loop For each tmux session
            Monitor->>TmuxClient: "CapturePane()"
            TmuxClient-->>Monitor: "pane content"
            Monitor->>ClaudeDetector: "IsClaudeSession(content)"
            ClaudeDetector-->>Monitor: "true/false"
            
            alt Claude session found
                Monitor->>TmuxClient: "GetSessionPath()"
                TmuxClient-->>Monitor: "working directory"
                Monitor->>UsageWatcher: "WatchSession(name, workingDir)"
                UsageWatcher->>FileSystem: "FindProjectDir(workingDir)"
                FileSystem-->>UsageWatcher: "Claude project path"
                UsageWatcher->>FileSystem: "FindSessionFiles()"
                FileSystem-->>UsageWatcher: "JSONL files"
            end
        end
        
        alt Usage update cycle
            Monitor->>Monitor: "updateUsage()"
            loop For each session
                Monitor->>UsageParser: "GetMostRecentSession(workingDir)"
                UsageParser->>FileSystem: "FindActiveSessionFile()"
                FileSystem-->>UsageParser: "latest JSONL file"
                UsageParser->>FileSystem: "ParseSessionFile()"
                FileSystem-->>UsageParser: "token usage data"
                UsageParser->>UsageParser: "CalculateCost()"
                UsageParser-->>Monitor: "SessionUsage with cost"
            end
        end
    end
    
    Monitor-->>User: "Display usage in TUI"
Loading

@greptile-apps greptile-apps Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional Comments (1)

  1. .github/workflows/release.yml, line 149 (link)

    syntax: Same Go version issue - 1.25.0 doesn't exist yet

10 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

Comment thread .github/workflows/ci.yml
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.25.0"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

syntax: Go 1.25.0 has not been released. Latest stable is 1.23.x. Change to "1.23" or "stable"

Suggested change
go-version: "1.25.0"
go-version: "1.23"
Prompt To Fix With AI
This is a comment left during a code review.
Path: .github/workflows/ci.yml
Line: 19:19

Comment:
**syntax:** Go 1.25.0 has not been released. Latest stable is 1.23.x. Change to `"1.23"` or `"stable"`

```suggestion
          go-version: "1.23"
```

How can I resolve this? If you propose a fix, please make it concise.

Comment thread internal/usage/parser.go
Comment on lines +76 to +140
func ParseSessionFileTail(path string, tailBytes int64) (*SessionUsage, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()

info, err := file.Stat()
if err != nil {
return nil, err
}

// If file is small enough, parse the whole thing
if info.Size() <= tailBytes {
return ParseSessionFile(path)
}

// Seek to near the end
_, err = file.Seek(-tailBytes, 2)
if err != nil {
return nil, err
}

usage := &SessionUsage{
SessionID: filepath.Base(strings.TrimSuffix(path, ".jsonl")),
ProjectPath: filepath.Dir(path),
LastUpdated: time.Now(),
}

scanner := bufio.NewScanner(file)
buf := make([]byte, 0, 1024*1024)
scanner.Buffer(buf, 10*1024*1024)

first := true
for scanner.Scan() {
line := scanner.Bytes()
// Skip partial first line when reading from middle
if first {
first = false
continue
}
if len(line) == 0 {
continue
}

var msg jsonlMessage
if err := json.Unmarshal(line, &msg); err != nil {
continue
}

if msg.Type == "assistant" {
usage.TotalUsage.Add(TokenUsage{
InputTokens: msg.Message.Usage.InputTokens,
OutputTokens: msg.Message.Usage.OutputTokens,
CacheCreationInputTokens: msg.Message.Usage.CacheCreationInputTokens,
CacheReadInputTokens: msg.Message.Usage.CacheReadInputTokens,
})
if msg.Message.Model != "" {
usage.Model = msg.Message.Model
}
}
}

return usage, nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: ParseSessionFileTail only counts tokens from the tail portion of the file, missing tokens from earlier in the session. This will underreport usage for long sessions.

If you want to use tail for efficiency, you need to parse the entire file at least once and cache it, or document that this function gives partial results. Currently it's misleading because it returns *SessionUsage which implies complete session data.

Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/usage/parser.go
Line: 76:140

Comment:
**logic:** `ParseSessionFileTail` only counts tokens from the tail portion of the file, missing tokens from earlier in the session. This will underreport usage for long sessions.

If you want to use tail for efficiency, you need to parse the entire file at least once and cache it, or document that this function gives partial results. Currently it's misleading because it returns `*SessionUsage` which implies complete session data.

How can I resolve this? If you propose a fix, please make it concise.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

10 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment thread internal/usage/watcher.go
Comment on lines +88 to +92
if _, exists := w.sessions[sessionName]; !exists {
w.sessions[sessionName] = &sessionWatch{
sessionFile: mostRecent,
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: If a session already exists, this silently ignores the new workingDir and keeps watching the old file. This could cause missed updates if the session moves to a different project directory. Should existing sessions be updated to watch the new working directory, or is it intentional to keep watching the original file?

Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/usage/watcher.go
Line: 88:92

Comment:
**logic:** If a session already exists, this silently ignores the new `workingDir` and keeps watching the old file. This could cause missed updates if the session moves to a different project directory. Should existing sessions be updated to watch the new working directory, or is it intentional to keep watching the original file?

How can I resolve this? If you propose a fix, please make it concise.

@MartianGreed
MartianGreed merged commit 1744a9c into main Jan 20, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant