Simplify TUI user input and approval models

This commit is contained in:
Eric Traut
2026-04-29 01:09:34 -07:00
parent a5d018ead5
commit 8bc5ba4694
24 changed files with 199 additions and 351 deletions
@@ -579,7 +579,7 @@ mod tests {
use super::super::ThreadBufferedEvent;
use super::super::ThreadEventStore;
use crate::app_command::AppCommand as Op;
use crate::approval_display::ReviewDecision;
use codex_app_server_protocol::CommandExecutionApprovalDecision;
use codex_app_server_protocol::CommandExecutionRequestApprovalParams;
use codex_app_server_protocol::FileChangeRequestApprovalParams;
use codex_app_server_protocol::McpElicitationObjectType;
@@ -766,7 +766,7 @@ mod tests {
store.note_outbound_op(&Op::ExecApproval {
id: "approval-1".to_string(),
turn_id: Some("turn-1".to_string()),
decision: ReviewDecision::Approved.into(),
decision: CommandExecutionApprovalDecision::Accept,
});
let snapshot = store.snapshot();
@@ -919,7 +919,7 @@ mod tests {
store.note_outbound_op(&Op::ExecApproval {
id: "call-1".to_string(),
turn_id: Some("turn-1".to_string()),
decision: ReviewDecision::Approved.into(),
decision: CommandExecutionApprovalDecision::Accept,
});
assert_eq!(store.has_pending_thread_approvals(), false);
+2 -2
View File
@@ -63,6 +63,7 @@ use codex_app_server_protocol::TurnCompletedNotification;
use codex_app_server_protocol::TurnError as AppServerTurnError;
use codex_app_server_protocol::TurnStartedNotification;
use codex_app_server_protocol::TurnStatus;
use codex_app_server_protocol::UserInput;
use codex_app_server_protocol::UserInput as AppServerUserInput;
use codex_app_server_protocol::WarningNotification;
use codex_otel::SessionTelemetry;
@@ -76,7 +77,6 @@ use codex_protocol::models::NetworkPermissions;
use codex_protocol::models::PermissionProfile;
use codex_protocol::request_permissions::RequestPermissionProfile;
use codex_protocol::user_input::TextElement;
use codex_protocol::user_input::UserInput;
use codex_utils_absolute_path::AbsolutePathBuf;
use crossterm::event::KeyModifiers;
use insta::assert_snapshot;
@@ -4496,7 +4496,7 @@ async fn backtrack_resubmit_preserves_data_image_urls_in_user_turn() {
assert!(items.iter().any(|item| {
matches!(
item,
UserInput::Image { image_url } if image_url == &data_image_url
UserInput::Image { url } if url == &data_image_url
)
}));
}
+1 -1
View File
@@ -257,7 +257,7 @@ impl App {
changes: self
.thread_file_change_changes(thread_id, &params.turn_id, &params.item_id)
.await
.map(crate::app_server_approval_conversions::file_update_changes_to_core)
.map(crate::app_server_approval_conversions::file_update_changes_to_display)
.unwrap_or_default(),
}),
),
+1 -1
View File
@@ -6,6 +6,7 @@ use codex_app_server_protocol::FileChangeApprovalDecision;
use codex_app_server_protocol::ReviewTarget;
use codex_app_server_protocol::ThreadRealtimeAudioChunk;
use codex_app_server_protocol::ThreadRealtimeStartTransport;
use codex_app_server_protocol::UserInput;
use codex_config::types::ApprovalsReviewer;
use codex_protocol::approvals::ElicitationAction;
use codex_protocol::approvals::GuardianAssessmentEvent;
@@ -19,7 +20,6 @@ use codex_protocol::models::PermissionProfile;
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
use codex_protocol::request_permissions::RequestPermissionsResponse;
use codex_protocol::request_user_input::RequestUserInputResponse;
use codex_protocol::user_input::UserInput;
use serde::Serialize;
use serde_json::Value;
+5
View File
@@ -1,3 +1,8 @@
//! Convenience sender for app events and common outbound TUI commands.
//!
//! This wraps the raw channel so call sites can submit typed `AppCommand`s
//! without duplicating event construction or session logging behavior.
use std::path::PathBuf;
use crate::app_command::AppCommand;
@@ -1,37 +1,19 @@
//! Narrow conversion helpers for approval-related app-server payloads.
//!
//! The TUI mostly keeps app-server approval types intact. These helpers cover
//! the remaining cases where the UI consumes a private file-change display
//! model or needs to translate a granted permission response for outbound
//! submission.
use crate::diff_model::FileChange;
use codex_app_server_protocol::AdditionalNetworkPermissions;
use codex_app_server_protocol::FileUpdateChange;
use codex_app_server_protocol::GrantedPermissionProfile;
use codex_app_server_protocol::NetworkApprovalContext;
use codex_app_server_protocol::NetworkApprovalContext as AppServerNetworkApprovalContext;
use codex_app_server_protocol::NetworkApprovalProtocol;
use codex_app_server_protocol::PatchChangeKind;
use codex_protocol::request_permissions::RequestPermissionProfile as CoreRequestPermissionProfile;
use std::collections::HashMap;
use std::path::PathBuf;
pub(crate) fn network_approval_context_to_core(
value: AppServerNetworkApprovalContext,
) -> NetworkApprovalContext {
NetworkApprovalContext {
host: value.host,
protocol: match value.protocol {
codex_app_server_protocol::NetworkApprovalProtocol::Http => {
NetworkApprovalProtocol::Http
}
codex_app_server_protocol::NetworkApprovalProtocol::Https => {
NetworkApprovalProtocol::Https
}
codex_app_server_protocol::NetworkApprovalProtocol::Socks5Tcp => {
NetworkApprovalProtocol::Socks5Tcp
}
codex_app_server_protocol::NetworkApprovalProtocol::Socks5Udp => {
NetworkApprovalProtocol::Socks5Udp
}
},
}
}
pub(crate) fn granted_permission_profile_from_request(
value: CoreRequestPermissionProfile,
) -> GrantedPermissionProfile {
@@ -43,7 +25,7 @@ pub(crate) fn granted_permission_profile_from_request(
}
}
pub(crate) fn file_update_changes_to_core(
pub(crate) fn file_update_changes_to_display(
changes: Vec<FileUpdateChange>,
) -> HashMap<PathBuf, FileChange> {
changes
@@ -69,13 +51,10 @@ pub(crate) fn file_update_changes_to_core(
#[cfg(test)]
mod tests {
use super::file_update_changes_to_core;
use super::file_update_changes_to_display;
use super::granted_permission_profile_from_request;
use super::network_approval_context_to_core;
use crate::diff_model::FileChange;
use codex_app_server_protocol::FileUpdateChange;
use codex_app_server_protocol::NetworkApprovalContext;
use codex_app_server_protocol::NetworkApprovalProtocol;
use codex_app_server_protocol::PatchChangeKind;
use codex_protocol::request_permissions::RequestPermissionProfile as CoreRequestPermissionProfile;
use codex_utils_absolute_path::AbsolutePathBuf;
@@ -88,23 +67,9 @@ mod tests {
}
#[test]
fn converts_app_server_network_approval_context_to_core() {
fn converts_file_update_changes_to_display() {
assert_eq!(
network_approval_context_to_core(codex_app_server_protocol::NetworkApprovalContext {
host: "example.com".to_string(),
protocol: codex_app_server_protocol::NetworkApprovalProtocol::Socks5Tcp,
}),
NetworkApprovalContext {
host: "example.com".to_string(),
protocol: NetworkApprovalProtocol::Socks5Tcp,
}
);
}
#[test]
fn converts_file_update_changes_to_core() {
assert_eq!(
file_update_changes_to_core(vec![FileUpdateChange {
file_update_changes_to_display(vec![FileUpdateChange {
path: "foo.txt".to_string(),
kind: PatchChangeKind::Add,
diff: "hello\n".to_string(),
+10 -4
View File
@@ -1,3 +1,8 @@
//! App-server session facade used by the TUI event loop.
//!
//! This module owns the typed JSON-RPC calls needed by the TUI and keeps
//! request/response plumbing out of `App` and `ChatWidget`.
use crate::bottom_pane::FeedbackAudience;
#[cfg(test)]
use crate::legacy_core::append_message_history_entry;
@@ -94,6 +99,7 @@ use codex_app_server_protocol::TurnStartParams;
use codex_app_server_protocol::TurnStartResponse;
use codex_app_server_protocol::TurnSteerParams;
use codex_app_server_protocol::TurnSteerResponse;
use codex_app_server_protocol::UserInput;
use codex_otel::TelemetryAuthMode;
use codex_protocol::ThreadId;
use codex_protocol::approvals::GuardianAssessmentEvent;
@@ -496,7 +502,7 @@ impl AppServerSession {
pub(crate) async fn turn_start(
&mut self,
thread_id: ThreadId,
items: Vec<codex_protocol::user_input::UserInput>,
items: Vec<UserInput>,
cwd: PathBuf,
approval_policy: AskForApproval,
approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer,
@@ -535,7 +541,7 @@ impl AppServerSession {
request_id,
params: TurnStartParams {
thread_id: thread_id.to_string(),
input: items.into_iter().map(Into::into).collect(),
input: items,
responsesapi_client_metadata: None,
environments: None,
cwd: Some(cwd),
@@ -584,7 +590,7 @@ impl AppServerSession {
&mut self,
thread_id: ThreadId,
turn_id: String,
items: Vec<codex_protocol::user_input::UserInput>,
items: Vec<UserInput>,
) -> std::result::Result<TurnSteerResponse, TypedRequestError> {
let request_id = self.next_request_id();
self.client
@@ -592,7 +598,7 @@ impl AppServerSession {
request_id,
params: TurnSteerParams {
thread_id: thread_id.to_string(),
input: items.into_iter().map(Into::into).collect(),
input: items,
responsesapi_client_metadata: None,
expected_turn_id: turn_id,
},
-45
View File
@@ -1,45 +0,0 @@
//! Display-level approval decisions used by the TUI approval overlay.
use codex_app_server_protocol::CommandExecutionApprovalDecision;
use codex_protocol::approvals::ExecPolicyAmendment;
use codex_protocol::approvals::NetworkPolicyAmendment;
use serde::Deserialize;
use serde::Serialize;
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum ReviewDecision {
Approved,
ApprovedExecpolicyAmendment {
proposed_execpolicy_amendment: ExecPolicyAmendment,
},
ApprovedForSession,
NetworkPolicyAmendment {
network_policy_amendment: NetworkPolicyAmendment,
},
#[default]
Denied,
TimedOut,
Abort,
}
impl From<ReviewDecision> for CommandExecutionApprovalDecision {
fn from(value: ReviewDecision) -> Self {
match value {
ReviewDecision::Approved => Self::Accept,
ReviewDecision::ApprovedExecpolicyAmendment {
proposed_execpolicy_amendment,
} => Self::AcceptWithExecpolicyAmendment {
execpolicy_amendment: proposed_execpolicy_amendment.into(),
},
ReviewDecision::ApprovedForSession => Self::AcceptForSession,
ReviewDecision::NetworkPolicyAmendment {
network_policy_amendment,
} => Self::ApplyNetworkPolicyAmendment {
network_policy_amendment: network_policy_amendment.into(),
},
ReviewDecision::Denied | ReviewDecision::TimedOut => Self::Decline,
ReviewDecision::Abort => Self::Cancel,
}
}
}
+36 -22
View File
@@ -1,15 +1,20 @@
//! TUI-owned approval request models used while rendering and queueing prompts.
//!
//! These structs normalize app-server request params into the shape the TUI
//! needs while an approval may be deferred behind streaming output. Exec
//! approvals keep app-server decision and permission types; patch approvals add
//! the file-change display model collected from nearby thread items.
use std::collections::HashMap;
use std::path::PathBuf;
use crate::approval_display::ReviewDecision;
use crate::diff_model::FileChange;
use codex_app_server_protocol::AdditionalPermissionProfile;
use codex_app_server_protocol::CommandExecutionApprovalDecision;
use codex_app_server_protocol::ExecPolicyAmendment;
use codex_app_server_protocol::NetworkApprovalContext;
use codex_protocol::approvals::ExecPolicyAmendment;
use codex_protocol::approvals::NetworkPolicyAmendment;
use codex_protocol::approvals::NetworkPolicyRuleAction;
use codex_protocol::parse_command::ParsedCommand;
use codex_app_server_protocol::NetworkPolicyAmendment;
use codex_app_server_protocol::NetworkPolicyRuleAction;
use codex_utils_absolute_path::AbsolutePathBuf;
use serde::Deserialize;
use serde::Serialize;
@@ -29,12 +34,11 @@ pub(crate) struct ExecApprovalRequestEvent {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) proposed_network_policy_amendments: Option<Vec<NetworkPolicyAmendment>>,
#[serde(default)]
pub(crate) available_decisions: Option<Vec<ReviewDecision>>,
pub(crate) available_decisions: Option<Vec<CommandExecutionApprovalDecision>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) network_approval_context: Option<NetworkApprovalContext>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) additional_permissions: Option<codex_protocol::models::AdditionalPermissionProfile>,
pub(crate) parsed_cmd: Vec<ParsedCommand>,
pub(crate) additional_permissions: Option<AdditionalPermissionProfile>,
}
impl ExecApprovalRequestEvent {
@@ -44,7 +48,7 @@ impl ExecApprovalRequestEvent {
.unwrap_or_else(|| self.call_id.clone())
}
pub(crate) fn effective_available_decisions(&self) -> Vec<ReviewDecision> {
pub(crate) fn effective_available_decisions(&self) -> Vec<CommandExecutionApprovalDecision> {
match &self.available_decisions {
Some(decisions) => decisions.clone(),
None => Self::default_available_decisions(
@@ -60,34 +64,44 @@ impl ExecApprovalRequestEvent {
network_approval_context: Option<&NetworkApprovalContext>,
proposed_execpolicy_amendment: Option<&ExecPolicyAmendment>,
proposed_network_policy_amendments: Option<&[NetworkPolicyAmendment]>,
additional_permissions: Option<&codex_protocol::models::AdditionalPermissionProfile>,
) -> Vec<ReviewDecision> {
additional_permissions: Option<&AdditionalPermissionProfile>,
) -> Vec<CommandExecutionApprovalDecision> {
if network_approval_context.is_some() {
let mut decisions = vec![ReviewDecision::Approved, ReviewDecision::ApprovedForSession];
let mut decisions = vec![
CommandExecutionApprovalDecision::Accept,
CommandExecutionApprovalDecision::AcceptForSession,
];
if let Some(amendment) = proposed_network_policy_amendments.and_then(|amendments| {
amendments
.iter()
.find(|amendment| amendment.action == NetworkPolicyRuleAction::Allow)
}) {
decisions.push(ReviewDecision::NetworkPolicyAmendment {
network_policy_amendment: amendment.clone(),
});
decisions.push(
CommandExecutionApprovalDecision::ApplyNetworkPolicyAmendment {
network_policy_amendment: amendment.clone(),
},
);
}
decisions.push(ReviewDecision::Abort);
decisions.push(CommandExecutionApprovalDecision::Cancel);
return decisions;
}
if additional_permissions.is_some() {
return vec![ReviewDecision::Approved, ReviewDecision::Abort];
return vec![
CommandExecutionApprovalDecision::Accept,
CommandExecutionApprovalDecision::Cancel,
];
}
let mut decisions = vec![ReviewDecision::Approved];
let mut decisions = vec![CommandExecutionApprovalDecision::Accept];
if let Some(prefix) = proposed_execpolicy_amendment {
decisions.push(ReviewDecision::ApprovedExecpolicyAmendment {
proposed_execpolicy_amendment: prefix.clone(),
});
decisions.push(
CommandExecutionApprovalDecision::AcceptWithExecpolicyAmendment {
execpolicy_amendment: prefix.clone(),
},
);
}
decisions.push(ReviewDecision::Abort);
decisions.push(CommandExecutionApprovalDecision::Cancel);
decisions
}
}
@@ -19,7 +19,6 @@ use crate::app::app_server_requests::ResolvedAppServerRequest;
use crate::app_command::AppCommand as Op;
use crate::app_event::AppEvent;
use crate::app_event_sender::AppEventSender;
use crate::approval_display::ReviewDecision;
use crate::bottom_pane::BottomPaneView;
use crate::bottom_pane::CancellationEvent;
use crate::bottom_pane::list_selection_view::ListSelectionView;
@@ -29,6 +28,7 @@ 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;
+54 -118
View File
@@ -46,8 +46,7 @@ use crate::app::app_server_requests::ResolvedAppServerRequest;
use crate::app_command::AppCommand;
use crate::app_event::HistoryLookupResponse;
use crate::app_event::RealtimeAudioDeviceKind;
use crate::app_server_approval_conversions::file_update_changes_to_core;
use crate::app_server_approval_conversions::network_approval_context_to_core;
use crate::app_server_approval_conversions::file_update_changes_to_display;
use crate::approval_events::ApplyPatchApprovalRequestEvent;
use crate::approval_events::ExecApprovalRequestEvent;
#[cfg(not(target_os = "linux"))]
@@ -126,6 +125,7 @@ use codex_app_server_protocol::ServerNotification;
use codex_app_server_protocol::ServerRequest;
use codex_app_server_protocol::SkillMetadata as ProtocolSkillMetadata;
use codex_app_server_protocol::SkillsListResponse;
use codex_app_server_protocol::TextElement as AppServerTextElement;
use codex_app_server_protocol::ThreadGoal as AppThreadGoal;
use codex_app_server_protocol::ThreadGoalStatus as AppThreadGoalStatus;
use codex_app_server_protocol::ThreadItem;
@@ -135,6 +135,7 @@ use codex_app_server_protocol::Turn;
use codex_app_server_protocol::TurnCompletedNotification;
use codex_app_server_protocol::TurnPlanStepStatus;
use codex_app_server_protocol::TurnStatus;
use codex_app_server_protocol::UserInput;
use codex_chatgpt::connectors;
use codex_config::ConfigLayerStackOrdering;
use codex_config::types::ApprovalsReviewer;
@@ -169,7 +170,6 @@ use codex_protocol::config_types::Settings;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::items::AgentMessageContent;
use codex_protocol::items::AgentMessageItem;
use codex_protocol::items::UserMessageItem;
use codex_protocol::models::MessagePhase;
use codex_protocol::models::local_image_label_text;
use codex_protocol::parse_command::ParsedCommand;
@@ -180,7 +180,6 @@ use codex_protocol::request_user_input::RequestUserInputEvent;
use codex_protocol::request_user_input::RequestUserInputQuestionOption;
use codex_protocol::user_input::ByteRange;
use codex_protocol::user_input::TextElement;
use codex_protocol::user_input::UserInput;
use codex_terminal_detection::Multiplexer;
use codex_terminal_detection::TerminalInfo;
use codex_terminal_detection::TerminalName;
@@ -361,8 +360,7 @@ use self::status_surfaces::CachedProjectRootName;
use self::status_surfaces::TerminalTitleStatusKind;
mod user_messages;
use self::user_messages::PendingSteerCompareKey;
use self::user_messages::RenderedUserMessageEvent;
use self::user_messages::UserMessageEvent;
use self::user_messages::UserMessageDisplay;
use crate::streaming::chunking::AdaptiveChunkingPolicy;
use crate::streaming::commit_tick::CommitTickScope;
use crate::streaming::commit_tick::run_commit_tick;
@@ -1007,7 +1005,7 @@ pub(crate) struct ChatWidget {
goal_status_active_turn_started_at: Option<Instant>,
external_editor_state: ExternalEditorState,
realtime_conversation: RealtimeConversationUiState,
last_rendered_user_message_event: Option<RenderedUserMessageEvent>,
last_rendered_user_message_display: Option<UserMessageDisplay>,
last_non_retry_error: Option<(String, String)>,
}
@@ -1230,6 +1228,10 @@ fn append_text_with_rebased_elements(
}));
}
fn app_server_text_elements(elements: &[TextElement]) -> Vec<AppServerTextElement> {
elements.iter().cloned().map(Into::into).collect()
}
fn build_placeholder_mapping(
local_images: Vec<LocalImageAttachment>,
next_label: &mut usize,
@@ -1442,14 +1444,14 @@ fn user_message_preview_text(
}
}
fn user_message_event_for_display(
fn user_message_display_for_history(
message: UserMessage,
history_record: &UserMessageHistoryRecord,
) -> UserMessageEvent {
) -> UserMessageDisplay {
let message = user_message_for_restore(message, history_record);
UserMessageEvent {
UserMessageDisplay {
message: message.text,
images: Some(message.remote_image_urls),
remote_image_urls: message.remote_image_urls,
local_images: message
.local_images
.into_iter()
@@ -1637,58 +1639,13 @@ fn exec_approval_request_from_params(
.unwrap_or_default(),
cwd: params.cwd.unwrap_or_else(|| fallback_cwd.clone()),
reason: params.reason,
network_approval_context: params
.network_approval_context
.map(network_approval_context_to_core),
additional_permissions: params.additional_permissions.map(Into::into),
network_approval_context: params.network_approval_context,
additional_permissions: params.additional_permissions,
turn_id: params.turn_id,
approval_id: params.approval_id,
proposed_execpolicy_amendment: params
.proposed_execpolicy_amendment
.map(codex_app_server_protocol::ExecPolicyAmendment::into_core),
proposed_network_policy_amendments: params.proposed_network_policy_amendments.map(
|amendments| {
amendments
.into_iter()
.map(codex_app_server_protocol::NetworkPolicyAmendment::into_core)
.collect()
},
),
available_decisions: params.available_decisions.map(|decisions| {
decisions
.into_iter()
.map(|decision| match decision {
codex_app_server_protocol::CommandExecutionApprovalDecision::Accept => {
crate::approval_display::ReviewDecision::Approved
}
codex_app_server_protocol::CommandExecutionApprovalDecision::AcceptForSession => {
crate::approval_display::ReviewDecision::ApprovedForSession
}
codex_app_server_protocol::CommandExecutionApprovalDecision::AcceptWithExecpolicyAmendment {
execpolicy_amendment,
} => crate::approval_display::ReviewDecision::ApprovedExecpolicyAmendment {
proposed_execpolicy_amendment: execpolicy_amendment.into_core(),
},
codex_app_server_protocol::CommandExecutionApprovalDecision::ApplyNetworkPolicyAmendment {
network_policy_amendment,
} => crate::approval_display::ReviewDecision::NetworkPolicyAmendment {
network_policy_amendment: network_policy_amendment.into_core(),
},
codex_app_server_protocol::CommandExecutionApprovalDecision::Decline => {
crate::approval_display::ReviewDecision::Denied
}
codex_app_server_protocol::CommandExecutionApprovalDecision::Cancel => {
crate::approval_display::ReviewDecision::Abort
}
})
.collect()
}),
parsed_cmd: params
.command_actions
.unwrap_or_default()
.into_iter()
.map(codex_app_server_protocol::CommandAction::into_core)
.collect(),
proposed_execpolicy_amendment: params.proposed_execpolicy_amendment,
proposed_network_policy_amendments: params.proposed_network_policy_amendments,
available_decisions: params.available_decisions,
}
}
@@ -3629,7 +3586,7 @@ impl ChatWidget {
let cell = if let Some(command) = guardian_command(&ev.action) {
history_cell::new_approval_decision_cell(
command,
crate::approval_display::ReviewDecision::Approved,
crate::history_cell::ReviewDecision::Approved,
history_cell::ApprovalDecisionActor::Guardian,
)
} else if let Some(summary) = guardian_action_summary(&ev.action) {
@@ -3649,7 +3606,7 @@ impl ChatWidget {
let cell = if let Some(command) = guardian_command(&ev.action) {
history_cell::new_approval_decision_cell(
command,
crate::approval_display::ReviewDecision::TimedOut,
crate::history_cell::ReviewDecision::TimedOut,
history_cell::ApprovalDecisionActor::Guardian,
)
} else {
@@ -3693,7 +3650,7 @@ impl ChatWidget {
let cell = if let Some(command) = guardian_command(&ev.action) {
history_cell::new_approval_decision_cell(
command,
crate::approval_display::ReviewDecision::Denied,
crate::history_cell::ReviewDecision::Denied,
history_cell::ApprovalDecisionActor::Guardian,
)
} else {
@@ -4522,11 +4479,7 @@ impl ChatWidget {
.unwrap_or_else(|_| ev.command.join(" "));
self.notify(Notification::ExecApprovalRequested { command });
let available_decisions = ev
.effective_available_decisions()
.into_iter()
.map(Into::into)
.collect();
let available_decisions = ev.effective_available_decisions();
let request = ApprovalRequest::Exec {
thread_id: self.thread_id.unwrap_or_default(),
thread_label: None,
@@ -4535,7 +4488,7 @@ impl ChatWidget {
reason: ev.reason,
available_decisions,
network_approval_context: ev.network_approval_context,
additional_permissions: ev.additional_permissions.map(Into::into),
additional_permissions: ev.additional_permissions,
};
self.bottom_pane
.push_approval_request(request, &self.config.features);
@@ -4960,7 +4913,7 @@ impl ChatWidget {
goal_status_active_turn_started_at: None,
external_editor_state: ExternalEditorState::Closed,
realtime_conversation: RealtimeConversationUiState::default(),
last_rendered_user_message_event: None,
last_rendered_user_message_display: None,
last_non_retry_error: None,
};
@@ -5601,7 +5554,7 @@ impl ChatWidget {
for image_url in &remote_image_urls {
items.push(UserInput::Image {
image_url: image_url.clone(),
url: image_url.clone(),
});
}
@@ -5614,7 +5567,7 @@ impl ChatWidget {
if !text.is_empty() {
items.push(UserInput::Text {
text: text.clone(),
text_elements: text_elements.clone(),
text_elements: app_server_text_elements(&text_elements),
});
}
@@ -5847,8 +5800,8 @@ impl ChatWidget {
.into_iter()
.map(|img| img.path)
.collect::<Vec<_>>();
self.last_rendered_user_message_event =
Some(Self::rendered_user_message_event_from_parts(
self.last_rendered_user_message_display =
Some(Self::user_message_display_from_parts(
text.clone(),
text_elements.clone(),
local_image_paths.clone(),
@@ -5862,8 +5815,8 @@ impl ChatWidget {
));
self.record_visible_user_turn_for_copy();
} else if !remote_image_urls.is_empty() {
self.last_rendered_user_message_event =
Some(Self::rendered_user_message_event_from_parts(
self.last_rendered_user_message_display =
Some(Self::user_message_display_from_parts(
String::new(),
Vec::new(),
Vec::new(),
@@ -5977,15 +5930,8 @@ impl ChatWidget {
let from_replay = render_source.is_replay();
let replay_kind = render_source.replay_kind();
match item {
ThreadItem::UserMessage { id, content } => {
let user_message = UserMessageItem {
id,
content: content
.into_iter()
.map(codex_app_server_protocol::UserInput::into_core)
.collect(),
};
self.on_committed_user_message(&user_message, from_replay);
ThreadItem::UserMessage { content, .. } => {
self.on_committed_user_message(&content, from_replay);
}
ThreadItem::AgentMessage {
id,
@@ -6118,7 +6064,7 @@ impl ChatWidget {
status,
codex_app_server_protocol::PatchApplyStatus::Failed
),
changes: file_update_changes_to_core(changes),
changes: file_update_changes_to_display(changes),
status: match status {
codex_app_server_protocol::PatchApplyStatus::Completed => {
codex_app_server_protocol::PatchApplyStatus::Completed
@@ -6647,7 +6593,7 @@ impl ChatWidget {
call_id: id,
turn_id: notification.turn_id,
auto_approved: false,
changes: file_update_changes_to_core(changes),
changes: file_update_changes_to_display(changes),
});
}
ThreadItem::McpToolCall {
@@ -6812,24 +6758,16 @@ impl ChatWidget {
self.request_redraw();
}
fn on_committed_user_message(&mut self, item: &UserMessageItem, from_replay: bool) {
let rendered_event = Self::rendered_user_message_event_from_inputs(&item.content);
let event = UserMessageEvent {
message: rendered_event.message,
images: (!rendered_event.remote_image_urls.is_empty())
.then_some(rendered_event.remote_image_urls),
local_images: rendered_event.local_images,
text_elements: rendered_event.text_elements,
};
fn on_committed_user_message(&mut self, items: &[UserInput], from_replay: bool) {
let display = Self::user_message_display_from_inputs(items);
if from_replay {
if !self.is_review_mode {
self.on_user_message_event(event);
self.on_user_message_display(display);
}
return;
}
let rendered = Self::rendered_user_message_event_from_event(&event);
let compare_key = Self::pending_steer_compare_key_from_item(item);
let compare_key = Self::pending_steer_compare_key_from_items(items);
if self
.pending_steers
.front()
@@ -6837,36 +6775,34 @@ impl ChatWidget {
{
if let Some(pending) = self.pending_steers.pop_front() {
self.refresh_pending_input_preview();
let pending_event =
user_message_event_for_display(pending.user_message, &pending.history_record);
self.on_user_message_event(pending_event);
} else if self.last_rendered_user_message_event.as_ref() != Some(&rendered) {
let pending_display =
user_message_display_for_history(pending.user_message, &pending.history_record);
self.on_user_message_display(pending_display);
} else if self.last_rendered_user_message_display.as_ref() != Some(&display) {
tracing::warn!(
"pending steer matched compare key but queue was empty when rendering committed user message"
);
self.on_user_message_event(event);
self.on_user_message_display(display);
}
} else if !self.is_review_mode
&& self.last_rendered_user_message_event.as_ref() != Some(&rendered)
&& self.last_rendered_user_message_display.as_ref() != Some(&display)
{
self.on_user_message_event(event);
self.on_user_message_display(display);
}
}
fn on_user_message_event(&mut self, event: UserMessageEvent) {
self.last_rendered_user_message_event =
Some(Self::rendered_user_message_event_from_event(&event));
let remote_image_urls = event.images.unwrap_or_default();
if !event.message.trim().is_empty()
|| !event.text_elements.is_empty()
|| !remote_image_urls.is_empty()
fn on_user_message_display(&mut self, display: UserMessageDisplay) {
self.last_rendered_user_message_display = Some(display.clone());
if !display.message.trim().is_empty()
|| !display.text_elements.is_empty()
|| !display.remote_image_urls.is_empty()
{
self.record_visible_user_turn_for_copy();
self.add_to_history(history_cell::new_user_prompt(
event.message,
event.text_elements,
event.local_images,
remote_image_urls,
display.message,
display.text_elements,
display.local_images,
display.remote_image_urls,
));
}
@@ -178,7 +178,6 @@ mod tests {
proposed_network_policy_amendments: None,
additional_permissions: None,
available_decisions: None,
parsed_cmd: Vec::new(),
}
}
+2 -2
View File
@@ -59,6 +59,7 @@ pub(super) use codex_app_server_protocol::CommandExecutionStatus as AppServerCom
pub(super) use codex_app_server_protocol::ConfigWarningNotification;
pub(super) use codex_app_server_protocol::CreditsSnapshot;
pub(super) use codex_app_server_protocol::ErrorNotification;
pub(super) use codex_app_server_protocol::ExecPolicyAmendment;
pub(super) use codex_app_server_protocol::FileUpdateChange;
pub(super) use codex_app_server_protocol::GuardianApprovalReview;
pub(super) use codex_app_server_protocol::GuardianApprovalReviewAction as AppServerGuardianApprovalReviewAction;
@@ -117,6 +118,7 @@ pub(super) use codex_app_server_protocol::TurnCompletedNotification;
pub(super) use codex_app_server_protocol::TurnError as AppServerTurnError;
pub(super) use codex_app_server_protocol::TurnStartedNotification;
pub(super) use codex_app_server_protocol::TurnStatus as AppServerTurnStatus;
pub(super) use codex_app_server_protocol::UserInput;
pub(super) use codex_app_server_protocol::UserInput as AppServerUserInput;
pub(super) use codex_app_server_protocol::WarningNotification;
pub(super) use codex_config::AppRequirementToml;
@@ -139,7 +141,6 @@ pub(super) use codex_otel::RuntimeMetricsSummary;
pub(super) use codex_otel::SessionTelemetry;
pub(super) use codex_protocol::ThreadId;
pub(super) use codex_protocol::account::PlanType;
pub(super) use codex_protocol::approvals::ExecPolicyAmendment;
pub(super) use codex_protocol::approvals::GuardianAssessmentAction;
pub(super) use codex_protocol::approvals::GuardianAssessmentDecisionSource;
pub(super) use codex_protocol::approvals::GuardianAssessmentEvent;
@@ -170,7 +171,6 @@ pub(super) use codex_protocol::request_user_input::RequestUserInputEvent;
pub(super) use codex_protocol::request_user_input::RequestUserInputQuestion;
pub(super) use codex_protocol::request_user_input::RequestUserInputQuestionOption;
pub(super) use codex_protocol::user_input::TextElement;
pub(super) use codex_protocol::user_input::UserInput;
pub(super) use codex_terminal_detection::Multiplexer;
pub(super) use codex_terminal_detection::TerminalInfo;
pub(super) use codex_terminal_detection::TerminalName;
@@ -23,7 +23,6 @@ async fn exec_approval_emits_proposed_command_and_decision_history() {
proposed_network_policy_amendments: None,
additional_permissions: None,
available_decisions: None,
parsed_cmd: vec![],
};
handle_exec_approval_request(&mut chat, "sub-short", ev);
@@ -130,14 +129,16 @@ fn app_server_exec_approval_request_preserves_permissions_context() {
);
assert_eq!(
request.additional_permissions,
Some(codex_protocol::models::AdditionalPermissionProfile {
network: Some(NetworkPermissions {
Some(AppServerAdditionalPermissionProfile {
network: Some(AppServerAdditionalNetworkPermissions {
enabled: Some(true),
}),
file_system: Some(FileSystemPermissions::from_read_write_roots(
Some(vec![read_path]),
Some(vec![write_path]),
)),
file_system: Some(AppServerAdditionalFileSystemPermissions {
read: Some(vec![read_path]),
write: Some(vec![write_path]),
glob_scan_max_depth: None,
entries: None,
}),
})
);
}
@@ -206,7 +207,6 @@ async fn exec_approval_uses_approval_id_when_present() {
proposed_network_policy_amendments: None,
additional_permissions: None,
available_decisions: None,
parsed_cmd: vec![],
},
);
@@ -249,7 +249,6 @@ async fn exec_approval_decision_truncates_multiline_and_long_commands() {
proposed_network_policy_amendments: None,
additional_permissions: None,
available_decisions: None,
parsed_cmd: vec![],
};
handle_exec_approval_request(&mut chat, "sub-multi", ev_multi);
let proposed_multi = drain_insert_history(&mut rx);
@@ -299,7 +298,6 @@ async fn exec_approval_decision_truncates_multiline_and_long_commands() {
proposed_network_policy_amendments: None,
additional_permissions: None,
available_decisions: None,
parsed_cmd: vec![],
};
handle_exec_approval_request(&mut chat, "sub-long", ev_long);
let proposed_long = drain_insert_history(&mut rx);
@@ -63,7 +63,7 @@ async fn submission_preserves_text_elements_and_local_images() {
items[1],
UserInput::Text {
text: text.clone(),
text_elements: text_elements.clone(),
text_elements: text_elements.clone().into_iter().map(Into::into).collect(),
}
);
@@ -253,7 +253,7 @@ async fn submission_with_remote_and_local_images_keeps_local_placeholder_numberi
assert_eq!(
items[0],
UserInput::Image {
image_url: remote_url.clone(),
url: remote_url.clone(),
}
);
assert_eq!(
@@ -266,7 +266,7 @@ async fn submission_with_remote_and_local_images_keeps_local_placeholder_numberi
items[2],
UserInput::Text {
text: text.clone(),
text_elements: text_elements.clone(),
text_elements: text_elements.clone().into_iter().map(Into::into).collect(),
}
);
assert_eq!(text_elements[0].placeholder(&text), Some("[Image #2]"));
@@ -335,7 +335,7 @@ async fn enter_with_only_remote_images_submits_user_turn() {
assert_eq!(
items,
vec![UserInput::Image {
image_url: remote_url.clone(),
url: remote_url.clone(),
}]
);
assert_eq!(summary, None);
@@ -1103,15 +1103,15 @@ async fn enqueueing_history_prompt_multiple_times_is_stable() {
}
#[test]
fn rendered_user_message_event_from_inputs_matches_flattened_user_message_shape() {
fn user_message_display_from_inputs_matches_flattened_user_message_shape() {
let local_image = PathBuf::from("/tmp/local.png");
let rendered = ChatWidget::rendered_user_message_event_from_inputs(&[
let rendered = ChatWidget::user_message_display_from_inputs(&[
UserInput::Text {
text: "hello ".to_string(),
text_elements: vec![TextElement::new((0..5).into(), /*placeholder*/ None)],
text_elements: vec![TextElement::new((0..5).into(), /*placeholder*/ None).into()],
},
UserInput::Image {
image_url: "https://example.com/remote.png".to_string(),
url: "https://example.com/remote.png".to_string(),
},
UserInput::LocalImage {
path: local_image.clone(),
@@ -1126,13 +1126,13 @@ fn rendered_user_message_event_from_inputs_matches_flattened_user_message_shape(
},
UserInput::Text {
text: "world".to_string(),
text_elements: vec![TextElement::new((0..5).into(), Some("planet".to_string()))],
text_elements: vec![TextElement::new((0..5).into(), Some("planet".to_string())).into()],
},
]);
assert_eq!(
rendered,
ChatWidget::rendered_user_message_event_from_parts(
ChatWidget::user_message_display_from_parts(
"hello world".to_string(),
vec![
TextElement::new((0..5).into(), Some("hello".to_string())),
+7 -18
View File
@@ -20,7 +20,6 @@ async fn exec_approval_emits_proposed_command_and_decision_history() {
proposed_network_policy_amendments: None,
additional_permissions: None,
available_decisions: None,
parsed_cmd: vec![],
};
handle_exec_approval_request(&mut chat, "sub-short", ev);
@@ -103,7 +102,6 @@ async fn exec_approval_uses_approval_id_when_present() {
proposed_network_policy_amendments: None,
additional_permissions: None,
available_decisions: None,
parsed_cmd: vec![],
},
);
@@ -147,7 +145,6 @@ async fn exec_approval_decision_truncates_multiline_and_long_commands() {
proposed_network_policy_amendments: None,
additional_permissions: None,
available_decisions: None,
parsed_cmd: vec![],
};
handle_exec_approval_request(&mut chat, "sub-multi", ev_multi);
let proposed_multi = drain_insert_history(&mut rx);
@@ -199,7 +196,6 @@ async fn exec_approval_decision_truncates_multiline_and_long_commands() {
proposed_network_policy_amendments: None,
additional_permissions: None,
available_decisions: None,
parsed_cmd: vec![],
};
handle_exec_approval_request(&mut chat, "sub-long", ev_long);
let proposed_long = drain_insert_history(&mut rx);
@@ -1091,15 +1087,12 @@ async fn approval_modal_exec_snapshot() -> anyhow::Result<()> {
"this is a test reason such as one that would be produced by the model".into(),
),
network_approval_context: None,
proposed_execpolicy_amendment: Some(ExecPolicyAmendment::new(vec![
"echo".into(),
"hello".into(),
"world".into(),
])),
proposed_execpolicy_amendment: Some(ExecPolicyAmendment {
command: vec!["echo".into(), "hello".into(), "world".into()],
}),
proposed_network_policy_amendments: None,
additional_permissions: None,
available_decisions: None,
parsed_cmd: vec![],
};
handle_exec_approval_request(&mut chat, "sub-approve", ev);
// Render to a fixed-size test terminal and snapshot.
@@ -1149,15 +1142,12 @@ async fn approval_modal_exec_without_reason_snapshot() -> anyhow::Result<()> {
cwd: AbsolutePathBuf::current_dir().expect("current dir"),
reason: None,
network_approval_context: None,
proposed_execpolicy_amendment: Some(ExecPolicyAmendment::new(vec![
"echo".into(),
"hello".into(),
"world".into(),
])),
proposed_execpolicy_amendment: Some(ExecPolicyAmendment {
command: vec!["echo".into(), "hello".into(), "world".into()],
}),
proposed_network_policy_amendments: None,
additional_permissions: None,
available_decisions: None,
parsed_cmd: vec![],
};
handle_exec_approval_request(&mut chat, "sub-approve-noreason", ev);
@@ -1198,11 +1188,10 @@ async fn approval_modal_exec_multiline_prefix_hides_execpolicy_option_snapshot()
cwd: AbsolutePathBuf::current_dir().expect("current dir"),
reason: None,
network_approval_context: None,
proposed_execpolicy_amendment: Some(ExecPolicyAmendment::new(command)),
proposed_execpolicy_amendment: Some(ExecPolicyAmendment { command }),
proposed_network_policy_amendments: None,
additional_permissions: None,
available_decisions: None,
parsed_cmd: vec![],
};
handle_exec_approval_request(&mut chat, "sub-approve-multiline-trunc", ev);
+2 -2
View File
@@ -317,7 +317,7 @@ pub(super) async fn make_chatwidget_manual(
goal_status_active_turn_started_at: None,
external_editor_state: ExternalEditorState::Closed,
realtime_conversation: RealtimeConversationUiState::default(),
last_rendered_user_message_event: None,
last_rendered_user_message_display: None,
last_non_retry_error: None,
};
widget.set_model(&resolved_model);
@@ -1081,7 +1081,7 @@ pub(super) fn complete_user_message_for_inputs(
turn_id: "turn-1".to_string(),
item: AppServerThreadItem::UserMessage {
id: item_id.to_string(),
content: content.into_iter().map(AppServerUserInput::from).collect(),
content,
},
}),
/*replay_kind*/ None,
@@ -551,7 +551,7 @@ async fn item_completed_pops_pending_steer_with_local_image_and_text_elements()
"user-1",
vec![
UserInput::Image {
image_url: "data:image/png;base64,placeholder".to_string(),
url: "data:image/png;base64,placeholder".to_string(),
},
UserInput::Text {
text,
@@ -832,10 +832,9 @@ async fn manual_interrupt_restores_pending_steer_mention_bindings_to_composer()
items,
vec![UserInput::Text {
text: "please use $figma".to_string(),
text_elements: vec![TextElement::new(
(11..17).into(),
Some("$figma".to_string()),
)],
text_elements: vec![
TextElement::new((11..17).into(), Some("$figma".to_string())).into()
],
}]
),
other => panic!("expected Op::UserTurn, got {other:?}"),
@@ -1108,14 +1108,12 @@ async fn status_widget_and_approval_modal_snapshot() {
"this is a test reason such as one that would be produced by the model".into(),
),
network_approval_context: None,
proposed_execpolicy_amendment: Some(ExecPolicyAmendment::new(vec![
"echo".into(),
"hello world".into(),
])),
proposed_execpolicy_amendment: Some(ExecPolicyAmendment {
command: vec!["echo".into(), "hello world".into()],
}),
proposed_network_policy_amendments: None,
additional_permissions: None,
available_decisions: None,
parsed_cmd: vec![],
};
handle_exec_approval_request(&mut chat, "sub-approve-exec", ev);
@@ -21,7 +21,6 @@ async fn terminal_title_shows_action_required_while_exec_approval_is_pending() {
proposed_network_policy_amendments: None,
additional_permissions: None,
available_decisions: None,
parsed_cmd: vec![],
};
handle_exec_approval_request(&mut chat, "sub-action-required", request);
@@ -64,7 +63,6 @@ async fn terminal_title_action_required_respects_spinner_setting() {
proposed_network_policy_amendments: None,
additional_permissions: None,
available_decisions: None,
parsed_cmd: vec![],
};
handle_exec_approval_request(&mut chat, "sub-no-spinner", request);
@@ -93,7 +91,6 @@ async fn terminal_title_action_required_blinks_when_animations_are_enabled() {
proposed_network_policy_amendments: None,
additional_permissions: None,
available_decisions: None,
parsed_cmd: vec![],
};
handle_exec_approval_request(&mut chat, "sub-blink", request);
@@ -129,7 +126,6 @@ async fn terminal_title_activity_indicators_do_not_animate_when_animations_are_d
proposed_network_policy_amendments: None,
additional_permissions: None,
available_decisions: None,
parsed_cmd: vec![],
};
handle_exec_approval_request(&mut chat, "sub-no-animations", request);
+19 -47
View File
@@ -1,29 +1,19 @@
//! User-message display models and helpers for the chat widget.
//!
//! App-server turn items and queued TUI submissions describe user input in
//! slightly different shapes. This module keeps the display-only representation
//! and comparison keys together so chat rendering can avoid duplicate user rows
//! while preserving local-only attachment metadata.
//! The app-server preserves user input as structured chunks, while chat history
//! renders a single prompt row. This module owns that display projection and
//! the small compare key used to suppress duplicate rows for pending steers.
use std::path::PathBuf;
use codex_protocol::items::UserMessageItem;
use codex_app_server_protocol::UserInput;
use codex_protocol::user_input::TextElement;
use codex_protocol::user_input::UserInput;
use super::ChatWidget;
use super::append_text_with_rebased_elements;
#[derive(Debug, Clone)]
pub(crate) struct UserMessageEvent {
pub(super) message: String,
pub(super) images: Option<Vec<String>>,
pub(super) local_images: Vec<PathBuf>,
pub(super) text_elements: Vec<TextElement>,
}
#[derive(Clone, Debug, PartialEq)]
pub(super) struct RenderedUserMessageEvent {
pub(super) struct UserMessageDisplay {
pub(super) message: String,
pub(super) remote_image_urls: Vec<String>,
pub(super) local_images: Vec<PathBuf>,
@@ -37,13 +27,13 @@ pub(super) struct PendingSteerCompareKey {
}
impl ChatWidget {
pub(super) fn rendered_user_message_event_from_parts(
pub(super) fn user_message_display_from_parts(
message: String,
text_elements: Vec<TextElement>,
local_images: Vec<PathBuf>,
remote_image_urls: Vec<String>,
) -> RenderedUserMessageEvent {
RenderedUserMessageEvent {
) -> UserMessageDisplay {
UserMessageDisplay {
message,
remote_image_urls,
local_images,
@@ -51,22 +41,10 @@ impl ChatWidget {
}
}
pub(super) fn rendered_user_message_event_from_event(
event: &UserMessageEvent,
) -> RenderedUserMessageEvent {
Self::rendered_user_message_event_from_parts(
event.message.clone(),
event.text_elements.clone(),
event.local_images.clone(),
event.images.clone().unwrap_or_default(),
)
}
/// Build the compare key for a submitted pending steer without invoking the
/// expensive request-serialization path. Pending steers only need to match the
/// committed `ItemCompleted(UserMessage)` emitted after core drains input, which
/// preserves flattened text and total image count but not UI-only text ranges or
/// local image paths.
/// committed app-server `UserMessage` item emitted after input drains, which
/// preserves flattened text and total image count.
pub(super) fn pending_steer_compare_key_from_items(
items: &[UserInput],
) -> PendingSteerCompareKey {
@@ -78,7 +56,6 @@ impl ChatWidget {
UserInput::Text { text, .. } => message.push_str(text),
UserInput::Image { .. } | UserInput::LocalImage { .. } => image_count += 1,
UserInput::Skill { .. } | UserInput::Mention { .. } => {}
_ => {}
}
}
@@ -88,15 +65,7 @@ impl ChatWidget {
}
}
pub(super) fn pending_steer_compare_key_from_item(
item: &UserMessageItem,
) -> PendingSteerCompareKey {
Self::pending_steer_compare_key_from_items(&item.content)
}
pub(super) fn rendered_user_message_event_from_inputs(
items: &[UserInput],
) -> RenderedUserMessageEvent {
pub(super) fn user_message_display_from_inputs(items: &[UserInput]) -> UserMessageDisplay {
let mut message = String::new();
let mut remote_image_urls = Vec::new();
let mut local_images = Vec::new();
@@ -112,20 +81,23 @@ impl ChatWidget {
&mut text_elements,
text,
current_text_elements.iter().map(|element| {
let range = element.byte_range.clone();
TextElement::new(
element.byte_range,
element.placeholder(text).map(str::to_string),
range.clone().into(),
element
.placeholder()
.or_else(|| text.get(range.start..range.end))
.map(str::to_string),
)
}),
),
UserInput::Image { image_url } => remote_image_urls.push(image_url.clone()),
UserInput::Image { url } => remote_image_urls.push(url.clone()),
UserInput::LocalImage { path } => local_images.push(path.clone()),
UserInput::Skill { .. } | UserInput::Mention { .. } => {}
_ => {}
}
}
Self::rendered_user_message_event_from_parts(
Self::user_message_display_from_parts(
message,
text_elements,
local_images,
+19 -2
View File
@@ -58,6 +58,8 @@ use codex_config::types::McpServerTransportConfig;
use codex_mcp::qualified_mcp_tool_name_prefix;
use codex_otel::RuntimeMetricsSummary;
use codex_protocol::account::PlanType;
use codex_protocol::approvals::ExecPolicyAmendment;
use codex_protocol::approvals::NetworkPolicyAmendment;
#[cfg(test)]
use codex_protocol::mcp::Resource;
#[cfg(test)]
@@ -862,12 +864,27 @@ fn exec_snippet(command: &[String]) -> String {
truncate_exec_snippet(&full_cmd)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ReviewDecision {
Approved,
ApprovedExecpolicyAmendment {
proposed_execpolicy_amendment: ExecPolicyAmendment,
},
ApprovedForSession,
NetworkPolicyAmendment {
network_policy_amendment: NetworkPolicyAmendment,
},
Denied,
TimedOut,
Abort,
}
pub fn new_approval_decision_cell(
command: Vec<String>,
decision: crate::approval_display::ReviewDecision,
decision: ReviewDecision,
actor: ApprovalDecisionActor,
) -> Box<dyn HistoryCell> {
use crate::approval_display::ReviewDecision::*;
use ReviewDecision::*;
use codex_protocol::approvals::NetworkPolicyRuleAction;
let (symbol, summary): (Span<'static>, Vec<Span<'static>>) = match decision {
-1
View File
@@ -84,7 +84,6 @@ mod app_event;
mod app_event_sender;
mod app_server_approval_conversions;
mod app_server_session;
mod approval_display;
mod approval_events;
mod ascii_animation;
#[cfg(not(target_os = "linux"))]
+1 -1
View File
@@ -912,7 +912,7 @@ fn render_offset_content(
#[cfg(test)]
mod tests {
use super::*;
use crate::approval_display::ReviewDecision;
use crate::history_cell::ReviewDecision;
use codex_app_server_protocol::CommandExecutionSource as ExecCommandSource;
use insta::assert_snapshot;
use pretty_assertions::assert_eq;