From 5e4e63c6abfac0cb4a4235a2e8d305c30736c6be Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Fri, 9 Jan 2026 12:53:11 -0800 Subject: [PATCH] Use [Image #1] format more consistently across Ctrl+V and drag/drop, put image above user message --- codex-rs/core/src/event_mapping.rs | 2 +- codex-rs/core/src/tools/spec.rs | 3 +- codex-rs/protocol/src/models.rs | 40 +- codex-rs/tui/src/bottom_pane/chat_composer.rs | 115 ++---- codex-rs/tui/src/bottom_pane/mod.rs | 11 +- codex-rs/tui/src/chatwidget.rs | 341 ++--------------- codex-rs/tui/src/chatwidget/tests.rs | 84 ++--- .../tui2/src/bottom_pane/chat_composer.rs | 88 ++--- codex-rs/tui2/src/bottom_pane/mod.rs | 11 +- codex-rs/tui2/src/chatwidget.rs | 350 ++---------------- codex-rs/tui2/src/chatwidget/tests.rs | 82 +--- 11 files changed, 171 insertions(+), 956 deletions(-) diff --git a/codex-rs/core/src/event_mapping.rs b/codex-rs/core/src/event_mapping.rs index 9329f772b7..329858b26b 100644 --- a/codex-rs/core/src/event_mapping.rs +++ b/codex-rs/core/src/event_mapping.rs @@ -191,7 +191,7 @@ mod tests { #[test] fn skips_local_image_label_text() { let image_url = "data:image/png;base64,abc".to_string(); - let label = local_image_label_text(std::path::Path::new("/tmp/example.png")); + let label = local_image_label_text(1); let user_text = "Please review this image.".to_string(); let item = ResponseItem::Message { diff --git a/codex-rs/core/src/tools/spec.rs b/codex-rs/core/src/tools/spec.rs index ac91e875a4..af293dc09c 100644 --- a/codex-rs/core/src/tools/spec.rs +++ b/codex-rs/core/src/tools/spec.rs @@ -405,7 +405,8 @@ fn create_view_image_tool() -> ToolSpec { ToolSpec::Function(ResponsesApiTool { name: VIEW_IMAGE_TOOL_NAME.to_string(), - description: "View a local image from the filesystem.".to_string(), + description: "View a local image from the filesystem (only use if given a full filepath by the user, and the image isn't already attached to the thread context within tags)." + .to_string(), strict: false, parameters: JsonSchema::Object { properties, diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index 90be1b4a38..26a7f28bb7 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -182,23 +182,17 @@ fn local_image_error_placeholder( pub const VIEW_IMAGE_TOOL_NAME: &str = "view_image"; -const LOCAL_IMAGE_OPEN_TAG_PREFIX: &str = ""; +const LOCAL_IMAGE_OPEN_TAG_PREFIX: &str = ""; const LOCAL_IMAGE_CLOSE_TAG: &str = ""; -fn local_image_label_text_with_number( - path: &std::path::Path, - label_number: Option, -) -> String { - let label = match label_number { - Some(label_number) => format!("[Image #{label_number}]"), - None => format!("[Image {}]", path.display()), - }; - format!("{LOCAL_IMAGE_OPEN_TAG_PREFIX}{label}{LOCAL_IMAGE_OPEN_TAG_SUFFIX}") +pub fn local_image_label_text(label_number: usize) -> String { + format!("[Image #{label_number}]") } -pub fn local_image_label_text(path: &std::path::Path) -> String { - local_image_label_text_with_number(path, None) +fn local_image_open_tag_text(label_number: usize) -> String { + let label = local_image_label_text(label_number); + format!("{LOCAL_IMAGE_OPEN_TAG_PREFIX}{label}{LOCAL_IMAGE_OPEN_TAG_SUFFIX}") } pub fn is_local_image_open_tag_text(text: &str) -> bool { @@ -234,20 +228,20 @@ fn unsupported_image_error_placeholder(path: &std::path::Path, mime: &str) -> Co } pub fn local_image_content_items(path: &std::path::Path, include_label: bool) -> Vec { - local_image_content_items_with_label_number(path, include_label, None) + local_image_content_items_with_label_number(path, include_label, 1) } fn local_image_content_items_with_label_number( path: &std::path::Path, include_label: bool, - label_number: Option, + label_number: usize, ) -> Vec { match load_and_resize_to_fit(path) { Ok(image) => { let mut items = Vec::with_capacity(3); if include_label { items.push(ContentItem::InputText { - text: local_image_label_text_with_number(path, label_number), + text: local_image_open_tag_text(label_number), }); } items.push(ContentItem::InputImage { @@ -285,13 +279,6 @@ fn local_image_content_items_with_label_number( } } -fn is_default_clipboard_image_path(path: &std::path::Path) -> bool { - path.file_name() - .and_then(|name| name.to_str()) - .map(|name| name.starts_with("codex-clipboard-")) - .unwrap_or(false) -} - impl From for ResponseItem { fn from(item: ResponseInputItem) -> Self { match item { @@ -395,12 +382,7 @@ impl From> for ResponseInputItem { UserInput::Image { image_url } => vec![ContentItem::InputImage { image_url }], UserInput::LocalImage { path } => { image_index += 1; - let label_number = if is_default_clipboard_image_path(&path) { - Some(image_index) - } else { - None - }; - local_image_content_items_with_label_number(&path, true, label_number) + local_image_content_items_with_label_number(&path, true, image_index) } UserInput::Skill { .. } => Vec::new(), // Skill bodies are injected later in core }) diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 4ad59dc2f1..de934965bf 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -46,6 +46,7 @@ use crate::style::user_message_style; use codex_common::fuzzy_match::fuzzy_match; use codex_protocol::custom_prompts::CustomPrompt; use codex_protocol::custom_prompts::PROMPTS_CMD_PREFIX; +use codex_protocol::models::local_image_label_text; use crate::app_event::AppEvent; use crate::app_event_sender::AppEventSender; @@ -59,7 +60,6 @@ use codex_core::skills::model::SkillMetadata; use codex_file_search::FileMatch; use std::cell::RefCell; use std::collections::HashMap; -use std::path::Path; use std::path::PathBuf; use std::time::Duration; use std::time::Instant; @@ -266,17 +266,15 @@ impl ChatComposer { // normalize_pasted_path already handles Windows → WSL path conversion, // so we can directly try to read the image dimensions. - match image::image_dimensions(&path_buf) { - Ok((w, h)) => { - tracing::info!("OK: {pasted}"); - let format_label = pasted_image_format(&path_buf).label(); - self.attach_image(path_buf, w, h, format_label); - true - } - Err(err) => { - tracing::trace!("ERR: {err}"); - false - } + if image::image_dimensions(&path_buf).is_ok() { + tracing::info!("OK: {pasted}"); + let format = pasted_image_format(&path_buf); + tracing::debug!("attached image format={}", format.label()); + self.attach_image(path_buf); + true + } else { + tracing::trace!("ERR: not an image path"); + false } } @@ -401,19 +399,10 @@ impl ChatComposer { } /// Attempt to start a burst by retro-capturing recent chars before the cursor. - pub fn attach_image(&mut self, path: PathBuf, width: u32, height: u32, _format_label: &str) { + pub fn attach_image(&mut self, path: PathBuf) { let image_number = self.attached_images.len() + 1; - let placeholder = if Self::is_default_clipboard_filename(&path) { - let base_placeholder = format!("Image #{image_number}"); - self.next_image_placeholder(&base_placeholder) - } else { - let file_label = path - .file_name() - .map(|name| name.to_string_lossy().into_owned()) - .unwrap_or_else(|| "image".to_string()); - let base_placeholder = format!("{file_label} {width}x{height}"); - self.next_image_placeholder(&base_placeholder) - }; + let base_placeholder = local_image_label_text(image_number); + let placeholder = self.next_image_placeholder(&base_placeholder); // Insert as an element to match large paste placeholder behavior: // styled distinctly and treated atomically for cursor/mutations. self.textarea.insert_element(&placeholder); @@ -481,9 +470,11 @@ impl ChatComposer { let mut suffix = 1; loop { let placeholder = if suffix == 1 { - format!("[{base}]") + base.to_string() + } else if let Some(base_trimmed) = base.strip_suffix(']') { + format!("{base_trimmed} #{suffix}]") } else { - format!("[{base} #{suffix}]") + format!("{base} #{suffix}") }; if !text.contains(&placeholder) { return placeholder; @@ -492,13 +483,6 @@ impl ChatComposer { } } - fn is_default_clipboard_filename(path: &Path) -> bool { - path.file_name() - .and_then(|name| name.to_str()) - .map(|name| name.starts_with("codex-clipboard-")) - .unwrap_or(false) - } - pub(crate) fn insert_str(&mut self, text: &str) { self.textarea.insert_str(text); self.sync_popups(); @@ -824,7 +808,7 @@ impl ChatComposer { if is_image { // Determine dimensions; if that fails fall back to normal path insertion. let path_buf = PathBuf::from(&sel_path); - if let Ok((w, h)) = image::image_dimensions(&path_buf) { + if image::image_dimensions(&path_buf).is_ok() { // Remove the current @token (mirror logic from insert_selected_path without inserting text) // using the flat text and byte-offset cursor API. let cursor_offset = self.textarea.cursor(); @@ -850,16 +834,7 @@ impl ChatComposer { self.textarea.replace_range(start_idx..end_idx, ""); self.textarea.set_cursor(start_idx); - let format_label = match Path::new(&sel_path) - .extension() - .and_then(|e| e.to_str()) - .map(str::to_ascii_lowercase) - { - Some(ext) if ext == "png" => "PNG", - Some(ext) if ext == "jpg" || ext == "jpeg" => "JPEG", - _ => "IMG", - }; - self.attach_image(path_buf, w, h, format_label); + self.attach_image(path_buf); // Add a trailing space to keep typing fluid. self.textarea.insert_str(" "); } else { @@ -3511,12 +3486,12 @@ mod tests { false, ); let path = PathBuf::from("/tmp/image1.png"); - composer.attach_image(path.clone(), 32, 16, "PNG"); + composer.attach_image(path.clone()); composer.handle_paste(" hi".into()); let (result, _) = composer.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); match result { - InputResult::Submitted(text) => assert_eq!(text, "[image1.png 32x16] hi"), + InputResult::Submitted(text) => assert_eq!(text, "[Image #1] hi"), _ => panic!("expected Submitted"), } let imgs = composer.take_recent_submission_images(); @@ -3535,11 +3510,11 @@ mod tests { false, ); let path = PathBuf::from("/tmp/image2.png"); - composer.attach_image(path.clone(), 10, 5, "PNG"); + composer.attach_image(path.clone()); let (result, _) = composer.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); match result { - InputResult::Submitted(text) => assert_eq!(text, "[image2.png 10x5]"), + InputResult::Submitted(text) => assert_eq!(text, "[Image #1]"), _ => panic!("expected Submitted"), } let imgs = composer.take_recent_submission_images(); @@ -3560,21 +3535,15 @@ mod tests { false, ); let path = PathBuf::from("/tmp/image_dup.png"); - composer.attach_image(path.clone(), 10, 5, "PNG"); + composer.attach_image(path.clone()); composer.handle_paste(" ".into()); - composer.attach_image(path, 10, 5, "PNG"); + composer.attach_image(path); let text = composer.textarea.text().to_string(); - assert!(text.contains("[image_dup.png 10x5]")); - assert!(text.contains("[image_dup.png 10x5 #2]")); - assert_eq!( - composer.attached_images[0].placeholder, - "[image_dup.png 10x5]" - ); - assert_eq!( - composer.attached_images[1].placeholder, - "[image_dup.png 10x5 #2]" - ); + assert!(text.contains("[Image #1]")); + assert!(text.contains("[Image #2]")); + assert_eq!(composer.attached_images[0].placeholder, "[Image #1]"); + assert_eq!(composer.attached_images[1].placeholder, "[Image #2]"); } #[test] @@ -3589,7 +3558,7 @@ mod tests { false, ); let path = PathBuf::from("/tmp/image3.png"); - composer.attach_image(path.clone(), 20, 10, "PNG"); + composer.attach_image(path.clone()); let placeholder = composer.attached_images[0].placeholder.clone(); // Case 1: backspace at end @@ -3600,7 +3569,7 @@ mod tests { // Re-add and test backspace in middle: should break the placeholder string // and drop the image mapping (same as text placeholder behavior). - composer.attach_image(path, 20, 10, "PNG"); + composer.attach_image(path); let placeholder2 = composer.attached_images[0].placeholder.clone(); // Move cursor to roughly middle of placeholder if let Some(start_pos) = composer.textarea.text().find(&placeholder2) { @@ -3632,7 +3601,7 @@ mod tests { // Insert an image placeholder at the start let path = PathBuf::from("/tmp/image_multibyte.png"); - composer.attach_image(path, 10, 5, "PNG"); + composer.attach_image(path); // Add multibyte text after the placeholder composer.textarea.insert_str("日本語"); @@ -3641,12 +3610,7 @@ mod tests { composer.handle_key_event(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE)); assert_eq!(composer.attached_images.len(), 1); - assert!( - composer - .textarea - .text() - .starts_with("[image_multibyte.png 10x5]") - ); + assert!(composer.textarea.text().starts_with("[Image #1]")); } #[test] @@ -3664,10 +3628,10 @@ mod tests { let path1 = PathBuf::from("/tmp/image_dup1.png"); let path2 = PathBuf::from("/tmp/image_dup2.png"); - composer.attach_image(path1, 10, 5, "PNG"); + composer.attach_image(path1); // separate placeholders with a space for clarity composer.handle_paste(" ".into()); - composer.attach_image(path2.clone(), 10, 5, "PNG"); + composer.attach_image(path2.clone()); let placeholder1 = composer.attached_images[0].placeholder.clone(); let placeholder2 = composer.attached_images[1].placeholder.clone(); @@ -3693,7 +3657,7 @@ mod tests { assert_eq!( vec![AttachedImage { path: path2, - placeholder: "[image_dup2.png 10x5]".to_string() + placeholder: "[Image #2]".to_string() }], composer.attached_images, "one image mapping remains" @@ -3720,12 +3684,7 @@ mod tests { let needs_redraw = composer.handle_paste(tmp_path.to_string_lossy().to_string()); assert!(needs_redraw); - assert!( - composer - .textarea - .text() - .starts_with("[codex_tui_test_paste_image.png 3x2] ") - ); + assert!(composer.textarea.text().starts_with("[Image #1] ")); let imgs = composer.take_recent_submission_images(); assert_eq!(imgs, vec![tmp_path]); diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index 8579597865..f795ea64f1 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -542,16 +542,9 @@ impl BottomPane { self.request_redraw(); } - pub(crate) fn attach_image( - &mut self, - path: PathBuf, - width: u32, - height: u32, - format_label: &str, - ) { + pub(crate) fn attach_image(&mut self, path: PathBuf) { if self.view_stack.is_empty() { - self.composer - .attach_image(path, width, height, format_label); + self.composer.attach_image(path); self.request_redraw(); } } diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 1a3e886e2f..ee64872efa 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -49,11 +49,11 @@ use codex_core::protocol::ReviewRequest; use codex_core::protocol::ReviewTarget; use codex_core::protocol::SkillsListEntry; use codex_core::protocol::StreamErrorEvent; +use codex_core::protocol::TaskCompleteEvent; use codex_core::protocol::TerminalInteractionEvent; use codex_core::protocol::TokenUsage; use codex_core::protocol::TokenUsageInfo; use codex_core::protocol::TurnAbortReason; -use codex_core::protocol::TurnCompleteEvent; use codex_core::protocol::TurnDiffEvent; use codex_core::protocol::UndoCompletedEvent; use codex_core::protocol::UndoStartedEvent; @@ -85,9 +85,6 @@ use tokio::task::JoinHandle; use tracing::debug; use crate::app_event::AppEvent; -#[cfg(target_os = "windows")] -use crate::app_event::WindowsSandboxEnableMode; -use crate::app_event::WindowsSandboxFallbackReason; use crate::app_event_sender::AppEventSender; use crate::bottom_pane::ApprovalRequest; use crate::bottom_pane::BetaFeatureItem; @@ -1599,7 +1596,7 @@ impl ChatWidget { info.height, info.encoded_format.label() ); - self.attach_image(path, info.width, info.height, info.encoded_format.label()); + self.attach_image(path); } Err(err) => { tracing::warn!("failed to paste image: {err}"); @@ -1652,16 +1649,9 @@ impl ChatWidget { } } - pub(crate) fn attach_image( - &mut self, - path: PathBuf, - width: u32, - height: u32, - format_label: &str, - ) { + pub(crate) fn attach_image(&mut self, path: PathBuf) { tracing::info!("attach_image path={path:?}"); - self.bottom_pane - .attach_image(path, width, height, format_label); + self.bottom_pane.attach_image(path); self.request_redraw(); } @@ -1702,12 +1692,6 @@ impl ChatWidget { } match cmd { SlashCommand::Feedback => { - if !self.config.feedback_enabled { - let params = crate::bottom_pane::feedback_disabled_params(); - self.bottom_pane.show_selection_view(params); - self.request_redraw(); - return; - } // Step 1: pick a category (UI built in feedback_view) let params = crate::bottom_pane::feedback_selection_params(self.app_event_tx.clone()); @@ -1745,45 +1729,6 @@ impl ChatWidget { SlashCommand::Approvals => { self.open_approvals_popup(); } - SlashCommand::ElevateSandbox => { - #[cfg(target_os = "windows")] - { - let windows_degraded_sandbox_enabled = codex_core::get_platform_sandbox() - .is_some() - && !codex_core::is_windows_elevated_sandbox_enabled(); - if !windows_degraded_sandbox_enabled - || !codex_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED - { - // This command should not be visible/recognized outside degraded mode, - // but guard anyway in case something dispatches it directly. - return; - } - - let Some(preset) = builtin_approval_presets() - .into_iter() - .find(|preset| preset.id == "auto") - else { - // Avoid panicking in interactive UI; treat this as a recoverable - // internal error. - self.add_error_message( - "Internal error: missing the 'auto' approval preset.".to_string(), - ); - return; - }; - - if let Err(err) = self.config.approval_policy.can_set(&preset.approval) { - self.add_error_message(err.to_string()); - return; - } - - self.app_event_tx - .send(AppEvent::BeginWindowsSandboxElevatedSetup { preset }); - } - #[cfg(not(target_os = "windows"))] - { - // Not supported; on non-Windows this command should never be reachable. - }; - } SlashCommand::Experimental => { self.open_experimental_popup(); } @@ -2111,8 +2056,8 @@ impl ChatWidget { self.on_agent_reasoning_final(); } EventMsg::AgentReasoningSectionBreak(_) => self.on_reasoning_section_break(), - EventMsg::TurnStarted(_) => self.on_task_started(), - EventMsg::TurnComplete(TurnCompleteEvent { last_agent_message }) => { + EventMsg::TaskStarted(_) => self.on_task_started(), + EventMsg::TaskComplete(TaskCompleteEvent { last_agent_message }) => { self.on_task_complete(last_agent_message) } EventMsg::TokenCount(ev) => { @@ -2888,25 +2833,10 @@ impl ChatWidget { let current_sandbox = self.config.sandbox_policy.get(); let mut items: Vec = Vec::new(); let presets: Vec = builtin_approval_presets(); - - #[cfg(target_os = "windows")] - let windows_degraded_sandbox_enabled = codex_core::get_platform_sandbox().is_some() - && !codex_core::is_windows_elevated_sandbox_enabled(); - #[cfg(not(target_os = "windows"))] - let windows_degraded_sandbox_enabled = false; - - let show_elevate_sandbox_hint = codex_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED - && windows_degraded_sandbox_enabled - && presets.iter().any(|preset| preset.id == "auto"); - for preset in presets.into_iter() { let is_current = Self::preset_matches_current(current_approval, current_sandbox, &preset); - let name = if preset.id == "auto" && windows_degraded_sandbox_enabled { - "Agent (non-elevated sandbox)".to_string() - } else { - preset.label.to_string() - }; + let name = preset.label.to_string(); let description = Some(preset.description.to_string()); let disabled_reason = match self.config.approval_policy.can_set(&preset.approval) { Ok(()) => None, @@ -2930,24 +2860,11 @@ impl ChatWidget { { if codex_core::get_platform_sandbox().is_none() { let preset_clone = preset.clone(); - if codex_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED - && codex_core::windows_sandbox::sandbox_setup_is_complete( - self.config.codex_home.as_path(), - ) - { - vec![Box::new(move |tx| { - tx.send(AppEvent::EnableWindowsSandboxForAgentMode { - preset: preset_clone.clone(), - mode: WindowsSandboxEnableMode::Elevated, - }); - })] - } else { - vec![Box::new(move |tx| { - tx.send(AppEvent::OpenWindowsSandboxEnablePrompt { - preset: preset_clone.clone(), - }); - })] - } + vec![Box::new(move |tx| { + tx.send(AppEvent::OpenWindowsSandboxEnablePrompt { + preset: preset_clone.clone(), + }); + })] } else if let Some((sample_paths, extra_count, failed_scan)) = self.world_writable_warning_details() { @@ -2982,18 +2899,8 @@ impl ChatWidget { }); } - let footer_note = show_elevate_sandbox_hint.then(|| { - vec![ - "The non-elevated sandbox protects your files and prevents network access under most circumstances. However, it carries greater risk if prompt injected. To upgrade to the elevated sandbox, run ".dim(), - "/setup-elevated-sandbox".cyan(), - ".".dim(), - ] - .into() - }); - self.bottom_pane.show_selection_view(SelectionViewParams { title: Some("Select Approval Mode".to_string()), - footer_note, footer_hint: Some(standard_popup_hint_line()), items, header: Box::new(()), @@ -3270,106 +3177,36 @@ impl ChatWidget { pub(crate) fn open_windows_sandbox_enable_prompt(&mut self, preset: ApprovalPreset) { use ratatui_macros::line; - if !codex_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED { - // Legacy flow (pre-NUX): explain the experimental sandbox and let the user enable it - // directly (no elevation prompts). - let mut header = ColumnRenderable::new(); - header.push(*Box::new( - Paragraph::new(vec![ - line!["Agent mode on Windows uses an experimental sandbox to limit network and filesystem access.".bold()], - line!["Learn more: https://developers.openai.com/codex/windows"], - ]) - .wrap(Wrap { trim: false }), - )); - - let preset_clone = preset; - let items = vec![ - SelectionItem { - name: "Enable experimental sandbox".to_string(), - description: None, - actions: vec![Box::new(move |tx| { - tx.send(AppEvent::EnableWindowsSandboxForAgentMode { - preset: preset_clone.clone(), - mode: WindowsSandboxEnableMode::Legacy, - }); - })], - dismiss_on_select: true, - ..Default::default() - }, - SelectionItem { - name: "Go back".to_string(), - description: None, - actions: vec![Box::new(|tx| { - tx.send(AppEvent::OpenApprovalsPopup); - })], - dismiss_on_select: true, - ..Default::default() - }, - ]; - - self.bottom_pane.show_selection_view(SelectionViewParams { - title: None, - footer_hint: Some(standard_popup_hint_line()), - items, - header: Box::new(header), - ..Default::default() - }); - return; - } - - let current_approval = self.config.approval_policy.value(); - let current_sandbox = self.config.sandbox_policy.get(); - let presets = builtin_approval_presets(); - let stay_full_access = presets - .iter() - .find(|preset| preset.id == "full-access") - .is_some_and(|preset| { - Self::preset_matches_current(current_approval, current_sandbox, preset) - }); - let stay_actions = if stay_full_access { - Vec::new() - } else { - presets - .iter() - .find(|preset| preset.id == "read-only") - .map(|preset| { - Self::approval_preset_actions(preset.approval, preset.sandbox.clone()) - }) - .unwrap_or_default() - }; - let stay_label = if stay_full_access { - "Stay in Agent Full Access".to_string() - } else { - "Stay in Read-Only".to_string() - }; - let mut header = ColumnRenderable::new(); header.push(*Box::new( Paragraph::new(vec![ - line!["Set Up Agent Sandbox".bold()], - line![""], - line!["Agent mode uses an experimental Windows sandbox that protects your files and prevents network access by default."], - line!["Learn more: https://developers.openai.com/codex/windows"], + line!["Agent mode on Windows uses an experimental sandbox to limit network and filesystem access.".bold()], + line![ + "Learn more: https://developers.openai.com/codex/windows" + ], ]) .wrap(Wrap { trim: false }), )); + let preset_clone = preset; let items = vec![ SelectionItem { - name: "Set up agent sandbox (requires elevation)".to_string(), + name: "Enable experimental sandbox".to_string(), description: None, actions: vec![Box::new(move |tx| { - tx.send(AppEvent::BeginWindowsSandboxElevatedSetup { - preset: preset.clone(), + tx.send(AppEvent::EnableWindowsSandboxForAgentMode { + preset: preset_clone.clone(), }); })], dismiss_on_select: true, ..Default::default() }, SelectionItem { - name: stay_label, + name: "Go back".to_string(), description: None, - actions: stay_actions, + actions: vec![Box::new(|tx| { + tx.send(AppEvent::OpenApprovalsPopup); + })], dismiss_on_select: true, ..Default::default() }, @@ -3387,107 +3224,6 @@ impl ChatWidget { #[cfg(not(target_os = "windows"))] pub(crate) fn open_windows_sandbox_enable_prompt(&mut self, _preset: ApprovalPreset) {} - #[cfg(target_os = "windows")] - pub(crate) fn open_windows_sandbox_fallback_prompt( - &mut self, - preset: ApprovalPreset, - reason: WindowsSandboxFallbackReason, - ) { - use ratatui_macros::line; - - let _ = reason; - - let current_approval = self.config.approval_policy.value(); - let current_sandbox = self.config.sandbox_policy.get(); - let presets = builtin_approval_presets(); - let stay_full_access = presets - .iter() - .find(|preset| preset.id == "full-access") - .is_some_and(|preset| { - Self::preset_matches_current(current_approval, current_sandbox, preset) - }); - let stay_actions = if stay_full_access { - Vec::new() - } else { - presets - .iter() - .find(|preset| preset.id == "read-only") - .map(|preset| { - Self::approval_preset_actions(preset.approval, preset.sandbox.clone()) - }) - .unwrap_or_default() - }; - let stay_label = if stay_full_access { - "Stay in Agent Full Access".to_string() - } else { - "Stay in Read-Only".to_string() - }; - - let mut lines = Vec::new(); - lines.push(line!["Use Non-Elevated Sandbox?".bold()]); - lines.push(line![""]); - lines.push(line![ - "Elevation failed. You can also use a non-elevated sandbox, which protects your files and prevents network access under most circumstances. However, it carries greater risk if prompt injected." - ]); - lines.push(line![ - "Learn more: https://developers.openai.com/codex/windows" - ]); - - let mut header = ColumnRenderable::new(); - header.push(*Box::new(Paragraph::new(lines).wrap(Wrap { trim: false }))); - - let elevated_preset = preset.clone(); - let legacy_preset = preset; - let items = vec![ - SelectionItem { - name: "Try elevated agent sandbox setup again".to_string(), - description: None, - actions: vec![Box::new(move |tx| { - tx.send(AppEvent::BeginWindowsSandboxElevatedSetup { - preset: elevated_preset.clone(), - }); - })], - dismiss_on_select: true, - ..Default::default() - }, - SelectionItem { - name: "Use non-elevated agent sandbox".to_string(), - description: None, - actions: vec![Box::new(move |tx| { - tx.send(AppEvent::EnableWindowsSandboxForAgentMode { - preset: legacy_preset.clone(), - mode: WindowsSandboxEnableMode::Legacy, - }); - })], - dismiss_on_select: true, - ..Default::default() - }, - SelectionItem { - name: stay_label, - description: None, - actions: stay_actions, - dismiss_on_select: true, - ..Default::default() - }, - ]; - - self.bottom_pane.show_selection_view(SelectionViewParams { - title: None, - footer_hint: Some(standard_popup_hint_line()), - items, - header: Box::new(header), - ..Default::default() - }); - } - - #[cfg(not(target_os = "windows"))] - pub(crate) fn open_windows_sandbox_fallback_prompt( - &mut self, - _preset: ApprovalPreset, - _reason: WindowsSandboxFallbackReason, - ) { - } - #[cfg(target_os = "windows")] pub(crate) fn maybe_prompt_windows_sandbox_enable(&mut self) { if self.config.forced_auto_mode_downgraded_on_windows @@ -3503,34 +3239,6 @@ impl ChatWidget { #[cfg(not(target_os = "windows"))] pub(crate) fn maybe_prompt_windows_sandbox_enable(&mut self) {} - #[cfg(target_os = "windows")] - pub(crate) fn show_windows_sandbox_setup_status(&mut self) { - // While elevated sandbox setup runs, prevent typing so the user doesn't - // accidentally queue messages that will run under an unexpected mode. - self.bottom_pane.set_composer_input_enabled( - false, - Some("Input disabled until setup completes.".to_string()), - ); - self.bottom_pane.ensure_status_indicator(); - self.bottom_pane.set_interrupt_hint_visible(false); - self.set_status_header("Setting up agent sandbox. This can take a minute.".to_string()); - self.request_redraw(); - } - - #[cfg(not(target_os = "windows"))] - #[allow(dead_code)] - pub(crate) fn show_windows_sandbox_setup_status(&mut self) {} - - #[cfg(target_os = "windows")] - pub(crate) fn clear_windows_sandbox_setup_status(&mut self) { - self.bottom_pane.set_composer_input_enabled(true, None); - self.bottom_pane.hide_status_indicator(); - self.request_redraw(); - } - - #[cfg(not(target_os = "windows"))] - pub(crate) fn clear_windows_sandbox_setup_status(&mut self) {} - #[cfg(target_os = "windows")] pub(crate) fn clear_forced_auto_mode_downgrade(&mut self) { self.config.forced_auto_mode_downgraded_on_windows = false; @@ -3563,7 +3271,6 @@ impl ChatWidget { Ok(()) } - #[cfg_attr(not(target_os = "windows"), allow(dead_code))] pub(crate) fn set_feature_enabled(&mut self, feature: Feature, enabled: bool) { if enabled { self.config.features.enable(feature); diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index 9b4870422d..42af29759b 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -39,12 +39,12 @@ use codex_core::protocol::RateLimitWindow; use codex_core::protocol::ReviewRequest; use codex_core::protocol::ReviewTarget; use codex_core::protocol::StreamErrorEvent; +use codex_core::protocol::TaskCompleteEvent; +use codex_core::protocol::TaskStartedEvent; use codex_core::protocol::TerminalInteractionEvent; use codex_core::protocol::TokenCountEvent; use codex_core::protocol::TokenUsage; use codex_core::protocol::TokenUsageInfo; -use codex_core::protocol::TurnCompleteEvent; -use codex_core::protocol::TurnStartedEvent; use codex_core::protocol::UndoCompletedEvent; use codex_core::protocol::UndoStartedEvent; use codex_core::protocol::ViewImageToolCallEvent; @@ -64,8 +64,6 @@ use crossterm::event::KeyEvent; use crossterm::event::KeyModifiers; use insta::assert_snapshot; use pretty_assertions::assert_eq; -#[cfg(target_os = "windows")] -use serial_test::serial; use std::collections::HashSet; use std::path::PathBuf; use tempfile::NamedTempFile; @@ -78,11 +76,6 @@ fn set_windows_sandbox_enabled(enabled: bool) { codex_core::set_windows_sandbox_enabled(enabled); } -#[cfg(target_os = "windows")] -fn set_windows_elevated_sandbox_enabled(enabled: bool) { - codex_core::set_windows_elevated_sandbox_enabled(enabled); -} - async fn test_config() -> Config { // Use base defaults to avoid depending on host state. let codex_home = std::env::temp_dir(); @@ -1114,7 +1107,7 @@ async fn ctrl_c_cleared_prompt_is_recoverable_via_history() { chat.bottom_pane.insert_str("draft message "); chat.bottom_pane - .attach_image(PathBuf::from("/tmp/preview.png"), 10, 10, "PNG"); + .attach_image(PathBuf::from("/tmp/preview.png")); let placeholder = "[Image #1]"; assert!( chat.bottom_pane.composer_text().ends_with(placeholder), @@ -1320,7 +1313,7 @@ async fn unified_exec_waiting_multiple_empty_snapshots() { chat.handle_codex_event(Event { id: "turn-wait-1".into(), - msg: EventMsg::TurnComplete(TurnCompleteEvent { + msg: EventMsg::TaskComplete(TaskCompleteEvent { last_agent_message: None, }), }); @@ -1369,7 +1362,7 @@ async fn unified_exec_non_empty_then_empty_snapshots() { chat.handle_codex_event(Event { id: "turn-wait-3".into(), - msg: EventMsg::TurnComplete(TurnCompleteEvent { + msg: EventMsg::TaskComplete(TaskCompleteEvent { last_agent_message: None, }), }); @@ -1763,7 +1756,7 @@ async fn interrupted_turn_error_message_snapshot() { // 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 { + msg: EventMsg::TaskStarted(TaskStartedEvent { model_context_window: None, }), }); @@ -2034,35 +2027,6 @@ async fn approvals_selection_popup_snapshot() { assert_snapshot!("approvals_selection_popup", popup); } -#[cfg(target_os = "windows")] -#[tokio::test] -#[serial] -async fn approvals_selection_popup_snapshot_windows_degraded_sandbox() { - let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await; - - let was_sandbox_enabled = codex_core::get_platform_sandbox().is_some(); - let was_elevated_enabled = codex_core::is_windows_elevated_sandbox_enabled(); - - chat.config.notices.hide_full_access_warning = None; - chat.config.features.enable(Feature::WindowsSandbox); - chat.config - .features - .disable(Feature::WindowsSandboxElevated); - set_windows_sandbox_enabled(true); - set_windows_elevated_sandbox_enabled(false); - - chat.open_approvals_popup(); - - let popup = render_bottom_popup(&chat, 80); - insta::with_settings!({ snapshot_suffix => "windows_degraded" }, { - assert_snapshot!("approvals_selection_popup", popup); - }); - - // Avoid leaking sandbox global state into other tests. - set_windows_sandbox_enabled(was_sandbox_enabled); - set_windows_elevated_sandbox_enabled(was_elevated_enabled); -} - #[tokio::test] async fn preset_matching_ignores_extra_writable_roots() { let preset = builtin_approval_presets() @@ -2113,8 +2077,8 @@ async fn windows_auto_mode_prompt_requests_enabling_sandbox_feature() { let popup = render_bottom_popup(&chat, 120); assert!( - popup.contains("requires elevation"), - "expected auto mode prompt to mention elevation, popup: {popup}" + popup.contains("Agent mode on Windows uses an experimental sandbox"), + "expected auto mode prompt to mention enabling the sandbox feature, popup: {popup}" ); } @@ -2130,16 +2094,12 @@ async fn startup_prompts_for_windows_sandbox_when_agent_requested() { let popup = render_bottom_popup(&chat, 120); assert!( - popup.contains("requires elevation"), - "expected startup prompt to explain elevation: {popup}" + popup.contains("Agent mode on Windows uses an experimental sandbox"), + "expected startup prompt to explain sandbox: {popup}" ); assert!( - popup.contains("Set up agent sandbox"), - "expected startup prompt to offer agent sandbox setup: {popup}" - ); - assert!( - popup.contains("Stay in"), - "expected startup prompt to offer staying in current mode: {popup}" + popup.contains("Enable experimental sandbox"), + "expected startup prompt to offer enabling the sandbox: {popup}" ); set_windows_sandbox_enabled(true); @@ -2756,7 +2716,7 @@ async fn ui_snapshots_small_heights_task_running() { // Activate status line chat.handle_codex_event(Event { id: "task-1".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { + msg: EventMsg::TaskStarted(TaskStartedEvent { model_context_window: None, }), }); @@ -2787,7 +2747,7 @@ async fn status_widget_and_approval_modal_snapshot() { // Begin a running task so the status indicator would be active. chat.handle_codex_event(Event { id: "task-1".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { + msg: EventMsg::TaskStarted(TaskStartedEvent { model_context_window: None, }), }); @@ -2839,7 +2799,7 @@ async fn status_widget_active_snapshot() { // Activate the status indicator by simulating a task start. chat.handle_codex_event(Event { id: "task-1".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { + msg: EventMsg::TaskStarted(TaskStartedEvent { model_context_window: None, }), }); @@ -3418,7 +3378,7 @@ async fn stream_recovery_restores_previous_status_header() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await; chat.handle_codex_event(Event { id: "task".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { + msg: EventMsg::TaskStarted(TaskStartedEvent { model_context_window: None, }), }); @@ -3455,7 +3415,7 @@ async fn multiple_agent_messages_in_single_turn_emit_multiple_headers() { // Begin turn chat.handle_codex_event(Event { id: "s1".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { + msg: EventMsg::TaskStarted(TaskStartedEvent { model_context_window: None, }), }); @@ -3479,7 +3439,7 @@ async fn multiple_agent_messages_in_single_turn_emit_multiple_headers() { // End turn chat.handle_codex_event(Event { id: "s1".into(), - msg: EventMsg::TurnComplete(TurnCompleteEvent { + msg: EventMsg::TaskComplete(TaskCompleteEvent { last_agent_message: None, }), }); @@ -3649,7 +3609,7 @@ async fn chatwidget_exec_and_status_layout_vt100_snapshot() { }); chat.handle_codex_event(Event { id: "t1".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { + msg: EventMsg::TaskStarted(TaskStartedEvent { model_context_window: None, }), }); @@ -3693,7 +3653,7 @@ async fn chatwidget_markdown_code_blocks_vt100_snapshot() { chat.handle_codex_event(Event { id: "t1".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { + msg: EventMsg::TaskStarted(TaskStartedEvent { model_context_window: None, }), }); @@ -3764,7 +3724,7 @@ printf 'fenced within fenced\n' // Finalize the stream without sending a final AgentMessage, to flush any tail. chat.handle_codex_event(Event { id: "t1".into(), - msg: EventMsg::TurnComplete(TurnCompleteEvent { + msg: EventMsg::TaskComplete(TaskCompleteEvent { last_agent_message: None, }), }); @@ -3781,7 +3741,7 @@ async fn chatwidget_tall() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await; chat.handle_codex_event(Event { id: "t1".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { + msg: EventMsg::TaskStarted(TaskStartedEvent { model_context_window: None, }), }); diff --git a/codex-rs/tui2/src/bottom_pane/chat_composer.rs b/codex-rs/tui2/src/bottom_pane/chat_composer.rs index bfd24482d2..5fbfd2acdf 100644 --- a/codex-rs/tui2/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui2/src/bottom_pane/chat_composer.rs @@ -49,6 +49,7 @@ use crate::style::user_message_style; use codex_common::fuzzy_match::fuzzy_match; use codex_protocol::custom_prompts::CustomPrompt; use codex_protocol::custom_prompts::PROMPTS_CMD_PREFIX; +use codex_protocol::models::local_image_label_text; use crate::app_event::AppEvent; use crate::app_event_sender::AppEventSender; @@ -62,7 +63,6 @@ use codex_core::skills::model::SkillMetadata; use codex_file_search::FileMatch; use std::cell::RefCell; use std::collections::HashMap; -use std::path::Path; use std::path::PathBuf; use std::time::Duration; use std::time::Instant; @@ -279,17 +279,15 @@ impl ChatComposer { // normalize_pasted_path already handles Windows → WSL path conversion, // so we can directly try to read the image dimensions. - match image::image_dimensions(&path_buf) { - Ok((w, h)) => { - tracing::info!("OK: {pasted}"); - let format_label = pasted_image_format(&path_buf).label(); - self.attach_image(path_buf, w, h, format_label); - true - } - Err(err) => { - tracing::trace!("ERR: {err}"); - false - } + if image::image_dimensions(&path_buf).is_ok() { + tracing::info!("OK: {pasted}"); + let format = pasted_image_format(&path_buf); + tracing::debug!("attached image format={}", format.label()); + self.attach_image(path_buf); + true + } else { + tracing::trace!("ERR: not an image path"); + false } } @@ -335,17 +333,9 @@ impl ChatComposer { } /// Attempt to start a burst by retro-capturing recent chars before the cursor. - pub fn attach_image(&mut self, path: PathBuf, width: u32, height: u32, _format_label: &str) { + pub fn attach_image(&mut self, path: PathBuf) { let image_number = self.attached_images.len() + 1; - let placeholder = if Self::is_default_clipboard_filename(&path) { - format!("[Image #{image_number}]") - } else { - let file_label = path - .file_name() - .map(|name| name.to_string_lossy().into_owned()) - .unwrap_or_else(|| "image".to_string()); - format!("[{file_label} {width}x{height}]") - }; + let placeholder = local_image_label_text(image_number); // Insert as an element to match large paste placeholder behavior: // styled distinctly and treated atomically for cursor/mutations. self.textarea.insert_element(&placeholder); @@ -408,13 +398,6 @@ impl ChatComposer { } } - fn is_default_clipboard_filename(path: &Path) -> bool { - path.file_name() - .and_then(|name| name.to_str()) - .map(|name| name.starts_with("codex-clipboard-")) - .unwrap_or(false) - } - pub(crate) fn insert_str(&mut self, text: &str) { self.textarea.insert_str(text); self.sync_popups(); @@ -740,7 +723,7 @@ impl ChatComposer { if is_image { // Determine dimensions; if that fails fall back to normal path insertion. let path_buf = PathBuf::from(&sel_path); - if let Ok((w, h)) = image::image_dimensions(&path_buf) { + if image::image_dimensions(&path_buf).is_ok() { // Remove the current @token (mirror logic from insert_selected_path without inserting text) // using the flat text and byte-offset cursor API. let cursor_offset = self.textarea.cursor(); @@ -766,16 +749,7 @@ impl ChatComposer { self.textarea.replace_range(start_idx..end_idx, ""); self.textarea.set_cursor(start_idx); - let format_label = match Path::new(&sel_path) - .extension() - .and_then(|e| e.to_str()) - .map(str::to_ascii_lowercase) - { - Some(ext) if ext == "png" => "PNG", - Some(ext) if ext == "jpg" || ext == "jpeg" => "JPEG", - _ => "IMG", - }; - self.attach_image(path_buf, w, h, format_label); + self.attach_image(path_buf); // Add a trailing space to keep typing fluid. self.textarea.insert_str(" "); } else { @@ -3431,12 +3405,12 @@ mod tests { false, ); let path = PathBuf::from("/tmp/image1.png"); - composer.attach_image(path.clone(), 32, 16, "PNG"); + composer.attach_image(path.clone()); composer.handle_paste(" hi".into()); let (result, _) = composer.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); match result { - InputResult::Submitted(text) => assert_eq!(text, "[image1.png 32x16] hi"), + InputResult::Submitted(text) => assert_eq!(text, "[Image #1] hi"), _ => panic!("expected Submitted"), } let imgs = composer.take_recent_submission_images(); @@ -3455,11 +3429,11 @@ mod tests { false, ); let path = PathBuf::from("/tmp/image2.png"); - composer.attach_image(path.clone(), 10, 5, "PNG"); + composer.attach_image(path.clone()); let (result, _) = composer.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); match result { - InputResult::Submitted(text) => assert_eq!(text, "[image2.png 10x5]"), + InputResult::Submitted(text) => assert_eq!(text, "[Image #1]"), _ => panic!("expected Submitted"), } let imgs = composer.take_recent_submission_images(); @@ -3480,7 +3454,7 @@ mod tests { false, ); let path = PathBuf::from("/tmp/image3.png"); - composer.attach_image(path.clone(), 20, 10, "PNG"); + composer.attach_image(path.clone()); let placeholder = composer.attached_images[0].placeholder.clone(); // Case 1: backspace at end @@ -3491,7 +3465,7 @@ mod tests { // Re-add and test backspace in middle: should break the placeholder string // and drop the image mapping (same as text placeholder behavior). - composer.attach_image(path, 20, 10, "PNG"); + composer.attach_image(path); let placeholder2 = composer.attached_images[0].placeholder.clone(); // Move cursor to roughly middle of placeholder if let Some(start_pos) = composer.textarea.text().find(&placeholder2) { @@ -3523,7 +3497,7 @@ mod tests { // Insert an image placeholder at the start let path = PathBuf::from("/tmp/image_multibyte.png"); - composer.attach_image(path, 10, 5, "PNG"); + composer.attach_image(path); // Add multibyte text after the placeholder composer.textarea.insert_str("日本語"); @@ -3532,12 +3506,7 @@ mod tests { composer.handle_key_event(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE)); assert_eq!(composer.attached_images.len(), 1); - assert!( - composer - .textarea - .text() - .starts_with("[image_multibyte.png 10x5]") - ); + assert!(composer.textarea.text().starts_with("[Image #1]")); } #[test] @@ -3555,10 +3524,10 @@ mod tests { let path1 = PathBuf::from("/tmp/image_dup1.png"); let path2 = PathBuf::from("/tmp/image_dup2.png"); - composer.attach_image(path1, 10, 5, "PNG"); + composer.attach_image(path1); // separate placeholders with a space for clarity composer.handle_paste(" ".into()); - composer.attach_image(path2.clone(), 10, 5, "PNG"); + composer.attach_image(path2.clone()); let placeholder1 = composer.attached_images[0].placeholder.clone(); let placeholder2 = composer.attached_images[1].placeholder.clone(); @@ -3584,7 +3553,7 @@ mod tests { assert_eq!( vec![AttachedImage { path: path2, - placeholder: "[image_dup2.png 10x5]".to_string() + placeholder: "[Image #2]".to_string() }], composer.attached_images, "one image mapping remains" @@ -3611,12 +3580,7 @@ mod tests { let needs_redraw = composer.handle_paste(tmp_path.to_string_lossy().to_string()); assert!(needs_redraw); - assert!( - composer - .textarea - .text() - .starts_with("[codex_tui_test_paste_image.png 3x2] ") - ); + assert!(composer.textarea.text().starts_with("[Image #1] ")); let imgs = composer.take_recent_submission_images(); assert_eq!(imgs, vec![tmp_path]); diff --git a/codex-rs/tui2/src/bottom_pane/mod.rs b/codex-rs/tui2/src/bottom_pane/mod.rs index 8258d6b50d..887fb3e64d 100644 --- a/codex-rs/tui2/src/bottom_pane/mod.rs +++ b/codex-rs/tui2/src/bottom_pane/mod.rs @@ -529,16 +529,9 @@ impl BottomPane { self.request_redraw(); } - pub(crate) fn attach_image( - &mut self, - path: PathBuf, - width: u32, - height: u32, - format_label: &str, - ) { + pub(crate) fn attach_image(&mut self, path: PathBuf) { if self.view_stack.is_empty() { - self.composer - .attach_image(path, width, height, format_label); + self.composer.attach_image(path); self.request_redraw(); } } diff --git a/codex-rs/tui2/src/chatwidget.rs b/codex-rs/tui2/src/chatwidget.rs index ce27581872..92a51f9a93 100644 --- a/codex-rs/tui2/src/chatwidget.rs +++ b/codex-rs/tui2/src/chatwidget.rs @@ -10,7 +10,6 @@ use codex_backend_client::Client as BackendClient; use codex_core::config::Config; use codex_core::config::ConstraintResult; use codex_core::config::types::Notifications; -use codex_core::features::Feature; use codex_core::git_info::current_branch_name; use codex_core::git_info::local_git_branches; use codex_core::models_manager::manager::ModelsManager; @@ -48,11 +47,11 @@ use codex_core::protocol::ReviewRequest; use codex_core::protocol::ReviewTarget; use codex_core::protocol::SkillsListEntry; use codex_core::protocol::StreamErrorEvent; +use codex_core::protocol::TaskCompleteEvent; use codex_core::protocol::TerminalInteractionEvent; use codex_core::protocol::TokenUsage; use codex_core::protocol::TokenUsageInfo; use codex_core::protocol::TurnAbortReason; -use codex_core::protocol::TurnCompleteEvent; use codex_core::protocol::TurnDiffEvent; use codex_core::protocol::UndoCompletedEvent; use codex_core::protocol::UndoStartedEvent; @@ -84,9 +83,6 @@ use tokio::task::JoinHandle; use tracing::debug; use crate::app_event::AppEvent; -#[cfg(target_os = "windows")] -use crate::app_event::WindowsSandboxEnableMode; -use crate::app_event::WindowsSandboxFallbackReason; use crate::app_event_sender::AppEventSender; use crate::bottom_pane::ApprovalRequest; use crate::bottom_pane::BottomPane; @@ -1458,7 +1454,7 @@ impl ChatWidget { info.height, info.encoded_format.label() ); - self.attach_image(path, info.width, info.height, info.encoded_format.label()); + self.attach_image(path); } Err(err) => { tracing::warn!("failed to paste image: {err}"); @@ -1511,16 +1507,9 @@ impl ChatWidget { } } - pub(crate) fn attach_image( - &mut self, - path: PathBuf, - width: u32, - height: u32, - format_label: &str, - ) { + pub(crate) fn attach_image(&mut self, path: PathBuf) { tracing::info!("attach_image path={path:?}"); - self.bottom_pane - .attach_image(path, width, height, format_label); + self.bottom_pane.attach_image(path); self.request_redraw(); } @@ -1536,12 +1525,6 @@ impl ChatWidget { } match cmd { SlashCommand::Feedback => { - if !self.config.feedback_enabled { - let params = crate::bottom_pane::feedback_disabled_params(); - self.bottom_pane.show_selection_view(params); - self.request_redraw(); - return; - } // Step 1: pick a category (UI built in feedback_view) let params = crate::bottom_pane::feedback_selection_params(self.app_event_tx.clone()); @@ -1579,45 +1562,6 @@ impl ChatWidget { SlashCommand::Approvals => { self.open_approvals_popup(); } - SlashCommand::ElevateSandbox => { - #[cfg(target_os = "windows")] - { - let windows_degraded_sandbox_enabled = codex_core::get_platform_sandbox() - .is_some() - && !codex_core::is_windows_elevated_sandbox_enabled(); - if !windows_degraded_sandbox_enabled - || !codex_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED - { - // This command should not be visible/recognized outside degraded mode, - // but guard anyway in case something dispatches it directly. - return; - } - - let Some(preset) = builtin_approval_presets() - .into_iter() - .find(|preset| preset.id == "auto") - else { - // Avoid panicking in interactive UI; treat this as a recoverable - // internal error. - self.add_error_message( - "Internal error: missing the 'auto' approval preset.".to_string(), - ); - return; - }; - - if let Err(err) = self.config.approval_policy.can_set(&preset.approval) { - self.add_error_message(err.to_string()); - return; - } - - self.app_event_tx - .send(AppEvent::BeginWindowsSandboxElevatedSetup { preset }); - } - #[cfg(not(target_os = "windows"))] - { - // Not supported; on non-Windows this command should never be reachable. - }; - } SlashCommand::Quit | SlashCommand::Exit => { self.request_exit(); } @@ -1917,8 +1861,8 @@ impl ChatWidget { self.on_agent_reasoning_final(); } EventMsg::AgentReasoningSectionBreak(_) => self.on_reasoning_section_break(), - EventMsg::TurnStarted(_) => self.on_task_started(), - EventMsg::TurnComplete(TurnCompleteEvent { last_agent_message }) => { + EventMsg::TaskStarted(_) => self.on_task_started(), + EventMsg::TaskComplete(TaskCompleteEvent { last_agent_message }) => { self.on_task_complete(last_agent_message) } EventMsg::TokenCount(ev) => { @@ -2642,25 +2586,10 @@ impl ChatWidget { let current_sandbox = self.config.sandbox_policy.get(); let mut items: Vec = Vec::new(); let presets: Vec = builtin_approval_presets(); - - #[cfg(target_os = "windows")] - let windows_degraded_sandbox_enabled = codex_core::get_platform_sandbox().is_some() - && !codex_core::is_windows_elevated_sandbox_enabled(); - #[cfg(not(target_os = "windows"))] - let windows_degraded_sandbox_enabled = false; - - let show_elevate_sandbox_hint = codex_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED - && windows_degraded_sandbox_enabled - && presets.iter().any(|preset| preset.id == "auto"); - for preset in presets.into_iter() { let is_current = Self::preset_matches_current(current_approval, current_sandbox, &preset); - let name = if preset.id == "auto" && windows_degraded_sandbox_enabled { - "Agent (non-elevated sandbox)".to_string() - } else { - preset.label.to_string() - }; + let name = preset.label.to_string(); let description_text = preset.description; let description = Some(description_text.to_string()); let requires_confirmation = preset.id == "full-access" @@ -2681,24 +2610,11 @@ impl ChatWidget { { if codex_core::get_platform_sandbox().is_none() { let preset_clone = preset.clone(); - if codex_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED - && codex_core::windows_sandbox::sandbox_setup_is_complete( - self.config.codex_home.as_path(), - ) - { - vec![Box::new(move |tx| { - tx.send(AppEvent::EnableWindowsSandboxForAgentMode { - preset: preset_clone.clone(), - mode: WindowsSandboxEnableMode::Elevated, - }); - })] - } else { - vec![Box::new(move |tx| { - tx.send(AppEvent::OpenWindowsSandboxEnablePrompt { - preset: preset_clone.clone(), - }); - })] - } + vec![Box::new(move |tx| { + tx.send(AppEvent::OpenWindowsSandboxEnablePrompt { + preset: preset_clone.clone(), + }); + })] } else if let Some((sample_paths, extra_count, failed_scan)) = self.world_writable_warning_details() { @@ -2732,18 +2648,8 @@ impl ChatWidget { }); } - let footer_note = show_elevate_sandbox_hint.then(|| { - vec![ - "The non-elevated sandbox protects your files and prevents network access under most circumstances. However, it carries greater risk if prompt injected. To upgrade to the elevated sandbox, run ".dim(), - "/setup-elevated-sandbox".cyan(), - ".".dim(), - ] - .into() - }); - self.bottom_pane.show_selection_view(SelectionViewParams { title: Some("Select Approval Mode".to_string()), - footer_note, footer_hint: Some(standard_popup_hint_line()), items, header: Box::new(()), @@ -3001,106 +2907,36 @@ impl ChatWidget { pub(crate) fn open_windows_sandbox_enable_prompt(&mut self, preset: ApprovalPreset) { use ratatui_macros::line; - if !codex_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED { - // Legacy flow (pre-NUX): explain the experimental sandbox and let the user enable it - // directly (no elevation prompts). - let mut header = ColumnRenderable::new(); - header.push(*Box::new( - Paragraph::new(vec![ - line!["Agent mode on Windows uses an experimental sandbox to limit network and filesystem access.".bold()], - line!["Learn more: https://developers.openai.com/codex/windows"], - ]) - .wrap(Wrap { trim: false }), - )); - - let preset_clone = preset; - let items = vec![ - SelectionItem { - name: "Enable experimental sandbox".to_string(), - description: None, - actions: vec![Box::new(move |tx| { - tx.send(AppEvent::EnableWindowsSandboxForAgentMode { - preset: preset_clone.clone(), - mode: WindowsSandboxEnableMode::Legacy, - }); - })], - dismiss_on_select: true, - ..Default::default() - }, - SelectionItem { - name: "Go back".to_string(), - description: None, - actions: vec![Box::new(|tx| { - tx.send(AppEvent::OpenApprovalsPopup); - })], - dismiss_on_select: true, - ..Default::default() - }, - ]; - - self.bottom_pane.show_selection_view(SelectionViewParams { - title: None, - footer_hint: Some(standard_popup_hint_line()), - items, - header: Box::new(header), - ..Default::default() - }); - return; - } - - let current_approval = self.config.approval_policy.value(); - let current_sandbox = self.config.sandbox_policy.get(); - let presets = builtin_approval_presets(); - let stay_full_access = presets - .iter() - .find(|preset| preset.id == "full-access") - .is_some_and(|preset| { - Self::preset_matches_current(current_approval, current_sandbox, preset) - }); - let stay_actions = if stay_full_access { - Vec::new() - } else { - presets - .iter() - .find(|preset| preset.id == "read-only") - .map(|preset| { - Self::approval_preset_actions(preset.approval, preset.sandbox.clone()) - }) - .unwrap_or_default() - }; - let stay_label = if stay_full_access { - "Stay in Agent Full Access".to_string() - } else { - "Stay in Read-Only".to_string() - }; - let mut header = ColumnRenderable::new(); header.push(*Box::new( Paragraph::new(vec![ - line!["Set Up Agent Sandbox".bold()], - line![""], - line!["Agent mode uses an experimental Windows sandbox that protects your files and prevents network access by default."], - line!["Learn more: https://developers.openai.com/codex/windows"], + line!["Agent mode on Windows uses an experimental sandbox to limit network and filesystem access.".bold()], + line![ + "Learn more: https://developers.openai.com/codex/windows" + ], ]) .wrap(Wrap { trim: false }), )); + let preset_clone = preset; let items = vec![ SelectionItem { - name: "Set up agent sandbox (requires elevation)".to_string(), + name: "Enable experimental sandbox".to_string(), description: None, actions: vec![Box::new(move |tx| { - tx.send(AppEvent::BeginWindowsSandboxElevatedSetup { - preset: preset.clone(), + tx.send(AppEvent::EnableWindowsSandboxForAgentMode { + preset: preset_clone.clone(), }); })], dismiss_on_select: true, ..Default::default() }, SelectionItem { - name: stay_label, + name: "Go back".to_string(), description: None, - actions: stay_actions, + actions: vec![Box::new(|tx| { + tx.send(AppEvent::OpenApprovalsPopup); + })], dismiss_on_select: true, ..Default::default() }, @@ -3118,107 +2954,6 @@ impl ChatWidget { #[cfg(not(target_os = "windows"))] pub(crate) fn open_windows_sandbox_enable_prompt(&mut self, _preset: ApprovalPreset) {} - #[cfg(target_os = "windows")] - pub(crate) fn open_windows_sandbox_fallback_prompt( - &mut self, - preset: ApprovalPreset, - reason: WindowsSandboxFallbackReason, - ) { - use ratatui_macros::line; - - let _ = reason; - - let current_approval = self.config.approval_policy.value(); - let current_sandbox = self.config.sandbox_policy.get(); - let presets = builtin_approval_presets(); - let stay_full_access = presets - .iter() - .find(|preset| preset.id == "full-access") - .is_some_and(|preset| { - Self::preset_matches_current(current_approval, current_sandbox, preset) - }); - let stay_actions = if stay_full_access { - Vec::new() - } else { - presets - .iter() - .find(|preset| preset.id == "read-only") - .map(|preset| { - Self::approval_preset_actions(preset.approval, preset.sandbox.clone()) - }) - .unwrap_or_default() - }; - let stay_label = if stay_full_access { - "Stay in Agent Full Access".to_string() - } else { - "Stay in Read-Only".to_string() - }; - - let mut lines = Vec::new(); - lines.push(line!["Use Non-Elevated Sandbox?".bold()]); - lines.push(line![""]); - lines.push(line![ - "Elevation failed. You can also use a non-elevated sandbox, which protects your files and prevents network access under most circumstances. However, it carries greater risk if prompt injected." - ]); - lines.push(line![ - "Learn more: https://developers.openai.com/codex/windows" - ]); - - let mut header = ColumnRenderable::new(); - header.push(*Box::new(Paragraph::new(lines).wrap(Wrap { trim: false }))); - - let elevated_preset = preset.clone(); - let legacy_preset = preset; - let items = vec![ - SelectionItem { - name: "Try elevated agent sandbox setup again".to_string(), - description: None, - actions: vec![Box::new(move |tx| { - tx.send(AppEvent::BeginWindowsSandboxElevatedSetup { - preset: elevated_preset.clone(), - }); - })], - dismiss_on_select: true, - ..Default::default() - }, - SelectionItem { - name: "Use non-elevated agent sandbox".to_string(), - description: None, - actions: vec![Box::new(move |tx| { - tx.send(AppEvent::EnableWindowsSandboxForAgentMode { - preset: legacy_preset.clone(), - mode: WindowsSandboxEnableMode::Legacy, - }); - })], - dismiss_on_select: true, - ..Default::default() - }, - SelectionItem { - name: stay_label, - description: None, - actions: stay_actions, - dismiss_on_select: true, - ..Default::default() - }, - ]; - - self.bottom_pane.show_selection_view(SelectionViewParams { - title: None, - footer_hint: Some(standard_popup_hint_line()), - items, - header: Box::new(header), - ..Default::default() - }); - } - - #[cfg(not(target_os = "windows"))] - pub(crate) fn open_windows_sandbox_fallback_prompt( - &mut self, - _preset: ApprovalPreset, - _reason: WindowsSandboxFallbackReason, - ) { - } - #[cfg(target_os = "windows")] pub(crate) fn maybe_prompt_windows_sandbox_enable(&mut self) { if self.config.forced_auto_mode_downgraded_on_windows @@ -3234,34 +2969,6 @@ impl ChatWidget { #[cfg(not(target_os = "windows"))] pub(crate) fn maybe_prompt_windows_sandbox_enable(&mut self) {} - #[cfg(target_os = "windows")] - pub(crate) fn show_windows_sandbox_setup_status(&mut self) { - // While elevated sandbox setup runs, prevent typing so the user doesn't - // accidentally queue messages that will run under an unexpected mode. - self.bottom_pane.set_composer_input_enabled( - false, - Some("Input disabled until setup completes.".to_string()), - ); - self.bottom_pane.ensure_status_indicator(); - self.bottom_pane.set_interrupt_hint_visible(false); - self.set_status_header("Setting up agent sandbox. This can take a minute.".to_string()); - self.request_redraw(); - } - - #[cfg(not(target_os = "windows"))] - #[allow(dead_code)] - pub(crate) fn show_windows_sandbox_setup_status(&mut self) {} - - #[cfg(target_os = "windows")] - pub(crate) fn clear_windows_sandbox_setup_status(&mut self) { - self.bottom_pane.set_composer_input_enabled(true, None); - self.bottom_pane.hide_status_indicator(); - self.request_redraw(); - } - - #[cfg(not(target_os = "windows"))] - pub(crate) fn clear_windows_sandbox_setup_status(&mut self) {} - #[cfg(target_os = "windows")] pub(crate) fn clear_forced_auto_mode_downgrade(&mut self) { self.config.forced_auto_mode_downgraded_on_windows = false; @@ -3294,15 +3001,6 @@ impl ChatWidget { Ok(()) } - #[cfg_attr(not(target_os = "windows"), allow(dead_code))] - pub(crate) fn set_feature_enabled(&mut self, feature: Feature, enabled: bool) { - if enabled { - self.config.features.enable(feature); - } else { - self.config.features.disable(feature); - } - } - pub(crate) fn set_full_access_warning_acknowledged(&mut self, acknowledged: bool) { self.config.notices.hide_full_access_warning = Some(acknowledged); } diff --git a/codex-rs/tui2/src/chatwidget/tests.rs b/codex-rs/tui2/src/chatwidget/tests.rs index e12e64e72d..e287344ccc 100644 --- a/codex-rs/tui2/src/chatwidget/tests.rs +++ b/codex-rs/tui2/src/chatwidget/tests.rs @@ -10,8 +10,6 @@ use codex_core::CodexAuth; use codex_core::config::Config; use codex_core::config::ConfigBuilder; use codex_core::config::Constrained; -#[cfg(target_os = "windows")] -use codex_core::features::Feature; use codex_core::models_manager::manager::ModelsManager; use codex_core::protocol::AgentMessageDeltaEvent; use codex_core::protocol::AgentMessageEvent; @@ -38,11 +36,11 @@ use codex_core::protocol::RateLimitWindow; use codex_core::protocol::ReviewRequest; use codex_core::protocol::ReviewTarget; use codex_core::protocol::StreamErrorEvent; +use codex_core::protocol::TaskCompleteEvent; +use codex_core::protocol::TaskStartedEvent; use codex_core::protocol::TokenCountEvent; use codex_core::protocol::TokenUsage; use codex_core::protocol::TokenUsageInfo; -use codex_core::protocol::TurnCompleteEvent; -use codex_core::protocol::TurnStartedEvent; use codex_core::protocol::UndoCompletedEvent; use codex_core::protocol::UndoStartedEvent; use codex_core::protocol::ViewImageToolCallEvent; @@ -62,8 +60,6 @@ use crossterm::event::KeyEvent; use crossterm::event::KeyModifiers; use insta::assert_snapshot; use pretty_assertions::assert_eq; -#[cfg(target_os = "windows")] -use serial_test::serial; use std::collections::HashSet; use std::path::PathBuf; use tempfile::NamedTempFile; @@ -76,11 +72,6 @@ fn set_windows_sandbox_enabled(enabled: bool) { codex_core::set_windows_sandbox_enabled(enabled); } -#[cfg(target_os = "windows")] -fn set_windows_elevated_sandbox_enabled(enabled: bool) { - codex_core::set_windows_elevated_sandbox_enabled(enabled); -} - async fn test_config() -> Config { // Use base defaults to avoid depending on host state. let codex_home = std::env::temp_dir(); @@ -1065,7 +1056,7 @@ async fn ctrl_c_cleared_prompt_is_recoverable_via_history() { chat.bottom_pane.insert_str("draft message "); chat.bottom_pane - .attach_image(PathBuf::from("/tmp/preview.png"), 10, 10, "PNG"); + .attach_image(PathBuf::from("/tmp/preview.png")); let placeholder = "[Image #1]"; assert!( chat.bottom_pane.composer_text().ends_with(placeholder), @@ -1589,7 +1580,7 @@ async fn interrupted_turn_error_message_snapshot() { // 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 { + msg: EventMsg::TaskStarted(TaskStartedEvent { model_context_window: None, }), }); @@ -1795,35 +1786,6 @@ async fn approvals_selection_popup_snapshot() { assert_snapshot!("approvals_selection_popup", popup); } -#[cfg(target_os = "windows")] -#[tokio::test] -#[serial] -async fn approvals_selection_popup_snapshot_windows_degraded_sandbox() { - let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await; - - let was_sandbox_enabled = codex_core::get_platform_sandbox().is_some(); - let was_elevated_enabled = codex_core::is_windows_elevated_sandbox_enabled(); - - chat.config.notices.hide_full_access_warning = None; - chat.config.features.enable(Feature::WindowsSandbox); - chat.config - .features - .disable(Feature::WindowsSandboxElevated); - set_windows_sandbox_enabled(true); - set_windows_elevated_sandbox_enabled(false); - - chat.open_approvals_popup(); - - let popup = render_bottom_popup(&chat, 80); - insta::with_settings!({ snapshot_suffix => "windows_degraded" }, { - assert_snapshot!("approvals_selection_popup", popup); - }); - - // Avoid leaking sandbox global state into other tests. - set_windows_sandbox_enabled(was_sandbox_enabled); - set_windows_elevated_sandbox_enabled(was_elevated_enabled); -} - #[tokio::test] async fn preset_matching_ignores_extra_writable_roots() { let preset = builtin_approval_presets() @@ -1874,8 +1836,8 @@ async fn windows_auto_mode_prompt_requests_enabling_sandbox_feature() { let popup = render_bottom_popup(&chat, 120); assert!( - popup.contains("requires elevation"), - "expected auto mode prompt to mention elevation, popup: {popup}" + popup.contains("Agent mode on Windows uses an experimental sandbox"), + "expected auto mode prompt to mention enabling the sandbox feature, popup: {popup}" ); } @@ -1891,16 +1853,12 @@ async fn startup_prompts_for_windows_sandbox_when_agent_requested() { let popup = render_bottom_popup(&chat, 120); assert!( - popup.contains("requires elevation"), - "expected startup prompt to explain elevation: {popup}" + popup.contains("Agent mode on Windows uses an experimental sandbox"), + "expected startup prompt to explain sandbox: {popup}" ); assert!( - popup.contains("Set up agent sandbox"), - "expected startup prompt to offer agent sandbox setup: {popup}" - ); - assert!( - popup.contains("Stay in"), - "expected startup prompt to offer staying in current mode: {popup}" + popup.contains("Enable experimental sandbox"), + "expected startup prompt to offer enabling the sandbox: {popup}" ); set_windows_sandbox_enabled(true); @@ -2383,7 +2341,7 @@ async fn ui_snapshots_small_heights_task_running() { // Activate status line chat.handle_codex_event(Event { id: "task-1".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { + msg: EventMsg::TaskStarted(TaskStartedEvent { model_context_window: None, }), }); @@ -2414,7 +2372,7 @@ async fn status_widget_and_approval_modal_snapshot() { // Begin a running task so the status indicator would be active. chat.handle_codex_event(Event { id: "task-1".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { + msg: EventMsg::TaskStarted(TaskStartedEvent { model_context_window: None, }), }); @@ -2466,7 +2424,7 @@ async fn status_widget_active_snapshot() { // Activate the status indicator by simulating a task start. chat.handle_codex_event(Event { id: "task-1".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { + msg: EventMsg::TaskStarted(TaskStartedEvent { model_context_window: None, }), }); @@ -3041,7 +2999,7 @@ async fn stream_recovery_restores_previous_status_header() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await; chat.handle_codex_event(Event { id: "task".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { + msg: EventMsg::TaskStarted(TaskStartedEvent { model_context_window: None, }), }); @@ -3078,7 +3036,7 @@ async fn multiple_agent_messages_in_single_turn_emit_multiple_headers() { // Begin turn chat.handle_codex_event(Event { id: "s1".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { + msg: EventMsg::TaskStarted(TaskStartedEvent { model_context_window: None, }), }); @@ -3102,7 +3060,7 @@ async fn multiple_agent_messages_in_single_turn_emit_multiple_headers() { // End turn chat.handle_codex_event(Event { id: "s1".into(), - msg: EventMsg::TurnComplete(TurnCompleteEvent { + msg: EventMsg::TaskComplete(TaskCompleteEvent { last_agent_message: None, }), }); @@ -3272,7 +3230,7 @@ async fn chatwidget_exec_and_status_layout_vt100_snapshot() { }); chat.handle_codex_event(Event { id: "t1".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { + msg: EventMsg::TaskStarted(TaskStartedEvent { model_context_window: None, }), }); @@ -3316,7 +3274,7 @@ async fn chatwidget_markdown_code_blocks_vt100_snapshot() { chat.handle_codex_event(Event { id: "t1".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { + msg: EventMsg::TaskStarted(TaskStartedEvent { model_context_window: None, }), }); @@ -3387,7 +3345,7 @@ printf 'fenced within fenced\n' // Finalize the stream without sending a final AgentMessage, to flush any tail. chat.handle_codex_event(Event { id: "t1".into(), - msg: EventMsg::TurnComplete(TurnCompleteEvent { + msg: EventMsg::TaskComplete(TaskCompleteEvent { last_agent_message: None, }), }); @@ -3404,7 +3362,7 @@ async fn chatwidget_tall() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await; chat.handle_codex_event(Event { id: "t1".into(), - msg: EventMsg::TurnStarted(TurnStartedEvent { + msg: EventMsg::TaskStarted(TaskStartedEvent { model_context_window: None, }), });