Skip to content

Tray-anchored flyout: resize, drag-reorder, zoom (+ Claude OAuth auto-refresh) - #120

Merged
Finesssee merged 4 commits into
nesszer:mainfrom
darknight105:feat/tray-flyout-and-oauth
Jul 4, 2026
Merged

Tray-anchored flyout: resize, drag-reorder, zoom (+ Claude OAuth auto-refresh)#120
Finesssee merged 4 commits into
nesszer:mainfrom
darknight105:feat/tray-flyout-and-oauth

Conversation

@darknight105

Copy link
Copy Markdown

Summary

Reworks the tray experience and fixes several Windows-specific bugs found along the way. Rebased onto current main (0.38.0); the diff is the two commits below.

  1. Tray flyout suite — "Pop Out Dashboard" becomes a tray-anchored, resizable flyout with drag-reorder and a zoom slider.
  2. Claude OAuth auto-refresh — fixes the recurring "Claude sign-in expired".

Tray flyout (commit 1)

  • Anchored popover above the tray icon (SurfaceMode::TrayPanel): always-on-top, blur-to-dismiss, skip-taskbar; left-clicking the tray toggles it. Previously "Pop Out Dashboard" and "Show Window" both mapped to the same PopOut window and collided.
  • Frontend-owned sizing — auto-fits to content until the user drags an edge, then remembers the manual size (persisted via set_flyout_size).
  • DPI-safe — all sizing in Tauri physical units (innerSize / PhysicalSize) behind an in-flight guard. Fixes a grow-on-every-open bug on 150% displays where logical / physical / Win32 scale factors disagreed.
  • Resize grips — a borderless WebView2 can't drive native edge-resize, so top/left/corner grips call startResizeDragging. Root cause of "grips appear but never work": the core:window:allow-start-resize-dragging capability was never granted, so every call was denied by the ACL and swallowed by a .catch. Now granted (plus core:window:default for innerSize/scaleFactor, which the sizing bookkeeping depends on). ACL-gated calls no longer swallow errors silently.
  • Gesture blur-guard — the Win32 modal resize loop and OLE DoDragDrop each fire a transient Focused(false); without a guard the flyout hid the instant you pressed a grip or started a drag. A gesture-scoped guard suppresses blur-dismiss during the gesture, re-armed on refocus or after a 15s timeout.
  • Drag-reorderdragDropEnabled: false on the main window so wry stops replacing Chromium's native OLE drop target (that hijack silently killed all in-page HTML5 drag-and-drop; dragover/drop never fired). Order persists via reorder_providers, which now emits settings-changed so the detail cards follow the new order.
  • Zoom — a Windows-volume-style slider in the footer applies CSS zoom to the surface (trayScalePercent, 100–200%, live preview + debounced persist), re-measured live so auto-fit tracks it; the footer pins to the bottom when user-sized; provider cards lay out side-by-side past 640px.
  • Off-screen fix — with no remembered geometry and no tray anchor, prefer the primary monitor so the flyout/popout can't open on a hidden secondary display.

Claude OAuth auto-refresh (commit 2)

The Claude Code OAuth access token lives only ~8h. CodexBar reads it but never uses the refresh token, so usage requests fail with "sign-in expired" until the CLI happens to refresh it. This refreshes proactively when the token is near expiry, atomically writing back the claudeAiOauth block (preserving mcpOAuth), with an in-memory fallback if the write fails and a re-read guard against a concurrent CLI refresh.

Maintainer heads-up (please read before merging this commit): the refresh works by calling Anthropic's OAuth token endpoint with the Claude Code client_id — constants that ship inside the official client, used here only to renew the user's own token. It's the same "reuse the user's existing Claude credentials" category the provider already relies on to read usage, extended to keep the session alive — but it depends on an endpoint Anthropic hasn't publicly documented, so it could break if they change it. If you'd rather not carry that, commit 1 (the tray flyout) stands entirely on its own — say the word and I'll drop commit 2 or move it to its own PR.

Verification

  • tsc --noEmit: clean
  • frontend (vitest): 121 passed
  • backend (cargo test -p codexbar-desktop-tauri): 269 passed
  • tauri build --no-bundle: success (release, ~1m)
  • Deployed and smoke-tested on Windows 11 @ 150% across dual monitors.

🤖 Generated with Claude Code

@Finesssee

Copy link
Copy Markdown
Collaborator

Thanks for the PR, I will review this.

@Finesssee

Copy link
Copy Markdown
Collaborator

Thermo-nuclear code quality review

