feat(providers): prepare provider terminology and metadata

This commit is contained in:
2026-06-02 15:09:50 +07:00
parent 38752ae5c5
commit 2c67dd1217
23 changed files with 727 additions and 96 deletions
+11 -8
View File
@@ -33,9 +33,9 @@ self-updater are all written from scratch against the same public APIs
(140360 logical pixels)
- Left-click the bubble for an expanded panel with both **5h** and **7d**
bars plus reset countdowns
- Right-click for refresh, displayed models, update frequency, language,
- Right-click for refresh, displayed providers, update frequency, language,
startup, updates, exit
- Optional system tray icons (one per enabled model)
- Optional system tray icons (one per enabled provider)
- Auto-hide when a fullscreen app is in the foreground (games, video,
presentations) — reappears when you leave fullscreen
@@ -43,7 +43,7 @@ self-updater are all written from scratch against the same public APIs
Windows 10/11 users who already have **Claude Code (CLI or App) installed
and signed in**. Codex support is optional — install and sign in to the
Codex CLI, then enable Codex from the right-click **Models** menu.
Codex CLI, then enable Codex from the right-click **Providers** menu.
If you use Claude Code through WSL, that is supported too. The monitor
can read your Claude Code credentials from Windows or from your WSL
@@ -85,7 +85,7 @@ corner of your primary monitor on first launch. Drag it where you want it,
release to snap to the nearest edge if you let go close to one.
- **Left-click** the bubble to open the expanded panel (5h + 7d + countdowns)
- **Right-click** for refresh, models, refresh frequency, language, "Start
- **Right-click** for refresh, providers, refresh frequency, language, "Start
with Windows", controls, auto-update check (Disabled / Hourly / Daily /
Weekly), manual "Check for updates", exit
- **Drag** anywhere — it floats on top of all other windows
@@ -94,14 +94,17 @@ release to snap to the nearest edge if you let go close to one.
- **Tray icon** (if enabled): left-click toggles the bubble visibility,
right-click opens the same menu
### Models
### Providers
Use the right-click **Models** menu to choose what is shown:
Use the right-click **Providers** menu to choose what is shown:
- **Claude Code** is enabled by default
- **Codex** can be enabled alongside Claude Code or shown by itself
- **OpenCode Go** is listed for future support; usage bars remain disabled
until OpenCode exposes a stable quota source or console integration is
explicitly enabled
When both models are shown, each gets its own bubble that you can position
When multiple providers are shown, each gets its own bubble that you can position
independently.
## Diagnostics
@@ -144,7 +147,7 @@ What the app stores locally:
- Polling frequency
- Language preference
- Last update check time
- Displayed model preferences
- Displayed provider preferences
What it does **not** do: send credentials to any third-party server, run a
backend service, collect analytics, upload your project files, or write to
@@ -0,0 +1,56 @@
---
phase: 1
title: Research OpenCode Go contracts
status: blocked
priority: P1
effort: 2h
dependencies: []
---
# Phase 1: Research OpenCode Go Contracts
## Overview
Prove the OpenCode Go credential, provider ID, and usage-data contracts before implementation. This is the gate that prevents guessed percentages.
Current result: blocked. Provider/model IDs and console quota math are verified, but there is no verified stable CLI/public API for this app to fetch current weekly/monthly quota usage.
## Requirements
- Functional: Identify exact local credential path(s), JSON shape, auth key name, refresh/login command, weekly/monthly usage percent, and weekly/monthly reset time.
- Non-functional: Use primary sources first: official OpenCode docs, OpenCode source, locally installed `opencode` behavior if available.
## Architecture
OpenCode Go will only become a supported provider if it can return normalized weekly/monthly data from a stable source. Official docs confirm Go limits are 5h/weekly/monthly and dollar-value based, but not a public usage endpoint. For this app, OpenCode Go must render a custom four-bar layout: weekly usage percent + weekly remaining time in the upper section, monthly usage percent + monthly remaining time in the lower section.
## Related Code Files
- Read: `src/creds/mod.rs`
- Read: `src/creds/codex_auth.rs`
- Read: `src/usage/chatgpt.rs`
- Read: `src/usage/types.rs`
- Read: `src/usage/registry.rs`
- Created: `plans/260602-1115-opencode-go-provider-support/reports/research-opencode-go-contracts.md`
## Implementation Steps
1. Check official OpenCode Go docs for provider ID, limits, endpoints, and console usage semantics.
2. Check official OpenCode CLI/source for `auth.json` storage path and schema.
3. If OpenCode is installed locally, run safe read-only commands: `opencode stats --help`, `opencode providers --help`, `opencode models --help`.
4. Inspect local `~/.local/share/opencode/auth.json` only with user approval if privacy hook blocks or if file may contain API keys.
5. Search for an official usage endpoint or CLI command that returns current weekly/monthly usage percent or used/limit values plus reset/period end times.
6. Record exact request/response shape or command output needed by Phase 3, including how to compute remaining time for weekly and monthly sections.
7. If only console-authenticated usage exists, stop and ask user before planning implementation beyond auth detection.
## Success Criteria
- [x] Exact credential docs path documented.
- [x] Exact provider key documented: `opencode-go`.
- [x] Weekly/monthly usage percent and reset-time shape verified in console source.
- [x] Stable app-callable quota source confirmed unavailable from current CLI/docs.
- [ ] User decision recorded: console integration, limited detector, or defer.
## Risk Assessment
Main risk: OpenCode docs mention tracking usage in the console but do not document a public usage API. Mitigation: make Phase 3 blocked until a stable source exists; do not infer usage from price tables.
@@ -0,0 +1,61 @@
---
phase: 2
title: Refactor provider metadata and settings
status: completed
priority: P1
effort: 4h
dependencies:
- 1
---
# Phase 2: Refactor Provider Metadata And Settings
## Overview
Remove hard-coded two-provider assumptions before adding OpenCode Go. Keep behavior identical for Claude Code and Codex.
## Requirements
- Functional: Represent enabled providers and positions by `ProviderId`, not one field per provider.
- Non-functional: Preserve old `settings.json` compatibility for `show_claude_code`, `show_codex`, and `bubble_positions.{claude,codex}`.
## Architecture
Introduce a small provider metadata layer near `usage::types` or `usage::registry`: stable ID, slug, display label key, tray icon ID, default enabled, and display mode. Existing providers use the current two-window mode. OpenCode Go uses a custom weekly/monthly four-bar mode.
Keep `ProviderId::ChatGpt` internally unless touching the code anyway. The user-facing label remains Codex. Avoid a mechanical rename that adds risk without feature value.
## Related Code Files
- Modify: `src/usage/types.rs`
- Modify: `src/usage/registry.rs`
- Modify: `src/settings.rs`
- Modify: `src/app.rs`
- Modify: `src/tray/mod.rs`
- Modify: `src/tray/badge.rs`
- Modify: `src/usage_color.rs`
- Modify: `src/bubble.rs`
- Modify: `src/panel.rs`
## Implementation Steps
1. Add `ProviderId::OpenCodeGo` and a stable `slug()` value `opencode-go`.
2. Add `ProviderId::all()` or equivalent ordered provider list: Claude, Codex, OpenCode Go.
3. Replace `Settings { show_claude_code, show_codex }` runtime logic with a provider-enabled map or compact struct that can hold `opencode_go`.
4. Preserve serde compatibility: deserialize old fields and write new fields only after migration, or keep old fields plus add `show_opencode_go` if map migration is too broad.
5. Extend `BubblePositions` to store OpenCode Go position while keeping old `claude` and `codex` JSON keys.
6. Update tray icon IDs and badge colors for third provider.
7. Add provider-specific display metadata: Claude/Codex mode=`two-window`; OpenCode Go mode=`weekly-monthly-four-bar`.
8. Update app loops to iterate providers instead of hand-writing Claude/Codex branches where this reduces match duplication.
## Success Criteria
- [ ] Existing `settings.json` with only Claude/Codex still loads.
- [ ] If all providers disabled, Claude Code is re-enabled as today.
- [ ] Three providers can have separate positions and tray icon IDs.
- [ ] OpenCode Go can select a custom four-bar renderer without changing Claude/Codex labels.
- [ ] No behavior change for existing Claude Code/Codex users.
## Risk Assessment
Settings migration is the highest regression risk. Mitigation: add serde/default tests with old two-provider JSON and new three-provider JSON.
@@ -0,0 +1,56 @@
---
phase: 3
title: "Implement OpenCode Go provider"
status: pending
priority: P1
effort: "5h"
dependencies: [1, 2]
---
# Phase 3: Implement OpenCode Go Provider
## Overview
Add OpenCode Go credential discovery and polling once Phase 1 proves the source of truth.
## Requirements
- Functional: When enabled and authenticated, OpenCode Go returns weekly/monthly usage percent and reset time, enough for four bars: usage percent + remaining time for each section.
- Non-functional: Do not store or transmit API keys outside OpenCode/OpenCode Go endpoints. Do not mutate OpenCode auth files.
## Architecture
Create an OpenCode Go credential source that reads OpenCode's auth store and a provider implementation that maps verified weekly/monthly usage percent + reset time into a provider-specific snapshot. Do not force OpenCode Go into the existing `UsageWindows.primary/secondary` shape if that would lose the grouped weekly/monthly rendering semantics.
## Related Code Files
- Create: `src/creds/opencode_auth.rs`
- Create: `src/usage/opencode_go.rs`
- Modify: `src/creds/mod.rs`
- Modify: `src/usage/mod.rs`
- Modify: `src/usage/registry.rs`
- Modify: `src/usage/refresh.rs`
- Modify: `src/app.rs`
## Implementation Steps
1. Add `LocalOpenCodeGoCreds` with path detection from Phase 1. Expected primary path: `%LOCALAPPDATA%`/XDG-equivalent for `opencode/auth.json`; verify exact Windows path before coding.
2. Parse only the OpenCode Go auth entry. Support API-key style auth if Phase 1 confirms schema.
3. Add `RefreshHint::LocalOpenCodeCli` and spawn `opencode auth login` or equivalent only if Phase 1 confirms the command.
4. Add `OpenCodeGoProvider::poll`, using the verified usage endpoint/command.
5. Add a provider-specific snapshot type if needed, for example `ProviderUsage::TwoWindow(UsageWindows)` and `ProviderUsage::WeeklyMonthly { weekly: Window, monthly: Window }`.
6. Map returned weekly/monthly source data to usage percent and reset-time windows. Existing renderer can then draw usage and remaining-time bars for each window.
7. Return `AuthRequired` on 401/403 or missing provider auth.
8. Add unit tests for credential parsing and response-to-snapshot mapping.
## Success Criteria
- [ ] Provider compiles behind the existing registry.
- [ ] Missing OpenCode auth produces `NoCredentials`, not panic.
- [ ] Expired/invalid auth produces `AuthRequired`.
- [ ] Weekly/monthly usage percent clamps to 0-100 and remaining-time bars derive from verified reset times.
- [ ] If Phase 1 cannot verify usage, this phase is not implemented.
## Risk Assessment
OpenCode Go limits are dollar-value based. If the usage source returns dollars instead of percentages, compute percent only as `used / limit * 100` from server-provided weekly/monthly values. Never estimate usage from model prices. If reset times are unavailable, stop and ask before shipping remaining-time bars with guessed periods.
@@ -0,0 +1,60 @@
---
phase: 4
title: Rename Models UI to Providers
status: in-progress
priority: P2
effort: 2h
dependencies:
- 2
---
# Phase 4: Rename Models UI To Providers
## Overview
Rename the menu and documentation wording from "Models" to "Providers" where the app is selecting quota sources.
Current result: partial. Provider wording and OpenCode Go listing are implemented. The OpenCode Go four-bar renderer is still blocked by Phase 1 because no stable quota source is available.
## Requirements
- Functional: Context menu uses "Providers" and lists Claude Code, Codex, OpenCode Go.
- Functional: OpenCode Go panel/bubble layout renders two grouped parts: Weekly on top and Monthly below, each with usage percent and remaining-time bars. Claude/Codex keep their current 5h/7d labels.
- Non-functional: Keep real model wording where it means actual LLM models, not provider selection.
## Architecture
This is mostly i18n and README copy. Keep internal `model` variable renames scoped to files touched by provider iteration. Do not churn every `model` local in renderer code just for style.
## Related Code Files
- Modify: `src/i18n/mod.rs`
- Modify: `src/i18n/locales/en.toml`
- Modify: `src/i18n/locales/ja.toml`
- Modify: `src/i18n/locales/ko.toml`
- Modify: `src/i18n/locales/vi.toml`
- Modify: `src/i18n/locales/zh-TW.toml`
- Modify: `src/app.rs`
- Modify: `README.md`
## Implementation Steps
1. Rename `LocaleStrings.models` to `providers`, or keep field name and change values if minimizing code churn is preferred.
2. Update English label to `Providers`; update other locales with best available translation.
3. Add `opencode_go_label = "OpenCode Go"` to all locale files.
4. Add localized generic labels for Weekly and Monthly group headers. Reuse existing usage-percent and remaining-time visual conventions.
5. Add or branch rendering for OpenCode Go's four-bar layout in `bubble.rs` and `panel.rs`; do not distort Claude/Codex two-bar layout.
6. Update README sections: "displayed models" -> "displayed providers"; "### Models" -> "### Providers".
7. Add locale schema tests for the new label.
## Success Criteria
- [x] Right-click menu says Providers.
- [x] README consistently describes selectable services as providers.
- [ ] OpenCode Go UI has Weekly and Monthly grouped sections, each with usage percent and remaining-time bars.
- [x] No accidental rename of OpenCode's documented `/models` command.
- [x] All embedded locale tests pass.
## Risk Assessment
Translations may be imperfect. Mitigation: keep provider product names untranslated and only translate the generic "Providers" label.
@@ -0,0 +1,56 @@
---
phase: 5
title: "Verify and document"
status: pending
priority: P1
effort: "3h"
dependencies: [3, 4]
---
# Phase 5: Verify And Document
## Overview
Prove the third-provider workflow works and document setup/privacy accurately.
## Requirements
- Functional: Claude Code, Codex, and OpenCode Go can be toggled independently; polling and tray refresh do not regress.
- Non-functional: Privacy docs must list every local file read and network endpoint called.
## Architecture
Verification must cover pure tests, build, and Windows manual smoke tests. OpenCode Go cannot be fully claimed without a real authenticated account or a captured fixture from Phase 1.
## Related Code Files
- Modify: `README.md`
- Optional Modify: `docs/release-process.md` only if release steps change
- Read: `src/app.rs`
- Read: `src/settings.rs`
- Read: `src/usage/*`
- Read: `src/creds/*`
## Implementation Steps
1. Run `cargo test`.
2. Run `cargo build --release`.
3. Run app with old settings file and confirm migration/defaults.
4. Run app with all three providers enabled; verify separate bubbles, positions, tray icons, panel labels, and menu toggles.
5. Verify missing OpenCode Go auth shows auth/no-credentials state without breaking other providers.
6. With real OpenCode Go auth, verify weekly/monthly percent and remaining-time bars match the official console/source found in Phase 1.
7. Update README privacy section with OpenCode auth file and endpoints.
## Success Criteria
- [ ] Tests pass.
- [ ] Release build passes.
- [ ] Existing two-provider settings migrate.
- [ ] Three-provider UI works on Windows.
- [ ] OpenCode Go renders four bars: weekly usage percent, weekly remaining time, monthly usage percent, monthly remaining time.
- [ ] README setup/privacy docs are accurate.
- [ ] No unresolved OpenCode Go usage-source uncertainty remains.
## Risk Assessment
Manual verification needs real OpenCode Go auth. If unavailable, ship code only behind disabled-by-default toggle and explicitly mark e2e verification deferred.
@@ -0,0 +1,72 @@
---
title: OpenCode Go provider support and providers terminology
description: >-
Add OpenCode Go as a usage provider and rename user-facing Models wording to
Providers.
status: in-progress
priority: P2
branch: main
tags:
- providers
- opencode-go
- usage
- ui-copy
blockedBy: []
blocks: []
created: '2026-06-02T04:20:10.983Z'
createdBy: 'ck:plan'
source: skill
---
# OpenCode Go Provider Support And Providers Terminology
## Overview
Add OpenCode Go as a third selectable usage provider beside Claude Code and Codex. Rename the app's user-facing "Models" menu/copy to "Providers" because the app chooses quota sources/services, not individual model IDs.
I agree with the rename. OpenCode itself still has a `/models` command, but this app's menu toggles Claude Code, Codex, and OpenCode Go providers. Internals already use `ProviderId`, so the naming is conceptually aligned.
## Codebase Findings
- Rust Win32 app; usage abstraction lives under `src/usage/*`.
- Current extension points: `UsageProvider`, `ProviderId`, `Registry`, `CredentialSource`, `RefreshHint`.
- Two-provider assumptions remain in `src/settings.rs`, `src/app.rs`, `src/tray/mod.rs`, `src/tray/badge.rs`, `src/panel.rs`, `src/usage_color.rs`, i18n TOMLs, and README.
- OpenCode official docs say Go is configured as an OpenCode provider, uses `/connect`, stores credentials in `~/.local/share/opencode/auth.json`, has 5h/weekly/monthly dollar-value limits, and current usage is visible in the console.
- User decision: OpenCode Go should display four bars in two parts: upper part is weekly usage, lower part is monthly usage. Each part has the same two concepts as current Claude/Codex: usage percent and remaining time. Keep Claude/Codex on their existing two-bar presentation unless a separate UI redesign decides otherwise.
- OpenCode docs expose model endpoints and `https://opencode.ai/zen/go/v1/models`, but do not document a stable usage endpoint. Do not fake usage percentages.
- Phase 1 found the OpenCode console computes `weeklyUsage` and `monthlyUsage` with `usagePercent` and `resetInSec`, but only through an authenticated console server query. `opencode stats` is local cost/token history and cannot drive the requested Go bars.
## Related Plans
- `plans/260516-0707-cleanroom-rewrite/plan.md`: introduced current provider abstraction; related, no blocking dependency.
- `plans/260523-ui-ux-improvement-plan/plan.md`: overlaps labels/tooltips; related, no blocking dependency.
## Phases
| Phase | Name | Status |
|-------|------|--------|
| 1 | [Research OpenCode Go contracts](./phase-01-research-opencode-go-contracts.md) | Blocked |
| 2 | [Refactor provider metadata and settings](./phase-02-refactor-provider-metadata-and-settings.md) | Completed |
| 3 | [Implement OpenCode Go provider](./phase-03-implement-opencode-go-provider.md) | Pending |
| 4 | [Rename Models UI to Providers](./phase-04-rename-models-ui-to-providers.md) | In Progress |
| 5 | [Verify and document](./phase-05-verify-and-document.md) | Pending |
## Dependencies
Phase 1 gates Phase 3. If no stable OpenCode Go usage source is found, pause before Phase 3 and ask user whether to defer usage support or ship a limited connectivity/auth detector.
Phase 2 is implemented. Phase 4 provider wording is implemented; OpenCode Go remains listed but disabled until Phase 1's quota-source decision is resolved.
## Success Criteria
- OpenCode Go can be enabled/disabled independently without breaking Claude Code or Codex.
- Existing settings migrate safely; at least one provider remains enabled.
- UI and README say "Providers" where the app means selectable services.
- No guessed OpenCode Go usage. Weekly/monthly percent and reset-time data comes from verified source, or feature pauses.
- `cargo test` and `cargo build --release` pass.
## Sources
- OpenCode Go docs: https://dev.opencode.ai/docs/go/
- OpenCode CLI docs: https://opencode.ai/docs/cli/
- Current repo README: `README.md`
@@ -0,0 +1,42 @@
# Research: OpenCode Go Contracts
Date: 2026-06-02
Status: blocked
## Summary
OpenCode Go provider/model identity is verified, and the official console computes the exact quota fields we want. Implementation should not proceed yet because the verified quota source is an authenticated console server query, not a documented CLI command or public endpoint this desktop app can call safely.
## Verified Facts
- Provider/model ID format: OpenCode Go models use `opencode-go/<model-id>`, for example `opencode-go/kimi-k2.6`.
- Official limits: Go has a rolling 5-hour window, a weekly window, and a monthly window. The requested UI only needs weekly and monthly sections.
- Limits are cost based, not request-count based: weekly `$30`, monthly `$60`, and rolling 5-hour `$12`.
- CLI auth docs say credentials are stored at `~/.local/share/opencode/auth.json`; locally, `opencode providers` is an alias for auth management.
- Local `opencode stats` only aggregates local sessions from the OpenCode database and reports cost/token totals. It does not report Go quota percentages or reset times.
- Console source has the desired shape:
- `weeklyUsage: { status, resetInSec, usagePercent }`
- `monthlyUsage: { status, resetInSec, usagePercent }`
- Console source derives those from `LiteTable.weeklyUsage`, `LiteTable.monthlyUsage`, `LiteTable.timeWeeklyUpdated`, `LiteTable.timeMonthlyUpdated`, `LiteTable.timeCreated`, and `LiteData.getLimits()`.
## Sources
- OpenCode Go docs: https://dev.opencode.ai/docs/go/
- OpenCode CLI docs: https://opencode.ai/docs/cli/
- OpenCode source: `packages/opencode/src/cli/cmd/stats.ts`
- OpenCode source: `packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx`
- OpenCode source: `packages/console/core/src/subscription.ts`
- Local command: `opencode stats --help`
## Decision
Do not implement OpenCode Go four-bar usage yet. The app cannot honestly render weekly/monthly usage percent and remaining time until one of these is chosen:
1. Use an official/detected console endpoint with user-approved authentication.
2. Ship only provider/auth detection now and hide/disable Go usage bars until quota data is available.
3. Ask OpenCode upstream for a documented quota API or CLI output.
## Unresolved Questions
- Should this app integrate with the authenticated OpenCode console, or should Go support be limited to detection until OpenCode exposes a stable quota API?
- Do we still want to continue Phase 2/4 now for the provider terminology rename, while deferring Phase 3?
+53 -42
View File
@@ -66,6 +66,7 @@ const IDM_FREQ_15MIN: u16 = 12;
const IDM_FREQ_1HOUR: u16 = 13;
const IDM_MODEL_CLAUDE: u16 = 20;
const IDM_MODEL_CHATGPT: u16 = 21;
const IDM_MODEL_OPENCODE_GO: u16 = 22;
const IDM_START_WITH_WINDOWS: u16 = 30;
const IDM_RESET_POSITION: u16 = 31;
const IDM_VERSION_ACTION: u16 = 32;
@@ -325,11 +326,10 @@ fn create_initial_bubbles() {
Some(s) => (s.settings.clone(), s.is_dark),
None => return,
};
if settings.show_claude_code {
spawn_bubble(ProviderId::Claude, &settings, is_dark);
}
if settings.show_codex {
spawn_bubble(ProviderId::ChatGpt, &settings, is_dark);
for provider in ProviderId::LIVE_USAGE {
if settings.is_provider_enabled(provider) {
spawn_bubble(provider, &settings, is_dark);
}
}
}
@@ -421,6 +421,7 @@ fn on_menu_command(id: u32, _owner_hwnd: HWND) {
IDM_FREQ_1HOUR => set_poll_interval(POLL_1_HOUR),
IDM_MODEL_CLAUDE => toggle_model(ProviderId::Claude),
IDM_MODEL_CHATGPT => toggle_model(ProviderId::ChatGpt),
IDM_MODEL_OPENCODE_GO => {}
IDM_START_WITH_WINDOWS => toggle_startup(),
IDM_RESET_POSITION => reset_positions(),
IDM_VERSION_ACTION => version_action(),
@@ -761,28 +762,23 @@ fn refresh_tray_icons() {
fn refresh_tray_icons_with(snap: &UiSnapshot) {
let mut icons = Vec::new();
if snap.settings.show_claude_code {
let entry = snap.snapshots.get(&ProviderId::Claude);
for provider in ProviderId::LIVE_USAGE {
if !snap.settings.is_provider_enabled(provider) {
continue;
}
let entry = snap.snapshots.get(&provider);
icons.push(TrayIconData {
kind: ProviderId::Claude,
kind: provider,
percent: if snap.last_poll_ok {
entry.map(|e| e.windows.primary.utilization)
} else {
None
},
tooltip: tray_tooltip(&snap.i18n_strings.claude_label, entry, &snap.i18n_strings),
});
}
if snap.settings.show_codex {
let entry = snap.snapshots.get(&ProviderId::ChatGpt);
icons.push(TrayIconData {
kind: ProviderId::ChatGpt,
percent: if snap.last_poll_ok {
entry.map(|e| e.windows.primary.utilization)
} else {
None
},
tooltip: tray_tooltip(&snap.i18n_strings.chatgpt_label, entry, &snap.i18n_strings),
tooltip: tray_tooltip(
&provider_label(provider, &snap.i18n_strings),
entry,
&snap.i18n_strings,
),
});
}
tray::sync(snap.msg_hwnd.to_hwnd(), &icons);
@@ -803,6 +799,14 @@ fn tray_tooltip(label: &str, entry: Option<&ProviderUiState>, strings: &LocaleSt
)
}
fn provider_label(provider: ProviderId, strings: &LocaleStrings) -> String {
match provider {
ProviderId::Claude => strings.claude_label.clone(),
ProviderId::ChatGpt => strings.chatgpt_label.clone(),
ProviderId::OpenCodeGo => strings.opencode_go_label.clone(),
}
}
fn handle_tray_action(action: TrayAction) {
match action {
TrayAction::None => {}
@@ -859,10 +863,7 @@ fn show_threshold_balloon(provider: ProviderId, threshold: u8) {
}
s.last_balloon_at = Some(Instant::now());
let strings = s.i18n.strings();
let provider_label = match provider {
ProviderId::Claude => strings.claude_label.clone(),
ProviderId::ChatGpt => strings.chatgpt_label.clone(),
};
let provider_label = provider_label(provider, strings);
let title = format!("{provider_label} · {threshold}%");
let body = if threshold >= 95 {
strings.threshold_95_body.clone()
@@ -896,6 +897,10 @@ fn show_token_expired_balloon(failed: ProviderId) {
strings.chatgpt_token_expired_title.clone(),
strings.chatgpt_token_expired_body.clone(),
),
ProviderId::OpenCodeGo => (
strings.opencode_go_label.clone(),
strings.update_failed.clone(),
),
};
(s.msg_hwnd, failed, title, body)
};
@@ -935,6 +940,7 @@ struct ContextMenuSnapshot {
update_check_interval_secs: Option<u64>,
show_claude: bool,
show_chatgpt: bool,
show_opencode_go: bool,
widget_visible: bool,
install_channel: InstallChannel,
update_status: UpdateStatus,
@@ -955,6 +961,7 @@ fn show_context_menu(owner_hwnd: HWND) {
update_check_interval_secs: s.settings.update_check_interval_secs,
show_claude: s.settings.show_claude_code,
show_chatgpt: s.settings.show_codex,
show_opencode_go: s.settings.show_opencode_go,
widget_visible: s.settings.widget_visible,
install_channel: s.install_channel,
update_status: s.update_status,
@@ -991,13 +998,13 @@ fn show_context_menu(owner_hwnd: HWND) {
}
append_submenu(menu, freq, &snap.strings.update_frequency);
let Ok(models) = CreatePopupMenu() else {
log::error!("CreatePopupMenu(models) failed");
let Ok(providers) = CreatePopupMenu() else {
log::error!("CreatePopupMenu(providers) failed");
let _ = DestroyMenu(menu);
return;
};
append_item(
models,
providers,
IDM_MODEL_CLAUDE,
&snap.strings.claude_label,
if snap.show_claude {
@@ -1007,7 +1014,7 @@ fn show_context_menu(owner_hwnd: HWND) {
},
);
append_item(
models,
providers,
IDM_MODEL_CHATGPT,
&snap.strings.chatgpt_label,
if snap.show_chatgpt {
@@ -1016,7 +1023,17 @@ fn show_context_menu(owner_hwnd: HWND) {
MENU_ITEM_FLAGS(0)
},
);
append_submenu(menu, models, &snap.strings.models);
append_item(
providers,
IDM_MODEL_OPENCODE_GO,
&snap.strings.opencode_go_label,
if snap.show_opencode_go {
MF_CHECKED | MF_GRAYED
} else {
MF_GRAYED
},
);
append_submenu(menu, providers, &snap.strings.providers);
let Ok(settings_menu) = CreatePopupMenu() else {
log::error!("CreatePopupMenu(settings_menu) failed");
@@ -1250,24 +1267,18 @@ fn toggle_model(model: ProviderId) {
let Some(s) = s.as_mut() else {
return;
};
match model {
ProviderId::Claude => s.settings.show_claude_code = !s.settings.show_claude_code,
ProviderId::ChatGpt => s.settings.show_codex = !s.settings.show_codex,
if !model.metadata().live_usage {
return;
}
if !s.settings.show_claude_code && !s.settings.show_codex {
match model {
ProviderId::Claude => s.settings.show_claude_code = true,
ProviderId::ChatGpt => s.settings.show_codex = true,
}
s.settings.toggle_provider(model);
if !s.settings.has_enabled_live_provider() {
s.settings.set_provider_enabled(model, true);
}
(s.settings.clone(), s.is_dark)
};
settings::save(&settings);
let want = match model {
ProviderId::Claude => settings.show_claude_code,
ProviderId::ChatGpt => settings.show_codex,
};
let want = settings.is_provider_enabled(model);
let existing = lock_state()
.as_ref()
.and_then(|s| s.bubbles.get(&model).copied());
+18 -3
View File
@@ -107,6 +107,8 @@ fn window_duration_secs(model: ProviderId, window: UsageWindowKind) -> u64 {
(ProviderId::Claude, UsageWindowKind::Secondary) => SEVEN_DAYS_SECS,
(ProviderId::ChatGpt, UsageWindowKind::Primary) => FIVE_HOURS_SECS,
(ProviderId::ChatGpt, UsageWindowKind::Secondary) => SEVEN_DAYS_SECS,
(ProviderId::OpenCodeGo, UsageWindowKind::Primary) => SEVEN_DAYS_SECS,
(ProviderId::OpenCodeGo, UsageWindowKind::Secondary) => 30 * 24 * 60 * 60,
}
}
@@ -1210,7 +1212,9 @@ mod fullscreen_tests {
fn style_bits_allow_popup_or_borderless_windows_only() {
assert!(window_style_bits_allow_fullscreen(WS_POPUP.0));
assert!(window_style_bits_allow_fullscreen(0));
assert!(!window_style_bits_allow_fullscreen(WS_CAPTION.0 | WS_THICKFRAME.0));
assert!(!window_style_bits_allow_fullscreen(
WS_CAPTION.0 | WS_THICKFRAME.0
));
assert!(!window_style_bits_allow_fullscreen(WS_CHILD.0 | WS_POPUP.0));
}
@@ -1922,7 +1926,12 @@ fn paint_bubble_text(hdc: HDC, layout: &BubbleLayout, inputs: &PaintInputs) {
}
SetTextColor(hdc, COLORREF(color.into_colorref()));
let weekly_pct_text = format!("{:.0}%", pct);
draw_tail_text_in_rect(hdc, &layout.tail_usage_pct_rect, &weekly_pct_text, DT_RIGHT);
draw_tail_text_in_rect(
hdc,
&layout.tail_usage_pct_rect,
&weekly_pct_text,
DT_RIGHT,
);
}
}
@@ -1932,7 +1941,12 @@ fn paint_bubble_text(hdc: HDC, layout: &BubbleLayout, inputs: &PaintInputs) {
SelectObject(hdc, main_font);
SetTextColor(hdc, COLORREF(muted_color.into_colorref()));
if !inputs.weekly_text.is_empty() {
draw_tail_text_in_rect(hdc, &layout.tail_time_text_rect, &inputs.weekly_text, DT_RIGHT);
draw_tail_text_in_rect(
hdc,
&layout.tail_time_text_rect,
&inputs.weekly_text,
DT_RIGHT,
);
}
SelectObject(hdc, prev_font);
@@ -2022,6 +2036,7 @@ fn default_position(width_px: i32, height_px: i32, model: ProviderId) -> (i32, i
let stagger = match model {
ProviderId::Claude => 0,
ProviderId::ChatGpt => height_px + gap,
ProviderId::OpenCodeGo => 2 * (height_px + gap),
};
let x = wa.right - width_px - gap;
let y = wa.bottom - height_px - gap - stagger;
+2 -1
View File
@@ -8,9 +8,10 @@ one_minute = "1 minute"
five_minutes = "5 minutes"
fifteen_minutes = "15 minutes"
one_hour = "1 hour"
models = "Models"
providers = "Providers"
claude_label = "Claude Code"
chatgpt_label = "Codex"
opencode_go_label = "OpenCode Go"
settings = "Settings"
start_with_windows = "Start with Windows"
reset_position = "Reset position"
+2 -1
View File
@@ -8,9 +8,10 @@ one_minute = "1分"
five_minutes = "5分"
fifteen_minutes = "15分"
one_hour = "1時間"
models = "モデル"
providers = "プロバイダー"
claude_label = "Claude Code"
chatgpt_label = "Codex"
opencode_go_label = "OpenCode Go"
settings = "設定"
start_with_windows = "Windows起動時に開始"
reset_position = "位置をリセット"
+2 -1
View File
@@ -8,9 +8,10 @@ one_minute = "1분"
five_minutes = "5분"
fifteen_minutes = "15분"
one_hour = "1시간"
models = "모델"
providers = "제공자"
claude_label = "Claude Code"
chatgpt_label = "Codex"
opencode_go_label = "OpenCode Go"
settings = "설정"
start_with_windows = "Windows 시작 시 실행"
reset_position = "위치 초기화"
+2 -1
View File
@@ -8,9 +8,10 @@ one_minute = "1 phút"
five_minutes = "5 phút"
fifteen_minutes = "15 phút"
one_hour = "1 giờ"
models = "Mô hình"
providers = "Nhà cung cấp"
claude_label = "Claude Code"
chatgpt_label = "Codex"
opencode_go_label = "OpenCode Go"
settings = "Cài đặt"
start_with_windows = "Khởi động cùng Windows"
reset_position = "Đặt lại vị trí"
+2 -1
View File
@@ -8,9 +8,10 @@ one_minute = "1 分鐘"
five_minutes = "5 分鐘"
fifteen_minutes = "15 分鐘"
one_hour = "1 小時"
models = "模型"
providers = "提供者"
claude_label = "Claude Code"
chatgpt_label = "Codex"
opencode_go_label = "OpenCode Go"
settings = "設定"
start_with_windows = "隨 Windows 啟動"
reset_position = "重設位置"
+24 -1
View File
@@ -28,9 +28,10 @@ pub struct LocaleStrings {
pub five_minutes: String,
pub fifteen_minutes: String,
pub one_hour: String,
pub models: String,
pub providers: String,
pub claude_label: String,
pub chatgpt_label: String,
pub opencode_go_label: String,
pub settings: String,
pub start_with_windows: String,
pub reset_position: String,
@@ -292,7 +293,9 @@ mod tests {
let strings = file.strings;
for (name, value) in [
("size_smaller", strings.size_smaller.as_str()),
("providers", strings.providers.as_str()),
("size_larger", strings.size_larger.as_str()),
("opencode_go_label", strings.opencode_go_label.as_str()),
("reset_size", strings.reset_size.as_str()),
("controls", strings.controls.as_str()),
("control_left_click", strings.control_left_click.as_str()),
@@ -334,4 +337,24 @@ mod tests {
"malformed control_tray_click should fail locale deserialization"
);
}
#[test]
fn locale_schema_rejects_missing_provider_strings() {
let (_, fallback_body) = RAW_LOCALES
.iter()
.find(|(code, _)| *code == FALLBACK_CODE)
.expect("fallback locale fixture must exist");
let missing_providers = fallback_body.replace("providers = \"Providers\"\n", "");
assert!(
toml::from_str::<LocaleFile>(&missing_providers).is_err(),
"missing providers should fail locale deserialization"
);
let missing_opencode = fallback_body.replace("opencode_go_label = \"OpenCode Go\"\n", "");
assert!(
toml::from_str::<LocaleFile>(&missing_opencode).is_err(),
"missing opencode_go_label should fail locale deserialization"
);
}
}
+3 -5
View File
@@ -317,6 +317,7 @@ fn paint(hwnd: HWND, hdc: HDC) {
let header = match data.model {
ProviderId::Claude => data.strings.claude_label.clone(),
ProviderId::ChatGpt => data.strings.chatgpt_label.clone(),
ProviderId::OpenCodeGo => data.strings.opencode_go_label.clone(),
};
draw_text(
hdc,
@@ -331,11 +332,8 @@ fn paint(hwnd: HWND, hdc: HDC) {
);
let bar_x = scaled(PADDING_LOGICAL) + scaled(LABEL_W_LOGICAL) + scaled(4);
let bar_w = rc.right
- bar_x
- scaled(PADDING_LOGICAL)
- scaled(RIGHT_TEXT_W_LOGICAL)
- scaled(4);
let bar_w =
rc.right - bar_x - scaled(PADDING_LOGICAL) - scaled(RIGHT_TEXT_W_LOGICAL) - scaled(4);
let row1_y = scaled(PADDING_LOGICAL) + scaled(24);
let row2_y = row1_y + scaled(BAR_HEIGHT_LOGICAL) + scaled(ROW_GAP_LOGICAL) + scaled(8);
+120 -13
View File
@@ -31,6 +31,9 @@ fn default_show_claude() -> bool {
fn default_show_codex() -> bool {
false
}
fn default_show_opencode_go() -> bool {
false
}
fn default_widget_visible() -> bool {
true
}
@@ -48,30 +51,35 @@ fn default_update_check_interval_secs() -> Option<u64> {
pub struct BubblePositions {
pub claude: Option<(i32, i32)>,
pub codex: Option<(i32, i32)>,
pub opencode_go: Option<(i32, i32)>,
}
impl BubblePositions {
pub fn get(&self, model: ProviderId) -> Option<(i32, i32)> {
match model {
pub fn get(&self, provider: ProviderId) -> Option<(i32, i32)> {
match provider {
ProviderId::Claude => self.claude,
ProviderId::ChatGpt => self.codex,
ProviderId::OpenCodeGo => self.opencode_go,
}
}
pub fn set(&mut self, model: ProviderId, pos: (i32, i32)) {
match model {
pub fn set(&mut self, provider: ProviderId, pos: (i32, i32)) {
match provider {
ProviderId::Claude => self.claude = Some(pos),
ProviderId::ChatGpt => self.codex = Some(pos),
ProviderId::OpenCodeGo => self.opencode_go = Some(pos),
}
}
pub fn reset(&mut self, model: ProviderId) {
match model {
pub fn reset(&mut self, provider: ProviderId) {
match provider {
ProviderId::Claude => self.claude = None,
ProviderId::ChatGpt => self.codex = None,
ProviderId::OpenCodeGo => self.opencode_go = None,
}
}
pub fn reset_all(&mut self) {
self.claude = None;
self.codex = None;
self.opencode_go = None;
}
/// Drop any saved position whose top-left no longer falls on a connected
@@ -80,16 +88,26 @@ impl BubblePositions {
pub fn validate(&mut self) {
if let Some((x, y)) = self.claude {
if !position_on_any_monitor(x, y) {
log::warn!("bubble position claude ({x},{y}) outside all monitors; resetting to default");
log::warn!(
"bubble position claude ({x},{y}) outside all monitors; resetting to default"
);
self.claude = None;
}
}
if let Some((x, y)) = self.codex {
if !position_on_any_monitor(x, y) {
log::warn!("bubble position codex ({x},{y}) outside all monitors; resetting to default");
log::warn!(
"bubble position codex ({x},{y}) outside all monitors; resetting to default"
);
self.codex = None;
}
}
if let Some((x, y)) = self.opencode_go {
if !position_on_any_monitor(x, y) {
log::warn!("bubble position opencode-go ({x},{y}) outside all monitors; resetting to default");
self.opencode_go = None;
}
}
}
}
@@ -112,6 +130,8 @@ pub struct Settings {
pub show_claude_code: bool,
#[serde(default = "default_show_codex")]
pub show_codex: bool,
#[serde(default = "default_show_opencode_go")]
pub show_opencode_go: bool,
#[serde(default)]
pub bubble_positions: BubblePositions,
#[serde(default = "default_bubble_size")]
@@ -133,6 +153,7 @@ impl Default for Settings {
Self {
show_claude_code: default_show_claude(),
show_codex: default_show_codex(),
show_opencode_go: default_show_opencode_go(),
bubble_positions: BubblePositions::default(),
bubble_size_logical: default_bubble_size(),
poll_interval_ms: default_poll_interval_ms(),
@@ -144,6 +165,35 @@ impl Default for Settings {
}
}
impl Settings {
pub fn is_provider_enabled(&self, provider: ProviderId) -> bool {
match provider {
ProviderId::Claude => self.show_claude_code,
ProviderId::ChatGpt => self.show_codex,
ProviderId::OpenCodeGo => self.show_opencode_go,
}
}
pub fn set_provider_enabled(&mut self, provider: ProviderId, enabled: bool) {
match provider {
ProviderId::Claude => self.show_claude_code = enabled,
ProviderId::ChatGpt => self.show_codex = enabled,
ProviderId::OpenCodeGo => self.show_opencode_go = enabled,
}
}
pub fn toggle_provider(&mut self, provider: ProviderId) {
let enabled = !self.is_provider_enabled(provider);
self.set_provider_enabled(provider, enabled);
}
pub fn has_enabled_live_provider(&self) -> bool {
ProviderId::LIVE_USAGE
.iter()
.any(|provider| self.is_provider_enabled(*provider))
}
}
pub fn settings_dir() -> Option<PathBuf> {
dirs::config_dir().map(|d| d.join(APP_DIR_NAME))
}
@@ -161,19 +211,76 @@ pub fn load() -> Settings {
Err(_) => return Settings::default(),
};
let mut settings: Settings = serde_json::from_str(&content).unwrap_or_default();
// At least one model must be visible. Otherwise the app has nothing to show.
if !settings.show_claude_code && !settings.show_codex {
// At least one live provider must be visible. Otherwise the app has nothing to show.
if !settings.has_enabled_live_provider() {
settings.show_claude_code = true;
}
// Clamp bubble size to safe range in case settings.json was hand-edited.
settings.bubble_size_logical = settings
.bubble_size_logical
.clamp(crate::bubble::MIN_BUBBLE_SIZE, crate::bubble::MAX_BUBBLE_SIZE);
settings.bubble_size_logical = settings.bubble_size_logical.clamp(
crate::bubble::MIN_BUBBLE_SIZE,
crate::bubble::MAX_BUBBLE_SIZE,
);
// Drop positions on monitors that have since been disconnected.
settings.bubble_positions.validate();
settings
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn old_two_provider_settings_json_still_loads() {
let json = r#"{
"show_claude_code": false,
"show_codex": true,
"bubble_positions": {
"claude": [10, 20],
"codex": [30, 40]
}
}"#;
let settings: Settings =
serde_json::from_str(json).expect("old settings json should parse");
assert!(!settings.show_claude_code);
assert!(settings.show_codex);
assert!(!settings.show_opencode_go);
assert_eq!(
settings.bubble_positions.get(ProviderId::Claude),
Some((10, 20))
);
assert_eq!(
settings.bubble_positions.get(ProviderId::ChatGpt),
Some((30, 40))
);
assert_eq!(settings.bubble_positions.get(ProviderId::OpenCodeGo), None);
}
#[test]
fn three_provider_settings_json_loads() {
let json = r#"{
"show_claude_code": true,
"show_codex": false,
"show_opencode_go": true,
"bubble_positions": {
"opencode_go": [50, 60]
}
}"#;
let settings: Settings =
serde_json::from_str(json).expect("new settings json should parse");
assert!(settings.show_claude_code);
assert!(!settings.show_codex);
assert!(settings.show_opencode_go);
assert_eq!(
settings.bubble_positions.get(ProviderId::OpenCodeGo),
Some((50, 60))
);
}
}
pub fn save(settings: &Settings) {
let path = settings_path();
if let Some(parent) = path.parent() {
+9 -1
View File
@@ -43,7 +43,13 @@ fn render_pixmap(kind: ProviderId, percent: Option<f64>) -> Pixmap {
let mut pb = PathBuilder::new();
pb.push_circle(cx, cy, inner);
if let Some(path) = pb.finish() {
pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None);
pixmap.fill_path(
&path,
&paint,
FillRule::Winding,
Transform::identity(),
None,
);
}
}
@@ -112,6 +118,8 @@ fn base_color(kind: ProviderId) -> [u8; 3] {
ProviderId::Claude => [0x2a, 0x1f, 0x1c],
// Cool dark slate for ChatGPT/Codex.
ProviderId::ChatGpt => [0x1a, 0x1f, 0x26],
// Deep green for OpenCode Go.
ProviderId::OpenCodeGo => [0x12, 0x2a, 0x20],
}
}
+3 -6
View File
@@ -11,8 +11,8 @@ use std::sync::{Mutex, OnceLock};
use windows::Win32::Foundation::HWND;
use windows::Win32::UI::Shell::{
Shell_NotifyIconW, NIF_ICON, NIF_INFO, NIF_MESSAGE, NIF_TIP, NIIF_INFO, NIIF_WARNING,
NIM_ADD, NIM_DELETE, NIM_MODIFY, NOTIFYICONDATAW, NOTIFY_ICON_INFOTIP_FLAGS,
Shell_NotifyIconW, NIF_ICON, NIF_INFO, NIF_MESSAGE, NIF_TIP, NIIF_INFO, NIIF_WARNING, NIM_ADD,
NIM_DELETE, NIM_MODIFY, NOTIFYICONDATAW, NOTIFY_ICON_INFOTIP_FLAGS,
};
use windows::Win32::UI::WindowsAndMessaging::DestroyIcon;
@@ -130,10 +130,7 @@ fn build_data(owner: HWND, kind: IconKind) -> NOTIFYICONDATAW {
}
fn icon_id(kind: IconKind) -> u32 {
match kind {
IconKind::Claude => 1,
IconKind::ChatGpt => 2,
}
kind.metadata().tray_icon_id
}
fn write_utf16(dst: &mut [u16], src: &str) {
+22 -7
View File
@@ -7,7 +7,10 @@
use crate::creds::Locator;
use crate::net::Client;
use crate::settings::Settings;
use crate::usage::{anthropic::ClaudeProvider, chatgpt::ChatGptProvider, refresh, Error, ProviderId, UsageProvider, UsageWindows};
use crate::usage::{
anthropic::ClaudeProvider, chatgpt::ChatGptProvider, refresh, Error, ProviderId, UsageProvider,
UsageWindows,
};
pub struct Registry {
claude: ClaudeProvider,
@@ -28,20 +31,32 @@ impl Registry {
settings: &Settings,
) -> Vec<(ProviderId, Result<UsageWindows, Error>)> {
let mut out = Vec::new();
if settings.show_claude_code {
out.push((ProviderId::Claude, self.claude.poll(http)));
}
if settings.show_codex {
out.push((ProviderId::ChatGpt, self.chatgpt.poll(http)));
for provider in ProviderId::LIVE_USAGE {
if !settings.is_provider_enabled(provider) {
continue;
}
let result = match provider {
ProviderId::Claude => self.claude.poll(http),
ProviderId::ChatGpt => self.chatgpt.poll(http),
ProviderId::OpenCodeGo => {
unreachable!("OpenCode Go has no live usage provider yet")
}
};
out.push((provider, result));
}
out
}
/// Attempt to refresh the active source for one provider.
pub fn try_refresh(&self, id: ProviderId, orchestrator: &refresh::Orchestrator) -> refresh::Outcome {
pub fn try_refresh(
&self,
id: ProviderId,
orchestrator: &refresh::Orchestrator,
) -> refresh::Outcome {
let locator = match id {
ProviderId::Claude => self.claude.locator(),
ProviderId::ChatGpt => self.chatgpt.locator(),
ProviderId::OpenCodeGo => return refresh::Outcome::CliMissing,
};
match locator.first_available() {
Some(src) => orchestrator.refresh(src),
+50 -5
View File
@@ -1,9 +1,4 @@
// Usage data shapes shared across providers.
//
// Every provider reports its quota as two named "windows" (short + long).
// For Claude: 5-hour and 7-day. For ChatGPT: primary and secondary. We
// normalise to `primary` + `secondary` so the UI layer doesn't care which
// provider produced the snapshot.
use std::time::SystemTime;
@@ -11,15 +6,65 @@ use std::time::SystemTime;
pub enum ProviderId {
Claude,
ChatGpt,
OpenCodeGo,
}
impl ProviderId {
pub const ALL: [Self; 3] = [Self::Claude, Self::ChatGpt, Self::OpenCodeGo];
pub const LIVE_USAGE: [Self; 2] = [Self::Claude, Self::ChatGpt];
pub fn slug(self) -> &'static str {
match self {
Self::Claude => "claude",
Self::ChatGpt => "chatgpt",
Self::OpenCodeGo => "opencode-go",
}
}
pub fn metadata(self) -> ProviderMetadata {
match self {
Self::Claude => ProviderMetadata {
id: self,
slug: "claude",
default_enabled: true,
tray_icon_id: 1,
display_mode: ProviderDisplayMode::TwoWindow,
live_usage: true,
},
Self::ChatGpt => ProviderMetadata {
id: self,
slug: "chatgpt",
default_enabled: false,
tray_icon_id: 2,
display_mode: ProviderDisplayMode::TwoWindow,
live_usage: true,
},
Self::OpenCodeGo => ProviderMetadata {
id: self,
slug: "opencode-go",
default_enabled: false,
tray_icon_id: 3,
display_mode: ProviderDisplayMode::WeeklyMonthlyFourBar,
live_usage: false,
},
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ProviderDisplayMode {
TwoWindow,
WeeklyMonthlyFourBar,
}
#[derive(Clone, Copy, Debug)]
pub struct ProviderMetadata {
pub id: ProviderId,
pub slug: &'static str,
pub default_enabled: bool,
pub tray_icon_id: u32,
pub display_mode: ProviderDisplayMode,
pub live_usage: bool,
}
/// One usage window: how much you've consumed (0100), and when it resets.
+1
View File
@@ -19,6 +19,7 @@ pub fn accent_color_for(model: ProviderId, is_dark: bool) -> Color {
Color::from_hex("#1A1A1A")
}
}
ProviderId::OpenCodeGo => Color::from_hex("#3BAE75"),
}
}