fix(tui): keep compact approvals and launch navigation safe [skip ci]

Complete the interrupted coordinator batch: three visible approval choices with default Deny, exact-rule preview gating and Details-gated hidden session/ask shortcuts; separate everyday CLI help, doctor --all, recent-session Up navigation and transcript-free cameos; localize the resume hint and update the existing glossary/feature rows. Add session.window.rerooted diagnostics while retaining the previous journal branch. This event is not a root-cause or seam-summary fix for #7142.

Verification: 398 existing approval cases passed, then the existing obsolete hidden-a expectation was corrected and its replacement passed (399 distinct cases covered). Feature registry 2 passed; UI launch 59 passed/1 intentionally ignored render fixture; cameo 3 passed; doctor-filter checks 142 passed. Prior localization 54 and session_tree 22 passes were reused after verifying their 17 input hashes unchanged. Current CLI build and CLI all-target/all-feature Clippy with the repository CI allowlist passed. Isolated help/advanced-help/uninstall-help and doctor compact/all runs exited 0; doctor showed 219 versus 232 lines. No new tests or hosted CI dispatch.

Remaining: the broader launch filter found late_launch_permit_still_gets_the_full_work_budget failing with BudgetExhausted; it also fails alone. Its runtime path is untouched here and remains the next integrity investigation, not a release pass. Full 0.10.3 implementation, provider/platform qualification and final validation remain open.
This commit is contained in:
CodeWhale Bot committed 2026-10-29 06:42:38 -07:00
1 parent 1d677d976e
commit 62f0cc9536
34 files changed
+425 -249

No files matched your search

