1use std::time::Duration;
4
5use chrono::Utc;
6use reqwest::Client;
7
8use crate::cache::DEFAULT_TTL;
9use crate::config::Config;
10use crate::error::Result;
11use crate::theme::Theme;
12use crate::vendor::{VendorId, VendorOutcome};
13
14#[derive(Debug, Clone)]
20pub enum TabState {
21 Loading,
22 Ready(Box<ReadyTab>),
23 Error(String),
24}
25
26#[derive(Debug, Clone)]
27pub struct ReadyTab {
28 pub snapshot: crate::usage::VendorSnapshot,
29 pub stale: bool,
30 pub last_error: Option<(u16, String)>,
31 pub fetched_at: Option<chrono::DateTime<chrono::Utc>>,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct TabId {
43 pub vendor: VendorId,
44 pub account: Option<String>,
45}
46
47impl TabId {
48 pub fn vendor(vendor: VendorId) -> Self {
50 Self {
51 vendor,
52 account: None,
53 }
54 }
55
56 pub fn account(label: impl Into<String>) -> Self {
58 Self {
59 vendor: VendorId::Anthropic,
60 account: Some(label.into()),
61 }
62 }
63}
64
65pub fn tabs_from_config(config: &Config) -> Vec<TabId> {
71 let mut tabs = Vec::new();
72 for vendor in config.enabled_vendors() {
73 if vendor == VendorId::Anthropic {
74 let accounts = config.anthropic.all_accounts();
75 if config.anthropic.show_default_account || accounts.is_empty() {
79 tabs.push(TabId::vendor(vendor));
80 }
81 for acct in accounts {
82 tabs.push(TabId::account(acct.label));
83 }
84 } else {
85 tabs.push(TabId::vendor(vendor));
86 }
87 }
88 tabs
89}
90
91#[derive(Debug)]
92pub struct App {
93 pub tabs_meta: Vec<TabId>,
94 pub active: usize,
95 pub tabs: Vec<TabState>,
96 pub tab_generation: u64,
100 pub overview: bool,
103 pub overview_vendors: Option<Vec<VendorId>>,
105 pub theme: Theme,
106 pub quit: bool,
107 pub settings: Option<crate::tui::settings::SettingsState>,
109 pub context_enabled: bool,
112 pub context_generation: u64,
115 pub context: Option<crate::tui::context::ContextState>,
117}
118
119impl App {
120 pub fn new(tabs_meta: Vec<TabId>) -> Self {
121 Self::with_theme(tabs_meta, Theme::default().merged_with_omarchy())
124 }
125
126 pub fn with_theme(tabs_meta: Vec<TabId>, theme: Theme) -> Self {
132 let n = tabs_meta.len();
133 Self {
134 tabs_meta,
135 active: 0,
136 tabs: vec![TabState::Loading; n],
137 tab_generation: 0,
138 overview: false,
139 overview_vendors: None,
140 theme,
141 quit: false,
142 settings: None,
143 context_enabled: false,
144 context_generation: 0,
145 context: None,
146 }
147 }
148
149 pub fn new_with_primary(tabs_meta: Vec<TabId>, primary: Option<VendorId>) -> Self {
153 let mut app = Self::new(tabs_meta);
154 if primary.is_some() {
157 app.select_primary(primary);
158 } else {
159 app.overview = true;
160 }
161 app
162 }
163
164 pub fn active_tab_id(&self) -> Option<&TabId> {
165 self.tabs_meta.get(self.active)
166 }
167
168 pub fn active_vendor(&self) -> Option<VendorId> {
169 self.tabs_meta.get(self.active).map(|t| t.vendor)
170 }
171
172 pub fn set_tabs(&mut self, tabs_meta: Vec<TabId>) {
178 self.tab_generation = self.tab_generation.wrapping_add(1);
179 self.active = self.active.min(tabs_meta.len().saturating_sub(1));
180 self.tabs = vec![TabState::Loading; tabs_meta.len()];
181 self.tabs_meta = tabs_meta;
182 }
183
184 pub fn apply_refresh(&mut self, generation: u64, tab: &TabId, state: TabState) -> bool {
189 if generation != self.tab_generation {
190 return false;
191 }
192 let Some(index) = self.tabs_meta.iter().position(|current| current == tab) else {
193 return false;
194 };
195 self.tabs[index] = state;
196 true
197 }
198
199 pub fn select_primary(&mut self, primary: Option<VendorId>) {
202 if let Some(p) = primary
203 && let Some(idx) = self.tabs_meta.iter().position(|t| t.vendor == p)
204 {
205 self.active = idx;
206 self.overview = false;
207 }
208 }
209
210 pub fn next_tab(&mut self) {
213 if self.overview {
214 if !self.tabs_meta.is_empty() {
215 self.overview = false;
216 self.active = 0;
217 }
218 } else if self.active + 1 < self.tabs_meta.len() {
219 self.active += 1;
220 } else {
221 self.overview = true;
222 }
223 }
224
225 pub fn prev_tab(&mut self) {
226 if self.overview {
227 if !self.tabs_meta.is_empty() {
228 self.overview = false;
229 self.active = self.tabs_meta.len() - 1;
230 }
231 } else if self.active > 0 {
232 self.active -= 1;
233 } else {
234 self.overview = true;
235 }
236 }
237
238 pub fn overview_tabs(&self) -> Vec<usize> {
241 match &self.overview_vendors {
242 None => (0..self.tabs_meta.len()).collect(),
243 Some(wanted) => wanted
244 .iter()
245 .flat_map(|v| {
246 self.tabs_meta
247 .iter()
248 .enumerate()
249 .filter(move |(_, t)| t.vendor == *v)
250 .map(|(i, _)| i)
251 })
252 .collect(),
253 }
254 }
255}
256
257pub async fn refresh_one(client: &Client, config: &Config, tab: &TabId) -> TabState {
259 match build_outcome(client, config, tab).await {
260 Ok(outcome) => {
261 let now = Utc::now();
266 let fetched_at = outcome
267 .cache_age
268 .map(|age| now - chrono::Duration::from_std(age).unwrap_or_default());
269 TabState::Ready(Box::new(ReadyTab {
270 snapshot: outcome.snapshot,
271 stale: outcome.stale,
272 last_error: outcome.last_error,
273 fetched_at,
274 }))
275 }
276 Err(e) => TabState::Error(e.to_string()),
277 }
278}
279
280async fn build_outcome(client: &Client, config: &Config, tab: &TabId) -> Result<VendorOutcome> {
281 match tab.vendor {
282 VendorId::Anthropic => {
283 let (creds_target, cache) = match tab.account.as_deref() {
289 Some(label) => config.anthropic.account_target(label)?,
290 None => {
291 let target = match config.anthropic.credentials_path.clone() {
292 Some(p) => crate::anthropic::creds::CredsTarget::Explicit(p),
293 None => crate::anthropic::creds::CredsTarget::Default(
294 crate::anthropic::creds::default_path().unwrap_or_default(),
295 ),
296 };
297 (target, crate::cache::Cache::for_vendor("anthropic")?)
298 }
299 };
300 let endpoints = crate::anthropic::fetch::Endpoints::default();
301 let outcome = crate::anthropic::fetch_snapshot(
302 client,
303 &creds_target,
304 &cache,
305 &endpoints,
306 DEFAULT_TTL,
307 )
308 .await?;
309 Ok(crate::vendor::VendorOutcome {
310 snapshot: crate::usage::VendorSnapshot::Anthropic(outcome.snapshot),
311 stale: outcome.stale,
312 last_error: outcome.last_error,
313 cache_age: outcome.cache_age,
314 })
315 }
316 VendorId::AnthropicApi => {
317 let key = crate::config::resolve_api_key(
318 "Anthropic_API",
319 &config.anthropic_api.api_key_env,
320 config.anthropic_api.api_key.as_deref(),
321 )?;
322 let cache = crate::cache::Cache::for_vendor("anthropic_api")?;
323 let endpoints = crate::anthropic_api::fetch::Endpoints::default();
324 let outcome = crate::anthropic_api::fetch_snapshot(
325 client,
326 &key,
327 &cache,
328 &endpoints,
329 DEFAULT_TTL,
330 config.anthropic_api.monthly_limit,
331 )
332 .await?;
333 Ok(outcome.into())
334 }
335 VendorId::Openrouter => {
336 let api_key = crate::config::resolve_api_key(
337 "OpenRouter",
338 &config.openrouter.api_key_env,
339 config.openrouter.api_key.as_deref(),
340 )?;
341 let cache = crate::cache::Cache::for_vendor("openrouter")?;
342 let endpoints = crate::openrouter::fetch::Endpoints::default();
343 let outcome = crate::openrouter::fetch_snapshot(
344 client,
345 &api_key,
346 &cache,
347 &endpoints,
348 DEFAULT_TTL,
349 )
350 .await?;
351 Ok(outcome.into())
352 }
353 VendorId::Zai => {
354 let api_key = crate::config::resolve_api_key(
355 "Zai",
356 &config.zai.api_key_env,
357 config.zai.api_key.as_deref(),
358 )?;
359 let cache = crate::cache::Cache::for_vendor("zai")?;
360 let endpoints = crate::zai::fetch::Endpoints::default();
361 let outcome = crate::zai::fetch_snapshot(
362 client,
363 &api_key,
364 &cache,
365 &endpoints,
366 DEFAULT_TTL,
367 config.zai.plan_tier.as_deref(),
368 )
369 .await?;
370 Ok(outcome.into())
371 }
372 VendorId::Openai => {
373 let cache = crate::cache::Cache::for_vendor("openai")?;
374 let creds_path = config
375 .openai
376 .codex_auth_path
377 .clone()
378 .unwrap_or_else(|| crate::openai::creds::default_path().unwrap_or_default());
379 let endpoints = crate::openai::fetch::Endpoints::default();
380 let outcome =
381 crate::openai::fetch_snapshot(client, &creds_path, &cache, &endpoints, DEFAULT_TTL)
382 .await?;
383 Ok(outcome.into())
384 }
385 VendorId::Deepseek => {
386 let api_key = crate::config::resolve_api_key(
387 "DeepSeek",
388 &config.deepseek.api_key_env,
389 config.deepseek.api_key.as_deref(),
390 )?;
391 let cache = crate::cache::Cache::for_vendor("deepseek")?;
392 let endpoints = crate::deepseek::fetch::Endpoints::default();
393 let outcome =
394 crate::deepseek::fetch_snapshot(client, &api_key, &cache, &endpoints, DEFAULT_TTL)
395 .await?;
396 Ok(outcome.into())
397 }
398 VendorId::Kimi => {
399 let api_key = crate::config::resolve_api_key(
400 "Kimi",
401 &config.kimi.api_key_env,
402 config.kimi.api_key.as_deref(),
403 )?;
404 let cache = crate::cache::Cache::for_vendor("kimi")?;
405 let endpoints = crate::kimi::fetch::Endpoints::default();
406 let outcome =
407 crate::kimi::fetch_snapshot(client, &api_key, &cache, &endpoints, DEFAULT_TTL)
408 .await?;
409 Ok(outcome.into())
410 }
411 VendorId::Kilo => {
412 let api_key = crate::config::resolve_api_key(
413 "Kilo",
414 &config.kilo.api_key_env,
415 config.kilo.api_key.as_deref(),
416 )?;
417 let cache = crate::cache::Cache::for_vendor("kilo")?;
418 let endpoints = crate::kilo::fetch::Endpoints::default();
419 let outcome = crate::kilo::fetch_snapshot(
420 client,
421 &api_key,
422 &cache,
423 &endpoints,
424 DEFAULT_TTL,
425 config.kilo.organization_id.as_deref(),
426 )
427 .await?;
428 Ok(outcome.into())
429 }
430 VendorId::Novita => {
431 let api_key = crate::config::resolve_api_key(
432 "Novita",
433 &config.novita.api_key_env,
434 config.novita.api_key.as_deref(),
435 )?;
436 let cache = crate::cache::Cache::for_vendor("novita")?;
437 let endpoints = crate::novita::fetch::Endpoints::default();
438 let outcome =
439 crate::novita::fetch_snapshot(client, &api_key, &cache, &endpoints, DEFAULT_TTL)
440 .await?;
441 Ok(outcome.into())
442 }
443 VendorId::Moonshot => {
444 let api_key = crate::config::resolve_api_key(
445 "Moonshot",
446 &config.moonshot.api_key_env,
447 config.moonshot.api_key.as_deref(),
448 )?;
449 let cache = crate::cache::Cache::for_vendor("moonshot")?;
450 let (endpoints, currency) =
451 crate::moonshot::fetch::Endpoints::for_region(&config.moonshot.region);
452 let outcome = crate::moonshot::fetch_snapshot(
453 client,
454 &api_key,
455 &cache,
456 &endpoints,
457 DEFAULT_TTL,
458 currency,
459 )
460 .await?;
461 Ok(outcome.into())
462 }
463 VendorId::Grok => {
464 let key = crate::config::resolve_api_key(
465 "Grok",
466 &config.grok.api_key_env,
467 config.grok.api_key.as_deref(),
468 )?;
469 let cache = crate::cache::Cache::for_vendor("grok")?;
470 let endpoints = crate::grok::fetch::Endpoints::default();
471 let outcome = crate::grok::fetch_snapshot(
472 client,
473 &key,
474 &cache,
475 &endpoints,
476 DEFAULT_TTL,
477 config.grok.team_id.as_deref(),
478 )
479 .await?;
480 Ok(outcome.into())
481 }
482 VendorId::Antigravity => {
483 let cache = crate::cache::Cache::for_vendor("antigravity")?;
485 let outcome = crate::antigravity::fetch_snapshot(client, &cache, DEFAULT_TTL).await?;
486 Ok(outcome.into())
487 }
488 VendorId::Cursor => {
489 let cache = crate::cache::Cache::for_vendor("cursor")?;
490 let db_path = config
491 .cursor
492 .db_path
493 .clone()
494 .map(Ok)
495 .unwrap_or_else(crate::cursor::db::default_db_path)?;
496 let endpoints = crate::cursor::fetch::Endpoints::default();
497 let outcome =
498 crate::cursor::fetch_snapshot(client, &db_path, &cache, &endpoints, DEFAULT_TTL)
499 .await?;
500 Ok(outcome.into())
501 }
502 }
503}
504
505pub const REFRESH_INTERVAL: Duration = Duration::from_secs(60);
508
509pub const ANTHROPIC_REFRESH_STAGGER: Duration = Duration::from_millis(800);
516
517pub fn refresh_stagger(tabs: &[TabId], step: Duration) -> Vec<Duration> {
523 let mut anthropic_seen: u32 = 0;
524 tabs.iter()
525 .map(|tab| {
526 if tab.vendor == VendorId::Anthropic {
527 let delay = step * anthropic_seen;
528 anthropic_seen += 1;
529 delay
530 } else {
531 Duration::ZERO
532 }
533 })
534 .collect()
535}
536
537#[cfg(test)]
538mod tests {
539 use super::*;
540 use chrono::TimeZone;
541
542 #[test]
546 fn refresh_stagger_spaces_out_anthropic_tabs_only() {
547 let step = Duration::from_millis(800);
548 let tabs = vec![
549 TabId::vendor(VendorId::Anthropic), TabId::account("work"),
551 TabId::account("personal"),
552 TabId::vendor(VendorId::Openai),
553 TabId::vendor(VendorId::Zai),
554 ];
555 let delays = refresh_stagger(&tabs, step);
556 assert_eq!(
557 delays,
558 vec![
559 Duration::ZERO, step, step * 2, Duration::ZERO, Duration::ZERO, ]
565 );
566 }
567
568 #[test]
569 fn refresh_stagger_is_a_noop_without_anthropic_accounts() {
570 let tabs = vec![
572 TabId::vendor(VendorId::Anthropic),
573 TabId::vendor(VendorId::Openrouter),
574 ];
575 assert!(
576 refresh_stagger(&tabs, Duration::from_millis(800))
577 .iter()
578 .all(|d| d.is_zero())
579 );
580 }
581
582 #[test]
583 fn select_primary_moves_to_enabled_vendor() {
584 let mut app = App::with_theme(
585 vec![
586 TabId::vendor(VendorId::Anthropic),
587 TabId::vendor(VendorId::Openrouter),
588 ],
589 Theme::default(),
590 );
591 app.select_primary(Some(VendorId::Openrouter));
592 assert_eq!(app.active_vendor(), Some(VendorId::Openrouter));
593 }
594
595 #[test]
596 fn select_primary_ignores_disabled_vendor() {
597 let mut app = App::with_theme(vec![TabId::vendor(VendorId::Anthropic)], Theme::default());
598 app.select_primary(Some(VendorId::Openai));
599 assert_eq!(app.active_vendor(), Some(VendorId::Anthropic));
600 }
601
602 #[test]
603 fn nav_ring_wraps_through_the_overview_at_both_ends() {
604 let mut app = App::with_theme(
605 vec![
606 TabId::vendor(VendorId::Anthropic),
607 TabId::vendor(VendorId::Openai),
608 ],
609 Theme::default(),
610 );
611 app.overview = true;
612
613 app.next_tab(); assert!(!app.overview);
615 assert_eq!(app.active, 0);
616 app.next_tab();
617 assert_eq!(app.active, 1);
618 app.next_tab(); assert!(app.overview);
620
621 app.prev_tab(); assert!(!app.overview);
623 assert_eq!(app.active, 1);
624 app.prev_tab();
625 assert_eq!(app.active, 0);
626 app.prev_tab(); assert!(app.overview);
628 }
629
630 #[test]
631 fn overview_tabs_defaults_to_all_and_honors_the_config_filter() {
632 let mut app = App::with_theme(
633 vec![
634 TabId::vendor(VendorId::Anthropic),
635 TabId::vendor(VendorId::Openai),
636 TabId::vendor(VendorId::Zai),
637 ],
638 Theme::default(),
639 );
640 assert_eq!(app.overview_tabs(), vec![0, 1, 2]);
641
642 app.overview_vendors = Some(vec![VendorId::Zai, VendorId::Anthropic]);
644 assert_eq!(app.overview_tabs(), vec![2, 0]);
645
646 app.overview_vendors = Some(vec![VendorId::Grok, VendorId::Openai]);
648 assert_eq!(app.overview_tabs(), vec![1]);
649 }
650
651 fn config_with_accounts(labels: &[&str]) -> Config {
652 let mut config = Config::default();
653 config.openai.enabled = false;
656 config.zai.enabled = false;
657 config.openrouter.enabled = false;
658 config.anthropic.accounts = labels
659 .iter()
660 .map(|l| crate::config::AnthropicAccount {
661 label: (*l).to_string(),
662 credentials_path: format!("/creds/{l}.json").into(),
663 })
664 .collect();
665 config
666 }
667
668 #[test]
669 fn show_default_account_false_hides_the_unnamed_claude_tab() {
670 let mut config = config_with_accounts(&["work", "personal"]);
673 config.anthropic.show_default_account = false;
674 assert_eq!(
675 tabs_from_config(&config),
676 vec![TabId::account("work"), TabId::account("personal")]
677 );
678
679 let mut empty = Config::default();
682 empty.openai.enabled = false;
683 empty.zai.enabled = false;
684 empty.openrouter.enabled = false;
685 empty.anthropic.show_default_account = false;
686 assert_eq!(
687 tabs_from_config(&empty),
688 vec![TabId::vendor(VendorId::Anthropic)]
689 );
690 }
691
692 #[test]
693 fn tabs_expand_anthropic_accounts_after_default() {
694 let tabs = tabs_from_config(&config_with_accounts(&["work", "personal"]));
696 assert_eq!(
697 tabs,
698 vec![
699 TabId::vendor(VendorId::Anthropic),
700 TabId::account("work"),
701 TabId::account("personal"),
702 ]
703 );
704 }
705
706 #[test]
707 fn tabs_without_accounts_are_just_enabled_vendors() {
708 let config = Config::default();
710 let tabs = tabs_from_config(&config);
711 let vendors: Vec<VendorId> = tabs.iter().map(|t| t.vendor).collect();
712 assert_eq!(vendors, config.enabled_vendors());
713 assert!(tabs.iter().all(|t| t.account.is_none()));
714 }
715
716 #[test]
717 fn tabs_include_accounts_auto_discovered_from_accounts_dir() {
718 let td = tempfile::tempdir().unwrap();
721 for label in ["work", "personal"] {
722 let dir = td.path().join(label);
723 std::fs::create_dir_all(&dir).unwrap();
724 std::fs::write(dir.join(".credentials.json"), "{}").unwrap();
725 }
726 let mut config = Config::default();
727 config.openai.enabled = false;
728 config.zai.enabled = false;
729 config.openrouter.enabled = false;
730 config.anthropic.accounts_dir = Some(td.path().to_path_buf());
731
732 let tabs = tabs_from_config(&config);
733 assert_eq!(
734 tabs,
735 vec![
736 TabId::vendor(VendorId::Anthropic),
737 TabId::account("personal"), TabId::account("work"),
739 ]
740 );
741 }
742
743 #[test]
744 fn set_tabs_resets_states_and_clamps_selection() {
745 let mut app = App::with_theme(
749 tabs_from_config(&config_with_accounts(&["work", "personal"])),
750 Theme::default(),
751 );
752 app.active = 2; app.tabs[0] = TabState::Error("old".into());
754
755 app.set_tabs(tabs_from_config(&config_with_accounts(&[])));
756 assert_eq!(app.tabs_meta, vec![TabId::vendor(VendorId::Anthropic)]);
757 assert_eq!(app.active, 0, "selection clamped after shrink");
758 assert!(matches!(app.tabs[0], TabState::Loading));
759 }
760
761 #[test]
762 fn refresh_from_old_generation_is_discarded() {
763 let mut app = App::with_theme(vec![TabId::vendor(VendorId::Anthropic)], Theme::default());
764 let old_generation = app.tab_generation;
765 app.set_tabs(vec![TabId::vendor(VendorId::Openai)]);
766
767 assert!(!app.apply_refresh(
768 old_generation,
769 &TabId::vendor(VendorId::Anthropic),
770 TabState::Error("old result".into()),
771 ));
772 assert!(matches!(app.tabs[0], TabState::Loading));
773 }
774
775 #[test]
776 fn refresh_identity_mismatch_is_discarded() {
777 let mut app = App::with_theme(vec![TabId::vendor(VendorId::Anthropic)], Theme::default());
778 let generation = app.tab_generation;
779
780 assert!(!app.apply_refresh(
781 generation,
782 &TabId::vendor(VendorId::Openai),
783 TabState::Error("wrong tab".into()),
784 ));
785 assert!(matches!(app.tabs[0], TabState::Loading));
786 }
787
788 #[test]
789 fn refresh_identity_lands_at_new_index_after_same_generation_reorder() {
790 let anthropic = TabId::vendor(VendorId::Anthropic);
791 let openai = TabId::vendor(VendorId::Openai);
792 let mut app = App::with_theme(vec![anthropic.clone(), openai.clone()], Theme::default());
793 let generation = app.tab_generation;
794
795 app.tabs_meta.swap(0, 1);
798 app.tabs.swap(0, 1);
799 assert!(app.apply_refresh(generation, &anthropic, TabState::Error("ready".into())));
800 assert!(matches!(app.tabs[0], TabState::Loading));
801 assert!(matches!(&app.tabs[1], TabState::Error(message) if message == "ready"));
802 }
803
804 fn ready_at(fetched_at: chrono::DateTime<Utc>) -> TabState {
805 TabState::Ready(Box::new(ReadyTab {
806 snapshot: crate::usage::VendorSnapshot::Openrouter(crate::usage::OpenRouterSnapshot {
807 label: "test".into(),
808 total_credits: 0.0,
809 total_usage: 0.0,
810 usage_daily: 0.0,
811 usage_weekly: 0.0,
812 usage_monthly: 0.0,
813 is_free_tier: false,
814 limit: None,
815 limit_remaining: None,
816 }),
817 stale: false,
818 last_error: None,
819 fetched_at: Some(fetched_at),
820 }))
821 }
822
823 #[test]
824 fn apply_refresh_stamps_fetched_at_on_only_the_matching_tab() {
825 let anthropic = TabId::vendor(VendorId::Anthropic);
831 let openai = TabId::vendor(VendorId::Openai);
832 let mut app = App::with_theme(vec![anthropic.clone(), openai], Theme::default());
833 let generation = app.tab_generation;
834 let fetched_at = Utc.with_ymd_and_hms(2026, 5, 23, 12, 0, 0).unwrap();
835
836 assert!(app.apply_refresh(generation, &anthropic, ready_at(fetched_at)));
837 match &app.tabs[0] {
838 TabState::Ready(ready) => assert_eq!(ready.fetched_at, Some(fetched_at)),
839 other => panic!("expected Anthropic tab Ready, got {other:?}"),
840 }
841 assert!(matches!(app.tabs[1], TabState::Loading));
842 }
843
844 #[test]
845 fn select_primary_lands_on_default_account_tab() {
846 let app = {
849 let tabs = tabs_from_config(&config_with_accounts(&["work"]));
850 let mut a = App::with_theme(tabs, Theme::default());
851 a.select_primary(Some(VendorId::Anthropic));
852 a
853 };
854 assert_eq!(app.active, 0);
855 assert_eq!(
856 app.active_tab_id(),
857 Some(&TabId::vendor(VendorId::Anthropic))
858 );
859 }
860}