Skip to content

feat(tui): add terminal title support to tui app server#15860

Merged
etraut-openai merged 4 commits intoopenai:mainfrom
fcoury:feat/title-tui-app-server
Mar 26, 2026
Merged

feat(tui): add terminal title support to tui app server#15860
etraut-openai merged 4 commits intoopenai:mainfrom
fcoury:feat/title-tui-app-server

Conversation

@fcoury
Copy link
Contributor

@fcoury fcoury commented Mar 26, 2026

TR;DR

Replicates the /title command from tui to tui_app_server.

Problem

The classic tui crate supports customizing the terminal window/tab title via /title, but the tui_app_server crate does not. Users on the app-server path have no way to configure what their terminal title shows (project name, status, spinner, thread, etc.), making it harder to identify Codex sessions across tabs or windows.

Mental model

The terminal title is a status surface -- conceptually parallel to the footer status line. Both surfaces are configurable lists of items, both share expensive inputs (git branch lookup, project root discovery), and both must be refreshed at the same lifecycle points. This change ports the classic tui's design verbatim:

  1. terminal_title.rs owns the low-level OSC write path and input sanitization. It strips control characters and bidi/invisible codepoints before placing untrusted text (model output, thread names, project paths) inside an escape sequence.

  2. title_setup.rs defines TerminalTitleItem (the 8 configurable items) and TerminalTitleSetupView (the interactive picker that wraps MultiSelectPicker).

  3. status_surfaces.rs is the shared refresh pipeline. It parses both surface configs once per refresh, warns about invalid items once per session, synchronizes the git-branch cache, then renders each surface from the same StatusSurfaceSelections snapshot.

  4. chatwidget.rs sets TerminalTitleStatusKind at each state transition (Working, Thinking, Undoing, WaitingForBackgroundTerminal) and calls refresh_terminal_title() whenever relevant state changes.

  5. app.rs handles the three setup events (confirm/preview/cancel), persists config via ConfigEditsBuilder, and clears the managed title on Drop.

Non-goals

  • Restoring the previous terminal title on exit. There is no portable way to read the terminal's current title, so Drop clears the managed title rather than restoring it.
  • Sharing code between tui and tui_app_server. The implementation is a parallel copy, matching the existing pattern for the status-line feature. Extracting a shared crate is future work.

Tradeoffs

  • Duplicate code across crates. The three core files (terminal_title.rs, title_setup.rs, status_surfaces.rs) are byte-for-byte copies from the classic tui. This was chosen for consistency with the existing status-line port and to avoid coupling the two crates at the dependency level. Future changes must be applied in both places.

  • status_surfaces.rs is large (~660 lines). It absorbs logic that previously lived inline in chatwidget.rs (status-line refresh, git branch management, project root discovery) plus all new terminal-title logic. This consolidation trades file size for a single place where both surfaces are coordinated.

  • Spinner scheduling on every refresh. The terminal title spinner (when active) schedules a frame every 100ms. This is the same pattern the status-indicator spinner already uses; the overhead is a timer registration, not a redraw.

Architecture

/title command
  -> SlashCommand::Title
  -> open_terminal_title_setup()
  -> TerminalTitleSetupView (MultiSelectPicker)
  -> on_change:  AppEvent::TerminalTitleSetupPreview  -> preview_terminal_title()
  -> on_confirm: AppEvent::TerminalTitleSetup         -> ConfigEditsBuilder + setup_terminal_title()
  -> on_cancel:  AppEvent::TerminalTitleSetupCancelled -> cancel_terminal_title_setup()

Runtime title refresh:
  state change (turn start, reasoning, undo, plan update, thread rename, ...)
  -> set terminal_title_status_kind
  -> refresh_terminal_title()
  -> status_surface_selections()  (parse configs, collect invalids)
  -> refresh_terminal_title_from_selections()
     -> terminal_title_value_for_item() for each configured item
     -> assemble title string with separators
     -> skip if identical to last_terminal_title (dedup OSC writes)
     -> set_terminal_title() (sanitize + OSC 0 write)
     -> schedule spinner frame if animating

Widget replacement:
  replace_chat_widget_with_app_server_thread()
  -> transfer last_terminal_title from old widget to new
  -> avoids redundant OSC clear+rewrite on session switch

