feat(tui) render request_permissions calls (#14004)

## Summary
Adds support for tui rendering of request_permission calls

<img width="724" height="245" alt="Screenshot 2026-03-08 at 9 04 07 PM"
src="https://github.com/user-attachments/assets/e1997825-a496-4bfb-bbda-43d0006460a5"
/>


## Testing
- [x] Added snapshot test
This commit is contained in:
Dylan Hurd
2026-03-09 04:24:04 +00:00
committed by GitHub
parent 05332b0e96
commit 06f82c123c
8 changed files with 305 additions and 3 deletions
@@ -39,6 +39,8 @@ pub(super) struct PendingInteractiveReplayState {
patch_approval_call_ids: HashSet<String>,
patch_approval_call_ids_by_turn_id: HashMap<String, Vec<String>>,
elicitation_requests: HashSet<ElicitationRequestKey>,
request_permissions_call_ids: HashSet<String>,
request_permissions_call_ids_by_turn_id: HashMap<String, Vec<String>>,
request_user_input_call_ids: HashSet<String>,
request_user_input_call_ids_by_turn_id: HashMap<String, Vec<String>>,
}
@@ -50,6 +52,7 @@ impl PendingInteractiveReplayState {
EventMsg::ExecApprovalRequest(_)
| EventMsg::ApplyPatchApprovalRequest(_)
| EventMsg::ElicitationRequest(_)
| EventMsg::RequestPermissions(_)
| EventMsg::ExecCommandBegin(_)
| EventMsg::PatchApplyBegin(_)
| EventMsg::TurnComplete(_)
@@ -64,6 +67,7 @@ impl PendingInteractiveReplayState {
Op::ExecApproval { .. }
| Op::PatchApproval { .. }
| Op::ResolveElicitation { .. }
| Op::RequestPermissionsResponse { .. }
| Op::UserInputAnswer { .. }
| Op::Shutdown
)
@@ -99,6 +103,13 @@ impl PendingInteractiveReplayState {
request_id.clone(),
));
}
Op::RequestPermissionsResponse { id, .. } => {
self.request_permissions_call_ids.remove(id);
Self::remove_call_id_from_turn_map(
&mut self.request_permissions_call_ids_by_turn_id,
id,
);
}
// `Op::UserInputAnswer` identifies the turn, not the prompt call_id. The UI
// answers queued prompts for the same turn in FIFO order, so remove the oldest
// queued call_id for that turn.
@@ -166,17 +177,26 @@ impl PendingInteractiveReplayState {
.or_default()
.push(ev.call_id.clone());
}
EventMsg::RequestPermissions(ev) => {
self.request_permissions_call_ids.insert(ev.call_id.clone());
self.request_permissions_call_ids_by_turn_id
.entry(ev.turn_id.clone())
.or_default()
.push(ev.call_id.clone());
}
// A turn ending (normally or aborted/replaced) invalidates any unresolved
// turn-scoped approvals and request_user_input prompts from that turn.
// turn-scoped approvals, permission prompts, and request_user_input prompts.
EventMsg::TurnComplete(ev) => {
self.clear_exec_approval_turn(&ev.turn_id);
self.clear_patch_approval_turn(&ev.turn_id);
self.clear_request_permissions_turn(&ev.turn_id);
self.clear_request_user_input_turn(&ev.turn_id);
}
EventMsg::TurnAborted(ev) => {
if let Some(turn_id) = &ev.turn_id {
self.clear_exec_approval_turn(turn_id);
self.clear_patch_approval_turn(turn_id);
self.clear_request_permissions_turn(turn_id);
self.clear_request_user_input_turn(turn_id);
}
}
@@ -228,6 +248,23 @@ impl PendingInteractiveReplayState {
.remove(&ev.turn_id);
}
}
EventMsg::RequestPermissions(ev) => {
self.request_permissions_call_ids.remove(&ev.call_id);
let mut remove_turn_entry = false;
if let Some(call_ids) = self
.request_permissions_call_ids_by_turn_id
.get_mut(&ev.turn_id)
{
call_ids.retain(|call_id| call_id != &ev.call_id);
if call_ids.is_empty() {
remove_turn_entry = true;
}
}
if remove_turn_entry {
self.request_permissions_call_ids_by_turn_id
.remove(&ev.turn_id);
}
}
_ => {}
}
}
@@ -250,6 +287,9 @@ impl PendingInteractiveReplayState {
EventMsg::RequestUserInput(ev) => {
self.request_user_input_call_ids.contains(&ev.call_id)
}
EventMsg::RequestPermissions(ev) => {
self.request_permissions_call_ids.contains(&ev.call_id)
}
_ => true,
}
}
@@ -258,6 +298,7 @@ impl PendingInteractiveReplayState {
!self.exec_approval_call_ids.is_empty()
|| !self.patch_approval_call_ids.is_empty()
|| !self.elicitation_requests.is_empty()
|| !self.request_permissions_call_ids.is_empty()
}
fn clear_request_user_input_turn(&mut self, turn_id: &str) {
@@ -268,6 +309,14 @@ impl PendingInteractiveReplayState {
}
}
fn clear_request_permissions_turn(&mut self, turn_id: &str) {
if let Some(call_ids) = self.request_permissions_call_ids_by_turn_id.remove(turn_id) {
for call_id in call_ids {
self.request_permissions_call_ids.remove(&call_id);
}
}
}
fn clear_exec_approval_turn(&mut self, turn_id: &str) {
if let Some(call_ids) = self.exec_approval_call_ids_by_turn_id.remove(turn_id) {
for call_id in call_ids {
@@ -317,6 +366,8 @@ impl PendingInteractiveReplayState {
self.patch_approval_call_ids.clear();
self.patch_approval_call_ids_by_turn_id.clear();
self.elicitation_requests.clear();
self.request_permissions_call_ids.clear();
self.request_permissions_call_ids_by_turn_id.clear();
self.request_user_input_call_ids.clear();
self.request_user_input_call_ids_by_turn_id.clear();
}