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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## [Unreleased]

### Improved
- Keep saved Grok account usage tied to its login, renew expiring credentials, and coordinate optional account selection and reconnects with Orca.
- Show both Codex account quota windows, expand account lists by default, and place Grok refresh in its card header.
- Open desktop launches near the cursor while retaining the current foreground activation policy.

# Changelog

## [Windows] Unreleased
Expand Down Expand Up @@ -141,6 +148,7 @@ allowances and per-model spending, clearer account switching, and steadier
menu-bar layout.

### Added
- Codex accounts: show both five-hour and weekly usage windows with independent reset times in the tray, and include both limits in Settings.
- Cost history: add provider-scoped, display-only Codex and Claude quota-window projection primitives with exact reset boundaries, independent token/cost completeness, and backward-compatible local evidence handling.
- Provider charts: show recent Codex and Claude quota-window history with explicit account scope and estimated-boundary markers; historical data remains display-only.
- Grok: Settings and tray **Add account** flow matching Codex/Claude — isolated `grok login --oauth`, save current CLI login, switch, and remove without logging out the active session.
Expand Down
67 changes: 57 additions & 10 deletions apps/desktop-tauri/src-tauri/src/commands/grok_accounts.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use super::invalidate_account_usage;
use crate::state::AppState;
use codexbar::core::ProviderId;
use codexbar::core::{ProviderError, ProviderId};
use codexbar::providers::grok::GrokProvider;
use codexbar::providers::grok::accounts::{self, AccountManager, GrokAccount, GrokAccountUsage};
use codexbar::providers::grok::accounts::{
self, AccountManager, GrokAccount, GrokAccountUsage, GrokUsageStatus,
};
use std::sync::Mutex;
use tauri::Emitter;
use tauri::Manager;
Expand Down Expand Up @@ -91,25 +93,70 @@ pub async fn grok_account_remove(app: tauri::AppHandle, id: String) -> Result<()

#[tauri::command]
pub async fn grok_account_fetch(id: String) -> Result<GrokAccountUsage, String> {
let text = AccountManager::new()
.and_then(|m| m.auth_text_for(&id))
.map_err(|e| e.to_string())?;
GrokProvider::new()
.fetch_usage_from_auth_json(&text)
let manager = AccountManager::new().map_err(|error| error.to_string())?;
let usage = match manager.auth_text_for_usage(&id, false).await {
Ok(text) => match GrokProvider::new().fetch_usage_from_auth_json(&text).await {
Err(ProviderError::AuthRequired) => {
match manager.auth_text_for_usage(&id, true).await {
Ok(text) => GrokProvider::new().fetch_usage_from_auth_json(&text).await,
Err(error) => Err(error),
}
}
result => result,
},
Err(error) => Err(error),
};
Ok(match usage {
Ok(usage) => usage,
Err(ProviderError::AuthRequired) => {
GrokAccountUsage::unavailable(GrokUsageStatus::SignInRequired)
}
Err(_) => GrokAccountUsage::unavailable(GrokUsageStatus::Failed),
})
}

#[tauri::command]
pub async fn grok_account_reauthenticate(app: tauri::AppHandle, id: String) -> Result<(), String> {
let _mutation = MUTATION
.try_lock()
.map_err(|_| "A Grok account operation is already in progress.")?;
let _ = accounts::cleanup_abandoned_logins();
accounts::begin_login();
let login = tauri::async_runtime::spawn_blocking(accounts::login)
.await
.map_err(|e| e.to_string())
.map_err(|e| e.to_string())?
.map_err(|e| e.to_string())?;
let _credentials = accounts::CREDENTIAL_OPERATION.lock().await;
let result = AccountManager::new().and_then(|manager| manager.reauthenticate(&id, login));
drop(_credentials);
// Even a partial persistence failure can have refreshed the saved login.
refresh_after_grok_change(app)?;
result.map_err(|e| e.to_string())
}

