Remove core protocol dependency [2/2] (#20325)

## Why

With the local model layer and app-server routing in place from PR1,
this PR moves the active TUI runtime onto app-server notifications. The
affected pieces share the same event flow, so the command surface,
session state, bottom-pane prompts, chat rendering, history/status
views, and tests move together to keep the stacked branch buildable.

This PR also removes the obsolete compatibility surface that is no
longer used after the migration. The proposed protocol-boundary verifier
layer was dropped from the stack; enforcing that final boundary will be
simpler once `codex-tui` no longer needs any `codex_protocol`
references.

This PR is part 2 of a 2-PR stack:

1. Add TUI-owned replacement models and extract app-server event
routing.
2. Move the active TUI flow to app-server notifications and delete
obsolete adapter code.

## What changed

- Rewired app command and session handling to use app-server request and
notification shapes.
- Moved approval overlays, request-user-input flows, MCP elicitation,
realtime events, and review commands onto the app-server-facing model
surface.
- Updated chat rendering, history cells, status views, multi-agent UI,
replay state, and TUI tests to use app-server notifications plus the
local models introduced in PR1.
- Deleted `codex-rs/tui/src/app/app_server_adapter.rs` and the
superseded `chatwidget/tests/background_events.rs` fixture path.

## Verification

- `cargo check -p codex-tui --tests`
- Top of stack: `cargo test -p codex-tui`
This commit is contained in:
Eric Traut
2026-04-30 11:34:34 -07:00
committed by GitHub
Unverified
parent 5cc5f12efc
commit f2bc2f26a9
76 changed files with 5078 additions and 9951 deletions
+8 -14
View File
@@ -3,9 +3,12 @@ use crate::history_cell::HistoryCell;
use crate::history_cell::PlainHistoryCell;
use crate::history_cell::with_border_with_inner_width;
use crate::legacy_core::config::Config;
use crate::token_usage::TokenUsage;
use crate::token_usage::TokenUsageInfo;
use crate::version::CODEX_CLI_VERSION;
use chrono::DateTime;
use chrono::Local;
use codex_app_server_protocol::AskForApproval;
use codex_model_provider_info::WireApi;
use codex_protocol::ThreadId;
use codex_protocol::account::PlanType;
@@ -14,9 +17,6 @@ use codex_protocol::models::ActivePermissionProfile;
use codex_protocol::models::ActivePermissionProfileModification;
use codex_protocol::models::PermissionProfile;
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::TokenUsage;
use codex_protocol::protocol::TokenUsageInfo;
use codex_utils_sandbox_summary::summarize_permission_profile;
use ratatui::prelude::*;
use ratatui::style::Stylize;
@@ -247,6 +247,8 @@ impl StatusHistoryCell {
agents_summary: String,
refreshing_rate_limits: bool,
) -> (Self, StatusHistoryHandle) {
let approval_policy = AskForApproval::from(config.permissions.approval_policy.value());
let permission_profile = config.permissions.permission_profile();
let mut config_entries = vec![
("workdir", config.cwd.display().to_string()),
("model", model_name.to_string()),
@@ -257,10 +259,7 @@ impl StatusHistoryCell {
),
(
"sandbox",
summarize_permission_profile(
&config.permissions.permission_profile(),
config.cwd.as_path(),
),
summarize_permission_profile(&permission_profile, config.cwd.as_path()),
),
];
if config.model_provider.wire_api == WireApi::Responses {
@@ -283,18 +282,13 @@ impl StatusHistoryCell {
.find(|(k, _)| *k == "approval")
.map(|(_, v)| v.clone())
.unwrap_or_else(|| "<unknown>".to_string());
let permission_profile = config.permissions.permission_profile();
let active_permission_profile = config.permissions.active_permission_profile();
let sandbox = status_permission_summary(&permission_profile, config.cwd.as_path());
let approval = status_approval_label(
config.permissions.approval_policy.value(),
config.approvals_reviewer,
&approval,
);
let approval = status_approval_label(approval_policy, config.approvals_reviewer, &approval);
let permissions = status_permissions_label(
active_permission_profile.as_ref(),
&permission_profile,
config.permissions.approval_policy.value(),
approval_policy,
&sandbox,
&approval,
);
+5 -5
View File
@@ -13,9 +13,9 @@ use chrono::DateTime;
use chrono::Duration as ChronoDuration;
use chrono::Local;
use chrono::Utc;
use codex_protocol::protocol::CreditsSnapshot as CoreCreditsSnapshot;
use codex_protocol::protocol::RateLimitSnapshot;
use codex_protocol::protocol::RateLimitWindow;
use codex_app_server_protocol::CreditsSnapshot as CoreCreditsSnapshot;
use codex_app_server_protocol::RateLimitSnapshot;
use codex_app_server_protocol::RateLimitWindow;
const STATUS_LIMIT_BAR_SEGMENTS: usize = 20;
const STATUS_LIMIT_BAR_FILLED: &str = "";
@@ -79,9 +79,9 @@ impl RateLimitWindowDisplay {
let resets_at = resets_at_utc.map(|dt| format_reset_timestamp(dt, captured_at));
Self {
used_percent: window.used_percent,
used_percent: f64::from(window.used_percent),
resets_at,
window_minutes: window.window_minutes,
window_minutes: window.window_duration_mins,
}
}
}
+103 -59
View File
@@ -8,29 +8,73 @@ use crate::legacy_core::config::ConfigBuilder;
use crate::status::StatusAccountDisplay;
use crate::test_support::PathBufExt;
use crate::test_support::test_path_buf;
use crate::token_usage::TokenUsage;
use crate::token_usage::TokenUsageInfo;
use chrono::Duration as ChronoDuration;
use chrono::TimeZone;
use chrono::Utc;
use codex_app_server_protocol::AskForApproval;
use codex_app_server_protocol::CreditsSnapshot;
use codex_app_server_protocol::FileSystemAccessMode;
use codex_app_server_protocol::FileSystemPath;
use codex_app_server_protocol::FileSystemSandboxEntry;
use codex_app_server_protocol::FileSystemSpecialPath;
use codex_app_server_protocol::PermissionProfile as AppServerPermissionProfile;
use codex_app_server_protocol::PermissionProfileFileSystemPermissions;
use codex_app_server_protocol::PermissionProfileNetworkPermissions;
use codex_app_server_protocol::RateLimitSnapshot;
use codex_app_server_protocol::RateLimitWindow;
use codex_protocol::ThreadId;
use codex_protocol::config_types::ApprovalsReviewer;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::models::ActivePermissionProfile;
use codex_protocol::models::ActivePermissionProfileModification;
use codex_protocol::models::ManagedFileSystemPermissions;
use codex_protocol::models::PermissionProfile;
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::CreditsSnapshot;
use codex_protocol::protocol::NetworkSandboxPolicy;
use codex_protocol::protocol::RateLimitSnapshot;
use codex_protocol::protocol::RateLimitWindow;
use codex_protocol::protocol::TokenUsage;
use codex_protocol::protocol::TokenUsageInfo;
use codex_protocol::permissions::NetworkSandboxPolicy;
use insta::assert_snapshot;
use pretty_assertions::assert_eq;
use ratatui::prelude::*;
use tempfile::TempDir;
fn app_server_workspace_write_profile(network_enabled: bool) -> PermissionProfile {
AppServerPermissionProfile::Managed {
network: PermissionProfileNetworkPermissions {
enabled: network_enabled,
},
file_system: PermissionProfileFileSystemPermissions::Restricted {
entries: vec![
FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::Root,
},
access: FileSystemAccessMode::Read,
},
FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::ProjectRoots { subpath: None },
},
access: FileSystemAccessMode::Write,
},
FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::SlashTmp,
},
access: FileSystemAccessMode::Write,
},
FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::Tmpdir,
},
access: FileSystemAccessMode::Write,
},
],
glob_scan_max_depth: None,
},
}
.into()
}
async fn test_config(temp_home: &TempDir) -> Config {
let mut config = ConfigBuilder::default()
.codex_home(temp_home.path().to_path_buf())
@@ -40,11 +84,8 @@ async fn test_config(temp_home: &TempDir) -> Config {
config.approvals_reviewer = ApprovalsReviewer::User;
config
.permissions
.set_permission_profile(PermissionProfile::workspace_write_with(
&[],
NetworkSandboxPolicy::Enabled,
/*exclude_tmpdir_env_var*/ false,
/*exclude_slash_tmp*/ false,
.set_permission_profile(app_server_workspace_write_profile(
/*network_enabled*/ true,
))
.expect("set permission profile");
config
@@ -171,13 +212,13 @@ async fn status_snapshot_includes_reasoning_details() {
limit_id: None,
limit_name: None,
primary: Some(RateLimitWindow {
used_percent: 72.5,
window_minutes: Some(300),
used_percent: 72,
window_duration_mins: Some(300),
resets_at: Some(reset_at_from(&captured_at, /*seconds*/ 600)),
}),
secondary: Some(RateLimitWindow {
used_percent: 45.0,
window_minutes: Some(10080),
used_percent: 45,
window_duration_mins: Some(10080),
resets_at: Some(reset_at_from(&captured_at, /*seconds*/ 1_200)),
}),
credits: None,
@@ -224,16 +265,13 @@ async fn status_permissions_non_default_workspace_write_uses_workspace_label() {
config
.permissions
.approval_policy
.set(AskForApproval::OnRequest)
.set(AskForApproval::OnRequest.to_core())
.expect("set approval policy");
config.cwd = test_path_buf("/workspace/tests").abs();
config
.permissions
.set_permission_profile(PermissionProfile::workspace_write_with(
&[],
NetworkSandboxPolicy::Enabled,
/*exclude_tmpdir_env_var*/ false,
/*exclude_slash_tmp*/ false,
.set_permission_profile(app_server_workspace_write_profile(
/*network_enabled*/ true,
))
.expect("set permission profile");
@@ -250,7 +288,7 @@ async fn status_permissions_named_read_only_profile_shows_builtin_label() {
config
.permissions
.approval_policy
.set(AskForApproval::OnRequest)
.set(AskForApproval::OnRequest.to_core())
.expect("set approval policy");
config
.permissions
@@ -273,7 +311,7 @@ async fn status_permissions_read_only_profile_shows_additional_writable_roots()
config
.permissions
.approval_policy
.set(AskForApproval::OnRequest)
.set(AskForApproval::OnRequest.to_core())
.expect("set approval policy");
let extra_root = test_path_buf("/workspace/extra").abs();
let file_system_policy = PermissionProfile::read_only()
@@ -309,7 +347,7 @@ async fn status_permissions_named_workspace_profile_shows_builtin_label() {
config
.permissions
.approval_policy
.set(AskForApproval::OnRequest)
.set(AskForApproval::OnRequest.to_core())
.expect("set approval policy");
config
.permissions
@@ -333,7 +371,7 @@ async fn status_permissions_workspace_auto_review_shows_reviewer_label() {
config
.permissions
.approval_policy
.set(AskForApproval::OnRequest)
.set(AskForApproval::OnRequest.to_core())
.expect("set approval policy");
config
.permissions
@@ -356,7 +394,7 @@ async fn status_permissions_named_profile_shows_additional_writable_roots() {
config
.permissions
.approval_policy
.set(AskForApproval::OnRequest)
.set(AskForApproval::OnRequest.to_core())
.expect("set approval policy");
let extra_root = test_path_buf("/workspace/extra").abs();
config
@@ -391,7 +429,7 @@ async fn status_permissions_broadened_workspace_profile_shows_builtin_label() {
config
.permissions
.approval_policy
.set(AskForApproval::OnRequest)
.set(AskForApproval::OnRequest.to_core())
.expect("set approval policy");
config
.permissions
@@ -532,14 +570,17 @@ async fn status_permissions_full_disk_managed_with_network_is_danger_full_access
config
.permissions
.approval_policy
.set(AskForApproval::OnRequest)
.set(AskForApproval::OnRequest.to_core())
.expect("set approval policy");
config
.permissions
.set_permission_profile(PermissionProfile::Managed {
file_system: ManagedFileSystemPermissions::Unrestricted,
network: NetworkSandboxPolicy::Enabled,
})
.set_permission_profile(
AppServerPermissionProfile::Managed {
network: PermissionProfileNetworkPermissions { enabled: true },
file_system: PermissionProfileFileSystemPermissions::Unrestricted,
}
.into(),
)
.expect("set permission profile");
assert_eq!(
@@ -555,14 +596,17 @@ async fn status_permissions_full_disk_managed_without_network_is_external_sandbo
config
.permissions
.approval_policy
.set(AskForApproval::OnRequest)
.set(AskForApproval::OnRequest.to_core())
.expect("set approval policy");
config
.permissions
.set_permission_profile(PermissionProfile::Managed {
file_system: ManagedFileSystemPermissions::Unrestricted,
network: NetworkSandboxPolicy::Restricted,
})
.set_permission_profile(
AppServerPermissionProfile::Managed {
network: PermissionProfileNetworkPermissions { enabled: false },
file_system: PermissionProfileFileSystemPermissions::Unrestricted,
}
.into(),
)
.expect("set permission profile");
assert_eq!(
@@ -650,8 +694,8 @@ async fn status_snapshot_includes_monthly_limit() {
limit_id: None,
limit_name: None,
primary: Some(RateLimitWindow {
used_percent: 12.0,
window_minutes: Some(43_200),
used_percent: 12,
window_duration_mins: Some(43_200),
resets_at: Some(reset_at_from(&captured_at, /*seconds*/ 86_400)),
}),
secondary: None,
@@ -956,8 +1000,8 @@ async fn status_snapshot_truncates_in_narrow_terminal() {
limit_id: None,
limit_name: None,
primary: Some(RateLimitWindow {
used_percent: 72.5,
window_minutes: Some(300),
used_percent: 72,
window_duration_mins: Some(300),
resets_at: Some(reset_at_from(&captured_at, /*seconds*/ 600)),
}),
secondary: None,
@@ -1116,13 +1160,13 @@ async fn status_snapshot_shows_refreshing_limits_notice() {
limit_id: None,
limit_name: None,
primary: Some(RateLimitWindow {
used_percent: 45.0,
window_minutes: Some(300),
used_percent: 45,
window_duration_mins: Some(300),
resets_at: Some(reset_at_from(&captured_at, /*seconds*/ 900)),
}),
secondary: Some(RateLimitWindow {
used_percent: 30.0,
window_minutes: Some(10_080),
used_percent: 30,
window_duration_mins: Some(10_080),
resets_at: Some(reset_at_from(&captured_at, /*seconds*/ 2_700)),
}),
credits: None,
@@ -1183,13 +1227,13 @@ async fn status_snapshot_includes_credits_and_limits() {
limit_id: None,
limit_name: None,
primary: Some(RateLimitWindow {
used_percent: 45.0,
window_minutes: Some(300),
used_percent: 45,
window_duration_mins: Some(300),
resets_at: Some(reset_at_from(&captured_at, /*seconds*/ 900)),
}),
secondary: Some(RateLimitWindow {
used_percent: 30.0,
window_minutes: Some(10_080),
used_percent: 30,
window_duration_mins: Some(10_080),
resets_at: Some(reset_at_from(&captured_at, /*seconds*/ 2_700)),
}),
credits: Some(CreditsSnapshot {
@@ -1369,13 +1413,13 @@ async fn status_snapshot_shows_stale_limits_message() {
limit_id: None,
limit_name: None,
primary: Some(RateLimitWindow {
used_percent: 72.5,
window_minutes: Some(300),
used_percent: 72,
window_duration_mins: Some(300),
resets_at: Some(reset_at_from(&captured_at, /*seconds*/ 600)),
}),
secondary: Some(RateLimitWindow {
used_percent: 40.0,
window_minutes: Some(10_080),
used_percent: 40,
window_duration_mins: Some(10_080),
resets_at: Some(reset_at_from(&captured_at, /*seconds*/ 1_800)),
}),
credits: None,
@@ -1436,13 +1480,13 @@ async fn status_snapshot_cached_limits_hide_credits_without_flag() {
limit_id: None,
limit_name: None,
primary: Some(RateLimitWindow {
used_percent: 60.0,
window_minutes: Some(300),
used_percent: 60,
window_duration_mins: Some(300),
resets_at: Some(reset_at_from(&captured_at, /*seconds*/ 1_200)),
}),
secondary: Some(RateLimitWindow {
used_percent: 35.0,
window_minutes: Some(10_080),
used_percent: 35,
window_duration_mins: Some(10_080),
resets_at: Some(reset_at_from(&captured_at, /*seconds*/ 2_400)),
}),
credits: Some(CreditsSnapshot {