Skip to content

feat: add ExternalUsage segment with provider-based extensibility#104

Open
yearth wants to merge 6 commits intoHaleclipse:masterfrom
yearth:feat/external-usage-segment
Open

feat: add ExternalUsage segment with provider-based extensibility#104
yearth wants to merge 6 commits intoHaleclipse:masterfrom
yearth:feat/external-usage-segment

Conversation

@yearth
Copy link
Copy Markdown

@yearth yearth commented Mar 23, 2026

Background

This PR supersedes #101 and #102. Based on your feedback that we should not create a separate Usage Segment for any single provider, I redesigned the approach around a unified extension mechanism.

Design

A new ExternalUsage segment that dispatches to pluggable UsageProvider implementations:

src/core/segments/external_usage/
├── mod.rs              # ExternalUsageSegment — caching, formatting, orchestration
├── cache.rs            # Shared file-based JSON cache (reusable across providers)
└── providers/
    ├── mod.rs          # UsageProvider trait + provider registry
    └── minimax.rs      # MiniMax coding_plan/remains implementation

The UsageProvider trait is minimal:

pub trait UsageProvider: Send + Sync {
    fn name(&self) -> &str;
    fn fetch(&self, options: &HashMap<String, Value>) -> Option<Vec<UsageMetric>>;
}

Adding a new provider only requires a new file + one line in the registry. No changes to segment core, config types, or UI.

Why not JSONPath / generic field extraction?

A JSONPath-based approach (configuring how to extract fields from arbitrary JSON responses) was considered but rejected for a few reasons:

  1. Different APIs have fundamentally different shapes. MiniMax returns an array of model objects where you need to filter by model name, then compute (total - used) / total. A generic path expression can't express this computation — it would need a mini query language, not just field access.

  2. Complexity vs. benefit tradeoff. A JSONPath extractor would be complex to implement, hard to validate in the TUI config editor, and brittle against API changes. A typed provider struct handles edge cases (zero total, fallback to first model, etc.) naturally.

  3. Providers are rare, not frequent. The number of external quota APIs users would realistically want to monitor is small. Adding a provider is a 50-line Rust file — a reasonable contribution cost.

Configuration (MiniMax example)

{
  "id": "external_usage",
  "enabled": true,
  "options": {
    "provider": "minimax",
    "auth_env": "MINIMAX_API_KEY",
    "cache_duration": 300,
    "timeout": 10
  }
}

Tests

12 unit tests covering cache validity, percentage calculation edge cases (zero total, over-used), and circle icon boundary values. All passing.

test core::segments::external_usage::cache::tests::test_cache_expired ... ok
test core::segments::external_usage::cache::tests::test_cache_invalid_timestamp ... ok
test core::segments::external_usage::cache::tests::test_cache_valid_within_duration ... ok
test core::segments::external_usage::providers::minimax::tests::test_remaining_pct_exhausted ... ok
test core::segments::external_usage::providers::minimax::tests::test_remaining_pct_full ... ok
test core::segments::external_usage::providers::minimax::tests::test_remaining_pct_normal ... ok
test core::segments::external_usage::providers::minimax::tests::test_remaining_pct_over_used ... ok
test core::segments::external_usage::providers::minimax::tests::test_remaining_pct_zero_total ... ok
test core::segments::external_usage::tests::test_circle_icon_boundary ... ok
test core::segments::external_usage::tests::test_circle_icon_empty ... ok
test core::segments::external_usage::tests::test_circle_icon_full ... ok
test core::segments::external_usage::tests::test_circle_icon_mid ... ok

test result: ok. 12 passed; 0 failed

Summary by Sourcery

Introduce an extensible External Usage statusline segment that displays remaining quota metrics from external providers and wire it into configuration, theming, and UI preview.

New Features:

  • Add an ExternalUsage segment type that shows remaining usage percentages with icon-based visualization.
  • Introduce a provider-based abstraction and initial MiniMax provider for fetching external quota data.
  • Add shared default configuration for the ExternalUsage segment and include it in all built-in themes and the settings/editor UI.

