Skip to content
Merged
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

## [Windows] Unreleased

- Settings providers: show only enabled providers from the sidebar options menu, search by CLI alias (such as `openai` for Codex), restore the default provider order, and undo a drag reorder or a restore with Ctrl+Z or the options menu. Hidden providers keep their slots when the order changes (upstream #4440).
- Add **Color by provider** to Settings > Menu Bar (off by default): tints the tray usage bars and percent text with the shown provider's brand or accent color, keeps pace colors and the error colors unchanged, and falls back to the usual colors when a tint would be hard to see on the tray background (upstream #4321).
- OpenAI API: each Admin refresh also reads the last hour of 1-minute usage from the Admin Usage API (input plus output tokens, cached input included, Batch API traffic kept out of limit headroom) and carries a Tokens/min grid with a reporting frontier, so buckets that arrive late show as pending instead of zero. The backend can also load 15-minute, 1-hour or 6-hour windows, read per-project rate-limit caps, and measure headroom against manual per-model organization limits stored in settings. The per-refresh request can be turned off, and a failure never affects the daily usage. The chart, menu bar token and Settings controls come in a later release (upstream #4495).
- **Claude**: show the plan renewal or expiry date from claude.ai billing under the quota rows when your cookie session supplies it, and make saved limit-reset credits, Extra usage (including a prepaid balance), and plan dates independently selectable under Visible usage items. Existing settings keep all three visible; Admin API spend is unaffected (upstream #4324, #4484).
Expand Down
9 changes: 9 additions & 0 deletions apps/desktop-tauri/src-tauri/src/commands/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -945,6 +945,8 @@ pub struct ProviderCatalogEntry {
pub(crate) id: String,
pub(crate) display_name: String,
pub(crate) cookie_domain: Option<String>,
/// CLI aliases the Settings search also matches (see `ProviderId::cli_aliases`).
pub(crate) cli_aliases: Vec<String>,
}

/// One provider's service status for the sidebar dot and the tray panel.
Expand Down Expand Up @@ -1019,6 +1021,7 @@ pub struct SettingsSnapshot {
menu_bar_color_by_provider: bool,
menu_bar_pace_reserve_positive: bool,
providers_sorted_alphabetically: bool,
providers_enabled_only: bool,
show_as_used: bool,
show_all_token_accounts_in_menu: bool,
enable_animations: bool,
Expand Down Expand Up @@ -1189,6 +1192,7 @@ impl From<Settings> for SettingsSnapshot {
menu_bar_color_by_provider: settings.menu_bar_color_by_provider,
menu_bar_pace_reserve_positive: settings.menu_bar_pace_reserve_positive,
providers_sorted_alphabetically: settings.providers_sorted_alphabetically,
providers_enabled_only: settings.providers_enabled_only,
show_as_used: settings.show_as_used,
show_all_token_accounts_in_menu: settings.show_all_token_accounts_in_menu,
enable_animations: settings.enable_animations,
Expand Down Expand Up @@ -1284,6 +1288,11 @@ pub(crate) fn provider_catalog_for(settings: &Settings) -> Vec<ProviderCatalogEn
id: provider.cli_name().to_string(),
display_name: provider.display_name().to_string(),
cookie_domain: provider.cookie_domain().map(ToString::to_string),
cli_aliases: provider
.cli_aliases()
.iter()
.map(ToString::to_string)
.collect(),
})
.collect()
}
Expand Down
41 changes: 41 additions & 0 deletions apps/desktop-tauri/src-tauri/src/commands/provider_settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,47 @@ pub fn reorder_providers(
Ok(build_provider_summaries(&settings))
}

/// Save a new order for the providers the Settings sidebar lists. Only their
/// slots change: a provider the sidebar does not list keeps its position.
fn save_visible_provider_order(
app: &tauri::AppHandle,
visible: Vec<String>,
) -> Result<Vec<ProviderSummary>, String> {
let mut settings = Settings::load();
let full = settings.provider_display_order_names();
settings.provider_order = codexbar::settings::merge_visible_provider_order(&full, &visible)
.ok_or_else(|| "Provider order names an unknown or repeated provider".to_string())?;
settings.save().map_err(|e| e.to_string())?;
crate::tray_bridge::refresh_tray_presentation(app);
crate::events::emit_settings_changed(app);
Ok(build_provider_summaries(&settings))
}

/// Settings sidebar reorder: `ids` is the full visible order after the move.
#[tauri::command]
pub fn reorder_visible_providers(
app: tauri::AppHandle,
ids: Vec<String>,
) -> Result<Vec<ProviderSummary>, String> {
save_visible_provider_order(&app, ids)
}

/// Restore the built-in order for the providers the sidebar lists.
#[tauri::command]
pub fn restore_default_provider_order(
app: tauri::AppHandle,
) -> Result<Vec<ProviderSummary>, String> {
let visible: Vec<String> = {
let settings = Settings::load();
ProviderId::all()
.iter()
.filter(|provider| settings.is_provider_listed(**provider))
.map(|provider| provider.cli_name().to_string())
.collect()
};
save_visible_provider_order(&app, visible)
}

// ── Per-provider usage source ─────────────────────────────────────────

pub(crate) fn provider_usage_source_lookup(
Expand Down
4 changes: 4 additions & 0 deletions apps/desktop-tauri/src-tauri/src/commands/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ pub struct SettingsUpdate {
pub menu_bar_color_by_provider: Option<bool>,
pub menu_bar_pace_reserve_positive: Option<bool>,
pub providers_sorted_alphabetically: Option<bool>,
pub providers_enabled_only: Option<bool>,
pub show_as_used: Option<bool>,
pub show_all_token_accounts_in_menu: Option<bool>,
pub enable_animations: Option<bool>,
Expand Down Expand Up @@ -406,6 +407,9 @@ impl SettingsUpdate {
if let Some(v) = self.providers_sorted_alphabetically {
settings.providers_sorted_alphabetically = v;
}
if let Some(v) = self.providers_enabled_only {
settings.providers_enabled_only = v;
}
if let Some(v) = self.show_all_token_accounts_in_menu {
settings.show_all_token_accounts_in_menu = v;
}
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop-tauri/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,8 @@ fn main() {
commands::get_spend_contract,
commands::get_codex_workspaces_snapshot,
commands::reorder_providers,
commands::reorder_visible_providers,
commands::restore_default_provider_order,
commands::set_provider_cookie_source,
commands::set_provider_usage_source,
commands::set_provider_auto_resume_after_quota_reset,
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop-tauri/src-tauri/src/tray_bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -556,11 +556,13 @@ mod tests {
id: "codex".into(),
display_name: "Codex".into(),
cookie_domain: None,
cli_aliases: Vec::new(),
},
ProviderCatalogEntry {
id: "claude".into(),
display_name: "Claude".into(),
cookie_domain: None,
cli_aliases: Vec::new(),
},
]
}
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop-tauri/src-tauri/src/tray_menu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,11 +195,13 @@ mod tests {
id: "codex".into(),
display_name: "Codex".into(),
cookie_domain: None,
cli_aliases: Vec::new(),
},
ProviderCatalogEntry {
id: "claude".into(),
display_name: "Claude".into(),
cookie_domain: None,
cli_aliases: Vec::new(),
},
]
}
Expand Down
4 changes: 4 additions & 0 deletions apps/desktop-tauri/src/i18n/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -940,6 +940,10 @@ export const ALL_LOCALE_KEYS = [
"ProviderSidebarReorderHint",
"ProviderSidebarMoveUp",
"ProviderSidebarMoveDown",
"ProviderSidebarOptions",
"ProviderSidebarShowEnabledOnly",
"ProviderSidebarRestoreDefaultOrder",
"ProviderSidebarUndoOrder",
"SettingsSidebarSortProviders",
"SettingsSidebarSortOffHint",
"SettingsSidebarSortOnHint",
Expand Down
10 changes: 10 additions & 0 deletions apps/desktop-tauri/src/lib/tauri.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,16 @@ export function reorderProviders(ids: string[]): Promise<ProviderSummary[]> {
return invoke<ProviderSummary[]>("reorder_providers", { ids });
}

/** Settings sidebar reorder: `ids` is the full listed order; hidden providers keep their slots. */
export function reorderVisibleProviders(ids: string[]): Promise<ProviderSummary[]> {
return invoke<ProviderSummary[]>("reorder_visible_providers", { ids });
}

/** Restore the built-in order for the providers the Settings sidebar lists. */
export function restoreDefaultProviderOrder(): Promise<ProviderSummary[]> {
return invoke<ProviderSummary[]>("restore_default_provider_order");
}

export function getSettingsSnapshot(): Promise<SettingsSnapshot> {
return invoke<SettingsSnapshot>("get_settings_snapshot");
}
Expand Down
125 changes: 118 additions & 7 deletions apps/desktop-tauri/src/surfaces/Settings.test.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { fireEvent, render, screen, within } from "@testing-library/react";
import { fireEvent, render, screen, waitFor, within } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { BootstrapState, SurfaceTarget } from "../types/bridge";

const mocks = vi.hoisted(() => ({
update: vi.fn(),
reorderProviders: vi.fn(),
reorderVisibleProviders: vi.fn(),
restoreDefaultProviderOrder: vi.fn(),
setSurfaceMode: vi.fn(),
windowLabel: "settings",
shellTarget: null as SurfaceTarget | null,
Expand All @@ -27,7 +28,8 @@ vi.mock("../lib/tauri", () => ({
getWorkAreaRect: () => Promise.resolve(null),
getProviderStatuses: () => Promise.resolve([]),
setSurfaceMode: mocks.setSurfaceMode,
reorderProviders: mocks.reorderProviders,
reorderVisibleProviders: mocks.reorderVisibleProviders,
restoreDefaultProviderOrder: mocks.restoreDefaultProviderOrder,
}));
vi.mock("../hooks/useSettings", () => ({
useSettings: (settings: unknown) => ({
Expand Down Expand Up @@ -102,7 +104,8 @@ describe("Settings sidebar", () => {
vi.clearAllMocks();
mocks.windowLabel = "settings";
mocks.shellTarget = null;
mocks.reorderProviders.mockResolvedValue(undefined);
mocks.reorderVisibleProviders.mockResolvedValue([]);
mocks.restoreDefaultProviderOrder.mockResolvedValue([]);
});

it("lists the app panes in the macOS order, then the providers with an on count", () => {
Expand Down Expand Up @@ -165,7 +168,7 @@ describe("Settings sidebar", () => {
expect(sort).toHaveAttribute("title", "SettingsSidebarSortOnHint");

fireEvent.keyDown(row("codex"), { key: "ArrowDown", altKey: true });
expect(mocks.reorderProviders).not.toHaveBeenCalled();
expect(mocks.reorderVisibleProviders).not.toHaveBeenCalled();

fireEvent.click(sort);
expect(mocks.update).toHaveBeenCalledWith({ providersSortedAlphabetically: false });
Expand Down Expand Up @@ -195,15 +198,123 @@ describe("Settings sidebar", () => {
it("reorders with Alt+Arrow and saves the new order", () => {
render(<Settings state={state} />);
fireEvent.keyDown(row("codex"), { key: "ArrowDown", altKey: true });
expect(mocks.reorderProviders).toHaveBeenCalledWith(["zai", "codex", "claude", "cline", "grok"]);
expect(mocks.reorderVisibleProviders).toHaveBeenCalledWith([
"zai",
"codex",
"claude",
"cline",
"grok",
]);
expect(providerNames()).toEqual(["zai", "codex", "claude", "cline", "grok"]);
});

it("does not reorder while a search filters the list", () => {
render(<Settings state={state} />);
fireEvent.change(screen.getByRole("searchbox"), { target: { value: "c" } });
fireEvent.keyDown(row("codex"), { key: "ArrowDown", altKey: true });
expect(mocks.reorderProviders).not.toHaveBeenCalled();
expect(mocks.reorderVisibleProviders).not.toHaveBeenCalled();
});

const optionsButton = () => screen.getByRole("button", { name: "ProviderSidebarOptions" });
const undoItem = () => screen.getByRole("menuitem", { name: "ProviderSidebarUndoOrder" });

it("shows only enabled providers with the enabled-only option, and does not reorder then", () => {
render(<Settings state={makeState({ providersEnabledOnly: true })} />);
expect(providerNames()).toEqual(["codex", "zai", "grok"]);
fireEvent.keyDown(row("codex"), { key: "ArrowDown", altKey: true });
expect(mocks.reorderVisibleProviders).not.toHaveBeenCalled();

fireEvent.click(optionsButton());
expect(screen.getByRole("menuitemcheckbox", { name: "ProviderSidebarShowEnabledOnly" })).toHaveAttribute(
"aria-checked",
"true",
);
fireEvent.click(screen.getByRole("menuitemcheckbox", { name: "ProviderSidebarShowEnabledOnly" }));
expect(mocks.update).toHaveBeenCalledWith({ providersEnabledOnly: false });
});

it("turns the enabled-only option on from the options menu", () => {
render(<Settings state={state} />);
fireEvent.click(optionsButton());
fireEvent.click(screen.getByRole("menuitemcheckbox", { name: "ProviderSidebarShowEnabledOnly" }));
expect(mocks.update).toHaveBeenCalledWith({ providersEnabledOnly: true });
});

it("undoes the last drag reorder from the options menu", () => {
render(<Settings state={state} />);
fireEvent.keyDown(row("codex"), { key: "ArrowDown", altKey: true });
expect(providerNames()).toEqual(["zai", "codex", "claude", "cline", "grok"]);

fireEvent.click(optionsButton());
fireEvent.click(undoItem());
expect(mocks.reorderVisibleProviders).toHaveBeenLastCalledWith([
"codex",
"zai",
"claude",
"cline",
"grok",
]);
expect(providerNames()).toEqual(["codex", "zai", "claude", "cline", "grok"]);
});

it("undoes the last drag reorder with Ctrl+Z", () => {
const { container } = render(<Settings state={state} />);
fireEvent.keyDown(row("codex"), { key: "ArrowDown", altKey: true });
fireEvent.keyDown(container.querySelector(".settings-sidebar")!, { key: "z", ctrlKey: true });
expect(mocks.reorderVisibleProviders).toHaveBeenLastCalledWith([
"codex",
"zai",
"claude",
"cline",
"grok",
]);
});

it("drops the undo once the saved order changes outside the sidebar", () => {
const { rerender } = render(<Settings state={state} />);
fireEvent.keyDown(row("codex"), { key: "ArrowDown", altKey: true });
fireEvent.click(optionsButton());
expect(undoItem()).toBeEnabled();
fireEvent.click(optionsButton());

const moved = [...providers].reverse();
rerender(<Settings state={{ ...state, providers: moved } as BootstrapState} />);
fireEvent.click(optionsButton());
expect(undoItem()).toBeDisabled();
});

it("restores the default order and turns sorting off, and undo brings both back", async () => {
mocks.restoreDefaultProviderOrder.mockResolvedValue(
providers.map((p, order) => ({ id: p.id, displayName: p.displayName, enabled: true, order })),
);
render(<Settings state={makeState({ providersSortedAlphabetically: true })} />);
fireEvent.click(optionsButton());
fireEvent.click(screen.getByRole("menuitem", { name: "ProviderSidebarRestoreDefaultOrder" }));
expect(mocks.update).toHaveBeenCalledWith({ providersSortedAlphabetically: false });
expect(mocks.restoreDefaultProviderOrder).toHaveBeenCalledTimes(1);

fireEvent.click(optionsButton());
await waitFor(() => expect(undoItem()).toBeEnabled());
fireEvent.click(undoItem());
expect(mocks.update).toHaveBeenLastCalledWith({ providersSortedAlphabetically: true });
expect(mocks.reorderVisibleProviders).toHaveBeenLastCalledWith([
"codex",
"zai",
"claude",
"cline",
"grok",
]);
});

it("turns sorting back on when restoring the default order fails", async () => {
mocks.restoreDefaultProviderOrder.mockRejectedValue(new Error("save failed"));
render(<Settings state={makeState({ providersSortedAlphabetically: true })} />);
fireEvent.click(optionsButton());
fireEvent.click(screen.getByRole("menuitem", { name: "ProviderSidebarRestoreDefaultOrder" }));
expect(mocks.update).toHaveBeenCalledWith({ providersSortedAlphabetically: false });
await waitFor(() =>
expect(mocks.update).toHaveBeenLastCalledWith({ providersSortedAlphabetically: true }),
);
});

it("drives the main window surface only outside the detached window", () => {
Expand Down
Loading