chore: sync local changes (core + TUI)

- Update core: codex.rs, config.rs, safety.rs
- Update TUI: app.rs, app_event.rs, bottom_pane/*, chatwidget.rs, lib.rs, user_approval_widget.rs
- Update README
This commit is contained in:
James Peter
2025-09-08 23:56:31 +10:00
parent e956e13633
commit 0a34e91252
11 changed files with 422 additions and 26 deletions
+1
View File
@@ -125,3 +125,4 @@ This folder is the root of a Cargo workspace. It contains quite a bit of experim
- [`exec/`](./exec) "headless" CLI for use in automation.
- [`tui/`](./tui) CLI that launches a fullscreen TUI built with [Ratatui](https://ratatui.rs/).
- [`cli/`](./cli) CLI multitool that provides the aforementioned CLIs via subcommands.
+12 -3
View File
@@ -5244,12 +5244,21 @@ async fn handle_sandbox_error(
// we're letting the model manage escalation requests. Otherwise, continue
match sess.approval_policy {
AskForApproval::Never | AskForApproval::OnRequest => {
// Clarify when Read Only mode is the reason a command cannot proceed.
let content = if matches!(sess.sandbox_policy, SandboxPolicy::ReadOnly) {
format!(
"command blocked by Read Only mode: {}",
error
)
} else {
format!(
"failed in sandbox {sandbox_type:?} with execution error: {error}"
)
};
return ResponseInputItem::FunctionCallOutput {
call_id,
output: FunctionCallOutputPayload {
content: format!(
"failed in sandbox {sandbox_type:?} with execution error: {error}"
),
content,
success: Some(false),
},
};
+116 -6
View File
@@ -426,6 +426,73 @@ pub fn set_github_check_on_push(codex_home: &Path, enabled: bool) -> anyhow::Res
Ok(())
}
/// Persist per-project access mode under `[projects."<path>"]` with
/// `approval_policy` and `sandbox_mode`.
pub fn set_project_access_mode(
codex_home: &Path,
project_path: &Path,
approval: AskForApproval,
sandbox_mode: SandboxMode,
) -> anyhow::Result<()> {
let config_path = codex_home.join(CONFIG_TOML_FILE);
// Parse existing config if present; otherwise start a new document.
let mut doc = match std::fs::read_to_string(config_path.clone()) {
Ok(s) => s.parse::<DocumentMut>()?,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => DocumentMut::new(),
Err(e) => return Err(e.into()),
};
// Ensure projects table and the per-project table exist
let project_key = project_path.to_string_lossy().to_string();
if !doc.as_table().contains_key("projects") {
doc["projects"] = TomlItem::Table(toml_edit::Table::new());
}
let projects_tbl = doc["projects"].as_table_mut().unwrap();
if !projects_tbl.contains_key(project_key.as_str()) {
projects_tbl.insert(project_key.as_str(), TomlItem::Table(toml_edit::Table::new()));
}
let proj_tbl = projects_tbl
.get_mut(project_key.as_str())
.and_then(|i| i.as_table_mut())
.ok_or_else(|| anyhow::anyhow!("failed to create projects.{} table", project_key))?;
// Write fields
proj_tbl.insert(
"approval_policy",
TomlItem::Value(toml_edit::Value::from(format!("{}", approval))),
);
proj_tbl.insert(
"sandbox_mode",
TomlItem::Value(toml_edit::Value::from(format!("{}", sandbox_mode))),
);
// Harmonize trust_level with selected access mode:
// - Full Access (Never + DangerFullAccess): set trust_level = "trusted" so future runs
// default to non-interactive behavior when no overrides are present.
// - Other modes: remove trust_level to avoid conflicting with per-project policy.
let full_access = matches!(
(approval, sandbox_mode),
(AskForApproval::Never, SandboxMode::DangerFullAccess)
);
if full_access {
proj_tbl.insert(
"trust_level",
TomlItem::Value(toml_edit::Value::from("trusted")),
);
} else {
proj_tbl.remove("trust_level");
}
// Ensure home exists; write atomically
std::fs::create_dir_all(codex_home)?;
let tmp = NamedTempFile::new_in(codex_home)?;
std::fs::write(tmp.path(), doc.to_string())?;
tmp.persist(config_path)?;
Ok(())
}
/// List MCP servers from `CODEX_HOME/config.toml`.
/// Returns `(enabled, disabled)` lists of `(name, McpServerConfig)`.
pub fn list_mcp_servers(codex_home: &Path) -> anyhow::Result<(
@@ -766,9 +833,11 @@ pub struct ConfigToml {
pub github: Option<GithubConfig>,
}
#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
#[derive(Deserialize, Debug, Clone)]
pub struct ProjectConfig {
pub trust_level: Option<String>,
pub approval_policy: Option<AskForApproval>,
pub sandbox_mode: Option<SandboxMode>,
}
#[derive(Deserialize, Debug, Clone, Default)]
@@ -792,6 +861,7 @@ pub struct ToolsToml {
impl ConfigToml {
/// Derive the effective sandbox policy from the configuration.
#[cfg(test)]
fn derive_sandbox_policy(&self, sandbox_mode_override: Option<SandboxMode>) -> SandboxPolicy {
let resolved_sandbox_mode = sandbox_mode_override
.or(self.sandbox_mode)
@@ -925,7 +995,7 @@ impl Config {
None => ConfigProfile::default(),
};
let sandbox_policy = cfg.derive_sandbox_policy(sandbox_mode);
// (removed placeholder) sandbox_policy computed below after resolving project overrides.
let mut model_providers = built_in_model_providers();
// Merge user-defined providers into the built-in list.
@@ -947,6 +1017,9 @@ impl Config {
})?
.clone();
// Capture workspace-write details early to avoid borrow after partial moves
let cfg_workspace = cfg.sandbox_workspace_write.clone();
let shell_environment_policy = cfg.shell_environment_policy.into();
let mut resolved_cwd = {
@@ -983,6 +1056,46 @@ impl Config {
}
}
// Project-specific overrides based on final resolved cwd (exact match)
let project_key = resolved_cwd.to_string_lossy().to_string();
let project_override = cfg
.projects
.as_ref()
.and_then(|m| m.get(&project_key));
// Resolve sandbox mode with correct precedence:
// CLI override > per-project override > global config.toml > default
let effective_sandbox_mode = sandbox_mode
.or(project_override.and_then(|p| p.sandbox_mode))
.or(cfg.sandbox_mode)
.unwrap_or_default();
let sandbox_policy = match effective_sandbox_mode {
SandboxMode::ReadOnly => SandboxPolicy::new_read_only_policy(),
SandboxMode::WorkspaceWrite => match cfg_workspace {
Some(SandboxWorkspaceWrite {
writable_roots,
network_access,
exclude_tmpdir_env_var,
exclude_slash_tmp,
allow_git_writes,
}) => SandboxPolicy::WorkspaceWrite {
writable_roots,
network_access,
exclude_tmpdir_env_var,
exclude_slash_tmp,
allow_git_writes,
},
None => SandboxPolicy::new_workspace_write_policy(),
},
SandboxMode::DangerFullAccess => SandboxPolicy::DangerFullAccess,
};
// Resolve approval policy with precedence:
// CLI override > profile override > per-project override > global config.toml > default
let effective_approval = approval_policy
.or(config_profile.approval_policy)
.or(project_override.and_then(|p| p.approval_policy))
.or(cfg.approval_policy)
.unwrap_or_else(AskForApproval::default);
let history = cfg.history.unwrap_or_default();
let tools_web_search_request = override_tools_web_search_request
@@ -1054,10 +1167,7 @@ impl Config {
model_provider_id,
model_provider,
cwd: resolved_cwd,
approval_policy: approval_policy
.or(config_profile.approval_policy)
.or(cfg.approval_policy)
.unwrap_or_else(AskForApproval::default),
approval_policy: effective_approval,
sandbox_policy,
shell_environment_policy,
disable_response_storage: config_profile
+8
View File
@@ -30,6 +30,14 @@ pub fn assess_patch_safety(
};
}
// In Read Only mode, write operations are not permitted. Reject immediately
// so the agent receives a clear failure without prompting for approval.
if matches!(sandbox_policy, SandboxPolicy::ReadOnly) {
return SafetyCheck::Reject {
reason: "write operations are disabled in read-only mode".to_string(),
};
}
match policy {
AskForApproval::OnFailure | AskForApproval::Never | AskForApproval::OnRequest => {
// Continue to see if this can be auto-approved.
+11 -3
View File
@@ -367,10 +367,12 @@ impl App<'_> {
},
AppState::Onboarding { .. } => {}
},
AppEvent::InsertBackgroundEvent(message) => match &mut self.app_state {
// InsertBackgroundEvent removed; use InsertBackgroundEventEarly for
// approval decisions to appear above command begin.
AppEvent::InsertBackgroundEventEarly(message) => match &mut self.app_state {
AppState::Chat { widget } => {
tracing::debug!("app: InsertBackgroundEvent len={}", message.len());
widget.push_background_event(message);
tracing::debug!("app: InsertBackgroundEventEarly len={}", message.len());
widget.insert_background_event_early(message);
}
AppState::Onboarding { .. } => {}
},
@@ -1028,6 +1030,12 @@ impl App<'_> {
}
self.schedule_redraw();
}
AppEvent::CycleAccessMode => {
if let AppState::Chat { widget } = &mut self.app_state {
widget.cycle_access_mode();
}
self.schedule_redraw();
}
AppEvent::OnboardingAuthComplete(result) => {
if let AppState::Onboarding { screen } = &mut self.app_state {
screen.on_auth_complete(result);
+5 -4
View File
@@ -91,6 +91,8 @@ pub(crate) enum AppEvent {
/// Preview theme (no history event)
PreviewTheme(ThemeName),
/// Rotate access/safety preset (Read Only → Write with Approval → Full Access)
CycleAccessMode,
/// Bottom composer expanded (e.g., slash command popup opened)
ComposerExpanded,
@@ -115,10 +117,9 @@ pub(crate) enum AppEvent {
InsertHistoryWithKind { id: Option<String>, kind: StreamKind, lines: Vec<Line<'static>> },
/// Finalized assistant answer with raw markdown for re-rendering under theme changes.
InsertFinalAnswer { id: Option<String>, lines: Vec<Line<'static>>, source: String },
/// Insert a background event (status/log-style) message into history.
/// These are non-streaming, non-assistant lines rendered with the
/// BackgroundEvent style ("event" header, dim text).
InsertBackgroundEvent(String),
/// Insert a background event near the top of the current request so it
/// appears above imminent provider output (e.g. above Exec begin).
InsertBackgroundEventEarly(String),
#[allow(dead_code)]
StartCommitAnimation,
+58 -1
View File
@@ -114,6 +114,10 @@ pub(crate) struct ChatComposer {
using_chatgpt_auth: bool,
// Ephemeral footer notice and its expiry
footer_notice: Option<(String, std::time::Instant)>,
// Persistent/ephemeral access-mode indicator shown on the left
access_mode_label: Option<String>,
access_mode_label_expiry: Option<std::time::Instant>,
access_mode_hint_expiry: Option<std::time::Instant>,
// Footer hint visibility flags
show_reasoning_hint: bool,
show_diffs_hint: bool,
@@ -164,6 +168,9 @@ impl ChatComposer {
animation_running: None,
using_chatgpt_auth,
footer_notice: None,
access_mode_label: None,
access_mode_label_expiry: None,
access_mode_hint_expiry: None,
show_reasoning_hint: false,
show_diffs_hint: false,
reasoning_shown: false,
@@ -231,6 +238,20 @@ impl ChatComposer {
}
}
pub fn set_access_mode_label(&mut self, label: Option<String>) {
self.access_mode_label = label;
self.access_mode_label_expiry = None;
self.access_mode_hint_expiry = None;
}
pub fn set_access_mode_label_ephemeral(&mut self, label: String, dur: std::time::Duration) {
self.access_mode_label = Some(label);
self.access_mode_label_expiry = Some(std::time::Instant::now() + dur);
self.access_mode_hint_expiry = self.access_mode_label_expiry;
}
pub fn set_access_mode_hint_for(&mut self, dur: std::time::Duration) {
self.access_mode_hint_expiry = Some(std::time::Instant::now() + dur);
}
pub fn set_reasoning_state(&mut self, shown: bool) {
self.reasoning_shown = shown;
}
@@ -1057,6 +1078,13 @@ impl ChatComposer {
(InputResult::None, true)
}
// -------------------------------------------------------------
// Shift+Tab — rotate access preset (Read Only → Write with Approval → Full Access)
// -------------------------------------------------------------
KeyEvent { code: KeyCode::BackTab, .. } => {
self.app_event_tx.send(crate::app_event::AppEvent::CycleAccessMode);
(InputResult::None, true)
}
// -------------------------------------------------------------
// Tab-press file search when not using @ or ./ and not in slash cmd
// -------------------------------------------------------------
KeyEvent { code: KeyCode::Tab, .. } => {
@@ -1435,8 +1463,32 @@ impl WidgetRef for ChatComposer {
let mut left_spans: Vec<Span> = Vec::new();
left_spans.push(Span::from(" "));
// Access mode indicator (Read Only / Write with Approval / Full Access)
let show_access_label = if let Some(until) = self.access_mode_label_expiry {
std::time::Instant::now() <= until
} else {
true
};
if show_access_label { if let Some(label) = &self.access_mode_label {
// Keep the access mode label bold so it stands out even with a dim footer.
left_spans.push(Span::from(label.clone()).style(label_style.add_modifier(Modifier::BOLD)));
// Hide the hint after the expiry window (or keep while label is ephemeral)
let show_suffix = if let Some(until) = self.access_mode_hint_expiry {
std::time::Instant::now() <= until
} else {
// If whole label is ephemeral, show suffix while visible
self.access_mode_label_expiry.is_some()
};
if show_suffix {
left_spans.push(Span::from(" (").style(label_style));
left_spans.push(Span::from("Shift+Tab").style(key_hint_style));
left_spans.push(Span::from(" change)").style(label_style));
}
}}
if self.ctrl_c_quit_hint {
// Treat as a notice; keep on the left
if !self.access_mode_label.is_none() { left_spans.push(Span::from(" ")); }
left_spans.push(Span::from("Ctrl+C").style(key_hint_style));
left_spans.push(Span::from(" again to quit").style(label_style));
}
@@ -1550,8 +1602,13 @@ impl WidgetRef for ChatComposer {
line_spans.extend(right_spans);
line_spans.push(Span::from(" "));
// Render footer slightly dimmer than normal content across themes
Line::from(line_spans)
.style(Style::default().fg(crate::colors::text_dim()))
.style(
Style::default()
.fg(crate::colors::text_dim())
.add_modifier(Modifier::DIM),
)
.render_ref(bottom_line_rect, buf);
}
}
+25
View File
@@ -593,6 +593,31 @@ impl BottomPane<'_> {
}
}
pub(crate) fn set_access_mode_label(&mut self, label: Option<String>) {
self.composer.set_access_mode_label(label);
// Hide the "(Shift+Tab change)" suffix after a short time for persistent modes.
// Avoid using ScheduleFrameIn (which coalesces timers and can stall animations);
// instead, spawn a lightweight timer that triggers a redraw when it expires.
let dur = Duration::from_secs(4);
self.composer.set_access_mode_hint_for(dur);
let tx = self.app_event_tx.clone();
std::thread::spawn(move || {
std::thread::sleep(dur + Duration::from_millis(120));
tx.send(AppEvent::RequestRedraw);
});
self.request_redraw();
}
pub(crate) fn set_access_mode_label_ephemeral(&mut self, label: String, dur: Duration) {
self.composer.set_access_mode_label_ephemeral(label, dur);
// Schedule a redraw after expiry without blocking other scheduled frames.
let tx = self.app_event_tx.clone();
std::thread::spawn(move || {
std::thread::sleep(dur + Duration::from_millis(120));
tx.send(AppEvent::RequestRedraw);
});
self.request_redraw();
}
// Removed restart_live_status_with_text – no longer used by the current streaming UI.
}
+167 -6
View File
@@ -288,6 +288,13 @@ pub(crate) struct ChatWidget<'a> {
// One-time hint to teach input history navigation
scroll_history_hint_shown: bool,
// Track and manage the access-mode background status cell so mode changes
// replace the existing status instead of stacking multiple entries.
access_status_idx: Option<usize>,
// Pending one-shot note about the latest access-mode change to inject into
// the agent's conversation history on the next user message.
pending_access_note: Option<String>,
}
struct PendingJumpBack {
@@ -1425,7 +1432,7 @@ impl ChatWidget<'_> {
// Initialize image protocol for rendering screenshots
let new_widget = Self {
let mut new_widget = Self {
app_event_tx: app_event_tx.clone(),
codex_op_tx,
bottom_pane: BottomPane::new(BottomPaneParams {
@@ -1539,7 +1546,11 @@ impl ChatWidget<'_> {
internal_seq: 0,
show_order_overlay,
scroll_history_hint_shown: false,
access_status_idx: None,
pending_access_note: None,
};
// Seed footer access indicator based on current config
new_widget.apply_access_mode_indicator_from_config();
// Insert the welcome cell as top-of-first-request so future model output
// appears below it. Also insert the Popular commands immediately so users
// don't wait for MCP initialization to finish.
@@ -1708,6 +1719,8 @@ impl ChatWidget<'_> {
internal_seq: 0,
show_order_overlay,
scroll_history_hint_shown: false,
access_status_idx: None,
pending_access_note: None,
};
// Welcome at top of first request for forked session too
w.history_push_top_next_req(history_cell::new_animated_welcome());
@@ -2262,9 +2275,15 @@ impl ChatWidget<'_> {
let key = self.next_internal_key();
let _ = self.history_insert_with_key_global_tagged(Box::new(cell), key, "epilogue");
}
/// Public helper to push a background event with the standard gutter and styling.
pub(crate) fn push_background_event(&mut self, message: String) {
self.history_push(history_cell::new_background_event(message));
/// Insert a background event near the top of the current request so it appears
/// before imminent provider output (e.g. Exec begin).
pub(crate) fn insert_background_event_early(&mut self, message: String) {
let key = self.near_time_key(None);
let _ = self.history_insert_with_key_global_tagged(
Box::new(history_cell::new_background_event(message)),
key,
"background-early",
);
}
/// Push a cell using a synthetic key at the TOP of the NEXT request.
fn history_push_top_next_req(&mut self, cell: impl HistoryCell + 'static) {
@@ -2693,6 +2712,12 @@ impl ChatWidget<'_> {
self.pending_user_prompts_for_next_turn.saturating_add(1);
}
// If an access-mode change was pending, record it in the agent's
// conversation history right before the next user turn.
if let Some(note) = self.pending_access_note.take() {
let _ = self.codex_op_tx.send(Op::AddToHistory { text: note });
}
// Now send to the agent
let _ = self.codex_op_tx.send(Op::UserInput { items });
if !original_text.is_empty() {
@@ -4096,8 +4121,6 @@ impl ChatWidget<'_> {
"Keyboard shortcuts",
t_fg.add_modifier(Modifier::BOLD),
)]));
lines.push(RtLine::from(""));
let kv = |k: &str, v: &str| -> RtLine<'static> {
RtLine::from(vec![
// Left-align the key column for improved readability
@@ -4106,6 +4129,12 @@ impl ChatWidget<'_> {
RtSpan::styled(v.to_string(), t_dim),
])
};
lines.push(RtLine::from(""));
// Top quick action
lines.push(kv(
"Shift+Tab",
"Rotate agent between Read Only / Write with Approval / Full Access",
));
// Global
lines.push(kv("Ctrl+H", "Help overlay"));
@@ -4485,6 +4514,134 @@ impl ChatWidget<'_> {
self.history_push(history_cell::new_background_event(message));
}
fn apply_access_mode_indicator_from_config(&mut self) {
use codex_core::protocol::{AskForApproval, SandboxPolicy};
let label = match (&self.config.sandbox_policy, self.config.approval_policy) {
(SandboxPolicy::ReadOnly, _) => Some("Read Only".to_string()),
(SandboxPolicy::WorkspaceWrite { network_access: false, .. }, AskForApproval::OnRequest)
| (SandboxPolicy::WorkspaceWrite { network_access: false, .. }, AskForApproval::OnFailure)
| (SandboxPolicy::WorkspaceWrite { network_access: false, .. }, AskForApproval::UnlessTrusted) => Some("Write with Approval".to_string()),
_ => None,
};
self.bottom_pane.set_access_mode_label(label);
}
/// Rotate the access preset: Read Only (Plan Mode) → Write with Approval → Full Access
pub(crate) fn cycle_access_mode(&mut self) {
use codex_core::protocol::{AskForApproval, SandboxPolicy};
use codex_core::config::set_project_access_mode;
// Determine current index
let idx = match (&self.config.sandbox_policy, self.config.approval_policy) {
(SandboxPolicy::ReadOnly, _) => 0,
(SandboxPolicy::WorkspaceWrite { network_access: false, .. }, AskForApproval::OnRequest)
| (SandboxPolicy::WorkspaceWrite { network_access: false, .. }, AskForApproval::OnFailure)
| (SandboxPolicy::WorkspaceWrite { network_access: false, .. }, AskForApproval::UnlessTrusted) => 1,
(SandboxPolicy::DangerFullAccess, AskForApproval::Never) => 2,
_ => 1,
};
let next = (idx + 1) % 3;
// Apply mapping
let (label, approval, sandbox) = match next {
0 => (
"Read Only (Plan Mode)",
AskForApproval::OnRequest,
SandboxPolicy::ReadOnly,
),
1 => (
"Write with Approval",
AskForApproval::OnRequest,
SandboxPolicy::new_workspace_write_policy(),
),
_ => (
"Full Access",
AskForApproval::Never,
SandboxPolicy::DangerFullAccess,
),
};
// Update local config
self.config.approval_policy = approval;
self.config.sandbox_policy = sandbox;
// Send ConfigureSession op to backend
let op = Op::ConfigureSession {
provider: self.config.model_provider.clone(),
model: self.config.model.clone(),
model_reasoning_effort: self.config.model_reasoning_effort,
model_reasoning_summary: self.config.model_reasoning_summary,
model_text_verbosity: self.config.model_text_verbosity,
user_instructions: self.config.user_instructions.clone(),
base_instructions: self.config.base_instructions.clone(),
approval_policy: self.config.approval_policy.clone(),
sandbox_policy: self.config.sandbox_policy.clone(),
disable_response_storage: self.config.disable_response_storage,
notify: self.config.notify.clone(),
cwd: self.config.cwd.clone(),
resume_path: None,
};
self.submit_op(op);
// Persist selection into CODEX_HOME/config.toml for this project directory
// so it sticks across restarts.
let _ = set_project_access_mode(
&self.config.codex_home,
&self.config.cwd,
self.config.approval_policy,
match &self.config.sandbox_policy {
SandboxPolicy::ReadOnly => codex_protocol::config_types::SandboxMode::ReadOnly,
SandboxPolicy::WorkspaceWrite { .. } => codex_protocol::config_types::SandboxMode::WorkspaceWrite,
SandboxPolicy::DangerFullAccess => codex_protocol::config_types::SandboxMode::DangerFullAccess,
},
);
// Footer indicator: persistent for RO/Approval; ephemeral for Full Access
if next == 2 {
self.bottom_pane.set_access_mode_label_ephemeral(
"Full Access".to_string(),
std::time::Duration::from_secs(4),
);
} else {
let persistent = if next == 0 { "Read Only" } else { "Write with Approval" };
self.bottom_pane.set_access_mode_label(Some(persistent.to_string()));
}
// Announce in history: replace the last access-mode status, inserting early
// in the current request so it appears above upcoming commands.
let msg = format!("✓ Access mode: {}", label);
self.set_access_status_message(msg);
// No footer notice: indicator covers this; avoid duplicate texts.
// Prepare a single consolidated note for the agent to see before the
// next turn begins. Subsequent cycles will overwrite this note.
let agent_note = match next {
0 => "System: access mode changed to Read Only. Do not attempt write operations or apply_patch.",
1 => "System: access mode changed to Write with Approval. Approval will be automatically requested before writes.",
_ => "System: access mode changed to Full Access. Writes and network are allowed.",
};
self.pending_access_note = Some(agent_note.to_string());
}
/// Insert or replace the access-mode status background event. Uses a near-time
/// key so it appears above any imminent Exec/Tool cells in this request.
fn set_access_status_message(&mut self, message: String) {
let cell = crate::history_cell::new_background_event(message);
if let Some(idx) = self.access_status_idx {
if idx < self.history_cells.len()
&& matches!(self.history_cells[idx].kind(), crate::history_cell::HistoryCellType::BackgroundEvent)
{
self.history_replace_at(idx, Box::new(cell));
self.request_redraw();
return;
}
}
// Insert new status near the top of this request window
let key = self.near_time_key(None);
let pos = self.history_insert_with_key_global_tagged(Box::new(cell), key, "access-status");
self.access_status_idx = Some(pos);
}
fn restyle_history_after_theme_change(&mut self) {
let old = self.last_theme.clone();
let new = crate::theme::current_theme();
@@ -7368,6 +7525,10 @@ impl ChatWidget<'_> {
enhanced_keys_supported,
using_chatgpt_auth: self.config.using_chatgpt_auth,
});
self.access_status_idx = None;
self.pending_access_note = None;
// Re-apply access indicator for new composer
self.apply_access_mode_indicator_from_config();
// Request redraw for the new animation
self.mark_needs_redraw();
+14 -1
View File
@@ -459,6 +459,19 @@ fn determine_repo_trust_state(
) -> std::io::Result<bool> {
let config_profile = config_toml.get_config_profile(config_profile_override)?;
// If this project has explicit per-project overrides for approval and/or sandbox,
// honor them and skip the trust screen entirely.
let proj_key = config.cwd.to_string_lossy().to_string();
let has_per_project_overrides = config_toml
.projects
.as_ref()
.and_then(|m| m.get(&proj_key))
.map(|p| p.approval_policy.is_some() || p.sandbox_mode.is_some())
.unwrap_or(false);
if has_per_project_overrides {
return Ok(false);
}
if approval_policy_overide.is_some() || sandbox_mode_override.is_some() {
// if the user has overridden either approval policy or sandbox mode,
// skip the trust flow
@@ -479,7 +492,7 @@ fn determine_repo_trust_state(
config.sandbox_policy = SandboxPolicy::DangerFullAccess;
Ok(false)
} else {
// if none of the above conditions are met, show the trust screen
// if none of the above conditions are met (and no per‑project overrides), show the trust screen
Ok(true)
}
}
+5 -2
View File
@@ -120,9 +120,10 @@ impl UserApprovalWidget<'_> {
let mut cmd_span: Span = cmd.clone().into();
cmd_span.style = cmd_span.style.add_modifier(Modifier::DIM);
let mut contents: Vec<Line> = vec![
Line::from(""), // extra spacing above the prompt
Line::from(vec![
"? ".fg(crate::colors::info()),
"Codex wants to run ".bold(),
"Code wants to run ".bold(),
cmd_span,
]),
Line::from(""),
@@ -255,7 +256,9 @@ impl UserApprovalWidget<'_> {
// Append feedback, preserving line breaks
format!("{}\nfeedback:\n{}", message, feedback)
};
self.app_event_tx.send(AppEvent::InsertBackgroundEvent(message));
// Insert above the upcoming command begin so the decision reads first.
self.app_event_tx
.send(AppEvent::InsertBackgroundEventEarly(message));
// If the user aborted an exec approval, immediately cancel any running task
// so the UI reflects their intent (clear spinner/status) without waiting