mirror of
https://github.com/openai/codex.git
synced 2026-09-17 12:23:33 +00:00
Merge b4f38201fe into sapling-pr-archive-bolinfest
This commit is contained in:
@@ -15,13 +15,12 @@ pub fn create_config_summary_entries(config: &Config) -> Vec<(&'static str, Stri
|
||||
if config.model_provider.wire_api == WireApi::Responses
|
||||
&& config.model_family.supports_reasoning_summaries
|
||||
{
|
||||
entries.push((
|
||||
"reasoning effort",
|
||||
config
|
||||
.model_reasoning_effort
|
||||
.map(|effort| effort.to_string())
|
||||
.unwrap_or_else(|| "none".to_string()),
|
||||
));
|
||||
let reasoning_effort = config
|
||||
.model_reasoning_effort
|
||||
.or(config.model_family.default_reasoning_effort)
|
||||
.map(|effort| effort.to_string())
|
||||
.unwrap_or_else(|| "none".to_string());
|
||||
entries.push(("reasoning effort", reasoning_effort));
|
||||
entries.push((
|
||||
"reasoning summaries",
|
||||
config.model_reasoning_summary.to_string(),
|
||||
|
||||
@@ -204,10 +204,21 @@ pub async fn default_user_shell() -> Shell {
|
||||
if cfg!(windows) {
|
||||
get_shell(ShellType::PowerShell, None).unwrap_or(Shell::Unknown)
|
||||
} else {
|
||||
get_user_shell_path()
|
||||
let user_default_shell = get_user_shell_path()
|
||||
.and_then(|shell| detect_shell_type(&shell))
|
||||
.and_then(|shell_type| get_shell(shell_type, None))
|
||||
.unwrap_or(Shell::Unknown)
|
||||
.and_then(|shell_type| get_shell(shell_type, None));
|
||||
|
||||
let shell_with_fallback = if cfg!(target_os = "macos") {
|
||||
user_default_shell
|
||||
.or_else(|| get_shell(ShellType::Zsh, None))
|
||||
.or_else(|| get_shell(ShellType::Bash, None))
|
||||
} else {
|
||||
user_default_shell
|
||||
.or_else(|| get_shell(ShellType::Bash, None))
|
||||
.or_else(|| get_shell(ShellType::Zsh, None))
|
||||
};
|
||||
|
||||
shell_with_fallback.unwrap_or(Shell::Unknown)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,6 @@ use rmcp::model::CreateElicitationRequestParam;
|
||||
use rmcp::model::CreateElicitationResult;
|
||||
use rmcp::model::ElicitationAction;
|
||||
use rmcp::model::ElicitationSchema;
|
||||
use rmcp::model::PrimitiveSchema;
|
||||
use rmcp::model::StringSchema;
|
||||
use rmcp::service::RequestContext;
|
||||
|
||||
use crate::posix::escalate_protocol::EscalateAction;
|
||||
@@ -54,12 +52,19 @@ impl McpEscalationPolicy {
|
||||
context
|
||||
.peer
|
||||
.create_elicitation(CreateElicitationRequestParam {
|
||||
message: format!("Allow Codex to run `{command:?}` in `{workdir:?}`?"),
|
||||
#[allow(clippy::expect_used)]
|
||||
message: format!("Allow agent to run `{command}` in `{}`?", workdir.display()),
|
||||
requested_schema: ElicitationSchema::builder()
|
||||
.property("dummy", PrimitiveSchema::String(StringSchema::new()))
|
||||
.title("Execution Permission Request")
|
||||
.optional_string_with("reason", |schema| {
|
||||
schema.description("Optional reason for allowing or denying execution")
|
||||
})
|
||||
.build()
|
||||
.expect("failed to build elicitation schema"),
|
||||
.map_err(|e| {
|
||||
McpError::internal_error(
|
||||
format!("failed to build elicitation schema: {e}"),
|
||||
None,
|
||||
)
|
||||
})?,
|
||||
})
|
||||
.await
|
||||
.map_err(|e| McpError::internal_error(e.to_string(), None))
|
||||
|
||||
@@ -114,6 +114,11 @@ impl BottomPane {
|
||||
self.status.as_ref()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn context_window_percent(&self) -> Option<i64> {
|
||||
self.context_window_percent
|
||||
}
|
||||
|
||||
fn active_view(&self) -> Option<&dyn BottomPaneView> {
|
||||
self.view_stack.last().map(std::convert::AsRef::as_ref)
|
||||
}
|
||||
|
||||
@@ -290,6 +290,8 @@ pub(crate) struct ChatWidget {
|
||||
pending_notification: Option<Notification>,
|
||||
// Simple review mode flag; used to adjust layout and banners.
|
||||
is_review_mode: bool,
|
||||
// Snapshot of token usage to restore after review mode exits.
|
||||
pre_review_token_info: Option<Option<TokenUsageInfo>>,
|
||||
// Whether to add a final message separator after the last message
|
||||
needs_final_message_separator: bool,
|
||||
|
||||
@@ -489,16 +491,39 @@ impl ChatWidget {
|
||||
}
|
||||
|
||||
pub(crate) fn set_token_info(&mut self, info: Option<TokenUsageInfo>) {
|
||||
if let Some(info) = info {
|
||||
let context_window = info
|
||||
.model_context_window
|
||||
.or(self.config.model_context_window);
|
||||
let percent = context_window.map(|window| {
|
||||
match info {
|
||||
Some(info) => self.apply_token_info(info),
|
||||
None => {
|
||||
self.bottom_pane.set_context_window_percent(None);
|
||||
self.token_info = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_token_info(&mut self, info: TokenUsageInfo) {
|
||||
let percent = self.context_remaining_percent(&info);
|
||||
self.bottom_pane.set_context_window_percent(percent);
|
||||
self.token_info = Some(info);
|
||||
}
|
||||
|
||||
fn context_remaining_percent(&self, info: &TokenUsageInfo) -> Option<i64> {
|
||||
info.model_context_window
|
||||
.or(self.config.model_context_window)
|
||||
.map(|window| {
|
||||
info.last_token_usage
|
||||
.percent_of_context_window_remaining(window)
|
||||
});
|
||||
self.bottom_pane.set_context_window_percent(percent);
|
||||
self.token_info = Some(info);
|
||||
})
|
||||
}
|
||||
|
||||
fn restore_pre_review_token_info(&mut self) {
|
||||
if let Some(saved) = self.pre_review_token_info.take() {
|
||||
match saved {
|
||||
Some(info) => self.apply_token_info(info),
|
||||
None => {
|
||||
self.bottom_pane.set_context_window_percent(None);
|
||||
self.token_info = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1150,6 +1175,7 @@ impl ChatWidget {
|
||||
suppress_session_configured_redraw: false,
|
||||
pending_notification: None,
|
||||
is_review_mode: false,
|
||||
pre_review_token_info: None,
|
||||
needs_final_message_separator: false,
|
||||
last_rendered_width: std::cell::Cell::new(None),
|
||||
feedback,
|
||||
@@ -1223,6 +1249,7 @@ impl ChatWidget {
|
||||
suppress_session_configured_redraw: true,
|
||||
pending_notification: None,
|
||||
is_review_mode: false,
|
||||
pre_review_token_info: None,
|
||||
needs_final_message_separator: false,
|
||||
last_rendered_width: std::cell::Cell::new(None),
|
||||
feedback,
|
||||
@@ -1693,6 +1720,9 @@ impl ChatWidget {
|
||||
|
||||
fn on_entered_review_mode(&mut self, review: ReviewRequest) {
|
||||
// Enter review mode and emit a concise banner
|
||||
if self.pre_review_token_info.is_none() {
|
||||
self.pre_review_token_info = Some(self.token_info.clone());
|
||||
}
|
||||
self.is_review_mode = true;
|
||||
let banner = format!(">> Code review started: {} <<", review.user_facing_hint);
|
||||
self.add_to_history(history_cell::new_review_status_line(banner));
|
||||
@@ -1733,6 +1763,7 @@ impl ChatWidget {
|
||||
}
|
||||
|
||||
self.is_review_mode = false;
|
||||
self.restore_pre_review_token_info();
|
||||
// Append a finishing banner at the end of this turn.
|
||||
self.add_to_history(history_cell::new_review_status_line(
|
||||
"<< Code review finished >>".to_string(),
|
||||
|
||||
@@ -38,6 +38,9 @@ use codex_core::protocol::ReviewRequest;
|
||||
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::UndoCompletedEvent;
|
||||
use codex_core::protocol::UndoStartedEvent;
|
||||
use codex_core::protocol::ViewImageToolCallEvent;
|
||||
@@ -215,6 +218,81 @@ fn exited_review_mode_emits_results_and_finishes() {
|
||||
assert!(!chat.is_review_mode);
|
||||
}
|
||||
|
||||
/// Exiting review restores the pre-review context window indicator.
|
||||
#[test]
|
||||
fn review_restores_context_window_indicator() {
|
||||
let (mut chat, mut rx, _ops) = make_chatwidget_manual();
|
||||
|
||||
let context_window = 13_000;
|
||||
let pre_review_tokens = 12_700; // ~30% remaining after subtracting baseline.
|
||||
let review_tokens = 12_030; // ~97% remaining after subtracting baseline.
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "token-before".into(),
|
||||
msg: EventMsg::TokenCount(TokenCountEvent {
|
||||
info: Some(make_token_info(pre_review_tokens, context_window)),
|
||||
rate_limits: None,
|
||||
}),
|
||||
});
|
||||
assert_eq!(chat.bottom_pane.context_window_percent(), Some(30));
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "review-start".into(),
|
||||
msg: EventMsg::EnteredReviewMode(ReviewRequest {
|
||||
prompt: "Review the latest changes".to_string(),
|
||||
user_facing_hint: "feature branch".to_string(),
|
||||
append_to_original_thread: true,
|
||||
}),
|
||||
});
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "token-review".into(),
|
||||
msg: EventMsg::TokenCount(TokenCountEvent {
|
||||
info: Some(make_token_info(review_tokens, context_window)),
|
||||
rate_limits: None,
|
||||
}),
|
||||
});
|
||||
assert_eq!(chat.bottom_pane.context_window_percent(), Some(97));
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "review-end".into(),
|
||||
msg: EventMsg::ExitedReviewMode(ExitedReviewModeEvent {
|
||||
review_output: None,
|
||||
}),
|
||||
});
|
||||
let _ = drain_insert_history(&mut rx);
|
||||
|
||||
assert_eq!(chat.bottom_pane.context_window_percent(), Some(30));
|
||||
assert!(!chat.is_review_mode);
|
||||
}
|
||||
|
||||
/// Receiving a TokenCount event without usage clears the context indicator.
|
||||
#[test]
|
||||
fn token_count_none_resets_context_indicator() {
|
||||
let (mut chat, _rx, _ops) = make_chatwidget_manual();
|
||||
|
||||
let context_window = 13_000;
|
||||
let pre_compact_tokens = 12_700;
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "token-before".into(),
|
||||
msg: EventMsg::TokenCount(TokenCountEvent {
|
||||
info: Some(make_token_info(pre_compact_tokens, context_window)),
|
||||
rate_limits: None,
|
||||
}),
|
||||
});
|
||||
assert_eq!(chat.bottom_pane.context_window_percent(), Some(30));
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "token-cleared".into(),
|
||||
msg: EventMsg::TokenCount(TokenCountEvent {
|
||||
info: None,
|
||||
rate_limits: None,
|
||||
}),
|
||||
});
|
||||
assert_eq!(chat.bottom_pane.context_window_percent(), None);
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
target_os = "macos",
|
||||
ignore = "system configuration APIs are blocked under macOS seatbelt"
|
||||
@@ -292,6 +370,7 @@ fn make_chatwidget_manual() -> (
|
||||
suppress_session_configured_redraw: false,
|
||||
pending_notification: None,
|
||||
is_review_mode: false,
|
||||
pre_review_token_info: None,
|
||||
needs_final_message_separator: false,
|
||||
last_rendered_width: std::cell::Cell::new(None),
|
||||
feedback: codex_feedback::CodexFeedback::new(),
|
||||
@@ -338,6 +417,21 @@ fn lines_to_single_string(lines: &[ratatui::text::Line<'static>]) -> String {
|
||||
s
|
||||
}
|
||||
|
||||
fn make_token_info(total_tokens: i64, context_window: i64) -> TokenUsageInfo {
|
||||
fn usage(total_tokens: i64) -> TokenUsage {
|
||||
TokenUsage {
|
||||
total_tokens,
|
||||
..TokenUsage::default()
|
||||
}
|
||||
}
|
||||
|
||||
TokenUsageInfo {
|
||||
total_token_usage: usage(total_tokens),
|
||||
last_token_usage: usage(total_tokens),
|
||||
model_context_window: Some(context_window),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rate_limit_warnings_emit_thresholds() {
|
||||
let mut state = RateLimitWarningState::default();
|
||||
|
||||
@@ -9,15 +9,29 @@ use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
use windows_sys::Win32::Foundation::CloseHandle;
|
||||
use windows_sys::Win32::Foundation::LocalFree;
|
||||
use windows_sys::Win32::Foundation::ERROR_SUCCESS;
|
||||
use windows_sys::Win32::Foundation::HLOCAL;
|
||||
use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;
|
||||
use windows_sys::Win32::Foundation::LocalFree;
|
||||
use windows_sys::Win32::Security::ACCESS_ALLOWED_ACE;
|
||||
use windows_sys::Win32::Security::ACE_HEADER;
|
||||
use windows_sys::Win32::Security::ACL;
|
||||
use windows_sys::Win32::Security::ACL_SIZE_INFORMATION;
|
||||
use windows_sys::Win32::Security::AclSizeInformation;
|
||||
use windows_sys::Win32::Security::Authorization::GetNamedSecurityInfoW;
|
||||
use windows_sys::Win32::Security::Authorization::GetSecurityInfo;
|
||||
use windows_sys::Win32::Security::DACL_SECURITY_INFORMATION;
|
||||
use windows_sys::Win32::Security::EqualSid;
|
||||
use windows_sys::Win32::Security::GetAce;
|
||||
use windows_sys::Win32::Security::GetAclInformation;
|
||||
use windows_sys::Win32::Security::MapGenericMask;
|
||||
use windows_sys::Win32::Security::GENERIC_MAPPING;
|
||||
use windows_sys::Win32::Storage::FileSystem::CreateFileW;
|
||||
use windows_sys::Win32::Storage::FileSystem::FILE_ALL_ACCESS;
|
||||
use windows_sys::Win32::Storage::FileSystem::FILE_APPEND_DATA;
|
||||
use windows_sys::Win32::Storage::FileSystem::FILE_FLAG_BACKUP_SEMANTICS;
|
||||
use windows_sys::Win32::Storage::FileSystem::FILE_GENERIC_EXECUTE;
|
||||
use windows_sys::Win32::Storage::FileSystem::FILE_GENERIC_READ;
|
||||
use windows_sys::Win32::Storage::FileSystem::FILE_GENERIC_WRITE;
|
||||
use windows_sys::Win32::Storage::FileSystem::FILE_SHARE_DELETE;
|
||||
use windows_sys::Win32::Storage::FileSystem::FILE_SHARE_READ;
|
||||
@@ -26,17 +40,6 @@ use windows_sys::Win32::Storage::FileSystem::FILE_WRITE_ATTRIBUTES;
|
||||
use windows_sys::Win32::Storage::FileSystem::FILE_WRITE_DATA;
|
||||
use windows_sys::Win32::Storage::FileSystem::FILE_WRITE_EA;
|
||||
use windows_sys::Win32::Storage::FileSystem::OPEN_EXISTING;
|
||||
const GENERIC_ALL_MASK: u32 = 0x1000_0000;
|
||||
const GENERIC_WRITE_MASK: u32 = 0x4000_0000;
|
||||
use windows_sys::Win32::Security::AclSizeInformation;
|
||||
use windows_sys::Win32::Security::EqualSid;
|
||||
use windows_sys::Win32::Security::GetAce;
|
||||
use windows_sys::Win32::Security::GetAclInformation;
|
||||
use windows_sys::Win32::Security::ACCESS_ALLOWED_ACE;
|
||||
use windows_sys::Win32::Security::ACE_HEADER;
|
||||
use windows_sys::Win32::Security::ACL;
|
||||
use windows_sys::Win32::Security::ACL_SIZE_INFORMATION;
|
||||
use windows_sys::Win32::Security::DACL_SECURITY_INFORMATION;
|
||||
|
||||
// Preflight scan limits
|
||||
const MAX_ITEMS_PER_DIR: i32 = 1000;
|
||||
@@ -304,7 +307,7 @@ pub fn world_writable_warning_details(
|
||||
}
|
||||
}
|
||||
// Fast mask-based check: does the DACL contain any ACCESS_ALLOWED ACE for
|
||||
// Everyone that includes generic or specific write bits? Skips inherit-only
|
||||
// Everyone that grants write after generic bits are expanded? Skips inherit-only
|
||||
// ACEs (do not apply to the current object).
|
||||
unsafe fn dacl_quick_world_write_mask_allows(p_dacl: *mut ACL, psid_world: *mut c_void) -> bool {
|
||||
if p_dacl.is_null() {
|
||||
@@ -321,6 +324,12 @@ unsafe fn dacl_quick_world_write_mask_allows(p_dacl: *mut ACL, psid_world: *mut
|
||||
if ok == 0 {
|
||||
return false;
|
||||
}
|
||||
let mapping = GENERIC_MAPPING {
|
||||
GenericRead: FILE_GENERIC_READ,
|
||||
GenericWrite: FILE_GENERIC_WRITE,
|
||||
GenericExecute: FILE_GENERIC_EXECUTE,
|
||||
GenericAll: FILE_ALL_ACCESS,
|
||||
};
|
||||
for i in 0..(info.AceCount as usize) {
|
||||
let mut p_ace: *mut c_void = std::ptr::null_mut();
|
||||
if GetAce(p_dacl as *const ACL, i as u32, &mut p_ace) == 0 {
|
||||
@@ -337,19 +346,16 @@ unsafe fn dacl_quick_world_write_mask_allows(p_dacl: *mut ACL, psid_world: *mut
|
||||
let base = p_ace as usize;
|
||||
let sid_ptr =
|
||||
(base + std::mem::size_of::<ACE_HEADER>() + std::mem::size_of::<u32>()) as *mut c_void; // skip header + mask
|
||||
if EqualSid(sid_ptr, psid_world) != 0 {
|
||||
let ace = &*(p_ace as *const ACCESS_ALLOWED_ACE);
|
||||
let mask = ace.Mask;
|
||||
let writey = FILE_GENERIC_WRITE
|
||||
| FILE_WRITE_DATA
|
||||
| FILE_APPEND_DATA
|
||||
| FILE_WRITE_EA
|
||||
| FILE_WRITE_ATTRIBUTES
|
||||
| GENERIC_WRITE_MASK
|
||||
| GENERIC_ALL_MASK;
|
||||
if (mask & writey) != 0 {
|
||||
return true;
|
||||
}
|
||||
if EqualSid(sid_ptr, psid_world) == 0 {
|
||||
continue;
|
||||
}
|
||||
let ace = &*(p_ace as *const ACCESS_ALLOWED_ACE);
|
||||
let mut mask = ace.Mask;
|
||||
// Expand generic bits to concrete file rights before checking for write.
|
||||
MapGenericMask(&mut mask, &mapping);
|
||||
let write_mask = FILE_WRITE_DATA | FILE_APPEND_DATA | FILE_WRITE_EA | FILE_WRITE_ATTRIBUTES;
|
||||
if (mask & write_mask) != 0 {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
|
||||
Reference in New Issue
Block a user