diff --git a/CODEX_PLUS_PLUS_LEDGER.md b/CODEX_PLUS_PLUS_LEDGER.md
index 3c3426bf5c69..4b7cad4c3bfc 100644
--- a/CODEX_PLUS_PLUS_LEDGER.md
+++ b/CODEX_PLUS_PLUS_LEDGER.md
@@ -20,7 +20,7 @@ for the capability to work; use `—` when the capability is fully isolated.
| Durable user message inbox | Runtime | Let root agents leave bounded, non-blocking human notes that survive legacy and paginated history without entering model context, render distinctly, remain reviewable in the current thread, and show session-local unread markers for background threads. Subagents use native parent-worker communication instead. | `codex-rs/config/src/codex_plus_plus/user_message_inbox.rs`
`codex-rs/core/src/codex_plus_plus/user_message_inbox.rs`
`codex-rs/protocol/src/codex_plus_plus/user_message.rs`
`codex-rs/tui/src/codex_plus_plus/user_message_inbox.rs`
`codex-rs/tui/src/codex_plus_plus/user_messages.rs` | `codex-rs/config/src/config_toml.rs`
`codex-rs/core/config.schema.json`
`codex-rs/core/src/tools/spec_plan.rs`
`codex-rs/app-server-protocol/src/protocol/thread_history.rs`
`codex-rs/tui/src/app.rs`
`codex-rs/tui/src/app/agent_status_feed.rs`
`codex-rs/tui/src/app/app_server_events.rs`
`codex-rs/tui/src/app/session_lifecycle.rs`
`codex-rs/tui/src/app/side.rs`
`codex-rs/tui/src/app/thread_routing.rs`
`codex-rs/tui/src/chatwidget.rs`
`codex-rs/tui/src/chatwidget/constructor.rs`
`codex-rs/tui/src/chatwidget/streaming.rs`
`codex-rs/tui/src/chatwidget/slash_dispatch.rs`
`codex-rs/tui/src/slash_command.rs` | [#64](https://github.com/Pimpmuckl/codex/pull/64), [#66](https://github.com/Pimpmuckl/codex/pull/66), [#67](https://github.com/Pimpmuckl/codex/pull/67), [#68](https://github.com/Pimpmuckl/codex/pull/68) |
| Codex++ settings menu | TUI | Persist and verify automatic account selection, weekly-window auto-start, capacity-retry policy, and the experimental agent inbox from one focused settings surface. | `codex-rs/tui/src/chatwidget/slash_dispatch/codex_plus_plus/mod.rs`
`codex-rs/tui/src/codex_plus_plus/account_policy.rs` | `codex-rs/tui/src/chatwidget/slash_dispatch.rs`
`codex-rs/tui/src/app_event.rs`
`codex-rs/tui/src/app/event_dispatch.rs`
`codex-rs/tui/src/slash_command.rs`
`codex-rs/config/src/config_toml.rs`
`codex-rs/core/config.schema.json` | [#15](https://github.com/Pimpmuckl/codex/pull/15), [#28](https://github.com/Pimpmuckl/codex/pull/28), [#37](https://github.com/Pimpmuckl/codex/pull/37), [#66](https://github.com/Pimpmuckl/codex/pull/66) |
| PreToolUse `ask` Guardian review | Hooks | Treat hook `ask` as a capability-gated automatic Guardian review for every tool and block unless that review approves. | `codex-rs/core/src/tools/codex_plus_plus/pre_tool_use_review.rs` | `codex-rs/core/src/tools/registry.rs`
`codex-rs/core/src/tools/handlers/mcp.rs`
`codex-rs/core/src/hook_runtime.rs`
`codex-rs/hooks/src/events/pre_tool_use.rs`
`codex-rs/protocol/src/approvals.rs`
`codex-rs/app-server-protocol/src/protocol/v2/item.rs` | [#30](https://github.com/Pimpmuckl/codex/pull/30), [#36](https://github.com/Pimpmuckl/codex/pull/36), [#42](https://github.com/Pimpmuckl/codex/pull/42) |
-| Managed Destructive Command Guard | Hooks | Detect, install, verify, enable, disable, update, and repair the pinned DCG companion through native marketplace, plugin, config, installer, and hook-trust APIs with binary rollback. | `codex-rs/tui/src/codex_plus_plus/destructive_command_guard/mod.rs` | — | [#72](https://github.com/Pimpmuckl/codex/pull/72) |
+| Managed Destructive Command Guard | Hooks | Detect, install, verify, enable, disable, update, and repair the pinned DCG companion through native APIs with binary rollback; expose those states and actions in Codex++ settings and introduce Guardian once per global profile. | `codex-rs/tui/src/codex_plus_plus/destructive_command_guard/mod.rs`
`codex-rs/tui/src/chatwidget/slash_dispatch/codex_plus_plus/dcg.rs`
`codex-rs/config/src/codex_plus_plus/dcg_nux.rs`
`codex-rs/tui/src/codex_plus_plus/welcome.rs` | `codex-rs/config/src/config_toml.rs`
`codex-rs/core/config.schema.json`
`codex-rs/tui/src/app_event.rs`
`codex-rs/tui/src/app/event_dispatch.rs`
`codex-rs/tui/src/app/startup_prompts.rs`
`codex-rs/tui/src/history_cell/session.rs`
`codex-rs/tui/src/chatwidget/slash_dispatch.rs` | [#72](https://github.com/Pimpmuckl/codex/pull/72) |
| Native Windows TUI input ownership | Platform | Keep Windows console input in native mode, repair runtime drift, rebuild the event stream safely, and restore the original mode on exit. | `codex-rs/tui/src/tui/codex_plus_plus/native_windows_input.rs` | `codex-rs/tui/src/tui.rs`
`codex-rs/tui/src/tui/event_stream.rs` | [#8](https://github.com/Pimpmuckl/codex/pull/8), [#11](https://github.com/Pimpmuckl/codex/pull/11), [#20](https://github.com/Pimpmuckl/codex/pull/20) |
| Cross-platform migration checksums | Compatibility | Keep embedded SQLite migration checksums stable across LF and CRLF packaging, while accepting databases created by either line-ending variant. | `codex-rs/state/src/migrations.rs` | `.gitattributes`
`codex-rs/state/src/runtime.rs` | `4662e307be` |
| Codex++ welcome guidance | TUI | Replace upstream app promotion with a concise Codex++ tip and preserve the same help in session history. | `codex-rs/tui/src/codex_plus_plus/welcome.rs` | `codex-rs/tui/src/tooltips.rs`
`codex-rs/tui/src/history_cell/session.rs` | [#43](https://github.com/Pimpmuckl/codex/pull/43) |
diff --git a/codex-rs/config/src/codex_plus_plus/dcg_nux.rs b/codex-rs/config/src/codex_plus_plus/dcg_nux.rs
new file mode 100644
index 000000000000..17833e0f20c8
--- /dev/null
+++ b/codex-rs/config/src/codex_plus_plus/dcg_nux.rs
@@ -0,0 +1,10 @@
+use crate::ConfigLayerStack;
+
+/// Returns whether the global Destructive Command Guard introduction was shown.
+pub fn shown(config: &ConfigLayerStack) -> bool {
+ config
+ .effective_user_config()
+ .and_then(|config| config.get("codex_plus_plus_dcg_nux_shown").cloned())
+ .and_then(|value| value.as_bool())
+ .unwrap_or(false)
+}
diff --git a/codex-rs/config/src/codex_plus_plus/mod.rs b/codex-rs/config/src/codex_plus_plus/mod.rs
index 9e8241bd91a2..78e57097d587 100644
--- a/codex-rs/config/src/codex_plus_plus/mod.rs
+++ b/codex-rs/config/src/codex_plus_plus/mod.rs
@@ -1,7 +1,9 @@
+mod dcg_nux;
mod model_capacity_retry;
mod user_message_inbox;
mod weekly_usage_window;
+pub use dcg_nux::shown as codex_plus_plus_dcg_nux_shown;
pub use model_capacity_retry::ModelCapacityRetryMode;
pub use user_message_inbox::UserMessageInbox;
pub use weekly_usage_window::WeeklyUsageWindowAutoStart;
diff --git a/codex-rs/config/src/config_toml.rs b/codex-rs/config/src/config_toml.rs
index db034ad8dcf8..6cde28d0f759 100644
--- a/codex-rs/config/src/config_toml.rs
+++ b/codex-rs/config/src/config_toml.rs
@@ -271,6 +271,8 @@ pub struct ConfigToml {
pub model_capacity_retry_mode: Option,
/// Whether agents can leave durable, non-blocking messages for the user.
pub user_message_inbox: Option,
+ /// Whether the global Destructive Command Guard introduction was shown.
+ pub codex_plus_plus_dcg_nux_shown: Option,
/// Definition for MCP servers that Codex can reach out to for tool calls.
#[serde(default)]
// Uses the raw MCP input shape (custom deserialization) rather than `McpServerConfig`.
diff --git a/codex-rs/config/src/lib.rs b/codex-rs/config/src/lib.rs
index e99a08e327af..1df46d506d93 100644
--- a/codex-rs/config/src/lib.rs
+++ b/codex-rs/config/src/lib.rs
@@ -49,6 +49,7 @@ pub use cloud_config_layers::cloud_config_layers_from_fragments;
pub use codex_plus_plus::ModelCapacityRetryMode;
pub use codex_plus_plus::UserMessageInbox;
pub use codex_plus_plus::WeeklyUsageWindowAutoStart;
+pub use codex_plus_plus::codex_plus_plus_dcg_nux_shown;
pub use codex_protocol::config_types::ProfileV2Name;
pub use codex_protocol::config_types::ProfileV2NameParseError;
pub use codex_utils_absolute_path::AbsolutePathBuf;
diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json
index 635203669c9d..6274d1d1ef9b 100644
--- a/codex-rs/core/config.schema.json
+++ b/codex-rs/core/config.schema.json
@@ -4749,6 +4749,10 @@
"default": null,
"description": "Preferred backend for storing CLI auth credentials. file (default): Use a file in the Codex home directory. keyring: Use an OS-specific keyring service. auto: Use the keyring if available, otherwise use a file."
},
+ "codex_plus_plus_dcg_nux_shown": {
+ "description": "Whether the global Destructive Command Guard introduction was shown.",
+ "type": "boolean"
+ },
"compact_prompt": {
"description": "Compact prompt used for history compaction.",
"type": "string"
diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs
index 31055cb84efd..4c25bae4a781 100644
--- a/codex-rs/tui/src/app.rs
+++ b/codex-rs/tui/src/app.rs
@@ -33,6 +33,7 @@ use crate::chatwidget::ChatWidget;
use crate::chatwidget::ExternalEditorState;
use crate::chatwidget::ReplayKind;
use crate::chatwidget::ThreadInputState;
+use crate::codex_plus_plus::destructive_command_guard::DcgManager;
use crate::cwd_prompt::CwdPromptAction;
use crate::diff_render::DiffSummary;
use crate::exec_command::split_command_string;
@@ -887,6 +888,12 @@ impl App {
&initial_images,
);
let thread_and_widget_started_at = Instant::now();
+ let _show_dcg_nux = DcgManager::management_supported(&app_server)
+ && !codex_config::codex_plus_plus_dcg_nux_shown(&config.config_layer_stack);
+ #[cfg(not(test))]
+ if _show_dcg_nux {
+ crate::codex_plus_plus::mark_dcg_nux_pending();
+ }
let pending_startup_thread_start = matches!(
&session_selection,
SessionSelection::StartFresh | SessionSelection::Exit
@@ -1344,6 +1351,9 @@ See the Codex keymap documentation for supported actions and examples."
// Allow widgets to process any pending timers before rendering.
self.chat_widget.pre_draw_tick();
let rendered_area = self.render_chat_widget_frame(tui)?;
+ if crate::codex_plus_plus::take_dcg_nux_render_pending() {
+ persist_dcg_nux(&self.config).await;
+ }
if self.chat_widget.ambient_pet_image_enabled() {
let terminal_size = tui.terminal.size()?;
let ambient_pet_area = Rect::new(
diff --git a/codex-rs/tui/src/app/event_dispatch.rs b/codex-rs/tui/src/app/event_dispatch.rs
index d8479239057f..e4ec87bbf7cb 100644
--- a/codex-rs/tui/src/app/event_dispatch.rs
+++ b/codex-rs/tui/src/app/event_dispatch.rs
@@ -5,6 +5,10 @@
use super::resize_reflow::trailing_run_start;
use super::*;
+use crate::codex_plus_plus::apply_dcg_action;
+use crate::codex_plus_plus::destructive_command_guard::DcgManager;
+use crate::codex_plus_plus::destructive_command_guard::DcgStatus;
+use crate::codex_plus_plus::destructive_command_guard::RepairReason;
use crate::config_update::format_config_error;
use crate::external_agent_config_migration_flow::ExternalAgentConfigMigrationFlowOutcome;
#[cfg(target_os = "windows")]
@@ -1831,6 +1835,55 @@ impl App {
)
.await;
}
+ AppEvent::OpenCodexPlusPlusSettings => {
+ let request_id = DcgManager::begin_status_detection();
+ let manager = DcgManager::new(app_server, &self.config);
+ let app_event_tx = self.app_event_tx.clone();
+ tokio::spawn(async move {
+ let status = match manager {
+ Ok(manager) => manager.detect_status().await,
+ Err(err) => {
+ tracing::error!(error = %err, "failed to initialize DCG manager");
+ DcgStatus::NeedsRepair(RepairReason::StatusUnavailable)
+ }
+ };
+ app_event_tx.send(AppEvent::DcgStatusDetected(request_id, status));
+ });
+ }
+ AppEvent::DcgStatusDetected(request_id, status) => {
+ if DcgManager::is_current_status_detection(request_id) {
+ self.chat_widget.finish_dcg_status_detection(status);
+ }
+ }
+ AppEvent::OpenDcgInstallConfirmation => {
+ self.chat_widget.open_dcg_install_confirmation();
+ }
+ AppEvent::ManageDcg(action) => {
+ if DcgManager::try_begin_operation() {
+ let manager = DcgManager::new(app_server, &self.config);
+ self.chat_widget.show_dcg_progress(action);
+ let app_event_tx = self.app_event_tx.clone();
+ tokio::spawn(async move {
+ let change = match manager {
+ Ok(manager) => apply_dcg_action(manager, action)
+ .await
+ .inspect_err(|err| {
+ tracing::error!(error = %err, ?action, "DCG operation failed");
+ })
+ .ok(),
+ Err(err) => {
+ tracing::error!(error = %err, "failed to initialize DCG manager");
+ None
+ }
+ };
+ app_event_tx.send(AppEvent::DcgOperationCompleted { action, change });
+ });
+ }
+ }
+ AppEvent::DcgOperationCompleted { action, change } => {
+ DcgManager::finish_operation();
+ self.chat_widget.finish_dcg_action(action, change);
+ }
AppEvent::OpenCodexPlusPlusAccounts => {
let statuses = self
.weekly_window_scheduler
diff --git a/codex-rs/tui/src/app/startup_prompts.rs b/codex-rs/tui/src/app/startup_prompts.rs
index 55a377ec1104..f906a14ef2fa 100644
--- a/codex-rs/tui/src/app/startup_prompts.rs
+++ b/codex-rs/tui/src/app/startup_prompts.rs
@@ -268,6 +268,19 @@ pub(super) async fn prepare_startup_tooltip_override(
Some(tooltip_override.message)
}
+pub(super) async fn persist_dcg_nux(config: &Config) {
+ if let Err(err) = ConfigEditsBuilder::for_config(config)
+ .with_edits([crate::legacy_core::config::edit::ConfigEdit::SetPath {
+ segments: vec!["codex_plus_plus_dcg_nux_shown".to_string()],
+ value: true.into(),
+ }])
+ .apply()
+ .await
+ {
+ tracing::error!(error = %err, "failed to persist DCG introduction state");
+ }
+}
+
pub(super) async fn handle_model_migration_prompt_if_needed(
tui: &mut tui::Tui,
config: &mut Config,
@@ -400,6 +413,28 @@ mod tests {
Ok(())
}
+ #[tokio::test]
+ async fn dcg_nux_persists_globally_and_prepares_once() {
+ let codex_home = tempdir().expect("temp codex home");
+ let config = ConfigBuilder::default()
+ .codex_home(codex_home.path().to_path_buf())
+ .build()
+ .await
+ .expect("config");
+ assert!(!codex_config::codex_plus_plus_dcg_nux_shown(
+ &config.config_layer_stack
+ ));
+ persist_dcg_nux(&config).await;
+ let reloaded = ConfigBuilder::default()
+ .codex_home(codex_home.path().to_path_buf())
+ .build()
+ .await
+ .expect("reloaded config");
+ assert!(codex_config::codex_plus_plus_dcg_nux_shown(
+ &reloaded.config_layer_stack
+ ));
+ }
+
fn skill_error(path: &str, message: &str) -> SkillErrorInfo {
SkillErrorInfo {
path: PathBuf::from(path),
diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs
index 5c4f1161e8c4..473c04fe040f 100644
--- a/codex-rs/tui/src/app_event.rs
+++ b/codex-rs/tui/src/app_event.rs
@@ -868,6 +868,18 @@ pub(crate) enum AppEvent {
user_message_inbox: UserMessageInbox,
},
+ OpenCodexPlusPlusSettings,
+ DcgStatusDetected(
+ u64,
+ crate::codex_plus_plus::destructive_command_guard::DcgStatus,
+ ),
+ OpenDcgInstallConfirmation,
+ ManageDcg(crate::codex_plus_plus::DcgAction),
+ DcgOperationCompleted {
+ action: crate::codex_plus_plus::DcgAction,
+ change: Option,
+ },
+
OpenCodexPlusPlusAccounts,
/// Update memory settings and persist them to config.toml.
UpdateMemorySettings {
diff --git a/codex-rs/tui/src/chatwidget/slash_dispatch.rs b/codex-rs/tui/src/chatwidget/slash_dispatch.rs
index eeb8bece5018..c9d5b430fa14 100644
--- a/codex-rs/tui/src/chatwidget/slash_dispatch.rs
+++ b/codex-rs/tui/src/chatwidget/slash_dispatch.rs
@@ -455,7 +455,8 @@ impl ChatWidget {
self.defer_input_until_settings_applied();
}
SlashCommand::CodexPlusPlus => {
- self.open_codex_plus_plus_popup();
+ self.show_dcg_status_progress();
+ self.app_event_tx.send(AppEvent::OpenCodexPlusPlusSettings);
self.defer_input_until_settings_applied();
}
SlashCommand::Inbox => {
diff --git a/codex-rs/tui/src/chatwidget/slash_dispatch/codex_plus_plus/codex_plus_plus_tests.rs b/codex-rs/tui/src/chatwidget/slash_dispatch/codex_plus_plus/codex_plus_plus_tests.rs
index 64e49e61f370..35d4359a9696 100644
--- a/codex-rs/tui/src/chatwidget/slash_dispatch/codex_plus_plus/codex_plus_plus_tests.rs
+++ b/codex-rs/tui/src/chatwidget/slash_dispatch/codex_plus_plus/codex_plus_plus_tests.rs
@@ -8,6 +8,8 @@ use super::*;
use crate::app_event_sender::AppEventSender;
use crate::bottom_pane::BottomPaneView;
use crate::bottom_pane::ListSelectionView;
+use crate::codex_plus_plus::destructive_command_guard::DcgUnsupportedReason;
+use crate::codex_plus_plus::destructive_command_guard::RepairReason;
use crate::keymap::RuntimeKeymap;
use crate::test_backend::VT100Backend;
@@ -30,6 +32,7 @@ fn settings_view(
capacity,
/*current_user_message_inbox*/ false,
weekly_supported,
+ None,
&keymap,
),
AppEventSender::new(tx),
@@ -175,7 +178,69 @@ fn settings_hint_uses_list_keymap() {
.map(|span| span.content.as_ref())
.collect::();
- assert_eq!(text, "Press space to toggle; enter to save; q to cancel");
+ assert_eq!(
+ text,
+ "Press space to toggle; enter to save or manage; q to cancel"
+ );
+}
+
+#[test]
+fn dcg_views_snapshot() {
+ let i = dcg::settings_item(
+ DcgStatus::NotInstalled,
+ Default::default(),
+ /*save_weekly*/ false,
+ );
+ let (tx, mut rx) = unbounded_channel();
+ (i.actions[0])(&AppEventSender::new(tx));
+ assert_matches!(
+ rx.try_recv(),
+ Ok(AppEvent::PersistCodexPlusPlusSettings { .. })
+ );
+ assert_matches!(rx.try_recv(), Ok(AppEvent::OpenDcgInstallConfirmation));
+ let states = [
+ DcgStatus::NotInstalled,
+ DcgStatus::Enabled("0.6.8-codexpp.1".to_string()),
+ DcgStatus::Disabled("0.6.8-codexpp.1".to_string()),
+ DcgStatus::UpdateAvailable {
+ installed_version: Some("0.6.7".to_string()),
+ target_version: "0.6.8".to_string(),
+ },
+ DcgStatus::ExternalInstallation("0.6.8".to_string()),
+ DcgStatus::NeedsRepair(RepairReason::HookUntrusted),
+ DcgStatus::NeedsRepair(RepairReason::StatusUnavailable),
+ DcgStatus::Unsupported(DcgUnsupportedReason::Platform),
+ ];
+ let flow = [
+ dcg::confirmation_params(),
+ dcg::progress_params(crate::codex_plus_plus::DcgAction::InstallAndEnable),
+ dcg::failure_params(),
+ ]
+ .map(summarize_params)
+ .join("\n");
+ insta::assert_snapshot!(
+ "codex_plus_plus_dcg_settings_states",
+ format!("{}\n{flow}", states.map(render_dcg_item).join(" || "))
+ );
+}
+
+fn render_dcg_item(status: DcgStatus) -> String {
+ let item = dcg::settings_item(
+ status,
+ SettingsSelection::default(),
+ /*save_weekly*/ false,
+ );
+ format!("{} | {} action(s)", item.name, item.actions.len())
+}
+
+fn summarize_params(params: SelectionViewParams) -> String {
+ let items = params
+ .items
+ .into_iter()
+ .map(|item| format!("{}|{}", item.name, item.description.unwrap_or_default()))
+ .collect::>()
+ .join("\n");
+ format!("{}\n{items}", params.title.unwrap_or_default())
}
#[test]
diff --git a/codex-rs/tui/src/chatwidget/slash_dispatch/codex_plus_plus/dcg.rs b/codex-rs/tui/src/chatwidget/slash_dispatch/codex_plus_plus/dcg.rs
new file mode 100644
index 000000000000..2f652027e988
--- /dev/null
+++ b/codex-rs/tui/src/chatwidget/slash_dispatch/codex_plus_plus/dcg.rs
@@ -0,0 +1,202 @@
+use super::*;
+use crate::app_event_sender::AppEventSender;
+use crate::bottom_pane::SelectionAction;
+use crate::codex_plus_plus::DcgAction;
+use crate::codex_plus_plus::destructive_command_guard::DcgChange;
+use crate::codex_plus_plus::destructive_command_guard::DcgStatus;
+use crate::codex_plus_plus::destructive_command_guard::DcgUnsupportedReason;
+use crate::codex_plus_plus::destructive_command_guard::RepairReason;
+
+const DESCRIPTION: &str = "Review destructive shell commands with Guardian.";
+const PROGRESS_VIEW_ID: &str = "dcg-operation-progress";
+const STATUS_VIEW_ID: &str = "dcg-status-progress";
+
+pub(super) fn settings_item(
+ status: DcgStatus,
+ selection: SettingsSelection,
+ save_weekly: bool,
+) -> SelectionItem {
+ let (state, action) = match status {
+ DcgStatus::NotInstalled => (
+ "Not installed".to_string(),
+ Some(DcgAction::InstallAndEnable),
+ ),
+ DcgStatus::Enabled(version) => (format!("Enabled · {version}"), Some(DcgAction::Disable)),
+ DcgStatus::Disabled(version) => (format!("Disabled · {version}"), Some(DcgAction::Enable)),
+ DcgStatus::UpdateAvailable { target_version, .. } => (
+ format!("Update available · {target_version}"),
+ Some(DcgAction::Update),
+ ),
+ DcgStatus::ExternalInstallation(_) => ("External installation".to_string(), None),
+ DcgStatus::NeedsRepair(reason) => (
+ "Needs repair".to_string(),
+ (!matches!(
+ reason,
+ RepairReason::MarketplaceConfigMalformed
+ | RepairReason::MarketplacePinMismatch
+ | RepairReason::HookDisabled
+ | RepairReason::StatusUnavailable
+ ))
+ .then_some(DcgAction::Repair(reason)),
+ ),
+ DcgStatus::Unsupported(DcgUnsupportedReason::Platform) => {
+ ("Unsupported on this platform".to_string(), None)
+ }
+ DcgStatus::Unsupported(DcgUnsupportedReason::RemoteHookHost) => {
+ ("Manage on the shell host".to_string(), None)
+ }
+ };
+ let actions = action
+ .map(|action| {
+ vec![Box::new(move |tx: &AppEventSender| {
+ tx.send(AppEvent::PersistCodexPlusPlusSettings {
+ automatic_account_selection: selected(
+ &selection.automatic,
+ AutomaticOn,
+ AutomaticOff,
+ ),
+ weekly_usage_window_auto_start: save_weekly
+ .then(|| selected(&selection.weekly, WeeklyOn, WeeklyOff)),
+ model_capacity_retry_mode: selected(
+ &selection.capacity_indefinite,
+ CapacityIndefinite,
+ CapacityBounded,
+ ),
+ user_message_inbox: selected(&selection.user_message_inbox, InboxOn, InboxOff),
+ });
+ tx.send(match action {
+ DcgAction::InstallAndEnable => AppEvent::OpenDcgInstallConfirmation,
+ _ => AppEvent::ManageDcg(action),
+ });
+ }) as SelectionAction]
+ })
+ .unwrap_or_default();
+ SelectionItem {
+ name: format!("Destructive Command Guard · {state}"),
+ description: Some(DESCRIPTION.to_string()),
+ is_disabled: action.is_none(),
+ actions,
+ dismiss_on_select: true,
+ ..Default::default()
+ }
+}
+
+fn selected(value: &AtomicBool, on: T, off: T) -> T {
+ [off, on][usize::from(value.load(Ordering::Relaxed))]
+}
+
+pub(super) fn confirmation_params() -> SelectionViewParams {
+ SelectionViewParams {
+ title: Some("Install Destructive Command Guard?".to_string()),
+ subtitle: Some("Codex++ will enable protection for new sessions.".to_string()),
+ items: vec![
+ SelectionItem {
+ name: "Install and enable".to_string(),
+ description: Some(DESCRIPTION.to_string()),
+ actions: vec![Box::new(|tx| {
+ tx.send(AppEvent::ManageDcg(DcgAction::InstallAndEnable));
+ })],
+ dismiss_on_select: true,
+ ..Default::default()
+ },
+ SelectionItem {
+ name: "Cancel".to_string(),
+ dismiss_on_select: true,
+ ..Default::default()
+ },
+ ],
+ ..Default::default()
+ }
+}
+
+pub(super) fn progress_params(action: DcgAction) -> SelectionViewParams {
+ let mut params = message_params(format!("{}…", action_verb(action)), "Working…");
+ params.view_id = Some(PROGRESS_VIEW_ID);
+ params.allow_cancel = false;
+ params
+}
+
+pub(super) fn failure_params() -> SelectionViewParams {
+ message_params(
+ "Operation failed".to_string(),
+ "The operation did not finish. Try again.",
+ )
+}
+
+fn message_params(name: String, description: &str) -> SelectionViewParams {
+ SelectionViewParams {
+ title: Some("Destructive Command Guard".to_string()),
+ items: vec![SelectionItem {
+ name,
+ description: Some(description.to_string()),
+ is_disabled: true,
+ ..Default::default()
+ }],
+ ..Default::default()
+ }
+}
+
+fn action_verb(action: DcgAction) -> &'static str {
+ match action {
+ DcgAction::InstallAndEnable => "Installing and enabling",
+ DcgAction::Enable => "Enabling",
+ DcgAction::Disable => "Disabling",
+ DcgAction::Update => "Updating",
+ DcgAction::Repair(_) => "Repairing",
+ }
+}
+
+fn action_succeeded(action: DcgAction, status: &DcgStatus) -> bool {
+ match action {
+ DcgAction::Disable => matches!(status, DcgStatus::Disabled(_)),
+ _ => matches!(status, DcgStatus::Enabled(_)),
+ }
+}
+
+impl ChatWidget {
+ pub(crate) fn show_dcg_status_progress(&mut self) {
+ let mut params = message_params("Checking command guard…".to_string(), "Working…");
+ params.view_id = Some(STATUS_VIEW_ID);
+ self.show_dcg_view(params);
+ }
+
+ pub(crate) fn finish_dcg_status_detection(&mut self, status: DcgStatus) {
+ if self.bottom_pane.dismiss_active_view_if_id(STATUS_VIEW_ID) {
+ self.open_codex_plus_plus_popup(status);
+ }
+ }
+
+ pub(crate) fn open_dcg_install_confirmation(&mut self) {
+ self.show_dcg_view(confirmation_params());
+ }
+
+ pub(crate) fn show_dcg_progress(&mut self, action: DcgAction) {
+ self.show_dcg_view(progress_params(action));
+ }
+
+ pub(crate) fn finish_dcg_action(&mut self, action: DcgAction, change: Option) {
+ if !self.bottom_pane.dismiss_active_view_if_id(PROGRESS_VIEW_ID) {
+ self.bottom_pane.dismiss_view_by_id(PROGRESS_VIEW_ID);
+ return;
+ }
+ match change {
+ Some(change) if action_succeeded(action, &change.status) => {
+ debug_assert!(!change.takes_effect_in_current_session);
+ self.add_info_message(
+ "Destructive Command Guard settings updated. Changes apply to new sessions."
+ .to_string(),
+ /*hint*/ None,
+ );
+ self.open_codex_plus_plus_popup(change.status);
+ }
+ _ => self.show_dcg_view(failure_params()),
+ }
+ }
+
+ fn show_dcg_view(&mut self, params: SelectionViewParams) {
+ let keymap = self.bottom_pane.list_keymap();
+ let view = ListSelectionView::new(params, self.app_event_tx.clone(), keymap);
+ self.bottom_pane.show_view(Box::new(view));
+ self.request_redraw();
+ }
+}
diff --git a/codex-rs/tui/src/chatwidget/slash_dispatch/codex_plus_plus/mod.rs b/codex-rs/tui/src/chatwidget/slash_dispatch/codex_plus_plus/mod.rs
index dd6f3395cd28..65607bb0ead4 100644
--- a/codex-rs/tui/src/chatwidget/slash_dispatch/codex_plus_plus/mod.rs
+++ b/codex-rs/tui/src/chatwidget/slash_dispatch/codex_plus_plus/mod.rs
@@ -1,6 +1,7 @@
//! Codex++ settings exposed through fork slash commands.
mod accounts;
+mod dcg;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
@@ -25,6 +26,7 @@ use ratatui::text::Line;
use super::super::*;
use crate::bottom_pane::ListSelectionView;
use crate::bottom_pane::SelectionToggle;
+use crate::codex_plus_plus::destructive_command_guard::DcgStatus;
use crate::key_hint;
use crate::keymap::ListKeymap;
use crate::keymap::primary_binding;
@@ -37,7 +39,7 @@ impl ChatWidget {
self.request_redraw();
}
- pub(super) fn open_codex_plus_plus_popup(&mut self) {
+ pub(crate) fn open_codex_plus_plus_popup(&mut self, dcg_status: DcgStatus) {
let list_keymap = settings_list_keymap(self.bottom_pane.list_keymap());
let params = codex_plus_plus_settings_params(
self.config.automatic_account_selection,
@@ -45,6 +47,7 @@ impl ChatWidget {
self.config.model_capacity_retry_mode,
crate::codex_plus_plus::user_message_inbox_enabled(&self.config.config_layer_stack),
self.weekly_start_supported,
+ Some(dcg_status),
&list_keymap,
);
let view = ListSelectionView::new(params, self.app_event_tx.clone(), list_keymap);
@@ -85,7 +88,7 @@ impl ChatWidget {
}
}
-#[derive(Clone)]
+#[derive(Clone, Default)]
struct SettingsSelection {
automatic: Arc,
weekly: Arc,
@@ -109,6 +112,7 @@ fn codex_plus_plus_settings_params(
current_capacity: ModelCapacityRetryMode,
current_user_message_inbox: bool,
weekly_supported: bool,
+ dcg_status: Option,
list_keymap: &ListKeymap,
) -> SelectionViewParams {
let selection = SettingsSelection {
@@ -124,6 +128,12 @@ fn codex_plus_plus_settings_params(
selection.clone(),
weekly_supported,
)];
+ if let Some(status) = dcg_status {
+ items.insert(
+ 0,
+ dcg::settings_item(status, selection.clone(), weekly_supported),
+ );
+ }
if weekly_supported {
items.push(settings_item(
"Start unused weekly windows",
@@ -211,7 +221,7 @@ fn settings_hint_line(list_keymap: &ListKeymap) -> Line<'static> {
" to toggle".into(),
];
if let Some(accept) = primary_binding(&list_keymap.accept) {
- spans.extend(["; ".into(), accept.into(), " to save".into()]);
+ spans.extend(["; ".into(), accept.into(), " to save or manage".into()]);
}
if let Some(cancel) = primary_binding(&list_keymap.cancel) {
spans.extend(["; ".into(), cancel.into(), " to cancel".into()]);
diff --git a/codex-rs/tui/src/chatwidget/slash_dispatch/codex_plus_plus/snapshots/codex_tui__chatwidget__slash_dispatch__codex_plus_plus__tests__codex_plus_plus_dcg_settings_states.snap b/codex-rs/tui/src/chatwidget/slash_dispatch/codex_plus_plus/snapshots/codex_tui__chatwidget__slash_dispatch__codex_plus_plus__tests__codex_plus_plus_dcg_settings_states.snap
new file mode 100644
index 000000000000..7cd49f5f0498
--- /dev/null
+++ b/codex-rs/tui/src/chatwidget/slash_dispatch/codex_plus_plus/snapshots/codex_tui__chatwidget__slash_dispatch__codex_plus_plus__tests__codex_plus_plus_dcg_settings_states.snap
@@ -0,0 +1,12 @@
+---
+source: tui/src/chatwidget/slash_dispatch/codex_plus_plus/codex_plus_plus_tests.rs
+expression: "format!(\"{}\\n{flow}\", states.map(render_dcg_item).join(\" || \"))"
+---
+Destructive Command Guard · Not installed | 1 action(s) || Destructive Command Guard · Enabled · 0.6.8-codexpp.1 | 1 action(s) || Destructive Command Guard · Disabled · 0.6.8-codexpp.1 | 1 action(s) || Destructive Command Guard · Update available · 0.6.8 | 1 action(s) || Destructive Command Guard · External installation | 0 action(s) || Destructive Command Guard · Needs repair | 1 action(s) || Destructive Command Guard · Needs repair | 0 action(s) || Destructive Command Guard · Unsupported on this platform | 0 action(s)
+Install Destructive Command Guard?
+Install and enable|Review destructive shell commands with Guardian.
+Cancel|
+Destructive Command Guard
+Installing and enabling…|Working…
+Destructive Command Guard
+Operation failed|The operation did not finish. Try again.
diff --git a/codex-rs/tui/src/chatwidget/tests/slash_commands.rs b/codex-rs/tui/src/chatwidget/tests/slash_commands.rs
index 2cf958f14917..4b0c102c42f0 100644
--- a/codex-rs/tui/src/chatwidget/tests/slash_commands.rs
+++ b/codex-rs/tui/src/chatwidget/tests/slash_commands.rs
@@ -407,6 +407,12 @@ async fn queued_slash_menu_cancel_drains_next_input() {
KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL),
)
.await;
+ assert_cancelled_queued_menu_drains_next_input(
+ "/codexplusplus",
+ "Checking command guard",
+ KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE),
+ )
+ .await;
}
#[tokio::test]
diff --git a/codex-rs/tui/src/codex_plus_plus/destructive_command_guard/mod.rs b/codex-rs/tui/src/codex_plus_plus/destructive_command_guard/mod.rs
index 7a687fcaa327..0a15c0bd9561 100644
--- a/codex-rs/tui/src/codex_plus_plus/destructive_command_guard/mod.rs
+++ b/codex-rs/tui/src/codex_plus_plus/destructive_command_guard/mod.rs
@@ -26,8 +26,14 @@ use codex_core_plugins::store::PluginStore;
use codex_plugin::PluginId;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::io::ErrorKind;
+#[cfg(unix)]
+use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use std::path::PathBuf;
+use std::sync::atomic::AtomicBool;
+use std::sync::atomic::AtomicU64;
+use std::sync::atomic::Ordering;
+use std::time::Duration;
use tokio::process::Command;
use uuid::Uuid;
@@ -38,6 +44,9 @@ const PINNED_SOURCE: &str = "https://github.com/Pimpmuckl/destructive_command_gu
const PINNED_TAG: &str = "v0.6.8-codexpp.1";
const PINNED_VERSION: &str = "0.6.8-codexpp.1";
const PINNED_COMMIT: &str = "fd4e90a41fa493aa9a61c715bd5114b7fcf4348e";
+const VERSION_PROBE_TIMEOUT: Duration = Duration::from_secs(2);
+static OPERATION_IN_FLIGHT: AtomicBool = AtomicBool::new(false);
+static STATUS_DETECTION_ID: AtomicU64 = AtomicU64::new(0);
#[cfg(target_os = "windows")]
const BINARY_NAME: &str = "dcg.exe";
#[cfg(not(target_os = "windows"))]
@@ -120,8 +129,11 @@ impl DcgManager {
return DcgStatus::NeedsRepair(RepairReason::PluginMissing);
}
Ok(None) => {
+ let Some(binary) = external_binary() else {
+ return DcgStatus::NotInstalled;
+ };
return self
- .reported_version(Path::new("dcg"))
+ .reported_version(&binary)
.await
.map_or(DcgStatus::NotInstalled, DcgStatus::ExternalInstallation);
}
@@ -178,6 +190,26 @@ impl DcgManager {
self.install_managed().await
}
+ pub(crate) fn try_begin_operation() -> bool {
+ !OPERATION_IN_FLIGHT.swap(true, Ordering::AcqRel)
+ }
+
+ pub(crate) fn finish_operation() {
+ OPERATION_IN_FLIGHT.store(false, Ordering::Release);
+ }
+
+ pub(crate) fn management_supported(app_server: &AppServerSession) -> bool {
+ PLATFORM_SUPPORTED && !app_server.uses_remote_workspace()
+ }
+
+ pub(crate) fn begin_status_detection() -> u64 {
+ STATUS_DETECTION_ID.fetch_add(1, Ordering::AcqRel) + 1
+ }
+
+ pub(crate) fn is_current_status_detection(id: u64) -> bool {
+ STATUS_DETECTION_ID.load(Ordering::Acquire) == id
+ }
+
pub(crate) async fn enable(&self) -> Result {
self.set_enabled(/*enabled*/ true).await
}
@@ -233,8 +265,10 @@ impl DcgManager {
.await?;
let plugin_was_enabled = self
.read_plugin(marketplace.installed_root.as_path())
- .await
- .is_ok_and(|response| response.plugin.summary.enabled);
+ .await?
+ .plugin
+ .summary
+ .enabled;
let binary = self.binary_path();
let data_root = binary.parent().context("DCG binary path has no parent")?;
tokio::fs::create_dir_all(data_root).await?;
@@ -490,7 +524,12 @@ impl DcgManager {
}
async fn reported_version(&self, binary: &Path) -> Option {
- let output = Command::new(binary).arg("--version").output().await.ok()?;
+ let mut command = Command::new(binary);
+ command.arg("--version").kill_on_drop(true);
+ let output = tokio::time::timeout(VERSION_PROBE_TIMEOUT, command.output())
+ .await
+ .ok()?
+ .ok()?;
if !output.status.success() {
return None;
}
@@ -525,6 +564,19 @@ impl DcgManager {
}
}
+fn external_binary() -> Option {
+ let path = std::env::var_os("PATH")?;
+ let mut candidates = std::env::split_paths(&path).map(|directory| directory.join(BINARY_NAME));
+ #[cfg(unix)]
+ return candidates.find(|candidate| {
+ candidate
+ .metadata()
+ .is_ok_and(|metadata| metadata.is_file() && metadata.permissions().mode() & 0o111 != 0)
+ });
+ #[cfg(not(unix))]
+ candidates.find(|candidate| candidate.is_file())
+}
+
fn request_id(prefix: &str) -> RequestId {
RequestId::String(format!("{prefix}-{}", Uuid::new_v4()))
}
diff --git a/codex-rs/tui/src/codex_plus_plus/mod.rs b/codex-rs/tui/src/codex_plus_plus/mod.rs
index 855fd0f8ac50..cf7a84c2e003 100644
--- a/codex-rs/tui/src/codex_plus_plus/mod.rs
+++ b/codex-rs/tui/src/codex_plus_plus/mod.rs
@@ -1,7 +1,6 @@
//! Codex++ TUI capabilities kept separate from upstream-owned orchestration.
mod account_policy;
-#[allow(dead_code)]
pub(crate) mod destructive_command_guard;
#[cfg(any(not(debug_assertions), test))]
mod lag_warning;
@@ -13,6 +12,10 @@ mod weekly_window_scheduler;
mod welcome;
pub(crate) use account_policy::persist_settings;
+use anyhow::Result;
+use destructive_command_guard::DcgChange;
+use destructive_command_guard::DcgManager;
+use destructive_command_guard::RepairReason;
#[cfg(not(debug_assertions))]
pub(crate) use lag_warning::lag_warning;
pub(crate) use model_capacity_retry::status_details as model_capacity_retry_status_details;
@@ -32,5 +35,28 @@ pub(crate) use user_message_inbox::recognize as recognize_user_message;
pub(crate) use weekly_window_scheduler::WeeklyWindowScheduler;
pub(crate) use weekly_window_scheduler::WeeklyWindowStatus;
pub(crate) use welcome::WELCOME_TIP;
+#[cfg(not(test))]
+pub(crate) use welcome::mark_dcg_nux_pending;
pub(crate) use welcome::replace_upstream_app_promo;
+pub(crate) use welcome::take_dcg_nux_help_lines;
+pub(crate) use welcome::take_dcg_nux_render_pending;
pub(crate) use welcome::welcome_help_lines;
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub(crate) enum DcgAction {
+ InstallAndEnable,
+ Enable,
+ Disable,
+ Update,
+ Repair(RepairReason),
+}
+
+pub(crate) async fn apply_dcg_action(manager: DcgManager, action: DcgAction) -> Result {
+ match action {
+ DcgAction::InstallAndEnable => manager.install_and_enable().await,
+ DcgAction::Enable => manager.enable().await,
+ DcgAction::Disable => manager.disable().await,
+ DcgAction::Update => manager.update().await,
+ DcgAction::Repair(reason) => manager.repair(reason).await,
+ }
+}
diff --git a/codex-rs/tui/src/codex_plus_plus/snapshots/codex_tui__codex_plus_plus__welcome__tests__codex_plus_plus_welcome_help.snap b/codex-rs/tui/src/codex_plus_plus/snapshots/codex_tui__codex_plus_plus__welcome__tests__codex_plus_plus_welcome_help.snap
index 85a82fa8b09b..3db67421d81c 100644
--- a/codex-rs/tui/src/codex_plus_plus/snapshots/codex_tui__codex_plus_plus__welcome__tests__codex_plus_plus_welcome_help.snap
+++ b/codex-rs/tui/src/codex_plus_plus/snapshots/codex_tui__codex_plus_plus__welcome__tests__codex_plus_plus_welcome_help.snap
@@ -1,6 +1,10 @@
---
source: tui/src/codex_plus_plus/welcome_tests.rs
-expression: terminal.backend().to_string()
+expression: "[render_welcome(true, 5), render_welcome(false, 3),].join(\"\\n\")"
---
Welcome to Codex++. Use /codexplusplus for settings and /accounts to enable
or disable accounts.
+ Tip: Enable Destructive Command Guard in /codexplusplus to send risky
+ commands to Guardian for review even in full --yolo mode.
+ Welcome to Codex++. Use /codexplusplus for settings and /accounts to enable
+ or disable accounts.
diff --git a/codex-rs/tui/src/codex_plus_plus/welcome.rs b/codex-rs/tui/src/codex_plus_plus/welcome.rs
index 91decb70ed79..21824a55ecdf 100644
--- a/codex-rs/tui/src/codex_plus_plus/welcome.rs
+++ b/codex-rs/tui/src/codex_plus_plus/welcome.rs
@@ -2,8 +2,19 @@
use ratatui::style::Stylize;
use ratatui::text::Line;
+use std::sync::atomic::AtomicBool;
+use std::sync::atomic::Ordering;
pub(crate) const WELCOME_TIP: &str = "Welcome to **Codex++**. Use **/codexplusplus** for settings and **/accounts** to enable or disable accounts.";
+pub(crate) const DCG_NUX_TIP: &str = "Tip: Enable Destructive Command Guard in /codexplusplus to send risky commands to Guardian for review even in full --yolo mode.";
+
+static DCG_NUX_PENDING: AtomicBool = AtomicBool::new(false);
+static DCG_NUX_RENDER_PENDING: AtomicBool = AtomicBool::new(false);
+
+#[cfg(not(test))]
+pub(crate) fn mark_dcg_nux_pending() {
+ DCG_NUX_PENDING.store(true, Ordering::Relaxed);
+}
pub(crate) fn replace_upstream_app_promo(tip: &'static str) -> &'static str {
if tip.contains("codex app") {
@@ -14,9 +25,32 @@ pub(crate) fn replace_upstream_app_promo(tip: &'static str) -> &'static str {
}
pub(crate) fn welcome_help_lines() -> Vec> {
- let tip = WELCOME_TIP.replace("**", "");
+ wrapped_tip(&WELCOME_TIP.replace("**", ""))
+}
+
+pub(crate) fn take_dcg_nux_help_lines() -> Option>> {
+ DCG_NUX_PENDING
+ .swap(false, Ordering::Relaxed)
+ .then(|| wrapped_tip(DCG_NUX_TIP))
+ .inspect(|_| DCG_NUX_RENDER_PENDING.store(true, Ordering::Relaxed))
+}
+
+pub(crate) fn take_dcg_nux_render_pending() -> bool {
+ DCG_NUX_RENDER_PENDING.swap(false, Ordering::Relaxed)
+}
+
+#[cfg(test)]
+fn welcome_help_lines_for(show_dcg_nux: bool) -> Vec> {
+ let mut lines = welcome_help_lines();
+ if show_dcg_nux {
+ lines.extend(wrapped_tip(DCG_NUX_TIP));
+ }
+ lines
+}
+
+fn wrapped_tip(tip: &str) -> Vec> {
textwrap::wrap(
- &tip,
+ tip,
textwrap::Options::new(/*width*/ 78)
.initial_indent(" ")
.subsequent_indent(" "),
diff --git a/codex-rs/tui/src/codex_plus_plus/welcome_tests.rs b/codex-rs/tui/src/codex_plus_plus/welcome_tests.rs
index 31a29ad38cde..17b4272dab7a 100644
--- a/codex-rs/tui/src/codex_plus_plus/welcome_tests.rs
+++ b/codex-rs/tui/src/codex_plus_plus/welcome_tests.rs
@@ -14,16 +14,27 @@ fn replaces_only_upstream_app_promo() {
assert_eq!(unrelated_tip, replace_upstream_app_promo(unrelated_tip));
}
-#[test]
-fn welcome_help_snapshot() {
- let mut terminal =
- Terminal::new(VT100Backend::new(/*width*/ 80, /*height*/ 3)).expect("terminal");
+fn render_welcome(show_dcg_nux: bool, height: u16) -> String {
+ let mut terminal = Terminal::new(VT100Backend::new(/*width*/ 80, height)).expect("terminal");
terminal
- .draw(|frame| frame.render_widget(Text::from(welcome_help_lines()), frame.area()))
+ .draw(|frame| {
+ frame.render_widget(
+ Text::from(welcome_help_lines_for(show_dcg_nux)),
+ frame.area(),
+ )
+ })
.expect("render welcome help");
+ terminal.backend().to_string()
+}
+#[test]
+fn welcome_help_first_and_later_startup_snapshot() {
insta::assert_snapshot!(
"codex_plus_plus_welcome_help",
- terminal.backend().to_string()
+ [
+ render_welcome(/*show_dcg_nux*/ true, /*height*/ 5),
+ render_welcome(/*show_dcg_nux*/ false, /*height*/ 3),
+ ]
+ .join("\n")
);
}
diff --git a/codex-rs/tui/src/history_cell/session.rs b/codex-rs/tui/src/history_cell/session.rs
index 3d47f228fb3f..9704768259a0 100644
--- a/codex-rs/tui/src/history_cell/session.rs
+++ b/codex-rs/tui/src/history_cell/session.rs
@@ -213,6 +213,14 @@ pub(crate) fn new_session_info(
parts.push(Box::new(PlainHistoryCell { lines }));
}
}
+ if let Some(mut dcg_lines) = crate::codex_plus_plus::take_dcg_nux_help_lines() {
+ if !is_first_event {
+ let mut welcome_lines = crate::codex_plus_plus::welcome_help_lines();
+ welcome_lines.append(&mut dcg_lines);
+ dcg_lines = welcome_lines;
+ }
+ parts.push(Box::new(PlainHistoryCell { lines: dcg_lines }));
+ }
SessionInfoCell(CompositeHistoryCell { parts })
}