Enhancements:

  • Implement a reusable file-based JSON cache for external usage metrics with time-based invalidation.
  • Expose mock ExternalUsage data in the preview component for consistent UI rendering.

Tests:

  • Add unit tests for cache validity handling, circle icon percentage boundaries, and MiniMax remaining-percentage edge cases.

yearth added 2 commits March 23, 2026 11:18
Replaces the per-provider MiniMaxTokenPlan approach with a general-purpose
ExternalUsage segment that dispatches to pluggable UsageProvider impls.

Design:
- `UsageProvider` trait: name() + fetch(options) -> Vec<UsageMetric>
- `ExternalUsageSegment`: handles caching, circle-icon formatting, config
- `MinimaxProvider`: queries MiniMax coding_plan/remains API
- Shared `Cache` utility extracted to cache.rs

Adding a new provider only requires implementing UsageProvider and
registering it in providers/mod.rs — no changes to segment core or types.

Default config (disabled, provider=minimax):
  options.provider = "minimax"
  options.auth_env = "MINIMAX_API_KEY"
  options.cache_duration = 300
  options.timeout = 10

Supersedes Haleclipse#101 and Haleclipse#102 (MiniMaxTokenPlan).
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai bot commented Mar 23, 2026

Reviewer's Guide

Introduce a new extensible ExternalUsage statusline segment that fetches quota/usage metrics via pluggable providers (initially MiniMax), caches responses on disk, and wires the segment through configuration, statusline collection, themes, and the TUI preview/settings flows.

Sequence diagram for ExternalUsageSegment collection with cache and MiniMax provider

sequenceDiagram
    actor User
    participant TUI as TUI_Statusline
    participant Statusline as Statusline_collect_all_segments
    participant Segment as ExternalUsageSegment
    participant Config as Config
    participant Providers as Providers_build
    participant Cache as Cache
    participant Provider as MinimaxProvider
    participant MiniMaxAPI as MiniMax_HTTP_API

    User->>TUI: trigger_statusline_refresh
    TUI->>Statusline: collect_all_segments(input)
    Statusline->>Segment: collect(input)

    Segment->>Config: load()
    Config-->>Segment: Config

    Segment->>Config: find SegmentConfig with id ExternalUsage
    Config-->>Segment: SegmentConfig(options)

    Segment->>Providers: build(provider_name)
    Providers-->>Segment: MinimaxProvider

    Segment->>Cache: new(provider_name)
    Cache-->>Segment: Cache

    Segment->>Cache: load()
    alt cache_entry_exists
        Cache-->>Segment: CacheEntry
        Segment->>Cache: is_valid(CacheEntry, cache_duration)
        alt cache_valid
            Segment->>Segment: cached_to_usage(CacheEntry)
            Segment-->>Segment: metrics_from_cache
        else cache_expired
            Segment->>Provider: fetch(options)
            alt provider_fetch_success
                Provider->>MiniMaxAPI: GET /coding_plan/remains
                MiniMaxAPI-->>Provider: ApiResponse JSON
                Provider-->>Segment: metrics
                Segment->>Cache: save(CacheEntry_from_metrics)
            else provider_fetch_failure
                Segment->>Segment: cached_to_usage(CacheEntry)
                Segment-->>Segment: metrics_from_stale_cache
            end
        end
    else no_cache_entry
        Cache-->>Segment: None
        Segment->>Provider: fetch(options)
        alt provider_fetch_success
            Provider->>MiniMaxAPI: GET /coding_plan/remains
            MiniMaxAPI-->>Provider: ApiResponse JSON
            Provider-->>Segment: metrics
            Segment->>Cache: save(CacheEntry_from_metrics)
        else provider_fetch_failure
            Segment-->>Statusline: None
            Statusline-->>TUI: omit_external_usage_segment
            TUI-->>User: statusline_without_external_usage
        end
    end

    Segment->>Segment: format primary string and metadata
    Segment-->>Statusline: SegmentData
    Statusline-->>TUI: aggregated_segments
    TUI-->>User: statusline_with_external_usage
Loading

Class diagram for ExternalUsage provider-based extensibility

