notedeck

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

commit f0162ea5587951ecb9f3d344d438620c2e036f63
parent 2be7bc400b3ad43c74e3edc83403be9253135989
Author: William Casarin <jb55@jb55.com>
Date:   Tue, 24 Feb 2026 18:51:42 -0800

dave: extract permission forwarding to shared.rs

should_auto_accept() and forward_permission_to_ui() consolidate the
duplicated auto-accept check and PermissionRequest/PendingPermission
construction that existed in both claude.rs and codex.rs.

The codex check_approval_or_forward() shrinks from 47 to 12 lines.
The claude permission block drops the manual PermissionRequest build.
The cached_plan (ExitPlanMode) logic now lives in the shared helper.

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

Diffstat:
Mcrates/notedeck_dave/src/backend/claude.rs | 61++++++++++++++++---------------------------------------------
Mcrates/notedeck_dave/src/backend/codex.rs | 44++++++--------------------------------------
Mcrates/notedeck_dave/src/backend/shared.rs | 68+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
3 files changed, 89 insertions(+), 84 deletions(-)

diff --git a/crates/notedeck_dave/src/backend/claude.rs b/crates/notedeck_dave/src/backend/claude.rs @@ -1,12 +1,10 @@ -use crate::auto_accept::AutoAcceptRules; use crate::backend::session_info::parse_session_info; use crate::backend::shared::{self, SessionCommand, SessionHandle}; use crate::backend::tool_summary::extract_response_content; use crate::backend::traits::AiBackend; use crate::file_update::FileUpdate; use crate::messages::{ - CompactionInfo, DaveApiResponse, ParsedMarkdown, PendingPermission, PermissionRequest, - PermissionResponse, SubagentInfo, SubagentStatus, + CompactionInfo, DaveApiResponse, PermissionResponse, SubagentInfo, SubagentStatus, }; use crate::tools::Tool; use crate::Message; @@ -24,7 +22,6 @@ use std::sync::mpsc; use std::sync::Arc; use tokio::sync::mpsc as tokio_mpsc; use tokio::sync::oneshot; -use uuid::Uuid; /// Convert a ToolResultContent to a serde_json::Value for use with tool summary formatting fn tool_result_content_to_value(content: &Option<ToolResultContent>) -> serde_json::Value { @@ -251,53 +248,27 @@ async fn session_actor( // Handle permission requests (they're blocking the SDK) Some(perm_req) = perm_rx.recv() => { - // Check auto-accept rules - let auto_accept_rules = AutoAcceptRules::default(); - if auto_accept_rules.should_auto_accept(&perm_req.tool_name, &perm_req.tool_input) { - tracing::debug!("Auto-accepting {}: matched auto-accept rule", perm_req.tool_name); + if shared::should_auto_accept(&perm_req.tool_name, &perm_req.tool_input) { let _ = perm_req.response_tx.send(PermissionResult::Allow(PermissionResultAllow::default())); continue; } - // Forward permission request to UI - let request_id = Uuid::new_v4(); - let (ui_resp_tx, ui_resp_rx) = oneshot::channel(); - - let cached_plan = if perm_req.tool_name == "ExitPlanMode" { - perm_req - .tool_input - .get("plan") - .and_then(|v| v.as_str()) - .map(ParsedMarkdown::parse) - } else { - None - }; - - let request = PermissionRequest { - id: request_id, - tool_name: perm_req.tool_name.clone(), - tool_input: perm_req.tool_input.clone(), - response: None, - answer_summary: None, - cached_plan, - }; - - let pending = PendingPermission { - request, - response_tx: ui_resp_tx, + let ui_resp_rx = match shared::forward_permission_to_ui( + &perm_req.tool_name, + perm_req.tool_input.clone(), + &response_tx, + &ctx, + ) { + Some(rx) => rx, + None => { + let _ = perm_req.response_tx.send(PermissionResult::Deny(PermissionResultDeny { + message: "UI channel closed".to_string(), + interrupt: true, + })); + continue; + } }; - if response_tx.send(DaveApiResponse::PermissionRequest(pending)).is_err() { - tracing::error!("Failed to send permission request to UI"); - let _ = perm_req.response_tx.send(PermissionResult::Deny(PermissionResultDeny { - message: "UI channel closed".to_string(), - interrupt: true, - })); - continue; - } - - ctx.request_repaint(); - // Wait for UI response inline - blocking is OK since stream is // waiting for permission result anyway let tool_name = perm_req.tool_name.clone(); diff --git a/crates/notedeck_dave/src/backend/codex.rs b/crates/notedeck_dave/src/backend/codex.rs @@ -3,12 +3,10 @@ use super::codex_protocol::*; use super::shared::{self, SessionCommand, SessionHandle}; -use crate::auto_accept::AutoAcceptRules; use crate::backend::traits::AiBackend; use crate::file_update::{FileUpdate, FileUpdateType}; use crate::messages::{ - CompactionInfo, DaveApiResponse, PendingPermission, PermissionRequest, PermissionResponse, - SessionInfo, SubagentInfo, SubagentStatus, + CompactionInfo, DaveApiResponse, PermissionResponse, SessionInfo, SubagentInfo, SubagentStatus, }; use crate::tools::Tool; use crate::Message; @@ -702,44 +700,14 @@ fn check_approval_or_forward( response_tx: &mpsc::Sender<DaveApiResponse>, ctx: &egui::Context, ) -> HandleResult { - let rules = AutoAcceptRules::default(); - if rules.should_auto_accept(tool_name, &tool_input) { - tracing::debug!("Auto-accepting {} (rpc_id={})", tool_name, rpc_id); + if shared::should_auto_accept(tool_name, &tool_input) { return HandleResult::AutoAccepted(rpc_id); } - // Forward to UI - let request_id = Uuid::new_v4(); - let (ui_resp_tx, ui_resp_rx) = oneshot::channel(); - - let request = PermissionRequest { - id: request_id, - tool_name: tool_name.to_string(), - tool_input, - response: None, - answer_summary: None, - cached_plan: None, - }; - - let pending = PendingPermission { - request, - response_tx: ui_resp_tx, - }; - - if response_tx - .send(DaveApiResponse::PermissionRequest(pending)) - .is_err() - { - tracing::error!("Failed to send permission request to UI"); - // Return auto-decline — can't reach UI - return HandleResult::AutoAccepted(rpc_id); // Will send Accept; could add a Declined variant - } - - ctx.request_repaint(); - - HandleResult::NeedsApproval { - rpc_id, - rx: ui_resp_rx, + match shared::forward_permission_to_ui(tool_name, tool_input, response_tx, ctx) { + Some(rx) => HandleResult::NeedsApproval { rpc_id, rx }, + // Can't reach UI — auto-accept as fallback + None => HandleResult::AutoAccepted(rpc_id), } } diff --git a/crates/notedeck_dave/src/backend/shared.rs b/crates/notedeck_dave/src/backend/shared.rs @@ -1,12 +1,15 @@ //! Shared utilities used by multiple AI backend implementations. +use crate::auto_accept::AutoAcceptRules; use crate::backend::tool_summary::{format_tool_summary, truncate_output}; use crate::file_update::FileUpdate; -use crate::messages::{DaveApiResponse, ExecutedTool}; +use crate::messages::{DaveApiResponse, ExecutedTool, PendingPermission, PermissionRequest}; use crate::Message; use claude_agent_sdk_rs::PermissionMode; use std::sync::mpsc; use tokio::sync::mpsc as tokio_mpsc; +use tokio::sync::oneshot; +use uuid::Uuid; /// Commands sent to a session's actor task. /// @@ -134,6 +137,69 @@ pub fn send_tool_result( ctx.request_repaint(); } +/// Check auto-accept rules for a tool invocation. +/// +/// Returns `true` (and logs) when the tool should be silently +/// accepted without asking the user. +pub fn should_auto_accept(tool_name: &str, tool_input: &serde_json::Value) -> bool { + let rules = AutoAcceptRules::default(); + let accepted = rules.should_auto_accept(tool_name, tool_input); + if accepted { + tracing::debug!("Auto-accepting {}: matched auto-accept rule", tool_name); + } + accepted +} + +/// Build a [`PermissionRequest`] + [`PendingPermission`], send it to +/// the UI via `response_tx`, and return the oneshot receiver the +/// caller can `await` to get the user's decision. +/// +/// Returns `None` if the UI channel is closed (the request could not +/// be delivered). +pub fn forward_permission_to_ui( + tool_name: &str, + tool_input: serde_json::Value, + response_tx: &mpsc::Sender<DaveApiResponse>, + ctx: &egui::Context, +) -> Option<oneshot::Receiver<crate::messages::PermissionResponse>> { + let request_id = Uuid::new_v4(); + let (ui_resp_tx, ui_resp_rx) = oneshot::channel(); + + let cached_plan = if tool_name == "ExitPlanMode" { + tool_input + .get("plan") + .and_then(|v| v.as_str()) + .map(crate::messages::ParsedMarkdown::parse) + } else { + None + }; + + let request = PermissionRequest { + id: request_id, + tool_name: tool_name.to_string(), + tool_input, + response: None, + answer_summary: None, + cached_plan, + }; + + let pending = PendingPermission { + request, + response_tx: ui_resp_tx, + }; + + if response_tx + .send(DaveApiResponse::PermissionRequest(pending)) + .is_err() + { + tracing::error!("Failed to send permission request to UI"); + return None; + } + + ctx.request_repaint(); + Some(ui_resp_rx) +} + /// Decide which prompt to send based on whether we're resuming a /// session and how many user messages exist. ///