Bucle principal del TUI y dispatch de eventos
App es la estructura superior de la UI de terminal (TUI) de codex. Sostiene tres handles — ChatWidget, Tui (terminal ratatui), AppServerSession — y reúne las pulsaciones del terminal, el flujo de eventos de app-server y la cola interna de AppEvent en un único bucle select!. Esta capa no renderiza ni corre el modelo; solo decide «de quién viene el próximo evento a procesar y si hay que salir».
Responsabilidades
- La entrada de arranque
run_mainparseaCli(prompt,--ask-for-approval,--no-alt-screen, etc.), carga la configuración y hace bootstrap del app-server, y luego cede el control aApp::run(codex-rs/tui/src/lib.rs:908-957). - El
select!principal escucha a la vez cuatro fuentes: mensajes internos deAppEvent, el flujo de eventos del thread actual, losTuiEventdel terminal y el flujo de notificaciones de app-server. Cada rama pasa el evento al handler correspondiente y devuelveAppRunControl::ContinueoExit(codex-rs/tui/src/app.rs:1185-1244). AppEventes el único bus de mensajes entre widget y nivel superior; las variantes van desdeNewSession,OpenResumePickerhastaConsolidateAgentMessage, de modo que el widget no necesita tomar handles internos deApp(codex-rs/tui/src/app_event.rs:179-295).- La salida tiene dos modos:
ExitMode::ShutdownFirstenvía primeroOp::Shutdowny espera a que el núcleo haga su cierre;ExitMode::Immediatesalta directamente fuera del bucle, saltándose el shutdown (codex-rs/tui/src/app_event.rs:1135-1148). - En el
Dropdel terminal se llama atui.terminal.clear()y se limpia la imagen del ambient pet, de modo que aunque algo falle a mitad, se restaura la pantalla original (codex-rs/tui/src/app.rs:1246-1266).
Motivación de diseño
Al principio, el TUI de codex mezclaba renderizado, teclas y notificaciones de app-server en un único loop; luego se sacó el bus AppEvent, porque desde el fondo del árbol de widgets se necesita a menudo disparar cosas como «abrir picker, cambiar config, cerrar thread» que solo el nivel superior puede hacer. Si se dejara que el widget sostuviera directamente el handle de AppServerSession, el árbol de componentes quedaría con una dependencia inversa hacia el estado superior. Al introducir AppEventSender (un unbounded_channel con sender clonable), el widget solo puede «enviar eventos», y la decisión queda en el nivel superior: dependencia unidireccional, fácil de testear y de reemplazar.
En el select! de cuatro vías, el flujo de app-server lleva un guard if: should_handle_active_thread_events decide cuándo conectar el canal del thread activo, evitando que cuando el thread aún no arrancó se quede bloqueado en recv(). El orden de salida también tiene su lógica: si el flujo de entrada del terminal se cierra, primero se prueba ShutdownFirst en vez de romper el bucle directamente — así, al caerse la SSH, el núcleo todavía puede terminar el cierre.
ExitMode se parte en dos modos porque algunas rutas (por ejemplo cambio de arg0, fatal error) ya saben que el núcleo o está cerrado o no hace falta esperarlo, y saltar con Immediate es más seguro; el Ctrl+D / /quit normal va por ShutdownFirst, dando a Op::Shutdown una ventana de timeout de 2 segundos.
Archivos clave
codex-rs/tui/src/main.rs:50-83 — entrada del binario; tras TopCli::parse llama a run_main, y al salir imprime el token usage y el resume hint.codex-rs/tui/src/cli.rs:8-76 — struct Cli, reúne prompt, --ask-for-approval, --no-alt-screen y los campos internos de resume/fork.codex-rs/tui/src/app.rs:766-787 — firma de App::run, con 18 parámetros de arranque en un único sitio; es la entrada principal de todo el TUI.codex-rs/tui/src/app.rs:1185-1244 — bucle select! principal; dispatch de eventos de las cuatro vías y decisión de AppRunControl.codex-rs/tui/src/app_event.rs:179-295 — primera mitad del enum AppEvent, el contrato de mensajes entre widget y nivel superior.codex-rs/tui/src/app/event_dispatch.rs:18-80 — handle_event reparte el gran match de AppEvent a submódulos; él mismo solo enruta.codex-rs/tui/src/tui.rs:542-566 — struct Tui, con terminal, frame requester, event broker y notification backend.codex-rs/tui/src/tui.rs:895-968 — Tui::draw usa stdout().sync_update para un redibujado atómico, gestionando viewport resize y pending history lines.En el bucle principal, las cuatro ramas son match Box::pin(app.handle_event(...)).await { Ok(control) => control, Err(err) => break Err(err) }; si cualquier rama falla, salta fuera y el app_server.shutdown() del nivel exterior hace de red de seguridad:
// app.rs:1185-1192 — AppEvent primero; si falla, sale
Some(event) = app_event_rx.recv() => {
match Box::pin(app.handle_event(tui, &mut app_server, event)).await {
Ok(control) => control,
Err(err) => break Err(err),
}
}En la rama de eventos del terminal, Draw / Resize son caminos de alta frecuencia: primero corren pre_draw_tick (dejan que el widget procese timers y paste burst), y luego render_chat_widget_frame redibuja de verdad; solo las teclas y el paste van al procesamiento de entrada del chat_widget. Draw y Resize comparten rama porque ambos solo necesitan un redibujado:
// app.rs:1307-1321 — Draw/Resize comparten ruta de redibujado
TuiEvent::Draw | TuiEvent::Resize => {
if self.backtrack_render_pending {
self.rebuild_transcript_after_backtrack(tui)?;
self.backtrack_render_pending = false;
}
self.chat_widget.maybe_post_pending_notification(tui);
if self.chat_widget.handle_paste_burst_tick(tui.frame_requester()) {
return Ok(AppRunControl::Continue);
}
self.chat_widget.pre_draw_tick();
let rendered_area = self.render_chat_widget_frame(tui)?;La semántica de ExitMode está directamente en el doc: ShutdownFirst espera a que Op::Shutdown termine; Immediate es una salida de emergencia para cuando ya se sabe que no hace falta esperar — por ejemplo, cambio de arg0 o fatal error:
// app_event.rs:1140-1148 — ExitMode distingue si espera al cierre del núcleo
pub(crate) enum ExitMode {
ShutdownFirst,
/// Exit the UI loop immediately without waiting for shutdown.
Immediate,
}Flujo de datos
Bordes y fallos
- El flujo de entrada del terminal cerrándose no equivale a salida activa del usuario: si el flujo de entrada es
None, se va porExitMode::ShutdownFirstpara que el núcleo haga el cierre, en vez de romper el bucle directo (codex-rs/tui/src/app.rs:1218-1222). - El flujo de eventos de app-server cortado solo cierra el flujo, no sale: con
listen_for_app_server_events = falseel bucle continúa, solo deja de recibir notificaciones por esa vía; la salida real la disparaAppEvent::Exit(codex-rs/tui/src/app.rs:1223-1232). ShutdownFirstcon timeout: elhandle_exit_modeexterior da al núcleo una ventana de cierre de 2 segundos; si se supera, fuerza la salida para no colgarse (codex-rs/tui/src/app/event_dispatch.rs:15-16).- Normalización de CR/LF en paste: terminales como iTerm2 al pegar convierten
\nen\r;handle_tui_eventhace explícitamentepasted.replace("\r", "\n")antes de pasárselo a tui-textarea (codex-rs/tui/src/app.rs:1299-1306). App::dropcomo red de seguridad de limpieza: aunquerunentre en panic a mitad,Dropsigue llamando atui.clear_ambient_pet_imageyterminal.clear, así la pantalla no se queda contaminada por estado residual (codex-rs/tui/src/app.rs:1394-1396).
Resumen
App::run es una capa pura de orquestación: cuatro fuentes de eventos → select! → handlers respectivos → AppRunControl. La lógica real de renderizado está en Componente de chat y renderizado; cómo se traducen los eventos de app-server a AppEvent se ve en Arquitectura de App-server. Ese montón de campos resume_* / fork_* en Cli son canales internos para los subcomandos codex resume / codex fork, no expuestos al usuario.