Move TUI on top of app server (parallel code) (#14717)

This PR replicates the `tui` code directory and creates a temporary
parallel `tui_app_server` directory. It also implements a new feature
flag `tui_app_server` to select between the two tui implementations.

Once the new app-server-based TUI is stabilized, we'll delete the old
`tui` directory and feature flag.
This commit is contained in:
Eric Traut
2026-03-16 10:49:19 -06:00
committed by GitHub
parent c04a0a7454
commit db89b73a9c
1109 changed files with 134253 additions and 17 deletions
@@ -0,0 +1,37 @@
use std::fmt;
use std::io;
use std::io::stdout;
use crossterm::Command;
use ratatui::crossterm::execute;
#[derive(Debug, Default)]
pub struct BelBackend;
impl BelBackend {
pub fn notify(&mut self, _message: &str) -> io::Result<()> {
execute!(stdout(), PostNotification)
}
}
/// Command that emits a BEL desktop notification.
#[derive(Debug, Clone)]
pub struct PostNotification;
impl Command for PostNotification {
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
write!(f, "\x07")
}
#[cfg(windows)]
fn execute_winapi(&self) -> io::Result<()> {
Err(std::io::Error::other(
"tried to execute PostNotification using WinAPI; use ANSI instead",
))
}
#[cfg(windows)]
fn is_ansi_code_supported(&self) -> bool {
true
}
}
@@ -0,0 +1,156 @@
mod bel;
mod osc9;
use std::env;
use std::io;
use bel::BelBackend;
use codex_core::config::types::NotificationMethod;
use osc9::Osc9Backend;
#[derive(Debug)]
pub enum DesktopNotificationBackend {
Osc9(Osc9Backend),
Bel(BelBackend),
}
impl DesktopNotificationBackend {
pub fn for_method(method: NotificationMethod) -> Self {
match method {
NotificationMethod::Auto => {
if supports_osc9() {
Self::Osc9(Osc9Backend)
} else {
Self::Bel(BelBackend)
}
}
NotificationMethod::Osc9 => Self::Osc9(Osc9Backend),
NotificationMethod::Bel => Self::Bel(BelBackend),
}
}
pub fn method(&self) -> NotificationMethod {
match self {
DesktopNotificationBackend::Osc9(_) => NotificationMethod::Osc9,
DesktopNotificationBackend::Bel(_) => NotificationMethod::Bel,
}
}
pub fn notify(&mut self, message: &str) -> io::Result<()> {
match self {
DesktopNotificationBackend::Osc9(backend) => backend.notify(message),
DesktopNotificationBackend::Bel(backend) => backend.notify(message),
}
}
}
pub fn detect_backend(method: NotificationMethod) -> DesktopNotificationBackend {
DesktopNotificationBackend::for_method(method)
}
fn supports_osc9() -> bool {
if env::var_os("WT_SESSION").is_some() {
return false;
}
// Prefer TERM_PROGRAM when present, but keep fallbacks for shells/launchers
// that don't set it (e.g., tmux/ssh) to avoid regressing OSC 9 support.
if matches!(
env::var("TERM_PROGRAM").ok().as_deref(),
Some("WezTerm" | "ghostty")
) {
return true;
}
// iTerm still provides a strong session signal even when TERM_PROGRAM is missing.
if env::var_os("ITERM_SESSION_ID").is_some() {
return true;
}
// TERM-based hints cover kitty/wezterm setups without TERM_PROGRAM.
matches!(
env::var("TERM").ok().as_deref(),
Some("xterm-kitty" | "wezterm" | "wezterm-mux")
)
}
#[cfg(test)]
mod tests {
use super::detect_backend;
use codex_core::config::types::NotificationMethod;
use serial_test::serial;
use std::ffi::OsString;
struct EnvVarGuard {
key: &'static str,
original: Option<OsString>,
}
impl EnvVarGuard {
fn set(key: &'static str, value: &str) -> Self {
let original = std::env::var_os(key);
unsafe {
std::env::set_var(key, value);
}
Self { key, original }
}
fn remove(key: &'static str) -> Self {
let original = std::env::var_os(key);
unsafe {
std::env::remove_var(key);
}
Self { key, original }
}
}
impl Drop for EnvVarGuard {
fn drop(&mut self) {
unsafe {
match &self.original {
Some(value) => std::env::set_var(self.key, value),
None => std::env::remove_var(self.key),
}
}
}
}
#[test]
fn selects_osc9_method() {
assert!(matches!(
detect_backend(NotificationMethod::Osc9),
super::DesktopNotificationBackend::Osc9(_)
));
}
#[test]
fn selects_bel_method() {
assert!(matches!(
detect_backend(NotificationMethod::Bel),
super::DesktopNotificationBackend::Bel(_)
));
}
#[test]
#[serial]
fn auto_prefers_bel_without_hints() {
let _term = EnvVarGuard::remove("TERM");
let _term_program = EnvVarGuard::remove("TERM_PROGRAM");
let _iterm = EnvVarGuard::remove("ITERM_SESSION_ID");
let _wt = EnvVarGuard::remove("WT_SESSION");
assert!(matches!(
detect_backend(NotificationMethod::Auto),
super::DesktopNotificationBackend::Bel(_)
));
}
#[test]
#[serial]
fn auto_uses_osc9_for_iterm() {
let _term = EnvVarGuard::remove("TERM");
let _term_program = EnvVarGuard::remove("TERM_PROGRAM");
let _iterm = EnvVarGuard::set("ITERM_SESSION_ID", "abc");
let _wt = EnvVarGuard::remove("WT_SESSION");
assert!(matches!(
detect_backend(NotificationMethod::Auto),
super::DesktopNotificationBackend::Osc9(_)
));
}
}
@@ -0,0 +1,37 @@
use std::fmt;
use std::io;
use std::io::stdout;
use crossterm::Command;
use ratatui::crossterm::execute;
#[derive(Debug, Default)]
pub struct Osc9Backend;
impl Osc9Backend {
pub fn notify(&mut self, message: &str) -> io::Result<()> {
execute!(stdout(), PostNotification(message.to_string()))
}
}
/// Command that emits an OSC 9 desktop notification with a message.
#[derive(Debug, Clone)]
pub struct PostNotification(pub String);
impl Command for PostNotification {
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
write!(f, "\x1b]9;{}\x07", self.0)
}
#[cfg(windows)]
fn execute_winapi(&self) -> io::Result<()> {
Err(std::io::Error::other(
"tried to execute PostNotification using WinAPI; use ANSI instead",
))
}
#[cfg(windows)]
fn is_ansi_code_supported(&self) -> bool {
true
}
}