Boucle principale TUI et dispatch d'événements
App est la structure de tête de la TUI (terminal UI) de codex ; elle détient ChatWidget, Tui (ratatui terminal) et AppServerSession — trois handles — et fusionne dans un select! unique les touches terminal, le flux d'événements app-server et la file interne d'AppEvent. Cette couche ne fait ni rendu ni exécution de modèle ; elle décide seulement « qui est le prochain événement à traiter, et faut-il quitter ».
Responsabilités
- L'entrée
run_mainparseCli(prompt,--ask-for-approval,--no-alt-screen, etc.), charge la config, bootstrap app-server, puis passe la main àApp::run(codex-rs/tui/src/lib.rs:908-957). - Le
select!principal attend quatre sources simultanément : messages internesAppEvent, flux d'événements du thread courant,TuiEventterminal, flux de notifications app-server. Chaque branche confie l'événement à son handler et renvoieAppRunControl::ContinueouExit(codex-rs/tui/src/app.rs:1185-1244). AppEventest le seul bus de messages entre widget et top-level ; les variantes couvrent deNewSession,OpenResumePickeràConsolidateAgentMessage, si bien que le widget n'a pas besoin de manipuler directement les handles internes deApp(codex-rs/tui/src/app_event.rs:179-295).- La sortie a deux modes :
ExitMode::ShutdownFirstenvoie d'abordOp::Shutdownet attend la fin de core,ExitMode::Immediatesort directement de la boucle en sautant le shutdown (codex-rs/tui/src/app_event.rs:1135-1148). - Au
Dropterminal, on appelletui.terminal.clear()et on nettoie l'image ambient pet, pour restaurer l'écran original même en cas d'erreur en cours de route (codex-rs/tui/src/app.rs:1246-1266).
Motivations de conception
Au début, la TUI de codex mélangeait rendu, touches et notifications app-server dans une seule boucle ; plus tard, le bus AppEvent a été extrait, parce que les profondeurs de l'arbre de widget doivent souvent déclencher « ouvrir picker, modifier config, fermer thread » — des actions que seul le top-level peut faire. Si on laissait le widget tenir directement un handle AppServerSession, l'arbre des composants dépendrait à l'envers de l'état top-level. En introduisant AppEventSender (un simple sender clone-able de unbounded_channel), le widget ne fait qu'« émettre des événements », et la décision reste au top-level — dépendance unidirectionnelle, plus facile à tester et à remplacer.
Dans le select! à quatre voies, le flux app-server a un garde if : should_handle_active_thread_events décide quand brancher le canal du thread actif, pour éviter que recv() ne bloque inutilement tant que le thread n'est pas lancé. L'ordre de sortie aussi compte : si le flux d'entrée terminal se ferme, on tente d'abord ShutdownFirst plutôt que de breaker directement — comme ça, en cas de coupure SSH, core peut encore finir sa propre clôture.
ExitMode a deux modes parce que certains chemins (par ex. switch arg0, fatal error) savent déjà que core est soit fermé soit inutile à attendre ; un Immediate direct est plus sûr ; Ctrl+D / /quit classiques passent par ShutdownFirst, qui laisse à Op::Shutdown une fenêtre de 2 secondes.
Fichiers clés
codex-rs/tui/src/main.rs:50-83 — entrée binaire, après TopCli::parse appelle run_main, et imprime le token usage et le resume hint à la sortie.codex-rs/tui/src/cli.rs:8-76 — structure Cli, rassemble prompt, --ask-for-approval, --no-alt-screen, champs internes resume/fork.codex-rs/tui/src/app.rs:766-787 — signature de App::run, regroupe 18 paramètres de démarrage, l'entrée principale de toute la TUI.codex-rs/tui/src/app.rs:1185-1244 — la boucle select! principale, dispatch des quatre sources et décision AppRunControl.codex-rs/tui/src/app_event.rs:179-295 — première moitié de l'énum AppEvent, contrat de messages entre widget et top-level.codex-rs/tui/src/app/event_dispatch.rs:18-80 — handle_event répartit le gros match d'AppEvent dans des sous-modules, ne fait que du routing.codex-rs/tui/src/tui.rs:542-566 — la structure Tui, porte terminal, frame requester, event broker, notification backend.codex-rs/tui/src/tui.rs:895-968 — Tui::draw utilise stdout().sync_update pour un redraw atomique, gère le resize du viewport et les pending history lines.Dans la boucle principale, les quatre branches font toutes match Box::pin(app.handle_event(...)).await { Ok(control) => control, Err(err) => break Err(err) } ; la moindre erreur sort, et l'app_server.shutdown() de l'extérieur prend le relais :
// app.rs:1185-1192 — AppEvent 优先,出错即跳出
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),
}
}Dans la branche événements terminal, Draw / Resize sont à haute fréquence : on lance d'abord pre_draw_tick (laisser le widget traiter les minuteurs et paste burst), puis render_chat_widget_frame pour le redraw effectif ; seuls les keypress et paste vont au traitement d'entrée de chat_widget. Draw et Resize partagent la même branche parce qu'ils ne nécessitent qu'un seul redraw :
// app.rs:1307-1321 — Draw/Resize 共用重绘路径
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 sémantique de ExitMode est directement dans la doc : ShutdownFirst attend la fin d'Op::Shutdown, Immediate est la sortie de secours quand on sait qu'il n'y a rien à attendre — par ex. switch arg0 ou fatal error :
// app_event.rs:1140-1148 — ExitMode 区分是否等核心收尾
pub(crate) enum ExitMode {
ShutdownFirst,
/// Exit the UI loop immediately without waiting for shutdown.
Immediate,
}Flux de données
Limites et échecs
- Flux d'entrée terminal fermé ≠ utilisateur qui quitte activement : quand le flux renvoie
None, on prendExitMode::ShutdownFirstpour laisser core finir, plutôt que de breaker direct (codex-rs/tui/src/app.rs:1218-1222). - Flux app-server coupé : on ferme juste le flux sans quitter : une fois
listen_for_app_server_events = false, la boucle continue, elle ne reçoit simplement plus cette voie ; la vraie sortie reste déclenchée parAppEvent::Exit(codex-rs/tui/src/app.rs:1223-1232). ShutdownFirstavec timeout : en amont,handle_exit_modedonne à core une fenêtre de clôture de 2 s ; au-delà, sortie forcée, pour éviter un blocage (codex-rs/tui/src/app/event_dispatch.rs:15-16).- Normalisation CR/LF du paste : iTerm2 et autres terminaux convertissent
\nen\rlors du paste ;handle_tui_eventfait explicitementpasted.replace("\r", "\n")avant d'alimenter tui-textarea (codex-rs/tui/src/app.rs:1299-1306). App::dropnettoie en filet de sécurité : même sirunpanic en cours,Dropappelletui.clear_ambient_pet_imageetterminal.clear, l'écran n'est pas pollué par un état résiduel (codex-rs/tui/src/app.rs:1394-1396).
Récapitulatif
App::run est une couche de pure orchestration : quatre sources → select! → handlers dédiés → AppRunControl. La logique de rendu réelle est dans Composant chat et rendu ; la traduction des événements app-server en AppEvent est décrite dans Architecture App-server. Les champs resume_* / fork_* de Cli sont un canal interne pour les sous-commandes codex resume / codex fork, non exposé à l'utilisateur.