Skip to content
Draft
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
67 changes: 21 additions & 46 deletions apps/desktop-tauri/src-tauri/src/auto_resume.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,19 @@

use codexbar::agent_sessions::{
AgentSession, AgentSessionProvider, AgentSessionSource, AgentSessionState,
LocalAgentSessionScanner, SessionFocusResult,
LocalAgentSessionScanner,
};
use codexbar::core::{ProviderId, TokenAccountStore};
use codexbar::settings::Settings;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::Mutex;

use crate::commands::{ProviderUsageSnapshot, RateWindowSnapshot};
use crate::state::AppState;
use tauri::Manager;

const MAX_SESSION_ID_LEN: usize = 256;
#[cfg(windows)]
const CREATE_NEW_CONSOLE: u32 = 0x0000_0010;
#[cfg(windows)]
const CREATE_NEW_PROCESS_GROUP: u32 = 0x0000_0200;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum QuotaSlot {
Expand Down Expand Up @@ -633,19 +628,22 @@ async fn resume_captured_session(
}
if let Some(session) = matching_session(&target, &result.sessions) {
// A live process already owns this exact session. Reopening it would
// create a duplicate terminal, so focus the existing window instead.
// create a duplicate terminal, so point the user at the existing
// window instead. This runs after a background refresh, not a user
// action: flash the window's taskbar button rather than restoring and
// activating it over the app the user is working in.
if session.pid.is_some() {
if !resume_attempt_is_still_valid(app, &target, operation, account_identity) {
clear_provider_if_resume_owner(app, target.provider, operation);
return;
}
let focus_result = codexbar::agent_sessions::focus_session(session);
let succeeded = matches!(focus_result, SessionFocusResult::Focused);
let attention = codexbar::agent_sessions::request_session_attention(session);
let succeeded = attention.is_ok();
tracing::info!(
provider = target.provider.cli_name(),
focus_result = ?focus_result,
attention = ?attention,
succeeded,
"captured CLI session is already running; attempting to focus it"
"captured CLI session is already running; flashing its window"
);
finish_resume_attempt(app, &target, operation, succeeded);
return;
Expand Down Expand Up @@ -804,14 +802,15 @@ fn launch_resume(target: &ResumeTarget) -> Result<(), String> {
.ok_or_else(|| format!("{} CLI was not found", target.provider.display_name()))?;

let command = build_resume_command(target, executable)?;
let mut process = Command::new(&command.program);
process.args(&command.args).current_dir(&command.cwd);
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
process.creation_flags(CREATE_NEW_CONSOLE | CREATE_NEW_PROCESS_GROUP);
}
process.spawn().map(|_| ()).map_err(|error| {
// This runs after a background refresh, not a user action: the console
// starts minimized and inactive, so it waits on the taskbar instead of
// taking focus from the app the user is working in.
codexbar::host::console_launch::spawn_minimized_console(
&command.program,
&command.args,
&command.cwd,
)
.map_err(|error| {
format!(
"failed to launch {} CLI: {error}",
target.provider.display_name()
Expand All @@ -835,40 +834,16 @@ fn build_resume_command(
_ => return Err("provider does not support auto-resume".to_string()),
};

#[cfg(windows)]
if executable
.extension()
.and_then(|extension| extension.to_str())
.is_some_and(|extension| matches!(extension.to_ascii_lowercase().as_str(), "cmd" | "bat"))
{
let command_line = std::iter::once(quote_cmd_arg(&executable.to_string_lossy()))
.chain(cli_args.iter().map(|arg| quote_cmd_arg(arg)))
.collect::<Vec<_>>()
.join(" ");
return Ok(ResumeCommand {
program: PathBuf::from("cmd.exe"),
args: vec![
"/d".to_string(),
"/s".to_string(),
"/c".to_string(),
command_line,
],
cwd: target.cwd.clone(),
});
}

// Batch shims such as npm's `claude.cmd` stay the program here;
// `spawn_minimized_console` runs them through cmd.exe with quoting cmd.exe
// parses.
Ok(ResumeCommand {
program: executable,
args: cli_args,
cwd: target.cwd.clone(),
})
}

#[cfg(windows)]
fn quote_cmd_arg(value: &str) -> String {
format!("\"{}\"", value.replace('"', "\\\""))
}

#[cfg(test)]
#[path = "auto_resume_tests.rs"]
mod auto_resume_tests;
16 changes: 9 additions & 7 deletions apps/desktop-tauri/src-tauri/src/auto_resume_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,22 +86,24 @@ mod tests {
);
}

#[cfg(windows)]
#[test]
fn cmd_wrapper_preserves_exact_resume_arguments() {
fn batch_shims_keep_exact_resume_arguments() {
// cmd.exe wrapping for `.cmd` shims happens at launch, in
// `codexbar::host::console_launch`, which tests its own quoting.
let command = build_resume_command(
&target(ProviderId::Claude),
PathBuf::from(r"C:\Program Files\Claude\claude.cmd"),
)
.unwrap();

assert_eq!(command.program, PathBuf::from("cmd.exe"));
assert_eq!(&command.args[..3], ["/d", "/s", "/c"]);
assert_eq!(
command.args[3],
r#""C:\Program Files\Claude\claude.cmd" "--resume" "12345678-1234-1234-1234-123456789abc""#
command.program,
PathBuf::from(r"C:\Program Files\Claude\claude.cmd")
);
assert_eq!(
command.args,
["--resume", "12345678-1234-1234-1234-123456789abc"]
);
assert!(!command.args[3].contains("prompt"));
}

#[test]
Expand Down
23 changes: 16 additions & 7 deletions apps/desktop-tauri/src-tauri/src/commands/surface.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use super::*;
use crate::shell::activation::Activation;

// ── Surface-mode commands ────────────────────────────────────────────

Expand All @@ -11,8 +12,15 @@ pub fn set_surface_mode(
let mode = SurfaceMode::parse(&mode).ok_or_else(|| format!("unknown surface mode: {mode}"))?;
let target = validate_surface_target(mode, target)?;

crate::shell::transition_to_target(window.app_handle(), mode, target, None)
.map(|mode| mode.as_str().to_string())
// The frontend only calls this from a click (e.g. a Settings tab).
crate::shell::transition_to_target(
window.app_handle(),
mode,
target,
None,
Activation::UserAction,
)
.map(|mode| mode.as_str().to_string())
}

#[tauri::command]
Expand Down Expand Up @@ -66,7 +74,7 @@ pub async fn open_settings_window(app: tauri::AppHandle, tab: String) -> Result<
/// on Windows.
#[tauri::command]
pub async fn open_flyout_window(app: tauri::AppHandle) -> Result<(), String> {
crate::shell::flyout_window::open_or_focus(&app, None)
crate::shell::flyout_window::open_or_focus(&app, None, Activation::UserAction)
}

/// Reveal the flyout window after the frontend's first layout pass. Called by
Expand All @@ -75,6 +83,8 @@ pub async fn open_flyout_window(app: tauri::AppHandle) -> Result<(), String> {
/// blank/backing frame.
///
/// No-ops when the flyout window doesn't exist or no one-shot reveal is pending.
/// The window takes focus only as far as the pending reveal's activation
/// allows (see `shell::activation`).
#[tauri::command]
pub fn reveal_tray_panel_window(
app: tauri::AppHandle,
Expand All @@ -86,17 +96,16 @@ pub fn reveal_tray_panel_window(
return Ok(());
};
let mut guard = state.lock().map_err(|e| e.to_string())?;
if !guard.take_pending_flyout_reveal() {
let Some(activation) = guard.take_pending_flyout_reveal() else {
return Ok(());
}
};
drop(guard);
window.show().map_err(|e| e.to_string())?;
state
.lock()
.map_err(|e| e.to_string())?
.mark_tray_panel_shown(std::time::Instant::now());
window.set_focus().map_err(|e| e.to_string())?;
Ok(())
crate::shell::activation::apply(&window, activation)
}

#[tauri::command]
Expand Down
33 changes: 25 additions & 8 deletions apps/desktop-tauri/src-tauri/src/floatbar/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ pub(super) fn recover_onto_primary<R: tauri::Runtime, M: WindowGeometry<R>>(
if window.is_minimized().unwrap_or(false)
|| is_windows_minimized_position(position.x, position.y)
{
let _ = window.unminimize();
let _ = window.restore_without_activation();
}
if window.set_physical_position(target).is_err() {
return false;
Expand Down Expand Up @@ -230,10 +230,10 @@ pub fn opacity_to_alpha(opacity: u8) -> u8 {
((clamped as u32) * 255 / 100) as u8
}

/// Open the floating-bar window, or focus + reapply attributes if already
/// open. Position is restored from the geometry store keyed by
/// `floatbar`; on first launch the window is centered horizontally near
/// the top of the primary monitor.
/// Open the floating-bar window, or re-show it and reapply attributes if
/// already open. The bar never takes focus. Position is restored from the
/// geometry store keyed by `floatbar`; on first launch the window is
/// centered horizontally near the top of the primary monitor.
pub fn show(
app: &tauri::AppHandle,
opacity: u8,
Expand Down Expand Up @@ -266,6 +266,9 @@ pub fn show(
.resizable(false)
.always_on_top(true)
.skip_taskbar(true)
// Show with SW_SHOWNOACTIVATE: a plain first show activates the bar
// and takes focus from the app the user is typing in.
.focused(false)
// Pin Dark: the floatbar is the only window without a theme pin, and
// WebView2 resolves prefers-color-scheme per shared process profile,
// so an unpinned (light-default) webview flips the Settings window's
Expand Down Expand Up @@ -381,7 +384,10 @@ pub trait WindowGeometry<R: tauri::Runtime> {
fn is_minimized(&self) -> tauri::Result<bool>;
fn primary_monitor(&self) -> tauri::Result<Option<tauri::Monitor>>;
fn available_monitors(&self) -> tauri::Result<Vec<tauri::Monitor>>;
fn unminimize(&self) -> tauri::Result<()>;
/// Un-minimize without taking the foreground. The bar is never meant to
/// be focused, and a plain `unminimize` activates it (see
/// [`crate::shell::activation::restore_minimized_without_activation`]).
fn restore_without_activation(&self) -> tauri::Result<()>;
fn set_physical_position(&self, position: PhysicalPosition<i32>) -> tauri::Result<()>;
}

Expand All @@ -404,7 +410,14 @@ impl<R: tauri::Runtime> WindowGeometry<R> for tauri::WebviewWindow<R> {
fn available_monitors(&self) -> tauri::Result<Vec<tauri::Monitor>> {
tauri::WebviewWindow::available_monitors(self)
}
fn unminimize(&self) -> tauri::Result<()> {
fn restore_without_activation(&self) -> tauri::Result<()> {
// Queued on the main thread ahead of the calls below, so the restore
// lands before `unminimize` re-reads the state and before the caller
// moves the window.
let window = self.clone();
tauri::WebviewWindow::run_on_main_thread(self, move || {
crate::shell::activation::restore_minimized_without_activation(&window);
})?;
tauri::WebviewWindow::unminimize(self)
}
fn set_physical_position(&self, position: PhysicalPosition<i32>) -> tauri::Result<()> {
Expand All @@ -431,7 +444,11 @@ impl<R: tauri::Runtime> WindowGeometry<R> for tauri::Window<R> {
fn available_monitors(&self) -> tauri::Result<Vec<tauri::Monitor>> {
tauri::Window::available_monitors(self)
}
fn unminimize(&self) -> tauri::Result<()> {
fn restore_without_activation(&self) -> tauri::Result<()> {
let window = self.clone();
tauri::Window::run_on_main_thread(self, move || {
crate::shell::activation::restore_minimized_without_activation(&window);
})?;
tauri::Window::unminimize(self)
}
fn set_physical_position(&self, position: PhysicalPosition<i32>) -> tauri::Result<()> {
Expand Down
22 changes: 20 additions & 2 deletions apps/desktop-tauri/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,21 @@ fn should_hide_close_request(mode: SurfaceMode) -> bool {
///
/// Spawned because building the flyout window synchronously can deadlock on
/// Windows (see `shell::flyout_window::open_or_focus`).
///
/// Launch and relaunch are not clicks in CodexBar, so the panel only asks
/// Windows for the foreground (`Activation::IfAllowed`): a launch from Start
/// or Explorer gets focus, a login-time or background launch does not.
fn open_primary_window(app: &tauri::AppHandle, delay: Duration) {
let app = app.clone();
tauri::async_runtime::spawn(async move {
if !delay.is_zero() {
tokio::time::sleep(delay).await;
}
if let Err(error) = shell::flyout_window::open_or_focus(&app, None) {
if let Err(error) = shell::flyout_window::open_or_focus(
&app,
None,
shell::activation::Activation::IfAllowed,
) {
tracing::warn!(%error, "failed to open the tray panel window");
}
});
Expand Down Expand Up @@ -153,6 +161,16 @@ fn main() {
initial_state.provider_cache_updated_at = Some(std::time::Instant::now());
}

let context = tauri::generate_context!();
if is_proof_mode {
// Proof runs show surfaces for automation but never take focus.
shell::activation::suppress_all();
} else {
// If CodexBar is already running, the single-instance plugin hands
// this launch off to it; pass our foreground permission along first.
shell::activation::grant_foreground_to_running_instance(&context.config().identifier);
}

tauri::Builder::default()
.manage(Mutex::new(initial_state))
.plugin(shortcut_bridge::plugin())
Expand Down Expand Up @@ -415,7 +433,7 @@ fn main() {
_ => {}
}
})
.run(tauri::generate_context!())
.run(context)
.expect("failed to run CodexBar desktop shell");
}

Expand Down
16 changes: 13 additions & 3 deletions apps/desktop-tauri/src-tauri/src/proof_harness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
//!
//! In proof mode the shell immediately transitions to the requested surface
//! and suppresses blur-dismiss so the window stays visible for automated
//! screenshot capture.
//! screenshot capture. Surfaces are shown without being activated: no
//! `set_focus`, no foreground change (see `shell::activation::suppress_all`),
//! so a proof run never takes focus from the app the user is working in.
//!
//! `CODEXBAR_SEED_USAGE_JSON=<abs-path>` additionally seeds one synthetic,
//! bridge-shaped Codex [`ProviderUsageSnapshot`] into the provider cache at
Expand All @@ -30,6 +32,7 @@ use tauri::{AppHandle, Manager};

use crate::commands::{CostSnapshotBridge, ProviderUsageSnapshot, RateWindowSnapshot};
use crate::shell;
use crate::shell::activation::Activation;
use crate::state::AppState;
use crate::surface::SurfaceMode;
use crate::surface_target::{SurfaceTarget, is_supported_settings_tab};
Expand Down Expand Up @@ -116,7 +119,8 @@ pub fn activate(app: &AppHandle) {
if target == SurfaceMode::PopOut {
tracing::info!("proof-harness: opening the tray-panel flyout window");
// Called from the async setup task, so building the window is safe.
if let Err(err) = shell::flyout_window::open_or_focus(app, None) {
// Proof automation must never take focus from the user's app.
if let Err(err) = shell::flyout_window::open_or_focus(app, None, Activation::Never) {
tracing::error!("proof-harness: flyout open FAILED: {err}");
}
return;
Expand All @@ -134,7 +138,13 @@ pub fn activate(app: &AppHandle) {
position,
);

match shell::transition_to_target(app, target, config.surface_target(), position) {
match shell::transition_to_target(
app,
target,
config.surface_target(),
position,
Activation::Never,
) {
Ok(mode) => tracing::info!("proof-harness: transition succeeded → {mode:?}"),
Err(err) => tracing::error!("proof-harness: transition FAILED: {err}"),
}
Expand Down
Loading