Requesting changes on structure, not behavior.

  1. rust/src/providers/claude/oauth.rs grows from ~811 to ~1138 lines and crosses the 1k-line boundary. It now mixes usage fetching, refresh HTTP calls, refreshed-token caching, credential persistence, JSON patching, and tests. That is too much ownership in one file. Please split refresh/persist/cache logic into focused modules (for example oauth::refresh and oauth::credentials_store) and keep the fetcher focused on usage fetching.

  2. The refreshed credential cache is a process-global Option<ClaudeOAuthCredentials> with no source/account key. That makes credential ownership implicit and brittle: an env token, file credential, keyring credential, or future account switch can all interact with the same hidden slot. Key the cache by credential source/account identity, or move it behind the credential-store abstraction so the boundary is explicit.

  3. set_flyout_size persists a size-only concept by fabricating StoredGeometry { x: 0, y: 0, ... }. That leaks geometry-store implementation details into the surface command layer and blurs size vs position semantics. Please add a StoredSize/save_size helper for TrayPanel instead of writing fake coordinates.

  4. TrayPanel sizing policy is now split across backend mode != TrayPanel branching, frontend layout constants, and surface mode defaults. This adds another special-case branch to an already busy window path. Can we encode TrayPanel as an explicit frontend-owned sizing policy rather than relying on negative checks in shared window layout code?

darknight105 and others added 4 commits July 4, 2026 03:06
Rework "Pop Out Dashboard" into a tray-anchored flyout (SurfaceMode::TrayPanel)
above the taskbar icon, so it no longer collides with the PopOut window
("Show Window").

- Anchored popover: always-on-top, blur-dismiss, skip-taskbar; left-click on the
  tray toggles it. The frontend owns sizing: auto-fit to content until the user
  drags an edge, then remember the manual size (persisted via set_flyout_size).
- DPI-safe sizing: use Tauri physical units end-to-end (innerSize + PhysicalSize)
  behind an in-flight guard, fixing a scale-factor growth bug on 150% displays.
- Resize grips: a borderless WebView2 can't drive native edge-resize, so expose
  top/left/corner grips that call startResizeDragging. This needs the
  core:window:allow-start-resize-dragging capability, now granted.
- Keep the flyout open during resize/drag: the Win32 modal size loop and OLE
  DoDragDrop each fire a transient Focused(false); a gesture guard suppresses
  blur-dismiss, re-armed on refocus or after a 15s timeout.
- Provider drag-reorder: set dragDropEnabled=false so wry stops hijacking
  Chromium's native drop target, then persist order via reorder_providers and
  emit settings-changed so the detail cards follow.
- Zoom: a Windows-volume-style slider in the footer applies CSS zoom to the
  surface (trayScalePercent 100-200%) and re-measures live; the footer pins to
  the bottom when user-sized, and provider cards go side-by-side past 640px.
- Positioning: with no remembered geometry and no tray anchor, prefer the
  primary monitor so the flyout/popout can't open off-screen on a secondary
  display.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Claude Code OAuth access token lives only ~8h. CodexBar read it but never
used the refresh token, so usage requests failed with "sign-in expired" until
the CLI happened to refresh it. Refresh proactively against the token endpoint
when the token is near expiry, atomically writing back the claudeAiOauth block
(preserving mcpOAuth) with an in-memory fallback and a re-read guard against a
concurrent CLI refresh.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…Show Window

Addresses review items nesszer#3 and nesszer#4 (structure, not behavior).

The "Pop Out Dashboard" flyout and "Show Window" were two mutually-exclusive
states of the single main window, so opening one closed the other. Give the
flyout its own OS window (label "flyout", modeled on the detached settings/
floatbar windows) so the two coexist; the flyout keeps blur-to-dismiss.

StoredSize map (load_size/save_size) in geometry_store backs set_flyout_size, so
the surface command layer no longer leaks geometry-store coordinate details or
blurs size-vs-position semantics.

window rather than a "mode != TrayPanel" negative special-case threaded through
shared window-layout code.

- New shell/flyout_window.rs: builder with disable_drag_drop_handler() (required
  for the provider-grid HTML5 drag-reorder), open/toggle/hide/is_open, and its
  own Focused-blur handler porting all four anti-flicker guards.
- Tray left-click and "Pop Out Dashboard" open the flyout window; "Show Window"
  stays on the main window. Both can now be open at once.
…che by source

Addresses review items #1 and nesszer#2 (structure/ownership, not behavior).