+3
View File
@@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- **Compact approvals keep their authority visible:** cards show three choices and default to Deny. An exact saved rule requires its scope preview on screen. Shortcuts for additional actions require opening Details; a hidden saved-rule choice never falls back to granting the session.
- **Quicker everyday navigation:** top-level help separates everyday and advanced commands, `doctor --all` expands path/backend details, and the launch screen labels how to resume recent work. Up selects the newest recent session. Whale cameos stay out of transcript text.
- **Session-window diagnostics:** the audit log records when a saved window starts a new journal root while retaining the previous branch, helping distinguish window replacement from lost durable history.
- **Usage reporting counts models, tokens and tools:** when usage reporting is on, each session now also reports, per provider and model, its turns by outcome, input, output and prompt-cache tokens, and turn-time buckets, plus how many times each built-in tool ran and failed. A model is named only when it is in Codewhale's bundled public model catalog; custom, local and fine-tuned names are sent as `other`, and MCP and plugin tools as `mcp` or `other`. No prompts, outputs, tool arguments or paths are sent. Runs under CI or the GitHub Action report the `ci` session source. The first-run notice is shown once more to describe this. `DO_NOT_TRACK=1` now turns usage reporting off for that run, like `CODEWHALE_TELEMETRY=0`; `codewhale config set telemetry false` still turns it off for good. See `docs/TELEMETRY.md`.
- **`exec` and `doctor` follow the TUI onto local Ollama:** with no cloud key and no explicit route, `codewhale exec` uses a running local Ollama chat model and says so, and `codewhale doctor --probe-local` checks that route. Nothing is saved. (#7115)
- **Thinking starts at auto:** new configurations and the built-in default use `reasoning_effort = "auto"` instead of `max`; an effort you set is never changed.
+59 -1
View File
@@ -514,6 +514,64 @@ const COMPLETION_BIN_NAME: &str = "codewhale";
/// types the short name.
const COMPLETION_ALIAS_NAME: &str = "codew";
const ADVANCED_COMMANDS: &[&str] = &[
"run",
"models",
"model",
"fork",
"rc",
"web",
"serve",
"app-server",
"mcp-server",
"login",
"logout",
"account",
"dispatch",
"fleet",
"workflow",
"lane",
"apply",
"install",
"setup",
"remote-setup",
"integrations",
"features",
"speech",
"thread",
"sandbox",
"eval",
"scorecard",
"session-diagnostics",
"metrics",
"providers",
"completion",
];
fn user_facing_command() -> clap::Command {
let mut cmd = Cli::command();
let mut listed = Vec::new();
for name in ADVANCED_COMMANDS {
if cmd.find_subcommand(name).is_some() {
cmd = cmd.mut_subcommand(name, |sub| sub.hide(true));
listed.push(*name);
}
}
let mut paragraph =
String::from("Advanced commands (`codewhale help <command>` for details):\n ");
let mut width = 1;
for name in listed {
if width + name.len() + 1 > 78 {
paragraph.push_str("\n ");
width = 1;
}
paragraph.push(' ');
paragraph.push_str(name);
width += name.len() + 1;
}
cmd.after_help(paragraph)
}
/// Render the completion script for `shell` from this binary's own clap tree,
/// registered for both published command names.
fn render_completion_script(shell: Shell) -> String {
@@ -2294,7 +2352,7 @@ fn argv_secret_warning(cli: &Cli, command: Option<&Commands>) -> Option<&'static
fn run() -> Result<()> {
let inherited_listener = codewhale_tui::prepare_runtime_listener_handoff()?;
let argv: Vec<_> = std::env::args_os().collect();
let matches = Cli::command().get_matches_from(&argv);
let matches = user_facing_command().get_matches_from(&argv);
let project_bundle_scope = config_command_targets_project(&matches);
let mut cli = Cli::from_arg_matches(&matches).unwrap_or_else(|error| error.exit());
anyhow::ensure!(
+3 -2
View File
@@ -1300,12 +1300,12 @@
"ApprovalFieldParams": "Paràmetres: ",
"ApprovalOptionApproveOnce": "Permet una vegada",
"ApprovalOptionApproveAlways": "Permet en aquesta conversa",
"ApprovalOptionAllowExactRepo": "Permet sempre en aquest repo",
"ApprovalOptionAllowExactRepo": "Sempre per a aquesta ordre",
"ApprovalSaveAskRuleHint": " · s permet ara i pregunta sempre abans de tornar-ho a fer",
"ApprovalOptionDeny": "No ho permetis",
"ApprovalOptionAbortTurn": "Atura aquest torn",
"ApprovalBlockTitle": "aprovació",
"ApprovalControlsHint": "Pg↑/↓ revisa · {details} detalls · Esc atura",
"ApprovalControlsHint": "Pg↑/↓ revisa · {details} més opcions · Esc atura",
"ApprovalOwnerHeader": "Agent: {agent} · {role}",
"ApprovalGoToAgent": "g ves a l'agent",
"UserInputActionRequired": "Cal una acció",
@@ -2458,6 +2458,7 @@
"LaunchNoticeClaude": "Claude Code detectat. Executa /import-claude per revisar què es pot importar: servidors MCP, valors segurs i permisos. Res s'aplica sense el teu consentiment.",
"LaunchNewSession": "Nova sessió",
"LaunchRecentHeading": "Recents",
"LaunchRecentResumeHint": "↑ per reprendre",
"LaunchHelpLine": "/help per a ordres · {dock} per a la barra de treball",
"LaunchSeeAllSessions": "Mostra totes les sessions…",
"LaunchNoRecentSessions": "Encara no hi ha sessions recents — escriu a sota per començar.",
+3 -2
View File
@@ -1300,12 +1300,12 @@
"ApprovalFieldParams": "Parameter: ",
"ApprovalOptionApproveOnce": "Einmal erlauben",
"ApprovalOptionApproveAlways": "In dieser Unterhaltung erlauben",
"ApprovalOptionAllowExactRepo": "In diesem Repo immer erlauben",
"ApprovalOptionAllowExactRepo": "Immer für diesen Befehl",
"ApprovalSaveAskRuleHint": " · s jetzt erlauben und künftig immer vorher fragen",
"ApprovalOptionDeny": "Nicht erlauben",
"ApprovalOptionAbortTurn": "Diese Runde stoppen",
"ApprovalBlockTitle": "Freigabe",
"ApprovalControlsHint": "Pg↑/↓ prüfen · {details} Details · Esc stoppen",
"ApprovalControlsHint": "Pg↑/↓ prüfen · {details} weitere Optionen · Esc stoppen",
"ApprovalOwnerHeader": "Agent: {agent} · {role}",
"ApprovalGoToAgent": "g zum Agenten",
"UserInputActionRequired": "Aktion erforderlich",
@@ -2458,6 +2458,7 @@
"LaunchNoticeClaude": "Claude Code erkannt. Führe /import-claude aus, um zu prüfen, was übernommen werden kann: MCP-Server, sichere Einstellungen und Berechtigungen. Nichts wird ohne deine Zustimmung angewendet.",
"LaunchNewSession": "Neue Sitzung",
"LaunchRecentHeading": "Zuletzt",
"LaunchRecentResumeHint": "↑ zum Fortsetzen",
"LaunchHelpLine": "/help für Befehle · {dock} für die Arbeitsleiste",
"LaunchSeeAllSessions": "Alle Sitzungen anzeigen…",
"LaunchNoRecentSessions": "Noch keine aktuellen Sitzungen — unten tippen, um zu starten.",
+3 -2
View File
@@ -1323,12 +1323,12 @@
"ApprovalFieldParams": "Params: ",
"ApprovalOptionApproveOnce": "Allow once",
"ApprovalOptionApproveAlways": "Allow for this conversation",
"ApprovalOptionAllowExactRepo": "Always allow in this repo",
"ApprovalOptionAllowExactRepo": "Always for this command",
"ApprovalSaveAskRuleHint": " · s allow now, and always ask before this again",
"ApprovalOptionDeny": "Don't allow",
"ApprovalOptionAbortTurn": "Stop this turn",
"ApprovalBlockTitle": "approval",
"ApprovalControlsHint": "Pg↑/↓ review · {details} details · Esc stop",
"ApprovalControlsHint": "Pg↑/↓ review · {details} more choices · Esc stop",
"ApprovalOwnerHeader": "Agent: {agent} · {role}",
"ApprovalGoToAgent": "g go to agent",
"UserInputActionRequired": "Action required",
@@ -2458,6 +2458,7 @@
"LaunchNoticeClaude": "Claude Code found. /import-claude to review; you approve every change.",
"LaunchNewSession": "New session",
"LaunchRecentHeading": "Recent",
"LaunchRecentResumeHint": "↑ to resume",
"LaunchHelpLine": "/help for commands · {dock} for the workbar",
"LaunchSeeAllSessions": "See all sessions…",
"LaunchNoRecentSessions": "No recent sessions yet — type below to start.",
+3 -2
View File
@@ -1321,12 +1321,12 @@
"ApprovalFieldParams": "Parámetros:",
"ApprovalOptionApproveOnce": "Permitir una vez",
"ApprovalOptionApproveAlways": "Permitir en esta conversación",
"ApprovalOptionAllowExactRepo": "Permitir siempre en este repositorio",
"ApprovalOptionAllowExactRepo": "Siempre para este comando",
"ApprovalSaveAskRuleHint": " · s permitir ahora y preguntar siempre antes de repetirlo",
"ApprovalOptionDeny": "No permitir",
"ApprovalOptionAbortTurn": "Detener este turno",
"ApprovalBlockTitle": "aprobación",
"ApprovalControlsHint": "Pg↑/↓ revisar · {details} detalles · Esc detener",
"ApprovalControlsHint": "Pg↑/↓ revisar · {details} más opciones · Esc detener",
"ApprovalOwnerHeader": "Agente: {agent} · {role}",
"ApprovalGoToAgent": "g ir al agente",
"UserInputActionRequired": "Se requiere acción",
@@ -2458,6 +2458,7 @@
"LaunchNoticeClaude": "Claude Code detectado. Ejecuta /import-claude para revisar qué puede importarse: servidores MCP, ajustes seguros y permisos. Nada se aplica sin tu aprobación.",
"LaunchNewSession": "Nueva sesión",
"LaunchRecentHeading": "Recientes",
"LaunchRecentResumeHint": "↑ para reanudar",
"LaunchHelpLine": "/help para comandos · {dock} para la barra de trabajo",
"LaunchSeeAllSessions": "Ver todas las sesiones…",
"LaunchNoRecentSessions": "Aún no hay sesiones recientes — escribe abajo para empezar.",
+3 -2
View File
@@ -1300,12 +1300,12 @@
"ApprovalFieldParams": "Paramètres : ",
"ApprovalOptionApproveOnce": "Autoriser une fois",
"ApprovalOptionApproveAlways": "Autoriser dans cette conversation",
"ApprovalOptionAllowExactRepo": "Toujours autoriser dans ce dépôt",
"ApprovalOptionAllowExactRepo": "Toujours pour cette commande",
"ApprovalSaveAskRuleHint": " · s autoriser maintenant et toujours demander avant de recommencer",
"ApprovalOptionDeny": "Ne pas autoriser",
"ApprovalOptionAbortTurn": "Arrêter ce tour",
"ApprovalBlockTitle": "approbation",
"ApprovalControlsHint": "Pg↑/↓ réviser · {details} détails · Esc arrêter",
"ApprovalControlsHint": "Pg↑/↓ réviser · {details} plus d'options · Esc arrêter",
"ApprovalOwnerHeader": "Agent : {agent} · {role}",
"ApprovalGoToAgent": "g aller à l'agent",
"UserInputActionRequired": "Action requise",
@@ -2458,6 +2458,7 @@
"LaunchNoticeClaude": "Claude Code détecté. Lancez /import-claude pour examiner ce qui peut être repris : serveurs MCP, réglages sûrs et permissions. Rien n'est appliqué sans votre accord.",
"LaunchNewSession": "Nouvelle session",
"LaunchRecentHeading": "Récent",
"LaunchRecentResumeHint": "↑ pour reprendre",
"LaunchHelpLine": "/help pour les commandes · {dock} pour la barre de travail",
"LaunchSeeAllSessions": "Voir toutes les sessions…",
"LaunchNoRecentSessions": "Aucune session récente — écrivez ci-dessous pour commencer.",
+3 -2
View File
@@ -1300,12 +1300,12 @@
"ApprovalFieldParams": "पैराम्स: ",
"ApprovalOptionApproveOnce": "एक बार अनुमति दें",
"ApprovalOptionApproveAlways": "इस बातचीत के लिए अनुमति दें",
"ApprovalOptionAllowExactRepo": "इस रेपो में हमेशा अनुमति दें",
"ApprovalOptionAllowExactRepo": "इस कमांड के लिए हमेशा",
"ApprovalSaveAskRuleHint": " · s अभी अनुमति दें, और अगली बार हमेशा पहले पूछें",
"ApprovalOptionDeny": "अनुमति न दें",
"ApprovalOptionAbortTurn": "यह टर्न रोकें",
"ApprovalBlockTitle": "अनुमति",
"ApprovalControlsHint": "Pg↑/↓ समीक्षा · {details} विवरण · Esc रोकें",
"ApprovalControlsHint": "Pg↑/↓ समीक्षा · {details} और विकल्प · Esc रोकें",
"ApprovalOwnerHeader": "एजेंट: {agent} · {role}",
"ApprovalGoToAgent": "g एजेंट पर जाएँ",
"UserInputActionRequired": "कार्रवाई आवश्यक",
@@ -2458,6 +2458,7 @@
"LaunchNoticeClaude": "Claude Code मिला। क्या लाया जा सकता है यह देखने के लिए /import-claude चलाएँ: MCP सर्वर, सुरक्षित सेटिंग्स और अनुमतियाँ। आपकी स्वीकृति के बिना कुछ लागू नहीं होता।",
"LaunchNewSession": "नया सत्र",
"LaunchRecentHeading": "हाल के",
"LaunchRecentResumeHint": "फिर से शुरू करने के लिए ↑",
"LaunchHelpLine": "कमांड के लिए /help · वर्क बार के लिए {dock}",
"LaunchSeeAllSessions": "सभी सत्र देखें…",
"LaunchNoRecentSessions": "अभी कोई हालिया सत्र नहीं — शुरू करने के लिए नीचे लिखें।",
+3 -2
View File
@@ -1300,12 +1300,12 @@
"ApprovalFieldParams": "Param: ",
"ApprovalOptionApproveOnce": "Izinkan sekali",
"ApprovalOptionApproveAlways": "Izinkan untuk percakapan ini",
"ApprovalOptionAllowExactRepo": "Selalu izinkan di repo ini",
"ApprovalOptionAllowExactRepo": "Selalu untuk perintah ini",
"ApprovalSaveAskRuleHint": " · s izinkan sekarang, dan selalu tanya dulu lain kali",
"ApprovalOptionDeny": "Jangan izinkan",
"ApprovalOptionAbortTurn": "Hentikan giliran ini",
"ApprovalBlockTitle": "persetujuan",
"ApprovalControlsHint": "Pg↑/↓ tinjau · {details} detail · Esc hentikan",
"ApprovalControlsHint": "Pg↑/↓ tinjau · {details} opsi lain · Esc hentikan",
"ApprovalOwnerHeader": "Agen: {agent} · {role}",
"ApprovalGoToAgent": "g buka agen",
"UserInputActionRequired": "Tindakan diperlukan",
@@ -2458,6 +2458,7 @@
"LaunchNoticeClaude": "Claude Code terdeteksi. Jalankan /import-claude untuk meninjau apa yang bisa dibawa: server MCP, pengaturan aman, dan izin. Tidak ada yang diterapkan tanpa persetujuan Anda.",
"LaunchNewSession": "Sesi baru",
"LaunchRecentHeading": "Terkini",
"LaunchRecentResumeHint": "↑ untuk melanjutkan",
"LaunchHelpLine": "/help untuk perintah · {dock} untuk bilah kerja",
"LaunchSeeAllSessions": "Lihat semua sesi…",
"LaunchNoRecentSessions": "Belum ada sesi terkini — ketik di bawah untuk memulai.",
+3 -2
View File
@@ -1321,12 +1321,12 @@
"ApprovalFieldParams": "パラメータ:",
"ApprovalOptionApproveOnce": "今回のみ許可",
"ApprovalOptionApproveAlways": "この会話で許可",
"ApprovalOptionAllowExactRepo": "このリポジトリで常に許可",
"ApprovalOptionAllowExactRepo": "このコマンドは常に許可",
"ApprovalSaveAskRuleHint": " · s 今回は許可し、次回からは毎回確認",
"ApprovalOptionDeny": "許可しない",
"ApprovalOptionAbortTurn": "このターンを停止",
"ApprovalBlockTitle": "承認",
"ApprovalControlsHint": "Pg↑/↓ 履歴 · {details} 詳細 · Esc 停止",
"ApprovalControlsHint": "Pg↑/↓ 履歴 · {details} その他の選択肢 · Esc 停止",
"ApprovalOwnerHeader": "エージェント: {agent} · {role}",
"ApprovalGoToAgent": "g エージェントへ移動",
"UserInputActionRequired": "対応が必要です",
@@ -2458,6 +2458,7 @@
"LaunchNoticeClaude": "Claude Code を検出しました。/import-claude を実行すると、MCP サーバー・安全な設定・権限のうち引き継げるものを確認できます。承認なしには何も適用されません。",
"LaunchNewSession": "新しいセッション",
"LaunchRecentHeading": "最近",
"LaunchRecentResumeHint": "↑ で再開",
"LaunchHelpLine": "コマンドは /help · ワークバーは {dock}",
"LaunchSeeAllSessions": "すべてのセッションを表示…",
"LaunchNoRecentSessions": "最近のセッションはまだありません — 下に入力して開始します。",
+3 -2
View File
@@ -1323,12 +1323,12 @@
"ApprovalFieldParams": "매개변수: ",
"ApprovalOptionApproveOnce": "한 번 허용",
"ApprovalOptionApproveAlways": "이 대화에서 허용",
"ApprovalOptionAllowExactRepo": "이 저장소에서 항상 허용",
"ApprovalOptionAllowExactRepo": "이 명령은 항상 허용",
"ApprovalSaveAskRuleHint": " · s 지금 허용하고 다음부터는 항상 먼저 묻기",
"ApprovalOptionDeny": "허용 안 함",
"ApprovalOptionAbortTurn": "이번 턴 중지",
"ApprovalBlockTitle": "승인",
"ApprovalControlsHint": "Pg↑/↓ 기록 · {details} 상세 · Esc 중지",
"ApprovalControlsHint": "Pg↑/↓ 기록 · {details} 추가 선택 · Esc 중지",
"ApprovalOwnerHeader": "에이전트: {agent} · {role}",
"ApprovalGoToAgent": "g 에이전트로 이동",
"UserInputActionRequired": "조치 필요",
@@ -2458,6 +2458,7 @@
"LaunchNoticeClaude": "Claude Code가 감지되었습니다. /import-claude를 실행해 가져올 수 있는 항목(MCP 서버, 안전한 설정, 권한)을 검토하세요. 승인 없이는 아무것도 적용되지 않습니다.",
"LaunchNewSession": "새 세션",
"LaunchRecentHeading": "최근",
"LaunchRecentResumeHint": "↑ 눌러 재개",
"LaunchHelpLine": "명령은 /help · 작업 바는 {dock}",
"LaunchSeeAllSessions": "모든 세션 보기…",
"LaunchNoRecentSessions": "최근 세션이 아직 없습니다 — 아래에 입력하여 시작하세요.",
+3 -2
View File
@@ -1321,12 +1321,12 @@
"ApprovalFieldParams": "Parâmetros:",
"ApprovalOptionApproveOnce": "Permitir uma vez",
"ApprovalOptionApproveAlways": "Permitir nesta conversa",
"ApprovalOptionAllowExactRepo": "Sempre permitir neste repositório",
"ApprovalOptionAllowExactRepo": "Sempre para este comando",
"ApprovalSaveAskRuleHint": " · s permitir agora e sempre perguntar antes de repetir",
"ApprovalOptionDeny": "Não permitir",
"ApprovalOptionAbortTurn": "Parar este turno",
"ApprovalBlockTitle": "aprovação",
"ApprovalControlsHint": "Pg↑/↓ revisar · {details} detalhes · Esc parar",
"ApprovalControlsHint": "Pg↑/↓ revisar · {details} mais opções · Esc parar",
"ApprovalOwnerHeader": "Agente: {agent} · {role}",
"ApprovalGoToAgent": "g ir para o agente",
"UserInputActionRequired": "Ação necessária",
@@ -2458,6 +2458,7 @@
"LaunchNoticeClaude": "Claude Code detectado. Execute /import-claude para revisar o que pode ser trazido: servidores MCP, configurações seguras e permissões. Nada é aplicado sem a sua aprovação.",
"LaunchNewSession": "Nova sessão",
"LaunchRecentHeading": "Recentes",
"LaunchRecentResumeHint": "↑ para retomar",
"LaunchHelpLine": "/help para comandos · {dock} para a barra de trabalho",
"LaunchSeeAllSessions": "Ver todas as sessões…",
"LaunchNoRecentSessions": "Ainda não há sessões recentes — digite abaixo para começar.",
+3 -2
View File
@@ -1300,12 +1300,12 @@
"ApprovalFieldParams": "Параметры: ",
"ApprovalOptionApproveOnce": "Разрешить один раз",
"ApprovalOptionApproveAlways": "Разрешить в этом разговоре",
"ApprovalOptionAllowExactRepo": "Всегда разрешать в этом репозитории",
"ApprovalOptionAllowExactRepo": "Всегда для этой команды",
"ApprovalSaveAskRuleHint": " · s разрешить сейчас и всегда спрашивать в следующий раз",
"ApprovalOptionDeny": "Не разрешать",
"ApprovalOptionAbortTurn": "Остановить этот ход",
"ApprovalBlockTitle": "одобрение",
"ApprovalControlsHint": "Pg↑/↓ просмотр · {details} детали · Esc стоп",
"ApprovalControlsHint": "Pg↑/↓ просмотр · {details} другие варианты · Esc стоп",
"ApprovalOwnerHeader": "Агент: {agent} · {role}",
"ApprovalGoToAgent": "g перейти к агенту",
"UserInputActionRequired": "Требуется действие",
@@ -2458,6 +2458,7 @@
"LaunchNoticeClaude": "Обнаружен Claude Code. Выполните /import-claude, чтобы посмотреть, что можно перенести: MCP-серверы, безопасные настройки и разрешения. Ничего не применяется без вашего согласия.",
"LaunchNewSession": "Новая сессия",
"LaunchRecentHeading": "Недавние",
"LaunchRecentResumeHint": "↑ — продолжить",
"LaunchHelpLine": "/help — команды · {dock} — рабочая панель",
"LaunchSeeAllSessions": "Показать все сессии…",
"LaunchNoRecentSessions": "Пока нет недавних сессий — введите ниже, чтобы начать.",
+3 -2
View File
@@ -1300,12 +1300,12 @@
"ApprovalFieldParams": "Параметри: ",
"ApprovalOptionApproveOnce": "Дозволити один раз",
"ApprovalOptionApproveAlways": "Дозволити в цій розмові",
"ApprovalOptionAllowExactRepo": "Завжди дозволяти в цьому репозиторії",
"ApprovalOptionAllowExactRepo": "Завжди для цієї команди",
"ApprovalSaveAskRuleHint": " · s дозволити зараз і завжди питати наступного разу",
"ApprovalOptionDeny": "Не дозволяти",
"ApprovalOptionAbortTurn": "Зупинити цей хід",
"ApprovalBlockTitle": "схвалення",
"ApprovalControlsHint": "Pg↑/↓ перегляд · {details} деталі · Esc зупинити",
"ApprovalControlsHint": "Pg↑/↓ перегляд · {details} інші варіанти · Esc зупинити",
"ApprovalOwnerHeader": "Агент: {agent} · {role}",
"ApprovalGoToAgent": "g перейти до агента",
"UserInputActionRequired": "Потрібна дія",
@@ -2458,6 +2458,7 @@
"LaunchNoticeClaude": "Виявлено Claude Code. Виконайте /import-claude, щоб переглянути, що можна перенести: MCP-сервери, безпечні налаштування та дозволи. Нічого не застосовується без вашої згоди.",
"LaunchNewSession": "Нова сесія",
"LaunchRecentHeading": "Нещодавні",
"LaunchRecentResumeHint": "↑ — продовжити",
"LaunchHelpLine": "/help — команди · {dock} — робоча панель",
"LaunchSeeAllSessions": "Показати всі сесії…",
"LaunchNoRecentSessions": "Поки немає нещодавніх сесій — введіть нижче, щоб почати.",
+3 -2
View File
@@ -1321,12 +1321,12 @@
"ApprovalFieldParams": "Tham số:",
"ApprovalOptionApproveOnce": "Cho phép một lần",
"ApprovalOptionApproveAlways": "Cho phép trong cuộc trò chuyện này",
"ApprovalOptionAllowExactRepo": "Luôn cho phép trong kho mã này",
"ApprovalOptionAllowExactRepo": "Luôn cho lệnh này",
"ApprovalSaveAskRuleHint": " · s cho phép lần này và luôn hỏi trước ở lần sau",
"ApprovalOptionDeny": "Không cho phép",
"ApprovalOptionAbortTurn": "Dừng lượt này",
"ApprovalBlockTitle": "phê duyệt",
"ApprovalControlsHint": "Pg↑/↓ xem lại · {details} chi tiết · Esc dừng",
"ApprovalControlsHint": "Pg↑/↓ xem lại · {details} thêm lựa chọn · Esc dừng",
"ApprovalOwnerHeader": "Agent: {agent} · {role}",
"ApprovalGoToAgent": "g tới agent",
"UserInputActionRequired": "Cần bạn thao tác",
@@ -2458,6 +2458,7 @@
"LaunchNoticeClaude": "Đã phát hiện Claude Code. Chạy /import-claude để xem những gì có thể chuyển sang: máy chủ MCP, cài đặt an toàn và quyền. Không có gì được áp dụng nếu bạn chưa duyệt.",
"LaunchNewSession": "Phiên mới",
"LaunchRecentHeading": "Gần đây",
"LaunchRecentResumeHint": "↑ để tiếp tục",
"LaunchHelpLine": "/help để xem lệnh · {dock} để mở thanh làm việc",
"LaunchSeeAllSessions": "Xem tất cả các phiên…",
"LaunchNoRecentSessions": "Chưa có phiên gần đây — nhập bên dưới để bắt đầu.",
+3 -2
View File
@@ -1323,12 +1323,12 @@
"ApprovalFieldParams": "参数: ",
"ApprovalOptionApproveOnce": "仅允许本次",
"ApprovalOptionApproveAlways": "在本次对话中允许",
"ApprovalOptionAllowExactRepo": "在此仓库中始终允许",
"ApprovalOptionAllowExactRepo": "此命令始终允许",
"ApprovalSaveAskRuleHint": " · s 本次允许,以后每次先询问",
"ApprovalOptionDeny": "不允许",
"ApprovalOptionAbortTurn": "停止本回合",
"ApprovalBlockTitle": "审批",
"ApprovalControlsHint": "Pg↑/↓ 回看 · {details} 详情 · Esc 停止",
"ApprovalControlsHint": "Pg↑/↓ 回看 · {details} 更多选项 · Esc 停止",
"ApprovalOwnerHeader": "智能体:{agent} · {role}",
"ApprovalGoToAgent": "g 前往智能体",
"UserInputActionRequired": "需要你操作",
@@ -2458,6 +2458,7 @@
"LaunchNoticeClaude": "检测到 Claude Code。用 /import-claude 复核;每项更改都需你确认。",
"LaunchNewSession": "新建会话",
"LaunchRecentHeading": "最近",
"LaunchRecentResumeHint": "↑ 恢复",
"LaunchHelpLine": "/help 查看命令 · {dock} 打开任务面板",
"LaunchSeeAllSessions": "查看全部会话…",
"LaunchNoRecentSessions": "暂无最近会话 — 在下方输入以开始。",
+3 -2
View File
@@ -1321,12 +1321,12 @@
"ApprovalFieldParams": "參數: ",
"ApprovalOptionApproveOnce": "僅允許一次",
"ApprovalOptionApproveAlways": "在本次對話中允許",
"ApprovalOptionAllowExactRepo": "在此儲存庫中一律允許",
"ApprovalOptionAllowExactRepo": "此命令一律允許",
"ApprovalSaveAskRuleHint": " · s 本次允許,以後每次先詢問",
"ApprovalOptionDeny": "不允許",
"ApprovalOptionAbortTurn": "停止本回合",
"ApprovalBlockTitle": "核准",
"ApprovalControlsHint": "Pg↑/↓ 回看 · {details} 詳情 · Esc 停止",
"ApprovalControlsHint": "Pg↑/↓ 回看 · {details} 更多選項 · Esc 停止",
"ApprovalOwnerHeader": "代理:{agent} · {role}",
"ApprovalGoToAgent": "g 前往代理",
"UserInputActionRequired": "需要你操作",
@@ -2456,6 +2456,7 @@
"LaunchNoticeClaude": "偵測到 Claude Code。用 /import-claude 檢視;每項變更都需你確認。",
"LaunchNewSession": "新增工作階段",
"LaunchRecentHeading": "最近",
"LaunchRecentResumeHint": "↑ 恢復",
"LaunchHelpLine": "/help 檢視指令 · {dock} 開啟任務面板",
"LaunchSeeAllSessions": "查看全部工作階段…",
"LaunchNoRecentSessions": "暫無最近工作階段 — 在下方輸入以開始。",
+2
View File
@@ -1928,6 +1928,7 @@ pub enum MessageId {
LaunchNoSavedSessions,
LaunchNewSession,
LaunchRecentHeading,
LaunchRecentResumeHint,
LaunchHelpLine,
LaunchSeeAllSessions,
LaunchNoRecentSessions,
@@ -4654,6 +4655,7 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[
MessageId::LaunchNoSavedSessions,
MessageId::LaunchNewSession,
MessageId::LaunchRecentHeading,
MessageId::LaunchRecentResumeHint,
MessageId::LaunchHelpLine,
MessageId::LaunchSeeAllSessions,
MessageId::LaunchNoRecentSessions,
+21 -3
View File
@@ -126,6 +126,19 @@ impl SessionEntry {
char_prefix(&self.id, 8)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Rebranch {
pub previous_active: usize,
pub shared_prefix: usize,
}
impl Rebranch {
#[must_use]
pub fn replaced_from_root(&self) -> bool {
self.previous_active > 0 && self.shared_prefix == 0
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct SessionJournal {
#[serde(default)]
@@ -335,8 +348,8 @@ impl SessionJournal {
///
/// The existing active branch remains as evidence. We reuse its longest
/// unchanged prefix, then append the repaired suffix as a sibling branch.
pub fn rebranch_active_messages(&mut self, messages: &[Message]) {
self.rebranch_active_messages_stamped(messages, &[]);
pub fn rebranch_active_messages(&mut self, messages: &[Message]) -> Rebranch {
self.rebranch_active_messages_stamped(messages, &[])
}
/// Preserve existing entry identity and timestamps; only append the changed
@@ -345,8 +358,9 @@ impl SessionJournal {
&mut self,
messages: &[Message],
stamps: &[DateTime<Utc>],
) {
) -> Rebranch {
let active_path = self.root_to_leaf();
let previous_active = active_path.len();
let shared_prefix = active_path
.iter()
.zip(messages)
@@ -363,6 +377,10 @@ impl SessionJournal {
stamps.get(index).copied().unwrap_or_else(Utc::now),
);
}
Rebranch {
previous_active,
shared_prefix,
}
}
/// Ids a bounded save may move out of this journal, in journal order.
+3
View File
@@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- **Compact approvals keep their authority visible:** cards show three choices and default to Deny. An exact saved rule requires its scope preview on screen. Shortcuts for additional actions require opening Details; a hidden saved-rule choice never falls back to granting the session.
- **Quicker everyday navigation:** top-level help separates everyday and advanced commands, `doctor --all` expands path/backend details, and the launch screen labels how to resume recent work. Up selects the newest recent session. Whale cameos stay out of transcript text.
- **Session-window diagnostics:** the audit log records when a saved window starts a new journal root while retaining the previous branch, helping distinguish window replacement from lost durable history.
- **Usage reporting counts models, tokens and tools:** when usage reporting is on, each session now also reports, per provider and model, its turns by outcome, input, output and prompt-cache tokens, and turn-time buckets, plus how many times each built-in tool ran and failed. A model is named only when it is in Codewhale's bundled public model catalog; custom, local and fine-tuned names are sent as `other`, and MCP and plugin tools as `mcp` or `other`. No prompts, outputs, tool arguments or paths are sent. Runs under CI or the GitHub Action report the `ci` session source. The first-run notice is shown once more to describe this. `DO_NOT_TRACK=1` now turns usage reporting off for that run, like `CODEWHALE_TELEMETRY=0`; `codewhale config set telemetry false` still turns it off for good. See `docs/TELEMETRY.md`.
- **`exec` and `doctor` follow the TUI onto local Ollama:** with no cloud key and no explicit route, `codewhale exec` uses a running local Ollama chat model and says so, and `codewhale doctor --probe-local` checks that route. Nothing is saved. (#7115)
- **Thinking starts at auto:** new configurations and the built-in default use `reasoning_effort = "auto"` instead of `max`; an effort you set is never changed.
+18 -3
View File
@@ -1311,6 +1311,9 @@ struct DoctorArgs {
conflicts_with_all = ["json", "context_json", "fix", "installs", "repair_sessions"]
)]
restore: Option<String>,
/// Show every resolved path, the search provider and secret-backend details
#[arg(long, default_value_t = false, conflicts_with_all = ["json", "context_json"])]
all: bool,
}
#[derive(Args, Debug, Clone)]
@@ -2611,6 +2614,7 @@ async fn run_async_main_dispatch(
effective_config_profile(&cli).as_deref(),
probes,
plugin_registry.as_ref(),
args.all,
)
.await;
if args.fix {
@@ -5018,6 +5022,7 @@ async fn run_doctor(
profile: Option<&str>,
probes: crate::doctor::DoctorProbeRequest,
plugins: &codewhale_plugins::PluginRegistry,
all: bool,
) {
use codewhale_palette as palette;
use colored::Colorize;
@@ -5184,20 +5189,30 @@ async fn run_doctor(
" workspace: {}",
codewhale_util::utils::display_path(workspace)
);
println!(" {}", doctor_search_provider_line(config));
let search_provider_line = doctor_search_provider_line(config);
if all || search_provider_line.contains("missing TAVILY_API_KEY") {
println!(" {search_provider_line}");
}
println!();
println!("{}", "Resolved User Paths (read-only):".bold());
for (label, path) in doctor_paths.entries() {
let entries = doctor_paths.entries();
let shown = if all { entries.len() } else { 5 };
for (label, path) in entries.iter().take(shown) {
println!(" · {label}: {}", codewhale_util::utils::display_path(path));
}
let secret_backend = codewhale_secrets::diagnose_secret_backend();
println!();
println!("{}", "Secret Backend (structural only):".bold());
for line in crate::doctor::secret_backend_human_lines(&secret_backend) {
let backend_lines = crate::doctor::secret_backend_human_lines(&secret_backend);
let shown = if all { backend_lines.len() } else { 1 };
for line in backend_lines.iter().take(shown) {
println!(" · {line}");
}
if !all {
println!(" · `codewhale doctor --all` shows every path and backend detail");
}
println!();
println!("{}", "Sessions:".bold());
+11 -5
View File
@@ -4,7 +4,9 @@
use super::pet_sim::{ChannelId, PetSim, PetState};
use super::pet_widget::PetWidget;
use super::{AmbientActivity, AmbientFrameStats, WhaleCameo, is_open_water, ocean};
use super::{
AmbientActivity, AmbientFrameStats, WhaleCameo, is_open_water, occupied_text_bounds, ocean,
};
use ratatui::{buffer::Buffer, layout::Rect, style::Color, text::Line, widgets::Widget};
use std::sync::LazyLock;
@@ -69,6 +71,9 @@ pub(super) fn paint(
if presence <= 0.0 {
return;
}
let transcript_has_text = lines
.iter()
.any(|line| occupied_text_bounds(line).is_some());
let pod = activity == AmbientActivity::Subagents;
let tape = if pod { &*POD } else { &*SOLO };
let slots: &[i32] = if pod { &[-1, 0, 1] } else { &[0] };
@@ -102,10 +107,11 @@ pub(super) fn paint(
.enumerate()
.filter(|(_, c)| c.symbol() != " ")
};
// Atomically withhold body and label. A word touching one fluke must
// never leave a severed animal or an unsupported label in the water.
if marks()
.any(|(i, _)| !is_open_water(lines, x + i as u16 % WIDTH, y + i as u16 / WIDTH, 1))
// Atomically withhold body and label, and keep the cameo out of the
// transcript column entirely while any transcript text is on screen.
if transcript_has_text
|| marks()
.any(|(i, _)| !is_open_water(lines, x + i as u16 % WIDTH, y + i as u16 / WIDTH, 1))
{
stats.marks_skipped_text += frame.marks;
continue;
+73 -43
View File
@@ -1188,8 +1188,6 @@ fn test_approval_view_navigation() {
let mut view = ApprovalView::new(benign_request());
assert_eq!(view.current_option(), ApprovalOption::Deny);
view.select_next();
assert_eq!(view.current_option(), ApprovalOption::Abort);
view.select_next();
assert_eq!(view.current_option(), ApprovalOption::ApproveOnce);
view.select_next();
@@ -1229,6 +1227,7 @@ fn benign_y_one_step_approves() {
fn save_ask_rule_shortcut_approves_once_with_rule() {
let mut view = ApprovalView::new(shell_request());
render_lines(&view, 120, 40);
view.handle_key(KeyEvent::new(KeyCode::Char('v'), KeyModifiers::ALT));
let action = view.handle_key(create_key_event(KeyCode::Char('s')));
let ViewAction::EmitAndClose(ViewEvent::ApprovalDecision {
@@ -1253,6 +1252,7 @@ fn save_file_ask_rule_shortcut_emits_file_rule() {
// workspace-relative file rule for persistence.
let mut view = ApprovalView::new(destructive_request());
render_lines(&view, 120, 40);
view.handle_key(KeyEvent::new(KeyCode::Char('v'), KeyModifiers::ALT));
let action = view.handle_key(create_key_event(KeyCode::Char('S')));
let ViewAction::EmitAndClose(ViewEvent::ApprovalDecision {
@@ -1296,13 +1296,20 @@ fn persistent_allow_option_approves_once_with_exact_repo_rule() {
);
}
/// The save offers work only while the card shows what the rule covers:
/// The `[2 / p]` save works only while the card shows what the rule covers:
/// never before the first paint, never on a band too small for the save
/// preview, never while collapsed to its banner.
/// preview, never while collapsed to its banner. `s` works only once the
/// details pager has listed what it saves.
#[test]
fn save_shortcuts_fail_closed_while_the_save_preview_is_off_screen() {
let saves = |view: &mut ApprovalView| {
[KeyCode::Char('p'), KeyCode::Char('s')].map(|code| {
[
KeyCode::Char('p'),
KeyCode::Char('s'),
KeyCode::Char('a'),
KeyCode::Char('2'),
]
.map(|code| {
matches!(
view.clone().handle_key(create_key_event(code)),
ViewAction::EmitAndClose(ViewEvent::ApprovalDecision { .. })
@@ -1310,20 +1317,42 @@ fn save_shortcuts_fail_closed_while_the_save_preview_is_off_screen() {
})
};
let mut view = ApprovalView::new(shell_request());
assert_eq!(saves(&mut view), [false, false], "before the first paint");
assert_eq!(
saves(&mut view),
[false, false, false, false],
"before the first paint"
);
render_lines(&view, 120, 40);
assert_eq!(saves(&mut view), [true, true], "preview on screen");
assert_eq!(
saves(&mut view),
[true, false, false, true],
"preview on screen"
);
view.handle_key(KeyEvent::new(KeyCode::Char('v'), KeyModifiers::ALT));
assert_eq!(
saves(&mut view),
[true, true, true, true],
"details pager opened"
);
let lines = render_lines(&view, 40, 9).join("\n");
let lines = render_lines(&view, 40, 7).join("\n");
assert!(!lines.contains("Save:"), "{lines}");
assert!(!lines.contains("[p]"), "{lines}");
assert_eq!(saves(&mut view), [false, false], "band too small: {lines}");
assert!(!lines.contains("[2 / p]"), "{lines}");
assert_eq!(
saves(&mut view),
[false, true, true, false],
"band too small: {lines}"
);
render_lines(&view, 120, 40);
view.handle_key(create_key_event(KeyCode::Tab));
render_lines(&view, 120, 40);
assert_eq!(saves(&mut view), [false, false], "collapsed banner");
assert_eq!(
saves(&mut view),
[false, true, true, false],
"collapsed banner"
);
}
/// With the save offer hidden, arrow keys skip its row and a stale selection
@@ -1335,13 +1364,13 @@ fn hidden_save_offer_is_skipped_by_navigation_and_enter() {
view.select_prev();
assert_eq!(view.current_option(), ApprovalOption::AllowExactRepo);
render_lines(&view, 40, 9);
render_lines(&view, 40, 7);
assert!(matches!(
view.handle_key(create_key_event(KeyCode::Enter)),
ViewAction::None
));
view.select_prev();
assert_eq!(view.current_option(), ApprovalOption::ApproveAlways);
assert_eq!(view.current_option(), ApprovalOption::ApproveOnce);
view.select_next();
assert_eq!(view.current_option(), ApprovalOption::Deny);
}
@@ -1434,19 +1463,17 @@ fn mouse_click_renders_and_approves_inline_option() {
/// A tiny frame keeps every one-off action and its hitbox. Where it has no
/// room for the save preview (most locales at 40x12) it withholds the
/// persistent save (`[p]`, index 2): its hitbox is empty and nothing clicks
/// it. Where it offers the save, the preview is on screen.
/// persistent save (`[2 / p]`, index 1): its hitbox is empty and nothing
/// clicks it. Where it offers the save, the preview is on screen.
#[test]
fn tiny_localized_approval_keeps_every_action_and_hitbox() {
const WIDTH: u16 = 40;
const HEIGHT: u16 = 12;
const PERSISTENT: usize = 2;
const PERSISTENT: usize = 1;
let expected = [
ReviewDecision::Approved,
ReviewDecision::ApprovedForSession,
ReviewDecision::Approved,
ReviewDecision::Denied,
ReviewDecision::Abort,
];
for &locale in Locale::shipped() {
@@ -1492,7 +1519,7 @@ fn tiny_localized_approval_keeps_every_action_and_hitbox() {
view.handle_key(create_key_event(KeyCode::Char('p'))),
ViewAction::None
),
"{locale:?}: [p] without its save preview"
"{locale:?}: [2 / p] without its save preview"
);
continue;
}
@@ -1572,15 +1599,16 @@ fn benign_esc_aborts() {
fn test_approval_view_enter_uses_selected_option() {
let mut view = ApprovalView::new(benign_request());
// The semantic default is Deny; navigate once to Abort and commit it.
// The semantic default is Deny; navigate once (wrapping) to Allow once
// and commit it.
view.select_next();
assert_eq!(view.current_option(), ApprovalOption::Abort);
assert_eq!(view.current_option(), ApprovalOption::ApproveOnce);
let action = view.handle_key(create_key_event(KeyCode::Enter));
assert!(matches!(
action,
ViewAction::EmitAndClose(ViewEvent::ApprovalDecision {
decision: ReviewDecision::Abort,
decision: ReviewDecision::Approved,
..
})
));
@@ -1597,13 +1625,13 @@ fn test_approval_view_navigation_keys() {
assert_eq!(view.current_option(), ApprovalOption::Deny);
view.handle_key(create_key_event(KeyCode::Down));
assert_eq!(view.current_option(), ApprovalOption::Abort);
view.handle_key(create_key_event(KeyCode::Char('j')));
assert_eq!(view.current_option(), ApprovalOption::ApproveOnce);
view.handle_key(create_key_event(KeyCode::Char('j')));
assert_eq!(view.current_option(), ApprovalOption::ApproveAlways);
view.handle_key(create_key_event(KeyCode::Char('k')));
assert_eq!(view.current_option(), ApprovalOption::Abort);
assert_eq!(view.current_option(), ApprovalOption::ApproveOnce);
}
#[test]
@@ -1896,16 +1924,16 @@ fn destructive_enter_denies_by_default() {
}
#[test]
fn destructive_navigation_then_enter_commits_highlighted_abort() {
fn destructive_navigation_then_enter_commits_highlighted_option() {
let mut view = ApprovalView::new(destructive_request());
view.handle_key(create_key_event(KeyCode::Down));
assert_eq!(view.current_option(), ApprovalOption::Abort);
assert_eq!(view.current_option(), ApprovalOption::ApproveOnce);
let action = view.handle_key(create_key_event(KeyCode::Enter));
assert!(matches!(
action,
ViewAction::EmitAndClose(ViewEvent::ApprovalDecision {
decision: ReviewDecision::Abort,
decision: ReviewDecision::Approved,
..
})
));
@@ -1920,10 +1948,15 @@ fn destructive_unrelated_key_keeps_modal_open() {
}
#[test]
fn destructive_a_first_press_approves_for_session() {
fn destructive_a_requires_details_before_approving_for_session() {
for code in [KeyCode::Char('a'), KeyCode::Char('A')] {
let mut view = ApprovalView::new(destructive_request());
assert!(matches!(
view.handle_key(create_key_event(code)),
ViewAction::None
));
view.handle_key(KeyEvent::new(KeyCode::Char('v'), KeyModifiers::ALT));
let action = view.handle_key(create_key_event(code));
assert!(
matches!(
@@ -1998,7 +2031,7 @@ fn compact_rendered_text(lines: &[String]) -> String {
}
fn assert_approval_key_badges_visible(joined: &str) {
for badge in ["[1 / y]", "[2 / a]", "[3 / d / n]", "[Esc]"] {
for badge in ["[1 / y]", "[3 / d / n]", "Esc"] {
assert!(
joined.contains(badge),
"missing key badge {badge}:\n{joined}"
@@ -2303,7 +2336,7 @@ fn render_elevated_zh_hans_is_calm_and_localized() {
}
#[test]
fn approval_review_and_save_hints_stay_on_one_row_at_80_columns() {
fn approval_review_and_stop_hints_stay_on_one_row_at_80_columns() {
for &locale in Locale::shipped() {
let view = ApprovalView::new_for_locale(destructive_request(), locale);
let lines = render_lines(&view, 80, 40);
@@ -2320,8 +2353,8 @@ fn approval_review_and_save_hints_stay_on_one_row_at_80_columns() {
);
let controls = review_rows[0];
assert!(
controls.contains("Esc") && controls.contains(" s "),
"review, abort, and save-rule hints wrapped for {locale:?}:\n{}",
controls.contains("Esc"),
"review and stop hints wrapped for {locale:?}:\n{}",
lines.join("\n")
);
}
@@ -3150,7 +3183,6 @@ fn delegated_work_cards_show_requested_authority_fields() {
}
fn native_band_expected_decisions(request: &ApprovalRequest) -> Vec<ReviewDecision> {
let mut decisions = vec![ReviewDecision::Approved, ReviewDecision::ApprovedForSession];
if request.tool_name == "workflow" {
return vec![
ReviewDecision::Approved,
@@ -3158,14 +3190,12 @@ fn native_band_expected_decisions(request: &ApprovalRequest) -> Vec<ReviewDecisi
ReviewDecision::Abort,
];
}
if request.owner.is_none() && request.can_save_allow_rule() {
decisions.push(ReviewDecision::Approved);
}
decisions.push(ReviewDecision::Denied);
if request.owner.is_none() {
decisions.push(ReviewDecision::Abort);
}
decisions
let second = if request.owner.is_none() && request.can_save_allow_rule() {
ReviewDecision::Approved
} else {
ReviewDecision::ApprovedForSession
};
vec![ReviewDecision::Approved, second, ReviewDecision::Denied]
}
#[test]
+54 -26
View File
@@ -35,26 +35,15 @@ pub enum ApprovalOption {
}
impl ApprovalOption {
const ORDER: [ApprovalOption; 4] = [
const ORDER: [ApprovalOption; 3] = [
ApprovalOption::ApproveOnce,
ApprovalOption::ApproveAlways,
ApprovalOption::Deny,
ApprovalOption::Abort,
];
const ORDER_WITH_PERSISTENT_ALLOW: [ApprovalOption; 5] = [
const ORDER_WITH_PERSISTENT_ALLOW: [ApprovalOption; 3] = [
ApprovalOption::ApproveOnce,
ApprovalOption::ApproveAlways,
ApprovalOption::AllowExactRepo,
ApprovalOption::Deny,
ApprovalOption::Abort,
];
/// A child agent's card (approvals C1): the person can hide it, never
/// stop the parent's turn from it, so "Stop this turn" is not offered.
const CHILD_ORDER: [ApprovalOption; 3] = [
ApprovalOption::ApproveOnce,
ApprovalOption::ApproveAlways,
ApprovalOption::Deny,
];
/// Workflow elevated-plan card (#4126): Approve / Edit plan / Cancel.
@@ -67,9 +56,7 @@ impl ApprovalOption {
fn order_for(request: &ApprovalRequest) -> &'static [ApprovalOption] {
if request.tool_name == "workflow" {
&Self::WORKFLOW_ORDER
} else if request.owner.is_some() {
&Self::CHILD_ORDER
} else if request.can_save_allow_rule() {
} else if request.owner.is_none() && request.can_save_allow_rule() {
&Self::ORDER_WITH_PERSISTENT_ALLOW
} else {
&Self::ORDER
@@ -109,10 +96,11 @@ pub struct ApprovalView {
pub(super) selected: usize,
pub(super) row_hitboxes: RefCell<Vec<Rect>>,
/// Whether the last paint showed the persistent-rule save preview. The
/// save offers (`[p]` and `s`) only work while it is on screen, so a
/// save offer (`[2 / p]`) only works while it is on screen, so a
/// person never saves a rule without seeing what it covers. Starts
/// false: nothing is offered before the card has been painted.
save_preview_shown: Cell<bool>,
details_shown: Cell<bool>,
locale: Locale,
pub(super) timeout: Option<Duration>,
pub(super) requested_at: Instant,
@@ -151,6 +139,7 @@ impl ApprovalView {
selected,
row_hitboxes: RefCell::new(Vec::new()),
save_preview_shown: Cell::new(false),
details_shown: Cell::new(false),
locale,
timeout: None,
requested_at: Instant::now(),
@@ -189,10 +178,17 @@ impl ApprovalView {
self.selected = selected;
}
/// Whether the card currently offers `option`. Saving a persistent allow
/// rule is offered only while its save preview is on screen.
/// Whether the card currently offers `option`. Persistent rules require
/// their painted preview; hidden session grants require opening details.
pub(crate) fn offers(&self, option: ApprovalOption) -> bool {
option != ApprovalOption::AllowExactRepo || self.save_preview_shown.get()
match option {
ApprovalOption::AllowExactRepo => self.save_preview_shown.get(),
ApprovalOption::ApproveAlways => {
ApprovalOption::order_for(&self.request).contains(&option)
|| self.details_shown.get()
}
_ => true,
}
}
/// Record whether the paint that just ran showed the save preview.
@@ -309,10 +305,40 @@ impl ApprovalView {
}
content.push('\n');
}
if !self.is_workflow_plan_approval() {
content.push_str(&format!(
"[a] {}\n",
tr(locale, MessageId::ApprovalOptionApproveAlways)
));
}
if let Some(preview) = self.request.ask_rule_save_preview() {
content.push_str(&format!(
"[s] {}\n Save: {} · {}{}\n",
tr(locale, MessageId::ApprovalSaveAskRuleHint)
.trim_start_matches(|c: char| c.is_whitespace() || c == '·')
.trim_start_matches("s ")
.trim(),
preview.summary(),
preview.entries.join(" · "),
if preview.omitted > 0 {
format!(" +{} more", preview.omitted)
} else {
String::new()
}
));
}
if self.request.owner.is_none() {
content.push_str(&format!(
"[Esc] {}\n",
tr(locale, MessageId::ApprovalOptionAbortTurn)
));
}
content.push('\n');
content.push_str(
&serde_json::to_string_pretty(&self.request.params)
.unwrap_or_else(|_| self.request.params.to_string()),
);
self.details_shown.set(true);
ViewAction::Emit(ViewEvent::OpenTextPager {
title: format!("Tool Params: {}", self.request.tool_name),
content,
@@ -371,16 +397,18 @@ impl ModalView for ApprovalView {
KeyCode::Char('y') | KeyCode::Char('Y') | KeyCode::Char('1') => {
self.commit_option(ApprovalOption::ApproveOnce)
}
KeyCode::Char('p') | KeyCode::Char('P') | KeyCode::Char('2')
if !self.is_workflow_plan_approval()
&& ApprovalOption::order_for(&self.request)
.contains(&ApprovalOption::AllowExactRepo) =>
{
self.commit_option(ApprovalOption::AllowExactRepo)
}
KeyCode::Char('a') | KeyCode::Char('A') | KeyCode::Char('2')
if !self.is_workflow_plan_approval() =>
{
self.commit_option(ApprovalOption::ApproveAlways)
}
KeyCode::Char('p') | KeyCode::Char('P')
if self.request.can_save_allow_rule() && self.save_preview_shown.get() =>
{
self.commit_option(ApprovalOption::AllowExactRepo)
}
// Workflow plan card (#4126): [2/e] Edit plan, [3/n/d] Cancel.
KeyCode::Char('e') | KeyCode::Char('E') | KeyCode::Char('2')
if self.is_workflow_plan_approval() =>
@@ -388,7 +416,7 @@ impl ModalView for ApprovalView {
self.commit_option(ApprovalOption::Deny)
}
KeyCode::Char('s') | KeyCode::Char('S')
if self.request.can_save_ask_rule() && self.save_preview_shown.get() =>
if self.request.can_save_ask_rule() && self.details_shown.get() =>
{
self.emit_decision_with_rules(
ReviewDecision::Approved,
+8 -3
View File
@@ -6242,10 +6242,15 @@ pub(crate) async fn run_event_loop(
// the plan shed on a tiny stage).
let rows = crate::tui::underwater::launch_rows_for_app(app);
let entries = rows.len().max(1) as i32;
// First arrow lands on the first (Up: last)
// row; from there it moves.
// First arrow lands on the first row; Up lands on
// the newest recent session, else the last row.
app.launch.menu_selected = Some(match app.launch.menu_selected {
None if delta < 0 => (entries - 1) as usize,
None if delta < 0 => rows
.iter()
.position(|row| {
matches!(row.id, crate::tui::app::LaunchRowId::Recent(_))
})
.unwrap_or((entries - 1) as usize),
None => 0,
Some(current) => {
(current as i32 + delta).rem_euclid(entries) as usize
+30 -1
View File
@@ -1218,6 +1218,26 @@ pub(crate) fn build_app_system_prompt_with_goal(
.0
}
fn record_window_reroot(
session_id: Option<&str>,
previous_window: usize,
window: usize,
journal_entries: usize,
) {
let details = serde_json::json!({
"session_id": session_id,
"reason": "first_message_changed",
"previous_window": previous_window,
"window": window,
"journal_entries": journal_entries,
});
if let Ok(handle) = tokio::runtime::Handle::try_current() {
handle.spawn_blocking(move || {
codewhale_runtime::audit::log_sensitive_event("session.window.rerooted", details);
});
}
}
/// Build the session snapshot every product caller queues into the
/// persistence actor. Journal-only (#6214 T3): the `messages` projection is
/// left empty because the queue drops it anyway, and serialization rehydrates
@@ -1242,8 +1262,17 @@ pub(crate) fn build_session_snapshot(
tracing::warn!(%error, "kept archived journal entries in memory");
}
}
app.session_journal
let rebranch = app
.session_journal
.rebranch_active_messages_stamped(&app.api_messages, &app.api_message_stamps);
if rebranch.replaced_from_root() && !app.api_messages.is_empty() {
record_window_reroot(
app.current_session_id.as_deref(),
rebranch.previous_active,
app.api_messages.len(),
app.session_journal.entries.len(),
);
}
let mut session = crate::session_manager::create_saved_session_journal_only(
app.current_session_id
.clone()
+1 -1
View File
@@ -548,7 +548,7 @@ pub(crate) fn push_approval_request_view(
request.approval_grouping_key = approval_grouping_key.to_string();
}
// A child's gate never consults saved repo allow rules, so a child card
// must not offer "Always allow in this repo" — it would do nothing. The
// must not offer "Always for this command" — it would do nothing. The
// card names its agent (approvals C1).
if crate::tools::subagent::SubAgentManager::is_child_approval_id(id) {
request.persistent_allow_rules.clear();
+1 -1
View File
@@ -8686,7 +8686,7 @@ fn child_approval_card_hides_always_allow_in_repo() {
assert_eq!(
matches!(action, ViewAction::EmitAndClose(_)),
expect_repo_rule,
"only the parent card offers Always allow in this repo"
"only the parent card offers Always for this command"
);
}
}
+13 -1
View File
@@ -1886,11 +1886,23 @@ pub fn launch_empty_state(app: &App, area: Rect) -> LaunchEmptyState {
}
}
let label = semantic_truncate(&tr(locale, MessageId::LaunchRecentHeading), text_width);
let remaining = text_width.saturating_sub(text_display_width(&label) + 2);
let mut remaining = text_width.saturating_sub(text_display_width(&label) + 2);
let mut spans = vec![Span::styled(
label,
Style::default().fg(theme.text_soft).bold(),
)];
let resume = tr(locale, MessageId::LaunchRecentResumeHint);
if card_rows
.iter()
.any(|row| matches!(row.id, crate::tui::app::LaunchRowId::Recent(_)))
&& remaining >= text_display_width(&resume) + 6
{
remaining = remaining.saturating_sub(text_display_width(&resume) + 2);
spans.push(Span::styled(
format!(" {resume}"),
Style::default().fg(theme.text_hint),
));
}
if remaining >= 4 {
let rule = if crate::tui::color_compat::ascii_safe_enabled() {
"-"
@@ -30,7 +30,7 @@ impl<'a> ApprovalWidget<'a> {
/// the preview above the controls, so a short band cuts the request
/// detail, never the preview. A frame too small (or too narrow) for even
/// the one-line preview fails closed: the card drops the preview and the
/// save offers together (`[p]`, `s`), keeping only one-off decisions.
/// save offer together (`[2 / p]`), keeping only one-off decisions.
fn build_inline_content(&self, area: Rect) -> InlineContent {
let (compact, save_start, controls) = self.build_inline_parts(area, true, true);
let save_reserve = measure_wrapped_rows(&compact[save_start..], area.width);
@@ -307,15 +307,6 @@ impl<'a> ApprovalWidget<'a> {
// Preview the validated persistent-rule candidates. Informational, so
// they live in the scrollable body rather than the action rows.
let essential_len = body.len();
if let Some(preview) = self.request.ask_rule_save_preview().filter(|_| offer_save) {
push_permission_rule_save_preview(
&mut body,
&preview,
palette_colors.shortcut,
area.width,
compact_save_preview,
);
}
if let Some(preview) = self
.request
.allow_rule_save_preview()
@@ -742,11 +733,6 @@ fn build_approval_controls(
},
Style::default().fg(palette::TEXT_MUTED),
),
if offer_save && request.can_save_ask_rule() {
Span::styled(save_ask_rule_hint(locale), Style::default().fg(shortcut))
} else {
Span::raw("")
},
]));
controls
}
@@ -1239,10 +1225,6 @@ fn approval_owner_header(owner: &crate::tui::approval::ApprovalOwner, locale: Lo
}
}
fn save_ask_rule_hint(locale: Locale) -> Cow<'static, str> {
tr(locale, MessageId::ApprovalSaveAskRuleHint)
}
#[derive(Clone)]
struct ApprovalOptionRow {
label: Cow<'static, str>,
@@ -1252,7 +1234,7 @@ struct ApprovalOptionRow {
persistent: bool,
}
fn approval_options_for(risk: RiskLevel, locale: Locale) -> [ApprovalOptionRow; 4] {
fn approval_options_for(risk: RiskLevel, locale: Locale) -> [ApprovalOptionRow; 3] {
let dangerous = matches!(risk, RiskLevel::Destructive);
[
ApprovalOptionRow {
@@ -1273,12 +1255,6 @@ fn approval_options_for(risk: RiskLevel, locale: Locale) -> [ApprovalOptionRow;
dangerous: false,
persistent: false,
},
ApprovalOptionRow {
label: option_abort(locale),
key_hint: "Esc",
dangerous: false,
persistent: false,
},
]
}
@@ -1316,21 +1292,13 @@ fn approval_options_for_request(
workflow_approval_options(risk, locale).to_vec()
} else {
let mut options = approval_options_for(risk, locale).to_vec();
if request.owner.is_some() {
// Must match `ApprovalOption::CHILD_ORDER`: no "Stop this turn".
options.pop();
return options;
}
if request.can_save_allow_rule() {
options.insert(
2,
ApprovalOptionRow {
label: tr(locale, MessageId::ApprovalOptionAllowExactRepo),
key_hint: "p",
dangerous: false,
persistent: true,
},
);
if request.owner.is_none() && request.can_save_allow_rule() {
options[1] = ApprovalOptionRow {
label: tr(locale, MessageId::ApprovalOptionAllowExactRepo),
key_hint: "2 / p",
dangerous: false,
persistent: true,
};
}
options
}
@@ -1369,8 +1337,4 @@ fn option_deny(locale: Locale) -> Cow<'static, str> {
tr(locale, MessageId::ApprovalOptionDeny)
}
fn option_abort(locale: Locale) -> Cow<'static, str> {
tr(locale, MessageId::ApprovalOptionAbortTurn)
}
// End exact frozen source counterpart; never included in production.
+68 -83
View File
@@ -1533,28 +1533,25 @@ impl<'a> ApprovalWidget<'a> {
let saves = if self.view.collapsed {
Vec::new()
} else {
[
self.request.ask_rule_save_preview(),
self.request.allow_rule_save_preview(),
]
.into_iter()
.flatten()
.map(|preview| DecisionBandSave {
summary: preview.summary(),
entries: preview.entries,
omitted: preview.omitted,
label: "Save: ".into(),
separator: " · ".into(),
compact_more: " +{count} more".into(),
full_more: "... {count} more".into(),
label_style: Style::default()
.fg(colors.shortcut)
.add_modifier(Modifier::BOLD),
summary_style: Style::default().fg(palette::TEXT_BODY),
entries_style: Style::default().fg(palette::TEXT_SECONDARY),
more_style: Style::default().fg(palette::TEXT_HINT),
})
.collect()
self.request
.allow_rule_save_preview()
.into_iter()
.map(|preview| DecisionBandSave {
summary: preview.summary(),
entries: preview.entries,
omitted: preview.omitted,
label: "Save: ".into(),
separator: " · ".into(),
compact_more: " +{count} more".into(),
full_more: "... {count} more".into(),
label_style: Style::default()
.fg(colors.shortcut)
.add_modifier(Modifier::BOLD),
summary_style: Style::default().fg(palette::TEXT_BODY),
entries_style: Style::default().fg(palette::TEXT_SECONDARY),
more_style: Style::default().fg(palette::TEXT_HINT),
})
.collect()
};
DecisionBand {
body: if self.view.collapsed {
@@ -1934,10 +1931,7 @@ fn approval_control_facts(
Style::default().fg(palette::TEXT_MUTED),
),
]);
let save_hint = request
.can_save_ask_rule()
.then(|| Span::styled(save_ask_rule_hint(locale), Style::default().fg(shortcut)));
(question, actions, footer, save_hint)
(question, actions, footer, None)
}
/// Key name appended to the highlighted option row, like `Esc` on its own row.
const APPROVAL_ENTER_TAG: &str = " (Enter)";
@@ -2364,10 +2358,6 @@ fn approval_owner_header(owner: &crate::tui::approval::ApprovalOwner, locale: Lo
}
}
fn save_ask_rule_hint(locale: Locale) -> Cow<'static, str> {
tr(locale, MessageId::ApprovalSaveAskRuleHint)
}
#[derive(Clone)]
struct ApprovalOptionRow {
label: Cow<'static, str>,
@@ -2377,7 +2367,7 @@ struct ApprovalOptionRow {
persistent: bool,
}
fn approval_options_for(risk: RiskLevel, locale: Locale) -> [ApprovalOptionRow; 4] {
fn approval_options_for(risk: RiskLevel, locale: Locale) -> [ApprovalOptionRow; 3] {
let dangerous = matches!(risk, RiskLevel::Destructive);
[
ApprovalOptionRow {
@@ -2398,12 +2388,6 @@ fn approval_options_for(risk: RiskLevel, locale: Locale) -> [ApprovalOptionRow;
dangerous: false,
persistent: false,
},
ApprovalOptionRow {
label: option_abort(locale),
key_hint: "Esc",
dangerous: false,
persistent: false,
},
]
}
@@ -2441,21 +2425,13 @@ fn approval_options_for_request(
workflow_approval_options(risk, locale).to_vec()
} else {
let mut options = approval_options_for(risk, locale).to_vec();
if request.owner.is_some() {
// Must match `ApprovalOption::CHILD_ORDER`: no "Stop this turn".
options.pop();
return options;
}
if request.can_save_allow_rule() {
options.insert(
2,
ApprovalOptionRow {
label: tr(locale, MessageId::ApprovalOptionAllowExactRepo),
key_hint: "p",
dangerous: false,
persistent: true,
},
);
if request.owner.is_none() && request.can_save_allow_rule() {
options[1] = ApprovalOptionRow {
label: tr(locale, MessageId::ApprovalOptionAllowExactRepo),
key_hint: "2 / p",
dangerous: false,
persistent: true,
};
}
options
}
@@ -2494,10 +2470,6 @@ fn option_deny(locale: Locale) -> Cow<'static, str> {
tr(locale, MessageId::ApprovalOptionDeny)
}
fn option_abort(locale: Locale) -> Cow<'static, str> {
tr(locale, MessageId::ApprovalOptionAbortTurn)
}
pub struct ElevationWidget<'a> {
request: &'a ElevationRequest,
selected: usize,
@@ -8132,7 +8104,7 @@ mod tests {
widget.render(area, &mut buf);
let rendered = buffer_text(&buf, area);
assert!(rendered.contains("[1 / y]"), "{rendered}");
assert!(rendered.contains("[Esc]"), "{rendered}");
assert!(rendered.contains("[3 / d / n]"), "{rendered}");
assert!(rendered.contains("truncated"), "{rendered}");
}
@@ -8161,7 +8133,7 @@ mod tests {
assert!(rendered.contains(marker), "{area:?}: {rendered}");
}
assert!(rendered.contains("[1 / y]"), "{area:?}: {rendered}");
assert!(rendered.contains("[Esc]"), "{area:?}: {rendered}");
assert!(rendered.contains("[3 / d / n]"), "{area:?}: {rendered}");
}
let tiny = Rect::new(0, 0, 40, 12);
@@ -8169,7 +8141,7 @@ mod tests {
widget.render(tiny, &mut buf);
let rendered = buffer_text(&buf, tiny);
assert!(rendered.contains("[1 / y]"), "{rendered}");
assert!(rendered.contains("[Esc]"), "{rendered}");
assert!(rendered.contains("[3 / d / n]"), "{rendered}");
assert!(
rendered.contains(crate::tui::shell_key_routing::tool_details_chord().as_ref()),
"{rendered}"
@@ -8344,14 +8316,16 @@ mod tests {
// #3766: option 2 / `a` maps to ReviewDecision::ApprovedForSession, so
// neither the full option rows nor the compact controls may tell the
// user that particular option is "always"/permanent. The distinct
// `[p]` row may use that word for an exact repo-scoped grant.
let request = crate::tui::approval::ApprovalRequest::new(
// `[2 / p]` row offered with a persistent rule may use that word for
// an exact repo-scoped grant.
let mut request = crate::tui::approval::ApprovalRequest::new(
"approval-1",
"exec_shell",
"Run git commit",
&serde_json::json!({ "command": "git commit -m fix" }),
"exec_shell:git commit",
);
request.persistent_allow_rules.clear();
// Full card (tall): full option rows render the session-scoped label.
let full = render_approval_request(&request, Rect::new(0, 0, 100, 30));
@@ -8427,20 +8401,39 @@ mod tests {
let rendered = render_approval_request(&request, Rect::new(0, 0, 120, 40));
assert!(
rendered.contains("s allow now, and always ask before this again"),
!rendered.contains("s allow now, and always ask before this again"),
"{rendered}"
);
assert!(rendered.contains("Always allow in this repo"), "{rendered}");
assert!(rendered.contains("Always for this command"), "{rendered}");
assert!(rendered.contains("Save:"), "{rendered}");
assert!(rendered.contains("always ask first"), "{rendered}");
assert!(!rendered.contains("always ask first"), "{rendered}");
assert!(rendered.contains("always allow"), "{rendered}");
assert!(
rendered.contains("run cargo test --workspace"),
"{rendered}"
);
assert!(rendered.contains("run exactly cargo test"), "{rendered}");
assert!(rendered.contains("in /workspace"), "{rendered}");
assert!(!rendered.contains("tool="), "{rendered}");
let mut view = crate::tui::approval::ApprovalView::new(request);
let crate::tui::views::ViewAction::Emit(crate::tui::views::ViewEvent::OpenTextPager {
content,
..
}) = crate::tui::views::ModalView::handle_key(
&mut view,
crossterm::event::KeyEvent::new(
crossterm::event::KeyCode::Char('v'),
crossterm::event::KeyModifiers::ALT,
),
)
else {
panic!("the details chord opens the pager");
};
assert!(
content.contains("[s] allow now, and always ask before this again"),
"{content}"
);
assert!(
content.contains("always ask first · run cargo test --workspace"),
"{content}"
);
}
#[test]
@@ -8478,7 +8471,7 @@ mod tests {
assert!(rendered.contains("Save:"), "{tool_name}:\n{rendered}");
assert!(
rendered.contains("always ask first"),
!rendered.contains("always ask first"),
"{tool_name}:\n{rendered}"
);
assert!(
@@ -8517,7 +8510,7 @@ diff --git a/src/b.rs b/src/b.rs\n\
let rendered = render_approval_request(&request, Rect::new(0, 0, 120, 40));
assert!(rendered.contains("Save:"), "{rendered}");
assert!(rendered.contains("always ask first"), "{rendered}");
assert!(!rendered.contains("always ask first"), "{rendered}");
assert!(rendered.contains("always allow"), "{rendered}");
assert!(rendered.contains("change src/a.rs"), "{rendered}");
assert!(rendered.contains("change src/b.rs"), "{rendered}");
@@ -8589,7 +8582,7 @@ diff --git a/src/b.rs b/src/b.rs\n\
// it drops the preview rather than calling the request truncated.
let rendered = render_approval_request(&request, Rect::new(0, 0, 120, 80));
assert!(rendered.contains("always ask first"), "{rendered}");
assert!(rendered.contains("always allow"), "{rendered}");
assert!(rendered.contains("change src/a.rs"), "{rendered}");
assert!(rendered.contains("... 1 more"), "{rendered}");
assert!(
@@ -8622,21 +8615,14 @@ diff --git a/src/b.rs b/src/b.rs\n\
let rendered = render_approval_request(&request, Rect::new(0, 0, 120, 40));
assert!(rendered.contains("src/a.rs"), "{rendered}");
assert!(
rendered.contains("always ask first · change src/a.rs"),
"{rendered}"
);
assert!(rendered.contains("+1 more"), "{rendered}");
assert!(rendered.contains("always allow"), "{rendered}");
assert!(rendered.contains("1 more"), "{rendered}");
assert!(!rendered.contains("truncated"), "{rendered}");
// Short bands cut the request detail, never the preview: what a
// saved rule covers stays pinned above the controls that save it.
for height in [10, 11, 12, 14, 16, 20] {
let rendered = render_approval_request(&request, Rect::new(0, 0, 120, height));
assert!(
rendered.contains("always ask first · change src/a.rs"),
"height {height}: {rendered}"
);
assert!(
rendered.contains("always allow · change src/a.rs"),
"height {height}: {rendered}"
@@ -8648,9 +8634,8 @@ diff --git a/src/b.rs b/src/b.rs\n\
for width in [40, 60, 120] {
for height in 6..=20 {
let rendered = render_approval_request(&request, Rect::new(0, 0, width, height));
let offers = rendered.contains("[p]") || rendered.contains("s allow now");
let previews =
rendered.contains("always ask first") && rendered.contains("always allow");
let offers = rendered.contains("[2 / p]");
let previews = rendered.contains("always allow");
assert!(
!offers || previews,
"{width}x{height} offers a save it does not preview:\n{rendered}"
+3
View File
@@ -726,6 +726,9 @@ this VM: only 0.10.0 was built)**.
### Clean up old copies
`codewhale doctor` has an **Installations** section, and
keeps routine path and secret-backend details brief. Use `codewhale doctor --all`
to show every resolved path and structural secret-backend detail.
`codewhale doctor --installs` prints only that (`--json` for machines,
`--brief` for nothing on a tidy machine). It lists every `codewhale`, `codew`,
`codewhale-tui`, `deepseek` and `deepseek-tui` in your `PATH`, `~/.local/bin`,
+2 -2
View File
@@ -49,7 +49,7 @@ owner = "crates/cli/src/update/uninstall.rs"
[[feature]]
id = "install-cleanup"
name = "Installation cleanup"
summary = "doctor --installs lists every copy and owner; doctor --fix moves old backups to recovery; doctor --restore undoes it."
summary = "doctor --all expands diagnostics; --installs lists owners, --fix recovers old copies and --restore reverses it."
status = "stable"
surfaces = { tui = "stable" }
since = "unreleased"
@@ -308,7 +308,7 @@ owner = "crates/config/src/app_mode.rs"
[[feature]]
id = "permission-postures"
name = "Permission postures"
summary = "Ask, Auto-Review and Full Access postures decide when a tool call needs approval."
summary = "Ask, Auto-Review and Full Access govern approvals; cards default to Deny and preview saved grants."
status = "stable"
surfaces = { tui = "stable", api = "stable" }
docs = "docs/MODES.md#permission-posture"
+1 -1
View File
@@ -34,7 +34,7 @@
| Repository law | 仓库保护规则 | 儲存庫保護規則 | 保留源码中的 `repo_law` 等标识符。 |
| Allow once | 仅允许本次 | 僅允許一次 | 审批选项。 |
| Allow for this conversation | 在本次对话中允许 | 在本次對話中允許 | conversation 与 session 不混用。 |
| Always allow in this repo | 在此仓库中始终允许 | 在此儲存庫中一律允許 | 不扩大到其他仓库。 |
| Always for this command | 此命令始终允许 | 此命令一律允許 | 只保存这条命令,不扩大到其他仓库。 |
| Don't allow | 不允许 | 不允許 | 拒绝是执行结果,不必作为按钮名称。 |
| Stop | 停止 | 停止 | 不把停止回合误译为删除会话。 |