From 250c938dc5f5bd18da85c684cb9dddc50d7b11ae Mon Sep 17 00:00:00 2001 From: Factory Agent Date: Fri, 16 Oct 2026 09:42:16 +0700 Subject: [PATCH 1/7] Add CLI aliases to provider search metadata --- rust/src/core/provider.rs | 168 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) diff --git a/rust/src/core/provider.rs b/rust/src/core/provider.rs index 90caa7b400..ee1eade68d 100755 --- a/rust/src/core/provider.rs +++ b/rust/src/core/provider.rs @@ -745,6 +745,140 @@ impl ProviderId { } } + /// Lowercase CLI aliases that `from_cli_name` also resolves to this provider, + /// excluding `cli_name` itself. Feeds the Settings provider search. + pub fn cli_aliases(&self) -> &'static [&'static str] { + match self { + ProviderId::DevPass => &[], + ProviderId::Manus => &[], + ProviderId::Xapi => &["x-api", "x api"], + ProviderId::Kilo => &[], + ProviderId::Devin => &[], + ProviderId::Warp => &["warp-ai", "warp-terminal"], + ProviderId::DeepInfra => &["deep-infra", "di"], + ProviderId::Fireworks => &["fireworks-ai", "fw"], + ProviderId::MuseAI => &["muse (muse.ai)"], + ProviderId::AzureOpenAI => &["azure-openai", "azure openai"], + ProviderId::Nous => &["nous-portal", "nous portal", "hermes"], + ProviderId::Venice => &[], + ProviderId::OpenCodeGo => &["opencode-go", "opencode go"], + ProviderId::Bifrost => &["bifrost-gateway", "bifrost gateway"], + ProviderId::Claude => &["anthropic"], + ProviderId::Tavily => &[], + ProviderId::LLMMan => &[], + ProviderId::IBMBob => &["ibm-bob", "ibm bob", "bob", "bobshell"], + ProviderId::Pi => &["pi-mono"], + ProviderId::Poe => &[], + ProviderId::Sakana => &["sakana-ai", "sakana ai"], + ProviderId::Linkup => &[], + ProviderId::Zed => &["zed-ai"], + ProviderId::WorkBuddy => &[], + ProviderId::JetBrains => &["jetbrains-ai", "jetbrains ai", "intellij"], + ProviderId::CodeBuddy => { + &["code-buddy", "codebuddy-cn", "codebuddycn", "腾讯codebuddy"] + } + ProviderId::NanoGPT => &["nano-gpt"], + ProviderId::Deepgram => &["dg"], + ProviderId::Wayfinder => &[], + ProviderId::Hyper => &["charm-hyper", "charm hyper"], + ProviderId::MiMo => &["xiaomi", "xiaomimimo", "xiaomi-mimo", "xiaomi mimo"], + ProviderId::Langdock => &[], + ProviderId::Infini => &["infini-ai"], + ProviderId::Kiro => &["aws"], + ProviderId::OpenRouter => &["or"], + ProviderId::Sub2Api => &["sub-2-api", "sub 2 api"], + ProviderId::Mistral => &["mistral-ai", "mistral ai"], + ProviderId::ZenMux => &["zen-mux"], + ProviderId::CommandCode => &["command-code", "command code"], + ProviderId::Factory => &["droid"], + ProviderId::XKiro => &["x-kiro"], + ProviderId::Flora => &[], + ProviderId::QwenCloud => &["qwencloud", "qwen", "qwen-token-plan", "qwen cloud"], + ProviderId::Doubao => &["ark", "volcengine"], + ProviderId::SailResearch => &["sail research"], + ProviderId::Raycast => &["raycast-ai"], + ProviderId::DeepSeek => &["deep-seek", "ds"], + ProviderId::Augment => &[], + ProviderId::Notion => &["notion-ai", "notionai", "notion ai"], + ProviderId::Xai => &["x.ai", "x-ai"], + ProviderId::AtlasCloud => &["atlas-cloud", "atlas cloud"], + ProviderId::Copilot => &["github"], + ProviderId::CrossModel => &["cross-model", "cross model", "crossmodel (removed)"], + ProviderId::LongCat => &["long-cat", "lc"], + ProviderId::Aixy => &["aixy-gateway", "aixy gateway"], + ProviderId::GitKraken => &["gitkraken-ai", "gitkraken ai"], + ProviderId::Windsurf => &["codeium"], + ProviderId::Vercel => &[ + "vercel-ai-gateway", + "vercel ai gateway", + "ai-gateway", + "ai gateway", + ], + ProviderId::LithosAI => &[], + ProviderId::Sofya => &[], + ProviderId::TypeSafe => &["type-safe", "type safe"], + ProviderId::T3Chat => &["t3-chat", "t3 chat"], + ProviderId::OpenCode => &[], + ProviderId::ClinePass => &["cline-pass", "cline"], + ProviderId::TinyApi => &["tiny-api"], + ProviderId::MiniMax => &[], + ProviderId::Ollama => &[], + ProviderId::Grok => &["supergrok", "super-grok"], + ProviderId::HuggingFace => &["hugging-face", "hugging face", "hf"], + ProviderId::Meta => &[ + "metaspark", + "meta-spark", + "muse-spark", + "musespark", + "muse spark", + "meta muse spark", + ], + ProviderId::ZoomMate => &["zoom-mate", "zoom mate"], + ProviderId::Abacus => &["abacus ai", "abacus-ai"], + ProviderId::Qoder => &[], + ProviderId::Kimi => &["moonshot"], + ProviderId::Replicate => &["r8"], + ProviderId::Perplexity => &["pplx"], + ProviderId::Helmcode => &["nan-builders", "nan builders"], + ProviderId::Cosmic => &["cosmic ai"], + ProviderId::Synthetic => &["synthetic.new"], + ProviderId::V0 => &["v0-dev", "v0.dev"], + ProviderId::AiAnd => &["ai&", "ai-and", "ai and"], + ProviderId::Cursor => &[], + ProviderId::CodeRabbit => &["code-rabbit", "code rabbit"], + ProviderId::Aerostack => &[], + ProviderId::ClawRouter => &["claw-router"], + ProviderId::Bedrock => &["aws-bedrock", "aws bedrock"], + ProviderId::Zai => &["z.ai"], + ProviderId::AlibabaTokenPlan => &[ + "alibaba-token-plan", + "alibaba token plan", + "alibaba-token", + "bailian-token-plan", + ], + ProviderId::LiteLLM => &["lite-llm", "lite llm"], + ProviderId::Fal => &[], + ProviderId::Chutes => &["chutes-ai", "chutes ai"], + ProviderId::Codex => &["openai"], + ProviderId::Antigravity => &["agy"], + ProviderId::Alibaba => &["tongyi", "qianwen"], + ProviderId::ElevenLabs => &["eleven-labs", "11labs"], + ProviderId::Groq => &["groqcloud", "groq-cloud", "groq cloud"], + ProviderId::OpenAIApi => &["openai-api", "openai api", "openai-balance"], + ProviderId::LLMProxy => &["llm-proxy", "llm proxy"], + ProviderId::Codebuff => &["manicode"], + ProviderId::Exa => &[], + ProviderId::Parallel => &["parallel.ai"], + ProviderId::KimiK2 => &["kimi-k2", "kimi k2", "k2", "kimi k2 (removed)"], + ProviderId::Neuralwatt => &["neural-watt", "nw", "neural"], + ProviderId::Gemini => &["google"], + ProviderId::StepFun => &["step-fun", "step fun"], + ProviderId::Muse => &["muse-code", "muse code"], + ProviderId::Amp => &["sourcegraph"], + ProviderId::VertexAI => &["vertex", "vertex ai"], + } + } + /// Soft-removed providers (upstream #2254: Kimi K2 + CrossModel). /// /// Modules and CLI resolution stay so existing configs and `--provider kimik2` @@ -1725,6 +1859,40 @@ mod tests { assert_eq!(map.get("agy"), Some(&ProviderId::Antigravity)); } + #[test] + fn cli_aliases_resolve_back_to_their_provider_and_are_unique() { + let mut seen = std::collections::HashMap::new(); + for id in ProviderId::all() { + for alias in id.cli_aliases() { + assert_ne!(*alias, id.cli_name(), "alias repeats cli_name for {id:?}"); + assert_eq!( + ProviderId::from_cli_name(alias), + Some(*id), + "alias {alias:?} does not resolve to {id:?}" + ); + assert_eq!( + alias.to_lowercase(), + *alias, + "alias {alias:?} must be lowercase" + ); + assert!( + seen.insert(*alias, *id).is_none(), + "alias {alias:?} is claimed by more than one provider" + ); + } + } + } + + #[test] + fn cli_aliases_cover_the_documented_search_aliases() { + assert!(ProviderId::Codex.cli_aliases().contains(&"openai")); + assert!(ProviderId::Claude.cli_aliases().contains(&"anthropic")); + assert!(ProviderId::Factory.cli_aliases().contains(&"droid")); + assert!(ProviderId::Antigravity.cli_aliases().contains(&"agy")); + assert!(!ProviderId::Zai.cli_aliases().is_empty()); + assert!(!ProviderId::Codex.cli_aliases().contains(&"codex")); + } + #[test] fn test_provider_id_cookie_domain() { // Cookie-based providers From f1157e579e222c08f65989d09d3ac0031a01656c Mon Sep 17 00:00:00 2001 From: Factory Agent Date: Fri, 16 Oct 2026 09:42:16 +0700 Subject: [PATCH 2/7] Store enabled-only provider filter and merge visible provider order --- rust/src/settings.rs | 33 +++++++++++++ rust/src/settings/preferences_document.rs | 2 + rust/src/settings/raw.rs | 4 ++ rust/src/settings/tests.rs | 56 +++++++++++++++++++++++ 4 files changed, 95 insertions(+) diff --git a/rust/src/settings.rs b/rust/src/settings.rs index dbd8873fa9..a8a090eb40 100755 --- a/rust/src/settings.rs +++ b/rust/src/settings.rs @@ -233,6 +233,11 @@ pub struct Settings { #[serde(default)] pub providers_sorted_alphabetically: bool, + /// Settings sidebar lists only enabled providers (upstream `providersEnabledOnly`). + /// Local browsing filter: not part of the portable preferences document. + #[serde(default)] + pub providers_enabled_only: bool, + /// Show usage bars as "used" (true) or "remaining" (false) pub show_as_used: bool, @@ -629,6 +634,33 @@ pub fn normalize_provider_order(requested: &[String]) -> Vec { out } +/// Replace only the slots held by `visible` inside `full`, so every other +/// provider (for example a soft-removed one) keeps its position. Returns +/// `None` unless `full` and `visible` have no repeats and every visible id is +/// present in `full`. +pub fn merge_visible_provider_order(full: &[String], visible: &[String]) -> Option> { + let full_set: HashSet<&str> = full.iter().map(String::as_str).collect(); + let visible_set: HashSet<&str> = visible.iter().map(String::as_str).collect(); + if full_set.len() != full.len() + || visible_set.len() != visible.len() + || !visible_set.is_subset(&full_set) + { + return None; + } + let mut next = visible.iter(); + Some( + full.iter() + .map(|id| { + if visible_set.contains(id.as_str()) { + next.next().cloned().unwrap_or_else(|| id.clone()) + } else { + id.clone() + } + }) + .collect(), + ) +} + fn default_global_shortcut() -> String { "Ctrl+Shift+U".to_string() } @@ -720,6 +752,7 @@ impl Default for Settings { menu_bar_color_pace: false, menu_bar_pace_reserve_positive: false, providers_sorted_alphabetically: false, + providers_enabled_only: false, show_as_used: false, enable_animations: true, // Animations enabled by default reset_time_relative: true, // Show relative times by default diff --git a/rust/src/settings/preferences_document.rs b/rust/src/settings/preferences_document.rs index b40d73808f..a634cea239 100644 --- a/rust/src/settings/preferences_document.rs +++ b/rust/src/settings/preferences_document.rs @@ -186,6 +186,8 @@ pub const EXCLUDED_KEYS: &[&str] = &[ "weekly_progress_work_days", "alibaba_token_plan_region", "cost_summary_display_style", + // Settings sidebar view filter: local to this machine, as upstream keeps it. + "providers_enabled_only", // Kept out of the portable set, as upstream (0.70.0) keeps them. "menu_bar_color_pace", "menu_bar_pace_reserve_positive", diff --git a/rust/src/settings/raw.rs b/rust/src/settings/raw.rs index 6f4bff8725..5b3037765f 100644 --- a/rust/src/settings/raw.rs +++ b/rust/src/settings/raw.rs @@ -167,6 +167,8 @@ pub(super) struct RawSettings { menu_bar_pace_reserve_positive: bool, #[serde(default)] providers_sorted_alphabetically: bool, + #[serde(default)] + providers_enabled_only: bool, show_as_used: bool, enable_animations: bool, reset_time_relative: bool, @@ -375,6 +377,7 @@ impl Default for RawSettings { menu_bar_color_pace: s.menu_bar_color_pace, menu_bar_pace_reserve_positive: s.menu_bar_pace_reserve_positive, providers_sorted_alphabetically: s.providers_sorted_alphabetically, + providers_enabled_only: s.providers_enabled_only, show_as_used: s.show_as_used, enable_animations: s.enable_animations, reset_time_relative: s.reset_time_relative, @@ -754,6 +757,7 @@ impl From for Settings { menu_bar_color_pace: raw.menu_bar_color_pace, menu_bar_pace_reserve_positive: raw.menu_bar_pace_reserve_positive, providers_sorted_alphabetically: raw.providers_sorted_alphabetically, + providers_enabled_only: raw.providers_enabled_only, show_as_used: raw.show_as_used, enable_animations: raw.enable_animations, reset_time_relative: raw.reset_time_relative, diff --git a/rust/src/settings/tests.rs b/rust/src/settings/tests.rs index 82f1548ce4..f34eb5d021 100644 --- a/rust/src/settings/tests.rs +++ b/rust/src/settings/tests.rs @@ -281,6 +281,62 @@ fn providers_sorted_alphabetically_defaults_off_and_round_trips() { assert!(loaded.providers_sorted_alphabetically); } +#[test] +fn providers_enabled_only_defaults_off_and_round_trips() { + assert!(!Settings::default().providers_enabled_only); + + let legacy: Settings = serde_json::from_str(r#"{ "enabled_providers": [] }"#) + .expect("legacy file without the sidebar enabled-only key"); + assert!(!legacy.providers_enabled_only); + + let filtered = Settings { + providers_enabled_only: true, + ..Settings::default() + }; + let json = serde_json::to_string(&filtered).expect("serialize enabled-only filter"); + assert!(json.contains(r#""providers_enabled_only":true"#)); + + let loaded: Settings = serde_json::from_str(&json).expect("deserialize enabled-only filter"); + assert!(loaded.providers_enabled_only); +} + +#[test] +fn merge_visible_provider_order_keeps_hidden_slots_in_place() { + let full: Vec = ["codex", "kimik2", "claude", "gemini"] + .into_iter() + .map(str::to_string) + .collect(); + let visible: Vec = ["gemini", "codex", "claude"] + .into_iter() + .map(str::to_string) + .collect(); + + // kimik2 is hidden from the sidebar; it keeps index 1. + assert_eq!( + merge_visible_provider_order(&full, &visible), + Some( + ["gemini", "kimik2", "codex", "claude"] + .into_iter() + .map(str::to_string) + .collect() + ) + ); +} + +#[test] +fn merge_visible_provider_order_rejects_unknown_or_repeated_ids() { + let full: Vec = ["codex", "claude"] + .into_iter() + .map(str::to_string) + .collect(); + let unknown: Vec = ["codex", "nope"].into_iter().map(str::to_string).collect(); + let repeated: Vec = ["codex", "codex"].into_iter().map(str::to_string).collect(); + + assert_eq!(merge_visible_provider_order(&full, &unknown), None); + assert_eq!(merge_visible_provider_order(&full, &repeated), None); + assert_eq!(merge_visible_provider_order(&full, &[]), Some(full.clone())); +} + #[test] fn cost_reporting_period_defaults_to_thirty_days_and_round_trips() { let defaulted: Settings = serde_json::from_str(r#"{ "enabled_providers": [] }"#) From a6112a371027faf8e5b13df2173960e0e2036217 Mon Sep 17 00:00:00 2001 From: Factory Agent Date: Fri, 16 Oct 2026 09:42:16 +0700 Subject: [PATCH 3/7] Add visible-slot provider reorder and restore commands --- .../src-tauri/src/commands/bridge.rs | 9 ++++ .../src/commands/provider_settings.rs | 41 +++++++++++++++++++ .../src-tauri/src/commands/settings.rs | 4 ++ apps/desktop-tauri/src-tauri/src/main.rs | 2 + .../src-tauri/src/tray_bridge.rs | 2 + apps/desktop-tauri/src-tauri/src/tray_menu.rs | 2 + 6 files changed, 60 insertions(+) diff --git a/apps/desktop-tauri/src-tauri/src/commands/bridge.rs b/apps/desktop-tauri/src-tauri/src/commands/bridge.rs index 2fa559fb0f..156658d70c 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/bridge.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/bridge.rs @@ -945,6 +945,8 @@ pub struct ProviderCatalogEntry { pub(crate) id: String, pub(crate) display_name: String, pub(crate) cookie_domain: Option, + /// CLI aliases the Settings search also matches (see `ProviderId::cli_aliases`). + pub(crate) cli_aliases: Vec, } /// One provider's service status for the sidebar dot and the tray panel. @@ -1018,6 +1020,7 @@ pub struct SettingsSnapshot { menu_bar_color_pace: 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, @@ -1187,6 +1190,7 @@ impl From for SettingsSnapshot { menu_bar_color_pace: settings.menu_bar_color_pace, 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, @@ -1282,6 +1286,11 @@ pub(crate) fn provider_catalog_for(settings: &Settings) -> Vec, +) -> Result, 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, +) -> Result, 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, String> { + let visible: Vec = { + 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( diff --git a/apps/desktop-tauri/src-tauri/src/commands/settings.rs b/apps/desktop-tauri/src-tauri/src/commands/settings.rs index fdcaf18209..c60cb580bc 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/settings.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/settings.rs @@ -46,6 +46,7 @@ pub struct SettingsUpdate { pub menu_bar_color_pace: Option, pub menu_bar_pace_reserve_positive: Option, pub providers_sorted_alphabetically: Option, + pub providers_enabled_only: Option, pub show_as_used: Option, pub show_all_token_accounts_in_menu: Option, pub enable_animations: Option, @@ -401,6 +402,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; } diff --git a/apps/desktop-tauri/src-tauri/src/main.rs b/apps/desktop-tauri/src-tauri/src/main.rs index 2b5f02b9de..c42aedbee7 100644 --- a/apps/desktop-tauri/src-tauri/src/main.rs +++ b/apps/desktop-tauri/src-tauri/src/main.rs @@ -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, diff --git a/apps/desktop-tauri/src-tauri/src/tray_bridge.rs b/apps/desktop-tauri/src-tauri/src/tray_bridge.rs index 02ecc6c673..0a8709d26f 100644 --- a/apps/desktop-tauri/src-tauri/src/tray_bridge.rs +++ b/apps/desktop-tauri/src-tauri/src/tray_bridge.rs @@ -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(), }, ] } diff --git a/apps/desktop-tauri/src-tauri/src/tray_menu.rs b/apps/desktop-tauri/src-tauri/src/tray_menu.rs index 3498739af8..f259b4e72b 100644 --- a/apps/desktop-tauri/src-tauri/src/tray_menu.rs +++ b/apps/desktop-tauri/src-tauri/src/tray_menu.rs @@ -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(), }, ] } From 548578099fb754eb4a89291ecb3d69116667bd4e Mon Sep 17 00:00:00 2001 From: Factory Agent Date: Fri, 16 Oct 2026 09:42:16 +0700 Subject: [PATCH 4/7] Add provider list option strings --- apps/desktop-tauri/src/i18n/keys.ts | 4 ++++ rust/src/locale.rs | 4 ++++ rust/src/locale/en-US.ftl | 4 ++++ rust/src/locale/es-MX.ftl | 4 ++++ rust/src/locale/ja-JP.ftl | 4 ++++ rust/src/locale/ko-KR.ftl | 4 ++++ rust/src/locale/pt-BR.ftl | 4 ++++ rust/src/locale/ru-RU.ftl | 4 ++++ rust/src/locale/tr-TR.ftl | 4 ++++ rust/src/locale/uk-UA.ftl | 4 ++++ rust/src/locale/zh-CN.ftl | 4 ++++ rust/src/locale/zh-TW.ftl | 4 ++++ 12 files changed, 48 insertions(+) diff --git a/apps/desktop-tauri/src/i18n/keys.ts b/apps/desktop-tauri/src/i18n/keys.ts index 57c0f2296a..918d52cb90 100644 --- a/apps/desktop-tauri/src/i18n/keys.ts +++ b/apps/desktop-tauri/src/i18n/keys.ts @@ -938,6 +938,10 @@ export const ALL_LOCALE_KEYS = [ "ProviderSidebarReorderHint", "ProviderSidebarMoveUp", "ProviderSidebarMoveDown", + "ProviderSidebarOptions", + "ProviderSidebarShowEnabledOnly", + "ProviderSidebarRestoreDefaultOrder", + "ProviderSidebarUndoOrder", "SettingsSidebarSortProviders", "SettingsSidebarSortOffHint", "SettingsSidebarSortOnHint", diff --git a/rust/src/locale.rs b/rust/src/locale.rs index a6b2a385b9..c3617b0899 100644 --- a/rust/src/locale.rs +++ b/rust/src/locale.rs @@ -1220,6 +1220,10 @@ locale_keys! { ProviderSidebarReorderHint, ProviderSidebarMoveUp, ProviderSidebarMoveDown, + ProviderSidebarOptions, + ProviderSidebarShowEnabledOnly, + ProviderSidebarRestoreDefaultOrder, + ProviderSidebarUndoOrder, SettingsSidebarSortProviders, SettingsSidebarSortOffHint, SettingsSidebarSortOnHint, diff --git a/rust/src/locale/en-US.ftl b/rust/src/locale/en-US.ftl index b83dbe84c0..e0f9cfc9a2 100644 --- a/rust/src/locale/en-US.ftl +++ b/rust/src/locale/en-US.ftl @@ -786,6 +786,10 @@ ProviderSidebarNoMatches = No matching providers ProviderSidebarReorderHint = Drag to reorder ProviderSidebarMoveUp = Move up ProviderSidebarMoveDown = Move down +ProviderSidebarOptions = Provider list options +ProviderSidebarShowEnabledOnly = Show enabled only +ProviderSidebarRestoreDefaultOrder = Restore default order +ProviderSidebarUndoOrder = Undo provider order change SettingsSidebarSortProviders = Sort providers alphabetically SettingsSidebarSortOffHint = Sort providers alphabetically (enabled first) SettingsSidebarSortOnHint = Sorted alphabetically (enabled first) — click to use your custom order diff --git a/rust/src/locale/es-MX.ftl b/rust/src/locale/es-MX.ftl index b8bce674f9..496cfa0768 100644 --- a/rust/src/locale/es-MX.ftl +++ b/rust/src/locale/es-MX.ftl @@ -807,6 +807,10 @@ ProviderSidebarNoMatches = Sin proveedores coincidentes ProviderSidebarReorderHint = Arrastrar para reordenar ProviderSidebarMoveUp = Subir ProviderSidebarMoveDown = Bajar +ProviderSidebarOptions = Opciones de la lista de proveedores +ProviderSidebarShowEnabledOnly = Mostrar solo activados +ProviderSidebarRestoreDefaultOrder = Restaurar orden predeterminado +ProviderSidebarUndoOrder = Deshacer cambio de orden de proveedores SettingsSidebarSortProviders = Ordenar proveedores alfabéticamente SettingsSidebarSortOffHint = Ordenar proveedores alfabéticamente (activados primero) SettingsSidebarSortOnHint = Orden alfabético (activados primero): haz clic para usar tu orden personalizado diff --git a/rust/src/locale/ja-JP.ftl b/rust/src/locale/ja-JP.ftl index b79d46f0b2..9005c49d7b 100644 --- a/rust/src/locale/ja-JP.ftl +++ b/rust/src/locale/ja-JP.ftl @@ -754,6 +754,10 @@ ProviderSidebarNoMatches = 一致するプロバイダーなし ProviderSidebarReorderHint = ドラッグで並べ替え ProviderSidebarMoveUp = 上へ ProviderSidebarMoveDown = 下へ +ProviderSidebarOptions = プロバイダー一覧のオプション +ProviderSidebarShowEnabledOnly = 有効なものだけ表示 +ProviderSidebarRestoreDefaultOrder = 既定の順序に戻す +ProviderSidebarUndoOrder = プロバイダーの順序変更を元に戻す SettingsSidebarSortProviders = プロバイダーをアルファベット順に並べ替え SettingsSidebarSortOffHint = プロバイダーをアルファベット順に並べ替え(有効なものを先頭) SettingsSidebarSortOnHint = アルファベット順(有効なものを先頭)— クリックするとカスタム順に戻ります diff --git a/rust/src/locale/ko-KR.ftl b/rust/src/locale/ko-KR.ftl index 23b1fad68b..1c7bb461d1 100644 --- a/rust/src/locale/ko-KR.ftl +++ b/rust/src/locale/ko-KR.ftl @@ -807,6 +807,10 @@ ProviderSidebarNoMatches = 일치하는 제공업체 없음 ProviderSidebarReorderHint = 끌어서 순서 변경 ProviderSidebarMoveUp = 위로 이동 ProviderSidebarMoveDown = 아래로 이동 +ProviderSidebarOptions = 제공업체 목록 옵션 +ProviderSidebarShowEnabledOnly = 활성 항목만 표시 +ProviderSidebarRestoreDefaultOrder = 기본 순서로 복원 +ProviderSidebarUndoOrder = 제공업체 순서 변경 실행 취소 SettingsSidebarSortProviders = 제공업체를 알파벳순으로 정렬 SettingsSidebarSortOffHint = 제공업체를 알파벳순으로 정렬(활성 항목 먼저) SettingsSidebarSortOnHint = 알파벳순 정렬됨(활성 항목 먼저) — 클릭하면 사용자 지정 순서로 돌아갑니다 diff --git a/rust/src/locale/pt-BR.ftl b/rust/src/locale/pt-BR.ftl index 76bcab0ed7..bef1865e6d 100644 --- a/rust/src/locale/pt-BR.ftl +++ b/rust/src/locale/pt-BR.ftl @@ -754,6 +754,10 @@ ProviderSidebarNoMatches = Nenhum provedor correspondente ProviderSidebarReorderHint = Arraste para reordenar ProviderSidebarMoveUp = Subir ProviderSidebarMoveDown = Mover para baixo +ProviderSidebarOptions = Opções da lista de provedores +ProviderSidebarShowEnabledOnly = Mostrar apenas ativados +ProviderSidebarRestoreDefaultOrder = Restaurar ordem padrão +ProviderSidebarUndoOrder = Desfazer alteração da ordem dos provedores SettingsSidebarSortProviders = Ordenar provedores em ordem alfabética SettingsSidebarSortOffHint = Ordenar provedores em ordem alfabética (ativados primeiro) SettingsSidebarSortOnHint = Em ordem alfabética (ativados primeiro) — clique para usar sua ordem personalizada diff --git a/rust/src/locale/ru-RU.ftl b/rust/src/locale/ru-RU.ftl index c8673d5fb8..f1883dc4cb 100644 --- a/rust/src/locale/ru-RU.ftl +++ b/rust/src/locale/ru-RU.ftl @@ -786,6 +786,10 @@ ProviderSidebarNoMatches = Нет подходящих провайдеров ProviderSidebarReorderHint = Перетащите, чтобы изменить порядок ProviderSidebarMoveUp = Вверх ProviderSidebarMoveDown = Двигаться вниз +ProviderSidebarOptions = Параметры списка провайдеров +ProviderSidebarShowEnabledOnly = Только включённые +ProviderSidebarRestoreDefaultOrder = Вернуть порядок по умолчанию +ProviderSidebarUndoOrder = Отменить изменение порядка провайдеров SettingsSidebarSortProviders = Сортировать провайдеров по алфавиту SettingsSidebarSortOffHint = Сортировать провайдеров по алфавиту (сначала включённые) SettingsSidebarSortOnHint = Отсортировано по алфавиту (сначала включённые) — нажмите, чтобы вернуть свой порядок diff --git a/rust/src/locale/tr-TR.ftl b/rust/src/locale/tr-TR.ftl index 71698e3c5b..9a04812a09 100644 --- a/rust/src/locale/tr-TR.ftl +++ b/rust/src/locale/tr-TR.ftl @@ -786,6 +786,10 @@ ProviderSidebarNoMatches = Eşleşen sağlayıcı yok ProviderSidebarReorderHint = Yeniden sıralamak için sürükleyin ProviderSidebarMoveUp = Yukarı taşı ProviderSidebarMoveDown = Aşağı taşı +ProviderSidebarOptions = Sağlayıcı listesi seçenekleri +ProviderSidebarShowEnabledOnly = Yalnızca etkin olanları göster +ProviderSidebarRestoreDefaultOrder = Varsayılan sıralamayı geri yükle +ProviderSidebarUndoOrder = Sağlayıcı sırası değişikliğini geri al SettingsSidebarSortProviders = Sağlayıcıları alfabetik sırala SettingsSidebarSortOffHint = Sağlayıcıları alfabetik sırala (etkin olanlar önce) SettingsSidebarSortOnHint = Alfabetik sıralı (etkin olanlar önce) — özel sıranıza dönmek için tıklayın diff --git a/rust/src/locale/uk-UA.ftl b/rust/src/locale/uk-UA.ftl index faa9aefef5..611c08d22e 100644 --- a/rust/src/locale/uk-UA.ftl +++ b/rust/src/locale/uk-UA.ftl @@ -786,6 +786,10 @@ ProviderSidebarNoMatches = Немає відповідних провайдер ProviderSidebarReorderHint = Перетягніть, щоб змінити порядок ProviderSidebarMoveUp = Вище ProviderSidebarMoveDown = Нижче +ProviderSidebarOptions = Параметри списку провайдерів +ProviderSidebarShowEnabledOnly = Лише увімкнені +ProviderSidebarRestoreDefaultOrder = Повернути типовий порядок +ProviderSidebarUndoOrder = Скасувати зміну порядку провайдерів SettingsSidebarSortProviders = Сортувати провайдерів за абеткою SettingsSidebarSortOffHint = Сортувати провайдерів за абеткою (спершу увімкнені) SettingsSidebarSortOnHint = Відсортовано за абеткою (спершу увімкнені) — натисніть, щоб повернути свій порядок diff --git a/rust/src/locale/zh-CN.ftl b/rust/src/locale/zh-CN.ftl index 5711873033..c2a15eed6e 100644 --- a/rust/src/locale/zh-CN.ftl +++ b/rust/src/locale/zh-CN.ftl @@ -754,6 +754,10 @@ ProviderSidebarNoMatches = 没有匹配的提供商 ProviderSidebarReorderHint = 拖动以重新排序 ProviderSidebarMoveUp = 上移 ProviderSidebarMoveDown = 下移 +ProviderSidebarOptions = 服务商列表选项 +ProviderSidebarShowEnabledOnly = 仅显示已启用的 +ProviderSidebarRestoreDefaultOrder = 恢复默认顺序 +ProviderSidebarUndoOrder = 撤销服务商顺序更改 SettingsSidebarSortProviders = 按字母顺序排列服务商 SettingsSidebarSortOffHint = 按字母顺序排列服务商(已启用的在前) SettingsSidebarSortOnHint = 已按字母顺序排列(已启用的在前)— 点击恢复自定义顺序 diff --git a/rust/src/locale/zh-TW.ftl b/rust/src/locale/zh-TW.ftl index b519c31e71..0d3215c254 100644 --- a/rust/src/locale/zh-TW.ftl +++ b/rust/src/locale/zh-TW.ftl @@ -754,6 +754,10 @@ ProviderSidebarNoMatches = 沒有匹配的提供商 ProviderSidebarReorderHint = 拖動以重新排序 ProviderSidebarMoveUp = 上移 ProviderSidebarMoveDown = 下移 +ProviderSidebarOptions = 提供者列表選項 +ProviderSidebarShowEnabledOnly = 僅顯示已啟用的 +ProviderSidebarRestoreDefaultOrder = 還原預設順序 +ProviderSidebarUndoOrder = 復原提供者順序變更 SettingsSidebarSortProviders = 依字母順序排列提供者 SettingsSidebarSortOffHint = 依字母順序排列提供者(已啟用的在前) SettingsSidebarSortOnHint = 已依字母順序排列(已啟用的在前)— 點擊以恢復自訂順序 From 491009782417182b2efe5ebd21165d0a9a683149 Mon Sep 17 00:00:00 2001 From: Factory Agent Date: Fri, 16 Oct 2026 09:42:16 +0700 Subject: [PATCH 5/7] Add enabled-only filter, alias search and undo to Settings providers --- apps/desktop-tauri/src/lib/tauri.ts | 10 ++ .../src/surfaces/Settings.test.tsx | 114 ++++++++++++++++- .../src/surfaces/settings/SettingsSidebar.tsx | 121 +++++++++++++++++- .../settings/providers/ProvidersSidebar.tsx | 53 +++++--- .../src/surfaces/settings/settings-layout.css | 46 ++++++- .../surfaces/settings/settingsTabs.test.ts | 52 ++++++++ .../src/surfaces/settings/settingsTabs.ts | 64 +++++++-- apps/desktop-tauri/src/types/bridge.ts | 5 + 8 files changed, 420 insertions(+), 45 deletions(-) diff --git a/apps/desktop-tauri/src/lib/tauri.ts b/apps/desktop-tauri/src/lib/tauri.ts index e752e57912..59691560ad 100644 --- a/apps/desktop-tauri/src/lib/tauri.ts +++ b/apps/desktop-tauri/src/lib/tauri.ts @@ -93,6 +93,16 @@ export function reorderProviders(ids: string[]): Promise { return invoke("reorder_providers", { ids }); } +/** Settings sidebar reorder: `ids` is the full listed order; hidden providers keep their slots. */ +export function reorderVisibleProviders(ids: string[]): Promise { + return invoke("reorder_visible_providers", { ids }); +} + +/** Restore the built-in order for the providers the Settings sidebar lists. */ +export function restoreDefaultProviderOrder(): Promise { + return invoke("restore_default_provider_order"); +} + export function getSettingsSnapshot(): Promise { return invoke("get_settings_snapshot"); } diff --git a/apps/desktop-tauri/src/surfaces/Settings.test.tsx b/apps/desktop-tauri/src/surfaces/Settings.test.tsx index ab24f2b742..7c422bc183 100644 --- a/apps/desktop-tauri/src/surfaces/Settings.test.tsx +++ b/apps/desktop-tauri/src/surfaces/Settings.test.tsx @@ -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, @@ -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) => ({ @@ -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", () => { @@ -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 }); @@ -195,7 +198,13 @@ describe("Settings sidebar", () => { it("reorders with Alt+Arrow and saves the new order", () => { render(); 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"]); }); @@ -203,7 +212,98 @@ describe("Settings sidebar", () => { render(); 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(); + 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(); + 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(); + 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(); + 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(); + fireEvent.keyDown(row("codex"), { key: "ArrowDown", altKey: true }); + fireEvent.click(optionsButton()); + expect(undoItem()).toBeEnabled(); + fireEvent.click(optionsButton()); + + const moved = [...providers].reverse(); + rerender(); + 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(); + 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("drives the main window surface only outside the detached window", () => { diff --git a/apps/desktop-tauri/src/surfaces/settings/SettingsSidebar.tsx b/apps/desktop-tauri/src/surfaces/settings/SettingsSidebar.tsx index 43bd24e62f..c26f90d6c0 100644 --- a/apps/desktop-tauri/src/surfaces/settings/SettingsSidebar.tsx +++ b/apps/desktop-tauri/src/surfaces/settings/SettingsSidebar.tsx @@ -8,17 +8,21 @@ import type { } from "../../types/bridge"; import { useLocale } from "../../hooks/useLocale"; import { useProviderStatuses } from "../../hooks/useProviderStatuses"; -import { reorderProviders } from "../../lib/tauri"; +import { reorderVisibleProviders, restoreDefaultProviderOrder } from "../../lib/tauri"; import codexbarIcon from "../../assets/codexbar-icon.png"; import { SIDEBAR_PANES, canReorderProviders, + canUndoProviderOrder, filterProvidersByQuery, nextEnabledProviders, + sameIds, sortProvidersAlphabetically, + type ProviderOrderUndo, type SidebarPane, } from "./settingsTabs"; import { + ProviderRowMenu, ProvidersSidebar, type ProviderSidebarRow, type ProviderSidebarStatus, @@ -162,19 +166,25 @@ export function SettingsSidebar({ } const sorted = settings.providersSortedAlphabetically; + const enabledOnly = settings.providersEnabledOnly === true; const enabled = useMemo(() => new Set(settings.enabledProviders), [settings.enabledProviders]); const enabledCount = ordered.filter((p) => enabled.has(p.id)).length; const serviceStatuses = useProviderStatuses(); + // The last saved order change, for Undo. It goes stale once the order moves again. + const [undo, setUndo] = useState(null); + const [filterMenu, setFilterMenu] = useState<{ x: number; y: number } | null>(null); + const undoable = canUndoProviderOrder(undo, ordered.map((p) => p.id)); const rows: ProviderSidebarRow[] = useMemo(() => { const snapshotMap = new Map(snapshots.map((s) => [s.providerId, s])); const serviceMap = new Map( serviceStatuses.map((e) => [e.providerId, e.status?.indicator ?? "unknown"] as const), ); - const shown = filterProvidersByQuery( + const searched = filterProvidersByQuery( sorted ? sortProvidersAlphabetically(ordered, enabled) : ordered, searchText, ); + const shown = enabledOnly ? searched.filter((p) => enabled.has(p.id)) : searched; return shown.map((p) => { const isOn = enabled.has(p.id); return { @@ -185,22 +195,72 @@ export function SettingsSidebar({ service: isOn ? serviceMap.get(p.id) : undefined, }; }); - }, [enabled, ordered, searchText, serviceStatuses, snapshots, sorted]); + }, [enabled, enabledOnly, ordered, searchText, serviceStatuses, snapshots, sorted]); const toggleEnabled = (id: string, on: boolean) => { set({ enabledProviders: nextEnabledProviders(ordered, enabled, id, on) }); }; - const handleReorder = (ids: string[]) => { + /** Shows `ids` at once; the saved order follows from the command. */ + const showOrder = (ids: string[]) => { const byId = new Map(ordered.map((p) => [p.id, p])); setOrdered(ids.map((id) => byId.get(id)).filter((p): p is ProviderCatalogEntry => Boolean(p))); - void reorderProviders(ids).catch(() => setOrdered(providers)); + }; + + const handleReorder = (ids: string[]) => { + const entry: ProviderOrderUndo = { + before: ordered.map((p) => p.id), + after: ids, + restoreAlphabetical: null, + }; + showOrder(ids); + setUndo(entry); + void reorderVisibleProviders(ids).catch(() => { + setOrdered(providers); + setUndo((current) => (current === entry ? null : current)); + }); + }; + + /** Restores the built-in order and turns alphabetical sorting off; undo brings both back. */ + const restoreOrder = () => { + const before = ordered.map((p) => p.id); + const wasSorted = sorted; + if (wasSorted) set({ providersSortedAlphabetically: false }); + void restoreDefaultProviderOrder() + .then((summaries) => { + const ids = summaries.map((s) => s.id); + showOrder(ids); + if (wasSorted || !sameIds(before, ids)) { + setUndo({ before, after: ids, restoreAlphabetical: wasSorted ? true : null }); + } + }) + .catch(() => {}); + }; + + const undoOrder = () => { + if (!undo || !undoable || saving) return; + const entry = undo; + setUndo(null); + showOrder(entry.before); + if (entry.restoreAlphabetical) set({ providersSortedAlphabetically: true }); + void reorderVisibleProviders(entry.before).catch(() => setOrdered(providers)); }; const sortHint = t(sorted ? "SettingsSidebarSortOnHint" : "SettingsSidebarSortOffHint"); return ( -