Skip to content
Merged
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ docs/*_PLAN.md
.envrc
.direnv
scripts/run_deep_swe.py
.claude/

# Benchmark artifacts and caches re-included by !scripts/**
results/
Expand Down
91 changes: 91 additions & 0 deletions crates/tui/src/localization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,17 @@ pub enum MessageId {
CtxInspChangesByTurn,
CtxInspStablePrefixOnly,
CtxInspCacheTip,
// Tool family labels (card headers, sidebar, footer).
ToolFamilyRead,
ToolFamilyPatch,
ToolFamilyRun,
ToolFamilyFind,
ToolFamilyDelegate,
ToolFamilyFanout,
ToolFamilyRlm,
ToolFamilyVerify,
ToolFamilyThink,
ToolFamilyGeneric,
}

#[allow(dead_code)]
Expand Down Expand Up @@ -974,6 +985,16 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[
MessageId::CtxInspChangesByTurn,
MessageId::CtxInspStablePrefixOnly,
MessageId::CtxInspCacheTip,
MessageId::ToolFamilyRead,
MessageId::ToolFamilyPatch,
MessageId::ToolFamilyRun,
MessageId::ToolFamilyFind,
MessageId::ToolFamilyDelegate,
MessageId::ToolFamilyFanout,
MessageId::ToolFamilyRlm,
MessageId::ToolFamilyVerify,
MessageId::ToolFamilyThink,
MessageId::ToolFamilyGeneric,
];

pub fn tr(locale: Locale, id: MessageId) -> &'static str {
Expand Down Expand Up @@ -1672,6 +1693,16 @@ fn english(id: MessageId) -> &'static str {
"Tip: Stable prefix blocks are DeepSeek V4 prefix-cache eligible. \
Volatile working-set changes break the cache only for the tail."
}
MessageId::ToolFamilyRead => "read",
MessageId::ToolFamilyPatch => "patch",
MessageId::ToolFamilyRun => "run",
MessageId::ToolFamilyFind => "find",
MessageId::ToolFamilyDelegate => "delegate",
MessageId::ToolFamilyFanout => "fanout",
MessageId::ToolFamilyRlm => "rlm",
MessageId::ToolFamilyVerify => "verify",
MessageId::ToolFamilyThink => "think",
MessageId::ToolFamilyGeneric => "tool",
}
}

Expand Down Expand Up @@ -2238,6 +2269,16 @@ fn vietnamese(id: MessageId) -> Option<&'static str> {
MessageId::CtxInspCacheTip => {
"Gợi ý: Các khối ổn định đủ điều kiện cho bộ nhớ đệm tiền tố DeepSeek V4. Thay đổi vùng làm việc chỉ phá vỡ bộ nhớ đệm ở phần cuối."
}
MessageId::ToolFamilyRead => "đọc",
MessageId::ToolFamilyPatch => "vá",
MessageId::ToolFamilyRun => "chạy",
MessageId::ToolFamilyFind => "tìm",
MessageId::ToolFamilyDelegate => "ủy quyền",
MessageId::ToolFamilyFanout => "fanout",
MessageId::ToolFamilyRlm => "rlm",
MessageId::ToolFamilyVerify => "xác minh",
MessageId::ToolFamilyThink => "suy nghĩ",
MessageId::ToolFamilyGeneric => "công cụ",
})
}

Expand Down Expand Up @@ -2358,6 +2399,16 @@ fn traditional_chinese(id: MessageId) -> Option<&'static str> {
MessageId::StatusPickerActionNone => "無 ",
MessageId::StatusPickerActionSave => "儲存 ",
MessageId::StatusPickerActionCancel => "取消 ",
MessageId::ToolFamilyRead => "讀取",
MessageId::ToolFamilyPatch => "修補",
MessageId::ToolFamilyRun => "執行",
MessageId::ToolFamilyFind => "搜尋",
MessageId::ToolFamilyDelegate => "委派",
MessageId::ToolFamilyFanout => "扇出",
MessageId::ToolFamilyRlm => "rlm",
MessageId::ToolFamilyVerify => "驗證",
MessageId::ToolFamilyThink => "思考",
MessageId::ToolFamilyGeneric => "工具",
other => chinese_simplified(other)?,
})
}
Expand Down Expand Up @@ -2883,6 +2934,16 @@ fn japanese(id: MessageId) -> Option<&'static str> {
MessageId::CtxInspCacheTip => {
"ヒント:安定プレフィックスブロックはDeepSeek V4プレフィックスキャッシュの対象です。揮発性ワーキングセットの変更は末尾のキャッシュのみを破壊します。"
}
MessageId::ToolFamilyRead => "読込",
MessageId::ToolFamilyPatch => "パッチ",
MessageId::ToolFamilyRun => "実行",
MessageId::ToolFamilyFind => "検索",
MessageId::ToolFamilyDelegate => "委任",
MessageId::ToolFamilyFanout => "ファンアウト",
MessageId::ToolFamilyRlm => "rlm",
MessageId::ToolFamilyVerify => "検証",
MessageId::ToolFamilyThink => "思考",
MessageId::ToolFamilyGeneric => "ツール",
})
}

Expand Down Expand Up @@ -3343,6 +3404,16 @@ fn chinese_simplified(id: MessageId) -> Option<&'static str> {
MessageId::CtxInspCacheTip => {
"提示:稳定前缀区块符合 DeepSeek V4 前缀缓存条件。易变工作集的更改仅会破坏缓存尾部。"
}
MessageId::ToolFamilyRead => "读取",
MessageId::ToolFamilyPatch => "修补",
MessageId::ToolFamilyRun => "运行",
MessageId::ToolFamilyFind => "搜索",
MessageId::ToolFamilyDelegate => "委派",
MessageId::ToolFamilyFanout => "扇出",
MessageId::ToolFamilyRlm => "rlm",
MessageId::ToolFamilyVerify => "验证",
MessageId::ToolFamilyThink => "思考",
MessageId::ToolFamilyGeneric => "工具",
})
}

Expand Down Expand Up @@ -3893,6 +3964,16 @@ fn portuguese_brazil(id: MessageId) -> Option<&'static str> {
MessageId::CtxInspCacheTip => {
"Dica: Blocos de prefixo estável são elegíveis para cache de prefixo DeepSeek V4. Alterações no conjunto de trabalho volátil quebram o cache apenas no final."
}
MessageId::ToolFamilyRead => "ler",
MessageId::ToolFamilyPatch => "corrigir",
MessageId::ToolFamilyRun => "executar",
MessageId::ToolFamilyFind => "buscar",
MessageId::ToolFamilyDelegate => "delegar",
MessageId::ToolFamilyFanout => "fanout",
MessageId::ToolFamilyRlm => "rlm",
MessageId::ToolFamilyVerify => "verificar",
MessageId::ToolFamilyThink => "pensar",
MessageId::ToolFamilyGeneric => "ferramenta",
})
}

Expand Down Expand Up @@ -4453,6 +4534,16 @@ fn spanish_latin_america(id: MessageId) -> Option<&'static str> {
MessageId::CtxInspCacheTip => {
"Consejo: Los bloques de prefijo estable son elegibles para caché de prefijo DeepSeek V4. Los cambios en el conjunto de trabajo volátil solo rompen la caché al final."
}
MessageId::ToolFamilyRead => "leer",
MessageId::ToolFamilyPatch => "parchear",
MessageId::ToolFamilyRun => "ejecutar",
MessageId::ToolFamilyFind => "buscar",
MessageId::ToolFamilyDelegate => "delegar",
MessageId::ToolFamilyFanout => "fanout",
MessageId::ToolFamilyRlm => "rlm",
MessageId::ToolFamilyVerify => "verificar",
MessageId::ToolFamilyThink => "pensar",
MessageId::ToolFamilyGeneric => "herramienta",
})
}

Expand Down
12 changes: 8 additions & 4 deletions crates/tui/src/tui/footer_ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::time::Instant;
use unicode_width::UnicodeWidthStr;

use crate::core::coherence::CoherenceState;
use crate::localization::MessageId;
use crate::localization::{Locale, MessageId};
use crate::palette;
use crate::tools::subagent::SubAgentStatus;
use crate::tui::app::App;
Expand Down Expand Up @@ -314,7 +314,7 @@ pub(crate) fn active_tool_status_label(app: &App) -> Option<String> {

let mut snapshot = ActiveToolStatusSnapshot::default();
for cell in active.entries() {
collect_active_tool_status(cell, &mut snapshot);
collect_active_tool_status(cell, &mut snapshot, app.ui_locale);
}
if snapshot.total() == 0 {
return None;
Expand Down Expand Up @@ -345,7 +345,11 @@ pub(crate) fn active_tool_status_label(app: &App) -> Option<String> {
Some(parts.join(" \u{00B7} "))
}

fn collect_active_tool_status(cell: &HistoryCell, snapshot: &mut ActiveToolStatusSnapshot) {
fn collect_active_tool_status(
cell: &HistoryCell,
snapshot: &mut ActiveToolStatusSnapshot,
locale: Locale,
) {
let HistoryCell::Tool(tool) = cell else {
return;
};
Expand Down Expand Up @@ -401,7 +405,7 @@ fn collect_active_tool_status(cell: &HistoryCell, snapshot: &mut ActiveToolStatu
return;
}
snapshot.record(
tool_activity_label_for_name(&generic.name),
tool_activity_label_for_name(&generic.name, locale),
generic.status,
None,
);
Expand Down
14 changes: 10 additions & 4 deletions crates/tui/src/tui/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9366,7 +9366,10 @@ fn activity_cell_label(app: &App, cell_index: usize, cell: &HistoryCell) -> Stri
HistoryCell::Error { .. } => "error".to_string(),
HistoryCell::SubAgent(_) => "sub-agent".to_string(),
HistoryCell::Tool(ToolCell::Generic(generic)) => {
crate::tui::widgets::tool_card::tool_activity_label_for_name(&generic.name)
crate::tui::widgets::tool_card::tool_activity_label_for_name(
&generic.name,
app.ui_locale,
)
}
HistoryCell::Tool(_) => {
detail_target_label(app, cell_index).unwrap_or_else(|| "tool activity".to_string())
Expand Down Expand Up @@ -9801,9 +9804,12 @@ pub(crate) fn detail_target_label(app: &App, cell_index: usize) -> Option<String
Some(format!("image {}", image.path.display()))
}
HistoryCell::Tool(ToolCell::WebSearch(search)) => Some(format!("search {}", search.query)),
HistoryCell::Tool(ToolCell::Generic(generic)) => {
Some(crate::tui::widgets::tool_card::tool_activity_label_for_name(&generic.name))
}
HistoryCell::Tool(ToolCell::Generic(generic)) => Some(
crate::tui::widgets::tool_card::tool_activity_label_for_name(
&generic.name,
app.ui_locale,
),
),
HistoryCell::SubAgent(_) => Some("sub-agent".to_string()),
_ => None,
}
Expand Down
116 changes: 109 additions & 7 deletions crates/tui/src/tui/widgets/tool_card.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
//! module is the vocabulary, not the layout engine. Keeping it small means
//! a future visual refresh only has to touch the constants here.

use crate::localization::Locale;

/// Tool family — the verb the agent is performing. Used to pick a glyph
/// and label for the card header.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
Expand Down Expand Up @@ -97,8 +99,9 @@ pub fn tool_family_for_name(name: &str) -> ToolFamily {

/// User-facing label for an arbitrary tool name. Known tools collapse to the
/// semantic verb; unknown tools keep their exact name for debugging.
#[cfg(test)]
#[must_use]
pub fn tool_display_label_for_name(name: &str) -> String {
fn tool_display_label_for_name(name: &str) -> String {
let family = tool_family_for_name(name);
if matches!(family, ToolFamily::Generic) {
name.to_string()
Expand All @@ -107,15 +110,31 @@ pub fn tool_display_label_for_name(name: &str) -> String {
}
}

fn family_message_id(family: ToolFamily) -> crate::localization::MessageId {
match family {
ToolFamily::Read => crate::localization::MessageId::ToolFamilyRead,
ToolFamily::Patch => crate::localization::MessageId::ToolFamilyPatch,
ToolFamily::Run => crate::localization::MessageId::ToolFamilyRun,
ToolFamily::Find => crate::localization::MessageId::ToolFamilyFind,
ToolFamily::Delegate => crate::localization::MessageId::ToolFamilyDelegate,
ToolFamily::Fanout => crate::localization::MessageId::ToolFamilyFanout,
ToolFamily::Rlm => crate::localization::MessageId::ToolFamilyRlm,
ToolFamily::Verify => crate::localization::MessageId::ToolFamilyVerify,
ToolFamily::Think => crate::localization::MessageId::ToolFamilyThink,
ToolFamily::Generic => crate::localization::MessageId::ToolFamilyGeneric,
}
}

/// Compact activity/status label for arbitrary tool names. Known built-ins use
/// the semantic verb; unknown tools keep the `tool NAME` form.
#[must_use]
pub fn tool_activity_label_for_name(name: &str) -> String {
pub fn tool_activity_label_for_name(name: &str, locale: Locale) -> String {
let family = tool_family_for_name(name);
let mid = family_message_id(family);
if matches!(family, ToolFamily::Generic) {
format!("tool {name}")
format!("{} {name}", crate::localization::tr(locale, mid))
} else {
tool_display_label_for_name(name)
crate::localization::tr(locale, mid).to_string()
}
}

Expand Down Expand Up @@ -237,6 +256,7 @@ mod tests {
tool_display_label_for_name, tool_family_for_name, tool_family_for_title,
tool_header_summary_for_name,
};
use crate::localization::{Locale, MessageId, tr};

#[test]
fn legacy_titles_route_to_expected_families() {
Expand Down Expand Up @@ -275,10 +295,16 @@ mod tests {
"future_private_tool"
);

assert_eq!(tool_activity_label_for_name("exec_shell"), "run");
assert_eq!(tool_activity_label_for_name("run_verifiers"), "verify");
assert_eq!(
tool_activity_label_for_name("future_private_tool"),
tool_activity_label_for_name("exec_shell", Locale::En),
"run"
);
assert_eq!(
tool_activity_label_for_name("run_verifiers", Locale::En),
"verify"
);
assert_eq!(
tool_activity_label_for_name("future_private_tool", Locale::En),
"tool future_private_tool"
);
}
Expand Down Expand Up @@ -344,4 +370,80 @@ mod tests {
assert_eq!(rail_glyph(CardRail::Bottom), "\u{2570}");
assert!(rail_glyph(CardRail::Single).is_empty());
}

#[test]
fn tool_family_labels_localized_no_english_leak() {
let checks: &[(MessageId, &str, &str)] = &[
(MessageId::ToolFamilyRead, "read", "đọc,读,読,读取,ler,leer"),
(
MessageId::ToolFamilyPatch,
"patch",
"vá,補,パ,修补,corrigir,parchear",
),
(
MessageId::ToolFamilyRun,
"run",
"chạy,執,実,运行,executar,ejecutar",
),
(
MessageId::ToolFamilyFind,
"find",
"tìm,搜,検,搜索,buscar,buscar",
),
(
MessageId::ToolFamilyVerify,
"verify",
"xác minh,驗,検,验,verificar,verificar",
),
];
for locale in [
Locale::Ja,
Locale::ZhHans,
Locale::ZhHant,
Locale::PtBr,
Locale::Es419,
Locale::Vi,
] {
for (id, eng, _) in checks {
let msg = tr(locale, *id);
assert!(
!msg.eq_ignore_ascii_case(eng),
"{} leaked exact English '{}' for '{:?}': {msg}",
locale.tag(),
eng,
id
);
}
}
Comment on lines +376 to +417

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Expected-translation data is never asserted

The third element of each checks tuple (e.g., "đọc,读,読,读取,ler,leer") is bound to _ and discarded. The test only proves each translation differs from the English baseline (eq_ignore_ascii_case), so a completely incorrect translation — for example, Vietnamese ToolFamilyRead accidentally set to "sai" instead of "đọc" — would pass silently. The third string reads as if it were the expected value to verify, making the intent ambiguous. Consider either dropping the column entirely (it's documentation at best) or asserting against it per locale so the test actually validates correctness.

Fix in Codex Fix in Claude Code Fix in Cursor

}

#[test]
fn tool_family_activity_label_localized_no_english_leak() {
let known = [
"exec_shell",
"read_file",
"apply_patch",
"grep_files",
"run_verifiers",
];
let english_labels = ["run", "read", "patch", "find", "verify"];
for locale in [
Locale::Ja,
Locale::ZhHans,
Locale::ZhHant,
Locale::PtBr,
Locale::Es419,
Locale::Vi,
] {
for (tool, eng) in known.iter().zip(english_labels.iter()) {
let label = tool_activity_label_for_name(tool, locale);
assert!(
!label.eq_ignore_ascii_case(eng),
"{} leaked English '{}' for tool '{tool}': {label}",
locale.tag(),
eng,
);
}
}
}
}
Loading