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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
- Cargo workspace (root `Cargo.toml`): members `rust`, `apps/desktop-tauri/src-tauri`; **default-member** is the Tauri crate.
- Path dependency: `codexbar-desktop-tauri` → `codexbar = { path = "../../../rust" }`.
- Frontend: React 18 + Vite in `apps/desktop-tauri/src/`. Typed invoke bridge in `src/lib/tauri.ts`; DTOs in `src/types/bridge.ts`.
- Surfaces: the hidden `main` webview routes by window label / surface mode — TrayPanel, PopOut, Settings, FloatBar. Settings, float bar, and flyout use detached windows where needed.
- Surfaces: the hidden `main` webview routes by window label / surface mode — TrayPanel, Settings, FloatBar. Settings, float bar, and the tray-panel flyout use detached windows. The flyout's TrayPanel is the only dashboard layout; the legacy PopOut layout is retired (`SurfaceMode::PopOut` remains only as a data key).
- **Provider refresh**: `codexbar::core::instantiate_provider` (`rust/src/core/provider_factory.rs`) → `Provider::fetch_usage` → shell `commands/providers.rs` (semaphore + timeout) → `AppState.provider_cache` → events → React `useProviders`.
- **Settings**: `%config%/CodexBar/settings.json` via `Settings::load` / `save` and `secure_file` (DPAPI-capable on Windows). Frontend `updateSettings` patch → save → `codexbar:settings-updated` / float-bar config events.
- **Tray**: `tray_bridge` + `tray_menu`. Icon pixels from shared `codexbar::tray::{render_bar_icon_rgba, render_percent_icon_rgba}`.
Expand Down
18 changes: 11 additions & 7 deletions apps/desktop-tauri/src-tauri/src/commands/surface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,11 @@ pub async fn open_settings_window(app: tauri::AppHandle, tab: String) -> Result<
crate::shell::settings_window::open_or_focus(&app, &tab)
}

/// Open (or focus) the detached flyout ("Pop Out Dashboard") window.
///
/// Used by `PopOutPanel`'s "back to tray" action, which previously called
/// `set_surface_mode("trayPanel", ...)` on the shared window — now that the
/// flyout is its own window, that action opens it directly instead. Same
/// `async` requirement as `open_settings_window`: `WebviewWindowBuilder::build`
/// deadlocks inside synchronous Tauri commands on Windows.
/// Open (or focus) the detached flyout ("Pop Out Dashboard") window, the
/// only dashboard layout. Used by the frontend global-shortcut fallback.
/// Same `async` requirement as `open_settings_window`:
/// `WebviewWindowBuilder::build` deadlocks inside synchronous Tauri commands
/// on Windows.
#[tauri::command]
pub async fn open_flyout_window(app: tauri::AppHandle) -> Result<(), String> {
crate::shell::flyout_window::open_or_focus(&app, None)
Expand Down Expand Up @@ -152,6 +150,12 @@ pub(crate) fn validate_surface_target(
return Err("set_surface_mode only supports visible surfaces".into());
}

// The legacy PopOut layout on `main` is retired; the dashboard is the
// tray-panel flyout, opened with `open_flyout_window`.
if mode == SurfaceMode::PopOut {
return Err("the popOut surface is retired; use open_flyout_window".into());
}

if target.mode() != mode {
return Err(format!(
"surface target '{}' is not valid for mode '{}'",
Expand Down
13 changes: 13 additions & 0 deletions apps/desktop-tauri/src-tauri/src/commands/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,19 @@ fn validate_surface_target_rejects_hidden_mode() {
assert!(error.contains("only supports visible surfaces"));
}

#[test]
fn validate_surface_target_rejects_retired_popout_mode() {
for target in [
SurfaceTarget::Dashboard,
SurfaceTarget::Provider {
provider_id: "codex".into(),
},
] {
let error = validate_surface_target(SurfaceMode::PopOut, target).unwrap_err();
assert!(error.contains("popOut surface is retired"));
}
}

#[test]
fn external_url_validation_allows_only_http_urls() {
assert_eq!(
Expand Down
50 changes: 21 additions & 29 deletions apps/desktop-tauri/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ use std::sync::Mutex;

use state::AppState;
use surface::SurfaceMode;
use surface_target::SurfaceTarget;
use tauri::Manager;

const PROOF_ACTIVATION_DELAY: Duration = Duration::from_millis(0);
Expand All @@ -46,12 +45,22 @@ fn should_hide_close_request(mode: SurfaceMode) -> bool {
)
}

fn primary_window_request() -> shell::ShellTransitionRequest {
shell::ShellTransitionRequest {
mode: SurfaceMode::PopOut,
target: SurfaceTarget::Dashboard,
position: None,
}
/// Open the primary window: the tray-panel flyout, the only dashboard
/// layout. The legacy PopOut layout on `main` is retired, so launches and
/// relaunches land on the same panel as a tray left-click.
///
/// Spawned because building the flyout window synchronously can deadlock on
/// Windows (see `shell::flyout_window::open_or_focus`).
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) {
tracing::warn!(%error, "failed to open the tray panel window");
}
});
}

fn should_open_primary_window_from_args<I, S>(args: I) -> bool
Expand Down Expand Up @@ -150,9 +159,7 @@ fn main() {
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
if should_reopen_primary_window_from_instance_args(args.iter().skip(1)) {
let request = primary_window_request();
let _ =
shell::reopen_to_target(app, request.mode, request.target, request.position);
open_primary_window(app, Duration::ZERO);
}
}))
.invoke_handler(tauri::generate_handler![
Expand Down Expand Up @@ -302,17 +309,10 @@ fn main() {
proof_harness::activate(&app_handle);
});
} else if launch.open_primary_window_at_start {
let app = app.handle().clone();
tauri::async_runtime::spawn(async move {
tokio::time::sleep(VISIBLE_START_ACTIVATION_DELAY).await;
let request = primary_window_request();
let _ = shell::reopen_to_target(
&app,
request.mode,
request.target,
request.position,
);
});
if launch.suppress_blur_dismiss {
shell::flyout_window::keep_open_on_blur();
}
open_primary_window(app.handle(), VISIBLE_START_ACTIVATION_DELAY);
}

