Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -116,6 +116,7 @@ See the full history in [CHANGELOG.md](CHANGELOG.md).
| Grok | Cookies / auth.json | Billing |
| Helmcode (also NaN Builders) | Browser cookies / manual Cookie header | Per-model token quotas, reset windows, Helmcode prepaid balance |
| Replicate | Cookies / token accounts | Monthly spend, credit balance |
| Aixy | API Key / token accounts | Applicable budget balances, 7-day key usage |
| ElevenLabs | API Key | Subscription Credits, Voice Slots |
| Deepgram | API Key | Project Usage |
| Groq | API Key | Enterprise Metrics |
Expand Down
3 changes: 3 additions & 0 deletions apps/desktop-tauri/src-tauri/src/commands/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,8 @@ pub struct ProviderDisplayProgressSnapshot {
#[serde(rename_all = "camelCase")]
pub struct ProviderDisplayDetailSnapshot {
pub id: String,
#[serde(default)]
pub section_title: Option<String>,
pub title: String,
pub value: String,
pub secondary_value: Option<String>,
Expand Down Expand Up @@ -438,6 +440,7 @@ impl ProviderUsageSnapshot {
.iter()
.map(|detail| ProviderDisplayDetailSnapshot {
id: detail.id().to_string(),
section_title: detail.section_title().map(ToOwned::to_owned),
title: detail.title().to_string(),
value: detail.value().to_string(),
secondary_value: detail.secondary_value().map(ToOwned::to_owned),
Expand Down
40 changes: 39 additions & 1 deletion apps/desktop-tauri/src-tauri/src/commands/provider_settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -400,15 +400,48 @@ mod tests {
);
}

#[test]
fn fetch_context_carries_saved_gateway_urls_for_every_gateway_provider() {
use codexbar::settings::{ApiKeys, ManualCookies, Settings};
use std::collections::HashMap;

let mut settings = Settings::default();
for (id, url) in [
(ProviderId::Wayfinder, "http://localhost:8787"),
(ProviderId::Bifrost, "https://bifrost.example.com"),
(ProviderId::Aixy, "https://aixy.example.com/prefix"),
] {
settings.set_gateway_url(id, url);
let ctx = super::super::providers::build_fetch_context(
id,
&settings,
&ManualCookies::default(),
&ApiKeys::default(),
&HashMap::new(),
);
assert_eq!(ctx.gateway_url.as_deref(), Some(url), "{id:?}");
}

let ctx = super::super::providers::build_fetch_context(
ProviderId::Codex,
&settings,
&ManualCookies::default(),
&ApiKeys::default(),
&HashMap::new(),
);
assert_eq!(ctx.gateway_url, None);
}

#[test]
fn maps_gitkraken_organization_provider() {
assert_eq!(workspace_provider("gitkraken"), Some(ProviderId::GitKraken));
}

#[test]
fn gateway_provider_exposes_wayfinder_and_bifrost_only() {
fn gateway_provider_exposes_gateway_providers_only() {
assert_eq!(gateway_provider("wayfinder"), Some(ProviderId::Wayfinder));
assert_eq!(gateway_provider("bifrost"), Some(ProviderId::Bifrost));
assert_eq!(gateway_provider("aixy"), Some(ProviderId::Aixy));
assert_eq!(gateway_provider("codex"), None);
}

Expand Down Expand Up @@ -466,6 +499,7 @@ fn gateway_provider(provider_id: &str) -> Option<codexbar::core::ProviderId> {
match provider_id {
"wayfinder" => Some(codexbar::core::ProviderId::Wayfinder),
"bifrost" => Some(codexbar::core::ProviderId::Bifrost),
"aixy" => Some(codexbar::core::ProviderId::Aixy),
_ => None,
}
}
Expand All @@ -491,6 +525,10 @@ pub fn set_provider_gateway_url(provider_id: String, gateway_url: String) -> Res
codexbar::providers::bifrost::validate_gateway_url(gateway_url)
.map_err(|error| error.to_string())?;
}
codexbar::core::ProviderId::Aixy => {
codexbar::providers::aixy::validate_gateway_url(gateway_url)
.map_err(|error| error.to_string())?;
}
_ => unreachable!("gateway_provider only returns gateway providers"),
}

Expand Down
7 changes: 5 additions & 2 deletions apps/desktop-tauri/src-tauri/src/commands/providers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,8 +214,11 @@ pub(crate) fn build_fetch_context(

let workspace_id = settings.workspace_id(id).trim().to_string();
let api_region = settings.api_region(id).trim().to_string();
let gateway_url = (id == ProviderId::Wayfinder && !settings.gateway_url(id).is_empty())
.then(|| settings.gateway_url(id).to_string());
// Every gateway-style provider (Wayfinder, Bifrost, Aixy) stores its base
// URL here; providers without one report an empty string.
let gateway_url = Some(settings.gateway_url(id))
.filter(|url| !url.is_empty())
.map(str::to_owned);
// Local-first Auto providers (OpenCode Go) flip to web-first when a
// token account or manual cookie source scopes the session to web creds.
let auto_prefer_web = token_override.is_some() || cookie_source == "manual";
Expand Down
10 changes: 10 additions & 0 deletions apps/desktop-tauri/src-tauri/src/commands/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1115,6 +1115,7 @@ fn provider_inventory_maps_to_the_bridge_without_token_ids() {
.with_display_detail(
ProviderDisplayDetail::new("credits", "Used this cycle", "12")
.and_then(|row| row.with_secondary_value("Monthly refill: 100"))
.and_then(|row| row.with_section_title("Credit usage"))
.and_then(|row| row.with_progress(12.0, 100.0)),
);
let metadata = instantiate_provider(ProviderId::Grok).metadata().clone();
Expand All @@ -1129,6 +1130,15 @@ fn provider_inventory_maps_to_the_bridge_without_token_ids() {
);
assert_eq!(snapshot.display_details.len(), 1);
assert_eq!(snapshot.display_details[0].value, "12");
assert_eq!(
snapshot.display_details[0].section_title.as_deref(),
Some("Credit usage")
);
let snapshot_json = serde_json::to_value(&snapshot).unwrap();
assert_eq!(
snapshot_json["displayDetails"][0]["sectionTitle"],
"Credit usage"
);
assert_eq!(
snapshot.display_details[0].secondary_value.as_deref(),
Some("Monthly refill: 100")
Expand Down
74 changes: 74 additions & 0 deletions apps/desktop-tauri/src/components/MenuCard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,80 @@ describe("MenuCard", () => {
expect(details.open).toBe(true);
});

it("renders provider detail rows once with separated secondary values and accessible progress", async () => {
const snapshot = provider(null);
snapshot.providerId = "aixy";
snapshot.displayName = "Aixy";
snapshot.displayDetails = [
{
id: "budget-0",
sectionTitle: "Applicable budgets",
title: "Project · Monthly · Shared · Hard",
value: "$70.00 / $100.00 remaining",
secondaryValue: "$20.00 spent · $10.00 reserved",
progress: { used: 30, total: 100 },
},
];

const { container } = renderCard(snapshot);

await screen.findByRole("progressbar", {
name: "Project · Monthly · Shared · Hard progress",
});
expect(container.querySelectorAll(".menu-card__provider-details")).toHaveLength(1);
expect(container.textContent).toContain(
"$70.00 / $100.00 remaining $20.00 spent · $10.00 reserved",
);
expect(
screen.getByRole("progressbar", {
name: "Project · Monthly · Shared · Hard progress",
}),
).toHaveAttribute("aria-valuenow", "30");
});

it("hides provider display details in compact overview cards", () => {
const snapshot = provider(null);
snapshot.providerId = "aixy";
snapshot.displayDetails = [
{
id: "budget-0",
sectionTitle: "Applicable budgets",
title: "Project · Monthly · Shared · Hard",
value: "$70.00 / $100.00 remaining",
secondaryValue: "$20.00 spent · $10.00 reserved",
progress: { used: 30, total: 100 },
},
];

const { container } = renderCard(snapshot, { compactOverview: true });

expect(container.querySelector(".menu-card__provider-details")).not.toBeInTheDocument();
});

it("splits provider detail sections after 24 rows", async () => {
const snapshot = provider(null);
snapshot.providerId = "aixy";
snapshot.displayDetails = Array.from({ length: 25 }, (_, index) => ({
id: `budget-${index}`,
sectionTitle: "Applicable budgets",
title: `Budget ${index}`,
value: "$0.00 remaining",
secondaryValue: null,
progress: null,
}));

const { container } = renderCard(snapshot);

await screen.findAllByRole("heading", { name: "Applicable budgets" });
const sections = container.querySelectorAll(".menu-card__provider-details");
expect(sections).toHaveLength(2);
expect(
screen.getAllByRole("heading", { name: "Applicable budgets" }),
).toHaveLength(2);
expect(sections[0].querySelectorAll(".menu-card__cost-line")).toHaveLength(24);
expect(sections[1].querySelectorAll(".menu-card__cost-line")).toHaveLength(1);
});

it("places Claude accounts above metrics and the collapsed usage details", async () => {
tauriMocks.claudeAccountsList.mockResolvedValue([
{ id: "a", email: "a@example.com", organization: "Personal", isActive: true, isSaved: true },
Expand Down
66 changes: 29 additions & 37 deletions apps/desktop-tauri/src/components/MenuCardDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { useState } from "react";
import type {
CostSummaryDisplayStyle,
DailyCostPoint,
ProviderDisplayDetail,
PaceSnapshot,
ProviderInventoryItem,
ProviderChartData,
Expand All @@ -22,6 +21,10 @@ import type { LocaleKey } from "../i18n/keys";
import { paceCategory } from "../surfaces/tray/paceCategory";
import { SimpleBarChart, StackedBarChart } from "./MiniBarChart";
import { InventoryItemRow } from "./InventoryRows";
import {
groupProviderDisplayDetails,
ProviderDisplayRow,
} from "./ProviderDisplayRow";
import { QuotaWindowHistory } from "./QuotaWindowHistory";
import { getPaceBudget, type PaceBudget } from "../lib/paceBudget";
import PaceDetailsChart from "./PaceDetailsChart";
Expand Down Expand Up @@ -541,6 +544,9 @@ export default function MenuCardDetails({
);
const localCostHistory = chartData?.costHistory ?? [];
const costStyle = display.costSummaryDisplayStyle ?? "detailed";
const displayDetailGroups = groupProviderDisplayDetails(
provider.displayDetails ?? [],
);

const {
hasMetrics,
Expand Down Expand Up @@ -596,19 +602,28 @@ export default function MenuCardDetails({
</section>
)}
{!provider.error && hasDisplayDetails && !compactOverview && (
<section className="menu-card__group menu-card__provider-details">
{provider.displayDetails?.map((detail, index) => (
<DisplayDetailRow key={`${detail.id}-${index}`} detail={detail} />
))}
</section>
)}

{!provider.error && hasDisplayDetails && (
<section className="menu-card__group menu-card__provider-details">
{provider.displayDetails?.map((detail, index) => (
<DisplayDetailRow key={`${detail.id}-${index}`} detail={detail} />
))}
</section>
displayDetailGroups.map((group) => (
<section
className="menu-card__group menu-card__provider-details"
key={group.id}
>
{group.title && (
<div className="menu-card__group-title" role="heading" aria-level={4}>
{group.title}
</div>
)}
{group.rows.map((detail, index) => (
<ProviderDisplayRow
key={`${detail.id}-${index}`}
detail={detail}
lineClassName="menu-card__cost-line"
secondaryClassName="menu-card__cost-line--muted"
trackClassName="menu-metric__bar"
fillClassName="menu-metric__bar-fill"
/>
))}
</section>
))
)}

{wayfinderUsage && !compactOverview && <WayfinderUsageBlock usage={wayfinderUsage} />}
Expand Down Expand Up @@ -786,26 +801,3 @@ export default function MenuCardDetails({
</div>
);
}

function DisplayDetailRow({ detail }: { detail: ProviderDisplayDetail }) {
const progress = detail.progress;
const progressPercent = progress && Number.isFinite(progress.used) && Number.isFinite(progress.total) && progress.total > 0
? Math.max(0, Math.min(100, (progress.used / progress.total) * 100))
: null;

return (
<div className="menu-card__provider-detail">
<div className="menu-card__cost-line">
<span>{detail.title}: {detail.value}</span>
{detail.secondaryValue && (
<span className="menu-card__cost-line--muted">{detail.secondaryValue}</span>
)}
</div>
{progressPercent != null && (
<div className="menu-metric__bar" aria-label={`${detail.title} progress`}>
<div className="menu-metric__bar-fill" style={{ width: `${progressPercent}%` }} />
</div>
)}
</div>
);
}
43 changes: 41 additions & 2 deletions apps/desktop-tauri/src/components/ProviderDisplayRow.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,34 @@
import type { ProviderDisplayDetail } from "../types/bridge";

const MAX_ROWS_PER_SECTION = 24;

export interface ProviderDisplayDetailGroup {
id: number;
title: string | null;
rows: ProviderDisplayDetail[];
}

/** Group consecutive provider details and cap each rendered section. */
export function groupProviderDisplayDetails(
details: ProviderDisplayDetail[],
): ProviderDisplayDetailGroup[] {
const groups: ProviderDisplayDetailGroup[] = [];
for (const detail of details) {
const title = detail.sectionTitle ?? null;
const current = groups[groups.length - 1];
if (
current &&
current.title === title &&
current.rows.length < MAX_ROWS_PER_SECTION
) {
current.rows.push(detail);
} else {
groups.push({ id: groups.length, title, rows: [detail] });
}
}
return groups;
}

/**
* One transient provider detail line: "{title}: {value} [secondary]"
* plus an optional clamped progress bar.
Expand Down Expand Up @@ -34,11 +63,21 @@ export function ProviderDisplayRow({
<div className={lineClassName}>
<span>{detail.title}: {detail.value}</span>
{detail.secondaryValue && secondaryClassName && (
<span className={secondaryClassName}>{detail.secondaryValue}</span>
<>
{" "}
<span className={secondaryClassName}>{detail.secondaryValue}</span>
</>
)}
</div>
{progressPercent != null && (
<div className={trackClassName} aria-label={`${detail.title} progress`}>
<div
className={trackClassName}
role="progressbar"
aria-label={`${detail.title} progress`}
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={progressPercent}
>
<div className={fillClassName} style={{ width: `${progressPercent}%` }} />
</div>
)}
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 @@ -20,6 +20,7 @@ import deepgram from "./icons/ProviderIcon-deepgram.svg?raw";
import deepinfra from "./icons/ProviderIcon-deepinfra.svg?raw";
import fireworks from "./icons/ProviderIcon-fireworks.svg?raw";
import aiand from "./icons/ProviderIcon-aiand.svg?raw";
import aixy from "./icons/ProviderIcon-aixy.svg?raw";
import clinepass from "./icons/ProviderIcon-clinepass.svg?raw";
import longcat from "./icons/ProviderIcon-longcat.svg?raw";
import neuralwatt from "./icons/ProviderIcon-neuralwatt.svg?raw";
Expand Down Expand Up @@ -107,6 +108,7 @@ const RAW: Record<string, string> = {
deepinfra: tint(deepinfra),
fireworks: tint(fireworks),
aiand: tint(aiand),
aixy: tint(aixy),
clinepass: tint(clinepass),
longcat: tint(longcat),
neuralwatt: tint(neuralwatt),
Expand Down Expand Up @@ -186,6 +188,7 @@ export const PROVIDER_ICON_REGISTRY: Record<string, ProviderIcon> = {
grok: { id: "grok", brandColor: "#111827", fallbackLetter: "G", svgPath: RAW.grok },
groq: { id: "groq", brandColor: "#f55036", fallbackLetter: "G", svgPath: RAW.groq },
bifrost: { id: "bifrost", brandColor: "#5b7cfa", fallbackLetter: "B" },
aixy: { id: "aixy", brandColor: "#123650", fallbackLetter: "A", svgPath: RAW.aixy },
gitkraken: { id: "gitkraken", brandColor: "#179287", fallbackLetter: "G" },
huggingface: { id: "huggingface", brandColor: "#ffd21e", fallbackLetter: "H", svgPath: RAW.huggingface },
hyper: { id: "hyper", brandColor: "#7c3aed", fallbackLetter: "H" },
Expand Down
Loading