classDiagram
    class Segment {
        <<trait>>
        collect(input~InputData~) SegmentData
        id() SegmentId
    }

    class ExternalUsageSegment {
        +new() ExternalUsageSegment
        +collect(input~InputData~) SegmentData
        +id() SegmentId
        -get_circle_icon(remaining_pct~u32~) &str
    }

    class Cache {
        -path PathBuf
        +new(provider_name~&str~) Cache
        +load() CacheEntry
        +save(entry~CacheEntry~) void
        +is_valid(entry~CacheEntry~, cache_duration_secs~u64~) bool
    }

    class CacheEntry {
        +metrics Vec~CachedMetric~
        +cached_at String
    }

    class CachedMetric {
        +label String
        +remaining_pct u32
    }

    class UsageMetric {
        +label String
        +remaining_pct u32
    }

    class UsageProvider {
        <<trait>>
        +name() &str
        +fetch(options~HashMap~String,Value~~) Vec~UsageMetric~
    }

    class MinimaxProvider {
        +name() &str
        +fetch(options~HashMap~String,Value~~) Vec~UsageMetric~
        -fetch_api(api_key~&str~, timeout_secs~u64~) ApiResponse
        -remaining_pct(usage~u64~, total~u64~) u32
    }

    class ApiResponse {
        +model_remains Vec~ModelRemain~
    }

    class ModelRemain {
        +model_name String
        +current_interval_total_count u64
        +current_interval_usage_count u64
        +current_weekly_total_count u64
        +current_weekly_usage_count u64
    }

    class ProvidersModule {
        +build(provider_name~&str~) UsageProvider
    }

    class Config {
        +segments Vec~SegmentConfig~
        +load() Config
    }

    class SegmentConfig {
        +id SegmentId
        +enabled bool
        +options HashMap~String,Value~
    }

    class ThemePresets {
        +default_external_usage_segment() SegmentConfig
    }

    class Statusline {
        +collect_all_segments(input~InputData~) Vec~SegmentData~
    }

    Segment <|.. ExternalUsageSegment
    UsageProvider <|.. MinimaxProvider

    ExternalUsageSegment --> Cache : uses
    ExternalUsageSegment --> UsageProvider : uses
    ExternalUsageSegment --> UsageMetric : formats
    ExternalUsageSegment --> Config : loads
    ExternalUsageSegment --> SegmentConfig : reads options
    ExternalUsageSegment --> ProvidersModule : build

    CacheEntry --> CachedMetric : contains
    Cache --> CacheEntry : reads_writes

    MinimaxProvider --> ApiResponse : parses
    ApiResponse --> ModelRemain : contains

    ThemePresets --> SegmentConfig : builds_default
    Statusline --> ExternalUsageSegment : dispatches
    Config --> SegmentConfig : contains
    SegmentConfig --> SegmentId : identifies
Loading

File-Level Changes

Change Details Files
Add ExternalUsage segment core logic with caching, provider dispatch, and display formatting.
  • Define ExternalUsageSegment implementing Segment, including id, configuration lookup, provider resolution, cache usage, and SegmentData construction.
  • Implement remaining percentage-to-circle-slice icon mapping for external usage, with tests for boundaries and typical values.
  • Convert between cached JSON representation and in-memory UsageMetric structures for reading/writing cache entries.
src/core/segments/external_usage/mod.rs
Implement a reusable file-based JSON cache for external usage metrics.
  • Introduce CacheEntry and CachedMetric types with serde serialization for storing metrics plus timestamp.
  • Implement Cache with provider-specific cache file path resolution, load/save helpers, and timestamp-based validity checks.
  • Add unit tests verifying cache validity within duration, expiration behavior, and handling of invalid timestamps.
src/core/segments/external_usage/cache.rs
Add a pluggable provider abstraction and initial MiniMax provider implementation for fetching external usage.
  • Define UsageMetric and UsageProvider trait plus a simple provider factory for name-based dispatch.
  • Implement MinimaxProvider that reads auth and options, calls the MiniMax coding_plan/remains API with timeout, and maps API response into UsageMetric values.
  • Add a remaining_pct helper that computes remaining quota from usage/total with edge-case handling and unit tests for normal, full, exhausted, zero-total, and over-used cases.