Ok(())
Expand Down Expand Up @@ -435,14 +435,6 @@ mod tests {
assert!(!should_hide_close_request(SurfaceMode::Hidden));
}

#[test]
fn primary_window_request_targets_popout_dashboard() {
let request = primary_window_request();
assert_eq!(request.mode, SurfaceMode::PopOut);
assert_eq!(request.target, SurfaceTarget::Dashboard);
assert_eq!(request.position, None);
}

#[test]
fn menubar_launch_arg_opens_primary_window() {
assert!(should_open_primary_window_from_args(["menubar"]));
Expand Down
60 changes: 25 additions & 35 deletions apps/desktop-tauri/src-tauri/src/proof_harness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
//! specifies a target surface and optional settings tab to display on
//! startup, e.g.:
//!
//! - `trayPanel` — show the tray panel
//! - `popOut` — show the pop-out dashboard
//! - `popOut:provider:codex` — show a provider pop-out
//! - `trayPanel` — show the tray panel on the `main` window
//! - `popOut` — open the tray-panel flyout window, the same
//! window tray left-click and "Pop Out Dashboard" open (the legacy
//! PopOut layout is retired, so `popOut:<target>` payloads are rejected)
//! - `settings` — show settings (General tab)
//! - `settings:menuBar` — show settings on the Menu Bar tab
//! - `settings:usageSpend` — show settings on the Usage & Spend tab
Expand All @@ -31,7 +32,7 @@ use crate::commands::{CostSnapshotBridge, ProviderUsageSnapshot, RateWindowSnaps
use crate::shell;
use crate::state::AppState;
use crate::surface::SurfaceMode;
use crate::surface_target::{SurfaceTarget, is_supported_provider_id, is_supported_settings_tab};
use crate::surface_target::{SurfaceTarget, is_supported_settings_tab};

/// Proof configuration parsed from `CODEXBAR_PROOF_MODE`.
#[derive(Debug, Clone, Serialize)]
Expand All @@ -41,8 +42,8 @@ pub struct ProofConfig {
pub target_surface: String,
/// Optional settings tab id (e.g. `"menuBar"`, `"usageSpend"`).
pub settings_tab: Option<String>,
/// Optional target payload for richer proof routing, such as
/// `"provider:codex"` for pop-out provider views.
/// Optional target payload for richer proof routing (currently only the
/// settings tab id).
pub target_payload: Option<String>,
}

Expand Down Expand Up @@ -90,12 +91,7 @@ impl ProofConfig {
pub fn surface_target(&self) -> SurfaceTarget {
match self.surface_mode() {
SurfaceMode::Hidden | SurfaceMode::TrayPanel => SurfaceTarget::Summary,
SurfaceMode::PopOut => self
.target_payload
.as_deref()
.and_then(SurfaceTarget::parse)
.filter(|target| target.mode() == SurfaceMode::PopOut)
.unwrap_or(SurfaceTarget::Dashboard),
SurfaceMode::PopOut => SurfaceTarget::Dashboard,
SurfaceMode::Settings => SurfaceTarget::Settings {
tab: self
.settings_tab
Expand All @@ -117,6 +113,14 @@ pub fn activate(app: &AppHandle) {

let Some(config) = config else { return };
let target = config.surface_mode();
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) {
tracing::error!("proof-harness: flyout open FAILED: {err}");
}
return;
}
let position = match target {
// Detached surfaces are larger than tray panels. Let their normal
// positioning paths center/clamp them instead of reusing tray coords.
Expand Down Expand Up @@ -352,17 +356,7 @@ fn proof_payload_is_supported(surface_mode: SurfaceMode, payload: Option<&str>)
(SurfaceMode::Settings, None) => true,
(SurfaceMode::Settings, Some(tab)) => is_supported_settings_tab(tab),
(SurfaceMode::PopOut, None) => true,
(SurfaceMode::PopOut, Some(raw_target)) => {
let Some(target) = SurfaceTarget::parse(raw_target) else {
return false;
};

match target {
SurfaceTarget::Dashboard => true,
SurfaceTarget::Provider { provider_id } => is_supported_provider_id(&provider_id),
_ => false,
}
}
(SurfaceMode::PopOut, Some(_)) => false,
}
}

Expand Down Expand Up @@ -455,18 +449,14 @@ mod tests {
}

#[test]
fn parse_provider_popout_proof_target() {
with_proof_mode_env(Some("popOut:provider:codex"), || {
let cfg = ProofConfig::from_env().unwrap();
assert_eq!(cfg.target_surface, "popOut");
assert_eq!(cfg.target_payload.as_deref(), Some("provider:codex"));
assert_eq!(
cfg.surface_target(),
SurfaceTarget::Provider {
provider_id: "codex".into()
}
);
});
fn retired_popout_provider_proof_targets_are_rejected() {
// The legacy PopOut layout (with provider deep links) is retired;
// `popOut` only opens the tray-panel flyout.
for raw in ["popOut:provider:codex", "popOut:dashboard"] {
with_proof_mode_env(Some(raw), || {
assert!(ProofConfig::from_env().is_none(), "{raw}");
});
}
}

#[test]
Expand Down
20 changes: 16 additions & 4 deletions apps/desktop-tauri/src-tauri/src/shell/flyout_window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
//! panel with optional always-on-top behavior that auto-hides on click-outside.
//!
//! Runs as an auxiliary Tauri window labeled `flyout`, independent of the
//! `main` window's surface state machine — it coexists with "Show Window"
//! (`SurfaceMode::PopOut`, which stays on `main`) instead of being a
//! mutually-exclusive state of the same window.
//! `main` window's surface state machine. It is the only dashboard layout:
//! tray left-click, "Pop Out Dashboard", the global shortcut, app launch and
//! single-instance relaunch all open it. The legacy PopOut layout on `main`
//! is retired.
//!
//! Structurally modeled on `crate::floatbar` (self-contained module owning
//! its window + a `handle_window_event` hook dispatched from `main.rs`
Expand All @@ -13,6 +14,7 @@
//! pass, `WebviewUrl::App` with a `?window=` query marker).

use std::sync::Mutex;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant};

use codexbar::settings::Settings;
Expand Down Expand Up @@ -42,6 +44,15 @@ const BLUR_DISMISS_CLICK_WINDOW: Duration = Duration::from_millis(250);
/// `was_tray_panel_recently_shown` guard for the old shared window.
const RECENTLY_SHOWN_GRACE: Duration = Duration::from_millis(500);

/// Set once at startup by `CODEXBAR_START_VISIBLE`: keeps the flyout open
/// when it loses focus, for automation flows that need it to stay visible.
static KEEP_OPEN_ON_BLUR: AtomicBool = AtomicBool::new(false);

/// Keep the flyout open on focus loss for the rest of this process.
pub fn keep_open_on_blur() {
KEEP_OPEN_ON_BLUR.store(true, Ordering::Relaxed);
}

/// Read the remembered flyout size, if any (migrating a legacy
/// `"trayPanel"`-keyed size on first read — see `geometry_store::load_size`).
pub fn stored_size() -> Option<(u32, u32)> {
Expand Down Expand Up @@ -259,7 +270,8 @@ pub fn handle_window_event(window: &tauri::Window, event: &tauri::WindowEvent) -
let _ = window.set_always_on_top(true);
return true;
}
if crate::proof_harness::is_proof_mode(app) {
if crate::proof_harness::is_proof_mode(app) || KEEP_OPEN_ON_BLUR.load(Ordering::Relaxed)
{
return true;
}
let Some(st) = app.try_state::<Mutex<AppState>>() else {
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop-tauri/src-tauri/src/shell/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ mod tests;

pub(crate) use position::inferred_tray_panel_position_for_monitor_size;
pub use position::{remember_current_geometry_if_eligible, tray_panel_position};
pub use transition::{reopen_to_target, transition_to_target};
pub use transition::transition_to_target;
pub use window::hide_to_tray_if_current;

#[derive(Debug, Clone, PartialEq, Eq)]
Expand Down
17 changes: 0 additions & 17 deletions apps/desktop-tauri/src-tauri/src/shell/transition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,23 +92,6 @@ pub fn transition_to_target(
)
}

pub fn reopen_to_target(
app: &AppHandle,
mode: SurfaceMode,
target: SurfaceTarget,
position: Option<(i32, i32)>,
) -> Result<SurfaceMode, String> {
apply_transition_request_with_strategy(
app,
ShellTransitionRequest {
mode,
target,
position,
},
true,
)
}

fn apply_transition_request_with_strategy(
app: &AppHandle,
request: ShellTransitionRequest,
Expand Down
17 changes: 1 addition & 16 deletions apps/desktop-tauri/src-tauri/src/surface_target.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
use serde::{Deserialize, Serialize};

use codexbar::core::ProviderId;

use crate::surface::SurfaceMode;

// Must mirror the frontend `SettingsTabId` union
Expand Down Expand Up @@ -83,19 +81,13 @@ impl SurfaceTarget {
}
}

pub fn is_supported_provider_id(provider_id: &str) -> bool {
ProviderId::all()
.iter()
.any(|provider| provider.cli_name() == provider_id)
}

pub fn is_supported_settings_tab(tab: &str) -> bool {
SETTINGS_TAB_IDS.contains(&tab)
}

#[cfg(test)]
mod tests {
use super::{SurfaceTarget, is_supported_provider_id, is_supported_settings_tab};
use super::{SurfaceTarget, is_supported_settings_tab};
use serde_json::json;

#[test]
Expand Down Expand Up @@ -169,13 +161,6 @@ mod tests {
);
}

#[test]
fn supported_provider_ids_match_catalog() {
assert!(is_supported_provider_id("codex"));
assert!(is_supported_provider_id("bedrock"));
assert!(!is_supported_provider_id("not-a-provider"));
}

#[test]
fn supported_settings_tabs_match_shell_tabs() {
assert!(is_supported_settings_tab("menuBar"));
Expand Down
Loading