Skip to content

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

Merged
Hmbown merged 14 commits into
Hmbown:mainfrom
HUQIANTAO:feat/deepseek-balance-carry
May 31, 2026
Merged

feat: add account balance status bar item (carries #1970)#2257
Hmbown merged 14 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

This PR carries forward #1970, adding an opt-in DeepSeek account balance chip to the footer status bar, a --proxy flag for codewhale update, and a new Matrix colour theme.

  • Balance chip: StatusItem::Balance is gated behind /statusline opt-in and a should_fetch_deepseek_balance guard that checks both the provider (Deepseek/DeepseekCN only) and the presence of the status item, so non-DeepSeek and non-opted-in users see no new network traffic. A 60-second cooldown prevents flooding. The StatusPickerView now filters rows by provider so the Balance toggle is invisible for users on other providers.
  • Proxy support: codewhale update --proxy <URL> threads a reqwest::Proxy through all blocking HTTP paths (GitHub release fetch + asset download); mirror-based releases correctly skip the network for metadata. validate_and_build_proxy is well-tested with both valid and malformed URLs.
  • Matrix theme: A complete UiTheme palette is added with a special reasoning-colour override and "hacker" as a normalised alias.

Confidence Score: 5/5

Safe to merge; all three balance fetch sites are correctly gated behind the opt-in status item and DeepSeek-provider check, so non-opted-in users are unaffected.

The feature is well-scoped and thoroughly tested with 6 balance unit tests and explicit guards at every fetch site. The one logic gap — the else branch in the provider-switch handler can clear a valid balance when rapidly toggling between DeepSeek variants within the 60-second cooldown — is a cosmetic edge case with no data-loss or security implications.

The SwitchProvider handler in crates/tui/src/tui/ui.rs around the balance-clear else branch deserves a second look for the rapid intra-DeepSeek switch case.

Important Files Changed

Filename Overview
crates/tui/src/tui/ui.rs Adds balance fetch logic at startup, turn completion, and provider switch; should_fetch_deepseek_balance correctly gates all three sites behind the opt-in status item and DeepSeek provider check. Minor logic gap in else branch of SwitchProvider handler clears balance on rapid intra-DeepSeek switches.
crates/cli/src/update.rs Adds --proxy support to the self-update workflow. Mirror path correctly constructs release metadata locally (no network call), so proxy is irrelevant there; GitHub and download paths all thread the proxy through. validate_and_build_proxy is well-tested.
crates/tui/src/tui/footer_ui.rs New footer_balance_spans renders the balance chip with three precision tiers; hides on zero/None. Currency lookup is CNY-aware with $ fallback (known trade-off, previously flagged).
crates/tui/src/config.rs Adds Balance variant to StatusItem with from_key/key round-trip, is_available_for provider gate, and a custom tolerant deserializer for status_items that silently skips unknown variants — good forward-compat design.
crates/tui/src/palette.rs Adds a complete Matrix theme with full UiTheme palette, ThemeId::Matrix variant, and special reasoning-colour override. Clean, self-contained addition.
crates/tui/src/tui/views/status_picker.rs Provider-aware filtering added to StatusPickerView::new; Balance row only shown for DeepSeek providers. Scroll/resize handling improved. Tests updated with explicit provider argument.
crates/tui/src/pricing.rs Adds BalanceResponse and BalanceInfo structs with total_balance_f64() helper. Well-tested, clean separation from cost-estimation logic.
crates/tui/src/tui/widgets/footer.rs Adds balance field to FooterProps and integrates it into the responsive width-tier layout between model name and cost. Priority ordering updated in comments and implementation.

Sequence Diagram

sequenceDiagram
    participant User
    participant App
    participant should_fetch as should_fetch_deepseek_balance
    participant tokio as tokio::spawn
    participant API as DeepSeek /user/balance
    participant Cell as balance_cell (Mutex)
    participant Footer as footer_balance_spans

    User->>App: Start session (DeepSeek + Balance in status_items)
    App->>should_fetch: check provider + status item
    should_fetch-->>App: true
    App->>tokio: spawn background fetch
    tokio->>API: GET /user/balance (Bearer token)
    API-->>tokio: BalanceResponse JSON
    tokio->>Cell: lock + write BalanceInfo

    User->>App: Send message to TurnComplete
    App->>should_fetch: check provider + status item + cooldown
    should_fetch-->>App: true (cooldown 60s+)
    App->>tokio: spawn background fetch
    tokio->>API: GET /user/balance
    API-->>tokio: BalanceResponse JSON
    tokio->>Cell: lock + write BalanceInfo

    App->>Footer: render frame
    Footer->>Cell: lock + read BalanceInfo
    Cell-->>Footer: Some(BalanceInfo)
    Footer-->>App: Span bal $42.5

    User->>App: switch to Openrouter
    App->>Cell: lock + clear (None)
    Footer->>Cell: lock + read
    Cell-->>Footer: None
    Footer-->>App: chip hidden
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 (6): Last reviewed commit: "fix: gate DeepSeek balance fetch opt-in" | 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
Contributor

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
Contributor

greptile-apps Bot commented May 27, 2026

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

@Hmbown
Copy link
Copy Markdown
Owner

Hmbown commented May 31, 2026

This is a useful statusline direction, thanks for carving it down from the earlier provider work. I am keeping it open because it is currently conflicting, and the balance fetch still appears to run for DeepSeek users on startup/turn/provider switch even when the Balance status item is not enabled.

That status chip should feel opt-in, not like surprise network activity. Please rebase, gate the fetch on the Balance item actually being enabled, and then run the focused status picker/footer tests plus a manual custom DEEPSEEK_BASE_URL check.

Merge current origin/main into Hmbown#2257 and keep the rescue scoped to the balance statusline feature.

Credits Hmbown#2257 by @HUQIANTAO and the carried Hmbown#1970 balance work by @MoriTang.

The rescue gates startup, turn-complete, and provider-switch balance fetches on StatusItem::Balance, preserves current main Cargo metadata, and adds the missing Vietnamese balance label.
@Hmbown
Copy link
Copy Markdown
Owner

Hmbown commented May 31, 2026

Rescued this on top of current main and pushed 7b1c0079. Thanks @HUQIANTAO for carrying the feature forward, and @MoriTang for the original #1970 design/tests.

What changed:

  • kept Balance opt-in by gating startup, turn-complete, and provider-switch balance fetches on StatusItem::Balance
  • kept current main Cargo/README/palette/update surfaces intact
  • added the missing Vietnamese FooterBalancePrefix locale arm
  • kept the footer balance/cost helper parameter order aligned with the rendered order

Verified:

  • cargo fmt --all -- --check
  • git diff --check origin/main
  • python3 scripts/check-provider-registry.py
  • CARGO_TARGET_DIR=/tmp/codewhale-pr2257-target cargo check -p codewhale-tui --all-features --locked
  • CARGO_TARGET_DIR=/tmp/codewhale-pr2257-target cargo test -p codewhale-tui balance -- --nocapture (23 passed)
  • CARGO_TARGET_DIR=/tmp/codewhale-pr2257-target cargo test -p codewhale-tui status_picker -- --nocapture (9 passed)

This should address the surprise-network-activity blocker; leaving the PR open for CI/review to settle.

@Hmbown
Copy link
Copy Markdown
Owner

Hmbown commented May 31, 2026

The remaining red macOS leg here is the same unrelated provider-picker test drift (Volcengine Ark missing from the expected provider list), now fixed on main by #2394. This PR itself has the balance-status blockers addressed: all balance fetch paths are gated by the StatusItem::Balance opt-in, the footer balance/cost ordering is corrected, and the missing Vietnamese label is included.

Local focused verification on the rescued branch passed: cargo fmt --all -- --check, git diff --check origin/main, python3 scripts/check-provider-registry.py, cargo check -p codewhale-tui --all-features --locked, cargo test -p codewhale-tui balance -- --nocapture (23 passed), and cargo test -p codewhale-tui status_picker -- --nocapture (9 passed). Greptile and GitGuardian are green on 7b1c007 too, so I am merging. Thanks @HUQIANTAO for carrying this forward, and @MoriTang for the original #1970 slice and follow-up on the zero-balance behavior.

@Hmbown Hmbown merged commit 26c3f41 into Hmbown:main May 31, 2026
7 of 9 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.

3 participants