Skip to content

feat: add account balance status bar item (carries #1970)#2257

Open
HUQIANTAO wants to merge 13 commits into
Hmbown:mainfrom
HUQIANTAO:feat/deepseek-balance-carry
Open

feat: add account balance status bar item (carries #1970)#2257
HUQIANTAO wants to merge 13 commits into
Hmbown:mainfrom
HUQIANTAO:feat/deepseek-balance-carry

Conversation

@HUQIANTAO
Copy link
Copy Markdown

@HUQIANTAO HUQIANTAO commented May 27, 2026

Summary

  • Add DeepSeek account balance display in the footer status bar, carried from feat(tui): add balance status item #1970 by @MoriTang
  • Fetches balance from https://api.deepseek.com/user/balance for DeepSeek/DeepSeekCN providers
  • Balance chip is opt-in via /statusline — does not appear in the default footer
  • Zero balance hides the chip entirely

Changes from #1970

  • Rebased onto latest main (resolved conflicts with upstream Tokens status item)
  • Fixed 3 Clippy warnings (collapsible if, useless format!, redundant closure)
  • Fixed footer_balance_spans_empty_when_balance_is_zero test — zero balance now hides the chip
  • Balance placed after Cost in status item ordering per review feedback

Test plan

  • cargo fmt --all -- --check passes
  • cargo clippy --workspace --all-targets --all-features --locked -- -D warnings passes
  • cargo test --workspace --all-features --locked passes (4 pre-existing failures unrelated)
  • All 6 balance unit tests pass
  • Manual test: /statusline toggle Balance, verify chip appears in footer
  • Manual test: switch to non-DeepSeek provider, verify Balance hides

Supersedes #1970

Greptile Summary

Adds a DeepSeek account balance footer chip: fetches from GET /user/balance on startup and after each turn completion, stores the result in a shared Arc<Mutex<Option<BalanceInfo>>>, and renders it in the left status cluster via a new FooterProps::balance field. The chip is opt-in via /statusline and hidden automatically for zero balances and non-DeepSeek providers.

  • crates/tui/src/config.rs — new Balance variant with a forward-compatible TOML deserializer (deser_status_items) that silently skips unknown items so stable and dev builds can share config files.
  • crates/tui/src/tui/ui.rs — three fetch sites (startup one-shot, per-turn refresh with 60 s cooldown, and provider-switch trigger); balance is cleared from the cell when switching away from DeepSeek.
  • crates/tui/src/tui/widgets/footer.rs — two-pass width-tiered layout updated to include balance between model and cost; build_status_line_spans receives a new balance parameter but its name and position are swapped at every call site.

Confidence Score: 4/5

Safe to merge after addressing the unconditional balance fetch, which makes periodic API calls for all DeepSeek users regardless of whether they have opted into the Balance display chip.

The balance API is fetched on startup and after every turn completion for every DeepSeek user, with no guard checking whether the Balance status item is actually enabled. The PR describes the chip as opt-in, so users who have never toggled it on will silently get periodic /user/balance API calls made with their key. Everything else — the Mutex-based shared cell, the 60 s cooldown, provider-switch clearing, the forward-compatible config deserializer, and the unit test coverage — is solid.

The three balance fetch sites in crates/tui/src/tui/ui.rs (startup, turn completion, and provider switch) need a status_items.contains(Balance) guard.

Important Files Changed

Filename Overview
crates/tui/src/tui/ui.rs Adds balance fetch logic (startup, per-turn, per-provider-switch); fetch is not gated by the Balance status-item opt-in, so all DeepSeek users get unconditional API calls.
crates/tui/src/tui/widgets/footer.rs Adds balance span field to FooterProps and a new rendering tier; build_status_line_spans has cost and balance parameter names swapped relative to how every call site passes them.
crates/tui/src/tui/footer_ui.rs Adds footer_balance_spans helper; correctly hides chip when balance cell is None or zero; minor: falls back to $ for all unknown currencies.
crates/tui/src/config.rs Adds Balance variant to StatusItem, forward-compatible deser_status_items helper, and is_available_for provider filter; all well-tested.
crates/tui/src/pricing.rs Adds BalanceResponse/BalanceInfo structs with serde deserialization and total_balance_f64 helper; well covered by unit tests.
crates/tui/src/tui/views/status_picker.rs Adds provider-aware filtering so the Balance toggle only appears for DeepSeek users; also adds scroll support for taller picker lists.
crates/tui/src/tui/ui/tests.rs Adds 8 balance chip unit tests covering zero, CNY, USD, large amounts, and render_footer_from integration; good coverage.

Sequence Diagram

sequenceDiagram
    participant UL as run_event_loop
    participant BG as tokio::spawn (background)
    participant DS as DeepSeek /user/balance
    participant BC as balance_cell (Mutex)
    participant UI as footer_balance_spans

    UL->>UL: "startup: balance_initiated=false & DeepSeek provider"
    UL->>BG: spawn fetch (api_key, base_url)
    BG->>DS: GET /user/balance (Bearer token)
    DS-->>BG: "BalanceResponse { balance_infos }"
    BG->>BC: lock → write BalanceInfo

    UL->>UL: TurnComplete event
    UL->>UL: cooldown_expired?
    UL->>BG: spawn fetch (api_key, base_url)
    BG->>DS: GET /user/balance
    DS-->>BG: BalanceResponse
    BG->>BC: lock → write BalanceInfo

    UL->>UL: SwitchProvider to DeepSeek
    UL->>BG: spawn fetch
    BG->>DS: GET /user/balance
    DS-->>BG: BalanceResponse
    BG->>BC: lock → write BalanceInfo

    UL->>UL: SwitchProvider away from DeepSeek
    UL->>BC: lock → write None (clear balance)

    UI->>BC: lock (read)
    BC-->>UI: "Option<BalanceInfo>"
    UI->>UI: format label (¥/$ + tier rounding)
    UI-->>UL: "Vec<Span> for footer render"
Loading

Comments Outside Diff (2)

  1. crates/tui/src/tui/views/status_picker.rs, line 801 (link)

    P2 Unintentional [✓][x] visual regression

    The checked mark was silently changed from the Unicode checkmark [✓] to a plain ASCII [x]. This isn't listed in the PR description's Clippy fixes and isn't related to the balance feature — it changes the visual appearance of the /statusline picker for all users, making checked rows look less distinct. If intentional (e.g. terminal-compat), it should be called out explicitly.

    Fix in Codex Fix in Claude Code Fix in Cursor

  2. crates/tui/src/tui/ui.rs, line 602-652 (link)

    P1 Balance API called for all DeepSeek users regardless of opt-in

    The PR description explicitly calls the Balance chip "opt-in via /statusline", yet the balance fetch fires unconditionally on startup (here) and after every turn completion — with no check for whether StatusItem::Balance is even present in app.status_items. A DeepSeek user who has never touched /statusline and never enabled the Balance chip will silently get periodic GET /user/balance calls made with their API key on every session start and every turn. Adding app.status_items.contains(&StatusItem::Balance) as an additional guard in all three fetch sites (startup, turn completion, and provider switch) would make the fetch truly opt-in and consistent with the stated design.

    Fix in Codex Fix in Claude Code Fix in Cursor

Fix All in Codex Fix All in Claude Code Fix All in Cursor

Reviews (5): Last reviewed commit: "fix(balance): address review feedback on..." | Re-trigger Greptile

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

Comment thread crates/tui/src/tui/ui.rs
Comment on lines +618 to +621
let currency = match info.currency.as_str() {
"CNY" | "cny" => "¥",
_ => "$",
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Unknown currency codes silently display a $ prefix

Any currency that isn't "CNY" or "cny" — including "USD", "EUR", "KRW", etc. — falls through to the _ => "$" arm. This means a hypothetical future "EUR" balance would show as $42.50 rather than €42.50, which is misleading. Matching "USD" explicitly and falling back to the raw code string is safer.

Suggested change
let currency = match info.currency.as_str() {
"CNY" | "cny" => "¥",
_ => "$",
};
let currency: std::borrow::Cow<'_, str> = match info.currency.as_str() {
"CNY" | "cny" => "¥".into(),
"USD" | "usd" => "$".into(),
other => other.into(),
};

Fix in Codex Fix in Claude Code Fix in Cursor

Hu Qiantao and others added 4 commits May 27, 2026 13:18
Previously the balance chip only appeared after a completed turn.
Now it also fetches:
- On first frame (startup) for DeepSeek/DeepSeekCN providers
- After switching to DeepSeek, and clears when switching away

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Balance (account remaining) is more actionable than session cost,
so it should drop later when the footer is width-constrained.

Tier order: status → cost → balance → model
(was:     status → balance → cost → model)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
When a dev build writes new status item variants (e.g. "balance") to
config.toml, the stable build must not crash with "unknown variant".
Add a tolerant deserializer that filters unrecognized keys via
StatusItem::from_key() and logs a warning.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@Hmbown
Copy link
Copy Markdown
Owner

Hmbown commented May 27, 2026

Independent review (Devin):

Carry fidelity: confirmed clean carry of #1970 — no scope creep beyond the target feature.

Rate-limiting / caching: balance fetches fire on startup (once), on every TurnComplete, and on provider switch. Each fetch is a background tokio::spawn — no per-turn blocking. No debounce/cooldown beyond "fire-and-forget"; a pathological rapid-switch sequence could issue many concurrent calls, though this is unlikely in practice.

Bug — hardcoded base URL: fetch_deepseek_balance always calls https://api.deepseek.com/user/balance, ignoring DEEPSEEK_BASE_URL and config.base_url. Users with a custom proxy or the CN region endpoint will silently get stale/empty balance.

Provider-gating: is_available_for correctly returns false for every non-DeepSeek provider; picker hides the toggle, status bar renders nothing, balance cell is cleared on switch. Graceful failure path returns None on any HTTP/parse error.

Silent visual regression: status_picker changes [✓][x] for checked items — not mentioned in the PR description.

Test coverage: good. 7 new footer_balance_spans tests, 6 new pricing.rs unit tests, is_available_for provider matrix test, tolerant-deser test, and picker balance_excluded_for_non_deepseek_provider. No fetch-path integration test (can't hit live API in CI), but that's expected.

v0.8.48 (PR #2256) overlap: high — both PRs touch config.rs, pricing.rs, app.rs, footer_ui.rs, ui.rs, and commands/config.rs. Rebase or cherry-pick will be needed before merging whichever lands second.

- Use config.deepseek_base_url() instead of hardcoded api.deepseek.com
- Add 60-second debounce (BALANCE_FETCH_COOLDOWN) to prevent rapid
  consecutive balance API calls during provider switches
- Fix [x] → [✓] regression in status_picker

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 27, 2026

Want your agent to iterate on Greptile's feedback? Try greploops.

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.

3 participants