src/core/segments/external_usage/providers/mod.rs
src/core/segments/external_usage/providers/minimax.rs
Integrate ExternalUsage segment into the statusline collection pipeline and config types.
  • Extend SegmentId enum to include ExternalUsage.
  • Export ExternalUsageSegment from the segments module and route SegmentId::ExternalUsage in collect_all_segments to ExternalUsageSegment::collect.
  • Ensure ExternalUsageSegment uses the global Config::load and its own SegmentId to obtain options for provider selection, auth, cache_duration, and timeout.
src/config/types.rs
src/core/segments/mod.rs
src/core/statusline.rs
Provide shared default configuration and theme wiring for ExternalUsage across all built-in themes.
  • Add default_external_usage_segment helper returning a disabled ExternalUsage SegmentConfig with default icon/color and minimax-based options (auth env, cache duration, timeout).
  • Add external_usage_segment helper to each theme module that delegates to presets::default_external_usage_segment.
  • Include external_usage_segment in the segment lists for all built-in theme presets so it appears consistently when enabled.
src/ui/themes/presets.rs
src/ui/themes/theme_cometix.rs
src/ui/themes/theme_default.rs
src/ui/themes/theme_gruvbox.rs
src/ui/themes/theme_minimal.rs
src/ui/themes/theme_nord.rs
src/ui/themes/theme_powerline_dark.rs
src/ui/themes/theme_powerline_light.rs
src/ui/themes/theme_powerline_rose_pine.rs
src/ui/themes/theme_powerline_tokyo_night.rs
Expose ExternalUsage in the TUI preview, segment list, and settings UI.
  • Add a mock SegmentData entry for SegmentId::ExternalUsage in PreviewComponent so users can preview the segment (including metadata keys for remaining percentages).
  • Update segment name mappings in App status messages, SegmentListComponent, and SettingsComponent to recognize and label ExternalUsage as "External Usage".
  • Ensure ExternalUsage participates in the general segment toggling and configuration flows in the TUI.
src/ui/components/preview.rs
src/ui/app.rs
src/ui/components/segment_list.rs
src/ui/components/settings.rs

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • The external_usage_segment definitions in all the theme_* files are essentially identical (including the options map); consider extracting a shared helper or default SegmentConfig builder so defaults stay in sync and are easier to update.
  • In ExternalUsageSegment::collect you reload the global Config and search for the segment on every call; if the existing segment framework allows it, consider passing the segment’s options via InputData or caching the resolved config to avoid repeated disk reads on each render.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `external_usage_segment` definitions in all the theme_* files are essentially identical (including the options map); consider extracting a shared helper or default `SegmentConfig` builder so defaults stay in sync and are easier to update.
- In `ExternalUsageSegment::collect` you reload the global `Config` and search for the segment on every call; if the existing segment framework allows it, consider passing the segment’s options via `InputData` or caching the resolved config to avoid repeated disk reads on each render.

## Individual Comments

