Compare commits

...

5 Commits

Author SHA1 Message Date
Michael Fan
06a0c6b131 test 2026-03-23 10:55:22 -04:00
jif-oai
d807d44ae7 nit: guard -> registry (#15317) 2026-03-23 10:02:11 +00:00
Charley Cunningham
5e3793def2 Use Shift+Left to edit queued messages in tmux (#15480)
## Summary
- use Shift+Left to edit the most recent queued message when running
under tmux
- mirror the same binding change in the app-server TUI
- add tmux-specific tests and snapshot coverage for the rendered
queued-message hint

## Testing
- just fmt
- cargo test -p codex-tui
- cargo test -p codex-tui-app-server
- just argument-comment-lint -p codex-tui -p codex-tui-app-server

Co-authored-by: Codex <noreply@openai.com>
2026-03-22 21:19:31 -07:00
Charley Cunningham
85065ea1b8 core: snapshot fork startup context injection (#15443)
## Summary
- add a snapshot-style core test for fork startup context injection
followed by first-turn diff injection
- capture the current duplicated startup-plus-turn context behavior
without changing runtime logic

## Testing
- not run locally; relying on CI
- just fmt

---------

Co-authored-by: Codex <noreply@openai.com>
2026-03-22 18:24:14 -07:00
Charley Cunningham
e830000e41 Remove smart_approvals alias migration (#15464)
Remove the legacy `smart_approvals` config migration from core config
loading.

This change:
- stops rewriting `smart_approvals` into `guardian_approval`
- stops backfilling `approvals_reviewer = "guardian_subagent"`
- replaces the migration tests with regression coverage that asserts the
deprecated key is ignored in root and profile scopes

Verification:
- `just fmt`
- `cargo test -p codex-core smart_approvals_alias_is_ignored`
- `cargo test -p codex-core approvals_reviewer_`
- `just argument-comment-lint`

Notes:
- `cargo test -p codex-core` still hits an unrelated existing failure in
`tools::js_repl::tests::js_repl_imported_local_files_can_access_repl_globals`;
the JS REPL kernel exits after `mktemp` fails under the current
environment.

Enhancement request: requested cleanup to delete the `smart_approvals`
alias migration; no public issue link is available.

Co-authored-by: Codex <noreply@openai.com>
2026-03-22 17:10:42 -07:00
17 changed files with 487 additions and 334 deletions

View File

@@ -1,6 +1,6 @@
use crate::agent::AgentStatus;
use crate::agent::guards::AgentMetadata;
use crate::agent::guards::Guards;
use crate::agent::registry::AgentMetadata;
use crate::agent::registry::AgentRegistry;
use crate::agent::role::DEFAULT_ROLE_NAME;
use crate::agent::role::resolve_role_config;
use crate::agent::status::is_final;
@@ -80,14 +80,14 @@ fn agent_nickname_candidates(
/// spawn new agents and the inter-agent communication layer.
/// An `AgentControl` instance is intended to be created at most once per root thread/session
/// tree. That same `AgentControl` is then shared with every sub-agent spawned from that root,
/// which keeps the guards scoped to that root thread rather than the entire `ThreadManager`.
/// which keeps the registry scoped to that root thread rather than the entire `ThreadManager`.
#[derive(Clone, Default)]
pub(crate) struct AgentControl {
/// Weak handle back to the global thread registry/state.
/// This is `Weak` to avoid reference cycles and shadow persistence of the form
/// `ThreadManagerState -> CodexThread -> Session -> SessionServices -> ThreadManagerState`.
manager: Weak<ThreadManagerState>,
state: Arc<Guards>,
state: Arc<AgentRegistry>,
}
impl AgentControl {
@@ -686,7 +686,7 @@ impl AgentControl {
#[allow(clippy::too_many_arguments)]
fn prepare_thread_spawn(
&self,
reservation: &mut crate::agent::guards::SpawnReservation,
reservation: &mut crate::agent::registry::SpawnReservation,
config: &crate::config::Config,
parent_thread_id: ThreadId,
depth: i32,

View File

@@ -1,11 +1,11 @@
pub(crate) mod agent_resolver;
pub(crate) mod control;
mod guards;
mod registry;
pub(crate) mod role;
pub(crate) mod status;
pub(crate) use codex_protocol::protocol::AgentStatus;
pub(crate) use control::AgentControl;
pub(crate) use guards::exceeds_thread_spawn_depth_limit;
pub(crate) use guards::next_thread_spawn_depth;
pub(crate) use registry::exceeds_thread_spawn_depth_limit;
pub(crate) use registry::next_thread_spawn_depth;
pub(crate) use status::agent_status_from_event;

View File

@@ -20,7 +20,7 @@ use std::sync::atomic::Ordering;
/// This structure is shared by all agents in the same user session (because the `AgentControl`
/// is).
#[derive(Default)]
pub(crate) struct Guards {
pub(crate) struct AgentRegistry {
active_agents: Mutex<ActiveAgents>,
total_count: AtomicUsize,
}
@@ -75,7 +75,7 @@ pub(crate) fn exceeds_thread_spawn_depth_limit(depth: i32, max_depth: i32) -> bo
depth > max_depth
}
impl Guards {
impl AgentRegistry {
pub(crate) fn reserve_spawn_slot(
self: &Arc<Self>,
max_threads: Option<usize>,
@@ -263,7 +263,7 @@ impl Guards {
}
pub(crate) struct SpawnReservation {
state: Arc<Guards>,
state: Arc<AgentRegistry>,
active: bool,
reserved_agent_nickname: Option<String>,
reserved_agent_path: Option<AgentPath>,
@@ -311,5 +311,5 @@ impl Drop for SpawnReservation {
}
#[cfg(test)]
#[path = "guards_tests.rs"]
#[path = "registry_tests.rs"]
mod tests;

View File

@@ -52,22 +52,22 @@ fn non_thread_spawn_subagents_default_to_depth_zero() {
#[test]
fn reservation_drop_releases_slot() {
let guards = Arc::new(Guards::default());
let reservation = guards.reserve_spawn_slot(Some(1)).expect("reserve slot");
let registry = Arc::new(AgentRegistry::default());
let reservation = registry.reserve_spawn_slot(Some(1)).expect("reserve slot");
drop(reservation);
let reservation = guards.reserve_spawn_slot(Some(1)).expect("slot released");
let reservation = registry.reserve_spawn_slot(Some(1)).expect("slot released");
drop(reservation);
}
#[test]
fn commit_holds_slot_until_release() {
let guards = Arc::new(Guards::default());
let reservation = guards.reserve_spawn_slot(Some(1)).expect("reserve slot");
let registry = Arc::new(AgentRegistry::default());
let reservation = registry.reserve_spawn_slot(Some(1)).expect("reserve slot");
let thread_id = ThreadId::new();
reservation.commit(agent_metadata(thread_id));
let err = match guards.reserve_spawn_slot(Some(1)) {
let err = match registry.reserve_spawn_slot(Some(1)) {
Ok(_) => panic!("limit should be enforced"),
Err(err) => err,
};
@@ -76,8 +76,8 @@ fn commit_holds_slot_until_release() {
};
assert_eq!(max_threads, 1);
guards.release_spawned_thread(thread_id);
let reservation = guards
registry.release_spawned_thread(thread_id);
let reservation = registry
.reserve_spawn_slot(Some(1))
.expect("slot released after thread removal");
drop(reservation);
@@ -85,14 +85,14 @@ fn commit_holds_slot_until_release() {
#[test]
fn release_ignores_unknown_thread_id() {
let guards = Arc::new(Guards::default());
let reservation = guards.reserve_spawn_slot(Some(1)).expect("reserve slot");
let registry = Arc::new(AgentRegistry::default());
let reservation = registry.reserve_spawn_slot(Some(1)).expect("reserve slot");
let thread_id = ThreadId::new();
reservation.commit(agent_metadata(thread_id));
guards.release_spawned_thread(ThreadId::new());
registry.release_spawned_thread(ThreadId::new());
let err = match guards.reserve_spawn_slot(Some(1)) {
let err = match registry.reserve_spawn_slot(Some(1)) {
Ok(_) => panic!("limit should still be enforced"),
Err(err) => err,
};
@@ -101,8 +101,8 @@ fn release_ignores_unknown_thread_id() {
};
assert_eq!(max_threads, 1);
guards.release_spawned_thread(thread_id);
let reservation = guards
registry.release_spawned_thread(thread_id);
let reservation = registry
.reserve_spawn_slot(Some(1))
.expect("slot released after real thread removal");
drop(reservation);
@@ -110,20 +110,20 @@ fn release_ignores_unknown_thread_id() {
#[test]
fn release_is_idempotent_for_registered_threads() {
let guards = Arc::new(Guards::default());
let reservation = guards.reserve_spawn_slot(Some(1)).expect("reserve slot");
let registry = Arc::new(AgentRegistry::default());
let reservation = registry.reserve_spawn_slot(Some(1)).expect("reserve slot");
let first_id = ThreadId::new();
reservation.commit(agent_metadata(first_id));
guards.release_spawned_thread(first_id);
registry.release_spawned_thread(first_id);
let reservation = guards.reserve_spawn_slot(Some(1)).expect("slot reused");
let reservation = registry.reserve_spawn_slot(Some(1)).expect("slot reused");
let second_id = ThreadId::new();
reservation.commit(agent_metadata(second_id));
guards.release_spawned_thread(first_id);
registry.release_spawned_thread(first_id);
let err = match guards.reserve_spawn_slot(Some(1)) {
let err = match registry.reserve_spawn_slot(Some(1)) {
Ok(_) => panic!("limit should still be enforced"),
Err(err) => err,
};
@@ -132,8 +132,8 @@ fn release_is_idempotent_for_registered_threads() {
};
assert_eq!(max_threads, 1);
guards.release_spawned_thread(second_id);
let reservation = guards
registry.release_spawned_thread(second_id);
let reservation = registry
.reserve_spawn_slot(Some(1))
.expect("slot released after second thread removal");
drop(reservation);
@@ -141,15 +141,15 @@ fn release_is_idempotent_for_registered_threads() {
#[test]
fn failed_spawn_keeps_nickname_marked_used() {
let guards = Arc::new(Guards::default());
let mut reservation = guards.reserve_spawn_slot(None).expect("reserve slot");
let registry = Arc::new(AgentRegistry::default());
let mut reservation = registry.reserve_spawn_slot(None).expect("reserve slot");
let agent_nickname = reservation
.reserve_agent_nickname_with_preference(&["alpha"], /*preferred*/ None)
.expect("reserve agent name");
assert_eq!(agent_nickname, "alpha");
drop(reservation);
let mut reservation = guards.reserve_spawn_slot(None).expect("reserve slot");
let mut reservation = registry.reserve_spawn_slot(None).expect("reserve slot");
let agent_nickname = reservation
.reserve_agent_nickname_with_preference(&["alpha", "beta"], /*preferred*/ None)
.expect("unused name should still be preferred");
@@ -158,8 +158,10 @@ fn failed_spawn_keeps_nickname_marked_used() {
#[test]
fn agent_nickname_resets_used_pool_when_exhausted() {
let guards = Arc::new(Guards::default());
let mut first = guards.reserve_spawn_slot(None).expect("reserve first slot");
let registry = Arc::new(AgentRegistry::default());
let mut first = registry
.reserve_spawn_slot(None)
.expect("reserve first slot");
let first_name = first
.reserve_agent_nickname_with_preference(&["alpha"], /*preferred*/ None)
.expect("reserve first agent name");
@@ -167,14 +169,14 @@ fn agent_nickname_resets_used_pool_when_exhausted() {
first.commit(agent_metadata(first_id));
assert_eq!(first_name, "alpha");
let mut second = guards
let mut second = registry
.reserve_spawn_slot(None)
.expect("reserve second slot");
let second_name = second
.reserve_agent_nickname_with_preference(&["alpha"], /*preferred*/ None)
.expect("name should be reused after pool reset");
assert_eq!(second_name, "alpha the 2nd");
let active_agents = guards
let active_agents = registry
.active_agents
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
@@ -183,9 +185,11 @@ fn agent_nickname_resets_used_pool_when_exhausted() {
#[test]
fn released_nickname_stays_used_until_pool_reset() {
let guards = Arc::new(Guards::default());
let registry = Arc::new(AgentRegistry::default());
let mut first = guards.reserve_spawn_slot(None).expect("reserve first slot");
let mut first = registry
.reserve_spawn_slot(None)
.expect("reserve first slot");
let first_name = first
.reserve_agent_nickname_with_preference(&["alpha"], /*preferred*/ None)
.expect("reserve first agent name");
@@ -193,9 +197,9 @@ fn released_nickname_stays_used_until_pool_reset() {
first.commit(agent_metadata(first_id));
assert_eq!(first_name, "alpha");
guards.release_spawned_thread(first_id);
registry.release_spawned_thread(first_id);
let mut second = guards
let mut second = registry
.reserve_spawn_slot(None)
.expect("reserve second slot");
let second_name = second
@@ -204,15 +208,17 @@ fn released_nickname_stays_used_until_pool_reset() {
assert_eq!(second_name, "beta");
let second_id = ThreadId::new();
second.commit(agent_metadata(second_id));
guards.release_spawned_thread(second_id);
registry.release_spawned_thread(second_id);
let mut third = guards.reserve_spawn_slot(None).expect("reserve third slot");
let mut third = registry
.reserve_spawn_slot(None)
.expect("reserve third slot");
let third_name = third
.reserve_agent_nickname_with_preference(&["alpha", "beta"], /*preferred*/ None)
.expect("pool reset should permit a duplicate");
let expected_names = HashSet::from(["alpha the 2nd".to_string(), "beta the 2nd".to_string()]);
assert!(expected_names.contains(&third_name));
let active_agents = guards
let active_agents = registry
.active_agents
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
@@ -221,18 +227,20 @@ fn released_nickname_stays_used_until_pool_reset() {
#[test]
fn repeated_resets_advance_the_ordinal_suffix() {
let guards = Arc::new(Guards::default());
let registry = Arc::new(AgentRegistry::default());
let mut first = guards.reserve_spawn_slot(None).expect("reserve first slot");
let mut first = registry
.reserve_spawn_slot(None)
.expect("reserve first slot");
let first_name = first
.reserve_agent_nickname_with_preference(&["Plato"], /*preferred*/ None)
.expect("reserve first agent name");
let first_id = ThreadId::new();
first.commit(agent_metadata(first_id));
assert_eq!(first_name, "Plato");
guards.release_spawned_thread(first_id);
registry.release_spawned_thread(first_id);
let mut second = guards
let mut second = registry
.reserve_spawn_slot(None)
.expect("reserve second slot");
let second_name = second
@@ -241,14 +249,16 @@ fn repeated_resets_advance_the_ordinal_suffix() {
let second_id = ThreadId::new();
second.commit(agent_metadata(second_id));
assert_eq!(second_name, "Plato the 2nd");
guards.release_spawned_thread(second_id);
registry.release_spawned_thread(second_id);
let mut third = guards.reserve_spawn_slot(None).expect("reserve third slot");
let mut third = registry
.reserve_spawn_slot(None)
.expect("reserve third slot");
let third_name = third
.reserve_agent_nickname_with_preference(&["Plato"], /*preferred*/ None)
.expect("reserve third agent name");
assert_eq!(third_name, "Plato the 3rd");
let active_agents = guards
let active_agents = registry
.active_agents
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
@@ -257,27 +267,29 @@ fn repeated_resets_advance_the_ordinal_suffix() {
#[test]
fn register_root_thread_indexes_root_path() {
let guards = Arc::new(Guards::default());
let registry = Arc::new(AgentRegistry::default());
let root_thread_id = ThreadId::new();
guards.register_root_thread(root_thread_id);
registry.register_root_thread(root_thread_id);
assert_eq!(
guards.agent_id_for_path(&AgentPath::root()),
registry.agent_id_for_path(&AgentPath::root()),
Some(root_thread_id)
);
}
#[test]
fn reserved_agent_path_is_released_when_spawn_fails() {
let guards = Arc::new(Guards::default());
let mut first = guards.reserve_spawn_slot(None).expect("reserve first slot");
let registry = Arc::new(AgentRegistry::default());
let mut first = registry
.reserve_spawn_slot(None)
.expect("reserve first slot");
first
.reserve_agent_path(&agent_path("/root/researcher"))
.expect("reserve first path");
drop(first);
let mut second = guards
let mut second = registry
.reserve_spawn_slot(None)
.expect("reserve second slot");
second
@@ -287,9 +299,9 @@ fn reserved_agent_path_is_released_when_spawn_fails() {
#[test]
fn committed_agent_path_is_indexed_until_release() {
let guards = Arc::new(Guards::default());
let registry = Arc::new(AgentRegistry::default());
let thread_id = ThreadId::new();
let mut reservation = guards.reserve_spawn_slot(None).expect("reserve slot");
let mut reservation = registry.reserve_spawn_slot(None).expect("reserve slot");
reservation
.reserve_agent_path(&agent_path("/root/researcher"))
.expect("reserve path");
@@ -300,13 +312,13 @@ fn committed_agent_path_is_indexed_until_release() {
});
assert_eq!(
guards.agent_id_for_path(&agent_path("/root/researcher")),
registry.agent_id_for_path(&agent_path("/root/researcher")),
Some(thread_id)
);
guards.release_spawned_thread(thread_id);
registry.release_spawned_thread(thread_id);
assert_eq!(
guards.agent_id_for_path(&agent_path("/root/researcher")),
registry.agent_id_for_path(&agent_path("/root/researcher")),
None
);
}

View File

@@ -64,17 +64,31 @@ use codex_execpolicy::NetworkRuleProtocol;
use codex_execpolicy::Policy;
use codex_network_proxy::NetworkProxyConfig;
use codex_otel::TelemetryAuthMode;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Settings;
use codex_protocol::models::BaseInstructions;
use codex_protocol::models::ContentItem;
use codex_protocol::models::DeveloperInstructions;
use codex_protocol::models::ResponseInputItem;
use codex_protocol::models::ResponseItem;
use codex_protocol::openai_models::ModelsResponse;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::ConversationAudioParams;
use codex_protocol::protocol::RealtimeAudioFrame;
use codex_protocol::protocol::Submission;
use codex_protocol::protocol::W3cTraceContext;
use core_test_support::context_snapshot;
use core_test_support::context_snapshot::ContextSnapshotOptions;
use core_test_support::context_snapshot::ContextSnapshotRenderMode;
use core_test_support::responses::ev_completed;
use core_test_support::responses::ev_response_created;
use core_test_support::responses::mount_sse_once;
use core_test_support::responses::sse;
use core_test_support::responses::start_mock_server;
use core_test_support::test_codex::test_codex;
use core_test_support::tracing::install_test_tracing;
use core_test_support::wait_for_event;
use opentelemetry::trace::TraceContextExt;
use opentelemetry::trace::TraceId;
use std::path::Path;
@@ -1115,6 +1129,111 @@ async fn record_initial_history_reconstructs_forked_transcript() {
assert_eq!(expected, history.raw_items());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn fork_startup_context_then_first_turn_diff_snapshot() -> anyhow::Result<()> {
let server = start_mock_server().await;
mount_sse_once(
&server,
sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]),
)
.await;
let first_forked_request = mount_sse_once(
&server,
sse(vec![ev_response_created("resp-2"), ev_completed("resp-2")]),
)
.await;
let mut builder = test_codex().with_config(|config| {
config.permissions.approval_policy =
codex_config::Constrained::allow_any(AskForApproval::OnRequest);
});
let initial = builder.build(&server).await?;
let rollout_path = initial
.session_configured
.rollout_path
.clone()
.expect("rollout path");
initial
.codex
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "fork seed".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
.await?;
wait_for_event(&initial.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
let mut fork_config = initial.config.clone();
fork_config.permissions.approval_policy =
codex_config::Constrained::allow_any(AskForApproval::UnlessTrusted);
let forked = initial
.thread_manager
.fork_thread(usize::MAX, fork_config, rollout_path, false, None)
.await?;
let collaboration_mode = CollaborationMode {
mode: ModeKind::Plan,
settings: Settings {
model: forked.session_configured.model.clone(),
reasoning_effort: None,
developer_instructions: Some("Fork turn collaboration instructions.".to_string()),
},
};
forked
.thread
.submit(Op::OverrideTurnContext {
cwd: None,
approval_policy: Some(AskForApproval::Never),
approvals_reviewer: None,
sandbox_policy: None,
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,
service_tier: None,
collaboration_mode: Some(collaboration_mode),
personality: None,
})
.await?;
forked
.thread
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "after fork".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
.await?;
wait_for_event(&forked.thread, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
let request = first_forked_request.single_request();
let snapshot = context_snapshot::format_labeled_requests_snapshot(
"First request after fork when fork startup changes approval policy and the first forked turn changes approval policy again and enters plan mode.",
&[("First Forked Turn Request", &request)],
&ContextSnapshotOptions::default()
.render_mode(ContextSnapshotRenderMode::KindWithTextPrefix { max_chars: 96 })
.strip_capability_instructions()
.strip_agents_md_user_context(),
);
let mut settings = insta::Settings::clone_current();
settings.set_snapshot_path("snapshots");
settings.set_prepend_module_to_snapshot(false);
settings.bind(|| {
insta::assert_snapshot!(
"codex_core__codex_tests__fork_startup_context_then_first_turn_diff",
snapshot
);
});
Ok(())
}
#[tokio::test]
async fn record_initial_history_forked_hydrates_previous_turn_settings() {
let (session, turn_context) = make_session_and_context().await;

View File

@@ -5677,7 +5677,7 @@ approvals_reviewer = "guardian_subagent"
}
#[tokio::test]
async fn smart_approvals_alias_is_migrated_to_guardian_approval() -> std::io::Result<()> {
async fn smart_approvals_alias_is_ignored() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
std::fs::write(
codex_home.path().join(CONFIG_TOML_FILE),
@@ -5692,23 +5692,19 @@ smart_approvals = true
.build()
.await?;
assert!(config.features.enabled(Feature::GuardianApproval));
assert_eq!(config.features.legacy_feature_usages().count(), 0);
assert_eq!(
config.approvals_reviewer,
ApprovalsReviewer::GuardianSubagent
);
assert!(!config.features.enabled(Feature::GuardianApproval));
assert_eq!(config.approvals_reviewer, ApprovalsReviewer::User);
let serialized = tokio::fs::read_to_string(codex_home.path().join(CONFIG_TOML_FILE)).await?;
assert!(serialized.contains("guardian_approval = true"));
assert!(serialized.contains("approvals_reviewer = \"guardian_subagent\""));
assert!(!serialized.contains("smart_approvals"));
assert!(serialized.contains("smart_approvals = true"));
assert!(!serialized.contains("guardian_approval"));
assert!(!serialized.contains("approvals_reviewer"));
Ok(())
}
#[tokio::test]
async fn smart_approvals_alias_is_migrated_in_profiles() -> std::io::Result<()> {
async fn smart_approvals_alias_is_ignored_in_profiles() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
std::fs::write(
codex_home.path().join(CONFIG_TOML_FILE),
@@ -5719,106 +5715,6 @@ smart_approvals = true
"#,
)?;
let config = ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.fallback_cwd(Some(codex_home.path().to_path_buf()))
.build()
.await?;
assert!(config.features.enabled(Feature::GuardianApproval));
assert_eq!(config.features.legacy_feature_usages().count(), 0);
assert_eq!(
config.approvals_reviewer,
ApprovalsReviewer::GuardianSubagent
);
let serialized = tokio::fs::read_to_string(codex_home.path().join(CONFIG_TOML_FILE)).await?;
assert!(serialized.contains("[profiles.guardian.features]"));
assert!(serialized.contains("guardian_approval = true"));
assert!(serialized.contains("approvals_reviewer = \"guardian_subagent\""));
assert!(!serialized.contains("smart_approvals"));
Ok(())
}
#[tokio::test]
async fn smart_approvals_alias_migration_preserves_disabled_profile_override() -> std::io::Result<()>
{
let codex_home = TempDir::new()?;
std::fs::write(
codex_home.path().join(CONFIG_TOML_FILE),
r#"[features]
guardian_approval = true
[profiles.guardian.features]
smart_approvals = false
"#,
)?;
let config = ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.fallback_cwd(Some(codex_home.path().to_path_buf()))
.harness_overrides(ConfigOverrides {
config_profile: Some("guardian".to_string()),
..Default::default()
})
.build()
.await?;
assert!(!config.features.enabled(Feature::GuardianApproval));
assert_eq!(config.features.legacy_feature_usages().count(), 0);
assert_eq!(config.approvals_reviewer, ApprovalsReviewer::User);
let serialized = tokio::fs::read_to_string(codex_home.path().join(CONFIG_TOML_FILE)).await?;
assert!(serialized.contains("[profiles.guardian.features]"));
assert!(serialized.contains("guardian_approval = false"));
assert!(!serialized.contains("smart_approvals"));
Ok(())
}
#[tokio::test]
async fn smart_approvals_alias_migration_preserves_existing_approvals_reviewer()
-> std::io::Result<()> {
let codex_home = TempDir::new()?;
std::fs::write(
codex_home.path().join(CONFIG_TOML_FILE),
r#"approvals_reviewer = "user"
[features]
smart_approvals = true
"#,
)?;
let config = ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.fallback_cwd(Some(codex_home.path().to_path_buf()))
.build()
.await?;
assert!(config.features.enabled(Feature::GuardianApproval));
assert_eq!(config.approvals_reviewer, ApprovalsReviewer::User);
let serialized = tokio::fs::read_to_string(codex_home.path().join(CONFIG_TOML_FILE)).await?;
assert!(serialized.contains("guardian_approval = true"));
assert!(serialized.contains("approvals_reviewer = \"user\""));
assert!(!serialized.contains("smart_approvals"));
Ok(())
}
#[tokio::test]
async fn smart_approvals_alias_migration_does_not_override_canonical_disabled_flag()
-> std::io::Result<()> {
let codex_home = TempDir::new()?;
std::fs::write(
codex_home.path().join(CONFIG_TOML_FILE),
r#"[features]
guardian_approval = false
smart_approvals = true
"#,
)?;
let config = ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.fallback_cwd(Some(codex_home.path().to_path_buf()))
@@ -5829,9 +5725,10 @@ smart_approvals = true
assert_eq!(config.approvals_reviewer, ApprovalsReviewer::User);
let serialized = tokio::fs::read_to_string(codex_home.path().join(CONFIG_TOML_FILE)).await?;
assert!(serialized.contains("guardian_approval = false"));
assert!(!serialized.contains("approvals_reviewer = \"guardian_subagent\""));
assert!(!serialized.contains("smart_approvals"));
assert!(serialized.contains("[profiles.guardian.features]"));
assert!(serialized.contains("smart_approvals = true"));
assert!(!serialized.contains("guardian_approval"));
assert!(!serialized.contains("approvals_reviewer"));
Ok(())
}

View File

@@ -104,7 +104,6 @@ use crate::config::profile::ConfigProfile;
use codex_network_proxy::NetworkProxyConfig;
use toml::Value as TomlValue;
use toml_edit::DocumentMut;
use toml_edit::value;
pub(crate) mod agent_roles;
pub mod edit;
@@ -652,9 +651,6 @@ impl ConfigBuilder {
fallback_cwd,
} = self;
let codex_home = codex_home.map_or_else(find_codex_home, std::io::Result::Ok)?;
if let Err(err) = maybe_migrate_smart_approvals_alias(&codex_home).await {
tracing::warn!(error = %err, "failed to migrate smart_approvals feature alias");
}
let cli_overrides = cli_overrides.unwrap_or_default();
let mut harness_overrides = harness_overrides.unwrap_or_default();
let loader_overrides = loader_overrides.unwrap_or_default();
@@ -702,111 +698,6 @@ impl ConfigBuilder {
}
}
fn config_scope_segments(scope: &[String], key: &str) -> Vec<String> {
let mut segments = scope.to_vec();
segments.push(key.to_string());
segments
}
fn feature_scope_segments(scope: &[String], feature_key: &str) -> Vec<String> {
let mut segments = scope.to_vec();
segments.push("features".to_string());
segments.push(feature_key.to_string());
segments
}
fn push_smart_approvals_alias_migration_edits(
edits: &mut Vec<ConfigEdit>,
scope: &[String],
features: &FeaturesToml,
approvals_reviewer_missing: bool,
) {
let Some(alias_enabled) = features.entries.get("smart_approvals").copied() else {
return;
};
let canonical_enabled = features
.entries
.get("guardian_approval")
.copied()
.unwrap_or(alias_enabled);
if !features.entries.contains_key("guardian_approval") {
edits.push(ConfigEdit::SetPath {
segments: feature_scope_segments(scope, "guardian_approval"),
value: value(alias_enabled),
});
}
if canonical_enabled && approvals_reviewer_missing {
edits.push(ConfigEdit::SetPath {
segments: config_scope_segments(scope, "approvals_reviewer"),
value: value(ApprovalsReviewer::GuardianSubagent.to_string()),
});
}
edits.push(ConfigEdit::ClearPath {
segments: feature_scope_segments(scope, "smart_approvals"),
});
}
/// Rewrites the legacy `smart_approvals` feature flag to
/// `guardian_approval` in `config.toml` before normal config loading.
///
/// If the old key is present, this preserves its value by setting
/// `guardian_approval = <alias value>` when the new key is not already present.
/// Because the deprecated flag historically meant "turn guardian review on",
/// this migration also backfills `approvals_reviewer = "guardian_subagent"`
/// in the same scope when that reviewer is not already configured there and the
/// migrated feature value is `true`.
/// In all cases it removes the deprecated `smart_approvals` entry so future
/// loads only see the canonical feature flag name.
async fn maybe_migrate_smart_approvals_alias(codex_home: &Path) -> std::io::Result<bool> {
let config_path = codex_home.join(CONFIG_TOML_FILE);
if !tokio::fs::try_exists(&config_path).await? {
return Ok(false);
}
let config_contents = tokio::fs::read_to_string(&config_path).await?;
let Ok(config_toml) = toml::from_str::<ConfigToml>(&config_contents) else {
return Ok(false);
};
let mut edits = Vec::new();
let root_scope = Vec::new();
if let Some(features) = config_toml.features.as_ref() {
push_smart_approvals_alias_migration_edits(
&mut edits,
&root_scope,
features,
config_toml.approvals_reviewer.is_none(),
);
}
for (profile_name, profile) in &config_toml.profiles {
if let Some(features) = profile.features.as_ref() {
let scope = vec!["profiles".to_string(), profile_name.clone()];
push_smart_approvals_alias_migration_edits(
&mut edits,
&scope,
features,
profile.approvals_reviewer.is_none(),
);
}
}
if edits.is_empty() {
return Ok(false);
}
ConfigEditsBuilder::new(codex_home)
.with_edits(edits)
.apply()
.await
.map_err(|err| {
std::io::Error::other(format!("failed to migrate guardian_approval alias: {err}"))
})?;
Ok(true)
}
impl Config {
/// This is the preferred way to create an instance of [Config].
pub async fn load_with_cli_overrides(
@@ -868,9 +759,6 @@ pub async fn load_config_as_toml_with_cli_overrides(
cwd: &AbsolutePathBuf,
cli_overrides: Vec<(String, TomlValue)>,
) -> std::io::Result<ConfigToml> {
if let Err(err) = maybe_migrate_smart_approvals_alias(codex_home).await {
tracing::warn!(error = %err, "failed to migrate smart_approvals feature alias");
}
let config_layer_stack = load_config_layers_state(
codex_home,
Some(cwd.clone()),

View File

@@ -0,0 +1,17 @@
---
source: core/src/codex_tests.rs
assertion_line: 1282
expression: snapshot
---
Scenario: First request after fork when fork startup changes approval policy and the first forked turn changes approval policy again and enters plan mode.
## First Forked Turn Request
00:message/developer:<PERMISSIONS_INSTRUCTIONS>
01:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
02:message/user:fork seed
03:message/developer:<PERMISSIONS_INSTRUCTIONS>
04:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
05:message/developer[2]:
[01] <PERMISSIONS_INSTRUCTIONS>
[02] <collaboration_mode>Fork turn collaboration instructions.</collaboration_mode>
06:message/user:after fork

View File

@@ -176,6 +176,21 @@ mod tests {
assert_snapshot!("render_one_message", format!("{buf:?}"));
}
#[test]
fn render_one_message_with_shift_left_binding() {
let mut queue = PendingInputPreview::new();
queue.queued_messages.push("Hello, world!".to_string());
queue.set_edit_binding(key_hint::shift(KeyCode::Left));
let width = 40;
let height = queue.desired_height(width);
let mut buf = Buffer::empty(Rect::new(0, 0, width, height));
queue.render(Rect::new(0, 0, width, height), &mut buf);
assert_snapshot!(
"render_one_message_with_shift_left_binding",
format!("{buf:?}")
);
}
#[test]
fn render_two_messages() {
let mut queue = PendingInputPreview::new();

View File

@@ -0,0 +1,21 @@
---
source: tui/src/bottom_pane/pending_input_preview.rs
expression: "format!(\"{buf:?}\")"
---
Buffer {
area: Rect { x: 0, y: 0, width: 40, height: 3 },
content: [
"• Queued follow-up messages ",
" ↳ Hello, world! ",
" shift + ← edit last queued message ",
],
styles: [
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
x: 2, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
x: 0, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
x: 4, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: DIM | ITALIC,
x: 17, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
x: 0, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
x: 38, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
]
}

View File

@@ -155,6 +155,8 @@ use codex_protocol::request_permissions::RequestPermissionsEvent;
use codex_protocol::request_user_input::RequestUserInputEvent;
use codex_protocol::user_input::TextElement;
use codex_protocol::user_input::UserInput;
use codex_terminal_detection::Multiplexer;
use codex_terminal_detection::TerminalInfo;
use codex_terminal_detection::TerminalName;
use codex_terminal_detection::terminal_info;
use codex_utils_sleep_inhibitor::SleepInhibitor;
@@ -195,14 +197,21 @@ const CONNECTORS_SELECTION_VIEW_ID: &str = "connectors-selection";
/// Choose the keybinding used to edit the most-recently queued message.
///
/// Apple Terminal, Warp, and VSCode integrated terminals intercept or silently
/// swallow Alt+Up, so users in those environments would never be able to trigger
/// the edit action. We fall back to Shift+Left for those terminals while
/// keeping the more discoverable Alt+Up everywhere else.
/// swallow Alt+Up, and tmux does not reliably pass that chord through. We fall
/// back to Shift+Left for those environments while keeping the more discoverable
/// Alt+Up everywhere else.
///
/// The match is exhaustive so that adding a new `TerminalName` variant forces
/// an explicit decision about which binding that terminal should use.
fn queued_message_edit_binding_for_terminal(terminal_name: TerminalName) -> KeyBinding {
match terminal_name {
fn queued_message_edit_binding_for_terminal(terminal_info: TerminalInfo) -> KeyBinding {
if matches!(
terminal_info.multiplexer.as_ref(),
Some(Multiplexer::Tmux { .. })
) {
return key_hint::shift(KeyCode::Left);
}
match terminal_info.name {
TerminalName::AppleTerminal | TerminalName::WarpTerminal | TerminalName::VsCode => {
key_hint::shift(KeyCode::Left)
}
@@ -3617,8 +3626,7 @@ impl ChatWidget {
let active_cell = Some(Self::placeholder_session_header_cell(&config));
let current_cwd = Some(config.cwd.clone());
let queued_message_edit_binding =
queued_message_edit_binding_for_terminal(terminal_info().name);
let queued_message_edit_binding = queued_message_edit_binding_for_terminal(terminal_info());
let mut widget = Self {
app_event_tx: app_event_tx.clone(),
frame_requester: frame_requester.clone(),
@@ -3819,8 +3827,7 @@ impl ChatWidget {
let active_cell = Some(Self::placeholder_session_header_cell(&config));
let current_cwd = Some(config.cwd.clone());
let queued_message_edit_binding =
queued_message_edit_binding_for_terminal(terminal_info().name);
let queued_message_edit_binding = queued_message_edit_binding_for_terminal(terminal_info());
let mut widget = Self {
app_event_tx: app_event_tx.clone(),
frame_requester: frame_requester.clone(),
@@ -4013,8 +4020,7 @@ impl ChatWidget {
settings: fallback_default,
};
let queued_message_edit_binding =
queued_message_edit_binding_for_terminal(terminal_info().name);
let queued_message_edit_binding = queued_message_edit_binding_for_terminal(terminal_info());
let mut widget = Self {
app_event_tx: app_event_tx.clone(),
frame_requester: frame_requester.clone(),

View File

@@ -120,6 +120,8 @@ use codex_protocol::request_user_input::RequestUserInputQuestion;
use codex_protocol::request_user_input::RequestUserInputQuestionOption;
use codex_protocol::user_input::TextElement;
use codex_protocol::user_input::UserInput;
use codex_terminal_detection::Multiplexer;
use codex_terminal_detection::TerminalInfo;
use codex_terminal_detection::TerminalName;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_approval_presets::builtin_approval_presets;
@@ -3756,10 +3758,10 @@ async fn alt_up_edits_most_recent_queued_message() {
}
async fn assert_shift_left_edits_most_recent_queued_message_for_terminal(
terminal_name: TerminalName,
terminal_info: TerminalInfo,
) {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await;
chat.queued_message_edit_binding = queued_message_edit_binding_for_terminal(terminal_name);
chat.queued_message_edit_binding = queued_message_edit_binding_for_terminal(terminal_info);
chat.bottom_pane
.set_queued_message_edit_binding(chat.queued_message_edit_binding);
@@ -3791,37 +3793,102 @@ async fn assert_shift_left_edits_most_recent_queued_message_for_terminal(
#[tokio::test]
async fn shift_left_edits_most_recent_queued_message_in_apple_terminal() {
assert_shift_left_edits_most_recent_queued_message_for_terminal(TerminalName::AppleTerminal)
.await;
assert_shift_left_edits_most_recent_queued_message_for_terminal(TerminalInfo {
name: TerminalName::AppleTerminal,
term_program: None,
version: None,
term: None,
multiplexer: None,
})
.await;
}
#[tokio::test]
async fn shift_left_edits_most_recent_queued_message_in_warp_terminal() {
assert_shift_left_edits_most_recent_queued_message_for_terminal(TerminalName::WarpTerminal)
.await;
assert_shift_left_edits_most_recent_queued_message_for_terminal(TerminalInfo {
name: TerminalName::WarpTerminal,
term_program: None,
version: None,
term: None,
multiplexer: None,
})
.await;
}
#[tokio::test]
async fn shift_left_edits_most_recent_queued_message_in_vscode_terminal() {
assert_shift_left_edits_most_recent_queued_message_for_terminal(TerminalName::VsCode).await;
assert_shift_left_edits_most_recent_queued_message_for_terminal(TerminalInfo {
name: TerminalName::VsCode,
term_program: None,
version: None,
term: None,
multiplexer: None,
})
.await;
}
#[tokio::test]
async fn shift_left_edits_most_recent_queued_message_in_tmux() {
assert_shift_left_edits_most_recent_queued_message_for_terminal(TerminalInfo {
name: TerminalName::Iterm2,
term_program: None,
version: None,
term: None,
multiplexer: Some(Multiplexer::Tmux { version: None }),
})
.await;
}
#[test]
fn queued_message_edit_binding_mapping_covers_special_terminals() {
fn queued_message_edit_binding_mapping_covers_special_terminals_and_tmux() {
assert_eq!(
queued_message_edit_binding_for_terminal(TerminalName::AppleTerminal),
queued_message_edit_binding_for_terminal(TerminalInfo {
name: TerminalName::AppleTerminal,
term_program: None,
version: None,
term: None,
multiplexer: None,
}),
crate::key_hint::shift(KeyCode::Left)
);
assert_eq!(
queued_message_edit_binding_for_terminal(TerminalName::WarpTerminal),
queued_message_edit_binding_for_terminal(TerminalInfo {
name: TerminalName::WarpTerminal,
term_program: None,
version: None,
term: None,
multiplexer: None,
}),
crate::key_hint::shift(KeyCode::Left)
);
assert_eq!(
queued_message_edit_binding_for_terminal(TerminalName::VsCode),
queued_message_edit_binding_for_terminal(TerminalInfo {
name: TerminalName::VsCode,
term_program: None,
version: None,
term: None,
multiplexer: None,
}),
crate::key_hint::shift(KeyCode::Left)
);
assert_eq!(
queued_message_edit_binding_for_terminal(TerminalName::Iterm2),
queued_message_edit_binding_for_terminal(TerminalInfo {
name: TerminalName::Iterm2,
term_program: None,
version: None,
term: None,
multiplexer: Some(Multiplexer::Tmux { version: None }),
}),
crate::key_hint::shift(KeyCode::Left)
);
assert_eq!(
queued_message_edit_binding_for_terminal(TerminalInfo {
name: TerminalName::Iterm2,
term_program: None,
version: None,
term: None,
multiplexer: None,
}),
crate::key_hint::alt(KeyCode::Up)
);
}

View File

@@ -176,6 +176,21 @@ mod tests {
assert_snapshot!("render_one_message", format!("{buf:?}"));
}
#[test]
fn render_one_message_with_shift_left_binding() {
let mut queue = PendingInputPreview::new();
queue.queued_messages.push("Hello, world!".to_string());
queue.set_edit_binding(key_hint::shift(KeyCode::Left));
let width = 40;
let height = queue.desired_height(width);
let mut buf = Buffer::empty(Rect::new(0, 0, width, height));
queue.render(Rect::new(0, 0, width, height), &mut buf);
assert_snapshot!(
"render_one_message_with_shift_left_binding",
format!("{buf:?}")
);
}
#[test]
fn render_two_messages() {
let mut queue = PendingInputPreview::new();

View File

@@ -0,0 +1,21 @@
---
source: tui_app_server/src/bottom_pane/pending_input_preview.rs
expression: "format!(\"{buf:?}\")"
---
Buffer {
area: Rect { x: 0, y: 0, width: 40, height: 3 },
content: [
"• Queued follow-up messages ",
" ↳ Hello, world! ",
" shift + ← edit last queued message ",
],
styles: [
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
x: 2, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
x: 0, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
x: 4, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: DIM | ITALIC,
x: 17, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
x: 0, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
x: 38, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
]
}

View File

@@ -199,6 +199,8 @@ use codex_protocol::request_user_input::RequestUserInputEvent;
use codex_protocol::request_user_input::RequestUserInputQuestionOption;
use codex_protocol::user_input::TextElement;
use codex_protocol::user_input::UserInput;
use codex_terminal_detection::Multiplexer;
use codex_terminal_detection::TerminalInfo;
use codex_terminal_detection::TerminalName;
use codex_terminal_detection::terminal_info;
use codex_utils_sleep_inhibitor::SleepInhibitor;
@@ -238,14 +240,21 @@ const APP_SERVER_TUI_STUB_MESSAGE: &str = "Not available in app-server TUI yet."
/// Choose the keybinding used to edit the most-recently queued message.
///
/// Apple Terminal, Warp, and VSCode integrated terminals intercept or silently
/// swallow Alt+Up, so users in those environments would never be able to trigger
/// the edit action. We fall back to Shift+Left for those terminals while
/// keeping the more discoverable Alt+Up everywhere else.
/// swallow Alt+Up, and tmux does not reliably pass that chord through. We fall
/// back to Shift+Left for those environments while keeping the more discoverable
/// Alt+Up everywhere else.
///
/// The match is exhaustive so that adding a new `TerminalName` variant forces
/// an explicit decision about which binding that terminal should use.
fn queued_message_edit_binding_for_terminal(terminal_name: TerminalName) -> KeyBinding {
match terminal_name {
fn queued_message_edit_binding_for_terminal(terminal_info: TerminalInfo) -> KeyBinding {
if matches!(
terminal_info.multiplexer.as_ref(),
Some(Multiplexer::Tmux { .. })
) {
return key_hint::shift(KeyCode::Left);
}
match terminal_info.name {
TerminalName::AppleTerminal | TerminalName::WarpTerminal | TerminalName::VsCode => {
key_hint::shift(KeyCode::Left)
}
@@ -4170,8 +4179,7 @@ impl ChatWidget {
let active_cell = Some(Self::placeholder_session_header_cell(&config));
let current_cwd = Some(config.cwd.clone());
let queued_message_edit_binding =
queued_message_edit_binding_for_terminal(terminal_info().name);
let queued_message_edit_binding = queued_message_edit_binding_for_terminal(terminal_info());
let mut widget = Self {
app_event_tx: app_event_tx.clone(),
frame_requester: frame_requester.clone(),

View File

@@ -143,6 +143,8 @@ use codex_protocol::request_user_input::RequestUserInputQuestion;
use codex_protocol::request_user_input::RequestUserInputQuestionOption;
use codex_protocol::user_input::TextElement;
use codex_protocol::user_input::UserInput;
use codex_terminal_detection::Multiplexer;
use codex_terminal_detection::TerminalInfo;
use codex_terminal_detection::TerminalName;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_approval_presets::builtin_approval_presets;
@@ -3765,10 +3767,10 @@ async fn alt_up_edits_most_recent_queued_message() {
}
async fn assert_shift_left_edits_most_recent_queued_message_for_terminal(
terminal_name: TerminalName,
terminal_info: TerminalInfo,
) {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await;
chat.queued_message_edit_binding = queued_message_edit_binding_for_terminal(terminal_name);
chat.queued_message_edit_binding = queued_message_edit_binding_for_terminal(terminal_info);
chat.bottom_pane
.set_queued_message_edit_binding(chat.queued_message_edit_binding);
@@ -3800,37 +3802,102 @@ async fn assert_shift_left_edits_most_recent_queued_message_for_terminal(
#[tokio::test]
async fn shift_left_edits_most_recent_queued_message_in_apple_terminal() {
assert_shift_left_edits_most_recent_queued_message_for_terminal(TerminalName::AppleTerminal)
.await;
assert_shift_left_edits_most_recent_queued_message_for_terminal(TerminalInfo {
name: TerminalName::AppleTerminal,
term_program: None,
version: None,
term: None,
multiplexer: None,
})
.await;
}
#[tokio::test]
async fn shift_left_edits_most_recent_queued_message_in_warp_terminal() {
assert_shift_left_edits_most_recent_queued_message_for_terminal(TerminalName::WarpTerminal)
.await;
assert_shift_left_edits_most_recent_queued_message_for_terminal(TerminalInfo {
name: TerminalName::WarpTerminal,
term_program: None,
version: None,
term: None,
multiplexer: None,
})
.await;
}
#[tokio::test]
async fn shift_left_edits_most_recent_queued_message_in_vscode_terminal() {
assert_shift_left_edits_most_recent_queued_message_for_terminal(TerminalName::VsCode).await;
assert_shift_left_edits_most_recent_queued_message_for_terminal(TerminalInfo {
name: TerminalName::VsCode,
term_program: None,
version: None,
term: None,
multiplexer: None,
})
.await;
}
#[tokio::test]
async fn shift_left_edits_most_recent_queued_message_in_tmux() {
assert_shift_left_edits_most_recent_queued_message_for_terminal(TerminalInfo {
name: TerminalName::Iterm2,
term_program: None,
version: None,
term: None,
multiplexer: Some(Multiplexer::Tmux { version: None }),
})
.await;
}
#[test]
fn queued_message_edit_binding_mapping_covers_special_terminals() {
fn queued_message_edit_binding_mapping_covers_special_terminals_and_tmux() {
assert_eq!(
queued_message_edit_binding_for_terminal(TerminalName::AppleTerminal),
queued_message_edit_binding_for_terminal(TerminalInfo {
name: TerminalName::AppleTerminal,
term_program: None,
version: None,
term: None,
multiplexer: None,
}),
crate::key_hint::shift(KeyCode::Left)
);
assert_eq!(
queued_message_edit_binding_for_terminal(TerminalName::WarpTerminal),
queued_message_edit_binding_for_terminal(TerminalInfo {
name: TerminalName::WarpTerminal,
term_program: None,
version: None,
term: None,
multiplexer: None,
}),
crate::key_hint::shift(KeyCode::Left)
);
assert_eq!(
queued_message_edit_binding_for_terminal(TerminalName::VsCode),
queued_message_edit_binding_for_terminal(TerminalInfo {
name: TerminalName::VsCode,
term_program: None,
version: None,
term: None,
multiplexer: None,
}),
crate::key_hint::shift(KeyCode::Left)
);
assert_eq!(
queued_message_edit_binding_for_terminal(TerminalName::Iterm2),
queued_message_edit_binding_for_terminal(TerminalInfo {
name: TerminalName::Iterm2,
term_program: None,
version: None,
term: None,
multiplexer: Some(Multiplexer::Tmux { version: None }),
}),
crate::key_hint::shift(KeyCode::Left)
);
assert_eq!(
queued_message_edit_binding_for_terminal(TerminalInfo {
name: TerminalName::Iterm2,
term_program: None,
version: None,
term: None,
multiplexer: None,
}),
crate::key_hint::alt(KeyCode::Up)
);
}

0
test_push.md Normal file
View File