Compare commits

...

7 Commits

Author SHA1 Message Date
easong-openai
45bccd36b0 fix permissions alignment 2025-09-15 17:34:04 -07:00
dependabot[bot]
404c126fc3 chore(deps): bump wildmatch from 2.4.0 to 2.5.0 in /codex-rs (#3619)
Bumps [wildmatch](https://github.com/becheran/wildmatch) from 2.4.0 to
2.5.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/becheran/wildmatch/releases">wildmatch's
releases</a>.</em></p>
<blockquote>
<h2>v2.5.0</h2>
<p><a
href="https://redirect.github.com/becheran/wildmatch/pull/27">becheran/wildmatch#27</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="b39902c120"><code>b39902c</code></a>
chore: Release wildmatch version 2.5.0</li>
<li><a
href="87a8cf4c80"><code>87a8cf4</code></a>
Merge pull request <a
href="https://redirect.github.com/becheran/wildmatch/issues/28">#28</a>
from smichaku/micha/fix-unicode-case-insensitive-matching</li>
<li><a
href="a3ab4903f5"><code>a3ab490</code></a>
fix: Fix unicode matching for non-ASCII characters</li>
<li>See full diff in <a
href="https://github.com/becheran/wildmatch/compare/v2.4.0...v2.5.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=wildmatch&package-manager=cargo&previous-version=2.4.0&new-version=2.5.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-15 12:57:17 -07:00
dependabot[bot]
88027552dd chore(deps): bump serde from 1.0.219 to 1.0.223 in /codex-rs (#3618)
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.219 to
1.0.223.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/serde-rs/serde/releases">serde's
releases</a>.</em></p>
<blockquote>
<h2>v1.0.223</h2>
<ul>
<li>Fix serde_core documentation links (<a
href="https://redirect.github.com/serde-rs/serde/issues/2978">#2978</a>)</li>
</ul>
<h2>v1.0.222</h2>
<ul>
<li>Make <code>serialize_with</code> attribute produce code that works
if respanned to 2024 edition (<a
href="https://redirect.github.com/serde-rs/serde/issues/2950">#2950</a>,
thanks <a href="https://github.com/aytey"><code>@​aytey</code></a>)</li>
</ul>
<h2>v1.0.221</h2>
<ul>
<li>Documentation improvements (<a
href="https://redirect.github.com/serde-rs/serde/issues/2973">#2973</a>)</li>
<li>Deprecate <code>serde_if_integer128!</code> macro (<a
href="https://redirect.github.com/serde-rs/serde/issues/2975">#2975</a>)</li>
</ul>
<h2>v1.0.220</h2>
<ul>
<li>Add a way for data formats to depend on serde traits without waiting
for serde_derive compilation: <a
href="https://docs.rs/serde_core">https://docs.rs/serde_core</a> (<a
href="https://redirect.github.com/serde-rs/serde/issues/2608">#2608</a>,
thanks <a
href="https://github.com/osiewicz"><code>@​osiewicz</code></a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="6c316d7cb5"><code>6c316d7</code></a>
Release 1.0.223</li>
<li><a
href="a4ac0c2bc6"><code>a4ac0c2</code></a>
Merge pull request <a
href="https://redirect.github.com/serde-rs/serde/issues/2978">#2978</a>
from dtolnay/htmlrooturl</li>
<li><a
href="ed76364f87"><code>ed76364</code></a>
Change serde_core's html_root_url to docs.rs/serde_core</li>
<li><a
href="57e21a1afa"><code>57e21a1</code></a>
Release 1.0.222</li>
<li><a
href="bb58726133"><code>bb58726</code></a>
Merge pull request <a
href="https://redirect.github.com/serde-rs/serde/issues/2950">#2950</a>
from aytey/fix_lifetime_issue_2024</li>
<li><a
href="3f6925125b"><code>3f69251</code></a>
Delete unneeded field of MapDeserializer</li>
<li><a
href="fd4decf2fe"><code>fd4decf</code></a>
Merge pull request <a
href="https://redirect.github.com/serde-rs/serde/issues/2976">#2976</a>
from dtolnay/content</li>
<li><a
href="00b1b6b2b5"><code>00b1b6b</code></a>
Move Content's Deserialize impl from serde_core to serde</li>
<li><a
href="cf141aa8c7"><code>cf141aa</code></a>
Move Content's Clone impl from serde_core to serde</li>
<li><a
href="ff3aee490a"><code>ff3aee4</code></a>
Release 1.0.221</li>
<li>Additional commits viewable in <a
href="https://github.com/serde-rs/serde/compare/v1.0.219...v1.0.223">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=serde&package-manager=cargo&previous-version=1.0.219&new-version=1.0.223)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-15 12:56:20 -07:00
Michael Bolin
ca8bd09d56 chore: simplify dep so serde=1 in Cargo.toml (#3664)
With this change, dependabot should just have to update `Cargo.lock` for
`serde`, e.g.:

- https://github.com/openai/codex/pull/3617
- https://github.com/openai/codex/pull/3618
2025-09-15 19:22:29 +00:00
dependabot[bot]
39ed8a7d26 chore(deps): bump serde_json from 1.0.143 to 1.0.145 in /codex-rs (#3617)
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.143 to
1.0.145.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/serde-rs/json/releases">serde_json's
releases</a>.</em></p>
<blockquote>
<h2>v1.0.145</h2>
<ul>
<li>Raise serde version requirement to &gt;=1.0.220</li>
</ul>
<h2>v1.0.144</h2>
<ul>
<li>Switch serde dependency to serde_core (<a
href="https://redirect.github.com/serde-rs/json/issues/1285">#1285</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="efa66e3a1d"><code>efa66e3</code></a>
Release 1.0.145</li>
<li><a
href="23679e2b9d"><code>23679e2</code></a>
Add serde version constraint</li>
<li><a
href="fc27bafbf7"><code>fc27baf</code></a>
Release 1.0.144</li>
<li><a
href="caef3c6ea6"><code>caef3c6</code></a>
Ignore uninlined_format_args pedantic clippy lint</li>
<li><a
href="81ba3aaaff"><code>81ba3aa</code></a>
Merge pull request <a
href="https://redirect.github.com/serde-rs/json/issues/1285">#1285</a>
from dtolnay/serdecore</li>
<li><a
href="d21e8ce7a7"><code>d21e8ce</code></a>
Switch serde dependency to serde_core</li>
<li><a
href="6beb6cd596"><code>6beb6cd</code></a>
Merge pull request <a
href="https://redirect.github.com/serde-rs/json/issues/1286">#1286</a>
from dtolnay/up</li>
<li><a
href="1dbc803749"><code>1dbc803</code></a>
Raise required compiler to Rust 1.61</li>
<li><a
href="0bf5d87003"><code>0bf5d87</code></a>
Enforce trybuild &gt;= 1.0.108</li>
<li><a
href="d12e943590"><code>d12e943</code></a>
Update actions/checkout@v4 -&gt; v5</li>
<li>See full diff in <a
href="https://github.com/serde-rs/json/compare/v1.0.143...v1.0.145">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=serde_json&package-manager=cargo&previous-version=1.0.143&new-version=1.0.145)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-15 11:58:57 -07:00
Michael Bolin
2df7f7efe5 chore: restore prerelease logic in rust-release.yml (#3659)
Revert #3645.
2025-09-15 17:52:49 +00:00
Jeremy Rose
0560079c41 notifications on approvals and turn end (#3329)
uses OSC 9 to notify when a turn ends or approval is required. won't
work in vs code or terminal.app but iterm2/kitty/wezterm supports it :)
2025-09-15 10:22:02 -07:00
20 changed files with 526 additions and 110 deletions

View File

@@ -211,8 +211,7 @@ jobs:
files: dist/**
# Mark as prerelease only when the version has a suffix after x.y.z
# (e.g. -alpha, -beta). Otherwise publish a normal release.
# prerelease: ${{ contains(steps.release_name.outputs.name, '-') }}
prerelease: true
prerelease: ${{ contains(steps.release_name.outputs.name, '-') }}
- uses: facebook/dotslash-publish-release@v2
env:

27
codex-rs/Cargo.lock generated
View File

@@ -3926,18 +3926,28 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.219"
version = "1.0.224"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
checksum = "6aaeb1e94f53b16384af593c71e20b095e958dab1d26939c1b70645c5cfbcc0b"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.224"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32f39390fa6346e24defbcdd3d9544ba8a19985d0af74df8501fbfe9a64341ab"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.219"
version = "1.0.224"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
checksum = "87ff78ab5e8561c9a675bfc1785cb07ae721f0ee53329a595cefd8c04c2ac4e0"
dependencies = [
"proc-macro2",
"quote",
@@ -3957,15 +3967,16 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.143"
version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a"
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
dependencies = [
"indexmap 2.10.0",
"itoa",
"memchr",
"ryu",
"serde",
"serde_core",
]
[[package]]
@@ -5281,9 +5292,9 @@ dependencies = [
[[package]]
name = "wildmatch"
version = "2.4.0"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ce1ab1f8c62655ebe1350f589c61e505cf94d385bc6a12899442d9081e71fd"
checksum = "39b7d07a236abaef6607536ccfaf19b396dbe3f5110ddb73d39f4562902ed382"
[[package]]
name = "winapi"

View File

@@ -57,7 +57,7 @@ tree-sitter = "0.25.9"
tree-sitter-bash = "0.25.0"
uuid = { version = "1", features = ["serde", "v4"] }
which = "6"
wildmatch = "2.4.0"
wildmatch = "2.5.0"
[target.'cfg(target_os = "linux")'.dependencies]

View File

@@ -1,6 +1,7 @@
use crate::config_profile::ConfigProfile;
use crate::config_types::History;
use crate::config_types::McpServerConfig;
use crate::config_types::Notifications;
use crate::config_types::ReasoningSummaryFormat;
use crate::config_types::SandboxWorkspaceWrite;
use crate::config_types::ShellEnvironmentPolicy;
@@ -117,6 +118,10 @@ pub struct Config {
/// If unset the feature is disabled.
pub notify: Option<Vec<String>>,
/// TUI notifications preference. When set, the TUI will send OSC 9 notifications on approvals
/// and turn completions when not focused.
pub tui_notifications: Notifications,
/// The directory that should be treated as the current working directory
/// for the session. All relative paths inside the business-logic layer are
/// resolved against this path.
@@ -1043,6 +1048,11 @@ impl Config {
include_view_image_tool,
active_profile: active_profile_name,
disable_paste_burst: cfg.disable_paste_burst.unwrap_or(false),
tui_notifications: cfg
.tui
.as_ref()
.map(|t| t.notifications.clone())
.unwrap_or_default(),
};
Ok(config)
}
@@ -1606,6 +1616,7 @@ model_verbosity = "high"
include_view_image_tool: true,
active_profile: Some("o3".to_string()),
disable_paste_burst: false,
tui_notifications: Default::default(),
},
o3_profile_config
);
@@ -1663,6 +1674,7 @@ model_verbosity = "high"
include_view_image_tool: true,
active_profile: Some("gpt3".to_string()),
disable_paste_burst: false,
tui_notifications: Default::default(),
};
assert_eq!(expected_gpt3_profile_config, gpt3_profile_config);
@@ -1735,6 +1747,7 @@ model_verbosity = "high"
include_view_image_tool: true,
active_profile: Some("zdr".to_string()),
disable_paste_burst: false,
tui_notifications: Default::default(),
};
assert_eq!(expected_zdr_profile_config, zdr_profile_config);
@@ -1793,6 +1806,7 @@ model_verbosity = "high"
include_view_image_tool: true,
active_profile: Some("gpt5".to_string()),
disable_paste_burst: false,
tui_notifications: Default::default(),
};
assert_eq!(expected_gpt5_profile_config, gpt5_profile_config);
@@ -1896,3 +1910,46 @@ trust_level = "trusted"
Ok(())
}
}
#[cfg(test)]
mod notifications_tests {
use crate::config_types::Notifications;
use serde::Deserialize;
#[derive(Deserialize, Debug, PartialEq)]
struct TuiTomlTest {
notifications: Notifications,
}
#[derive(Deserialize, Debug, PartialEq)]
struct RootTomlTest {
tui: TuiTomlTest,
}
#[test]
fn test_tui_notifications_true() {
let toml = r#"
[tui]
notifications = true
"#;
let parsed: RootTomlTest = toml::from_str(toml).expect("deserialize notifications=true");
assert!(matches!(
parsed.tui.notifications,
Notifications::Enabled(true)
));
}
#[test]
fn test_tui_notifications_custom_array() {
let toml = r#"
[tui]
notifications = ["foo"]
"#;
let parsed: RootTomlTest =
toml::from_str(toml).expect("deserialize notifications=[\"foo\"]");
assert!(matches!(
parsed.tui.notifications,
Notifications::Custom(ref v) if v == &vec!["foo".to_string()]
));
}
}

View File

@@ -76,9 +76,26 @@ pub enum HistoryPersistence {
None,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(untagged)]
pub enum Notifications {
Enabled(bool),
Custom(Vec<String>),
}
impl Default for Notifications {
fn default() -> Self {
Self::Enabled(false)
}
}
/// Collection of settings that are specific to the TUI.
#[derive(Deserialize, Debug, Clone, PartialEq, Default)]
pub struct Tui {}
pub struct Tui {
/// Enable desktop notifications from the TUI when the terminal is unfocused.
/// Defaults to `false`.
pub notifications: Notifications,
}
#[derive(Deserialize, Debug, Clone, PartialEq, Default)]
pub struct SandboxWorkspaceWrite {

View File

@@ -1,7 +1,7 @@
[package]
edition = "2024"
name = "codex-execpolicy"
version = { workspace = true }
edition = "2024"
[[bin]]
name = "codex-execpolicy"
@@ -15,9 +15,8 @@ path = "src/lib.rs"
workspace = true
[dependencies]
anyhow = "1"
starlark = "0.13.0"
allocative = "0.3.3"
anyhow = "1"
clap = { version = "4", features = ["derive"] }
derive_more = { version = "2", features = ["display"] }
env_logger = "0.11.5"
@@ -25,9 +24,10 @@ log = "0.4"
multimap = "0.10.0"
path-absolutize = "3.1.1"
regex-lite = "0.1"
serde = { version = "1.0.194", features = ["derive"] }
serde_json = "1.0.143"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_with = { version = "3", features = ["macros"] }
starlark = "0.13.0"
[dev-dependencies]
tempfile = "3.13.0"

View File

@@ -17,5 +17,5 @@ clap = { version = "4", features = ["derive"] }
ignore = "0.4.23"
nucleo-matcher = "0.3.1"
serde = { version = "1", features = ["derive"] }
serde_json = "1.0.143"
serde_json = "1"
tokio = { version = "1", features = ["full"] }

View File

@@ -177,6 +177,7 @@ impl App {
self.chat_widget.handle_paste(pasted);
}
TuiEvent::Draw => {
self.chat_widget.maybe_post_pending_notification(tui);
if self
.chat_widget
.handle_paste_burst_tick(tui.frame_requester())

View File

@@ -142,14 +142,16 @@ impl ChatComposer {
.desired_height(width.saturating_sub(LIVE_PREFIX_COLS))
+ match &self.active_popup {
ActivePopup::None => FOOTER_HEIGHT_WITH_HINT,
ActivePopup::Command(c) => c.calculate_required_height(),
ActivePopup::Command(c) => c.calculate_required_height(width),
ActivePopup::File(c) => c.calculate_required_height(),
}
}
pub fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)> {
let popup_constraint = match &self.active_popup {
ActivePopup::Command(popup) => Constraint::Max(popup.calculate_required_height()),
ActivePopup::Command(popup) => {
Constraint::Max(popup.calculate_required_height(area.width))
}
ActivePopup::File(popup) => Constraint::Max(popup.calculate_required_height()),
ActivePopup::None => Constraint::Max(FOOTER_HEIGHT_WITH_HINT),
};
@@ -1232,7 +1234,10 @@ impl ChatComposer {
impl WidgetRef for ChatComposer {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
let (popup_constraint, hint_spacing) = match &self.active_popup {
ActivePopup::Command(popup) => (Constraint::Max(popup.calculate_required_height()), 0),
ActivePopup::Command(popup) => (
Constraint::Max(popup.calculate_required_height(area.width)),
0,
),
ActivePopup::File(popup) => (Constraint::Max(popup.calculate_required_height()), 0),
ActivePopup::None => (
Constraint::Length(FOOTER_HEIGHT_WITH_HINT),

View File

@@ -92,10 +92,35 @@ impl CommandPopup {
.ensure_visible(matches_len, MAX_POPUP_ROWS.min(matches_len));
}
/// Determine the preferred height of the popup. This is the number of
/// rows required to show at most MAX_POPUP_ROWS commands.
pub(crate) fn calculate_required_height(&self) -> u16 {
self.filtered_items().len().clamp(1, MAX_POPUP_ROWS) as u16
/// Determine the preferred height of the popup for a given width.
/// Accounts for wrapped descriptions so that long tooltips don't overflow.
pub(crate) fn calculate_required_height(&self, width: u16) -> u16 {
use super::selection_popup_common::GenericDisplayRow;
use super::selection_popup_common::measure_rows_height;
let matches = self.filtered();
let rows_all: Vec<GenericDisplayRow> = if matches.is_empty() {
Vec::new()
} else {
matches
.into_iter()
.map(|(item, indices, _)| match item {
CommandItem::Builtin(cmd) => GenericDisplayRow {
name: format!("/{}", cmd.command()),
match_indices: indices.map(|v| v.into_iter().map(|i| i + 1).collect()),
is_current: false,
description: Some(cmd.description().to_string()),
},
CommandItem::UserPrompt(i) => GenericDisplayRow {
name: format!("/{}", self.prompts[i].name),
match_indices: indices.map(|v| v.into_iter().map(|i| i + 1).collect()),
is_current: false,
description: Some("send saved prompt".to_string()),
},
})
.collect()
};
measure_rows_height(&rows_all, &self.state, MAX_POPUP_ROWS, width)
}
/// Compute fuzzy-filtered matches over built-in commands and user prompts,

View File

@@ -17,6 +17,7 @@ use super::bottom_pane_view::BottomPaneView;
use super::popup_consts::MAX_POPUP_ROWS;
use super::scroll_state::ScrollState;
use super::selection_popup_common::GenericDisplayRow;
use super::selection_popup_common::measure_rows_height;
use super::selection_popup_common::render_rows;
/// One selectable item in the generic selection list.
@@ -135,11 +136,36 @@ impl BottomPaneView for ListSelectionView {
CancellationEvent::Handled
}
fn desired_height(&self, _width: u16) -> u16 {
let rows = (self.items.len()).clamp(1, MAX_POPUP_ROWS);
fn desired_height(&self, width: u16) -> u16 {
// Measure wrapped height for up to MAX_POPUP_ROWS items at the given width.
// Build the same display rows used by the renderer so wrapping math matches.
let rows: Vec<GenericDisplayRow> = self
.items
.iter()
.enumerate()
.map(|(i, it)| {
let is_selected = self.state.selected_idx == Some(i);
let prefix = if is_selected { '>' } else { ' ' };
let name_with_marker = if it.is_current {
format!("{} (current)", it.name)
} else {
it.name.clone()
};
let display_name = format!("{} {}. {}", prefix, i + 1, name_with_marker);
GenericDisplayRow {
name: display_name,
match_indices: None,
is_current: it.is_current,
description: it.description.clone(),
}
})
.collect();
let rows_height = measure_rows_height(&rows, &self.state, MAX_POPUP_ROWS, width);
// +1 for the title row, +1 for a spacer line beneath the header,
// +1 for optional subtitle, +1 for optional footer
let mut height = rows as u16 + 2;
// +1 for optional subtitle, +1 for optional footer (2 lines incl. spacing)
let mut height = rows_height + 2;
if self.subtitle.is_some() {
// +1 for subtitle (the spacer is accounted for above)
height = height.saturating_add(1);

View File

@@ -1,6 +1,7 @@
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::prelude::Constraint;
// Note: Table-based layout previously used Constraint; the manual renderer
// below no longer requires it.
use ratatui::style::Color;
use ratatui::style::Modifier;
use ratatui::style::Style;
@@ -10,9 +11,7 @@ use ratatui::text::Span;
use ratatui::widgets::Block;
use ratatui::widgets::BorderType;
use ratatui::widgets::Borders;
use ratatui::widgets::Cell;
use ratatui::widgets::Row;
use ratatui::widgets::Table;
use ratatui::widgets::Paragraph;
use ratatui::widgets::Widget;
use super::scroll_state::ScrollState;
@@ -27,6 +26,61 @@ pub(crate) struct GenericDisplayRow {
impl GenericDisplayRow {}
/// Compute a shared description-column start based on the widest visible name
/// plus two spaces of padding. Ensures at least one column is left for the
/// description.
fn compute_desc_col(
rows_all: &[GenericDisplayRow],
start_idx: usize,
visible_items: usize,
content_width: u16,
) -> usize {
let visible_range = start_idx..(start_idx + visible_items);
let max_name_width = rows_all
.iter()
.enumerate()
.filter(|(i, _)| visible_range.contains(i))
.map(|(_, r)| Line::from(r.name.clone()).width())
.max()
.unwrap_or(0);
let mut desc_col = max_name_width.saturating_add(2);
if (desc_col as u16) >= content_width {
desc_col = content_width.saturating_sub(1) as usize;
}
desc_col
}
/// Build the full display line for a row with the description padded to start
/// at `desc_col`. Applies fuzzy-match bolding when indices are present and
/// dims the description.
fn build_full_line(row: &GenericDisplayRow, desc_col: usize) -> Line<'static> {
let mut name_spans: Vec<Span> = Vec::with_capacity(row.name.len());
if let Some(idxs) = row.match_indices.as_ref() {
let mut idx_iter = idxs.iter().peekable();
for (char_idx, ch) in row.name.chars().enumerate() {
if idx_iter.peek().is_some_and(|next| **next == char_idx) {
idx_iter.next();
name_spans.push(ch.to_string().bold());
} else {
name_spans.push(ch.to_string().into());
}
}
} else {
name_spans.push(row.name.clone().into());
}
let this_name_width = Line::from(name_spans.clone()).width();
let mut full_spans: Vec<Span> = name_spans;
if let Some(desc) = row.description.as_ref() {
let gap = desc_col.saturating_sub(this_name_width);
if gap > 0 {
full_spans.push(" ".repeat(gap).into());
}
full_spans.push(desc.clone().dim());
}
Line::from(full_spans)
}
/// Render a list of rows using the provided ScrollState, with shared styling
/// and behavior for selection popups.
pub(crate) fn render_rows(
@@ -38,84 +92,168 @@ pub(crate) fn render_rows(
_dim_non_selected: bool,
empty_message: &str,
) {
let mut rows: Vec<Row> = Vec::new();
// Always draw a dim left border to match other popups.
let block = Block::default()
.borders(Borders::LEFT)
.border_type(BorderType::QuadrantOutside)
.border_style(Style::default().add_modifier(Modifier::DIM));
block.render(area, buf);
// Content renders to the right of the border.
let content_area = Rect {
x: area.x.saturating_add(1),
y: area.y,
width: area.width.saturating_sub(1),
height: area.height,
};
if rows_all.is_empty() {
rows.push(Row::new(vec![Cell::from(Line::from(
empty_message.dim().italic(),
))]));
} else {
let max_rows_from_area = area.height as usize;
let visible_rows = max_results
.min(rows_all.len())
.min(max_rows_from_area.max(1));
// Compute starting index based on scroll state and selection.
let mut start_idx = state.scroll_top.min(rows_all.len().saturating_sub(1));
if let Some(sel) = state.selected_idx {
if sel < start_idx {
start_idx = sel;
} else if visible_rows > 0 {
let bottom = start_idx + visible_rows - 1;
if sel > bottom {
start_idx = sel + 1 - visible_rows;
}
}
if content_area.height > 0 {
let para = Paragraph::new(Line::from(empty_message.dim().italic()));
para.render(
Rect {
x: content_area.x,
y: content_area.y,
width: content_area.width,
height: 1,
},
buf,
);
}
return;
}
for (i, row) in rows_all
.iter()
.enumerate()
.skip(start_idx)
.take(visible_rows)
{
let GenericDisplayRow {
name,
match_indices,
is_current: _is_current,
description,
} = row;
// Determine which logical rows (items) are visible given the selection and
// the max_results clamp. Scrolling is still item-based for simplicity.
let max_rows_from_area = content_area.height as usize;
let visible_items = max_results
.min(rows_all.len())
.min(max_rows_from_area.max(1));
// Highlight fuzzy indices when present.
let mut spans: Vec<Span> = Vec::with_capacity(name.len());
if let Some(idxs) = match_indices.as_ref() {
let mut idx_iter = idxs.iter().peekable();
for (char_idx, ch) in name.chars().enumerate() {
if idx_iter.peek().is_some_and(|next| **next == char_idx) {
idx_iter.next();
spans.push(ch.to_string().bold());
} else {
spans.push(ch.to_string().into());
}
}
} else {
spans.push(name.clone().into());
let mut start_idx = state.scroll_top.min(rows_all.len().saturating_sub(1));
if let Some(sel) = state.selected_idx {
if sel < start_idx {
start_idx = sel;
} else if visible_items > 0 {
let bottom = start_idx + visible_items - 1;
if sel > bottom {
start_idx = sel + 1 - visible_items;
}
if let Some(desc) = description.as_ref() {
spans.push(" ".into());
spans.push(desc.clone().dim());
}
let mut cell = Cell::from(Line::from(spans));
if Some(i) == state.selected_idx {
cell = cell.style(
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
);
}
rows.push(Row::new(vec![cell]));
}
}
let table = Table::new(rows, vec![Constraint::Percentage(100)])
.block(
Block::default()
.borders(Borders::LEFT)
.border_type(BorderType::QuadrantOutside)
.border_style(Style::default().add_modifier(Modifier::DIM)),
)
.widths([Constraint::Percentage(100)]);
let desc_col = compute_desc_col(rows_all, start_idx, visible_items, content_area.width);
table.render(area, buf);
// Render items, wrapping descriptions and aligning wrapped lines under the
// shared description column. Stop when we run out of vertical space.
let mut cur_y = content_area.y;
for (i, row) in rows_all
.iter()
.enumerate()
.skip(start_idx)
.take(visible_items)
{
if cur_y >= content_area.y + content_area.height {
break;
}
let GenericDisplayRow {
name,
match_indices,
is_current: _is_current,
description,
} = row;
let full_line = build_full_line(
&GenericDisplayRow {
name: name.clone(),
match_indices: match_indices.clone(),
is_current: *_is_current,
description: description.clone(),
},
desc_col,
);
// Wrap with subsequent indent aligned to the description column.
use crate::wrapping::RtOptions;
use crate::wrapping::word_wrap_line;
let options = RtOptions::new(content_area.width as usize)
.initial_indent(Line::from(""))
.subsequent_indent(Line::from(" ".repeat(desc_col)));
let wrapped = word_wrap_line(&full_line, options);
// Render the wrapped lines.
for mut line in wrapped {
if cur_y >= content_area.y + content_area.height {
break;
}
if Some(i) == state.selected_idx {
// Match previous behavior: cyan + bold for the selected row.
line.style = Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD);
}
let para = Paragraph::new(line);
para.render(
Rect {
x: content_area.x,
y: cur_y,
width: content_area.width,
height: 1,
},
buf,
);
cur_y = cur_y.saturating_add(1);
}
}
}
/// Compute the number of terminal rows required to render up to `max_results`
/// items from `rows_all` given the current scroll/selection state and the
/// available `width`. Accounts for description wrapping and alignment so the
/// caller can allocate sufficient vertical space.
pub(crate) fn measure_rows_height(
rows_all: &[GenericDisplayRow],
state: &ScrollState,
max_results: usize,
width: u16,
) -> u16 {
if rows_all.is_empty() {
return 1; // placeholder "no matches" line
}
let content_width = width.saturating_sub(1).max(1);
let visible_items = max_results.min(rows_all.len());
let mut start_idx = state.scroll_top.min(rows_all.len().saturating_sub(1));
if let Some(sel) = state.selected_idx {
if sel < start_idx {
start_idx = sel;
} else if visible_items > 0 {
let bottom = start_idx + visible_items - 1;
if sel > bottom {
start_idx = sel + 1 - visible_items;
}
}
}
let desc_col = compute_desc_col(rows_all, start_idx, visible_items, content_width);
use crate::wrapping::RtOptions;
use crate::wrapping::word_wrap_line;
let mut total: u16 = 0;
for row in rows_all
.iter()
.enumerate()
.skip(start_idx)
.take(visible_items)
.map(|(_, r)| r)
{
let full_line = build_full_line(row, desc_col);
let opts = RtOptions::new(content_width as usize)
.initial_indent(Line::from(""))
.subsequent_indent(Line::from(" ".repeat(desc_col)));
total = total.saturating_add(word_wrap_line(&full_line, opts).len() as u16);
}
total.max(1)
}

View File

@@ -4,5 +4,5 @@ expression: terminal.backend()
---
"▌ /mo "
"▌ "
"▌/model choose what model and reasoning effort to use "
"▌/model choose what model and reasoning effort to use "
"▌/mention mention a file "

View File

@@ -6,6 +6,6 @@ expression: render_lines(&view)
▌ Switch between Codex approval presets
▌> 1. Read Only (current) Codex can read files
▌ 2. Full Access Codex can edit files
▌ 2. Full Access Codex can edit files
Press Enter to confirm or Esc to go back

View File

@@ -5,6 +5,6 @@ expression: render_lines(&view)
▌ Select Approval Mode
▌> 1. Read Only (current) Codex can read files
▌ 2. Full Access Codex can edit files
▌ 2. Full Access Codex can edit files
Press Enter to confirm or Esc to go back

View File

@@ -4,6 +4,7 @@ use std::path::PathBuf;
use std::sync::Arc;
use codex_core::config::Config;
use codex_core::config_types::Notifications;
use codex_core::protocol::AgentMessageDeltaEvent;
use codex_core::protocol::AgentMessageEvent;
use codex_core::protocol::AgentReasoningDeltaEvent;
@@ -59,6 +60,7 @@ use crate::bottom_pane::InputResult;
use crate::bottom_pane::SelectionAction;
use crate::bottom_pane::SelectionItem;
use crate::clipboard_paste::paste_image_to_temp_png;
use crate::diff_render::display_path_for;
use crate::get_git_diff::get_git_diff;
use crate::history_cell;
use crate::history_cell::CommandOutput;
@@ -66,6 +68,7 @@ use crate::history_cell::ExecCell;
use crate::history_cell::HistoryCell;
use crate::history_cell::PatchEventType;
use crate::slash_command::SlashCommand;
use crate::text_formatting::truncate_text;
use crate::tui::FrameRequester;
// streaming internals are provided by crate::streaming and crate::markdown_stream
use crate::user_approval_widget::ApprovalRequest;
@@ -136,6 +139,8 @@ pub(crate) struct ChatWidget {
suppress_session_configured_redraw: bool,
// User messages queued while a turn is in progress
queued_user_messages: VecDeque<UserMessage>,
// Pending notification to show when unfocused on next Draw
pending_notification: Option<Notification>,
}
struct UserMessage {
@@ -265,6 +270,8 @@ impl ChatWidget {
// If there is a queued user message, send exactly one now to begin the next turn.
self.maybe_send_next_queued_input();
// Emit a notification when the turn completes (suppressed if focused).
self.notify(Notification::AgentTurnComplete);
}
pub(crate) fn set_token_info(&mut self, info: Option<TokenUsageInfo>) {
@@ -531,6 +538,9 @@ impl ChatWidget {
self.flush_answer_stream_with_separator();
// Emit the proposed command into history (like proposed patches)
self.add_to_history(history_cell::new_proposed_command(&ev.command));
let command = shlex::try_join(ev.command.iter().map(|s| s.as_str()))
.unwrap_or_else(|_| ev.command.join(" "));
self.notify(Notification::ExecApprovalRequested { command });
let request = ApprovalRequest::Exec {
id,
@@ -560,6 +570,10 @@ impl ChatWidget {
};
self.bottom_pane.push_approval_request(request);
self.request_redraw();
self.notify(Notification::EditApprovalRequested {
cwd: self.config.cwd.clone(),
changes: ev.changes.keys().cloned().collect(),
});
}
pub(crate) fn handle_exec_begin_now(&mut self, ev: ExecCommandBeginEvent) {
@@ -686,6 +700,7 @@ impl ChatWidget {
queued_user_messages: VecDeque::new(),
show_welcome_banner: true,
suppress_session_configured_redraw: false,
pending_notification: None,
}
}
@@ -741,6 +756,7 @@ impl ChatWidget {
queued_user_messages: VecDeque::new(),
show_welcome_banner: true,
suppress_session_configured_redraw: true,
pending_notification: None,
}
}
@@ -1137,6 +1153,20 @@ impl ChatWidget {
self.frame_requester.schedule_frame();
}
fn notify(&mut self, notification: Notification) {
if !notification.allowed_for(&self.config.tui_notifications) {
return;
}
self.pending_notification = Some(notification);
self.request_redraw();
}
pub(crate) fn maybe_post_pending_notification(&mut self, tui: &mut crate::tui::Tui) {
if let Some(notif) = self.pending_notification.take() {
tui.notify(notif.display());
}
}
/// Mark the active exec cell as failed (✗) and flush it into history.
fn finalize_active_exec_cell_as_failed(&mut self) {
if let Some(cell) = self.active_exec_cell.take() {
@@ -1449,6 +1479,49 @@ impl WidgetRef for &ChatWidget {
}
}
enum Notification {
AgentTurnComplete,
ExecApprovalRequested { command: String },
EditApprovalRequested { cwd: PathBuf, changes: Vec<PathBuf> },
}
impl Notification {
fn display(&self) -> String {
match self {
Notification::AgentTurnComplete => "Agent turn complete".to_string(),
Notification::ExecApprovalRequested { command } => {
format!("Approval requested: {}", truncate_text(command, 30))
}
Notification::EditApprovalRequested { cwd, changes } => {
format!(
"Codex wants to edit {}",
if changes.len() == 1 {
#[allow(clippy::unwrap_used)]
display_path_for(changes.first().unwrap(), cwd)
} else {
format!("{} files", changes.len())
}
)
}
}
}
fn type_name(&self) -> &str {
match self {
Notification::AgentTurnComplete => "agent-turn-complete",
Notification::ExecApprovalRequested { .. }
| Notification::EditApprovalRequested { .. } => "approval-requested",
}
}
fn allowed_for(&self, settings: &Notifications) -> bool {
match settings {
Notifications::Enabled(enabled) => *enabled,
Notifications::Custom(allowed) => allowed.iter().any(|a| a == self.type_name()),
}
}
}
const EXAMPLE_PROMPTS: [&str; 6] = [
"Explain this codebase",
"Summarize recent commits",

View File

@@ -251,6 +251,7 @@ fn make_chatwidget_manual() -> (
show_welcome_banner: true,
queued_user_messages: VecDeque::new(),
suppress_session_configured_redraw: false,
pending_notification: None,
};
(widget, rx, op_rx)
}

View File

@@ -265,7 +265,7 @@ fn render_changes_block(
out
}
fn display_path_for(path: &Path, cwd: &Path) -> String {
pub(crate) fn display_path_for(path: &Path, cwd: &Path) -> String {
let path_in_same_repo = match (get_git_repo_root(cwd), get_git_repo_root(path)) {
(Some(cwd_repo), Some(path_repo)) => cwd_repo == path_repo,
_ => false,

View File

@@ -17,7 +17,9 @@ use crossterm::SynchronizedUpdate;
use crossterm::cursor;
use crossterm::cursor::MoveTo;
use crossterm::event::DisableBracketedPaste;
use crossterm::event::DisableFocusChange;
use crossterm::event::EnableBracketedPaste;
use crossterm::event::EnableFocusChange;
use crossterm::event::Event;
use crossterm::event::KeyEvent;
use crossterm::event::KeyboardEnhancementFlags;
@@ -60,6 +62,8 @@ pub fn set_modes() -> Result<()> {
| KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS
)
);
let _ = execute!(stdout(), EnableFocusChange);
Ok(())
}
@@ -111,6 +115,7 @@ pub fn restore() -> Result<()> {
// Pop may fail on platforms that didn't support the push; ignore errors.
let _ = execute!(stdout(), PopKeyboardEnhancementFlags);
execute!(stdout(), DisableBracketedPaste)?;
let _ = execute!(stdout(), DisableFocusChange);
disable_raw_mode()?;
let _ = execute!(stdout(), crossterm::cursor::Show);
Ok(())
@@ -163,6 +168,8 @@ pub struct Tui {
suspend_cursor_y: Arc<AtomicU16>, // Bottom line of inline viewport
// True when overlay alt-screen UI is active
alt_screen_active: Arc<AtomicBool>,
// True when terminal/tab is focused; updated internally from crossterm events
terminal_focused: Arc<AtomicBool>,
}
#[cfg(unix)]
@@ -214,6 +221,16 @@ impl FrameRequester {
}
impl Tui {
/// Emit a desktop notification now if the terminal is unfocused.
/// Returns true if a notification was posted.
pub fn notify(&mut self, message: impl AsRef<str>) -> bool {
if !self.terminal_focused.load(Ordering::Relaxed) {
let _ = execute!(stdout(), PostNotification(message.as_ref().to_string()));
true
} else {
false
}
}
pub fn new(terminal: Terminal) -> Self {
let (frame_schedule_tx, frame_schedule_rx) = tokio::sync::mpsc::unbounded_channel();
let (draw_tx, _) = tokio::sync::broadcast::channel(1);
@@ -270,6 +287,7 @@ impl Tui {
#[cfg(unix)]
suspend_cursor_y: Arc::new(AtomicU16::new(0)),
alt_screen_active: Arc::new(AtomicBool::new(false)),
terminal_focused: Arc::new(AtomicBool::new(true)),
}
}
@@ -289,6 +307,7 @@ impl Tui {
let alt_screen_active = self.alt_screen_active.clone();
#[cfg(unix)]
let suspend_cursor_y = self.suspend_cursor_y.clone();
let terminal_focused = self.terminal_focused.clone();
let event_stream = async_stream::stream! {
loop {
select! {
@@ -332,6 +351,12 @@ impl Tui {
Event::Paste(pasted) => {
yield TuiEvent::Paste(pasted);
}
Event::FocusGained => {
terminal_focused.store(true, Ordering::Relaxed);
}
Event::FocusLost => {
terminal_focused.store(false, Ordering::Relaxed);
}
_ => {}
}
}
@@ -535,3 +560,25 @@ impl Tui {
})?
}
}
/// Command that emits an OSC 9 desktop notification with a message.
#[derive(Debug, Clone)]
pub struct PostNotification(pub String);
impl Command for PostNotification {
fn write_ansi(&self, f: &mut impl std::fmt::Write) -> std::fmt::Result {
write!(f, "\x1b]9;{}\x07", self.0)
}
#[cfg(windows)]
fn execute_winapi(&self) -> std::io::Result<()> {
Err(std::io::Error::other(
"tried to execute PostNotification using WinAPI; use ANSI instead",
))
}
#[cfg(windows)]
fn is_ansi_code_supported(&self) -> bool {
true
}
}

View File

@@ -504,6 +504,9 @@ To have Codex use this script for notifications, you would configure it via `not
notify = ["python3", "/Users/mbolin/.codex/notify.py"]
```
> [!NOTE]
> Use `notify` for automation and integrations: Codex invokes your external program with a single JSON argument for each event, independent of the TUI. If you only want lightweight desktop notifications while using the TUI, prefer `tui.notifications`, which uses terminal escape codes and requires no external program. You can enable both; `tui.notifications` covers inTUI alerts (e.g., approval prompts), while `notify` is best for systemlevel hooks or custom notifiers. Currently, `notify` emits only `agent-turn-complete`, whereas `tui.notifications` supports `agent-turn-complete` and `approval-requested` with optional filtering.
## history
By default, Codex CLI records messages sent to the model in `$CODEX_HOME/history.jsonl`. Note that on UNIX, the file permissions are set to `o600`, so it should only be readable and writable by the owner.
@@ -576,9 +579,21 @@ Options that are specific to the TUI.
```toml
[tui]
# More to come here
# Send desktop notifications when approvals are required or a turn completes.
# Defaults to false.
notifications = true
# You can optionally filter to specific notification types.
# Available types are "agent-turn-complete" and "approval-requested".
notifications = [ "agent-turn-complete", "approval-requested" ]
```
> [!NOTE]
> Codex emits desktop notifications using terminal escape codes. Not all terminals support these (notably, macOS Terminal.app and VS Code's terminal do not support custom notifications. iTerm2, Ghostty and WezTerm do support these notifications).
> [!NOTE]
> `tui.notifications` is builtin and limited to the TUI session. For programmatic or crossenvironment notifications—or to integrate with OSspecific notifiers—use the toplevel `notify` option to run an external program that receives event JSON. The two settings are independent and can be used together.
## Config reference
| Key | Type / Values | Notes |
@@ -616,7 +631,8 @@ Options that are specific to the TUI.
| `history.persistence` | `save-all` \| `none` | History file persistence (default: `save-all`). |
| `history.max_bytes` | number | Currently ignored (not enforced). |
| `file_opener` | `vscode` \| `vscode-insiders` \| `windsurf` \| `cursor` \| `none` | URI scheme for clickable citations (default: `vscode`). |
| `tui` | table | TUIspecific options (reserved). |
| `tui` | table | TUIspecific options. |
| `tui.notifications` | boolean \| array<string> | Enable desktop notifications in the tui (default: false). |
| `hide_agent_reasoning` | boolean | Hide model reasoning events. |
| `show_raw_agent_reasoning` | boolean | Show raw reasoning (when available). |
| `model_reasoning_effort` | `minimal` \| `low` \| `medium` \| `high` | Responses API reasoning effort. |