mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-07 10:36:05 -10:00
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:
1 parent
1d677d976e
commit
62f0cc9536
34 files changed
+425
-249
No files matched your search
@@ -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
@@ -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!(
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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": "अभी कोई हालिया सत्र नहीं — शुरू करने के लिए नीचे लिखें।",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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": "最近のセッションはまだありません — 下に入力して開始します。",
|
||||
|
||||
@@ -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": "최근 세션이 아직 없습니다 — 아래에 입력하여 시작하세요.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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": "Пока нет недавних сессий — введите ниже, чтобы начать.",
|
||||
|
||||
@@ -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": "Поки немає нещодавніх сесій — введіть нижче, щоб почати.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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": "暂无最近会话 — 在下方输入以开始。",
|
||||
|
||||
@@ -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": "暫無最近工作階段 — 在下方輸入以開始。",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -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}"
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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 | 停止 | 停止 | 不把停止回合误译为删除会话。 |
|
||||
|
||||
|
||||
Reference in new issue
Block a user