Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
eb07834
Add X API provider (xapi) - cookie-based prepaid credits
Finesssee Oct 12, 2026
80a75d2
Fix code formatting
Finesssee Oct 12, 2026
a7815cb
Merge remote-tracking branch 'origin/main' into feat/provider-xapi
Finesssee Oct 12, 2026
5faa1d3
Add X API provider wiring and tests
Finesssee Oct 12, 2026
a452ab5
Add X API provider to documentation, settings, and tests
Finesssee Oct 12, 2026
d63e056
Fix test_provider_id_all: update expected count from 96 to 97 for X API
Finesssee Oct 12, 2026
80becb9
Add X API mock fixtures for real-window testing
Finesssee Oct 12, 2026
f2c321e
Move X API fixtures to proof report directory
Finesssee Oct 12, 2026
d254355
Add X API cookie-source tests to session_cookie_scope and settings
Finesssee Oct 12, 2026
b279a51
Add Xapi to cookie_source defaults and fix test
Finesssee Oct 12, 2026
81c4ed0
Add X API to cookie_source_options_for function
Finesssee Oct 12, 2026
7c1a196
Add xapi provider assert to test
Finesssee Oct 12, 2026
13c35aa
Remove xapi usage-source policy and unused locale keys
Finesssee Oct 12, 2026
3527e33
Fix xapi balance display - remove duplicate Balance label
Finesssee Oct 12, 2026
dbd04bf
fmt: xapi formatting
Finesssee Oct 12, 2026
4ecb0e2
Fix X API display structure and parsing validation
Oct 12, 2026
77e7325
Match upstream X API amounts, account IDs and card layout
Oct 12, 2026
1e84c29
Skip card-hidden windows in the provider settings pane
Oct 12, 2026
d7c858a
Format X API account ids without a lossy cast
Oct 12, 2026
3f834e0
Hide the X API cost block on the card like upstream
Oct 12, 2026
9e21529
Add X API fetch fixtures and qualify Automatic mode
Oct 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ See the full history in [CHANGELOG.md](CHANGELOG.md).
| LithosAI | Browser cookies / manual Cookie header | Prepaid balance, UTC spend today and this month |
| Muse (muse.ai) | Browser cookies / manual Cookie header | Weekly usage, tokens left, top-up tokens |
| WorkBuddy | Browser cookies / manual Cookie header | Monthly credits left, plan, cycle reset |
| X API | Browser cookies / manual Cookie header | Prepaid balance, purchased and free credits |

</details>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ fn cookie_source_provider(provider_id: &str) -> Option<codexbar::core::ProviderI
"museai" => ProviderId::MuseAI,
"lithosai" => ProviderId::LithosAI,
"langdock" => ProviderId::Langdock,
"xapi" => ProviderId::Xapi,
_ => return None,
})
}
Expand Down Expand Up @@ -892,6 +893,13 @@ pub fn cookie_source_options_for(provider_id: &str, lang: Language) -> Vec<Cooki
cookie_option(lang, "manual", None),
cookie_option(lang, "off", None),
],
// X API cookie source: automatic browser import,
// a pasted header, or off.
"xapi" => vec![
cookie_option(lang, "auto", None),
cookie_option(lang, "manual", None),
cookie_option(lang, "off", None),
],
_ => Vec::new(),
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -324,3 +324,44 @@ fn workbuddy_exposes_a_cookie_source_picker_and_routes_each_choice() {
assert_eq!(ctx.source_mode, SourceMode::Cli);
assert!(ctx.manual_cookie_header.is_none());
}

/// X API is web-only: the picker chooses the session, a stored header
/// always wins, an empty manual source fails closed without reading a browser,
/// and off reaches the provider as Cli (its "cookies are disabled" error).
#[test]
fn xapi_exposes_a_cookie_source_picker_and_routes_each_choice() {
let settings = Settings::default();
assert_eq!(
super::provider_cookie_source_lookup(&settings, "xapi").as_deref(),
Some("auto")
);
let values: Vec<_> = super::cookie_source_options_for("xapi", Language::English)
.into_iter()
.map(|option| option.value)
.collect();
assert_eq!(values, vec!["auto", "manual", "off"]);

let cookie = "auth_token=fixture; ct0=fixture";
for source in ["auto", "manual"] {
let ctx = session_context(ProviderId::Xapi, Some(source), "auto", Some(cookie));
assert!(
matches!(ctx.source_mode, SourceMode::Auto | SourceMode::Web),
"{source}"
);
assert_eq!(
ctx.manual_cookie_header.as_deref(),
Some(cookie),
"{source}"
);
assert!(!ctx.manual_cookie_missing, "{source}");
}

let ctx = session_context(ProviderId::Xapi, Some("manual"), "auto", None);
assert_eq!(ctx.source_mode, SourceMode::Web);
assert!(ctx.manual_cookie_header.is_none());
assert!(ctx.manual_cookie_missing);

let ctx = session_context(ProviderId::Xapi, Some("off"), "auto", Some(cookie));
assert_eq!(ctx.source_mode, SourceMode::Cli);
assert!(ctx.manual_cookie_header.is_none());
}
42 changes: 42 additions & 0 deletions apps/desktop-tauri/src-tauri/src/commands/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1437,6 +1437,48 @@ fn usage_item_descriptors_include_detail_sections() {
assert_eq!(hidden.title, "quota");
}

