commit 2964a2d9c2e5ac03382c50adfac6df6fe3b43dd0
parent 7688fd854d02031fe5960b4243d086805a194cd0
Author: William Casarin <jb55@jb55.com>
Date: Wed, 18 Feb 2026 13:06:47 -0800
fix tentative permission buttons: show Send when composing message
When in tentative state (composing a message to attach to a permission
response), replace Allow/Deny buttons with a Send button so tapping
the button actually submits the response with the typed message. This
fixes mobile where clicking Allow/Deny would discard the message.
Extract tentative_send_ui() and add_msg_link() helpers to share the
tentative-state UI between permission_buttons and exit_plan_mode_ui.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat:
1 file changed, 150 insertions(+), 155 deletions(-)
diff --git a/crates/notedeck_dave/src/ui/dave.rs b/crates/notedeck_dave/src/ui/dave.rs
@@ -621,96 +621,59 @@ impl<'a> DaveUi<'a> {
action: &mut Option<DaveAction>,
) {
let shift_held = ui.input(|i| i.modifiers.shift);
+ let in_tentative = self.permission_message_state != PermissionMessageState::None;
ui.with_layout(egui::Layout::left_to_right(egui::Align::Center), |ui| {
- let button_text_color = ui.visuals().widgets.active.fg_stroke.color;
-
- // Allow button (green) with integrated keybind hint
- let allow_response = super::badge::ActionButton::new(
- "Allow",
- egui::Color32::from_rgb(34, 139, 34),
- button_text_color,
- )
- .keybind("1")
- .show(ui)
- .on_hover_text("Press 1 to allow, Shift+1 to allow with message");
-
- // Deny button (red) with integrated keybind hint
- let deny_response = super::badge::ActionButton::new(
- "Deny",
- egui::Color32::from_rgb(178, 34, 34),
- button_text_color,
- )
- .keybind("2")
- .show(ui)
- .on_hover_text("Press 2 to deny, Shift+2 to deny with message");
-
- if deny_response.clicked() {
- if shift_held {
- // Shift+click: enter tentative deny mode
- *action = Some(DaveAction::TentativeDeny);
- } else {
- // Normal click: immediate deny
- *action = Some(DaveAction::PermissionResponse {
- request_id: request.id,
- response: PermissionResponse::Deny {
- reason: "User denied".into(),
- },
- });
- }
- }
-
- if allow_response.clicked() {
- if shift_held {
- // Shift+click: enter tentative accept mode
- *action = Some(DaveAction::TentativeAccept);
- } else {
- // Normal click: immediate allow
- *action = Some(DaveAction::PermissionResponse {
- request_id: request.id,
- response: PermissionResponse::Allow { message: None },
- });
- }
- }
+ if in_tentative {
+ tentative_send_ui(self.permission_message_state, "Allow", "Deny", ui, action);
+ } else {
+ let button_text_color = ui.visuals().widgets.active.fg_stroke.color;
+
+ // Allow button (green) with integrated keybind hint
+ let allow_response = super::badge::ActionButton::new(
+ "Allow",
+ egui::Color32::from_rgb(34, 139, 34),
+ button_text_color,
+ )
+ .keybind("1")
+ .show(ui)
+ .on_hover_text("Press 1 to allow, Shift+1 to allow with message");
+
+ // Deny button (red) with integrated keybind hint
+ let deny_response = super::badge::ActionButton::new(
+ "Deny",
+ egui::Color32::from_rgb(178, 34, 34),
+ button_text_color,
+ )
+ .keybind("2")
+ .show(ui)
+ .on_hover_text("Press 2 to deny, Shift+2 to deny with message");
- // Show tentative state indicator (clickable to toggle) or "+ msg" button
- match self.permission_message_state {
- PermissionMessageState::TentativeAccept => {
- if ui
- .link(
- egui::RichText::new("✓ Will Allow")
- .color(egui::Color32::from_rgb(100, 180, 100))
- .strong(),
- )
- .clicked()
- {
+ if deny_response.clicked() {
+ if shift_held {
*action = Some(DaveAction::TentativeDeny);
+ } else {
+ *action = Some(DaveAction::PermissionResponse {
+ request_id: request.id,
+ response: PermissionResponse::Deny {
+ reason: "User denied".into(),
+ },
+ });
}
}
- PermissionMessageState::TentativeDeny => {
- if ui
- .link(
- egui::RichText::new("✗ Will Deny")
- .color(egui::Color32::from_rgb(200, 100, 100))
- .strong(),
- )
- .clicked()
- {
- *action = Some(DaveAction::TentativeAccept);
- }
- }
- PermissionMessageState::None => {
- if ui
- .link(
- egui::RichText::new("+ msg")
- .color(ui.visuals().weak_text_color())
- .small(),
- )
- .clicked()
- {
+
+ if allow_response.clicked() {
+ if shift_held {
*action = Some(DaveAction::TentativeAccept);
+ } else {
+ *action = Some(DaveAction::PermissionResponse {
+ request_id: request.id,
+ response: PermissionResponse::Allow { message: None },
+ });
}
}
+
+ add_msg_link(ui, action);
}
});
}
@@ -762,90 +725,64 @@ impl<'a> DaveUi<'a> {
// Approve/Reject buttons with shift support for adding message
let shift_held = ui.input(|i| i.modifiers.shift);
+ let in_tentative =
+ self.permission_message_state != PermissionMessageState::None;
ui.with_layout(egui::Layout::left_to_right(egui::Align::Center), |ui| {
- let button_text_color = ui.visuals().widgets.active.fg_stroke.color;
+ if in_tentative {
+ tentative_send_ui(
+ self.permission_message_state,
+ "Approve",
+ "Reject",
+ ui,
+ &mut action,
+ );
+ } else {
+ let button_text_color = ui.visuals().widgets.active.fg_stroke.color;
- // Approve button (green)
- let approve_response = super::badge::ActionButton::new(
- "Approve",
- egui::Color32::from_rgb(34, 139, 34),
- button_text_color,
- )
- .keybind("1")
- .show(ui)
- .on_hover_text("Press 1 to approve, Shift+1 to approve with message");
+ // Approve button (green)
+ let approve_response = super::badge::ActionButton::new(
+ "Approve",
+ egui::Color32::from_rgb(34, 139, 34),
+ button_text_color,
+ )
+ .keybind("1")
+ .show(ui)
+ .on_hover_text("Press 1 to approve, Shift+1 to approve with message");
- if approve_response.clicked() {
- if shift_held {
- action = Some(DaveAction::TentativeAccept);
- } else {
- action = Some(DaveAction::ExitPlanMode {
- request_id: request.id,
- approved: true,
- });
+ if approve_response.clicked() {
+ if shift_held {
+ action = Some(DaveAction::TentativeAccept);
+ } else {
+ action = Some(DaveAction::ExitPlanMode {
+ request_id: request.id,
+ approved: true,
+ });
+ }
}
- }
- // Reject button (red)
- let reject_response = super::badge::ActionButton::new(
- "Reject",
- egui::Color32::from_rgb(178, 34, 34),
- button_text_color,
- )
- .keybind("2")
- .show(ui)
- .on_hover_text("Press 2 to reject, Shift+2 to reject with message");
-
- if reject_response.clicked() {
- if shift_held {
- action = Some(DaveAction::TentativeDeny);
- } else {
- action = Some(DaveAction::ExitPlanMode {
- request_id: request.id,
- approved: false,
- });
- }
- }
+ // Reject button (red)
+ let reject_response = super::badge::ActionButton::new(
+ "Reject",
+ egui::Color32::from_rgb(178, 34, 34),
+ button_text_color,
+ )
+ .keybind("2")
+ .show(ui)
+ .on_hover_text("Press 2 to reject, Shift+2 to reject with message");
- // Show tentative state indicator (clickable to toggle) or "+ msg" button
- match self.permission_message_state {
- PermissionMessageState::TentativeAccept => {
- if ui
- .link(
- egui::RichText::new("✓ Will Approve")
- .color(egui::Color32::from_rgb(100, 180, 100))
- .strong(),
- )
- .clicked()
- {
+ if reject_response.clicked() {
+ if shift_held {
action = Some(DaveAction::TentativeDeny);
+ } else {
+ action = Some(DaveAction::ExitPlanMode {
+ request_id: request.id,
+ approved: false,
+ });
}
}
- PermissionMessageState::TentativeDeny => {
- if ui
- .link(
- egui::RichText::new("✗ Will Reject")
- .color(egui::Color32::from_rgb(200, 100, 100))
- .strong(),
- )
- .clicked()
- {
- action = Some(DaveAction::TentativeAccept);
- }
- }
- PermissionMessageState::None => {
- if ui
- .link(
- egui::RichText::new("+ msg")
- .color(ui.visuals().weak_text_color())
- .small(),
- )
- .clicked()
- {
- action = Some(DaveAction::TentativeAccept);
- }
- }
+
+ add_msg_link(ui, &mut action);
}
});
});
@@ -1146,6 +1083,64 @@ impl<'a> DaveUi<'a> {
}
}
+/// Send button + clickable accept/deny toggle shown when in tentative state.
+fn tentative_send_ui(
+ state: PermissionMessageState,
+ accept_label: &str,
+ deny_label: &str,
+ ui: &mut egui::Ui,
+ action: &mut Option<DaveAction>,
+) {
+ if ui
+ .add(egui::Button::new(egui::RichText::new("Send").strong()))
+ .clicked()
+ {
+ *action = Some(DaveAction::Send);
+ }
+
+ match state {
+ PermissionMessageState::TentativeAccept => {
+ if ui
+ .link(
+ egui::RichText::new(format!("✓ Will {accept_label}"))
+ .color(egui::Color32::from_rgb(100, 180, 100))
+ .strong(),
+ )
+ .clicked()
+ {
+ *action = Some(DaveAction::TentativeDeny);
+ }
+ }
+ PermissionMessageState::TentativeDeny => {
+ if ui
+ .link(
+ egui::RichText::new(format!("✗ Will {deny_label}"))
+ .color(egui::Color32::from_rgb(200, 100, 100))
+ .strong(),
+ )
+ .clicked()
+ {
+ *action = Some(DaveAction::TentativeAccept);
+ }
+ }
+ PermissionMessageState::None => {}
+ }
+}
+
+/// Clickable "+ msg" link that enters tentative accept mode.
+fn add_msg_link(ui: &mut egui::Ui, action: &mut Option<DaveAction>) {
+ if ui
+ .link(
+ egui::RichText::new("+ msg")
+ .color(ui.visuals().weak_text_color())
+ .small(),
+ )
+ .clicked()
+ {
+ *action = Some(DaveAction::TentativeAccept);
+ }
+}
+
/// Renders the status bar containing git status and toggle badges.
fn status_bar_ui(
mut git_status: Option<&mut GitStatusCache>,