Skip to content

TUI-Hauptschleife und Ereignisverteilung

源码版本rust-v0.145.0

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

  1. Einstieg run_main parst die Cli (prompt, --ask-for-approval, --no-alt-screen usw.), lädt die Konfiguration, bootstrapt den app-server und übergibt die Kontrolle an App::run (codex-rs/tui/src/lib.rs:908-957).
  2. 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 liefert AppRunControl::Continue oder Exit (codex-rs/tui/src/app.rs:1185-1244).
  3. AppEvent ist der einzige Message-Bus zwischen Widget und Top-Ebene; Varianten decken alles von NewSession, OpenResumePicker bis ConsolidateAgentMessage ab, sodass Widgets nicht direkt an interne Handles von App heranreichen (codex-rs/tui/src/app_event.rs:179-295).
  4. Ausstieg in zwei Stufen: ExitMode::ShutdownFirst sendet erst Op::Shutdown und wartet auf den Abschluss des Core-Abschlusses; ExitMode::Immediate springt direkt aus der Schleife und überspringt den Shutdown (codex-rs/tui/src/app_event.rs:1135-1148).
  5. Beim Drop des Terminals werden tui.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-76Cli-Struktur; fasst prompt, --ask-for-approval, --no-alt-screen, resume/fork-interne Felder zusammen.codex-rs/tui/src/app.rs:766-787App::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-80handle_event verlegt das große AppEvent-Match in Submodule und betreibt selbst nur Routing.codex-rs/tui/src/tui.rs:542-566Tui-Struktur; hält terminal, frame requester, event broker, notification backend.codex-rs/tui/src/tui.rs:895-968Tui::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:

rust
// 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:

rust
// 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:

rust
// 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 über ExitMode::ShutdownFirst in 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 = false läuft die Schleife weiter, nur diese Quelle liefert keine Nachrichten mehr; der echte Ausstieg erfolgt weiterhin über AppEvent::Exit (codex-rs/tui/src/app.rs:1223-1232).
  • ShutdownFirst mit Timeout: Der äußere handle_exit_mode gibt 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 \n beim Einfügen in \r; handle_tui_event macht explizit pasted.replace("\r", "\n"), bevor es an tui-textarea geht (codex-rs/tui/src/app.rs:1299-1306).
  • App::drop als Auffangräumer: Selbst wenn run mittendrin panic-t, ruft Drop tui.clear_ambient_pet_image und terminal.clear auf, 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.