#[test]
fn usage_item_descriptors_skip_card_hidden_windows() {
let metadata = instantiate_provider(ProviderId::Codex).metadata().clone();
let result = ProviderFetchResult {
usage: codexbar::core::UsageSnapshot::new(
codexbar::core::RateWindow::informational("$1.00").hidden_on_card(),
)
.with_secondary(codexbar::core::RateWindow::new(20.0)),
cost: None,
wayfinder_usage: None,
inventory: Vec::new(),
reset_credits: None,
display_details: vec![
codexbar::core::ProviderDisplayDetail::new("spend", "Spend", "$1.00")
.expect("valid detail"),
],
source_label: "api".to_string(),
has_successful_claude_cli_quota: false,
pace_authoritative: true,
account_identity: None,
open_ai_api_usage: None,
last_good_owner: None,
};
let mut snapshot =
ProviderUsageSnapshot::from_fetch_result(ProviderId::Codex, &metadata, &result, None, None);
let mut hidden_extra = snapshot.primary.clone();
hidden_extra.hidden_on_card = true;
snapshot.extra_rate_windows = vec![NamedRateWindowSnapshot {
id: "hidden".to_string(),
title: "Hidden".to_string(),
window: hidden_extra,
fallback_lane: false,
icon_fallback: None,
}];

let items =
super::usage_item_descriptors(Some(&snapshot), &Settings::default(), ProviderId::Codex);
let ids: Vec<&str> = items.iter().map(|item| item.id.as_str()).collect();

assert_eq!(ids, ["detailSection:Spend", "metric:secondary"]);
}

