mirror of
https://github.com/openai/codex.git
synced 2026-05-30 07:50:17 +00:00
431 lines
18 KiB
Rust
431 lines
18 KiB
Rust
use super::*;
|
|
|
|
impl ChatWidget {
|
|
pub(crate) fn handle_server_notification(
|
|
&mut self,
|
|
notification: ServerNotification,
|
|
replay_kind: Option<ReplayKind>,
|
|
) {
|
|
if self.active_side_conversation
|
|
&& replay_kind.is_none()
|
|
&& matches!(notification, ServerNotification::McpServerStatusUpdated(_))
|
|
{
|
|
return;
|
|
}
|
|
let from_replay = replay_kind.is_some();
|
|
let is_resume_initial_replay =
|
|
matches!(replay_kind, Some(ReplayKind::ResumeInitialMessages));
|
|
let is_retry_error = matches!(
|
|
¬ification,
|
|
ServerNotification::Error(ErrorNotification {
|
|
will_retry: true,
|
|
..
|
|
})
|
|
);
|
|
if !is_resume_initial_replay && !is_retry_error {
|
|
self.restore_retry_status_header_if_present();
|
|
}
|
|
match notification {
|
|
ServerNotification::ThreadTokenUsageUpdated(notification) => {
|
|
self.set_token_info(Some(token_usage_info_from_app_server(
|
|
notification.token_usage,
|
|
)));
|
|
}
|
|
ServerNotification::ThreadNameUpdated(notification) => {
|
|
match ThreadId::from_string(¬ification.thread_id) {
|
|
Ok(thread_id) => {
|
|
self.on_thread_name_updated(thread_id, notification.thread_name)
|
|
}
|
|
Err(err) => {
|
|
tracing::warn!(
|
|
thread_id = notification.thread_id,
|
|
error = %err,
|
|
"ignoring app-server ThreadNameUpdated with invalid thread_id"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
ServerNotification::ThreadGoalUpdated(notification) => {
|
|
self.on_thread_goal_updated(notification.goal, notification.turn_id);
|
|
}
|
|
ServerNotification::ThreadGoalCleared(notification) => {
|
|
self.on_thread_goal_cleared(notification.thread_id.as_str());
|
|
}
|
|
ServerNotification::ThreadSettingsUpdated(notification) => {
|
|
self.apply_thread_settings(notification.thread_settings);
|
|
}
|
|
ServerNotification::TurnStarted(notification) => {
|
|
self.turn_lifecycle.last_turn_id = Some(notification.turn.id);
|
|
self.last_non_retry_error = None;
|
|
if !matches!(replay_kind, Some(ReplayKind::ResumeInitialMessages)) {
|
|
self.on_task_started();
|
|
}
|
|
}
|
|
ServerNotification::TurnCompleted(notification) => {
|
|
self.handle_turn_completed_notification(notification, replay_kind);
|
|
}
|
|
ServerNotification::ItemStarted(notification) => {
|
|
self.handle_item_started_notification(notification, replay_kind.is_some());
|
|
}
|
|
ServerNotification::ItemCompleted(notification) => {
|
|
self.handle_item_completed_notification(notification, replay_kind);
|
|
}
|
|
ServerNotification::AgentMessageDelta(notification) => {
|
|
self.on_agent_message_delta(notification.delta);
|
|
}
|
|
ServerNotification::PlanDelta(notification) => self.on_plan_delta(notification.delta),
|
|
ServerNotification::ReasoningSummaryTextDelta(notification) => {
|
|
self.on_agent_reasoning_delta(notification.delta);
|
|
}
|
|
ServerNotification::ReasoningTextDelta(notification) => {
|
|
if self.config.show_raw_agent_reasoning {
|
|
self.on_agent_reasoning_delta(notification.delta);
|
|
}
|
|
}
|
|
ServerNotification::ReasoningSummaryPartAdded(_) => self.on_reasoning_section_break(),
|
|
ServerNotification::TerminalInteraction(notification) => {
|
|
self.on_terminal_interaction(notification.process_id, notification.stdin)
|
|
}
|
|
ServerNotification::CommandExecutionOutputDelta(notification) => {
|
|
self.on_exec_command_output_delta(¬ification.item_id, ¬ification.delta);
|
|
}
|
|
ServerNotification::FileChangeOutputDelta(notification) => {
|
|
self.on_patch_apply_output_delta(notification.item_id, notification.delta);
|
|
}
|
|
ServerNotification::TurnDiffUpdated(notification) => {
|
|
self.on_turn_diff(notification.diff)
|
|
}
|
|
ServerNotification::TurnPlanUpdated(notification) => {
|
|
self.on_plan_update(UpdatePlanArgs {
|
|
explanation: notification.explanation,
|
|
plan: notification
|
|
.plan
|
|
.into_iter()
|
|
.map(|step| UpdatePlanItemArg {
|
|
step: step.step,
|
|
status: match step.status {
|
|
TurnPlanStepStatus::Pending => UpdatePlanItemStatus::Pending,
|
|
TurnPlanStepStatus::InProgress => UpdatePlanItemStatus::InProgress,
|
|
TurnPlanStepStatus::Completed => UpdatePlanItemStatus::Completed,
|
|
},
|
|
})
|
|
.collect(),
|
|
})
|
|
}
|
|
ServerNotification::HookStarted(notification) => {
|
|
self.on_hook_started(notification.run);
|
|
}
|
|
ServerNotification::HookCompleted(notification) => {
|
|
self.on_hook_completed(notification.run);
|
|
}
|
|
ServerNotification::Error(notification) => {
|
|
if notification.will_retry {
|
|
if !from_replay {
|
|
self.on_stream_error(
|
|
notification.error.message,
|
|
notification.error.additional_details,
|
|
);
|
|
}
|
|
} else {
|
|
self.last_non_retry_error = Some((
|
|
notification.turn_id.clone(),
|
|
notification.error.message.clone(),
|
|
));
|
|
self.handle_non_retry_error(
|
|
notification.error.message,
|
|
notification.error.codex_error_info,
|
|
);
|
|
}
|
|
}
|
|
ServerNotification::SkillsChanged(_) => {
|
|
self.refresh_skills_for_current_cwd(/*force_reload*/ true);
|
|
}
|
|
ServerNotification::ModelRerouted(_) => {}
|
|
ServerNotification::ModelVerification(notification) => {
|
|
self.on_app_server_model_verification(¬ification.verifications)
|
|
}
|
|
ServerNotification::Warning(notification) => self.on_warning(notification.message),
|
|
ServerNotification::GuardianWarning(notification) => {
|
|
self.on_warning(notification.message)
|
|
}
|
|
ServerNotification::DeprecationNotice(notification) => {
|
|
self.on_deprecation_notice(notification.summary, notification.details)
|
|
}
|
|
ServerNotification::ConfigWarning(notification) => self.on_warning(
|
|
notification
|
|
.details
|
|
.map(|details| format!("{}: {details}", notification.summary))
|
|
.unwrap_or(notification.summary),
|
|
),
|
|
ServerNotification::McpServerStatusUpdated(notification) => {
|
|
self.on_mcp_server_status_updated(notification)
|
|
}
|
|
ServerNotification::ItemGuardianApprovalReviewStarted(notification) => {
|
|
self.on_guardian_review_notification(
|
|
notification.review_id,
|
|
notification.turn_id,
|
|
notification.started_at_ms,
|
|
notification.review,
|
|
/*completion*/ None,
|
|
notification.action,
|
|
);
|
|
}
|
|
ServerNotification::ItemGuardianApprovalReviewCompleted(notification) => {
|
|
self.on_guardian_review_notification(
|
|
notification.review_id,
|
|
notification.turn_id,
|
|
notification.started_at_ms,
|
|
notification.review,
|
|
Some((notification.completed_at_ms, notification.decision_source)),
|
|
notification.action,
|
|
);
|
|
}
|
|
ServerNotification::ThreadClosed(_) => {
|
|
if !from_replay {
|
|
self.on_shutdown_complete();
|
|
}
|
|
}
|
|
ServerNotification::ThreadRealtimeStarted(notification) => {
|
|
if !from_replay {
|
|
self.on_realtime_conversation_started(notification);
|
|
}
|
|
}
|
|
ServerNotification::ThreadRealtimeItemAdded(notification) => {
|
|
if !from_replay {
|
|
self.on_realtime_item_added(notification);
|
|
}
|
|
}
|
|
ServerNotification::ThreadRealtimeOutputAudioDelta(notification) => {
|
|
if !from_replay {
|
|
self.on_realtime_output_audio_delta(notification);
|
|
}
|
|
}
|
|
ServerNotification::ThreadRealtimeError(notification) => {
|
|
if !from_replay {
|
|
self.on_realtime_error(notification);
|
|
}
|
|
}
|
|
ServerNotification::ThreadRealtimeClosed(notification) => {
|
|
if !from_replay {
|
|
self.on_realtime_conversation_closed(notification);
|
|
}
|
|
}
|
|
ServerNotification::ThreadRealtimeSdp(notification) => {
|
|
if !from_replay {
|
|
self.on_realtime_conversation_sdp(notification.sdp);
|
|
}
|
|
}
|
|
ServerNotification::ServerRequestResolved(_)
|
|
| ServerNotification::AccountUpdated(_)
|
|
| ServerNotification::AccountRateLimitsUpdated(_)
|
|
| ServerNotification::ThreadStarted(_)
|
|
| ServerNotification::ThreadStatusChanged(_)
|
|
| ServerNotification::ThreadArchived(_)
|
|
| ServerNotification::ThreadUnarchived(_)
|
|
| ServerNotification::RawResponseItemCompleted(_)
|
|
| ServerNotification::CommandExecOutputDelta(_)
|
|
| ServerNotification::ProcessOutputDelta(_)
|
|
| ServerNotification::ProcessExited(_)
|
|
| ServerNotification::FileChangePatchUpdated(_)
|
|
| ServerNotification::McpToolCallProgress(_)
|
|
| ServerNotification::McpServerOauthLoginCompleted(_)
|
|
| ServerNotification::AppListUpdated(_)
|
|
| ServerNotification::RemoteControlStatusChanged(_)
|
|
| ServerNotification::ExternalAgentConfigImportCompleted(_)
|
|
| ServerNotification::FsChanged(_)
|
|
| ServerNotification::FuzzyFileSearchSessionUpdated(_)
|
|
| ServerNotification::FuzzyFileSearchSessionCompleted(_)
|
|
| ServerNotification::ThreadRealtimeTranscriptDelta(_)
|
|
| ServerNotification::ThreadRealtimeTranscriptDone(_)
|
|
| ServerNotification::WindowsWorldWritableWarning(_)
|
|
| ServerNotification::WindowsSandboxSetupCompleted(_)
|
|
| ServerNotification::AccountLoginCompleted(_) => {}
|
|
ServerNotification::ContextCompacted(_) => {}
|
|
}
|
|
}
|
|
|
|
fn apply_thread_settings(
|
|
&mut self,
|
|
thread_settings: codex_app_server_protocol::ThreadSettings,
|
|
) {
|
|
let previous_cwd = std::mem::replace(&mut self.config.cwd, thread_settings.cwd.clone());
|
|
self.current_cwd = Some(thread_settings.cwd.to_path_buf());
|
|
if crate::session_state::retarget_implicit_workspace_root(
|
|
&mut self.config.workspace_roots,
|
|
previous_cwd,
|
|
thread_settings.cwd.clone(),
|
|
) {
|
|
self.config
|
|
.permissions
|
|
.set_workspace_roots(self.config.workspace_roots.clone());
|
|
}
|
|
self.config.model_reasoning_summary = thread_settings.summary;
|
|
self.config.personality = thread_settings.personality;
|
|
self.effective_service_tier = thread_settings.service_tier.clone();
|
|
self.config.service_tier = thread_settings.service_tier;
|
|
if let Err(err) = self
|
|
.config
|
|
.permissions
|
|
.approval_policy
|
|
.set(thread_settings.approval_policy.to_core())
|
|
{
|
|
tracing::warn!(%err, "failed to sync approval_policy from thread settings update");
|
|
self.config.permissions.approval_policy =
|
|
Constrained::allow_only(thread_settings.approval_policy.to_core());
|
|
}
|
|
let permission_profile: PermissionProfile = thread_settings.permission_profile.into();
|
|
let active_permission_profile = thread_settings
|
|
.active_permission_profile
|
|
.map(codex_protocol::models::ActivePermissionProfile::from);
|
|
let permission_snapshot = PermissionProfileSnapshot::from_session_snapshot(
|
|
permission_profile,
|
|
active_permission_profile,
|
|
);
|
|
if let Err(err) = self
|
|
.config
|
|
.permissions
|
|
.set_permission_profile_from_session_snapshot(permission_snapshot.clone())
|
|
{
|
|
tracing::warn!(%err, "failed to sync permissions from thread settings update");
|
|
if let Err(replace_err) = self
|
|
.config
|
|
.permissions
|
|
.replace_permission_profile_from_session_snapshot(permission_snapshot)
|
|
{
|
|
tracing::warn!(
|
|
%replace_err,
|
|
"failed to replace permissions from thread settings update"
|
|
);
|
|
}
|
|
}
|
|
self.config.approvals_reviewer = thread_settings.approvals_reviewer.to_core();
|
|
self.current_collaboration_mode = thread_settings.collaboration_mode;
|
|
self.active_collaboration_mask = Some(CollaborationModeMask {
|
|
name: self
|
|
.current_collaboration_mode
|
|
.mode
|
|
.display_name()
|
|
.to_string(),
|
|
mode: Some(self.current_collaboration_mode.mode),
|
|
model: Some(thread_settings.model),
|
|
reasoning_effort: Some(thread_settings.effort),
|
|
developer_instructions: Some(
|
|
self.current_collaboration_mode
|
|
.settings
|
|
.developer_instructions
|
|
.clone(),
|
|
),
|
|
});
|
|
self.update_collaboration_mode_indicator();
|
|
self.refresh_plan_mode_nudge();
|
|
self.refresh_model_dependent_surfaces();
|
|
self.refresh_status_surfaces();
|
|
self.request_redraw();
|
|
}
|
|
|
|
pub(super) fn handle_turn_completed_notification(
|
|
&mut self,
|
|
notification: TurnCompletedNotification,
|
|
replay_kind: Option<ReplayKind>,
|
|
) {
|
|
match notification.turn.status {
|
|
TurnStatus::Completed => {
|
|
self.last_non_retry_error = None;
|
|
self.on_task_complete(
|
|
/*last_agent_message*/ None,
|
|
notification.turn.duration_ms,
|
|
replay_kind.is_some(),
|
|
)
|
|
}
|
|
TurnStatus::Interrupted => {
|
|
self.last_non_retry_error = None;
|
|
let reason = if self
|
|
.turn_lifecycle
|
|
.take_budget_limited(notification.turn.id.as_str())
|
|
{
|
|
TurnAbortReason::BudgetLimited
|
|
} else {
|
|
TurnAbortReason::Interrupted
|
|
};
|
|
self.on_interrupted_turn(reason);
|
|
}
|
|
TurnStatus::Failed => {
|
|
if let Some(error) = notification.turn.error {
|
|
if self.last_non_retry_error.as_ref()
|
|
== Some(&(notification.turn.id.clone(), error.message.clone()))
|
|
{
|
|
self.last_non_retry_error = None;
|
|
} else {
|
|
self.handle_non_retry_error(error.message, error.codex_error_info);
|
|
}
|
|
} else {
|
|
self.last_non_retry_error = None;
|
|
self.finalize_turn();
|
|
self.request_redraw();
|
|
self.maybe_send_next_queued_input();
|
|
}
|
|
}
|
|
TurnStatus::InProgress => {}
|
|
}
|
|
}
|
|
|
|
fn handle_item_started_notification(
|
|
&mut self,
|
|
notification: ItemStartedNotification,
|
|
from_replay: bool,
|
|
) {
|
|
match notification.item {
|
|
item @ ThreadItem::CommandExecution { .. } => self.on_command_execution_started(item),
|
|
ThreadItem::FileChange { id: _, changes, .. } => {
|
|
self.on_patch_apply_begin(file_update_changes_to_display(changes));
|
|
}
|
|
item @ ThreadItem::McpToolCall { .. } => self.on_mcp_tool_call_started(item),
|
|
ThreadItem::WebSearch { id, .. } => {
|
|
self.on_web_search_begin(id);
|
|
}
|
|
ThreadItem::ImageGeneration { .. } => {
|
|
self.on_image_generation_begin();
|
|
}
|
|
ThreadItem::CollabAgentToolCall {
|
|
id,
|
|
tool,
|
|
status,
|
|
sender_thread_id,
|
|
receiver_thread_ids,
|
|
prompt,
|
|
model,
|
|
reasoning_effort,
|
|
agents_states,
|
|
} => self.on_collab_agent_tool_call(ThreadItem::CollabAgentToolCall {
|
|
id,
|
|
tool,
|
|
status,
|
|
sender_thread_id,
|
|
receiver_thread_ids,
|
|
prompt,
|
|
model,
|
|
reasoning_effort,
|
|
agents_states,
|
|
}),
|
|
ThreadItem::EnteredReviewMode { review, .. } => {
|
|
if !from_replay {
|
|
self.enter_review_mode_with_hint(review, /*from_replay*/ false);
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
fn handle_item_completed_notification(
|
|
&mut self,
|
|
notification: ItemCompletedNotification,
|
|
replay_kind: Option<ReplayKind>,
|
|
) {
|
|
self.handle_thread_item(
|
|
notification.item,
|
|
notification.turn_id,
|
|
replay_kind.map_or(ThreadItemRenderSource::Live, ThreadItemRenderSource::Replay),
|
|
);
|
|
}
|
|
}
|