fix(tui): localize activity and file receipts [skip ci]

Use the shared locale catalog for grouped activity, tool status and file mutation receipts. Rebuild cached activity summaries when the UI locale changes and retain raw paths and diffs.

Refs #7071 (CW-7527). On frozen main 9fc5c898c7 plus 62 inventoried dirty paths: TUI lib and feature registry compiled; 197 focused nextest cases passed, including locale cache and receipt cases. Localization lib: 54 passed on unchanged localization inputs in the preceding frozen run. All 15 packs have 2732 keys; diff check and changelog sync passed. Earlier broader mixed-tree run had 2118 passed and one unrelated compaction failure. No native-speaker, real-provider, Windows, installed-artifact or release CI qualification is claimed.
This commit is contained in:
CodeWhale Bot committed 2026-10-29 15:31:20 -07:00
1 parent 9fc5c898c7
commit 896e781fbb
37 files changed
+1611 -307

No files matched your search

+1
View File
@@ -122,6 +122,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- **Localized activity and file receipts:** grouped tool activity, file counts, patch outcomes and tool status use the shared 15-language catalog. Language changes refresh cached summaries; filenames, diffs and execution receipts keep their original data.
- **Automatic compaction stays off when disabled:** preflight and provider context-overflow recovery preserve the conversation and report the setting instead of forcing a summary. Manual compaction remains available, and a rejected recovery request retains its actual cause in both paths. (#7142)
- **VS Code packaging security:** use patched MSAL Node in the extension packager, addressing [GHSA-xcx6-jx8p-x3h5](https://github.com/AzureAD/microsoft-authentication-library-for-js/security/advisories/GHSA-xcx6-jx8p-x3h5).
+58 -1
View File
@@ -2673,5 +2673,62 @@
"InstructionDetailRulesCap": "supera el límit de {count} fitxers de regles per directori",
"InstructionDetailSymlinkedRules": "es rebutja el directori de regles perquè és un enllaç simbòlic",
"InstructionBudgetRefused": "el pressupost d'instruccions ha rebutjat aquesta sol·licitud: {reason}",
"InstructionBudgetOverGuard": "el prefix estàtic de ~{measured} tokens estimats supera el límit {guard}"
"InstructionBudgetOverGuard": "el prefix estàtic de ~{measured} tokens estimats supera el límit {guard}",
"ActivityFileOne": "{count} fitxer",
"ActivityFileMany": "{count} fitxers",
"ActivitySearchOne": "{count} cerca",
"ActivitySearchMany": "{count} cerques",
"ActivityCommandOne": "{count} ordre",
"ActivityCommandMany": "{count} ordres",
"ActivityTaskOne": "{count} tasca",
"ActivityTaskMany": "{count} tasques",
"ActivityDirectoryOne": "{count} directori",
"ActivityDirectoryMany": "{count} directoris",
"ActivityPatternOne": "{count} patró",
"ActivityPatternMany": "{count} patrons",
"ActivityExplored": "Ha explorat {items}",
"ActivityRan": "ha executat {items}",
"ActivityEdited": "ha editat {items}",
"ActivityDelegated": "ha delegat {items}",
"ActivityUpdatedMetadata": "ha actualitzat les metadades",
"ActivityRead": "Ha llegit {items}",
"ActivityListed": "Ha llistat {items}",
"ActivitySearched": "Ha cercat {items}",
"ActivityWrote": "Ha escrit {items}",
"ToolReceiptRunning": "en execució",
"ToolReceiptHydrated": "carregat, no executat",
"ToolReceiptStopped": "aturat",
"ToolReceiptIssue": "incidència",
"ToolReceiptFailed": "ha fallat",
"AgentActivityChecking": "comprovant",
"AgentActivityChecked": "comprovat",
"AgentActivityWaiting": "esperant",
"AgentActivityWaited": "espera acabada",
"FileMutationCreated": "Creat: {path}",
"FileMutationUpdated": "Actualitzat: {path}",
"FileMutationDeleted": "Suprimit: {path}",
"FileMutationRenamed": "Reanomenat: {path}",
"FileMutationCreatedCount": "creats: {count}",
"FileMutationUpdatedCount": "actualitzats: {count}",
"FileMutationDeletedCount": "suprimits: {count}",
"FileMutationRenamedCount": "reanomenats: {count}",
"FileMutationChangedFiles": "Fitxers modificats",
"FileMutationMissingFile": "fitxer",
"FileMutationNoText": "(sense canvis de text)",
"FileMutationMoreDiff": "+{count} línies de diff · {hint}",
"FileMutationChange": "canvi",
"FileMutationSelectReceipt": "Selecciona el canvi corresponent; {hint}.",
"FileMutationExactEvidence": "evidència exacta al registre corresponent",
"FileMutationExactHeading": "Canvi exacte del fitxer",
"FileActivityReadLabel": "Lectura",
"FileActivityListLabel": "Llista",
"FileActivitySearchLabel": "Cerca",
"FileActivityWriteLabel": "Escriptura",
"FileActivityDetailTitle": "Feina · activitat dels fitxers",
"ToolReceiptQuiet": "en execució · sense sortida des de fa {duration}",
"FileActivityEditedLabel": "Editats",
"FileMutationAcrossFiles": " en {count} fitxers",
"FileActivityNoDetail": "No s'han conservat detalls de la destinació que es puguin mostrar amb seguretat",
"FileActivityPatterns": "patrons",
"FileActivitySettled": "finalitzat"
}
+58 -1
View File
@@ -2673,5 +2673,62 @@
"InstructionDetailRulesCap": "über der Grenze von {count} Regeldateien pro Verzeichnis",
"InstructionDetailSymlinkedRules": "Regelverzeichnis als Symlink abgelehnt",
"InstructionBudgetRefused": "Das Anweisungsbudget hat diese Anfrage abgelehnt: {reason}",
"InstructionBudgetOverGuard": "statisches Präfix von ~{measured} geschätzten Tokens überschreitet die Grenze {guard}"
"InstructionBudgetOverGuard": "statisches Präfix von ~{measured} geschätzten Tokens überschreitet die Grenze {guard}",
"ActivityFileOne": "{count} Datei",
"ActivityFileMany": "{count} Dateien",
"ActivitySearchOne": "{count} Suchvorgang",
"ActivitySearchMany": "{count} Suchvorgänge",
"ActivityCommandOne": "{count} Befehl",
"ActivityCommandMany": "{count} Befehle",
"ActivityTaskOne": "{count} Aufgabe",
"ActivityTaskMany": "{count} Aufgaben",
"ActivityDirectoryOne": "{count} Verzeichnis",
"ActivityDirectoryMany": "{count} Verzeichnisse",
"ActivityPatternOne": "{count} Muster",
"ActivityPatternMany": "{count} Muster",
"ActivityExplored": "Untersucht: {items}",
"ActivityRan": "ausgeführt: {items}",
"ActivityEdited": "bearbeitet: {items}",
"ActivityDelegated": "delegiert: {items}",
"ActivityUpdatedMetadata": "Metadaten aktualisiert",
"ActivityRead": "Gelesen: {items}",
"ActivityListed": "Aufgelistet: {items}",
"ActivitySearched": "Gesucht: {items}",
"ActivityWrote": "Geschrieben: {items}",
"ToolReceiptRunning": "läuft",
"ToolReceiptHydrated": "geladen, nicht ausgeführt",
"ToolReceiptStopped": "gestoppt",
"ToolReceiptIssue": "Problem",
"ToolReceiptFailed": "fehlgeschlagen",
"AgentActivityChecking": "wird geprüft",
"AgentActivityChecked": "geprüft",
"AgentActivityWaiting": "wartet",
"AgentActivityWaited": "Warten beendet",
"FileMutationCreated": "Erstellt: {path}",
"FileMutationUpdated": "Aktualisiert: {path}",
"FileMutationDeleted": "Gelöscht: {path}",
"FileMutationRenamed": "Umbenannt: {path}",
"FileMutationCreatedCount": "{count} erstellt",
"FileMutationUpdatedCount": "{count} aktualisiert",
"FileMutationDeletedCount": "{count} gelöscht",
"FileMutationRenamedCount": "{count} umbenannt",
"FileMutationChangedFiles": "Geänderte Dateien",
"FileMutationMissingFile": "Datei",
"FileMutationNoText": "(keine Textänderungen)",
"FileMutationMoreDiff": "+{count} Diff-Zeilen · {hint}",
"FileMutationChange": "Änderung",
"FileMutationSelectReceipt": "Wähle die zugehörige Änderung; {hint}.",
"FileMutationExactEvidence": "genauer Änderungsnachweis im zugehörigen Beleg",
"FileMutationExactHeading": "Genaue Dateiänderung",
"FileActivityReadLabel": "Lesen",
"FileActivityListLabel": "Auflisten",
"FileActivitySearchLabel": "Suchen",
"FileActivityWriteLabel": "Schreiben",
"FileActivityDetailTitle": "Arbeit · Dateiaktivität",
"ToolReceiptQuiet": "läuft · keine Ausgabe seit {duration}",
"FileActivityEditedLabel": "Bearbeitet",
"FileMutationAcrossFiles": " in {count} Dateien",
"FileActivityNoDetail": "Keine sicher anzeigbaren Zieldetails gespeichert",
"FileActivityPatterns": "Muster",
"FileActivitySettled": "abgeschlossen"
}
+58 -1
View File
@@ -2673,5 +2673,62 @@
"InstructionDetailRulesCap": "beyond the per-directory rules cap of {count} files",
"InstructionDetailSymlinkedRules": "refusing symlinked rules directory",
"InstructionBudgetRefused": "instruction budget refused this request: {reason}",
"InstructionBudgetOverGuard": "static prefix ~{measured} estimated tokens exceeds guard {guard}"
"InstructionBudgetOverGuard": "static prefix ~{measured} estimated tokens exceeds guard {guard}",
"ActivityFileOne": "{count} file",
"ActivityFileMany": "{count} files",
"ActivitySearchOne": "{count} search",
"ActivitySearchMany": "{count} searches",
"ActivityCommandOne": "{count} command",
"ActivityCommandMany": "{count} commands",
"ActivityTaskOne": "{count} task",
"ActivityTaskMany": "{count} tasks",
"ActivityDirectoryOne": "{count} directory",
"ActivityDirectoryMany": "{count} directories",
"ActivityPatternOne": "{count} pattern",
"ActivityPatternMany": "{count} patterns",
"ActivityExplored": "Explored {items}",
"ActivityRan": "ran {items}",
"ActivityEdited": "edited {items}",
"ActivityDelegated": "delegated {items}",
"ActivityUpdatedMetadata": "updated metadata",
"ActivityRead": "Read {items}",
"ActivityListed": "Listed {items}",
"ActivitySearched": "Searched {items}",
"ActivityWrote": "Wrote {items}",
"ToolReceiptRunning": "running",
"ToolReceiptHydrated": "loaded, not run",
"ToolReceiptStopped": "stopped",
"ToolReceiptIssue": "issue",
"ToolReceiptFailed": "failed",
"AgentActivityChecking": "checking",
"AgentActivityChecked": "checked",
"AgentActivityWaiting": "waiting",
"AgentActivityWaited": "waited",
"FileMutationCreated": "Created {path}",
"FileMutationUpdated": "Updated {path}",
"FileMutationDeleted": "Deleted {path}",
"FileMutationRenamed": "Renamed {path}",
"FileMutationCreatedCount": "{count} created",
"FileMutationUpdatedCount": "{count} updated",
"FileMutationDeletedCount": "{count} deleted",
"FileMutationRenamedCount": "{count} renamed",
"FileMutationChangedFiles": "Changed files",
"FileMutationMissingFile": "file",
"FileMutationNoText": "(no textual changes)",
"FileMutationMoreDiff": "+{count} diff lines · {hint}",
"FileMutationChange": "change",
"FileMutationSelectReceipt": "Select the matching File receipt; {hint}.",
"FileMutationExactEvidence": "exact change evidence on the matching File receipt",
"FileMutationExactHeading": "Exact File change",
"FileActivityReadLabel": "Read",
"FileActivityListLabel": "List",
"FileActivitySearchLabel": "Search",
"FileActivityWriteLabel": "Write",
"FileActivityDetailTitle": "Work · file activity",
"ToolReceiptQuiet": "running · no output for {duration}",
"FileActivityEditedLabel": "Edited",
"FileMutationAcrossFiles": " across {count} files",
"FileActivityNoDetail": "No safe target detail retained",
"FileActivityPatterns": "patterns",
"FileActivitySettled": "settled"
}
+58 -1
View File
@@ -2673,5 +2673,62 @@
"InstructionDetailRulesCap": "excede el tope de {count} archivos de reglas por directorio",
"InstructionDetailSymlinkedRules": "se rechaza el directorio de reglas por ser un enlace simbólico",
"InstructionBudgetRefused": "el presupuesto de instrucciones rechazó esta solicitud: {reason}",
"InstructionBudgetOverGuard": "el prefijo estático de ~{measured} tokens estimados supera el límite {guard}"
"InstructionBudgetOverGuard": "el prefijo estático de ~{measured} tokens estimados supera el límite {guard}",
"ActivityFileOne": "{count} archivo",
"ActivityFileMany": "{count} archivos",
"ActivitySearchOne": "{count} búsqueda",
"ActivitySearchMany": "{count} búsquedas",
"ActivityCommandOne": "{count} comando",
"ActivityCommandMany": "{count} comandos",
"ActivityTaskOne": "{count} tarea",
"ActivityTaskMany": "{count} tareas",
"ActivityDirectoryOne": "{count} directorio",
"ActivityDirectoryMany": "{count} directorios",
"ActivityPatternOne": "{count} patrón",
"ActivityPatternMany": "{count} patrones",
"ActivityExplored": "Exploró {items}",
"ActivityRan": "ejecutó {items}",
"ActivityEdited": "editó {items}",
"ActivityDelegated": "delegó {items}",
"ActivityUpdatedMetadata": "actualizó los metadatos",
"ActivityRead": "Leyó {items}",
"ActivityListed": "Enumeró {items}",
"ActivitySearched": "Buscó {items}",
"ActivityWrote": "Escribió {items}",
"ToolReceiptRunning": "en ejecución",
"ToolReceiptHydrated": "cargado, sin ejecutar",
"ToolReceiptStopped": "detenido",
"ToolReceiptIssue": "problema",
"ToolReceiptFailed": "falló",
"AgentActivityChecking": "comprobando",
"AgentActivityChecked": "comprobado",
"AgentActivityWaiting": "esperando",
"AgentActivityWaited": "espera terminada",
"FileMutationCreated": "Creado: {path}",
"FileMutationUpdated": "Actualizado: {path}",
"FileMutationDeleted": "Eliminado: {path}",
"FileMutationRenamed": "Renombrado: {path}",
"FileMutationCreatedCount": "creados: {count}",
"FileMutationUpdatedCount": "actualizados: {count}",
"FileMutationDeletedCount": "eliminados: {count}",
"FileMutationRenamedCount": "renombrados: {count}",
"FileMutationChangedFiles": "Archivos modificados",
"FileMutationMissingFile": "archivo",
"FileMutationNoText": "(sin cambios de texto)",
"FileMutationMoreDiff": "+{count} líneas de diff · {hint}",
"FileMutationChange": "cambio",
"FileMutationSelectReceipt": "Selecciona el cambio correspondiente; {hint}.",
"FileMutationExactEvidence": "evidencia exacta en el registro correspondiente",
"FileMutationExactHeading": "Cambio exacto del archivo",
"FileActivityReadLabel": "Lectura",
"FileActivityListLabel": "Listado",
"FileActivitySearchLabel": "Búsqueda",
"FileActivityWriteLabel": "Escritura",
"FileActivityDetailTitle": "Trabajo · actividad de archivos",
"ToolReceiptQuiet": "en ejecución · sin salida desde hace {duration}",
"FileActivityEditedLabel": "Editados",
"FileMutationAcrossFiles": " en {count} archivos",
"FileActivityNoDetail": "No se conservaron detalles del destino que puedan mostrarse de forma segura",
"FileActivityPatterns": "patrones",
"FileActivitySettled": "finalizado"
}
+58 -1
View File
@@ -2673,5 +2673,62 @@
"InstructionDetailRulesCap": "au-delà du plafond de {count} fichiers de règles par répertoire",
"InstructionDetailSymlinkedRules": "refus : le répertoire de règles est un lien symbolique",
"InstructionBudgetRefused": "le budget d'instructions a refusé cette requête : {reason}",
"InstructionBudgetOverGuard": "le préfixe statique de ~{measured} tokens estimés dépasse la limite {guard}"
"InstructionBudgetOverGuard": "le préfixe statique de ~{measured} tokens estimés dépasse la limite {guard}",
"ActivityFileOne": "{count} fichier",
"ActivityFileMany": "{count} fichiers",
"ActivitySearchOne": "{count} recherche",
"ActivitySearchMany": "{count} recherches",
"ActivityCommandOne": "{count} commande",
"ActivityCommandMany": "{count} commandes",
"ActivityTaskOne": "{count} tâche",
"ActivityTaskMany": "{count} tâches",
"ActivityDirectoryOne": "{count} répertoire",
"ActivityDirectoryMany": "{count} répertoires",
"ActivityPatternOne": "{count} motif",
"ActivityPatternMany": "{count} motifs",
"ActivityExplored": "Exploration de {items}",
"ActivityRan": "exécution de {items}",
"ActivityEdited": "modification de {items}",
"ActivityDelegated": "délégation de {items}",
"ActivityUpdatedMetadata": "métadonnées mises à jour",
"ActivityRead": "Lecture de {items}",
"ActivityListed": "Liste de {items}",
"ActivitySearched": "Recherche de {items}",
"ActivityWrote": "Écriture de {items}",
"ToolReceiptRunning": "en cours",
"ToolReceiptHydrated": "chargé, non exécuté",
"ToolReceiptStopped": "arrêté",
"ToolReceiptIssue": "problème",
"ToolReceiptFailed": "échec",
"AgentActivityChecking": "vérification en cours",
"AgentActivityChecked": "vérifié",
"AgentActivityWaiting": "en attente",
"AgentActivityWaited": "attente terminée",
"FileMutationCreated": "Créé : {path}",
"FileMutationUpdated": "Mis à jour : {path}",
"FileMutationDeleted": "Supprimé : {path}",
"FileMutationRenamed": "Renommé : {path}",
"FileMutationCreatedCount": "créés : {count}",
"FileMutationUpdatedCount": "mis à jour : {count}",
"FileMutationDeletedCount": "supprimés : {count}",
"FileMutationRenamedCount": "renommés : {count}",
"FileMutationChangedFiles": "Fichiers modifiés",
"FileMutationMissingFile": "fichier",
"FileMutationNoText": "(aucune modification textuelle)",
"FileMutationMoreDiff": "+{count} lignes de diff · {hint}",
"FileMutationChange": "modification",
"FileMutationSelectReceipt": "Sélectionnez la modification correspondante ; {hint}.",
"FileMutationExactEvidence": "preuve exacte sur le reçu correspondant",
"FileMutationExactHeading": "Modification exacte du fichier",
"FileActivityReadLabel": "Lecture",
"FileActivityListLabel": "Liste",
"FileActivitySearchLabel": "Recherche",
"FileActivityWriteLabel": "Écriture",
"FileActivityDetailTitle": "Travail · activité des fichiers",
"ToolReceiptQuiet": "en cours · aucune sortie depuis {duration}",
"FileActivityEditedLabel": "Modifiés",
"FileMutationAcrossFiles": " dans {count} fichiers",
"FileActivityNoDetail": "Aucun détail de cible conservé pour un affichage sûr",
"FileActivityPatterns": "motifs",
"FileActivitySettled": "terminé"
}
+58 -1
View File
@@ -2673,5 +2673,62 @@
"InstructionDetailRulesCap": "प्रति निर्देशिका {count} नियम फ़ाइलों की सीमा से अधिक",
"InstructionDetailSymlinkedRules": "सिमलिंक वाली नियम निर्देशिका अस्वीकार की गई",
"InstructionBudgetRefused": "निर्देश बजट ने इस अनुरोध को अस्वीकार कर दिया: {reason}",
"InstructionBudgetOverGuard": "स्थिर प्रीफ़िक्स ~{measured} अनुमानित टोकन सीमा {guard} से अधिक है"
"InstructionBudgetOverGuard": "स्थिर प्रीफ़िक्स ~{measured} अनुमानित टोकन सीमा {guard} से अधिक है",
"ActivityFileOne": "{count} फ़ाइल",
"ActivityFileMany": "{count} फ़ाइलें",
"ActivitySearchOne": "{count} खोज",
"ActivitySearchMany": "{count} खोजें",
"ActivityCommandOne": "{count} कमांड",
"ActivityCommandMany": "{count} कमांड",
"ActivityTaskOne": "{count} कार्य",
"ActivityTaskMany": "{count} कार्य",
"ActivityDirectoryOne": "{count} डायरेक्टरी",
"ActivityDirectoryMany": "{count} डायरेक्टरियाँ",
"ActivityPatternOne": "{count} पैटर्न",
"ActivityPatternMany": "{count} पैटर्न",
"ActivityExplored": "जाँच: {items}",
"ActivityRan": "निष्पादन: {items}",
"ActivityEdited": "संपादन: {items}",
"ActivityDelegated": "सौंपे गए: {items}",
"ActivityUpdatedMetadata": "मेटाडेटा अपडेट किया",
"ActivityRead": "पढ़ना: {items}",
"ActivityListed": "सूची: {items}",
"ActivitySearched": "खोज: {items}",
"ActivityWrote": "लेखन: {items}",
"ToolReceiptRunning": "चल रहा है",
"ToolReceiptHydrated": "लोड हुआ, चलाया नहीं गया",
"ToolReceiptStopped": "रोका गया",
"ToolReceiptIssue": "समस्या",
"ToolReceiptFailed": "विफल",
"AgentActivityChecking": "जाँच जारी",
"AgentActivityChecked": "जाँच पूरी",
"AgentActivityWaiting": "प्रतीक्षा जारी",
"AgentActivityWaited": "प्रतीक्षा पूरी",
"FileMutationCreated": "बनाया: {path}",
"FileMutationUpdated": "अपडेट किया: {path}",
"FileMutationDeleted": "हटाया: {path}",
"FileMutationRenamed": "नाम बदला: {path}",
"FileMutationCreatedCount": "बनाए: {count}",
"FileMutationUpdatedCount": "अपडेट किए: {count}",
"FileMutationDeletedCount": "हटाए: {count}",
"FileMutationRenamedCount": "नाम बदले: {count}",
"FileMutationChangedFiles": "बदली गई फ़ाइलें",
"FileMutationMissingFile": "फ़ाइल",
"FileMutationNoText": "(पाठ में कोई बदलाव नहीं)",
"FileMutationMoreDiff": "+{count} डिफ़ पंक्तियाँ · {hint}",
"FileMutationChange": "बदलाव",
"FileMutationSelectReceipt": "संबंधित बदलाव चुनें; {hint}.",
"FileMutationExactEvidence": "संबंधित रिकॉर्ड में बदलाव का सटीक प्रमाण",
"FileMutationExactHeading": "फ़ाइल का सटीक बदलाव",
"FileActivityReadLabel": "पढ़ना",
"FileActivityListLabel": "सूची",
"FileActivitySearchLabel": "खोज",
"FileActivityWriteLabel": "लेखन",
"FileActivityDetailTitle": "कार्य · फ़ाइल गतिविधि",
"ToolReceiptQuiet": "चल रहा है · {duration} से कोई आउटपुट नहीं",
"FileActivityEditedLabel": "संपादित",
"FileMutationAcrossFiles": " · {count} फ़ाइलों में",
"FileActivityNoDetail": "सुरक्षित रूप से दिखाने योग्य लक्ष्य विवरण नहीं रखा गया",
"FileActivityPatterns": "पैटर्न",
"FileActivitySettled": "पूरा हुआ"
}
+58 -1
View File
@@ -2673,5 +2673,62 @@
"InstructionDetailRulesCap": "melebihi batas {count} berkas aturan per direktori",
"InstructionDetailSymlinkedRules": "menolak direktori aturan berupa symlink",
"InstructionBudgetRefused": "anggaran instruksi menolak permintaan ini: {reason}",
"InstructionBudgetOverGuard": "prefiks statis ~{measured} token perkiraan melebihi batas {guard}"
"InstructionBudgetOverGuard": "prefiks statis ~{measured} token perkiraan melebihi batas {guard}",
"ActivityFileOne": "{count} berkas",
"ActivityFileMany": "{count} berkas",
"ActivitySearchOne": "{count} pencarian",
"ActivitySearchMany": "{count} pencarian",
"ActivityCommandOne": "{count} perintah",
"ActivityCommandMany": "{count} perintah",
"ActivityTaskOne": "{count} tugas",
"ActivityTaskMany": "{count} tugas",
"ActivityDirectoryOne": "{count} direktori",
"ActivityDirectoryMany": "{count} direktori",
"ActivityPatternOne": "{count} pola",
"ActivityPatternMany": "{count} pola",
"ActivityExplored": "Menjelajahi {items}",
"ActivityRan": "menjalankan {items}",
"ActivityEdited": "mengedit {items}",
"ActivityDelegated": "mendelegasikan {items}",
"ActivityUpdatedMetadata": "memperbarui metadata",
"ActivityRead": "Membaca {items}",
"ActivityListed": "Mendaftar {items}",
"ActivitySearched": "Mencari {items}",
"ActivityWrote": "Menulis {items}",
"ToolReceiptRunning": "berjalan",
"ToolReceiptHydrated": "dimuat, belum dijalankan",
"ToolReceiptStopped": "dihentikan",
"ToolReceiptIssue": "masalah",
"ToolReceiptFailed": "gagal",
"AgentActivityChecking": "memeriksa",
"AgentActivityChecked": "diperiksa",
"AgentActivityWaiting": "menunggu",
"AgentActivityWaited": "penantian selesai",
"FileMutationCreated": "Dibuat: {path}",
"FileMutationUpdated": "Diperbarui: {path}",
"FileMutationDeleted": "Dihapus: {path}",
"FileMutationRenamed": "Diubah namanya: {path}",
"FileMutationCreatedCount": "{count} dibuat",
"FileMutationUpdatedCount": "{count} diperbarui",
"FileMutationDeletedCount": "{count} dihapus",
"FileMutationRenamedCount": "{count} diubah namanya",
"FileMutationChangedFiles": "Berkas yang diubah",
"FileMutationMissingFile": "berkas",
"FileMutationNoText": "(tidak ada perubahan teks)",
"FileMutationMoreDiff": "+{count} baris diff · {hint}",
"FileMutationChange": "perubahan",
"FileMutationSelectReceipt": "Pilih perubahan yang sesuai; {hint}.",
"FileMutationExactEvidence": "bukti perubahan persis pada catatan yang sesuai",
"FileMutationExactHeading": "Perubahan berkas persis",
"FileActivityReadLabel": "Baca",
"FileActivityListLabel": "Daftar",
"FileActivitySearchLabel": "Cari",
"FileActivityWriteLabel": "Tulis",
"FileActivityDetailTitle": "Kerja · aktivitas berkas",
"ToolReceiptQuiet": "berjalan · tidak ada keluaran selama {duration}",
"FileActivityEditedLabel": "Diedit",
"FileMutationAcrossFiles": " di {count} berkas",
"FileActivityNoDetail": "Tidak ada detail target yang aman untuk ditampilkan",
"FileActivityPatterns": "pola",
"FileActivitySettled": "selesai"
}
+58 -1
View File
@@ -2673,5 +2673,62 @@
"InstructionDetailRulesCap": "ディレクトリごとのルール上限 {count} ファイルを超えています",
"InstructionDetailSymlinkedRules": "シンボリックリンクのルールディレクトリは拒否します",
"InstructionBudgetRefused": "指示の予算により、このリクエストは拒否されました: {reason}",
"InstructionBudgetOverGuard": "静的プレフィックス ~{measured} 推定トークンが上限 {guard} を超えています"
"InstructionBudgetOverGuard": "静的プレフィックス ~{measured} 推定トークンが上限 {guard} を超えています",
"ActivityFileOne": "{count}ファイル",
"ActivityFileMany": "{count}ファイル",
"ActivitySearchOne": "{count}回の検索",
"ActivitySearchMany": "{count}回の検索",
"ActivityCommandOne": "{count}コマンド",
"ActivityCommandMany": "{count}コマンド",
"ActivityTaskOne": "{count}タスク",
"ActivityTaskMany": "{count}タスク",
"ActivityDirectoryOne": "{count}ディレクトリ",
"ActivityDirectoryMany": "{count}ディレクトリ",
"ActivityPatternOne": "{count}パターン",
"ActivityPatternMany": "{count}パターン",
"ActivityExplored": "{items}を確認",
"ActivityRan": "{items}を実行",
"ActivityEdited": "{items}を編集",
"ActivityDelegated": "{items}を委任",
"ActivityUpdatedMetadata": "メタデータを更新",
"ActivityRead": "{items}を読み取り",
"ActivityListed": "{items}を一覧表示",
"ActivitySearched": "{items}を検索",
"ActivityWrote": "{items}に書き込み",
"ToolReceiptRunning": "実行中",
"ToolReceiptHydrated": "読込済み、未実行",
"ToolReceiptStopped": "停止",
"ToolReceiptIssue": "問題あり",
"ToolReceiptFailed": "失敗",
"AgentActivityChecking": "確認中",
"AgentActivityChecked": "確認済み",
"AgentActivityWaiting": "待機中",
"AgentActivityWaited": "待機終了",
"FileMutationCreated": "作成: {path}",
"FileMutationUpdated": "更新: {path}",
"FileMutationDeleted": "削除: {path}",
"FileMutationRenamed": "名前変更: {path}",
"FileMutationCreatedCount": "作成 {count}件",
"FileMutationUpdatedCount": "更新 {count}件",
"FileMutationDeletedCount": "削除 {count}件",
"FileMutationRenamedCount": "名前変更 {count}件",
"FileMutationChangedFiles": "変更されたファイル",
"FileMutationMissingFile": "ファイル",
"FileMutationNoText": "(テキストの変更なし)",
"FileMutationMoreDiff": "差分があと{count}行 · {hint}",
"FileMutationChange": "変更",
"FileMutationSelectReceipt": "該当する変更を選択してください。{hint}。",
"FileMutationExactEvidence": "該当する記録の正確な変更内容",
"FileMutationExactHeading": "正確なファイル変更",
"FileActivityReadLabel": "読み取り",
"FileActivityListLabel": "一覧",
"FileActivitySearchLabel": "検索",
"FileActivityWriteLabel": "書き込み",
"FileActivityDetailTitle": "作業 · ファイル操作",
"ToolReceiptQuiet": "実行中 · {duration}出力なし",
"FileActivityEditedLabel": "編集済み",
"FileMutationAcrossFiles": "({count}ファイル全体)",
"FileActivityNoDetail": "安全に表示できる対象の詳細は保持されていません",
"FileActivityPatterns": "パターン",
"FileActivitySettled": "完了"
}
+58 -1
View File
@@ -2673,5 +2673,62 @@
"InstructionDetailRulesCap": "디렉터리당 규칙 파일 한도 {count}개를 초과합니다",
"InstructionDetailSymlinkedRules": "심볼릭 링크 규칙 디렉터리는 거부합니다",
"InstructionBudgetRefused": "지침 예산이 이 요청을 거부했습니다: {reason}",
"InstructionBudgetOverGuard": "정적 접두부 ~{measured} 추정 토큰이 한도 {guard}를 초과합니다"
"InstructionBudgetOverGuard": "정적 접두부 ~{measured} 추정 토큰이 한도 {guard}를 초과합니다",
"ActivityFileOne": "파일 {count}개",
"ActivityFileMany": "파일 {count}개",
"ActivitySearchOne": "검색 {count}회",
"ActivitySearchMany": "검색 {count}회",
"ActivityCommandOne": "명령 {count}개",
"ActivityCommandMany": "명령 {count}개",
"ActivityTaskOne": "작업 {count}개",
"ActivityTaskMany": "작업 {count}개",
"ActivityDirectoryOne": "디렉터리 {count}개",
"ActivityDirectoryMany": "디렉터리 {count}개",
"ActivityPatternOne": "패턴 {count}개",
"ActivityPatternMany": "패턴 {count}개",
"ActivityExplored": "{items} 확인",
"ActivityRan": "{items} 실행",
"ActivityEdited": "{items} 편집",
"ActivityDelegated": "{items} 위임",
"ActivityUpdatedMetadata": "메타데이터 업데이트",
"ActivityRead": "{items} 읽음",
"ActivityListed": "{items} 나열",
"ActivitySearched": "{items} 검색",
"ActivityWrote": "{items} 기록",
"ToolReceiptRunning": "실행 중",
"ToolReceiptHydrated": "불러옴, 실행하지 않음",
"ToolReceiptStopped": "중지됨",
"ToolReceiptIssue": "문제 있음",
"ToolReceiptFailed": "실패",
"AgentActivityChecking": "확인 중",
"AgentActivityChecked": "확인됨",
"AgentActivityWaiting": "대기 중",
"AgentActivityWaited": "대기 완료",
"FileMutationCreated": "생성: {path}",
"FileMutationUpdated": "수정: {path}",
"FileMutationDeleted": "삭제: {path}",
"FileMutationRenamed": "이름 변경: {path}",
"FileMutationCreatedCount": "생성 {count}개",
"FileMutationUpdatedCount": "수정 {count}개",
"FileMutationDeletedCount": "삭제 {count}개",
"FileMutationRenamedCount": "이름 변경 {count}개",
"FileMutationChangedFiles": "변경된 파일",
"FileMutationMissingFile": "파일",
"FileMutationNoText": "(텍스트 변경 없음)",
"FileMutationMoreDiff": "차이 {count}줄 더 있음 · {hint}",
"FileMutationChange": "변경",
"FileMutationSelectReceipt": "해당 변경을 선택하세요. {hint}.",
"FileMutationExactEvidence": "해당 기록의 정확한 변경 증거",
"FileMutationExactHeading": "정확한 파일 변경",
"FileActivityReadLabel": "읽기",
"FileActivityListLabel": "목록",
"FileActivitySearchLabel": "검색",
"FileActivityWriteLabel": "쓰기",
"FileActivityDetailTitle": "작업 · 파일 활동",
"ToolReceiptQuiet": "실행 중 · {duration} 동안 출력 없음",
"FileActivityEditedLabel": "편집됨",
"FileMutationAcrossFiles": " · 파일 {count}개에 걸침",
"FileActivityNoDetail": "안전하게 표시할 대상 세부 정보가 보관되지 않음",
"FileActivityPatterns": "패턴",
"FileActivitySettled": "완료됨"
}
+58 -1
View File
@@ -2673,5 +2673,62 @@
"InstructionDetailRulesCap": "acima do limite de {count} arquivos de regras por diretório",
"InstructionDetailSymlinkedRules": "recusando diretório de regras que é um link simbólico",
"InstructionBudgetRefused": "o orçamento de instruções recusou esta solicitação: {reason}",
"InstructionBudgetOverGuard": "o prefixo estático de ~{measured} tokens estimados excede o limite {guard}"
"InstructionBudgetOverGuard": "o prefixo estático de ~{measured} tokens estimados excede o limite {guard}",
"ActivityFileOne": "{count} arquivo",
"ActivityFileMany": "{count} arquivos",
"ActivitySearchOne": "{count} busca",
"ActivitySearchMany": "{count} buscas",
"ActivityCommandOne": "{count} comando",
"ActivityCommandMany": "{count} comandos",
"ActivityTaskOne": "{count} tarefa",
"ActivityTaskMany": "{count} tarefas",
"ActivityDirectoryOne": "{count} diretório",
"ActivityDirectoryMany": "{count} diretórios",
"ActivityPatternOne": "{count} padrão",
"ActivityPatternMany": "{count} padrões",
"ActivityExplored": "Explorou {items}",
"ActivityRan": "executou {items}",
"ActivityEdited": "editou {items}",
"ActivityDelegated": "delegou {items}",
"ActivityUpdatedMetadata": "atualizou os metadados",
"ActivityRead": "Leu {items}",
"ActivityListed": "Listou {items}",
"ActivitySearched": "Pesquisou {items}",
"ActivityWrote": "Gravou {items}",
"ToolReceiptRunning": "em execução",
"ToolReceiptHydrated": "carregado, não executado",
"ToolReceiptStopped": "parado",
"ToolReceiptIssue": "problema",
"ToolReceiptFailed": "falhou",
"AgentActivityChecking": "verificando",
"AgentActivityChecked": "verificado",
"AgentActivityWaiting": "aguardando",
"AgentActivityWaited": "espera concluída",
"FileMutationCreated": "Criado: {path}",
"FileMutationUpdated": "Atualizado: {path}",
"FileMutationDeleted": "Excluído: {path}",
"FileMutationRenamed": "Renomeado: {path}",
"FileMutationCreatedCount": "{count} criados",
"FileMutationUpdatedCount": "{count} atualizados",
"FileMutationDeletedCount": "{count} excluídos",
"FileMutationRenamedCount": "{count} renomeados",
"FileMutationChangedFiles": "Arquivos alterados",
"FileMutationMissingFile": "arquivo",
"FileMutationNoText": "(sem alterações de texto)",
"FileMutationMoreDiff": "+{count} linhas de diff · {hint}",
"FileMutationChange": "alteração",
"FileMutationSelectReceipt": "Selecione a alteração correspondente; {hint}.",
"FileMutationExactEvidence": "evidência exata no registro correspondente",
"FileMutationExactHeading": "Alteração exata do arquivo",
"FileActivityReadLabel": "Leitura",
"FileActivityListLabel": "Listagem",
"FileActivitySearchLabel": "Busca",
"FileActivityWriteLabel": "Gravação",
"FileActivityDetailTitle": "Trabalho · atividade de arquivos",
"ToolReceiptQuiet": "em execução · sem saída há {duration}",
"FileActivityEditedLabel": "Editados",
"FileMutationAcrossFiles": " em {count} arquivos",
"FileActivityNoDetail": "Nenhum detalhe do destino foi mantido para exibição segura",
"FileActivityPatterns": "padrões",
"FileActivitySettled": "concluído"
}
+58 -1
View File
@@ -2673,5 +2673,62 @@
"InstructionDetailRulesCap": "сверх предела в {count} файлов правил на каталог",
"InstructionDetailSymlinkedRules": "отклонено: каталог правил является символической ссылкой",
"InstructionBudgetRefused": "бюджет инструкций отклонил этот запрос: {reason}",
"InstructionBudgetOverGuard": "статический префикс ~{measured} оценочных токенов превышает предел {guard}"
"InstructionBudgetOverGuard": "статический префикс ~{measured} оценочных токенов превышает предел {guard}",
"ActivityFileOne": "файл — {count}",
"ActivityFileMany": "файлы — {count}",
"ActivitySearchOne": "поиск — {count}",
"ActivitySearchMany": "поиски — {count}",
"ActivityCommandOne": "команда — {count}",
"ActivityCommandMany": "команды — {count}",
"ActivityTaskOne": "задача — {count}",
"ActivityTaskMany": "задачи — {count}",
"ActivityDirectoryOne": "каталог — {count}",
"ActivityDirectoryMany": "каталоги — {count}",
"ActivityPatternOne": "шаблон — {count}",
"ActivityPatternMany": "шаблоны — {count}",
"ActivityExplored": "Просмотрено: {items}",
"ActivityRan": "выполнено: {items}",
"ActivityEdited": "изменено: {items}",
"ActivityDelegated": "делегировано: {items}",
"ActivityUpdatedMetadata": "метаданные обновлены",
"ActivityRead": "Прочитано: {items}",
"ActivityListed": "Перечислено: {items}",
"ActivitySearched": "Найдено: {items}",
"ActivityWrote": "Записано: {items}",
"ToolReceiptRunning": "выполняется",
"ToolReceiptHydrated": "загружено, не запущено",
"ToolReceiptStopped": "остановлено",
"ToolReceiptIssue": "проблема",
"ToolReceiptFailed": "ошибка",
"AgentActivityChecking": "проверка",
"AgentActivityChecked": "проверено",
"AgentActivityWaiting": "ожидание",
"AgentActivityWaited": "ожидание завершено",
"FileMutationCreated": "Создано: {path}",
"FileMutationUpdated": "Обновлено: {path}",
"FileMutationDeleted": "Удалено: {path}",
"FileMutationRenamed": "Переименовано: {path}",
"FileMutationCreatedCount": "создано: {count}",
"FileMutationUpdatedCount": "обновлено: {count}",
"FileMutationDeletedCount": "удалено: {count}",
"FileMutationRenamedCount": "переименовано: {count}",
"FileMutationChangedFiles": "Изменённые файлы",
"FileMutationMissingFile": "файл",
"FileMutationNoText": "(текстовых изменений нет)",
"FileMutationMoreDiff": "ещё {count} строк diff · {hint}",
"FileMutationChange": "изменение",
"FileMutationSelectReceipt": "Выберите соответствующее изменение; {hint}.",
"FileMutationExactEvidence": "точные данные изменения в соответствующей записи",
"FileMutationExactHeading": "Точное изменение файла",
"FileActivityReadLabel": "Чтение",
"FileActivityListLabel": "Список",
"FileActivitySearchLabel": "Поиск",
"FileActivityWriteLabel": "Запись",
"FileActivityDetailTitle": "Работа · действия с файлами",
"ToolReceiptQuiet": "выполняется · нет вывода {duration}",
"FileActivityEditedLabel": "Изменено",
"FileMutationAcrossFiles": " · файлов: {count}",
"FileActivityNoDetail": "Не сохранены сведения о цели, которые можно безопасно показать",
"FileActivityPatterns": "шаблоны",
"FileActivitySettled": "завершено"
}
+58 -1
View File
@@ -2673,5 +2673,62 @@
"InstructionDetailRulesCap": "понад ліміт у {count} файлів правил на каталог",
"InstructionDetailSymlinkedRules": "відхилено: каталог правил є символічним посиланням",
"InstructionBudgetRefused": "бюджет інструкцій відхилив цей запит: {reason}",
"InstructionBudgetOverGuard": "статичний префікс ~{measured} оціночних токенів перевищує ліміт {guard}"
"InstructionBudgetOverGuard": "статичний префікс ~{measured} оціночних токенів перевищує ліміт {guard}",
"ActivityFileOne": "файл — {count}",
"ActivityFileMany": "файли — {count}",
"ActivitySearchOne": "пошук — {count}",
"ActivitySearchMany": "пошуки — {count}",
"ActivityCommandOne": "команда — {count}",
"ActivityCommandMany": "команди — {count}",
"ActivityTaskOne": "завдання — {count}",
"ActivityTaskMany": "завдання — {count}",
"ActivityDirectoryOne": "каталог — {count}",
"ActivityDirectoryMany": "каталоги — {count}",
"ActivityPatternOne": "шаблон — {count}",
"ActivityPatternMany": "шаблони — {count}",
"ActivityExplored": "Переглянуто: {items}",
"ActivityRan": "виконано: {items}",
"ActivityEdited": "змінено: {items}",
"ActivityDelegated": "делеговано: {items}",
"ActivityUpdatedMetadata": "метадані оновлено",
"ActivityRead": "Прочитано: {items}",
"ActivityListed": "Перелічено: {items}",
"ActivitySearched": "Знайдено: {items}",
"ActivityWrote": "Записано: {items}",
"ToolReceiptRunning": "виконується",
"ToolReceiptHydrated": "завантажено, не запущено",
"ToolReceiptStopped": "зупинено",
"ToolReceiptIssue": "проблема",
"ToolReceiptFailed": "помилка",
"AgentActivityChecking": "перевірка",
"AgentActivityChecked": "перевірено",
"AgentActivityWaiting": "очікування",
"AgentActivityWaited": "очікування завершено",
"FileMutationCreated": "Створено: {path}",
"FileMutationUpdated": "Оновлено: {path}",
"FileMutationDeleted": "Видалено: {path}",
"FileMutationRenamed": "Перейменовано: {path}",
"FileMutationCreatedCount": "створено: {count}",
"FileMutationUpdatedCount": "оновлено: {count}",
"FileMutationDeletedCount": "видалено: {count}",
"FileMutationRenamedCount": "перейменовано: {count}",
"FileMutationChangedFiles": "Змінені файли",
"FileMutationMissingFile": "файл",
"FileMutationNoText": "(текстових змін немає)",
"FileMutationMoreDiff": "ще {count} рядків diff · {hint}",
"FileMutationChange": "зміна",
"FileMutationSelectReceipt": "Виберіть відповідну зміну; {hint}.",
"FileMutationExactEvidence": "точні дані зміни у відповідному записі",
"FileMutationExactHeading": "Точна зміна файлу",
"FileActivityReadLabel": "Читання",
"FileActivityListLabel": "Список",
"FileActivitySearchLabel": "Пошук",
"FileActivityWriteLabel": "Запис",
"FileActivityDetailTitle": "Робота · дії з файлами",
"ToolReceiptQuiet": "виконується · немає виводу {duration}",
"FileActivityEditedLabel": "Змінено",
"FileMutationAcrossFiles": " · файлів: {count}",
"FileActivityNoDetail": "Не збережено відомостей про ціль, які можна безпечно показати",
"FileActivityPatterns": "шаблони",
"FileActivitySettled": "завершено"
}
+58 -1
View File
@@ -2673,5 +2673,62 @@
"InstructionDetailRulesCap": "vượt giới hạn {count} tệp quy tắc mỗi thư mục",
"InstructionDetailSymlinkedRules": "từ chối thư mục quy tắc là liên kết tượng trưng",
"InstructionBudgetRefused": "ngân sách chỉ dẫn đã từ chối yêu cầu này: {reason}",
"InstructionBudgetOverGuard": "tiền tố tĩnh ~{measured} token ước tính vượt giới hạn {guard}"
"InstructionBudgetOverGuard": "tiền tố tĩnh ~{measured} token ước tính vượt giới hạn {guard}",
"ActivityFileOne": "{count} tệp",
"ActivityFileMany": "{count} tệp",
"ActivitySearchOne": "{count} lượt tìm kiếm",
"ActivitySearchMany": "{count} lượt tìm kiếm",
"ActivityCommandOne": "{count} lệnh",
"ActivityCommandMany": "{count} lệnh",
"ActivityTaskOne": "{count} tác vụ",
"ActivityTaskMany": "{count} tác vụ",
"ActivityDirectoryOne": "{count} thư mục",
"ActivityDirectoryMany": "{count} thư mục",
"ActivityPatternOne": "{count} mẫu",
"ActivityPatternMany": "{count} mẫu",
"ActivityExplored": "Đã xem {items}",
"ActivityRan": "đã chạy {items}",
"ActivityEdited": "đã sửa {items}",
"ActivityDelegated": "đã giao {items}",
"ActivityUpdatedMetadata": "đã cập nhật siêu dữ liệu",
"ActivityRead": "Đã đọc {items}",
"ActivityListed": "Đã liệt kê {items}",
"ActivitySearched": "Đã tìm {items}",
"ActivityWrote": "Đã ghi {items}",
"ToolReceiptRunning": "đang chạy",
"ToolReceiptHydrated": "đã tải, chưa chạy",
"ToolReceiptStopped": "đã dừng",
"ToolReceiptIssue": "có vấn đề",
"ToolReceiptFailed": "thất bại",
"AgentActivityChecking": "đang kiểm tra",
"AgentActivityChecked": "đã kiểm tra",
"AgentActivityWaiting": "đang chờ",
"AgentActivityWaited": "đã chờ xong",
"FileMutationCreated": "Đã tạo: {path}",
"FileMutationUpdated": "Đã cập nhật: {path}",
"FileMutationDeleted": "Đã xóa: {path}",
"FileMutationRenamed": "Đã đổi tên: {path}",
"FileMutationCreatedCount": "đã tạo: {count}",
"FileMutationUpdatedCount": "đã cập nhật: {count}",
"FileMutationDeletedCount": "đã xóa: {count}",
"FileMutationRenamedCount": "đã đổi tên: {count}",
"FileMutationChangedFiles": "Tệp đã thay đổi",
"FileMutationMissingFile": "tệp",
"FileMutationNoText": "(không có thay đổi văn bản)",
"FileMutationMoreDiff": "+{count} dòng diff · {hint}",
"FileMutationChange": "thay đổi",
"FileMutationSelectReceipt": "Chọn thay đổi tương ứng; {hint}.",
"FileMutationExactEvidence": "bằng chứng chính xác trong bản ghi tương ứng",
"FileMutationExactHeading": "Thay đổi tệp chính xác",
"FileActivityReadLabel": "Đọc",
"FileActivityListLabel": "Liệt kê",
"FileActivitySearchLabel": "Tìm kiếm",
"FileActivityWriteLabel": "Ghi",
"FileActivityDetailTitle": "Công việc · hoạt động tệp",
"ToolReceiptQuiet": "đang chạy · không có đầu ra trong {duration}",
"FileActivityEditedLabel": "Đã sửa",
"FileMutationAcrossFiles": " trong {count} tệp",
"FileActivityNoDetail": "Không giữ lại chi tiết mục tiêu có thể hiển thị an toàn",
"FileActivityPatterns": "mẫu",
"FileActivitySettled": "đã hoàn tất"
}
+58 -1
View File
@@ -2673,5 +2673,62 @@
"InstructionDetailRulesCap": "超过每个目录 {count} 个规则文件的上限",
"InstructionDetailSymlinkedRules": "拒绝符号链接的规则目录",
"InstructionBudgetRefused": "指令预算拒绝了此请求:{reason}",
"InstructionBudgetOverGuard": "静态前缀约 {measured} 估算 token 超出上限 {guard}"
"InstructionBudgetOverGuard": "静态前缀约 {measured} 估算 token 超出上限 {guard}",
"ActivityFileOne": "{count}个文件",
"ActivityFileMany": "{count}个文件",
"ActivitySearchOne": "{count}次搜索",
"ActivitySearchMany": "{count}次搜索",
"ActivityCommandOne": "{count}条命令",
"ActivityCommandMany": "{count}条命令",
"ActivityTaskOne": "{count}个任务",
"ActivityTaskMany": "{count}个任务",
"ActivityDirectoryOne": "{count}个目录",
"ActivityDirectoryMany": "{count}个目录",
"ActivityPatternOne": "{count}个模式",
"ActivityPatternMany": "{count}个模式",
"ActivityExplored": "浏览了{items}",
"ActivityRan": "运行了{items}",
"ActivityEdited": "编辑了{items}",
"ActivityDelegated": "委派了{items}",
"ActivityUpdatedMetadata": "更新了元数据",
"ActivityRead": "读取了{items}",
"ActivityListed": "列出了{items}",
"ActivitySearched": "搜索了{items}",
"ActivityWrote": "写入了{items}",
"ToolReceiptRunning": "运行中",
"ToolReceiptHydrated": "已加载,未运行",
"ToolReceiptStopped": "已停止",
"ToolReceiptIssue": "存在问题",
"ToolReceiptFailed": "失败",
"AgentActivityChecking": "检查中",
"AgentActivityChecked": "已检查",
"AgentActivityWaiting": "等待中",
"AgentActivityWaited": "等待结束",
"FileMutationCreated": "已创建{path}",
"FileMutationUpdated": "已更新{path}",
"FileMutationDeleted": "已删除{path}",
"FileMutationRenamed": "已重命名{path}",
"FileMutationCreatedCount": "创建了{count}个",
"FileMutationUpdatedCount": "更新了{count}个",
"FileMutationDeletedCount": "删除了{count}个",
"FileMutationRenamedCount": "重命名了{count}个",
"FileMutationChangedFiles": "已更改的文件",
"FileMutationMissingFile": "文件",
"FileMutationNoText": "(无文本更改)",
"FileMutationMoreDiff": "还有{count}行差异 · {hint}",
"FileMutationChange": "更改",
"FileMutationSelectReceipt": "选择对应的更改;{hint}。",
"FileMutationExactEvidence": "对应回执中的精确更改证据",
"FileMutationExactHeading": "精确文件更改",
"FileActivityReadLabel": "读取",
"FileActivityListLabel": "列出",
"FileActivitySearchLabel": "搜索",
"FileActivityWriteLabel": "写入",
"FileActivityDetailTitle": "工作 · 文件活动",
"ToolReceiptQuiet": "运行中 · 已有{duration}没有输出",
"FileActivityEditedLabel": "已编辑",
"FileMutationAcrossFiles": "(涉及{count}个文件)",
"FileActivityNoDetail": "未保留可安全显示的目标详情",
"FileActivityPatterns": "模式",
"FileActivitySettled": "已结束"
}
+58 -1
View File
@@ -2673,5 +2673,62 @@
"InstructionDetailRulesCap": "超過每個目錄 {count} 個規則檔案的上限",
"InstructionDetailSymlinkedRules": "拒絕符號連結的規則目錄",
"InstructionBudgetRefused": "指令預算拒絕了此請求:{reason}",
"InstructionBudgetOverGuard": "靜態前綴約 {measured} 估算 token 超出上限 {guard}"
"InstructionBudgetOverGuard": "靜態前綴約 {measured} 估算 token 超出上限 {guard}",
"ActivityFileOne": "{count}個檔案",
"ActivityFileMany": "{count}個檔案",
"ActivitySearchOne": "{count}次搜尋",
"ActivitySearchMany": "{count}次搜尋",
"ActivityCommandOne": "{count}條命令",
"ActivityCommandMany": "{count}條命令",
"ActivityTaskOne": "{count}個工作",
"ActivityTaskMany": "{count}個工作",
"ActivityDirectoryOne": "{count}個目錄",
"ActivityDirectoryMany": "{count}個目錄",
"ActivityPatternOne": "{count}個模式",
"ActivityPatternMany": "{count}個模式",
"ActivityExplored": "瀏覽了{items}",
"ActivityRan": "執行了{items}",
"ActivityEdited": "編輯了{items}",
"ActivityDelegated": "委派了{items}",
"ActivityUpdatedMetadata": "更新了中繼資料",
"ActivityRead": "讀取了{items}",
"ActivityListed": "列出了{items}",
"ActivitySearched": "搜尋了{items}",
"ActivityWrote": "寫入了{items}",
"ToolReceiptRunning": "執行中",
"ToolReceiptHydrated": "已載入,未執行",
"ToolReceiptStopped": "已停止",
"ToolReceiptIssue": "發生問題",
"ToolReceiptFailed": "失敗",
"AgentActivityChecking": "檢查中",
"AgentActivityChecked": "已檢查",
"AgentActivityWaiting": "等待中",
"AgentActivityWaited": "等待結束",
"FileMutationCreated": "已建立{path}",
"FileMutationUpdated": "已更新{path}",
"FileMutationDeleted": "已刪除{path}",
"FileMutationRenamed": "已重新命名{path}",
"FileMutationCreatedCount": "建立了{count}個",
"FileMutationUpdatedCount": "更新了{count}個",
"FileMutationDeletedCount": "刪除了{count}個",
"FileMutationRenamedCount": "重新命名了{count}個",
"FileMutationChangedFiles": "已變更的檔案",
"FileMutationMissingFile": "檔案",
"FileMutationNoText": "(無文字變更)",
"FileMutationMoreDiff": "還有{count}行差異 · {hint}",
"FileMutationChange": "變更",
"FileMutationSelectReceipt": "選取對應的變更;{hint}。",
"FileMutationExactEvidence": "對應回執中的精確變更證據",
"FileMutationExactHeading": "精確檔案變更",
"FileActivityReadLabel": "讀取",
"FileActivityListLabel": "列出",
"FileActivitySearchLabel": "搜尋",
"FileActivityWriteLabel": "寫入",
"FileActivityDetailTitle": "工作 · 檔案活動",
"ToolReceiptQuiet": "執行中 · 已有{duration}沒有輸出",
"FileActivityEditedLabel": "已編輯",
"FileMutationAcrossFiles": "(涉及{count}個檔案)",
"FileActivityNoDetail": "未保留可安全顯示的目標詳細資訊",
"FileActivityPatterns": "模式",
"FileActivitySettled": "已結束"
}
+114
View File
@@ -2915,6 +2915,63 @@ pub enum MessageId {
ExecAfterHelp,
ExecPromptHelp,
ExecFlagAfterPrompt,
ActivityFileOne,
ActivityFileMany,
ActivitySearchOne,
ActivitySearchMany,
ActivityCommandOne,
ActivityCommandMany,
ActivityTaskOne,
ActivityTaskMany,
ActivityDirectoryOne,
ActivityDirectoryMany,
ActivityPatternOne,
ActivityPatternMany,
ActivityExplored,
ActivityRan,
ActivityEdited,
ActivityDelegated,
ActivityUpdatedMetadata,
ActivityRead,
ActivityListed,
ActivitySearched,
ActivityWrote,
ToolReceiptRunning,
ToolReceiptHydrated,
ToolReceiptStopped,
ToolReceiptIssue,
ToolReceiptFailed,
AgentActivityChecking,
AgentActivityChecked,
AgentActivityWaiting,
AgentActivityWaited,
FileMutationCreated,
FileMutationUpdated,
FileMutationDeleted,
FileMutationRenamed,
FileMutationCreatedCount,
FileMutationUpdatedCount,
FileMutationDeletedCount,
FileMutationRenamedCount,
FileMutationChangedFiles,
FileMutationMissingFile,
FileMutationNoText,
FileMutationMoreDiff,
FileMutationChange,
FileMutationSelectReceipt,
FileMutationExactEvidence,
FileMutationExactHeading,
FileActivityReadLabel,
FileActivityListLabel,
FileActivitySearchLabel,
FileActivityWriteLabel,
FileActivityDetailTitle,
ToolReceiptQuiet,
FileActivityEditedLabel,
FileMutationAcrossFiles,
FileActivityNoDetail,
FileActivityPatterns,
FileActivitySettled,
}
pub const ALL_MESSAGE_IDS: &[MessageId] = &[
@@ -5594,6 +5651,63 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[
MessageId::ExecAfterHelp,
MessageId::ExecPromptHelp,
MessageId::ExecFlagAfterPrompt,
MessageId::ActivityFileOne,
MessageId::ActivityFileMany,
MessageId::ActivitySearchOne,
MessageId::ActivitySearchMany,
MessageId::ActivityCommandOne,
MessageId::ActivityCommandMany,
MessageId::ActivityTaskOne,
MessageId::ActivityTaskMany,
MessageId::ActivityDirectoryOne,
MessageId::ActivityDirectoryMany,
MessageId::ActivityPatternOne,
MessageId::ActivityPatternMany,
MessageId::ActivityExplored,
MessageId::ActivityRan,
MessageId::ActivityEdited,
MessageId::ActivityDelegated,
MessageId::ActivityUpdatedMetadata,
MessageId::ActivityRead,
MessageId::ActivityListed,
MessageId::ActivitySearched,
MessageId::ActivityWrote,
MessageId::ToolReceiptRunning,
MessageId::ToolReceiptHydrated,
MessageId::ToolReceiptStopped,
MessageId::ToolReceiptIssue,
MessageId::ToolReceiptFailed,
MessageId::AgentActivityChecking,
MessageId::AgentActivityChecked,
MessageId::AgentActivityWaiting,
MessageId::AgentActivityWaited,
MessageId::FileMutationCreated,
MessageId::FileMutationUpdated,
MessageId::FileMutationDeleted,
MessageId::FileMutationRenamed,
MessageId::FileMutationCreatedCount,
MessageId::FileMutationUpdatedCount,
MessageId::FileMutationDeletedCount,
MessageId::FileMutationRenamedCount,
MessageId::FileMutationChangedFiles,
MessageId::FileMutationMissingFile,
MessageId::FileMutationNoText,
MessageId::FileMutationMoreDiff,
MessageId::FileMutationChange,
MessageId::FileMutationSelectReceipt,
MessageId::FileMutationExactEvidence,
MessageId::FileMutationExactHeading,
MessageId::FileActivityReadLabel,
MessageId::FileActivityListLabel,
MessageId::FileActivitySearchLabel,
MessageId::FileActivityWriteLabel,
MessageId::FileActivityDetailTitle,
MessageId::ToolReceiptQuiet,
MessageId::FileActivityEditedLabel,
MessageId::FileMutationAcrossFiles,
MessageId::FileActivityNoDetail,
MessageId::FileActivityPatterns,
MessageId::FileActivitySettled,
];
pub fn tr(locale: Locale, id: MessageId) -> Cow<'static, str> {
+1
View File
@@ -122,6 +122,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- **Localized activity and file receipts:** grouped tool activity, file counts, patch outcomes and tool status use the shared 15-language catalog. Language changes refresh cached summaries; filenames, diffs and execution receipts keep their original data.
- **Automatic compaction stays off when disabled:** preflight and provider context-overflow recovery preserve the conversation and report the setting instead of forcing a summary. Manual compaction remains available, and a rejected recovery request retains its actual cause in both paths. (#7142)
- **VS Code packaging security:** use patched MSAL Node in the extension packager, addressing [GHSA-xcx6-jx8p-x3h5](https://github.com/AzureAD/microsoft-authentication-library-for-js/security/advisories/GHSA-xcx6-jx8p-x3h5).
+2
View File
@@ -2710,6 +2710,7 @@ pub(crate) struct ToolRunCache {
pub(crate) threshold: usize,
pub(crate) mode: ToolCollapseMode,
pub(crate) calm_mode: bool,
pub(crate) locale: Locale,
#[cfg(test)]
pub(crate) projection_builds: usize,
pub(crate) history_len: usize,
@@ -2778,6 +2779,7 @@ impl Default for ToolRunCache {
threshold: usize::MAX,
mode: ToolCollapseMode::Expanded,
calm_mode: false,
locale: Locale::En,
#[cfg(test)]
projection_builds: 0,
history_len: usize::MAX,
+155 -100
View File
@@ -13,7 +13,7 @@ use crate::tools::plan::PlanSnapshot;
use crate::tools::review::ReviewOutput;
use crate::tui::app::TranscriptSpacing;
use crate::tui::motion::MotionMode;
use codewhale_localization::Locale;
use codewhale_localization::{Locale, MessageId, tr};
use codewhale_models::{ContentBlock, Message};
use codewhale_palette as palette;
use codewhale_tui_kit::diff_render;
@@ -1009,7 +1009,9 @@ impl ToolCell {
match self {
ToolCell::Exec(cell) => cell.render_with_locale(width, low_motion, mode, locale),
ToolCell::Exploring(cell) => cell.render_with_locale(width, low_motion, mode, locale),
ToolCell::PlanUpdate(cell) => cell.lines_with_motion(width, low_motion),
ToolCell::PlanUpdate(cell) => {
cell.lines_with_motion_and_locale(width, low_motion, locale)
}
ToolCell::PatchSummary(cell) => cell.render(
width,
low_motion,
@@ -1019,8 +1021,12 @@ impl ToolCell {
),
ToolCell::Review(cell) => cell.render(width, low_motion, mode, locale),
ToolCell::Mcp(cell) => cell.render(width, low_motion, mode, locale),
ToolCell::ViewImage(cell) => cell.lines_with_motion(width, low_motion),
ToolCell::WebSearch(cell) => cell.lines_with_motion(width, low_motion),
ToolCell::ViewImage(cell) => {
cell.lines_with_motion_and_locale(width, low_motion, locale)
}
ToolCell::WebSearch(cell) => {
cell.lines_with_motion_and_locale(width, low_motion, locale)
}
ToolCell::Generic(cell) => {
cell.lines_with_mode_and_locale(width, low_motion, mode, locale)
}
@@ -1118,10 +1124,15 @@ impl ExecCell {
lines.push(render_tool_header_with_summary(
"Shell",
header_summary,
status_text.as_ref(),
if self.status == ToolStatus::Success || held_status.is_some() {
Some(status_text.as_ref())
} else {
None
},
self.status,
self.started_at,
low_motion || held_status.is_some(),
locale,
));
lines.extend(self.description_lines(width, locale));
@@ -1397,10 +1408,10 @@ impl ExecCell {
let state = held.or_else(|| match self.status {
ToolStatus::Success => None,
ToolStatus::Running => Some(self.started_at.map_or_else(
|| tool_status_label(ToolStatus::Running).to_string(),
|started| running_status_label_with_elapsed(started.elapsed().as_secs()),
|| tool_status_label(ToolStatus::Running, locale).to_string(),
|started| running_status_label_with_elapsed(started.elapsed().as_secs(), locale),
)),
status => Some(tool_status_label(status).to_string()),
status => Some(tool_status_label(status, locale).to_string()),
});
let mut suffix = Vec::new();
if let Some(state) = state {
@@ -1462,7 +1473,7 @@ impl ExecCell {
),
_ => self
.stale_elapsed_since_output_ms
.map(stale_shell_status_label),
.map(|ms| stale_shell_status_label(ms, locale)),
}
}
@@ -1577,10 +1588,10 @@ impl ExploringCell {
codewhale_localization::MessageId::ToolReceiptDone,
)
} else {
Cow::Borrowed(tool_status_label(status))
tool_status_label(status, locale)
}
} else {
Cow::Borrowed("running")
tr(locale, MessageId::ToolReceiptRunning)
};
// Search-only exploration cards read with the `find` verb so a
// completed grep renders `find done · Searching for …` instead of the
@@ -1590,10 +1601,15 @@ impl ExploringCell {
let header = render_tool_header_with_family_and_summary(
family,
header_summary.as_deref(),
header_state.as_ref(),
if status == ToolStatus::Success {
Some(header_state.as_ref())
} else {
None
},
status,
None,
low_motion,
locale,
);
let header_shows_label = !multi_entry
&& header_summary
@@ -1743,24 +1759,25 @@ impl PatchSummaryCell {
.as_ref()
.map(|receipt| {
if inline_diff_mode == codewhale_runtime::settings::InlineDiffMode::Off {
receipt.outcome_label()
receipt.outcome_label(locale)
} else {
receipt.semantic_summary()
receipt.semantic_summary(locale)
}
})
.unwrap_or_else(|| self.path.clone());
lines.push(render_tool_header_with_summary(
"File",
Some(&header_summary),
tool_status_label(self.status),
None,
self.status,
None,
low_motion,
locale,
));
if self.status == ToolStatus::Success
&& let Some(receipt) = self.receipt.as_ref()
{
lines.extend(receipt.render_inline(width, inline_diff_mode));
lines.extend(receipt.render_inline(width, inline_diff_mode, locale));
} else {
lines.extend(render_compact_kv(
"file",
@@ -1809,10 +1826,11 @@ impl ReviewCell {
let mut lines = Vec::new();
lines.push(render_tool_header(
"Review",
tool_status_label(self.status),
None,
self.status,
None,
low_motion,
locale,
));
if !self.target.trim().is_empty() {
@@ -1948,10 +1966,11 @@ impl McpToolCell {
lines.push(render_tool_header_with_summary(
"Tool",
Some(&self.tool),
tool_status_label(self.status),
None,
self.status,
None,
low_motion,
locale,
));
if self.is_image {
@@ -1992,15 +2011,21 @@ pub struct ViewImageCell {
impl ViewImageCell {
/// Render the image view cell into lines.
pub fn lines_with_motion(&self, width: u16, low_motion: bool) -> Vec<Line<'static>> {
pub fn lines_with_motion_and_locale(
&self,
width: u16,
low_motion: bool,
locale: Locale,
) -> Vec<Line<'static>> {
let path = self.path.display().to_string();
let header = render_tool_header_with_summary(
"Image",
Some(&path),
"done",
None,
ToolStatus::Success,
None,
low_motion,
locale,
);
let header_shows_path = header_shows_in_full(&header, &path, usize::from(width));
let mut lines = vec![header];
@@ -2024,15 +2049,26 @@ pub struct WebSearchCell {
impl WebSearchCell {
/// Render the web search cell into lines.
#[cfg(test)]
pub fn lines_with_motion(&self, width: u16, low_motion: bool) -> Vec<Line<'static>> {
self.lines_with_motion_and_locale(width, low_motion, Locale::En)
}
pub fn lines_with_motion_and_locale(
&self,
width: u16,
low_motion: bool,
locale: Locale,
) -> Vec<Line<'static>> {
let mut lines = Vec::new();
let header = render_tool_header_with_summary(
"Search",
Some(&self.query),
tool_status_label(self.status),
None,
self.status,
None,
low_motion,
locale,
);
let header_shows_query = header_shows_in_full(&header, &self.query, usize::from(width));
lines.push(header);
@@ -2139,7 +2175,7 @@ impl GenericToolCell {
locale: Locale,
) -> Vec<Line<'static>> {
if self.name == "activity_group" {
return agent_activity::render_activity_group(self, width);
return agent_activity::render_activity_group(self, width, locale);
}
// Issue #241: when the underlying tool is a checklist/todo update and
@@ -2162,7 +2198,7 @@ impl GenericToolCell {
// wait) stay as a single compact line (#4112 dogfood A5).
if self.name == "agent" {
if agent_activity::is_agent_inspection(self) {
return agent_activity::render_agent_compact(self, low_motion);
return agent_activity::render_agent_compact(self, low_motion, locale);
}
// Spawn / start / run: suppress the generic tool card entirely.
return Vec::new();
@@ -2182,10 +2218,11 @@ impl GenericToolCell {
vec![render_tool_header_with_family_and_summary(
family,
Some(summary.as_str()),
tool_status_label(self.status),
None,
self.status,
None,
low_motion,
locale,
)],
self.status,
);
@@ -2211,10 +2248,14 @@ impl GenericToolCell {
let mut collapsed = vec![render_tool_header_with_family_and_summary(
family,
header_summary.as_deref(),
&tool_receipt_label(family, self.status, self.output.as_deref(), locale),
Some(
tool_receipt_label(family, self.status, self.output.as_deref(), locale)
.as_ref(),
),
self.status,
None,
low_motion,
locale,
)];
if self.spillover_path.is_some() {
collapsed.push(render_spillover_annotation(width));
@@ -2236,10 +2277,11 @@ impl GenericToolCell {
let header = render_tool_header_with_family_and_summary(
family,
header_summary.as_deref(),
&tool_receipt_label(family, self.status, self.output.as_deref(), locale),
Some(tool_receipt_label(family, self.status, self.output.as_deref(), locale).as_ref()),
self.status,
None,
low_motion,
locale,
);
let header_has_args = self.input_summary.as_deref().is_some_and(|args| {
header_repeats_args(&header, header_summary.as_deref(), args, usize::from(width))
@@ -2294,10 +2336,11 @@ impl GenericToolCell {
lines.push(render_tool_header_with_summary(
"Diff",
diff_summary.as_deref(),
tool_status_label(self.status),
None,
self.status,
None,
low_motion,
locale,
));
if matches!(mode, RenderMode::Live) {
let limit = if self.status == ToolStatus::Failed {
@@ -2380,6 +2423,7 @@ impl GenericToolCell {
width,
low_motion,
mode,
locale,
))
}
@@ -2417,10 +2461,11 @@ impl GenericToolCell {
let mut lines = vec![render_tool_header_with_family_and_summary(
family,
None,
tool_status_label(self.status),
None,
self.status,
None,
low_motion,
locale,
)];
lines.extend(render_card_detail_line(
None,
@@ -2457,10 +2502,11 @@ impl GenericToolCell {
lines.push(render_tool_header_with_family_and_summary(
family,
Some(header.as_str()),
tool_status_label(self.status),
None,
self.status,
None,
low_motion,
locale,
));
if let Some(runs) = runs {
for run in runs {
@@ -2513,11 +2559,13 @@ impl GenericToolCell {
lines.push(render_tool_header_with_family_and_summary(
family,
Some(name.as_str()),
codewhale_localization::tr(
locale,
codewhale_localization::MessageId::WorkflowLineStarted,
)
.as_ref(),
Some(
codewhale_localization::tr(
locale,
codewhale_localization::MessageId::WorkflowLineStarted,
)
.as_ref(),
),
if owns_finish {
ToolStatus::Success
} else {
@@ -2525,6 +2573,7 @@ impl GenericToolCell {
},
None,
low_motion,
locale,
));
}
let finish = owns_finish.then(|| panel.finish_line()).flatten();
@@ -2543,10 +2592,11 @@ impl GenericToolCell {
lines.push(render_tool_header_with_family_and_summary(
family,
Some(facts.as_str()),
&state,
Some(&state),
finish_status,
None,
low_motion,
locale,
));
if let Some(detail) = detail {
// Why a run fell short is the one fact worth reading: its first
@@ -3046,64 +3096,58 @@ fn render_error_message(
fn render_tool_header(
title: &str,
state: &str,
state: Option<&str>,
status: ToolStatus,
started_at: Option<Instant>,
low_motion: bool,
locale: Locale,
) -> Line<'static> {
let family = crate::tui::widgets::tool_card::tool_family_for_title(title);
render_tool_header_with_family(family, state, status, started_at, low_motion)
render_tool_header_with_family_and_summary(
family, None, state, status, started_at, low_motion, locale,
)
}
fn render_tool_header_with_summary(
title: &str,
summary: Option<&str>,
state: &str,
state: Option<&str>,
status: ToolStatus,
started_at: Option<Instant>,
low_motion: bool,
locale: Locale,
) -> Line<'static> {
let family = crate::tui::widgets::tool_card::tool_family_for_title(title);
render_tool_header_with_family_and_summary(
family, summary, state, status, started_at, low_motion,
family, summary, state, status, started_at, low_motion, locale,
)
}
/// Render a tool-card header with an explicit verb family. Lets callers
/// (e.g. `GenericToolCell`) bypass the legacy title→family mapping when
/// they already know the actual tool name.
fn render_tool_header_with_family(
family: crate::tui::widgets::tool_card::ToolFamily,
state: &str,
status: ToolStatus,
started_at: Option<Instant>,
low_motion: bool,
) -> Line<'static> {
render_tool_header_with_family_and_summary(family, None, state, status, started_at, low_motion)
}
fn render_tool_header_with_family_and_summary(
family: crate::tui::widgets::tool_card::ToolFamily,
summary: Option<&str>,
state: &str,
state: Option<&str>,
status: ToolStatus,
started_at: Option<Instant>,
low_motion: bool,
locale: Locale,
) -> Line<'static> {
// For long-running tools, append elapsed seconds so the user can see the
// call isn't stuck. Threshold matches the eye's "did this hang?" reflex
// — under 3s we stay quiet so quick reads/greps don't visually churn.
let state_owned: String = if state == "running"
&& status == ToolStatus::Running
&& let Some(started) = started_at
{
running_status_label_with_elapsed(started.elapsed().as_secs())
} else {
state.to_string()
let state_owned = match (state, status, started_at) {
(Some(state), _, _) => state.to_string(),
(None, ToolStatus::Running, Some(started)) => {
running_status_label_with_elapsed(started.elapsed().as_secs(), locale)
}
_ => tool_status_label(status, locale).into_owned(),
};
let glyph = crate::tui::widgets::tool_card::family_glyph(family);
let verb = crate::tui::widgets::tool_card::family_label(family);
let verb = crate::tui::widgets::tool_card::family_label(family, locale);
let glyph_style = Style::default().fg(tool_glyph_color(status, family));
let mut spans = vec![
@@ -3125,7 +3169,7 @@ fn render_tool_header_with_family_and_summary(
// verb, the verb already carries the signal, so drop the redundant tail.
if let Some(summary) = summary
.and_then(normalize_header_summary)
.filter(|summary| !summary.eq_ignore_ascii_case(verb))
.filter(|summary| !summary.eq_ignore_ascii_case(verb.as_ref()))
{
spans.push(Span::styled(" · ", Style::default().fg(palette::TEXT_DIM)));
spans.push(Span::styled(
@@ -3178,18 +3222,19 @@ fn normalize_header_summary(summary: &str) -> Option<String> {
/// tools. Below 3s the badge is suppressed to avoid visual churn for tools
/// that resolve in milliseconds; at 3s and beyond the badge appears and ticks
/// every second the tool stays in flight.
pub(crate) fn running_status_label_with_elapsed(elapsed_secs: u64) -> String {
pub(crate) fn running_status_label_with_elapsed(elapsed_secs: u64, locale: Locale) -> String {
let state = tool_status_label(ToolStatus::Running, locale);
if elapsed_secs < 3 {
"running".to_string()
state.into_owned()
} else {
format!("running ({elapsed_secs}s)")
format!("{state} ({elapsed_secs}s)")
}
}
pub(crate) fn stale_shell_status_label(elapsed_since_output_ms: u64) -> String {
format!(
"running · no output for {}",
codewhale_command_contract::elapsed::format_elapsed_ms(elapsed_since_output_ms)
pub(crate) fn stale_shell_status_label(elapsed_since_output_ms: u64, locale: Locale) -> String {
tr(locale, MessageId::ToolReceiptQuiet).replace(
"{duration}",
&codewhale_command_contract::elapsed::format_elapsed_ms(elapsed_since_output_ms),
)
}
@@ -3371,15 +3416,18 @@ fn tool_glyph_color(
tool_rail_color(status)
}
pub(crate) fn tool_status_label(status: ToolStatus) -> &'static str {
match status {
ToolStatus::Running => "running",
ToolStatus::Success => "done",
ToolStatus::Hydrated => "loaded, not run",
ToolStatus::Stopped => "stopped",
ToolStatus::Warning => "issue",
ToolStatus::Failed => "failed",
}
pub(crate) fn tool_status_label(status: ToolStatus, locale: Locale) -> Cow<'static, str> {
tr(
locale,
match status {
ToolStatus::Running => MessageId::ToolReceiptRunning,
ToolStatus::Success => MessageId::ToolReceiptDone,
ToolStatus::Hydrated => MessageId::ToolReceiptHydrated,
ToolStatus::Stopped => MessageId::ToolReceiptStopped,
ToolStatus::Warning => MessageId::ToolReceiptIssue,
ToolStatus::Failed => MessageId::ToolReceiptFailed,
},
)
}
/// A finished read/find card can truthfully name how many rendered lines came
@@ -3393,7 +3441,7 @@ pub(crate) fn tool_receipt_label(
locale: Locale,
) -> Cow<'static, str> {
if status != ToolStatus::Success {
return Cow::Borrowed(tool_status_label(status));
return tool_status_label(status, locale);
}
use crate::tui::widgets::tool_card::ToolFamily;
match family {
@@ -3624,31 +3672,38 @@ impl FileActivitySummary {
&& self.files_written == 0
}
pub fn compact_display(&self) -> Vec<String> {
pub fn compact_display(&self, locale: Locale) -> Vec<String> {
let mut parts = Vec::new();
if self.files_read > 0 {
parts.push(format!(
"Read {}",
tool_run::counted(self.files_read as usize, "file", "files")
));
}
if self.dirs_listed > 0 {
parts.push(format!(
"Listed {}",
tool_run::counted(self.dirs_listed as usize, "directory", "directories")
));
}
if self.patterns_searched > 0 {
parts.push(format!(
"Searched {}",
tool_run::counted(self.patterns_searched as usize, "pattern", "patterns")
));
}
if self.files_written > 0 {
parts.push(format!(
"Wrote {}",
tool_run::counted(self.files_written as usize, "file", "files")
));
for (count, clause, singular, plural) in [
(
self.files_read,
MessageId::ActivityRead,
MessageId::ActivityFileOne,
MessageId::ActivityFileMany,
),
(
self.dirs_listed,
MessageId::ActivityListed,
MessageId::ActivityDirectoryOne,
MessageId::ActivityDirectoryMany,
),
(
self.patterns_searched,
MessageId::ActivitySearched,
MessageId::ActivityPatternOne,
MessageId::ActivityPatternMany,
),
(
self.files_written,
MessageId::ActivityWrote,
MessageId::ActivityFileOne,
MessageId::ActivityFileMany,
),
] {
if count > 0 {
let count = tool_run::counted(count as usize, locale, singular, plural);
parts.push(tr(locale, clause).replace("{items}", &count));
}
}
parts
}
+32 -17
View File
@@ -3,13 +3,16 @@
use ratatui::style::Style;
use ratatui::text::{Line, Span};
use codewhale_localization::{Locale, MessageId, tr};
use codewhale_palette as palette;
use super::{
GenericToolCell, render_tool_header_with_family_and_summary, tool_status_label, truncate_text,
};
use super::{GenericToolCell, render_tool_header_with_family_and_summary, truncate_text};
pub(super) fn render_agent_compact(cell: &GenericToolCell, low_motion: bool) -> Vec<Line<'static>> {
pub(super) fn render_agent_compact(
cell: &GenericToolCell,
low_motion: bool,
locale: Locale,
) -> Vec<Line<'static>> {
let family = crate::tui::widgets::tool_card::ToolFamily::Delegate;
let agent_id = cell
.output
@@ -21,24 +24,29 @@ pub(super) fn render_agent_compact(cell: &GenericToolCell, low_motion: bool) ->
// spawn — during a fan-out session every peek/status/wait would otherwise
// read as yet another completed delegate (#4112, dogfood A5). The action
// is stamped at the front of the args summary by tool_routing.
let state_label = match agent_inspection_action(cell) {
Some(AgentCompactAction::Check) => match cell.status {
super::ToolStatus::Running => "checking",
_ => "checked",
},
Some(AgentCompactAction::Wait) => match cell.status {
super::ToolStatus::Running => "waiting",
_ => "waited",
},
None => tool_status_label(cell.status),
let state_label = match (agent_inspection_action(cell), cell.status) {
(Some(AgentCompactAction::Check), super::ToolStatus::Running) => {
Some(tr(locale, MessageId::AgentActivityChecking))
}
(Some(AgentCompactAction::Check), super::ToolStatus::Success) => {
Some(tr(locale, MessageId::AgentActivityChecked))
}
(Some(AgentCompactAction::Wait), super::ToolStatus::Running) => {
Some(tr(locale, MessageId::AgentActivityWaiting))
}
(Some(AgentCompactAction::Wait), super::ToolStatus::Success) => {
Some(tr(locale, MessageId::AgentActivityWaited))
}
_ => None,
};
vec![render_tool_header_with_family_and_summary(
family,
Some(agent_id.as_str()),
state_label,
state_label.as_deref(),
cell.status,
None,
low_motion,
locale,
)]
}
@@ -66,8 +74,15 @@ fn agent_inspection_action(cell: &GenericToolCell) -> Option<AgentCompactAction>
}
}
pub(super) fn render_activity_group(cell: &GenericToolCell, width: u16) -> Vec<Line<'static>> {
let summary = cell.input_summary.as_deref().unwrap_or("Updated metadata");
pub(super) fn render_activity_group(
cell: &GenericToolCell,
width: u16,
locale: Locale,
) -> Vec<Line<'static>> {
let fallback = super::tool_run::sentence_case_activity(
tr(locale, MessageId::ActivityUpdatedMetadata).into_owned(),
);
let summary = cell.input_summary.as_deref().unwrap_or(&fallback);
let suffix = cell
.output_summary
.as_deref()
+6 -4
View File
@@ -10,8 +10,7 @@ use codewhale_palette as palette;
use super::{
RenderMode, TRANSCRIPT_RAIL, ToolStatus, render_card_detail_line_single,
render_tool_header_with_family_and_summary, tool_status_label, tool_value_style, truncate_text,
wrap_text,
render_tool_header_with_family_and_summary, tool_value_style, truncate_text, wrap_text,
};
pub(crate) fn is_checklist_tool_name(name: &str) -> bool {
@@ -148,10 +147,11 @@ pub(super) fn render_checklist_change_card(
lines.push(render_tool_header_with_family_and_summary(
family,
Some(&header_summary),
tool_status_label(status),
None,
status,
None,
low_motion,
locale,
));
// Look up the title from the snapshot. `id` in tool input is
@@ -240,6 +240,7 @@ pub(super) fn render_checklist_card(
width: u16,
low_motion: bool,
mode: RenderMode,
locale: Locale,
) -> Vec<Line<'static>> {
let mut lines = Vec::new();
let header_summary = format!(
@@ -250,10 +251,11 @@ pub(super) fn render_checklist_card(
lines.push(render_tool_header_with_family_and_summary(
family,
Some(&header_summary),
tool_status_label(status),
None,
status,
None,
low_motion,
locale,
));
let cap = match mode {
+110 -37
View File
@@ -11,6 +11,7 @@ use ratatui::text::Line;
use serde_json::Value;
use crate::tools::spec::ToolResult;
use codewhale_localization::{Locale, MessageId, tr};
use codewhale_palette as palette;
use codewhale_runtime::settings::InlineDiffMode;
use codewhale_tui_kit::diff_render;
@@ -111,38 +112,74 @@ impl FileMutationReceipt {
}
#[must_use]
pub fn outcome_label(&self) -> String {
pub fn outcome_label(&self, locale: Locale) -> String {
let created = self.count(FileMutationOutcome::Created);
let updated = self.count(FileMutationOutcome::Updated);
let deleted = self.count(FileMutationOutcome::Deleted);
let renamed = self.count(FileMutationOutcome::Renamed);
let mut outcome_parts = Vec::new();
push_count(&mut outcome_parts, created, "created");
push_count(&mut outcome_parts, updated, "updated");
push_count(&mut outcome_parts, deleted, "deleted");
push_count(&mut outcome_parts, renamed, "renamed");
push_count(
&mut outcome_parts,
created,
locale,
MessageId::FileMutationCreatedCount,
);
push_count(
&mut outcome_parts,
updated,
locale,
MessageId::FileMutationUpdatedCount,
);
push_count(
&mut outcome_parts,
deleted,
locale,
MessageId::FileMutationDeletedCount,
);
push_count(
&mut outcome_parts,
renamed,
locale,
MessageId::FileMutationRenamedCount,
);
if self.files.is_empty() {
"Changed files".to_string()
tr(locale, MessageId::FileMutationChangedFiles).into_owned()
} else if self.files.len() == 1 {
let file = &self.files[0];
match file.outcome {
FileMutationOutcome::Created => format!("Created {}", file.path),
FileMutationOutcome::Updated => format!("Updated {}", file.path),
FileMutationOutcome::Deleted => format!("Deleted {}", file.path),
FileMutationOutcome::Renamed => format!(
"Renamed {} → {}",
file.previous_path.as_deref().unwrap_or("file"),
file.path
),
FileMutationOutcome::Created => {
tr(locale, MessageId::FileMutationCreated).replace("{path}", &file.path)
}
FileMutationOutcome::Updated => {
tr(locale, MessageId::FileMutationUpdated).replace("{path}", &file.path)
}
FileMutationOutcome::Deleted => {
tr(locale, MessageId::FileMutationDeleted).replace("{path}", &file.path)
}
FileMutationOutcome::Renamed => {
let fallback = tr(locale, MessageId::FileMutationMissingFile);
let paths = format!(
"{} → {}",
file.previous_path.as_deref().unwrap_or(fallback.as_ref()),
file.path
);
tr(locale, MessageId::FileMutationRenamed).replace("{path}", &paths)
}
}
} else {
format!("{} files · {}", self.files.len(), outcome_parts.join(" · "))
let count = super::tool_run::counted(
self.files.len(),
locale,
MessageId::ActivityFileOne,
MessageId::ActivityFileMany,
);
format!("{count} · {}", outcome_parts.join(" · "))
}
}
#[must_use]
pub fn semantic_summary(&self) -> String {
pub fn semantic_summary(&self, locale: Locale) -> String {
let stats = format!("+{} -{}", self.added, self.deleted);
let separator_chars = " · ".chars().count();
let outcome_budget = MAX_SUMMARY_CHARS
@@ -150,23 +187,28 @@ impl FileMutationReceipt {
.saturating_sub(separator_chars);
format!(
"{} · {stats}",
bounded_text(&self.outcome_label(), outcome_budget)
bounded_text(&self.outcome_label(locale), outcome_budget)
)
}
#[must_use]
pub fn inspect_text(&self) -> String {
let summary = self.semantic_summary();
pub fn inspect_text(&self, locale: Locale) -> String {
let summary = self.semantic_summary(locale);
if self.exact_diff.trim().is_empty() {
format!("{summary}\n\n(no textual changes)")
format!("{summary}\n\n{}", tr(locale, MessageId::FileMutationNoText))
} else {
format!("{summary}\n\n{}", self.exact_diff)
}
}
pub fn render_inline(&self, width: u16, mode: InlineDiffMode) -> Vec<Line<'static>> {
pub fn render_inline(
&self,
width: u16,
mode: InlineDiffMode,
locale: Locale,
) -> Vec<Line<'static>> {
match mode {
InlineDiffMode::Off | InlineDiffMode::Summary => vec![exact_evidence_hint()],
InlineDiffMode::Off | InlineDiffMode::Summary => vec![exact_evidence_hint(locale)],
InlineDiffMode::Full => {
let mut lines = Vec::new();
if !self.display_diff.trim().is_empty() {
@@ -178,16 +220,20 @@ impl FileMutationReceipt {
lines.extend(rendered.lines);
if rendered.omitted_rows > 0 {
let detail_hint =
crate::tui::key_shortcuts::tool_details_shortcut_action_hint("change");
crate::tui::key_shortcuts::tool_details_shortcut_action_hint(
tr(locale, MessageId::FileMutationChange).as_ref(),
);
lines.push(details_affordance_line(
&format!("+{} diff lines · {detail_hint}", rendered.omitted_rows),
&tr(locale, MessageId::FileMutationMoreDiff)
.replace("{count}", &rendered.omitted_rows.to_string())
.replace("{hint}", &detail_hint),
Style::default().fg(palette::TEXT_MUTED).italic(),
));
} else {
lines.push(exact_evidence_hint());
lines.push(exact_evidence_hint(locale));
}
} else {
lines.push(exact_evidence_hint());
lines.push(exact_evidence_hint(locale));
}
lines
}
@@ -202,16 +248,18 @@ impl FileMutationReceipt {
}
}
fn exact_evidence_hint() -> Line<'static> {
fn exact_evidence_hint(locale: Locale) -> Line<'static> {
details_affordance_line(
&crate::tui::key_shortcuts::tool_details_shortcut_action_hint("change"),
&crate::tui::key_shortcuts::tool_details_shortcut_action_hint(
tr(locale, MessageId::FileMutationChange).as_ref(),
),
Style::default().fg(palette::TEXT_MUTED).italic(),
)
}
fn push_count(parts: &mut Vec<String>, count: usize, label: &str) {
fn push_count(parts: &mut Vec<String>, count: usize, locale: Locale, label: MessageId) {
if count > 0 {
parts.push(format!("{count} {label}"));
parts.push(tr(locale, label).replace("{count}", &count.to_string()));
}
}
@@ -340,7 +388,7 @@ mod tests {
assert!(!receipt.display_diff.contains("alice"));
assert!(
receipt
.semantic_summary()
.semantic_summary(Locale::En)
.contains("Created <external file>")
);
}
@@ -358,9 +406,34 @@ mod tests {
assert_eq!(receipt.added, 1);
assert_eq!(receipt.deleted, 1);
assert_eq!(
receipt.semantic_summary(),
receipt.semantic_summary(Locale::En),
"2 files · 1 updated · 1 renamed · +1 -1"
);
let renamed = FileMutationReceipt {
exact_diff: String::new(),
display_diff: String::new(),
files: vec![FileMutationFile {
path: "new{path}.rs".into(),
previous_path: Some("old{path}.rs".into()),
outcome: FileMutationOutcome::Renamed,
}],
added: 0,
deleted: 0,
};
assert_eq!(
renamed.outcome_label(Locale::En),
"Renamed old{path}.rs → new{path}.rs"
);
for (locale, verb) in [
(Locale::ZhHans, "已重命名"),
(Locale::ZhHant, "已重新命名"),
(Locale::Ja, "名前変更"),
] {
let text = renamed.semantic_summary(locale);
assert!(text.starts_with(verb), "{text}");
assert!(text.contains("old{path}.rs → new{path}.rs"), "{text}");
assert!(text.ends_with("+0 -0"), "{text}");
}
}
#[test]
@@ -389,7 +462,7 @@ mod tests {
let receipt =
FileMutationReceipt::from_success(Path::new("/workspace"), &result).expect("receipt");
let full = receipt.render_inline(80, InlineDiffMode::Full);
let full = receipt.render_inline(80, InlineDiffMode::Full, Locale::En);
let full_text = plain(&full);
assert!(full.len() <= MAX_INLINE_DIFF_LINES + 2, "{full_text}");
assert!(full_text.contains("line 0"), "{full_text}");
@@ -397,13 +470,13 @@ mod tests {
assert!(full_text.contains(":change"), "{full_text}");
assert!(!full_text.contains("summary:"), "{full_text}");
let summary = plain(&receipt.render_inline(80, InlineDiffMode::Summary));
let summary = plain(&receipt.render_inline(80, InlineDiffMode::Summary, Locale::En));
assert!(!summary.contains("Created src/lib.rs"), "{summary}");
assert!(!summary.contains("+30 -0"), "{summary}");
assert!(!summary.contains("line 0"), "{summary}");
assert!(summary.contains(":change"), "{summary}");
let off = plain(&receipt.render_inline(80, InlineDiffMode::Off));
let off = plain(&receipt.render_inline(80, InlineDiffMode::Off, Locale::En));
assert!(!off.contains("line 0"), "{off}");
assert!(!off.contains("+30 -0"), "{off}");
assert!(off.contains(":change"), "{off}");
@@ -418,7 +491,7 @@ mod tests {
}));
let receipt =
FileMutationReceipt::from_success(Path::new("/workspace"), &result).expect("receipt");
let full_text = plain(&receipt.render_inline(80, InlineDiffMode::Full));
let full_text = plain(&receipt.render_inline(80, InlineDiffMode::Full, Locale::En));
assert!(full_text.contains("- old"), "{full_text}");
assert!(full_text.contains("+ new"), "{full_text}");
@@ -435,7 +508,7 @@ mod tests {
}));
let receipt =
FileMutationReceipt::from_success(Path::new("/workspace"), &result).expect("receipt");
let summary = receipt.semantic_summary();
let summary = receipt.semantic_summary(Locale::En);
assert!(summary.ends_with(" · +1 -1"), "{summary}");
assert!(summary.chars().count() <= MAX_SUMMARY_CHARS);
}
+14 -4
View File
@@ -1,12 +1,11 @@
//! Rendering for plan-update transcript cells.
use codewhale_localization::Locale;
use ratatui::text::Line;
use crate::tools::plan::{PlanSnapshot, StepStatus};
use super::{
ToolStatus, render_compact_kv, render_tool_header, tool_status_label, tool_value_style,
};
use super::{ToolStatus, render_compact_kv, render_tool_header, tool_value_style};
/// Cell for plan updates emitted by the plan tool.
#[derive(Debug, Clone)]
@@ -17,14 +16,25 @@ pub struct PlanUpdateCell {
impl PlanUpdateCell {
/// Render the plan update cell into lines.
#[cfg(test)]
pub fn lines_with_motion(&self, width: u16, low_motion: bool) -> Vec<Line<'static>> {
self.lines_with_motion_and_locale(width, low_motion, Locale::En)
}
pub fn lines_with_motion_and_locale(
&self,
width: u16,
low_motion: bool,
locale: Locale,
) -> Vec<Line<'static>> {
let mut lines = Vec::new();
lines.push(render_tool_header(
"Legacy plan",
tool_status_label(self.status),
None,
self.status,
None,
low_motion,
locale,
));
render_plan_snapshot_lines(&self.snapshot, &mut lines, width);
+93 -30
View File
@@ -1428,10 +1428,10 @@ fn a_card_verb_agrees_with_its_own_label_in_every_locale() {
(
"Searching for `TranscriptScroll`",
"find done",
"find 完成",
"搜索 完成",
"read done",
),
("Reading src/foo.rs", "read done", "read 完成", "find done"),
("Reading src/foo.rs", "read done", "读取 完成", "find done"),
] {
let cell = super::ExploringCell {
entries: vec![super::ExploringEntry {
@@ -1534,6 +1534,18 @@ fn receipts_count_only_what_they_actually_counted() {
"running",
"an unfinished read has nothing to count yet"
);
for (status, text) in [
(ToolStatus::Running, "运行中"),
(ToolStatus::Failed, "失败"),
(ToolStatus::Stopped, "已停止"),
(ToolStatus::Hydrated, "已加载,未运行"),
(ToolStatus::Warning, "存在问题"),
] {
assert_eq!(
super::tool_receipt_label(ToolFamily::Run, status, None, Locale::ZhHans),
text
);
}
}
/// The same truthfulness contract through the real shell render path, where a
@@ -1563,7 +1575,7 @@ fn shell_headers_stay_truthful_through_the_output_formatters() {
for (locale, done, unit) in [
(Locale::En, "done", "lines"),
(Locale::ZhHans, "完成", "行"),
(Locale::ZhHans, "完成", " 行"),
] {
let header = line_text(&cell.render_with_locale(80, true, RenderMode::Live, locale)[0]);
assert!(
@@ -1654,6 +1666,16 @@ fn agent_cards_stay_one_line_and_spawn_cards_yield_to_the_delegate_card() {
text.contains(" agent "),
"the agent family label should appear once in the summary: {text:?}"
);
let mut failed = agent("action: peek agent_id: agent_scout_1", Some("failed"));
failed.status = ToolStatus::Failed;
let text = lines_text(&failed.lines_with_mode_and_locale(
120,
true,
RenderMode::Live,
codewhale_localization::Locale::ZhHans,
));
assert!(text.contains("失败"), "{text}");
assert!(!text.contains("已检查"), "{text}");
}
/// A tool the catalog does not have produces one useful sentence — the catalog
@@ -2230,6 +2252,15 @@ fn an_activity_group_renders_as_a_single_metadata_line() {
assert_eq!(lines.len(), 1);
assert_eq!(lines_text(&lines), "Explored 2 files, 1 search ›");
assert!(!lines_text(&lines).contains("activity_group"));
cell.input_summary = None;
let localized = cell.lines_with_mode_and_locale(
120,
true,
RenderMode::Live,
codewhale_localization::Locale::ZhHans,
);
assert!(lines_text(&localized).contains("更新了元数据"));
assert!(!lines_text(&localized).contains("Updated metadata"));
}
// ---------------------------------------------------------------------------
@@ -2573,38 +2604,47 @@ fn a_collapsed_run_summary_names_what_actually_happened() {
};
assert_eq!(
super::tool_run_summary(&run(
&["read_file", "list_dir"],
super::ToolRunActivitySummary {
files: 4,
searches: 1,
..Default::default()
}
)),
super::tool_run_summary(
&run(
&["read_file", "list_dir"],
super::ToolRunActivitySummary {
files: 4,
searches: 1,
..Default::default()
}
),
codewhale_localization::Locale::En
),
"Explored 4 files, 1 search: read file, list dir"
);
assert_eq!(
super::tool_run_summary(&run(
&["read_file", "run_tests", "validate_data"],
super::ToolRunActivitySummary {
files: 2,
commands: 2,
..Default::default()
}
)),
super::tool_run_summary(
&run(
&["read_file", "run_tests", "validate_data"],
super::ToolRunActivitySummary {
files: 2,
commands: 2,
..Default::default()
}
),
codewhale_localization::Locale::En
),
"Explored 2 files: read file, ran 2 commands: run tests, validate data",
"each clause lists only its own families"
);
assert_eq!(
super::tool_run_summary(&run(
&["session_sync"],
super::ToolRunActivitySummary {
other: 2,
..Default::default()
}
)),
super::tool_run_summary(
&run(
&["session_sync"],
super::ToolRunActivitySummary {
other: 2,
..Default::default()
}
),
codewhale_localization::Locale::En
),
"Updated metadata",
"a run of tools with no user-facing family falls back to a plain note"
);
@@ -2621,7 +2661,7 @@ fn a_collapsed_run_summary_names_what_actually_happened() {
);
assert_eq!(commands[0].activity.commands, 3);
assert_eq!(
super::tool_run_summary(&commands[0]),
super::tool_run_summary(&commands[0], codewhale_localization::Locale::En),
"Ran 3 commands: run tests, run verifiers, validate data"
);
@@ -2635,9 +2675,29 @@ fn a_collapsed_run_summary_names_what_actually_happened() {
);
assert_eq!(git[0].activity.files, 3);
assert_eq!(
super::tool_run_summary(&git[0]),
super::tool_run_summary(&git[0], codewhale_localization::Locale::En),
"Explored 3 files: git log, git show, git blame"
);
let sample = run(
&["read_file", "search"],
super::ToolRunActivitySummary {
files: 2,
searches: 1,
..Default::default()
},
);
for (locale, expected) in [
(codewhale_localization::Locale::ZhHans, "浏览了2个文件"),
(codewhale_localization::Locale::ZhHant, "瀏覽了2個檔案"),
(codewhale_localization::Locale::Ja, "2ファイル"),
(codewhale_localization::Locale::Ru, "Просмотрено: файлы — 2"),
(codewhale_localization::Locale::Uk, "Переглянуто: файли — 2"),
] {
let text = super::tool_run_summary(&sample, locale);
assert!(text.contains(expected), "{locale:?}: {text}");
assert!(!text.contains("Explored"), "{text}");
assert!(text.contains("read file"), "{text}");
}
}
/// The small pure helpers behind the cards, as one table each. Every row is a
@@ -2700,11 +2760,14 @@ fn the_card_helpers_accept_their_documented_forms_and_decline_the_rest() {
// The elapsed badge appears at three seconds and not before, so quick
// reads and greps do not visually churn.
for secs in [0, 1, 2] {
assert_eq!(running_status_label_with_elapsed(secs), "running");
assert_eq!(
running_status_label_with_elapsed(secs, codewhale_localization::Locale::En),
"running"
);
}
for secs in [3u64, 7, 120] {
assert_eq!(
running_status_label_with_elapsed(secs),
running_status_label_with_elapsed(secs, codewhale_localization::Locale::En),
format!("running ({secs}s)")
);
}
+54 -33
View File
@@ -1,6 +1,7 @@
//! Tool-run grouping for transcript collapse.
use super::{HistoryCell, ToolCell};
use codewhale_localization::{Locale, MessageId, tr};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ToolRun {
@@ -256,58 +257,74 @@ fn classify_tool_name_activity(name: &str) -> ToolRunActivity {
}
#[must_use]
pub fn tool_run_summary(run: &ToolRun) -> String {
pub fn tool_run_summary(run: &ToolRun, locale: Locale) -> String {
let activity = &run.activity;
let mut parts = Vec::new();
if activity.files > 0 {
parts.push(counted(activity.files, "file", "files"));
parts.push(counted(
activity.files,
locale,
MessageId::ActivityFileOne,
MessageId::ActivityFileMany,
));
}
if activity.searches > 0 {
parts.push(counted(activity.searches, "search", "searches"));
parts.push(counted(
activity.searches,
locale,
MessageId::ActivitySearchOne,
MessageId::ActivitySearchMany,
));
}
let mut clauses = Vec::new();
if !parts.is_empty() {
let mut explore_clause = format!("Explored {}", parts.join(", "));
let mut clause =
tr(locale, MessageId::ActivityExplored).replace("{items}", &parts.join(", "));
if let Some(families) =
activity_family_summary(run, &[ToolRunActivity::File, ToolRunActivity::Search])
{
explore_clause.push_str(": ");
explore_clause.push_str(&families);
clause.push_str(": ");
clause.push_str(&families);
}
clauses.push(explore_clause);
clauses.push(clause);
}
if activity.commands > 0 {
let mut command_clause =
format!("ran {}", counted(activity.commands, "command", "commands"));
let count = counted(
activity.commands,
locale,
MessageId::ActivityCommandOne,
MessageId::ActivityCommandMany,
);
let mut clause = tr(locale, MessageId::ActivityRan).replace("{items}", &count);
if let Some(families) = activity_family_summary(run, &[ToolRunActivity::Command]) {
command_clause.push_str(": ");
command_clause.push_str(&families);
clause.push_str(": ");
clause.push_str(&families);
}
clauses.push(command_clause);
clauses.push(clause);
}
if activity.edits > 0 {
clauses.push(format!(
"edited {}",
counted(activity.edits, "file", "files")
));
let count = counted(
activity.edits,
locale,
MessageId::ActivityFileOne,
MessageId::ActivityFileMany,
);
clauses.push(tr(locale, MessageId::ActivityEdited).replace("{items}", &count));
}
if activity.delegates > 0 {
clauses.push(format!(
"delegated {}",
counted(activity.delegates, "task", "tasks")
));
let count = counted(
activity.delegates,
locale,
MessageId::ActivityTaskOne,
MessageId::ActivityTaskMany,
);
clauses.push(tr(locale, MessageId::ActivityDelegated).replace("{items}", &count));
}
if activity.metadata > 0 || activity.other > 0 {
clauses.push("updated metadata".to_string());
if activity.metadata > 0 || activity.other > 0 || clauses.is_empty() {
clauses.push(tr(locale, MessageId::ActivityUpdatedMetadata).into_owned());
}
if clauses.is_empty() {
return "Updated metadata".to_string();
}
let summary = clauses.join(", ");
sentence_case_activity(summary)
sentence_case_activity(clauses.join(", "))
}
fn activity_family_summary(run: &ToolRun, activities: &[ToolRunActivity]) -> Option<String> {
@@ -322,12 +339,16 @@ fn activity_family_summary(run: &ToolRun, activities: &[ToolRunActivity]) -> Opt
(!families.is_empty()).then(|| families.join(", "))
}
pub(super) fn counted(count: usize, singular: &str, plural: &str) -> String {
let noun = if count == 1 { singular } else { plural };
format!("{count} {noun}")
pub(super) fn counted(
count: usize,
locale: Locale,
singular: MessageId,
plural: MessageId,
) -> String {
tr(locale, if count == 1 { singular } else { plural }).replace("{count}", &count.to_string())
}
fn sentence_case_activity(text: String) -> String {
pub(super) fn sentence_case_activity(text: String) -> String {
let mut chars = text.chars();
let Some(first) = chars.next() else {
return text;
+1 -1
View File
@@ -230,7 +230,7 @@ fn detail_status_label(job: &ShellJobSnapshot) -> String {
u64::try_from(crate::tools::shell::QUIET_NO_OUTPUT_AFTER.as_millis()).unwrap_or(u64::MAX);
match job.elapsed_since_output_ms {
Some(ms) if job.status == ShellStatus::Running && !job.stale && ms >= quiet => {
crate::tui::history::stale_shell_status_label(ms)
crate::tui::history::stale_shell_status_label(ms, codewhale_localization::Locale::En)
}
_ => status_label(&job.status, job.stale).to_string(),
}
+1 -1
View File
@@ -1567,7 +1567,7 @@ fn push_orphan_tool_completion(
.files
.first()
.map_or_else(|| "<file>".to_string(), |file| file.path.clone());
let summary = receipt.semantic_summary();
let summary = receipt.semantic_summary(app.ui_locale);
HistoryCell::Tool(ToolCell::PatchSummary(PatchSummaryCell {
path,
summary,
+22 -9
View File
@@ -398,10 +398,18 @@ pub(crate) fn open_details_pager_for_cell(app: &mut App, cell_index: usize) -> b
// model received against the full payload.
let spillover_section = spillover_pager_section(app, cell_index);
let mutation_section = match app.cell_at_virtual_index(cell_index) {
Some(HistoryCell::Tool(ToolCell::PatchSummary(cell))) => cell
.receipt
.as_ref()
.map(|receipt| format!("── Exact File change ──\n{}", receipt.inspect_text())),
Some(HistoryCell::Tool(ToolCell::PatchSummary(cell))) => {
cell.receipt.as_ref().map(|receipt| {
format!(
"── {} ──\n{}",
codewhale_localization::tr(
app.ui_locale,
codewhale_localization::MessageId::FileMutationExactHeading
),
receipt.inspect_text(app.ui_locale)
)
})
}
_ => None,
};
@@ -1173,12 +1181,14 @@ fn turn_timeline_lines(app: &App, start: usize, end: usize) -> Vec<String> {
let (kind, summary) = timeline_tool_summary(app, idx, tool);
let duration = tool_duration_for_activity(tool)
.map(codewhale_command_contract::elapsed::format_elapsed_ms);
let status = tool.status().map(tool_status_label);
let status = tool
.status()
.map(|status| tool_status_label(status, app.ui_locale));
let actions = timeline_cell_actions(app, idx, cell);
rows.push(timeline_row(
kind,
&summary,
status,
status.as_deref(),
duration.as_deref(),
&actions,
));
@@ -1427,7 +1437,7 @@ fn turn_files_changed(app: &App, start: usize, end: usize) -> Vec<String> {
lines.push(format!(
"• {} — {}",
truncate_line_to_width(&patch.path, 60),
tool_status_label(patch.status)
tool_status_label(patch.status, app.ui_locale)
));
}
_ => {}
@@ -1496,7 +1506,7 @@ fn turn_verifier_lines(app: &App, start: usize, end: usize) -> Vec<String> {
lines.push(format!(
"• {} — {}",
truncate_line_to_width(&exec.command, 56),
tool_status_label(exec.status)
tool_status_label(exec.status, app.ui_locale)
));
}
ToolCell::Review(review) => {
@@ -1506,7 +1516,10 @@ fn turn_verifier_lines(app: &App, start: usize, end: usize) -> Vec<String> {
} else {
format!("review {target}")
};
lines.push(format!("• {target} — {}", tool_status_label(review.status)));
lines.push(format!(
"• {target} — {}",
tool_status_label(review.status, app.ui_locale)
));
}
_ => {}
}
+10 -3
View File
@@ -1656,7 +1656,10 @@ fn canonical_file_mutations_attach_receipts_only_to_successful_outcomes() {
);
assert!(cell.receipt.is_some(), "{approval_mode:?} File.{action}");
assert_eq!(
cell.receipt.as_ref().unwrap().outcome_label(),
cell.receipt
.as_ref()
.unwrap()
.outcome_label(codewhale_localization::Locale::En),
"Updated src/lib.rs",
"{approval_mode:?} File.{action}"
);
@@ -26150,7 +26153,9 @@ fn orphan_success_with_mutation_metadata_keeps_the_file_receipt() {
};
assert_eq!(cell.status, ToolStatus::Success);
assert_eq!(
cell.receipt.as_ref().map(|receipt| receipt.outcome_label()),
cell.receipt
.as_ref()
.map(|receipt| receipt.outcome_label(codewhale_localization::Locale::En)),
Some("Updated src/lib.rs".to_string())
);
assert_eq!(app.tool_details_by_cell[&0].tool_name, "File");
@@ -27455,7 +27460,9 @@ fn patch_approval_modal_does_not_displace_the_active_file_receipt() {
};
assert_eq!(cell.status, ToolStatus::Success);
assert_eq!(
cell.receipt.as_ref().map(|receipt| receipt.outcome_label()),
cell.receipt
.as_ref()
.map(|receipt| receipt.outcome_label(codewhale_localization::Locale::En)),
Some("Updated src/lib.rs".to_string())
);
}
+27 -3
View File
@@ -365,6 +365,7 @@ impl ChatWidget {
&& app.tool_run_cache.threshold == app.tool_collapse_threshold
&& app.tool_run_cache.mode == app.tool_collapse_mode
&& app.tool_run_cache.calm_mode == app.calm_mode
&& app.tool_run_cache.locale == app.ui_locale
&& app.tool_run_cache.expanded_runs == app.expanded_tool_runs;
if !cache_key_matches {
let superseded_todos = superseded_todo_write_indices(&app.history, active_entries);
@@ -396,7 +397,7 @@ impl ChatWidget {
cache.summaries.insert(
run.start,
(
tool_run_summary_cell(run),
tool_run_summary_cell(run, app.ui_locale),
tool_run_summary_revision(
run,
&app.history_revisions,
@@ -417,6 +418,7 @@ impl ChatWidget {
cache.threshold = app.tool_collapse_threshold;
cache.mode = app.tool_collapse_mode;
cache.calm_mode = app.calm_mode;
cache.locale = app.ui_locale;
cache.expanded_runs.clone_from(&app.expanded_tool_runs);
}
// v0.9.1: do not collapse concurrent sub-agent cards into an Enter-
@@ -788,11 +790,11 @@ fn receipt_is_settling(receipt_order: usize, elapsed_ms: u128) -> bool {
elapsed_ms < delay + 140
}
fn tool_run_summary_cell(run: &ToolRun) -> HistoryCell {
fn tool_run_summary_cell(run: &ToolRun, locale: codewhale_localization::Locale) -> HistoryCell {
HistoryCell::Tool(ToolCell::Generic(GenericToolCell {
name: "activity_group".to_string(),
status: ToolStatus::Success,
input_summary: Some(crate::tui::history::tool_run_summary(run)),
input_summary: Some(crate::tui::history::tool_run_summary(run, locale)),
output: None,
prompts: None,
spillover_path: None,
@@ -4173,6 +4175,28 @@ mod tests {
app.scroll_up(3);
let _ = ChatWidget::new(&mut app, area);
assert_eq!(app.tool_run_cache.projection_builds, 1);
app.ui_locale = codewhale_localization::Locale::ZhHans;
let mut localized = Buffer::empty(area);
ChatWidget::new(&mut app, area).render(area, &mut localized);
assert_eq!(app.tool_run_cache.projection_builds, 2);
let cell = &app.tool_run_cache.summaries.get(&0).unwrap().0;
let HistoryCell::Tool(ToolCell::Generic(cell)) = cell else {
panic!("activity cell");
};
assert!(
cell.input_summary
.as_deref()
.unwrap()
.contains("浏览了2个文件")
);
assert!(!buffer_text(&localized, area).contains("Explored"));
let _ = ChatWidget::new(&mut app, area);
assert_eq!(app.tool_run_cache.projection_builds, 2);
app.ui_locale = codewhale_localization::Locale::En;
let mut english = Buffer::empty(area);
ChatWidget::new(&mut app, area).render(area, &mut english);
assert_eq!(app.tool_run_cache.projection_builds, 3);
assert!(buffer_text(&english, area).contains("Explored 2 files"));
app.expanded_tool_runs.insert(0);
let _ = ChatWidget::new(&mut app, area);
assert_eq!(app.collapsed_cell_map, vec![0, 1, 2]);
+8 -21
View File
@@ -125,7 +125,7 @@ fn tool_display_label_for_name(name: &str) -> String {
if matches!(family, ToolFamily::Generic) {
name.to_string()
} else {
family_label(family).to_string()
family_label(family, Locale::En).to_string()
}
}
@@ -149,11 +149,11 @@ fn family_message_id(family: ToolFamily) -> codewhale_localization::MessageId {
#[must_use]
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);
let label = family_label(family, locale);
if matches!(family, ToolFamily::Generic) {
format!("{} {name}", codewhale_localization::tr(locale, mid))
format!("{label} {name}")
} else {
codewhale_localization::tr(locale, mid).to_string()
label.into_owned()
}
}
@@ -293,23 +293,10 @@ pub fn family_glyph(family: ToolFamily) -> &'static str {
}
}
/// The short verb label for a family — appears in card headers next to the
/// glyph. Lowercased on purpose; the verb-glyph + label is the new card
/// title vocabulary.
/// The catalog-owned verb label for a family, shown beside its header glyph.
#[must_use]
pub fn family_label(family: ToolFamily) -> &'static str {
match family {
ToolFamily::Read => "read",
ToolFamily::Patch => "patch",
ToolFamily::Run => "run",
ToolFamily::Find => "find",
ToolFamily::Delegate => "agent",
ToolFamily::Fanout => "fanout",
ToolFamily::Rlm => "rlm",
ToolFamily::Verify => "verify",
ToolFamily::Think => "think",
ToolFamily::Generic => "tool",
}
pub fn family_label(family: ToolFamily, locale: Locale) -> std::borrow::Cow<'static, str> {
codewhale_localization::tr(locale, family_message_id(family))
}
/// Position of a line within a multi-line card — drives the left-rail
@@ -507,7 +494,7 @@ mod tests {
"family {family:?} has empty glyph",
);
assert!(
!family_label(family).is_empty(),
!family_label(family, Locale::En).is_empty(),
"family {family:?} has empty label",
);
}
@@ -303,6 +303,7 @@ impl ChatWidget {
&& app.tool_run_cache.threshold == app.tool_collapse_threshold
&& app.tool_run_cache.mode == app.tool_collapse_mode
&& app.tool_run_cache.calm_mode == app.calm_mode
&& app.tool_run_cache.locale == app.ui_locale
&& app.tool_run_cache.expanded_runs == app.expanded_tool_runs;
if !cache_key_matches {
let superseded_todos = superseded_todo_write_indices(&app.history, active_entries);
@@ -334,7 +335,7 @@ impl ChatWidget {
cache.summaries.insert(
run.start,
(
tool_run_summary_cell(run),
tool_run_summary_cell(run, app.ui_locale),
tool_run_summary_revision(
run,
&app.history_revisions,
@@ -355,6 +356,7 @@ impl ChatWidget {
cache.threshold = app.tool_collapse_threshold;
cache.mode = app.tool_collapse_mode;
cache.calm_mode = app.calm_mode;
cache.locale = app.ui_locale;
cache.expanded_runs.clone_from(&app.expanded_tool_runs);
}
// v0.9.1: do not collapse concurrent sub-agent cards into an Enter-
@@ -679,11 +681,11 @@ fn receipt_is_settling(receipt_order: usize, elapsed_ms: u128) -> bool {
elapsed_ms < delay + 140
}
fn tool_run_summary_cell(run: &ToolRun) -> HistoryCell {
fn tool_run_summary_cell(run: &ToolRun, locale: codewhale_localization::Locale) -> HistoryCell {
HistoryCell::Tool(ToolCell::Generic(GenericToolCell {
name: "activity_group".to_string(),
status: ToolStatus::Success,
input_summary: Some(crate::tui::history::tool_run_summary(run)),
input_summary: Some(crate::tui::history::tool_run_summary(run, locale)),
output: None,
prompts: None,
spillover_path: None,
+61 -19
View File
@@ -1257,8 +1257,9 @@ struct RankedWorkRow {
}
/// Complete unique targets; only the aggregate summary presentation is capped.
#[derive(Default, Clone)]
#[derive(Clone)]
pub(super) struct SettledFileActivity {
pub(super) locale: Locale,
pub(super) summary: FileActivitySummary,
pub(super) read: Vec<String>,
list: Vec<String>,
@@ -1268,6 +1269,21 @@ pub(super) struct SettledFileActivity {
pub(super) inline_diff_mode: InlineDiffMode,
}
impl Default for SettledFileActivity {
fn default() -> Self {
Self {
locale: Locale::En,
summary: FileActivitySummary::default(),
read: Vec::new(),
list: Vec::new(),
search: Vec::new(),
write: Vec::new(),
mutations: Vec::new(),
inline_diff_mode: InlineDiffMode::default(),
}
}
}
impl std::fmt::Debug for SettledFileActivity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SettledFileActivity")
@@ -2313,6 +2329,7 @@ const fn agent_mark(bucket: WorkBucket) -> &'static str {
pub(super) fn settled_file_activity(app: &App) -> SettledFileActivity {
let mut activity = SettledFileActivity {
locale: app.ui_locale,
inline_diff_mode: app.inline_diff_mode,
..SettledFileActivity::default()
};
@@ -2372,7 +2389,8 @@ fn aggregate_activity_row(activity: &SettledFileActivity) -> Option<RankedWorkRo
if activity.is_empty() {
return None;
}
let summaries = activity.summary.compact_display();
let locale = activity.locale;
let summaries = activity.summary.compact_display(locale);
if summaries.is_empty() {
return None;
}
@@ -2384,21 +2402,28 @@ fn aggregate_activity_row(activity: &SettledFileActivity) -> Option<RankedWorkRo
};
let mutation_detail = activity.mutations.last().map(|receipt| {
if activity.inline_diff_mode == InlineDiffMode::Off {
receipt.outcome_label()
receipt.outcome_label(locale)
} else {
receipt.semantic_summary()
receipt.semantic_summary(locale)
}
});
let mutation_body = settled_mutation_body(&activity.mutations, activity.inline_diff_mode);
let mutation_body =
settled_mutation_body(&activity.mutations, activity.inline_diff_mode, locale);
let mut body_parts = Vec::new();
if !mutation_body.is_empty() {
body_parts.push(mutation_body);
}
for (kind, details) in [
("Read", &activity.read),
("List", &activity.list),
("Search", &activity.search),
("Write", &activity.write),
(tr(locale, MessageId::FileActivityReadLabel), &activity.read),
(tr(locale, MessageId::FileActivityListLabel), &activity.list),
(
tr(locale, MessageId::FileActivitySearchLabel),
&activity.search,
),
(
tr(locale, MessageId::FileActivityWriteLabel),
&activity.write,
),
] {
if details.is_empty() {
continue;
@@ -2409,7 +2434,7 @@ fn aggregate_activity_row(activity: &SettledFileActivity) -> Option<RankedWorkRo
));
}
if body_parts.is_empty() {
body_parts.push("No safe target detail retained".to_string());
body_parts.push(tr(locale, MessageId::FileActivityNoDetail).into_owned());
}
let detail = mutation_detail
.or_else(|| {
@@ -2418,10 +2443,15 @@ fn aggregate_activity_row(activity: &SettledFileActivity) -> Option<RankedWorkRo
.first()
.cloned()
.or_else(|| activity.read.first().cloned())
.or_else(|| activity.search.first().map(|_| "patterns".to_string()))
.or_else(|| {
activity
.search
.first()
.map(|_| tr(locale, MessageId::FileActivityPatterns).into_owned())
})
.or_else(|| activity.list.first().cloned())
})
.unwrap_or_else(|| "settled".to_string());
.unwrap_or_else(|| tr(locale, MessageId::FileActivitySettled).into_owned());
let detail = sanitize_summary_title(&detail);
Some(RankedWorkRow {
bucket: WorkBucket::Recent,
@@ -2435,7 +2465,7 @@ fn aggregate_activity_row(activity: &SettledFileActivity) -> Option<RankedWorkRo
tone: WorkTone::Success,
selectable: true,
primary_action: Some(SidebarRowAction::InspectWork {
title: "Work · file activity".to_string(),
title: tr(locale, MessageId::FileActivityDetailTitle).into_owned(),
body: body_parts.join("\n\n"),
stop_action: None,
}),
@@ -2452,17 +2482,18 @@ fn activity_rows(activity: SettledFileActivity) -> Vec<RankedWorkRow> {
pub(super) fn settled_mutation_body(
receipts: &[FileMutationReceipt],
mode: InlineDiffMode,
locale: Locale,
) -> String {
let Some(receipt) = receipts.last() else {
return String::new();
};
let details = crate::tui::key_shortcuts::tool_details_shortcut_action_hint(
"exact change evidence on the matching File receipt",
tr(locale, MessageId::FileMutationExactEvidence).as_ref(),
);
let hint = format!("Select the matching File receipt; {details}.");
let hint = tr(locale, MessageId::FileMutationSelectReceipt).replace("{hint}", &details);
match mode {
InlineDiffMode::Off => format!("{}\n\n{hint}", receipt.outcome_label()),
InlineDiffMode::Summary => format!("{}\n\n{hint}", receipt.semantic_summary()),
InlineDiffMode::Off => format!("{}\n\n{hint}", receipt.outcome_label(locale)),
InlineDiffMode::Summary => format!("{}\n\n{hint}", receipt.semantic_summary(locale)),
InlineDiffMode::Full => {
let diff = receipt
.display_diff
@@ -2471,9 +2502,9 @@ pub(super) fn settled_mutation_body(
.collect::<Vec<_>>()
.join("\n");
if diff.trim().is_empty() {
format!("{}\n\n{hint}", receipt.semantic_summary())
format!("{}\n\n{hint}", receipt.semantic_summary(locale))
} else {
format!("{}\n\n{diff}\n\n{hint}", receipt.semantic_summary())
format!("{}\n\n{diff}\n\n{hint}", receipt.semantic_summary(locale))
}
}
}
@@ -4237,6 +4268,17 @@ mod tests {
assert!(body.contains(&format!("{action}-11.rs")));
assert!(!body.contains(&format!("{action}-12.rs")));
}
app.ui_locale = Locale::ZhHans;
app.work_surface.file_activity = settled_file_activity(&app);
let rows = files_rows(&mut app);
assert!(rows.iter().any(|row| row.label == "读取 13"));
assert!(rows.iter().any(|row| row.label == "已编辑 13"));
let summary = aggregate_activity_row(&app.work_surface.file_activity).unwrap();
assert!(
summary.row.label.contains("读取了14个文件"),
"{}",
summary.row.label
);
}
#[test]
+21 -4
View File
@@ -6,6 +6,7 @@
use crate::agent_roster::format_tokens;
use crate::tui::app::{App, SidebarRowAction};
use codewhale_localization::{MessageId, tr};
use super::model::{RailPanel, WorkRow, WorkRowId, WorkTone};
@@ -57,7 +58,14 @@ pub(super) fn files_rows(app: &mut App) -> Vec<WorkRow> {
let mut out = Vec::new();
let edited = edited_files(&activity);
if !edited.is_empty() {
out.push(heading("files:edited", format!("Edited {}", edited.len())));
out.push(heading(
"files:edited",
format!(
"{} {}",
tr(activity.locale, MessageId::FileActivityEditedLabel),
edited.len()
),
));
for (path, receipt) in &edited {
let Some(receipt) = receipt else {
out.push(WorkRow {
@@ -68,7 +76,8 @@ pub(super) fn files_rows(app: &mut App) -> Vec<WorkRow> {
continue;
};
let across = if receipt.files.len() > 1 {
format!(" across {} files", receipt.files.len())
tr(activity.locale, MessageId::FileMutationAcrossFiles)
.replace("{count}", &receipt.files.len().to_string())
} else {
String::new()
};
@@ -80,10 +89,14 @@ pub(super) fn files_rows(app: &mut App) -> Vec<WorkRow> {
tone: WorkTone::Success,
selectable: true,
primary_action: Some(SidebarRowAction::InspectWork {
title: format!("File · {path}"),
title: format!(
"{} · {path}",
tr(activity.locale, MessageId::ApprovalLabelFile)
),
body: super::model::settled_mutation_body(
std::slice::from_ref(*receipt),
activity.inline_diff_mode,
activity.locale,
),
stop_action: None,
}),
@@ -94,7 +107,11 @@ pub(super) fn files_rows(app: &mut App) -> Vec<WorkRow> {
if !activity.read.is_empty() {
out.push(heading(
"files:read",
format!("Read {}", activity.read.len()),
format!(
"{} {}",
tr(activity.locale, MessageId::FileActivityReadLabel),
activity.read.len()
),
));
for path in &activity.read {
out.push(WorkRow {
+1 -1
View File
@@ -651,7 +651,7 @@ owner = "crates/tui/src/commands/groups/session/color.rs"
[[feature]]
id = "localization"
name = "Localization"
summary = "Interface text in 15 locales."
summary = "Interface text in 15 locales, including tool activity, file receipts and language-aware transcript caches."
status = "stable"
surfaces = { tui = "stable" }
docs = "docs/LOCALIZATION.md"
+2 -2
View File
@@ -2,7 +2,7 @@
"format": "codewhale.features.v1",
"source": {
"path": "docs/features.toml",
"sha256": "0b7740404421a766df45a7e5787b2e2dbf6f9ee6361ad3a479044d9c972ea3d5"
"sha256": "5e18921ce90014e26473bc496fc80a29abf1a288144f59c25fcea5b6939d38e1"
},
"features": [
{
@@ -844,7 +844,7 @@
{
"id": "localization",
"name": "Localization",
"summary": "Interface text in 15 locales.",
"summary": "Interface text in 15 locales, including tool activity, file receipts and language-aware transcript caches.",
"status": "stable",
"surfaces": {
"tui": "stable"