mirror of
https://github.com/openai/codex.git
synced 2026-10-22 17:58:46 +00:00
Simplify TUI user input and approval models
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -257,7 +257,7 @@ impl App {
|
||||
changes: self
|
||||
.thread_file_change_changes(thread_id, ¶ms.turn_id, ¶ms.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(),
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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())),
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"))]
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user