Files
codex/codex-rs/tui/src/notifications/mod.rs
T
Casey Chow 41652665f5 [codex] Add tmux-aware OSC 9 notifications (#17836)
## Summary
- wrap OSC 9 notifications in tmux's DCS passthrough so terminal
notifications make it through tmux
- use codex-terminal-detection for OSC 9 auto-selection so tmux sessions
inherit the underlying client terminal support
- add focused notification backend tests for plain OSC 9 and
tmux-wrapped output

## Stack
- base PR: #18479
- review order: #18479, then this PR

## Why
Tmux does not forward OSC 9 notifications directly; the sequence has to
be wrapped in tmux's DCS passthrough envelope. Codex also had local
notification heuristics that could miss supported terminals when running
under tmux, even though codex-terminal-detection already knows how to
attribute tmux sessions to the client terminal.

## Validation
- `just fmt`
- `cargo test -p codex-tui` *(currently blocked by an unrelated existing
compile error in `app-server/src/message_processor.rs:754` referencing
`connection_id` out of scope; not caused by this change)*

Co-authored-by: Codex <noreply@openai.com>
2026-04-21 17:10:36 +00:00

136 lines
3.6 KiB
Rust

mod bel;
mod osc9;
use std::io;
use bel::BelBackend;
use codex_config::types::NotificationMethod;
use codex_terminal_detection::TerminalInfo;
use codex_terminal_detection::TerminalName;
use codex_terminal_detection::terminal_info;
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(&terminal_info()) {
Self::Osc9(Osc9Backend::new())
} else {
Self::Bel(BelBackend)
}
}
NotificationMethod::Osc9 => Self::Osc9(Osc9Backend::new()),
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(terminal: &TerminalInfo) -> bool {
matches!(
terminal.name,
TerminalName::Ghostty
| TerminalName::Iterm2
| TerminalName::Kitty
| TerminalName::WarpTerminal
| TerminalName::WezTerm
)
}
#[cfg(test)]
mod tests {
use super::detect_backend;
use super::supports_osc9;
use codex_config::types::NotificationMethod;
use codex_terminal_detection::TerminalInfo;
use codex_terminal_detection::TerminalName;
use pretty_assertions::assert_eq;
fn test_terminal(name: TerminalName) -> TerminalInfo {
TerminalInfo {
name,
term_program: None,
version: None,
term: None,
multiplexer: None,
}
}
#[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]
fn supports_osc9_for_supported_terminals() {
for name in [
TerminalName::Ghostty,
TerminalName::Iterm2,
TerminalName::Kitty,
TerminalName::WarpTerminal,
TerminalName::WezTerm,
] {
assert!(
supports_osc9(&test_terminal(name)),
"{name:?} should support OSC 9"
);
}
}
#[test]
fn supports_osc9_for_unsupported_terminals() {
for name in [
TerminalName::AppleTerminal,
TerminalName::Alacritty,
TerminalName::Dumb,
TerminalName::GnomeTerminal,
TerminalName::Konsole,
TerminalName::Unknown,
TerminalName::VsCode,
TerminalName::Vte,
TerminalName::WindowsTerminal,
] {
assert_eq!(
supports_osc9(&test_terminal(name)),
false,
"{name:?} should not support OSC 9"
);
}
}
}