diff --git a/apps/desktop-tauri/src-tauri/src/commands/claude_accounts.rs b/apps/desktop-tauri/src-tauri/src/commands/claude_accounts.rs index 0186abeebb..27fa2af84d 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/claude_accounts.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/claude_accounts.rs @@ -17,6 +17,8 @@ pub fn claude_accounts_list() -> Result, String> { fn changed(app: &tauri::AppHandle) { let _emit = app.emit("claude-accounts-updated", ()); + let handle = app.clone(); + let _dispatch = app.run_on_main_thread(move || crate::tray_bridge::rebuild_tray_menu(&handle)); } #[tauri::command] diff --git a/apps/desktop-tauri/src-tauri/src/commands/codex_accounts.rs b/apps/desktop-tauri/src-tauri/src/commands/codex_accounts.rs index 7d8510bd0f..a40b4cc336 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/codex_accounts.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/codex_accounts.rs @@ -411,6 +411,8 @@ fn refresh_persisted_accounts(app: tauri::AppHandle) -> Result<(), String> { fn accounts_changed(app: &tauri::AppHandle) { events::emit_codex_accounts_updated(app); + let handle = app.clone(); + let _ = app.run_on_main_thread(move || crate::tray_bridge::rebuild_tray_menu(&handle)); } fn into_user_message(error: CodexAccountManagerError) -> String { diff --git a/apps/desktop-tauri/src-tauri/src/main.rs b/apps/desktop-tauri/src-tauri/src/main.rs index dc139423cd..131d854840 100644 --- a/apps/desktop-tauri/src-tauri/src/main.rs +++ b/apps/desktop-tauri/src-tauri/src/main.rs @@ -15,6 +15,7 @@ mod shortcut_bridge; mod state; mod surface; mod surface_target; +mod tray_accounts; mod tray_bridge; mod tray_menu; mod tray_visibility; diff --git a/apps/desktop-tauri/src-tauri/src/tray_accounts.rs b/apps/desktop-tauri/src-tauri/src/tray_accounts.rs new file mode 100644 index 0000000000..44b6355303 --- /dev/null +++ b/apps/desktop-tauri/src-tauri/src/tray_accounts.rs @@ -0,0 +1,373 @@ +//! Account-specific native tray menu construction and dispatch. +//! +//! Keep provider/account workflows out of the generic tray shell so adding a +//! new account action does not grow `tray_bridge.rs` into another controller. + +use codexbar::codex_accounts::CodexAccount; +use codexbar::locale::{self, LocaleKey}; +use codexbar::settings::{Language, Settings}; +use tauri::AppHandle; + +use crate::tray_menu::TrayMenuEntry; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum AccountMenuAction { + AddCodexAccount, + AddClaudeAccount, + SaveClaudeAccount, + CancelClaudeLogin, + SwitchClaudeAccount(String), + SwitchCodexAccount(String), +} + +pub(crate) fn prepend_account_menus(spec: &mut Vec, settings: &Settings) { + let accounts = crate::commands::load_codex_accounts().unwrap_or_default(); + let active = + codexbar::codex_accounts::CodexAccountManager::new().discover_ambient_account(&accounts); + spec.insert( + 0, + codex_accounts_menu( + &accounts, + active.as_ref(), + settings.ui_language, + settings.hide_personal_info, + ), + ); + + let claude_accounts = crate::commands::claude_accounts_list().unwrap_or_default(); + spec.insert( + 1, + claude_accounts_menu( + &claude_accounts, + settings.ui_language, + settings.hide_personal_info, + ), + ); +} + +pub(crate) fn resolve_action(id: &str) -> Option { + match id { + "add_codex_account" => Some(AccountMenuAction::AddCodexAccount), + "add_claude_account" => Some(AccountMenuAction::AddClaudeAccount), + "save_claude_account" => Some(AccountMenuAction::SaveClaudeAccount), + "cancel_claude_login" => Some(AccountMenuAction::CancelClaudeLogin), + _ if id.starts_with("switch_claude_account:") => { + let id = id.strip_prefix("switch_claude_account:")?; + (!id.is_empty()).then(|| AccountMenuAction::SwitchClaudeAccount(id.to_string())) + } + _ if id.starts_with("switch_codex_account:") => { + let id = id.strip_prefix("switch_codex_account:")?; + uuid::Uuid::parse_str(id).ok()?; + Some(AccountMenuAction::SwitchCodexAccount(id.to_string())) + } + _ => None, + } +} + +pub(crate) fn handle_action(app: &AppHandle, action: AccountMenuAction) { + match action { + AccountMenuAction::AddCodexAccount => { + let handle = app.clone(); + tauri::async_runtime::spawn(async move { + match crate::commands::codex_account_add(handle.clone()).await { + Ok(_) => show_codex_message(&handle, "Codex account added."), + Err(error) => show_codex_message(&handle, &error), + } + }); + } + AccountMenuAction::SwitchCodexAccount(id) => { + let handle = app.clone(); + tauri::async_runtime::spawn(async move { + match crate::commands::codex_account_switch(handle.clone(), id).await { + Ok(result) => { + use tauri_plugin_dialog::{DialogExt, MessageDialogButtons}; + if result.desktop_session_restore_path.is_some() { + let dialog_handle = handle.clone(); + let restart = tauri::async_runtime::spawn_blocking(move || { + dialog_handle + .dialog() + .message("Account switched. Restart Codex Desktop to use it? This stops running desktop tasks.") + .title("Codex Accounts") + .buttons(MessageDialogButtons::OkCancelCustom( + "Restart".into(), + "Later".into(), + )) + .blocking_show() + }) + .await + .unwrap_or(false); + if restart { + let restart_result = + crate::commands::codex_account_restart_desktop( + handle.clone(), + result.switch_id.to_string(), + ) + .await; + if let Err(error) = restart_result { + show_codex_message(&handle, &error); + } + } + } else { + show_codex_message(&handle, "Codex account switched."); + } + } + Err(error) => show_codex_message(&handle, &error), + } + }); + } + AccountMenuAction::CancelClaudeLogin => crate::commands::claude_account_cancel_login(), + action @ (AccountMenuAction::AddClaudeAccount + | AccountMenuAction::SaveClaudeAccount + | AccountMenuAction::SwitchClaudeAccount(_)) => { + let handle = app.clone(); + tauri::async_runtime::spawn(async move { + use tauri_plugin_dialog::DialogExt; + let (result, message) = match action { + AccountMenuAction::AddClaudeAccount => ( + crate::commands::claude_account_add(handle.clone()).await, + "Claude Code account added. Select it to switch.", + ), + AccountMenuAction::SaveClaudeAccount => ( + crate::commands::claude_account_save_current(handle.clone()).await, + "Current Claude Code account saved.", + ), + AccountMenuAction::SwitchClaudeAccount(id) => ( + crate::commands::claude_account_switch(handle.clone(), id).await, + "Claude Code account switched. Reopen the Claude Code CLI to use it.", + ), + _ => unreachable!(), + }; + handle + .dialog() + .message(result.err().unwrap_or_else(|| message.to_string())) + .title("Claude Code accounts") + .show(|_| {}); + }); + } + } +} + +fn show_codex_message(app: &AppHandle, message: &str) { + use tauri_plugin_dialog::DialogExt; + app.dialog() + .message(message) + .title("Codex Accounts") + .show(|_| {}); +} + +fn codex_accounts_menu( + accounts: &[CodexAccount], + active: Option<&CodexAccount>, + lang: Language, + hide_personal_info: bool, +) -> TrayMenuEntry { + let text = |key| locale::get_text(lang, key); + let mut children: Vec<_> = accounts + .iter() + .map(|account| { + let is_active = active.is_some_and(|current| current.matches(account)); + let mut entry = TrayMenuEntry::check_item( + format!("switch_codex_account:{}", account.id), + if hide_personal_info + && account + .nickname + .as_deref() + .is_none_or(|n| n.trim().is_empty()) + { + codexbar::core::PersonalInfoRedactor::partial_redact_email( + account.email_hint.as_deref(), + true, + ) + } else { + account.display_name() + }, + is_active, + ); + entry.disabled = is_active; + entry + }) + .collect(); + if children.is_empty() { + children.push(TrayMenuEntry::status_row( + "codex_accounts_empty", + text(LocaleKey::CodexAccountsEmpty), + )); + } + children.push(TrayMenuEntry::separator()); + children.push(TrayMenuEntry::item( + "add_codex_account", + text(LocaleKey::CodexAccountsAddButton), + )); + TrayMenuEntry::submenu( + "codex_accounts", + text(LocaleKey::CodexAccountsTitle), + children, + ) +} + +fn claude_accounts_menu( + accounts: &[codexbar::providers::claude::accounts::ClaudeAccount], + lang: Language, + hide_personal_info: bool, +) -> TrayMenuEntry { + let text = |key| locale::get_text(lang, key); + let mut children: Vec<_> = accounts + .iter() + .map(|account| { + let label = if hide_personal_info { + codexbar::core::PersonalInfoRedactor::partial_redact_email( + Some(&account.email), + true, + ) + } else { + account.organization.as_ref().map_or_else( + || account.email.clone(), + |org| format!("{} ({org})", account.email), + ) + }; + let mut entry = TrayMenuEntry::check_item( + format!("switch_claude_account:{}", account.id), + label, + account.is_active, + ); + entry.disabled = account.is_active || !account.is_saved; + entry + }) + .collect(); + if children.is_empty() { + children.push(TrayMenuEntry::status_row( + "claude_accounts_empty", + text(LocaleKey::ClaudeAccountsEmpty), + )); + } + children.push(TrayMenuEntry::separator()); + children.push(TrayMenuEntry::item( + "add_claude_account", + text(LocaleKey::CodexAccountsAddButton), + )); + children.push(TrayMenuEntry::item( + "save_claude_account", + text(LocaleKey::ClaudeAccountsSaveCurrent), + )); + children.push(TrayMenuEntry::item( + "cancel_claude_login", + text(LocaleKey::ClaudeAccountsCancelLogin), + )); + TrayMenuEntry::submenu( + "claude_accounts", + text(LocaleKey::ClaudeAccountsTitle), + children, + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn menu_contains(menu: &[TrayMenuEntry], id: &str) -> bool { + menu.iter().any(|entry| { + entry.id.as_deref() == Some(id) + || (!entry.children.is_empty() && menu_contains(&entry.children, id)) + }) + } + + #[test] + fn account_action_ids_are_typed_and_validated() { + assert_eq!( + resolve_action("add_claude_account"), + Some(AccountMenuAction::AddClaudeAccount) + ); + assert_eq!( + resolve_action("save_claude_account"), + Some(AccountMenuAction::SaveClaudeAccount) + ); + assert_eq!( + resolve_action("cancel_claude_login"), + Some(AccountMenuAction::CancelClaudeLogin) + ); + assert_eq!( + resolve_action("switch_claude_account:a:org"), + Some(AccountMenuAction::SwitchClaudeAccount("a:org".into())) + ); + assert!(resolve_action("switch_claude_account:").is_none()); + assert!(resolve_action("switch_codex_account:not-a-uuid").is_none()); + } + + #[test] + fn codex_accounts_can_be_switched_or_added_from_tray() { + use codexbar::codex_accounts::{CodexAccountSource, utc_now}; + let make = |name: &str| { + CodexAccount::new( + uuid::Uuid::new_v4(), + Some(name.into()), + None, + None, + Some(name.into()), + std::path::PathBuf::from(name), + CodexAccountSource::ManagedByApp, + utc_now(), + utc_now(), + None, + ) + }; + let first = make("Personal"); + let second = make("Work"); + let menu = codex_accounts_menu( + &[first.clone(), second.clone()], + Some(&first), + Language::English, + false, + ); + assert_eq!(menu.children[0].checked, Some(true)); + assert!(menu.children[0].disabled); + assert_eq!( + menu.children[1].id.as_deref(), + Some(format!("switch_codex_account:{}", second.id).as_str()) + ); + assert_eq!(menu.children[1].checked, Some(false)); + assert!(!menu.children[1].disabled); + assert!(menu_contains(&menu.children, "add_codex_account")); + let empty = codex_accounts_menu(&[], None, Language::English, false); + assert!(menu_contains(&empty.children, "add_codex_account")); + let mut email_account = second; + email_account.nickname = None; + email_account.email_hint = Some("private@example.com".into()); + let private = codex_accounts_menu(&[email_account.clone()], None, Language::English, true); + assert!(!private.children[0].label.contains("private@example.com")); + let visible = codex_accounts_menu(&[email_account], None, Language::English, false); + assert_eq!(visible.children[0].label, "private@example.com"); + } + + #[test] + fn claude_menu_checks_current_account_and_routes_saved_accounts() { + use codexbar::providers::claude::accounts::ClaudeAccount; + let current = ClaudeAccount { + id: "a:org".into(), + email: "a@example.com".into(), + organization: None, + plan: None, + is_active: true, + is_saved: false, + }; + let saved = ClaudeAccount { + id: "b:org".into(), + is_active: false, + is_saved: true, + ..current.clone() + }; + let menu = claude_accounts_menu(&[current, saved], Language::English, false); + assert_eq!(menu.id.as_deref(), Some("claude_accounts")); + assert_eq!(menu.children[0].checked, Some(true)); + assert!(menu.children[0].disabled); + assert_eq!( + menu.children[1].id.as_deref(), + Some("switch_claude_account:b:org") + ); + assert!(!menu.children[1].disabled); + assert!(menu_contains(&menu.children, "add_claude_account")); + assert!(menu_contains( + &claude_accounts_menu(&[], Language::English, false).children, + "add_claude_account" + )); + } +} diff --git a/apps/desktop-tauri/src-tauri/src/tray_bridge.rs b/apps/desktop-tauri/src-tauri/src/tray_bridge.rs index 5d223c3881..00ba7aff36 100644 --- a/apps/desktop-tauri/src-tauri/src/tray_bridge.rs +++ b/apps/desktop-tauri/src-tauri/src/tray_bridge.rs @@ -130,13 +130,14 @@ fn build_native_tray_menu( ) -> tauri::Result> { let settings = Settings::load(); let enabled = settings.enabled_providers.clone(); - let spec = build_tray_menu_with( + let mut spec = build_tray_menu_with( providers, status_labels, &enabled, settings.float_bar_enabled, settings.ui_language, ); + crate::tray_accounts::prepend_account_menus(&mut spec, &settings); let entries = spec .iter() .map(|entry| build_native_menu_entry(app, entry)) @@ -184,6 +185,7 @@ enum MenuAction { ToggleProvider(String), /// Toggle the floating bar window on/off. ToggleFloatBar, + Account(crate::tray_accounts::AccountMenuAction), Quit, } @@ -193,6 +195,9 @@ enum MenuTransitionDispatch { } fn resolve_menu_action(id: &str) -> Option { + if let Some(action) = crate::tray_accounts::resolve_action(id) { + return Some(MenuAction::Account(action)); + } match id { "refresh" => Some(MenuAction::Refresh), "check_for_updates" => Some(MenuAction::CheckForUpdates), @@ -328,6 +333,7 @@ fn schedule_tray_promotion_retries(app_handle: AppHandle) { /// Route a native menu-item click to the corresponding shell action. fn handle_menu_event(app: &AppHandle, id: &str) { match resolve_menu_action(id) { + Some(MenuAction::Account(action)) => crate::tray_accounts::handle_action(app, action), Some(MenuAction::Transition(request)) => { crate::auto_refresh::note_menu_open(); match resolve_menu_transition_dispatch(id, request) { diff --git a/apps/desktop-tauri/src-tauri/src/tray_menu.rs b/apps/desktop-tauri/src-tauri/src/tray_menu.rs index d4ed60feac..688e56b42b 100644 --- a/apps/desktop-tauri/src-tauri/src/tray_menu.rs +++ b/apps/desktop-tauri/src-tauri/src/tray_menu.rs @@ -17,7 +17,7 @@ pub(crate) struct TrayMenuEntry { } impl TrayMenuEntry { - fn item(id: impl Into, label: impl Into) -> Self { + pub(crate) fn item(id: impl Into, label: impl Into) -> Self { Self { id: Some(id.into()), label: label.into(), @@ -29,7 +29,11 @@ impl TrayMenuEntry { } /// A checkbox menu item. `checked` mirrors the provider's enabled state. - fn check_item(id: impl Into, label: impl Into, checked: bool) -> Self { + pub(crate) fn check_item( + id: impl Into, + label: impl Into, + checked: bool, + ) -> Self { Self { id: Some(id.into()), label: label.into(), @@ -40,7 +44,11 @@ impl TrayMenuEntry { } } - fn submenu(id: impl Into, label: impl Into, children: Vec) -> Self { + pub(crate) fn submenu( + id: impl Into, + label: impl Into, + children: Vec, + ) -> Self { Self { id: Some(id.into()), label: label.into(), @@ -51,7 +59,7 @@ impl TrayMenuEntry { } } - fn separator() -> Self { + pub(crate) fn separator() -> Self { Self { id: None, label: String::new(), diff --git a/apps/desktop-tauri/src/components/ClaudeAccountsMenu.test.tsx b/apps/desktop-tauri/src/components/ClaudeAccountsMenu.test.tsx new file mode 100644 index 0000000000..b403a87540 --- /dev/null +++ b/apps/desktop-tauri/src/components/ClaudeAccountsMenu.test.tsx @@ -0,0 +1,71 @@ +import { act, fireEvent, render, screen } from "@testing-library/react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { ClaudeAccount } from "../types/bridge"; + +const mocks = vi.hoisted(() => ({ claudeAccountsList: vi.fn(), claudeAccountSwitch: vi.fn(), refreshProviders: vi.fn() })); +vi.mock("../lib/tauri", () => mocks); +vi.mock("@tauri-apps/api/event", () => ({ listen: vi.fn(() => Promise.resolve(() => {})) })); +vi.mock("../hooks/useLocale", () => ({ useLocale: () => ({ t: (key: string) => key }) })); +import ClaudeAccountsMenu from "./ClaudeAccountsMenu"; + +const first: ClaudeAccount = { id: "first:org", email: "first@example.com", organization: "Personal", plan: "max", isActive: true, isSaved: true }; +const second: ClaudeAccount = { ...first, id: "second:org", email: "second@example.com", organization: "Work", isActive: false }; + +describe("ClaudeAccountsMenu", () => { + beforeEach(() => { + vi.resetAllMocks(); + mocks.claudeAccountsList.mockResolvedValue([first, second]); + mocks.refreshProviders.mockResolvedValue(undefined); + }); + + it("marks the current account and switches the selected saved account", async () => { + render(); + await screen.findByText(first.email); + const buttons = screen.getAllByText("CodexAccountsSwitchButton") as HTMLButtonElement[]; + expect(buttons[0].disabled).toBe(true); + expect(buttons[1].disabled).toBe(false); + await act(async () => fireEvent.click(buttons[1])); + expect(mocks.claudeAccountSwitch).toHaveBeenCalledWith(second.id); + expect(screen.getByRole("status").textContent).toBe("ClaudeAccountsSwitched"); + }); + + it("shows a single inactive saved account so the first login can be activated", async () => { + mocks.claudeAccountsList.mockResolvedValue([second]); + render(); + await screen.findByText(second.email); + const button = screen.getByText("CodexAccountsSwitchButton"); + expect(button).not.toBeDisabled(); + await act(async () => fireEvent.click(button)); + expect(mocks.claudeAccountSwitch).toHaveBeenCalledWith(second.id); + }); + + it("masks emails, including tooltips, when hideEmail is enabled", async () => { + mocks.claudeAccountsList.mockResolvedValue([first, { ...second, organization: `${second.email}'s Organization` }]); + const { container } = render(); + await screen.findByText("ClaudeAccountsTitle"); + expect(container.textContent).not.toContain(first.email); + expect(container.innerHTML).not.toContain(second.email); + }); + + it("shows switch failures and leaves the current account marked active", async () => { + mocks.claudeAccountSwitch.mockRejectedValue("Close Claude Code first."); + render(); + await screen.findByText(first.email); + await act(async () => fireEvent.click(screen.getAllByText("CodexAccountsSwitchButton")[1])); + expect(screen.getByRole("alert").textContent).toContain("Close Claude Code first."); + expect(screen.queryByRole("status")).toBeNull(); + expect(mocks.refreshProviders).not.toHaveBeenCalled(); + }); + + it("keeps account loading failures discoverable and retries when the window gains focus", async () => { + mocks.claudeAccountsList.mockRejectedValueOnce("Account storage unavailable."); + const onLayoutChange = vi.fn(); + render(); + await screen.findByText("Account storage unavailable."); + expect(screen.getByText("ClaudeAccountsTitle")).toBeInTheDocument(); + await act(async () => window.dispatchEvent(new Event("focus"))); + expect(await screen.findByText(second.email)).toBeInTheDocument(); + expect(screen.queryByText("Account storage unavailable.")).toBeNull(); + expect(onLayoutChange).toHaveBeenCalled(); + }); +}); diff --git a/apps/desktop-tauri/src/components/ClaudeAccountsMenu.tsx b/apps/desktop-tauri/src/components/ClaudeAccountsMenu.tsx new file mode 100644 index 0000000000..eccc12b2b5 --- /dev/null +++ b/apps/desktop-tauri/src/components/ClaudeAccountsMenu.tsx @@ -0,0 +1,98 @@ +import { useCallback, useEffect, useRef, useState } from "react"; +import { listen } from "@tauri-apps/api/event"; +import type { ClaudeAccount } from "../types/bridge"; +import { claudeAccountsList, claudeAccountSwitch } from "../lib/tauri"; +import { useLocale } from "../hooks/useLocale"; +import { maskEmail } from "./MenuCard"; + +export default function ClaudeAccountsMenu({ hideEmail, onLayoutChange }: { + hideEmail: boolean; + onLayoutChange?: () => void; +}) { + const { t } = useLocale(); + const [accounts, setAccounts] = useState([]); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(null); + const [switched, setSwitched] = useState(false); + const mounted = useRef(false); + const load = useCallback(async () => { + const next = await claudeAccountsList(); + if (mounted.current) { + setAccounts(next); + setError(null); + } + }, []); + useEffect(() => { + mounted.current = true; + const reload = () => { + void load().catch(e => { if (mounted.current) setError(String(e)); }); + }; + reload(); + window.addEventListener("focus", reload); + const unlisten = listen("claude-accounts-updated", reload); + return () => { + mounted.current = false; + window.removeEventListener("focus", reload); + void unlisten.then(fn => fn()).catch(() => {}); + }; + }, [load]); + useEffect(() => { + onLayoutChange?.(); + }, [accounts.length, error, switched, onLayoutChange]); + + const switchAccount = async (id: string) => { + setBusy(true); + setError(null); + setSwitched(false); + try { + await claudeAccountSwitch(id); + await load(); + if (mounted.current) setSwitched(true); + } catch (e) { + if (mounted.current) setError(String(e)); + } finally { + if (mounted.current) setBusy(false); + } + }; + + const hasSwitchableAccount = accounts.some(account => account.isSaved && !account.isActive); + if (accounts.length <= 1 && !hasSwitchableAccount && !error) return null; + return ( +
+ + {t("ClaudeAccountsTitle")} + {accounts.length} + + {error &&
{error}
} + {switched &&

{t("ClaudeAccountsSwitched")}

} +
    + {accounts.map(account => { + const email = hideEmail ? maskEmail(account.email) : account.email; + return ( +
  • +
    +
    + + {email} + {account.isActive && {t("TokenAccountActive")}} + + {!hideEmail && account.organization && !account.organization.includes(account.email) && ( + {account.organization} + )} +
    + +
    +
  • + ); + })} +
+
+ ); +} diff --git a/apps/desktop-tauri/src/components/MenuCard.tsx b/apps/desktop-tauri/src/components/MenuCard.tsx index 0408a05f9b..a2591e19d4 100644 --- a/apps/desktop-tauri/src/components/MenuCard.tsx +++ b/apps/desktop-tauri/src/components/MenuCard.tsx @@ -11,6 +11,7 @@ import type { LocaleKey } from "../i18n/keys"; import { providerSupportsChartData } from "../lib/providerCharts"; import MenuCardDetails, { describeCard, type MetricEntry } from "./MenuCardDetails"; import CodexAccountsMenu from "./CodexAccountsMenu"; +import ClaudeAccountsMenu from "./ClaudeAccountsMenu"; import { DEEPSEEK_PRICING_EVENT } from "../hooks/useDeepSeekPricingStatus"; import { getDeepSeekPricingStatus } from "../lib/tauri"; import type { DeepSeekPricingStatus } from "../types/bridge"; @@ -334,6 +335,9 @@ export default function MenuCard({ resetTimeRelative={resetTimeRelative} /> )} + {provider.providerId === "claude" && ( + + )} ); }