#[test]
fn pace_stage_serializes_to_snake_case_string() {
use codexbar::core::PaceStage;
Expand Down
40 changes: 30 additions & 10 deletions apps/desktop-tauri/src-tauri/src/commands/usage_items.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,33 +150,53 @@ pub(crate) fn usage_item_descriptors(
};

if let Some(snapshot) = snapshot {
let session = locale::get_text(lang, LocaleKey::ProviderSessionLabel);
push(
"primary",
snapshot.primary_label.as_deref().unwrap_or(&session),
true,
);
if snapshot.secondary.is_some() {
// A card-hidden window is never drawn as a row, so a visibility
// toggle for it would control nothing.
if !snapshot.primary.hidden_on_card {
let session = locale::get_text(lang, LocaleKey::ProviderSessionLabel);
push(
"primary",
snapshot.primary_label.as_deref().unwrap_or(&session),
true,
);
}
if snapshot
.secondary
.as_ref()
.is_some_and(|w| !w.hidden_on_card)
{
let weekly = locale::get_text(lang, LocaleKey::ProviderWeeklyLabel);
push(
"secondary",
snapshot.secondary_label.as_deref().unwrap_or(&weekly),
true,
);
}
if snapshot.model_specific.is_some() {
if snapshot
.model_specific
.as_ref()
.is_some_and(|w| !w.hidden_on_card)
{
let model = locale::get_text(lang, LocaleKey::DetailWindowModelSpecific);
push("model-specific", &model, true);
}
if snapshot.tertiary.is_some() {
if snapshot
.tertiary
.as_ref()
.is_some_and(|w| !w.hidden_on_card)
{
let tertiary = locale::get_text(lang, LocaleKey::DetailWindowTertiary);
push(
"tertiary",
snapshot.tertiary_label.as_deref().unwrap_or(&tertiary),
true,
);
}
for extra in &snapshot.extra_rate_windows {
for extra in snapshot
.extra_rate_windows
.iter()
.filter(|extra| !extra.window.hidden_on_card)
{
push(&format!("extra-{}", extra.id), &extra.title, true);
}
}
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions apps/desktop-tauri/src/components/providers/providerIcons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import notion from "./icons/ProviderIcon-notion.svg?raw";
import nous from "./icons/ProviderIcon-nous.svg?raw";
import workbuddy from "./icons/ProviderIcon-workbuddy.svg?raw";
import xai from "./icons/ProviderIcon-xai.svg?raw";
import xapi from "./icons/ProviderIcon-xapi.svg?raw";
import xkiro from "./icons/ProviderIcon-xkiro.svg?raw";
import ollama from "./icons/ProviderIcon-ollama.svg?raw";
import opencode from "./icons/ProviderIcon-opencode.svg?raw";
Expand Down Expand Up @@ -154,6 +155,7 @@ const RAW: Record<string, string> = {
notion: tint(notion),
nous: tint(nous),
xai: tint(xai),
xapi: tint(xapi),
xkiro: tint(xkiro),
mistral: tint(mistral),
muse: tint(muse),
Expand Down Expand Up @@ -282,6 +284,7 @@ export const PROVIDER_ICON_REGISTRY: Record<string, ProviderIcon> = {
notion: { id: "notion", brandColor: "#337EA9", fallbackLetter: "N", svgPath: RAW.notion },
nous: { id: "nous", brandColor: "#D6A55C", fallbackLetter: "N", svgPath: RAW.nous },
xai: { id: "xai", brandColor: "#8e8e93", fallbackLetter: "X", svgPath: RAW.xai },
xapi: { id: "xapi", brandColor: "#71767B", fallbackLetter: "X", svgPath: RAW.xapi },
xkiro: { id: "xkiro", brandColor: "#52c99b", fallbackLetter: "X", svgPath: RAW.xkiro },
meta: { id: "meta", brandColor: "#0467DF", fallbackLetter: "M", svgPath: RAW.meta },
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,22 @@ describe("UsageSection", () => {
expect(screen.getByText("58% left")).toBeInTheDocument();
});

it("omits card-hidden windows, as the tray card does", async () => {
const detail = provider();
detail.session = { ...rateWindow(0), hiddenOnCard: true };
detail.primaryLabel = "Spend";

render(
<LocaleProvider>
<UsageSection provider={detail} resetTimeRelative={true} t={(key) => key} />
</LocaleProvider>,
);

expect(await screen.findByText("Additional Budget")).toBeInTheDocument();
expect(screen.queryByText("Spend")).not.toBeInTheDocument();
expect(screen.queryByText("100% left")).not.toBeInTheDocument();
});

it.each([false, true])(
"renders detail-backed amounts as a detail line, not reset text (resetsAt: %s)",
async (hasReset) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,14 +88,17 @@ export function UsageSection({ provider, resetTimeRelative, display, accentColor
});
}

// Card-hidden windows only feed the tray icon and float bar, as on MenuCard.
const shownBars = bars.filter((bar) => !bar.rate.hiddenOnCard);

// Cached snapshots outlive the pool reset; re-check blocks on the clock.
const monthlyLimitBlockNow = useMonthlyLimitBlockNow(
bars.map((bar) => bar.rate.monthlyLimitBlock),
shownBars.map((bar) => bar.rate.monthlyLimitBlock),
);
const inventory = provider.inventory ?? [];
const displayDetails = provider.displayDetails ?? [];
const displayDetailGroups = groupProviderDisplayDetails(displayDetails);
if (bars.length === 0 && inventory.length === 0 && displayDetails.length === 0) {
if (shownBars.length === 0 && inventory.length === 0 && displayDetails.length === 0) {
return null;
}

Expand All @@ -105,7 +108,7 @@ export function UsageSection({ provider, resetTimeRelative, display, accentColor
style={accentColor ? ({ "--provider-accent": accentColor } as CSSProperties) : undefined}
>
<h4>{t("ProviderUsage")}</h4>
{bars.map((b) => (
{shownBars.map((b) => (
<CardMetricRow
key={b.key}
title={b.label}
Expand Down
1 change: 1 addition & 0 deletions apps/desktop-tauri/src/test/providerCatalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,5 @@ export const TEST_PROVIDER_CATALOG: Array<[string, string]> = [
["lithosai", "LithosAI"],
["museai", "Muse (muse.ai)"],
["workbuddy", "WorkBuddy"],
["xapi", "X API"],
];
26 changes: 26 additions & 0 deletions docs/PROVIDERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,32 @@ portal in Settings → Providers → IBM Bob, as token accounts, or with
- 401 and 403 report that the key was rejected; response bodies are never
echoed in errors.

### X API prepaid credits

X API is disabled by default and reads prepaid credit balances from the X
(Twitter) API console. Sign in to console.x.com in your browser, and CodexBar
will read the `auth_token` and `ct0` cookies from that session.

- **Automatic:** A previously imported Cookie header is used on its own.
Otherwise CodexBar tries each detected browser (Chrome, Edge, Brave,
Firefox) holding both cookies. If a browser session expires (401), it moves
to the next browser. No session means sign in to console.x.com.
- **Manual:** Paste the Cookie header from a console.x.com request (with
`auth_token` and `ct0`). Manual mode does not fall back to browser sessions.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- **Off:** Cookies are disabled.

The primary lane shows the balance (paid + free credits). Details rows show:
- Balance (total: paid + free)
- Purchased credits
- Free credits

Balance is a prepaid meter (no quota percentage); costs are always zero (no
usage tracking available). The provider reports identity as "Browser session".
Cost resets are never inferred. Session rotation on 401 is implemented: sessions
are tried in browser order, stale sessions are skipped, and a missing session
after all attempts triggers "sign in to console.x.com". Errors never echo
response bodies.

## Listing what is enabled

```powershell
Expand Down
10 changes: 9 additions & 1 deletion rust/src/core/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ pub enum ProviderId {
QwenCloud,
Notion,
Xai,
Xapi,
Fireworks,
AtlasCloud,
#[serde(alias = "metaspark")]
Expand Down Expand Up @@ -190,6 +191,7 @@ impl ProviderId {
ProviderId::QwenCloud,
ProviderId::Notion,
ProviderId::Xai,
ProviderId::Xapi,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
ProviderId::Fireworks,
ProviderId::AtlasCloud,
ProviderId::Meta,
Expand Down Expand Up @@ -312,6 +314,7 @@ impl ProviderId {
ProviderId::QwenCloud => "qwen-cloud",
ProviderId::Notion => "notion",
ProviderId::Xai => "xai",
ProviderId::Xapi => "xapi",
ProviderId::Replicate => "replicate",
ProviderId::AtlasCloud => "atlascloud",
}
Expand Down Expand Up @@ -416,6 +419,7 @@ impl ProviderId {
ProviderId::QwenCloud => "Qwen Cloud",
ProviderId::Notion => "Notion AI",
ProviderId::Xai => "xAI",
ProviderId::Xapi => "X API",
ProviderId::Replicate => "Replicate",
ProviderId::AtlasCloud => "Atlas Cloud",
}
Expand Down Expand Up @@ -526,6 +530,7 @@ impl ProviderId {
ProviderId::ZoomMate => Some("zoommate.zoom.us"),
ProviderId::QwenCloud => Some("qwencloud.com"),
ProviderId::Notion => Some("app.notion.com"),
ProviderId::Xapi => Some("x.com"),
ProviderId::Xai => None,
}
}
Expand Down Expand Up @@ -621,6 +626,7 @@ impl ProviderId {
}
"grok" | "supergrok" | "super-grok" => Some(ProviderId::Grok),
"xai" | "x.ai" | "x-ai" => Some(ProviderId::Xai),
"xapi" | "x-api" | "x api" => Some(ProviderId::Xapi),
"elevenlabs" | "eleven-labs" | "11labs" => Some(ProviderId::ElevenLabs),
"deepgram" | "dg" => Some(ProviderId::Deepgram),
"groq" | "groqcloud" | "groq-cloud" | "groq cloud" => Some(ProviderId::Groq),
Expand Down Expand Up @@ -1355,6 +1361,7 @@ pub fn brand_color(id: ProviderId) -> &'static str {
ProviderId::QwenCloud => "#615CED",
ProviderId::Notion => "#337EA9",
ProviderId::Xai => "#8E8E93",
ProviderId::Xapi => "#71767B",
ProviderId::Fireworks => "#F25B1C",
ProviderId::Meta => "#0467DF",
ProviderId::Muse => "#0668E1",
Expand Down Expand Up @@ -1394,7 +1401,7 @@ mod tests {
#[test]
fn test_provider_id_all() {
let all = ProviderId::all();
assert_eq!(all.len(), 96);
assert_eq!(all.len(), 97);
assert!(all.contains(&ProviderId::Claude));
assert!(all.contains(&ProviderId::Codex));
assert!(all.contains(&ProviderId::Pi));
Expand Down Expand Up @@ -1450,6 +1457,7 @@ mod tests {
assert!(all.contains(&ProviderId::QwenCloud));
assert!(all.contains(&ProviderId::Notion));
assert!(all.contains(&ProviderId::Xai));
assert!(all.contains(&ProviderId::Xapi));
assert!(all.contains(&ProviderId::Meta));
assert!(all.contains(&ProviderId::Replicate));
assert!(all.contains(&ProviderId::Muse));
Expand Down
Loading