Observability

  • Invalid terminal-title item IDs in config emit a one-per-session warning via on_warning() (gated by terminal_title_invalid_items_warned AtomicBool).
  • OSC write failures are logged at tracing::debug level.
  • Config persistence failures are logged at tracing::error and surfaced to the user via add_error_message().

Tests

  • terminal_title.rs: 4 unit tests covering sanitization (control chars, bidi codepoints, truncation) and OSC output format.
  • title_setup.rs: 3 tests covering setup view snapshot rendering, parse order preservation, and invalid-ID rejection.
  • chatwidget/tests.rs: Updated test helpers with new fields; existing tests continue to pass.

Copilot AI review requested due to automatic review settings March 26, 2026 13:21
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds configurable terminal title support to the tui_app_server, including a /title setup UI, persistence, and runtime OSC title updates driven by shared “status surface” rendering logic.

Changes:

  • Introduce low-level OSC terminal-title writer with sanitization and tests.
  • Add /title slash command and a bottom-pane picker to configure/preview title segments, persisting selection to config.
  • Refactor shared status-line + terminal-title rendering/state (git branch lookup, invalid-item warnings, caches) into chatwidget/status_surfaces.rs, and attempt to preserve the last-written title cache across widget replacement.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
codex-rs/tui_app_server/src/terminal_title.rs New OSC title writer + sanitization and unit tests.
codex-rs/tui_app_server/src/slash_command.rs Adds Title slash command metadata.
codex-rs/tui_app_server/src/lib.rs Wires in the new terminal_title module.
codex-rs/tui_app_server/src/chatwidget/tests.rs Updates ChatWidget test builders for new title/state fields.
codex-rs/tui_app_server/src/chatwidget/status_surfaces.rs New shared status-line/terminal-title parsing, caching, and rendering.
codex-rs/tui_app_server/src/chatwidget.rs Integrates terminal title updates, setup flows, and shared refresh entrypoints.
codex-rs/tui_app_server/src/bottom_pane/title_setup.rs New terminal-title configuration picker UI + snapshot tests.
codex-rs/tui_app_server/src/bottom_pane/snapshots/codex_tui_app_server__bottom_pane__title_setup__tests__terminal_title_setup_basic.snap Snapshot for the new picker UI.
codex-rs/tui_app_server/src/bottom_pane/mod.rs Exposes TerminalTitleItem + TerminalTitleSetupView.
codex-rs/tui_app_server/src/app_event.rs Adds app events for title setup/preview/cancel.
codex-rs/tui_app_server/src/app.rs Persists title config changes; tries to preserve title cache across widget replacement; clears title on drop.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

fcoury added 2 commits March 26, 2026 10:28
Add `/title` support to `tui_app_server`, including the title setup
popup, config persistence, live terminal-title preview, and terminal
OSC title updates that track runtime state.

Port the shared status/title surface structure toward classic `tui`
parity so title rendering, git branch lookup, and invalid-item
handling are coordinated in `chatwidget/status_surfaces.rs`.

Also preserve the terminal title cache across widget replacement
(closing a gap vs the classic `tui` implementation) to avoid
redundant OSC writes on session switches.
Document undocumented public methods, implicit contracts, and
confusing constructs across the terminal title implementation.
Adds rationale for MAX_TERMINAL_TITLE_CHARS, explains the
double-Option setup state field, documents the strum serialization
config contract, and fixes unconventional doc-after-derive placement.
No runtime behavior changes.
@fcoury fcoury force-pushed the feat/title-tui-app-server branch from dac4bc4 to 6eced7b Compare March 26, 2026 13:33
Fix the app-server TUI title setup and refresh paths so configured
terminal-title items parse correctly, title/status surfaces refresh when
session and branch state changes, and terminal-title sanitization keeps a
visible character when the last slot would otherwise be consumed by a
pending space.

Add the missing `terminal_title_invalid_items_warned` test initializers,
extract shared invalid-item parsing, and update the title setup snapshot
and parser coverage so the reviewed behavior stays exercised.
Copy link
Collaborator

@etraut-openai etraut-openai left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code looks good. I also built and tested it locally.

@etraut-openai etraut-openai merged commit 2c54d4b into openai:main Mar 26, 2026
30 of 36 checks passed
@github-actions github-actions bot locked and limited conversation to collaborators Mar 26, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants