mirror of
https://github.com/openai/codex.git
synced 2026-11-03 00:21:31 +00:00
## 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`
187 lines
7.1 KiB
Rust
187 lines
7.1 KiB
Rust
//! App-server event stream handling for the TUI app.
|
|
|
|
use super::App;
|
|
use super::app_server_event_targets::ServerNotificationThreadTarget;
|
|
use super::app_server_event_targets::server_notification_thread_target;
|
|
use super::app_server_event_targets::server_request_thread_id;
|
|
use crate::app_command::AppCommand;
|
|
use crate::app_event::AppEvent;
|
|
use crate::app_server_session::AppServerSession;
|
|
use crate::app_server_session::status_account_display_from_auth_mode;
|
|
use codex_app_server_client::AppServerEvent;
|
|
use codex_app_server_protocol::AuthMode;
|
|
use codex_app_server_protocol::ServerNotification;
|
|
use codex_app_server_protocol::ServerRequest;
|
|
|
|
impl App {
|
|
fn refresh_mcp_startup_expected_servers_from_config(&mut self) {
|
|
let enabled_config_mcp_servers: Vec<String> = self
|
|
.chat_widget
|
|
.config_ref()
|
|
.mcp_servers
|
|
.get()
|
|
.iter()
|
|
.filter_map(|(name, server)| server.enabled.then_some(name.clone()))
|
|
.collect();
|
|
self.chat_widget
|
|
.set_mcp_startup_expected_servers(enabled_config_mcp_servers);
|
|
}
|
|
|
|
pub(super) async fn handle_app_server_event(
|
|
&mut self,
|
|
app_server_client: &AppServerSession,
|
|
event: AppServerEvent,
|
|
) {
|
|
match event {
|
|
AppServerEvent::Lagged { skipped } => {
|
|
tracing::warn!(
|
|
skipped,
|
|
"app-server event consumer lagged; dropping ignored events"
|
|
);
|
|
self.refresh_mcp_startup_expected_servers_from_config();
|
|
self.chat_widget.finish_mcp_startup_after_lag();
|
|
}
|
|
AppServerEvent::ServerNotification(notification) => {
|
|
self.handle_server_notification_event(app_server_client, notification)
|
|
.await;
|
|
}
|
|
AppServerEvent::ServerRequest(request) => {
|
|
self.handle_server_request_event(app_server_client, request)
|
|
.await;
|
|
}
|
|
AppServerEvent::Disconnected { message } => {
|
|
tracing::warn!("app-server event stream disconnected: {message}");
|
|
self.chat_widget.add_error_message(message.clone());
|
|
self.app_event_tx.send(AppEvent::FatalExitRequest(message));
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn handle_server_notification_event(
|
|
&mut self,
|
|
app_server_client: &AppServerSession,
|
|
notification: ServerNotification,
|
|
) {
|
|
match ¬ification {
|
|
ServerNotification::ServerRequestResolved(notification) => {
|
|
if let Some(request) = self
|
|
.pending_app_server_requests
|
|
.resolve_notification(¬ification.request_id)
|
|
{
|
|
self.chat_widget.dismiss_app_server_request(&request);
|
|
}
|
|
}
|
|
ServerNotification::McpServerStatusUpdated(_) => {
|
|
self.refresh_mcp_startup_expected_servers_from_config();
|
|
}
|
|
ServerNotification::AccountRateLimitsUpdated(notification) => {
|
|
self.chat_widget
|
|
.on_rate_limit_snapshot(Some(notification.rate_limits.clone()));
|
|
return;
|
|
}
|
|
ServerNotification::AccountUpdated(notification) => {
|
|
self.chat_widget.update_account_state(
|
|
status_account_display_from_auth_mode(
|
|
notification.auth_mode,
|
|
notification.plan_type,
|
|
),
|
|
notification.plan_type,
|
|
matches!(
|
|
notification.auth_mode,
|
|
Some(AuthMode::Chatgpt) | Some(AuthMode::ChatgptAuthTokens)
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
ServerNotification::ExternalAgentConfigImportCompleted(_) => {
|
|
let cwd = self.chat_widget.config_ref().cwd.to_path_buf();
|
|
if let Err(err) = self.refresh_in_memory_config_from_disk().await {
|
|
tracing::warn!(
|
|
error = %err,
|
|
"failed to refresh config after external agent config import"
|
|
);
|
|
}
|
|
self.chat_widget.refresh_plugin_mentions();
|
|
self.chat_widget.submit_op(AppCommand::reload_user_config());
|
|
self.fetch_plugins_list(app_server_client, cwd);
|
|
return;
|
|
}
|
|
_ => {}
|
|
}
|
|
|
|
match server_notification_thread_target(¬ification) {
|
|
ServerNotificationThreadTarget::Thread(thread_id) => {
|
|
let result = if self.primary_thread_id == Some(thread_id)
|
|
|| self.primary_thread_id.is_none()
|
|
{
|
|
self.enqueue_primary_thread_notification(notification).await
|
|
} else {
|
|
self.enqueue_thread_notification(thread_id, notification)
|
|
.await
|
|
};
|
|
|
|
if let Err(err) = result {
|
|
tracing::warn!("failed to enqueue app-server notification: {err}");
|
|
}
|
|
return;
|
|
}
|
|
ServerNotificationThreadTarget::InvalidThreadId(thread_id) => {
|
|
tracing::warn!(
|
|
thread_id,
|
|
"ignoring app-server notification with invalid thread_id"
|
|
);
|
|
return;
|
|
}
|
|
ServerNotificationThreadTarget::Global => {}
|
|
}
|
|
|
|
self.chat_widget
|
|
.handle_server_notification(notification, /*replay_kind*/ None);
|
|
}
|
|
|
|
async fn handle_server_request_event(
|
|
&mut self,
|
|
app_server_client: &AppServerSession,
|
|
request: ServerRequest,
|
|
) {
|
|
if let Some(unsupported) = self
|
|
.pending_app_server_requests
|
|
.note_server_request(&request)
|
|
{
|
|
tracing::warn!(
|
|
request_id = ?unsupported.request_id,
|
|
message = unsupported.message,
|
|
"rejecting unsupported app-server request"
|
|
);
|
|
self.chat_widget
|
|
.add_error_message(unsupported.message.clone());
|
|
if let Err(err) = self
|
|
.reject_app_server_request(
|
|
app_server_client,
|
|
unsupported.request_id,
|
|
unsupported.message,
|
|
)
|
|
.await
|
|
{
|
|
tracing::warn!("{err}");
|
|
}
|
|
return;
|
|
}
|
|
|
|
let Some(thread_id) = server_request_thread_id(&request) else {
|
|
tracing::warn!("ignoring threadless app-server request");
|
|
return;
|
|
};
|
|
|
|
let result =
|
|
if self.primary_thread_id == Some(thread_id) || self.primary_thread_id.is_none() {
|
|
self.enqueue_primary_thread_request(request).await
|
|
} else {
|
|
self.enqueue_thread_request(thread_id, request).await
|
|
};
|
|
if let Err(err) = result {
|
|
tracing::warn!("failed to enqueue app-server request: {err}");
|
|
}
|
|
}
|
|
}
|