Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions crates/tui/src/localization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,8 @@ pub enum MessageId {
CtxMenuContextInspectorDesc,
CtxMenuHelp,
CtxMenuHelpDesc,
// Agent fanout card.
FanoutCounts,
}

#[allow(dead_code)]
Expand Down Expand Up @@ -752,6 +754,7 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[
MessageId::CtxMenuContextInspectorDesc,
MessageId::CtxMenuHelp,
MessageId::CtxMenuHelpDesc,
MessageId::FanoutCounts,
];

pub fn tr(locale: Locale, id: MessageId) -> &'static str {
Expand Down Expand Up @@ -1319,6 +1322,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"
}
}
}

Expand Down Expand Up @@ -1754,6 +1760,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ờ"
}
})
}

Expand All @@ -1767,6 +1776,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)?,
})
}
Expand Down Expand Up @@ -2163,6 +2175,9 @@ fn japanese(id: MessageId) -> Option<&'static str> {
MessageId::CtxMenuContextInspectorDesc => "アクティブなコンテキストとキャッシュヒント",
MessageId::CtxMenuHelp => "ヘルプ",
MessageId::CtxMenuHelpDesc => "キー操作とコマンド",
MessageId::FanoutCounts => {
"{done} 完了 · {running} 実行中 · {failed} 失敗 · {pending} 待機"
}
})
}

Expand Down Expand Up @@ -2500,6 +2515,9 @@ fn chinese_simplified(id: MessageId) -> Option<&'static str> {
MessageId::CtxMenuContextInspectorDesc => "活动上下文和缓存提示",
MessageId::CtxMenuHelp => "帮助",
MessageId::CtxMenuHelpDesc => "快捷键和命令",
MessageId::FanoutCounts => {
"{done} 已完成 · {running} 运行中 · {failed} 失败 · {pending} 等待中"
}
})
}

Expand Down Expand Up @@ -2919,6 +2937,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"
}
})
}

Expand Down Expand Up @@ -3346,6 +3367,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"
}
})
}

Expand Down
5 changes: 4 additions & 1 deletion crates/tui/src/tui/subagent_routing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
14 changes: 9 additions & 5 deletions crates/tui/src/tui/ui/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2512,11 +2512,12 @@ fn turn_liveness_leaves_active_turn_running() {
#[test]
fn turn_liveness_uses_recent_turn_activity_not_turn_start() {
let mut app = create_test_app();
let now = Instant::now();
app.is_loading = true;
app.runtime_turn_status = Some("in_progress".to_string());
app.turn_started_at = Some(now - TURN_STALL_WATCHDOG_TIMEOUT - Duration::from_secs(30));
app.turn_last_activity_at = Some(now - Duration::from_secs(1));
app.turn_started_at = Some(Instant::now());
app.turn_last_activity_at =
Some(app.turn_started_at.unwrap() + TURN_STALL_WATCHDOG_TIMEOUT + Duration::from_secs(29));
let now = app.turn_last_activity_at.unwrap() + Duration::from_secs(1);

let recovered = reconcile_turn_liveness(&mut app, now, false);

Expand All @@ -2529,11 +2530,14 @@ fn turn_liveness_uses_recent_turn_activity_not_turn_start() {
#[test]
fn turn_liveness_does_not_abort_running_tool() {
let mut app = create_test_app();
let now = Instant::now();
app.is_loading = true;
app.runtime_turn_status = Some("in_progress".to_string());
app.turn_started_at = Some(now - TURN_STALL_WATCHDOG_TIMEOUT - Duration::from_secs(30));
app.turn_started_at = Some(Instant::now());
app.turn_last_activity_at = app.turn_started_at;
let now = app.turn_started_at.unwrap()
+ TURN_STALL_WATCHDOG_TIMEOUT
+ Duration::from_secs(30)
+ Duration::from_secs(1);
let mut active = ActiveCell::new();
active.push_tool(
"tool-1",
Expand Down
2 changes: 1 addition & 1 deletion crates/tui/src/tui/views/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2203,7 +2203,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)));
Expand Down
47 changes: 37 additions & 10 deletions crates/tui/src/tui/widgets/agent_card.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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,
}
}

Expand Down Expand Up @@ -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),
),
]));
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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(),
Expand All @@ -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);

Expand All @@ -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);
Expand Down Expand Up @@ -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);
}
Expand All @@ -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}");
}
}
Loading