diff --git a/crates/tui/src/localization.rs b/crates/tui/src/localization.rs index a1fa10b8..f08fbccc 100644 --- a/crates/tui/src/localization.rs +++ b/crates/tui/src/localization.rs @@ -503,6 +503,8 @@ pub enum MessageId { CtxMenuContextInspectorDesc, CtxMenuHelp, CtxMenuHelpDesc, + // Agent fanout card. + FanoutCounts, } #[allow(dead_code)] @@ -782,6 +784,7 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[ MessageId::CtxMenuContextInspectorDesc, MessageId::CtxMenuHelp, MessageId::CtxMenuHelpDesc, + MessageId::FanoutCounts, ]; pub fn tr(locale: Locale, id: MessageId) -> &'static str { @@ -1370,6 +1373,9 @@ fn english(id: MessageId) -> &'static str { MessageId::CtxMenuContextInspectorDesc => "active context and cache hints", MessageId::CtxMenuHelp => "Help", MessageId::CtxMenuHelpDesc => "keybindings and commands", + MessageId::FanoutCounts => { + "{done} done · {running} running · {failed} failed · {pending} pending" + } } } @@ -1826,6 +1832,9 @@ fn vietnamese(id: MessageId) -> Option<&'static str> { MessageId::CtxMenuContextInspectorDesc => "ngữ cảnh đang hoạt động và gợi ý bộ nhớ đệm", MessageId::CtxMenuHelp => "Trợ giúp", MessageId::CtxMenuHelpDesc => "phím tắt và lệnh", + MessageId::FanoutCounts => { + "{done} hoàn thành · {running} đang chạy · {failed} thất bại · {pending} chờ" + } }) } @@ -1839,6 +1848,9 @@ fn traditional_chinese(id: MessageId) -> Option<&'static str> { MessageId::TranslationComplete => "翻譯完成", MessageId::TranslationFailed => "翻譯失敗", MessageId::FooterBalancePrefix => "餘額", + MessageId::FanoutCounts => { + "{done} 已完成 · {running} 運行中 · {failed} 失敗 · {pending} 等待中" + } other => chinese_simplified(other)?, }) } @@ -2256,6 +2268,9 @@ fn japanese(id: MessageId) -> Option<&'static str> { MessageId::CtxMenuContextInspectorDesc => "アクティブなコンテキストとキャッシュヒント", MessageId::CtxMenuHelp => "ヘルプ", MessageId::CtxMenuHelpDesc => "キー操作とコマンド", + MessageId::FanoutCounts => { + "{done} 完了 · {running} 実行中 · {failed} 失敗 · {pending} 待機" + } }) } @@ -2612,6 +2627,9 @@ fn chinese_simplified(id: MessageId) -> Option<&'static str> { MessageId::CtxMenuContextInspectorDesc => "活动上下文和缓存提示", MessageId::CtxMenuHelp => "帮助", MessageId::CtxMenuHelpDesc => "快捷键和命令", + MessageId::FanoutCounts => { + "{done} 已完成 · {running} 运行中 · {failed} 失败 · {pending} 等待中" + } }) } @@ -3052,6 +3070,9 @@ fn portuguese_brazil(id: MessageId) -> Option<&'static str> { MessageId::CtxMenuContextInspectorDesc => "contexto ativo e dicas de cache", MessageId::CtxMenuHelp => "Ajuda", MessageId::CtxMenuHelpDesc => "atalhos de teclado e comandos", + MessageId::FanoutCounts => { + "{done} concluído · {running} executando · {failed} falhou · {pending} pendente" + } }) } @@ -3502,6 +3523,9 @@ fn spanish_latin_america(id: MessageId) -> Option<&'static str> { MessageId::CtxMenuContextInspectorDesc => "contexto activo y sugerencias de caché", MessageId::CtxMenuHelp => "Ayuda", MessageId::CtxMenuHelpDesc => "atajos de teclado y comandos", + MessageId::FanoutCounts => { + "{done} completado · {running} ejecutando · {failed} falló · {pending} pendiente" + } }) } diff --git a/crates/tui/src/tui/subagent_routing.rs b/crates/tui/src/tui/subagent_routing.rs index 94c9e975..afe48361 100644 --- a/crates/tui/src/tui/subagent_routing.rs +++ b/crates/tui/src/tui/subagent_routing.rs @@ -154,7 +154,10 @@ pub(super) fn handle_subagent_mailbox(app: &mut App, seq: u64, message: &Mailbox card.claim_pending_worker(&agent_id, AgentLifecycle::Running); app.subagent_card_index.insert(agent_id, idx); } else { - let mut card = FanoutCard::new(dispatch_kind.unwrap_or("rlm_eval").to_string()); + let mut card = FanoutCard::new( + dispatch_kind.unwrap_or("rlm_eval").to_string(), + app.ui_locale, + ); card.upsert_worker(&agent_id, AgentLifecycle::Running); app.add_message(HistoryCell::SubAgent(SubAgentCell::Fanout(card))); let idx = app.history.len().saturating_sub(1); diff --git a/crates/tui/src/tui/views/mod.rs b/crates/tui/src/tui/views/mod.rs index 49e213c6..7cc4c11b 100644 --- a/crates/tui/src/tui/views/mod.rs +++ b/crates/tui/src/tui/views/mod.rs @@ -2252,7 +2252,7 @@ mod tests { #[test] fn subagent_view_agents_includes_live_fanout_workers_when_cache_is_empty() { let mut app = create_test_app(); - let mut card = FanoutCard::new("rlm").with_workers(["chunk_1", "chunk_2"]); + let mut card = FanoutCard::new("rlm", app.ui_locale).with_workers(["chunk_1", "chunk_2"]); card.upsert_worker("chunk_1", AgentLifecycle::Completed); card.upsert_worker("chunk_2", AgentLifecycle::Running); app.add_message(HistoryCell::SubAgent(SubAgentCell::Fanout(card))); diff --git a/crates/tui/src/tui/widgets/agent_card.rs b/crates/tui/src/tui/widgets/agent_card.rs index 5b7098a0..7765780d 100644 --- a/crates/tui/src/tui/widgets/agent_card.rs +++ b/crates/tui/src/tui/widgets/agent_card.rs @@ -17,6 +17,7 @@ use ratatui::style::{Color, Modifier, Style}; use ratatui::text::{Line, Span}; +use crate::localization::{Locale, MessageId, tr}; use crate::palette; use crate::tools::subagent::MailboxMessage; use crate::tui::widgets::tool_card::{ToolFamily, family_glyph, family_label}; @@ -193,14 +194,16 @@ impl WorkerSlot { pub struct FanoutCard { pub kind: String, pub workers: Vec, + pub locale: Locale, } impl FanoutCard { #[must_use] - pub fn new(kind: impl Into) -> Self { + pub fn new(kind: impl Into, locale: Locale) -> Self { Self { kind: kind.into(), workers: Vec::new(), + locale, } } @@ -309,9 +312,11 @@ impl FanoutCard { lines.push(Line::from(vec![ Span::styled(" ", Style::default()), Span::styled( - format!( - "{done} done \u{00B7} {running} running \u{00B7} {failed} failed \u{00B7} {pending} pending" - ), + tr(self.locale, MessageId::FanoutCounts) + .replace("{done}", &done.to_string()) + .replace("{running}", &running.to_string()) + .replace("{failed}", &failed.to_string()) + .replace("{pending}", &pending.to_string()), Style::default().fg(palette::TEXT_MUTED), ), ])); @@ -632,7 +637,7 @@ mod tests { #[test] fn fanout_card_dot_grid_renders_stateful_worker_slots() { - let mut card = FanoutCard::new("fanout") + let mut card = FanoutCard::new("fanout", Locale::En) .with_workers(["w_1", "w_2", "w_3", "w_4", "w_5", "w_6", "w_7"]); card.upsert_worker("w_1", AgentLifecycle::Completed); card.upsert_worker("w_2", AgentLifecycle::Completed); @@ -649,7 +654,8 @@ mod tests { #[test] fn fanout_card_aggregate_counts_match_dot_grid() { - let mut card = FanoutCard::new("rlm").with_workers(["w_1", "w_2", "w_3", "w_4"]); + let mut card = + FanoutCard::new("rlm", Locale::En).with_workers(["w_1", "w_2", "w_3", "w_4"]); card.upsert_worker("w_1", AgentLifecycle::Completed); card.upsert_worker("w_2", AgentLifecycle::Completed); card.upsert_worker("w_3", AgentLifecycle::Completed); @@ -672,7 +678,7 @@ mod tests { #[test] fn fanout_apply_inserts_unknown_worker_via_child_spawned() { - let mut card = FanoutCard::new("fanout"); + let mut card = FanoutCard::new("fanout", Locale::En); let msg = MailboxMessage::ChildSpawned { parent_id: "root".into(), child_id: "agent_late".into(), @@ -685,7 +691,7 @@ mod tests { #[test] fn fanout_started_claims_seeded_pending_slot_without_growing_grid() { - let mut card = FanoutCard::new("fanout").with_workers(["task:a", "task:b"]); + let mut card = FanoutCard::new("fanout", Locale::En).with_workers(["task:a", "task:b"]); let started = MailboxMessage::started("agent_live", crate::tools::subagent::SubAgentType::General); @@ -700,7 +706,7 @@ mod tests { #[test] fn fanout_apply_transitions_worker_through_lifecycle() { - let mut card = FanoutCard::new("fanout").with_workers(["w_1"]); + let mut card = FanoutCard::new("fanout", Locale::En).with_workers(["w_1"]); let started = MailboxMessage::started("w_1", crate::tools::subagent::SubAgentType::General); apply_to_fanout(&mut card, &started); assert_eq!(card.workers[0].status, AgentLifecycle::Running); @@ -729,7 +735,7 @@ mod tests { ]; for (total, done, expected) in cases { let ids: Vec = (0..*total).map(|i| format!("w_{i}")).collect(); - let mut card = FanoutCard::new("fanout").with_workers(ids.iter().cloned()); + let mut card = FanoutCard::new("fanout", Locale::En).with_workers(ids.iter().cloned()); for id in ids.iter().take(*done) { card.upsert_worker(id, AgentLifecycle::Completed); } @@ -740,4 +746,25 @@ mod tests { ); } } + + #[test] + fn fanout_counts_are_localized() { + let ids: Vec = (0..16).map(|i| format!("w_{i}")).collect(); + let mut card = FanoutCard::new("fanout", Locale::ZhHans).with_workers(ids.iter().cloned()); + for id in ids.iter().take(12) { + card.upsert_worker(id, AgentLifecycle::Completed); + } + card.upsert_worker("w_12", AgentLifecycle::Running); + // w_13..w_15 stay Pending; 0 failed + + let rendered = render_to_strings(&card.render_lines(80)); + let stats = rendered + .iter() + .find(|line| line.contains('·')) + .expect("counts line present"); + assert!(stats.contains("已完成"), "{stats}"); + assert!(stats.contains("运行中"), "{stats}"); + assert!(stats.contains("失败"), "{stats}"); + assert!(stats.contains("等待中"), "{stats}"); + } }