feat(i18n): add FanoutCounts MessageId, wire into FanoutCard stats line

This commit is contained in:
gordonlu
2026-06-02 10:34:33 +08:00
committed by Hunter B
parent 25017091e1
commit cc60129f3a
4 changed files with 66 additions and 12 deletions
+24
View File
@@ -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"
}
})
}
+4 -1
View File
@@ -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);
+1 -1
View File
@@ -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)));
+37 -10
View File
@@ -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<WorkerSlot>,
pub locale: Locale,
}
impl FanoutCard {
#[must_use]
pub fn new(kind: impl Into<String>) -> Self {
pub fn new(kind: impl Into<String>, 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<String> = (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<String> = (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}");
}
}