mirror of
https://github.com/openai/codex.git
synced 2026-11-09 20:50:33 +00:00
Remove legacy ChatWidget test event shims
This commit is contained in:
@@ -547,9 +547,6 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::app::test_support::app_enabled_in_effective_config;
|
||||
use crate::app::test_support::make_test_app;
|
||||
use crate::chatwidget::test_events::Event;
|
||||
use crate::chatwidget::test_events::EventMsg;
|
||||
use crate::chatwidget::test_events::SessionConfiguredEvent;
|
||||
use crate::test_support::PathBufExt;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -635,11 +632,11 @@ mod tests {
|
||||
let next_cwd_tmp = tempdir()?;
|
||||
let next_cwd = next_cwd_tmp.path().to_path_buf();
|
||||
|
||||
app.chat_widget.handle_codex_event(Event {
|
||||
id: String::new(),
|
||||
msg: EventMsg::SessionConfigured(SessionConfiguredEvent {
|
||||
session_id: ThreadId::new(),
|
||||
app.chat_widget
|
||||
.handle_thread_session(crate::session_state::ThreadSessionState {
|
||||
thread_id: ThreadId::new(),
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
thread_name: None,
|
||||
model: "gpt-test".to_string(),
|
||||
model_provider_id: "test-provider".to_string(),
|
||||
@@ -648,14 +645,13 @@ mod tests {
|
||||
approvals_reviewer: ApprovalsReviewer::User,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
cwd: next_cwd.clone().abs(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: None,
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
initial_messages: None,
|
||||
network_proxy: None,
|
||||
rollout_path: Some(PathBuf::new()),
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
assert_eq!(app.chat_widget.config_ref().cwd.to_path_buf(), next_cwd);
|
||||
assert_eq!(app.config.cwd, original_cwd);
|
||||
|
||||
@@ -32,9 +32,9 @@ impl ElicitationRequestKey {
|
||||
// - buffer eviction (`note_evicted_event`)
|
||||
//
|
||||
// We keep both fast lookup sets (for snapshot filtering by call_id/request key) and
|
||||
// turn-indexed queues/vectors so `TurnComplete`/`TurnAborted` can clear stale prompts tied
|
||||
// to a turn. `request_user_input` removal is FIFO because the overlay answers queued prompts
|
||||
// in FIFO order for a shared `turn_id`.
|
||||
// turn-indexed queues/vectors so turn completion or interruption can clear
|
||||
// stale prompts tied to a turn. `request_user_input` removal is FIFO because
|
||||
// the overlay answers queued prompts in FIFO order for a shared `turn_id`.
|
||||
pub(super) struct PendingInteractiveReplayState {
|
||||
exec_approval_call_ids: HashSet<String>,
|
||||
exec_approval_call_ids_by_turn_id: HashMap<String, Vec<String>>,
|
||||
|
||||
@@ -22,9 +22,6 @@ use crate::multi_agents::AgentPickerThreadEntry;
|
||||
use assert_matches::assert_matches;
|
||||
|
||||
use crate::app_command::AppCommand as Op;
|
||||
use crate::chatwidget::test_events::Event;
|
||||
use crate::chatwidget::test_events::EventMsg;
|
||||
use crate::chatwidget::test_events::SessionConfiguredEvent;
|
||||
use crate::diff_model::FileChange;
|
||||
use crate::legacy_core::config::ConfigBuilder;
|
||||
use crate::legacy_core::config::ConfigOverrides;
|
||||
@@ -3176,6 +3173,7 @@ async fn side_thread_snapshot_hides_forked_parent_transcript() {
|
||||
let mut store = ThreadEventStore::new(/*capacity*/ 4);
|
||||
let session = ThreadSessionState {
|
||||
forked_from_id: Some(parent_thread_id),
|
||||
fork_parent_title: None,
|
||||
..test_thread_session(side_thread_id, test_path_buf("/tmp/side"))
|
||||
};
|
||||
let parent_turn = test_turn(
|
||||
@@ -3238,6 +3236,7 @@ async fn side_thread_snapshot_skips_session_header_preamble() {
|
||||
let snapshot = ThreadEventSnapshot {
|
||||
session: Some(ThreadSessionState {
|
||||
forked_from_id: Some(parent_thread_id),
|
||||
fork_parent_title: None,
|
||||
..test_thread_session(side_thread_id, test_path_buf("/tmp/side"))
|
||||
}),
|
||||
turns: Vec::new(),
|
||||
@@ -4350,11 +4349,11 @@ async fn backtrack_selection_with_duplicate_history_targets_unique_turn() {
|
||||
assert_eq!(user_count(&app.transcript_cells), 2);
|
||||
|
||||
let base_id = ThreadId::new();
|
||||
app.chat_widget.handle_codex_event(Event {
|
||||
id: String::new(),
|
||||
msg: EventMsg::SessionConfigured(SessionConfiguredEvent {
|
||||
session_id: base_id,
|
||||
app.chat_widget
|
||||
.handle_thread_session(crate::session_state::ThreadSessionState {
|
||||
thread_id: base_id,
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
thread_name: None,
|
||||
model: "gpt-test".to_string(),
|
||||
model_provider_id: "test-provider".to_string(),
|
||||
@@ -4363,14 +4362,13 @@ async fn backtrack_selection_with_duplicate_history_targets_unique_turn() {
|
||||
approvals_reviewer: ApprovalsReviewer::User,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: None,
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
initial_messages: None,
|
||||
network_proxy: None,
|
||||
rollout_path: Some(PathBuf::new()),
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
app.backtrack.base_id = Some(base_id);
|
||||
app.backtrack.primed = true;
|
||||
@@ -4443,11 +4441,11 @@ async fn backtrack_resubmit_preserves_data_image_urls_in_user_turn() {
|
||||
let (mut app, _app_event_rx, mut op_rx) = make_test_app_with_channels().await;
|
||||
|
||||
let thread_id = ThreadId::new();
|
||||
app.chat_widget.handle_codex_event(Event {
|
||||
id: String::new(),
|
||||
msg: EventMsg::SessionConfigured(SessionConfiguredEvent {
|
||||
session_id: thread_id,
|
||||
app.chat_widget
|
||||
.handle_thread_session(crate::session_state::ThreadSessionState {
|
||||
thread_id,
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
thread_name: None,
|
||||
model: "gpt-test".to_string(),
|
||||
model_provider_id: "test-provider".to_string(),
|
||||
@@ -4456,14 +4454,13 @@ async fn backtrack_resubmit_preserves_data_image_urls_in_user_turn() {
|
||||
approvals_reviewer: ApprovalsReviewer::User,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: None,
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
initial_messages: None,
|
||||
network_proxy: None,
|
||||
rollout_path: Some(PathBuf::new()),
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
let data_image_url = "data:image/png;base64,abc123".to_string();
|
||||
app.transcript_cells = vec![Arc::new(UserHistoryCell {
|
||||
@@ -4684,6 +4681,7 @@ async fn refreshed_snapshot_session_persists_resumed_turns() {
|
||||
)];
|
||||
let resumed_session = ThreadSessionState {
|
||||
cwd: test_path_buf("/tmp/refreshed").abs(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
..initial_session.clone()
|
||||
};
|
||||
let mut snapshot = ThreadEventSnapshot {
|
||||
@@ -4831,9 +4829,10 @@ async fn new_session_requests_shutdown_for_previous_conversation() {
|
||||
let (mut app, mut app_event_rx, mut op_rx) = make_test_app_with_channels().await;
|
||||
|
||||
let thread_id = ThreadId::new();
|
||||
let event = SessionConfiguredEvent {
|
||||
session_id: thread_id,
|
||||
let event = crate::session_state::ThreadSessionState {
|
||||
thread_id,
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
thread_name: None,
|
||||
model: "gpt-test".to_string(),
|
||||
model_provider_id: "test-provider".to_string(),
|
||||
@@ -4842,18 +4841,15 @@ async fn new_session_requests_shutdown_for_previous_conversation() {
|
||||
approvals_reviewer: ApprovalsReviewer::User,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: None,
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
initial_messages: None,
|
||||
network_proxy: None,
|
||||
rollout_path: Some(PathBuf::new()),
|
||||
};
|
||||
|
||||
app.chat_widget.handle_codex_event(Event {
|
||||
id: String::new(),
|
||||
msg: EventMsg::SessionConfigured(event),
|
||||
});
|
||||
app.chat_widget.handle_thread_session(event);
|
||||
|
||||
while app_event_rx.try_recv().is_ok() {}
|
||||
while op_rx.try_recv().is_ok() {}
|
||||
@@ -4941,11 +4937,11 @@ async fn interrupt_without_active_turn_is_treated_as_handled() {
|
||||
async fn clear_only_ui_reset_preserves_chat_session_state() {
|
||||
let mut app = make_test_app().await;
|
||||
let thread_id = ThreadId::new();
|
||||
app.chat_widget.handle_codex_event(Event {
|
||||
id: String::new(),
|
||||
msg: EventMsg::SessionConfigured(SessionConfiguredEvent {
|
||||
session_id: thread_id,
|
||||
app.chat_widget
|
||||
.handle_thread_session(crate::session_state::ThreadSessionState {
|
||||
thread_id,
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
thread_name: Some("keep me".to_string()),
|
||||
model: "gpt-test".to_string(),
|
||||
model_provider_id: "test-provider".to_string(),
|
||||
@@ -4954,14 +4950,13 @@ async fn clear_only_ui_reset_preserves_chat_session_state() {
|
||||
approvals_reviewer: ApprovalsReviewer::User,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
cwd: test_path_buf("/tmp/project").abs(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: None,
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
initial_messages: None,
|
||||
network_proxy: None,
|
||||
rollout_path: Some(PathBuf::new()),
|
||||
}),
|
||||
});
|
||||
});
|
||||
app.chat_widget
|
||||
.apply_external_edit("draft prompt".to_string());
|
||||
app.transcript_cells = vec![Arc::new(UserHistoryCell {
|
||||
|
||||
@@ -497,8 +497,8 @@ impl App {
|
||||
self.backtrack.pending_rollback = None;
|
||||
}
|
||||
|
||||
/// Apply rollback semantics for `ThreadRolledBack` events where this TUI does not have an
|
||||
/// in-flight backtrack request (`pending_rollback` is `None`).
|
||||
/// Apply rollback semantics for a confirmed rollback where this TUI does
|
||||
/// not have an in-flight backtrack request (`pending_rollback` is `None`).
|
||||
///
|
||||
/// Returns `true` when local transcript state changed.
|
||||
pub(crate) fn apply_non_pending_thread_rollback(&mut self, num_turns: u32) -> bool {
|
||||
|
||||
@@ -42,44 +42,6 @@ use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
#[cfg(test)]
|
||||
use self::test_events::AgentMessageDeltaEvent;
|
||||
#[cfg(test)]
|
||||
use self::test_events::AgentMessageEvent;
|
||||
#[cfg(test)]
|
||||
use self::test_events::AgentReasoningDeltaEvent;
|
||||
#[cfg(test)]
|
||||
use self::test_events::AgentReasoningEvent;
|
||||
#[cfg(test)]
|
||||
use self::test_events::AgentReasoningRawContentDeltaEvent;
|
||||
#[cfg(test)]
|
||||
use self::test_events::AgentReasoningRawContentEvent;
|
||||
#[cfg(test)]
|
||||
use self::test_events::BackgroundEventEvent;
|
||||
#[cfg(test)]
|
||||
use self::test_events::ErrorEvent;
|
||||
#[cfg(test)]
|
||||
use self::test_events::Event;
|
||||
#[cfg(test)]
|
||||
use self::test_events::EventMsg;
|
||||
#[cfg(test)]
|
||||
use self::test_events::ExitedReviewModeEvent;
|
||||
#[cfg(test)]
|
||||
use self::test_events::McpListToolsResponseEvent;
|
||||
#[cfg(test)]
|
||||
use self::test_events::SessionConfiguredEvent;
|
||||
#[cfg(test)]
|
||||
use self::test_events::StreamErrorEvent;
|
||||
#[cfg(test)]
|
||||
use self::test_events::TurnCompleteEvent;
|
||||
#[cfg(test)]
|
||||
use self::test_events::TurnDiffEvent;
|
||||
#[cfg(test)]
|
||||
use self::test_events::UndoCompletedEvent;
|
||||
#[cfg(test)]
|
||||
use self::test_events::UndoStartedEvent;
|
||||
#[cfg(test)]
|
||||
use self::test_events::WarningEvent;
|
||||
use crate::app::app_server_requests::ResolvedAppServerRequest;
|
||||
use crate::app_command::AppCommand;
|
||||
use crate::app_event::HistoryLookupResponse;
|
||||
@@ -143,8 +105,6 @@ use codex_app_server_protocol::AddCreditsNudgeCreditType;
|
||||
use codex_app_server_protocol::AddCreditsNudgeEmailStatus;
|
||||
use codex_app_server_protocol::AppInfo;
|
||||
use codex_app_server_protocol::AppSummary;
|
||||
#[cfg(test)]
|
||||
use codex_app_server_protocol::CodexErrorInfo as CoreCodexErrorInfo;
|
||||
use codex_app_server_protocol::CodexErrorInfo as AppServerCodexErrorInfo;
|
||||
use codex_app_server_protocol::CollabAgentTool;
|
||||
use codex_app_server_protocol::CollabAgentToolCallStatus;
|
||||
@@ -158,8 +118,6 @@ use codex_app_server_protocol::GuardianApprovalReviewAction;
|
||||
use codex_app_server_protocol::ItemCompletedNotification;
|
||||
use codex_app_server_protocol::ItemStartedNotification;
|
||||
use codex_app_server_protocol::McpServerStatusDetail;
|
||||
#[cfg(test)]
|
||||
use codex_app_server_protocol::ModelVerification as CoreModelVerification;
|
||||
use codex_app_server_protocol::ModelVerification as AppServerModelVerification;
|
||||
use codex_app_server_protocol::RateLimitReachedType;
|
||||
use codex_app_server_protocol::RateLimitSnapshot;
|
||||
@@ -649,18 +607,6 @@ enum RateLimitErrorKind {
|
||||
Generic,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn core_rate_limit_error_kind(info: &CoreCodexErrorInfo) -> Option<RateLimitErrorKind> {
|
||||
match info {
|
||||
CoreCodexErrorInfo::ServerOverloaded => Some(RateLimitErrorKind::ServerOverloaded),
|
||||
CoreCodexErrorInfo::UsageLimitExceeded => Some(RateLimitErrorKind::UsageLimit),
|
||||
CoreCodexErrorInfo::ResponseTooManyFailedAttempts {
|
||||
http_status_code: Some(429),
|
||||
} => Some(RateLimitErrorKind::Generic),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn app_server_rate_limit_error_kind(info: &AppServerCodexErrorInfo) -> Option<RateLimitErrorKind> {
|
||||
match info {
|
||||
AppServerCodexErrorInfo::ServerOverloaded => Some(RateLimitErrorKind::ServerOverloaded),
|
||||
@@ -672,11 +618,6 @@ fn app_server_rate_limit_error_kind(info: &AppServerCodexErrorInfo) -> Option<Ra
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn is_core_cyber_policy_error(info: &CoreCodexErrorInfo) -> bool {
|
||||
matches!(info, CoreCodexErrorInfo::CyberPolicy)
|
||||
}
|
||||
|
||||
fn is_app_server_cyber_policy_error(info: &AppServerCodexErrorInfo) -> bool {
|
||||
matches!(info, AppServerCodexErrorInfo::CyberPolicy)
|
||||
}
|
||||
@@ -2013,15 +1954,7 @@ impl ChatWidget {
|
||||
.iter()
|
||||
.any(|item| item == "run-state" || item == "status")
|
||||
});
|
||||
let title_uses_activity = self.config.tui_terminal_title.as_ref().is_none_or(|items| {
|
||||
items
|
||||
.iter()
|
||||
.any(|item| item == "activity" || item == "spinner")
|
||||
});
|
||||
if title_uses_status
|
||||
|| (title_uses_activity
|
||||
&& self.terminal_title_status_kind == TerminalTitleStatusKind::Undoing)
|
||||
{
|
||||
if title_uses_status {
|
||||
self.refresh_status_surfaces();
|
||||
}
|
||||
}
|
||||
@@ -2199,23 +2132,11 @@ impl ChatWidget {
|
||||
}
|
||||
|
||||
// --- Small event handlers ---
|
||||
#[cfg(test)]
|
||||
fn on_session_configured(&mut self, event: SessionConfiguredEvent) {
|
||||
let (session, initial_messages) = event.into_session();
|
||||
self.on_session_configured_with_display_and_fork_parent_title(
|
||||
session,
|
||||
SessionConfiguredDisplay::Normal,
|
||||
/*fork_parent_title*/ None,
|
||||
initial_messages,
|
||||
);
|
||||
}
|
||||
|
||||
fn on_session_configured_with_display_and_fork_parent_title(
|
||||
&mut self,
|
||||
session: ThreadSessionState,
|
||||
display: SessionConfiguredDisplay,
|
||||
fork_parent_title: Option<String>,
|
||||
#[cfg(test)] initial_messages: Option<Vec<EventMsg>>,
|
||||
) {
|
||||
self.last_agent_markdown = None;
|
||||
self.agent_turn_markdowns.clear();
|
||||
@@ -2300,11 +2221,6 @@ impl ChatWidget {
|
||||
show_fast_status,
|
||||
);
|
||||
self.apply_session_info_cell(session_info_cell);
|
||||
|
||||
#[cfg(test)]
|
||||
if let Some(messages) = initial_messages {
|
||||
self.replay_initial_messages(messages);
|
||||
}
|
||||
} else if self
|
||||
.active_cell
|
||||
.as_ref()
|
||||
@@ -2352,9 +2268,6 @@ impl ChatWidget {
|
||||
session,
|
||||
SessionConfiguredDisplay::Normal,
|
||||
fork_parent_title,
|
||||
#[cfg(test)]
|
||||
/*initial_messages*/
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2364,9 +2277,6 @@ impl ChatWidget {
|
||||
session,
|
||||
SessionConfiguredDisplay::Quiet,
|
||||
/*fork_parent_title*/ None,
|
||||
#[cfg(test)]
|
||||
/*initial_messages*/
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2377,9 +2287,6 @@ impl ChatWidget {
|
||||
session,
|
||||
SessionConfiguredDisplay::SideConversation,
|
||||
fork_parent_title,
|
||||
#[cfg(test)]
|
||||
/*initial_messages*/
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2496,11 +2403,6 @@ impl ChatWidget {
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn on_agent_message(&mut self, message: String) {
|
||||
self.finalize_completed_assistant_message(Some(&message));
|
||||
}
|
||||
|
||||
fn on_agent_message_delta(&mut self, delta: String) {
|
||||
self.handle_streaming_delta(delta);
|
||||
}
|
||||
@@ -2917,14 +2819,6 @@ impl ChatWidget {
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn handle_steer_rejected_error(&mut self, codex_error_info: &CoreCodexErrorInfo) -> bool {
|
||||
matches!(
|
||||
codex_error_info,
|
||||
CoreCodexErrorInfo::ActiveTurnNotSteerable { .. }
|
||||
) && self.enqueue_rejected_steer()
|
||||
}
|
||||
|
||||
fn handle_app_server_steer_rejected_error(
|
||||
&mut self,
|
||||
codex_error_info: &AppServerCodexErrorInfo,
|
||||
@@ -3036,28 +2930,6 @@ impl ChatWidget {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn apply_turn_started_context_window(&mut self, model_context_window: Option<i64>) {
|
||||
let info = match self.token_info.take() {
|
||||
Some(mut info) => {
|
||||
info.model_context_window = model_context_window;
|
||||
info
|
||||
}
|
||||
None => {
|
||||
let Some(model_context_window) = model_context_window else {
|
||||
return;
|
||||
};
|
||||
TokenUsageInfo {
|
||||
total_token_usage: TokenUsage::default(),
|
||||
last_token_usage: TokenUsage::default(),
|
||||
model_context_window: Some(model_context_window),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
self.apply_token_info(info);
|
||||
}
|
||||
|
||||
fn apply_token_info(&mut self, info: TokenUsageInfo) {
|
||||
let percent = self.context_remaining_percent(&info);
|
||||
let used_tokens = self.context_used_tokens(&info, percent.is_some());
|
||||
@@ -3344,13 +3216,6 @@ impl ChatWidget {
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn on_core_model_verification(&mut self, verifications: &[CoreModelVerification]) {
|
||||
if verifications.contains(&CoreModelVerification::TrustedAccessForCyber) {
|
||||
self.on_warning(TRUSTED_ACCESS_FOR_CYBER_VERIFICATION_WARNING);
|
||||
}
|
||||
}
|
||||
|
||||
fn on_app_server_model_verification(&mut self, verifications: &[AppServerModelVerification]) {
|
||||
if verifications.contains(&AppServerModelVerification::TrustedAccessForCyber) {
|
||||
self.on_warning(TRUSTED_ACCESS_FOR_CYBER_VERIFICATION_WARNING);
|
||||
@@ -3366,9 +3231,7 @@ impl ChatWidget {
|
||||
self.finalize_turn();
|
||||
let send_pending_steers_immediately = self.submit_pending_steers_after_interrupt;
|
||||
self.submit_pending_steers_after_interrupt = false;
|
||||
if reason != TurnAbortReason::ReviewEnded
|
||||
&& self.interrupted_turn_notice_mode != InterruptedTurnNoticeMode::Suppress
|
||||
{
|
||||
if self.interrupted_turn_notice_mode != InterruptedTurnNoticeMode::Suppress {
|
||||
if send_pending_steers_immediately {
|
||||
self.add_to_history(history_cell::new_info_event(
|
||||
"Model interrupted to submit steer instructions.".to_owned(),
|
||||
@@ -3381,8 +3244,9 @@ impl ChatWidget {
|
||||
}
|
||||
}
|
||||
|
||||
// Core clears pending_input before emitting TurnAborted, so any unacknowledged steers
|
||||
// still tracked here must be restored locally instead of waiting for a later commit.
|
||||
// The server has already discarded pending input by the time the
|
||||
// interrupted turn reaches the UI, so any unacknowledged steers still
|
||||
// tracked here must be restored locally instead of waiting for a later commit.
|
||||
if send_pending_steers_immediately {
|
||||
let pending_steers = self
|
||||
.pending_steers
|
||||
@@ -4226,16 +4090,6 @@ impl ChatWidget {
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn on_background_event(&mut self, message: String) {
|
||||
debug!("BackgroundEvent: {message}");
|
||||
self.bottom_pane.ensure_status_indicator();
|
||||
self.bottom_pane
|
||||
.set_interrupt_hint_visible(/*visible*/ true);
|
||||
self.terminal_title_status_kind = TerminalTitleStatusKind::Thinking;
|
||||
self.set_status_header(message);
|
||||
}
|
||||
|
||||
fn on_hook_started(&mut self, event: HookStartedEvent) {
|
||||
self.flush_answer_stream_with_separator();
|
||||
self.flush_completed_hook_output();
|
||||
@@ -4358,38 +4212,6 @@ impl ChatWidget {
|
||||
self.frame_requester.schedule_frame_in(delay);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn on_undo_started(&mut self, event: UndoStartedEvent) {
|
||||
self.bottom_pane.ensure_status_indicator();
|
||||
self.bottom_pane
|
||||
.set_interrupt_hint_visible(/*visible*/ false);
|
||||
let message = event
|
||||
.message
|
||||
.unwrap_or_else(|| "Undo in progress...".to_string());
|
||||
self.terminal_title_status_kind = TerminalTitleStatusKind::Undoing;
|
||||
self.set_status_header(message);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn on_undo_completed(&mut self, event: UndoCompletedEvent) {
|
||||
let UndoCompletedEvent { success, message } = event;
|
||||
self.bottom_pane.hide_status_indicator();
|
||||
self.terminal_title_status_kind = TerminalTitleStatusKind::Working;
|
||||
self.refresh_terminal_title();
|
||||
let message = message.unwrap_or_else(|| {
|
||||
if success {
|
||||
"Undo completed successfully.".to_string()
|
||||
} else {
|
||||
"Undo failed.".to_string()
|
||||
}
|
||||
});
|
||||
if success {
|
||||
self.add_info_message(message, /*hint*/ None);
|
||||
} else {
|
||||
self.add_error_message(message);
|
||||
}
|
||||
}
|
||||
|
||||
fn on_stream_error(&mut self, message: String, additional_details: Option<String>) {
|
||||
if self.retry_status_header.is_none() {
|
||||
self.retry_status_header = Some(self.current_status.header.clone());
|
||||
@@ -6965,289 +6787,6 @@ impl ChatWidget {
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn replay_initial_messages(&mut self, events: Vec<EventMsg>) {
|
||||
for msg in events {
|
||||
if matches!(
|
||||
msg,
|
||||
EventMsg::SessionConfigured(_) | EventMsg::ThreadNameUpdated(_)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
// `id: None` indicates a synthetic/fake id coming from replay.
|
||||
self.dispatch_event_msg(
|
||||
/*id*/ None,
|
||||
msg,
|
||||
Some(ReplayKind::ResumeInitialMessages),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn handle_codex_event(&mut self, event: Event) {
|
||||
let Event { id, msg } = event;
|
||||
self.dispatch_event_msg(Some(id), msg, /*replay_kind*/ None);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn handle_codex_event_replay(&mut self, event: Event) {
|
||||
let Event { msg, .. } = event;
|
||||
if matches!(msg, EventMsg::ShutdownComplete) {
|
||||
return;
|
||||
}
|
||||
self.dispatch_event_msg(/*id*/ None, msg, Some(ReplayKind::ThreadSnapshot));
|
||||
}
|
||||
|
||||
/// Dispatch a test-only event message to the appropriate handler.
|
||||
///
|
||||
/// `id` is `Some` for live events and `None` for replayed events from
|
||||
/// `replay_initial_messages()`. Callers should treat `None` as a "fake" id
|
||||
/// that must not be used to correlate follow-up actions.
|
||||
#[cfg(test)]
|
||||
fn dispatch_event_msg(
|
||||
&mut self,
|
||||
id: Option<String>,
|
||||
msg: EventMsg,
|
||||
replay_kind: Option<ReplayKind>,
|
||||
) {
|
||||
let from_replay = replay_kind.is_some();
|
||||
let is_resume_initial_replay =
|
||||
matches!(replay_kind, Some(ReplayKind::ResumeInitialMessages));
|
||||
let is_stream_error = matches!(&msg, EventMsg::StreamError(_));
|
||||
if !is_resume_initial_replay && !is_stream_error {
|
||||
self.restore_retry_status_header_if_present();
|
||||
}
|
||||
|
||||
match msg {
|
||||
EventMsg::AgentMessageDelta(_)
|
||||
| EventMsg::PlanDelta(_)
|
||||
| EventMsg::AgentReasoningDelta(_)
|
||||
| EventMsg::TerminalInteraction(_)
|
||||
| EventMsg::ExecCommandOutputDelta(_) => {}
|
||||
_ => {
|
||||
tracing::trace!("handle_codex_event: {:?}", msg);
|
||||
}
|
||||
}
|
||||
|
||||
match msg {
|
||||
EventMsg::SessionConfigured(e) => self.on_session_configured(e),
|
||||
EventMsg::ThreadNameUpdated(e) => {
|
||||
self.on_thread_name_updated(e.thread_id, e.thread_name)
|
||||
}
|
||||
// NOTE: All three AgentMessage arms feed `record_agent_markdown` even
|
||||
// when the message is otherwise not rendered (thread-snapshot replay,
|
||||
// non-review live messages). This ensures the copy source stays
|
||||
// populated across replay, resume, and live paths.
|
||||
EventMsg::AgentMessage(AgentMessageEvent { message, .. })
|
||||
if matches!(replay_kind, Some(ReplayKind::ThreadSnapshot))
|
||||
&& !self.is_review_mode =>
|
||||
{
|
||||
if !message.is_empty() {
|
||||
self.record_agent_markdown(&message);
|
||||
}
|
||||
}
|
||||
EventMsg::AgentMessage(AgentMessageEvent { message, .. })
|
||||
if from_replay || self.is_review_mode =>
|
||||
{
|
||||
if !message.is_empty() {
|
||||
self.record_agent_markdown(&message);
|
||||
}
|
||||
// TODO(ccunningham): stop relying on legacy AgentMessage in review mode,
|
||||
// including thread-snapshot replay, and forward
|
||||
// ItemCompleted(TurnItem::AgentMessage(_)) instead.
|
||||
self.on_agent_message(message)
|
||||
}
|
||||
EventMsg::AgentMessage(AgentMessageEvent { message, .. }) => {
|
||||
if !message.is_empty() {
|
||||
self.record_agent_markdown(&message);
|
||||
}
|
||||
}
|
||||
EventMsg::AgentMessageDelta(AgentMessageDeltaEvent { delta }) => {
|
||||
self.on_agent_message_delta(delta)
|
||||
}
|
||||
EventMsg::PlanDelta(event) => self.on_plan_delta(event.delta),
|
||||
EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent { delta })
|
||||
| EventMsg::AgentReasoningRawContentDelta(AgentReasoningRawContentDeltaEvent {
|
||||
delta,
|
||||
}) => self.on_agent_reasoning_delta(delta),
|
||||
EventMsg::AgentReasoning(AgentReasoningEvent { .. }) => self.on_agent_reasoning_final(),
|
||||
EventMsg::AgentReasoningRawContent(AgentReasoningRawContentEvent { text }) => {
|
||||
self.on_agent_reasoning_delta(text);
|
||||
self.on_agent_reasoning_final();
|
||||
}
|
||||
EventMsg::TurnStarted(event) => {
|
||||
let turn_id = event.turn_id;
|
||||
let model_context_window = event.model_context_window;
|
||||
self.last_turn_id = Some(turn_id);
|
||||
if !is_resume_initial_replay {
|
||||
self.apply_turn_started_context_window(model_context_window);
|
||||
self.on_task_started();
|
||||
}
|
||||
}
|
||||
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
last_agent_message,
|
||||
duration_ms,
|
||||
..
|
||||
}) => {
|
||||
self.on_task_complete(last_agent_message, duration_ms, from_replay);
|
||||
}
|
||||
EventMsg::TokenCount(ev) => {
|
||||
self.set_token_info(ev.info);
|
||||
self.on_rate_limit_snapshot(ev.rate_limits);
|
||||
}
|
||||
EventMsg::Warning(WarningEvent { message })
|
||||
| EventMsg::GuardianWarning(WarningEvent { message }) => self.on_warning(message),
|
||||
EventMsg::GuardianAssessment(ev) => self.on_guardian_assessment(ev),
|
||||
EventMsg::ModelVerification(event) => {
|
||||
self.on_core_model_verification(&event.verifications)
|
||||
}
|
||||
EventMsg::Error(ErrorEvent {
|
||||
message,
|
||||
codex_error_info,
|
||||
}) => {
|
||||
if codex_error_info
|
||||
.as_ref()
|
||||
.is_some_and(|info| self.handle_steer_rejected_error(info))
|
||||
{
|
||||
} else if codex_error_info
|
||||
.as_ref()
|
||||
.is_some_and(is_core_cyber_policy_error)
|
||||
{
|
||||
self.on_cyber_policy_error();
|
||||
} else if let Some(kind) = codex_error_info
|
||||
.as_ref()
|
||||
.and_then(core_rate_limit_error_kind)
|
||||
{
|
||||
match kind {
|
||||
RateLimitErrorKind::ServerOverloaded => {
|
||||
self.on_server_overloaded_error(message)
|
||||
}
|
||||
RateLimitErrorKind::UsageLimit | RateLimitErrorKind::Generic => {
|
||||
self.on_rate_limit_error(kind, message)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.on_error(message);
|
||||
}
|
||||
}
|
||||
EventMsg::TurnAborted(ev) => match ev.reason {
|
||||
TurnAbortReason::Interrupted => {
|
||||
let reason = if ev
|
||||
.turn_id
|
||||
.as_deref()
|
||||
.is_some_and(|turn_id| self.budget_limited_turn_ids.remove(turn_id))
|
||||
{
|
||||
TurnAbortReason::BudgetLimited
|
||||
} else {
|
||||
ev.reason
|
||||
};
|
||||
self.on_interrupted_turn(reason);
|
||||
}
|
||||
TurnAbortReason::Replaced => {
|
||||
self.submit_pending_steers_after_interrupt = false;
|
||||
self.pending_steers.clear();
|
||||
self.refresh_pending_input_preview();
|
||||
self.on_error("Turn aborted: replaced by a new task".to_owned())
|
||||
}
|
||||
TurnAbortReason::ReviewEnded => {
|
||||
self.on_interrupted_turn(ev.reason);
|
||||
}
|
||||
TurnAbortReason::BudgetLimited => {
|
||||
if let Some(turn_id) = ev.turn_id.as_deref() {
|
||||
self.budget_limited_turn_ids.remove(turn_id);
|
||||
}
|
||||
self.on_interrupted_turn(ev.reason);
|
||||
}
|
||||
},
|
||||
EventMsg::PlanUpdate(update) => self.on_plan_update(update),
|
||||
EventMsg::ExecApprovalRequest(ev) => {
|
||||
// For replayed events, synthesize an empty id (these should not occur).
|
||||
self.on_exec_approval_request(id.unwrap_or_default(), ev)
|
||||
}
|
||||
EventMsg::ApplyPatchApprovalRequest(ev) => {
|
||||
self.on_apply_patch_approval_request(id.unwrap_or_default(), ev)
|
||||
}
|
||||
EventMsg::ElicitationRequest(ev) => {
|
||||
self.on_elicitation_request(ev);
|
||||
}
|
||||
EventMsg::RequestUserInput(ev) => {
|
||||
self.on_request_user_input(ev);
|
||||
}
|
||||
EventMsg::RequestPermissions(ev) => {
|
||||
self.on_request_permissions(ev);
|
||||
}
|
||||
EventMsg::ExecCommandBegin(ev) => self.on_exec_command_begin(ev),
|
||||
EventMsg::TerminalInteraction(delta) => self.on_terminal_interaction(delta),
|
||||
EventMsg::ExecCommandOutputDelta(delta) => self.on_exec_command_output_delta(delta),
|
||||
EventMsg::PatchApplyBegin(ev) => self.on_patch_apply_begin(ev),
|
||||
EventMsg::PatchApplyEnd(ev) => self.on_patch_apply_end(ev),
|
||||
EventMsg::ExecCommandEnd(ev) => self.on_exec_command_end(ev),
|
||||
EventMsg::ViewImageToolCall(ev) => self.on_view_image_tool_call(ev),
|
||||
EventMsg::ImageGenerationBegin(ev) => self.on_image_generation_begin(ev),
|
||||
EventMsg::ImageGenerationEnd(ev) => self.on_image_generation_end(ev),
|
||||
EventMsg::McpToolCallBegin(ev) => self.on_mcp_tool_call_begin(ev),
|
||||
EventMsg::McpToolCallEnd(ev) => self.on_mcp_tool_call_end(ev),
|
||||
EventMsg::WebSearchBegin(ev) => self.on_web_search_begin(ev),
|
||||
EventMsg::WebSearchEnd(ev) => self.on_web_search_end(ev),
|
||||
EventMsg::McpListToolsResponse(ev) => self.on_list_mcp_tools(ev),
|
||||
EventMsg::ListSkillsResponse(ev) => self.on_list_skills(ev),
|
||||
EventMsg::SkillsUpdateAvailable => {
|
||||
self.refresh_skills_for_current_cwd(/*force_reload*/ true);
|
||||
}
|
||||
EventMsg::ShutdownComplete => self.on_shutdown_complete(),
|
||||
EventMsg::TurnDiff(TurnDiffEvent { unified_diff }) => self.on_turn_diff(unified_diff),
|
||||
EventMsg::DeprecationNotice(ev) => self.on_deprecation_notice(ev.summary, ev.details),
|
||||
EventMsg::BackgroundEvent(BackgroundEventEvent { message }) => {
|
||||
self.on_background_event(message)
|
||||
}
|
||||
EventMsg::UndoStarted(ev) => self.on_undo_started(ev),
|
||||
EventMsg::UndoCompleted(ev) => self.on_undo_completed(ev),
|
||||
EventMsg::StreamError(StreamErrorEvent {
|
||||
message,
|
||||
additional_details,
|
||||
..
|
||||
}) => {
|
||||
if !is_resume_initial_replay {
|
||||
self.on_stream_error(message, additional_details);
|
||||
}
|
||||
}
|
||||
EventMsg::UserMessage(ev) => {
|
||||
if from_replay || self.should_render_realtime_user_message_event(&ev) {
|
||||
self.on_user_message_event(ev);
|
||||
}
|
||||
}
|
||||
EventMsg::EnteredReviewMode(hint) => {
|
||||
self.enter_review_mode_with_hint(hint, from_replay)
|
||||
}
|
||||
EventMsg::ExitedReviewMode(review) => self.on_exited_review_mode(review),
|
||||
EventMsg::ThreadRolledBack(rollback) => {
|
||||
if from_replay {
|
||||
self.app_event_tx.send(AppEvent::ApplyThreadRollback {
|
||||
num_turns: rollback.num_turns,
|
||||
});
|
||||
}
|
||||
}
|
||||
EventMsg::HookStarted(event) => self.on_hook_started(event),
|
||||
EventMsg::HookCompleted(event) => self.on_hook_completed(event),
|
||||
EventMsg::ItemCompleted(event) => {
|
||||
let item = event.item;
|
||||
if !from_replay && let codex_protocol::items::TurnItem::UserMessage(item) = &item {
|
||||
self.on_committed_user_message(item, from_replay);
|
||||
}
|
||||
if let codex_protocol::items::TurnItem::Plan(plan_item) = &item {
|
||||
self.on_plan_item_completed(plan_item.text.clone());
|
||||
}
|
||||
if let codex_protocol::items::TurnItem::AgentMessage(item) = item {
|
||||
self.on_agent_message_item_completed(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !from_replay && self.agent_turn_running {
|
||||
self.refresh_runtime_metrics();
|
||||
}
|
||||
}
|
||||
|
||||
fn enter_review_mode_with_hint(&mut self, hint: String, from_replay: bool) {
|
||||
if self.pre_review_token_info.is_none() {
|
||||
self.pre_review_token_info = Some(self.token_info.clone());
|
||||
@@ -7273,11 +6812,6 @@ impl ChatWidget {
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn on_exited_review_mode(&mut self, _review: ExitedReviewModeEvent) {
|
||||
self.exit_review_mode_after_item();
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -11115,17 +10649,6 @@ impl ChatWidget {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn on_list_mcp_tools(&mut self, ev: McpListToolsResponseEvent) {
|
||||
self.add_to_history(history_cell::new_mcp_tools_output(
|
||||
&self.config,
|
||||
ev.tools,
|
||||
ev.resources,
|
||||
ev.resource_templates,
|
||||
&ev.auth_statuses,
|
||||
));
|
||||
}
|
||||
|
||||
fn on_list_skills(&mut self, ev: SkillsListResponse) {
|
||||
self.set_skills_from_response(&ev);
|
||||
self.refresh_plugin_mentions();
|
||||
@@ -11744,8 +11267,5 @@ pub(crate) fn show_review_commit_picker_with_entries(
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod test_events;
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests;
|
||||
|
||||
@@ -65,18 +65,6 @@ impl RealtimeConversationUiState {
|
||||
}
|
||||
|
||||
impl ChatWidget {
|
||||
#[cfg(test)]
|
||||
pub(super) fn should_render_realtime_user_message_event(
|
||||
&self,
|
||||
event: &UserMessageEvent,
|
||||
) -> bool {
|
||||
if !self.realtime_conversation.is_live() {
|
||||
return false;
|
||||
}
|
||||
let key = Self::rendered_user_message_event_from_event(event);
|
||||
self.last_rendered_user_message_event.as_ref() != Some(&key)
|
||||
}
|
||||
|
||||
fn realtime_footer_hint_items() -> Vec<(String, String)> {
|
||||
vec![("/realtime".to_string(), "stop live voice".to_string())]
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests.rs
|
||||
source: tui/src/chatwidget/tests/status_and_layout.rs
|
||||
expression: combined
|
||||
---
|
||||
|
||||
• Here is the result.
|
||||
|
||||
@@ -33,7 +33,6 @@ const TERMINAL_TITLE_ACTION_REQUIRED_PREFIX_HIDDEN: &str = "[ . ] Action Require
|
||||
pub(super) enum TerminalTitleStatusKind {
|
||||
Working,
|
||||
WaitingForBackgroundTerminal,
|
||||
Undoing,
|
||||
#[default]
|
||||
Thinking,
|
||||
}
|
||||
@@ -711,7 +710,6 @@ impl ChatWidget {
|
||||
}
|
||||
TerminalTitleStatusKind::Working => "Working".to_string(),
|
||||
TerminalTitleStatusKind::WaitingForBackgroundTerminal => "Waiting".to_string(),
|
||||
TerminalTitleStatusKind::Undoing => "Undoing".to_string(),
|
||||
TerminalTitleStatusKind::Thinking => "Thinking".to_string(),
|
||||
}
|
||||
}
|
||||
@@ -748,9 +746,7 @@ impl ChatWidget {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.mcp_startup_status.is_some()
|
||||
|| self.bottom_pane.is_task_running()
|
||||
|| self.terminal_title_status_kind == TerminalTitleStatusKind::Undoing
|
||||
self.mcp_startup_status.is_some() || self.bottom_pane.is_task_running()
|
||||
}
|
||||
|
||||
pub(super) fn should_animate_terminal_title_spinner(&self) -> bool {
|
||||
|
||||
@@ -1,309 +0,0 @@
|
||||
//! Test-only event fixtures for legacy-style ChatWidget coverage.
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::approval_events::ApplyPatchApprovalRequestEvent;
|
||||
use crate::approval_events::ExecApprovalRequestEvent;
|
||||
use crate::session_state::SessionNetworkProxyRuntime;
|
||||
use crate::session_state::ThreadSessionState;
|
||||
use crate::token_usage::TokenCountEvent;
|
||||
use crate::tool_activity::ExecCommandBeginEvent;
|
||||
use crate::tool_activity::ExecCommandEndEvent;
|
||||
use crate::tool_activity::ExecCommandOutputDeltaEvent;
|
||||
use crate::tool_activity::HookCompletedEvent;
|
||||
use crate::tool_activity::HookStartedEvent;
|
||||
use crate::tool_activity::ImageGenerationBeginEvent;
|
||||
use crate::tool_activity::ImageGenerationEndEvent;
|
||||
use crate::tool_activity::McpToolCallBeginEvent;
|
||||
use crate::tool_activity::McpToolCallEndEvent;
|
||||
use crate::tool_activity::PatchApplyBeginEvent;
|
||||
use crate::tool_activity::PatchApplyEndEvent;
|
||||
use crate::tool_activity::TerminalInteractionEvent;
|
||||
use crate::tool_activity::ViewImageToolCallEvent;
|
||||
use crate::tool_activity::WebSearchBeginEvent;
|
||||
use crate::tool_activity::WebSearchEndEvent;
|
||||
use crate::turn_state::TurnAbortReason;
|
||||
use codex_app_server_protocol::AskForApproval;
|
||||
use codex_app_server_protocol::CodexErrorInfo;
|
||||
use codex_app_server_protocol::McpAuthStatus;
|
||||
use codex_app_server_protocol::ModelVerification;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::approvals::ElicitationRequestEvent;
|
||||
use codex_protocol::approvals::GuardianAssessmentEvent;
|
||||
use codex_protocol::config_types::ApprovalsReviewer;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
use codex_protocol::items::TurnItem;
|
||||
use codex_protocol::mcp::Resource as McpResource;
|
||||
use codex_protocol::mcp::ResourceTemplate as McpResourceTemplate;
|
||||
use codex_protocol::mcp::Tool as McpTool;
|
||||
use codex_protocol::memory_citation::MemoryCitation;
|
||||
use codex_protocol::models::MessagePhase;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
|
||||
use codex_protocol::plan_tool::UpdatePlanArgs;
|
||||
use codex_protocol::request_permissions::RequestPermissionsEvent;
|
||||
use codex_protocol::request_user_input::RequestUserInputEvent;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use serde::Deserialize;
|
||||
|
||||
use super::user_messages::UserMessageEvent;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct Event {
|
||||
pub(crate) id: String,
|
||||
pub(crate) msg: EventMsg,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct SessionConfiguredEvent {
|
||||
pub(crate) session_id: ThreadId,
|
||||
pub(crate) forked_from_id: Option<ThreadId>,
|
||||
pub(crate) thread_name: Option<String>,
|
||||
pub(crate) model: String,
|
||||
#[allow(dead_code)]
|
||||
pub(crate) model_provider_id: String,
|
||||
pub(crate) service_tier: Option<ServiceTier>,
|
||||
pub(crate) approval_policy: AskForApproval,
|
||||
pub(crate) approvals_reviewer: ApprovalsReviewer,
|
||||
pub(crate) permission_profile: PermissionProfile,
|
||||
pub(crate) cwd: AbsolutePathBuf,
|
||||
pub(crate) reasoning_effort: Option<ReasoningEffortConfig>,
|
||||
pub(crate) history_log_id: u64,
|
||||
pub(crate) history_entry_count: usize,
|
||||
pub(crate) initial_messages: Option<Vec<EventMsg>>,
|
||||
pub(crate) network_proxy: Option<SessionNetworkProxyRuntime>,
|
||||
pub(crate) rollout_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl SessionConfiguredEvent {
|
||||
pub(crate) fn into_session(self) -> (ThreadSessionState, Option<Vec<EventMsg>>) {
|
||||
(
|
||||
ThreadSessionState {
|
||||
thread_id: self.session_id,
|
||||
forked_from_id: self.forked_from_id,
|
||||
fork_parent_title: None,
|
||||
thread_name: self.thread_name,
|
||||
model: self.model,
|
||||
model_provider_id: self.model_provider_id,
|
||||
service_tier: self.service_tier,
|
||||
approval_policy: self.approval_policy,
|
||||
approvals_reviewer: self.approvals_reviewer,
|
||||
permission_profile: self.permission_profile,
|
||||
cwd: self.cwd,
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: self.reasoning_effort,
|
||||
history_log_id: self.history_log_id,
|
||||
history_entry_count: u64::try_from(self.history_entry_count).unwrap_or(u64::MAX),
|
||||
network_proxy: self.network_proxy,
|
||||
rollout_path: self.rollout_path,
|
||||
},
|
||||
self.initial_messages,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub(crate) enum EventMsg {
|
||||
Error(ErrorEvent),
|
||||
Warning(WarningEvent),
|
||||
GuardianWarning(WarningEvent),
|
||||
ModelVerification(ModelVerificationEvent),
|
||||
ThreadRolledBack(ThreadRolledBackEvent),
|
||||
TurnStarted(TurnStartedEvent),
|
||||
TurnComplete(TurnCompleteEvent),
|
||||
TokenCount(TokenCountEvent),
|
||||
AgentMessage(AgentMessageEvent),
|
||||
UserMessage(UserMessageEvent),
|
||||
AgentMessageDelta(AgentMessageDeltaEvent),
|
||||
AgentReasoning(AgentReasoningEvent),
|
||||
AgentReasoningDelta(AgentReasoningDeltaEvent),
|
||||
AgentReasoningRawContent(AgentReasoningRawContentEvent),
|
||||
AgentReasoningRawContentDelta(AgentReasoningRawContentDeltaEvent),
|
||||
SessionConfigured(SessionConfiguredEvent),
|
||||
ThreadNameUpdated(ThreadNameUpdatedEvent),
|
||||
McpToolCallBegin(McpToolCallBeginEvent),
|
||||
McpToolCallEnd(McpToolCallEndEvent),
|
||||
WebSearchBegin(WebSearchBeginEvent),
|
||||
WebSearchEnd(WebSearchEndEvent),
|
||||
ImageGenerationBegin(ImageGenerationBeginEvent),
|
||||
ImageGenerationEnd(ImageGenerationEndEvent),
|
||||
ExecCommandBegin(ExecCommandBeginEvent),
|
||||
ExecCommandOutputDelta(ExecCommandOutputDeltaEvent),
|
||||
TerminalInteraction(TerminalInteractionEvent),
|
||||
ExecCommandEnd(ExecCommandEndEvent),
|
||||
ViewImageToolCall(ViewImageToolCallEvent),
|
||||
ExecApprovalRequest(ExecApprovalRequestEvent),
|
||||
RequestPermissions(RequestPermissionsEvent),
|
||||
RequestUserInput(RequestUserInputEvent),
|
||||
ElicitationRequest(ElicitationRequestEvent),
|
||||
ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent),
|
||||
GuardianAssessment(GuardianAssessmentEvent),
|
||||
DeprecationNotice(DeprecationNoticeEvent),
|
||||
BackgroundEvent(BackgroundEventEvent),
|
||||
UndoStarted(UndoStartedEvent),
|
||||
UndoCompleted(UndoCompletedEvent),
|
||||
StreamError(StreamErrorEvent),
|
||||
PatchApplyBegin(PatchApplyBeginEvent),
|
||||
PatchApplyEnd(PatchApplyEndEvent),
|
||||
TurnDiff(TurnDiffEvent),
|
||||
McpListToolsResponse(McpListToolsResponseEvent),
|
||||
ListSkillsResponse(codex_app_server_protocol::SkillsListResponse),
|
||||
SkillsUpdateAvailable,
|
||||
PlanUpdate(UpdatePlanArgs),
|
||||
TurnAborted(TurnAbortedEvent),
|
||||
ShutdownComplete,
|
||||
EnteredReviewMode(String),
|
||||
ExitedReviewMode(ExitedReviewModeEvent),
|
||||
ItemCompleted(ItemCompletedEvent),
|
||||
HookStarted(HookStartedEvent),
|
||||
HookCompleted(HookCompletedEvent),
|
||||
PlanDelta(PlanDeltaEvent),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ErrorEvent {
|
||||
pub(crate) message: String,
|
||||
pub(crate) codex_error_info: Option<CodexErrorInfo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct WarningEvent {
|
||||
pub(crate) message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct ModelVerificationEvent {
|
||||
pub(crate) verifications: Vec<ModelVerification>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub(crate) struct TurnCompleteEvent {
|
||||
pub(crate) turn_id: String,
|
||||
pub(crate) last_agent_message: Option<String>,
|
||||
pub(crate) completed_at: Option<i64>,
|
||||
pub(crate) duration_ms: Option<i64>,
|
||||
pub(crate) time_to_first_token_ms: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct TurnStartedEvent {
|
||||
pub(crate) turn_id: String,
|
||||
pub(crate) started_at: Option<i64>,
|
||||
pub(crate) model_context_window: Option<i64>,
|
||||
pub(crate) collaboration_mode_kind: codex_protocol::config_types::ModeKind,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct AgentMessageEvent {
|
||||
pub(crate) message: String,
|
||||
pub(crate) phase: Option<MessagePhase>,
|
||||
pub(crate) memory_citation: Option<MemoryCitation>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct AgentMessageDeltaEvent {
|
||||
pub(crate) delta: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct AgentReasoningEvent {
|
||||
pub(crate) text: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct AgentReasoningRawContentEvent {
|
||||
pub(crate) text: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct AgentReasoningRawContentDeltaEvent {
|
||||
pub(crate) delta: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct AgentReasoningDeltaEvent {
|
||||
pub(crate) delta: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct BackgroundEventEvent {
|
||||
pub(crate) message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct DeprecationNoticeEvent {
|
||||
pub(crate) summary: String,
|
||||
pub(crate) details: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct UndoStartedEvent {
|
||||
pub(crate) message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct UndoCompletedEvent {
|
||||
pub(crate) success: bool,
|
||||
pub(crate) message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ThreadRolledBackEvent {
|
||||
pub(crate) num_turns: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct StreamErrorEvent {
|
||||
pub(crate) message: String,
|
||||
pub(crate) codex_error_info: Option<CodexErrorInfo>,
|
||||
pub(crate) additional_details: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct TurnDiffEvent {
|
||||
pub(crate) unified_diff: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct McpListToolsResponseEvent {
|
||||
pub(crate) tools: HashMap<String, McpTool>,
|
||||
pub(crate) resources: HashMap<String, Vec<McpResource>>,
|
||||
pub(crate) resource_templates: HashMap<String, Vec<McpResourceTemplate>>,
|
||||
pub(crate) auth_statuses: HashMap<String, McpAuthStatus>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ThreadNameUpdatedEvent {
|
||||
pub(crate) thread_id: ThreadId,
|
||||
pub(crate) thread_name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct TurnAbortedEvent {
|
||||
pub(crate) turn_id: Option<String>,
|
||||
pub(crate) reason: TurnAbortReason,
|
||||
pub(crate) completed_at: Option<i64>,
|
||||
pub(crate) duration_ms: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ItemCompletedEvent {
|
||||
pub(crate) thread_id: ThreadId,
|
||||
pub(crate) turn_id: String,
|
||||
pub(crate) item: TurnItem,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct PlanDeltaEvent {
|
||||
pub(crate) thread_id: String,
|
||||
pub(crate) turn_id: String,
|
||||
pub(crate) item_id: String,
|
||||
pub(crate) delta: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ExitedReviewModeEvent;
|
||||
@@ -17,25 +17,6 @@ pub(super) use crate::bottom_pane::LocalImageAttachment;
|
||||
pub(super) use crate::bottom_pane::MentionBinding;
|
||||
pub(super) use crate::bottom_pane::QueuedInputAction;
|
||||
pub(super) use crate::chatwidget::realtime::RealtimeConversationPhase;
|
||||
pub(super) use crate::chatwidget::test_events::AgentMessageDeltaEvent;
|
||||
pub(super) use crate::chatwidget::test_events::AgentMessageEvent;
|
||||
pub(super) use crate::chatwidget::test_events::AgentReasoningDeltaEvent;
|
||||
pub(super) use crate::chatwidget::test_events::AgentReasoningEvent;
|
||||
pub(super) use crate::chatwidget::test_events::BackgroundEventEvent;
|
||||
pub(super) use crate::chatwidget::test_events::ErrorEvent;
|
||||
pub(super) use crate::chatwidget::test_events::Event;
|
||||
pub(super) use crate::chatwidget::test_events::EventMsg;
|
||||
pub(super) use crate::chatwidget::test_events::ExitedReviewModeEvent;
|
||||
pub(super) use crate::chatwidget::test_events::ItemCompletedEvent;
|
||||
pub(super) use crate::chatwidget::test_events::ModelVerificationEvent;
|
||||
pub(super) use crate::chatwidget::test_events::SessionConfiguredEvent;
|
||||
pub(super) use crate::chatwidget::test_events::StreamErrorEvent;
|
||||
pub(super) use crate::chatwidget::test_events::ThreadRolledBackEvent;
|
||||
pub(super) use crate::chatwidget::test_events::TurnCompleteEvent;
|
||||
pub(super) use crate::chatwidget::test_events::TurnStartedEvent;
|
||||
pub(super) use crate::chatwidget::test_events::UndoCompletedEvent;
|
||||
pub(super) use crate::chatwidget::test_events::UndoStartedEvent;
|
||||
pub(super) use crate::chatwidget::test_events::WarningEvent;
|
||||
pub(super) use crate::diff_model::FileChange;
|
||||
pub(super) use crate::history_cell::UserHistoryCell;
|
||||
pub(super) use crate::legacy_core::config::Config;
|
||||
@@ -47,7 +28,6 @@ pub(super) use crate::test_backend::VT100Backend;
|
||||
pub(super) use crate::test_support::PathBufExt;
|
||||
pub(super) use crate::test_support::test_path_buf;
|
||||
pub(super) use crate::test_support::test_path_display;
|
||||
pub(super) use crate::token_usage::TokenCountEvent;
|
||||
pub(super) use crate::token_usage::TokenUsage;
|
||||
pub(super) use crate::token_usage::TokenUsageInfo;
|
||||
pub(super) use crate::tool_activity::ExecCommandBeginEvent;
|
||||
@@ -75,7 +55,6 @@ pub(super) use codex_app_server_protocol::CommandAction as AppServerCommandActio
|
||||
pub(super) use codex_app_server_protocol::CommandExecutionRequestApprovalParams as AppServerCommandExecutionRequestApprovalParams;
|
||||
pub(super) use codex_app_server_protocol::CommandExecutionSource as ExecCommandSource;
|
||||
pub(super) use codex_app_server_protocol::CommandExecutionSource as AppServerCommandExecutionSource;
|
||||
pub(super) use codex_app_server_protocol::CommandExecutionStatus as CoreExecCommandStatus;
|
||||
pub(super) use codex_app_server_protocol::CommandExecutionStatus as AppServerCommandExecutionStatus;
|
||||
pub(super) use codex_app_server_protocol::ConfigWarningNotification;
|
||||
pub(super) use codex_app_server_protocol::CreditsSnapshot;
|
||||
@@ -107,11 +86,9 @@ pub(super) use codex_app_server_protocol::MarketplaceInterface;
|
||||
pub(super) use codex_app_server_protocol::McpServerStartupState;
|
||||
pub(super) use codex_app_server_protocol::McpServerStatusDetail;
|
||||
pub(super) use codex_app_server_protocol::McpServerStatusUpdatedNotification;
|
||||
pub(super) use codex_app_server_protocol::ModelVerification as CoreModelVerification;
|
||||
pub(super) use codex_app_server_protocol::ModelVerification as AppServerModelVerification;
|
||||
pub(super) use codex_app_server_protocol::ModelVerificationNotification;
|
||||
pub(super) use codex_app_server_protocol::NonSteerableTurnKind;
|
||||
pub(super) use codex_app_server_protocol::PatchApplyStatus as CorePatchApplyStatus;
|
||||
pub(super) use codex_app_server_protocol::PatchApplyStatus as AppServerPatchApplyStatus;
|
||||
pub(super) use codex_app_server_protocol::PatchChangeKind;
|
||||
pub(super) use codex_app_server_protocol::PermissionsRequestApprovalParams as AppServerPermissionsRequestApprovalParams;
|
||||
@@ -175,11 +152,6 @@ pub(super) use codex_protocol::config_types::ModeKind;
|
||||
pub(super) use codex_protocol::config_types::Personality;
|
||||
pub(super) use codex_protocol::config_types::ServiceTier;
|
||||
pub(super) use codex_protocol::config_types::Settings;
|
||||
pub(super) use codex_protocol::items::AgentMessageContent;
|
||||
pub(super) use codex_protocol::items::AgentMessageItem;
|
||||
pub(super) use codex_protocol::items::PlanItem;
|
||||
pub(super) use codex_protocol::items::TurnItem;
|
||||
pub(super) use codex_protocol::items::UserMessageItem;
|
||||
pub(super) use codex_protocol::models::FileSystemPermissions;
|
||||
pub(super) use codex_protocol::models::MessagePhase;
|
||||
pub(super) use codex_protocol::models::NetworkPermissions;
|
||||
@@ -257,7 +229,6 @@ macro_rules! assert_chatwidget_snapshot {
|
||||
|
||||
mod app_server;
|
||||
mod approval_requests;
|
||||
mod background_events;
|
||||
mod composer_submission;
|
||||
mod exec_flow;
|
||||
mod goal_menu;
|
||||
|
||||
@@ -25,10 +25,7 @@ async fn exec_approval_emits_proposed_command_and_decision_history() {
|
||||
available_decisions: None,
|
||||
parsed_cmd: vec![],
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
id: "sub-short".into(),
|
||||
msg: EventMsg::ExecApprovalRequest(ev),
|
||||
});
|
||||
handle_exec_approval_request(&mut chat, "sub-short", ev);
|
||||
|
||||
let proposed_cells = drain_insert_history(&mut rx);
|
||||
assert!(
|
||||
@@ -192,9 +189,10 @@ fn app_server_request_permissions_preserves_file_system_permissions() {
|
||||
async fn exec_approval_uses_approval_id_when_present() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "sub-short".into(),
|
||||
msg: EventMsg::ExecApprovalRequest(ExecApprovalRequestEvent {
|
||||
handle_exec_approval_request(
|
||||
&mut chat,
|
||||
"sub-short",
|
||||
ExecApprovalRequestEvent {
|
||||
call_id: "call-parent".into(),
|
||||
approval_id: Some("approval-subcommand".into()),
|
||||
turn_id: "turn-short".into(),
|
||||
@@ -209,8 +207,8 @@ async fn exec_approval_uses_approval_id_when_present() {
|
||||
additional_permissions: None,
|
||||
available_decisions: None,
|
||||
parsed_cmd: vec![],
|
||||
}),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE));
|
||||
|
||||
@@ -253,10 +251,7 @@ async fn exec_approval_decision_truncates_multiline_and_long_commands() {
|
||||
available_decisions: None,
|
||||
parsed_cmd: vec![],
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
id: "sub-multi".into(),
|
||||
msg: EventMsg::ExecApprovalRequest(ev_multi),
|
||||
});
|
||||
handle_exec_approval_request(&mut chat, "sub-multi", ev_multi);
|
||||
let proposed_multi = drain_insert_history(&mut rx);
|
||||
assert!(
|
||||
proposed_multi.is_empty(),
|
||||
@@ -306,10 +301,7 @@ async fn exec_approval_decision_truncates_multiline_and_long_commands() {
|
||||
available_decisions: None,
|
||||
parsed_cmd: vec![],
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
id: "sub-long".into(),
|
||||
msg: EventMsg::ExecApprovalRequest(ev_long),
|
||||
});
|
||||
handle_exec_approval_request(&mut chat, "sub-long", ev_long);
|
||||
let proposed_long = drain_insert_history(&mut rx);
|
||||
assert!(
|
||||
proposed_long.is_empty(),
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[tokio::test]
|
||||
async fn background_event_updates_status_header() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "bg-1".into(),
|
||||
msg: EventMsg::BackgroundEvent(BackgroundEventEvent {
|
||||
message: "Waiting for `vim`".to_string(),
|
||||
}),
|
||||
});
|
||||
|
||||
assert!(chat.bottom_pane.status_indicator_visible());
|
||||
assert_eq!(chat.current_status.header, "Waiting for `vim`");
|
||||
assert!(drain_insert_history(&mut rx).is_empty());
|
||||
}
|
||||
@@ -14,9 +14,10 @@ async fn submission_preserves_text_elements_and_local_images() {
|
||||
|
||||
let conversation_id = ThreadId::new();
|
||||
let rollout_file = NamedTempFile::new().unwrap();
|
||||
let configured = crate::chatwidget::test_events::SessionConfiguredEvent {
|
||||
session_id: conversation_id,
|
||||
let configured = crate::session_state::ThreadSessionState {
|
||||
thread_id: conversation_id,
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
thread_name: None,
|
||||
model: "test-model".to_string(),
|
||||
model_provider_id: "test-provider".to_string(),
|
||||
@@ -25,17 +26,14 @@ async fn submission_preserves_text_elements_and_local_images() {
|
||||
approvals_reviewer: ApprovalsReviewer::User,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
initial_messages: None,
|
||||
network_proxy: None,
|
||||
rollout_path: Some(rollout_file.path().to_path_buf()),
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
id: "initial".into(),
|
||||
msg: EventMsg::SessionConfigured(configured),
|
||||
});
|
||||
chat.handle_thread_session(configured);
|
||||
drain_insert_history(&mut rx);
|
||||
|
||||
let placeholder = "[Image #1]";
|
||||
@@ -119,9 +117,10 @@ async fn submission_includes_configured_permission_profile() {
|
||||
},
|
||||
}
|
||||
.into();
|
||||
let configured = crate::chatwidget::test_events::SessionConfiguredEvent {
|
||||
session_id: conversation_id,
|
||||
let configured = crate::session_state::ThreadSessionState {
|
||||
thread_id: conversation_id,
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
thread_name: None,
|
||||
model: "test-model".to_string(),
|
||||
model_provider_id: "test-provider".to_string(),
|
||||
@@ -130,17 +129,14 @@ async fn submission_includes_configured_permission_profile() {
|
||||
approvals_reviewer: ApprovalsReviewer::User,
|
||||
permission_profile: expected_permission_profile.clone(),
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
initial_messages: None,
|
||||
network_proxy: None,
|
||||
rollout_path: Some(rollout_file.path().to_path_buf()),
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
id: "initial".into(),
|
||||
msg: EventMsg::SessionConfigured(configured),
|
||||
});
|
||||
chat.handle_thread_session(configured);
|
||||
drain_insert_history(&mut rx);
|
||||
|
||||
chat.bottom_pane.set_composer_text(
|
||||
@@ -170,9 +166,10 @@ async fn submission_keeps_profile_when_legacy_projection_is_external() {
|
||||
file_system: PermissionProfileFileSystemPermissions::Unrestricted,
|
||||
}
|
||||
.into();
|
||||
let configured = crate::chatwidget::test_events::SessionConfiguredEvent {
|
||||
session_id: conversation_id,
|
||||
let configured = crate::session_state::ThreadSessionState {
|
||||
thread_id: conversation_id,
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
thread_name: None,
|
||||
model: "test-model".to_string(),
|
||||
model_provider_id: "test-provider".to_string(),
|
||||
@@ -181,17 +178,14 @@ async fn submission_keeps_profile_when_legacy_projection_is_external() {
|
||||
approvals_reviewer: ApprovalsReviewer::User,
|
||||
permission_profile: expected_permission_profile.clone(),
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
initial_messages: None,
|
||||
network_proxy: None,
|
||||
rollout_path: Some(rollout_file.path().to_path_buf()),
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
id: "initial".into(),
|
||||
msg: EventMsg::SessionConfigured(configured),
|
||||
});
|
||||
chat.handle_thread_session(configured);
|
||||
drain_insert_history(&mut rx);
|
||||
|
||||
chat.bottom_pane
|
||||
@@ -213,9 +207,10 @@ async fn submission_with_remote_and_local_images_keeps_local_placeholder_numberi
|
||||
|
||||
let conversation_id = ThreadId::new();
|
||||
let rollout_file = NamedTempFile::new().unwrap();
|
||||
let configured = crate::chatwidget::test_events::SessionConfiguredEvent {
|
||||
session_id: conversation_id,
|
||||
let configured = crate::session_state::ThreadSessionState {
|
||||
thread_id: conversation_id,
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
thread_name: None,
|
||||
model: "test-model".to_string(),
|
||||
model_provider_id: "test-provider".to_string(),
|
||||
@@ -224,17 +219,14 @@ async fn submission_with_remote_and_local_images_keeps_local_placeholder_numberi
|
||||
approvals_reviewer: ApprovalsReviewer::User,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
initial_messages: None,
|
||||
network_proxy: None,
|
||||
rollout_path: Some(rollout_file.path().to_path_buf()),
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
id: "initial".into(),
|
||||
msg: EventMsg::SessionConfigured(configured),
|
||||
});
|
||||
chat.handle_thread_session(configured);
|
||||
drain_insert_history(&mut rx);
|
||||
|
||||
let remote_url = "https://example.com/remote.png".to_string();
|
||||
@@ -308,9 +300,10 @@ async fn enter_with_only_remote_images_submits_user_turn() {
|
||||
|
||||
let conversation_id = ThreadId::new();
|
||||
let rollout_file = NamedTempFile::new().unwrap();
|
||||
let configured = crate::chatwidget::test_events::SessionConfiguredEvent {
|
||||
session_id: conversation_id,
|
||||
let configured = crate::session_state::ThreadSessionState {
|
||||
thread_id: conversation_id,
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
thread_name: None,
|
||||
model: "test-model".to_string(),
|
||||
model_provider_id: "test-provider".to_string(),
|
||||
@@ -319,17 +312,14 @@ async fn enter_with_only_remote_images_submits_user_turn() {
|
||||
approvals_reviewer: ApprovalsReviewer::User,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
initial_messages: None,
|
||||
network_proxy: None,
|
||||
rollout_path: Some(rollout_file.path().to_path_buf()),
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
id: "initial".into(),
|
||||
msg: EventMsg::SessionConfigured(configured),
|
||||
});
|
||||
chat.handle_thread_session(configured);
|
||||
drain_insert_history(&mut rx);
|
||||
|
||||
let remote_url = "https://example.com/remote-only.png".to_string();
|
||||
@@ -373,9 +363,10 @@ async fn shift_enter_with_only_remote_images_does_not_submit_user_turn() {
|
||||
|
||||
let conversation_id = ThreadId::new();
|
||||
let rollout_file = NamedTempFile::new().unwrap();
|
||||
let configured = crate::chatwidget::test_events::SessionConfiguredEvent {
|
||||
session_id: conversation_id,
|
||||
let configured = crate::session_state::ThreadSessionState {
|
||||
thread_id: conversation_id,
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
thread_name: None,
|
||||
model: "test-model".to_string(),
|
||||
model_provider_id: "test-provider".to_string(),
|
||||
@@ -384,17 +375,14 @@ async fn shift_enter_with_only_remote_images_does_not_submit_user_turn() {
|
||||
approvals_reviewer: ApprovalsReviewer::User,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
initial_messages: None,
|
||||
network_proxy: None,
|
||||
rollout_path: Some(rollout_file.path().to_path_buf()),
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
id: "initial".into(),
|
||||
msg: EventMsg::SessionConfigured(configured),
|
||||
});
|
||||
chat.handle_thread_session(configured);
|
||||
drain_insert_history(&mut rx);
|
||||
|
||||
let remote_url = "https://example.com/remote-only.png".to_string();
|
||||
@@ -413,9 +401,10 @@ async fn enter_with_only_remote_images_does_not_submit_when_modal_is_active() {
|
||||
|
||||
let conversation_id = ThreadId::new();
|
||||
let rollout_file = NamedTempFile::new().unwrap();
|
||||
let configured = crate::chatwidget::test_events::SessionConfiguredEvent {
|
||||
session_id: conversation_id,
|
||||
let configured = crate::session_state::ThreadSessionState {
|
||||
thread_id: conversation_id,
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
thread_name: None,
|
||||
model: "test-model".to_string(),
|
||||
model_provider_id: "test-provider".to_string(),
|
||||
@@ -424,17 +413,14 @@ async fn enter_with_only_remote_images_does_not_submit_when_modal_is_active() {
|
||||
approvals_reviewer: ApprovalsReviewer::User,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
initial_messages: None,
|
||||
network_proxy: None,
|
||||
rollout_path: Some(rollout_file.path().to_path_buf()),
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
id: "initial".into(),
|
||||
msg: EventMsg::SessionConfigured(configured),
|
||||
});
|
||||
chat.handle_thread_session(configured);
|
||||
drain_insert_history(&mut rx);
|
||||
|
||||
let remote_url = "https://example.com/remote-only.png".to_string();
|
||||
@@ -453,9 +439,10 @@ async fn enter_with_only_remote_images_does_not_submit_when_input_disabled() {
|
||||
|
||||
let conversation_id = ThreadId::new();
|
||||
let rollout_file = NamedTempFile::new().unwrap();
|
||||
let configured = crate::chatwidget::test_events::SessionConfiguredEvent {
|
||||
session_id: conversation_id,
|
||||
let configured = crate::session_state::ThreadSessionState {
|
||||
thread_id: conversation_id,
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
thread_name: None,
|
||||
model: "test-model".to_string(),
|
||||
model_provider_id: "test-provider".to_string(),
|
||||
@@ -464,17 +451,14 @@ async fn enter_with_only_remote_images_does_not_submit_when_input_disabled() {
|
||||
approvals_reviewer: ApprovalsReviewer::User,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
initial_messages: None,
|
||||
network_proxy: None,
|
||||
rollout_path: Some(rollout_file.path().to_path_buf()),
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
id: "initial".into(),
|
||||
msg: EventMsg::SessionConfigured(configured),
|
||||
});
|
||||
chat.handle_thread_session(configured);
|
||||
drain_insert_history(&mut rx);
|
||||
|
||||
let remote_url = "https://example.com/remote-only.png".to_string();
|
||||
@@ -496,9 +480,10 @@ async fn submission_prefers_selected_duplicate_skill_path() {
|
||||
|
||||
let conversation_id = ThreadId::new();
|
||||
let rollout_file = NamedTempFile::new().unwrap();
|
||||
let configured = crate::chatwidget::test_events::SessionConfiguredEvent {
|
||||
session_id: conversation_id,
|
||||
let configured = crate::session_state::ThreadSessionState {
|
||||
thread_id: conversation_id,
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
thread_name: None,
|
||||
model: "test-model".to_string(),
|
||||
model_provider_id: "test-provider".to_string(),
|
||||
@@ -507,17 +492,14 @@ async fn submission_prefers_selected_duplicate_skill_path() {
|
||||
approvals_reviewer: ApprovalsReviewer::User,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
initial_messages: None,
|
||||
network_proxy: None,
|
||||
rollout_path: Some(rollout_file.path().to_path_buf()),
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
id: "initial".into(),
|
||||
msg: EventMsg::SessionConfigured(configured),
|
||||
});
|
||||
chat.handle_thread_session(configured);
|
||||
drain_insert_history(&mut rx);
|
||||
|
||||
let repo_skill_path = test_path_buf("/tmp/repo/figma/SKILL.md").abs();
|
||||
@@ -738,15 +720,7 @@ async fn interrupted_turn_restore_keeps_active_mode_for_resubmission() {
|
||||
);
|
||||
chat.refresh_pending_input_preview();
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "interrupt".into(),
|
||||
msg: EventMsg::TurnAborted(crate::chatwidget::test_events::TurnAbortedEvent {
|
||||
turn_id: Some("turn-1".to_string()),
|
||||
reason: TurnAbortReason::Interrupted,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
}),
|
||||
});
|
||||
handle_turn_interrupted(&mut chat, "turn-1");
|
||||
|
||||
assert_eq!(chat.bottom_pane.composer_text(), "Implement the plan.");
|
||||
assert!(chat.queued_user_messages.is_empty());
|
||||
@@ -1184,16 +1158,8 @@ async fn interrupt_restores_queued_messages_into_composer() {
|
||||
.push_back(UserMessage::from("second queued".to_string()).into());
|
||||
chat.refresh_pending_input_preview();
|
||||
|
||||
// Deliver a TurnAborted event with Interrupted reason (as if Esc was pressed).
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-1".into(),
|
||||
msg: EventMsg::TurnAborted(crate::chatwidget::test_events::TurnAbortedEvent {
|
||||
turn_id: Some("turn-1".to_string()),
|
||||
reason: TurnAbortReason::Interrupted,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
}),
|
||||
});
|
||||
// Deliver an interrupted turn notification as if Esc was pressed.
|
||||
handle_turn_interrupted(&mut chat, "turn-1");
|
||||
|
||||
// Composer should now contain the queued messages joined by newlines, in order.
|
||||
assert_eq!(
|
||||
@@ -1226,15 +1192,7 @@ async fn interrupt_prepends_queued_messages_before_existing_composer_text() {
|
||||
.push_back(UserMessage::from("second queued".to_string()).into());
|
||||
chat.refresh_pending_input_preview();
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-1".into(),
|
||||
msg: EventMsg::TurnAborted(crate::chatwidget::test_events::TurnAbortedEvent {
|
||||
turn_id: Some("turn-1".to_string()),
|
||||
reason: TurnAbortReason::Interrupted,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
}),
|
||||
});
|
||||
handle_turn_interrupted(&mut chat, "turn-1");
|
||||
|
||||
assert_eq!(
|
||||
chat.bottom_pane.composer_text(),
|
||||
|
||||
@@ -22,10 +22,7 @@ async fn exec_approval_emits_proposed_command_and_decision_history() {
|
||||
available_decisions: None,
|
||||
parsed_cmd: vec![],
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
id: "sub-short".into(),
|
||||
msg: EventMsg::ExecApprovalRequest(ev),
|
||||
});
|
||||
handle_exec_approval_request(&mut chat, "sub-short", ev);
|
||||
|
||||
let proposed_cells = drain_insert_history(&mut rx);
|
||||
assert!(
|
||||
@@ -89,9 +86,10 @@ fn app_server_exec_approval_request_splits_shell_wrapped_command() {
|
||||
async fn exec_approval_uses_approval_id_when_present() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "sub-short".into(),
|
||||
msg: EventMsg::ExecApprovalRequest(ExecApprovalRequestEvent {
|
||||
handle_exec_approval_request(
|
||||
&mut chat,
|
||||
"sub-short",
|
||||
ExecApprovalRequestEvent {
|
||||
call_id: "call-parent".into(),
|
||||
approval_id: Some("approval-subcommand".into()),
|
||||
turn_id: "turn-short".into(),
|
||||
@@ -106,8 +104,8 @@ async fn exec_approval_uses_approval_id_when_present() {
|
||||
additional_permissions: None,
|
||||
available_decisions: None,
|
||||
parsed_cmd: vec![],
|
||||
}),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE));
|
||||
|
||||
@@ -151,10 +149,7 @@ async fn exec_approval_decision_truncates_multiline_and_long_commands() {
|
||||
available_decisions: None,
|
||||
parsed_cmd: vec![],
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
id: "sub-multi".into(),
|
||||
msg: EventMsg::ExecApprovalRequest(ev_multi),
|
||||
});
|
||||
handle_exec_approval_request(&mut chat, "sub-multi", ev_multi);
|
||||
let proposed_multi = drain_insert_history(&mut rx);
|
||||
assert!(
|
||||
proposed_multi.is_empty(),
|
||||
@@ -206,10 +201,7 @@ async fn exec_approval_decision_truncates_multiline_and_long_commands() {
|
||||
available_decisions: None,
|
||||
parsed_cmd: vec![],
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
id: "sub-long".into(),
|
||||
msg: EventMsg::ExecApprovalRequest(ev_long),
|
||||
});
|
||||
handle_exec_approval_request(&mut chat, "sub-long", ev_long);
|
||||
let proposed_long = drain_insert_history(&mut rx);
|
||||
assert!(
|
||||
proposed_long.is_empty(),
|
||||
@@ -361,9 +353,9 @@ async fn exec_end_without_begin_uses_event_command() {
|
||||
];
|
||||
let parsed_cmd = codex_shell_command::parse_command::parse_command(&command);
|
||||
let cwd = AbsolutePathBuf::current_dir().expect("current dir");
|
||||
chat.handle_codex_event(Event {
|
||||
id: "call-orphan".to_string(),
|
||||
msg: EventMsg::ExecCommandEnd(ExecCommandEndEvent {
|
||||
handle_exec_end(
|
||||
&mut chat,
|
||||
ExecCommandEndEvent {
|
||||
call_id: "call-orphan".to_string(),
|
||||
process_id: None,
|
||||
turn_id: "turn-1".to_string(),
|
||||
@@ -378,9 +370,9 @@ async fn exec_end_without_begin_uses_event_command() {
|
||||
exit_code: 0,
|
||||
duration: std::time::Duration::from_millis(5),
|
||||
formatted_output: "done".to_string(),
|
||||
status: CoreExecCommandStatus::Completed,
|
||||
}),
|
||||
});
|
||||
status: AppServerCommandExecutionStatus::Completed,
|
||||
},
|
||||
);
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert_eq!(cells.len(), 1, "expected finalized exec cell to flush");
|
||||
@@ -621,14 +613,10 @@ async fn unified_exec_interaction_after_task_complete_is_suppressed() {
|
||||
/*last_agent_message*/ None, /*duration_ms*/ None, /*from_replay*/ false,
|
||||
);
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "call-1".to_string(),
|
||||
msg: EventMsg::TerminalInteraction(TerminalInteractionEvent {
|
||||
call_id: "call-1".to_string(),
|
||||
process_id: "proc-1".to_string(),
|
||||
stdin: "ls\n".to_string(),
|
||||
}),
|
||||
});
|
||||
handle_terminal_interaction(
|
||||
&mut chat,
|
||||
terminal_interaction_event("call-1", "proc-1", "ls\n"),
|
||||
);
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert!(
|
||||
@@ -640,30 +628,13 @@ async fn unified_exec_interaction_after_task_complete_is_suppressed() {
|
||||
#[tokio::test]
|
||||
async fn unified_exec_wait_after_final_agent_message_snapshot() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-1".into(),
|
||||
msg: EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
}),
|
||||
});
|
||||
handle_turn_started(&mut chat, "turn-1");
|
||||
|
||||
begin_unified_exec_startup(&mut chat, "call-wait", "proc-1", "cargo test -p codex-core");
|
||||
terminal_interaction(&mut chat, "call-wait-stdin", "proc-1", "");
|
||||
|
||||
complete_assistant_message(&mut chat, "msg-1", "Final response.", /*phase*/ None);
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-1".into(),
|
||||
msg: EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
last_agent_message: Some("Final response.".into()),
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
time_to_first_token_ms: None,
|
||||
}),
|
||||
});
|
||||
handle_turn_completed(&mut chat, "turn-1", None);
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
let combined = cells
|
||||
@@ -676,15 +647,7 @@ async fn unified_exec_wait_after_final_agent_message_snapshot() {
|
||||
#[tokio::test]
|
||||
async fn unified_exec_wait_before_streamed_agent_message_snapshot() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-1".into(),
|
||||
msg: EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
}),
|
||||
});
|
||||
handle_turn_started(&mut chat, "turn-1");
|
||||
|
||||
begin_unified_exec_startup(
|
||||
&mut chat,
|
||||
@@ -694,22 +657,8 @@ async fn unified_exec_wait_before_streamed_agent_message_snapshot() {
|
||||
);
|
||||
terminal_interaction(&mut chat, "call-wait-stream-stdin", "proc-1", "");
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-1".into(),
|
||||
msg: EventMsg::AgentMessageDelta(AgentMessageDeltaEvent {
|
||||
delta: "Streaming response.".into(),
|
||||
}),
|
||||
});
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-1".into(),
|
||||
msg: EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
last_agent_message: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
time_to_first_token_ms: None,
|
||||
}),
|
||||
});
|
||||
handle_agent_message_delta(&mut chat, "Streaming response.");
|
||||
handle_turn_completed(&mut chat, "turn-wait-1", None);
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
let combined = cells
|
||||
@@ -722,15 +671,7 @@ async fn unified_exec_wait_before_streamed_agent_message_snapshot() {
|
||||
#[tokio::test]
|
||||
async fn final_worked_for_uses_cumulative_turn_duration_snapshot() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-1".into(),
|
||||
msg: EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
}),
|
||||
});
|
||||
handle_turn_started(&mut chat, "turn-1");
|
||||
|
||||
let exec = begin_exec_with_source(
|
||||
&mut chat,
|
||||
@@ -746,16 +687,7 @@ async fn final_worked_for_uses_cumulative_turn_duration_snapshot() {
|
||||
"Final response.",
|
||||
Some(MessagePhase::FinalAnswer),
|
||||
);
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-1".into(),
|
||||
msg: EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
last_agent_message: Some("Final response.".to_string()),
|
||||
completed_at: None,
|
||||
duration_ms: Some(125_000),
|
||||
time_to_first_token_ms: None,
|
||||
}),
|
||||
});
|
||||
handle_turn_completed(&mut chat, "turn-1", Some(125_000));
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
let combined = cells
|
||||
@@ -818,16 +750,7 @@ async fn unified_exec_waiting_multiple_empty_snapshots() {
|
||||
assert_eq!(status.header(), "Waiting for background terminal");
|
||||
assert_eq!(status.details(), Some("just fix"));
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-wait-1".into(),
|
||||
msg: EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
last_agent_message: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
time_to_first_token_ms: None,
|
||||
}),
|
||||
});
|
||||
handle_turn_completed(&mut chat, "turn-wait-3", None);
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
let combined = cells
|
||||
@@ -899,16 +822,7 @@ async fn unified_exec_non_empty_then_empty_snapshots() {
|
||||
.collect::<String>();
|
||||
assert_chatwidget_snapshot!("unified_exec_non_empty_then_empty_active", active_combined);
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-wait-3".into(),
|
||||
msg: EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
last_agent_message: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
time_to_first_token_ms: None,
|
||||
}),
|
||||
});
|
||||
handle_turn_completed(&mut chat, "turn-1", None);
|
||||
|
||||
let post_cells = drain_insert_history(&mut rx);
|
||||
let mut combined = pre_cells
|
||||
@@ -931,13 +845,13 @@ async fn view_image_tool_call_adds_history_cell() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
let image_path = chat.config.cwd.join("example.png");
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "sub-image".into(),
|
||||
msg: EventMsg::ViewImageToolCall(ViewImageToolCallEvent {
|
||||
handle_view_image_tool_call(
|
||||
&mut chat,
|
||||
ViewImageToolCallEvent {
|
||||
call_id: "call-image".into(),
|
||||
path: image_path,
|
||||
}),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert_eq!(cells.len(), 1, "expected a single history cell");
|
||||
@@ -949,16 +863,16 @@ async fn view_image_tool_call_adds_history_cell() {
|
||||
async fn image_generation_call_adds_history_cell() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "sub-image-generation".into(),
|
||||
msg: EventMsg::ImageGenerationEnd(ImageGenerationEndEvent {
|
||||
handle_image_generation_end(
|
||||
&mut chat,
|
||||
ImageGenerationEndEvent {
|
||||
call_id: "call-image-generation".into(),
|
||||
status: "completed".into(),
|
||||
revised_prompt: Some("A tiny blue square".into()),
|
||||
result: "Zm9v".into(),
|
||||
saved_path: Some(test_path_buf("/tmp/ig-1.png").abs()),
|
||||
}),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert_eq!(cells.len(), 1, "expected a single history cell");
|
||||
@@ -1052,9 +966,10 @@ async fn bang_shell_enter_while_task_running_submits_run_user_shell_command() {
|
||||
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
let conversation_id = ThreadId::new();
|
||||
let rollout_file = NamedTempFile::new().unwrap();
|
||||
let configured = crate::chatwidget::test_events::SessionConfiguredEvent {
|
||||
session_id: conversation_id,
|
||||
let configured = crate::session_state::ThreadSessionState {
|
||||
thread_id: conversation_id,
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
thread_name: None,
|
||||
model: "test-model".to_string(),
|
||||
model_provider_id: "test-provider".to_string(),
|
||||
@@ -1063,28 +978,17 @@ async fn bang_shell_enter_while_task_running_submits_run_user_shell_command() {
|
||||
approvals_reviewer: ApprovalsReviewer::User,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
initial_messages: None,
|
||||
network_proxy: None,
|
||||
rollout_path: Some(rollout_file.path().to_path_buf()),
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
id: "initial".into(),
|
||||
msg: EventMsg::SessionConfigured(configured),
|
||||
});
|
||||
chat.handle_thread_session(configured);
|
||||
drain_insert_history(&mut rx);
|
||||
while op_rx.try_recv().is_ok() {}
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-start".into(),
|
||||
msg: EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
}),
|
||||
});
|
||||
handle_turn_started(&mut chat, "turn-1");
|
||||
|
||||
chat.bottom_pane
|
||||
.set_composer_text("!echo hi".to_string(), Vec::new(), Vec::new());
|
||||
@@ -1105,15 +1009,7 @@ async fn bang_shell_enter_while_task_running_submits_run_user_shell_command() {
|
||||
async fn user_message_during_user_shell_command_is_queued_not_steered() {
|
||||
let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.thread_id = Some(ThreadId::new());
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-start".into(),
|
||||
msg: EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
}),
|
||||
});
|
||||
handle_turn_started(&mut chat, "turn-1");
|
||||
let begin = begin_exec_with_source(
|
||||
&mut chat,
|
||||
"user-shell-sleep",
|
||||
@@ -1130,16 +1026,13 @@ async fn user_message_during_user_shell_command_is_queued_not_steered() {
|
||||
assert_eq!(chat.queued_user_message_texts(), vec!["hi".to_string()]);
|
||||
|
||||
end_exec(&mut chat, begin, "", "", /*exit_code*/ 0);
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-complete".into(),
|
||||
msg: EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
last_agent_message: Some("done".to_string()),
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
time_to_first_token_ms: None,
|
||||
}),
|
||||
});
|
||||
complete_assistant_message(
|
||||
&mut chat,
|
||||
"msg-done",
|
||||
"done",
|
||||
Some(MessagePhase::FinalAnswer),
|
||||
);
|
||||
handle_turn_completed(&mut chat, "turn-1", None);
|
||||
|
||||
match next_submit_op(&mut op_rx) {
|
||||
Op::UserTurn { items, .. } => assert_eq!(
|
||||
@@ -1176,7 +1069,7 @@ async fn disabled_slash_command_while_task_running_snapshot() {
|
||||
//
|
||||
// Snapshot test: command approval modal
|
||||
//
|
||||
// Synthesizes a Codex ExecApprovalRequest event to trigger the approval modal
|
||||
// Synthesizes an exec approval request to trigger the approval modal
|
||||
// and snapshots the visual output using the ratatui TestBackend.
|
||||
#[tokio::test]
|
||||
async fn approval_modal_exec_snapshot() -> anyhow::Result<()> {
|
||||
@@ -1208,10 +1101,7 @@ async fn approval_modal_exec_snapshot() -> anyhow::Result<()> {
|
||||
available_decisions: None,
|
||||
parsed_cmd: vec![],
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
id: "sub-approve".into(),
|
||||
msg: EventMsg::ExecApprovalRequest(ev),
|
||||
});
|
||||
handle_exec_approval_request(&mut chat, "sub-approve", ev);
|
||||
// Render to a fixed-size test terminal and snapshot.
|
||||
// Call desired_height first and use that exact height for rendering.
|
||||
let width = 100;
|
||||
@@ -1269,10 +1159,7 @@ async fn approval_modal_exec_without_reason_snapshot() -> anyhow::Result<()> {
|
||||
available_decisions: None,
|
||||
parsed_cmd: vec![],
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
id: "sub-approve-noreason".into(),
|
||||
msg: EventMsg::ExecApprovalRequest(ev),
|
||||
});
|
||||
handle_exec_approval_request(&mut chat, "sub-approve-noreason", ev);
|
||||
|
||||
let width = 100;
|
||||
let height = chat.desired_height(width);
|
||||
@@ -1317,10 +1204,7 @@ async fn approval_modal_exec_multiline_prefix_hides_execpolicy_option_snapshot()
|
||||
available_decisions: None,
|
||||
parsed_cmd: vec![],
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
id: "sub-approve-multiline-trunc".into(),
|
||||
msg: EventMsg::ExecApprovalRequest(ev),
|
||||
});
|
||||
handle_exec_approval_request(&mut chat, "sub-approve-multiline-trunc", ev);
|
||||
|
||||
let width = 100;
|
||||
let height = chat.desired_height(width);
|
||||
@@ -1364,10 +1248,7 @@ async fn approval_modal_patch_snapshot() -> anyhow::Result<()> {
|
||||
reason: Some("The model wants to apply changes".into()),
|
||||
grant_root: Some(PathBuf::from("/tmp")),
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
id: "sub-approve-patch".into(),
|
||||
msg: EventMsg::ApplyPatchApprovalRequest(ev),
|
||||
});
|
||||
handle_apply_patch_approval_request(&mut chat, "sub-approve-patch", ev);
|
||||
|
||||
// Render at the widget's desired height and snapshot.
|
||||
let height = chat.desired_height(/*width*/ 80);
|
||||
@@ -1392,15 +1273,7 @@ async fn interrupt_preserves_unified_exec_processes() {
|
||||
begin_unified_exec_startup(&mut chat, "call-2", "process-2", "sleep 6");
|
||||
assert_eq!(chat.unified_exec_processes.len(), 2);
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-1".into(),
|
||||
msg: EventMsg::TurnAborted(crate::chatwidget::test_events::TurnAbortedEvent {
|
||||
turn_id: Some("turn-1".to_string()),
|
||||
reason: TurnAbortReason::Interrupted,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
}),
|
||||
});
|
||||
handle_turn_interrupted(&mut chat, "turn-1");
|
||||
|
||||
assert_eq!(chat.unified_exec_processes.len(), 2);
|
||||
|
||||
@@ -1427,28 +1300,12 @@ async fn interrupt_preserves_unified_exec_processes() {
|
||||
async fn interrupt_preserves_unified_exec_wait_streak_snapshot() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-1".into(),
|
||||
msg: EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
}),
|
||||
});
|
||||
handle_turn_started(&mut chat, "turn-1");
|
||||
|
||||
let begin = begin_unified_exec_startup(&mut chat, "call-1", "process-1", "just fix");
|
||||
terminal_interaction(&mut chat, "call-1a", "process-1", "");
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-1".into(),
|
||||
msg: EventMsg::TurnAborted(crate::chatwidget::test_events::TurnAbortedEvent {
|
||||
turn_id: Some("turn-1".to_string()),
|
||||
reason: TurnAbortReason::Interrupted,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
}),
|
||||
});
|
||||
handle_turn_interrupted(&mut chat, "turn-1");
|
||||
|
||||
end_exec(&mut chat, begin, "", "", /*exit_code*/ 0);
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
@@ -1469,16 +1326,7 @@ async fn turn_complete_keeps_unified_exec_processes() {
|
||||
begin_unified_exec_startup(&mut chat, "call-2", "process-2", "sleep 6");
|
||||
assert_eq!(chat.unified_exec_processes.len(), 2);
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-1".into(),
|
||||
msg: EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
last_agent_message: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
time_to_first_token_ms: None,
|
||||
}),
|
||||
});
|
||||
handle_turn_completed(&mut chat, "turn-1", None);
|
||||
|
||||
assert_eq!(chat.unified_exec_processes.len(), 2);
|
||||
|
||||
@@ -1520,10 +1368,7 @@ async fn apply_patch_events_emit_history_cells() {
|
||||
reason: None,
|
||||
grant_root: None,
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
id: "s1".into(),
|
||||
msg: EventMsg::ApplyPatchApprovalRequest(ev),
|
||||
});
|
||||
handle_apply_patch_approval_request(&mut chat, "s1", ev);
|
||||
assert!(
|
||||
drain_insert_history(&mut rx).is_empty(),
|
||||
"expected approval request to surface via modal without emitting history cells"
|
||||
@@ -1543,10 +1388,7 @@ async fn apply_patch_events_emit_history_cells() {
|
||||
auto_approved: true,
|
||||
changes: changes2,
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
id: "s1".into(),
|
||||
msg: EventMsg::PatchApplyBegin(begin),
|
||||
});
|
||||
handle_patch_apply_begin(&mut chat, begin);
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert!(!cells.is_empty(), "expected apply block cell to be sent");
|
||||
let blob = lines_to_single_string(cells.last().unwrap());
|
||||
@@ -1570,12 +1412,9 @@ async fn apply_patch_events_emit_history_cells() {
|
||||
stderr: String::new(),
|
||||
success: true,
|
||||
changes: end_changes,
|
||||
status: CorePatchApplyStatus::Completed,
|
||||
status: AppServerPatchApplyStatus::Completed,
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
id: "s1".into(),
|
||||
msg: EventMsg::PatchApplyEnd(end),
|
||||
});
|
||||
handle_patch_apply_end(&mut chat, end);
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert!(
|
||||
cells.is_empty(),
|
||||
@@ -1594,16 +1433,17 @@ async fn apply_patch_manual_approval_adjusts_header() {
|
||||
content: "hello\n".to_string(),
|
||||
},
|
||||
);
|
||||
chat.handle_codex_event(Event {
|
||||
id: "s1".into(),
|
||||
msg: EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent {
|
||||
handle_apply_patch_approval_request(
|
||||
&mut chat,
|
||||
"s1",
|
||||
ApplyPatchApprovalRequestEvent {
|
||||
call_id: "c1".into(),
|
||||
turn_id: "turn-c1".into(),
|
||||
changes: proposed_changes,
|
||||
reason: None,
|
||||
grant_root: None,
|
||||
}),
|
||||
});
|
||||
},
|
||||
);
|
||||
drain_insert_history(&mut rx);
|
||||
|
||||
let mut apply_changes = HashMap::new();
|
||||
@@ -1613,15 +1453,15 @@ async fn apply_patch_manual_approval_adjusts_header() {
|
||||
content: "hello\n".to_string(),
|
||||
},
|
||||
);
|
||||
chat.handle_codex_event(Event {
|
||||
id: "s1".into(),
|
||||
msg: EventMsg::PatchApplyBegin(PatchApplyBeginEvent {
|
||||
handle_patch_apply_begin(
|
||||
&mut chat,
|
||||
PatchApplyBeginEvent {
|
||||
call_id: "c1".into(),
|
||||
turn_id: "turn-c1".into(),
|
||||
auto_approved: false,
|
||||
changes: apply_changes,
|
||||
}),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert!(!cells.is_empty(), "expected apply block cell to be sent");
|
||||
@@ -1643,16 +1483,17 @@ async fn apply_patch_manual_flow_snapshot() {
|
||||
content: "hello\n".to_string(),
|
||||
},
|
||||
);
|
||||
chat.handle_codex_event(Event {
|
||||
id: "s1".into(),
|
||||
msg: EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent {
|
||||
handle_apply_patch_approval_request(
|
||||
&mut chat,
|
||||
"s1",
|
||||
ApplyPatchApprovalRequestEvent {
|
||||
call_id: "c1".into(),
|
||||
turn_id: "turn-c1".into(),
|
||||
changes: proposed_changes,
|
||||
reason: Some("Manual review required".into()),
|
||||
grant_root: None,
|
||||
}),
|
||||
});
|
||||
},
|
||||
);
|
||||
let history_before_apply = drain_insert_history(&mut rx);
|
||||
assert!(
|
||||
history_before_apply.is_empty(),
|
||||
@@ -1666,15 +1507,15 @@ async fn apply_patch_manual_flow_snapshot() {
|
||||
content: "hello\n".to_string(),
|
||||
},
|
||||
);
|
||||
chat.handle_codex_event(Event {
|
||||
id: "s1".into(),
|
||||
msg: EventMsg::PatchApplyBegin(PatchApplyBeginEvent {
|
||||
handle_patch_apply_begin(
|
||||
&mut chat,
|
||||
PatchApplyBeginEvent {
|
||||
call_id: "c1".into(),
|
||||
turn_id: "turn-c1".into(),
|
||||
auto_approved: false,
|
||||
changes: apply_changes,
|
||||
}),
|
||||
});
|
||||
},
|
||||
);
|
||||
let approved_lines = drain_insert_history(&mut rx)
|
||||
.pop()
|
||||
.expect("approved patch cell");
|
||||
@@ -1703,10 +1544,7 @@ async fn apply_patch_approval_sends_op_with_call_id() {
|
||||
reason: None,
|
||||
grant_root: None,
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
id: "sub-123".into(),
|
||||
msg: EventMsg::ApplyPatchApprovalRequest(ev),
|
||||
});
|
||||
handle_apply_patch_approval_request(&mut chat, "sub-123", ev);
|
||||
|
||||
// Approve via key press 'y'
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE));
|
||||
@@ -1741,16 +1579,17 @@ async fn apply_patch_full_flow_integration_like() {
|
||||
PathBuf::from("pkg.rs"),
|
||||
FileChange::Add { content: "".into() },
|
||||
);
|
||||
chat.handle_codex_event(Event {
|
||||
id: "sub-xyz".into(),
|
||||
msg: EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent {
|
||||
handle_apply_patch_approval_request(
|
||||
&mut chat,
|
||||
"sub-xyz",
|
||||
ApplyPatchApprovalRequestEvent {
|
||||
call_id: "call-1".into(),
|
||||
turn_id: "turn-call-1".into(),
|
||||
changes,
|
||||
reason: None,
|
||||
grant_root: None,
|
||||
}),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// 2) User approves via 'y' and App receives a thread-scoped op
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE));
|
||||
@@ -1785,32 +1624,32 @@ async fn apply_patch_full_flow_integration_like() {
|
||||
PathBuf::from("pkg.rs"),
|
||||
FileChange::Add { content: "".into() },
|
||||
);
|
||||
chat.handle_codex_event(Event {
|
||||
id: "sub-xyz".into(),
|
||||
msg: EventMsg::PatchApplyBegin(PatchApplyBeginEvent {
|
||||
handle_patch_apply_begin(
|
||||
&mut chat,
|
||||
PatchApplyBeginEvent {
|
||||
call_id: "call-1".into(),
|
||||
turn_id: "turn-call-1".into(),
|
||||
auto_approved: false,
|
||||
changes: changes2,
|
||||
}),
|
||||
});
|
||||
},
|
||||
);
|
||||
let mut end_changes = HashMap::new();
|
||||
end_changes.insert(
|
||||
PathBuf::from("pkg.rs"),
|
||||
FileChange::Add { content: "".into() },
|
||||
);
|
||||
chat.handle_codex_event(Event {
|
||||
id: "sub-xyz".into(),
|
||||
msg: EventMsg::PatchApplyEnd(PatchApplyEndEvent {
|
||||
handle_patch_apply_end(
|
||||
&mut chat,
|
||||
PatchApplyEndEvent {
|
||||
call_id: "call-1".into(),
|
||||
turn_id: "turn-call-1".into(),
|
||||
stdout: String::from("ok"),
|
||||
stderr: String::new(),
|
||||
success: true,
|
||||
changes: end_changes,
|
||||
status: CorePatchApplyStatus::Completed,
|
||||
}),
|
||||
});
|
||||
status: AppServerPatchApplyStatus::Completed,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -1828,16 +1667,17 @@ async fn apply_patch_untrusted_shows_approval_modal() -> anyhow::Result<()> {
|
||||
PathBuf::from("a.rs"),
|
||||
FileChange::Add { content: "".into() },
|
||||
);
|
||||
chat.handle_codex_event(Event {
|
||||
id: "sub-1".into(),
|
||||
msg: EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent {
|
||||
handle_apply_patch_approval_request(
|
||||
&mut chat,
|
||||
"sub-1",
|
||||
ApplyPatchApprovalRequestEvent {
|
||||
call_id: "call-1".into(),
|
||||
turn_id: "turn-call-1".into(),
|
||||
changes,
|
||||
reason: None,
|
||||
grant_root: None,
|
||||
}),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// Render and ensure the approval modal title is present
|
||||
let area = Rect::new(0, 0, 80, 12);
|
||||
@@ -1882,16 +1722,17 @@ async fn apply_patch_request_omits_diff_summary_from_modal() -> anyhow::Result<(
|
||||
content: "line one\nline two\n".into(),
|
||||
},
|
||||
);
|
||||
chat.handle_codex_event(Event {
|
||||
id: "sub-apply".into(),
|
||||
msg: EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent {
|
||||
handle_apply_patch_approval_request(
|
||||
&mut chat,
|
||||
"sub-apply",
|
||||
ApplyPatchApprovalRequestEvent {
|
||||
call_id: "call-apply".into(),
|
||||
turn_id: "turn-apply".into(),
|
||||
changes,
|
||||
reason: None,
|
||||
grant_root: None,
|
||||
}),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
assert!(
|
||||
drain_insert_history(&mut rx).is_empty(),
|
||||
|
||||
@@ -23,10 +23,7 @@ fn auto_review_denial_event() -> GuardianAssessmentEvent {
|
||||
async fn auto_review_denials_popup_lists_stored_auto_review_denials() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.thread_id = Some(ThreadId::new());
|
||||
chat.handle_codex_event(Event {
|
||||
id: "guardian-assessment".into(),
|
||||
msg: EventMsg::GuardianAssessment(auto_review_denial_event()),
|
||||
});
|
||||
chat.on_guardian_assessment(auto_review_denial_event());
|
||||
drain_insert_history(&mut rx);
|
||||
|
||||
chat.open_auto_review_denials_popup();
|
||||
@@ -40,10 +37,7 @@ async fn approving_recent_denial_emits_structured_core_op_once() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
let thread_id = ThreadId::new();
|
||||
chat.thread_id = Some(thread_id);
|
||||
chat.handle_codex_event(Event {
|
||||
id: "guardian-assessment".into(),
|
||||
msg: EventMsg::GuardianAssessment(auto_review_denial_event()),
|
||||
});
|
||||
chat.on_guardian_assessment(auto_review_denial_event());
|
||||
drain_insert_history(&mut rx);
|
||||
|
||||
chat.approve_recent_auto_review_denial(thread_id, "auto-review-recent-1".to_string());
|
||||
@@ -75,39 +69,28 @@ async fn guardian_denied_exec_renders_warning_and_denied_request() {
|
||||
cwd: test_path_buf("/tmp").abs(),
|
||||
};
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "guardian-in-progress".into(),
|
||||
msg: EventMsg::GuardianAssessment(GuardianAssessmentEvent {
|
||||
id: "guardian-1".into(),
|
||||
target_item_id: Some("guardian-target-1".into()),
|
||||
turn_id: "turn-1".into(),
|
||||
status: GuardianAssessmentStatus::InProgress,
|
||||
risk_level: None,
|
||||
user_authorization: None,
|
||||
rationale: None,
|
||||
decision_source: None,
|
||||
action: action.clone(),
|
||||
}),
|
||||
chat.on_guardian_assessment(GuardianAssessmentEvent {
|
||||
id: "guardian-1".into(),
|
||||
target_item_id: Some("guardian-target-1".into()),
|
||||
turn_id: "turn-1".into(),
|
||||
status: GuardianAssessmentStatus::InProgress,
|
||||
risk_level: None,
|
||||
user_authorization: None,
|
||||
rationale: None,
|
||||
decision_source: None,
|
||||
action: action.clone(),
|
||||
});
|
||||
chat.handle_codex_event(Event {
|
||||
id: "guardian-warning".into(),
|
||||
msg: EventMsg::GuardianWarning(WarningEvent {
|
||||
message: "Automatic approval review denied (risk: high): The planned action would transmit the full contents of a workspace source file (`core/src/codex.rs`) to `https://example.com`, which is an external and untrusted endpoint.".into(),
|
||||
}),
|
||||
});
|
||||
chat.handle_codex_event(Event {
|
||||
id: "guardian-assessment".into(),
|
||||
msg: EventMsg::GuardianAssessment(GuardianAssessmentEvent {
|
||||
id: "guardian-1".into(),
|
||||
target_item_id: Some("guardian-target-1".into()),
|
||||
turn_id: "turn-1".into(),
|
||||
status: GuardianAssessmentStatus::Denied,
|
||||
risk_level: Some(GuardianRiskLevel::High),
|
||||
user_authorization: Some(GuardianUserAuthorization::Low),
|
||||
rationale: Some("Would exfiltrate local source code.".into()),
|
||||
decision_source: Some(GuardianAssessmentDecisionSource::Agent),
|
||||
action,
|
||||
}),
|
||||
chat.on_warning("Automatic approval review denied (risk: high): The planned action would transmit the full contents of a workspace source file (`core/src/codex.rs`) to `https://example.com`, which is an external and untrusted endpoint.");
|
||||
chat.on_guardian_assessment(GuardianAssessmentEvent {
|
||||
id: "guardian-1".into(),
|
||||
target_item_id: Some("guardian-target-1".into()),
|
||||
turn_id: "turn-1".into(),
|
||||
status: GuardianAssessmentStatus::Denied,
|
||||
risk_level: Some(GuardianRiskLevel::High),
|
||||
user_authorization: Some(GuardianUserAuthorization::Low),
|
||||
rationale: Some("Would exfiltrate local source code.".into()),
|
||||
decision_source: Some(GuardianAssessmentDecisionSource::Agent),
|
||||
action,
|
||||
});
|
||||
|
||||
let width: u16 = 140;
|
||||
@@ -140,23 +123,20 @@ async fn guardian_approved_exec_renders_approved_request() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.show_welcome_banner = false;
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "guardian-assessment".into(),
|
||||
msg: EventMsg::GuardianAssessment(GuardianAssessmentEvent {
|
||||
id: "thread:child-thread:guardian-1".into(),
|
||||
target_item_id: Some("guardian-approved-target".into()),
|
||||
turn_id: "turn-1".into(),
|
||||
status: GuardianAssessmentStatus::Approved,
|
||||
risk_level: Some(GuardianRiskLevel::Low),
|
||||
user_authorization: Some(GuardianUserAuthorization::High),
|
||||
rationale: Some("Narrowly scoped to the requested file.".into()),
|
||||
decision_source: Some(GuardianAssessmentDecisionSource::Agent),
|
||||
action: GuardianAssessmentAction::Command {
|
||||
source: GuardianCommandSource::Shell,
|
||||
command: "rm -f /tmp/guardian-approved.sqlite".to_string(),
|
||||
cwd: test_path_buf("/tmp").abs(),
|
||||
},
|
||||
}),
|
||||
chat.on_guardian_assessment(GuardianAssessmentEvent {
|
||||
id: "thread:child-thread:guardian-1".into(),
|
||||
target_item_id: Some("guardian-approved-target".into()),
|
||||
turn_id: "turn-1".into(),
|
||||
status: GuardianAssessmentStatus::Approved,
|
||||
risk_level: Some(GuardianRiskLevel::Low),
|
||||
user_authorization: Some(GuardianUserAuthorization::High),
|
||||
rationale: Some("Narrowly scoped to the requested file.".into()),
|
||||
decision_source: Some(GuardianAssessmentDecisionSource::Agent),
|
||||
action: GuardianAssessmentAction::Command {
|
||||
source: GuardianCommandSource::Shell,
|
||||
command: "rm -f /tmp/guardian-approved.sqlite".to_string(),
|
||||
cwd: test_path_buf("/tmp").abs(),
|
||||
},
|
||||
});
|
||||
|
||||
let width: u16 = 120;
|
||||
@@ -199,19 +179,16 @@ async fn guardian_approved_request_permissions_renders_request_summary() {
|
||||
},
|
||||
};
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "guardian-in-progress".into(),
|
||||
msg: EventMsg::GuardianAssessment(GuardianAssessmentEvent {
|
||||
id: "guardian-request-permissions".into(),
|
||||
target_item_id: None,
|
||||
turn_id: "turn-1".into(),
|
||||
status: GuardianAssessmentStatus::InProgress,
|
||||
risk_level: None,
|
||||
user_authorization: None,
|
||||
rationale: None,
|
||||
decision_source: None,
|
||||
action: action.clone(),
|
||||
}),
|
||||
chat.on_guardian_assessment(GuardianAssessmentEvent {
|
||||
id: "guardian-request-permissions".into(),
|
||||
target_item_id: None,
|
||||
turn_id: "turn-1".into(),
|
||||
status: GuardianAssessmentStatus::InProgress,
|
||||
risk_level: None,
|
||||
user_authorization: None,
|
||||
rationale: None,
|
||||
decision_source: None,
|
||||
action: action.clone(),
|
||||
});
|
||||
|
||||
let status = chat
|
||||
@@ -224,19 +201,16 @@ async fn guardian_approved_request_permissions_renders_request_summary() {
|
||||
Some("permission request: Need write access for generated report assets.")
|
||||
);
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "guardian-assessment".into(),
|
||||
msg: EventMsg::GuardianAssessment(GuardianAssessmentEvent {
|
||||
id: "guardian-request-permissions".into(),
|
||||
target_item_id: None,
|
||||
turn_id: "turn-1".into(),
|
||||
status: GuardianAssessmentStatus::Approved,
|
||||
risk_level: Some(GuardianRiskLevel::Low),
|
||||
user_authorization: Some(GuardianUserAuthorization::High),
|
||||
rationale: Some("Request is scoped to report output.".into()),
|
||||
decision_source: Some(GuardianAssessmentDecisionSource::Agent),
|
||||
action,
|
||||
}),
|
||||
chat.on_guardian_assessment(GuardianAssessmentEvent {
|
||||
id: "guardian-request-permissions".into(),
|
||||
target_item_id: None,
|
||||
turn_id: "turn-1".into(),
|
||||
status: GuardianAssessmentStatus::Approved,
|
||||
risk_level: Some(GuardianRiskLevel::Low),
|
||||
user_authorization: Some(GuardianUserAuthorization::High),
|
||||
rationale: Some("Request is scoped to report output.".into()),
|
||||
decision_source: Some(GuardianAssessmentDecisionSource::Agent),
|
||||
action,
|
||||
});
|
||||
|
||||
let width: u16 = 110;
|
||||
@@ -275,43 +249,30 @@ async fn guardian_timed_out_exec_renders_warning_and_timed_out_request() {
|
||||
cwd: test_path_buf("/tmp").abs(),
|
||||
};
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "guardian-in-progress".into(),
|
||||
msg: EventMsg::GuardianAssessment(GuardianAssessmentEvent {
|
||||
id: "guardian-1".into(),
|
||||
target_item_id: Some("guardian-target-1".into()),
|
||||
turn_id: "turn-1".into(),
|
||||
status: GuardianAssessmentStatus::InProgress,
|
||||
risk_level: None,
|
||||
user_authorization: None,
|
||||
rationale: None,
|
||||
decision_source: None,
|
||||
action: action.clone(),
|
||||
}),
|
||||
chat.on_guardian_assessment(GuardianAssessmentEvent {
|
||||
id: "guardian-1".into(),
|
||||
target_item_id: Some("guardian-target-1".into()),
|
||||
turn_id: "turn-1".into(),
|
||||
status: GuardianAssessmentStatus::InProgress,
|
||||
risk_level: None,
|
||||
user_authorization: None,
|
||||
rationale: None,
|
||||
decision_source: None,
|
||||
action: action.clone(),
|
||||
});
|
||||
chat.handle_codex_event(Event {
|
||||
id: "guardian-warning".into(),
|
||||
msg: EventMsg::GuardianWarning(WarningEvent {
|
||||
message: "Automatic approval review timed out while evaluating the requested approval."
|
||||
.into(),
|
||||
}),
|
||||
});
|
||||
chat.handle_codex_event(Event {
|
||||
id: "guardian-assessment".into(),
|
||||
msg: EventMsg::GuardianAssessment(GuardianAssessmentEvent {
|
||||
id: "guardian-1".into(),
|
||||
target_item_id: Some("guardian-target-1".into()),
|
||||
turn_id: "turn-1".into(),
|
||||
status: GuardianAssessmentStatus::TimedOut,
|
||||
risk_level: None,
|
||||
user_authorization: None,
|
||||
rationale: Some(
|
||||
"Automatic approval review timed out while evaluating the requested approval."
|
||||
.into(),
|
||||
),
|
||||
decision_source: Some(GuardianAssessmentDecisionSource::Agent),
|
||||
action,
|
||||
}),
|
||||
chat.on_warning("Automatic approval review timed out while evaluating the requested approval.");
|
||||
chat.on_guardian_assessment(GuardianAssessmentEvent {
|
||||
id: "guardian-1".into(),
|
||||
target_item_id: Some("guardian-target-1".into()),
|
||||
turn_id: "turn-1".into(),
|
||||
status: GuardianAssessmentStatus::TimedOut,
|
||||
risk_level: None,
|
||||
user_authorization: None,
|
||||
rationale: Some(
|
||||
"Automatic approval review timed out while evaluating the requested approval.".into(),
|
||||
),
|
||||
decision_source: Some(GuardianAssessmentDecisionSource::Agent),
|
||||
action,
|
||||
});
|
||||
|
||||
let width: u16 = 140;
|
||||
@@ -541,23 +502,20 @@ async fn guardian_parallel_reviews_render_aggregate_status_snapshot() {
|
||||
("guardian-1", "rm -rf '/tmp/guardian target 1'"),
|
||||
("guardian-2", "rm -rf '/tmp/guardian target 2'"),
|
||||
] {
|
||||
chat.handle_codex_event(Event {
|
||||
id: format!("event-{id}"),
|
||||
msg: EventMsg::GuardianAssessment(GuardianAssessmentEvent {
|
||||
id: id.to_string(),
|
||||
target_item_id: Some(format!("{id}-target")),
|
||||
turn_id: "turn-1".to_string(),
|
||||
status: GuardianAssessmentStatus::InProgress,
|
||||
risk_level: None,
|
||||
user_authorization: None,
|
||||
rationale: None,
|
||||
decision_source: None,
|
||||
action: GuardianAssessmentAction::Command {
|
||||
source: GuardianCommandSource::Shell,
|
||||
command: command.to_string(),
|
||||
cwd: test_path_buf("/tmp").abs(),
|
||||
},
|
||||
}),
|
||||
chat.on_guardian_assessment(GuardianAssessmentEvent {
|
||||
id: id.to_string(),
|
||||
target_item_id: Some(format!("{id}-target")),
|
||||
turn_id: "turn-1".to_string(),
|
||||
status: GuardianAssessmentStatus::InProgress,
|
||||
risk_level: None,
|
||||
user_authorization: None,
|
||||
rationale: None,
|
||||
decision_source: None,
|
||||
action: GuardianAssessmentAction::Command {
|
||||
source: GuardianCommandSource::Shell,
|
||||
command: command.to_string(),
|
||||
cwd: test_path_buf("/tmp").abs(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -573,59 +531,50 @@ async fn guardian_parallel_reviews_keep_remaining_review_visible_after_denial()
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.on_task_started();
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "event-guardian-1".into(),
|
||||
msg: EventMsg::GuardianAssessment(GuardianAssessmentEvent {
|
||||
id: "guardian-1".to_string(),
|
||||
target_item_id: Some("guardian-1-target".to_string()),
|
||||
turn_id: "turn-1".to_string(),
|
||||
status: GuardianAssessmentStatus::InProgress,
|
||||
risk_level: None,
|
||||
user_authorization: None,
|
||||
rationale: None,
|
||||
decision_source: None,
|
||||
action: GuardianAssessmentAction::Command {
|
||||
source: GuardianCommandSource::Shell,
|
||||
command: "rm -rf '/tmp/guardian target 1'".to_string(),
|
||||
cwd: test_path_buf("/tmp").abs(),
|
||||
},
|
||||
}),
|
||||
chat.on_guardian_assessment(GuardianAssessmentEvent {
|
||||
id: "guardian-1".to_string(),
|
||||
target_item_id: Some("guardian-1-target".to_string()),
|
||||
turn_id: "turn-1".to_string(),
|
||||
status: GuardianAssessmentStatus::InProgress,
|
||||
risk_level: None,
|
||||
user_authorization: None,
|
||||
rationale: None,
|
||||
decision_source: None,
|
||||
action: GuardianAssessmentAction::Command {
|
||||
source: GuardianCommandSource::Shell,
|
||||
command: "rm -rf '/tmp/guardian target 1'".to_string(),
|
||||
cwd: test_path_buf("/tmp").abs(),
|
||||
},
|
||||
});
|
||||
chat.handle_codex_event(Event {
|
||||
id: "event-guardian-2".into(),
|
||||
msg: EventMsg::GuardianAssessment(GuardianAssessmentEvent {
|
||||
id: "guardian-2".to_string(),
|
||||
target_item_id: Some("guardian-2-target".to_string()),
|
||||
turn_id: "turn-1".to_string(),
|
||||
status: GuardianAssessmentStatus::InProgress,
|
||||
risk_level: None,
|
||||
user_authorization: None,
|
||||
rationale: None,
|
||||
decision_source: None,
|
||||
action: GuardianAssessmentAction::Command {
|
||||
source: GuardianCommandSource::Shell,
|
||||
command: "rm -rf '/tmp/guardian target 2'".to_string(),
|
||||
cwd: test_path_buf("/tmp").abs(),
|
||||
},
|
||||
}),
|
||||
chat.on_guardian_assessment(GuardianAssessmentEvent {
|
||||
id: "guardian-2".to_string(),
|
||||
target_item_id: Some("guardian-2-target".to_string()),
|
||||
turn_id: "turn-1".to_string(),
|
||||
status: GuardianAssessmentStatus::InProgress,
|
||||
risk_level: None,
|
||||
user_authorization: None,
|
||||
rationale: None,
|
||||
decision_source: None,
|
||||
action: GuardianAssessmentAction::Command {
|
||||
source: GuardianCommandSource::Shell,
|
||||
command: "rm -rf '/tmp/guardian target 2'".to_string(),
|
||||
cwd: test_path_buf("/tmp").abs(),
|
||||
},
|
||||
});
|
||||
chat.handle_codex_event(Event {
|
||||
id: "event-guardian-1-denied".into(),
|
||||
msg: EventMsg::GuardianAssessment(GuardianAssessmentEvent {
|
||||
id: "guardian-1".to_string(),
|
||||
target_item_id: Some("guardian-1-target".to_string()),
|
||||
turn_id: "turn-1".to_string(),
|
||||
status: GuardianAssessmentStatus::Denied,
|
||||
risk_level: Some(GuardianRiskLevel::High),
|
||||
user_authorization: Some(GuardianUserAuthorization::Low),
|
||||
rationale: Some("Would delete important data.".to_string()),
|
||||
decision_source: Some(GuardianAssessmentDecisionSource::Agent),
|
||||
action: GuardianAssessmentAction::Command {
|
||||
source: GuardianCommandSource::Shell,
|
||||
command: "rm -rf '/tmp/guardian target 1'".to_string(),
|
||||
cwd: test_path_buf("/tmp").abs(),
|
||||
},
|
||||
}),
|
||||
chat.on_guardian_assessment(GuardianAssessmentEvent {
|
||||
id: "guardian-1".to_string(),
|
||||
target_item_id: Some("guardian-1-target".to_string()),
|
||||
turn_id: "turn-1".to_string(),
|
||||
status: GuardianAssessmentStatus::Denied,
|
||||
risk_level: Some(GuardianRiskLevel::High),
|
||||
user_authorization: Some(GuardianUserAuthorization::Low),
|
||||
rationale: Some("Would delete important data.".to_string()),
|
||||
decision_source: Some(GuardianAssessmentDecisionSource::Agent),
|
||||
action: GuardianAssessmentAction::Command {
|
||||
source: GuardianCommandSource::Shell,
|
||||
command: "rm -rf '/tmp/guardian target 1'".to_string(),
|
||||
cwd: test_path_buf("/tmp").abs(),
|
||||
},
|
||||
});
|
||||
|
||||
assert_eq!(chat.current_status.header, "Reviewing approval request");
|
||||
|
||||
@@ -497,6 +497,405 @@ pub(super) fn make_token_info(total_tokens: i64, context_window: i64) -> TokenUs
|
||||
}
|
||||
}
|
||||
|
||||
fn thread_id(chat: &ChatWidget) -> String {
|
||||
chat.thread_id.map(|id| id.to_string()).unwrap_or_default()
|
||||
}
|
||||
|
||||
fn token_usage_breakdown(usage: TokenUsage) -> codex_app_server_protocol::TokenUsageBreakdown {
|
||||
codex_app_server_protocol::TokenUsageBreakdown {
|
||||
total_tokens: usage.total_tokens,
|
||||
input_tokens: usage.input_tokens,
|
||||
cached_input_tokens: usage.cached_input_tokens,
|
||||
output_tokens: usage.output_tokens,
|
||||
reasoning_output_tokens: usage.reasoning_output_tokens,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn handle_token_count(chat: &mut ChatWidget, info: Option<TokenUsageInfo>) {
|
||||
match info {
|
||||
Some(info) => {
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::ThreadTokenUsageUpdated(
|
||||
codex_app_server_protocol::ThreadTokenUsageUpdatedNotification {
|
||||
thread_id: thread_id(chat),
|
||||
turn_id: chat
|
||||
.last_turn_id
|
||||
.clone()
|
||||
.unwrap_or_else(|| "turn-1".to_string()),
|
||||
token_usage: codex_app_server_protocol::ThreadTokenUsage {
|
||||
total: token_usage_breakdown(info.total_token_usage),
|
||||
last: token_usage_breakdown(info.last_token_usage),
|
||||
model_context_window: info.model_context_window,
|
||||
},
|
||||
},
|
||||
),
|
||||
/*replay_kind*/ None,
|
||||
);
|
||||
}
|
||||
None => chat.set_token_info(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn handle_error(
|
||||
chat: &mut ChatWidget,
|
||||
message: impl Into<String>,
|
||||
codex_error_info: Option<CodexErrorInfo>,
|
||||
) {
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::Error(ErrorNotification {
|
||||
error: AppServerTurnError {
|
||||
message: message.into(),
|
||||
codex_error_info,
|
||||
additional_details: None,
|
||||
},
|
||||
will_retry: false,
|
||||
thread_id: thread_id(chat),
|
||||
turn_id: chat
|
||||
.last_turn_id
|
||||
.clone()
|
||||
.unwrap_or_else(|| "turn-1".to_string()),
|
||||
}),
|
||||
/*replay_kind*/ None,
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn handle_stream_error(
|
||||
chat: &mut ChatWidget,
|
||||
message: impl Into<String>,
|
||||
additional_details: Option<String>,
|
||||
) {
|
||||
handle_stream_error_with_replay(chat, message, additional_details, /*replay_kind*/ None);
|
||||
}
|
||||
|
||||
pub(super) fn handle_stream_error_with_replay(
|
||||
chat: &mut ChatWidget,
|
||||
message: impl Into<String>,
|
||||
additional_details: Option<String>,
|
||||
replay_kind: Option<ReplayKind>,
|
||||
) {
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::Error(ErrorNotification {
|
||||
error: AppServerTurnError {
|
||||
message: message.into(),
|
||||
codex_error_info: None,
|
||||
additional_details,
|
||||
},
|
||||
will_retry: true,
|
||||
thread_id: thread_id(chat),
|
||||
turn_id: chat
|
||||
.last_turn_id
|
||||
.clone()
|
||||
.unwrap_or_else(|| "turn-1".to_string()),
|
||||
}),
|
||||
replay_kind,
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn handle_warning(chat: &mut ChatWidget, message: impl Into<String>) {
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::Warning(WarningNotification {
|
||||
thread_id: Some(thread_id(chat)),
|
||||
message: message.into(),
|
||||
}),
|
||||
/*replay_kind*/ None,
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn handle_model_verification(
|
||||
chat: &mut ChatWidget,
|
||||
verifications: Vec<AppServerModelVerification>,
|
||||
) {
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::ModelVerification(ModelVerificationNotification {
|
||||
thread_id: thread_id(chat),
|
||||
turn_id: chat
|
||||
.last_turn_id
|
||||
.clone()
|
||||
.unwrap_or_else(|| "turn-1".to_string()),
|
||||
verifications,
|
||||
}),
|
||||
/*replay_kind*/ None,
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn handle_agent_message_delta(chat: &mut ChatWidget, delta: impl Into<String>) {
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::AgentMessageDelta(
|
||||
codex_app_server_protocol::AgentMessageDeltaNotification {
|
||||
thread_id: thread_id(chat),
|
||||
turn_id: chat
|
||||
.last_turn_id
|
||||
.clone()
|
||||
.unwrap_or_else(|| "turn-1".to_string()),
|
||||
item_id: "msg-1".to_string(),
|
||||
delta: delta.into(),
|
||||
},
|
||||
),
|
||||
/*replay_kind*/ None,
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn handle_agent_reasoning_delta(chat: &mut ChatWidget, delta: impl Into<String>) {
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::ReasoningSummaryTextDelta(ReasoningSummaryTextDeltaNotification {
|
||||
thread_id: thread_id(chat),
|
||||
turn_id: chat
|
||||
.last_turn_id
|
||||
.clone()
|
||||
.unwrap_or_else(|| "turn-1".to_string()),
|
||||
item_id: "reasoning-1".to_string(),
|
||||
delta: delta.into(),
|
||||
summary_index: 0,
|
||||
}),
|
||||
/*replay_kind*/ None,
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn handle_agent_reasoning_final(chat: &mut ChatWidget) {
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::ItemCompleted(ItemCompletedNotification {
|
||||
thread_id: thread_id(chat),
|
||||
turn_id: chat
|
||||
.last_turn_id
|
||||
.clone()
|
||||
.unwrap_or_else(|| "turn-1".to_string()),
|
||||
item: AppServerThreadItem::Reasoning {
|
||||
id: "reasoning-1".to_string(),
|
||||
summary: Vec::new(),
|
||||
content: Vec::new(),
|
||||
},
|
||||
}),
|
||||
/*replay_kind*/ None,
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn handle_entered_review_mode(chat: &mut ChatWidget, review: impl Into<String>) {
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::ItemStarted(ItemStartedNotification {
|
||||
thread_id: thread_id(chat),
|
||||
turn_id: chat
|
||||
.last_turn_id
|
||||
.clone()
|
||||
.unwrap_or_else(|| "turn-1".to_string()),
|
||||
item: AppServerThreadItem::EnteredReviewMode {
|
||||
id: "review-start".to_string(),
|
||||
review: review.into(),
|
||||
},
|
||||
}),
|
||||
/*replay_kind*/ None,
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn replay_entered_review_mode(chat: &mut ChatWidget, review: impl Into<String>) {
|
||||
chat.replay_thread_item(
|
||||
AppServerThreadItem::EnteredReviewMode {
|
||||
id: "review-start".to_string(),
|
||||
review: review.into(),
|
||||
},
|
||||
"turn-1".to_string(),
|
||||
ReplayKind::ThreadSnapshot,
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn handle_exited_review_mode(chat: &mut ChatWidget) {
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::ItemCompleted(ItemCompletedNotification {
|
||||
thread_id: thread_id(chat),
|
||||
turn_id: chat
|
||||
.last_turn_id
|
||||
.clone()
|
||||
.unwrap_or_else(|| "turn-1".to_string()),
|
||||
item: AppServerThreadItem::ExitedReviewMode {
|
||||
id: "review-end".to_string(),
|
||||
review: String::new(),
|
||||
},
|
||||
}),
|
||||
/*replay_kind*/ None,
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn handle_exec_approval_request(
|
||||
chat: &mut ChatWidget,
|
||||
id: impl Into<String>,
|
||||
event: ExecApprovalRequestEvent,
|
||||
) {
|
||||
chat.on_exec_approval_request(id.into(), event);
|
||||
}
|
||||
|
||||
pub(super) fn handle_apply_patch_approval_request(
|
||||
chat: &mut ChatWidget,
|
||||
id: impl Into<String>,
|
||||
event: ApplyPatchApprovalRequestEvent,
|
||||
) {
|
||||
chat.on_apply_patch_approval_request(id.into(), event);
|
||||
}
|
||||
|
||||
fn file_update_changes_from_tui(changes: HashMap<PathBuf, FileChange>) -> Vec<FileUpdateChange> {
|
||||
changes
|
||||
.into_iter()
|
||||
.map(|(path, change)| {
|
||||
let (kind, diff) = match change {
|
||||
FileChange::Add { content } => (PatchChangeKind::Add, content),
|
||||
FileChange::Delete { content } => (PatchChangeKind::Delete, content),
|
||||
FileChange::Update {
|
||||
unified_diff,
|
||||
move_path,
|
||||
} => (PatchChangeKind::Update { move_path }, unified_diff),
|
||||
};
|
||||
FileUpdateChange {
|
||||
path: path.display().to_string(),
|
||||
kind,
|
||||
diff,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(super) fn handle_patch_apply_begin(chat: &mut ChatWidget, event: PatchApplyBeginEvent) {
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::ItemStarted(ItemStartedNotification {
|
||||
thread_id: thread_id(chat),
|
||||
turn_id: event.turn_id.clone(),
|
||||
item: AppServerThreadItem::FileChange {
|
||||
id: event.call_id,
|
||||
changes: file_update_changes_from_tui(event.changes),
|
||||
status: AppServerPatchApplyStatus::InProgress,
|
||||
},
|
||||
}),
|
||||
/*replay_kind*/ None,
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn handle_patch_apply_end(chat: &mut ChatWidget, event: PatchApplyEndEvent) {
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::ItemCompleted(ItemCompletedNotification {
|
||||
thread_id: thread_id(chat),
|
||||
turn_id: event.turn_id.clone(),
|
||||
item: AppServerThreadItem::FileChange {
|
||||
id: event.call_id,
|
||||
changes: file_update_changes_from_tui(event.changes),
|
||||
status: event.status,
|
||||
},
|
||||
}),
|
||||
/*replay_kind*/ None,
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn handle_view_image_tool_call(chat: &mut ChatWidget, event: ViewImageToolCallEvent) {
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::ItemCompleted(ItemCompletedNotification {
|
||||
thread_id: thread_id(chat),
|
||||
turn_id: "turn-1".to_string(),
|
||||
item: AppServerThreadItem::ImageView {
|
||||
id: event.call_id,
|
||||
path: event.path,
|
||||
},
|
||||
}),
|
||||
/*replay_kind*/ None,
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn handle_image_generation_end(chat: &mut ChatWidget, event: ImageGenerationEndEvent) {
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::ItemCompleted(ItemCompletedNotification {
|
||||
thread_id: thread_id(chat),
|
||||
turn_id: "turn-1".to_string(),
|
||||
item: AppServerThreadItem::ImageGeneration {
|
||||
id: event.call_id,
|
||||
status: event.status,
|
||||
revised_prompt: event.revised_prompt,
|
||||
result: event.result,
|
||||
saved_path: event.saved_path,
|
||||
},
|
||||
}),
|
||||
/*replay_kind*/ None,
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn replay_user_message_inputs(
|
||||
chat: &mut ChatWidget,
|
||||
item_id: &str,
|
||||
content: Vec<AppServerUserInput>,
|
||||
replay_kind: ReplayKind,
|
||||
) {
|
||||
chat.replay_thread_item(
|
||||
AppServerThreadItem::UserMessage {
|
||||
id: item_id.to_string(),
|
||||
content,
|
||||
},
|
||||
"turn-1".to_string(),
|
||||
replay_kind,
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn replay_user_message_text(
|
||||
chat: &mut ChatWidget,
|
||||
item_id: &str,
|
||||
text: impl Into<String>,
|
||||
replay_kind: ReplayKind,
|
||||
) {
|
||||
replay_user_message_inputs(
|
||||
chat,
|
||||
item_id,
|
||||
vec![AppServerUserInput::Text {
|
||||
text: text.into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
replay_kind,
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn replay_agent_message(
|
||||
chat: &mut ChatWidget,
|
||||
item_id: &str,
|
||||
text: impl Into<String>,
|
||||
replay_kind: ReplayKind,
|
||||
) {
|
||||
chat.replay_thread_item(
|
||||
AppServerThreadItem::AgentMessage {
|
||||
id: item_id.to_string(),
|
||||
text: text.into(),
|
||||
phase: Some(MessagePhase::FinalAnswer),
|
||||
memory_citation: None,
|
||||
},
|
||||
"turn-1".to_string(),
|
||||
replay_kind,
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn replay_turn_started(chat: &mut ChatWidget, replay_kind: ReplayKind) {
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::TurnStarted(TurnStartedNotification {
|
||||
thread_id: thread_id(chat),
|
||||
turn: app_server_turn(
|
||||
"turn-1",
|
||||
AppServerTurnStatus::InProgress,
|
||||
/*duration_ms*/ None,
|
||||
/*error*/ None,
|
||||
),
|
||||
}),
|
||||
Some(replay_kind),
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn replay_agent_message_delta(
|
||||
chat: &mut ChatWidget,
|
||||
delta: impl Into<String>,
|
||||
replay_kind: ReplayKind,
|
||||
) {
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::AgentMessageDelta(
|
||||
codex_app_server_protocol::AgentMessageDeltaNotification {
|
||||
thread_id: thread_id(chat),
|
||||
turn_id: "turn-1".to_string(),
|
||||
item_id: "msg-1".to_string(),
|
||||
delta: delta.into(),
|
||||
},
|
||||
),
|
||||
Some(replay_kind),
|
||||
);
|
||||
}
|
||||
|
||||
// --- Small helpers to tersely drive exec begin/end and snapshot active cell ---
|
||||
pub(super) fn begin_exec_with_source(
|
||||
chat: &mut ChatWidget,
|
||||
@@ -521,10 +920,7 @@ pub(super) fn begin_exec_with_source(
|
||||
source,
|
||||
interaction_input,
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
id: call_id.to_string(),
|
||||
msg: EventMsg::ExecCommandBegin(event.clone()),
|
||||
});
|
||||
handle_exec_begin(chat, event.clone());
|
||||
event
|
||||
}
|
||||
|
||||
@@ -546,27 +942,89 @@ pub(super) fn begin_unified_exec_startup(
|
||||
source: ExecCommandSource::UnifiedExecStartup,
|
||||
interaction_input: None,
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
id: call_id.to_string(),
|
||||
msg: EventMsg::ExecCommandBegin(event.clone()),
|
||||
});
|
||||
handle_exec_begin(chat, event.clone());
|
||||
event
|
||||
}
|
||||
|
||||
pub(super) fn handle_exec_begin(chat: &mut ChatWidget, event: ExecCommandBeginEvent) {
|
||||
let command_actions = event
|
||||
.parsed_cmd
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|parsed| AppServerCommandAction::from_core_with_cwd(parsed, &event.cwd))
|
||||
.collect();
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::ItemStarted(ItemStartedNotification {
|
||||
thread_id: thread_id(chat),
|
||||
turn_id: event.turn_id.clone(),
|
||||
item: AppServerThreadItem::CommandExecution {
|
||||
id: event.call_id,
|
||||
command: codex_shell_command::parse_command::shlex_join(&event.command),
|
||||
cwd: event.cwd,
|
||||
process_id: event.process_id,
|
||||
source: event.source,
|
||||
status: AppServerCommandExecutionStatus::InProgress,
|
||||
command_actions,
|
||||
aggregated_output: None,
|
||||
exit_code: None,
|
||||
duration_ms: None,
|
||||
},
|
||||
}),
|
||||
/*replay_kind*/ None,
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn terminal_interaction(
|
||||
chat: &mut ChatWidget,
|
||||
call_id: &str,
|
||||
process_id: &str,
|
||||
stdin: &str,
|
||||
) {
|
||||
chat.handle_codex_event(Event {
|
||||
id: call_id.to_string(),
|
||||
msg: EventMsg::TerminalInteraction(TerminalInteractionEvent {
|
||||
call_id: call_id.to_string(),
|
||||
process_id: process_id.to_string(),
|
||||
stdin: stdin.to_string(),
|
||||
}),
|
||||
});
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::TerminalInteraction(
|
||||
codex_app_server_protocol::TerminalInteractionNotification {
|
||||
thread_id: thread_id(chat),
|
||||
turn_id: chat
|
||||
.last_turn_id
|
||||
.clone()
|
||||
.unwrap_or_else(|| "turn-1".to_string()),
|
||||
item_id: call_id.to_string(),
|
||||
process_id: process_id.to_string(),
|
||||
stdin: stdin.to_string(),
|
||||
},
|
||||
),
|
||||
/*replay_kind*/ None,
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn handle_terminal_interaction(chat: &mut ChatWidget, event: TerminalInteractionEvent) {
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::TerminalInteraction(
|
||||
codex_app_server_protocol::TerminalInteractionNotification {
|
||||
thread_id: thread_id(chat),
|
||||
turn_id: chat
|
||||
.last_turn_id
|
||||
.clone()
|
||||
.unwrap_or_else(|| "turn-1".to_string()),
|
||||
item_id: event.call_id,
|
||||
process_id: event.process_id,
|
||||
stdin: event.stdin,
|
||||
},
|
||||
),
|
||||
/*replay_kind*/ None,
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn terminal_interaction_event(
|
||||
call_id: &str,
|
||||
process_id: &str,
|
||||
stdin: &str,
|
||||
) -> TerminalInteractionEvent {
|
||||
TerminalInteractionEvent {
|
||||
call_id: call_id.to_string(),
|
||||
process_id: process_id.to_string(),
|
||||
stdin: stdin.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn complete_assistant_message(
|
||||
@@ -575,21 +1033,19 @@ pub(super) fn complete_assistant_message(
|
||||
text: &str,
|
||||
phase: Option<MessagePhase>,
|
||||
) {
|
||||
chat.handle_codex_event(Event {
|
||||
id: format!("raw-{item_id}"),
|
||||
msg: EventMsg::ItemCompleted(ItemCompletedEvent {
|
||||
thread_id: ThreadId::new(),
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::ItemCompleted(ItemCompletedNotification {
|
||||
thread_id: chat.thread_id.map(|id| id.to_string()).unwrap_or_default(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
item: TurnItem::AgentMessage(AgentMessageItem {
|
||||
item: AppServerThreadItem::AgentMessage {
|
||||
id: item_id.to_string(),
|
||||
content: vec![AgentMessageContent::Text {
|
||||
text: text.to_string(),
|
||||
}],
|
||||
text: text.to_string(),
|
||||
phase,
|
||||
memory_citation: None,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
});
|
||||
/*replay_kind*/ None,
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn pending_steer(text: &str) -> PendingSteer {
|
||||
@@ -619,17 +1075,88 @@ pub(super) fn complete_user_message_for_inputs(
|
||||
item_id: &str,
|
||||
content: Vec<UserInput>,
|
||||
) {
|
||||
chat.handle_codex_event(Event {
|
||||
id: format!("raw-{item_id}"),
|
||||
msg: EventMsg::ItemCompleted(ItemCompletedEvent {
|
||||
thread_id: ThreadId::new(),
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::ItemCompleted(ItemCompletedNotification {
|
||||
thread_id: chat.thread_id.map(|id| id.to_string()).unwrap_or_default(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
item: TurnItem::UserMessage(UserMessageItem {
|
||||
item: AppServerThreadItem::UserMessage {
|
||||
id: item_id.to_string(),
|
||||
content,
|
||||
}),
|
||||
content: content.into_iter().map(AppServerUserInput::from).collect(),
|
||||
},
|
||||
}),
|
||||
});
|
||||
/*replay_kind*/ None,
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn app_server_turn(
|
||||
turn_id: &str,
|
||||
status: AppServerTurnStatus,
|
||||
duration_ms: Option<i64>,
|
||||
error: Option<AppServerTurnError>,
|
||||
) -> AppServerTurn {
|
||||
AppServerTurn {
|
||||
id: turn_id.to_string(),
|
||||
items: Vec::new(),
|
||||
status,
|
||||
error,
|
||||
started_at: None,
|
||||
completed_at: None,
|
||||
duration_ms,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn handle_turn_started(chat: &mut ChatWidget, turn_id: &str) {
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::TurnStarted(TurnStartedNotification {
|
||||
thread_id: chat.thread_id.map(|id| id.to_string()).unwrap_or_default(),
|
||||
turn: app_server_turn(
|
||||
turn_id,
|
||||
AppServerTurnStatus::InProgress,
|
||||
/*duration_ms*/ None,
|
||||
/*error*/ None,
|
||||
),
|
||||
}),
|
||||
/*replay_kind*/ None,
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn handle_turn_completed(
|
||||
chat: &mut ChatWidget,
|
||||
turn_id: &str,
|
||||
duration_ms: Option<i64>,
|
||||
) {
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::TurnCompleted(TurnCompletedNotification {
|
||||
thread_id: chat.thread_id.map(|id| id.to_string()).unwrap_or_default(),
|
||||
turn: app_server_turn(
|
||||
turn_id,
|
||||
AppServerTurnStatus::Completed,
|
||||
duration_ms,
|
||||
/*error*/ None,
|
||||
),
|
||||
}),
|
||||
/*replay_kind*/ None,
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn handle_turn_interrupted(chat: &mut ChatWidget, turn_id: &str) {
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::TurnCompleted(TurnCompletedNotification {
|
||||
thread_id: chat.thread_id.map(|id| id.to_string()).unwrap_or_default(),
|
||||
turn: app_server_turn(
|
||||
turn_id,
|
||||
AppServerTurnStatus::Interrupted,
|
||||
/*duration_ms*/ None,
|
||||
/*error*/ None,
|
||||
),
|
||||
}),
|
||||
/*replay_kind*/ None,
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn handle_budget_limited_turn(chat: &mut ChatWidget, turn_id: &str) {
|
||||
chat.budget_limited_turn_ids.insert(turn_id.to_string());
|
||||
handle_turn_interrupted(chat, turn_id);
|
||||
}
|
||||
|
||||
pub(super) fn begin_exec(
|
||||
@@ -662,9 +1189,9 @@ pub(super) fn end_exec(
|
||||
interaction_input,
|
||||
process_id,
|
||||
} = begin_event;
|
||||
chat.handle_codex_event(Event {
|
||||
id: call_id.clone(),
|
||||
msg: EventMsg::ExecCommandEnd(ExecCommandEndEvent {
|
||||
handle_exec_end(
|
||||
chat,
|
||||
ExecCommandEndEvent {
|
||||
call_id,
|
||||
process_id,
|
||||
turn_id,
|
||||
@@ -680,12 +1207,42 @@ pub(super) fn end_exec(
|
||||
duration: std::time::Duration::from_millis(5),
|
||||
formatted_output: aggregated,
|
||||
status: if exit_code == 0 {
|
||||
CoreExecCommandStatus::Completed
|
||||
AppServerCommandExecutionStatus::Completed
|
||||
} else {
|
||||
CoreExecCommandStatus::Failed
|
||||
AppServerCommandExecutionStatus::Failed
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn handle_exec_end(chat: &mut ChatWidget, event: ExecCommandEndEvent) {
|
||||
let duration_ms = i64::try_from(event.duration.as_millis()).unwrap_or(i64::MAX);
|
||||
let command_actions = event
|
||||
.parsed_cmd
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|parsed| AppServerCommandAction::from_core_with_cwd(parsed, &event.cwd))
|
||||
.collect();
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::ItemCompleted(ItemCompletedNotification {
|
||||
thread_id: thread_id(chat),
|
||||
turn_id: event.turn_id.clone(),
|
||||
item: AppServerThreadItem::CommandExecution {
|
||||
id: event.call_id,
|
||||
command: codex_shell_command::parse_command::shlex_join(&event.command),
|
||||
cwd: event.cwd,
|
||||
process_id: event.process_id,
|
||||
source: event.source,
|
||||
status: event.status,
|
||||
command_actions,
|
||||
aggregated_output: (!event.aggregated_output.is_empty())
|
||||
.then_some(event.aggregated_output),
|
||||
exit_code: Some(event.exit_code),
|
||||
duration_ms: Some(duration_ms),
|
||||
},
|
||||
}),
|
||||
});
|
||||
/*replay_kind*/ None,
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn active_blob(chat: &ChatWidget) -> String {
|
||||
@@ -1015,6 +1572,67 @@ pub(super) fn type_plugins_search_query(chat: &mut ChatWidget, query: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn handle_hook_started(chat: &mut ChatWidget, run: AppServerHookRunSummary) {
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::HookStarted(AppServerHookStartedNotification {
|
||||
thread_id: thread_id(chat),
|
||||
turn_id: None,
|
||||
run,
|
||||
}),
|
||||
/*replay_kind*/ None,
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn handle_hook_completed(chat: &mut ChatWidget, run: AppServerHookRunSummary) {
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::HookCompleted(AppServerHookCompletedNotification {
|
||||
thread_id: thread_id(chat),
|
||||
turn_id: None,
|
||||
run,
|
||||
}),
|
||||
/*replay_kind*/ None,
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn hook_run(
|
||||
run_id: &str,
|
||||
event_name: codex_app_server_protocol::HookEventName,
|
||||
status: codex_app_server_protocol::HookRunStatus,
|
||||
status_message: &str,
|
||||
entries: Vec<codex_app_server_protocol::HookOutputEntry>,
|
||||
) -> codex_app_server_protocol::HookRunSummary {
|
||||
codex_app_server_protocol::HookRunSummary {
|
||||
id: run_id.to_string(),
|
||||
event_name,
|
||||
handler_type: codex_app_server_protocol::HookHandlerType::Command,
|
||||
execution_mode: codex_app_server_protocol::HookExecutionMode::Sync,
|
||||
scope: codex_app_server_protocol::HookScope::Turn,
|
||||
source_path: PathBuf::from(test_path_display("/tmp/hooks.json")).abs(),
|
||||
source: codex_app_server_protocol::HookSource::User,
|
||||
display_order: 0,
|
||||
status,
|
||||
status_message: Some(status_message.to_string()),
|
||||
started_at: 1,
|
||||
completed_at: matches!(
|
||||
status,
|
||||
codex_app_server_protocol::HookRunStatus::Completed
|
||||
| codex_app_server_protocol::HookRunStatus::Failed
|
||||
| codex_app_server_protocol::HookRunStatus::Blocked
|
||||
| codex_app_server_protocol::HookRunStatus::Stopped
|
||||
)
|
||||
.then_some(11),
|
||||
duration_ms: matches!(
|
||||
status,
|
||||
codex_app_server_protocol::HookRunStatus::Completed
|
||||
| codex_app_server_protocol::HookRunStatus::Failed
|
||||
| codex_app_server_protocol::HookRunStatus::Blocked
|
||||
| codex_app_server_protocol::HookRunStatus::Stopped
|
||||
)
|
||||
.then_some(10),
|
||||
entries,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn assert_hook_events_snapshot(
|
||||
event_name: codex_app_server_protocol::HookEventName,
|
||||
run_id: &str,
|
||||
@@ -1023,28 +1641,16 @@ pub(super) async fn assert_hook_events_snapshot(
|
||||
) {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "hook-1".into(),
|
||||
msg: EventMsg::HookStarted(crate::tool_activity::HookStartedEvent {
|
||||
turn_id: None,
|
||||
run: codex_app_server_protocol::HookRunSummary {
|
||||
id: run_id.to_string(),
|
||||
event_name,
|
||||
handler_type: codex_app_server_protocol::HookHandlerType::Command,
|
||||
execution_mode: codex_app_server_protocol::HookExecutionMode::Sync,
|
||||
scope: codex_app_server_protocol::HookScope::Turn,
|
||||
source_path: PathBuf::from(test_path_display("/tmp/hooks.json")).abs(),
|
||||
source: codex_app_server_protocol::HookSource::User,
|
||||
display_order: 0,
|
||||
status: codex_app_server_protocol::HookRunStatus::Running,
|
||||
status_message: Some(status_message.to_string()),
|
||||
started_at: 1,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
entries: vec![],
|
||||
},
|
||||
}),
|
||||
});
|
||||
handle_hook_started(
|
||||
&mut chat,
|
||||
hook_run(
|
||||
run_id,
|
||||
event_name,
|
||||
codex_app_server_protocol::HookRunStatus::Running,
|
||||
status_message,
|
||||
Vec::new(),
|
||||
),
|
||||
);
|
||||
assert!(
|
||||
drain_insert_history(&mut rx).is_empty(),
|
||||
"hook start should update the live hook cell instead of writing history"
|
||||
@@ -1058,37 +1664,25 @@ pub(super) async fn assert_hook_events_snapshot(
|
||||
"hook start should render in the live hook cell"
|
||||
);
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "hook-1".into(),
|
||||
msg: EventMsg::HookCompleted(crate::tool_activity::HookCompletedEvent {
|
||||
turn_id: None,
|
||||
run: codex_app_server_protocol::HookRunSummary {
|
||||
id: run_id.to_string(),
|
||||
event_name,
|
||||
handler_type: codex_app_server_protocol::HookHandlerType::Command,
|
||||
execution_mode: codex_app_server_protocol::HookExecutionMode::Sync,
|
||||
scope: codex_app_server_protocol::HookScope::Turn,
|
||||
source_path: PathBuf::from(test_path_display("/tmp/hooks.json")).abs(),
|
||||
source: codex_app_server_protocol::HookSource::User,
|
||||
display_order: 0,
|
||||
status: codex_app_server_protocol::HookRunStatus::Completed,
|
||||
status_message: Some(status_message.to_string()),
|
||||
started_at: 1,
|
||||
completed_at: Some(11),
|
||||
duration_ms: Some(10),
|
||||
entries: vec![
|
||||
codex_app_server_protocol::HookOutputEntry {
|
||||
kind: codex_app_server_protocol::HookOutputEntryKind::Warning,
|
||||
text: "Heads up from the hook".to_string(),
|
||||
},
|
||||
codex_app_server_protocol::HookOutputEntry {
|
||||
kind: codex_app_server_protocol::HookOutputEntryKind::Context,
|
||||
text: "Remember the startup checklist.".to_string(),
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
});
|
||||
handle_hook_completed(
|
||||
&mut chat,
|
||||
hook_run(
|
||||
run_id,
|
||||
event_name,
|
||||
codex_app_server_protocol::HookRunStatus::Completed,
|
||||
status_message,
|
||||
vec![
|
||||
codex_app_server_protocol::HookOutputEntry {
|
||||
kind: codex_app_server_protocol::HookOutputEntryKind::Warning,
|
||||
text: "Heads up from the hook".to_string(),
|
||||
},
|
||||
codex_app_server_protocol::HookOutputEntry {
|
||||
kind: codex_app_server_protocol::HookOutputEntryKind::Context,
|
||||
text: "Remember the startup checklist.".to_string(),
|
||||
},
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
let combined = cells
|
||||
|
||||
@@ -16,41 +16,39 @@ async fn resumed_initial_messages_render_history() {
|
||||
|
||||
let conversation_id = ThreadId::new();
|
||||
let rollout_file = NamedTempFile::new().unwrap();
|
||||
let configured = crate::chatwidget::test_events::SessionConfiguredEvent {
|
||||
session_id: conversation_id,
|
||||
let configured = crate::session_state::ThreadSessionState {
|
||||
thread_id: conversation_id,
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
thread_name: None,
|
||||
model: "test-model".to_string(),
|
||||
model_provider_id: "test-provider".to_string(),
|
||||
service_tier: None,
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: ApprovalsReviewer::User,
|
||||
permission_profile: codex_protocol::models::PermissionProfile::read_only(),
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
initial_messages: Some(vec![
|
||||
EventMsg::UserMessage(UserMessageEvent {
|
||||
message: "hello from user".to_string(),
|
||||
images: None,
|
||||
text_elements: Vec::new(),
|
||||
local_images: Vec::new(),
|
||||
}),
|
||||
EventMsg::AgentMessage(AgentMessageEvent {
|
||||
message: "assistant reply".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
}),
|
||||
]),
|
||||
network_proxy: None,
|
||||
rollout_path: Some(rollout_file.path().to_path_buf()),
|
||||
};
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "initial".into(),
|
||||
msg: EventMsg::SessionConfigured(configured),
|
||||
});
|
||||
chat.handle_thread_session(configured);
|
||||
replay_user_message_text(
|
||||
&mut chat,
|
||||
"user-1",
|
||||
"hello from user",
|
||||
ReplayKind::ResumeInitialMessages,
|
||||
);
|
||||
replay_agent_message(
|
||||
&mut chat,
|
||||
"assistant-1",
|
||||
"assistant reply",
|
||||
ReplayKind::ResumeInitialMessages,
|
||||
);
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
let mut merged_lines = Vec::new();
|
||||
@@ -74,47 +72,6 @@ async fn resumed_initial_messages_render_history() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_snapshot_replay_does_not_duplicate_agent_message_history() {
|
||||
let (mut chat, mut rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
chat.handle_codex_event_replay(Event {
|
||||
id: "turn-1".into(),
|
||||
msg: EventMsg::ItemCompleted(ItemCompletedEvent {
|
||||
thread_id: ThreadId::new(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
item: TurnItem::AgentMessage(AgentMessageItem {
|
||||
id: "msg-1".to_string(),
|
||||
content: vec![AgentMessageContent::Text {
|
||||
text: "assistant reply".to_string(),
|
||||
}],
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
}),
|
||||
}),
|
||||
});
|
||||
chat.handle_codex_event_replay(Event {
|
||||
id: "turn-1".into(),
|
||||
msg: EventMsg::AgentMessage(AgentMessageEvent {
|
||||
message: "assistant reply".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
}),
|
||||
});
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert_eq!(
|
||||
cells.len(),
|
||||
1,
|
||||
"expected replayed assistant message to render once"
|
||||
);
|
||||
let rendered = lines_to_single_string(&cells[0]);
|
||||
assert!(
|
||||
rendered.contains("assistant reply"),
|
||||
"expected replayed assistant message, got {rendered:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn replayed_user_message_preserves_text_elements_and_local_images() {
|
||||
let (mut chat, mut rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
@@ -129,34 +86,41 @@ async fn replayed_user_message_preserves_text_elements_and_local_images() {
|
||||
|
||||
let conversation_id = ThreadId::new();
|
||||
let rollout_file = NamedTempFile::new().unwrap();
|
||||
let configured = crate::chatwidget::test_events::SessionConfiguredEvent {
|
||||
session_id: conversation_id,
|
||||
let configured = crate::session_state::ThreadSessionState {
|
||||
thread_id: conversation_id,
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
thread_name: None,
|
||||
model: "test-model".to_string(),
|
||||
model_provider_id: "test-provider".to_string(),
|
||||
service_tier: None,
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: ApprovalsReviewer::User,
|
||||
permission_profile: codex_protocol::models::PermissionProfile::read_only(),
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
initial_messages: Some(vec![EventMsg::UserMessage(UserMessageEvent {
|
||||
message: message.clone(),
|
||||
images: None,
|
||||
text_elements: text_elements.clone(),
|
||||
local_images: local_images.clone(),
|
||||
})]),
|
||||
network_proxy: None,
|
||||
rollout_path: Some(rollout_file.path().to_path_buf()),
|
||||
};
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "initial".into(),
|
||||
msg: EventMsg::SessionConfigured(configured),
|
||||
});
|
||||
chat.handle_thread_session(configured);
|
||||
replay_user_message_inputs(
|
||||
&mut chat,
|
||||
"user-1",
|
||||
vec![
|
||||
AppServerUserInput::Text {
|
||||
text: message.clone(),
|
||||
text_elements: text_elements.clone().into_iter().map(Into::into).collect(),
|
||||
},
|
||||
AppServerUserInput::LocalImage {
|
||||
path: local_images[0].clone(),
|
||||
},
|
||||
],
|
||||
ReplayKind::ResumeInitialMessages,
|
||||
);
|
||||
|
||||
let mut user_cell = None;
|
||||
while let Ok(ev) = rx.try_recv() {
|
||||
@@ -190,34 +154,41 @@ async fn replayed_user_message_preserves_remote_image_urls() {
|
||||
|
||||
let conversation_id = ThreadId::new();
|
||||
let rollout_file = NamedTempFile::new().unwrap();
|
||||
let configured = crate::chatwidget::test_events::SessionConfiguredEvent {
|
||||
session_id: conversation_id,
|
||||
let configured = crate::session_state::ThreadSessionState {
|
||||
thread_id: conversation_id,
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
thread_name: None,
|
||||
model: "test-model".to_string(),
|
||||
model_provider_id: "test-provider".to_string(),
|
||||
service_tier: None,
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: ApprovalsReviewer::User,
|
||||
permission_profile: codex_protocol::models::PermissionProfile::read_only(),
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
initial_messages: Some(vec![EventMsg::UserMessage(UserMessageEvent {
|
||||
message: message.clone(),
|
||||
images: Some(remote_image_urls.clone()),
|
||||
text_elements: Vec::new(),
|
||||
local_images: Vec::new(),
|
||||
})]),
|
||||
network_proxy: None,
|
||||
rollout_path: Some(rollout_file.path().to_path_buf()),
|
||||
};
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "initial".into(),
|
||||
msg: EventMsg::SessionConfigured(configured),
|
||||
});
|
||||
chat.handle_thread_session(configured);
|
||||
replay_user_message_inputs(
|
||||
&mut chat,
|
||||
"user-1",
|
||||
vec![
|
||||
AppServerUserInput::Text {
|
||||
text: message.clone(),
|
||||
text_elements: Vec::new(),
|
||||
},
|
||||
AppServerUserInput::Image {
|
||||
url: remote_image_urls[0].clone(),
|
||||
},
|
||||
],
|
||||
ReplayKind::ResumeInitialMessages,
|
||||
);
|
||||
|
||||
let mut user_cell = None;
|
||||
while let Ok(ev) = rx.try_recv() {
|
||||
@@ -282,9 +253,10 @@ async fn session_configured_syncs_widget_config_permissions_and_cwd() {
|
||||
.to_legacy_sandbox_policy(expected_cwd.as_path())
|
||||
.expect("permission profile should project to legacy sandbox policy");
|
||||
let expected_sandbox = SandboxPolicy::from(expected_core_sandbox);
|
||||
let configured = crate::chatwidget::test_events::SessionConfiguredEvent {
|
||||
session_id: ThreadId::new(),
|
||||
let configured = crate::session_state::ThreadSessionState {
|
||||
thread_id: ThreadId::new(),
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
thread_name: None,
|
||||
model: "test-model".to_string(),
|
||||
model_provider_id: "test-provider".to_string(),
|
||||
@@ -293,18 +265,15 @@ async fn session_configured_syncs_widget_config_permissions_and_cwd() {
|
||||
approvals_reviewer: ApprovalsReviewer::User,
|
||||
permission_profile: expected_permission_profile,
|
||||
cwd: expected_cwd.clone(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
initial_messages: None,
|
||||
network_proxy: None,
|
||||
rollout_path: None,
|
||||
};
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "session-configured".into(),
|
||||
msg: EventMsg::SessionConfigured(configured),
|
||||
});
|
||||
chat.handle_thread_session(configured);
|
||||
|
||||
assert_eq!(
|
||||
AskForApproval::from(chat.config_ref().permissions.approval_policy.value()),
|
||||
@@ -340,9 +309,10 @@ async fn session_configured_external_sandbox_keeps_external_runtime_policy() {
|
||||
let expected_sandbox = SandboxPolicy::ExternalSandbox {
|
||||
network_access: NetworkAccess::Restricted,
|
||||
};
|
||||
let configured = crate::chatwidget::test_events::SessionConfiguredEvent {
|
||||
session_id: ThreadId::new(),
|
||||
let configured = crate::session_state::ThreadSessionState {
|
||||
thread_id: ThreadId::new(),
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
thread_name: None,
|
||||
model: "test-model".to_string(),
|
||||
model_provider_id: "test-provider".to_string(),
|
||||
@@ -351,18 +321,15 @@ async fn session_configured_external_sandbox_keeps_external_runtime_policy() {
|
||||
approvals_reviewer: ApprovalsReviewer::User,
|
||||
permission_profile: expected_permission_profile,
|
||||
cwd: test_path_buf("/home/user/external").abs(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
initial_messages: None,
|
||||
network_proxy: None,
|
||||
rollout_path: None,
|
||||
};
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "session-configured".into(),
|
||||
msg: EventMsg::SessionConfigured(configured),
|
||||
});
|
||||
chat.handle_thread_session(configured);
|
||||
|
||||
let actual_sandbox = SandboxPolicy::from(chat.config_ref().legacy_sandbox_policy());
|
||||
assert_eq!(&actual_sandbox, &expected_sandbox);
|
||||
@@ -380,34 +347,35 @@ async fn replayed_user_message_with_only_remote_images_renders_history_cell() {
|
||||
|
||||
let conversation_id = ThreadId::new();
|
||||
let rollout_file = NamedTempFile::new().unwrap();
|
||||
let configured = crate::chatwidget::test_events::SessionConfiguredEvent {
|
||||
session_id: conversation_id,
|
||||
let configured = crate::session_state::ThreadSessionState {
|
||||
thread_id: conversation_id,
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
thread_name: None,
|
||||
model: "test-model".to_string(),
|
||||
model_provider_id: "test-provider".to_string(),
|
||||
service_tier: None,
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: ApprovalsReviewer::User,
|
||||
permission_profile: codex_protocol::models::PermissionProfile::read_only(),
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
initial_messages: Some(vec![EventMsg::UserMessage(UserMessageEvent {
|
||||
message: String::new(),
|
||||
images: Some(remote_image_urls.clone()),
|
||||
text_elements: Vec::new(),
|
||||
local_images: Vec::new(),
|
||||
})]),
|
||||
network_proxy: None,
|
||||
rollout_path: Some(rollout_file.path().to_path_buf()),
|
||||
};
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "initial".into(),
|
||||
msg: EventMsg::SessionConfigured(configured),
|
||||
});
|
||||
chat.handle_thread_session(configured);
|
||||
replay_user_message_inputs(
|
||||
&mut chat,
|
||||
"user-1",
|
||||
vec![AppServerUserInput::Image {
|
||||
url: remote_image_urls[0].clone(),
|
||||
}],
|
||||
ReplayKind::ResumeInitialMessages,
|
||||
);
|
||||
|
||||
let mut user_cell = None;
|
||||
while let Ok(ev) = rx.try_recv() {
|
||||
@@ -429,38 +397,39 @@ async fn replayed_user_message_with_only_remote_images_renders_history_cell() {
|
||||
async fn replayed_user_message_with_only_local_images_does_not_render_history_cell() {
|
||||
let (mut chat, mut rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
let local_images = vec![PathBuf::from("/tmp/replay-local-only.png")];
|
||||
let local_images = [PathBuf::from("/tmp/replay-local-only.png")];
|
||||
|
||||
let conversation_id = ThreadId::new();
|
||||
let rollout_file = NamedTempFile::new().unwrap();
|
||||
let configured = crate::chatwidget::test_events::SessionConfiguredEvent {
|
||||
session_id: conversation_id,
|
||||
let configured = crate::session_state::ThreadSessionState {
|
||||
thread_id: conversation_id,
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
thread_name: None,
|
||||
model: "test-model".to_string(),
|
||||
model_provider_id: "test-provider".to_string(),
|
||||
service_tier: None,
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: ApprovalsReviewer::User,
|
||||
permission_profile: codex_protocol::models::PermissionProfile::read_only(),
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
initial_messages: Some(vec![EventMsg::UserMessage(UserMessageEvent {
|
||||
message: String::new(),
|
||||
images: None,
|
||||
text_elements: Vec::new(),
|
||||
local_images,
|
||||
})]),
|
||||
network_proxy: None,
|
||||
rollout_path: Some(rollout_file.path().to_path_buf()),
|
||||
};
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "initial".into(),
|
||||
msg: EventMsg::SessionConfigured(configured),
|
||||
});
|
||||
chat.handle_thread_session(configured);
|
||||
replay_user_message_inputs(
|
||||
&mut chat,
|
||||
"user-1",
|
||||
vec![AppServerUserInput::LocalImage {
|
||||
path: local_images[0].clone(),
|
||||
}],
|
||||
ReplayKind::ResumeInitialMessages,
|
||||
);
|
||||
|
||||
let mut found_user_history_cell = false;
|
||||
while let Ok(ev) = rx.try_recv() {
|
||||
@@ -620,72 +589,21 @@ async fn app_server_forked_thread_history_line_without_app_server_name_ignores_l
|
||||
async fn thread_snapshot_replay_preserves_agent_message_during_review_mode() {
|
||||
let (mut chat, mut rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
chat.handle_codex_event_replay(Event {
|
||||
id: "review-start".into(),
|
||||
msg: EventMsg::EnteredReviewMode("current changes".to_string()),
|
||||
});
|
||||
replay_entered_review_mode(&mut chat, "current changes");
|
||||
let _ = drain_insert_history(&mut rx);
|
||||
|
||||
chat.handle_codex_event_replay(Event {
|
||||
id: "review-message".into(),
|
||||
msg: EventMsg::AgentMessage(AgentMessageEvent {
|
||||
message: "Review progress update".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
}),
|
||||
});
|
||||
replay_agent_message(
|
||||
&mut chat,
|
||||
"review-message",
|
||||
"Review progress update",
|
||||
ReplayKind::ThreadSnapshot,
|
||||
);
|
||||
|
||||
let inserted = drain_insert_history(&mut rx);
|
||||
assert_eq!(inserted.len(), 1);
|
||||
assert!(lines_to_single_string(&inserted[0]).contains("Review progress update"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn replayed_thread_rollback_emits_ordered_app_event() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5")).await;
|
||||
|
||||
chat.replay_initial_messages(vec![EventMsg::ThreadRolledBack(ThreadRolledBackEvent {
|
||||
num_turns: 2,
|
||||
})]);
|
||||
|
||||
let mut saw = false;
|
||||
while let Ok(event) = rx.try_recv() {
|
||||
if let AppEvent::ApplyThreadRollback { num_turns } = event {
|
||||
saw = true;
|
||||
assert_eq!(num_turns, 2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
assert!(saw, "expected replay rollback app event");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn live_legacy_agent_message_after_item_completed_does_not_duplicate_assistant_message() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
complete_assistant_message(
|
||||
&mut chat,
|
||||
"msg-live",
|
||||
"hello",
|
||||
Some(MessagePhase::FinalAnswer),
|
||||
);
|
||||
let inserted = drain_insert_history(&mut rx);
|
||||
assert_eq!(inserted.len(), 1);
|
||||
assert!(lines_to_single_string(&inserted[0]).contains("hello"));
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "legacy-live".into(),
|
||||
msg: EventMsg::AgentMessage(AgentMessageEvent {
|
||||
message: "hello".into(),
|
||||
phase: Some(MessagePhase::FinalAnswer),
|
||||
memory_citation: None,
|
||||
}),
|
||||
});
|
||||
|
||||
assert!(drain_insert_history(&mut rx).is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn replayed_retryable_app_server_error_keeps_turn_running() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
@@ -749,26 +667,24 @@ async fn replayed_thread_closed_notification_does_not_exit_tui() {
|
||||
async fn replayed_reasoning_item_hides_raw_reasoning_when_disabled() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.config.show_raw_agent_reasoning = false;
|
||||
chat.handle_codex_event(Event {
|
||||
id: "configured".into(),
|
||||
msg: EventMsg::SessionConfigured(SessionConfiguredEvent {
|
||||
session_id: ThreadId::new(),
|
||||
forked_from_id: None,
|
||||
thread_name: None,
|
||||
model: "test-model".to_string(),
|
||||
model_provider_id: "test-provider".to_string(),
|
||||
service_tier: None,
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: ApprovalsReviewer::User,
|
||||
permission_profile: codex_protocol::models::PermissionProfile::read_only(),
|
||||
cwd: test_project_path().abs(),
|
||||
reasoning_effort: None,
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
initial_messages: None,
|
||||
network_proxy: None,
|
||||
rollout_path: None,
|
||||
}),
|
||||
chat.handle_thread_session(crate::session_state::ThreadSessionState {
|
||||
thread_id: ThreadId::new(),
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
thread_name: None,
|
||||
model: "test-model".to_string(),
|
||||
model_provider_id: "test-provider".to_string(),
|
||||
service_tier: None,
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: ApprovalsReviewer::User,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
cwd: test_project_path().abs(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: None,
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
network_proxy: None,
|
||||
rollout_path: None,
|
||||
});
|
||||
let _ = drain_insert_history(&mut rx);
|
||||
|
||||
@@ -796,26 +712,24 @@ async fn replayed_reasoning_item_hides_raw_reasoning_when_disabled() {
|
||||
async fn replayed_reasoning_item_shows_raw_reasoning_when_enabled() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.config.show_raw_agent_reasoning = true;
|
||||
chat.handle_codex_event(Event {
|
||||
id: "configured".into(),
|
||||
msg: EventMsg::SessionConfigured(SessionConfiguredEvent {
|
||||
session_id: ThreadId::new(),
|
||||
forked_from_id: None,
|
||||
thread_name: None,
|
||||
model: "test-model".to_string(),
|
||||
model_provider_id: "test-provider".to_string(),
|
||||
service_tier: None,
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: ApprovalsReviewer::User,
|
||||
permission_profile: codex_protocol::models::PermissionProfile::read_only(),
|
||||
cwd: test_project_path().abs(),
|
||||
reasoning_effort: None,
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
initial_messages: None,
|
||||
network_proxy: None,
|
||||
rollout_path: None,
|
||||
}),
|
||||
chat.handle_thread_session(crate::session_state::ThreadSessionState {
|
||||
thread_id: ThreadId::new(),
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
thread_name: None,
|
||||
model: "test-model".to_string(),
|
||||
model_provider_id: "test-provider".to_string(),
|
||||
service_tier: None,
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: ApprovalsReviewer::User,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
cwd: test_project_path().abs(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: None,
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
network_proxy: None,
|
||||
rollout_path: None,
|
||||
});
|
||||
let _ = drain_insert_history(&mut rx);
|
||||
|
||||
@@ -893,34 +807,11 @@ async fn live_reasoning_summary_is_not_rendered_twice_when_item_completes() {
|
||||
assert_eq!(rendered.matches("Summary only").count(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn replayed_turn_started_does_not_mark_task_running() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
chat.replay_initial_messages(vec![EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
})]);
|
||||
|
||||
assert!(!chat.bottom_pane.is_task_running());
|
||||
assert!(chat.bottom_pane.status_widget().is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_snapshot_replayed_turn_started_marks_task_running() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
chat.handle_codex_event_replay(Event {
|
||||
id: "turn-1".into(),
|
||||
msg: EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
}),
|
||||
});
|
||||
replay_turn_started(&mut chat, ReplayKind::ThreadSnapshot);
|
||||
|
||||
drain_insert_history(&mut rx);
|
||||
assert!(chat.bottom_pane.is_task_running());
|
||||
@@ -962,11 +853,12 @@ async fn replayed_stream_error_does_not_set_retry_status_or_status_indicator() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.set_status_header("Idle".to_string());
|
||||
|
||||
chat.replay_initial_messages(vec![EventMsg::StreamError(StreamErrorEvent {
|
||||
message: "Reconnecting... 2/5".to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::Other),
|
||||
additional_details: Some("Idle timeout waiting for SSE".to_string()),
|
||||
})]);
|
||||
handle_stream_error_with_replay(
|
||||
&mut chat,
|
||||
"Reconnecting... 2/5",
|
||||
Some("Idle timeout waiting for SSE".to_string()),
|
||||
Some(ReplayKind::ResumeInitialMessages),
|
||||
);
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert!(
|
||||
@@ -982,33 +874,18 @@ async fn replayed_stream_error_does_not_set_retry_status_or_status_indicator() {
|
||||
async fn thread_snapshot_replayed_stream_recovery_restores_previous_status_header() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
chat.handle_codex_event_replay(Event {
|
||||
id: "task".into(),
|
||||
msg: EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
}),
|
||||
});
|
||||
replay_turn_started(&mut chat, ReplayKind::ThreadSnapshot);
|
||||
drain_insert_history(&mut rx);
|
||||
|
||||
chat.handle_codex_event_replay(Event {
|
||||
id: "retry".into(),
|
||||
msg: EventMsg::StreamError(StreamErrorEvent {
|
||||
message: "Reconnecting... 1/5".to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::Other),
|
||||
additional_details: None,
|
||||
}),
|
||||
});
|
||||
handle_stream_error_with_replay(
|
||||
&mut chat,
|
||||
"Reconnecting... 1/5",
|
||||
/*additional_details*/ None,
|
||||
Some(ReplayKind::ThreadSnapshot),
|
||||
);
|
||||
drain_insert_history(&mut rx);
|
||||
|
||||
chat.handle_codex_event_replay(Event {
|
||||
id: "delta".into(),
|
||||
msg: EventMsg::AgentMessageDelta(AgentMessageDeltaEvent {
|
||||
delta: "hello".to_string(),
|
||||
}),
|
||||
});
|
||||
replay_agent_message_delta(&mut chat, "hello", ReplayKind::ThreadSnapshot);
|
||||
|
||||
let status = chat
|
||||
.bottom_pane
|
||||
@@ -1019,93 +896,18 @@ async fn thread_snapshot_replayed_stream_recovery_restores_previous_status_heade
|
||||
assert!(chat.retry_status_header.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resume_replay_interrupted_reconnect_does_not_leave_stale_working_state() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.set_status_header("Idle".to_string());
|
||||
|
||||
chat.replay_initial_messages(vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
}),
|
||||
EventMsg::StreamError(StreamErrorEvent {
|
||||
message: "Reconnecting... 1/5".to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::Other),
|
||||
additional_details: None,
|
||||
}),
|
||||
EventMsg::AgentMessageDelta(AgentMessageDeltaEvent {
|
||||
delta: "hello".to_string(),
|
||||
}),
|
||||
]);
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert!(
|
||||
cells.is_empty(),
|
||||
"expected no history cells for replayed interrupted reconnect sequence"
|
||||
);
|
||||
assert!(!chat.bottom_pane.is_task_running());
|
||||
assert!(chat.bottom_pane.status_widget().is_none());
|
||||
assert_eq!(chat.current_status.header, "Idle");
|
||||
assert!(chat.retry_status_header.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn replayed_interrupted_reconnect_footer_row_snapshot() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
chat.replay_initial_messages(vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
}),
|
||||
EventMsg::StreamError(StreamErrorEvent {
|
||||
message: "Reconnecting... 2/5".to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::Other),
|
||||
additional_details: Some("Idle timeout waiting for SSE".to_string()),
|
||||
}),
|
||||
]);
|
||||
|
||||
let header = render_bottom_first_row(&chat, /*width*/ 80);
|
||||
assert!(
|
||||
!header.contains("Reconnecting") && !header.contains("Working"),
|
||||
"expected replayed interrupted reconnect to avoid active status row, got {header:?}"
|
||||
);
|
||||
assert_chatwidget_snapshot!("replayed_interrupted_reconnect_footer_row", header);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn stream_recovery_restores_previous_status_header() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.handle_codex_event(Event {
|
||||
id: "task".into(),
|
||||
msg: EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
}),
|
||||
});
|
||||
handle_turn_started(&mut chat, "turn-1");
|
||||
drain_insert_history(&mut rx);
|
||||
chat.handle_codex_event(Event {
|
||||
id: "retry".into(),
|
||||
msg: EventMsg::StreamError(StreamErrorEvent {
|
||||
message: "Reconnecting... 1/5".to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::Other),
|
||||
additional_details: None,
|
||||
}),
|
||||
});
|
||||
handle_stream_error(
|
||||
&mut chat,
|
||||
"Reconnecting... 1/5",
|
||||
/*additional_details*/ None,
|
||||
);
|
||||
drain_insert_history(&mut rx);
|
||||
chat.handle_codex_event(Event {
|
||||
id: "delta".into(),
|
||||
msg: EventMsg::AgentMessageDelta(AgentMessageDeltaEvent {
|
||||
delta: "hello".to_string(),
|
||||
}),
|
||||
});
|
||||
handle_agent_message_delta(&mut chat, "hello");
|
||||
|
||||
let status = chat
|
||||
.bottom_pane
|
||||
|
||||
@@ -46,15 +46,7 @@ async fn mcp_startup_header_booting_snapshot() {
|
||||
async fn mcp_startup_complete_does_not_clear_running_task() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "task-1".into(),
|
||||
msg: EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
}),
|
||||
});
|
||||
handle_turn_started(&mut chat, "turn-1");
|
||||
|
||||
assert!(chat.bottom_pane.is_task_running());
|
||||
assert!(chat.bottom_pane.status_indicator_visible());
|
||||
|
||||
@@ -571,26 +571,24 @@ async fn permissions_selection_marks_auto_review_current_after_session_configure
|
||||
.features
|
||||
.set_enabled(Feature::GuardianApproval, /*enabled*/ true);
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "session-configured".to_string(),
|
||||
msg: EventMsg::SessionConfigured(SessionConfiguredEvent {
|
||||
session_id: ThreadId::new(),
|
||||
forked_from_id: None,
|
||||
thread_name: None,
|
||||
model: "gpt-test".to_string(),
|
||||
model_provider_id: "test-provider".to_string(),
|
||||
service_tier: None,
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
approvals_reviewer: ApprovalsReviewer::AutoReview,
|
||||
permission_profile: PermissionProfile::workspace_write(),
|
||||
cwd: test_project_path().abs(),
|
||||
reasoning_effort: None,
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
initial_messages: None,
|
||||
network_proxy: None,
|
||||
rollout_path: Some(PathBuf::new()),
|
||||
}),
|
||||
chat.handle_thread_session(crate::session_state::ThreadSessionState {
|
||||
thread_id: ThreadId::new(),
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
thread_name: None,
|
||||
model: "gpt-test".to_string(),
|
||||
model_provider_id: "test-provider".to_string(),
|
||||
service_tier: None,
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
approvals_reviewer: ApprovalsReviewer::AutoReview,
|
||||
permission_profile: PermissionProfile::workspace_write(),
|
||||
cwd: test_project_path().abs(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: None,
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
network_proxy: None,
|
||||
rollout_path: Some(PathBuf::new()),
|
||||
});
|
||||
|
||||
chat.open_permissions_popup();
|
||||
@@ -620,26 +618,24 @@ async fn permissions_selection_marks_auto_review_current_with_custom_workspace_w
|
||||
let cwd = test_project_path().abs();
|
||||
let permission_profile = app_server_workspace_write_profile(extra_root);
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "session-configured-custom-workspace".to_string(),
|
||||
msg: EventMsg::SessionConfigured(SessionConfiguredEvent {
|
||||
session_id: ThreadId::new(),
|
||||
forked_from_id: None,
|
||||
thread_name: None,
|
||||
model: "gpt-test".to_string(),
|
||||
model_provider_id: "test-provider".to_string(),
|
||||
service_tier: None,
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
approvals_reviewer: ApprovalsReviewer::AutoReview,
|
||||
permission_profile,
|
||||
cwd,
|
||||
reasoning_effort: None,
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
initial_messages: None,
|
||||
network_proxy: None,
|
||||
rollout_path: Some(PathBuf::new()),
|
||||
}),
|
||||
chat.handle_thread_session(crate::session_state::ThreadSessionState {
|
||||
thread_id: ThreadId::new(),
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
thread_name: None,
|
||||
model: "gpt-test".to_string(),
|
||||
model_provider_id: "test-provider".to_string(),
|
||||
service_tier: None,
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
approvals_reviewer: ApprovalsReviewer::AutoReview,
|
||||
permission_profile,
|
||||
cwd,
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: None,
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
network_proxy: None,
|
||||
rollout_path: Some(PathBuf::new()),
|
||||
});
|
||||
|
||||
chat.open_permissions_popup();
|
||||
|
||||
@@ -802,13 +802,23 @@ async fn plan_implementation_popup_skips_replayed_turn_complete() {
|
||||
.expect("expected plan collaboration mask");
|
||||
chat.set_collaboration_mask(plan_mask);
|
||||
|
||||
chat.replay_initial_messages(vec![EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
last_agent_message: Some("Plan details".to_string()),
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
time_to_first_token_ms: None,
|
||||
})]);
|
||||
chat.replay_thread_turns(
|
||||
vec![AppServerTurn {
|
||||
id: "turn-1".to_string(),
|
||||
items: vec![AppServerThreadItem::AgentMessage {
|
||||
id: "msg-plan".to_string(),
|
||||
text: "Plan details".to_string(),
|
||||
phase: Some(MessagePhase::FinalAnswer),
|
||||
memory_citation: None,
|
||||
}],
|
||||
status: AppServerTurnStatus::Completed,
|
||||
error: None,
|
||||
started_at: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
}],
|
||||
ReplayKind::ResumeInitialMessages,
|
||||
);
|
||||
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 80);
|
||||
assert!(
|
||||
@@ -829,29 +839,36 @@ async fn plan_implementation_popup_shows_once_when_replay_precedes_live_turn_com
|
||||
chat.on_plan_delta("- Step 1\n- Step 2\n".to_string());
|
||||
chat.on_plan_item_completed("- Step 1\n- Step 2\n".to_string());
|
||||
|
||||
chat.replay_initial_messages(vec![EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
last_agent_message: Some("Plan details".to_string()),
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
time_to_first_token_ms: None,
|
||||
})]);
|
||||
chat.replay_thread_turns(
|
||||
vec![AppServerTurn {
|
||||
id: "turn-1".to_string(),
|
||||
items: vec![AppServerThreadItem::AgentMessage {
|
||||
id: "msg-plan-replay".to_string(),
|
||||
text: "Plan details".to_string(),
|
||||
phase: Some(MessagePhase::FinalAnswer),
|
||||
memory_citation: None,
|
||||
}],
|
||||
status: AppServerTurnStatus::Completed,
|
||||
error: None,
|
||||
started_at: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
}],
|
||||
ReplayKind::ResumeInitialMessages,
|
||||
);
|
||||
let replay_popup = render_bottom_popup(&chat, /*width*/ 80);
|
||||
assert!(
|
||||
!replay_popup.contains(PLAN_IMPLEMENTATION_TITLE),
|
||||
"expected no prompt for replayed turn completion, got {replay_popup:?}"
|
||||
);
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "live-turn-complete-1".to_string(),
|
||||
msg: EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
last_agent_message: Some("Plan details".to_string()),
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
time_to_first_token_ms: None,
|
||||
}),
|
||||
});
|
||||
complete_assistant_message(
|
||||
&mut chat,
|
||||
"msg-plan-live-1",
|
||||
"Plan details",
|
||||
Some(MessagePhase::FinalAnswer),
|
||||
);
|
||||
handle_turn_completed(&mut chat, "live-turn-complete-1", None);
|
||||
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 80);
|
||||
assert!(
|
||||
@@ -866,16 +883,13 @@ async fn plan_implementation_popup_shows_once_when_replay_precedes_live_turn_com
|
||||
"expected prompt to dismiss on Esc, got {dismissed_popup:?}"
|
||||
);
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "live-turn-complete-2".to_string(),
|
||||
msg: EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
last_agent_message: Some("Plan details".to_string()),
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
time_to_first_token_ms: None,
|
||||
}),
|
||||
});
|
||||
complete_assistant_message(
|
||||
&mut chat,
|
||||
"msg-plan-live-2",
|
||||
"Plan details",
|
||||
Some(MessagePhase::FinalAnswer),
|
||||
);
|
||||
handle_turn_completed(&mut chat, "live-turn-complete-2", None);
|
||||
let duplicate_popup = render_bottom_popup(&chat, /*width*/ 80);
|
||||
assert!(
|
||||
!duplicate_popup.contains(PLAN_IMPLEMENTATION_TITLE),
|
||||
@@ -1137,15 +1151,13 @@ async fn submit_user_message_queues_while_compaction_turn_is_running() {
|
||||
other => panic!("expected running-turn compact steer submit, got {other:?}"),
|
||||
}
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "steer-rejected".into(),
|
||||
msg: EventMsg::Error(ErrorEvent {
|
||||
message: "cannot steer a compact turn".to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::ActiveTurnNotSteerable {
|
||||
turn_kind: NonSteerableTurnKind::Compact,
|
||||
}),
|
||||
handle_error(
|
||||
&mut chat,
|
||||
"cannot steer a compact turn",
|
||||
Some(CodexErrorInfo::ActiveTurnNotSteerable {
|
||||
turn_kind: NonSteerableTurnKind::Compact,
|
||||
}),
|
||||
});
|
||||
);
|
||||
|
||||
assert!(chat.pending_steers.is_empty());
|
||||
assert_eq!(
|
||||
@@ -1186,9 +1198,10 @@ async fn submit_user_message_emits_structured_plugin_mentions_from_bindings() {
|
||||
let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
let conversation_id = ThreadId::new();
|
||||
let rollout_file = NamedTempFile::new().unwrap();
|
||||
let configured = crate::chatwidget::test_events::SessionConfiguredEvent {
|
||||
session_id: conversation_id,
|
||||
let configured = crate::session_state::ThreadSessionState {
|
||||
thread_id: conversation_id,
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
thread_name: None,
|
||||
model: "test-model".to_string(),
|
||||
model_provider_id: "test-provider".to_string(),
|
||||
@@ -1197,17 +1210,14 @@ async fn submit_user_message_emits_structured_plugin_mentions_from_bindings() {
|
||||
approvals_reviewer: ApprovalsReviewer::User,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
initial_messages: None,
|
||||
network_proxy: None,
|
||||
rollout_path: Some(rollout_file.path().to_path_buf()),
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
id: "initial".into(),
|
||||
msg: EventMsg::SessionConfigured(configured),
|
||||
});
|
||||
chat.handle_thread_session(configured);
|
||||
chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true);
|
||||
chat.bottom_pane
|
||||
.set_plugin_mentions(Some(vec![codex_plugin::PluginCapabilitySummary {
|
||||
@@ -1431,9 +1441,10 @@ async fn plan_slash_command_with_args_submits_prompt_in_plan_mode() {
|
||||
let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.set_feature_enabled(Feature::CollaborationModes, /*enabled*/ true);
|
||||
|
||||
let configured = crate::chatwidget::test_events::SessionConfiguredEvent {
|
||||
session_id: ThreadId::new(),
|
||||
let configured = crate::session_state::ThreadSessionState {
|
||||
thread_id: ThreadId::new(),
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
thread_name: None,
|
||||
model: "test-model".to_string(),
|
||||
model_provider_id: "test-provider".to_string(),
|
||||
@@ -1442,17 +1453,14 @@ async fn plan_slash_command_with_args_submits_prompt_in_plan_mode() {
|
||||
approvals_reviewer: ApprovalsReviewer::User,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
initial_messages: None,
|
||||
network_proxy: None,
|
||||
rollout_path: None,
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
id: "configured".into(),
|
||||
msg: EventMsg::SessionConfigured(configured),
|
||||
});
|
||||
chat.handle_thread_session(configured);
|
||||
|
||||
chat.bottom_pane
|
||||
.set_composer_text("/plan build the plan".to_string(), Vec::new(), Vec::new());
|
||||
@@ -1652,10 +1660,7 @@ async fn plan_update_renders_history_cell() {
|
||||
},
|
||||
],
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
id: "sub-1".into(),
|
||||
msg: EventMsg::PlanUpdate(update),
|
||||
});
|
||||
chat.on_plan_update(update);
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert!(!cells.is_empty(), "expected plan update cell to be sent");
|
||||
let blob = lines_to_single_string(cells.last().unwrap());
|
||||
|
||||
@@ -2204,13 +2204,11 @@ async fn server_overloaded_error_does_not_switch_models() {
|
||||
while rx.try_recv().is_ok() {}
|
||||
while op_rx.try_recv().is_ok() {}
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "err-1".to_string(),
|
||||
msg: EventMsg::Error(ErrorEvent {
|
||||
message: "server overloaded".to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::ServerOverloaded),
|
||||
}),
|
||||
});
|
||||
handle_error(
|
||||
&mut chat,
|
||||
"server overloaded",
|
||||
Some(CodexErrorInfo::ServerOverloaded),
|
||||
);
|
||||
|
||||
while let Ok(event) = rx.try_recv() {
|
||||
if let AppEvent::UpdateModel(model) = event {
|
||||
|
||||
@@ -62,15 +62,7 @@ async fn interrupted_turn_restores_queued_messages_with_images_and_elements() {
|
||||
|
||||
// When interrupted, queued messages are merged into the composer; image placeholders
|
||||
// must be renumbered to match the combined local image list.
|
||||
chat.handle_codex_event(Event {
|
||||
id: "interrupt".into(),
|
||||
msg: EventMsg::TurnAborted(crate::chatwidget::test_events::TurnAbortedEvent {
|
||||
turn_id: Some("turn-1".to_string()),
|
||||
reason: TurnAbortReason::Interrupted,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
}),
|
||||
});
|
||||
handle_turn_interrupted(&mut chat, "turn-1");
|
||||
|
||||
let first = "[Image #1] first".to_string();
|
||||
let second = "[Image #2] second".to_string();
|
||||
@@ -111,10 +103,7 @@ async fn interrupted_turn_restores_queued_messages_with_images_and_elements() {
|
||||
async fn entered_review_mode_uses_request_hint() {
|
||||
let (mut chat, mut rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "review-start".into(),
|
||||
msg: EventMsg::EnteredReviewMode("feature branch".to_string()),
|
||||
});
|
||||
handle_entered_review_mode(&mut chat, "feature branch");
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
let banner = lines_to_single_string(cells.last().expect("review banner"));
|
||||
@@ -127,10 +116,7 @@ async fn entered_review_mode_uses_request_hint() {
|
||||
async fn entered_review_mode_defaults_to_current_changes_banner() {
|
||||
let (mut chat, mut rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "review-start".into(),
|
||||
msg: EventMsg::EnteredReviewMode("current changes".to_string()),
|
||||
});
|
||||
handle_entered_review_mode(&mut chat, "current changes");
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
let banner = lines_to_single_string(cells.last().expect("review banner"));
|
||||
@@ -139,13 +125,10 @@ async fn entered_review_mode_defaults_to_current_changes_banner() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn live_core_review_prompt_item_is_not_rendered() {
|
||||
async fn live_review_prompt_item_is_not_rendered() {
|
||||
let (mut chat, mut rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "review-start".into(),
|
||||
msg: EventMsg::EnteredReviewMode("changes against 'main'".to_string()),
|
||||
});
|
||||
handle_entered_review_mode(&mut chat, "changes against 'main'");
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert_eq!(cells.len(), 1);
|
||||
assert!(lines_to_single_string(&cells[0]).contains("Code review started"));
|
||||
@@ -211,19 +194,8 @@ async fn live_app_server_review_prompt_item_is_not_rendered() {
|
||||
async fn steer_rejection_queues_review_follow_up_before_existing_queued_messages() {
|
||||
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.thread_id = Some(ThreadId::new());
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-start".into(),
|
||||
msg: EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
}),
|
||||
});
|
||||
chat.handle_codex_event(Event {
|
||||
id: "review-start".into(),
|
||||
msg: EventMsg::EnteredReviewMode("feature branch".to_string()),
|
||||
});
|
||||
handle_turn_started(&mut chat, "turn-1");
|
||||
handle_entered_review_mode(&mut chat, "feature branch");
|
||||
let _ = drain_insert_history(&mut rx);
|
||||
chat.queued_user_messages
|
||||
.push_back(UserMessage::from("queued later").into());
|
||||
@@ -253,24 +225,20 @@ async fn steer_rejection_queues_review_follow_up_before_existing_queued_messages
|
||||
other => panic!("expected second running-turn steer submit, got {other:?}"),
|
||||
}
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "steer-rejected-1".into(),
|
||||
msg: EventMsg::Error(ErrorEvent {
|
||||
message: "cannot steer a review turn".to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::ActiveTurnNotSteerable {
|
||||
turn_kind: NonSteerableTurnKind::Review,
|
||||
}),
|
||||
handle_error(
|
||||
&mut chat,
|
||||
"cannot steer a review turn",
|
||||
Some(CodexErrorInfo::ActiveTurnNotSteerable {
|
||||
turn_kind: NonSteerableTurnKind::Review,
|
||||
}),
|
||||
});
|
||||
chat.handle_codex_event(Event {
|
||||
id: "steer-rejected-2".into(),
|
||||
msg: EventMsg::Error(ErrorEvent {
|
||||
message: "cannot steer a review turn".to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::ActiveTurnNotSteerable {
|
||||
turn_kind: NonSteerableTurnKind::Review,
|
||||
}),
|
||||
);
|
||||
handle_error(
|
||||
&mut chat,
|
||||
"cannot steer a review turn",
|
||||
Some(CodexErrorInfo::ActiveTurnNotSteerable {
|
||||
turn_kind: NonSteerableTurnKind::Review,
|
||||
}),
|
||||
});
|
||||
);
|
||||
|
||||
assert!(chat.pending_steers.is_empty());
|
||||
assert_eq!(
|
||||
@@ -283,20 +251,8 @@ async fn steer_rejection_queues_review_follow_up_before_existing_queued_messages
|
||||
);
|
||||
assert!(drain_insert_history(&mut rx).is_empty());
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "review-exit".into(),
|
||||
msg: EventMsg::ExitedReviewMode(ExitedReviewModeEvent),
|
||||
});
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-complete".into(),
|
||||
msg: EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
last_agent_message: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
time_to_first_token_ms: None,
|
||||
}),
|
||||
});
|
||||
handle_exited_review_mode(&mut chat);
|
||||
handle_turn_completed(&mut chat, "turn-1", None);
|
||||
|
||||
match next_submit_op(&mut op_rx) {
|
||||
Op::UserTurn { items, .. } => assert_eq!(
|
||||
@@ -309,16 +265,7 @@ async fn steer_rejection_queues_review_follow_up_before_existing_queued_messages
|
||||
other => panic!("expected merged rejected-steer follow-up submit, got {other:?}"),
|
||||
}
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-complete-2".into(),
|
||||
msg: EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-2".to_string(),
|
||||
last_agent_message: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
time_to_first_token_ms: None,
|
||||
}),
|
||||
});
|
||||
handle_turn_completed(&mut chat, "turn-1", None);
|
||||
|
||||
match next_submit_op(&mut op_rx) {
|
||||
Op::UserTurn { items, .. } => assert_eq!(
|
||||
@@ -336,20 +283,15 @@ async fn steer_rejection_queues_review_follow_up_before_existing_queued_messages
|
||||
async fn live_agent_message_renders_during_review_mode() {
|
||||
let (mut chat, mut rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "review-start".into(),
|
||||
msg: EventMsg::EnteredReviewMode("current changes".to_string()),
|
||||
});
|
||||
handle_entered_review_mode(&mut chat, "current changes");
|
||||
let _ = drain_insert_history(&mut rx);
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "review-message".into(),
|
||||
msg: EventMsg::AgentMessage(AgentMessageEvent {
|
||||
message: "Review progress update".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
}),
|
||||
});
|
||||
complete_assistant_message(
|
||||
&mut chat,
|
||||
"review-message",
|
||||
"Review progress update",
|
||||
/*phase*/ None,
|
||||
);
|
||||
|
||||
let inserted = drain_insert_history(&mut rx);
|
||||
assert_eq!(inserted.len(), 1);
|
||||
@@ -365,33 +307,21 @@ async fn review_restores_context_window_indicator() {
|
||||
let pre_review_tokens = 12_700; // ~30% remaining after subtracting baseline.
|
||||
let review_tokens = 12_030; // ~97% remaining after subtracting baseline.
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "token-before".into(),
|
||||
msg: EventMsg::TokenCount(TokenCountEvent {
|
||||
info: Some(make_token_info(pre_review_tokens, context_window)),
|
||||
rate_limits: None,
|
||||
}),
|
||||
});
|
||||
handle_token_count(
|
||||
&mut chat,
|
||||
Some(make_token_info(pre_review_tokens, context_window)),
|
||||
);
|
||||
assert_eq!(chat.bottom_pane.context_window_percent(), Some(30));
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "review-start".into(),
|
||||
msg: EventMsg::EnteredReviewMode("feature branch".to_string()),
|
||||
});
|
||||
handle_entered_review_mode(&mut chat, "feature branch");
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "token-review".into(),
|
||||
msg: EventMsg::TokenCount(TokenCountEvent {
|
||||
info: Some(make_token_info(review_tokens, context_window)),
|
||||
rate_limits: None,
|
||||
}),
|
||||
});
|
||||
handle_token_count(
|
||||
&mut chat,
|
||||
Some(make_token_info(review_tokens, context_window)),
|
||||
);
|
||||
assert_eq!(chat.bottom_pane.context_window_percent(), Some(97));
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "review-end".into(),
|
||||
msg: EventMsg::ExitedReviewMode(ExitedReviewModeEvent),
|
||||
});
|
||||
handle_exited_review_mode(&mut chat);
|
||||
let _ = drain_insert_history(&mut rx);
|
||||
|
||||
assert_eq!(chat.bottom_pane.context_window_percent(), Some(30));
|
||||
@@ -960,61 +890,6 @@ queued draft"
|
||||
assert_no_submit_op(&mut op_rx);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn replaced_turn_clears_pending_steers_but_keeps_queued_drafts() {
|
||||
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.thread_id = Some(ThreadId::new());
|
||||
chat.on_task_started();
|
||||
chat.on_agent_message_delta(
|
||||
"Final answer line
|
||||
"
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
chat.bottom_pane
|
||||
.set_composer_text("pending steer".to_string(), Vec::new(), Vec::new());
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
chat.queued_user_messages
|
||||
.push_back(UserMessage::from("queued draft".to_string()).into());
|
||||
chat.refresh_pending_input_preview();
|
||||
|
||||
match next_submit_op(&mut op_rx) {
|
||||
Op::UserTurn { items, .. } => assert_eq!(
|
||||
items,
|
||||
vec![UserInput::Text {
|
||||
text: "pending steer".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}]
|
||||
),
|
||||
other => panic!("expected Op::UserTurn, got {other:?}"),
|
||||
}
|
||||
assert!(drain_insert_history(&mut rx).is_empty());
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "replaced".into(),
|
||||
msg: EventMsg::TurnAborted(crate::chatwidget::test_events::TurnAbortedEvent {
|
||||
turn_id: Some("turn-1".to_string()),
|
||||
reason: TurnAbortReason::Replaced,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
}),
|
||||
});
|
||||
|
||||
assert!(chat.pending_steers.is_empty());
|
||||
assert!(chat.queued_user_messages.is_empty());
|
||||
assert_eq!(chat.bottom_pane.composer_text(), "");
|
||||
match next_submit_op(&mut op_rx) {
|
||||
Op::UserTurn { items, .. } => assert_eq!(
|
||||
items,
|
||||
vec![UserInput::Text {
|
||||
text: "queued draft".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}]
|
||||
),
|
||||
other => panic!("expected queued draft Op::UserTurn, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ctrl_c_shutdown_works_with_caps_lock() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
@@ -1230,15 +1105,7 @@ async fn interrupt_exec_marks_failed_snapshot() {
|
||||
|
||||
// Simulate the task being aborted (as if ESC was pressed), which should
|
||||
// cause the active exec cell to be finalized as failed and flushed.
|
||||
chat.handle_codex_event(Event {
|
||||
id: "call-int".into(),
|
||||
msg: EventMsg::TurnAborted(crate::chatwidget::test_events::TurnAbortedEvent {
|
||||
turn_id: Some("turn-1".to_string()),
|
||||
reason: TurnAbortReason::Interrupted,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
}),
|
||||
});
|
||||
handle_turn_interrupted(&mut chat, "turn-1");
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert!(
|
||||
@@ -1258,26 +1125,10 @@ async fn interrupted_turn_error_message_snapshot() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
// Simulate an in-progress task so the widget is in a running state.
|
||||
chat.handle_codex_event(Event {
|
||||
id: "task-1".into(),
|
||||
msg: EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
}),
|
||||
});
|
||||
handle_turn_started(&mut chat, "turn-1");
|
||||
|
||||
// Abort the turn (like pressing Esc) and drain inserted history.
|
||||
chat.handle_codex_event(Event {
|
||||
id: "task-1".into(),
|
||||
msg: EventMsg::TurnAborted(crate::chatwidget::test_events::TurnAbortedEvent {
|
||||
turn_id: Some("turn-1".to_string()),
|
||||
reason: TurnAbortReason::Interrupted,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
}),
|
||||
});
|
||||
handle_turn_interrupted(&mut chat, "turn-1");
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert!(
|
||||
@@ -1356,24 +1207,8 @@ async fn interrupted_turn_after_goal_budget_limited_uses_budget_message_snapshot
|
||||
async fn direct_budget_limited_turn_uses_budget_message_snapshot() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "task-1".into(),
|
||||
msg: EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
}),
|
||||
});
|
||||
chat.handle_codex_event(Event {
|
||||
id: "task-1".into(),
|
||||
msg: EventMsg::TurnAborted(crate::chatwidget::test_events::TurnAbortedEvent {
|
||||
turn_id: Some("turn-1".to_string()),
|
||||
reason: TurnAbortReason::BudgetLimited,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
}),
|
||||
});
|
||||
handle_turn_started(&mut chat, "turn-1");
|
||||
handle_budget_limited_turn(&mut chat, "turn-1");
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
let last = lines_to_single_string(cells.last().unwrap());
|
||||
@@ -1387,24 +1222,8 @@ async fn budget_limited_turn_restores_queued_input_without_submitting() {
|
||||
.push_back(UserMessage::from("follow-up after budget stop").into());
|
||||
chat.refresh_pending_input_preview();
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "task-1".into(),
|
||||
msg: EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
}),
|
||||
});
|
||||
chat.handle_codex_event(Event {
|
||||
id: "task-1".into(),
|
||||
msg: EventMsg::TurnAborted(crate::chatwidget::test_events::TurnAbortedEvent {
|
||||
turn_id: Some("turn-1".to_string()),
|
||||
reason: TurnAbortReason::BudgetLimited,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
}),
|
||||
});
|
||||
handle_turn_started(&mut chat, "turn-1");
|
||||
handle_budget_limited_turn(&mut chat, "turn-1");
|
||||
|
||||
assert!(chat.queued_user_messages.is_empty());
|
||||
assert_eq!(
|
||||
@@ -1424,25 +1243,9 @@ async fn interrupted_turn_pending_steers_message_snapshot() {
|
||||
chat.pending_steers.push_back(pending_steer("steer 1"));
|
||||
chat.submit_pending_steers_after_interrupt = true;
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "task-1".into(),
|
||||
msg: EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
}),
|
||||
});
|
||||
handle_turn_started(&mut chat, "turn-1");
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "task-1".into(),
|
||||
msg: EventMsg::TurnAborted(crate::chatwidget::test_events::TurnAbortedEvent {
|
||||
turn_id: Some("turn-1".to_string()),
|
||||
reason: TurnAbortReason::Interrupted,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
}),
|
||||
});
|
||||
handle_turn_interrupted(&mut chat, "turn-1");
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
let info = cells
|
||||
@@ -1524,63 +1327,13 @@ async fn review_branch_picker_escape_navigates_back_then_dismisses() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn review_ended_keeps_unified_exec_processes() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
begin_unified_exec_startup(&mut chat, "call-1", "process-1", "sleep 5");
|
||||
begin_unified_exec_startup(&mut chat, "call-2", "process-2", "sleep 6");
|
||||
assert_eq!(chat.unified_exec_processes.len(), 2);
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-1".into(),
|
||||
msg: EventMsg::TurnAborted(crate::chatwidget::test_events::TurnAbortedEvent {
|
||||
turn_id: Some("turn-1".to_string()),
|
||||
reason: TurnAbortReason::ReviewEnded,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
}),
|
||||
});
|
||||
|
||||
assert_eq!(chat.unified_exec_processes.len(), 2);
|
||||
|
||||
chat.add_ps_output();
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
let combined = cells
|
||||
.iter()
|
||||
.map(|lines| lines_to_single_string(lines))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
assert!(
|
||||
combined.contains("Background terminals"),
|
||||
"expected /ps to remain available after review-ended abort; got {combined:?}"
|
||||
);
|
||||
assert!(
|
||||
combined.contains("sleep 5") && combined.contains("sleep 6"),
|
||||
"expected /ps to list running unified exec processes; got {combined:?}"
|
||||
);
|
||||
|
||||
let _ = drain_insert_history(&mut rx);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn enter_submits_steer_while_review_is_running() {
|
||||
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.thread_id = Some(ThreadId::new());
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-start".into(),
|
||||
msg: EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
}),
|
||||
});
|
||||
handle_turn_started(&mut chat, "turn-1");
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "review-1".into(),
|
||||
msg: EventMsg::EnteredReviewMode("current changes".to_string()),
|
||||
});
|
||||
handle_entered_review_mode(&mut chat, "current changes");
|
||||
let _ = drain_insert_history(&mut rx);
|
||||
|
||||
chat.bottom_pane.set_composer_text(
|
||||
@@ -1613,34 +1366,21 @@ async fn enter_submits_steer_while_review_is_running() {
|
||||
async fn review_queues_user_messages_snapshot() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.thread_id = Some(ThreadId::new());
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-start".into(),
|
||||
msg: EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
}),
|
||||
});
|
||||
handle_turn_started(&mut chat, "turn-1");
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "review-1".into(),
|
||||
msg: EventMsg::EnteredReviewMode("current changes".to_string()),
|
||||
});
|
||||
handle_entered_review_mode(&mut chat, "current changes");
|
||||
let _ = drain_insert_history(&mut rx);
|
||||
|
||||
chat.submit_user_message(UserMessage::from(
|
||||
"Steer submitted while /review was running.".to_string(),
|
||||
));
|
||||
chat.handle_codex_event(Event {
|
||||
id: "steer-rejected".into(),
|
||||
msg: EventMsg::Error(ErrorEvent {
|
||||
message: "cannot steer a review turn".to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::ActiveTurnNotSteerable {
|
||||
turn_kind: NonSteerableTurnKind::Review,
|
||||
}),
|
||||
handle_error(
|
||||
&mut chat,
|
||||
"cannot steer a review turn",
|
||||
Some(CodexErrorInfo::ActiveTurnNotSteerable {
|
||||
turn_kind: NonSteerableTurnKind::Review,
|
||||
}),
|
||||
});
|
||||
);
|
||||
|
||||
let width: u16 = 80;
|
||||
let height: u16 = 18;
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
fn turn_complete_event(turn_id: &str, last_agent_message: Option<&str>) -> TurnCompleteEvent {
|
||||
serde_json::from_value(serde_json::json!({
|
||||
"turn_id": turn_id,
|
||||
"last_agent_message": last_agent_message,
|
||||
}))
|
||||
.expect("turn complete event should deserialize")
|
||||
fn complete_turn_with_message(chat: &mut ChatWidget, turn_id: &str, message: Option<&str>) {
|
||||
if let Some(message) = message {
|
||||
complete_assistant_message(
|
||||
chat,
|
||||
&format!("{turn_id}-message"),
|
||||
message,
|
||||
Some(MessagePhase::FinalAnswer),
|
||||
);
|
||||
}
|
||||
handle_turn_completed(chat, turn_id, /*duration_ms*/ None);
|
||||
}
|
||||
|
||||
fn submit_composer_text(chat: &mut ChatWidget, text: &str) {
|
||||
@@ -75,15 +79,7 @@ async fn slash_compact_eagerly_queues_follow_up_before_turn_start() {
|
||||
async fn queued_slash_compact_dispatches_after_active_turn() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.thread_id = Some(ThreadId::new());
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-start".into(),
|
||||
msg: EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
}),
|
||||
});
|
||||
handle_turn_started(&mut chat, "turn-1");
|
||||
|
||||
queue_composer_text_with_tab(&mut chat, "/compact");
|
||||
|
||||
@@ -94,10 +90,7 @@ async fn queued_slash_compact_dispatches_after_active_turn() {
|
||||
);
|
||||
assert_matches!(rx.try_recv(), Err(TryRecvError::Empty));
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-complete".into(),
|
||||
msg: EventMsg::TurnComplete(turn_complete_event("turn-1", Some("done"))),
|
||||
});
|
||||
complete_turn_with_message(&mut chat, "turn-1", Some("done"));
|
||||
|
||||
let events = std::iter::from_fn(|| rx.try_recv().ok()).collect::<Vec<_>>();
|
||||
assert!(
|
||||
@@ -112,22 +105,11 @@ async fn queued_slash_compact_dispatches_after_active_turn() {
|
||||
async fn queued_slash_review_with_args_dispatches_after_active_turn() {
|
||||
let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.thread_id = Some(ThreadId::new());
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-start".into(),
|
||||
msg: EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
}),
|
||||
});
|
||||
handle_turn_started(&mut chat, "turn-1");
|
||||
|
||||
queue_composer_text_with_tab(&mut chat, "/review check regressions");
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-complete".into(),
|
||||
msg: EventMsg::TurnComplete(turn_complete_event("turn-1", Some("done"))),
|
||||
});
|
||||
complete_turn_with_message(&mut chat, "turn-1", Some("done"));
|
||||
|
||||
match op_rx.try_recv() {
|
||||
Ok(Op::AddToHistory { .. }) => match op_rx.try_recv() {
|
||||
@@ -153,15 +135,7 @@ async fn queued_slash_review_with_args_dispatches_after_active_turn() {
|
||||
async fn queued_slash_review_with_args_restores_for_edit() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.thread_id = Some(ThreadId::new());
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-start".into(),
|
||||
msg: EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
}),
|
||||
});
|
||||
handle_turn_started(&mut chat, "turn-1");
|
||||
|
||||
queue_composer_text_with_tab(&mut chat, "/review check regressions");
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Up, KeyModifiers::ALT));
|
||||
@@ -176,15 +150,7 @@ async fn queued_slash_review_with_args_restores_for_edit() {
|
||||
async fn queued_bang_shell_dispatches_after_active_turn() {
|
||||
let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.thread_id = Some(ThreadId::new());
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-start".into(),
|
||||
msg: EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
}),
|
||||
});
|
||||
handle_turn_started(&mut chat, "turn-1");
|
||||
|
||||
queue_composer_text_with_tab(&mut chat, "!echo hi");
|
||||
|
||||
@@ -195,10 +161,7 @@ async fn queued_bang_shell_dispatches_after_active_turn() {
|
||||
);
|
||||
assert_matches!(op_rx.try_recv(), Err(TryRecvError::Empty));
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-complete".into(),
|
||||
msg: EventMsg::TurnComplete(turn_complete_event("turn-1", Some("done"))),
|
||||
});
|
||||
complete_turn_with_message(&mut chat, "turn-1", Some("done"));
|
||||
|
||||
match op_rx.try_recv() {
|
||||
Ok(Op::RunUserShellCommand { command }) => assert_eq!(command, "echo hi"),
|
||||
@@ -215,25 +178,14 @@ async fn queued_bang_shell_dispatches_after_active_turn() {
|
||||
async fn queued_empty_bang_shell_reports_help_when_dequeued_and_drains_next_input() {
|
||||
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.thread_id = Some(ThreadId::new());
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-start".into(),
|
||||
msg: EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
}),
|
||||
});
|
||||
handle_turn_started(&mut chat, "turn-1");
|
||||
|
||||
queue_composer_text_with_tab(&mut chat, "!");
|
||||
queue_composer_text_with_tab(&mut chat, "hello after help");
|
||||
|
||||
assert!(drain_insert_history(&mut rx).is_empty());
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-complete".into(),
|
||||
msg: EventMsg::TurnComplete(turn_complete_event("turn-1", Some("done"))),
|
||||
});
|
||||
complete_turn_with_message(&mut chat, "turn-1", Some("done"));
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
let rendered = cells
|
||||
@@ -263,23 +215,12 @@ async fn queued_empty_bang_shell_reports_help_when_dequeued_and_drains_next_inpu
|
||||
async fn queued_bang_shell_waits_for_user_shell_completion_before_next_input() {
|
||||
let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.thread_id = Some(ThreadId::new());
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-start".into(),
|
||||
msg: EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
}),
|
||||
});
|
||||
handle_turn_started(&mut chat, "turn-1");
|
||||
|
||||
queue_composer_text_with_tab(&mut chat, "!echo hi");
|
||||
queue_composer_text_with_tab(&mut chat, "hello after shell");
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-complete".into(),
|
||||
msg: EventMsg::TurnComplete(turn_complete_event("turn-1", Some("done"))),
|
||||
});
|
||||
complete_turn_with_message(&mut chat, "turn-1", Some("done"));
|
||||
|
||||
match op_rx.try_recv() {
|
||||
Ok(Op::RunUserShellCommand { command }) => assert_eq!(command, "echo hi"),
|
||||
@@ -315,23 +256,12 @@ async fn queued_bang_shell_waits_for_user_shell_completion_before_next_input() {
|
||||
async fn assert_cancelled_queued_menu_drains_next_input(command: &str, expected_popup_text: &str) {
|
||||
let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(Some("gpt-5.2")).await;
|
||||
chat.thread_id = Some(ThreadId::new());
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-start".into(),
|
||||
msg: EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
}),
|
||||
});
|
||||
handle_turn_started(&mut chat, "turn-1");
|
||||
|
||||
queue_composer_text_with_tab(&mut chat, command);
|
||||
queue_composer_text_with_tab(&mut chat, "hello after menu");
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-complete".into(),
|
||||
msg: EventMsg::TurnComplete(turn_complete_event("turn-1", Some("done"))),
|
||||
});
|
||||
complete_turn_with_message(&mut chat, "turn-1", Some("done"));
|
||||
|
||||
assert_eq!(chat.queued_user_messages.len(), 1);
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 80);
|
||||
@@ -367,23 +297,12 @@ async fn queued_slash_menu_cancel_drains_next_input() {
|
||||
async fn queued_slash_menu_selection_drains_next_input() {
|
||||
let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(Some("gpt-5.2")).await;
|
||||
chat.thread_id = Some(ThreadId::new());
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-start".into(),
|
||||
msg: EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
}),
|
||||
});
|
||||
handle_turn_started(&mut chat, "turn-1");
|
||||
|
||||
queue_composer_text_with_tab(&mut chat, "/permissions");
|
||||
queue_composer_text_with_tab(&mut chat, "hello after selection");
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-complete".into(),
|
||||
msg: EventMsg::TurnComplete(turn_complete_event("turn-1", Some("done"))),
|
||||
});
|
||||
complete_turn_with_message(&mut chat, "turn-1", Some("done"));
|
||||
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 80);
|
||||
assert!(
|
||||
@@ -411,23 +330,12 @@ async fn queued_bare_rename_drains_next_input_after_name_update() {
|
||||
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
let thread_id = ThreadId::new();
|
||||
chat.thread_id = Some(thread_id);
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-start".into(),
|
||||
msg: EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
}),
|
||||
});
|
||||
handle_turn_started(&mut chat, "turn-1");
|
||||
|
||||
queue_composer_text_with_tab(&mut chat, "/rename");
|
||||
queue_composer_text_with_tab(&mut chat, "hello after rename");
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-complete".into(),
|
||||
msg: EventMsg::TurnComplete(turn_complete_event("turn-1", Some("done"))),
|
||||
});
|
||||
complete_turn_with_message(&mut chat, "turn-1", Some("done"));
|
||||
|
||||
assert_eq!(chat.queued_user_messages.len(), 1);
|
||||
assert!(render_bottom_popup(&chat, /*width*/ 80).contains("Name thread"));
|
||||
@@ -445,13 +353,15 @@ async fn queued_bare_rename_drains_next_input_after_name_update() {
|
||||
"expected rename prompt to submit thread name; events: {events:?}"
|
||||
);
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "rename".into(),
|
||||
msg: EventMsg::ThreadNameUpdated(crate::chatwidget::test_events::ThreadNameUpdatedEvent {
|
||||
thread_id,
|
||||
thread_name: Some("Queued rename".to_string()),
|
||||
}),
|
||||
});
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::ThreadNameUpdated(
|
||||
codex_app_server_protocol::ThreadNameUpdatedNotification {
|
||||
thread_id: thread_id.to_string(),
|
||||
thread_name: Some("Queued rename".to_string()),
|
||||
},
|
||||
),
|
||||
/*replay_kind*/ None,
|
||||
);
|
||||
|
||||
match next_submit_op(&mut op_rx) {
|
||||
Op::UserTurn { items, .. } => assert_eq!(
|
||||
@@ -471,24 +381,13 @@ async fn queued_inline_rename_does_not_drain_again_before_turn_started() {
|
||||
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
let thread_id = ThreadId::new();
|
||||
chat.thread_id = Some(thread_id);
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-start".into(),
|
||||
msg: EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
}),
|
||||
});
|
||||
handle_turn_started(&mut chat, "turn-1");
|
||||
|
||||
queue_composer_text_with_tab(&mut chat, "/rename Queued rename");
|
||||
queue_composer_text_with_tab(&mut chat, "first after rename");
|
||||
queue_composer_text_with_tab(&mut chat, "second after rename");
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-complete".into(),
|
||||
msg: EventMsg::TurnComplete(turn_complete_event("turn-1", Some("done"))),
|
||||
});
|
||||
complete_turn_with_message(&mut chat, "turn-1", Some("done"));
|
||||
|
||||
let events = std::iter::from_fn(|| rx.try_recv().ok()).collect::<Vec<_>>();
|
||||
assert!(
|
||||
@@ -528,13 +427,15 @@ async fn queued_inline_rename_does_not_drain_again_before_turn_started() {
|
||||
vec!["second after rename"]
|
||||
);
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "rename".into(),
|
||||
msg: EventMsg::ThreadNameUpdated(crate::chatwidget::test_events::ThreadNameUpdatedEvent {
|
||||
thread_id,
|
||||
thread_name: Some("Queued rename".to_string()),
|
||||
}),
|
||||
});
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::ThreadNameUpdated(
|
||||
codex_app_server_protocol::ThreadNameUpdatedNotification {
|
||||
thread_id: thread_id.to_string(),
|
||||
thread_name: Some("Queued rename".to_string()),
|
||||
},
|
||||
),
|
||||
/*replay_kind*/ None,
|
||||
);
|
||||
|
||||
assert_matches!(op_rx.try_recv(), Err(TryRecvError::Empty));
|
||||
assert_eq!(
|
||||
@@ -542,19 +443,8 @@ async fn queued_inline_rename_does_not_drain_again_before_turn_started() {
|
||||
vec!["second after rename"]
|
||||
);
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-2-start".into(),
|
||||
msg: EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-2".to_string(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
}),
|
||||
});
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-2-complete".into(),
|
||||
msg: EventMsg::TurnComplete(turn_complete_event("turn-2", Some("done"))),
|
||||
});
|
||||
handle_turn_started(&mut chat, "turn-2");
|
||||
complete_turn_with_message(&mut chat, "turn-2", Some("done"));
|
||||
|
||||
match next_submit_op(&mut op_rx) {
|
||||
Op::UserTurn { items, .. } => assert_eq!(
|
||||
@@ -573,24 +463,13 @@ async fn queued_inline_rename_does_not_drain_again_before_turn_started() {
|
||||
async fn queued_unknown_slash_reports_error_when_dequeued() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.thread_id = Some(ThreadId::new());
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-start".into(),
|
||||
msg: EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
}),
|
||||
});
|
||||
handle_turn_started(&mut chat, "turn-1");
|
||||
|
||||
queue_composer_text_with_tab(&mut chat, "/does-not-exist");
|
||||
|
||||
assert!(drain_insert_history(&mut rx).is_empty());
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-complete".into(),
|
||||
msg: EventMsg::TurnComplete(turn_complete_event("turn-1", Some("done"))),
|
||||
});
|
||||
complete_turn_with_message(&mut chat, "turn-1", Some("done"));
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
let rendered = cells
|
||||
@@ -1253,16 +1132,7 @@ async fn slash_logout_requests_app_server_logout() {
|
||||
async fn slash_copy_state_tracks_turn_complete_final_reply() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-1".into(),
|
||||
msg: EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
last_agent_message: Some("Final reply **markdown**".to_string()),
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
time_to_first_token_ms: None,
|
||||
}),
|
||||
});
|
||||
complete_turn_with_message(&mut chat, "turn-1", Some("Final reply **markdown**"));
|
||||
|
||||
assert_eq!(
|
||||
chat.last_agent_markdown_text(),
|
||||
@@ -1275,27 +1145,18 @@ async fn slash_copy_state_tracks_plan_item_completion() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
let plan_text = "## Plan\n\n1. Build it\n2. Test it".to_string();
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "item-plan".into(),
|
||||
msg: EventMsg::ItemCompleted(ItemCompletedEvent {
|
||||
thread_id: ThreadId::new(),
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::ItemCompleted(ItemCompletedNotification {
|
||||
thread_id: String::new(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
item: TurnItem::Plan(PlanItem {
|
||||
item: AppServerThreadItem::Plan {
|
||||
id: "plan-1".to_string(),
|
||||
text: plan_text.clone(),
|
||||
}),
|
||||
},
|
||||
}),
|
||||
});
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-1".into(),
|
||||
msg: EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
last_agent_message: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
time_to_first_token_ms: None,
|
||||
}),
|
||||
});
|
||||
/*replay_kind*/ None,
|
||||
);
|
||||
handle_turn_completed(&mut chat, "turn-1", None);
|
||||
|
||||
assert_eq!(chat.last_agent_markdown_text(), Some(plan_text.as_str()));
|
||||
assert_matches!(
|
||||
@@ -1432,16 +1293,7 @@ async fn slash_copy_stores_clipboard_lease_and_preserves_it_on_failure() {
|
||||
async fn slash_copy_state_is_preserved_during_running_task() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-1".into(),
|
||||
msg: EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
last_agent_message: Some("Previous completed reply".to_string()),
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
time_to_first_token_ms: None,
|
||||
}),
|
||||
});
|
||||
complete_turn_with_message(&mut chat, "turn-1", Some("Previous completed reply"));
|
||||
chat.on_task_started();
|
||||
|
||||
assert_eq!(
|
||||
@@ -1450,50 +1302,11 @@ async fn slash_copy_state_is_preserved_during_running_task() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn slash_copy_tracks_replayed_legacy_agent_message_when_turn_complete_omits_text() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
chat.handle_codex_event_replay(Event {
|
||||
id: "turn-1".into(),
|
||||
msg: EventMsg::AgentMessage(AgentMessageEvent {
|
||||
message: "Legacy final message".into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
}),
|
||||
});
|
||||
let _ = drain_insert_history(&mut rx);
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-1".into(),
|
||||
msg: EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
last_agent_message: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
time_to_first_token_ms: None,
|
||||
}),
|
||||
});
|
||||
let _ = drain_insert_history(&mut rx);
|
||||
|
||||
assert_eq!(
|
||||
chat.last_agent_markdown_text(),
|
||||
Some("Legacy final message")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn slash_copy_uses_agent_message_item_when_turn_complete_omits_final_text() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-1".into(),
|
||||
msg: EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
}),
|
||||
});
|
||||
handle_turn_started(&mut chat, "turn-1");
|
||||
complete_assistant_message(
|
||||
&mut chat,
|
||||
"msg-1",
|
||||
@@ -1501,16 +1314,7 @@ async fn slash_copy_uses_agent_message_item_when_turn_complete_omits_final_text(
|
||||
/*phase*/ None,
|
||||
);
|
||||
let _ = drain_insert_history(&mut rx);
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-1".into(),
|
||||
msg: EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
last_agent_message: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
time_to_first_token_ms: None,
|
||||
}),
|
||||
});
|
||||
handle_turn_completed(&mut chat, "turn-1", None);
|
||||
let _ = drain_insert_history(&mut rx);
|
||||
|
||||
assert_eq!(
|
||||
@@ -1527,18 +1331,10 @@ async fn slash_copy_uses_agent_message_item_when_turn_complete_omits_final_text(
|
||||
async fn agent_turn_complete_notification_does_not_reuse_stale_copy_source() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-1".into(),
|
||||
msg: EventMsg::TurnComplete(turn_complete_event("turn-1", Some("Previous reply"))),
|
||||
});
|
||||
complete_turn_with_message(&mut chat, "turn-1", Some("Previous reply"));
|
||||
chat.pending_notification = None;
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-2".into(),
|
||||
msg: EventMsg::TurnComplete(turn_complete_event(
|
||||
"turn-2", /*last_agent_message*/ None,
|
||||
)),
|
||||
});
|
||||
handle_turn_completed(&mut chat, "turn-2", None);
|
||||
|
||||
assert_matches!(
|
||||
chat.pending_notification,
|
||||
@@ -1570,10 +1366,7 @@ async fn active_goal_without_follow_up_suppresses_agent_turn_complete_notificati
|
||||
/*replay_kind*/ None,
|
||||
);
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-1".into(),
|
||||
msg: EventMsg::TurnComplete(turn_complete_event("turn-1", Some("Still working"))),
|
||||
});
|
||||
complete_turn_with_message(&mut chat, "turn-1", Some("Still working"));
|
||||
|
||||
assert_matches!(chat.pending_notification, None);
|
||||
}
|
||||
@@ -1582,21 +1375,10 @@ async fn active_goal_without_follow_up_suppresses_agent_turn_complete_notificati
|
||||
async fn queued_follow_up_suppresses_agent_turn_complete_notification() {
|
||||
let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.thread_id = Some(ThreadId::new());
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-1".into(),
|
||||
msg: EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
}),
|
||||
});
|
||||
handle_turn_started(&mut chat, "turn-1");
|
||||
chat.queue_user_message("Continue".into());
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-1".into(),
|
||||
msg: EventMsg::TurnComplete(turn_complete_event("turn-1", Some("Still working"))),
|
||||
});
|
||||
complete_turn_with_message(&mut chat, "turn-1", Some("Still working"));
|
||||
|
||||
assert_matches!(chat.pending_notification, None);
|
||||
assert!(chat.queued_user_messages.is_empty());
|
||||
@@ -1607,21 +1389,10 @@ async fn queued_follow_up_suppresses_agent_turn_complete_notification() {
|
||||
async fn queued_menu_slash_keeps_agent_turn_complete_notification() {
|
||||
let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(Some("gpt-5.2")).await;
|
||||
chat.thread_id = Some(ThreadId::new());
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-1".into(),
|
||||
msg: EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
}),
|
||||
});
|
||||
handle_turn_started(&mut chat, "turn-1");
|
||||
queue_composer_text_with_tab(&mut chat, "/model");
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-1".into(),
|
||||
msg: EventMsg::TurnComplete(turn_complete_event("turn-1", Some("Done"))),
|
||||
});
|
||||
complete_turn_with_message(&mut chat, "turn-1", Some("Done"));
|
||||
|
||||
assert_matches!(
|
||||
chat.pending_notification,
|
||||
@@ -1635,40 +1406,20 @@ async fn queued_menu_slash_keeps_agent_turn_complete_notification() {
|
||||
async fn slash_copy_uses_latest_surviving_response_after_rollback() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
chat.handle_codex_event_replay(Event {
|
||||
id: "user-1".into(),
|
||||
msg: EventMsg::UserMessage(UserMessageEvent {
|
||||
message: "foo".to_string(),
|
||||
images: None,
|
||||
local_images: Vec::new(),
|
||||
text_elements: Vec::new(),
|
||||
}),
|
||||
});
|
||||
chat.handle_codex_event_replay(Event {
|
||||
id: "agent-1".into(),
|
||||
msg: EventMsg::AgentMessage(AgentMessageEvent {
|
||||
message: "foo response".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
}),
|
||||
});
|
||||
chat.handle_codex_event_replay(Event {
|
||||
id: "user-2".into(),
|
||||
msg: EventMsg::UserMessage(UserMessageEvent {
|
||||
message: "bar".to_string(),
|
||||
images: None,
|
||||
local_images: Vec::new(),
|
||||
text_elements: Vec::new(),
|
||||
}),
|
||||
});
|
||||
chat.handle_codex_event_replay(Event {
|
||||
id: "agent-2".into(),
|
||||
msg: EventMsg::AgentMessage(AgentMessageEvent {
|
||||
message: "bar response".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
}),
|
||||
});
|
||||
replay_user_message_text(&mut chat, "user-1", "foo", ReplayKind::ThreadSnapshot);
|
||||
replay_agent_message(
|
||||
&mut chat,
|
||||
"agent-1",
|
||||
"foo response",
|
||||
ReplayKind::ThreadSnapshot,
|
||||
);
|
||||
replay_user_message_text(&mut chat, "user-2", "bar", ReplayKind::ThreadSnapshot);
|
||||
replay_agent_message(
|
||||
&mut chat,
|
||||
"agent-2",
|
||||
"bar response",
|
||||
ReplayKind::ThreadSnapshot,
|
||||
);
|
||||
let _ = drain_insert_history(&mut rx);
|
||||
assert_eq!(chat.last_agent_markdown_text(), Some("bar response"));
|
||||
|
||||
@@ -1685,23 +1436,13 @@ async fn slash_copy_uses_latest_surviving_response_after_rollback() {
|
||||
async fn slash_copy_reports_when_rewind_exceeds_retained_copy_history() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
chat.handle_codex_event_replay(Event {
|
||||
id: "user-1".into(),
|
||||
msg: EventMsg::UserMessage(UserMessageEvent {
|
||||
message: "foo".to_string(),
|
||||
images: None,
|
||||
local_images: Vec::new(),
|
||||
text_elements: Vec::new(),
|
||||
}),
|
||||
});
|
||||
chat.handle_codex_event_replay(Event {
|
||||
id: "agent-1".into(),
|
||||
msg: EventMsg::AgentMessage(AgentMessageEvent {
|
||||
message: "foo response".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
}),
|
||||
});
|
||||
replay_user_message_text(&mut chat, "user-1", "foo", ReplayKind::ThreadSnapshot);
|
||||
replay_agent_message(
|
||||
&mut chat,
|
||||
"agent-1",
|
||||
"foo response",
|
||||
ReplayKind::ThreadSnapshot,
|
||||
);
|
||||
let _ = drain_insert_history(&mut rx);
|
||||
|
||||
chat.truncate_agent_copy_history_to_user_turn_count(/*user_turn_count*/ 0);
|
||||
@@ -1948,97 +1689,6 @@ async fn slash_rollout_handles_missing_path() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn undo_success_events_render_info_messages() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-1".to_string(),
|
||||
msg: EventMsg::UndoStarted(UndoStartedEvent {
|
||||
message: Some("Undo requested for the last turn...".to_string()),
|
||||
}),
|
||||
});
|
||||
assert!(
|
||||
chat.bottom_pane.status_indicator_visible(),
|
||||
"status indicator should be visible during undo"
|
||||
);
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-1".to_string(),
|
||||
msg: EventMsg::UndoCompleted(UndoCompletedEvent {
|
||||
success: true,
|
||||
message: None,
|
||||
}),
|
||||
});
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert_eq!(cells.len(), 1, "expected final status only");
|
||||
assert!(
|
||||
!chat.bottom_pane.status_indicator_visible(),
|
||||
"status indicator should be hidden after successful undo"
|
||||
);
|
||||
|
||||
let completed = lines_to_single_string(&cells[0]);
|
||||
assert!(
|
||||
completed.contains("Undo completed successfully."),
|
||||
"expected default success message, got {completed:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn undo_failure_events_render_error_message() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-2".to_string(),
|
||||
msg: EventMsg::UndoStarted(UndoStartedEvent { message: None }),
|
||||
});
|
||||
assert!(
|
||||
chat.bottom_pane.status_indicator_visible(),
|
||||
"status indicator should be visible during undo"
|
||||
);
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-2".to_string(),
|
||||
msg: EventMsg::UndoCompleted(UndoCompletedEvent {
|
||||
success: false,
|
||||
message: Some("Failed to restore workspace state.".to_string()),
|
||||
}),
|
||||
});
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert_eq!(cells.len(), 1, "expected final status only");
|
||||
assert!(
|
||||
!chat.bottom_pane.status_indicator_visible(),
|
||||
"status indicator should be hidden after failed undo"
|
||||
);
|
||||
|
||||
let completed = lines_to_single_string(&cells[0]);
|
||||
assert!(
|
||||
completed.contains("Failed to restore workspace state."),
|
||||
"expected failure message, got {completed:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn undo_started_hides_interrupt_hint() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-hint".to_string(),
|
||||
msg: EventMsg::UndoStarted(UndoStartedEvent { message: None }),
|
||||
});
|
||||
|
||||
let status = chat
|
||||
.bottom_pane
|
||||
.status_widget()
|
||||
.expect("status indicator should be active");
|
||||
assert!(
|
||||
!status.interrupt_hint_visible(),
|
||||
"undo should hide the interrupt hint because the operation cannot be cancelled"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fast_slash_command_updates_and_persists_local_service_tier() {
|
||||
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(Some("gpt-5.3-codex")).await;
|
||||
@@ -2100,23 +1750,12 @@ async fn queued_fast_slash_applies_before_next_queued_message() {
|
||||
chat.thread_id = Some(ThreadId::new());
|
||||
set_chatgpt_auth(&mut chat);
|
||||
chat.set_feature_enabled(Feature::FastMode, /*enabled*/ true);
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-start".into(),
|
||||
msg: EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
}),
|
||||
});
|
||||
handle_turn_started(&mut chat, "turn-1");
|
||||
|
||||
queue_composer_text_with_tab(&mut chat, "/fast on");
|
||||
queue_composer_text_with_tab(&mut chat, "hello after fast");
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-complete".into(),
|
||||
msg: EventMsg::TurnComplete(turn_complete_event("turn-1", Some("done"))),
|
||||
});
|
||||
complete_turn_with_message(&mut chat, "turn-1", Some("done"));
|
||||
|
||||
let events = std::iter::from_fn(|| rx.try_recv().ok()).collect::<Vec<_>>();
|
||||
assert!(
|
||||
@@ -2193,28 +1832,18 @@ async fn user_turn_sends_standard_override_after_fast_is_turned_off() {
|
||||
async fn compact_queues_user_messages_snapshot() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.thread_id = Some(ThreadId::new());
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-start".into(),
|
||||
msg: EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: ModeKind::Default,
|
||||
}),
|
||||
});
|
||||
handle_turn_started(&mut chat, "turn-1");
|
||||
|
||||
chat.submit_user_message(UserMessage::from(
|
||||
"Steer submitted while /compact was running.".to_string(),
|
||||
));
|
||||
chat.handle_codex_event(Event {
|
||||
id: "steer-rejected".into(),
|
||||
msg: EventMsg::Error(ErrorEvent {
|
||||
message: "cannot steer a compact turn".to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::ActiveTurnNotSteerable {
|
||||
turn_kind: NonSteerableTurnKind::Compact,
|
||||
}),
|
||||
handle_error(
|
||||
&mut chat,
|
||||
"cannot steer a compact turn",
|
||||
Some(CodexErrorInfo::ActiveTurnNotSteerable {
|
||||
turn_kind: NonSteerableTurnKind::Compact,
|
||||
}),
|
||||
});
|
||||
);
|
||||
|
||||
let width: u16 = 80;
|
||||
let height: u16 = 18;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -23,10 +23,7 @@ async fn terminal_title_shows_action_required_while_exec_approval_is_pending() {
|
||||
available_decisions: None,
|
||||
parsed_cmd: vec![],
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
id: "sub-action-required".into(),
|
||||
msg: EventMsg::ExecApprovalRequest(request),
|
||||
});
|
||||
handle_exec_approval_request(&mut chat, "sub-action-required", request);
|
||||
|
||||
chat.pre_draw_tick();
|
||||
|
||||
@@ -69,10 +66,7 @@ async fn terminal_title_action_required_respects_spinner_setting() {
|
||||
available_decisions: None,
|
||||
parsed_cmd: vec![],
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
id: "sub-no-spinner".into(),
|
||||
msg: EventMsg::ExecApprovalRequest(request),
|
||||
});
|
||||
handle_exec_approval_request(&mut chat, "sub-no-spinner", request);
|
||||
|
||||
chat.pre_draw_tick();
|
||||
|
||||
@@ -101,10 +95,7 @@ async fn terminal_title_action_required_blinks_when_animations_are_enabled() {
|
||||
available_decisions: None,
|
||||
parsed_cmd: vec![],
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
id: "sub-blink".into(),
|
||||
msg: EventMsg::ExecApprovalRequest(request),
|
||||
});
|
||||
handle_exec_approval_request(&mut chat, "sub-blink", request);
|
||||
|
||||
chat.pre_draw_tick();
|
||||
|
||||
@@ -140,10 +131,7 @@ async fn terminal_title_activity_indicators_do_not_animate_when_animations_are_d
|
||||
available_decisions: None,
|
||||
parsed_cmd: vec![],
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
id: "sub-no-animations".into(),
|
||||
msg: EventMsg::ExecApprovalRequest(request),
|
||||
});
|
||||
handle_exec_approval_request(&mut chat, "sub-no-animations", request);
|
||||
|
||||
chat.pre_draw_tick();
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! 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
|
||||
//! without depending on core protocol event wrappers.
|
||||
//! while preserving local-only attachment metadata.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
|
||||
@@ -148,11 +148,6 @@ impl StatusIndicatorWidget {
|
||||
self.show_interrupt_hint = visible;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn interrupt_hint_visible(&self) -> bool {
|
||||
self.show_interrupt_hint
|
||||
}
|
||||
|
||||
pub(crate) fn pause_timer(&mut self) {
|
||||
self.pause_timer_at(Instant::now());
|
||||
}
|
||||
|
||||
@@ -57,13 +57,6 @@ pub(crate) struct TokenUsageInfo {
|
||||
pub(crate) model_context_window: Option<i64>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct TokenCountEvent {
|
||||
pub(crate) info: Option<TokenUsageInfo>,
|
||||
pub(crate) rate_limits: Option<codex_app_server_protocol::RateLimitSnapshot>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FinalOutput {
|
||||
pub token_usage: TokenUsage,
|
||||
|
||||
@@ -7,7 +7,5 @@ use serde::Serialize;
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub(crate) enum TurnAbortReason {
|
||||
Interrupted,
|
||||
Replaced,
|
||||
ReviewEnded,
|
||||
BudgetLimited,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user