Merge PR #2892 from gordonlu: localize sandbox elevation dialog across 7 locales
feat(i18n): localize sandbox elevation dialog across 7 locales
This commit is contained in:
@@ -80,6 +80,7 @@ npm/*/bin/downloads/
|
||||
apps/
|
||||
|
||||
# Claude Code runtime artifacts
|
||||
.claude/settings.json
|
||||
.claude/scheduled_tasks.lock
|
||||
.claude/worktrees/
|
||||
.worktrees/
|
||||
|
||||
@@ -323,13 +323,8 @@ impl DeepSeekClient {
|
||||
.and_then(|s| s.as_str())
|
||||
.unwrap_or("completed");
|
||||
let stop_reason = match status {
|
||||
"completed" => {
|
||||
if saw_tool_call {
|
||||
"tool_use"
|
||||
} else {
|
||||
"end_turn"
|
||||
}
|
||||
}
|
||||
"completed" if saw_tool_call => "tool_use",
|
||||
"completed" => "end_turn",
|
||||
"incomplete" => "max_tokens",
|
||||
_ => "end_turn",
|
||||
};
|
||||
|
||||
@@ -537,6 +537,25 @@ pub enum MessageId {
|
||||
ApprovalChooseAction,
|
||||
ApprovalIntentLabel,
|
||||
ApprovalMoreLines,
|
||||
// Sandbox elevation dialog.
|
||||
ElevationTitleSandboxDenied,
|
||||
ElevationTitleRequired,
|
||||
ElevationFieldTool,
|
||||
ElevationFieldCmd,
|
||||
ElevationFieldReason,
|
||||
ElevationImpactHeader,
|
||||
ElevationImpactNetwork,
|
||||
ElevationImpactWrite,
|
||||
ElevationImpactFullAccess,
|
||||
ElevationPromptProceed,
|
||||
ElevationOptionNetwork,
|
||||
ElevationOptionWrite,
|
||||
ElevationOptionFullAccess,
|
||||
ElevationOptionAbort,
|
||||
ElevationOptionNetworkDesc,
|
||||
ElevationOptionWriteDesc,
|
||||
ElevationOptionFullAccessDesc,
|
||||
ElevationOptionAbortDesc,
|
||||
|
||||
CtxInspTitle,
|
||||
CtxInspSessionContext,
|
||||
@@ -892,6 +911,24 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[
|
||||
MessageId::ApprovalChooseAction,
|
||||
MessageId::ApprovalIntentLabel,
|
||||
MessageId::ApprovalMoreLines,
|
||||
MessageId::ElevationTitleSandboxDenied,
|
||||
MessageId::ElevationTitleRequired,
|
||||
MessageId::ElevationFieldTool,
|
||||
MessageId::ElevationFieldCmd,
|
||||
MessageId::ElevationFieldReason,
|
||||
MessageId::ElevationImpactHeader,
|
||||
MessageId::ElevationImpactNetwork,
|
||||
MessageId::ElevationImpactWrite,
|
||||
MessageId::ElevationImpactFullAccess,
|
||||
MessageId::ElevationPromptProceed,
|
||||
MessageId::ElevationOptionNetwork,
|
||||
MessageId::ElevationOptionWrite,
|
||||
MessageId::ElevationOptionFullAccess,
|
||||
MessageId::ElevationOptionAbort,
|
||||
MessageId::ElevationOptionNetworkDesc,
|
||||
MessageId::ElevationOptionWriteDesc,
|
||||
MessageId::ElevationOptionFullAccessDesc,
|
||||
MessageId::ElevationOptionAbortDesc,
|
||||
MessageId::CtxInspTitle,
|
||||
MessageId::CtxInspSessionContext,
|
||||
MessageId::CtxInspSystemPrompt,
|
||||
@@ -1555,6 +1592,37 @@ fn english(id: MessageId) -> &'static str {
|
||||
MessageId::ApprovalChooseAction => "Enter selected option, or press y/a/d directly",
|
||||
MessageId::ApprovalIntentLabel => "Intent: ",
|
||||
MessageId::ApprovalMoreLines => " … (+{count} lines)",
|
||||
// Sandbox elevation dialog.
|
||||
MessageId::ElevationTitleSandboxDenied => " \u{26a0} Sandbox Denied ",
|
||||
MessageId::ElevationTitleRequired => " Sandbox Elevation Required ",
|
||||
MessageId::ElevationFieldTool => " Tool: ",
|
||||
MessageId::ElevationFieldCmd => " Cmd: ",
|
||||
MessageId::ElevationFieldReason => " Reason: ",
|
||||
MessageId::ElevationImpactHeader => " Impact if approved:",
|
||||
MessageId::ElevationImpactNetwork => {
|
||||
" - network retry enables outbound downloads and HTTP requests"
|
||||
}
|
||||
MessageId::ElevationImpactWrite => {
|
||||
" - write retry expands writable filesystem scope for this tool call"
|
||||
}
|
||||
MessageId::ElevationImpactFullAccess => {
|
||||
" - full access removes sandbox restrictions entirely for this retry"
|
||||
}
|
||||
MessageId::ElevationPromptProceed => " Choose how to proceed:",
|
||||
MessageId::ElevationOptionNetwork => "Allow outbound network",
|
||||
MessageId::ElevationOptionWrite => "Allow extra write access",
|
||||
MessageId::ElevationOptionFullAccess => "Full access (filesystem + network)",
|
||||
MessageId::ElevationOptionAbort => "Abort",
|
||||
MessageId::ElevationOptionNetworkDesc => {
|
||||
"Retry this tool call with outbound network access for downloads and HTTP requests"
|
||||
}
|
||||
MessageId::ElevationOptionWriteDesc => {
|
||||
"Retry this tool call with additional writable filesystem scope"
|
||||
}
|
||||
MessageId::ElevationOptionFullAccessDesc => {
|
||||
"Retry without sandbox limits; grants unrestricted filesystem and network access"
|
||||
}
|
||||
MessageId::ElevationOptionAbortDesc => "Cancel this tool execution",
|
||||
|
||||
MessageId::CtxInspTitle => "Context inspector",
|
||||
MessageId::CtxInspSessionContext => "Session Context",
|
||||
@@ -2090,6 +2158,38 @@ fn vietnamese(id: MessageId) -> Option<&'static str> {
|
||||
MessageId::ApprovalChooseAction => "Enter để chọn, hoặc nhấn y/a/d trực tiếp",
|
||||
MessageId::ApprovalIntentLabel => "Ý định: ",
|
||||
MessageId::ApprovalMoreLines => " … (+{count} dòng)",
|
||||
// Sandbox elevation dialog.
|
||||
// Sandbox elevation dialog.
|
||||
MessageId::ElevationTitleSandboxDenied => " \u{26a0} Sandbox Bị Từ Chối ",
|
||||
MessageId::ElevationTitleRequired => " Yêu Cầu Nâng Cấp Sandbox ",
|
||||
MessageId::ElevationFieldTool => " Công cụ: ",
|
||||
MessageId::ElevationFieldCmd => " Lệnh: ",
|
||||
MessageId::ElevationFieldReason => " Lý do: ",
|
||||
MessageId::ElevationImpactHeader => " Tác động nếu được chấp thuận:",
|
||||
MessageId::ElevationImpactNetwork => {
|
||||
" - thử lại với mạng cho phép tải xuống và yêu cầu HTTP"
|
||||
}
|
||||
MessageId::ElevationImpactWrite => {
|
||||
" - thử lại với quyền ghi mở rộng phạm vi hệ thống tệp"
|
||||
}
|
||||
MessageId::ElevationImpactFullAccess => {
|
||||
" - truy cập đầy đủ loại bỏ hoàn toàn hạn chế sandbox"
|
||||
}
|
||||
MessageId::ElevationPromptProceed => " Chọn cách tiếp tục:",
|
||||
MessageId::ElevationOptionNetwork => "Cho phép mạng ngoài",
|
||||
MessageId::ElevationOptionWrite => "Cho phép quyền ghi bổ sung",
|
||||
MessageId::ElevationOptionFullAccess => "Truy cập đầy đủ (hệ thống tệp + mạng)",
|
||||
MessageId::ElevationOptionAbort => "Hủy bỏ",
|
||||
MessageId::ElevationOptionNetworkDesc => {
|
||||
"Thử lại cuộc gọi công cụ này với quyền truy cập mạng ngoài"
|
||||
}
|
||||
MessageId::ElevationOptionWriteDesc => {
|
||||
"Thử lại cuộc gọi công cụ này với phạm vi hệ thống tệp có thể ghi bổ sung"
|
||||
}
|
||||
MessageId::ElevationOptionFullAccessDesc => {
|
||||
"Thử lại không giới hạn sandbox; cấp quyền truy cập không hạn chế"
|
||||
}
|
||||
MessageId::ElevationOptionAbortDesc => "Hủy thực thi công cụ này",
|
||||
|
||||
MessageId::CtxInspTitle => "Trình kiểm tra ngữ cảnh",
|
||||
MessageId::CtxInspSessionContext => "Ngữ cảnh phiên",
|
||||
@@ -2179,6 +2279,30 @@ fn traditional_chinese(id: MessageId) -> Option<&'static str> {
|
||||
MessageId::ApprovalChooseAction => "Enter 執行選中項,或直接按 y/a/d",
|
||||
MessageId::ApprovalIntentLabel => "意圖:",
|
||||
MessageId::ApprovalMoreLines => " … (還有 {count} 行)",
|
||||
// Sandbox elevation dialog.
|
||||
// Sandbox elevation dialog.
|
||||
MessageId::ElevationTitleSandboxDenied => " \u{26a0} 沙箱拒絕 ",
|
||||
MessageId::ElevationTitleRequired => " 沙箱提權 ",
|
||||
MessageId::ElevationFieldTool => " 工具:",
|
||||
MessageId::ElevationFieldCmd => " 命令:",
|
||||
MessageId::ElevationFieldReason => " 原因:",
|
||||
MessageId::ElevationImpactHeader => " 批准後的影響:",
|
||||
MessageId::ElevationImpactNetwork => " - 網路重試允許外部下載和 HTTP 請求",
|
||||
MessageId::ElevationImpactWrite => " - 寫入重試擴大此工具呼叫的檔案系統寫入範圍",
|
||||
MessageId::ElevationImpactFullAccess => " - 完全訪問解除沙箱限制",
|
||||
MessageId::ElevationPromptProceed => " 請選擇處理方式:",
|
||||
MessageId::ElevationOptionNetwork => "允許外部網路訪問",
|
||||
MessageId::ElevationOptionWrite => "允許額外寫入權限",
|
||||
MessageId::ElevationOptionFullAccess => "完全訪問(檔案系統 + 網路)",
|
||||
MessageId::ElevationOptionAbort => "中止",
|
||||
MessageId::ElevationOptionNetworkDesc => {
|
||||
"使用外部網路訪問重試此工具呼叫(下載和 HTTP 請求)"
|
||||
}
|
||||
MessageId::ElevationOptionWriteDesc => "重試此工具呼叫,擴大可寫入的檔案系統範圍",
|
||||
MessageId::ElevationOptionFullAccessDesc => {
|
||||
"無沙箱限制重試(授予無限制的檔案系統和網路訪問權限)"
|
||||
}
|
||||
MessageId::ElevationOptionAbortDesc => "取消此工具呼叫",
|
||||
|
||||
MessageId::CtxInspTitle => "上下文檢查器",
|
||||
MessageId::CtxInspSessionContext => "會話上下文",
|
||||
@@ -2679,6 +2803,36 @@ fn japanese(id: MessageId) -> Option<&'static str> {
|
||||
MessageId::ApprovalChooseAction => "Enterで選択、または y/a/d を直接入力",
|
||||
MessageId::ApprovalIntentLabel => "意図:",
|
||||
MessageId::ApprovalMoreLines => " … (+{count} 行)",
|
||||
// Sandbox elevation dialog.
|
||||
// Sandbox elevation dialog.
|
||||
MessageId::ElevationTitleSandboxDenied => " \u{26a0} サンドボックス拒否 ",
|
||||
MessageId::ElevationTitleRequired => " サンドボックス昇格 ",
|
||||
MessageId::ElevationFieldTool => " ツール:",
|
||||
MessageId::ElevationFieldCmd => " コマンド:",
|
||||
MessageId::ElevationFieldReason => " 理由:",
|
||||
MessageId::ElevationImpactHeader => " 承認された場合の影響:",
|
||||
MessageId::ElevationImpactNetwork => {
|
||||
" - ネットワーク再試行で外部ダウンロードとHTTPリクエストが可能"
|
||||
}
|
||||
MessageId::ElevationImpactWrite => {
|
||||
" - 書き込み再試行でファイルシステムの書き込み範囲が拡大"
|
||||
}
|
||||
MessageId::ElevationImpactFullAccess => {
|
||||
" - フルアクセスでサンドボックス制限を完全に解除"
|
||||
}
|
||||
MessageId::ElevationPromptProceed => " 方法を選択:",
|
||||
MessageId::ElevationOptionNetwork => "外部ネットワークを許可",
|
||||
MessageId::ElevationOptionWrite => "追加の書き込みアクセスを許可",
|
||||
MessageId::ElevationOptionFullAccess => "フルアクセス(ファイルシステム + ネットワーク)",
|
||||
MessageId::ElevationOptionAbort => "中止",
|
||||
MessageId::ElevationOptionNetworkDesc => {
|
||||
"外部ネットワークアクセスでこのツール呼び出しを再試行(ダウンロードとHTTPリクエスト用)"
|
||||
}
|
||||
MessageId::ElevationOptionWriteDesc => "追加の書き込み可能ファイルシステム範囲で再試行",
|
||||
MessageId::ElevationOptionFullAccessDesc => {
|
||||
"サンドボックス制限なしで再試行(ファイルシステムとネットワークへの無制限アクセス)"
|
||||
}
|
||||
MessageId::ElevationOptionAbortDesc => "このツール実行をキャンセル",
|
||||
|
||||
MessageId::CtxInspTitle => "コンテキストインスペクタ",
|
||||
MessageId::CtxInspSessionContext => "セッションコンテキスト",
|
||||
@@ -3117,6 +3271,30 @@ fn chinese_simplified(id: MessageId) -> Option<&'static str> {
|
||||
MessageId::ApprovalChooseAction => "Enter 执行选中项,或直接按 y/a/d",
|
||||
MessageId::ApprovalIntentLabel => "意图:",
|
||||
MessageId::ApprovalMoreLines => " … (还有 {count} 行)",
|
||||
// Sandbox elevation dialog.
|
||||
// Sandbox elevation dialog.
|
||||
MessageId::ElevationTitleSandboxDenied => " \u{26a0} 沙箱拒绝 ",
|
||||
MessageId::ElevationTitleRequired => " 沙箱提权 ",
|
||||
MessageId::ElevationFieldTool => " 工具:",
|
||||
MessageId::ElevationFieldCmd => " 命令:",
|
||||
MessageId::ElevationFieldReason => " 原因:",
|
||||
MessageId::ElevationImpactHeader => " 批准后的影响:",
|
||||
MessageId::ElevationImpactNetwork => " - 网络重试允许外部下载和 HTTP 请求",
|
||||
MessageId::ElevationImpactWrite => " - 写入重试扩大此工具调用的文件系统写入范围",
|
||||
MessageId::ElevationImpactFullAccess => " - 完全访问解除沙箱限制",
|
||||
MessageId::ElevationPromptProceed => " 请选择处理方式:",
|
||||
MessageId::ElevationOptionNetwork => "允许外部网络访问",
|
||||
MessageId::ElevationOptionWrite => "允许额外写入权限",
|
||||
MessageId::ElevationOptionFullAccess => "完全访问(文件系统 + 网络)",
|
||||
MessageId::ElevationOptionAbort => "中止",
|
||||
MessageId::ElevationOptionNetworkDesc => {
|
||||
"使用外部网络访问重试此工具调用(下载和 HTTP 请求)"
|
||||
}
|
||||
MessageId::ElevationOptionWriteDesc => "重试此工具调用,扩大可写入的文件系统范围",
|
||||
MessageId::ElevationOptionFullAccessDesc => {
|
||||
"无沙箱限制重试(授予无限制的文件系统和网络访问权限)"
|
||||
}
|
||||
MessageId::ElevationOptionAbortDesc => "取消此工具调用",
|
||||
|
||||
MessageId::CtxInspTitle => "上下文检查器",
|
||||
MessageId::CtxInspSessionContext => "会话上下文",
|
||||
@@ -3631,6 +3809,38 @@ fn portuguese_brazil(id: MessageId) -> Option<&'static str> {
|
||||
MessageId::ApprovalChooseAction => "Enter para selecionar, ou pressione y/a/d diretamente",
|
||||
MessageId::ApprovalIntentLabel => "Intenção: ",
|
||||
MessageId::ApprovalMoreLines => " … (+{count} linhas)",
|
||||
// Sandbox elevation dialog.
|
||||
// Sandbox elevation dialog.
|
||||
MessageId::ElevationTitleSandboxDenied => " \u{26a0} Sandbox Negado ",
|
||||
MessageId::ElevationTitleRequired => " Elevação de Sandbox Necessária ",
|
||||
MessageId::ElevationFieldTool => " Ferramenta: ",
|
||||
MessageId::ElevationFieldCmd => " Comando: ",
|
||||
MessageId::ElevationFieldReason => " Motivo: ",
|
||||
MessageId::ElevationImpactHeader => " Impacto se aprovado:",
|
||||
MessageId::ElevationImpactNetwork => {
|
||||
" - retry de rede permite downloads externos e requisições HTTP"
|
||||
}
|
||||
MessageId::ElevationImpactWrite => {
|
||||
" - retry de escrita expande o escopo do sistema de arquivos para esta chamada"
|
||||
}
|
||||
MessageId::ElevationImpactFullAccess => {
|
||||
" - acesso total remove todas as restrições de sandbox para este retry"
|
||||
}
|
||||
MessageId::ElevationPromptProceed => " Escolha como prosseguir:",
|
||||
MessageId::ElevationOptionNetwork => "Permitir rede externa",
|
||||
MessageId::ElevationOptionWrite => "Permitir acesso extra de escrita",
|
||||
MessageId::ElevationOptionFullAccess => "Acesso total (sistema de arquivos + rede)",
|
||||
MessageId::ElevationOptionAbort => "Abortar",
|
||||
MessageId::ElevationOptionNetworkDesc => {
|
||||
"Retry esta chamada com acesso de rede externa para downloads e requisições HTTP"
|
||||
}
|
||||
MessageId::ElevationOptionWriteDesc => {
|
||||
"Retry esta chamada com escopo adicional de sistema de arquivos gravável"
|
||||
}
|
||||
MessageId::ElevationOptionFullAccessDesc => {
|
||||
"Retry sem limites de sandbox; concede acesso irrestrito ao sistema de arquivos e rede"
|
||||
}
|
||||
MessageId::ElevationOptionAbortDesc => "Cancelar esta execução de ferramenta",
|
||||
|
||||
MessageId::CtxInspTitle => "Inspetor de contexto",
|
||||
MessageId::CtxInspSessionContext => "Contexto da sessão",
|
||||
@@ -4159,6 +4369,38 @@ fn spanish_latin_america(id: MessageId) -> Option<&'static str> {
|
||||
MessageId::ApprovalChooseAction => "Enter para seleccionar, o presione y/a/d directamente",
|
||||
MessageId::ApprovalIntentLabel => "Intención: ",
|
||||
MessageId::ApprovalMoreLines => " … (+{count} líneas)",
|
||||
// Sandbox elevation dialog.
|
||||
// Sandbox elevation dialog.
|
||||
MessageId::ElevationTitleSandboxDenied => " \u{26a0} Sandbox Denegado ",
|
||||
MessageId::ElevationTitleRequired => " Elevación de Sandbox Requerida ",
|
||||
MessageId::ElevationFieldTool => " Herramienta: ",
|
||||
MessageId::ElevationFieldCmd => " Comando: ",
|
||||
MessageId::ElevationFieldReason => " Motivo: ",
|
||||
MessageId::ElevationImpactHeader => " Impacto si se aprueba:",
|
||||
MessageId::ElevationImpactNetwork => {
|
||||
" - reintento de red permite descargas y solicitudes HTTP externas"
|
||||
}
|
||||
MessageId::ElevationImpactWrite => {
|
||||
" - reintento de escritura expande el ámbito del sistema de archivos para esta llamada"
|
||||
}
|
||||
MessageId::ElevationImpactFullAccess => {
|
||||
" - acceso total elimina todas las restricciones de sandbox para este reintento"
|
||||
}
|
||||
MessageId::ElevationPromptProceed => " Elige cómo proceder:",
|
||||
MessageId::ElevationOptionNetwork => "Permitir red externa",
|
||||
MessageId::ElevationOptionWrite => "Permitir acceso extra de escritura",
|
||||
MessageId::ElevationOptionFullAccess => "Acceso total (sistema de archivos + red)",
|
||||
MessageId::ElevationOptionAbort => "Abortar",
|
||||
MessageId::ElevationOptionNetworkDesc => {
|
||||
"Reintenta esta llamada con acceso de red externa para descargas y solicitudes HTTP"
|
||||
}
|
||||
MessageId::ElevationOptionWriteDesc => {
|
||||
"Reintenta esta llamada con ámbito adicional de sistema de archivos grabable"
|
||||
}
|
||||
MessageId::ElevationOptionFullAccessDesc => {
|
||||
"Reintenta sin límites de sandbox; concede acceso sin restricciones al sistema de archivos y red"
|
||||
}
|
||||
MessageId::ElevationOptionAbortDesc => "Cancelar esta ejecución de herramienta",
|
||||
|
||||
MessageId::CtxInspTitle => "Inspector de contexto",
|
||||
MessageId::CtxInspSessionContext => "Contexto de la sesión",
|
||||
|
||||
+144
-10
@@ -1026,6 +1026,7 @@ pub enum ElevationOption {
|
||||
|
||||
impl ElevationOption {
|
||||
/// Get the display label for this option.
|
||||
#[cfg(test)]
|
||||
pub fn label(&self) -> &'static str {
|
||||
match self {
|
||||
ElevationOption::WithNetwork => "Allow outbound network",
|
||||
@@ -1036,6 +1037,7 @@ impl ElevationOption {
|
||||
}
|
||||
|
||||
/// Get a short description.
|
||||
#[cfg(test)]
|
||||
pub fn description(&self) -> &'static str {
|
||||
match self {
|
||||
ElevationOption::WithNetwork => {
|
||||
@@ -1132,13 +1134,15 @@ impl ElevationRequest {
|
||||
pub struct ElevationView {
|
||||
request: ElevationRequest,
|
||||
selected: usize,
|
||||
locale: Locale,
|
||||
}
|
||||
|
||||
impl ElevationView {
|
||||
pub fn new(request: ElevationRequest) -> Self {
|
||||
pub fn new(request: ElevationRequest, locale: Locale) -> Self {
|
||||
Self {
|
||||
request,
|
||||
selected: 0,
|
||||
locale,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1213,7 +1217,7 @@ impl ModalView for ElevationView {
|
||||
}
|
||||
|
||||
fn render(&self, area: ratatui::layout::Rect, buf: &mut ratatui::buffer::Buffer) {
|
||||
let elevation_widget = ElevationWidget::new(&self.request, self.selected);
|
||||
let elevation_widget = ElevationWidget::new(&self.request, self.selected, self.locale);
|
||||
elevation_widget.render(area, buf);
|
||||
}
|
||||
}
|
||||
@@ -1991,7 +1995,7 @@ mod tests {
|
||||
fn test_elevation_view_initial_state() {
|
||||
let request =
|
||||
ElevationRequest::for_shell("test-id", "cargo build", "network blocked", true, false);
|
||||
let view = ElevationView::new(request);
|
||||
let view = ElevationView::new(request, Locale::En);
|
||||
assert_eq!(view.selected, 0);
|
||||
}
|
||||
|
||||
@@ -1999,7 +2003,7 @@ mod tests {
|
||||
fn test_elevation_view_keybindings() {
|
||||
let request =
|
||||
ElevationRequest::for_shell("test-id", "cargo test", "write blocked", false, true);
|
||||
let mut view = ElevationView::new(request);
|
||||
let mut view = ElevationView::new(request, Locale::En);
|
||||
|
||||
let action = view.handle_key(create_key_event(KeyCode::Char('n')));
|
||||
assert!(matches!(
|
||||
@@ -2012,7 +2016,7 @@ mod tests {
|
||||
|
||||
let request =
|
||||
ElevationRequest::for_shell("test-id", "cargo build", "write blocked", false, true);
|
||||
let mut view = ElevationView::new(request);
|
||||
let mut view = ElevationView::new(request, Locale::En);
|
||||
let action = view.handle_key(create_key_event(KeyCode::Char('w')));
|
||||
assert!(matches!(
|
||||
action,
|
||||
@@ -2024,7 +2028,7 @@ mod tests {
|
||||
|
||||
let request =
|
||||
ElevationRequest::for_shell("test-id", "cargo build", "blocked", false, false);
|
||||
let mut view = ElevationView::new(request);
|
||||
let mut view = ElevationView::new(request, Locale::En);
|
||||
let action = view.handle_key(create_key_event(KeyCode::Char('f')));
|
||||
assert!(matches!(
|
||||
action,
|
||||
@@ -2036,7 +2040,7 @@ mod tests {
|
||||
|
||||
let request =
|
||||
ElevationRequest::for_shell("test-id", "cargo build", "blocked", false, false);
|
||||
let mut view = ElevationView::new(request);
|
||||
let mut view = ElevationView::new(request, Locale::En);
|
||||
let action = view.handle_key(create_key_event(KeyCode::Esc));
|
||||
assert!(matches!(
|
||||
action,
|
||||
@@ -2048,7 +2052,7 @@ mod tests {
|
||||
|
||||
let request =
|
||||
ElevationRequest::for_shell("test-id", "cargo build", "blocked", false, false);
|
||||
let mut view = ElevationView::new(request);
|
||||
let mut view = ElevationView::new(request, Locale::En);
|
||||
let action = view.handle_key(create_key_event(KeyCode::Char('a')));
|
||||
assert!(matches!(
|
||||
action,
|
||||
@@ -2062,7 +2066,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_elevation_view_navigation() {
|
||||
let request = ElevationRequest::for_shell("test-id", "cargo build", "blocked", true, false);
|
||||
let mut view = ElevationView::new(request);
|
||||
let mut view = ElevationView::new(request, Locale::En);
|
||||
|
||||
assert_eq!(view.selected, 0);
|
||||
|
||||
@@ -2082,7 +2086,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_elevation_view_enter_uses_selected_option() {
|
||||
let request = ElevationRequest::for_shell("test-id", "cargo build", "blocked", true, false);
|
||||
let mut view = ElevationView::new(request);
|
||||
let mut view = ElevationView::new(request, Locale::En);
|
||||
|
||||
view.handle_key(create_key_event(KeyCode::Down));
|
||||
assert_eq!(view.selected, 1);
|
||||
@@ -2097,6 +2101,136 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
fn render_elevation_lines(view: &ElevationView, w: u16, h: u16) -> Vec<String> {
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, w, h));
|
||||
view.render(Rect::new(0, 0, w, h), &mut buf);
|
||||
(0..h)
|
||||
.map(|row| {
|
||||
(0..w)
|
||||
.map(|col| buf[(col, row)].symbol().to_string())
|
||||
.collect::<String>()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn compact_elevation_text(lines: &[String]) -> String {
|
||||
lines.join("\n").replace(' ', "")
|
||||
}
|
||||
|
||||
fn elevation_shell_request() -> ElevationRequest {
|
||||
ElevationRequest::for_shell("test-id", "cargo build", "network blocked", true, false)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_elevation_render_en_has_expected_strings() {
|
||||
let view = ElevationView::new(elevation_shell_request(), Locale::En);
|
||||
let lines = render_elevation_lines(&view, 70, 22);
|
||||
let joined = compact_elevation_text(&lines);
|
||||
assert!(
|
||||
joined.contains("SandboxDenied"),
|
||||
"missing en title:\n{joined}"
|
||||
);
|
||||
assert!(joined.contains("Tool:"), "missing en tool label:\n{joined}");
|
||||
assert!(joined.contains("Cmd:"), "missing en cmd label:\n{joined}");
|
||||
assert!(
|
||||
joined.contains("Reason:"),
|
||||
"missing en reason label:\n{joined}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_elevation_render_zh_hans_localizes_copy() {
|
||||
let view = ElevationView::new(elevation_shell_request(), Locale::ZhHans);
|
||||
let lines = render_elevation_lines(&view, 70, 22);
|
||||
let joined = compact_elevation_text(&lines);
|
||||
assert!(joined.contains("沙箱拒绝"), "missing zh title:\n{joined}");
|
||||
assert!(
|
||||
joined.contains("工具:"),
|
||||
"missing zh tool label:\n{joined}"
|
||||
);
|
||||
assert!(joined.contains("命令:"), "missing zh cmd label:\n{joined}");
|
||||
assert!(
|
||||
joined.contains("原因:"),
|
||||
"missing zh reason label:\n{joined}"
|
||||
);
|
||||
assert!(
|
||||
joined.contains("批准后的影响"),
|
||||
"missing zh impact header:\n{joined}"
|
||||
);
|
||||
let en_artifacts = [
|
||||
"SandboxDenied",
|
||||
"Tool:",
|
||||
"Cmd:",
|
||||
"Reason:",
|
||||
"Impactifapproved",
|
||||
"Choosehowtoproceed",
|
||||
"Allowoutboundnetwork",
|
||||
"Allowextrawriteaccess",
|
||||
"Fullaccess",
|
||||
"Abort",
|
||||
];
|
||||
for artifact in &en_artifacts {
|
||||
assert!(
|
||||
!joined.contains(artifact),
|
||||
"English leak '{artifact}' in zh rendering:\n{joined}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_elevation_render_ja_has_translated_copy() {
|
||||
let view = ElevationView::new(elevation_shell_request(), Locale::Ja);
|
||||
let lines = render_elevation_lines(&view, 70, 22);
|
||||
let joined = compact_elevation_text(&lines);
|
||||
assert!(
|
||||
joined.contains("サンドボックス拒否"),
|
||||
"missing ja title:\n{joined}"
|
||||
);
|
||||
assert!(
|
||||
joined.contains("ツール:"),
|
||||
"missing ja tool label:\n{joined}"
|
||||
);
|
||||
assert!(
|
||||
joined.contains("コマンド:"),
|
||||
"missing ja cmd label:\n{joined}"
|
||||
);
|
||||
assert!(
|
||||
joined.contains("理由:"),
|
||||
"missing ja reason label:\n{joined}"
|
||||
);
|
||||
for eng in &["SandboxDenied", "Tool:", "Cmd:", "Reason:"] as &[&str] {
|
||||
assert!(
|
||||
!joined.contains(eng),
|
||||
"English leak '{eng}' in ja:\n{joined}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_elevation_render_zh_hant_has_translated_copy() {
|
||||
let view = ElevationView::new(elevation_shell_request(), Locale::ZhHant);
|
||||
let lines = render_elevation_lines(&view, 70, 22);
|
||||
let joined = compact_elevation_text(&lines);
|
||||
assert!(
|
||||
joined.contains("沙箱拒絕"),
|
||||
"missing zh-Hant title:\n{joined}"
|
||||
);
|
||||
assert!(
|
||||
joined.contains("工具:"),
|
||||
"missing zh-Hant tool label:\n{joined}"
|
||||
);
|
||||
assert!(
|
||||
joined.contains("命令:"),
|
||||
"missing zh-Hant cmd label:\n{joined}"
|
||||
);
|
||||
assert!(
|
||||
joined.contains("原因:"),
|
||||
"missing zh-Hant reason label:\n{joined}"
|
||||
);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// ElevationOption Tests
|
||||
// ========================================================================
|
||||
|
||||
@@ -2554,7 +2554,8 @@ async fn run_event_loop(
|
||||
blocked_network,
|
||||
blocked_write,
|
||||
);
|
||||
app.view_stack.push(ElevationView::new(request));
|
||||
app.view_stack
|
||||
.push(ElevationView::new(request, app.ui_locale));
|
||||
if let Some((method, _, _)) =
|
||||
crate::tui::notifications::settings(config)
|
||||
{
|
||||
|
||||
@@ -1615,16 +1615,24 @@ fn option_abort(locale: Locale) -> &'static str {
|
||||
pub struct ElevationWidget<'a> {
|
||||
request: &'a ElevationRequest,
|
||||
selected: usize,
|
||||
locale: Locale,
|
||||
}
|
||||
|
||||
impl<'a> ElevationWidget<'a> {
|
||||
pub fn new(request: &'a ElevationRequest, selected: usize) -> Self {
|
||||
Self { request, selected }
|
||||
pub fn new(request: &'a ElevationRequest, selected: usize, locale: Locale) -> Self {
|
||||
Self {
|
||||
request,
|
||||
selected,
|
||||
locale,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Renderable for ElevationWidget<'_> {
|
||||
fn render(&self, area: Rect, buf: &mut Buffer) {
|
||||
use crate::localization::MessageId;
|
||||
use crate::localization::tr;
|
||||
|
||||
let popup_width = 70.min(area.width.saturating_sub(4));
|
||||
let popup_height = 22.min(area.height.saturating_sub(4));
|
||||
let popup_area = Rect {
|
||||
@@ -1639,14 +1647,14 @@ impl Renderable for ElevationWidget<'_> {
|
||||
let mut lines = vec![
|
||||
Line::from(""),
|
||||
Line::from(vec![Span::styled(
|
||||
" ⚠ Sandbox Denied ",
|
||||
tr(self.locale, MessageId::ElevationTitleSandboxDenied),
|
||||
Style::default()
|
||||
.fg(palette::STATUS_ERROR)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)]),
|
||||
Line::from(""),
|
||||
Line::from(vec![
|
||||
Span::raw(" Tool: "),
|
||||
Span::raw(tr(self.locale, MessageId::ElevationFieldTool)),
|
||||
Span::styled(
|
||||
&self.request.tool_name,
|
||||
Style::default()
|
||||
@@ -1656,18 +1664,17 @@ impl Renderable for ElevationWidget<'_> {
|
||||
]),
|
||||
];
|
||||
|
||||
// Show command if it's a shell command
|
||||
if let Some(ref command) = self.request.command {
|
||||
let cmd_display = crate::utils::truncate_with_ellipsis(command, 45, "...");
|
||||
lines.push(Line::from(vec![
|
||||
Span::raw(" Cmd: "),
|
||||
Span::raw(tr(self.locale, MessageId::ElevationFieldCmd)),
|
||||
Span::styled(cmd_display, Style::default().fg(palette::TEXT_MUTED)),
|
||||
]));
|
||||
}
|
||||
|
||||
lines.push(Line::from(""));
|
||||
lines.push(Line::from(vec![
|
||||
Span::raw(" Reason: "),
|
||||
Span::raw(tr(self.locale, MessageId::ElevationFieldReason)),
|
||||
Span::styled(
|
||||
&self.request.denial_reason,
|
||||
Style::default().fg(palette::STATUS_WARNING),
|
||||
@@ -1676,7 +1683,7 @@ impl Renderable for ElevationWidget<'_> {
|
||||
|
||||
lines.push(Line::from(""));
|
||||
lines.push(Line::from(Span::styled(
|
||||
" Impact if approved:",
|
||||
tr(self.locale, MessageId::ElevationImpactHeader),
|
||||
Style::default().fg(palette::TEXT_MUTED),
|
||||
)));
|
||||
if self
|
||||
@@ -1686,7 +1693,7 @@ impl Renderable for ElevationWidget<'_> {
|
||||
.any(|option| matches!(option, ElevationOption::WithNetwork))
|
||||
{
|
||||
lines.push(Line::from(Span::styled(
|
||||
" - network retry enables outbound downloads and HTTP requests",
|
||||
tr(self.locale, MessageId::ElevationImpactNetwork),
|
||||
Style::default().fg(palette::TEXT_PRIMARY),
|
||||
)));
|
||||
}
|
||||
@@ -1697,22 +1704,21 @@ impl Renderable for ElevationWidget<'_> {
|
||||
.any(|option| matches!(option, ElevationOption::WithWriteAccess(_)))
|
||||
{
|
||||
lines.push(Line::from(Span::styled(
|
||||
" - write retry expands writable filesystem scope for this tool call",
|
||||
tr(self.locale, MessageId::ElevationImpactWrite),
|
||||
Style::default().fg(palette::TEXT_PRIMARY),
|
||||
)));
|
||||
}
|
||||
lines.push(Line::from(Span::styled(
|
||||
" - full access removes sandbox restrictions entirely for this retry",
|
||||
tr(self.locale, MessageId::ElevationImpactFullAccess),
|
||||
Style::default().fg(palette::TEXT_PRIMARY),
|
||||
)));
|
||||
lines.push(Line::from(""));
|
||||
lines.push(Line::from(Span::styled(
|
||||
" Choose how to proceed:",
|
||||
tr(self.locale, MessageId::ElevationPromptProceed),
|
||||
Style::default().fg(palette::TEXT_MUTED),
|
||||
)));
|
||||
lines.push(Line::from(""));
|
||||
|
||||
// Render options
|
||||
for (i, option) in self.request.options.iter().enumerate() {
|
||||
let is_selected = i == self.selected;
|
||||
let style = if is_selected {
|
||||
@@ -1723,11 +1729,27 @@ impl Renderable for ElevationWidget<'_> {
|
||||
Style::default()
|
||||
};
|
||||
|
||||
let key = match option {
|
||||
ElevationOption::WithNetwork => "n",
|
||||
ElevationOption::WithWriteAccess(_) => "w",
|
||||
ElevationOption::FullAccess => "f",
|
||||
ElevationOption::Abort => "a",
|
||||
let (key, label_id, desc_id) = match option {
|
||||
ElevationOption::WithNetwork => (
|
||||
"n",
|
||||
MessageId::ElevationOptionNetwork,
|
||||
MessageId::ElevationOptionNetworkDesc,
|
||||
),
|
||||
ElevationOption::WithWriteAccess(_) => (
|
||||
"w",
|
||||
MessageId::ElevationOptionWrite,
|
||||
MessageId::ElevationOptionWriteDesc,
|
||||
),
|
||||
ElevationOption::FullAccess => (
|
||||
"f",
|
||||
MessageId::ElevationOptionFullAccess,
|
||||
MessageId::ElevationOptionFullAccessDesc,
|
||||
),
|
||||
ElevationOption::Abort => (
|
||||
"a",
|
||||
MessageId::ElevationOptionAbort,
|
||||
MessageId::ElevationOptionAbortDesc,
|
||||
),
|
||||
};
|
||||
|
||||
let label_color = match option {
|
||||
@@ -1742,18 +1764,18 @@ impl Renderable for ElevationWidget<'_> {
|
||||
format!("[{key}] "),
|
||||
Style::default().fg(palette::STATUS_SUCCESS),
|
||||
),
|
||||
Span::styled(option.label(), style.fg(label_color)),
|
||||
Span::styled(tr(self.locale, label_id), style.fg(label_color)),
|
||||
]));
|
||||
lines.push(Line::from(vec![
|
||||
Span::raw(" "),
|
||||
Span::styled(
|
||||
option.description(),
|
||||
tr(self.locale, desc_id),
|
||||
Style::default().fg(palette::TEXT_MUTED),
|
||||
),
|
||||
]));
|
||||
}
|
||||
|
||||
let title = " Sandbox Elevation Required ";
|
||||
let title = tr(self.locale, MessageId::ElevationTitleRequired);
|
||||
let block = Block::default()
|
||||
.title(title)
|
||||
.borders(Borders::ALL)
|
||||
|
||||
Reference in New Issue
Block a user