notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

commit fac3b9245c0cb1b66f1bfd8388e9a6487459993f
parent 35cd2a2ad41f20e101d024c1fc28cd4a1f1d50d7
Author: William Casarin <jb55@jb55.com>
Date:   Thu, 26 Feb 2026 09:21:48 -0800

dave: cycle permission mode badge through Default/Plan/AcceptEdits

The PLAN badge now cycles through three states on click or Ctrl+M:
Default (inactive) → Plan (highlighted) → Auto Edit (orange) → Default.
This mirrors Claude Code's tab-cycling between plan and auto-accept.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Diffstat:
Mcrates/notedeck_dave/src/session.rs | 8++++++++
Mcrates/notedeck_dave/src/ui/dave.rs | 54+++++++++++++++++++++++++++++-------------------------
Mcrates/notedeck_dave/src/ui/keybindings.rs | 8++++----
Mcrates/notedeck_dave/src/ui/mod.rs | 12++++++------
Mcrates/notedeck_dave/src/update.rs | 11++++++-----
5 files changed, 53 insertions(+), 40 deletions(-)

diff --git a/crates/notedeck_dave/src/session.rs b/crates/notedeck_dave/src/session.rs @@ -500,6 +500,14 @@ impl ChatSession { .is_some_and(|a| a.permission_mode == PermissionMode::Plan) } + /// Get the current permission mode (defaults to Default for non-agentic) + pub fn permission_mode(&self) -> PermissionMode { + self.agentic + .as_ref() + .map(|a| a.permission_mode) + .unwrap_or(PermissionMode::Default) + } + /// Get the working directory (agentic only) pub fn cwd(&self) -> Option<&PathBuf> { self.agentic.as_ref().map(|a| &a.cwd) diff --git a/crates/notedeck_dave/src/ui/dave.rs b/crates/notedeck_dave/src/ui/dave.rs @@ -18,6 +18,7 @@ use crate::{ tools::{PresentNotesCall, ToolCall, ToolCalls, ToolResponse, ToolResponses}, }; use bitflags::bitflags; +use claude_agent_sdk_rs::PermissionMode; use egui::{Align, Key, KeyboardShortcut, Layout, Modifiers}; use nostrdb::Transaction; use notedeck::{tr, AppContext, Localization, NoteAction, NoteContext}; @@ -34,10 +35,9 @@ bitflags! { const IsWorking = 1 << 2; const InterruptPending = 1 << 3; const HasPendingPerm = 1 << 4; - const PlanModeActive = 1 << 5; - const IsCompacting = 1 << 6; - const AutoStealFocus = 1 << 7; - const IsRemote = 1 << 8; + const IsCompacting = 1 << 5; + const AutoStealFocus = 1 << 6; + const IsRemote = 1 << 7; } } @@ -72,6 +72,8 @@ pub struct DaveUi<'a> { dispatch_state: crate::session::DispatchState, /// Which backend this session uses backend_type: BackendType, + /// Current permission mode (Default, Plan, AcceptEdits) + permission_mode: PermissionMode, } /// The response the app generates. The response contains an optional @@ -149,8 +151,8 @@ pub enum DaveAction { CompactAndApprove { request_id: Uuid, }, - /// Toggle plan mode (clicked PLAN badge) - TogglePlanMode, + /// Cycle permission mode: Default → Plan → AcceptEdits (clicked mode badge) + CyclePermissionMode, /// Toggle auto-steal focus mode (clicked AUTO badge) ToggleAutoSteal, /// Trigger manual context compaction @@ -188,6 +190,7 @@ impl<'a> DaveUi<'a> { context_window: crate::messages::context_window_for_model(None), dispatch_state: crate::session::DispatchState::default(), backend_type: BackendType::Remote, + permission_mode: PermissionMode::Default, } } @@ -241,8 +244,8 @@ impl<'a> DaveUi<'a> { self } - pub fn plan_mode_active(mut self, val: bool) -> Self { - self.flags.set(DaveUiFlags::PlanModeActive, val); + pub fn permission_mode(mut self, mode: PermissionMode) -> Self { + self.permission_mode = mode; self } @@ -349,7 +352,7 @@ impl<'a> DaveUi<'a> { .inner; { - let plan_mode_active = self.flags.contains(DaveUiFlags::PlanModeActive); + let permission_mode = self.permission_mode; let auto_steal_focus = self.flags.contains(DaveUiFlags::AutoStealFocus); let is_agentic = self.ai_mode == AiMode::Agentic; let has_git = self.git_status.is_some(); @@ -377,7 +380,7 @@ impl<'a> DaveUi<'a> { status_bar_ui( self.git_status.as_deref_mut(), is_agentic, - plan_mode_active, + permission_mode, auto_steal_focus, self.usage, self.context_window, @@ -1411,7 +1414,7 @@ fn add_msg_link(ui: &mut egui::Ui, shift_held: bool, action: &mut Option<DaveAct fn status_bar_ui( mut git_status: Option<&mut GitStatusCache>, is_agentic: bool, - plan_mode_active: bool, + permission_mode: PermissionMode, auto_steal_focus: bool, usage: Option<&crate::messages::UsageInfo>, context_window: u64, @@ -1432,7 +1435,7 @@ fn status_bar_ui( // Right-aligned section: usage bar, badges, then refresh ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { let badge_action = if is_agentic { - toggle_badges_ui(ui, plan_mode_active, auto_steal_focus) + toggle_badges_ui(ui, permission_mode, auto_steal_focus) } else { None }; @@ -1445,7 +1448,7 @@ fn status_bar_ui( } else if is_agentic { // No git status (remote session) - just show badges and usage ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - let badge_action = toggle_badges_ui(ui, plan_mode_active, auto_steal_focus); + let badge_action = toggle_badges_ui(ui, permission_mode, auto_steal_focus); usage_bar_ui(usage, context_window, ui); badge_action }) @@ -1537,10 +1540,10 @@ fn usage_bar_ui( painter.rect_filled(fill_rect, 3.0, bar_color); } -/// Render clickable PLAN and AUTO toggle badges. Returns an action if clicked. +/// Render clickable permission mode and AUTO toggle badges. Returns an action if clicked. fn toggle_badges_ui( ui: &mut egui::Ui, - plan_mode_active: bool, + permission_mode: PermissionMode, auto_steal_focus: bool, ) -> Option<DaveAction> { let ctrl_held = ui.input(|i| i.modifiers.ctrl); @@ -1563,21 +1566,22 @@ fn toggle_badges_ui( action = Some(DaveAction::ToggleAutoSteal); } - // PLAN badge - let mut plan_badge = super::badge::StatusBadge::new("PLAN").variant(if plan_mode_active { - super::badge::BadgeVariant::Info - } else { - super::badge::BadgeVariant::Default - }); + // Permission mode badge: cycles Default → Plan → AcceptEdits + let (label, variant) = match permission_mode { + PermissionMode::Plan => ("PLAN", BadgeVariant::Info), + PermissionMode::AcceptEdits => ("AUTO EDIT", BadgeVariant::Warning), + _ => ("PLAN", BadgeVariant::Default), + }; + let mut mode_badge = StatusBadge::new(label).variant(variant); if ctrl_held { - plan_badge = plan_badge.keybind("M"); + mode_badge = mode_badge.keybind("M"); } - if plan_badge + if mode_badge .show(ui) - .on_hover_text("Click or Ctrl+M to toggle plan mode") + .on_hover_text("Click or Ctrl+M to cycle: Default → Plan → Auto Edit") .clicked() { - action = Some(DaveAction::TogglePlanMode); + action = Some(DaveAction::CyclePermissionMode); } // COMPACT badge diff --git a/crates/notedeck_dave/src/ui/keybindings.rs b/crates/notedeck_dave/src/ui/keybindings.rs @@ -26,8 +26,8 @@ pub enum KeyAction { Interrupt, /// Toggle between scene view and classic view ToggleView, - /// Toggle plan mode for the active session (Ctrl+M) - TogglePlanMode, + /// Cycle permission mode: Default → Plan → AcceptEdits (Ctrl+M) + CyclePermissionMode, /// Delete the active session DeleteActiveSession, /// Navigate to next item in focus queue (Ctrl+N) @@ -119,9 +119,9 @@ pub fn check_keybindings( return Some(KeyAction::OpenExternalEditor); } - // Ctrl+M to toggle plan mode - agentic only + // Ctrl+M to cycle permission mode - agentic only if is_agentic && ctx.input(|i| i.modifiers.matches_exact(ctrl) && i.key_pressed(Key::M)) { - return Some(KeyAction::TogglePlanMode); + return Some(KeyAction::CyclePermissionMode); } // Ctrl+D to toggle Done status for current focus queue item - agentic only diff --git a/crates/notedeck_dave/src/ui/mod.rs b/crates/notedeck_dave/src/ui/mod.rs @@ -48,7 +48,7 @@ fn build_dave_ui<'a>( ) -> DaveUi<'a> { let is_working = session.status() == AgentStatus::Working; let has_pending_permission = session.has_pending_permissions(); - let plan_mode_active = session.is_plan_mode(); + let permission_mode = session.permission_mode(); let is_remote = session.is_remote(); let mut ui_builder = DaveUi::new( @@ -62,7 +62,7 @@ fn build_dave_ui<'a>( .is_working(is_working) .interrupt_pending(is_interrupt_pending) .has_pending_permission(has_pending_permission) - .plan_mode_active(plan_mode_active) + .permission_mode(permission_mode) .auto_steal_focus(auto_steal_focus) .is_remote(is_remote) .dispatch_state(session.dispatch_state) @@ -642,8 +642,8 @@ pub fn handle_key_action( KeyAction::CloneAgent => KeyActionResult::CloneAgent, KeyAction::Interrupt => KeyActionResult::HandleInterrupt, KeyAction::ToggleView => KeyActionResult::ToggleView, - KeyAction::TogglePlanMode => { - update::toggle_plan_mode(session_manager, backend, ctx); + KeyAction::CyclePermissionMode => { + update::cycle_permission_mode(session_manager, backend, ctx); if let Some(session) = session_manager.get_active_mut() { session.focus_requested = true; } @@ -826,8 +826,8 @@ pub fn handle_ui_action( UiActionResult::Handled, UiActionResult::PublishPermissionResponse, ), - DaveAction::TogglePlanMode => { - update::toggle_plan_mode(session_manager, backend, ctx); + DaveAction::CyclePermissionMode => { + update::cycle_permission_mode(session_manager, backend, ctx); if let Some(session) = session_manager.get_active_mut() { session.focus_requested = true; } diff --git a/crates/notedeck_dave/src/update.rs b/crates/notedeck_dave/src/update.rs @@ -90,8 +90,8 @@ pub fn check_interrupt_timeout(pending_since: Option<Instant>) -> Option<Instant // Plan Mode // ============================================================================= -/// Toggle plan mode for the active session. -pub fn toggle_plan_mode( +/// Cycle permission mode for the active session: Default → Plan → AcceptEdits → Default. +pub fn cycle_permission_mode( session_manager: &mut SessionManager, backend: &dyn AiBackend, ctx: &egui::Context, @@ -99,8 +99,9 @@ pub fn toggle_plan_mode( if let Some(session) = session_manager.get_active_mut() { if let Some(agentic) = &mut session.agentic { let new_mode = match agentic.permission_mode { - PermissionMode::Plan => PermissionMode::Default, - _ => PermissionMode::Plan, + PermissionMode::Default => PermissionMode::Plan, + PermissionMode::Plan => PermissionMode::AcceptEdits, + _ => PermissionMode::Default, }; agentic.permission_mode = new_mode; @@ -108,7 +109,7 @@ pub fn toggle_plan_mode( backend.set_permission_mode(session_id, new_mode, ctx.clone()); tracing::debug!( - "Toggled plan mode for session {} to {:?}", + "Cycled permission mode for session {} to {:?}", session.id, new_mode );