mirror of
https://github.com/openai/codex.git
synced 2026-02-01 22:47:52 +00:00
Compare commits
8 Commits
model-sele
...
daniel/not
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
89b8877f6a | ||
|
|
f564f0518a | ||
|
|
66ab8fb43d | ||
|
|
01f4b41851 | ||
|
|
a0306495c7 | ||
|
|
0bc3b4bcbf | ||
|
|
9b74053cb7 | ||
|
|
2984f90dc4 |
@@ -51,6 +51,9 @@ pub(crate) struct App {
|
||||
|
||||
// Esc-backtracking state grouped
|
||||
pub(crate) backtrack: crate::app_backtrack::BacktrackState,
|
||||
|
||||
// Whether the terminal has focus (tracked via TuiEvent::FocusChanged)
|
||||
pub(crate) app_focused: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
@@ -93,6 +96,7 @@ impl App {
|
||||
deferred_history_lines: Vec::new(),
|
||||
commit_anim_running: Arc::new(AtomicBool::new(false)),
|
||||
backtrack: BacktrackState::default(),
|
||||
app_focused: Arc::new(AtomicBool::new(true)),
|
||||
};
|
||||
|
||||
let tui_events = tui.event_stream();
|
||||
@@ -124,6 +128,10 @@ impl App {
|
||||
TuiEvent::Key(key_event) => {
|
||||
self.handle_key_event(tui, key_event).await;
|
||||
}
|
||||
TuiEvent::FocusChanged(focused) => {
|
||||
self.chat_widget.set_input_focus(focused);
|
||||
self.app_focused.store(focused, Ordering::Relaxed);
|
||||
}
|
||||
TuiEvent::Paste(pasted) => {
|
||||
// Many terminals convert newlines to \r when pasting (e.g., iTerm2),
|
||||
// but tui-textarea expects \n. Normalize CR to LF.
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::app_event_sender::AppEventSender;
|
||||
use crate::notifications;
|
||||
use crate::tui::FrameRequester;
|
||||
use crate::user_approval_widget::ApprovalRequest;
|
||||
use bottom_pane_view::BottomPaneView;
|
||||
@@ -99,6 +100,16 @@ impl BottomPane {
|
||||
}
|
||||
}
|
||||
|
||||
/// Update whether the bottom pane's composer has input focus.
|
||||
pub(crate) fn set_has_input_focus(&mut self, has_focus: bool) {
|
||||
self.has_input_focus = has_focus;
|
||||
// Use existing API to propagate focus to the composer without changing the
|
||||
// current Ctrl-C hint visibility.
|
||||
self.composer
|
||||
.set_ctrl_c_quit_hint(self.ctrl_c_quit_hint, self.has_input_focus);
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
pub fn desired_height(&self, width: u16) -> u16 {
|
||||
let top_margin = if self.active_view.is_some() { 0 } else { 1 };
|
||||
|
||||
@@ -373,6 +384,29 @@ impl BottomPane {
|
||||
|
||||
/// Called when the agent requests user approval.
|
||||
pub fn push_approval_request(&mut self, request: ApprovalRequest) {
|
||||
if !self.has_input_focus {
|
||||
// Send a system notification whenever an approval dialog is about to be shown.
|
||||
match &request {
|
||||
ApprovalRequest::Exec { command, .. } => {
|
||||
let preview = command.join(" ");
|
||||
let msg = format!("Approve \"{preview}\"?");
|
||||
notifications::send_os_notification(&msg);
|
||||
}
|
||||
ApprovalRequest::ApplyPatch {
|
||||
reason, grant_root, ..
|
||||
} => {
|
||||
let msg = if let Some(root) = grant_root {
|
||||
format!("Approve patch changes? Grant write to {}", root.display())
|
||||
} else if let Some(r) = reason {
|
||||
format!("Approve patch changes? {r}")
|
||||
} else {
|
||||
"Approve patch changes?".to_string()
|
||||
};
|
||||
notifications::send_os_notification(&msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let request = if let Some(view) = self.active_view.as_mut() {
|
||||
match view.try_consume_approval_request(request) {
|
||||
Some(request) => request,
|
||||
|
||||
@@ -693,6 +693,12 @@ impl ChatWidget {
|
||||
.map_or(0, |c| c.desired_height(width))
|
||||
}
|
||||
|
||||
/// Update input focus state for the bottom pane/composer.
|
||||
pub(crate) fn set_input_focus(&mut self, has_focus: bool) {
|
||||
self.bottom_pane.set_has_input_focus(has_focus);
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
pub(crate) fn handle_key_event(&mut self, key_event: KeyEvent) {
|
||||
match key_event {
|
||||
KeyEvent {
|
||||
|
||||
@@ -45,6 +45,7 @@ pub mod insert_history;
|
||||
pub mod live_wrap;
|
||||
mod markdown;
|
||||
mod markdown_stream;
|
||||
mod notifications;
|
||||
pub mod onboarding;
|
||||
mod pager_overlay;
|
||||
mod render;
|
||||
|
||||
62
codex-rs/tui/src/notifications.rs
Normal file
62
codex-rs/tui/src/notifications.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
use std::process::Command;
|
||||
|
||||
/// Send a simple OS notification with a fixed app title.
|
||||
/// Best-effort and silently ignores errors if the platform/tooling is unavailable.
|
||||
pub fn send_os_notification(message: &str) {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
fn detect_bundle_id() -> Option<&'static str> {
|
||||
use std::env;
|
||||
// Common terminal mappings.
|
||||
let term_program = env::var("TERM_PROGRAM").unwrap_or_default();
|
||||
match term_program.as_str() {
|
||||
"Apple_Terminal" => Some("com.apple.Terminal"),
|
||||
"iTerm.app" | "iTerm2" | "iTerm2.app" => Some("com.googlecode.iterm2"),
|
||||
"WezTerm" => Some("com.github.wez.wezterm"),
|
||||
"Alacritty" => Some("io.alacritty"),
|
||||
other => {
|
||||
// Fallback heuristics.
|
||||
let term = env::var("TERM").unwrap_or_default();
|
||||
if other.to_lowercase().contains("kitty") || term.contains("xterm-kitty") {
|
||||
Some("net.kovidgoyal.kitty")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Prefer terminal-notifier on macOS and attempt to activate the current terminal on click.
|
||||
let mut cmd = Command::new("terminal-notifier");
|
||||
cmd.arg("-title").arg("Codex").arg("-message").arg(message);
|
||||
if let Some(bundle) = detect_bundle_id() {
|
||||
cmd.arg("-activate").arg(bundle);
|
||||
}
|
||||
let _ = cmd.spawn();
|
||||
}
|
||||
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
{
|
||||
// Use notify-send if available (Linux/BSD). Title first, then body.
|
||||
let _ = Command::new("notify-send")
|
||||
.arg("Codex")
|
||||
.arg(message)
|
||||
.spawn();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// Best-effort: try a lightweight Toast via PowerShell if available.
|
||||
// Fall back to no-op if this fails.
|
||||
let ps = r#"
|
||||
Add-Type -AssemblyName System.Windows.Forms | Out-Null
|
||||
[System.Windows.Forms.MessageBox]::Show($args[0], 'Codex') | Out-Null
|
||||
"#;
|
||||
let _ = Command::new("powershell")
|
||||
.arg("-NoProfile")
|
||||
.arg("-Command")
|
||||
.arg(ps)
|
||||
.arg(message)
|
||||
.spawn();
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,9 @@ use crossterm::SynchronizedUpdate;
|
||||
use crossterm::cursor;
|
||||
use crossterm::cursor::MoveTo;
|
||||
use crossterm::event::DisableBracketedPaste;
|
||||
use crossterm::event::DisableFocusChange;
|
||||
use crossterm::event::EnableBracketedPaste;
|
||||
use crossterm::event::EnableFocusChange;
|
||||
use crossterm::event::Event;
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
@@ -49,6 +51,8 @@ pub type Terminal = CustomTerminal<CrosstermBackend<Stdout>>;
|
||||
|
||||
pub fn set_modes() -> Result<()> {
|
||||
execute!(stdout(), EnableBracketedPaste)?;
|
||||
// Enable focus change reporting where supported; ignore errors on unsupported terminals.
|
||||
let _ = execute!(stdout(), EnableFocusChange);
|
||||
|
||||
enable_raw_mode()?;
|
||||
// Enable keyboard enhancement flags so modifiers for keys like Enter are disambiguated.
|
||||
@@ -116,6 +120,8 @@ pub fn restore() -> Result<()> {
|
||||
// Pop may fail on platforms that didn't support the push; ignore errors.
|
||||
let _ = execute!(stdout(), PopKeyboardEnhancementFlags);
|
||||
execute!(stdout(), DisableBracketedPaste)?;
|
||||
// Disable focus change reporting if it was enabled; ignore errors.
|
||||
let _ = execute!(stdout(), DisableFocusChange);
|
||||
disable_raw_mode()?;
|
||||
let _ = execute!(stdout(), crossterm::cursor::Show);
|
||||
Ok(())
|
||||
@@ -154,6 +160,8 @@ pub enum TuiEvent {
|
||||
Key(KeyEvent),
|
||||
Paste(String),
|
||||
Draw,
|
||||
/// Terminal focus changed: true when focused, false when unfocused.
|
||||
FocusChanged(bool),
|
||||
AttachImage {
|
||||
path: PathBuf,
|
||||
width: u32,
|
||||
@@ -323,12 +331,12 @@ impl Tui {
|
||||
}
|
||||
Err(_) => {
|
||||
// Fall back to normal key handling if no image is available.
|
||||
yield TuiEvent::Key(key_event);
|
||||
}
|
||||
}
|
||||
}
|
||||
yield TuiEvent::Key(key_event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
crossterm::event::Event::Key(key_event) => {
|
||||
crossterm::event::Event::Key(key_event) => {
|
||||
#[cfg(unix)]
|
||||
if matches!(
|
||||
key_event,
|
||||
@@ -363,6 +371,12 @@ impl Tui {
|
||||
Event::Resize(_, _) => {
|
||||
yield TuiEvent::Draw;
|
||||
}
|
||||
Event::FocusGained => {
|
||||
yield TuiEvent::FocusChanged(true);
|
||||
}
|
||||
Event::FocusLost => {
|
||||
yield TuiEvent::FocusChanged(false);
|
||||
}
|
||||
Event::Paste(pasted) => {
|
||||
yield TuiEvent::Paste(pasted);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user