Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CODEX_PLUS_PLUS_LEDGER.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`<br>`codex-rs/core/src/codex_plus_plus/user_message_inbox.rs`<br>`codex-rs/protocol/src/codex_plus_plus/user_message.rs`<br>`codex-rs/tui/src/codex_plus_plus/user_message_inbox.rs`<br>`codex-rs/tui/src/codex_plus_plus/user_messages.rs` | `codex-rs/config/src/config_toml.rs`<br>`codex-rs/core/config.schema.json`<br>`codex-rs/core/src/tools/spec_plan.rs`<br>`codex-rs/app-server-protocol/src/protocol/thread_history.rs`<br>`codex-rs/tui/src/app.rs`<br>`codex-rs/tui/src/app/agent_status_feed.rs`<br>`codex-rs/tui/src/app/app_server_events.rs`<br>`codex-rs/tui/src/app/session_lifecycle.rs`<br>`codex-rs/tui/src/app/side.rs`<br>`codex-rs/tui/src/app/thread_routing.rs`<br>`codex-rs/tui/src/chatwidget.rs`<br>`codex-rs/tui/src/chatwidget/constructor.rs`<br>`codex-rs/tui/src/chatwidget/streaming.rs`<br>`codex-rs/tui/src/chatwidget/slash_dispatch.rs`<br>`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`<br>`codex-rs/tui/src/codex_plus_plus/account_policy.rs` | `codex-rs/tui/src/chatwidget/slash_dispatch.rs`<br>`codex-rs/tui/src/app_event.rs`<br>`codex-rs/tui/src/app/event_dispatch.rs`<br>`codex-rs/tui/src/slash_command.rs`<br>`codex-rs/config/src/config_toml.rs`<br>`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`<br>`codex-rs/core/src/tools/handlers/mcp.rs`<br>`codex-rs/core/src/hook_runtime.rs`<br>`codex-rs/hooks/src/events/pre_tool_use.rs`<br>`codex-rs/protocol/src/approvals.rs`<br>`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`<br>`codex-rs/tui/src/chatwidget/slash_dispatch/codex_plus_plus/dcg.rs`<br>`codex-rs/config/src/codex_plus_plus/dcg_nux.rs`<br>`codex-rs/tui/src/codex_plus_plus/welcome.rs` | `codex-rs/config/src/config_toml.rs`<br>`codex-rs/core/config.schema.json`<br>`codex-rs/tui/src/app_event.rs`<br>`codex-rs/tui/src/app/event_dispatch.rs`<br>`codex-rs/tui/src/app/startup_prompts.rs`<br>`codex-rs/tui/src/history_cell/session.rs`<br>`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`<br>`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`<br>`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`<br>`codex-rs/tui/src/history_cell/session.rs` | [#43](https://github.com/Pimpmuckl/codex/pull/43) |
Expand Down
10 changes: 10 additions & 0 deletions codex-rs/config/src/codex_plus_plus/dcg_nux.rs
Original file line number Diff line number Diff line change
@@ -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)
}
2 changes: 2 additions & 0 deletions codex-rs/config/src/codex_plus_plus/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
2 changes: 2 additions & 0 deletions codex-rs/config/src/config_toml.rs
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,8 @@ pub struct ConfigToml {
pub model_capacity_retry_mode: Option<ModelCapacityRetryMode>,
/// Whether agents can leave durable, non-blocking messages for the user.
pub user_message_inbox: Option<UserMessageInbox>,
/// Whether the global Destructive Command Guard introduction was shown.
pub codex_plus_plus_dcg_nux_shown: Option<bool>,
/// 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`.
Expand Down
1 change: 1 addition & 0 deletions codex-rs/config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 4 additions & 0 deletions codex-rs/core/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
10 changes: 10 additions & 0 deletions codex-rs/tui/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
53 changes: 53 additions & 0 deletions codex-rs/tui/src/app/event_dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down Expand Up @@ -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
Expand Down
35 changes: 35 additions & 0 deletions codex-rs/tui/src/app/startup_prompts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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),
Expand Down
12 changes: 12 additions & 0 deletions codex-rs/tui/src/app_event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<crate::codex_plus_plus::destructive_command_guard::DcgChange>,
},

OpenCodexPlusPlusAccounts,
/// Update memory settings and persist them to config.toml.
UpdateMemorySettings {
Expand Down
3 changes: 2 additions & 1 deletion codex-rs/tui/src/chatwidget/slash_dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -30,6 +32,7 @@ fn settings_view(
capacity,
/*current_user_message_inbox*/ false,
weekly_supported,
None,
&keymap,
),
AppEventSender::new(tx),
Expand Down Expand Up @@ -175,7 +178,69 @@ fn settings_hint_uses_list_keymap() {
.map(|span| span.content.as_ref())
.collect::<String>();

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::<Vec<_>>()
.join("\n");
format!("{}\n{items}", params.title.unwrap_or_default())
}

#[test]
Expand Down
Loading
Loading