Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion codex-rs/tui_app_server/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ use toml::Value as TomlValue;
use uuid::Uuid;
mod agent_navigation;
mod app_server_adapter;
mod app_server_requests;
pub(crate) mod app_server_requests;
mod pending_interactive_replay;

use self::agent_navigation::AgentNavigationDirection;
Expand Down
8 changes: 6 additions & 2 deletions codex-rs/tui_app_server/src/app/app_server_adapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,12 @@ impl App {
) {
match &notification {
ServerNotification::ServerRequestResolved(notification) => {
self.pending_app_server_requests
.resolve_notification(&notification.request_id);
if let Some(request) = self
.pending_app_server_requests
.resolve_notification(&notification.request_id)
{
self.chat_widget.dismiss_app_server_request(&request);
}
}
ServerNotification::AccountRateLimitsUpdated(notification) => {
self.chat_widget.on_rate_limit_snapshot(Some(
Expand Down
154 changes: 146 additions & 8 deletions codex-rs/tui_app_server/src/app/app_server_requests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,26 @@ pub(super) struct UnsupportedAppServerRequest {
pub(super) message: String,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ResolvedAppServerRequest {
ExecApproval {
id: String,
},
FileChangeApproval {
id: String,
},
PermissionsApproval {
id: String,
},
UserInput {
id: String,
},
McpElicitation {
server_name: String,
request_id: McpRequestId,
},
}

#[derive(Debug, Default)]
pub(super) struct PendingAppServerRequests {
exec_approvals: HashMap<String, AppServerRequestId>,
Expand Down Expand Up @@ -237,14 +257,59 @@ impl PendingAppServerRequests {
Ok(resolution)
}

pub(super) fn resolve_notification(&mut self, request_id: &AppServerRequestId) {
self.exec_approvals.retain(|_, value| value != request_id);
self.file_change_approvals
.retain(|_, value| value != request_id);
self.permissions_approvals
.retain(|_, value| value != request_id);
self.user_inputs.retain(|_, value| value != request_id);
self.mcp_requests.retain(|_, value| value != request_id);
pub(super) fn resolve_notification(
&mut self,
request_id: &AppServerRequestId,
) -> Option<ResolvedAppServerRequest> {
if let Some(id) = self
.exec_approvals
.iter()
.find_map(|(id, value)| (value == request_id).then(|| id.clone()))
{
self.exec_approvals.remove(&id);
return Some(ResolvedAppServerRequest::ExecApproval { id });
}

if let Some(id) = self
.file_change_approvals
.iter()
.find_map(|(id, value)| (value == request_id).then(|| id.clone()))
{
self.file_change_approvals.remove(&id);
return Some(ResolvedAppServerRequest::FileChangeApproval { id });
}

if let Some(id) = self
.permissions_approvals
.iter()
.find_map(|(id, value)| (value == request_id).then(|| id.clone()))
{
self.permissions_approvals.remove(&id);
return Some(ResolvedAppServerRequest::PermissionsApproval { id });
}

if let Some(id) = self
.user_inputs
.iter()
.find_map(|(id, value)| (value == request_id).then(|| id.clone()))
{
self.user_inputs.remove(&id);
return Some(ResolvedAppServerRequest::UserInput { id });
}

if let Some(key) = self
.mcp_requests
.iter()
.find_map(|(key, value)| (value == request_id).then(|| key.clone()))
{
self.mcp_requests.remove(&key);
return Some(ResolvedAppServerRequest::McpElicitation {
server_name: key.server_name,
request_id: key.request_id,
});
}

None
}
}

Expand Down Expand Up @@ -279,6 +344,7 @@ fn file_change_decision(decision: &ReviewDecision) -> Result<FileChangeApprovalD
#[cfg(test)]
mod tests {
use super::PendingAppServerRequests;
use super::ResolvedAppServerRequest;
use codex_app_server_protocol::CommandExecutionRequestApprovalParams;
use codex_app_server_protocol::FileChangeRequestApprovalParams;
use codex_app_server_protocol::McpElicitationObjectType;
Expand Down Expand Up @@ -551,4 +617,76 @@ mod tests {
"execpolicy amendment is not a valid file change approval decision"
);
}

#[test]
fn resolve_notification_returns_resolved_exec_request() {
let mut pending = PendingAppServerRequests::default();
assert_eq!(
pending.note_server_request(&ServerRequest::CommandExecutionRequestApproval {
request_id: AppServerRequestId::Integer(41),
params: CommandExecutionRequestApprovalParams {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
item_id: "call-1".to_string(),
approval_id: Some("approval-1".to_string()),
reason: None,
network_approval_context: None,
command: Some("ls".to_string()),
cwd: None,
command_actions: None,
additional_permissions: None,
skill_metadata: None,
proposed_execpolicy_amendment: None,
proposed_network_policy_amendments: None,
available_decisions: None,
},
}),
None
);

assert_eq!(
pending.resolve_notification(&AppServerRequestId::Integer(41)),
Some(ResolvedAppServerRequest::ExecApproval {
id: "approval-1".to_string(),
})
);
assert_eq!(
pending.resolve_notification(&AppServerRequestId::Integer(41)),
None
);
}

#[test]
fn resolve_notification_returns_resolved_mcp_request() {
let mut pending = PendingAppServerRequests::default();
assert_eq!(
pending.note_server_request(&ServerRequest::McpServerElicitationRequest {
request_id: AppServerRequestId::Integer(12),
params: McpServerElicitationRequestParams {
thread_id: "thread-1".to_string(),
turn_id: Some("turn-1".to_string()),
server_name: "example".to_string(),
request: McpServerElicitationRequest::Form {
meta: None,
message: "Need input".to_string(),
requested_schema: McpElicitationSchema {
schema_uri: None,
type_: McpElicitationObjectType::Object,
properties: BTreeMap::new(),
required: None,
},
},
},
}),
None
);

assert_eq!(
pending.resolve_notification(&AppServerRequestId::Integer(12)),
Some(ResolvedAppServerRequest::McpElicitation {
server_name: "example".to_string(),
request_id: McpRequestId::Integer(12),
})
);
}
}
72 changes: 72 additions & 0 deletions codex-rs/tui_app_server/src/bottom_pane/approval_overlay.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::collections::HashMap;
use std::path::PathBuf;

use crate::app::app_server_requests::ResolvedAppServerRequest;
use crate::app_event::AppEvent;
use crate::app_event_sender::AppEventSender;
use crate::bottom_pane::BottomPaneView;
Expand Down Expand Up @@ -99,6 +100,35 @@ impl ApprovalRequest {
| ApprovalRequest::McpElicitation { thread_label, .. } => thread_label.as_deref(),
}
}

fn matches_resolved_request(&self, request: &ResolvedAppServerRequest) -> bool {
match (self, request) {
(
ApprovalRequest::Exec { id, .. },
ResolvedAppServerRequest::ExecApproval { id: resolved_id },
) => id == resolved_id,
(
ApprovalRequest::Permissions { call_id, .. },
ResolvedAppServerRequest::PermissionsApproval { id },
) => call_id == id,
(
ApprovalRequest::ApplyPatch { id, .. },
ResolvedAppServerRequest::FileChangeApproval { id: resolved_id },
) => id == resolved_id,
(
ApprovalRequest::McpElicitation {
server_name,
request_id,
..
},
ResolvedAppServerRequest::McpElicitation {
server_name: resolved_server_name,
request_id: resolved_request_id,
},
) => server_name == resolved_server_name && request_id == resolved_request_id,
_ => false,
}
}
}

/// Modal overlay asking the user to approve or deny one or more requests.
Expand Down Expand Up @@ -133,6 +163,23 @@ impl ApprovalOverlay {
self.queue.push(req);
}

fn dismiss_resolved_request(&mut self, request: &ResolvedAppServerRequest) -> bool {
let queue_len = self.queue.len();
self.queue
.retain(|queued_request| !queued_request.matches_resolved_request(request));
if self
.current_request
.as_ref()
.is_some_and(|current_request| current_request.matches_resolved_request(request))
{
self.current_complete = true;
self.advance_queue();
return true;
}

self.queue.len() != queue_len
}

fn set_current(&mut self, request: ApprovalRequest) {
self.current_complete = false;
let header = build_header(&request);
Expand Down Expand Up @@ -465,6 +512,10 @@ impl BottomPaneView for ApprovalOverlay {
self.enqueue_request(request);
None
}

fn dismiss_app_server_request(&mut self, request: &ResolvedAppServerRequest) -> bool {
self.dismiss_resolved_request(request)
}
}

impl Renderable for ApprovalOverlay {
Expand Down Expand Up @@ -995,6 +1046,27 @@ mod tests {
assert!(saw_op, "expected approval decision to emit an op");
}

#[test]
fn resolved_request_dismisses_overlay_without_emitting_abort() {
let (tx, mut rx) = unbounded_channel::<AppEvent>();
let tx = AppEventSender::new(tx);
let mut view = ApprovalOverlay::new(make_exec_request(), tx, Features::with_defaults());

assert!(
view.dismiss_app_server_request(&ResolvedAppServerRequest::ExecApproval {
id: "test".to_string(),
})
);
assert!(
view.is_complete(),
"resolved request should close the overlay"
);
assert!(
rx.try_recv().is_err(),
"dismissing a stale request should not emit an approval op"
);
}

#[test]
fn o_opens_source_thread_for_cross_thread_approval() {
let (tx, mut rx) = unbounded_channel::<AppEvent>();
Expand Down
8 changes: 8 additions & 0 deletions codex-rs/tui_app_server/src/bottom_pane/bottom_pane_view.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::app::app_server_requests::ResolvedAppServerRequest;
use crate::bottom_pane::ApprovalRequest;
use crate::bottom_pane::McpServerElicitationFormRequest;
use crate::render::renderable::Renderable;
Expand Down Expand Up @@ -87,4 +88,11 @@ pub(crate) trait BottomPaneView: Renderable {
) -> Option<McpServerElicitationFormRequest> {
Some(request)
}

/// Dismiss a request that was resolved by another client.
///
/// Returns `true` when the view changed state.
fn dismiss_app_server_request(&mut self, _request: &ResolvedAppServerRequest) -> bool {
false
}
}
Loading
Loading