);
}
-type ResizeDirection = Parameters[0];
-
-/** Every edge and corner, so a flyout the user moved away from the tray can
- * be resized from whichever side faces open screen. */
-const RESIZE_GRIPS: ReadonlyArray<{ edge: string; direction: ResizeDirection }> = [
- { edge: "top", direction: "North" },
- { edge: "bottom", direction: "South" },
- { edge: "left", direction: "West" },
- { edge: "right", direction: "East" },
- { edge: "topleft", direction: "NorthWest" },
- { edge: "topright", direction: "NorthEast" },
- { edge: "bottomleft", direction: "SouthWest" },
- { edge: "bottomright", direction: "SouthEast" },
-];
-
-/**
- * Window handles for the borderless flyout: a move strip along the top and
- * invisible resize grips on every edge and corner. The native frame left
- * around the borderless WebView2 is only a few pixels wide, so both are driven
- * explicitly with `startDragging` / `startResizeDragging`. Those calls enter a
- * Win32 modal move/size loop which transiently steals focus from the WebView2
- * child — Windows fires a spurious `Focused(false)` the instant the press
- * starts. The backend keeps the flyout open on a blur while a mouse button is
- * held on it, so no gesture guard is armed here; arming one would also keep
- * the next genuine outside click from dismissing the panel for up to 15s.
- *
- * Dragging the strip moves the flyout away from the tray and the backend
- * remembers the spot; double-clicking it anchors the flyout to the tray again.
- */
-function TrayWindowHandles({ moveHint }: { moveHint: string }) {
- return (
- <>
-
{
- if (e.button !== 0) return;
- e.preventDefault();
- void getCurrentWindow()
- .startResizeDragging(direction)
- .catch((err) => console.error("[tray-resize] startResizeDragging failed:", err));
- }}
- />
- ))}
- >
- );
-}
-
function OverviewSpendSummary({
providerIds,
period,
diff --git a/apps/desktop-tauri/src/surfaces/menu-surface-layout.css b/apps/desktop-tauri/src/surfaces/menu-surface-layout.css
index 25c444888b..275af2cd36 100644
--- a/apps/desktop-tauri/src/surfaces/menu-surface-layout.css
+++ b/apps/desktop-tauri/src/surfaces/menu-surface-layout.css
@@ -155,10 +155,11 @@
border-radius: 0;
padding: 0;
}
-.menu-surface--tray .menu-stack__sep {
+.menu-surface--tray .menu-stack__sep,
+.menu-surface--tray .provider-grid__divider {
height: 1px;
- margin: 4px var(--surface-inset);
- background: var(--panel-border);
+ margin: 5px 14px;
+ background: var(--mac-separator);
}
.menu-surface--tray .provider-grid {
padding: 8px var(--surface-inset);
@@ -167,15 +168,3 @@
.menu-surface--tray .provider-grid__item {
min-height: 32px;
}
-.menu-surface--tray .menu-surface__footer-row {
- min-height: 28px;
- font-size: 12px;
- padding-top: 5px;
- padding-bottom: 5px;
-}
-.context-actions {
- padding: 0 var(--surface-inset);
-}
-.context-actions__btn {
- min-height: 28px;
-}
diff --git a/rust/src/locale.rs b/rust/src/locale.rs
index eeafc73f0f..75b3767cf2 100644
--- a/rust/src/locale.rs
+++ b/rust/src/locale.rs
@@ -445,7 +445,11 @@ locale_keys! {
// Main popup - Bottom menu (Actions)
MenuSettings,
MenuAbout,
+ MenuAboutVersion,
MenuQuit,
+ TrayMenuSwitchAccount,
+ TrayMenuUsageDashboard,
+ TrayMenuStatusPage,
// Main popup - Status strings
StatusJustUpdated,
@@ -964,7 +968,6 @@ locale_keys! {
DisplayModeMinimal,
TrayPanelAlwaysOnTopLabel,
TrayPanelAlwaysOnTopHelper,
- TrayMoveHandleHint,
WindowMinimize,
WindowRestore,
WindowClose,
@@ -1062,7 +1065,6 @@ locale_keys! {
PanelAllProvidersShort,
PanelShowAllProviders,
PanelShowFewerProviders,
- PanelZoom,
PanelMenu,
PanelCopied,
PanelToday,
diff --git a/rust/src/locale/en-US.ftl b/rust/src/locale/en-US.ftl
index 4d60d79956..bc3c4f2f2c 100644
--- a/rust/src/locale/en-US.ftl
+++ b/rust/src/locale/en-US.ftl
@@ -212,7 +212,11 @@ UpdateFailed = Update failed
ButtonOpenProviderSettings = Open provider settings
MenuSettings = Settings...
MenuAbout = About CodexBar
+MenuAboutVersion = About CodexBar (v{ "{}" })
MenuQuit = Quit
+TrayMenuSwitchAccount = Switch Account...
+TrayMenuUsageDashboard = Usage Dashboard
+TrayMenuStatusPage = Status Page
StatusJustUpdated = Just updated
StatusUnableToGetUsage = Unable to get usage
ActionRefresh = Refresh
@@ -560,7 +564,6 @@ DisplayModeCompact = Compact
DisplayModeMinimal = Minimal
TrayPanelAlwaysOnTopLabel = Always on top
TrayPanelAlwaysOnTopHelper = Keep the tray panel above other windows, even when it loses focus.
-TrayMoveHandleHint = Drag to move. Double-click to put it back by the tray.
WindowMinimize = Minimize
WindowRestore = Restore
WindowClose = Close
@@ -650,7 +653,6 @@ PanelAllProviders = All providers
PanelAllProvidersShort = All
PanelShowAllProviders = Show all providers
PanelShowFewerProviders = Show fewer providers
-PanelZoom = Zoom
PanelMenu = Menu
PanelCopied = Copied
PanelToday = Today
diff --git a/rust/src/locale/es-MX.ftl b/rust/src/locale/es-MX.ftl
index af7de2d65b..c0cb13867f 100644
--- a/rust/src/locale/es-MX.ftl
+++ b/rust/src/locale/es-MX.ftl
@@ -185,7 +185,11 @@ UpdateFailed = Actualización fallida
ButtonOpenProviderSettings = Abrir configuración del proveedor
MenuSettings = Configuración...
MenuAbout = Acerca de CodexBar
+MenuAboutVersion = Acerca de CodexBar (v{ "{}" })
MenuQuit = Salir
+TrayMenuSwitchAccount = Cambiar cuenta...
+TrayMenuUsageDashboard = Panel de uso
+TrayMenuStatusPage = Página de estado
StatusJustUpdated = Recién actualizado
StatusUnableToGetUsage = No se pudo obtener el uso
ActionRefresh = Actualizar
@@ -578,7 +582,6 @@ DisplayModeCompact = Compacto
DisplayModeMinimal = Mínimo
TrayPanelAlwaysOnTopLabel = Siempre visible encima
TrayPanelAlwaysOnTopHelper = Mantén el panel de bandeja sobre otras ventanas, incluso cuando pierda el foco.
-TrayMoveHandleHint = Arrastra para mover. Haz doble clic para devolverlo junto a la bandeja.
WindowMinimize = Minimizar
WindowRestore = Restaurar
WindowClose = Cerrar
@@ -668,7 +671,6 @@ PanelAllProviders = Todos los proveedores
PanelAllProvidersShort = Todos
PanelShowAllProviders = Mostrar todos los proveedores
PanelShowFewerProviders = Mostrar menos proveedores
-PanelZoom = Zoom
PanelMenu = Menú
PanelCopied = Copiado
PanelToday = Hoy
diff --git a/rust/src/locale/ja-JP.ftl b/rust/src/locale/ja-JP.ftl
index 4eee267303..bf4d3504e9 100644
--- a/rust/src/locale/ja-JP.ftl
+++ b/rust/src/locale/ja-JP.ftl
@@ -185,7 +185,11 @@ UpdateFailed = 更新失敗
ButtonOpenProviderSettings = プロバイダー設定を開く
MenuSettings = 設定...
MenuAbout = CodexBar について
+MenuAboutVersion = CodexBar について (v{ "{}" })
MenuQuit = 終了
+TrayMenuSwitchAccount = アカウントを切り替え…
+TrayMenuUsageDashboard = 使用量ダッシュボード
+TrayMenuStatusPage = ステータスページ
StatusJustUpdated = 更新済み
StatusUnableToGetUsage = 使用量を取得できません
ActionRefresh = 更新
@@ -533,7 +537,6 @@ DisplayModeCompact = コンパクト
DisplayModeMinimal = 最小
TrayPanelAlwaysOnTopLabel = 常に手前に表示
TrayPanelAlwaysOnTopHelper = フォーカスを失っても、トレイパネルを他のウィンドウより前面に表示します。
-TrayMoveHandleHint = ドラッグで移動。ダブルクリックでトレイの横に戻します。
WindowMinimize = 最小化
WindowRestore = 元に戻す
WindowClose = 閉じる
@@ -623,7 +626,6 @@ PanelAllProviders = すべてのプロバイダー
PanelAllProvidersShort = すべて
PanelShowAllProviders = すべてのプロバイダーを表示
PanelShowFewerProviders = 表示を減らす
-PanelZoom = ズーム
PanelMenu = メニュー
PanelCopied = コピーしました
PanelToday = 今日
diff --git a/rust/src/locale/ko-KR.ftl b/rust/src/locale/ko-KR.ftl
index 5dcb6cdd23..ee48a147ea 100644
--- a/rust/src/locale/ko-KR.ftl
+++ b/rust/src/locale/ko-KR.ftl
@@ -185,7 +185,11 @@ UpdateFailed = 업데이트 실패
ButtonOpenProviderSettings = 제공업체 설정 열기
MenuSettings = 설정...
MenuAbout = CodexBar 정보
+MenuAboutVersion = CodexBar 정보 (v{ "{}" })
MenuQuit = 종료
+TrayMenuSwitchAccount = 계정 전환...
+TrayMenuUsageDashboard = 사용량 대시보드
+TrayMenuStatusPage = 상태 페이지
StatusJustUpdated = 방금 업데이트됨
StatusUnableToGetUsage = 사용량을 가져올 수 없음
ActionRefresh = 새로고침
@@ -578,7 +582,6 @@ DisplayModeCompact = 압축
DisplayModeMinimal = 최소
TrayPanelAlwaysOnTopLabel = 항상 위에 표시
TrayPanelAlwaysOnTopHelper = 포커스를 잃어도 트레이 패널을 다른 창보다 앞에 유지합니다.
-TrayMoveHandleHint = 드래그하여 이동합니다. 두 번 클릭하면 트레이 옆으로 돌아갑니다.
WindowMinimize = 최소화
WindowRestore = 이전 크기로 복원
WindowClose = 닫기
@@ -668,7 +671,6 @@ PanelAllProviders = 모든 제공자
PanelAllProvidersShort = 전체
PanelShowAllProviders = 모든 제공자 표시
PanelShowFewerProviders = 적은 제공자 표시
-PanelZoom = 확대/축소
PanelMenu = 메뉴
PanelCopied = 복사됨
PanelToday = 오늘
diff --git a/rust/src/locale/pt-BR.ftl b/rust/src/locale/pt-BR.ftl
index ef6f9509f1..ea8e5cf0e0 100644
--- a/rust/src/locale/pt-BR.ftl
+++ b/rust/src/locale/pt-BR.ftl
@@ -208,7 +208,11 @@ UpdateFailed = Falha na atualização
ButtonOpenProviderSettings = Abrir configurações do provedor
MenuSettings = Configurações...
MenuAbout = Sobre o CodexBar
+MenuAboutVersion = Sobre o CodexBar (v{ "{}" })
MenuQuit = Sair
+TrayMenuSwitchAccount = Trocar de conta...
+TrayMenuUsageDashboard = Painel de uso
+TrayMenuStatusPage = Página de status
StatusJustUpdated = Atualizado agora
StatusUnableToGetUsage = Não foi possível obter o uso
ActionRefresh = Atualizar
@@ -531,7 +535,6 @@ DisplayModeCompact = Compacto
DisplayModeMinimal = Mínimo
TrayPanelAlwaysOnTopLabel = Sempre no topo
TrayPanelAlwaysOnTopHelper = Mantenha o painel da bandeja acima das outras janelas, mesmo quando ele perder o foco.
-TrayMoveHandleHint = Arraste para mover. Clique duas vezes para voltar ao lado da bandeja.
WindowMinimize = Minimizar
WindowRestore = Restaurar
WindowClose = Fechar
@@ -621,7 +624,6 @@ PanelAllProviders = Todos os provedores
PanelAllProvidersShort = Todos
PanelShowAllProviders = Mostrar todos os provedores
PanelShowFewerProviders = Mostrar menos provedores
-PanelZoom = Zoom
PanelMenu = Menu
PanelCopied = Copiado
PanelToday = Hoje
diff --git a/rust/src/locale/ru-RU.ftl b/rust/src/locale/ru-RU.ftl
index 7a00337d21..7796355475 100644
--- a/rust/src/locale/ru-RU.ftl
+++ b/rust/src/locale/ru-RU.ftl
@@ -212,7 +212,11 @@ UpdateFailed = Обновление не выполнено
ButtonOpenProviderSettings = Открыть настройки провайдера
MenuSettings = Настройки...
MenuAbout = О CodexBar
+MenuAboutVersion = О CodexBar (v{ "{}" })
MenuQuit = Выйти
+TrayMenuSwitchAccount = Сменить аккаунт...
+TrayMenuUsageDashboard = Панель управления использованием
+TrayMenuStatusPage = Страница статуса
StatusJustUpdated = Только что обновлено
StatusUnableToGetUsage = Не удалось получить использование
ActionRefresh = Обновить
@@ -560,7 +564,6 @@ DisplayModeCompact = Компактный
DisplayModeMinimal = Минимальный
TrayPanelAlwaysOnTopLabel = Поверх всех окон
TrayPanelAlwaysOnTopHelper = Панель в области уведомлений остается поверх других окон даже после потери фокуса.
-TrayMoveHandleHint = Перетащите, чтобы переместить. Двойной щелчок вернёт панель к области уведомлений.
WindowMinimize = Свернуть
WindowRestore = Восстановить
WindowClose = Закрыть
@@ -650,7 +653,6 @@ PanelAllProviders = Все провайдеры
PanelAllProvidersShort = Все
PanelShowAllProviders = Показать всех провайдеров
PanelShowFewerProviders = Показать меньше провайдеров
-PanelZoom = Увеличить
PanelMenu = Меню
PanelCopied = Скопировано
PanelToday = Сегодня
diff --git a/rust/src/locale/tests.rs b/rust/src/locale/tests.rs
index 569c6c0b00..7f64280fa8 100644
--- a/rust/src/locale/tests.rs
+++ b/rust/src/locale/tests.rs
@@ -145,7 +145,6 @@ fn test_japanese_tray_panel_locale_values_are_translated() {
(LocaleKey::StateError, "エラー"),
(LocaleKey::PanelAllProviders, "すべてのプロバイダー"),
(LocaleKey::PanelAllProvidersShort, "すべて"),
- (LocaleKey::PanelZoom, "ズーム"),
(LocaleKey::PanelMenu, "メニュー"),
(LocaleKey::PanelToday, "今日"),
(LocaleKey::PanelLatestTokens, "最新トークン"),
@@ -181,7 +180,6 @@ fn test_chinese_tray_panel_locale_values_are_translated() {
let cases = [
(LocaleKey::PanelAllProviders, "所有提供者"),
(LocaleKey::PanelAllProvidersShort, "全部"),
- (LocaleKey::PanelZoom, "缩放"),
(LocaleKey::PanelMenu, "菜单"),
(LocaleKey::PanelToday, "今日"),
(LocaleKey::PanelLatestTokens, "最新令牌"),
@@ -215,7 +213,6 @@ fn test_korean_tray_panel_locale_values_are_translated() {
let cases = [
(LocaleKey::PanelAllProviders, "모든 제공자"),
(LocaleKey::PanelAllProvidersShort, "전체"),
- (LocaleKey::PanelZoom, "확대/축소"),
(LocaleKey::PanelMenu, "메뉴"),
(LocaleKey::PanelToday, "오늘"),
(LocaleKey::PanelLatestTokens, "최신 토큰"),
@@ -249,7 +246,6 @@ fn test_spanish_tray_panel_locale_values_are_translated() {
let cases = [
(LocaleKey::PanelAllProviders, "Todos los proveedores"),
(LocaleKey::PanelAllProvidersShort, "Todos"),
- (LocaleKey::PanelZoom, "Zoom"),
(LocaleKey::PanelMenu, "Menú"),
(LocaleKey::PanelToday, "Hoy"),
(LocaleKey::PanelLatestTokens, "Últimos tokens"),
diff --git a/rust/src/locale/tr-TR.ftl b/rust/src/locale/tr-TR.ftl
index 41ec3e2c98..31810dd288 100644
--- a/rust/src/locale/tr-TR.ftl
+++ b/rust/src/locale/tr-TR.ftl
@@ -212,7 +212,11 @@ UpdateFailed = Güncelleme başarısız
ButtonOpenProviderSettings = Sağlayıcı ayarlarını aç
MenuSettings = Ayarlar...
MenuAbout = CodexBar hakkında
+MenuAboutVersion = CodexBar hakkında (v{ "{}" })
MenuQuit = Çık
+TrayMenuSwitchAccount = Hesap değiştir...
+TrayMenuUsageDashboard = Kullanım panosu
+TrayMenuStatusPage = Durum sayfası
StatusJustUpdated = Az önce güncellendi
StatusUnableToGetUsage = Kullanım bilgisi alınamıyor
ActionRefresh = Yenile
@@ -560,7 +564,6 @@ DisplayModeCompact = Kompakt
DisplayModeMinimal = En az
TrayPanelAlwaysOnTopLabel = Her zaman üstte
TrayPanelAlwaysOnTopHelper = Odak kaybolduğunda bile tepsi panelini diğer pencerelerin üzerinde tutar.
-TrayMoveHandleHint = Taşımak için sürükleyin. Tepsinin yanına geri döndürmek için çift tıklayın.
WindowMinimize = Simge durumuna küçült
WindowRestore = Geri yükle
WindowClose = Kapat
@@ -650,7 +653,6 @@ PanelAllProviders = Tüm sağlayıcılar
PanelAllProvidersShort = Tümü
PanelShowAllProviders = Tüm sağlayıcıları göster
PanelShowFewerProviders = Daha az sağlayıcı göster
-PanelZoom = Yakınlaştırma
PanelMenu = Menü
PanelCopied = Kopyalandı
PanelToday = Bugün
diff --git a/rust/src/locale/uk-UA.ftl b/rust/src/locale/uk-UA.ftl
index 525fd917c6..dcd5b0c82a 100644
--- a/rust/src/locale/uk-UA.ftl
+++ b/rust/src/locale/uk-UA.ftl
@@ -212,7 +212,11 @@ UpdateFailed = Не вдалося оновити
ButtonOpenProviderSettings = Відкрити налаштування провайдера
MenuSettings = Налаштування...
MenuAbout = Про CodexBar
+MenuAboutVersion = Про CodexBar (v{ "{}" })
MenuQuit = Вийти
+TrayMenuSwitchAccount = Перемкнути обліковий запис...
+TrayMenuUsageDashboard = Панель використання
+TrayMenuStatusPage = Сторінка стану
StatusJustUpdated = Щойно оновлено
StatusUnableToGetUsage = Не вдалося отримати дані про використання
ActionRefresh = Оновити
@@ -560,7 +564,6 @@ DisplayModeCompact = Компактний
DisplayModeMinimal = Мінімальний
TrayPanelAlwaysOnTopLabel = Поверх усіх вікон
TrayPanelAlwaysOnTopHelper = Тримати панель трею поверх інших вікон, навіть коли вона втрачає фокус.
-TrayMoveHandleHint = Перетягніть, щоб перемістити. Двічі клацніть, щоб повернути панель до трею.
WindowMinimize = Згорнути
WindowRestore = Відновити
WindowClose = Закрити
@@ -650,7 +653,6 @@ PanelAllProviders = Усі провайдери
PanelAllProvidersShort = Усі
PanelShowAllProviders = Показати всіх провайдерів
PanelShowFewerProviders = Показати менше провайдерів
-PanelZoom = Масштаб
PanelMenu = Меню
PanelCopied = Скопійовано
PanelToday = Сьогодні
diff --git a/rust/src/locale/zh-CN.ftl b/rust/src/locale/zh-CN.ftl
index f5e05665fc..9936510a7c 100644
--- a/rust/src/locale/zh-CN.ftl
+++ b/rust/src/locale/zh-CN.ftl
@@ -185,7 +185,11 @@ UpdateFailed = 更新失败
ButtonOpenProviderSettings = 打开服务商设置
MenuSettings = 设置...
MenuAbout = 关于 CodexBar
+MenuAboutVersion = 关于 CodexBar (v{ "{}" })
MenuQuit = 退出
+TrayMenuSwitchAccount = 切换账号...
+TrayMenuUsageDashboard = 用量仪表盘
+TrayMenuStatusPage = 状态页面
StatusJustUpdated = 刚刚更新
StatusUnableToGetUsage = 无法获取用量
ActionRefresh = 刷新
@@ -533,7 +537,6 @@ DisplayModeCompact = 紧凑
DisplayModeMinimal = 最简
TrayPanelAlwaysOnTopLabel = 始终置顶
TrayPanelAlwaysOnTopHelper = 即使失去焦点,也让托盘面板保持在其他窗口之上。
-TrayMoveHandleHint = 拖动以移动。双击可放回托盘旁。
WindowMinimize = 最小化
WindowRestore = 还原
WindowClose = 关闭
@@ -623,7 +626,6 @@ PanelAllProviders = 所有提供者
PanelAllProvidersShort = 全部
PanelShowAllProviders = 显示所有提供者
PanelShowFewerProviders = 显示较少提供者
-PanelZoom = 缩放
PanelMenu = 菜单
PanelCopied = 已复制
PanelToday = 今日
diff --git a/rust/src/locale/zh-TW.ftl b/rust/src/locale/zh-TW.ftl
index e9faab8be6..ba0201928f 100644
--- a/rust/src/locale/zh-TW.ftl
+++ b/rust/src/locale/zh-TW.ftl
@@ -185,7 +185,11 @@ UpdateFailed = 更新失敗
ButtonOpenProviderSettings = 開啟提供者設定
MenuSettings = 設定...
MenuAbout = 關於 CodexBar
+MenuAboutVersion = 關於 CodexBar (v{ "{}" })
MenuQuit = 退出
+TrayMenuSwitchAccount = 切換賬號...
+TrayMenuUsageDashboard = 用量儀表板
+TrayMenuStatusPage = 狀態頁面
StatusJustUpdated = 剛剛更新
StatusUnableToGetUsage = 無法獲取用量
ActionRefresh = 重新整理
@@ -533,7 +537,6 @@ DisplayModeCompact = 緊湊
DisplayModeMinimal = 最簡
TrayPanelAlwaysOnTopLabel = 永遠置頂
TrayPanelAlwaysOnTopHelper = 即使失去焦點,也讓系統匣面板保持在其他視窗上方。
-TrayMoveHandleHint = 拖曳以移動。按兩下可放回系統匣旁。
WindowMinimize = 最小化
WindowRestore = 還原
WindowClose = 關閉
@@ -623,7 +626,6 @@ PanelAllProviders = 所有提供者
PanelAllProvidersShort = 全部
PanelShowAllProviders = 顯示所有提供者
PanelShowFewerProviders = 顯示較少提供者
-PanelZoom = 縮放
PanelMenu = 選單
PanelCopied = 已複製
PanelToday = 今日
From c569b0663c35a37c8079f98fbf078cbc44ac29fe Mon Sep 17 00:00:00 2001
From: RCD <90105158+Finesssee@users.noreply.github.com>
Date: Sat, 10 Oct 2026 13:40:14 +0700
Subject: [PATCH 02/10] Draw the tray flyout as a fixed light frame
DWM gives the flyout Windows 11 rounded corners and a #8A8B8E border,
and the window erases to the panel fill, so it never flashes dark. It
has no thick frame and opens next to the tray every time.
The set_flyout_size, reset_flyout_position and flyout_stored_size
commands, the remembered placement and the stored size are removed.
get_system_accent_color reads the Windows accent for the menu
selection.
---
.../src-tauri/src/commands/surface.rs | 35 +-
.../src-tauri/src/commands/system.rs | 41 ++
.../src-tauri/src/geometry_store.rs | 208 +-------
apps/desktop-tauri/src-tauri/src/main.rs | 8 +-
apps/desktop-tauri/src-tauri/src/shell/dwm.rs | 126 ++++-
.../src-tauri/src/shell/flyout_placement.rs | 478 ------------------
.../src-tauri/src/shell/flyout_window.rs | 154 ++----
apps/desktop-tauri/src-tauri/src/shell/mod.rs | 1 -
.../src-tauri/src/shell/position.rs | 8 +-
.../src-tauri/src/shell/tests.rs | 7 +-
.../src-tauri/src/shell/window.rs | 3 +-
apps/desktop-tauri/src-tauri/src/state.rs | 4 +-
apps/desktop-tauri/src-tauri/src/surface.rs | 22 +-
.../src-tauri/src/tray_bridge.rs | 4 +-
14 files changed, 234 insertions(+), 865 deletions(-)
delete mode 100644 apps/desktop-tauri/src-tauri/src/shell/flyout_placement.rs
diff --git a/apps/desktop-tauri/src-tauri/src/commands/surface.rs b/apps/desktop-tauri/src-tauri/src/commands/surface.rs
index 00b268abdf..45c54a47d4 100644
--- a/apps/desktop-tauri/src-tauri/src/commands/surface.rs
+++ b/apps/desktop-tauri/src-tauri/src/commands/surface.rs
@@ -28,8 +28,8 @@ pub fn dismiss_tray_panel(app: tauri::AppHandle) -> Result<(), String> {
crate::shell::flyout_window::hide(&app)
}
-/// Arm the gesture blur guard before a resize-grip drag or drag-reorder
-/// gesture starts its Win32/OLE modal loop, so the transient
+/// Arm the gesture blur guard before a drag-reorder gesture starts its
+/// OLE modal loop, so the transient
/// `Focused(false)` that loop produces doesn't auto-hide the flyout.
#[tauri::command]
pub fn begin_flyout_gesture(app: tauri::AppHandle) -> Result<(), String> {
@@ -116,37 +116,6 @@ pub fn close_settings_window(
crate::shell::settings_window::dismiss(&app, &window)
}
-/// Persist a user-chosen size (logical px) for the "Pop Out Dashboard" flyout
-/// window. Only the size is stored (via a size-only `StoredSize` entry — no
-/// fabricated `x`/`y`); the position is tracked separately, only once the
-/// user drags the flyout. The frontend calls this on genuine user drag-resizes, not on its own
-/// auto-fit resizes, so auto-fit sizes never freeze the panel.
-#[tauri::command]
-pub fn set_flyout_size(width: f64, height: f64) -> Result<(), String> {
- let width = (width.round() as i64).clamp(1, i64::from(u32::MAX)) as u32;
- let height = (height.round() as i64).clamp(1, i64::from(u32::MAX)) as u32;
- crate::shell::flyout_window::save_stored_size(width, height);
- Ok(())
-}
-
-/// Forget where the user dragged the flyout and anchor it to the tray again
-/// (double-click on the panel's move handle).
-#[tauri::command]
-pub fn reset_flyout_position(app: tauri::AppHandle) -> Result<(), String> {
- crate::shell::flyout_window::reset_position(&app)
-}
-
-/// Return the remembered flyout size, if the user has manually resized it.
-/// The frontend uses this to decide whether to auto-fit (no stored size) or
-/// honor the user's size (stored) on open. Transparently migrates a
-/// pre-existing size stored under the legacy `SurfaceMode::TrayPanel`
-/// shared-window geometry key (from before the flyout became its own
-/// window), so upgrading users don't lose their remembered size.
-#[tauri::command]
-pub fn flyout_stored_size() -> Result
}
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index 2c20b4ca68..294221c390 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -38,7 +38,7 @@ Cargo workspace (root `Cargo.toml`): members `rust`, `apps/desktop-tauri/src-tau
## Surfaces (desktop)
-- **Tray panel** — left-click tray; blur-dismiss (suppressed in proof mode, and while a mouse button is held on the panel so a frame move/resize doesn't close it). Opens next to the tray until the user drags its top strip elsewhere; `shell/flyout_placement.rs` remembers that spot (double-click the strip to forget it). Resizable from every edge and corner.
+- **Tray panel** — left-click tray; blur-dismiss (suppressed in proof mode and while a provider tab is dragged to reorder). A light, Mac-style panel 310 px wide (times the **Panel scale** setting in Settings > Menu) whose height fits its content. It always opens next to the tray and can't be moved or resized.
- **Tray panel flyout** — larger dashboard window opened from the tray
menu ("Pop Out Dashboard"); the tray panel itself is the only
dashboard layout.
diff --git a/rust/src/locale.rs b/rust/src/locale.rs
index 75b3767cf2..fd5ef31d07 100644
--- a/rust/src/locale.rs
+++ b/rust/src/locale.rs
@@ -968,6 +968,8 @@ locale_keys! {
DisplayModeMinimal,
TrayPanelAlwaysOnTopLabel,
TrayPanelAlwaysOnTopHelper,
+ PanelScaleLabel,
+ PanelScaleHelper,
WindowMinimize,
WindowRestore,
WindowClose,
diff --git a/rust/src/locale/en-US.ftl b/rust/src/locale/en-US.ftl
index bc3c4f2f2c..94d30118c9 100644
--- a/rust/src/locale/en-US.ftl
+++ b/rust/src/locale/en-US.ftl
@@ -564,6 +564,8 @@ DisplayModeCompact = Compact
DisplayModeMinimal = Minimal
TrayPanelAlwaysOnTopLabel = Always on top
TrayPanelAlwaysOnTopHelper = Keep the tray panel above other windows, even when it loses focus.
+PanelScaleLabel = Panel scale
+PanelScaleHelper = Scales the tray panel width, text, and spacing.
WindowMinimize = Minimize
WindowRestore = Restore
WindowClose = Close
diff --git a/rust/src/locale/es-MX.ftl b/rust/src/locale/es-MX.ftl
index c0cb13867f..01da040375 100644
--- a/rust/src/locale/es-MX.ftl
+++ b/rust/src/locale/es-MX.ftl
@@ -582,6 +582,8 @@ DisplayModeCompact = Compacto
DisplayModeMinimal = Mínimo
TrayPanelAlwaysOnTopLabel = Siempre visible encima
TrayPanelAlwaysOnTopHelper = Mantén el panel de bandeja sobre otras ventanas, incluso cuando pierda el foco.
+PanelScaleLabel = Escala del panel
+PanelScaleHelper = Ajusta el ancho, el texto y el espaciado del panel de la bandeja.
WindowMinimize = Minimizar
WindowRestore = Restaurar
WindowClose = Cerrar
diff --git a/rust/src/locale/ja-JP.ftl b/rust/src/locale/ja-JP.ftl
index bf4d3504e9..ad7f714bcb 100644
--- a/rust/src/locale/ja-JP.ftl
+++ b/rust/src/locale/ja-JP.ftl
@@ -537,6 +537,8 @@ DisplayModeCompact = コンパクト
DisplayModeMinimal = 最小
TrayPanelAlwaysOnTopLabel = 常に手前に表示
TrayPanelAlwaysOnTopHelper = フォーカスを失っても、トレイパネルを他のウィンドウより前面に表示します。
+PanelScaleLabel = パネルの拡大率
+PanelScaleHelper = トレイパネルの幅、文字、余白を拡大します。
WindowMinimize = 最小化
WindowRestore = 元に戻す
WindowClose = 閉じる
diff --git a/rust/src/locale/ko-KR.ftl b/rust/src/locale/ko-KR.ftl
index ee48a147ea..794ad768e4 100644
--- a/rust/src/locale/ko-KR.ftl
+++ b/rust/src/locale/ko-KR.ftl
@@ -582,6 +582,8 @@ DisplayModeCompact = 압축
DisplayModeMinimal = 최소
TrayPanelAlwaysOnTopLabel = 항상 위에 표시
TrayPanelAlwaysOnTopHelper = 포커스를 잃어도 트레이 패널을 다른 창보다 앞에 유지합니다.
+PanelScaleLabel = 패널 배율
+PanelScaleHelper = 트레이 패널의 너비, 글자, 간격을 조정합니다.
WindowMinimize = 최소화
WindowRestore = 이전 크기로 복원
WindowClose = 닫기
diff --git a/rust/src/locale/pt-BR.ftl b/rust/src/locale/pt-BR.ftl
index ea8e5cf0e0..b72df9bfba 100644
--- a/rust/src/locale/pt-BR.ftl
+++ b/rust/src/locale/pt-BR.ftl
@@ -535,6 +535,8 @@ DisplayModeCompact = Compacto
DisplayModeMinimal = Mínimo
TrayPanelAlwaysOnTopLabel = Sempre no topo
TrayPanelAlwaysOnTopHelper = Mantenha o painel da bandeja acima das outras janelas, mesmo quando ele perder o foco.
+PanelScaleLabel = Escala do painel
+PanelScaleHelper = Ajusta a largura, o texto e o espaçamento do painel da bandeja.
WindowMinimize = Minimizar
WindowRestore = Restaurar
WindowClose = Fechar
diff --git a/rust/src/locale/ru-RU.ftl b/rust/src/locale/ru-RU.ftl
index 7796355475..1034d439a2 100644
--- a/rust/src/locale/ru-RU.ftl
+++ b/rust/src/locale/ru-RU.ftl
@@ -564,6 +564,8 @@ DisplayModeCompact = Компактный
DisplayModeMinimal = Минимальный
TrayPanelAlwaysOnTopLabel = Поверх всех окон
TrayPanelAlwaysOnTopHelper = Панель в области уведомлений остается поверх других окон даже после потери фокуса.
+PanelScaleLabel = Масштаб панели
+PanelScaleHelper = Масштабирует ширину, текст и отступы панели в трее.
WindowMinimize = Свернуть
WindowRestore = Восстановить
WindowClose = Закрыть
diff --git a/rust/src/locale/tr-TR.ftl b/rust/src/locale/tr-TR.ftl
index 31810dd288..93fb051a15 100644
--- a/rust/src/locale/tr-TR.ftl
+++ b/rust/src/locale/tr-TR.ftl
@@ -564,6 +564,8 @@ DisplayModeCompact = Kompakt
DisplayModeMinimal = En az
TrayPanelAlwaysOnTopLabel = Her zaman üstte
TrayPanelAlwaysOnTopHelper = Odak kaybolduğunda bile tepsi panelini diğer pencerelerin üzerinde tutar.
+PanelScaleLabel = Panel ölçeği
+PanelScaleHelper = Tepsi panelinin genişliğini, metnini ve boşluklarını ölçekler.
WindowMinimize = Simge durumuna küçült
WindowRestore = Geri yükle
WindowClose = Kapat
diff --git a/rust/src/locale/uk-UA.ftl b/rust/src/locale/uk-UA.ftl
index dcd5b0c82a..185a41b80c 100644
--- a/rust/src/locale/uk-UA.ftl
+++ b/rust/src/locale/uk-UA.ftl
@@ -564,6 +564,8 @@ DisplayModeCompact = Компактний
DisplayModeMinimal = Мінімальний
TrayPanelAlwaysOnTopLabel = Поверх усіх вікон
TrayPanelAlwaysOnTopHelper = Тримати панель трею поверх інших вікон, навіть коли вона втрачає фокус.
+PanelScaleLabel = Масштаб панелі
+PanelScaleHelper = Масштабує ширину, текст і відступи панелі в треї.
WindowMinimize = Згорнути
WindowRestore = Відновити
WindowClose = Закрити
diff --git a/rust/src/locale/zh-CN.ftl b/rust/src/locale/zh-CN.ftl
index 9936510a7c..176946e679 100644
--- a/rust/src/locale/zh-CN.ftl
+++ b/rust/src/locale/zh-CN.ftl
@@ -537,6 +537,8 @@ DisplayModeCompact = 紧凑
DisplayModeMinimal = 最简
TrayPanelAlwaysOnTopLabel = 始终置顶
TrayPanelAlwaysOnTopHelper = 即使失去焦点,也让托盘面板保持在其他窗口之上。
+PanelScaleLabel = 面板缩放
+PanelScaleHelper = 缩放托盘面板的宽度、文字和间距。
WindowMinimize = 最小化
WindowRestore = 还原
WindowClose = 关闭
diff --git a/rust/src/locale/zh-TW.ftl b/rust/src/locale/zh-TW.ftl
index ba0201928f..b10ee45da6 100644
--- a/rust/src/locale/zh-TW.ftl
+++ b/rust/src/locale/zh-TW.ftl
@@ -537,6 +537,8 @@ DisplayModeCompact = 緊湊
DisplayModeMinimal = 最簡
TrayPanelAlwaysOnTopLabel = 永遠置頂
TrayPanelAlwaysOnTopHelper = 即使失去焦點,也讓系統匣面板保持在其他視窗上方。
+PanelScaleLabel = 面板縮放
+PanelScaleHelper = 縮放系統匣面板的寬度、文字和間距。
WindowMinimize = 最小化
WindowRestore = 還原
WindowClose = 關閉
From 49d799ba8d8217812fbe9b92a00960f6ff5d71b3 Mon Sep 17 00:00:00 2001
From: RCD <90105158+Finesssee@users.noreply.github.com>
Date: Sat, 10 Oct 2026 14:07:45 +0700
Subject: [PATCH 04/10] Draw tray captions in the Mac secondary text color
Muted tray text used the tertiary color, which macOS keeps for
shortcuts. Captions such as the spend summary note read at about
1.8:1 on the panel fill. They now use the secondary color.
---
apps/desktop-tauri/src/styles.css | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/apps/desktop-tauri/src/styles.css b/apps/desktop-tauri/src/styles.css
index 1b7b7b47f7..01fbf89525 100644
--- a/apps/desktop-tauri/src/styles.css
+++ b/apps/desktop-tauri/src/styles.css
@@ -3467,7 +3467,7 @@ html:has(.menu-surface--tray) {
--menu-popover-bg: var(--mac-panel-bg);
--text-primary: var(--mac-text);
--text-secondary: var(--mac-text-2);
- --text-muted: var(--mac-text-3);
+ --text-muted: var(--mac-text-2);
--panel-border: var(--mac-divider);
--usage-bar-track: var(--mac-track);
From 5d94b67e77c17bd5d60499dfbd013a557b19a7a5 Mon Sep 17 00:00:00 2001
From: RCD <90105158+Finesssee@users.noreply.github.com>
Date: Sat, 10 Oct 2026 15:08:06 +0700
Subject: [PATCH 05/10] Follow accent changes and simplify the tray chrome
- Re-read the Windows accent each time the panel takes focus.
- Use black selection text when white falls below 4.5:1 on the accent.
- Drop the white plate behind the selected card.
- Hoist --mac-panel-bg to :root and derive the fill test from PANEL_FILL.
- Keep the dark erase brush call for every window, as on main.
- Drop the unused flyout minimum size.
---
apps/desktop-tauri/src-tauri/src/shell/dwm.rs | 17 +++--------
.../src-tauri/src/shell/flyout_window.rs | 15 ++++------
.../src/hooks/useTrayPanelController.ts | 28 ++++++++++++++-----
.../src/lib/menuSelection.test.ts | 11 ++++++--
apps/desktop-tauri/src/lib/menuSelection.ts | 5 ++--
apps/desktop-tauri/src/styles.css | 9 +++---
.../src/surfaces/TrayPanel.test.tsx | 22 +++++++++++++++
7 files changed, 69 insertions(+), 38 deletions(-)
diff --git a/apps/desktop-tauri/src-tauri/src/shell/dwm.rs b/apps/desktop-tauri/src-tauri/src/shell/dwm.rs
index 88714ed110..9b6ea667dc 100644
--- a/apps/desktop-tauri/src-tauri/src/shell/dwm.rs
+++ b/apps/desktop-tauri/src-tauri/src/shell/dwm.rs
@@ -196,10 +196,6 @@ impl Chrome {
self == Self::DarkResizable
}
- fn paints_dark_brush(self) -> bool {
- self != Self::LightPanel
- }
-
/// `DWMWA_WINDOW_CORNER_PREFERENCE`, when the window asks for one.
fn corner_preference(self) -> Option {
(self == Self::LightPanel).then_some(DWMWCP_ROUND)
@@ -311,11 +307,9 @@ fn apply_chrome(win: &tauri::WebviewWindow, chrome: Chrome) {
// Set background brush to dark (reuse a single GDI brush)
const GCL_HBRBACKGROUND: i32 = -10;
- if chrome.paints_dark_brush() {
- let brush = *DARK_BRUSH.get_or_init(|| CreateSolidBrush(0x001C1C1E));
- if brush != 0 {
- SetWindowLongPtrW(hwnd, GCL_HBRBACKGROUND, brush);
- }
+ let brush = *DARK_BRUSH.get_or_init(|| CreateSolidBrush(0x001C1C1E));
+ if brush != 0 {
+ SetWindowLongPtrW(hwnd, GCL_HBRBACKGROUND, brush);
}
// Remove WS_CAPTION; only strip WS_THICKFRAME for non-resizable windows
@@ -387,12 +381,9 @@ mod tests {
}
#[test]
- fn the_light_panel_is_fixed_size_and_skips_the_dark_brush() {
+ fn only_the_resizable_dark_chrome_keeps_the_resize_frame() {
assert!(!Chrome::LightPanel.keeps_resize_frame());
- assert!(!Chrome::LightPanel.paints_dark_brush());
assert!(!Chrome::Dark.keeps_resize_frame());
- assert!(Chrome::Dark.paints_dark_brush());
assert!(Chrome::DarkResizable.keeps_resize_frame());
- assert!(Chrome::DarkResizable.paints_dark_brush());
}
}
diff --git a/apps/desktop-tauri/src-tauri/src/shell/flyout_window.rs b/apps/desktop-tauri/src-tauri/src/shell/flyout_window.rs
index fc0ce9e3ab..6ec518e806 100644
--- a/apps/desktop-tauri/src-tauri/src/shell/flyout_window.rs
+++ b/apps/desktop-tauri/src-tauri/src/shell/flyout_window.rs
@@ -126,7 +126,7 @@ fn open_with_anchor(
let props = SurfaceMode::TrayPanel.window_properties();
let url = WebviewUrl::App("index.html?window=flyout".into());
- let mut builder = tauri::WebviewWindowBuilder::new(app, FLYOUT_LABEL, url)
+ let builder = tauri::WebviewWindowBuilder::new(app, FLYOUT_LABEL, url)
.title("CodexBar")
.inner_size(props.width, props.height)
.decorations(props.decorations)
@@ -152,9 +152,6 @@ fn open_with_anchor(
// focus.
.focused(false)
.visible(false);
- if let (Some(min_w), Some(min_h)) = (props.min_width, props.min_height) {
- builder = builder.min_inner_size(min_w, min_h);
- }
let win = builder.build().map_err(|e| e.to_string())?;
apply_window_always_on_top(&win, settings.tray_panel_always_on_top)?;
@@ -449,17 +446,15 @@ mod tests {
#[test]
fn panel_fill_matches_the_css_panel_background() {
let css = include_str!("../../../src/styles.css");
- assert!(css.contains("--mac-panel-bg: #dedee2;"));
- assert_eq!(
- (PANEL_FILL.0, PANEL_FILL.1, PANEL_FILL.2, PANEL_FILL.3),
- (0xDE, 0xDE, 0xE2, 0xFF)
- );
+ let tauri::utils::config::Color(r, g, b, a) = PANEL_FILL;
+ assert_eq!(a, 0xFF);
+ assert!(css.contains(&format!("--mac-panel-bg: #{r:02x}{g:02x}{b:02x};")));
}
#[test]
fn tray_panel_window_properties_still_the_single_source_for_flyout_shape() {
// `open_or_focus`'s builder reads size/decorations/resizable/
- // skip_taskbar/min-size from
+ // skip_taskbar from
// `SurfaceMode::TrayPanel.window_properties()` directly (not
// independent duplicated constants) — this pins down the values that
// relationship depends on, so a change to `surface.rs` shows up here
diff --git a/apps/desktop-tauri/src/hooks/useTrayPanelController.ts b/apps/desktop-tauri/src/hooks/useTrayPanelController.ts
index 64c15de620..66e79b5997 100644
--- a/apps/desktop-tauri/src/hooks/useTrayPanelController.ts
+++ b/apps/desktop-tauri/src/hooks/useTrayPanelController.ts
@@ -67,7 +67,6 @@ export function useTrayPanelController(state: BootstrapState) {
const trayScale = clampTrayScalePercent(settings.trayScalePercent) / 100;
const [appVersion, setAppVersion] = useState(null);
- const [accentColor, setAccentColor] = useState(null);
useEffect(() => {
let cancelled = false;
void getAppInfo().then(
@@ -76,17 +75,32 @@ export function useTrayPanelController(state: BootstrapState) {
},
() => {},
);
- void getSystemAccentColor().then(
- (color) => {
- if (!cancelled) setAccentColor(color);
- },
- () => {},
- );
return () => {
cancelled = true;
};
}, []);
+ // The hidden panel window lives for the whole app run, so a focus re-read
+ // is what picks up an accent changed in Windows settings.
+ const [accentColor, setAccentColor] = useState(null);
+ useEffect(() => {
+ let cancelled = false;
+ const readAccent = () => {
+ void getSystemAccentColor().then(
+ (color) => {
+ if (!cancelled) setAccentColor(color);
+ },
+ () => {},
+ );
+ };
+ readAccent();
+ window.addEventListener("focus", readAccent);
+ return () => {
+ cancelled = true;
+ window.removeEventListener("focus", readAccent);
+ };
+ }, []);
+
const sorted = useMemo(
() =>
orderProviderSnapshots(
diff --git a/apps/desktop-tauri/src/lib/menuSelection.test.ts b/apps/desktop-tauri/src/lib/menuSelection.test.ts
index 0e2d2b5792..34bd3c309d 100644
--- a/apps/desktop-tauri/src/lib/menuSelection.test.ts
+++ b/apps/desktop-tauri/src/lib/menuSelection.test.ts
@@ -9,10 +9,17 @@ describe("accentSelectionStyle", () => {
});
});
- it("switches to dark text on a light accent", () => {
+ it("switches to black text on a light accent", () => {
expect(accentSelectionStyle("#ffb900")).toEqual({
"--mac-selection-bg": "#ffb900",
- "--mac-selection-text": "rgba(0, 0, 0, 0.85)",
+ "--mac-selection-text": "#000",
+ });
+ });
+
+ it("uses solid black on a mid gray accent, where 85% black would fall below 4.5:1", () => {
+ expect(accentSelectionStyle("#7a7a7a")).toEqual({
+ "--mac-selection-bg": "#7a7a7a",
+ "--mac-selection-text": "#000",
});
});
diff --git a/apps/desktop-tauri/src/lib/menuSelection.ts b/apps/desktop-tauri/src/lib/menuSelection.ts
index 381f184bdb..ab430ef33d 100644
--- a/apps/desktop-tauri/src/lib/menuSelection.ts
+++ b/apps/desktop-tauri/src/lib/menuSelection.ts
@@ -10,7 +10,8 @@ function linearChannel(byte: number): number {
/**
* Menu selection colors from the Windows accent, the way macOS menus use the
- * system accent. White text stays only where it keeps 4.5:1 contrast.
+ * system accent. White text stays only where it keeps 4.5:1 contrast. Where
+ * white fails, black clears 4.67:1 on any accent.
*/
export function accentSelectionStyle(accent: string | null): CSSProperties {
const match = accent ? HEX_COLOR.exec(accent) : null;
@@ -20,6 +21,6 @@ export function accentSelectionStyle(accent: string | null): CSSProperties {
const whiteContrast = 1.05 / (luminance + 0.05);
return {
"--mac-selection-bg": accent,
- "--mac-selection-text": whiteContrast >= 4.5 ? "#fff" : "rgba(0, 0, 0, 0.85)",
+ "--mac-selection-text": whiteContrast >= 4.5 ? "#fff" : "#000",
} as CSSProperties;
}
diff --git a/apps/desktop-tauri/src/styles.css b/apps/desktop-tauri/src/styles.css
index 01fbf89525..173ff27a6e 100644
--- a/apps/desktop-tauri/src/styles.css
+++ b/apps/desktop-tauri/src/styles.css
@@ -1049,10 +1049,13 @@ html {
fades the content in smoothly. The page behind the panel carries the
panel fill (--mac-panel-bg, and PANEL_FILL in shell/flyout_window.rs) so
no other color shows while the panel is hidden. */
+:root {
+ --mac-panel-bg: #dedee2;
+}
html:has(.tray-panel-reveal),
body:has(.tray-panel-reveal),
.tray-panel-reveal {
- background: #dedee2;
+ background: var(--mac-panel-bg);
}
.tray-panel-reveal {
display: flex;
@@ -3396,7 +3399,6 @@ html:has(.menu-surface--tray) {
--mac-text-menu: 400 13px/16px var(--mac-font);
--mac-text-shortcut: 400 13px/16px var(--mac-font);
- --mac-panel-bg: #dedee2;
--mac-panel-highlight: inset 0 1px 0 rgba(255, 255, 255, 0.25);
--mac-panel-border: rgba(0, 0, 0, 0.38);
--mac-text: rgba(0, 0, 0, 0.85);
@@ -3534,9 +3536,8 @@ html:has(.menu-surface--tray) {
box-sizing: border-box;
overflow: visible;
}
-/* Selected card — subtle highlight like macOS active provider */
+/* Selected card: no highlight plate, like the macOS menu card. */
.menu-surface--tray .menu-stack__item--selected {
- background: rgba(255, 255, 255, 0.06);
border: none;
border-radius: 6px;
padding: 4px 4px;
diff --git a/apps/desktop-tauri/src/surfaces/TrayPanel.test.tsx b/apps/desktop-tauri/src/surfaces/TrayPanel.test.tsx
index a144310725..3b5e717586 100644
--- a/apps/desktop-tauri/src/surfaces/TrayPanel.test.tsx
+++ b/apps/desktop-tauri/src/surfaces/TrayPanel.test.tsx
@@ -932,6 +932,28 @@ describe("TrayPanel provider grid", () => {
expect(surface.style.zoom).toBe("1.5");
});
+ it("follows a new Windows accent the next time the panel takes focus", async () => {
+ tauriMocks.getSystemAccentColor.mockResolvedValue("#0078d4");
+ const { container } = renderTrayPanel([provider("claude", "Claude", 35)]);
+ const selectionBg = () =>
+ container
+ .querySelector(".menu-surface--tray")
+ ?.style.getPropertyValue("--mac-selection-bg");
+ await waitFor(() => expect(selectionBg()).toBe("#0078d4"));
+
+ tauriMocks.getSystemAccentColor.mockResolvedValue("#ffb900");
+ act(() => {
+ window.dispatchEvent(new Event("focus"));
+ });
+
+ await waitFor(() => expect(selectionBg()).toBe("#ffb900"));
+ expect(
+ container
+ .querySelector(".menu-surface--tray")!
+ .style.getPropertyValue("--mac-selection-text"),
+ ).toBe("#000");
+ });
+
it("reveals the tray panel if the native resize pass fails", async () => {
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
windowMocks.getCurrentWindow.mockReturnValue({
From ad0f94f01515cecce448ee09ca9bae5f89b919b0 Mon Sep 17 00:00:00 2001
From: RCD <90105158+Finesssee@users.noreply.github.com>
Date: Sat, 10 Oct 2026 15:33:58 +0700
Subject: [PATCH 06/10] Report failed tray panel resizes
applySize swallowed every setSize and innerSize error, so the resize
pass's own catch never saw a failed native resize. The error now
reaches that catch, which logs a warning and still reveals the panel.
---
apps/desktop-tauri/src/hooks/useTrayPanelLayout.ts | 10 +++-------
apps/desktop-tauri/src/surfaces/TrayPanel.test.tsx | 1 +
2 files changed, 4 insertions(+), 7 deletions(-)
diff --git a/apps/desktop-tauri/src/hooks/useTrayPanelLayout.ts b/apps/desktop-tauri/src/hooks/useTrayPanelLayout.ts
index 730b913283..f3231b098c 100644
--- a/apps/desktop-tauri/src/hooks/useTrayPanelLayout.ts
+++ b/apps/desktop-tauri/src/hooks/useTrayPanelLayout.ts
@@ -60,13 +60,9 @@ export function useTrayPanelLayout({
const applySize = useCallback(async (size: LogicalSize): Promise => {
const win = getCurrentWindow();
- try {
- await win.setSize(size);
- const actual = await win.innerSize();
- lastSizeRef.current = { width: actual.width, height: actual.height };
- } catch {
- /* ignore */
- }
+ await win.setSize(size);
+ const actual = await win.innerSize();
+ lastSizeRef.current = { width: actual.width, height: actual.height };
}, []);
const requestLayout = useCallback(() => {
diff --git a/apps/desktop-tauri/src/surfaces/TrayPanel.test.tsx b/apps/desktop-tauri/src/surfaces/TrayPanel.test.tsx
index 3b5e717586..86fbb2f366 100644
--- a/apps/desktop-tauri/src/surfaces/TrayPanel.test.tsx
+++ b/apps/desktop-tauri/src/surfaces/TrayPanel.test.tsx
@@ -967,6 +967,7 @@ describe("TrayPanel provider grid", () => {
await waitFor(() => {
expect(container.querySelector(".tray-panel-reveal--ready")).not.toBeNull();
});
+ expect(warn).toHaveBeenCalledWith("CodexBar tray panel resize failed", expect.any(Error));
warn.mockRestore();
});
From c9823ffe4b0d8c581862b0fda1b40790de96f9dc Mon Sep 17 00:00:00 2001
From: RCD <90105158+Finesssee@users.noreply.github.com>
Date: Sat, 10 Oct 2026 15:33:58 +0700
Subject: [PATCH 07/10] Trim tray panel comments
- Drop comments that restate the code or describe Mac parity.
- Name the panel hairline color and the 1 px height slack.
- Rename linearChannel to srgbByteToLinear.
---
.../src-tauri/src/commands/surface.rs | 3 --
.../src-tauri/src/commands/system.rs | 2 --
.../src-tauri/src/geometry_store.rs | 6 +---
apps/desktop-tauri/src-tauri/src/main.rs | 5 ---
apps/desktop-tauri/src-tauri/src/shell/dwm.rs | 15 +++-----
.../src-tauri/src/shell/flyout_window.rs | 34 +++----------------
.../src-tauri/src/shell/window.rs | 10 ------
apps/desktop-tauri/src-tauri/src/state.rs | 3 --
apps/desktop-tauri/src-tauri/src/surface.rs | 3 --
.../src-tauri/src/tray_bridge.rs | 3 --
.../src/components/MenuSurface.tsx | 9 -----
.../desktop-tauri/src/hooks/useDraftNumber.ts | 6 ----
.../src/hooks/useTrayPanelController.ts | 8 -----
.../hooks/useTrayPanelLayout.sizing.test.tsx | 6 ----
.../src/hooks/useTrayPanelLayout.ts | 31 ++---------------
apps/desktop-tauri/src/lib/menuSelection.ts | 10 ++----
apps/desktop-tauri/src/lib/tauri.ts | 1 -
apps/desktop-tauri/src/lib/trayScale.ts | 2 --
apps/desktop-tauri/src/lib/traySizing.ts | 1 -
apps/desktop-tauri/src/styles.css | 19 +----------
.../src/surfaces/TrayPanel.test.tsx | 16 ---------
apps/desktop-tauri/src/surfaces/TrayPanel.tsx | 3 --
.../settings/tabs/DisplayTab.test.tsx | 1 -
23 files changed, 16 insertions(+), 181 deletions(-)
diff --git a/apps/desktop-tauri/src-tauri/src/commands/surface.rs b/apps/desktop-tauri/src-tauri/src/commands/surface.rs
index 45c54a47d4..2bcc47a0fe 100644
--- a/apps/desktop-tauri/src-tauri/src/commands/surface.rs
+++ b/apps/desktop-tauri/src-tauri/src/commands/surface.rs
@@ -28,9 +28,6 @@ pub fn dismiss_tray_panel(app: tauri::AppHandle) -> Result<(), String> {
crate::shell::flyout_window::hide(&app)
}
-/// Arm the gesture blur guard before a drag-reorder gesture starts its
-/// OLE modal loop, so the transient
-/// `Focused(false)` that loop produces doesn't auto-hide the flyout.
#[tauri::command]
pub fn begin_flyout_gesture(app: tauri::AppHandle) -> Result<(), String> {
let state = app
diff --git a/apps/desktop-tauri/src-tauri/src/commands/system.rs b/apps/desktop-tauri/src-tauri/src/commands/system.rs
index 4da4512bb0..26d6a20f9d 100644
--- a/apps/desktop-tauri/src-tauri/src/commands/system.rs
+++ b/apps/desktop-tauri/src-tauri/src/commands/system.rs
@@ -170,8 +170,6 @@ pub fn get_work_area_rect(app: tauri::AppHandle) -> Result
// ── Misc UX ────────────────────────────────────────────────────────────
-/// The Windows accent color as `#rrggbb`. The tray panel paints its menu
-/// selection with it, the way macOS menus use the system accent.
#[tauri::command]
pub fn get_system_accent_color() -> Option {
system_accent_abgr().map(abgr_to_hex)
diff --git a/apps/desktop-tauri/src-tauri/src/geometry_store.rs b/apps/desktop-tauri/src-tauri/src/geometry_store.rs
index 2a3bcd3d65..928970bfe7 100644
--- a/apps/desktop-tauri/src-tauri/src/geometry_store.rs
+++ b/apps/desktop-tauri/src-tauri/src/geometry_store.rs
@@ -1,8 +1,6 @@
//! Persistent window-geometry store for the Tauri desktop shell.
//!
-//! Remembers position (and size where applicable) for detached user surfaces:
-//! PopOut, Settings and the float bar. The tray-panel flyout remembers
-//! nothing: it is a fixed panel anchored to the tray on every open.
+//! Remembers position (and size where applicable) for detached user surfaces.
use std::fs;
use std::path::PathBuf;
@@ -49,8 +47,6 @@ fn geometry_path() -> Option {
/// Surface modes eligible for geometry persistence.
///
/// - `Hidden`: never remembered.
-/// - `TrayPanel`: never remembered; the flyout is a fixed panel anchored to
-/// the tray on every open.
/// - `PopOut` / `Settings`: user-movable, position + size remembered.
pub fn should_remember(mode: SurfaceMode) -> bool {
matches!(mode, SurfaceMode::PopOut | SurfaceMode::Settings)
diff --git a/apps/desktop-tauri/src-tauri/src/main.rs b/apps/desktop-tauri/src-tauri/src/main.rs
index a874c9f406..145962c8c3 100644
--- a/apps/desktop-tauri/src-tauri/src/main.rs
+++ b/apps/desktop-tauri/src-tauri/src/main.rs
@@ -399,11 +399,6 @@ fn main() {
{
return;
}
- // Gesture guard: ignore blur while an HTML5 drag-reorder
- // is running its OLE modal loop.
- // Windows produces a spurious Focused(false) the instant
- // such a loop starts even though the user never left the
- // window; see AppState::begin_gesture_blur_guard.
if let Some(st) = window.app_handle().try_state::>()
&& st
.lock()
diff --git a/apps/desktop-tauri/src-tauri/src/shell/dwm.rs b/apps/desktop-tauri/src-tauri/src/shell/dwm.rs
index 9b6ea667dc..7e0561c9fe 100644
--- a/apps/desktop-tauri/src-tauri/src/shell/dwm.rs
+++ b/apps/desktop-tauri/src-tauri/src/shell/dwm.rs
@@ -168,15 +168,13 @@ unsafe extern "system" fn borderless_subclass_proc(
}
}
-/// The native chrome a borderless window gets from DWM.
#[cfg(windows)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Chrome {
Dark,
/// Keeps `WS_THICKFRAME` so the native resize affordance still works.
DarkResizable,
- /// The tray flyout: a fixed light panel with Windows 11 rounded corners.
- /// Its erase color comes from the builder's `background_color`, which
+ /// The erase color comes from the builder's `background_color`, which
/// tao paints on `WM_ERASEBKGND`.
LightPanel,
}
@@ -190,23 +188,21 @@ const fn colorref(r: u8, g: u8, b: u8) -> u32 {
((b as u32) << 16) | ((g as u32) << 8) | r as u32
}
+#[cfg(windows)]
+const PANEL_HAIRLINE: u32 = colorref(0x8A, 0x8B, 0x8E);
+
#[cfg(windows)]
impl Chrome {
fn keeps_resize_frame(self) -> bool {
self == Self::DarkResizable
}
- /// `DWMWA_WINDOW_CORNER_PREFERENCE`, when the window asks for one.
fn corner_preference(self) -> Option {
(self == Self::LightPanel).then_some(DWMWCP_ROUND)
}
- /// `DWMWA_BORDER_COLOR`. DWM draws its border along the rounded corner,
- /// where the panel's CSS hairline is clipped, so both use the measured
- /// Mac hairline `#8A8B8E`. Without it the dark-mode border would ring
- /// the light panel.
fn border_color(self) -> Option {
- (self == Self::LightPanel).then_some(colorref(0x8A, 0x8B, 0x8E))
+ (self == Self::LightPanel).then_some(PANEL_HAIRLINE)
}
}
@@ -224,7 +220,6 @@ pub fn force_dark_caption_resizable(win: &tauri::WebviewWindow) {
apply_chrome(win, Chrome::DarkResizable);
}
-/// Borderless light panel with rounded corners, for the tray flyout only.
#[cfg(windows)]
pub fn light_panel_chrome(win: &tauri::WebviewWindow) {
apply_chrome(win, Chrome::LightPanel);
diff --git a/apps/desktop-tauri/src-tauri/src/shell/flyout_window.rs b/apps/desktop-tauri/src-tauri/src/shell/flyout_window.rs
index 6ec518e806..1ca79f05c2 100644
--- a/apps/desktop-tauri/src-tauri/src/shell/flyout_window.rs
+++ b/apps/desktop-tauri/src-tauri/src/shell/flyout_window.rs
@@ -1,20 +1,8 @@
-//! Detached "Pop Out Dashboard" flyout window: a fixed-width light panel,
-//! anchored to the tray, 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 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. Like the Mac menu, it always opens next to the tray (or the
-//! launch cursor) and can't be moved or resized; the frontend sets its height
-//! to fit the content.
-//!
-//! Structurally modeled on `crate::floatbar` (self-contained module owning
-//! its window + a `handle_window_event` hook dispatched from `main.rs`
-//! before the `main`-window-only handling); the window itself is built with
-//! `settings_window.rs`'s builder recipe (async open, manual DWM chrome
-//! pass, `WebviewUrl::App` with a `?window=` query marker).
+//! is retired.
use std::sync::Mutex;
use std::sync::atomic::{AtomicBool, Ordering};
@@ -29,8 +17,7 @@ use crate::surface::SurfaceMode;
pub const FLYOUT_LABEL: &str = "flyout";
-/// The panel fill, `--mac-panel-bg` in `styles.css`. The window paints it
-/// before the page loads and on every erase, so the panel never flashes dark.
+/// The window paints this before the page loads and on every erase.
const PANEL_FILL: tauri::utils::config::Color = tauri::utils::config::Color(0xDE, 0xDE, 0xE2, 0xFF);
/// Same window used to close a same-click blur-dismiss/reopen race as the
@@ -132,13 +119,11 @@ fn open_with_anchor(
.decorations(props.decorations)
.shadow(false)
.resizable(props.resizable)
- // No maximize box, so Windows never snaps or maximizes the panel.
.maximizable(false)
.always_on_top(settings.tray_panel_always_on_top)
.skip_taskbar(props.skip_taskbar)
- // WebView2 shares `prefers-color-scheme` across the process, so the
- // flyout stays pinned dark like the other windows; the panel's CSS
- // draws it light regardless.
+ // WebView2 shares `prefers-color-scheme` across the profile, so the
+ // flyout stays pinned dark like the other windows.
.theme(Some(tauri::Theme::Dark))
.background_color(PANEL_FILL)
// CRITICAL: dynamically-built windows default to drag-drop ENABLED,
@@ -340,11 +325,6 @@ pub fn handle_window_event(window: &tauri::Window, event: &tauri::WindowEvent) -
}
}
-/// 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.
pub fn reanchor(app: &AppHandle) -> Result<(), String> {
use crate::window_positioner::{PanelSize, Rect};
@@ -453,12 +433,6 @@ mod tests {
#[test]
fn tray_panel_window_properties_still_the_single_source_for_flyout_shape() {
- // `open_or_focus`'s builder reads size/decorations/resizable/
- // skip_taskbar from
- // `SurfaceMode::TrayPanel.window_properties()` directly (not
- // independent duplicated constants) — this pins down the values that
- // relationship depends on, so a change to `surface.rs` shows up here
- // instead of silently drifting from what the flyout actually builds.
let props = SurfaceMode::TrayPanel.window_properties();
assert_eq!(props.width, 310.0);
assert_eq!(props.height, 776.0);
diff --git a/apps/desktop-tauri/src-tauri/src/shell/window.rs b/apps/desktop-tauri/src-tauri/src/shell/window.rs
index 8ddf6e876f..0ae1237def 100644
--- a/apps/desktop-tauri/src-tauri/src/shell/window.rs
+++ b/apps/desktop-tauri/src-tauri/src/shell/window.rs
@@ -65,16 +65,6 @@ pub fn apply_window_layout(
}
if props.visible {
- // The flyout ("Pop Out Dashboard") window is sized entirely by the
- // frontend (content auto-fit at a fixed width) —
- // it is its own dedicated window now, never `main`, so this function
- // (which only ever runs against `main`'s window; see callers in
- // `shell/transition.rs`) needs no special-case for it. `main`'s
- // surface machine only ever holds Hidden/PopOut/Settings, none of
- // which race a frontend-owned size the way the old shared-window
- // TrayPanel mode did — so the backend is the sole size-setter here,
- // unconditionally, for every mode this function can be called with.
- //
// The canonical surface mode drives geometry restore — no brittle
// shape-matching of WindowProperties. logical_size_from_geometry
// falls back to the mode's default size for non-remembered modes.
diff --git a/apps/desktop-tauri/src-tauri/src/state.rs b/apps/desktop-tauri/src-tauri/src/state.rs
index 2a9f39c1f2..76e0c926d4 100644
--- a/apps/desktop-tauri/src-tauri/src/state.rs
+++ b/apps/desktop-tauri/src-tauri/src/state.rs
@@ -276,9 +276,6 @@ impl AppState {
self.flyout_reveal_pending.take()
}
- /// Arm the gesture blur guard for 15s. Called when the frontend reports
- /// a drag-reorder mousedown is about to start an OLE modal loop that will
- /// transiently blur the window.
pub fn begin_gesture_blur_guard(&mut self, now: std::time::Instant) {
self.gesture_blur_guard = Some((now, now + std::time::Duration::from_secs(15)));
}
diff --git a/apps/desktop-tauri/src-tauri/src/surface.rs b/apps/desktop-tauri/src-tauri/src/surface.rs
index bb413a4e35..9395af04ba 100644
--- a/apps/desktop-tauri/src-tauri/src/surface.rs
+++ b/apps/desktop-tauri/src-tauri/src/surface.rs
@@ -50,8 +50,6 @@ impl SurfaceMode {
// tray icon, auto-hides on click-outside (blur), and never shows in
// the taskbar. Its optional always-on-top setting is applied by the
// dedicated flyout window, which can read persisted settings.
- // Fixed 310px wide like the Mac menu; the frontend sizes the
- // height to its content and scales both by "Panel scale".
Self::TrayPanel => WindowProperties {
visible: true,
decorations: false,
@@ -277,7 +275,6 @@ mod tests {
#[test]
fn tray_panel_is_fixed_size_blur_dismiss_flyout() {
let props = SurfaceMode::TrayPanel.window_properties();
- // "Pop Out Dashboard" flyout: fixed size, anchored, auto-hide, no taskbar.
assert!(!props.resizable);
assert!(props.blur_dismiss);
assert!(!props.always_on_top);
diff --git a/apps/desktop-tauri/src-tauri/src/tray_bridge.rs b/apps/desktop-tauri/src-tauri/src/tray_bridge.rs
index bca684acbf..c1a50e9ac1 100644
--- a/apps/desktop-tauri/src-tauri/src/tray_bridge.rs
+++ b/apps/desktop-tauri/src-tauri/src/tray_bridge.rs
@@ -599,9 +599,6 @@ mod tests {
let action = resolve_menu_action("pop_out").expect("pop_out action");
assert!(matches!(action, MenuAction::OpenFlyout));
- // SurfaceMode::TrayPanel is retained purely as a data key (geometry
- // key / window_properties source / panel-size reference) for the
- // flyout window's builder. The panel is a fixed-width menu.
let props = SurfaceMode::TrayPanel.window_properties();
assert!(!props.resizable && props.blur_dismiss && props.skip_taskbar);
}
diff --git a/apps/desktop-tauri/src/components/MenuSurface.tsx b/apps/desktop-tauri/src/components/MenuSurface.tsx
index 9ec13766ab..256bb67ba8 100644
--- a/apps/desktop-tauri/src/components/MenuSurface.tsx
+++ b/apps/desktop-tauri/src/components/MenuSurface.tsx
@@ -32,9 +32,6 @@ function lineIcon(...paths: ReactNode[]) {
);
}
-/** 16px line icons after the SF Symbols the macOS menu uses
- * (key, chart.xyaxis.line, waveform.path.ecg, gearshape, info.circle,
- * xmark.rectangle). Refresh has no icon on the Mac. */
const ROW_ICONS: Record = {
switchAccount: lineIcon(
,
@@ -67,8 +64,6 @@ const ROW_ICONS: Record = {
interface MenuSurfaceProps {
summary?: ReactNode;
banner?: ReactNode;
- /** Footer rows in groups. A separator is drawn before each non-empty
- * group, like the separators between the macOS menu sections. */
footerGroups?: MenuFooterRow[][];
/** Inline style applied to the root `menu-surface` element (e.g. CSS
* `zoom` for the tray flyout). */
@@ -79,10 +74,6 @@ interface MenuSurfaceProps {
/**
* Flush, compact container for the tray panel (`TrayPanel`), the only
* dashboard layout. It renders in the tray-panel flyout window.
- *
- * Mirrors the upstream macOS `MenuContent`: a 310pt menu panel holding a
- * stack of full provider cards (`MenuCard`), one per enabled provider,
- * followed by the menu rows.
*/
export default function MenuSurface({
summary,
diff --git a/apps/desktop-tauri/src/hooks/useDraftNumber.ts b/apps/desktop-tauri/src/hooks/useDraftNumber.ts
index 983d08aabb..bc8406ce75 100644
--- a/apps/desktop-tauri/src/hooks/useDraftNumber.ts
+++ b/apps/desktop-tauri/src/hooks/useDraftNumber.ts
@@ -1,6 +1,5 @@
import { useCallback, useState } from "react";
-/** Local slider value that follows the saved setting and commits on release. */
export function useDraftNumber(value: number) {
const [draft, setDraft] = useState(value);
const [prev, setPrev] = useState(value);
@@ -11,11 +10,6 @@ export function useDraftNumber(value: number) {
const commit = useCallback(
(next: number, onCommit: (value: number) => void) => {
- // Dedupe against the committed prop value, which is the persisted
- // source of truth. The parent's save is fire-and-forget, so we can't
- // observe success/failure here — comparing to `value` (rather than an
- // optimistically-advanced marker) means a failed save leaves the prop
- // unchanged and a re-commit of the same number still fires the retry.
if (next === value) return;
onCommit(next);
},
diff --git a/apps/desktop-tauri/src/hooks/useTrayPanelController.ts b/apps/desktop-tauri/src/hooks/useTrayPanelController.ts
index 66e79b5997..1a69719eac 100644
--- a/apps/desktop-tauri/src/hooks/useTrayPanelController.ts
+++ b/apps/desktop-tauri/src/hooks/useTrayPanelController.ts
@@ -33,7 +33,6 @@ import {
const TRAY_INITIAL_REFRESH_DELAY_MS = 250;
const DENSE_OVERVIEW_THRESHOLD = 32;
-/** Which of a provider's web pages the backend can open. */
interface ProviderLinks {
providerId: string;
dashboard: boolean;
@@ -63,7 +62,6 @@ export function useTrayPanelController(state: BootstrapState) {
const { t } = useLocale();
const surfaceTarget = useSurfaceTarget("trayPanel");
- // Settings > Menu > Panel scale, applied as CSS zoom on the panel.
const trayScale = clampTrayScalePercent(settings.trayScalePercent) / 100;
const [appVersion, setAppVersion] = useState(null);
@@ -80,8 +78,6 @@ export function useTrayPanelController(state: BootstrapState) {
};
}, []);
- // The hidden panel window lives for the whole app run, so a focus re-read
- // is what picks up an accent changed in Windows settings.
const [accentColor, setAccentColor] = useState(null);
useEffect(() => {
let cancelled = false;
@@ -169,9 +165,6 @@ export function useTrayPanelController(state: BootstrapState) {
return [match];
}, [denseTrayProviders, sorted, selectedProviderId, gridExpanded]);
- // The account and link rows act on the selected provider, or on the only
- // one: macOS shows them on a provider tab, and with a single provider there
- // is no overview tab.
const actionProvider =
selectedProviderId !== null
? (providersById.get(selectedProviderId) ?? null)
@@ -198,7 +191,6 @@ export function useTrayPanelController(state: BootstrapState) {
cancelled = true;
};
}, [actionProviderId]);
- // Links resolved for a previously selected provider never show.
const links =
providerLinks !== null && providerLinks.providerId === actionProviderId
? providerLinks
diff --git a/apps/desktop-tauri/src/hooks/useTrayPanelLayout.sizing.test.tsx b/apps/desktop-tauri/src/hooks/useTrayPanelLayout.sizing.test.tsx
index 2ac0eadd54..21dc7b133d 100644
--- a/apps/desktop-tauri/src/hooks/useTrayPanelLayout.sizing.test.tsx
+++ b/apps/desktop-tauri/src/hooks/useTrayPanelLayout.sizing.test.tsx
@@ -81,8 +81,6 @@ function mountSurface(): void {
surface = document.querySelector(".menu-surface--tray")!;
}
-/** Drive the auto-fit measure: jsdom rects are 0, so scrollHeight dominates →
- * contentHeight = scrollHeight + 1 (measure pipeline, zoom=1). */
function setScrollHeight(px: number): void {
Object.defineProperty(surface, "scrollHeight", {
configurable: true,
@@ -227,8 +225,6 @@ describe("useTrayPanelLayout sizing", () => {
const lockedResizes = windowMocks.setSize.mock.calls.length;
const lockedAnchors = tauriMocks.reanchorTrayPanel.mock.calls.length;
- // Flip-down evidence (measure 542→543): detected, suppressed, and the DOM
- // constraint stays the RETAINED height — never the smaller candidate.
await nudgePass(result, 542, "549px");
expect(windowMocks.setSize.mock.calls.length).toBe(lockedResizes);
expect(tauriMocks.reanchorTrayPanel.mock.calls.length).toBe(lockedAnchors);
@@ -309,8 +305,6 @@ describe("useTrayPanelLayout sizing", () => {
it("sizes the window at 310 px times the Panel scale and leaves the spare height above the footer out", async () => {
const body = surface.querySelector(".menu-surface__body")!;
const footer = surface.querySelector(".menu-surface__footer")!;
- // WebView2 at zoom 1.5: rects are rendered px, scrollHeight is local px,
- // and the filled surface parks 201 px of spare height above the footer.
surface.getBoundingClientRect = () =>
({ top: 0, bottom: 800, height: 800 }) as DOMRect;
body.getBoundingClientRect = () =>
diff --git a/apps/desktop-tauri/src/hooks/useTrayPanelLayout.ts b/apps/desktop-tauri/src/hooks/useTrayPanelLayout.ts
index f3231b098c..afbe9f7d9b 100644
--- a/apps/desktop-tauri/src/hooks/useTrayPanelLayout.ts
+++ b/apps/desktop-tauri/src/hooks/useTrayPanelLayout.ts
@@ -12,20 +12,18 @@ import {
type TrayAutoFitState,
} from "../lib/traySizing";
-/** The macOS 0.70 menu width (tray-card-spec.md section 1) at 100% Panel scale. */
const TRAY_BASE_WIDTH = 310;
const TRAY_MAX_MEASURE_HEIGHT = 920;
const TRAY_OVERVIEW_MIN_HEIGHT = 200;
const TRAY_DETAIL_MIN_HEIGHT = 420;
const TRAY_DENSE_OVERVIEW_HEIGHT = 776;
+const TRAY_HEIGHT_SLACK = 1;
export interface TrayPanelLayoutOptions {
canMeasure: boolean;
denseOverview: boolean;
detailMode: boolean;
layoutKey: string;
- /** Panel scale as a factor: the CSS `zoom` TrayPanel puts on the surface.
- * The window width and height follow it. Defaults to 1. */
zoom?: number;
}
@@ -34,10 +32,6 @@ export interface TrayPanelLayout {
requestLayout: () => void;
}
-/**
- * Fits the tray flyout window to its content: a fixed width of 310 px times
- * the Panel scale, and a height measured from the rendered surface.
- */
export function useTrayPanelLayout({
canMeasure,
denseOverview,
@@ -50,12 +44,8 @@ export function useTrayPanelLayout({
const layoutReadyRef = useRef(false);
const resizeRunRef = useRef(0);
const layoutTimerRef = useRef(undefined);
- // The window's actual physical size after our last resize. Win32 may snap
- // a requested size, and lib/traySizing compares candidates against it.
const lastSizeRef = useRef<{ width: number; height: number } | null>(null);
const programmaticInFlightRef = useRef(0);
- // Sizing decision state (committed frame, one-frame history, learned
- // oscillation pair). The #261 cycle detection lives in lib/traySizing.
const sizingStateRef = useRef(EMPTY_AUTOFIT_STATE);
const applySize = useCallback(async (size: LogicalSize): Promise => {
@@ -82,10 +72,6 @@ export function useTrayPanelLayout({
const surface = document.querySelector(".menu-surface--tray");
if (!surface || typeof ResizeObserver === "undefined") return;
const observer = new ResizeObserver(() => {
- // Measuring temporarily removes the surface/body constraints, which
- // resizes the observed elements. Do not feed that programmatic change
- // back into another pass or the capped flyout flashes between its
- // measured and committed layouts forever.
if (
layoutReadyRef.current &&
programmaticInFlightRef.current > 0
@@ -94,8 +80,6 @@ export function useTrayPanelLayout({
}
requestLayout();
});
- // The surface fills the window, so content changes show up on the body
- // and the footer rather than on the surface itself.
for (const part of [
surface,
surface.querySelector(".menu-surface__body"),
@@ -182,12 +166,8 @@ export function useTrayPanelLayout({
}
};
- // Every resize this pass causes, and any that arrive shortly after,
- // must not start another pass. The trailing delay absorbs late events.
programmaticInFlightRef.current += 1;
try {
- // Text wraps at the window width, so the width has to be in place
- // before the height is measured.
const committed = sizingStateRef.current.committed;
if (!layoutReadyRef.current || committed?.width !== width) {
const height =
@@ -208,9 +188,7 @@ export function useTrayPanelLayout({
// WebView2 reports bounding rects in rendered px but scrollHeight in
// the zoomed surface's local px (measured on Edge 154), so the local
- // value is scaled before the two are compared. The surface fills the
- // window and parks spare height above the footer; that gap is not
- // content. The extra 1 px absorbs DPI rounding of the window height.
+ // value is scaled before the two are compared.
const surfaceRect = surface.getBoundingClientRect();
const bodyRect = body?.getBoundingClientRect();
const footerRect = footer?.getBoundingClientRect();
@@ -223,7 +201,7 @@ export function useTrayPanelLayout({
surfaceRect.bottom,
);
const height = Math.min(
- Math.max(Math.ceil(contentBottom - spare) + 1, minHeight),
+ Math.max(Math.ceil(contentBottom - spare) + TRAY_HEIGHT_SLACK, minHeight),
maxHeight,
);
@@ -239,8 +217,6 @@ export function useTrayPanelLayout({
expectedWidth: width,
minHeight,
maxHeight,
- // WebView layout px ↔ Win32 physical px ratio; CSS zoom does not
- // affect it.
scaleFactor: window.devicePixelRatio,
zoom,
lastAppliedPhysicalHeight: lastSizeRef.current?.height ?? null,
@@ -248,7 +224,6 @@ export function useTrayPanelLayout({
sizingStateRef.current,
);
sizingStateRef.current = decision.state;
- // A zoomed element's max-height renders at value × zoom.
surface.style.maxHeight = `${decision.height / zoom}px`;
committedHeight = true;
diff --git a/apps/desktop-tauri/src/lib/menuSelection.ts b/apps/desktop-tauri/src/lib/menuSelection.ts
index ab430ef33d..4e8d4586ca 100644
--- a/apps/desktop-tauri/src/lib/menuSelection.ts
+++ b/apps/desktop-tauri/src/lib/menuSelection.ts
@@ -2,21 +2,15 @@ import type { CSSProperties } from "react";
const HEX_COLOR = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i;
-/** WCAG 2 linear value of one sRGB channel byte. */
-function linearChannel(byte: number): number {
+function srgbByteToLinear(byte: number): number {
const c = byte / 255;
return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
}
-/**
- * Menu selection colors from the Windows accent, the way macOS menus use the
- * system accent. White text stays only where it keeps 4.5:1 contrast. Where
- * white fails, black clears 4.67:1 on any accent.
- */
export function accentSelectionStyle(accent: string | null): CSSProperties {
const match = accent ? HEX_COLOR.exec(accent) : null;
if (!match) return {};
- const [r, g, b] = match.slice(1).map((hex) => linearChannel(parseInt(hex, 16)));
+ const [r, g, b] = match.slice(1).map((hex) => srgbByteToLinear(parseInt(hex, 16)));
const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
const whiteContrast = 1.05 / (luminance + 0.05);
return {
diff --git a/apps/desktop-tauri/src/lib/tauri.ts b/apps/desktop-tauri/src/lib/tauri.ts
index a843d6e9e0..67e5f1bf88 100644
--- a/apps/desktop-tauri/src/lib/tauri.ts
+++ b/apps/desktop-tauri/src/lib/tauri.ts
@@ -550,7 +550,6 @@ export function revealTrayPanelWindow(): Promise {
return invoke("reveal_tray_panel_window");
}
-/** The Windows accent color as `#rrggbb`, or null when Windows has none. */
export function getSystemAccentColor(): Promise {
return invoke("get_system_accent_color");
}
diff --git a/apps/desktop-tauri/src/lib/trayScale.ts b/apps/desktop-tauri/src/lib/trayScale.ts
index 2936c3d2bb..ef8878157e 100644
--- a/apps/desktop-tauri/src/lib/trayScale.ts
+++ b/apps/desktop-tauri/src/lib/trayScale.ts
@@ -1,5 +1,3 @@
-/** Panel scale bounds in percent. The backend clamps `trayScalePercent` to
- * the same range when it loads or saves settings. */
export const TRAY_SCALE_MIN = 100;
export const TRAY_SCALE_MAX = 200;
export const TRAY_SCALE_STEP = 5;
diff --git a/apps/desktop-tauri/src/lib/traySizing.ts b/apps/desktop-tauri/src/lib/traySizing.ts
index cf53edcc71..136800e523 100644
--- a/apps/desktop-tauri/src/lib/traySizing.ts
+++ b/apps/desktop-tauri/src/lib/traySizing.ts
@@ -74,7 +74,6 @@ export const EMPTY_AUTOFIT_STATE: TrayAutoFitState = {
export interface TraySizingInput {
/** Measured content height, logical px, post zoom-scale, pre clamp. */
measuredHeight: number;
- /** Logical window width: 310 px times the Panel scale. */
expectedWidth: number;
minHeight: number;
maxHeight: number;
diff --git a/apps/desktop-tauri/src/styles.css b/apps/desktop-tauri/src/styles.css
index 173ff27a6e..6f78479c4e 100644
--- a/apps/desktop-tauri/src/styles.css
+++ b/apps/desktop-tauri/src/styles.css
@@ -1046,9 +1046,7 @@ html {
/* Hide the panel during the initial resize + reposition dance so the
user never sees the window jump around. Once layout is measured and
the window is anchored in its final position, .tray-panel-reveal--ready
- fades the content in smoothly. The page behind the panel carries the
- panel fill (--mac-panel-bg, and PANEL_FILL in shell/flyout_window.rs) so
- no other color shows while the panel is hidden. */
+ fades the content in smoothly. */
:root {
--mac-panel-bg: #dedee2;
}
@@ -1067,9 +1065,6 @@ body:has(.tray-panel-reveal),
opacity: 1;
transition: opacity 80ms ease-out;
}
-/* The window keeps a height floor, so it can be taller than the content.
- The panel fills the window and parks the spare height above the footer,
- which keeps the footer rows next to the tray. */
.tray-panel-reveal .menu-surface--tray {
flex-grow: 1;
}
@@ -3383,10 +3378,6 @@ html:has(.menu-surface--tray) {
overflow: visible;
}
-/* Tray panel: a fixed light panel after the macOS 0.70 menu material
- (tray-card-spec.md section 1) under every app theme. The wrapper sets
- data-theme="light"; tokens that only the dark theme block defines get
- tray values here so theme dark, light and auto render the same panel. */
.menu-surface--tray {
--mac-font: "Segoe UI Variable Text", "Segoe UI Variable", "Segoe UI", system-ui, sans-serif;
--mac-text-headline: 600 13px/16px var(--mac-font);
@@ -3412,7 +3403,6 @@ html:has(.menu-surface--tray) {
--mac-pace-bad: #ff3b30;
--mac-marker: rgba(0, 0, 0, 0.58);
--mac-accent: #007aff;
- /* TrayPanel overrides this inline with the Windows accent color. */
--mac-selection-bg: #0078d4;
--mac-selection-text: #fff;
@@ -3457,8 +3447,6 @@ html:has(.menu-surface--tray) {
--mac-menu-row-h: 22px;
--mac-row-pl: 15px;
--mac-row-icon: 16px;
- /* The Mac code says 4.5pt, but the 0.70 shots put footer labels 38px from
- the panel edge, which a 6px gap after the 16px icon reproduces. */
--mac-row-icon-gap: 6px;
--mac-shortcut-w: 44px;
@@ -3536,14 +3524,11 @@ html:has(.menu-surface--tray) {
box-sizing: border-box;
overflow: visible;
}
-/* Selected card: no highlight plate, like the macOS menu card. */
.menu-surface--tray .menu-stack__item--selected {
border: none;
border-radius: 6px;
padding: 4px 4px;
}
-/* The panel is light under every app theme. These base rules paint white
- tints meant for a dark surface, so the tray gets dark tints instead. */
.menu-surface--tray .menu-card__copy-btn:hover {
background: rgba(0, 0, 0, 0.06);
}
@@ -4529,8 +4514,6 @@ html:has(.menu-surface--tray) {
font-size: 13px;
}
-/* Footer rows follow the macOS menu: 22px rows (24px for Refresh), a 16px
- icon column, a 44px shortcut column, and an inset selection plate. */
.menu-surface__footer {
display: flex;
flex-direction: column;
diff --git a/apps/desktop-tauri/src/surfaces/TrayPanel.test.tsx b/apps/desktop-tauri/src/surfaces/TrayPanel.test.tsx
index 86fbb2f366..19d69ad9a8 100644
--- a/apps/desktop-tauri/src/surfaces/TrayPanel.test.tsx
+++ b/apps/desktop-tauri/src/surfaces/TrayPanel.test.tsx
@@ -231,7 +231,6 @@ function renderTrayPanel(
);
}
-/** The pages `get_provider_detail` reports for a provider with both links. */
function providerLinks(id: string) {
return {
dashboardUrl: `https://${id}.example/usage`,
@@ -338,12 +337,6 @@ describe("TrayPanel provider grid", () => {
});
it("reveals regardless of the shared surface-mode snapshot (TrayPanel now runs in its own dedicated window)", async () => {
- // TrayPanel is now hosted exclusively in the dedicated `flyout` OS
- // window (see App.tsx's isFlyoutWindow() routing), so it must not depend
- // on `main`'s surface-mode machine to know it's "open" — that machine
- // can report something other than "trayPanel". Overriding the snapshot
- // mock to another mode confirms the reveal gate is not wired to
- // useSurfaceMode() at all.
tauriMocks.getCurrentSurfaceState.mockResolvedValue({
mode: "settings",
target: { kind: "settings", tab: "general" },
@@ -474,7 +467,6 @@ describe("TrayPanel provider grid", () => {
await waitFor(() => {
expect(container.querySelector(".tray-panel-reveal--ready")).not.toBeNull();
});
- // The overview of several providers has no provider to act on.
expect(screen.queryByRole("button", { name: "Usage Dashboard" })).toBeNull();
expect(container.querySelectorAll(".menu-surface__footer-sep")).toHaveLength(1);
@@ -881,10 +873,6 @@ describe("TrayPanel provider grid", () => {
});
it("renders the default tray panel layout with no legacy window chrome", async () => {
- // Pins the one dashboard layout: tray-variant surface, icon-first
- // provider switcher, and the Refresh / Settings... / About / Quit rows
- // with their Ctrl shortcuts. The retired PopOut layout had a "CodexBar"
- // title bar with window controls.
const { container } = renderTrayPanel([
provider("claude", "Claude", 35),
provider("codex", "Codex", 20),
@@ -1072,8 +1060,6 @@ describe("TrayPanel provider grid", () => {
trayScalePercent: 150,
});
- // Width 310 × 1.5 = 465. Height 505 × 1.5 = 757.5, rounded up to 758,
- // plus 1 px for DPI rounding = 759.
await waitFor(() => {
expect(setSize).toHaveBeenCalledWith(
expect.objectContaining({ width: 465, height: 759 }),
@@ -1082,8 +1068,6 @@ describe("TrayPanel provider grid", () => {
first.unmount();
setSize.mockClear();
- // Same zoom, taller content: 700 × 1.5 + 1 = 1051 exceeds the mocked
- // work-area cap (900 - 16 = 884), so the clamp still wins.
scrollHeight.mockReturnValue(700);
renderTrayPanel([provider("codex", "Codex", 61)], {
trayScalePercent: 150,
diff --git a/apps/desktop-tauri/src/surfaces/TrayPanel.tsx b/apps/desktop-tauri/src/surfaces/TrayPanel.tsx
index 013376b486..d9888e2ced 100644
--- a/apps/desktop-tauri/src/surfaces/TrayPanel.tsx
+++ b/apps/desktop-tauri/src/surfaces/TrayPanel.tsx
@@ -19,9 +19,6 @@ import { filterUsageSpendSummaryForOverview } from "../lib/usageSpendSharing";
* Tray popover surface — two modes like macOS CodexBar:
* 1. Overview (default): provider grid + all cards stacked
* 2. Detail: click a provider in grid → show only that provider's card
- *
- * The panel is a light menu under every app theme, like the macOS menu, so
- * the wrappers pin `data-theme="light"`.
*/
export default function TrayPanel({ state }: { state: BootstrapState }) {
const stayAwakeHeld = useStayAwakeStatus();
diff --git a/apps/desktop-tauri/src/surfaces/settings/tabs/DisplayTab.test.tsx b/apps/desktop-tauri/src/surfaces/settings/tabs/DisplayTab.test.tsx
index 679ded5d32..8cff53314d 100644
--- a/apps/desktop-tauri/src/surfaces/settings/tabs/DisplayTab.test.tsx
+++ b/apps/desktop-tauri/src/surfaces/settings/tabs/DisplayTab.test.tsx
@@ -60,7 +60,6 @@ describe("DisplayTab menu settings", () => {
it("shows the saved Panel scale and saves the new one when the slider is released", async () => {
const set = vi.fn();
- // Settles the tab's tray-visibility read before the slider re-renders it.
await act(async () => {
render(
Date: Sat, 10 Oct 2026 20:18:52 +0700
Subject: [PATCH 08/10] Test that a failed Panel scale save reports and reverts
---
apps/desktop-tauri/src/hooks/useSettings.test.tsx | 15 +++++++++++++++
1 file changed, 15 insertions(+)
diff --git a/apps/desktop-tauri/src/hooks/useSettings.test.tsx b/apps/desktop-tauri/src/hooks/useSettings.test.tsx
index aa79b350c1..dd9afb1664 100644
--- a/apps/desktop-tauri/src/hooks/useSettings.test.tsx
+++ b/apps/desktop-tauri/src/hooks/useSettings.test.tsx
@@ -222,6 +222,21 @@ describe("useSettings update", () => {
expect(result.current.settings.windowScalePercent).toBe(150);
});
+ it("reports a failed Panel scale save and reverts to the saved value", async () => {
+ const onDisk = { trayScalePercent: 100 } as unknown as SettingsSnapshot;
+ tauriMocks.getSettingsSnapshot.mockResolvedValue(onDisk);
+ tauriMocks.updateSettings.mockRejectedValueOnce(new Error("disk busy"));
+ const { result } = renderHook(() => useSettings(onDisk));
+ await act(async () => {});
+
+ await act(async () => {
+ await result.current.update({ trayScalePercent: 150 });
+ });
+
+ expect(result.current.error).toBe("disk busy");
+ expect(result.current.settings.trayScalePercent).toBe(100);
+ });
+
it("keeps the resolved shortcut map until the response supplies it", async () => {
tauriMocks.updateSettings.mockReturnValueOnce(new Promise(() => {}));
const initial = {
From a372eafbe4698f8a7738fac3b275bd24914efcb3 Mon Sep 17 00:00:00 2001
From: RCD <90105158+Finesssee@users.noreply.github.com>
Date: Sat, 10 Oct 2026 20:18:52 +0700
Subject: [PATCH 09/10] Drop stale resize and stored-size wording from flyout
docs
---
apps/desktop-tauri/src-tauri/src/commands/surface.rs | 5 ++---
apps/desktop-tauri/src-tauri/src/state.rs | 2 +-
2 files changed, 3 insertions(+), 4 deletions(-)
diff --git a/apps/desktop-tauri/src-tauri/src/commands/surface.rs b/apps/desktop-tauri/src-tauri/src/commands/surface.rs
index 2bcc47a0fe..65c5b8ff1b 100644
--- a/apps/desktop-tauri/src-tauri/src/commands/surface.rs
+++ b/apps/desktop-tauri/src-tauri/src/commands/surface.rs
@@ -75,9 +75,8 @@ pub async fn open_flyout_window(app: tauri::AppHandle) -> Result<(), String> {
}
/// Reveal the flyout window after the frontend's first layout pass. Called by
-/// `useTrayPanelLayout` once content has been measured/auto-fit (or the
-/// remembered fixed size re-applied), so Windows never shows a pre-measure
-/// blank/backing frame.
+/// `useTrayPanelLayout` once content has been measured and auto-fit, so
+/// Windows never shows a pre-measure 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
diff --git a/apps/desktop-tauri/src-tauri/src/state.rs b/apps/desktop-tauri/src-tauri/src/state.rs
index 76e0c926d4..32e438e520 100644
--- a/apps/desktop-tauri/src-tauri/src/state.rs
+++ b/apps/desktop-tauri/src-tauri/src/state.rs
@@ -167,7 +167,7 @@ pub struct AppState {
/// One-shot permission for frontend layout code to reveal a newly opened
/// flyout, carrying whether the revealed window may take focus.
pub flyout_reveal_pending: Option,
- /// Active while a user gesture (resize drag, HTML5 drag-reorder) is
+ /// Active while a user gesture (the provider tab HTML5 drag-reorder) is
/// running a Win32 modal loop that transiently steals focus from the
/// WebView2 child. `(began, until)` — `until` is the hard expiry;
/// `began` lets a genuine refocus clear the guard early once the
From c9d3683b1a9728c16d466eed00c1ec31ff981cde Mon Sep 17 00:00:00 2001
From: RCD <90105158+Finesssee@users.noreply.github.com>
Date: Sat, 10 Oct 2026 20:48:37 +0700
Subject: [PATCH 10/10] Keep tray auto-fit passes from restoring each other's
overrides
---
.../hooks/useTrayPanelLayout.sizing.test.tsx | 29 +++++++++++++++++++
.../src/hooks/useTrayPanelLayout.ts | 27 +++++++++++++----
2 files changed, 50 insertions(+), 6 deletions(-)
diff --git a/apps/desktop-tauri/src/hooks/useTrayPanelLayout.sizing.test.tsx b/apps/desktop-tauri/src/hooks/useTrayPanelLayout.sizing.test.tsx
index 21dc7b133d..f4578071b9 100644
--- a/apps/desktop-tauri/src/hooks/useTrayPanelLayout.sizing.test.tsx
+++ b/apps/desktop-tauri/src/hooks/useTrayPanelLayout.sizing.test.tsx
@@ -200,6 +200,35 @@ describe("useTrayPanelLayout sizing", () => {
);
});
+ it("leaves no measuring overrides behind when style feedback overlaps the first pass", async () => {
+ vi.stubGlobal("ResizeObserver", StyleFeedbackResizeObserver);
+ setScrollHeight(1_200);
+ const html = document.documentElement;
+ const body = surface.querySelector(".menu-surface__body")!;
+ const stack = surface.querySelector(".menu-stack")!;
+
+ const { result } = renderHook(() => useTrayPanelLayout(hookProps()));
+ await waitFor(() => expect(result.current.layoutReady).toBe(true), {
+ timeout: 3000,
+ });
+ await act(async () => {
+ await new Promise((resolve) => window.setTimeout(resolve, 1_000));
+ });
+
+ expect(feedbackObserverCallbacks).toBeGreaterThan(0);
+ expect(lastResize()).toEqual({ width: 310, height: 884 });
+ expect(surface.style.maxHeight).toBe("884px");
+ expect(surface.style.minHeight).toBe("");
+ expect(surface.style.height).toBe("");
+ expect(surface.style.overflow).toBe("");
+ expect(body.style.overflow).toBe("");
+ expect(body.style.flex).toBe("");
+ expect(stack.style.overflow).toBe("");
+ expect(document.body.style.overflow).toBe("");
+ expect(document.body.style.minHeight).toBe("");
+ expect(html.style.overflow).toBe("");
+ });
+
it("commits stable small changes, locks the reporter pair on the larger member, tracks retained height in the DOM", async () => {
setScrollHeight(538); // → 539 logical → 674 physical
const { result } = renderHook(() => useTrayPanelLayout(hookProps()));
diff --git a/apps/desktop-tauri/src/hooks/useTrayPanelLayout.ts b/apps/desktop-tauri/src/hooks/useTrayPanelLayout.ts
index afbe9f7d9b..7267f83357 100644
--- a/apps/desktop-tauri/src/hooks/useTrayPanelLayout.ts
+++ b/apps/desktop-tauri/src/hooks/useTrayPanelLayout.ts
@@ -43,6 +43,7 @@ export function useTrayPanelLayout({
const [layoutRevision, setLayoutRevision] = useState(0);
const layoutReadyRef = useRef(false);
const resizeRunRef = useRef(0);
+ const activeResizeRef = useRef>(Promise.resolve());
const layoutTimerRef = useRef(undefined);
const lastSizeRef = useRef<{ width: number; height: number } | null>(null);
const programmaticInFlightRef = useRef(0);
@@ -72,12 +73,9 @@ export function useTrayPanelLayout({
const surface = document.querySelector(".menu-surface--tray");
if (!surface || typeof ResizeObserver === "undefined") return;
const observer = new ResizeObserver(() => {
- if (
- layoutReadyRef.current &&
- programmaticInFlightRef.current > 0
- ) {
- return;
- }
+ // Measuring lifts the surface/body constraints, which resizes the
+ // observed parts. Feeding that back would restart the pass forever.
+ if (programmaticInFlightRef.current > 0) return;
requestLayout();
});
for (const part of [
@@ -110,6 +108,23 @@ export function useTrayPanelLayout({
const resize = async () => {
const run = ++resizeRunRef.current;
+ // A superseded pass can still be suspended with its measuring
+ // overrides applied; wait for it so `previous` below is never those.
+ const prior = activeResizeRef.current;
+ let finished!: () => void;
+ activeResizeRef.current = new Promise((resolve) => {
+ finished = resolve;
+ });
+ try {
+ await prior;
+ if (run !== resizeRunRef.current) return;
+ await measure(run);
+ } finally {
+ finished();
+ }
+ };
+
+ const measure = async (run: number) => {
const surface = document.querySelector(".menu-surface--tray");
if (!surface) return;
const html = document.documentElement;