Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions crates/codex-mac-engine/src/swap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ pub fn codex_running() -> bool {

/// Ask Codex to quit gracefully (AppleScript), polling up to `timeout_secs`.
/// Never force-kills.
///
/// Codex may answer the quit event with its own in-app confirmation dialog
/// instead of quitting (e.g. "Quit Codex? Enabled automations won't run…"
/// when automations are enabled). When Codex isn't frontmost that dialog sits
/// on a window the user never sees, so the quit silently stalls. After a short
/// grace period we therefore `activate` Codex — bringing the pending dialog
/// frontmost so the user can answer it — and keep waiting until the timeout.
pub fn quit_codex(timeout_secs: u64) -> Result<(), EngineError> {
if !codex_running() {
return Ok(());
Expand All @@ -35,14 +42,26 @@ pub fn quit_codex(timeout_secs: u64) -> Result<(), EngineError> {
.args(["-e", r#"tell application "Codex" to quit"#])
.status();

for _ in 0..(timeout_secs * 4) {
// 250ms ticks; if Codex is still running after ~5s it is most likely
// waiting on its quit-confirmation dialog — surface it.
let activate_tick = 5 * 4;
for tick in 0..(timeout_secs * 4) {
if !codex_running() {
return Ok(());
}
if tick == activate_tick {
let _ = Command::new("osascript")
.args(["-e", r#"tell application "Codex" to activate"#])
.status();
}
std::thread::sleep(std::time::Duration::from_millis(250));
}
Err(EngineError::Io(
"Codex did not quit within the timeout (left running to protect in-flight work)".to_string(),
"Codex did not quit within the timeout — it may be waiting on its own \
quit-confirmation dialog (e.g. \"Quit Codex?\" when automations are \
enabled); confirm the quit in Codex and retry (left running to \
protect in-flight work)"
.to_string(),
))
}

Expand Down
19 changes: 17 additions & 2 deletions src-tauri/src/app/mac_update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,21 @@ fn host_of(url: &str) -> String {
/// No-op progress sink for downloads whose progress isn't surfaced (e.g. stage).
fn no_progress(_p: DownloadProgress) {}

/// Graceful quit with a user-actionable failure message. Codex often answers
/// the quit event with its own confirmation dialog ("Quit Codex?" when
/// automations are enabled); the engine brings that dialog frontmost after a
/// grace period, and if the user still hasn't confirmed by the timeout we say
/// exactly what to click instead of a bare timeout. Never force-kills.
fn quit_codex_gracefully() -> Result<(), AppError> {
quit_codex(30).map_err(|_| {
AppError::Engine(
"Codex 未在限时内退出——它可能正在等待退出确认(如「Quit Codex?」对话框,已尝试将其带到前台)。\
请在 Codex 中确认退出后重试;为保护进行中的会话,不会强制结束 Codex"
.to_string(),
)
})
}

fn download_and_verify(
url: &str,
size: u64,
Expand Down Expand Up @@ -622,7 +637,7 @@ pub fn perform_macos_update(
// the swap fails after the quit, swap_in_place has restored the old
// bundle in place — bring the user's app back before surfacing the error.
let was_running = codex_running();
quit_codex(30).map_err(|e| AppError::Engine(e.to_string()))?;
quit_codex_gracefully()?;
if let Err(err) = swap_in_place(&install_path, &out_app, &backup) {
if was_running {
let _ = relaunch(&install_path);
Expand Down Expand Up @@ -874,7 +889,7 @@ pub fn uninstall_macos(keep_codex_home: bool) -> Result<MacUninstallReport, AppE
));
}

quit_codex(30).map_err(|e| AppError::Engine(e.to_string()))?;
quit_codex_gracefully()?;

// Delete first: if we lack permission to remove the bundle (e.g. a root-owned
// install), the managed record stays intact so the user can retry without
Expand Down
22 changes: 11 additions & 11 deletions src/app/i18n.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ const ZH = {
"prov.external": "未管理",

"confirm.title": "更新到 {version}?",
"confirm.body": "更新时会关闭 Codex,完成后自动重启,大约一分钟。",
"confirm.body": "更新时会关闭 Codex,完成后自动重启,大约一分钟。若 Codex 弹出退出确认框,请在 Codex 中点击确认。",
"confirm.cancel": "取消",
"confirm.ok": "更新",

Expand Down Expand Up @@ -212,7 +212,7 @@ const EN: Record<Key, string> = {
"prov.external": "Unmanaged",

"confirm.title": "Update to {version}?",
"confirm.body": "Codex will close and reopen automatically — about a minute.",
"confirm.body": "Codex will close and reopen automatically — about a minute. If Codex asks you to confirm quitting, click Quit there.",
"confirm.cancel": "Cancel",
"confirm.ok": "Update",

Expand Down Expand Up @@ -359,7 +359,7 @@ const FR: Record<Key, string> = {
"prov.external": "Non géré",

"confirm.title": "Mettre à jour vers {version} ?",
"confirm.body": "Codex va se fermer puis redémarrer automatiquement — environ une minute.",
"confirm.body": "Codex va se fermer puis redémarrer automatiquement — environ une minute. Si Codex demande de confirmer la fermeture, confirmez-la dans Codex.",
"confirm.cancel": "Annuler",
"confirm.ok": "Mettre à jour",

Expand Down Expand Up @@ -507,7 +507,7 @@ const ZH_TW: Record<Key, string> = {
"prov.external": "未管理",

"confirm.title": "更新至 {version}?",
"confirm.body": "更新時 Codex 會自動關閉並重新啟動,約需一分鐘。",
"confirm.body": "更新時 Codex 會自動關閉並重新啟動,約需一分鐘。若 Codex 跳出結束確認視窗,請在 Codex 中點按確認。",
"confirm.cancel": "取消",
"confirm.ok": "更新",

Expand Down Expand Up @@ -665,7 +665,7 @@ const DE: Record<Key, string> = {
"prov.external": "Nicht verwaltet",

"confirm.title": "Auf {version} aktualisieren?",
"confirm.body": "Codex wird geschlossen und automatisch neu gestartet — dauert etwa eine Minute.",
"confirm.body": "Codex wird geschlossen und automatisch neu gestartet — dauert etwa eine Minute. Falls Codex eine Beenden-Bestätigung anzeigt, bestätigen Sie diese in Codex.",
"confirm.cancel": "Abbrechen",
"confirm.ok": "Aktualisieren",

Expand Down Expand Up @@ -823,7 +823,7 @@ const KO: Record<Key, string> = {
"prov.external": "미관리",

"confirm.title": "{version}으로 업데이트할까요?",
"confirm.body": "업데이트 중 Codex가 종료되며, 완료 후 자동으로 재시작됩니다 — 약 1분 소요.",
"confirm.body": "업데이트 중 Codex가 종료되며, 완료 후 자동으로 재시작됩니다 — 약 1분 소요. Codex가 종료 확인 창을 표시하면 Codex에서 확인을 눌러 주세요.",
"confirm.cancel": "취소",
"confirm.ok": "업데이트",

Expand Down Expand Up @@ -975,7 +975,7 @@ const JA: Record<Key, string> = {
"prov.managed": "管理済み",
"prov.external": "未管理",
"confirm.title": "{version} にアップデートしますか?",
"confirm.body": "アップデート中は Codex が閉じ、完了後に自動で再起動します — 約 1 分かかります。",
"confirm.body": "アップデート中は Codex が閉じ、完了後に自動で再起動します — 約 1 分かかります。Codex が終了確認を表示した場合は、Codex 側で確認してください。",
"confirm.cancel": "キャンセル",
"confirm.ok": "アップデート",
"close.confirm.title": "マネージャーを閉じますか?",
Expand Down Expand Up @@ -1113,7 +1113,7 @@ const RU: Record<Key, string> = {
"prov.managed": "Управляется",
"prov.external": "Не управляется",
"confirm.title": "Обновить до {version}?",
"confirm.body": "Codex закроется и перезапустится автоматически — около минуты.",
"confirm.body": "Codex закроется и перезапустится автоматически — около минуты. Если Codex запросит подтверждение выхода, подтвердите его в Codex.",
"confirm.cancel": "Отмена",
"confirm.ok": "Обновить",
"close.confirm.title": "Закрыть менеджер?",
Expand Down Expand Up @@ -1251,7 +1251,7 @@ const AR: Record<Key, string> = {
"prov.managed": "مُدار",
"prov.external": "غير مُدار",
"confirm.title": "تحديث إلى {version}؟",
"confirm.body": "سيُغلق Codex ثم يُعاد تشغيله تلقائياً — دقيقة تقريباً.",
"confirm.body": "سيُغلق Codex ثم يُعاد تشغيله تلقائياً — دقيقة تقريباً. إذا طلب Codex تأكيد الإنهاء، فأكِّده في Codex.",
"confirm.cancel": "إلغاء",
"confirm.ok": "تحديث",
"close.confirm.title": "إغلاق المدير؟",
Expand Down Expand Up @@ -1389,7 +1389,7 @@ const ES: Record<Key, string> = {
"prov.managed": "Gestionado",
"prov.external": "No gestionado",
"confirm.title": "¿Actualizar a {version}?",
"confirm.body": "Codex se cerrará y se reabrirá automáticamente — tarda alrededor de un minuto.",
"confirm.body": "Codex se cerrará y se reabrirá automáticamente — tarda alrededor de un minuto. Si Codex pide confirmar el cierre, confírmalo en Codex.",
"confirm.cancel": "Cancelar",
"confirm.ok": "Actualizar",
"close.confirm.title": "¿Cerrar el gestor?",
Expand Down Expand Up @@ -1527,7 +1527,7 @@ const PT_BR: Record<Key, string> = {
"prov.managed": "Gerenciado",
"prov.external": "Não gerenciado",
"confirm.title": "Atualizar para {version}?",
"confirm.body": "O Codex vai fechar e reabrir automaticamente — cerca de um minuto.",
"confirm.body": "O Codex vai fechar e reabrir automaticamente — cerca de um minuto. Se o Codex pedir confirmação para sair, confirme no Codex.",
"confirm.cancel": "Cancelar",
"confirm.ok": "Atualizar",
"close.confirm.title": "Fechar o gerenciador?",
Expand Down
Loading