Remove core protocol dependency [2/2] (#20325)

## Why

With the local model layer and app-server routing in place from PR1,
this PR moves the active TUI runtime onto app-server notifications. The
affected pieces share the same event flow, so the command surface,
session state, bottom-pane prompts, chat rendering, history/status
views, and tests move together to keep the stacked branch buildable.

This PR also removes the obsolete compatibility surface that is no
longer used after the migration. The proposed protocol-boundary verifier
layer was dropped from the stack; enforcing that final boundary will be
simpler once `codex-tui` no longer needs any `codex_protocol`
references.

This PR is part 2 of a 2-PR stack:

1. Add TUI-owned replacement models and extract app-server event
routing.
2. Move the active TUI flow to app-server notifications and delete
obsolete adapter code.

## What changed

- Rewired app command and session handling to use app-server request and
notification shapes.
- Moved approval overlays, request-user-input flows, MCP elicitation,
realtime events, and review commands onto the app-server-facing model
surface.
- Updated chat rendering, history cells, status views, multi-agent UI,
replay state, and TUI tests to use app-server notifications plus the
local models introduced in PR1.
- Deleted `codex-rs/tui/src/app/app_server_adapter.rs` and the
superseded `chatwidget/tests/background_events.rs` fixture path.

## Verification

- `cargo check -p codex-tui --tests`
- Top of stack: `cargo test -p codex-tui`
This commit is contained in:
Eric Traut
2026-04-30 11:34:34 -07:00
committed by GitHub
parent 5cc5f12efc
commit f2bc2f26a9
76 changed files with 5078 additions and 9951 deletions
+19 -19
View File
@@ -1,8 +1,8 @@
use codex_protocol::ThreadId;
use codex_protocol::approvals::ElicitationAction;
use codex_protocol::mcp::RequestId as McpRequestId;
#[cfg(test)]
use codex_protocol::protocol::Op;
use crate::app_command::AppCommand as Op;
use codex_app_server_protocol::McpServerElicitationAction;
use codex_app_server_protocol::RequestId as AppServerRequestId;
use codex_protocol::ThreadId;
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
use crossterm::event::KeyModifiers;
@@ -50,7 +50,7 @@ pub(crate) enum AppLinkSuggestionType {
pub(crate) struct AppLinkElicitationTarget {
pub(crate) thread_id: ThreadId,
pub(crate) server_name: String,
pub(crate) request_id: McpRequestId,
pub(crate) request_id: AppServerRequestId,
}
pub(crate) struct AppLinkViewParams {
@@ -148,7 +148,7 @@ impl AppLinkView {
self.elicitation_target.is_some()
}
fn resolve_elicitation(&self, decision: ElicitationAction) {
fn resolve_elicitation(&self, decision: McpServerElicitationAction) {
let Some(target) = self.elicitation_target.as_ref() else {
return;
};
@@ -163,7 +163,7 @@ impl AppLinkView {
}
fn decline_tool_suggestion(&mut self) {
self.resolve_elicitation(ElicitationAction::Decline);
self.resolve_elicitation(McpServerElicitationAction::Decline);
self.complete = true;
}
@@ -182,7 +182,7 @@ impl AppLinkView {
force_refetch: true,
});
if self.is_tool_suggestion() {
self.resolve_elicitation(ElicitationAction::Accept);
self.resolve_elicitation(McpServerElicitationAction::Accept);
}
self.complete = true;
}
@@ -199,7 +199,7 @@ impl AppLinkView {
enabled: self.is_enabled,
});
if self.is_tool_suggestion() {
self.resolve_elicitation(ElicitationAction::Accept);
self.resolve_elicitation(McpServerElicitationAction::Accept);
self.complete = true;
}
}
@@ -466,7 +466,7 @@ impl BottomPaneView for AppLinkView {
fn on_ctrl_c(&mut self) -> CancellationEvent {
if self.is_tool_suggestion() {
self.resolve_elicitation(ElicitationAction::Decline);
self.resolve_elicitation(McpServerElicitationAction::Decline);
}
self.complete = true;
CancellationEvent::Handled
@@ -582,7 +582,7 @@ mod tests {
thread_id: ThreadId::try_from("00000000-0000-0000-0000-000000000001")
.expect("valid thread id"),
server_name: "codex_apps".to_string(),
request_id: McpRequestId::String("request-1".to_string()),
request_id: AppServerRequestId::String("request-1".to_string()),
}
}
@@ -856,8 +856,8 @@ mod tests {
op,
Op::ResolveElicitation {
server_name: "codex_apps".to_string(),
request_id: McpRequestId::String("request-1".to_string()),
decision: ElicitationAction::Accept,
request_id: AppServerRequestId::String("request-1".to_string()),
decision: McpServerElicitationAction::Accept,
content: None,
meta: None,
}
@@ -898,8 +898,8 @@ mod tests {
op,
Op::ResolveElicitation {
server_name: "codex_apps".to_string(),
request_id: McpRequestId::String("request-1".to_string()),
decision: ElicitationAction::Decline,
request_id: AppServerRequestId::String("request-1".to_string()),
decision: McpServerElicitationAction::Decline,
content: None,
meta: None,
}
@@ -948,8 +948,8 @@ mod tests {
op,
Op::ResolveElicitation {
server_name: "codex_apps".to_string(),
request_id: McpRequestId::String("request-1".to_string()),
decision: ElicitationAction::Accept,
request_id: AppServerRequestId::String("request-1".to_string()),
decision: McpServerElicitationAction::Accept,
content: None,
meta: None,
}
@@ -984,7 +984,7 @@ mod tests {
assert!(
view.dismiss_app_server_request(&ResolvedAppServerRequest::McpElicitation {
server_name: "codex_apps".to_string(),
request_id: McpRequestId::String("request-1".to_string()),
request_id: AppServerRequestId::String("request-1".to_string()),
})
);
assert!(view.is_complete());
@@ -1013,7 +1013,7 @@ mod tests {
assert!(
!view.dismiss_app_server_request(&ResolvedAppServerRequest::McpElicitation {
server_name: "other_server".to_string(),
request_id: McpRequestId::String("request-1".to_string()),
request_id: AppServerRequestId::String("request-1".to_string()),
})
);
assert!(!view.is_complete());
+222 -133
View File
@@ -15,6 +15,8 @@ use std::collections::HashMap;
use std::path::PathBuf;
use crate::app::app_server_requests::ResolvedAppServerRequest;
#[cfg(test)]
use crate::app_command::AppCommand as Op;
use crate::app_event::AppEvent;
use crate::app_event_sender::AppEventSender;
use crate::bottom_pane::BottomPaneView;
@@ -23,8 +25,10 @@ use crate::bottom_pane::list_selection_view::ListSelectionView;
use crate::bottom_pane::list_selection_view::SelectionItem;
use crate::bottom_pane::list_selection_view::SelectionViewParams;
use crate::bottom_pane::popup_consts::accept_cancel_hint_line;
use crate::diff_model::FileChange;
use crate::exec_command::strip_bash_lc_and_escape;
use crate::history_cell;
use crate::history_cell::ReviewDecision;
use crate::key_hint;
use crate::key_hint::KeyBinding;
use crate::key_hint::KeyBindingListExt;
@@ -34,19 +38,19 @@ use crate::keymap::primary_binding;
use crate::render::highlight::highlight_bash_to_lines;
use crate::render::renderable::ColumnRenderable;
use crate::render::renderable::Renderable;
use codex_app_server_protocol::AdditionalPermissionProfile;
use codex_app_server_protocol::CommandExecutionApprovalDecision;
use codex_app_server_protocol::FileChangeApprovalDecision;
use codex_app_server_protocol::FileSystemAccessMode;
use codex_app_server_protocol::FileSystemPath;
use codex_app_server_protocol::FileSystemSandboxEntry;
use codex_app_server_protocol::FileSystemSpecialPath;
use codex_app_server_protocol::McpServerElicitationAction;
use codex_app_server_protocol::NetworkApprovalContext;
use codex_app_server_protocol::NetworkPolicyRuleAction;
use codex_app_server_protocol::RequestId;
use codex_features::Features;
use codex_protocol::ThreadId;
use codex_protocol::mcp::RequestId;
use codex_protocol::models::AdditionalPermissionProfile;
use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSandboxEntry;
use codex_protocol::permissions::FileSystemSpecialPath;
use codex_protocol::protocol::ElicitationAction;
use codex_protocol::protocol::FileChange;
use codex_protocol::protocol::NetworkApprovalContext;
use codex_protocol::protocol::NetworkPolicyRuleAction;
use codex_protocol::protocol::ReviewDecision;
use codex_protocol::request_permissions::PermissionGrantScope;
use codex_protocol::request_permissions::RequestPermissionProfile;
use codex_utils_absolute_path::AbsolutePathBuf;
@@ -71,7 +75,7 @@ pub(crate) enum ApprovalRequest {
id: String,
command: Vec<String>,
reason: Option<String>,
available_decisions: Vec<ReviewDecision>,
available_decisions: Vec<CommandExecutionApprovalDecision>,
network_approval_context: Option<NetworkApprovalContext>,
additional_permissions: Option<AdditionalPermissionProfile>,
},
@@ -302,7 +306,10 @@ impl ApprovalOverlay {
};
if let Some(request) = self.current_request.as_ref() {
match (request, &option.decision) {
(ApprovalRequest::Exec { id, command, .. }, ApprovalDecision::Review(decision)) => {
(
ApprovalRequest::Exec { id, command, .. },
ApprovalDecision::Command(decision),
) => {
self.handle_exec_decision(id, command, decision.clone());
}
(
@@ -313,7 +320,10 @@ impl ApprovalOverlay {
},
ApprovalDecision::Permissions(decision),
) => self.handle_permissions_decision(call_id, permissions, *decision),
(ApprovalRequest::ApplyPatch { id, .. }, ApprovalDecision::Review(decision)) => {
(
ApprovalRequest::ApplyPatch { id, .. },
ApprovalDecision::FileChange(decision),
) => {
self.handle_patch_decision(id, decision.clone());
}
(
@@ -334,14 +344,19 @@ impl ApprovalOverlay {
self.advance_queue();
}
fn handle_exec_decision(&self, id: &str, command: &[String], decision: ReviewDecision) {
fn handle_exec_decision(
&self,
id: &str,
command: &[String],
decision: CommandExecutionApprovalDecision,
) {
let Some(request) = self.current_request.as_ref() else {
return;
};
if request.thread_label().is_none() {
let cell = history_cell::new_approval_decision_cell(
command.to_vec(),
decision.clone(),
command_decision_to_review_decision(&decision),
history_cell::ApprovalDecisionActor::User,
);
self.app_event_tx.send(AppEvent::InsertHistoryCell(cell));
@@ -401,7 +416,7 @@ impl ApprovalOverlay {
);
}
fn handle_patch_decision(&self, id: &str, decision: ReviewDecision) {
fn handle_patch_decision(&self, id: &str, decision: FileChangeApprovalDecision) {
let Some(thread_id) = self
.current_request
.as_ref()
@@ -417,7 +432,7 @@ impl ApprovalOverlay {
&self,
server_name: &str,
request_id: &RequestId,
decision: ElicitationAction,
decision: McpServerElicitationAction,
) {
let Some(thread_id) = self
.current_request
@@ -453,7 +468,11 @@ impl ApprovalOverlay {
{
match request {
ApprovalRequest::Exec { id, command, .. } => {
self.handle_exec_decision(id, command, ReviewDecision::Abort);
self.handle_exec_decision(
id,
command,
CommandExecutionApprovalDecision::Cancel,
);
}
ApprovalRequest::Permissions {
call_id,
@@ -467,7 +486,7 @@ impl ApprovalOverlay {
);
}
ApprovalRequest::ApplyPatch { id, .. } => {
self.handle_patch_decision(id, ReviewDecision::Abort);
self.handle_patch_decision(id, FileChangeApprovalDecision::Cancel);
}
ApprovalRequest::McpElicitation {
server_name,
@@ -477,7 +496,7 @@ impl ApprovalOverlay {
self.handle_elicitation_decision(
server_name,
request_id,
ElicitationAction::Cancel,
McpServerElicitationAction::Cancel,
);
}
}
@@ -726,9 +745,10 @@ fn build_header(request: &ApprovalRequest) -> Box<dyn Renderable> {
#[derive(Clone)]
enum ApprovalDecision {
Review(ReviewDecision),
Command(CommandExecutionApprovalDecision),
FileChange(FileChangeApprovalDecision),
Permissions(PermissionsDecision),
McpElicitation(ElicitationAction),
McpElicitation(McpServerElicitationAction),
}
#[derive(Clone, Copy)]
@@ -746,8 +766,29 @@ struct ApprovalOption {
shortcuts: Vec<KeyBinding>,
}
fn command_decision_to_review_decision(
decision: &CommandExecutionApprovalDecision,
) -> ReviewDecision {
match decision {
CommandExecutionApprovalDecision::Accept => ReviewDecision::Approved,
CommandExecutionApprovalDecision::AcceptForSession => ReviewDecision::ApprovedForSession,
CommandExecutionApprovalDecision::AcceptWithExecpolicyAmendment {
execpolicy_amendment,
} => ReviewDecision::ApprovedExecpolicyAmendment {
proposed_execpolicy_amendment: execpolicy_amendment.clone().into_core(),
},
CommandExecutionApprovalDecision::ApplyNetworkPolicyAmendment {
network_policy_amendment,
} => ReviewDecision::NetworkPolicyAmendment {
network_policy_amendment: network_policy_amendment.clone().into_core(),
},
CommandExecutionApprovalDecision::Decline => ReviewDecision::Denied,
CommandExecutionApprovalDecision::Cancel => ReviewDecision::Abort,
}
}
fn exec_options(
available_decisions: &[ReviewDecision],
available_decisions: &[CommandExecutionApprovalDecision],
network_approval_context: Option<&NetworkApprovalContext>,
additional_permissions: Option<&AdditionalPermissionProfile>,
keymap: &ApprovalKeymap,
@@ -755,20 +796,19 @@ fn exec_options(
available_decisions
.iter()
.filter_map(|decision| match decision {
ReviewDecision::Approved => Some(ApprovalOption {
CommandExecutionApprovalDecision::Accept => Some(ApprovalOption {
label: if network_approval_context.is_some() {
"Yes, just this once".to_string()
} else {
"Yes, proceed".to_string()
},
decision: ApprovalDecision::Review(ReviewDecision::Approved),
decision: ApprovalDecision::Command(CommandExecutionApprovalDecision::Accept),
shortcuts: keymap.approve.clone(),
}),
ReviewDecision::ApprovedExecpolicyAmendment {
proposed_execpolicy_amendment,
CommandExecutionApprovalDecision::AcceptWithExecpolicyAmendment {
execpolicy_amendment,
} => {
let rendered_prefix =
strip_bash_lc_and_escape(proposed_execpolicy_amendment.command());
let rendered_prefix = strip_bash_lc_and_escape(&execpolicy_amendment.command);
if rendered_prefix.contains('\n') || rendered_prefix.contains('\r') {
return None;
}
@@ -777,15 +817,15 @@ fn exec_options(
label: format!(
"Yes, and don't ask again for commands that start with `{rendered_prefix}`"
),
decision: ApprovalDecision::Review(
ReviewDecision::ApprovedExecpolicyAmendment {
proposed_execpolicy_amendment: proposed_execpolicy_amendment.clone(),
decision: ApprovalDecision::Command(
CommandExecutionApprovalDecision::AcceptWithExecpolicyAmendment {
execpolicy_amendment: execpolicy_amendment.clone(),
},
),
shortcuts: keymap.approve_for_prefix.clone(),
})
}
ReviewDecision::ApprovedForSession => Some(ApprovalOption {
CommandExecutionApprovalDecision::AcceptForSession => Some(ApprovalOption {
label: if network_approval_context.is_some() {
"Yes, and allow this host for this conversation".to_string()
} else if additional_permissions.is_some() {
@@ -793,10 +833,12 @@ fn exec_options(
} else {
"Yes, and don't ask again for this command in this session".to_string()
},
decision: ApprovalDecision::Review(ReviewDecision::ApprovedForSession),
decision: ApprovalDecision::Command(
CommandExecutionApprovalDecision::AcceptForSession,
),
shortcuts: keymap.approve_for_session.clone(),
}),
ReviewDecision::NetworkPolicyAmendment {
CommandExecutionApprovalDecision::ApplyNetworkPolicyAmendment {
network_policy_amendment,
} => {
let (label, shortcuts) = match network_policy_amendment.action {
@@ -811,21 +853,22 @@ fn exec_options(
};
Some(ApprovalOption {
label,
decision: ApprovalDecision::Review(ReviewDecision::NetworkPolicyAmendment {
network_policy_amendment: network_policy_amendment.clone(),
}),
decision: ApprovalDecision::Command(
CommandExecutionApprovalDecision::ApplyNetworkPolicyAmendment {
network_policy_amendment: network_policy_amendment.clone(),
},
),
shortcuts,
})
}
ReviewDecision::Denied => Some(ApprovalOption {
CommandExecutionApprovalDecision::Decline => Some(ApprovalOption {
label: "No, continue without running it".to_string(),
decision: ApprovalDecision::Review(ReviewDecision::Denied),
decision: ApprovalDecision::Command(CommandExecutionApprovalDecision::Decline),
shortcuts: keymap.deny.clone(),
}),
ReviewDecision::TimedOut => None,
ReviewDecision::Abort => Some(ApprovalOption {
CommandExecutionApprovalDecision::Cancel => Some(ApprovalOption {
label: "No, and tell Codex what to do differently".to_string(),
decision: ApprovalDecision::Review(ReviewDecision::Abort),
decision: ApprovalDecision::Command(CommandExecutionApprovalDecision::Cancel),
shortcuts: keymap.decline.clone(),
}),
})
@@ -849,6 +892,7 @@ pub(crate) fn format_additional_permissions_rule(
file_system
.entries
.iter()
.flatten()
.filter(|entry| entry.access == FileSystemAccessMode::Read),
);
if !reads.is_empty() {
@@ -858,6 +902,7 @@ pub(crate) fn format_additional_permissions_rule(
file_system
.entries
.iter()
.flatten()
.filter(|entry| entry.access == FileSystemAccessMode::Write),
);
if !writes.is_empty() {
@@ -867,6 +912,7 @@ pub(crate) fn format_additional_permissions_rule(
file_system
.entries
.iter()
.flatten()
.filter(|entry| entry.access == FileSystemAccessMode::None),
);
if !denied_reads.is_empty() {
@@ -883,7 +929,14 @@ pub(crate) fn format_additional_permissions_rule(
pub(crate) fn format_requested_permissions_rule(
permissions: &RequestPermissionProfile,
) -> Option<String> {
format_additional_permissions_rule(&permissions.clone().into())
let permissions =
crate::app_server_approval_conversions::granted_permission_profile_from_request(
permissions.clone(),
);
format_additional_permissions_rule(&AdditionalPermissionProfile {
network: permissions.network,
file_system: permissions.file_system,
})
}
fn format_file_system_entry_paths<'a>(
@@ -921,17 +974,17 @@ fn patch_options(keymap: &ApprovalKeymap) -> Vec<ApprovalOption> {
vec![
ApprovalOption {
label: "Yes, proceed".to_string(),
decision: ApprovalDecision::Review(ReviewDecision::Approved),
decision: ApprovalDecision::FileChange(FileChangeApprovalDecision::Accept),
shortcuts: keymap.approve.clone(),
},
ApprovalOption {
label: "Yes, and don't ask again for these files".to_string(),
decision: ApprovalDecision::Review(ReviewDecision::ApprovedForSession),
decision: ApprovalDecision::FileChange(FileChangeApprovalDecision::AcceptForSession),
shortcuts: keymap.approve_for_session.clone(),
},
ApprovalOption {
label: "No, and tell Codex what to do differently".to_string(),
decision: ApprovalDecision::Review(ReviewDecision::Abort),
decision: ApprovalDecision::FileChange(FileChangeApprovalDecision::Cancel),
shortcuts: keymap.decline.clone(),
},
]
@@ -996,17 +1049,17 @@ fn elicitation_options(keymap: &ApprovalKeymap) -> Vec<ApprovalOption> {
vec![
ApprovalOption {
label: "Yes, provide the requested info".to_string(),
decision: ApprovalDecision::McpElicitation(ElicitationAction::Accept),
decision: ApprovalDecision::McpElicitation(McpServerElicitationAction::Accept),
shortcuts: keymap.approve.clone(),
},
ApprovalOption {
label: "No, but continue without it".to_string(),
decision: ApprovalDecision::McpElicitation(ElicitationAction::Decline),
decision: ApprovalDecision::McpElicitation(McpServerElicitationAction::Decline),
shortcuts: decline_shortcuts,
},
ApprovalOption {
label: "Cancel this request".to_string(),
decision: ApprovalDecision::McpElicitation(ElicitationAction::Cancel),
decision: ApprovalDecision::McpElicitation(McpServerElicitationAction::Cancel),
shortcuts: cancel_shortcuts,
},
]
@@ -1015,16 +1068,14 @@ fn elicitation_options(keymap: &ApprovalKeymap) -> Vec<ApprovalOption> {
#[cfg(test)]
mod tests {
use super::*;
use crate::app_command::AppCommand;
use crate::app_event::AppEvent;
use codex_app_server_protocol::AdditionalFileSystemPermissions;
use codex_app_server_protocol::AdditionalNetworkPermissions;
use codex_app_server_protocol::ExecPolicyAmendment;
use codex_app_server_protocol::NetworkApprovalProtocol;
use codex_app_server_protocol::NetworkPolicyAmendment;
use codex_protocol::models::FileSystemPermissions;
use codex_protocol::models::NetworkPermissions;
use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSandboxEntry;
use codex_protocol::permissions::FileSystemSpecialPath;
use codex_protocol::protocol::ExecPolicyAmendment;
use codex_protocol::protocol::NetworkApprovalProtocol;
use codex_protocol::protocol::NetworkPolicyAmendment;
use codex_utils_absolute_path::AbsolutePathBuf;
use crossterm::event::KeyModifiers;
use insta::assert_snapshot;
@@ -1100,7 +1151,10 @@ mod tests {
id: "test".to_string(),
command: vec!["echo".to_string(), "hi".to_string()],
reason: Some("reason".to_string()),
available_decisions: vec![ReviewDecision::Approved, ReviewDecision::Abort],
available_decisions: vec![
CommandExecutionApprovalDecision::Accept,
CommandExecutionApprovalDecision::Cancel,
],
network_approval_context: None,
additional_permissions: None,
}
@@ -1165,7 +1219,7 @@ mod tests {
let mut decision = None;
while let Ok(ev) = rx.try_recv() {
if let AppEvent::SubmitThreadOp {
op: AppCommand::ExecApproval { decision: d, .. },
op: Op::ExecApproval { decision: d, .. },
..
} = ev
{
@@ -1173,7 +1227,7 @@ mod tests {
break;
}
}
assert_eq!(decision, Some(ReviewDecision::Abort));
assert_eq!(decision, Some(CommandExecutionApprovalDecision::Cancel));
}
#[test]
@@ -1196,7 +1250,7 @@ mod tests {
let mut decision = None;
while let Ok(ev) = rx.try_recv() {
if let AppEvent::SubmitThreadOp {
op: AppCommand::ResolveElicitation { decision: d, .. },
op: Op::ResolveElicitation { decision: d, .. },
..
} = ev
{
@@ -1204,7 +1258,7 @@ mod tests {
break;
}
}
assert_eq!(decision, Some(ElicitationAction::Cancel));
assert_eq!(decision, Some(McpServerElicitationAction::Cancel));
}
#[test]
@@ -1236,7 +1290,10 @@ mod tests {
id: "test".to_string(),
command: vec!["echo".to_string(), "hi".to_string()],
reason: None,
available_decisions: vec![ReviewDecision::Approved, ReviewDecision::Denied],
available_decisions: vec![
CommandExecutionApprovalDecision::Accept,
CommandExecutionApprovalDecision::Decline,
],
network_approval_context: None,
additional_permissions: None,
},
@@ -1249,11 +1306,11 @@ mod tests {
let mut saw_denied = false;
while let Ok(ev) = rx.try_recv() {
if let AppEvent::SubmitThreadOp {
op: AppCommand::ExecApproval { decision, .. },
op: Op::ExecApproval { decision, .. },
..
} = ev
{
assert_eq!(decision, ReviewDecision::Denied);
assert_eq!(decision, CommandExecutionApprovalDecision::Decline);
saw_denied = true;
break;
}
@@ -1277,8 +1334,8 @@ mod tests {
command: vec!["curl".to_string(), "https://example.com".to_string()],
reason: None,
available_decisions: vec![
ReviewDecision::Approved,
ReviewDecision::NetworkPolicyAmendment {
CommandExecutionApprovalDecision::Accept,
CommandExecutionApprovalDecision::ApplyNetworkPolicyAmendment {
network_policy_amendment: amendment.clone(),
},
],
@@ -1297,13 +1354,13 @@ mod tests {
let mut saw_deny = false;
while let Ok(ev) = rx.try_recv() {
if let AppEvent::SubmitThreadOp {
op: AppCommand::ExecApproval { decision, .. },
op: Op::ExecApproval { decision, .. },
..
} = ev
{
assert_eq!(
decision,
ReviewDecision::NetworkPolicyAmendment {
CommandExecutionApprovalDecision::ApplyNetworkPolicyAmendment {
network_policy_amendment: amendment
}
);
@@ -1350,7 +1407,10 @@ mod tests {
id: "test".to_string(),
command: vec!["echo".to_string(), "hi".to_string()],
reason: None,
available_decisions: vec![ReviewDecision::Approved, ReviewDecision::Abort],
available_decisions: vec![
CommandExecutionApprovalDecision::Accept,
CommandExecutionApprovalDecision::Cancel,
],
network_approval_context: None,
additional_permissions: None,
},
@@ -1381,7 +1441,10 @@ mod tests {
id: "test".to_string(),
command: vec!["echo".to_string(), "hi".to_string()],
reason: None,
available_decisions: vec![ReviewDecision::Approved, ReviewDecision::Abort],
available_decisions: vec![
CommandExecutionApprovalDecision::Accept,
CommandExecutionApprovalDecision::Cancel,
],
network_approval_context: None,
additional_permissions: None,
},
@@ -1416,7 +1479,10 @@ mod tests {
id: "test".to_string(),
command: vec!["echo".to_string(), "hi".to_string()],
reason: None,
available_decisions: vec![ReviewDecision::Approved, ReviewDecision::Abort],
available_decisions: vec![
CommandExecutionApprovalDecision::Accept,
CommandExecutionApprovalDecision::Cancel,
],
network_approval_context: None,
additional_permissions: None,
},
@@ -1442,13 +1508,13 @@ mod tests {
command: vec!["echo".to_string()],
reason: None,
available_decisions: vec![
ReviewDecision::Approved,
ReviewDecision::ApprovedExecpolicyAmendment {
proposed_execpolicy_amendment: ExecPolicyAmendment::new(vec![
"echo".to_string(),
]),
CommandExecutionApprovalDecision::Accept,
CommandExecutionApprovalDecision::AcceptWithExecpolicyAmendment {
execpolicy_amendment: ExecPolicyAmendment {
command: vec!["echo".to_string()],
},
},
ReviewDecision::Abort,
CommandExecutionApprovalDecision::Cancel,
],
network_approval_context: None,
additional_permissions: None,
@@ -1460,16 +1526,16 @@ mod tests {
let mut saw_op = false;
while let Ok(ev) = rx.try_recv() {
if let AppEvent::SubmitThreadOp {
op: AppCommand::ExecApproval { decision, .. },
op: Op::ExecApproval { decision, .. },
..
} = ev
{
assert_eq!(
decision,
ReviewDecision::ApprovedExecpolicyAmendment {
proposed_execpolicy_amendment: ExecPolicyAmendment::new(vec![
"echo".to_string()
])
CommandExecutionApprovalDecision::AcceptWithExecpolicyAmendment {
execpolicy_amendment: ExecPolicyAmendment {
command: vec!["echo".to_string()],
}
}
);
saw_op = true;
@@ -1494,15 +1560,15 @@ mod tests {
command: vec!["curl".to_string(), "https://example.com".to_string()],
reason: None,
available_decisions: vec![
ReviewDecision::Approved,
ReviewDecision::ApprovedForSession,
ReviewDecision::NetworkPolicyAmendment {
CommandExecutionApprovalDecision::Accept,
CommandExecutionApprovalDecision::AcceptForSession,
CommandExecutionApprovalDecision::ApplyNetworkPolicyAmendment {
network_policy_amendment: NetworkPolicyAmendment {
host: "example.com".to_string(),
action: NetworkPolicyRuleAction::Allow,
},
},
ReviewDecision::Abort,
CommandExecutionApprovalDecision::Cancel,
],
network_approval_context: Some(NetworkApprovalContext {
host: "example.com".to_string(),
@@ -1532,7 +1598,10 @@ mod tests {
id: "test".into(),
command,
reason: None,
available_decisions: vec![ReviewDecision::Approved, ReviewDecision::Abort],
available_decisions: vec![
CommandExecutionApprovalDecision::Accept,
CommandExecutionApprovalDecision::Cancel,
],
network_approval_context: None,
additional_permissions: None,
};
@@ -1568,15 +1637,15 @@ mod tests {
let keymap = crate::keymap::RuntimeKeymap::defaults();
let options = exec_options(
&[
ReviewDecision::Approved,
ReviewDecision::ApprovedForSession,
ReviewDecision::NetworkPolicyAmendment {
CommandExecutionApprovalDecision::Accept,
CommandExecutionApprovalDecision::AcceptForSession,
CommandExecutionApprovalDecision::ApplyNetworkPolicyAmendment {
network_policy_amendment: NetworkPolicyAmendment {
host: "example.com".to_string(),
action: NetworkPolicyRuleAction::Allow,
},
},
ReviewDecision::Abort,
CommandExecutionApprovalDecision::Cancel,
],
Some(&network_context),
/*additional_permissions*/ None,
@@ -1600,9 +1669,9 @@ mod tests {
let keymap = crate::keymap::RuntimeKeymap::defaults();
let options = exec_options(
&[
ReviewDecision::Approved,
ReviewDecision::ApprovedForSession,
ReviewDecision::Abort,
CommandExecutionApprovalDecision::Accept,
CommandExecutionApprovalDecision::AcceptForSession,
CommandExecutionApprovalDecision::Cancel,
],
/*network_approval_context*/ None,
/*additional_permissions*/ None,
@@ -1624,14 +1693,20 @@ mod tests {
fn additional_permissions_exec_options_hide_execpolicy_amendment() {
let keymap = crate::keymap::RuntimeKeymap::defaults();
let additional_permissions = AdditionalPermissionProfile {
file_system: Some(FileSystemPermissions::from_read_write_roots(
Some(vec![absolute_path("/tmp/readme.txt")]),
Some(vec![absolute_path("/tmp/out.txt")]),
)),
..Default::default()
network: None,
file_system: Some(
FileSystemPermissions::from_read_write_roots(
Some(vec![absolute_path("/tmp/readme.txt")]),
Some(vec![absolute_path("/tmp/out.txt")]),
)
.into(),
),
};
let options = exec_options(
&[ReviewDecision::Approved, ReviewDecision::Abort],
&[
CommandExecutionApprovalDecision::Accept,
CommandExecutionApprovalDecision::Cancel,
],
/*network_approval_context*/ None,
Some(&additional_permissions),
&keymap.approval,
@@ -1668,8 +1743,11 @@ mod tests {
#[test]
fn additional_permissions_rule_shows_non_path_file_system_entries() {
let additional_permissions = AdditionalPermissionProfile {
file_system: Some(FileSystemPermissions {
entries: vec![
network: None,
file_system: Some(AdditionalFileSystemPermissions {
read: None,
write: None,
entries: Some(vec![
FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::Root,
@@ -1682,10 +1760,9 @@ mod tests {
},
access: FileSystemAccessMode::None,
},
],
]),
glob_scan_max_depth: None,
}),
..Default::default()
};
assert_eq!(
@@ -1705,7 +1782,7 @@ mod tests {
let mut saw_op = false;
while let Ok(ev) = rx.try_recv() {
if let AppEvent::SubmitThreadOp {
op: AppCommand::RequestPermissionsResponse { response, .. },
op: Op::RequestPermissionsResponse { response, .. },
..
} = ev
{
@@ -1740,7 +1817,7 @@ mod tests {
let mut saw_op = false;
while let Ok(ev) = rx.try_recv() {
if let AppEvent::SubmitThreadOp {
op: AppCommand::RequestPermissionsResponse { response, .. },
op: Op::RequestPermissionsResponse { response, .. },
..
} = ev
{
@@ -1768,7 +1845,7 @@ mod tests {
let mut saw_op = false;
while let Ok(ev) = rx.try_recv() {
if let AppEvent::SubmitThreadOp {
op: AppCommand::RequestPermissionsResponse { response, .. },
op: Op::RequestPermissionsResponse { response, .. },
..
} = ev
{
@@ -1794,16 +1871,22 @@ mod tests {
id: "test".into(),
command: vec!["cat".into(), "/tmp/readme.txt".into()],
reason: None,
available_decisions: vec![ReviewDecision::Approved, ReviewDecision::Abort],
available_decisions: vec![
CommandExecutionApprovalDecision::Accept,
CommandExecutionApprovalDecision::Cancel,
],
network_approval_context: None,
additional_permissions: Some(AdditionalPermissionProfile {
network: Some(NetworkPermissions {
network: Some(AdditionalNetworkPermissions {
enabled: Some(true),
}),
file_system: Some(FileSystemPermissions::from_read_write_roots(
Some(vec![absolute_path("/tmp/readme.txt")]),
Some(vec![absolute_path("/tmp/out.txt")]),
)),
file_system: Some(
FileSystemPermissions::from_read_write_roots(
Some(vec![absolute_path("/tmp/readme.txt")]),
Some(vec![absolute_path("/tmp/out.txt")]),
)
.into(),
),
}),
};
@@ -1844,16 +1927,22 @@ mod tests {
id: "test".into(),
command: vec!["cat".into(), "/tmp/readme.txt".into()],
reason: Some("need filesystem access".into()),
available_decisions: vec![ReviewDecision::Approved, ReviewDecision::Abort],
available_decisions: vec![
CommandExecutionApprovalDecision::Accept,
CommandExecutionApprovalDecision::Cancel,
],
network_approval_context: None,
additional_permissions: Some(AdditionalPermissionProfile {
network: Some(NetworkPermissions {
network: Some(AdditionalNetworkPermissions {
enabled: Some(true),
}),
file_system: Some(FileSystemPermissions::from_read_write_roots(
Some(vec![absolute_path("/tmp/readme.txt")]),
Some(vec![absolute_path("/tmp/out.txt")]),
)),
file_system: Some(
FileSystemPermissions::from_read_write_roots(
Some(vec![absolute_path("/tmp/readme.txt")]),
Some(vec![absolute_path("/tmp/out.txt")]),
)
.into(),
),
}),
};
@@ -1919,15 +2008,15 @@ mod tests {
command: vec!["curl".into(), "https://example.com".into()],
reason: Some("network request blocked".into()),
available_decisions: vec![
ReviewDecision::Approved,
ReviewDecision::ApprovedForSession,
ReviewDecision::NetworkPolicyAmendment {
CommandExecutionApprovalDecision::Accept,
CommandExecutionApprovalDecision::AcceptForSession,
CommandExecutionApprovalDecision::ApplyNetworkPolicyAmendment {
network_policy_amendment: NetworkPolicyAmendment {
host: "example.com".to_string(),
action: NetworkPolicyRuleAction::Allow,
},
},
ReviewDecision::Abort,
CommandExecutionApprovalDecision::Cancel,
],
network_approval_context: Some(NetworkApprovalContext {
host: "example.com".to_string(),
@@ -2031,7 +2120,7 @@ mod tests {
let mut decision = None;
while let Ok(ev) = rx.try_recv() {
if let AppEvent::SubmitThreadOp {
op: AppCommand::ResolveElicitation { decision: d, .. },
op: Op::ResolveElicitation { decision: d, .. },
..
} = ev
{
@@ -2039,7 +2128,7 @@ mod tests {
break;
}
}
assert_eq!(decision, Some(ElicitationAction::Cancel));
assert_eq!(decision, Some(McpServerElicitationAction::Cancel));
}
#[test]
@@ -2065,7 +2154,7 @@ mod tests {
let mut esc_decision = None;
while let Ok(ev) = rx.try_recv() {
if let AppEvent::SubmitThreadOp {
op: AppCommand::ResolveElicitation { decision, .. },
op: Op::ResolveElicitation { decision, .. },
..
} = ev
{
@@ -2073,7 +2162,7 @@ mod tests {
break;
}
}
assert_eq!(esc_decision, Some(ElicitationAction::Cancel));
assert_eq!(esc_decision, Some(McpServerElicitationAction::Cancel));
let (tx_raw, mut rx) = unbounded_channel::<AppEvent>();
let tx = AppEventSender::new(tx_raw);
@@ -2095,7 +2184,7 @@ mod tests {
let mut n_decision = None;
while let Ok(ev) = rx.try_recv() {
if let AppEvent::SubmitThreadOp {
op: AppCommand::ResolveElicitation { decision, .. },
op: Op::ResolveElicitation { decision, .. },
..
} = ev
{
@@ -2103,7 +2192,7 @@ mod tests {
break;
}
}
assert_eq!(n_decision, Some(ElicitationAction::Decline));
assert_eq!(n_decision, Some(McpServerElicitationAction::Decline));
}
#[test]
@@ -2121,7 +2210,7 @@ mod tests {
let mut decision = None;
while let Ok(ev) = rx.try_recv() {
if let AppEvent::SubmitThreadOp {
op: AppCommand::ExecApproval { decision: d, .. },
op: Op::ExecApproval { decision: d, .. },
..
} = ev
{
@@ -2129,6 +2218,6 @@ mod tests {
break;
}
}
assert_eq!(decision, Some(ReviewDecision::Approved));
assert_eq!(decision, Some(CommandExecutionApprovalDecision::Accept));
}
}
@@ -2,7 +2,7 @@ use crate::app::app_server_requests::ResolvedAppServerRequest;
use crate::bottom_pane::ApprovalRequest;
use crate::bottom_pane::McpServerElicitationFormRequest;
use crate::render::renderable::Renderable;
use codex_protocol::request_user_input::RequestUserInputEvent;
use codex_app_server_protocol::ToolRequestUserInputParams;
use crossterm::event::KeyEvent;
use super::CancellationEvent;
@@ -101,8 +101,8 @@ pub(crate) trait BottomPaneView: Renderable {
/// consumed.
fn try_consume_user_input_request(
&mut self,
request: RequestUserInputEvent,
) -> Option<RequestUserInputEvent> {
request: ToolRequestUserInputParams,
) -> Option<ToolRequestUserInputParams> {
Some(request)
}
@@ -5679,7 +5679,7 @@ mod tests {
dependencies: None,
policy: None,
path_to_skills_md: skill_path.clone(),
scope: codex_protocol::protocol::SkillScope::User,
scope: crate::test_support::skill_scope_user(),
}]));
let ActivePopup::Skill(popup) = &composer.active_popup else {
@@ -5721,7 +5721,7 @@ mod tests {
dependencies: None,
policy: None,
path_to_skills_md: skill_path.clone(),
scope: codex_protocol::protocol::SkillScope::Repo,
scope: crate::test_support::skill_scope_repo(),
}]));
composer.set_plugin_mentions(Some(vec![PluginCapabilitySummary {
config_name: "google-calendar@debug".to_string(),
@@ -5812,7 +5812,7 @@ mod tests {
dependencies: None,
policy: None,
path_to_skills_md: test_path_buf("/tmp/repo/google-calendar/SKILL.md").abs(),
scope: codex_protocol::protocol::SkillScope::Repo,
scope: crate::test_support::skill_scope_repo(),
}]));
composer.set_plugin_mentions(Some(vec![PluginCapabilitySummary {
config_name: "google-calendar@debug".to_string(),
@@ -15,12 +15,11 @@ use std::collections::HashMap;
use std::collections::HashSet;
use std::path::PathBuf;
use crate::app_command::AppCommand;
use crate::app_command::AppCommand as Op;
use crate::app_event::AppEvent;
use crate::app_event_sender::AppEventSender;
use crate::bottom_pane::MentionBinding;
use crate::mention_codec::decode_history_mentions;
use codex_protocol::protocol::Op;
use codex_protocol::user_input::TextElement;
/// A composer history entry that can rehydrate draft state.
@@ -599,9 +598,7 @@ impl ChatComposerHistory {
boundary_if_exhausted,
});
}
app_event_tx.send(AppEvent::CodexOp(AppCommand::from(
Op::GetHistoryEntryRequest { offset, log_id },
)));
app_event_tx.send(AppEvent::CodexOp(Op::history_lookup(offset, log_id)));
return HistorySearchResult::Pending;
}
@@ -719,12 +716,7 @@ impl ChatComposerHistory {
self.last_history_text = Some(entry.text.clone());
return Some(entry);
} else if let Some(log_id) = self.history_log_id {
app_event_tx.send(AppEvent::CodexOp(AppCommand::from(
Op::GetHistoryEntryRequest {
offset: global_idx,
log_id,
},
)));
app_event_tx.send(AppEvent::CodexOp(Op::history_lookup(global_idx, log_id)));
}
None
}
@@ -2,18 +2,16 @@ use std::collections::HashSet;
use std::collections::VecDeque;
use std::path::PathBuf;
#[cfg(test)]
use crate::app_command::AppCommand as Op;
use codex_app_server_protocol::McpElicitationEnumSchema;
use codex_app_server_protocol::McpElicitationPrimitiveSchema;
use codex_app_server_protocol::McpElicitationSingleSelectEnumSchema;
use codex_app_server_protocol::McpServerElicitationAction;
use codex_app_server_protocol::McpServerElicitationRequest;
use codex_app_server_protocol::McpServerElicitationRequestParams;
use codex_app_server_protocol::RequestId as AppServerRequestId;
use codex_protocol::ThreadId;
use codex_protocol::approvals::ElicitationAction;
use codex_protocol::approvals::ElicitationRequest;
use codex_protocol::approvals::ElicitationRequestEvent;
use codex_protocol::mcp::RequestId as McpRequestId;
#[cfg(test)]
use codex_protocol::protocol::Op;
use codex_protocol::user_input::TextElement;
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
@@ -166,7 +164,7 @@ struct McpToolApprovalDisplayParam {
pub(crate) struct McpServerElicitationFormRequest {
thread_id: ThreadId,
server_name: String,
request_id: McpRequestId,
request_id: AppServerRequestId,
message: String,
approval_display_params: Vec<McpToolApprovalDisplayParam>,
response_mode: McpServerElicitationResponseMode,
@@ -206,7 +204,7 @@ impl FooterTip {
impl McpServerElicitationFormRequest {
pub(crate) fn from_app_server_request(
thread_id: ThreadId,
request_id: McpRequestId,
request_id: AppServerRequestId,
request: McpServerElicitationRequestParams,
) -> Option<Self> {
let McpServerElicitationRequestParams {
@@ -234,33 +232,10 @@ impl McpServerElicitationFormRequest {
)
}
pub(crate) fn from_event(
thread_id: ThreadId,
request: ElicitationRequestEvent,
) -> Option<Self> {
let ElicitationRequest::Form {
meta,
message,
requested_schema,
} = request.request
else {
return None;
};
Self::from_parts(
thread_id,
request.server_name,
request.id,
meta,
message,
requested_schema,
)
}
fn from_parts(
thread_id: ThreadId,
server_name: String,
request_id: McpRequestId,
request_id: AppServerRequestId,
meta: Option<Value>,
message: String,
requested_schema: Value,
@@ -388,7 +363,7 @@ impl McpServerElicitationFormRequest {
self.server_name.as_str()
}
pub(crate) fn request_id(&self) -> &McpRequestId {
pub(crate) fn request_id(&self) -> &AppServerRequestId {
&self.request_id
}
}
@@ -1140,7 +1115,7 @@ impl McpServerElicitationOverlay {
self.request.thread_id,
self.request.server_name.clone(),
self.request.request_id.clone(),
ElicitationAction::Cancel,
McpServerElicitationAction::Cancel,
/*content*/ None,
/*meta*/ None,
);
@@ -1167,22 +1142,22 @@ impl McpServerElicitationOverlay {
if self.request.response_mode == McpServerElicitationResponseMode::ApprovalAction {
let (decision, meta) =
match self.field_value(/*idx*/ 0).as_ref().and_then(Value::as_str) {
Some(APPROVAL_ACCEPT_ONCE_VALUE) => (ElicitationAction::Accept, None),
Some(APPROVAL_ACCEPT_ONCE_VALUE) => (McpServerElicitationAction::Accept, None),
Some(APPROVAL_ACCEPT_SESSION_VALUE) => (
ElicitationAction::Accept,
McpServerElicitationAction::Accept,
Some(serde_json::json!({
APPROVAL_PERSIST_KEY: APPROVAL_PERSIST_SESSION_VALUE,
})),
),
Some(APPROVAL_ACCEPT_ALWAYS_VALUE) => (
ElicitationAction::Accept,
McpServerElicitationAction::Accept,
Some(serde_json::json!({
APPROVAL_PERSIST_KEY: APPROVAL_PERSIST_ALWAYS_VALUE,
})),
),
Some(APPROVAL_DECLINE_VALUE) => (ElicitationAction::Decline, None),
Some(APPROVAL_CANCEL_VALUE) => (ElicitationAction::Cancel, None),
_ => (ElicitationAction::Cancel, None),
Some(APPROVAL_DECLINE_VALUE) => (McpServerElicitationAction::Decline, None),
Some(APPROVAL_CANCEL_VALUE) => (McpServerElicitationAction::Cancel, None),
_ => (McpServerElicitationAction::Cancel, None),
};
self.app_event_tx.resolve_elicitation(
self.request.thread_id,
@@ -1206,7 +1181,7 @@ impl McpServerElicitationOverlay {
self.request.thread_id,
self.request.server_name.clone(),
self.request.request_id.clone(),
ElicitationAction::Accept,
McpServerElicitationAction::Accept,
Some(Value::Object(content)),
/*meta*/ None,
);
@@ -1732,19 +1707,35 @@ mod tests {
message: &str,
requested_schema: Value,
meta: Option<Value>,
) -> ElicitationRequestEvent {
ElicitationRequestEvent {
) -> McpServerElicitationRequestParams {
McpServerElicitationRequestParams {
thread_id: "thread-1".to_string(),
turn_id: Some("turn-1".to_string()),
server_name: "server-1".to_string(),
id: McpRequestId::String("request-1".to_string()),
request: ElicitationRequest::Form {
request: McpServerElicitationRequest::Form {
meta,
message: message.to_string(),
requested_schema,
requested_schema: serde_json::from_value(requested_schema)
.expect("test schema should deserialize"),
},
}
}
fn request_id(value: &str) -> AppServerRequestId {
AppServerRequestId::String(value.to_string())
}
fn from_form_request(
thread_id: ThreadId,
request: McpServerElicitationRequestParams,
) -> Option<McpServerElicitationFormRequest> {
McpServerElicitationFormRequest::from_app_server_request(
thread_id,
request_id("request-1"),
request,
)
}
fn empty_object_schema() -> Value {
serde_json::json!({
"type": "object",
@@ -1816,7 +1807,7 @@ mod tests {
#[test]
fn parses_boolean_form_request() {
let thread_id = ThreadId::default();
let request = McpServerElicitationFormRequest::from_event(
let request = from_form_request(
thread_id,
form_request(
"Allow this request?",
@@ -1841,7 +1832,7 @@ mod tests {
McpServerElicitationFormRequest {
thread_id,
server_name: "server-1".to_string(),
request_id: McpRequestId::String("request-1".to_string()),
request_id: request_id("request-1"),
message: "Allow this request?".to_string(),
approval_display_params: Vec::new(),
response_mode: McpServerElicitationResponseMode::FormContent,
@@ -1873,7 +1864,7 @@ mod tests {
#[test]
fn unsupported_numeric_form_falls_back() {
let request = McpServerElicitationFormRequest::from_event(
let request = from_form_request(
ThreadId::default(),
form_request(
"Pick a number",
@@ -1894,11 +1885,15 @@ mod tests {
}
#[test]
fn missing_schema_uses_approval_actions() {
fn empty_object_schema_uses_approval_actions() {
let thread_id = ThreadId::default();
let request = McpServerElicitationFormRequest::from_event(
let request = from_form_request(
thread_id,
form_request("Allow this request?", Value::Null, /*meta*/ None),
form_request(
"Allow this request?",
empty_object_schema(),
/*meta*/ None,
),
)
.expect("expected approval fallback");
@@ -1907,7 +1902,7 @@ mod tests {
McpServerElicitationFormRequest {
thread_id,
server_name: "server-1".to_string(),
request_id: McpRequestId::String("request-1".to_string()),
request_id: request_id("request-1"),
message: "Allow this request?".to_string(),
approval_display_params: Vec::new(),
response_mode: McpServerElicitationResponseMode::ApprovalAction,
@@ -1945,7 +1940,7 @@ mod tests {
#[test]
fn empty_tool_approval_schema_uses_approval_actions() {
let thread_id = ThreadId::default();
let request = McpServerElicitationFormRequest::from_event(
let request = from_form_request(
thread_id,
form_request(
"Allow this request?",
@@ -1964,7 +1959,7 @@ mod tests {
McpServerElicitationFormRequest {
thread_id,
server_name: "server-1".to_string(),
request_id: McpRequestId::String("request-1".to_string()),
request_id: request_id("request-1"),
message: "Allow this request?".to_string(),
approval_display_params: Vec::new(),
response_mode: McpServerElicitationResponseMode::ApprovalAction,
@@ -1996,7 +1991,7 @@ mod tests {
#[test]
fn tool_suggestion_meta_is_parsed_into_request_payload() {
let request = McpServerElicitationFormRequest::from_event(
let request = from_form_request(
ThreadId::default(),
form_request(
"Suggest Google Calendar",
@@ -2029,7 +2024,7 @@ mod tests {
#[test]
fn plugin_tool_suggestion_meta_without_install_url_is_parsed_into_request_payload() {
let request = McpServerElicitationFormRequest::from_event(
let request = from_form_request(
ThreadId::default(),
form_request(
"Suggest Slack",
@@ -2061,7 +2056,7 @@ mod tests {
#[test]
fn tool_approval_display_params_prefer_explicit_display_order() {
let request = McpServerElicitationFormRequest::from_event(
let request = from_form_request(
ThreadId::default(),
form_request(
"Allow Calendar to create an event",
@@ -2110,7 +2105,7 @@ mod tests {
fn submit_sends_accept_with_typed_content() {
let (tx, mut rx) = test_sender();
let thread_id = ThreadId::default();
let request = McpServerElicitationFormRequest::from_event(
let request = from_form_request(
thread_id,
form_request(
"Allow this request?",
@@ -2150,8 +2145,8 @@ mod tests {
op,
Op::ResolveElicitation {
server_name: "server-1".to_string(),
request_id: McpRequestId::String("request-1".to_string()),
decision: ElicitationAction::Accept,
request_id: request_id("request-1"),
decision: McpServerElicitationAction::Accept,
content: Some(serde_json::json!({
"confirmed": true,
})),
@@ -2164,7 +2159,7 @@ mod tests {
fn empty_tool_approval_schema_session_choice_sets_persist_meta() {
let (tx, mut rx) = test_sender();
let thread_id = ThreadId::default();
let request = McpServerElicitationFormRequest::from_event(
let request = from_form_request(
thread_id,
form_request(
"Allow this request?",
@@ -2204,8 +2199,8 @@ mod tests {
op,
Op::ResolveElicitation {
server_name: "server-1".to_string(),
request_id: McpRequestId::String("request-1".to_string()),
decision: ElicitationAction::Accept,
request_id: request_id("request-1"),
decision: McpServerElicitationAction::Accept,
content: None,
meta: Some(serde_json::json!({
APPROVAL_PERSIST_KEY: APPROVAL_PERSIST_SESSION_VALUE,
@@ -2218,7 +2213,7 @@ mod tests {
fn empty_tool_approval_schema_always_allow_sets_persist_meta() {
let (tx, mut rx) = test_sender();
let thread_id = ThreadId::default();
let request = McpServerElicitationFormRequest::from_event(
let request = from_form_request(
thread_id,
form_request(
"Allow this request?",
@@ -2258,8 +2253,8 @@ mod tests {
op,
Op::ResolveElicitation {
server_name: "server-1".to_string(),
request_id: McpRequestId::String("request-1".to_string()),
decision: ElicitationAction::Accept,
request_id: request_id("request-1"),
decision: McpServerElicitationAction::Accept,
content: None,
meta: Some(serde_json::json!({
APPROVAL_PERSIST_KEY: APPROVAL_PERSIST_ALWAYS_VALUE,
@@ -2272,7 +2267,7 @@ mod tests {
fn ctrl_c_cancels_elicitation() {
let (tx, mut rx) = test_sender();
let thread_id = ThreadId::default();
let request = McpServerElicitationFormRequest::from_event(
let request = from_form_request(
thread_id,
form_request(
"Allow this request?",
@@ -2311,8 +2306,8 @@ mod tests {
op,
Op::ResolveElicitation {
server_name: "server-1".to_string(),
request_id: McpRequestId::String("request-1".to_string()),
decision: ElicitationAction::Cancel,
request_id: request_id("request-1"),
decision: McpServerElicitationAction::Cancel,
content: None,
meta: None,
}
@@ -2322,7 +2317,7 @@ mod tests {
#[test]
fn queues_requests_fifo() {
let (tx, _rx) = test_sender();
let first = McpServerElicitationFormRequest::from_event(
let first = from_form_request(
ThreadId::default(),
form_request(
"First",
@@ -2339,7 +2334,7 @@ mod tests {
),
)
.expect("expected supported form");
let second = McpServerElicitationFormRequest::from_event(
let second = from_form_request(
ThreadId::default(),
form_request(
"Second",
@@ -2356,7 +2351,7 @@ mod tests {
),
)
.expect("expected supported form");
let third = McpServerElicitationFormRequest::from_event(
let third = from_form_request(
ThreadId::default(),
form_request(
"Third",
@@ -2405,7 +2400,7 @@ mod tests {
},
});
let mut overlay = McpServerElicitationOverlay::new(
McpServerElicitationFormRequest::from_event(
from_form_request(
thread_id,
form_request("First", supported_form_schema.clone(), /*meta*/ None),
)
@@ -2416,16 +2411,18 @@ mod tests {
/*disable_paste_burst*/ false,
);
overlay.try_consume_mcp_server_elicitation_request(
McpServerElicitationFormRequest::from_event(
McpServerElicitationFormRequest::from_app_server_request(
thread_id,
ElicitationRequestEvent {
request_id("request-2"),
McpServerElicitationRequestParams {
thread_id: "thread-1".to_string(),
turn_id: Some("turn-2".to_string()),
server_name: "server-1".to_string(),
id: McpRequestId::String("request-2".to_string()),
request: ElicitationRequest::Form {
request: McpServerElicitationRequest::Form {
meta: None,
message: "Second".to_string(),
requested_schema: supported_form_schema,
requested_schema: serde_json::from_value(supported_form_schema)
.expect("test schema should deserialize"),
},
},
)
@@ -2435,7 +2432,7 @@ mod tests {
assert!(
overlay.dismiss_app_server_request(&ResolvedAppServerRequest::McpElicitation {
server_name: "server-1".to_string(),
request_id: McpRequestId::String("request-1".to_string()),
request_id: request_id("request-1"),
})
);
assert_eq!(overlay.request.message, "Second");
@@ -2447,7 +2444,7 @@ mod tests {
assert!(
overlay.dismiss_app_server_request(&ResolvedAppServerRequest::McpElicitation {
server_name: "server-1".to_string(),
request_id: McpRequestId::String("request-2".to_string()),
request_id: request_id("request-2"),
})
);
assert!(overlay.is_complete());
@@ -2460,7 +2457,7 @@ mod tests {
#[test]
fn boolean_form_snapshot() {
let (tx, _rx) = test_sender();
let request = McpServerElicitationFormRequest::from_event(
let request = from_form_request(
ThreadId::default(),
form_request(
"Allow this request?",
@@ -2493,7 +2490,7 @@ mod tests {
#[test]
fn approval_form_tool_approval_snapshot() {
let (tx, _rx) = test_sender();
let request = McpServerElicitationFormRequest::from_event(
let request = from_form_request(
ThreadId::default(),
form_request(
"Allow this request?",
@@ -2520,7 +2517,7 @@ mod tests {
#[test]
fn message_only_form_snapshot() {
let (tx, _rx) = test_sender();
let request = McpServerElicitationFormRequest::from_event(
let request = from_form_request(
ThreadId::default(),
form_request(
"Boolean elicit MCP example: do you confirm?",
@@ -2543,7 +2540,7 @@ mod tests {
#[test]
fn message_only_form_with_persist_options_snapshot() {
let (tx, _rx) = test_sender();
let request = McpServerElicitationFormRequest::from_event(
let request = from_form_request(
ThreadId::default(),
form_request(
"Boolean elicit MCP example: do you confirm?",
@@ -2571,7 +2568,7 @@ mod tests {
#[test]
fn approval_form_tool_approval_with_persist_options_snapshot() {
let (tx, _rx) = test_sender();
let request = McpServerElicitationFormRequest::from_event(
let request = from_form_request(
ThreadId::default(),
form_request(
"Allow this request?",
@@ -2601,7 +2598,7 @@ mod tests {
#[test]
fn approval_form_tool_approval_with_param_summary_snapshot() {
let (tx, _rx) = test_sender();
let request = McpServerElicitationFormRequest::from_event(
let request = from_form_request(
ThreadId::default(),
form_request(
"Allow Calendar to create an event",
+17 -15
View File
@@ -31,11 +31,11 @@ use crate::render::renderable::RenderableItem;
use crate::tui::FrameRequester;
pub(crate) use bottom_pane_view::BottomPaneView;
use bottom_pane_view::ViewCompletion;
use codex_app_server_protocol::ToolRequestUserInputParams;
use codex_core_skills::model::SkillMetadata;
use codex_features::Features;
use codex_file_search::FileMatch;
use codex_plugin::PluginCapabilitySummary;
use codex_protocol::request_user_input::RequestUserInputEvent;
use codex_protocol::user_input::TextElement;
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
@@ -1205,7 +1205,7 @@ impl BottomPane {
}
/// Called when the agent requests user input.
pub fn push_user_input_request(&mut self, request: RequestUserInputEvent) {
pub fn push_user_input_request(&mut self, request: ToolRequestUserInputParams) {
let request = if let Some(view) = self.view_stack.last_mut() {
match view.try_consume_user_input_request(request) {
Some(request) => request,
@@ -1567,14 +1567,13 @@ impl Renderable for BottomPane {
mod tests {
use super::*;
use crate::app::app_server_requests::ResolvedAppServerRequest;
use crate::app_command::AppCommand;
use crate::app_command::AppCommand as Op;
use crate::app_event::AppEvent;
use crate::status_indicator_widget::STATUS_DETAILS_DEFAULT_MAX_LINES;
use crate::status_indicator_widget::StatusDetailsCapitalization;
use crate::test_support::PathBufExt;
use crate::test_support::test_path_buf;
use codex_protocol::protocol::ReviewDecision;
use codex_protocol::protocol::SkillScope;
use codex_app_server_protocol::CommandExecutionApprovalDecision;
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
use crossterm::event::KeyEventKind;
@@ -1634,8 +1633,8 @@ mod tests {
command: vec!["echo".into(), "ok".into()],
reason: None,
available_decisions: vec![
codex_protocol::protocol::ReviewDecision::Approved,
codex_protocol::protocol::ReviewDecision::Abort,
CommandExecutionApprovalDecision::Accept,
CommandExecutionApprovalDecision::Cancel,
],
network_approval_context: None,
additional_permissions: None,
@@ -1890,14 +1889,17 @@ mod tests {
let mut approval_decision = None;
while let Ok(event) = rx.try_recv() {
if let AppEvent::SubmitThreadOp {
op: AppCommand::ExecApproval { decision, .. },
op: Op::ExecApproval { decision, .. },
..
} = event
{
approval_decision = Some(decision);
}
}
assert_eq!(approval_decision, Some(ReviewDecision::Approved));
assert_eq!(
approval_decision,
Some(CommandExecutionApprovalDecision::Accept)
);
}
#[test]
@@ -2354,7 +2356,7 @@ mod tests {
dependencies: None,
policy: None,
path_to_skills_md: test_path_buf("/tmp/test-skill/SKILL.md").abs(),
scope: SkillScope::User,
scope: crate::test_support::skill_scope_user(),
}]),
});
@@ -2371,7 +2373,7 @@ mod tests {
while let Ok(ev) = rx.try_recv() {
assert!(
!matches!(ev, AppEvent::CodexOp(AppCommand::Interrupt)),
!matches!(ev, AppEvent::CodexOp(Op::Interrupt)),
"expected Esc to not send Op::Interrupt when dismissing skill popup"
);
}
@@ -2409,7 +2411,7 @@ mod tests {
while let Ok(ev) = rx.try_recv() {
assert!(
!matches!(ev, AppEvent::CodexOp(AppCommand::Interrupt)),
!matches!(ev, AppEvent::CodexOp(Op::Interrupt)),
"expected Esc to not send Op::Interrupt while command popup is active"
);
}
@@ -2445,7 +2447,7 @@ mod tests {
while let Ok(ev) = rx.try_recv() {
assert!(
!matches!(ev, AppEvent::CodexOp(AppCommand::Interrupt)),
!matches!(ev, AppEvent::CodexOp(Op::Interrupt)),
"expected Esc to not send Op::Interrupt while typing `/agent`"
);
}
@@ -2490,7 +2492,7 @@ mod tests {
while let Ok(ev) = rx.try_recv() {
assert!(
!matches!(ev, AppEvent::CodexOp(AppCommand::Interrupt)),
!matches!(ev, AppEvent::CodexOp(Op::Interrupt)),
"expected Esc release after dismissing agent picker to not interrupt"
);
}
@@ -2520,7 +2522,7 @@ mod tests {
pane.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
assert!(
matches!(rx.try_recv(), Ok(AppEvent::CodexOp(AppCommand::Interrupt))),
matches!(rx.try_recv(), Ok(AppEvent::CodexOp(Op::Interrupt))),
"expected Esc to send Op::Interrupt while a task is running"
);
}
@@ -32,10 +32,13 @@ use crate::history_cell;
use crate::render::renderable::Renderable;
#[cfg(test)]
use codex_protocol::protocol::Op;
use codex_protocol::request_user_input::RequestUserInputAnswer;
use codex_protocol::request_user_input::RequestUserInputEvent;
use codex_protocol::request_user_input::RequestUserInputResponse;
use crate::app_command::AppCommand as Op;
use codex_app_server_protocol::ToolRequestUserInputAnswer;
#[cfg(test)]
use codex_app_server_protocol::ToolRequestUserInputOption;
use codex_app_server_protocol::ToolRequestUserInputParams;
use codex_app_server_protocol::ToolRequestUserInputQuestion;
use codex_app_server_protocol::ToolRequestUserInputResponse;
use codex_protocol::user_input::TextElement;
use unicode_width::UnicodeWidthStr;
@@ -122,9 +125,9 @@ impl FooterTip {
pub(crate) struct RequestUserInputOverlay {
app_event_tx: AppEventSender,
request: RequestUserInputEvent,
request: ToolRequestUserInputParams,
// Queue of incoming requests to process after the current one.
queue: VecDeque<RequestUserInputEvent>,
queue: VecDeque<ToolRequestUserInputParams>,
// Reuse the shared chat composer so notes/freeform answers match the
// primary input styling and behavior.
composer: ChatComposer,
@@ -139,7 +142,7 @@ pub(crate) struct RequestUserInputOverlay {
impl RequestUserInputOverlay {
pub(crate) fn new(
request: RequestUserInputEvent,
request: ToolRequestUserInputParams,
app_event_tx: AppEventSender,
has_input_focus: bool,
enhanced_keys_supported: bool,
@@ -179,9 +182,7 @@ impl RequestUserInputOverlay {
self.current_idx
}
fn current_question(
&self,
) -> Option<&codex_protocol::request_user_input::RequestUserInputQuestion> {
fn current_question(&self) -> Option<&ToolRequestUserInputQuestion> {
self.request.questions.get(self.current_index())
}
@@ -586,9 +587,7 @@ impl RequestUserInputOverlay {
self.pending_submission_draft = None;
}
fn options_len_for_question(
question: &codex_protocol::request_user_input::RequestUserInputQuestion,
) -> usize {
fn options_len_for_question(question: &ToolRequestUserInputQuestion) -> usize {
let options_len = question
.options
.as_ref()
@@ -601,9 +600,7 @@ impl RequestUserInputOverlay {
}
}
fn other_option_enabled_for_question(
question: &codex_protocol::request_user_input::RequestUserInputQuestion,
) -> bool {
fn other_option_enabled_for_question(question: &ToolRequestUserInputQuestion) -> bool {
question.is_other
&& question
.options
@@ -612,7 +609,7 @@ impl RequestUserInputOverlay {
}
fn option_label_for_index(
question: &codex_protocol::request_user_input::RequestUserInputQuestion,
question: &ToolRequestUserInputQuestion,
idx: usize,
) -> Option<String> {
let options = question.options.as_ref()?;
@@ -753,14 +750,14 @@ impl RequestUserInputOverlay {
}
answers.insert(
question.id.clone(),
RequestUserInputAnswer {
ToolRequestUserInputAnswer {
answers: answer_list,
},
);
}
self.app_event_tx.user_input_answer(
self.request.turn_id.clone(),
RequestUserInputResponse {
ToolRequestUserInputResponse {
answers: answers.clone(),
},
);
@@ -781,8 +778,8 @@ impl RequestUserInputOverlay {
let queue_len = self.queue.len();
self.queue
.retain(|queued_request| queued_request.call_id != *call_id);
if self.request.call_id == *call_id {
.retain(|queued_request| queued_request.item_id != *call_id);
if self.request.item_id == *call_id {
self.advance_queue_or_complete();
return true;
}
@@ -1294,8 +1291,8 @@ impl BottomPaneView for RequestUserInputOverlay {
fn try_consume_user_input_request(
&mut self,
request: RequestUserInputEvent,
) -> Option<RequestUserInputEvent> {
request: ToolRequestUserInputParams,
) -> Option<ToolRequestUserInputParams> {
self.queue.push_back(request);
None
}
@@ -1308,12 +1305,9 @@ impl BottomPaneView for RequestUserInputOverlay {
#[cfg(test)]
mod tests {
use super::*;
use crate::app_command::AppCommand;
use crate::app_event::AppEvent;
use crate::bottom_pane::selection_popup_common::menu_surface_inset;
use crate::render::renderable::Renderable;
use codex_protocol::request_user_input::RequestUserInputQuestion;
use codex_protocol::request_user_input::RequestUserInputQuestionOption;
use pretty_assertions::assert_eq;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
@@ -1341,23 +1335,23 @@ mod tests {
);
}
fn question_with_options(id: &str, header: &str) -> RequestUserInputQuestion {
RequestUserInputQuestion {
fn question_with_options(id: &str, header: &str) -> ToolRequestUserInputQuestion {
ToolRequestUserInputQuestion {
id: id.to_string(),
header: header.to_string(),
question: "Choose an option.".to_string(),
is_other: false,
is_secret: false,
options: Some(vec![
RequestUserInputQuestionOption {
ToolRequestUserInputOption {
label: "Option 1".to_string(),
description: "First choice.".to_string(),
},
RequestUserInputQuestionOption {
ToolRequestUserInputOption {
label: "Option 2".to_string(),
description: "Second choice.".to_string(),
},
RequestUserInputQuestionOption {
ToolRequestUserInputOption {
label: "Option 3".to_string(),
description: "Third choice.".to_string(),
},
@@ -1365,23 +1359,23 @@ mod tests {
}
}
fn question_with_options_and_other(id: &str, header: &str) -> RequestUserInputQuestion {
RequestUserInputQuestion {
fn question_with_options_and_other(id: &str, header: &str) -> ToolRequestUserInputQuestion {
ToolRequestUserInputQuestion {
id: id.to_string(),
header: header.to_string(),
question: "Choose an option.".to_string(),
is_other: true,
is_secret: false,
options: Some(vec![
RequestUserInputQuestionOption {
ToolRequestUserInputOption {
label: "Option 1".to_string(),
description: "First choice.".to_string(),
},
RequestUserInputQuestionOption {
ToolRequestUserInputOption {
label: "Option 2".to_string(),
description: "Second choice.".to_string(),
},
RequestUserInputQuestionOption {
ToolRequestUserInputOption {
label: "Option 3".to_string(),
description: "Third choice.".to_string(),
},
@@ -1389,27 +1383,27 @@ mod tests {
}
}
fn question_with_wrapped_options(id: &str, header: &str) -> RequestUserInputQuestion {
RequestUserInputQuestion {
fn question_with_wrapped_options(id: &str, header: &str) -> ToolRequestUserInputQuestion {
ToolRequestUserInputQuestion {
id: id.to_string(),
header: header.to_string(),
question: "Choose the next step for this task.".to_string(),
is_other: false,
is_secret: false,
options: Some(vec![
RequestUserInputQuestionOption {
ToolRequestUserInputOption {
label: "Discuss a code change".to_string(),
description:
"Walk through a plan, then implement it together with careful checks."
.to_string(),
},
RequestUserInputQuestionOption {
ToolRequestUserInputOption {
label: "Run targeted tests".to_string(),
description:
"Pick the most relevant crate and validate the current behavior first."
.to_string(),
},
RequestUserInputQuestionOption {
ToolRequestUserInputOption {
label: "Review the diff".to_string(),
description:
"Summarize the changes and highlight the most important risks and gaps."
@@ -1419,19 +1413,19 @@ mod tests {
}
}
fn question_with_very_long_option_text(id: &str, header: &str) -> RequestUserInputQuestion {
RequestUserInputQuestion {
fn question_with_very_long_option_text(id: &str, header: &str) -> ToolRequestUserInputQuestion {
ToolRequestUserInputQuestion {
id: id.to_string(),
header: header.to_string(),
question: "Choose one option.".to_string(),
is_other: false,
is_secret: false,
options: Some(vec![
RequestUserInputQuestionOption {
ToolRequestUserInputOption {
label: "Job: running/completed/failed/expired; Run/Experiment: succeeded/failed/unknown (Recommended when triaging long-running background work and status transitions)".to_string(),
description: "Keep async job statuses for progress tracking and include enough context for debugging retries, stale workers, and unexpected expiration paths.".to_string(),
},
RequestUserInputQuestionOption {
ToolRequestUserInputOption {
label: "Add a short status model".to_string(),
description: "Simpler labels with less detail for quick rollouts.".to_string(),
},
@@ -1439,8 +1433,8 @@ mod tests {
}
}
fn question_with_long_scroll_options(id: &str, header: &str) -> RequestUserInputQuestion {
RequestUserInputQuestion {
fn question_with_long_scroll_options(id: &str, header: &str) -> ToolRequestUserInputQuestion {
ToolRequestUserInputQuestion {
id: id.to_string(),
header: header.to_string(),
question:
@@ -1449,19 +1443,19 @@ mod tests {
is_other: false,
is_secret: false,
options: Some(vec![
RequestUserInputQuestionOption {
ToolRequestUserInputOption {
label: "Use Detailed Hint A (Recommended)".to_string(),
description: "Select this if you want a deliberately overextended explanatory hint that reads like a miniature specification, including context, rationale, expected behavior, and an explicit statement that this choice is mainly for testing how gracefully the interface wraps, truncates, and preserves readability under unusually verbose helper text conditions.".to_string(),
},
RequestUserInputQuestionOption {
ToolRequestUserInputOption {
label: "Use Detailed Hint B".to_string(),
description: "Select this if you want an equally verbose but differently phrased guidance block that emphasizes user-facing clarity, spacing tolerance, multiline wrapping, visual hierarchy interactions, and whether long descriptive metadata remains understandable when scanned quickly in a constrained layout where cognitive load is already high.".to_string(),
},
RequestUserInputQuestionOption {
ToolRequestUserInputOption {
label: "Use Detailed Hint C".to_string(),
description: "Select this when you specifically want to verify that navigating downward will keep the currently highlighted option visible, even when previous options consume many wrapped lines and would otherwise push the selection out of the viewport.".to_string(),
},
RequestUserInputQuestionOption {
ToolRequestUserInputOption {
label: "None of the above".to_string(),
description:
"Use this only if the previous long-form options do not apply.".to_string(),
@@ -1470,8 +1464,8 @@ mod tests {
}
}
fn question_without_options(id: &str, header: &str) -> RequestUserInputQuestion {
RequestUserInputQuestion {
fn question_without_options(id: &str, header: &str) -> ToolRequestUserInputQuestion {
ToolRequestUserInputQuestion {
id: id.to_string(),
header: header.to_string(),
question: "Share details.".to_string(),
@@ -1483,10 +1477,11 @@ mod tests {
fn request_event(
turn_id: &str,
questions: Vec<RequestUserInputQuestion>,
) -> RequestUserInputEvent {
RequestUserInputEvent {
call_id: "call-1".to_string(),
questions: Vec<ToolRequestUserInputQuestion>,
) -> ToolRequestUserInputParams {
ToolRequestUserInputParams {
thread_id: "thread-1".to_string(),
item_id: "call-1".to_string(),
turn_id: turn_id.to_string(),
questions,
}
@@ -1546,13 +1541,15 @@ mod tests {
/*enhanced_keys_supported*/ false,
/*disable_paste_burst*/ false,
);
overlay.try_consume_user_input_request(RequestUserInputEvent {
call_id: "call-2".to_string(),
overlay.try_consume_user_input_request(ToolRequestUserInputParams {
thread_id: "thread-1".to_string(),
item_id: "call-2".to_string(),
turn_id: "turn-2".to_string(),
questions: vec![question_with_options("q2", "Second")],
});
overlay.try_consume_user_input_request(RequestUserInputEvent {
call_id: "call-3".to_string(),
overlay.try_consume_user_input_request(ToolRequestUserInputParams {
thread_id: "thread-1".to_string(),
item_id: "call-3".to_string(),
turn_id: "turn-3".to_string(),
questions: vec![question_with_options("q3", "Third")],
});
@@ -1567,8 +1564,9 @@ mod tests {
fn resolved_request_dismisses_overlay_without_emitting_events() {
let (tx, mut rx) = test_sender();
let mut overlay = RequestUserInputOverlay::new(
RequestUserInputEvent {
call_id: "call-1".to_string(),
ToolRequestUserInputParams {
thread_id: "thread-1".to_string(),
item_id: "call-1".to_string(),
turn_id: "turn-1".to_string(),
questions: vec![question_with_options("q1", "First")],
},
@@ -1594,8 +1592,9 @@ mod tests {
fn resolved_current_request_advances_to_next_same_turn_prompt() {
let (tx, mut rx) = test_sender();
let mut overlay = RequestUserInputOverlay::new(
RequestUserInputEvent {
call_id: "call-1".to_string(),
ToolRequestUserInputParams {
thread_id: "thread-1".to_string(),
item_id: "call-1".to_string(),
turn_id: "turn-1".to_string(),
questions: vec![question_with_options("q1", "First")],
},
@@ -1604,8 +1603,9 @@ mod tests {
/*enhanced_keys_supported*/ false,
/*disable_paste_burst*/ false,
);
overlay.try_consume_user_input_request(RequestUserInputEvent {
call_id: "call-2".to_string(),
overlay.try_consume_user_input_request(ToolRequestUserInputParams {
thread_id: "thread-1".to_string(),
item_id: "call-2".to_string(),
turn_id: "turn-1".to_string(),
questions: vec![question_with_options("q2", "Second")],
});
@@ -1617,7 +1617,7 @@ mod tests {
);
assert!(!overlay.done, "newer same-turn prompt should stay pending");
assert_eq!(overlay.request.call_id, "call-2");
assert_eq!(overlay.request.item_id, "call-2");
assert_eq!(overlay.request.turn_id, "turn-1");
assert_eq!(overlay.request.questions[0].id, "q2");
assert!(
@@ -1630,8 +1630,9 @@ mod tests {
fn resolved_queued_request_removes_only_that_prompt() {
let (tx, mut rx) = test_sender();
let mut overlay = RequestUserInputOverlay::new(
RequestUserInputEvent {
call_id: "call-1".to_string(),
ToolRequestUserInputParams {
thread_id: "thread-1".to_string(),
item_id: "call-1".to_string(),
turn_id: "turn-1".to_string(),
questions: vec![question_with_options("q1", "First")],
},
@@ -1640,13 +1641,15 @@ mod tests {
/*enhanced_keys_supported*/ false,
/*disable_paste_burst*/ false,
);
overlay.try_consume_user_input_request(RequestUserInputEvent {
call_id: "call-2".to_string(),
overlay.try_consume_user_input_request(ToolRequestUserInputParams {
thread_id: "thread-1".to_string(),
item_id: "call-2".to_string(),
turn_id: "turn-1".to_string(),
questions: vec![question_with_options("q2", "Second")],
});
overlay.try_consume_user_input_request(RequestUserInputEvent {
call_id: "call-3".to_string(),
overlay.try_consume_user_input_request(ToolRequestUserInputParams {
thread_id: "thread-1".to_string(),
item_id: "call-3".to_string(),
turn_id: "turn-1".to_string(),
questions: vec![question_with_options("q3", "Third")],
});
@@ -1657,13 +1660,13 @@ mod tests {
})
);
assert_eq!(overlay.request.call_id, "call-1");
assert_eq!(overlay.request.item_id, "call-1");
assert!(
rx.try_recv().is_err(),
"dismissing a stale queued request should not emit an event"
);
overlay.submit_answers();
assert_eq!(overlay.request.call_id, "call-3");
assert_eq!(overlay.request.item_id, "call-3");
assert_eq!(overlay.request.questions[0].id, "q3");
assert!(
rx.try_recv().is_ok(),
@@ -1689,7 +1692,7 @@ mod tests {
overlay.submit_answers();
let event = rx.try_recv().expect("expected AppEvent");
let AppEvent::CodexOp(AppCommand::UserInputAnswer { id, response, .. }) = event else {
let AppEvent::CodexOp(Op::UserInputAnswer { id, response, .. }) = event else {
panic!("expected UserInputAnswer");
};
assert_eq!(id, "turn-1");
@@ -1711,7 +1714,7 @@ mod tests {
overlay.handle_key_event(KeyEvent::from(KeyCode::Enter));
let event = rx.try_recv().expect("expected AppEvent");
let AppEvent::CodexOp(AppCommand::UserInputAnswer { response, .. }) = event else {
let AppEvent::CodexOp(Op::UserInputAnswer { response, .. }) = event else {
panic!("expected UserInputAnswer");
};
let answer = response.answers.get("q1").expect("answer missing");
@@ -1747,19 +1750,19 @@ mod tests {
overlay.handle_key_event(KeyEvent::from(KeyCode::Enter));
let event = rx.try_recv().expect("expected AppEvent");
let AppEvent::CodexOp(AppCommand::UserInputAnswer { response, .. }) = event else {
let AppEvent::CodexOp(Op::UserInputAnswer { response, .. }) = event else {
panic!("expected UserInputAnswer");
};
let mut expected = HashMap::new();
expected.insert(
"q1".to_string(),
RequestUserInputAnswer {
ToolRequestUserInputAnswer {
answers: vec!["Option 1".to_string()],
},
);
expected.insert(
"q2".to_string(),
RequestUserInputAnswer {
ToolRequestUserInputAnswer {
answers: vec!["Option 1".to_string()],
},
);
@@ -1780,7 +1783,7 @@ mod tests {
overlay.handle_key_event(KeyEvent::from(KeyCode::Char('2')));
let event = rx.try_recv().expect("expected AppEvent");
let AppEvent::CodexOp(AppCommand::UserInputAnswer { response, .. }) = event else {
let AppEvent::CodexOp(Op::UserInputAnswer { response, .. }) = event else {
panic!("expected UserInputAnswer");
};
let answer = response.answers.get("q1").expect("answer missing");
@@ -2030,7 +2033,7 @@ mod tests {
overlay.handle_key_event(KeyEvent::from(KeyCode::Enter));
let event = rx.try_recv().expect("expected AppEvent");
let AppEvent::CodexOp(AppCommand::UserInputAnswer { response, .. }) = event else {
let AppEvent::CodexOp(Op::UserInputAnswer { response, .. }) = event else {
panic!("expected UserInputAnswer");
};
let answer = response.answers.get("q1").expect("answer missing");
@@ -2330,7 +2333,7 @@ mod tests {
overlay.submit_answers();
let event = rx.try_recv().expect("expected AppEvent");
let AppEvent::CodexOp(AppCommand::UserInputAnswer { response, .. }) = event else {
let AppEvent::CodexOp(Op::UserInputAnswer { response, .. }) = event else {
panic!("expected UserInputAnswer");
};
let answer = response.answers.get("q1").expect("answer missing");
@@ -2355,7 +2358,7 @@ mod tests {
overlay.submit_answers();
let event = rx.try_recv().expect("expected AppEvent");
let AppEvent::CodexOp(AppCommand::UserInputAnswer { response, .. }) = event else {
let AppEvent::CodexOp(Op::UserInputAnswer { response, .. }) = event else {
panic!("expected UserInputAnswer");
};
let answer = response.answers.get("q1").expect("answer missing");
@@ -2398,7 +2401,7 @@ mod tests {
overlay.submit_answers();
let event = rx.try_recv().expect("expected AppEvent");
let AppEvent::CodexOp(AppCommand::UserInputAnswer { response, .. }) = event else {
let AppEvent::CodexOp(Op::UserInputAnswer { response, .. }) = event else {
panic!("expected UserInputAnswer");
};
let answer = response.answers.get("q1").expect("answer missing");
@@ -2434,7 +2437,7 @@ mod tests {
overlay.submit_answers();
let event = rx.try_recv().expect("expected AppEvent");
let AppEvent::CodexOp(AppCommand::UserInputAnswer { response, .. }) = event else {
let AppEvent::CodexOp(Op::UserInputAnswer { response, .. }) = event else {
panic!("expected UserInputAnswer");
};
let answer = response.answers.get("q1").expect("answer missing");
@@ -2519,7 +2522,7 @@ mod tests {
overlay.submit_answers();
let event = rx.try_recv().expect("expected AppEvent");
let AppEvent::CodexOp(AppCommand::UserInputAnswer { response, .. }) = event else {
let AppEvent::CodexOp(Op::UserInputAnswer { response, .. }) = event else {
panic!("expected UserInputAnswer");
};
let answer = response.answers.get("q1").expect("answer missing");
@@ -2852,30 +2855,30 @@ mod tests {
let mut overlay = RequestUserInputOverlay::new(
request_event(
"turn-1",
vec![RequestUserInputQuestion {
vec![ToolRequestUserInputQuestion {
id: "q1".to_string(),
header: "Next Step".to_string(),
question: "What would you like to do next?".to_string(),
is_other: false,
is_secret: false,
options: Some(vec![
RequestUserInputQuestionOption {
ToolRequestUserInputOption {
label: "Discuss a code change (Recommended)".to_string(),
description: "Walk through a plan and edit code together.".to_string(),
},
RequestUserInputQuestionOption {
ToolRequestUserInputOption {
label: "Run tests".to_string(),
description: "Pick a crate and run its tests.".to_string(),
},
RequestUserInputQuestionOption {
ToolRequestUserInputOption {
label: "Review a diff".to_string(),
description: "Summarize or review current changes.".to_string(),
},
RequestUserInputQuestionOption {
ToolRequestUserInputOption {
label: "Refactor".to_string(),
description: "Tighten structure and remove dead code.".to_string(),
},
RequestUserInputQuestionOption {
ToolRequestUserInputOption {
label: "Ship it".to_string(),
description: "Finalize and open a PR.".to_string(),
},
@@ -2904,30 +2907,30 @@ mod tests {
let mut overlay = RequestUserInputOverlay::new(
request_event(
"turn-1",
vec![RequestUserInputQuestion {
vec![ToolRequestUserInputQuestion {
id: "q1".to_string(),
header: "Next Step".to_string(),
question: "What would you like to do next?".to_string(),
is_other: false,
is_secret: false,
options: Some(vec![
RequestUserInputQuestionOption {
ToolRequestUserInputOption {
label: "Discuss a code change (Recommended)".to_string(),
description: "Walk through a plan and edit code together.".to_string(),
},
RequestUserInputQuestionOption {
ToolRequestUserInputOption {
label: "Run tests".to_string(),
description: "Pick a crate and run its tests.".to_string(),
},
RequestUserInputQuestionOption {
ToolRequestUserInputOption {
label: "Review a diff".to_string(),
description: "Summarize or review current changes.".to_string(),
},
RequestUserInputQuestionOption {
ToolRequestUserInputOption {
label: "Refactor".to_string(),
description: "Tighten structure and remove dead code.".to_string(),
},
RequestUserInputQuestionOption {
ToolRequestUserInputOption {
label: "Ship it".to_string(),
description: "Finalize and open a PR.".to_string(),
},