mirror of
https://github.com/openai/codex.git
synced 2026-02-03 07:23:39 +00:00
Compare commits
14 Commits
remove/doc
...
dev/ccunni
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94ddb3bc23 | ||
|
|
61812e5695 | ||
|
|
b4aea65cf7 | ||
|
|
0865baaeba | ||
|
|
1c4e08caf9 | ||
|
|
20e4a2cf3f | ||
|
|
509952dc4b | ||
|
|
c6d6fa8e71 | ||
|
|
0d4873f671 | ||
|
|
5e4e63c6ab | ||
|
|
29c2859e6e | ||
|
|
95bc7bbf8a | ||
|
|
aa07b6b6fd | ||
|
|
9395ef9834 |
File diff suppressed because it is too large
Load Diff
22
codex-rs/core/tests/fixtures/rollout_copy_paste_local_image.json
vendored
Normal file
22
codex-rs/core/tests/fixtures/rollout_copy_paste_local_image.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"type": "message",
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "input_text",
|
||||
"text": "<image name=[Image #1]>"
|
||||
},
|
||||
{
|
||||
"type": "input_image",
|
||||
"image_url": "__IMAGE_URL__"
|
||||
},
|
||||
{
|
||||
"type": "input_text",
|
||||
"text": "</image>"
|
||||
},
|
||||
{
|
||||
"type": "input_text",
|
||||
"text": "pasted image"
|
||||
}
|
||||
]
|
||||
}
|
||||
22
codex-rs/core/tests/fixtures/rollout_drag_drop_image.json
vendored
Normal file
22
codex-rs/core/tests/fixtures/rollout_drag_drop_image.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"type": "message",
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "input_text",
|
||||
"text": "<image>"
|
||||
},
|
||||
{
|
||||
"type": "input_image",
|
||||
"image_url": "__IMAGE_URL__"
|
||||
},
|
||||
{
|
||||
"type": "input_text",
|
||||
"text": "</image>"
|
||||
},
|
||||
{
|
||||
"type": "input_text",
|
||||
"text": "dropped image"
|
||||
}
|
||||
]
|
||||
}
|
||||
203
codex-rs/core/tests/suite/image_rollout.rs
Normal file
203
codex-rs/core/tests/suite/image_rollout.rs
Normal file
@@ -0,0 +1,203 @@
|
||||
use anyhow::Context;
|
||||
use codex_core::protocol::AskForApproval;
|
||||
use codex_core::protocol::EventMsg;
|
||||
use codex_core::protocol::Op;
|
||||
use codex_core::protocol::RolloutItem;
|
||||
use codex_core::protocol::RolloutLine;
|
||||
use codex_core::protocol::SandboxPolicy;
|
||||
use codex_protocol::config_types::ReasoningSummary;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use codex_utils_cargo_bin::find_resource;
|
||||
use core_test_support::responses;
|
||||
use core_test_support::responses::ev_assistant_message;
|
||||
use core_test_support::responses::ev_completed;
|
||||
use core_test_support::responses::ev_response_created;
|
||||
use core_test_support::responses::sse;
|
||||
use core_test_support::responses::start_mock_server;
|
||||
use core_test_support::skip_if_no_network;
|
||||
use core_test_support::test_codex::TestCodex;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
use core_test_support::wait_for_event;
|
||||
use image::ImageBuffer;
|
||||
use image::Rgba;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::path::Path;
|
||||
|
||||
fn load_expected_response_item(path: &str, image_url: &str) -> anyhow::Result<ResponseItem> {
|
||||
let full_path = find_resource!(path).context("fixture path should resolve")?;
|
||||
let raw = std::fs::read_to_string(&full_path)
|
||||
.with_context(|| format!("read fixture {}", full_path.display()))?;
|
||||
let replaced = raw.replace("__IMAGE_URL__", image_url);
|
||||
let response = serde_json::from_str(&replaced)
|
||||
.with_context(|| format!("parse response item fixture {}", full_path.display()))?;
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
fn find_user_message_with_image(text: &str) -> Option<ResponseItem> {
|
||||
for line in text.lines() {
|
||||
let trimmed = line.trim();
|
||||
if trimmed.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let rollout: RolloutLine = match serde_json::from_str(trimmed) {
|
||||
Ok(rollout) => rollout,
|
||||
Err(_) => continue,
|
||||
};
|
||||
if let RolloutItem::ResponseItem(ResponseItem::Message { role, content, .. }) =
|
||||
&rollout.item
|
||||
&& role == "user"
|
||||
&& content
|
||||
.iter()
|
||||
.any(|span| matches!(span, ContentItem::InputImage { .. }))
|
||||
&& let RolloutItem::ResponseItem(item) = rollout.item.clone()
|
||||
{
|
||||
return Some(item);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn write_test_png(path: &Path, color: [u8; 4]) -> anyhow::Result<()> {
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
let image = ImageBuffer::from_pixel(2, 2, Rgba(color));
|
||||
image.save(path)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn extract_image_url(item: &ResponseItem) -> Option<String> {
|
||||
match item {
|
||||
ResponseItem::Message { content, .. } => content.iter().find_map(|span| match span {
|
||||
ContentItem::InputImage { image_url } => Some(image_url.clone()),
|
||||
_ => None,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn copy_paste_local_image_persists_rollout_request_shape() -> anyhow::Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
|
||||
let TestCodex {
|
||||
codex,
|
||||
cwd,
|
||||
session_configured,
|
||||
..
|
||||
} = test_codex().build(&server).await?;
|
||||
|
||||
let rel_path = "images/paste.png";
|
||||
let abs_path = cwd.path().join(rel_path);
|
||||
write_test_png(&abs_path, [12, 34, 56, 255])?;
|
||||
|
||||
let response = sse(vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_assistant_message("msg-1", "done"),
|
||||
ev_completed("resp-1"),
|
||||
]);
|
||||
responses::mount_sse_once(&server, response).await;
|
||||
|
||||
let session_model = session_configured.model.clone();
|
||||
|
||||
codex
|
||||
.submit(Op::UserTurn {
|
||||
items: vec![
|
||||
UserInput::LocalImage {
|
||||
path: abs_path.clone(),
|
||||
},
|
||||
UserInput::Text {
|
||||
text: "pasted image".to_string(),
|
||||
},
|
||||
],
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
summary: ReasoningSummary::Auto,
|
||||
})
|
||||
.await?;
|
||||
|
||||
wait_for_event(&codex, |event| matches!(event, EventMsg::TaskComplete(_))).await;
|
||||
|
||||
let rollout_text = std::fs::read_to_string(codex.rollout_path())?;
|
||||
let actual = find_user_message_with_image(&rollout_text)
|
||||
.expect("expected user message with input image in rollout");
|
||||
|
||||
let image_url = extract_image_url(&actual).expect("expected image url in rollout");
|
||||
let expected = load_expected_response_item(
|
||||
"tests/fixtures/rollout_copy_paste_local_image.json",
|
||||
&image_url,
|
||||
)?;
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn drag_drop_image_persists_rollout_request_shape() -> anyhow::Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
|
||||
let TestCodex {
|
||||
codex,
|
||||
cwd,
|
||||
session_configured,
|
||||
..
|
||||
} = test_codex().build(&server).await?;
|
||||
|
||||
let image_url =
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGNgYAAAAAMAASsJTYQAAAAASUVORK5CYII="
|
||||
.to_string();
|
||||
|
||||
let response = sse(vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_assistant_message("msg-1", "done"),
|
||||
ev_completed("resp-1"),
|
||||
]);
|
||||
responses::mount_sse_once(&server, response).await;
|
||||
|
||||
let session_model = session_configured.model.clone();
|
||||
|
||||
codex
|
||||
.submit(Op::UserTurn {
|
||||
items: vec![
|
||||
UserInput::Image {
|
||||
image_url: image_url.clone(),
|
||||
},
|
||||
UserInput::Text {
|
||||
text: "dropped image".to_string(),
|
||||
},
|
||||
],
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
summary: ReasoningSummary::Auto,
|
||||
})
|
||||
.await?;
|
||||
|
||||
wait_for_event(&codex, |event| matches!(event, EventMsg::TaskComplete(_))).await;
|
||||
|
||||
let rollout_text = std::fs::read_to_string(codex.rollout_path())?;
|
||||
let actual = find_user_message_with_image(&rollout_text)
|
||||
.expect("expected user message with input image in rollout");
|
||||
|
||||
let image_url = extract_image_url(&actual).expect("expected image url in rollout");
|
||||
let expected =
|
||||
load_expected_response_item("tests/fixtures/rollout_drag_drop_image.json", &image_url)?;
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -31,6 +31,7 @@ mod exec_policy;
|
||||
mod fork_thread;
|
||||
mod grep_files;
|
||||
mod hierarchical_agents;
|
||||
mod image_rollout;
|
||||
mod items;
|
||||
mod json_result;
|
||||
mod list_dir;
|
||||
|
||||
@@ -65,13 +65,6 @@ use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
fn windows_degraded_sandbox_active() -> bool {
|
||||
cfg!(target_os = "windows")
|
||||
&& codex_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED
|
||||
&& codex_core::get_platform_sandbox().is_some()
|
||||
&& !codex_core::is_windows_elevated_sandbox_enabled()
|
||||
}
|
||||
|
||||
/// If the pasted content exceeds this number of characters, replace it with a
|
||||
/// placeholder in the UI.
|
||||
const LARGE_PASTE_CHAR_THRESHOLD: usize = 1000;
|
||||
@@ -1215,10 +1208,6 @@ impl ChatComposer {
|
||||
&& rest.is_empty()
|
||||
&& let Some((_n, cmd)) = built_in_slash_commands()
|
||||
.into_iter()
|
||||
.filter(|(_, cmd)| {
|
||||
windows_degraded_sandbox_active()
|
||||
|| *cmd != SlashCommand::ElevateSandbox
|
||||
})
|
||||
.find(|(n, _)| *n == name)
|
||||
{
|
||||
self.textarea.set_text("");
|
||||
@@ -1286,10 +1275,6 @@ impl ChatComposer {
|
||||
if !treat_as_plain_text {
|
||||
let is_builtin = built_in_slash_commands()
|
||||
.into_iter()
|
||||
.filter(|(_, cmd)| {
|
||||
windows_degraded_sandbox_active()
|
||||
|| *cmd != SlashCommand::ElevateSandbox
|
||||
})
|
||||
.any(|(command_name, _)| command_name == name);
|
||||
let prompt_prefix = format!("{PROMPTS_CMD_PREFIX}:");
|
||||
let is_known_prompt = name
|
||||
@@ -1611,6 +1596,7 @@ impl ChatComposer {
|
||||
self.active_popup = ActivePopup::None;
|
||||
return;
|
||||
}
|
||||
|
||||
let skill_token = self.current_skill_token();
|
||||
|
||||
let allow_command_popup = file_token.is_none() && skill_token.is_none();
|
||||
@@ -1680,9 +1666,6 @@ impl ChatComposer {
|
||||
|
||||
let builtin_match = built_in_slash_commands()
|
||||
.into_iter()
|
||||
.filter(|(_, cmd)| {
|
||||
windows_degraded_sandbox_active() || *cmd != SlashCommand::ElevateSandbox
|
||||
})
|
||||
.any(|(cmd_name, _)| fuzzy_match(cmd_name, name).is_some());
|
||||
|
||||
if builtin_match {
|
||||
@@ -2780,6 +2763,18 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_placeholder_snapshots() {
|
||||
snapshot_composer_state("image_placeholder_single", false, |composer| {
|
||||
composer.attach_image(PathBuf::from("/tmp/image1.png"));
|
||||
});
|
||||
|
||||
snapshot_composer_state("image_placeholder_multiple", false, |composer| {
|
||||
composer.attach_image(PathBuf::from("/tmp/image1.png"));
|
||||
composer.attach_image(PathBuf::from("/tmp/image2.png"));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slash_popup_model_first_for_mo_ui() {
|
||||
use ratatui::Terminal;
|
||||
@@ -2952,6 +2947,44 @@ mod tests {
|
||||
assert!(composer.textarea.is_empty(), "composer should be cleared");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slash_review_with_args_dispatches_command_with_args() {
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
use crossterm::event::KeyModifiers;
|
||||
|
||||
let (tx, _rx) = unbounded_channel::<AppEvent>();
|
||||
let sender = AppEventSender::new(tx);
|
||||
let mut composer = ChatComposer::new(
|
||||
true,
|
||||
sender,
|
||||
false,
|
||||
"Ask Codex to do anything".to_string(),
|
||||
false,
|
||||
);
|
||||
|
||||
type_chars_humanlike(&mut composer, &['/', 'r', 'e', 'v', 'i', 'e', 'w', ' ']);
|
||||
type_chars_humanlike(&mut composer, &['f', 'i', 'x', ' ', 't', 'h', 'i', 's']);
|
||||
|
||||
let (result, _needs_redraw) =
|
||||
composer.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
|
||||
match result {
|
||||
InputResult::CommandWithArgs(cmd, args) => {
|
||||
assert_eq!(cmd, SlashCommand::Review);
|
||||
assert_eq!(args, "fix this");
|
||||
}
|
||||
InputResult::Command(cmd) => {
|
||||
panic!("expected args for '/review', got bare command: {cmd:?}")
|
||||
}
|
||||
InputResult::Submitted(text) => {
|
||||
panic!("expected command dispatch, got literal submit: {text}")
|
||||
}
|
||||
InputResult::None => panic!("expected CommandWithArgs result for '/review'"),
|
||||
}
|
||||
assert!(composer.textarea.is_empty(), "composer should be cleared");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_args_supports_quoted_paths_single_arg() {
|
||||
let args = extract_positional_args_for_prompt_line(
|
||||
@@ -4278,6 +4311,59 @@ mod tests {
|
||||
assert_eq!(result, InputResult::None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_navigation_takes_priority_over_popups() {
|
||||
use codex_protocol::protocol::SkillScope;
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
use crossterm::event::KeyModifiers;
|
||||
use tokio::sync::mpsc::unbounded_channel;
|
||||
|
||||
let (tx, _rx) = unbounded_channel::<AppEvent>();
|
||||
let sender = AppEventSender::new(tx);
|
||||
let mut composer = ChatComposer::new(
|
||||
true,
|
||||
sender,
|
||||
false,
|
||||
"Ask Codex to do anything".to_string(),
|
||||
false,
|
||||
);
|
||||
|
||||
composer.set_skill_mentions(Some(vec![SkillMetadata {
|
||||
name: "codex-cli-release-notes".to_string(),
|
||||
description: "example".to_string(),
|
||||
short_description: None,
|
||||
path: PathBuf::from("skills/codex-cli-release-notes/SKILL.md"),
|
||||
scope: SkillScope::Repo,
|
||||
}]));
|
||||
|
||||
// Seed local history; the newest entry triggers the skills popup.
|
||||
composer.history.record_local_submission("older");
|
||||
composer
|
||||
.history
|
||||
.record_local_submission("$codex-cli-release-notes");
|
||||
|
||||
// First Up recalls "$...", but we should not open the skills popup while browsing history.
|
||||
let (result, _redraw) =
|
||||
composer.handle_key_event(KeyEvent::new(KeyCode::Up, KeyModifiers::NONE));
|
||||
assert_eq!(result, InputResult::None);
|
||||
assert_eq!(composer.textarea.text(), "$codex-cli-release-notes");
|
||||
assert!(
|
||||
matches!(composer.active_popup, ActivePopup::None),
|
||||
"expected no skills popup while browsing history"
|
||||
);
|
||||
|
||||
// Second Up should navigate history again (no popup should interfere).
|
||||
let (result, _redraw) =
|
||||
composer.handle_key_event(KeyEvent::new(KeyCode::Up, KeyModifiers::NONE));
|
||||
assert_eq!(result, InputResult::None);
|
||||
assert_eq!(composer.textarea.text(), "older");
|
||||
assert!(
|
||||
matches!(composer.active_popup, ActivePopup::None),
|
||||
"expected popup to be dismissed after history navigation"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slash_popup_activated_for_bare_slash_and_valid_prefixes() {
|
||||
// use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
@@ -4435,7 +4521,6 @@ mod tests {
|
||||
);
|
||||
assert_eq!(composer.attached_images.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_disabled_ignores_keypresses_and_hides_cursor() {
|
||||
use crossterm::event::KeyCode;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/chat_composer.rs
|
||||
assertion_line: 2099
|
||||
expression: terminal.backend()
|
||||
---
|
||||
" "
|
||||
"› [Image #1][Image #2] "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" 100% context left "
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/chat_composer.rs
|
||||
assertion_line: 2099
|
||||
expression: terminal.backend()
|
||||
---
|
||||
" "
|
||||
"› [Image #1] "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" 100% context left "
|
||||
@@ -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;
|
||||
@@ -1695,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());
|
||||
@@ -1738,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();
|
||||
}
|
||||
@@ -2104,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) => {
|
||||
@@ -2881,25 +2833,10 @@ impl ChatWidget {
|
||||
let current_sandbox = self.config.sandbox_policy.get();
|
||||
let mut items: Vec<SelectionItem> = Vec::new();
|
||||
let presets: Vec<ApprovalPreset> = 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,
|
||||
@@ -2923,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()
|
||||
{
|
||||
@@ -2975,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(()),
|
||||
@@ -3263,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()
|
||||
},
|
||||
@@ -3380,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
|
||||
@@ -3496,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;
|
||||
@@ -3556,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);
|
||||
|
||||
@@ -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();
|
||||
@@ -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,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -68,13 +68,6 @@ use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
fn windows_degraded_sandbox_active() -> bool {
|
||||
cfg!(target_os = "windows")
|
||||
&& codex_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED
|
||||
&& codex_core::get_platform_sandbox().is_some()
|
||||
&& !codex_core::is_windows_elevated_sandbox_enabled()
|
||||
}
|
||||
|
||||
/// If the pasted content exceeds this number of characters, replace it with a
|
||||
/// placeholder in the UI.
|
||||
const LARGE_PASTE_CHAR_THRESHOLD: usize = 1000;
|
||||
@@ -1149,10 +1142,6 @@ impl ChatComposer {
|
||||
&& rest.is_empty()
|
||||
&& let Some((_n, cmd)) = built_in_slash_commands()
|
||||
.into_iter()
|
||||
.filter(|(_, cmd)| {
|
||||
windows_degraded_sandbox_active()
|
||||
|| *cmd != SlashCommand::ElevateSandbox
|
||||
})
|
||||
.find(|(n, _)| *n == name)
|
||||
{
|
||||
self.textarea.set_text("");
|
||||
@@ -1220,10 +1209,6 @@ impl ChatComposer {
|
||||
if !treat_as_plain_text {
|
||||
let is_builtin = built_in_slash_commands()
|
||||
.into_iter()
|
||||
.filter(|(_, cmd)| {
|
||||
windows_degraded_sandbox_active()
|
||||
|| *cmd != SlashCommand::ElevateSandbox
|
||||
})
|
||||
.any(|(command_name, _)| command_name == name);
|
||||
let prompt_prefix = format!("{PROMPTS_CMD_PREFIX}:");
|
||||
let is_known_prompt = name
|
||||
@@ -1571,15 +1556,6 @@ impl ChatComposer {
|
||||
|
||||
fn sync_popups(&mut self) {
|
||||
let file_token = Self::current_at_token(&self.textarea);
|
||||
let browsing_history = self
|
||||
.history
|
||||
.should_handle_navigation(self.textarea.text(), self.textarea.cursor());
|
||||
// When browsing input history (shell-style Up/Down recall), skip all popup
|
||||
// synchronization so nothing steals focus from continued history navigation.
|
||||
if browsing_history {
|
||||
self.active_popup = ActivePopup::None;
|
||||
return;
|
||||
}
|
||||
let skill_token = self.current_skill_token();
|
||||
|
||||
let allow_command_popup = file_token.is_none() && skill_token.is_none();
|
||||
@@ -1649,9 +1625,6 @@ impl ChatComposer {
|
||||
|
||||
let builtin_match = built_in_slash_commands()
|
||||
.into_iter()
|
||||
.filter(|(_, cmd)| {
|
||||
windows_degraded_sandbox_active() || *cmd != SlashCommand::ElevateSandbox
|
||||
})
|
||||
.any(|(cmd_name, _)| fuzzy_match(cmd_name, name).is_some());
|
||||
|
||||
if builtin_match {
|
||||
@@ -2899,6 +2872,44 @@ mod tests {
|
||||
assert!(composer.textarea.is_empty(), "composer should be cleared");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slash_review_with_args_dispatches_command_with_args() {
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
use crossterm::event::KeyModifiers;
|
||||
|
||||
let (tx, _rx) = unbounded_channel::<AppEvent>();
|
||||
let sender = AppEventSender::new(tx);
|
||||
let mut composer = ChatComposer::new(
|
||||
true,
|
||||
sender,
|
||||
false,
|
||||
"Ask Codex to do anything".to_string(),
|
||||
false,
|
||||
);
|
||||
|
||||
type_chars_humanlike(&mut composer, &['/', 'r', 'e', 'v', 'i', 'e', 'w', ' ']);
|
||||
type_chars_humanlike(&mut composer, &['f', 'i', 'x', ' ', 't', 'h', 'i', 's']);
|
||||
|
||||
let (result, _needs_redraw) =
|
||||
composer.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
|
||||
match result {
|
||||
InputResult::CommandWithArgs(cmd, args) => {
|
||||
assert_eq!(cmd, SlashCommand::Review);
|
||||
assert_eq!(args, "fix this");
|
||||
}
|
||||
InputResult::Command(cmd) => {
|
||||
panic!("expected args for '/review', got bare command: {cmd:?}")
|
||||
}
|
||||
InputResult::Submitted(text) => {
|
||||
panic!("expected command dispatch, got literal submit: {text}")
|
||||
}
|
||||
InputResult::None => panic!("expected CommandWithArgs result for '/review'"),
|
||||
}
|
||||
assert!(composer.textarea.is_empty(), "composer should be cleared");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_args_supports_quoted_paths_single_arg() {
|
||||
let args = extract_positional_args_for_prompt_line(
|
||||
@@ -4215,7 +4226,6 @@ mod tests {
|
||||
"'/zzz' should not activate slash popup because it is not a prefix of any built-in command"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_disabled_ignores_keypresses_and_hides_cursor() {
|
||||
use crossterm::event::KeyCode;
|
||||
|
||||
@@ -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;
|
||||
@@ -1529,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());
|
||||
@@ -1572,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();
|
||||
}
|
||||
@@ -1910,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) => {
|
||||
@@ -2635,25 +2586,10 @@ impl ChatWidget {
|
||||
let current_sandbox = self.config.sandbox_policy.get();
|
||||
let mut items: Vec<SelectionItem> = Vec::new();
|
||||
let presets: Vec<ApprovalPreset> = 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"
|
||||
@@ -2674,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()
|
||||
{
|
||||
@@ -2725,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(()),
|
||||
@@ -2994,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()
|
||||
},
|
||||
@@ -3111,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
|
||||
@@ -3227,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;
|
||||
@@ -3287,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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
@@ -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,
|
||||
}),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user