TUI-Hauptschleife und Ereignisverteilung
App ist die oberste Struktur der Terminal-UI (TUI) von codex. Sie hält ChatWidget, Tui (ratatui-Terminal) und AppServerSession als drei Handles und führt Tastatureingaben, den app-server-Ereignisstrom und die interne AppEvent-Warteschlange in einer select!-Schleife zusammen. Diese Schicht zeichnet nicht und ruft kein Modell auf; sie entscheidet nur „welches das nächste zu behandelnde Ereignis ist und ob ausgestiegen wird".
Verantwortlichkeiten
- Einstieg
run_mainparst dieCli(prompt,--ask-for-approval,--no-alt-screenusw.), lädt die Konfiguration, bootstrapt den app-server und übergibt die Kontrolle anApp::run(codex-rs/tui/src/lib.rs:908-957). - Haupt-
select!wartet gleichzeitig auf vier Quellen:AppEvent-interne Nachrichten, den Ereignisstrom des aktuellen Threads, Terminal-TuiEvent, den app-server-Benachrichtigungsstrom. Jede Verzweigung übergibt das Ereignis an den zugehörigen Handler und liefertAppRunControl::ContinueoderExit(codex-rs/tui/src/app.rs:1185-1244). AppEventist der einzige Message-Bus zwischen Widget und Top-Ebene; Varianten decken alles vonNewSession,OpenResumePickerbisConsolidateAgentMessageab, sodass Widgets nicht direkt an interne Handles vonAppheranreichen (codex-rs/tui/src/app_event.rs:179-295).- Ausstieg in zwei Stufen:
ExitMode::ShutdownFirstsendet erstOp::Shutdownund wartet auf den Abschluss des Core-Abschlusses;ExitMode::Immediatespringt direkt aus der Schleife und überspringt den Shutdown (codex-rs/tui/src/app_event.rs:1135-1148). - Beim
Dropdes Terminals werdentui.terminal.clear()aufgerufen und das ambient-pet-Bild gelöscht; auch bei einem Zwischenfehler wird der Bildschirm wiederhergestellt (codex-rs/tui/src/app.rs:1246-1266).
Entwurfsbeweggründe
Früh vermischte die codex-TUI Rendering, Tasten und app-server-Benachrichtigungen in einer Schleife; später wurde der AppEvent-Bus ausgekoppelt, weil tiefe Widget-Knoten oft Aktionen auslösen müssen, die nur die Top-Ebene darf („Picker öffnen", „Config ändern", „Thread schließen"). Hielte das Widget direkt ein AppServerSession-Handle, hinge der Komponentenbaum rückwärts vom Top-Zustand ab. Mit AppEventSender (einem klonbaren Sender eines unbounded_channel) kann das Widget nur „Ereignisse senden"; die Entscheidung bleibt bei der Top-Ebene — einseitige Abhängigkeit, leichter zu testen und zu ersetzen.
In der vierwegigen select! ist der app-server-Strom mit einem if-Guard versehen: should_handle_active_thread_events entscheidet, wann der active-thread-Kanal zugeschaltet wird, damit er nicht blockiert, wenn der Thread noch nicht gestartet ist. Auch die Ausstiegsreihenfolge ist durchdacht: Schließt der Terminal-Eingabestrom, wird zuerst ShutdownFirst versucht statt direkt zu breaken — so kann core bei SSH-Abbruch noch aufräumen.
ExitMode ist zweiteilig, weil einige Pfade (etwa arg0-Wechsel, fatal error) bereits wissen, dass core entweder schon zu oder nicht wartenswert ist; hier ist Immediate sicherer. Gewöhnliches Ctrl+D / /quit geht über ShutdownFirst und gibt Op::Shutdown ein 2-Sekunden-Zeitfenster.
Wichtige Dateien
codex-rs/tui/src/main.rs:50-83 — Binary-Einstieg; nach TopCli::parse Aufruf von run_main, am Ende Token-Usage und Resume-Hint.codex-rs/tui/src/cli.rs:8-76 — Cli-Struktur; fasst prompt, --ask-for-approval, --no-alt-screen, resume/fork-interne Felder zusammen.codex-rs/tui/src/app.rs:766-787 — App::run-Signatur; bündelt 18 Startparameter an einer Stelle, Haupteinstieg der gesamten TUI.codex-rs/tui/src/app.rs:1185-1244 — Haupt-select!-Schleife; vierwegige Ereignisverteilung und AppRunControl-Entscheidung.codex-rs/tui/src/app_event.rs:179-295 — Erste Hälfte des AppEvent-Enums; Nachrichtenvertrag zwischen Widget und Top-Ebene.codex-rs/tui/src/app/event_dispatch.rs:18-80 — handle_event verlegt das große AppEvent-Match in Submodule und betreibt selbst nur Routing.codex-rs/tui/src/tui.rs:542-566 — Tui-Struktur; hält terminal, frame requester, event broker, notification backend.codex-rs/tui/src/tui.rs:895-968 — Tui::draw nutzt stdout().sync_update für atomares Neuzeichnen; behandelt viewport resize und pending history lines.In der Hauptschleife ist jeder Zweig match Box::pin(app.handle_event(...)).await { Ok(control) => control, Err(err) => break Err(err) }; bei Fehler in einem Zweig wird direkt ausgesprungen und der äußere app_server.shutdown() fängt auf:
// 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),
}
}In der Terminal-Ereignis-Verzweigung sind Draw / Resize hochfrequente Pfade; sie laufen erst pre_draw_tick (lassen das Widget Timer und paste burst behandeln), dann render_chat_widget_frame für das eigentliche Neuzeichnen; Tasten und paste gehen in die Eingabebehandlung des chat_widget. Draw und Resize teilen sich einen Zweig, weil beide nur ein Neuzeichnen brauchen:
// 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)?;Die Semantik von ExitMode steht direkt im Doc: ShutdownFirst wartet auf Op::Shutdown; Immediate ist der Notausgang, wenn schon klar ist, dass nicht gewartet werden muss — etwa bei arg0-Wechsel oder fatal error:
// app_event.rs:1140-1148 — ExitMode 区分是否等核心收尾
pub(crate) enum ExitMode {
ShutdownFirst,
/// Exit the UI loop immediately without waiting for shutdown.
Immediate,
}Datenfluss
Grenzen und Fehler
- Geschlossener Terminal-Eingabestrom heißt nicht aktiver Nutzer-Ausstieg: Liefert der Eingabestrom
None, geht es überExitMode::ShutdownFirstin den core-Abschluss statt direkt zu breaken (codex-rs/tui/src/app.rs:1218-1222). - app-server-Ereignisstromabbruch schließt nur den Strom, ohne auszusteigen: Nach
listen_for_app_server_events = falseläuft die Schleife weiter, nur diese Quelle liefert keine Nachrichten mehr; der echte Ausstieg erfolgt weiterhin überAppEvent::Exit(codex-rs/tui/src/app.rs:1223-1232). ShutdownFirstmit Timeout: Der äußerehandle_exit_modegibt core ein 2-Sekunden-Fenster; bei Timeout wird der Ausstieg erzwungen, um ein Hängen zu vermeiden (codex-rs/tui/src/app/event_dispatch.rs:15-16).- CR/LF-Normalisierung beim paste: iTerm2 und ähnliche Terminal wandeln
\nbeim Einfügen in\r;handle_tui_eventmacht explizitpasted.replace("\r", "\n"), bevor es an tui-textarea geht (codex-rs/tui/src/app.rs:1299-1306). App::dropals Auffangräumer: Selbst wennrunmittendrin panic-t, ruftDroptui.clear_ambient_pet_imageundterminal.clearauf, damit der Bildschirm nicht in einem verbleibenden Zustand hängen bleibt (codex-rs/tui/src/app.rs:1394-1396).
Zusammenfassung
App::run ist eine reine Planungsschicht: vier Ereignisquellen → select! → jeweiliger Handler → AppRunControl. Die eigentliche Rendering-Logik siehe Chat-Komponente und Rendering; wie app-server-Ereignisse zu AppEvent übersetzt werden, siehe App-server-Architektur. Die resume_* / fork_*-Felder der Cli sind interne Kanäle für die Subkommandos codex resume / codex fork und dem Nutzer nicht ausgesetzt.