### Comment 1
<location path="src/core/segments/external_usage/mod.rs" line_range="60-63" />
<code_context>
+        let provider = providers::build(provider_name)?;
+        let cache = Cache::new(provider_name)?;
+
+        // 尝试使用有效缓存
+        let metrics = if let Some(entry) = cache.load() {
+            if Cache::is_valid(&entry, cache_duration) {
+                entry.metrics.iter().map(|m| providers::UsageMetric {
+                    label: m.label.clone(),
+                    remaining_pct: m.remaining_pct,
</code_context>
<issue_to_address>
**suggestion:** There is duplicated mapping logic between `CachedMetric` and `UsageMetric` that could be centralized.

The `CachedMetric``UsageMetric` conversion is duplicated in both the valid-cache path and the “fetch failed, fall back to cache” path. Please extract a helper (e.g. `fn cached_to_usage(entry: &CacheEntry) -> Vec<UsageMetric>`) so changes to the cached or metric structure only need to be made in one place.

Suggested implementation:

```rust
        fn cached_to_usage(entry: &CacheEntry) -> Vec<providers::UsageMetric> {
            entry
                .metrics
                .iter()
                .map(|m| providers::UsageMetric {
                    label: m.label.clone(),
                    remaining_pct: m.remaining_pct,
                })
                .collect()
        }

        let cache_duration = options

```

```rust
        // 尝试使用有效缓存
        let metrics = if let Some(entry) = cache.load() {
            if Cache::is_valid(&entry, cache_duration) {
                cached_to_usage(&entry)

```

There is likely a second occurrence of the same mapping logic in the “fetch failed, fall back to cache” path, something like:

```rust
entry.metrics.iter().map(|m| providers::UsageMetric {
    label: m.label.clone(),
    remaining_pct: m.remaining_pct,
}).collect()
```

That should also be replaced with:

```rust
cached_to_usage(&entry)
```

to fully centralize the conversion logic.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

…licate theme configs, translate comments to English
@yearth
Copy link
Copy Markdown
Author

yearth commented Mar 23, 2026

@sourcery-ai review

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • ExternalUsageSegment::collect() reloads the entire Config from disk on every call; consider passing the config (or just the segment options) in via InputData or a shared context to avoid repeated IO on each render.
  • get_circle_icon() allocates a new String on every call even though the icons are static; returning &'static str (and only allocating at the formatting/join callsite) would reduce per-render allocations.
  • UsageProvider::fetch() returns Option, which makes it hard to distinguish different failure modes (network vs auth vs parse); using Result with a concrete error type (or at least an error string) would make provider failures easier to diagnose and handle.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- ExternalUsageSegment::collect() reloads the entire Config from disk on every call; consider passing the config (or just the segment options) in via InputData or a shared context to avoid repeated IO on each render.
- get_circle_icon() allocates a new String on every call even though the icons are static; returning &'static str (and only allocating at the formatting/join callsite) would reduce per-render allocations.
- UsageProvider::fetch() returns Option, which makes it hard to distinguish different failure modes (network vs auth vs parse); using Result with a concrete error type (or at least an error string) would make provider failures easier to diagnose and handle.

## Individual Comments

### Comment 1
<location path="src/core/segments/external_usage/providers/minimax.rs" line_range="98-104" />
<code_context>
+    }
+}
+
+fn remaining_pct(usage: u64, total: u64) -> u32 {
+    if total == 0 {
+        return 100;
+    }
+    let used_pct = (usage as f64 / total as f64 * 100.0) as u32;
+    100u32.saturating_sub(used_pct)
+}
</code_context>
<issue_to_address>
**suggestion (bug_risk):** The percentage calculation uses floating point where integer math could be simpler and avoid precision issues.

`used_pct` is computed via `f64` and cast back to `u32`, which can lose precision for large `usage`/`total` values. You can keep this in integer math instead, e.g. `let used_pct = usage.saturating_mul(100) / total;`, which avoids floating point and naturally clamps on overflow. With `100u32.saturating_sub(used_pct)` this still behaves correctly when `usage > total`.

```suggestion
fn remaining_pct(usage: u64, total: u64) -> u32 {
    if total == 0 {
        return 100;
    }
    let used_pct = (usage.saturating_mul(100) / total) as u32;
    100u32.saturating_sub(used_pct)
}
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@yearth
Copy link
Copy Markdown
Author

yearth commented Mar 23, 2026

Thanks for the updated review. Addressed the get_circle_icon allocation issue — it now returns &'static str.

On the other two points:

collect() reloading Config on every call — This is a real concern, but the current segment framework doesn't pass options through InputData, so there's no way to receive them without reading the config. This is the same pattern used by UsageSegment in the existing codebase. Happy to address this if/when the framework adds option passing to InputData, or as a follow-up.

UsageProvider::fetch() returning Option instead of Result — Agreed that Result would give better error visibility. Changing the trait signature would require updating all provider implementations and call sites, which feels like a larger refactor than fits in this PR. Happy to track it as a follow-up issue if that works.

@yearth
Copy link
Copy Markdown
Author

yearth commented Mar 23, 2026

@sourcery-ai review

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • ExternalUsageSegment::collect() calls Config::load() on every render, which adds disk I/O to the hot path; consider passing the config via InputData (as other segments do) or otherwise caching it to avoid repeatedly loading from disk.
  • The cache file is keyed only by provider_name, so changing options like auth_env or preferred_model while keeping the same provider will continue using the old cache; consider incorporating a hash of the relevant options into the cache key or invalidating the cache when these options change.
  • The provider trait module (providers/mod.rs) uses Chinese comments in an otherwise English codebase; consider standardizing comment language for consistency with the rest of the project.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- ExternalUsageSegment::collect() calls Config::load() on every render, which adds disk I/O to the hot path; consider passing the config via InputData (as other segments do) or otherwise caching it to avoid repeatedly loading from disk.
- The cache file is keyed only by provider_name, so changing options like auth_env or preferred_model while keeping the same provider will continue using the old cache; consider incorporating a hash of the relevant options into the cache key or invalidating the cache when these options change.
- The provider trait module (providers/mod.rs) uses Chinese comments in an otherwise English codebase; consider standardizing comment language for consistency with the rest of the project.

## Individual Comments

### Comment 1
<location path="src/core/segments/external_usage/cache.rs" line_range="47-53" />
<code_context>
+        }
+    }
+
+    pub fn is_valid(entry: &CacheEntry, cache_duration_secs: u64) -> bool {
+        DateTime::parse_from_rfc3339(&entry.cached_at)
+            .map(|cached_at| {
+                let elapsed = Utc::now().signed_duration_since(cached_at.with_timezone(&Utc));
+                elapsed.num_seconds() < cache_duration_secs as i64
+            })
+            .unwrap_or(false)
+    }
+}
</code_context>
<issue_to_address>
**issue (bug_risk):** Future-dated cache entries are treated as valid, which may not be intended.

If `cached_at` is in the future (e.g. from clock skew), `elapsed` is negative and the comparison will always treat the entry as valid. To avoid effectively never-expiring entries, consider rejecting future timestamps (e.g. return `false` when `elapsed.num_seconds().is_negative()`) or clamp negative elapsed values to zero before comparing.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Copy link
Copy Markdown

@SourceryAI SourceryAI left a comment

Choose a reason for hiding this comment

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

Hey - I've left some high level feedback:

  • The cache path in Cache::new is hardcoded to ~/.claude/ccline/...; consider reusing whatever central config/cache directory helper the rest of the app uses so this segment follows the same location and any future directory layout changes automatically apply here.
  • In ExternalUsageSegment::collect, metadata keys are derived directly from label (e.g., format!("{}_remaining_pct", m.label)); it would be safer to normalize or restrict label characters before using them as keys to avoid surprises if a provider ever returns labels with spaces or non-ASCII characters.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The cache path in `Cache::new` is hardcoded to `~/.claude/ccline/...`; consider reusing whatever central config/cache directory helper the rest of the app uses so this segment follows the same location and any future directory layout changes automatically apply here.
- In `ExternalUsageSegment::collect`, metadata keys are derived directly from `label` (e.g., `format!("{}_remaining_pct", m.label)`); it would be safer to normalize or restrict label characters before using them as keys to avoid surprises if a provider ever returns labels with spaces or non-ASCII characters.

Hi @yearth! 👋

Thanks for trying out Sourcery by commenting with @sourcery-ai review! 🚀

Install the sourcery-ai bot to get automatic code reviews on every pull request ✨

Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@yearth
Copy link
Copy Markdown
Author

yearth commented Mar 23, 2026

Addressed the latest review:

  • Future-dated cache entries: now rejected (elapsed >= 0 check added). Added a test to cover this case.
  • Metadata key normalization: label characters are now sanitized to ASCII alphanumeric before use as keys.
  • Chinese comments in providers/mod.rs: translated to English.

On the hardcoded cache path: the rest of the codebase (loader.rs, updater.rs, models.rs, presets.rs) all construct ~/.claude/ccline/ directly via dirs::home_dir().join(".claude").join("ccline") — there is no shared helper. The cache path follows the same pattern, so this is consistent with the existing convention.

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.

2 participants