Files
codex/codex-rs/features/src/lib.rs
Felipe Coury 5591912f0b fix(tui): reflow scrollback on terminal resize (#18575)
Fixes multiple scrollback and terminal resize issues: #5538, #5576,
#8352, #12223, #16165, and #15380.

## Why

Codex writes finalized transcript output into terminal scrollback after
wrapping it for the current viewport width. A later terminal resize
could leave that scrollback shaped for the old width, so wider windows
kept narrow output and narrower windows could show stale wrapping
artifacts until enough new output replaced the visible area.

This is also the foundation PR for responsive markdown tables. Table
rendering needs finalized transcript content to be width-sensitive after
insertion, not only while content is first streaming. Markdown table
rendering itself stays in #18576.

## Stack

- PR1: resize backlog reflow and interrupt cleanup
- #18576: markdown table support

## What Changed

- Rebuild source-backed transcript history when the terminal width
changes. `terminal_resize_reflow` is introduced through the experimental
feature system, but is enabled by default for this rollout so we can
validate behavior across real terminals.
- Preserve assistant and plan stream source so finalized streaming
output can participate in resize reflow after consolidation.
- Debounce resize work, but force a final source-backed reflow when a
resize happened during active or unconsolidated streaming output.
- Clear stale pending history lines on resize so old-width wrapped
output is not emitted just before rebuilt scrollback.
- Bound replay work with `[tui.terminal_resize_reflow].max_rows`:
omitted uses terminal-specific defaults, `0` keeps all rendered rows,
and a positive value sets an explicit cap. The cap applies both while
initially replaying a resumed transcript into scrollback and when
rebuilding scrollback after terminal resize.
- Consolidate interrupted assistant streams before cleanup, then clear
pending stream output and active-tail state consistently.
- Move resize reflow and thread event buffering helpers out of `app.rs`
into dedicated TUI modules.
- Add focused coverage for resize reflow, feature-gated behavior,
streaming source preservation, interrupted output cleanup,
unicode-neutral text, terminal-specific row caps, and composer/layout
stability.

## Runtime Bounds

Resize reflow keeps only the most recent rendered rows when a row cap is
active. The default is `auto`, which maps to the detected terminal's
default scrollback size where Codex can identify it: VS Code `1000`,
Windows Terminal `9001`, WezTerm `3500`, and Alacritty `10000`.
Terminals without a dedicated mapping use the conservative fallback of
`1000` rows. Users can override this with `[tui.terminal_resize_reflow]
max_rows = N`, or set `max_rows = 0` to disable row limiting.

## Validation

- `just fmt`
- `git diff --check`
- `cargo test --manifest-path codex-rs/Cargo.toml -p codex-tui reflow`
- `cargo test --manifest-path codex-rs/Cargo.toml -p codex-tui
transcript_reflow`
- `just fix -p codex-tui`
- PR CI in progress on the squashed branch
2026-04-25 22:00:32 -03:00

1093 lines
34 KiB
Rust

//! Centralized feature flags and metadata.
//!
//! This crate defines the feature registry plus the logic used to resolve an
//! effective feature set from config-like inputs.
use codex_otel::SessionTelemetry;
use codex_protocol::protocol::Event;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::WarningEvent;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::collections::BTreeMap;
use std::collections::BTreeSet;
use toml::Table;
mod feature_configs;
mod legacy;
pub use feature_configs::MultiAgentV2ConfigToml;
use legacy::LegacyFeatureToggles;
pub use legacy::legacy_feature_keys;
/// High-level lifecycle stage for a feature.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Stage {
/// Features that are still under development, not ready for external use
UnderDevelopment,
/// Experimental features made available to users through the `/experimental` menu
Experimental {
name: &'static str,
menu_description: &'static str,
announcement: &'static str,
},
/// Stable features. The feature flag is kept for ad-hoc enabling/disabling
Stable,
/// Deprecated feature that should not be used anymore.
Deprecated,
/// The feature flag is useless but kept for backward compatibility reason.
Removed,
}
impl Stage {
pub fn experimental_menu_name(self) -> Option<&'static str> {
match self {
Stage::Experimental { name, .. } => Some(name),
Stage::UnderDevelopment | Stage::Stable | Stage::Deprecated | Stage::Removed => None,
}
}
pub fn experimental_menu_description(self) -> Option<&'static str> {
match self {
Stage::Experimental {
menu_description, ..
} => Some(menu_description),
Stage::UnderDevelopment | Stage::Stable | Stage::Deprecated | Stage::Removed => None,
}
}
pub fn experimental_announcement(self) -> Option<&'static str> {
match self {
Stage::Experimental {
announcement: "", ..
} => None,
Stage::Experimental { announcement, .. } => Some(announcement),
Stage::UnderDevelopment | Stage::Stable | Stage::Deprecated | Stage::Removed => None,
}
}
}
/// Unique features toggled via configuration.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Feature {
// Stable.
/// Create a ghost commit at each turn.
GhostCommit,
/// Enable the default shell tool.
ShellTool,
/// Enable Claude-style lifecycle hooks loaded from hooks.json files.
CodexHooks,
// Experimental
/// Removed compatibility flag for the deleted JavaScript REPL feature.
JsRepl,
/// Enable JavaScript code mode backed by the in-process V8 runtime.
CodeMode,
/// Restrict model-visible tools to code mode entrypoints (`exec`, `wait`).
CodeModeOnly,
/// Removed compatibility flag for the deleted JavaScript REPL tool-only mode.
JsReplToolsOnly,
/// Use the single unified PTY-backed exec tool.
UnifiedExec,
/// Route shell tool execution through the zsh exec bridge.
ShellZshFork,
/// Reflow transcript scrollback when the terminal is resized.
TerminalResizeReflow,
/// Include the freeform apply_patch tool.
ApplyPatchFreeform,
/// Stream structured progress while apply_patch input is being generated.
ApplyPatchStreamingEvents,
/// Allow exec tools to request additional permissions while staying sandboxed.
ExecPermissionApprovals,
/// Expose the built-in request_permissions tool.
RequestPermissionsTool,
/// Allow the model to request web searches that fetch live content.
WebSearchRequest,
/// Allow the model to request web searches that fetch cached content.
/// Takes precedence over `WebSearchRequest`.
WebSearchCached,
/// Legacy search-tool feature flag kept for backward compatibility.
SearchTool,
/// Removed legacy Linux bubblewrap opt-in flag retained as a no-op so old
/// wrappers and config can still parse it.
UseLinuxSandboxBwrap,
/// Use the legacy Landlock Linux sandbox fallback instead of the default
/// bubblewrap pipeline.
UseLegacyLandlock,
/// Allow the model to request approval and propose exec rules.
RequestRule,
/// Enable Windows sandbox (restricted token) on Windows.
WindowsSandbox,
/// Use the elevated Windows sandbox pipeline (setup + runner).
WindowsSandboxElevated,
/// Legacy remote models flag kept for backward compatibility.
RemoteModels,
/// Experimental shell snapshotting.
ShellSnapshot,
/// Enable git commit attribution guidance via model instructions.
CodexGitCommit,
/// Enable runtime metrics snapshots via a manual reader.
RuntimeMetrics,
/// Enable thread lifecycle analytics emitted via the app-server analytics pipeline.
GeneralAnalytics,
/// Persist rollout metadata to a local SQLite database.
Sqlite,
/// Enable startup memory extraction and file-backed memory consolidation.
MemoryTool,
/// Enable the Chronicle sidecar for passive screen-context memories.
Chronicle,
/// Append additional AGENTS.md guidance to user instructions.
ChildAgentsMd,
/// Compress request bodies (zstd) when sending streaming requests to codex-backend.
EnableRequestCompression,
/// Enable collab tools.
Collab,
/// Enable task-path-based multi-agent routing.
MultiAgentV2,
/// Enable CSV-backed agent job tools.
SpawnCsv,
/// Enable apps.
Apps,
/// Enable the tool_search tool for apps.
ToolSearch,
/// Always defer MCP tools behind tool_search instead of exposing small sets directly.
ToolSearchAlwaysDeferMcpTools,
/// Expose placeholder tools for unavailable historical tool calls.
UnavailableDummyTools,
/// Enable discoverable tool suggestions for apps.
ToolSuggest,
/// Enable plugins.
Plugins,
/// Allow the in-app browser pane in desktop apps.
///
/// Requirements-only gate: this should be set from requirements, not user config.
InAppBrowser,
/// Allow Browser Use agent integration in desktop apps.
///
/// Requirements-only gate: this should be set from requirements, not user config.
BrowserUse,
/// Allow Codex Computer Use.
///
/// Requirements-only gate: this should be set from requirements, not user config.
ComputerUse,
/// Temporary internal-only flag for PS-backed remote plugin catalog development.
RemotePlugin,
/// Show the startup prompt for migrating external agent config into Codex.
ExternalMigration,
/// Allow the model to invoke the built-in image generation tool.
ImageGeneration,
/// Allow prompting and installing missing MCP dependencies.
SkillMcpDependencyInstall,
/// Prompt for missing skill env var dependencies.
SkillEnvVarDependencyPrompt,
/// Steer feature flag - when enabled, Enter submits immediately instead of queuing.
/// Kept for config backward compatibility; behavior is always steer-enabled.
Steer,
/// Allow request_user_input in Default collaboration mode.
DefaultModeRequestUserInput,
/// Enable automatic review for approval prompts.
GuardianApproval,
/// Enable persisted thread goals and automatic goal continuation.
Goals,
/// Enable collaboration modes (Plan, Default).
/// Kept for config backward compatibility; behavior is always collaboration-modes-enabled.
CollaborationModes,
/// Route MCP tool approval prompts through the MCP elicitation request path.
ToolCallMcpElicitation,
/// Enable personality selection in the TUI.
Personality,
/// Enable native artifact tools.
Artifact,
/// Enable Fast mode selection in the TUI and request layer.
FastMode,
/// Enable experimental realtime voice conversation mode in the TUI.
RealtimeConversation,
/// Connect app-server to the ChatGPT remote control service.
RemoteControl,
/// Removed compatibility flag retained as a no-op so old wrappers can
/// still pass `--enable image_detail_original`.
ImageDetailOriginal,
/// Removed compatibility flag. The TUI now always uses the app-server implementation.
TuiAppServer,
/// Prevent idle system sleep while a turn is actively running.
PreventIdleSleep,
/// Enable workspace-specific owner nudge copy and prompts in the TUI.
WorkspaceOwnerUsageNudge,
/// Legacy rollout flag for Responses API WebSocket transport experiments.
ResponsesWebsockets,
/// Legacy rollout flag for Responses API WebSocket transport v2 experiments.
ResponsesWebsocketsV2,
/// Enable workspace dependency support.
WorkspaceDependencies,
}
impl Feature {
pub fn key(self) -> &'static str {
self.info().key
}
pub fn stage(self) -> Stage {
self.info().stage
}
pub fn default_enabled(self) -> bool {
self.info().default_enabled
}
fn info(self) -> &'static FeatureSpec {
FEATURES
.iter()
.find(|spec| spec.id == self)
.unwrap_or_else(|| unreachable!("missing FeatureSpec for {self:?}"))
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct LegacyFeatureUsage {
pub alias: String,
pub feature: Feature,
pub summary: String,
pub details: Option<String>,
}
/// Holds the effective set of enabled features.
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Features {
enabled: BTreeSet<Feature>,
legacy_usages: BTreeSet<LegacyFeatureUsage>,
}
#[derive(Debug, Clone, Default)]
pub struct FeatureOverrides {
pub include_apply_patch_tool: Option<bool>,
pub web_search_request: Option<bool>,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct FeatureConfigSource<'a> {
pub features: Option<&'a FeaturesToml>,
pub include_apply_patch_tool: Option<bool>,
pub experimental_use_freeform_apply_patch: Option<bool>,
pub experimental_use_unified_exec_tool: Option<bool>,
}
impl FeatureOverrides {
fn apply(self, features: &mut Features) {
LegacyFeatureToggles {
include_apply_patch_tool: self.include_apply_patch_tool,
..Default::default()
}
.apply(features);
if let Some(enabled) = self.web_search_request {
if enabled {
features.enable(Feature::WebSearchRequest);
} else {
features.disable(Feature::WebSearchRequest);
}
features.record_legacy_usage("web_search_request", Feature::WebSearchRequest);
}
}
}
impl Features {
/// Starts with built-in defaults.
pub fn with_defaults() -> Self {
let mut set = BTreeSet::new();
for spec in FEATURES {
if spec.default_enabled {
set.insert(spec.id);
}
}
Self {
enabled: set,
legacy_usages: BTreeSet::new(),
}
}
pub fn enabled(&self, f: Feature) -> bool {
self.enabled.contains(&f)
}
pub fn apps_enabled_for_auth(&self, has_chatgpt_auth: bool) -> bool {
self.enabled(Feature::Apps) && has_chatgpt_auth
}
pub fn use_legacy_landlock(&self) -> bool {
self.enabled(Feature::UseLegacyLandlock)
}
pub fn enable(&mut self, f: Feature) -> &mut Self {
self.enabled.insert(f);
self
}
pub fn disable(&mut self, f: Feature) -> &mut Self {
self.enabled.remove(&f);
self
}
pub fn set_enabled(&mut self, f: Feature, enabled: bool) -> &mut Self {
if enabled {
self.enable(f)
} else {
self.disable(f)
}
}
pub fn record_legacy_usage_force(&mut self, alias: &str, feature: Feature) {
let (summary, details) = legacy_usage_notice(alias, feature);
self.legacy_usages.insert(LegacyFeatureUsage {
alias: alias.to_string(),
feature,
summary,
details,
});
}
pub fn record_legacy_usage(&mut self, alias: &str, feature: Feature) {
if alias == feature.key() {
return;
}
self.record_legacy_usage_force(alias, feature);
}
pub fn legacy_feature_usages(&self) -> impl Iterator<Item = &LegacyFeatureUsage> + '_ {
self.legacy_usages.iter()
}
pub fn emit_metrics(&self, otel: &SessionTelemetry) {
for feature in FEATURES {
if matches!(feature.stage, Stage::Removed) {
continue;
}
if self.enabled(feature.id) != feature.default_enabled {
otel.counter(
"codex.feature.state",
/*inc*/ 1,
&[
("feature", feature.key),
("value", &self.enabled(feature.id).to_string()),
],
);
}
}
}
/// Apply a table of key -> bool toggles (e.g. from TOML).
pub fn apply_map(&mut self, m: &BTreeMap<String, bool>) {
for (k, v) in m {
match k.as_str() {
"web_search_request" => {
self.record_legacy_usage_force(
"features.web_search_request",
Feature::WebSearchRequest,
);
}
"web_search_cached" => {
self.record_legacy_usage_force(
"features.web_search_cached",
Feature::WebSearchCached,
);
}
"tui_app_server" => {
continue;
}
"js_repl" => {
continue;
}
"js_repl_tools_only" => {
continue;
}
"image_detail_original" => {
continue;
}
"use_legacy_landlock" => {
self.record_legacy_usage_force(
"features.use_legacy_landlock",
Feature::UseLegacyLandlock,
);
}
_ => {}
}
match feature_for_key(k) {
Some(feat) => {
if matches!(feat, Feature::TuiAppServer) {
continue;
}
if k != feat.key() {
self.record_legacy_usage(k.as_str(), feat);
}
if *v {
self.enable(feat);
} else {
self.disable(feat);
}
}
None => {
tracing::warn!("unknown feature key in config: {k}");
}
}
}
}
pub fn from_sources(
base: FeatureConfigSource<'_>,
profile: FeatureConfigSource<'_>,
overrides: FeatureOverrides,
) -> Self {
let mut features = Features::with_defaults();
for source in [base, profile] {
LegacyFeatureToggles {
include_apply_patch_tool: source.include_apply_patch_tool,
experimental_use_freeform_apply_patch: source.experimental_use_freeform_apply_patch,
experimental_use_unified_exec_tool: source.experimental_use_unified_exec_tool,
}
.apply(&mut features);
if let Some(feature_entries) = source.features {
features.apply_toml(feature_entries);
}
}
overrides.apply(&mut features);
features.normalize_dependencies();
features
}
pub fn enabled_features(&self) -> Vec<Feature> {
self.enabled.iter().copied().collect()
}
pub fn normalize_dependencies(&mut self) {
if self.enabled(Feature::SpawnCsv) && !self.enabled(Feature::Collab) {
self.enable(Feature::Collab);
}
if self.enabled(Feature::CodeModeOnly) && !self.enabled(Feature::CodeMode) {
self.enable(Feature::CodeMode);
}
}
}
fn legacy_usage_notice(alias: &str, feature: Feature) -> (String, Option<String>) {
let canonical = feature.key();
match feature {
Feature::WebSearchRequest | Feature::WebSearchCached => {
let label = match alias {
"web_search" => "[features].web_search",
"features.web_search_request" | "web_search_request" => {
"[features].web_search_request"
}
"features.web_search_cached" | "web_search_cached" => {
"[features].web_search_cached"
}
_ => alias,
};
let summary =
format!("`{label}` is deprecated because web search is enabled by default.");
(summary, Some(web_search_details().to_string()))
}
Feature::UseLegacyLandlock => {
let label = match alias {
"features.use_legacy_landlock" | "use_legacy_landlock" => {
"[features].use_legacy_landlock"
}
_ => alias,
};
let summary = format!("`{label}` is deprecated and will be removed soon.");
let details =
"Remove this setting to stop opting into the legacy Linux sandbox behavior."
.to_string();
(summary, Some(details))
}
_ => {
let label = if alias.contains('.') || alias.starts_with('[') {
alias.to_string()
} else {
format!("[features].{alias}")
};
let summary = format!("`{label}` is deprecated. Use `[features].{canonical}` instead.");
let details = if alias == canonical {
None
} else {
Some(format!(
"Enable it with `--enable {canonical}` or `[features].{canonical}` in config.toml. See https://developers.openai.com/codex/config-basic#feature-flags for details."
))
};
(summary, details)
}
}
}
fn web_search_details() -> &'static str {
"Set `web_search` to `\"live\"`, `\"cached\"`, or `\"disabled\"` at the top level (or under a profile) in config.toml if you want to override it."
}
/// Keys accepted in `[features]` tables.
pub fn feature_for_key(key: &str) -> Option<Feature> {
for spec in FEATURES {
if spec.key == key {
return Some(spec.id);
}
}
legacy::feature_for_key(key)
}
pub fn canonical_feature_for_key(key: &str) -> Option<Feature> {
FEATURES
.iter()
.find(|spec| spec.key == key)
.map(|spec| spec.id)
}
/// Returns `true` if the provided string matches a known feature toggle key.
pub fn is_known_feature_key(key: &str) -> bool {
feature_for_key(key).is_some()
}
/// Deserializable features table for TOML.
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, JsonSchema)]
pub struct FeaturesToml {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub multi_agent_v2: Option<FeatureToml<MultiAgentV2ConfigToml>>,
/// Boolean feature toggles keyed by canonical or legacy feature name.
#[serde(flatten)]
entries: BTreeMap<String, bool>,
}
impl Features {
fn apply_toml(&mut self, features: &FeaturesToml) {
let entries = features.entries();
self.apply_map(&entries);
}
}
impl FeaturesToml {
pub fn entries(&self) -> BTreeMap<String, bool> {
let mut entries = self.entries.clone();
if let Some(enabled) = self.multi_agent_v2.as_ref().and_then(FeatureToml::enabled) {
entries.insert(Feature::MultiAgentV2.key().to_string(), enabled);
}
entries
}
}
impl From<BTreeMap<String, bool>> for FeaturesToml {
fn from(entries: BTreeMap<String, bool>) -> Self {
Self {
entries,
..Default::default()
}
}
}
// To be used for features that need more configuration than just enabled/disabled and
// require a custom config struct under `[features]`.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)]
#[serde(untagged)]
pub enum FeatureToml<T> {
Enabled(bool),
Config(T),
}
impl<T: FeatureConfig> FeatureToml<T> {
pub fn enabled(&self) -> Option<bool> {
match self {
Self::Enabled(enabled) => Some(*enabled),
Self::Config(config) => config.enabled(),
}
}
}
// A trait to be implemented by custom feature config structs when defining a feature that needs more configuration than
// just enabled/disabled.
pub trait FeatureConfig {
fn enabled(&self) -> Option<bool>;
}
/// Single, easy-to-read registry of all feature definitions.
#[derive(Debug, Clone, Copy)]
pub struct FeatureSpec {
pub id: Feature,
pub key: &'static str,
pub stage: Stage,
pub default_enabled: bool,
}
pub const FEATURES: &[FeatureSpec] = &[
// Stable features.
FeatureSpec {
id: Feature::GhostCommit,
key: "undo",
stage: Stage::Stable,
default_enabled: false,
},
FeatureSpec {
id: Feature::ShellTool,
key: "shell_tool",
stage: Stage::Stable,
default_enabled: true,
},
FeatureSpec {
id: Feature::UnifiedExec,
key: "unified_exec",
stage: Stage::Stable,
default_enabled: !cfg!(windows),
},
FeatureSpec {
id: Feature::ShellZshFork,
key: "shell_zsh_fork",
stage: Stage::UnderDevelopment,
default_enabled: false,
},
FeatureSpec {
id: Feature::ShellSnapshot,
key: "shell_snapshot",
stage: Stage::Stable,
default_enabled: true,
},
FeatureSpec {
id: Feature::JsRepl,
key: "js_repl",
stage: Stage::Removed,
default_enabled: false,
},
FeatureSpec {
id: Feature::CodeMode,
key: "code_mode",
stage: Stage::UnderDevelopment,
default_enabled: false,
},
FeatureSpec {
id: Feature::CodeModeOnly,
key: "code_mode_only",
stage: Stage::UnderDevelopment,
default_enabled: false,
},
FeatureSpec {
id: Feature::JsReplToolsOnly,
key: "js_repl_tools_only",
stage: Stage::Removed,
default_enabled: false,
},
FeatureSpec {
id: Feature::TerminalResizeReflow,
key: "terminal_resize_reflow",
stage: Stage::Experimental {
name: "Terminal resize reflow",
menu_description: "Rebuild Codex-owned transcript scrollback when the terminal width changes.",
announcement: "",
},
default_enabled: true,
},
FeatureSpec {
id: Feature::WebSearchRequest,
key: "web_search_request",
stage: Stage::Deprecated,
default_enabled: false,
},
FeatureSpec {
id: Feature::WebSearchCached,
key: "web_search_cached",
stage: Stage::Deprecated,
default_enabled: false,
},
FeatureSpec {
id: Feature::SearchTool,
key: "search_tool",
stage: Stage::Removed,
default_enabled: false,
},
// Experimental program. Rendered in the `/experimental` menu for users.
FeatureSpec {
id: Feature::CodexGitCommit,
key: "codex_git_commit",
stage: Stage::UnderDevelopment,
default_enabled: false,
},
FeatureSpec {
id: Feature::RuntimeMetrics,
key: "runtime_metrics",
stage: Stage::UnderDevelopment,
default_enabled: false,
},
FeatureSpec {
id: Feature::GeneralAnalytics,
key: "general_analytics",
stage: Stage::Stable,
default_enabled: true,
},
FeatureSpec {
id: Feature::Sqlite,
key: "sqlite",
stage: Stage::Removed,
default_enabled: true,
},
FeatureSpec {
id: Feature::MemoryTool,
key: "memories",
stage: Stage::Experimental {
name: "Memories",
menu_description: "Allow Codex to create new memories from conversations and bring relevant memories into new conversations.",
announcement: "NEW: Codex can now generate and uses memories. Try is now with `/memories`",
},
default_enabled: false,
},
FeatureSpec {
id: Feature::Chronicle,
key: "chronicle",
stage: Stage::UnderDevelopment,
default_enabled: false,
},
FeatureSpec {
id: Feature::ChildAgentsMd,
key: "child_agents_md",
stage: Stage::UnderDevelopment,
default_enabled: false,
},
FeatureSpec {
id: Feature::ApplyPatchFreeform,
key: "apply_patch_freeform",
stage: Stage::UnderDevelopment,
default_enabled: false,
},
FeatureSpec {
id: Feature::ApplyPatchStreamingEvents,
key: "apply_patch_streaming_events",
stage: Stage::UnderDevelopment,
default_enabled: false,
},
FeatureSpec {
id: Feature::ExecPermissionApprovals,
key: "exec_permission_approvals",
stage: Stage::UnderDevelopment,
default_enabled: false,
},
FeatureSpec {
id: Feature::CodexHooks,
key: "codex_hooks",
stage: Stage::Stable,
default_enabled: true,
},
FeatureSpec {
id: Feature::RequestPermissionsTool,
key: "request_permissions_tool",
stage: Stage::UnderDevelopment,
default_enabled: false,
},
FeatureSpec {
id: Feature::UseLinuxSandboxBwrap,
key: "use_linux_sandbox_bwrap",
stage: Stage::Removed,
default_enabled: false,
},
FeatureSpec {
id: Feature::UseLegacyLandlock,
key: "use_legacy_landlock",
stage: Stage::Deprecated,
default_enabled: false,
},
FeatureSpec {
id: Feature::RequestRule,
key: "request_rule",
stage: Stage::Removed,
default_enabled: false,
},
FeatureSpec {
id: Feature::WindowsSandbox,
key: "experimental_windows_sandbox",
stage: Stage::Removed,
default_enabled: false,
},
FeatureSpec {
id: Feature::WindowsSandboxElevated,
key: "elevated_windows_sandbox",
stage: Stage::Removed,
default_enabled: false,
},
FeatureSpec {
id: Feature::RemoteModels,
key: "remote_models",
stage: Stage::Removed,
default_enabled: false,
},
FeatureSpec {
id: Feature::EnableRequestCompression,
key: "enable_request_compression",
stage: Stage::Stable,
default_enabled: true,
},
FeatureSpec {
id: Feature::Collab,
key: "multi_agent",
stage: Stage::Stable,
default_enabled: true,
},
FeatureSpec {
id: Feature::MultiAgentV2,
key: "multi_agent_v2",
stage: Stage::UnderDevelopment,
default_enabled: false,
},
FeatureSpec {
id: Feature::SpawnCsv,
key: "enable_fanout",
stage: Stage::UnderDevelopment,
default_enabled: false,
},
FeatureSpec {
id: Feature::Apps,
key: "apps",
stage: Stage::Stable,
default_enabled: true,
},
FeatureSpec {
id: Feature::ToolSearch,
key: "tool_search",
stage: Stage::Stable,
default_enabled: true,
},
FeatureSpec {
id: Feature::ToolSearchAlwaysDeferMcpTools,
key: "tool_search_always_defer_mcp_tools",
stage: Stage::UnderDevelopment,
default_enabled: false,
},
FeatureSpec {
id: Feature::UnavailableDummyTools,
key: "unavailable_dummy_tools",
stage: Stage::Stable,
default_enabled: true,
},
FeatureSpec {
id: Feature::ToolSuggest,
key: "tool_suggest",
stage: Stage::Stable,
default_enabled: true,
},
FeatureSpec {
id: Feature::Plugins,
key: "plugins",
stage: Stage::Stable,
default_enabled: true,
},
FeatureSpec {
id: Feature::InAppBrowser,
key: "in_app_browser",
stage: Stage::Stable,
default_enabled: true,
},
FeatureSpec {
id: Feature::BrowserUse,
key: "browser_use",
stage: Stage::Stable,
default_enabled: true,
},
FeatureSpec {
id: Feature::ComputerUse,
key: "computer_use",
stage: Stage::Stable,
default_enabled: true,
},
FeatureSpec {
id: Feature::RemotePlugin,
key: "remote_plugin",
stage: Stage::UnderDevelopment,
default_enabled: false,
},
FeatureSpec {
id: Feature::ExternalMigration,
key: "external_migration",
stage: Stage::Experimental {
name: "External migration",
menu_description: "Show a startup prompt when Codex detects migratable external agent config for this machine or project.",
announcement: "",
},
default_enabled: false,
},
FeatureSpec {
id: Feature::ImageGeneration,
key: "image_generation",
stage: Stage::Stable,
default_enabled: true,
},
FeatureSpec {
id: Feature::SkillMcpDependencyInstall,
key: "skill_mcp_dependency_install",
stage: Stage::Stable,
default_enabled: true,
},
FeatureSpec {
id: Feature::SkillEnvVarDependencyPrompt,
key: "skill_env_var_dependency_prompt",
stage: Stage::UnderDevelopment,
default_enabled: false,
},
FeatureSpec {
id: Feature::Steer,
key: "steer",
stage: Stage::Removed,
default_enabled: true,
},
FeatureSpec {
id: Feature::DefaultModeRequestUserInput,
key: "default_mode_request_user_input",
stage: Stage::UnderDevelopment,
default_enabled: false,
},
FeatureSpec {
id: Feature::GuardianApproval,
key: "guardian_approval",
stage: Stage::Stable,
default_enabled: true,
},
FeatureSpec {
id: Feature::Goals,
key: "goals",
stage: Stage::UnderDevelopment,
default_enabled: false,
},
FeatureSpec {
id: Feature::CollaborationModes,
key: "collaboration_modes",
stage: Stage::Removed,
default_enabled: true,
},
FeatureSpec {
id: Feature::ToolCallMcpElicitation,
key: "tool_call_mcp_elicitation",
stage: Stage::Stable,
default_enabled: true,
},
FeatureSpec {
id: Feature::Personality,
key: "personality",
stage: Stage::Stable,
default_enabled: true,
},
FeatureSpec {
id: Feature::Artifact,
key: "artifact",
stage: Stage::UnderDevelopment,
default_enabled: false,
},
FeatureSpec {
id: Feature::FastMode,
key: "fast_mode",
stage: Stage::Stable,
default_enabled: true,
},
FeatureSpec {
id: Feature::RealtimeConversation,
key: "realtime_conversation",
stage: Stage::UnderDevelopment,
default_enabled: false,
},
FeatureSpec {
id: Feature::RemoteControl,
key: "remote_control",
stage: Stage::UnderDevelopment,
default_enabled: false,
},
FeatureSpec {
id: Feature::ImageDetailOriginal,
key: "image_detail_original",
stage: Stage::Removed,
default_enabled: false,
},
FeatureSpec {
id: Feature::TuiAppServer,
key: "tui_app_server",
stage: Stage::Removed,
default_enabled: true,
},
FeatureSpec {
id: Feature::PreventIdleSleep,
key: "prevent_idle_sleep",
stage: if cfg!(any(
target_os = "macos",
target_os = "linux",
target_os = "windows"
)) {
Stage::Experimental {
name: "Prevent sleep while running",
menu_description: "Keep your computer awake while Codex is running a thread.",
announcement: "NEW: Prevent sleep while running is now available in /experimental.",
}
} else {
Stage::UnderDevelopment
},
default_enabled: false,
},
FeatureSpec {
id: Feature::WorkspaceOwnerUsageNudge,
key: "workspace_owner_usage_nudge",
stage: Stage::UnderDevelopment,
default_enabled: false,
},
FeatureSpec {
id: Feature::ResponsesWebsockets,
key: "responses_websockets",
stage: Stage::Removed,
default_enabled: false,
},
FeatureSpec {
id: Feature::ResponsesWebsocketsV2,
key: "responses_websockets_v2",
stage: Stage::Removed,
default_enabled: false,
},
FeatureSpec {
id: Feature::WorkspaceDependencies,
key: "workspace_dependencies",
stage: Stage::Stable,
default_enabled: true,
},
];
pub fn unstable_features_warning_event(
effective_features: Option<&Table>,
suppress_unstable_features_warning: bool,
features: &Features,
config_path: &str,
) -> Option<Event> {
if suppress_unstable_features_warning {
return None;
}
let mut under_development_feature_keys = Vec::new();
if let Some(table) = effective_features {
for (key, value) in table {
if value.as_bool() != Some(true) {
continue;
}
let Some(spec) = FEATURES.iter().find(|spec| spec.key == key.as_str()) else {
continue;
};
if !features.enabled(spec.id) {
continue;
}
if matches!(spec.stage, Stage::UnderDevelopment) {
under_development_feature_keys.push(spec.key.to_string());
}
}
}
if under_development_feature_keys.is_empty() {
return None;
}
let under_development_feature_keys = under_development_feature_keys.join(", ");
let message = format!(
"Under-development features enabled: {under_development_feature_keys}. Under-development features are incomplete and may behave unpredictably. To suppress this warning, set `suppress_unstable_features_warning = true` in {config_path}."
);
Some(Event {
id: String::new(),
msg: EventMsg::Warning(WarningEvent { message }),
})
}
#[cfg(test)]
mod tests;