mirror of
https://github.com/openai/codex.git
synced 2026-11-08 20:40:33 +00:00
WIP
This commit is contained in:
@@ -91,7 +91,7 @@ seccompiler = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
core-foundation = "0.9"
|
||||
mac-notification-sys = "0.6.6"
|
||||
mac-notification-sys = "=0.6.6"
|
||||
|
||||
# Build OpenSSL from source for musl builds.
|
||||
[target.x86_64-unknown-linux-musl.dependencies]
|
||||
|
||||
@@ -109,20 +109,254 @@ mod tests {
|
||||
mod macos {
|
||||
use super::UserNotification;
|
||||
use crate::config;
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use anyhow::anyhow;
|
||||
use mac_notification_sys::Notification;
|
||||
use mac_notification_sys::NotificationResponse;
|
||||
use serde_json;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use tempfile::NamedTempFile;
|
||||
use tracing::debug;
|
||||
use tracing::warn;
|
||||
|
||||
const HELPER_ENV: &str = "CODEX_NOTIFIER_APP";
|
||||
const HELPER_BUNDLE_NAME: &str = "CodexNotifier.app";
|
||||
const HELPER_EXECUTABLE: &str = "Contents/MacOS/codex-notifier";
|
||||
const INFO_PLIST_TEMPLATE: &[u8] = include_bytes!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/../notifier/Resources/Info.plist"
|
||||
));
|
||||
const ICON_BYTES: &[u8] = include_bytes!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/../notifier/Resources/Codex.icns"
|
||||
));
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct MacNotifier {
|
||||
icon_path: Option<PathBuf>,
|
||||
helper: Option<HelperApp>,
|
||||
legacy: LegacyNotifier,
|
||||
}
|
||||
|
||||
impl MacNotifier {
|
||||
pub(crate) fn new() -> Self {
|
||||
let helper = match HelperApp::discover() {
|
||||
Ok(app) => {
|
||||
debug!(
|
||||
"using Codex helper notifier at {}",
|
||||
app.bundle_path.display()
|
||||
);
|
||||
Some(app)
|
||||
}
|
||||
Err(err) => {
|
||||
debug!("no helper notifier available: {err:?}");
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
Self {
|
||||
helper,
|
||||
legacy: LegacyNotifier::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn notify(&self, notification: &UserNotification) {
|
||||
if env::var("CODEX_SANDBOX").is_ok() {
|
||||
return;
|
||||
}
|
||||
if env::var("CI").is_ok() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(helper) = &self.helper {
|
||||
if let Err(err) = helper.notify(notification) {
|
||||
warn!("failed to send macOS notification via helper: {err:?}");
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
self.legacy.notify(notification);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct HelperApp {
|
||||
bundle_path: PathBuf,
|
||||
}
|
||||
|
||||
impl HelperApp {
|
||||
fn discover() -> Result<Self> {
|
||||
if let Ok(custom) = env::var(HELPER_ENV) {
|
||||
let candidate = PathBuf::from(custom);
|
||||
if Self::is_valid_bundle(&candidate) {
|
||||
return Ok(Self {
|
||||
bundle_path: candidate,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for path in Self::candidate_paths()? {
|
||||
if Self::is_valid_bundle(&path) {
|
||||
return Ok(Self { bundle_path: path });
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(installed) = Self::install_to_codex_home() {
|
||||
if Self::is_valid_bundle(&installed) {
|
||||
return Ok(Self {
|
||||
bundle_path: installed,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Err(anyhow!("helper bundle not found"))
|
||||
}
|
||||
|
||||
fn candidate_paths() -> Result<Vec<PathBuf>> {
|
||||
let mut candidates = Vec::new();
|
||||
|
||||
if let Ok(home) = config::find_codex_home() {
|
||||
candidates.push(home.join(HELPER_BUNDLE_NAME));
|
||||
candidates.push(home.join("bin").join(HELPER_BUNDLE_NAME));
|
||||
}
|
||||
|
||||
if let Ok(exe) = env::current_exe() {
|
||||
if let Some(parent) = exe.parent() {
|
||||
candidates.push(parent.join(HELPER_BUNDLE_NAME));
|
||||
if let Some(grand) = parent.parent() {
|
||||
candidates.push(grand.join(HELPER_BUNDLE_NAME));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(candidates)
|
||||
}
|
||||
|
||||
fn install_to_codex_home() -> Result<PathBuf> {
|
||||
let mut base = config::find_codex_home()?;
|
||||
base.push("bin");
|
||||
fs::create_dir_all(&base)?;
|
||||
let bundle_path = base.join(HELPER_BUNDLE_NAME);
|
||||
if !Self::is_valid_bundle(&bundle_path) {
|
||||
Self::write_bundle(&bundle_path)?;
|
||||
}
|
||||
Ok(bundle_path)
|
||||
}
|
||||
|
||||
fn write_bundle(destination: &Path) -> Result<()> {
|
||||
let contents = destination.join("Contents");
|
||||
let macos_dir = contents.join("MacOS");
|
||||
let resources_dir = contents.join("Resources");
|
||||
fs::create_dir_all(&macos_dir)?;
|
||||
fs::create_dir_all(&resources_dir)?;
|
||||
|
||||
fs::write(contents.join("Info.plist"), INFO_PLIST_TEMPLATE)
|
||||
.context("failed to write Info.plist")?;
|
||||
fs::write(resources_dir.join("Codex.icns"), ICON_BYTES)
|
||||
.context("failed to write Codex.icns")?;
|
||||
|
||||
let binary_src = Self::locate_binary().context("codex-notifier binary not found")?;
|
||||
let binary_dest = macos_dir.join("codex-notifier");
|
||||
fs::copy(&binary_src, &binary_dest)
|
||||
.with_context(|| format!("failed to copy notifier binary from {binary_src:?}"))?;
|
||||
|
||||
let mut perms = fs::metadata(&binary_dest)?.permissions();
|
||||
perms.set_mode(0o755);
|
||||
fs::set_permissions(&binary_dest, perms)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn locate_binary() -> Result<PathBuf> {
|
||||
for candidate in Self::binary_candidate_paths()? {
|
||||
if candidate.is_file() {
|
||||
return Ok(candidate);
|
||||
}
|
||||
if candidate.is_dir() {
|
||||
let nested = candidate.join("codex-notifier");
|
||||
if nested.is_file() {
|
||||
return Ok(nested);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(anyhow!("codex-notifier binary unavailable"))
|
||||
}
|
||||
|
||||
fn binary_candidate_paths() -> Result<Vec<PathBuf>> {
|
||||
let mut paths = Vec::new();
|
||||
|
||||
if let Ok(exe) = env::current_exe() {
|
||||
if let Some(parent) = exe.parent() {
|
||||
paths.push(parent.join("codex-notifier"));
|
||||
paths.push(parent.join("../codex-notifier"));
|
||||
paths.push(parent.join("../../codex-notifier"));
|
||||
if let Some(grand) = parent.parent() {
|
||||
paths.push(grand.join("codex-notifier"));
|
||||
paths.push(grand.join("release").join("codex-notifier"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(home) = config::find_codex_home() {
|
||||
paths.push(home.join("bin").join("codex-notifier"));
|
||||
}
|
||||
|
||||
Ok(paths)
|
||||
}
|
||||
|
||||
fn is_valid_bundle(path: &Path) -> bool {
|
||||
let exec = path.join(HELPER_EXECUTABLE);
|
||||
exec.is_file()
|
||||
}
|
||||
|
||||
fn notify(&self, notification: &UserNotification) -> Result<()> {
|
||||
let payload = serde_json::to_vec(notification)?;
|
||||
|
||||
let mut temp = NamedTempFile::new().context("failed to create temp payload file")?;
|
||||
temp.as_file_mut()
|
||||
.write_all(&payload)
|
||||
.context("failed to write payload")?;
|
||||
temp.flush().ok();
|
||||
|
||||
let temp_path = temp.into_temp_path();
|
||||
let payload_path = temp_path
|
||||
.keep()
|
||||
.context("failed to persist payload file for helper app")?;
|
||||
|
||||
let status = Command::new("open")
|
||||
.arg("-n")
|
||||
.arg(&self.bundle_path)
|
||||
.arg("--args")
|
||||
.arg("--payload-file")
|
||||
.arg(&payload_path)
|
||||
.status()
|
||||
.with_context(|| {
|
||||
format!("failed to invoke open for {}", self.bundle_path.display())
|
||||
})?;
|
||||
|
||||
if !status.success() {
|
||||
let _ = fs::remove_file(&payload_path);
|
||||
return Err(anyhow!("open exited with status {status:?}"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct LegacyNotifier {
|
||||
icon_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl LegacyNotifier {
|
||||
fn new() -> Self {
|
||||
if let Err(err) = mac_notification_sys::set_application("com.openai.codex") {
|
||||
warn!("failed to register bundle id for notifications: {err}");
|
||||
}
|
||||
@@ -136,17 +370,7 @@ mod macos {
|
||||
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;
|
||||
}
|
||||
|
||||
fn notify(&self, notification: &UserNotification) {
|
||||
let (title, subtitle, message) = match notification {
|
||||
UserNotification::AgentTurnComplete {
|
||||
last_assistant_message,
|
||||
@@ -191,7 +415,7 @@ mod macos {
|
||||
debug!("Codex notification clicked");
|
||||
}
|
||||
Ok(NotificationResponse::None) => {}
|
||||
Err(err) => warn!("failed to deliver macOS notification: {err}"),
|
||||
Err(err) => warn!("failed to deliver macOS notification via legacy path: {err}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,6 +437,9 @@ mod macos {
|
||||
|
||||
if needs_write {
|
||||
fs::write(&path, ICON_BYTES)?;
|
||||
let mut perms = fs::metadata(&path)?.permissions();
|
||||
perms.set_mode(0o644);
|
||||
fs::set_permissions(&path, perms)?;
|
||||
}
|
||||
|
||||
Ok(path)
|
||||
|
||||
Reference in New Issue
Block a user