Files
codex/codex-rs/tui/src/app_command.rs
T
Eric Traut 8b8ee28a9b Acknowledge pending TUI steers by submission ID (#42451)
## Why

Completion receipts for identical user messages could acknowledge the wrong
pending steer. When the receipt arrived while another thread was displayed,
the stale pending state could later be restored and resubmitted.

## What changed

- Assign each TUI submission a client message ID and pass it through turn start
  and steer requests.
- Preserve pending steer IDs across thread input snapshots and match completion
  receipts by ID, while retaining content-based matching for older app servers
  that do not echo IDs.
- Acknowledge matching receipts for inactive threads before their notifications
  can be discarded, including threads stored in the agents overview.

## Testing

Added coverage for identical steer receipts and for switching within and
between tasks before interrupting an active turn.

GitOrigin-RevId: 28bd80a735ff86cd7bf836f24b448e7499f0a902
2026-09-03 05:15:20 +00:00

255 lines
7.7 KiB
Rust

use std::path::PathBuf;
use codex_app_server_protocol::AskForApproval;
use codex_app_server_protocol::CommandExecutionApprovalDecision;
use codex_app_server_protocol::FileChangeApprovalDecision;
use codex_app_server_protocol::McpServerElicitationAction;
use codex_app_server_protocol::RequestId as AppServerRequestId;
use codex_app_server_protocol::ReviewTarget;
use codex_app_server_protocol::ToolRequestUserInputResponse;
use codex_app_server_protocol::UserInput;
use codex_config::types::ApprovalsReviewer;
use codex_protocol::approvals::GuardianAssessmentEvent;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::Personality;
use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::models::ActivePermissionProfile;
use codex_protocol::models::PermissionProfile;
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
use codex_protocol::request_permissions::RequestPermissionsResponse;
use serde::Serialize;
use serde_json::Value;
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Clone, PartialEq, Serialize)]
pub(crate) enum AppCommand {
Interrupt,
CleanBackgroundTerminals,
RunUserShellCommand {
command: String,
},
UserTurn {
client_user_message_id: String,
items: Vec<UserInput>,
cwd: PathBuf,
approval_policy: AskForApproval,
approvals_reviewer: Option<ApprovalsReviewer>,
active_permission_profile: Option<ActivePermissionProfile>,
model: String,
effort: Option<ReasoningEffortConfig>,
summary: Option<ReasoningSummaryConfig>,
service_tier: Option<Option<String>>,
final_output_json_schema: Option<Value>,
collaboration_mode: Option<CollaborationMode>,
personality: Option<Personality>,
},
OverrideTurnContext {
cwd: Option<PathBuf>,
approval_policy: Option<AskForApproval>,
approvals_reviewer: Option<ApprovalsReviewer>,
permission_profile: Option<PermissionProfile>,
active_permission_profile: Option<ActivePermissionProfile>,
windows_sandbox_level: Option<WindowsSandboxLevel>,
model: Option<String>,
effort: Option<Option<ReasoningEffortConfig>>,
summary: Option<ReasoningSummaryConfig>,
service_tier: Option<Option<String>>,
collaboration_mode: Option<CollaborationMode>,
personality: Option<Personality>,
},
ExecApproval {
id: String,
turn_id: Option<String>,
decision: CommandExecutionApprovalDecision,
},
PatchApproval {
id: String,
decision: FileChangeApprovalDecision,
},
ResolveElicitation {
server_name: String,
request_id: AppServerRequestId,
decision: McpServerElicitationAction,
content: Option<Value>,
meta: Option<Value>,
},
UserInputAnswer {
id: String,
response: ToolRequestUserInputResponse,
},
RequestPermissionsResponse {
id: String,
response: RequestPermissionsResponse,
},
ReloadUserConfig,
ListSkills {
cwds: Vec<PathBuf>,
force_reload: bool,
},
Compact,
SetThreadName {
name: String,
},
Review {
target: ReviewTarget,
},
ApproveGuardianDeniedAction {
event: GuardianAssessmentEvent,
},
}
impl AppCommand {
pub(crate) fn interrupt() -> Self {
Self::Interrupt
}
pub(crate) fn clean_background_terminals() -> Self {
Self::CleanBackgroundTerminals
}
pub(crate) fn run_user_shell_command(command: String) -> Self {
Self::RunUserShellCommand { command }
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn user_turn(
client_user_message_id: String,
items: Vec<UserInput>,
cwd: PathBuf,
approval_policy: AskForApproval,
active_permission_profile: Option<ActivePermissionProfile>,
model: String,
effort: Option<ReasoningEffortConfig>,
summary: Option<ReasoningSummaryConfig>,
service_tier: Option<Option<String>>,
final_output_json_schema: Option<Value>,
collaboration_mode: Option<CollaborationMode>,
personality: Option<Personality>,
) -> Self {
Self::UserTurn {
client_user_message_id,
items,
cwd,
approval_policy,
approvals_reviewer: None,
active_permission_profile,
model,
effort,
summary,
service_tier,
final_output_json_schema,
collaboration_mode,
personality,
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn override_turn_context(
cwd: Option<PathBuf>,
approval_policy: Option<AskForApproval>,
approvals_reviewer: Option<ApprovalsReviewer>,
permission_profile: Option<PermissionProfile>,
active_permission_profile: Option<ActivePermissionProfile>,
windows_sandbox_level: Option<WindowsSandboxLevel>,
model: Option<String>,
effort: Option<Option<ReasoningEffortConfig>>,
summary: Option<ReasoningSummaryConfig>,
service_tier: Option<Option<String>>,
collaboration_mode: Option<CollaborationMode>,
personality: Option<Personality>,
) -> Self {
Self::OverrideTurnContext {
cwd,
approval_policy,
approvals_reviewer,
permission_profile,
active_permission_profile,
windows_sandbox_level,
model,
effort,
summary,
service_tier,
collaboration_mode,
personality,
}
}
pub(crate) fn exec_approval(
id: String,
turn_id: Option<String>,
decision: CommandExecutionApprovalDecision,
) -> Self {
Self::ExecApproval {
id,
turn_id,
decision,
}
}
pub(crate) fn patch_approval(id: String, decision: FileChangeApprovalDecision) -> Self {
Self::PatchApproval { id, decision }
}
pub(crate) fn resolve_elicitation(
server_name: String,
request_id: AppServerRequestId,
decision: McpServerElicitationAction,
content: Option<Value>,
meta: Option<Value>,
) -> Self {
Self::ResolveElicitation {
server_name,
request_id,
decision,
content,
meta,
}
}
pub(crate) fn user_input_answer(id: String, response: ToolRequestUserInputResponse) -> Self {
Self::UserInputAnswer { id, response }
}
pub(crate) fn request_permissions_response(
id: String,
response: RequestPermissionsResponse,
) -> Self {
Self::RequestPermissionsResponse { id, response }
}
pub(crate) fn reload_user_config() -> Self {
Self::ReloadUserConfig
}
pub(crate) fn list_skills(cwds: Vec<PathBuf>, force_reload: bool) -> Self {
Self::ListSkills { cwds, force_reload }
}
pub(crate) fn compact() -> Self {
Self::Compact
}
pub(crate) fn set_thread_name(name: String) -> Self {
Self::SetThreadName { name }
}
pub(crate) fn review(target: ReviewTarget) -> Self {
Self::Review { target }
}
pub(crate) fn approve_guardian_denied_action(event: GuardianAssessmentEvent) -> Self {
Self::ApproveGuardianDeniedAction { event }
}
pub(crate) fn is_review(&self) -> bool {
matches!(self, Self::Review { .. })
}
}
impl From<&AppCommand> for AppCommand {
fn from(value: &AppCommand) -> Self {
value.clone()
}
}