#1 — `providers/claude/oauth.rs` had grown to ~1138 lines mixing usage fetching,
refresh HTTP, refreshed-token caching, credential persistence, JSON patching and
tests. Split into a directory module:
  - oauth/mod.rs             — ClaudeOAuthFetcher: usage fetching + orchestration
                               (fetch/fetch_usage/rate-limit/snapshot) + types.
  - oauth/refresh.rs         — the OAuth token refresh HTTP call + its response type.
  - oauth/credentials_store.rs — credential loading (env/file/keyring), persistence,
                               and the refreshed-credentials cache.
The public surface is unchanged: `pub use oauth::ClaudeOAuthFetcher` still resolves
(directory module), and ClaudeOAuthFetcher's external API (new/fetch/
fetch_with_access_token) is untouched.

nesszer#2 — the refreshed-credential cache was a single process-global
`Option<ClaudeOAuthCredentials>` shared across every credential source. Because
`load_credentials` tries env→file→keyring and env tokens have no `expires_at`, a
file-refreshed token could shadow a freshly-read environment token (the
`(Some(_), None) => true` freshness arm). The cache is now
`HashMap<CredentialSource, _>` keyed by an explicit
`CredentialSource { Environment, File(path), Keyring(account) }`; `load_credentials`
returns the source and `ensure_fresh_credentials` threads it through the cache
lookup/store. Regression test `env_source_not_shadowed_by_file_cache` covers it.

Behavior-preserving. cargo check clean; core `codexbar` 490 tests, tauri 280 tests,
frontend 127 tests all green.
@darknight105
darknight105 force-pushed the feat/tray-flyout-and-oauth branch from 844caf3 to 135a064 Compare July 3, 2026 20:24
@darknight105

Copy link
Copy Markdown
Author

Thanks for the thorough review. Rebased onto current main and addressed all four structural points. The branch is now four commits (the two original + two structural follow-ups) and mergeable.

#1 — split oauth.rs. The 1138-line file is now a directory module providers/claude/oauth/:

  • mod.rsClaudeOAuthFetcher focused on usage fetching + the refresh/adopt orchestration, plus the usage types and rate-limit helpers.
  • refresh.rs — the OAuth token refresh HTTP call and its response type.
  • credentials_store.rs — credential loading (env/file/keyring), persistence + JSON patching, and the refreshed-credentials cache.

Public surface is unchanged: pub use oauth::ClaudeOAuthFetcher still resolves against the directory module and the fetcher's external API is untouched.

#2 — key the refreshed-credential cache. Replaced the process-global Option<ClaudeOAuthCredentials> with a HashMap<CredentialSource, _>, where CredentialSource is an explicit { Environment, File(PathBuf), Keyring(account) }. load_credentials() now returns the source and ensure_fresh_credentials threads it through the cache lookup/store. This also fixes a latent bug: since load_credentials tries env→file→keyring and env tokens have no expires_at, the old shared slot let a file-refreshed token shadow a freshly-read environment token via the (Some(_), None) => true freshness arm. Regression test env_source_not_shadowed_by_file_cache covers it.

#3set_flyout_size fabricating StoredGeometry{x:0,y:0}. Added a size-only StoredSize type with load_size/save_size in geometry_store; set_flyout_size now persists through that, so the surface command layer no longer writes fake coordinates or blurs size-vs-position semantics.

#4 — TrayPanel sizing policy. The flyout now lives in its own dedicated OS window (label flyout, modeled on the detached settings/floatbar windows) instead of being a TrayPanel mode of the main window. Sizing is a frontend-owned policy on that window, so the mode != TrayPanel negative special-case is gone from the shared window-layout path. A side benefit: "Pop Out Dashboard" and "Show Window" are now independent windows and can coexist, instead of being mutually-exclusive states of one window.

Rebase note. Two of the upstream commits touched the same code paths. In particular, folding the separate-window flyout on top of #121 (vertical-taskbar tray placement) required routing the flyout's no-anchor reanchor fallback through inferred_tray_panel_position_for_monitor_size rather than the old bottom-right calculate_popout_position, so the taskbar-side inference is preserved for the flyout window too. Also dropped a tray_left_click_target helper (and its test) that the separate-window left-click path made dead.

Verification (Windows 11):

  • tsc --noEmit: clean
  • frontend (vitest): 127 passed
  • core crate (cargo test -p codexbar): 490 passed
  • backend (cargo test -p codexbar-desktop-tauri): 280 passed
  • cargo check (release + --tests): no warnings
  • tauri build --no-bundle: release build succeeds

@Finesssee
Finesssee merged commit c39232a into nesszer:main Jul 4, 2026
2 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants