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:
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.
///