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:
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
);