mirror of
https://github.com/openai/codex.git
synced 2026-04-24 22:54:54 +00:00
WIP
This commit is contained in:
24
codex-rs/Cargo.lock
generated
24
codex-rs/Cargo.lock
generated
@@ -614,6 +614,15 @@ dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block2"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
|
||||
dependencies = [
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blocking"
|
||||
version = "1.6.2"
|
||||
@@ -1086,6 +1095,7 @@ dependencies = [
|
||||
"keyring",
|
||||
"landlock",
|
||||
"libc",
|
||||
"mac-notification-sys",
|
||||
"maplit",
|
||||
"mcp-types",
|
||||
"openssl-sys",
|
||||
@@ -3616,6 +3626,18 @@ dependencies = [
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mac-notification-sys"
|
||||
version = "0.6.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "119c8490084af61b44c9eda9d626475847a186737c0378c85e32d77c33a01cd4"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "maplit"
|
||||
version = "1.0.2"
|
||||
@@ -4061,6 +4083,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"block2",
|
||||
"libc",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
@@ -46,7 +46,7 @@ Use `codex mcp` to add/list/get/remove MCP server launchers defined in `config.t
|
||||
|
||||
### Notifications
|
||||
|
||||
You can enable notifications by configuring a script that is run whenever the agent finishes a turn. The [notify documentation](../docs/config.md#notify) includes a detailed example that explains how to get desktop notifications via [terminal-notifier](https://github.com/julienXX/terminal-notifier) on macOS.
|
||||
On macOS, the CLI now ships with a signed Notification Center integration, so you’ll get trusted desktop alerts (with the Codex icon) as soon as a turn completes—no extra tooling required. If you want to customize the behaviour or are on another platform, you can still point `notify` at your own script; see the [notify documentation](../docs/config.md#notify) for an example.
|
||||
|
||||
### `codex exec` to run Codex programmatically/non-interactively
|
||||
|
||||
|
||||
@@ -91,6 +91,7 @@ seccompiler = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
core-foundation = "0.9"
|
||||
mac-notification-sys = "0.6.6"
|
||||
|
||||
# Build OpenSSL from source for musl builds.
|
||||
[target.x86_64-unknown-linux-musl.dependencies]
|
||||
|
||||
BIN
codex-rs/core/assets/codex-notification.png
Normal file
BIN
codex-rs/core/assets/codex-notification.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 139 KiB |
@@ -2,9 +2,11 @@ use serde::Serialize;
|
||||
use tracing::error;
|
||||
use tracing::warn;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct UserNotifier {
|
||||
notify_command: Option<Vec<String>>,
|
||||
#[cfg(target_os = "macos")]
|
||||
native: macos::MacNotifier,
|
||||
}
|
||||
|
||||
impl UserNotifier {
|
||||
@@ -12,14 +14,19 @@ impl UserNotifier {
|
||||
if let Some(notify_command) = &self.notify_command
|
||||
&& !notify_command.is_empty()
|
||||
{
|
||||
self.invoke_notify(notify_command, notification)
|
||||
if self.invoke_notify(notify_command, notification) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
self.native.notify(notification);
|
||||
}
|
||||
|
||||
fn invoke_notify(&self, notify_command: &[String], notification: &UserNotification) {
|
||||
fn invoke_notify(&self, notify_command: &[String], notification: &UserNotification) -> bool {
|
||||
let Ok(json) = serde_json::to_string(¬ification) else {
|
||||
error!("failed to serialise notification payload");
|
||||
return;
|
||||
return false;
|
||||
};
|
||||
|
||||
let mut command = std::process::Command::new(¬ify_command[0]);
|
||||
@@ -29,18 +36,30 @@ impl UserNotifier {
|
||||
command.arg(json);
|
||||
|
||||
// Fire-and-forget – we do not wait for completion.
|
||||
if let Err(e) = command.spawn() {
|
||||
warn!("failed to spawn notifier '{}': {e}", notify_command[0]);
|
||||
match command.spawn() {
|
||||
Ok(_) => true,
|
||||
Err(e) => {
|
||||
warn!("failed to spawn notifier '{}': {e}", notify_command[0]);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn new(notify: Option<Vec<String>>) -> Self {
|
||||
Self {
|
||||
notify_command: notify,
|
||||
#[cfg(target_os = "macos")]
|
||||
native: macos::MacNotifier::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for UserNotifier {
|
||||
fn default() -> Self {
|
||||
Self::new(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// User can configure a program that will receive notifications. Each
|
||||
/// notification is serialized as JSON and passed as an argument to the
|
||||
/// program.
|
||||
@@ -85,3 +104,118 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod macos {
|
||||
use super::UserNotification;
|
||||
use crate::config;
|
||||
use mac_notification_sys::Notification;
|
||||
use mac_notification_sys::NotificationResponse;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use tracing::debug;
|
||||
use tracing::warn;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct MacNotifier {
|
||||
icon_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl MacNotifier {
|
||||
pub(crate) fn new() -> Self {
|
||||
if let Err(err) = mac_notification_sys::set_application("com.openai.codex") {
|
||||
warn!("failed to register bundle id for notifications: {err}");
|
||||
}
|
||||
|
||||
let icon_path = Self::ensure_icon()
|
||||
.map_err(|err| {
|
||||
warn!("failed to prepare macOS notification icon: {err}");
|
||||
})
|
||||
.ok();
|
||||
|
||||
Self { icon_path }
|
||||
}
|
||||
|
||||
pub(crate) fn notify(&self, notification: &UserNotification) {
|
||||
if std::env::var("CODEX_SANDBOX").is_ok() {
|
||||
// Avoid firing real notifications when running inside our sandboxed Seatbelt harness.
|
||||
return;
|
||||
}
|
||||
|
||||
if std::env::var("CI").is_ok() {
|
||||
// Skip macOS notifications when running in CI environments.
|
||||
return;
|
||||
}
|
||||
|
||||
let (title, subtitle, message) = match notification {
|
||||
UserNotification::AgentTurnComplete {
|
||||
last_assistant_message,
|
||||
input_messages,
|
||||
..
|
||||
} => {
|
||||
let title = "Codex CLI";
|
||||
let subtitle = last_assistant_message
|
||||
.as_ref()
|
||||
.map(std::string::String::as_str)
|
||||
.unwrap_or("Turn complete");
|
||||
let message = if input_messages.is_empty() {
|
||||
String::from("Agent turn finished")
|
||||
} else {
|
||||
input_messages.join(" ")
|
||||
};
|
||||
(title.to_string(), subtitle.to_string(), message)
|
||||
}
|
||||
};
|
||||
|
||||
let mut payload = Notification::new();
|
||||
payload.title(&title);
|
||||
payload.maybe_subtitle(Some(&subtitle));
|
||||
payload.message(&message);
|
||||
payload.default_sound();
|
||||
|
||||
if let Some(icon_path) = self.icon_path.as_ref().and_then(|p| p.to_str()) {
|
||||
payload.app_icon(icon_path);
|
||||
}
|
||||
|
||||
match payload.send() {
|
||||
Ok(NotificationResponse::ActionButton(action)) => {
|
||||
debug!("Codex notification action pressed: {action}");
|
||||
}
|
||||
Ok(NotificationResponse::CloseButton(label)) => {
|
||||
debug!("Codex notification dismissed via '{label}' button");
|
||||
}
|
||||
Ok(NotificationResponse::Reply(body)) => {
|
||||
debug!("Codex notification reply entered (ignored): {body}");
|
||||
}
|
||||
Ok(NotificationResponse::Click) => {
|
||||
debug!("Codex notification clicked");
|
||||
}
|
||||
Ok(NotificationResponse::None) => {}
|
||||
Err(err) => warn!("failed to deliver macOS notification: {err}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_icon() -> anyhow::Result<PathBuf> {
|
||||
const ICON_BYTES: &[u8] = include_bytes!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/assets/codex-notification.png"
|
||||
));
|
||||
|
||||
let mut path = config::find_codex_home()?;
|
||||
path.push("assets");
|
||||
fs::create_dir_all(&path)?;
|
||||
path.push("codex-notification.png");
|
||||
|
||||
let needs_write = match fs::read(&path) {
|
||||
Ok(existing) => existing != ICON_BYTES,
|
||||
Err(_) => true,
|
||||
};
|
||||
|
||||
if needs_write {
|
||||
fs::write(&path, ICON_BYTES)?;
|
||||
}
|
||||
|
||||
Ok(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -615,6 +615,8 @@ function without the extra dependencies.
|
||||
|
||||
### notify
|
||||
|
||||
On macOS, Codex now emits native Notification Center alerts by default for supported events. The CLI packages a signed notification helper and a Codex icon, so you’ll see trusted banners without needing to install `terminal-notifier`. If you prefer to take full control, or are on another platform, you can still wire up your own script via the `notify` setting described below. Providing a `notify` command overrides the built-in macOS integration.
|
||||
|
||||
Specify a program that will be executed to get notified about events generated by Codex. Note that the program will receive the notification argument as a string of JSON, e.g.:
|
||||
|
||||
```json
|
||||
|
||||
Reference in New Issue
Block a user