#[tauri::command]
pub async fn grok_account_switch(app: tauri::AppHandle, id: String) -> Result<(), String> {
let _mutation = MUTATION
.try_lock()
.map_err(|_| "A Grok account operation is already in progress.")?;
AccountManager::new()
.map_err(|error| error.to_string())?
.auth_text_for_usage(&id, false)
.await
.map_err(|error| error.to_string())?;
let _credentials = accounts::CREDENTIAL_OPERATION.lock().await;
tauri::async_runtime::spawn_blocking(move || AccountManager::new()?.switch(&id))
let local_id = id.clone();
tauri::async_runtime::spawn_blocking(move || AccountManager::new()?.switch(&local_id))
.await
.map_err(|e| e.to_string())?
.map_err(|e| e.to_string())?;
drop(_credentials);
refresh_after_grok_change(app)
// Select in Orca only after the local switch succeeded, so a failed switch
// never leaves Orca and the Grok CLI on different accounts. The local switch
// stands even if Orca fails, so refresh before reporting that error.
let orca = accounts::select_orca_account(&id)
.await
.map_err(|error| error.to_string());
refresh_after_grok_change(app)?;
orca
}
9 changes: 6 additions & 3 deletions apps/desktop-tauri/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ fn should_hide_close_request(mode: SurfaceMode) -> bool {

/// 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.
/// relaunches use that same panel near the cursor, while tray clicks anchor it to the tray.
///
/// Spawned because building the flyout window synchronously can deadlock on
/// Windows (see `shell::flyout_window::open_or_focus`).
Expand All @@ -58,14 +58,16 @@ fn should_hide_close_request(mode: SurfaceMode) -> bool {
/// 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) {
// Capture the launch location before startup work or a delayed reveal moves the cursor.
let cursor = app.cursor_position().ok().map(|point| (point.x, point.y));
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(
if let Err(error) = shell::flyout_window::open_near_cursor(
&app,
None,
cursor,
shell::activation::Activation::IfAllowed,
) {
tracing::warn!(%error, "failed to open the tray panel window");
Expand Down Expand Up @@ -228,6 +230,7 @@ fn main() {
commands::claude_account_switch,
commands::grok_accounts_list,
commands::grok_account_add,
commands::grok_account_reauthenticate,
commands::grok_account_cancel_login,
commands::grok_account_save_current,
commands::grok_account_remove,
Expand Down
68 changes: 55 additions & 13 deletions apps/desktop-tauri/src-tauri/src/shell/flyout_window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ use crate::surface::SurfaceMode;
pub const FLYOUT_LABEL: &str = "flyout";

/// Geometry-store key for the flyout's remembered SIZE (position is never
/// stored — the flyout always re-anchors above the tray on open). Kept as
/// stored — each open chooses a tray or captured cursor anchor). Kept as
/// its own key (distinct from the legacy `SurfaceMode::TrayPanel::as_str()`
/// `"trayPanel"` key) — `geometry_store::load_size` migrates a pre-existing
/// `"trayPanel"` entry into this key on first read, so upgrading users keep
Expand Down Expand Up @@ -88,6 +88,29 @@ pub fn open_or_focus(
position: Option<(i32, i32)>,
activation: Activation,
) -> Result<(), String> {
open_with_anchor(app, position, None, activation)
}

/// Open a desktop launch near its captured cursor location, retaining that
/// anchor through subsequent content-driven resizes.
pub fn open_near_cursor(
app: &AppHandle,
cursor: Option<(f64, f64)>,
activation: Activation,
) -> Result<(), String> {
open_with_anchor(app, None, cursor, activation)
}

fn open_with_anchor(
app: &AppHandle,
position: Option<(i32, i32)>,
cursor: Option<(f64, f64)>,
activation: Activation,
) -> Result<(), String> {
app.state::<Mutex<AppState>>()
.lock()
.map_err(|e| e.to_string())?
.flyout_cursor_anchor = cursor;
let settings = Settings::load();
if let Some(window) = app.get_webview_window(FLYOUT_LABEL) {
apply_window_always_on_top(&window, settings.tray_panel_always_on_top)?;
Expand Down Expand Up @@ -150,9 +173,11 @@ pub fn open_or_focus(
// Settings window.
super::dwm::force_dark_caption_resizable(&win);

let target_position =
position.or_else(|| super::position::default_surface_position(app, SurfaceMode::TrayPanel));
if let Some((x, y)) = target_position {
if cursor.is_some() {
reanchor(app)?;
} else if let Some((x, y)) =
position.or_else(|| super::position::default_surface_position(app, SurfaceMode::TrayPanel))
{
// Best-effort initial placement; the frontend re-reveals the
// window, so a failed set_position here is non-fatal.
let _set_initial = win.set_position(PhysicalPosition::new(x, y));
Expand Down Expand Up @@ -322,7 +347,7 @@ pub fn handle_window_event(window: &tauri::Window, event: &tauri::WindowEvent) -
// drag-resizes call `set_flyout_size`, auto-fit resizes never do) —
// mirrors `shell::position::remember_current_geometry_if_eligible`
// skipping TrayPanel for the same reason on the old shared window.
// Position is never persisted (always re-anchored above the tray).
// Position is never persisted; resizing follows this open's selected anchor.
tauri::WindowEvent::Moved(_) | tauri::WindowEvent::Resized(_) => true,
tauri::WindowEvent::CloseRequested { api, .. } => {
// Hide-not-close on a native close request (Alt+F4-equivalent from
Expand All @@ -337,8 +362,8 @@ pub fn handle_window_event(window: &tauri::Window, event: &tauri::WindowEvent) -
}
}

/// Reposition the flyout so its bottom-right corner stays anchored to the
/// system-tray area, using the window's CURRENT logical size (after a
/// Reposition the flyout at its captured cursor or system-tray anchor,
/// using the window's CURRENT logical size (after a
/// frontend-driven resize). Canonical anchor-math implementation for the
/// flyout window; the `reanchor_tray_panel` Tauri command
/// (`commands/system.rs`) is a thin retarget onto this function.
Expand All @@ -357,18 +382,28 @@ pub fn reanchor(app: &AppHandle) -> Result<(), String> {
height: (outer.height as f64 / scale).round() as u32,
};

let anchor = app
let (anchor, cursor) = app
.try_state::<Mutex<AppState>>()
.and_then(|state| state.lock().ok()?.tray_anchor);
.and_then(|state| {
let state = state.lock().ok()?;
Some((state.tray_anchor, state.flyout_cursor_anchor))
})
.unwrap_or_default();
let monitors = window.available_monitors().unwrap_or_default();
let monitor = anchor
.and_then(|anchor| crate::shell::geometry::monitor_for_anchor(&monitors, anchor))
.cloned()
let monitor = cursor
.and_then(|(x, y)| app.monitor_from_point(x, y).ok().flatten())
.or_else(|| {
anchor
.and_then(|anchor| crate::shell::geometry::monitor_for_anchor(&monitors, anchor))
.cloned()
})
.or_else(|| window.current_monitor().ok().flatten())
.or_else(|| window.primary_monitor().ok().flatten())
.ok_or_else(|| "no monitor".to_string())?;

let work_area = crate::shell::geometry::monitor_work_area_rect(&monitor);
// The target monitor can differ from the window's previous monitor.
let scale = monitor.scale_factor().max(1.0);
let monitor_bounds = Rect {
x: monitor.position().x,
y: monitor.position().y,
Expand All @@ -377,7 +412,14 @@ pub fn reanchor(app: &AppHandle) -> Result<(), String> {
};

let (x, y) = {
if let Some(a) = anchor {
if let Some(cursor) = cursor {
crate::window_positioner::calculate_cursor_position(
cursor,
&work_area,
&panel_size,
scale,
)
} else if let Some(a) = anchor {
crate::window_positioner::calculate_panel_position(
&Rect {
x: a.x,
Expand Down
3 changes: 3 additions & 0 deletions apps/desktop-tauri/src-tauri/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ pub struct AppState {
pub surface_machine: SurfaceStateMachine,
pub current_target: SurfaceTarget,
pub tray_anchor: Option<TrayAnchor>,
/// Cursor location captured for a desktop launch; cleared by tray/other opens.
pub flyout_cursor_anchor: Option<(f64, f64)>,
pub provider_cache: Vec<ProviderUsageSnapshot>,
pub claude_account_usage: HashMap<String, crate::commands::ClaudeAccountUsageState>,
pub codex_account_needs_authentication: HashMap<uuid::Uuid, bool>,
Expand Down Expand Up @@ -200,6 +202,7 @@ impl AppState {
surface_machine: SurfaceStateMachine::new(),
current_target: SurfaceTarget::Summary,
tray_anchor: None,
flyout_cursor_anchor: None,
provider_cache: Vec::new(),
claude_account_usage: HashMap::new(),
codex_account_needs_authentication: HashMap::new(),
Expand Down
72 changes: 71 additions & 1 deletion apps/desktop-tauri/src-tauri/src/window_positioner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,27 @@ pub fn calculate_panel_position(
}
}

/// Position for shortcut-triggered opening: 22 % from left, vertically centred.
/// Position beside a desktop launch's physical cursor point, kept in the work area.
pub fn calculate_cursor_position(
cursor: (f64, f64),
work_area: &Rect,
panel_size: &PanelSize,
scale_factor: f64,
) -> (i32, i32) {
#[expect(
clippy::cast_possible_truncation,
reason = "physical cursor coordinates fit i32"
)]
let x = (cursor.0.round() as i32).saturating_add(GAP);
#[expect(
clippy::cast_possible_truncation,
reason = "physical cursor coordinates fit i32"
)]
let y = (cursor.1.round() as i32).saturating_add(GAP);
clamp_to_work_area(x, y, work_area, panel_size, scale_factor)
}

/// Legacy shortcut placement: 22 % from left, vertically centred.
pub fn calculate_shortcut_position(
monitor_rect: &Rect,
panel_size: &PanelSize,
Expand Down Expand Up @@ -477,6 +497,56 @@ mod tests {

// --- shortcut-anchor tests ---

#[test]
fn desktop_open_stays_beside_cursor_after_content_grows() {
let monitor = hd_monitor();
let cursor = (50.0, 100.0);
assert_eq!(
calculate_cursor_position(cursor, &monitor, &panel(), 1.0),
(58, 108)
);
let taller = PanelSize {
width: 360,
height: 800,
};
assert_eq!(
calculate_cursor_position(cursor, &monitor, &taller, 1.0),
(58, 108)
);
}

#[test]
fn cursor_open_clamps_on_high_dpi_monitor_with_negative_origin() {
let work_area = Rect {
x: -2560,
y: 0,
width: 2560,
height: 1400,
};
let panel = PanelSize {
width: 400,
height: 600,
};
assert_eq!(
calculate_cursor_position((-10.0, 1390.0), &work_area, &panel, 2.0),
(-808, 192)
);
}

#[test]
fn oversized_cursor_panel_keeps_its_top_left_visible() {
let work_area = Rect {
x: 1920,
y: -100,
width: 500,
height: 400,
};
assert_eq!(
calculate_cursor_position((2200.0, 250.0), &work_area, &panel(), 2.0),
(1928, -92)
);
}

#[test]
fn shortcut_position_22_pct_from_left() {
let monitor = hd_monitor();
Expand Down
Loading