mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
define/emit some metrics for windows sandbox setup (#9573)
This should give us visibility into how users are using the elevated sandbox nux flow, and the timing of the elevated setup.
This commit is contained in:
committed by
GitHub
Unverified
parent
8179312ff5
commit
f81dd128a2
Generated
+2
@@ -1756,6 +1756,7 @@ dependencies = [
|
||||
"codex-feedback",
|
||||
"codex-file-search",
|
||||
"codex-login",
|
||||
"codex-otel",
|
||||
"codex-protocol",
|
||||
"codex-utils-absolute-path",
|
||||
"codex-utils-cargo-bin",
|
||||
@@ -1831,6 +1832,7 @@ dependencies = [
|
||||
"codex-feedback",
|
||||
"codex-file-search",
|
||||
"codex-login",
|
||||
"codex-otel",
|
||||
"codex-protocol",
|
||||
"codex-tui",
|
||||
"codex-utils-absolute-path",
|
||||
|
||||
@@ -39,6 +39,7 @@ codex-core = { workspace = true }
|
||||
codex-feedback = { workspace = true }
|
||||
codex-file-search = { workspace = true }
|
||||
codex-login = { workspace = true }
|
||||
codex-otel = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
codex-utils-absolute-path = { workspace = true }
|
||||
color-eyre = { workspace = true }
|
||||
|
||||
+100
-12
@@ -29,6 +29,7 @@ use crate::tui::TuiEvent;
|
||||
use crate::update_action::UpdateAction;
|
||||
use codex_ansi_escape::ansi_escape_line;
|
||||
use codex_core::AuthManager;
|
||||
use codex_core::CodexAuth;
|
||||
use codex_core::ThreadManager;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config::edit::ConfigEdit;
|
||||
@@ -47,6 +48,7 @@ use codex_core::protocol::Op;
|
||||
use codex_core::protocol::SessionSource;
|
||||
use codex_core::protocol::SkillErrorInfo;
|
||||
use codex_core::protocol::TokenUsage;
|
||||
use codex_otel::OtelManager;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::openai_models::ModelPreset;
|
||||
use codex_protocol::openai_models::ModelUpgrade;
|
||||
@@ -70,6 +72,7 @@ use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
use tokio::select;
|
||||
use tokio::sync::broadcast;
|
||||
use tokio::sync::mpsc::unbounded_channel;
|
||||
@@ -326,6 +329,7 @@ async fn handle_model_migration_prompt_if_needed(
|
||||
|
||||
pub(crate) struct App {
|
||||
pub(crate) server: Arc<ThreadManager>,
|
||||
pub(crate) otel_manager: OtelManager,
|
||||
pub(crate) app_event_tx: AppEventSender,
|
||||
pub(crate) chat_widget: ChatWidget,
|
||||
pub(crate) auth_manager: Arc<AuthManager>,
|
||||
@@ -362,8 +366,7 @@ pub(crate) struct App {
|
||||
/// stopping a thread (e.g., before starting a new one).
|
||||
suppress_shutdown_complete: bool,
|
||||
|
||||
// One-shot suppression of the next world-writable scan after user confirmation.
|
||||
skip_world_writable_scan_once: bool,
|
||||
windows_sandbox: WindowsSandboxState,
|
||||
|
||||
// TODO(jif) drop once new UX is here.
|
||||
// Track external agent approvals spawned via AgentControl.
|
||||
@@ -373,6 +376,13 @@ pub(crate) struct App {
|
||||
paused_codex_events: VecDeque<Event>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct WindowsSandboxState {
|
||||
setup_started_at: Option<Instant>,
|
||||
// One-shot suppression of the next world-writable scan after user confirmation.
|
||||
skip_world_writable_scan_once: bool,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn chatwidget_init_for_forked_or_resumed_thread(
|
||||
&self,
|
||||
@@ -391,6 +401,7 @@ impl App {
|
||||
feedback: self.feedback.clone(),
|
||||
is_first_run: false,
|
||||
model: Some(self.chat_widget.current_model().to_string()),
|
||||
otel_manager: self.otel_manager.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -450,6 +461,24 @@ impl App {
|
||||
model = updated_model;
|
||||
}
|
||||
|
||||
let auth = auth_manager.auth().await;
|
||||
let auth_ref = auth.as_ref();
|
||||
let model_info = thread_manager
|
||||
.get_models_manager()
|
||||
.get_model_info(model.as_str(), &config)
|
||||
.await;
|
||||
let otel_manager = OtelManager::new(
|
||||
ThreadId::new(),
|
||||
model.as_str(),
|
||||
model_info.slug.as_str(),
|
||||
auth_ref.and_then(CodexAuth::get_account_id),
|
||||
auth_ref.and_then(CodexAuth::get_account_email),
|
||||
auth_ref.map(|auth| auth.mode),
|
||||
config.otel.log_user_prompt,
|
||||
codex_core::terminal::user_agent(),
|
||||
SessionSource::Cli,
|
||||
);
|
||||
|
||||
let enhanced_keys_supported = tui.enhanced_keys_supported();
|
||||
let mut chat_widget = match session_selection {
|
||||
SessionSelection::StartFresh | SessionSelection::Exit => {
|
||||
@@ -469,6 +498,7 @@ impl App {
|
||||
feedback: feedback.clone(),
|
||||
is_first_run,
|
||||
model: Some(model.clone()),
|
||||
otel_manager: otel_manager.clone(),
|
||||
};
|
||||
ChatWidget::new(init, thread_manager.clone())
|
||||
}
|
||||
@@ -496,6 +526,7 @@ impl App {
|
||||
feedback: feedback.clone(),
|
||||
is_first_run,
|
||||
model: config.model.clone(),
|
||||
otel_manager: otel_manager.clone(),
|
||||
};
|
||||
ChatWidget::new_from_existing(init, resumed.thread, resumed.session_configured)
|
||||
}
|
||||
@@ -523,6 +554,7 @@ impl App {
|
||||
feedback: feedback.clone(),
|
||||
is_first_run,
|
||||
model: config.model.clone(),
|
||||
otel_manager: otel_manager.clone(),
|
||||
};
|
||||
ChatWidget::new_from_existing(init, forked.thread, forked.session_configured)
|
||||
}
|
||||
@@ -536,6 +568,7 @@ impl App {
|
||||
|
||||
let mut app = Self {
|
||||
server: thread_manager.clone(),
|
||||
otel_manager: otel_manager.clone(),
|
||||
app_event_tx,
|
||||
chat_widget,
|
||||
auth_manager: auth_manager.clone(),
|
||||
@@ -553,7 +586,7 @@ impl App {
|
||||
feedback: feedback.clone(),
|
||||
pending_update_action: None,
|
||||
suppress_shutdown_complete: false,
|
||||
skip_world_writable_scan_once: false,
|
||||
windows_sandbox: WindowsSandboxState::default(),
|
||||
external_approval_routes: HashMap::new(),
|
||||
paused_codex_events: VecDeque::new(),
|
||||
};
|
||||
@@ -723,6 +756,7 @@ impl App {
|
||||
feedback: self.feedback.clone(),
|
||||
is_first_run: false,
|
||||
model: Some(model),
|
||||
otel_manager: self.otel_manager.clone(),
|
||||
};
|
||||
self.chat_widget = ChatWidget::new(init, self.server.clone());
|
||||
if let Some(summary) = summary {
|
||||
@@ -1060,7 +1094,16 @@ impl App {
|
||||
self.chat_widget.open_windows_sandbox_enable_prompt(preset);
|
||||
}
|
||||
AppEvent::OpenWindowsSandboxFallbackPrompt { preset, reason } => {
|
||||
self.otel_manager
|
||||
.counter("codex.windows_sandbox.fallback_prompt_shown", 1, &[]);
|
||||
self.chat_widget.clear_windows_sandbox_setup_status();
|
||||
if let Some(started_at) = self.windows_sandbox.setup_started_at.take() {
|
||||
self.otel_manager.record_duration(
|
||||
"codex.windows_sandbox.elevated_setup_duration_ms",
|
||||
started_at.elapsed(),
|
||||
&[("result", "failure")],
|
||||
);
|
||||
}
|
||||
self.chat_widget
|
||||
.open_windows_sandbox_fallback_prompt(preset, reason);
|
||||
}
|
||||
@@ -1087,6 +1130,8 @@ impl App {
|
||||
}
|
||||
|
||||
self.chat_widget.show_windows_sandbox_setup_status();
|
||||
self.windows_sandbox.setup_started_at = Some(Instant::now());
|
||||
let otel_manager = self.otel_manager.clone();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let result = codex_core::windows_sandbox::run_elevated_setup(
|
||||
&policy,
|
||||
@@ -1096,11 +1141,23 @@ impl App {
|
||||
codex_home.as_path(),
|
||||
);
|
||||
let event = match result {
|
||||
Ok(()) => AppEvent::EnableWindowsSandboxForAgentMode {
|
||||
preset: preset.clone(),
|
||||
mode: WindowsSandboxEnableMode::Elevated,
|
||||
},
|
||||
Ok(()) => {
|
||||
otel_manager.counter(
|
||||
"codex.windows_sandbox.elevated_setup_success",
|
||||
1,
|
||||
&[],
|
||||
);
|
||||
AppEvent::EnableWindowsSandboxForAgentMode {
|
||||
preset: preset.clone(),
|
||||
mode: WindowsSandboxEnableMode::Elevated,
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
otel_manager.counter(
|
||||
"codex.windows_sandbox.elevated_setup_failure",
|
||||
1,
|
||||
&[],
|
||||
);
|
||||
tracing::error!(
|
||||
error = %err,
|
||||
"failed to run elevated Windows sandbox setup"
|
||||
@@ -1123,6 +1180,13 @@ impl App {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
self.chat_widget.clear_windows_sandbox_setup_status();
|
||||
if let Some(started_at) = self.windows_sandbox.setup_started_at.take() {
|
||||
self.otel_manager.record_duration(
|
||||
"codex.windows_sandbox.elevated_setup_duration_ms",
|
||||
started_at.elapsed(),
|
||||
&[("result", "success")],
|
||||
);
|
||||
}
|
||||
let profile = self.active_profile.as_deref();
|
||||
let feature_key = Feature::WindowsSandbox.key();
|
||||
let elevated_key = Feature::WindowsSandboxElevated.key();
|
||||
@@ -1272,8 +1336,8 @@ impl App {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// One-shot suppression if the user just confirmed continue.
|
||||
if self.skip_world_writable_scan_once {
|
||||
self.skip_world_writable_scan_once = false;
|
||||
if self.windows_sandbox.skip_world_writable_scan_once {
|
||||
self.windows_sandbox.skip_world_writable_scan_once = false;
|
||||
return Ok(AppRunControl::Continue);
|
||||
}
|
||||
|
||||
@@ -1334,7 +1398,7 @@ impl App {
|
||||
}
|
||||
}
|
||||
AppEvent::SkipNextWorldWritableScan => {
|
||||
self.skip_world_writable_scan_once = true;
|
||||
self.windows_sandbox.skip_world_writable_scan_once = true;
|
||||
}
|
||||
AppEvent::UpdateFullAccessWarningAcknowledged(ack) => {
|
||||
self.chat_widget.set_full_access_warning_acknowledged(ack);
|
||||
@@ -1766,11 +1830,14 @@ mod tests {
|
||||
use codex_core::CodexAuth;
|
||||
use codex_core::ThreadManager;
|
||||
use codex_core::config::ConfigBuilder;
|
||||
use codex_core::models_manager::manager::ModelsManager;
|
||||
use codex_core::protocol::AskForApproval;
|
||||
use codex_core::protocol::Event;
|
||||
use codex_core::protocol::EventMsg;
|
||||
use codex_core::protocol::SandboxPolicy;
|
||||
use codex_core::protocol::SessionConfiguredEvent;
|
||||
use codex_core::protocol::SessionSource;
|
||||
use codex_otel::OtelManager;
|
||||
use codex_protocol::ThreadId;
|
||||
use insta::assert_snapshot;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -1790,9 +1857,12 @@ mod tests {
|
||||
let auth_manager =
|
||||
AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key"));
|
||||
let file_search = FileSearchManager::new(config.cwd.clone(), app_event_tx.clone());
|
||||
let model = ModelsManager::get_model_offline(config.model.as_deref());
|
||||
let otel_manager = test_otel_manager(&config, model.as_str());
|
||||
|
||||
App {
|
||||
server,
|
||||
otel_manager,
|
||||
app_event_tx,
|
||||
chat_widget,
|
||||
auth_manager,
|
||||
@@ -1810,7 +1880,7 @@ mod tests {
|
||||
feedback: codex_feedback::CodexFeedback::new(),
|
||||
pending_update_action: None,
|
||||
suppress_shutdown_complete: false,
|
||||
skip_world_writable_scan_once: false,
|
||||
windows_sandbox: WindowsSandboxState::default(),
|
||||
external_approval_routes: HashMap::new(),
|
||||
paused_codex_events: VecDeque::new(),
|
||||
}
|
||||
@@ -1830,10 +1900,13 @@ mod tests {
|
||||
let auth_manager =
|
||||
AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key"));
|
||||
let file_search = FileSearchManager::new(config.cwd.clone(), app_event_tx.clone());
|
||||
let model = ModelsManager::get_model_offline(config.model.as_deref());
|
||||
let otel_manager = test_otel_manager(&config, model.as_str());
|
||||
|
||||
(
|
||||
App {
|
||||
server,
|
||||
otel_manager,
|
||||
app_event_tx,
|
||||
chat_widget,
|
||||
auth_manager,
|
||||
@@ -1851,7 +1924,7 @@ mod tests {
|
||||
feedback: codex_feedback::CodexFeedback::new(),
|
||||
pending_update_action: None,
|
||||
suppress_shutdown_complete: false,
|
||||
skip_world_writable_scan_once: false,
|
||||
windows_sandbox: WindowsSandboxState::default(),
|
||||
external_approval_routes: HashMap::new(),
|
||||
paused_codex_events: VecDeque::new(),
|
||||
},
|
||||
@@ -1860,6 +1933,21 @@ mod tests {
|
||||
)
|
||||
}
|
||||
|
||||
fn test_otel_manager(config: &Config, model: &str) -> OtelManager {
|
||||
let model_info = ModelsManager::construct_model_info_offline(model, config);
|
||||
OtelManager::new(
|
||||
ThreadId::new(),
|
||||
model,
|
||||
model_info.slug.as_str(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
"test".to_string(),
|
||||
SessionSource::Cli,
|
||||
)
|
||||
}
|
||||
|
||||
fn all_model_presets() -> Vec<ModelPreset> {
|
||||
codex_core::models_manager::model_presets::all_model_presets().clone()
|
||||
}
|
||||
|
||||
@@ -89,6 +89,7 @@ use codex_core::protocol::WebSearchBeginEvent;
|
||||
use codex_core::protocol::WebSearchEndEvent;
|
||||
use codex_core::skills::model::SkillInterface;
|
||||
use codex_core::skills::model::SkillMetadata;
|
||||
use codex_otel::OtelManager;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::account::PlanType;
|
||||
use codex_protocol::approvals::ElicitationRequestEvent;
|
||||
@@ -359,6 +360,7 @@ pub(crate) struct ChatWidgetInit {
|
||||
pub(crate) feedback: codex_feedback::CodexFeedback,
|
||||
pub(crate) is_first_run: bool,
|
||||
pub(crate) model: Option<String>,
|
||||
pub(crate) otel_manager: OtelManager,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -413,6 +415,7 @@ pub(crate) struct ChatWidget {
|
||||
stored_collaboration_mode: CollaborationMode,
|
||||
auth_manager: Arc<AuthManager>,
|
||||
models_manager: Arc<ModelsManager>,
|
||||
otel_manager: OtelManager,
|
||||
session_header: SessionHeader,
|
||||
initial_user_message: Option<UserMessage>,
|
||||
token_info: Option<TokenUsageInfo>,
|
||||
@@ -1816,6 +1819,7 @@ impl ChatWidget {
|
||||
feedback,
|
||||
is_first_run,
|
||||
model,
|
||||
otel_manager,
|
||||
} = common;
|
||||
let model = model.filter(|m| !m.trim().is_empty());
|
||||
let mut config = config;
|
||||
@@ -1867,6 +1871,7 @@ impl ChatWidget {
|
||||
stored_collaboration_mode,
|
||||
auth_manager,
|
||||
models_manager,
|
||||
otel_manager,
|
||||
session_header: SessionHeader::new(model_for_header),
|
||||
initial_user_message,
|
||||
token_info: None,
|
||||
@@ -1934,6 +1939,7 @@ impl ChatWidget {
|
||||
models_manager,
|
||||
feedback,
|
||||
model,
|
||||
otel_manager,
|
||||
..
|
||||
} = common;
|
||||
let model = model.filter(|m| !m.trim().is_empty());
|
||||
@@ -1981,6 +1987,7 @@ impl ChatWidget {
|
||||
stored_collaboration_mode,
|
||||
auth_manager,
|
||||
models_manager,
|
||||
otel_manager,
|
||||
session_header: SessionHeader::new(header_model),
|
||||
initial_user_message,
|
||||
token_info: None,
|
||||
@@ -2296,11 +2303,17 @@ impl ChatWidget {
|
||||
return;
|
||||
}
|
||||
|
||||
self.otel_manager.counter(
|
||||
"codex.windows_sandbox.setup_elevated_sandbox_command",
|
||||
1,
|
||||
&[],
|
||||
);
|
||||
self.app_event_tx
|
||||
.send(AppEvent::BeginWindowsSandboxElevatedSetup { preset });
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
let _ = &self.otel_manager;
|
||||
// Not supported; on non-Windows this command should never be reachable.
|
||||
};
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests.rs
|
||||
assertion_line: 1980
|
||||
assertion_line: 2654
|
||||
expression: popup
|
||||
---
|
||||
Select Approval Mode
|
||||
|
||||
@@ -48,6 +48,7 @@ use codex_core::protocol::PatchApplyEndEvent;
|
||||
use codex_core::protocol::RateLimitWindow;
|
||||
use codex_core::protocol::ReviewRequest;
|
||||
use codex_core::protocol::ReviewTarget;
|
||||
use codex_core::protocol::SessionSource;
|
||||
use codex_core::protocol::StreamErrorEvent;
|
||||
use codex_core::protocol::TerminalInteractionEvent;
|
||||
use codex_core::protocol::TokenCountEvent;
|
||||
@@ -59,6 +60,7 @@ use codex_core::protocol::UndoCompletedEvent;
|
||||
use codex_core::protocol::UndoStartedEvent;
|
||||
use codex_core::protocol::ViewImageToolCallEvent;
|
||||
use codex_core::protocol::WarningEvent;
|
||||
use codex_otel::OtelManager;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::account::PlanType;
|
||||
use codex_protocol::config_types::CollaborationMode;
|
||||
@@ -700,6 +702,7 @@ async fn helpers_are_available_and_do_not_panic() {
|
||||
let tx = AppEventSender::new(tx_raw);
|
||||
let cfg = test_config().await;
|
||||
let resolved_model = ModelsManager::get_model_offline(cfg.model.as_deref());
|
||||
let otel_manager = test_otel_manager(&cfg, resolved_model.as_str());
|
||||
let thread_manager = Arc::new(ThreadManager::with_models_provider(
|
||||
CodexAuth::from_api_key("test"),
|
||||
cfg.model_provider.clone(),
|
||||
@@ -716,12 +719,28 @@ async fn helpers_are_available_and_do_not_panic() {
|
||||
feedback: codex_feedback::CodexFeedback::new(),
|
||||
is_first_run: true,
|
||||
model: Some(resolved_model),
|
||||
otel_manager,
|
||||
};
|
||||
let mut w = ChatWidget::new(init, thread_manager);
|
||||
// Basic construction sanity.
|
||||
let _ = &mut w;
|
||||
}
|
||||
|
||||
fn test_otel_manager(config: &Config, model: &str) -> OtelManager {
|
||||
let model_info = ModelsManager::construct_model_info_offline(model, config);
|
||||
OtelManager::new(
|
||||
ThreadId::new(),
|
||||
model,
|
||||
model_info.slug.as_str(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
"test".to_string(),
|
||||
SessionSource::Cli,
|
||||
)
|
||||
}
|
||||
|
||||
// --- Helpers for tests that need direct construction and event draining ---
|
||||
async fn make_chatwidget_manual(
|
||||
model_override: Option<&str>,
|
||||
@@ -740,6 +759,7 @@ async fn make_chatwidget_manual(
|
||||
if let Some(model) = model_override {
|
||||
cfg.model = Some(model.to_string());
|
||||
}
|
||||
let otel_manager = test_otel_manager(&cfg, resolved_model.as_str());
|
||||
let mut bottom = BottomPane::new(BottomPaneParams {
|
||||
app_event_tx: app_event_tx.clone(),
|
||||
frame_requester: FrameRequester::test_dummy(),
|
||||
@@ -782,6 +802,7 @@ async fn make_chatwidget_manual(
|
||||
stored_collaboration_mode,
|
||||
auth_manager,
|
||||
models_manager,
|
||||
otel_manager,
|
||||
session_header: SessionHeader::new(resolved_model),
|
||||
initial_user_message: None,
|
||||
token_info: None,
|
||||
|
||||
@@ -40,6 +40,7 @@ codex-core = { workspace = true }
|
||||
codex-feedback = { workspace = true }
|
||||
codex-file-search = { workspace = true }
|
||||
codex-login = { workspace = true }
|
||||
codex-otel = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
codex-utils-absolute-path = { workspace = true }
|
||||
codex-tui = { workspace = true }
|
||||
|
||||
+98
-10
@@ -45,6 +45,7 @@ use crate::tui::scrolling::TranscriptScroll;
|
||||
use crate::update_action::UpdateAction;
|
||||
use codex_ansi_escape::ansi_escape_line;
|
||||
use codex_core::AuthManager;
|
||||
use codex_core::CodexAuth;
|
||||
use codex_core::ThreadManager;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config::edit::ConfigEdit;
|
||||
@@ -63,6 +64,7 @@ use codex_core::protocol::SessionSource;
|
||||
use codex_core::protocol::SkillErrorInfo;
|
||||
use codex_core::protocol::TokenUsage;
|
||||
use codex_core::terminal::terminal_info;
|
||||
use codex_otel::OtelManager;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::openai_models::ModelPreset;
|
||||
use codex_protocol::openai_models::ModelUpgrade;
|
||||
@@ -89,6 +91,7 @@ use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
use tokio::select;
|
||||
use tokio::sync::mpsc::unbounded_channel;
|
||||
|
||||
@@ -356,6 +359,7 @@ async fn handle_model_migration_prompt_if_needed(
|
||||
|
||||
pub(crate) struct App {
|
||||
pub(crate) server: Arc<ThreadManager>,
|
||||
pub(crate) otel_manager: OtelManager,
|
||||
pub(crate) app_event_tx: AppEventSender,
|
||||
pub(crate) chat_widget: ChatWidget,
|
||||
pub(crate) auth_manager: Arc<AuthManager>,
|
||||
@@ -418,6 +422,12 @@ pub(crate) struct App {
|
||||
/// stopping a conversation (e.g., before starting a new one).
|
||||
suppress_shutdown_complete: bool,
|
||||
|
||||
windows_sandbox: WindowsSandboxState,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct WindowsSandboxState {
|
||||
setup_started_at: Option<Instant>,
|
||||
// One-shot suppression of the next world-writable scan after user confirmation.
|
||||
skip_world_writable_scan_once: bool,
|
||||
}
|
||||
@@ -439,6 +449,7 @@ impl App {
|
||||
feedback: self.feedback.clone(),
|
||||
is_first_run: false,
|
||||
model: Some(self.chat_widget.current_model().to_string()),
|
||||
otel_manager: self.otel_manager.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -498,6 +509,24 @@ impl App {
|
||||
model = updated_model;
|
||||
}
|
||||
|
||||
let auth = auth_manager.auth().await;
|
||||
let auth_ref = auth.as_ref();
|
||||
let model_info = thread_manager
|
||||
.get_models_manager()
|
||||
.get_model_info(model.as_str(), &config)
|
||||
.await;
|
||||
let otel_manager = OtelManager::new(
|
||||
ThreadId::new(),
|
||||
model.as_str(),
|
||||
model_info.slug.as_str(),
|
||||
auth_ref.and_then(CodexAuth::get_account_id),
|
||||
auth_ref.and_then(CodexAuth::get_account_email),
|
||||
auth_ref.map(|auth| auth.mode),
|
||||
config.otel.log_user_prompt,
|
||||
codex_core::terminal::user_agent(),
|
||||
SessionSource::Cli,
|
||||
);
|
||||
|
||||
let enhanced_keys_supported = tui.enhanced_keys_supported();
|
||||
let mut chat_widget = match session_selection {
|
||||
SessionSelection::StartFresh | SessionSelection::Exit => {
|
||||
@@ -517,6 +546,7 @@ impl App {
|
||||
feedback: feedback.clone(),
|
||||
is_first_run,
|
||||
model: Some(model.clone()),
|
||||
otel_manager: otel_manager.clone(),
|
||||
};
|
||||
ChatWidget::new(init, thread_manager.clone())
|
||||
}
|
||||
@@ -544,6 +574,7 @@ impl App {
|
||||
feedback: feedback.clone(),
|
||||
is_first_run,
|
||||
model: config.model.clone(),
|
||||
otel_manager: otel_manager.clone(),
|
||||
};
|
||||
ChatWidget::new_from_existing(init, resumed.thread, resumed.session_configured)
|
||||
}
|
||||
@@ -571,6 +602,7 @@ impl App {
|
||||
feedback: feedback.clone(),
|
||||
is_first_run,
|
||||
model: config.model.clone(),
|
||||
otel_manager: otel_manager.clone(),
|
||||
};
|
||||
ChatWidget::new_from_existing(init, forked.thread, forked.session_configured)
|
||||
}
|
||||
@@ -600,6 +632,7 @@ impl App {
|
||||
|
||||
let mut app = Self {
|
||||
server: thread_manager.clone(),
|
||||
otel_manager: otel_manager.clone(),
|
||||
app_event_tx,
|
||||
chat_widget,
|
||||
auth_manager: auth_manager.clone(),
|
||||
@@ -628,7 +661,7 @@ impl App {
|
||||
feedback: feedback.clone(),
|
||||
pending_update_action: None,
|
||||
suppress_shutdown_complete: false,
|
||||
skip_world_writable_scan_once: false,
|
||||
windows_sandbox: WindowsSandboxState::default(),
|
||||
};
|
||||
|
||||
// On startup, if Agent mode (workspace-write) or ReadOnly is active, warn about world-writable dirs on Windows.
|
||||
@@ -1495,6 +1528,7 @@ impl App {
|
||||
feedback: self.feedback.clone(),
|
||||
is_first_run: false,
|
||||
model: Some(model),
|
||||
otel_manager: self.otel_manager.clone(),
|
||||
};
|
||||
self.chat_widget = ChatWidget::new(init, self.server.clone());
|
||||
if let Some(summary) = summary {
|
||||
@@ -1755,7 +1789,16 @@ impl App {
|
||||
self.chat_widget.open_windows_sandbox_enable_prompt(preset);
|
||||
}
|
||||
AppEvent::OpenWindowsSandboxFallbackPrompt { preset, reason } => {
|
||||
self.otel_manager
|
||||
.counter("codex.windows_sandbox.fallback_prompt_shown", 1, &[]);
|
||||
self.chat_widget.clear_windows_sandbox_setup_status();
|
||||
if let Some(started_at) = self.windows_sandbox.setup_started_at.take() {
|
||||
self.otel_manager.record_duration(
|
||||
"codex.windows_sandbox.elevated_setup_duration_ms",
|
||||
started_at.elapsed(),
|
||||
&[("result", "failure")],
|
||||
);
|
||||
}
|
||||
self.chat_widget
|
||||
.open_windows_sandbox_fallback_prompt(preset, reason);
|
||||
}
|
||||
@@ -1782,6 +1825,8 @@ impl App {
|
||||
}
|
||||
|
||||
self.chat_widget.show_windows_sandbox_setup_status();
|
||||
self.windows_sandbox.setup_started_at = Some(Instant::now());
|
||||
let otel_manager = self.otel_manager.clone();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let result = codex_core::windows_sandbox::run_elevated_setup(
|
||||
&policy,
|
||||
@@ -1791,11 +1836,23 @@ impl App {
|
||||
codex_home.as_path(),
|
||||
);
|
||||
let event = match result {
|
||||
Ok(()) => AppEvent::EnableWindowsSandboxForAgentMode {
|
||||
preset: preset.clone(),
|
||||
mode: WindowsSandboxEnableMode::Elevated,
|
||||
},
|
||||
Ok(()) => {
|
||||
otel_manager.counter(
|
||||
"codex.windows_sandbox.elevated_setup_success",
|
||||
1,
|
||||
&[],
|
||||
);
|
||||
AppEvent::EnableWindowsSandboxForAgentMode {
|
||||
preset: preset.clone(),
|
||||
mode: WindowsSandboxEnableMode::Elevated,
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
otel_manager.counter(
|
||||
"codex.windows_sandbox.elevated_setup_failure",
|
||||
1,
|
||||
&[],
|
||||
);
|
||||
tracing::error!(
|
||||
error = %err,
|
||||
"failed to run elevated Windows sandbox setup"
|
||||
@@ -1818,6 +1875,13 @@ impl App {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
self.chat_widget.clear_windows_sandbox_setup_status();
|
||||
if let Some(started_at) = self.windows_sandbox.setup_started_at.take() {
|
||||
self.otel_manager.record_duration(
|
||||
"codex.windows_sandbox.elevated_setup_duration_ms",
|
||||
started_at.elapsed(),
|
||||
&[("result", "success")],
|
||||
);
|
||||
}
|
||||
let profile = self.active_profile.as_deref();
|
||||
let feature_key = Feature::WindowsSandbox.key();
|
||||
let elevated_key = Feature::WindowsSandboxElevated.key();
|
||||
@@ -1967,8 +2031,8 @@ impl App {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// One-shot suppression if the user just confirmed continue.
|
||||
if self.skip_world_writable_scan_once {
|
||||
self.skip_world_writable_scan_once = false;
|
||||
if self.windows_sandbox.skip_world_writable_scan_once {
|
||||
self.windows_sandbox.skip_world_writable_scan_once = false;
|
||||
return Ok(AppRunControl::Continue);
|
||||
}
|
||||
|
||||
@@ -2026,7 +2090,7 @@ impl App {
|
||||
}
|
||||
}
|
||||
AppEvent::SkipNextWorldWritableScan => {
|
||||
self.skip_world_writable_scan_once = true;
|
||||
self.windows_sandbox.skip_world_writable_scan_once = true;
|
||||
}
|
||||
AppEvent::UpdateFullAccessWarningAcknowledged(ack) => {
|
||||
self.chat_widget.set_full_access_warning_acknowledged(ack);
|
||||
@@ -2383,11 +2447,14 @@ mod tests {
|
||||
use codex_core::CodexAuth;
|
||||
use codex_core::ThreadManager;
|
||||
use codex_core::config::ConfigBuilder;
|
||||
use codex_core::models_manager::manager::ModelsManager;
|
||||
use codex_core::protocol::AskForApproval;
|
||||
use codex_core::protocol::Event;
|
||||
use codex_core::protocol::EventMsg;
|
||||
use codex_core::protocol::SandboxPolicy;
|
||||
use codex_core::protocol::SessionConfiguredEvent;
|
||||
use codex_core::protocol::SessionSource;
|
||||
use codex_otel::OtelManager;
|
||||
use codex_protocol::ThreadId;
|
||||
use insta::assert_snapshot;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -2407,9 +2474,12 @@ mod tests {
|
||||
let auth_manager =
|
||||
AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key"));
|
||||
let file_search = FileSearchManager::new(config.cwd.clone(), app_event_tx.clone());
|
||||
let model = ModelsManager::get_model_offline(config.model.as_deref());
|
||||
let otel_manager = test_otel_manager(&config, model.as_str());
|
||||
|
||||
App {
|
||||
server,
|
||||
otel_manager,
|
||||
app_event_tx,
|
||||
chat_widget,
|
||||
auth_manager,
|
||||
@@ -2440,7 +2510,7 @@ mod tests {
|
||||
feedback: codex_feedback::CodexFeedback::new(),
|
||||
pending_update_action: None,
|
||||
suppress_shutdown_complete: false,
|
||||
skip_world_writable_scan_once: false,
|
||||
windows_sandbox: WindowsSandboxState::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2458,10 +2528,13 @@ mod tests {
|
||||
let auth_manager =
|
||||
AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key"));
|
||||
let file_search = FileSearchManager::new(config.cwd.clone(), app_event_tx.clone());
|
||||
let model = ModelsManager::get_model_offline(config.model.as_deref());
|
||||
let otel_manager = test_otel_manager(&config, model.as_str());
|
||||
|
||||
(
|
||||
App {
|
||||
server,
|
||||
otel_manager,
|
||||
app_event_tx,
|
||||
chat_widget,
|
||||
auth_manager,
|
||||
@@ -2492,13 +2565,28 @@ mod tests {
|
||||
feedback: codex_feedback::CodexFeedback::new(),
|
||||
pending_update_action: None,
|
||||
suppress_shutdown_complete: false,
|
||||
skip_world_writable_scan_once: false,
|
||||
windows_sandbox: WindowsSandboxState::default(),
|
||||
},
|
||||
rx,
|
||||
op_rx,
|
||||
)
|
||||
}
|
||||
|
||||
fn test_otel_manager(config: &Config, model: &str) -> OtelManager {
|
||||
let model_info = ModelsManager::construct_model_info_offline(model, config);
|
||||
OtelManager::new(
|
||||
ThreadId::new(),
|
||||
model,
|
||||
model_info.slug.as_str(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
"test".to_string(),
|
||||
SessionSource::Cli,
|
||||
)
|
||||
}
|
||||
|
||||
fn all_model_presets() -> Vec<ModelPreset> {
|
||||
codex_core::models_manager::model_presets::all_model_presets().clone()
|
||||
}
|
||||
|
||||
@@ -88,6 +88,7 @@ use codex_core::protocol::WebSearchBeginEvent;
|
||||
use codex_core::protocol::WebSearchEndEvent;
|
||||
use codex_core::skills::model::SkillInterface;
|
||||
use codex_core::skills::model::SkillMetadata;
|
||||
use codex_otel::OtelManager;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::account::PlanType;
|
||||
use codex_protocol::approvals::ElicitationRequestEvent;
|
||||
@@ -314,6 +315,7 @@ pub(crate) struct ChatWidgetInit {
|
||||
pub(crate) feedback: codex_feedback::CodexFeedback,
|
||||
pub(crate) is_first_run: bool,
|
||||
pub(crate) model: Option<String>,
|
||||
pub(crate) otel_manager: OtelManager,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -360,6 +362,7 @@ pub(crate) struct ChatWidget {
|
||||
stored_collaboration_mode: CollaborationMode,
|
||||
auth_manager: Arc<AuthManager>,
|
||||
models_manager: Arc<ModelsManager>,
|
||||
otel_manager: OtelManager,
|
||||
session_header: SessionHeader,
|
||||
initial_user_message: Option<UserMessage>,
|
||||
token_info: Option<TokenUsageInfo>,
|
||||
@@ -1604,6 +1607,7 @@ impl ChatWidget {
|
||||
feedback,
|
||||
is_first_run,
|
||||
model,
|
||||
otel_manager,
|
||||
} = common;
|
||||
let model = model.filter(|m| !m.trim().is_empty());
|
||||
let mut config = config;
|
||||
@@ -1654,6 +1658,7 @@ impl ChatWidget {
|
||||
stored_collaboration_mode,
|
||||
auth_manager,
|
||||
models_manager,
|
||||
otel_manager,
|
||||
session_header: SessionHeader::new(model_for_header),
|
||||
initial_user_message,
|
||||
token_info: None,
|
||||
@@ -1718,6 +1723,7 @@ impl ChatWidget {
|
||||
models_manager,
|
||||
feedback,
|
||||
model,
|
||||
otel_manager,
|
||||
..
|
||||
} = common;
|
||||
let model = model.filter(|m| !m.trim().is_empty());
|
||||
@@ -1766,6 +1772,7 @@ impl ChatWidget {
|
||||
stored_collaboration_mode,
|
||||
auth_manager,
|
||||
models_manager,
|
||||
otel_manager,
|
||||
session_header: SessionHeader::new(header_model),
|
||||
initial_user_message,
|
||||
token_info: None,
|
||||
@@ -2053,11 +2060,17 @@ impl ChatWidget {
|
||||
return;
|
||||
}
|
||||
|
||||
self.otel_manager.counter(
|
||||
"codex.windows_sandbox.setup_elevated_sandbox_command",
|
||||
1,
|
||||
&[],
|
||||
);
|
||||
self.app_event_tx
|
||||
.send(AppEvent::BeginWindowsSandboxElevatedSetup { preset });
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
let _ = &self.otel_manager;
|
||||
// Not supported; on non-Windows this command should never be reachable.
|
||||
};
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,5 +1,6 @@
|
||||
---
|
||||
source: tui2/src/chatwidget/tests.rs
|
||||
assertion_line: 2370
|
||||
expression: popup
|
||||
---
|
||||
Select Approval Mode
|
||||
@@ -11,4 +12,3 @@ expression: popup
|
||||
using.
|
||||
|
||||
Press enter to confirm or esc to go back
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ use codex_core::protocol::PatchApplyEndEvent;
|
||||
use codex_core::protocol::RateLimitWindow;
|
||||
use codex_core::protocol::ReviewRequest;
|
||||
use codex_core::protocol::ReviewTarget;
|
||||
use codex_core::protocol::SessionSource;
|
||||
use codex_core::protocol::StreamErrorEvent;
|
||||
use codex_core::protocol::TokenCountEvent;
|
||||
use codex_core::protocol::TokenUsage;
|
||||
@@ -56,6 +57,7 @@ use codex_core::protocol::UndoCompletedEvent;
|
||||
use codex_core::protocol::UndoStartedEvent;
|
||||
use codex_core::protocol::ViewImageToolCallEvent;
|
||||
use codex_core::protocol::WarningEvent;
|
||||
use codex_otel::OtelManager;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::account::PlanType;
|
||||
use codex_protocol::config_types::CollaborationMode;
|
||||
@@ -689,6 +691,8 @@ async fn helpers_are_available_and_do_not_panic() {
|
||||
let tx = AppEventSender::new(tx_raw);
|
||||
let cfg = test_config().await;
|
||||
let model = cfg.model.clone();
|
||||
let resolved_model = ModelsManager::get_model_offline(cfg.model.as_deref());
|
||||
let otel_manager = test_otel_manager(&cfg, resolved_model.as_str());
|
||||
let thread_manager = Arc::new(ThreadManager::with_models_provider(
|
||||
CodexAuth::from_api_key("test"),
|
||||
cfg.model_provider.clone(),
|
||||
@@ -705,12 +709,28 @@ async fn helpers_are_available_and_do_not_panic() {
|
||||
feedback: codex_feedback::CodexFeedback::new(),
|
||||
is_first_run: true,
|
||||
model,
|
||||
otel_manager,
|
||||
};
|
||||
let mut w = ChatWidget::new(init, thread_manager);
|
||||
// Basic construction sanity.
|
||||
let _ = &mut w;
|
||||
}
|
||||
|
||||
fn test_otel_manager(config: &Config, model: &str) -> OtelManager {
|
||||
let model_info = ModelsManager::construct_model_info_offline(model, config);
|
||||
OtelManager::new(
|
||||
ThreadId::new(),
|
||||
model,
|
||||
model_info.slug.as_str(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
"test".to_string(),
|
||||
SessionSource::Cli,
|
||||
)
|
||||
}
|
||||
|
||||
// --- Helpers for tests that need direct construction and event draining ---
|
||||
async fn make_chatwidget_manual(
|
||||
model_override: Option<&str>,
|
||||
@@ -729,6 +749,7 @@ async fn make_chatwidget_manual(
|
||||
if let Some(model) = model_override {
|
||||
cfg.model = Some(model.to_string());
|
||||
}
|
||||
let otel_manager = test_otel_manager(&cfg, resolved_model.as_str());
|
||||
let mut bottom = BottomPane::new(BottomPaneParams {
|
||||
app_event_tx: app_event_tx.clone(),
|
||||
frame_requester: FrameRequester::test_dummy(),
|
||||
@@ -771,6 +792,7 @@ async fn make_chatwidget_manual(
|
||||
stored_collaboration_mode,
|
||||
auth_manager,
|
||||
models_manager,
|
||||
otel_manager,
|
||||
session_header: SessionHeader::new(resolved_model),
|
||||
initial_user_message: None,
|
||||
token_info: None,
|
||||
|
||||
Reference in New Issue
Block a user