Skip to content

feat(desktop): Warpライクなウィンドウ透過 (macOS、デフォルトOFF)#184

Merged
MocA-Love merged 17 commits intomainfrom
feat/window-vibrancy
Apr 15, 2026
Merged

feat(desktop): Warpライクなウィンドウ透過 (macOS、デフォルトOFF)#184
MocA-Love merged 17 commits intomainfrom
feat/window-vibrancy

Conversation

@MocA-Love
Copy link
Copy Markdown
Owner

@MocA-Love MocA-Love commented Apr 15, 2026

概要

Warpターミナルのような半透明ウィンドウを macOS ネイティブの vibrancy (NSVisualEffectView) で実装。デフォルト OFF で opt-in。Appearance 設定に新セクションを追加し、有効/無効・不透明度・ブラー強度をリアルタイムで切り替えられる。

スコープ

  • macOS のみ の機能。Windows / Linux ではトグルが非表示になり、`transparent: true` も付与しないため既存挙動を変えない (Wayland での黒画面を回避)。
  • 既存の `backdrop-filter` を使う箇所は触っていない (Electron issue #39529 の共存問題はあるが、現行の `bg-*/95` + `backdrop-blur-sm` はほぼ不透明のため実害は小さいと判断。問題が確認されたら後続 PR で対応)。
  • webview (BrowserPane) は Chromium の合成制約で不透明のまま残る。UI コピーでこの仕様を明示。

実装内容

Main プロセス

  • `apps/desktop/src/shared/vibrancy-types.ts` — Electron 非依存の型と定数 (`VibrancyState`, `VibrancyBlurLevel`, `DEFAULT_VIBRANCY_STATE`, opacity 範囲)。main/renderer/schemas から安全に参照できる
  • `apps/desktop/src/main/lib/vibrancy/index.ts` — `applyVibrancy` / `getInitialWindowOptions` / `computeBackgroundColor` / `normalizeVibrancyState`
  • `apps/desktop/src/main/lib/vibrancy/emitter.ts` — tRPC subscription 用の EventEmitter
  • `apps/desktop/src/main/windows/main.ts` / `apps/desktop/src/main/lib/window-manager/index.ts` — メインおよび tearoff ウィンドウ生成時に macOS のみ `transparent: true` + 初期 vibrancy options を spread
  • `apps/desktop/src/main/lib/app-state/schemas.ts` — `AppState` に `vibrancyState` フィールドを追加 (lowdb JSON に永続化、drizzle migration 不要)
  • `apps/desktop/src/main/lib/app-state/index.ts` — `ensureValidShape` で新フィールドをマージ

tRPC

  • `apps/desktop/src/lib/trpc/routers/vibrancy.ts` — `get` / `set` / `getSupported` / `onChanged` (observable パターン必須 — `apps/desktop/AGENTS.md` 準拠)
  • `apps/desktop/src/lib/trpc/routers/index.ts` — 登録

Renderer

  • `apps/desktop/src/renderer/stores/vibrancy/store.ts` — zustand store。in-flight promise で StrictMode 二重 hydrate を防止、subscription に `onError` 付与、optimistic update とロールバック
  • `apps/desktop/src/renderer/index.tsx` — 起動時に `hydrate()` を先行実行
  • `apps/desktop/src/renderer/globals.css` — `:root[data-vibrancy="on"]` で `--background` / `--card` / `--muted` / `--sidebar` 等を rgba 化。`--vibrancy-alpha` を JS から差し替え。`min()` / `max()` で calc() が範囲外にならないようにクランプ。ダーク/ライト両対応
  • `apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/components/VibrancySection/` — 新 Appearance セクション (Switch + Slider + Select)

動的変更の仕組み

macOS で `transparent: true` は生成時しか指定できないため、常に true で生成し、オフ状態では不透明な `backgroundColor` を使う。ユーザーがトグルを操作すると tRPC mutation が走り、main 側で `setBackgroundColor(rgba)` と `setVibrancy(type)` を呼び出して即時反映。ウィンドウ再生成は不要。

既存機能への影響

  • `forceRepaint` (GPU crash 時の compositor 再構築) と vibrancy は別レイヤーで競合なし (Electron PR #40109 で修正済み)
  • テーマ切替 (Ember ↔ Light) は CSS 変数の書き換えのみで、vibrancy 設定との衝突なし
  • デフォルト OFF なので既存ユーザーは明示的に有効化しない限り挙動変化なし

セルフレビュー済み項目

  • 型安全性 (`any` ゼロ、shared 型による main/renderer 間の drift 防止)
  • レイヤー境界 (schemas は `shared/` 経由で型参照、`main/lib/vibrancy` の Electron 依存は読み込まない)
  • observable パターン遵守
  • StrictMode 下での hydrate 二重実行ガード
  • 非 macOS での `transparent: true` 非付与
  • CSS calc() の negative / >1 alpha 発生防止

テスト計画

  • macOS で Appearance 設定を開き、有効トグル → ウィンドウ全体がブラー越しに透ける
  • 不透明度スライダーを動かすと即座に反映される
  • ブラー強度セレクトで material 種類が切り替わる (subtle/standard/strong/ultra)
  • 設定が再起動後も保持される
  • テーマ切替 (Ember ↔ Light) が vibrancy と衝突しない
  • Tearoff ウィンドウにも同じ vibrancy 設定が伝播する
  • Windows / Linux でトグルが非表示になり、起動時に黒画面にならない
  • BrowserPane (webview) は不透明のまま残り、UI の説明と一致する

Summary by CodeRabbit

リリースノート

  • 新機能
    • macOSのウィンドウビブランシー設定を外観設定に追加しました
    • 有効/無効の切り替えオプションを提供
    • 不透明度とぼかしレベルの調整機能を実装
    • ビブランシー有効時にターミナルの透明性をサポート
    • ビブランシーの設定を永続化

macOS only, default OFF. Adds a new Appearance section with an enable
toggle, opacity slider, and blur-level select. Backed by NSVisualEffectView
via BrowserWindow.setVibrancy + setBackgroundColor(rgba), dynamically
switchable without recreating the window. BrowserPane (webview) stays
opaque due to Chromium compositor constraints — documented in the UI copy.

- main: new lib/vibrancy module (types in shared/ for layer-safety)
- main: transparent:true + initial vibrancy spread into main and tearoff windows on macOS only; no-op on Win/Linux to avoid the Wayland black-window pitfall
- state: AppState.vibrancyState persisted via app-state (lowdb JSON)
- trpc: new vibrancy router with observable onChanged subscription
- renderer: vibrancy zustand store with hydrate-once guard, subscription
  cleanup, optimistic updates, and early bootstrap in index.tsx
- css: :root[data-vibrancy="on"] overrides background/card/muted/sidebar
  to rgba variants controlled by --vibrancy-alpha; uses min/max() to
  clamp calc() derivations safely
- ui: new VibrancySection in Appearance settings; disabled on non-macOS
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 15, 2026

Warning

Rate limit exceeded

@MocA-Love has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 32 minutes and 38 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 32 minutes and 38 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 333ff2ab-ded0-4d8f-85f2-69edcdd19d49

📥 Commits

Reviewing files that changed from the base of the PR and between 6066d72 and 044d1c6.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (19)
  • apps/desktop/package.json
  • apps/desktop/runtime-dependencies.ts
  • apps/desktop/src/lib/trpc/routers/vibrancy.ts
  • apps/desktop/src/main/lib/vibrancy/index.ts
  • apps/desktop/src/main/lib/window-manager/index.ts
  • apps/desktop/src/main/windows/main.ts
  • apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/AppearanceSettings.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/components/VibrancySection/VibrancySection.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/utils/settings-search/settings-search.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/SpreadsheetViewer/SpreadsheetDiffViewer.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/SpreadsheetViewer/SpreadsheetViewer.tsx
  • apps/desktop/src/renderer/stores/vibrancy/store.ts
  • apps/desktop/src/shared/vibrancy-types.ts
  • packages/macos-window-blur/.gitignore
  • packages/macos-window-blur/binding.gyp
  • packages/macos-window-blur/index.d.ts
  • packages/macos-window-blur/index.js
  • packages/macos-window-blur/package.json
  • packages/macos-window-blur/src/addon.mm
📝 Walkthrough

ウォークスルー

このPRはmacOSウィンドウビブランシー機能をElectronデスクトップアプリケーションに追加します。共有型定義、主プロセスビブランシーライブラリ、アプリ状態の永続化、TRPCルーター、レンダラーストア、CSS変数、設定UIをサポートしており、ターミナルと各エディタで透明度効果を実現します。

変更内容

Cohort / File(s) 概要
共有ビブランシー型定義
apps/desktop/src/shared/vibrancy-types.ts
VibrancyStateVibrancyBlurLevel型とDEFAULT_VIBRANCY_STATE定数を定義。不透明度スライダー境界値も含む。
主プロセスビブランシーライブラリ
apps/desktop/src/main/lib/vibrancy/index.ts, apps/desktop/src/main/lib/vibrancy/emitter.ts
ビブランシー支援チェック、状態正規化、背景色計算、型解決、ウィンドウ適用、イベントエミッターを実装。
アプリ状態スキーマ
apps/desktop/src/main/lib/app-state/schemas.ts, apps/desktop/src/main/lib/app-state/index.ts
AppStatevibrancyStateフィールドを追加し、デフォルト値を設定。永続化ロジックを統合。
ウィンドウマネージャー統合
apps/desktop/src/main/lib/window-manager/index.ts, apps/desktop/src/main/windows/main.ts
ビブランシー状態からウィンドウ初期オプション(背景色、ビブランシー型)を導出し、メイン・ティアオフウィンドウ作成に適用。
TRPCビブランシーRouter
apps/desktop/src/lib/trpc/routers/vibrancy.ts, apps/desktop/src/lib/trpc/routers/index.ts
getSupportedgetsetonChangedエンドポイントを公開。TRPC層でビブランシー制御を一元化。
レンダラーストア&状態管理
apps/desktop/src/renderer/stores/vibrancy/store.ts, apps/desktop/src/renderer/stores/vibrancy/index.ts, apps/desktop/src/renderer/stores/index.ts
ZustandベースのuseVibrancyStoreでDOM更新、プレビュー、TRPC永続化を処理。useEffectiveTerminalThemeでテーマ着色を提供。
グローバルCSS&テーマ変数
apps/desktop/src/renderer/globals.css
data-vibrancy="on"でビブランシーモード有効化。RGBA背景でmacOSぼかし効果をサポート。
設定UIビブランシーセクション
apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/AppearanceSettings.tsx, apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/components/VibrancySection/VibrancySection.tsx, apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/components/VibrancySection/index.ts
ビブランシー有効化スイッチ、不透明度スライダー、ぼかし強度スライダーを提供。macOS限定で表示。
ターミナルレンダリング
apps/desktop/src/renderer/lib/terminal/terminal-runtime.ts, apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx, apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/config.ts, apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/hooks/useTerminalAppearance/useTerminalAppearance.ts
xterm allowTransparencyを有効化。ターミナルテーマをビブランシーストアから取得。
コードエディター&ビューア
apps/desktop/src/renderer/screens/main/components/WorkspaceView/components/CodeEditor/CodeEditor.tsx, apps/desktop/src/renderer/screens/main/components/WorkspaceView/components/CodeEditor/createCodeMirrorTheme.ts, apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/CodeMirrorDiffViewer/CodeMirrorDiffViewer.tsx, apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/ConflictViewer/ConflictViewer.tsx
ビブランシー不透明度をcreateCodeMirrorThemeに渡し、テーマをcolor-mixで半透明化。
ファイルビューア背景
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/FileViewerContent/FileViewerContent.tsx, apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/SpreadsheetViewer/SpreadsheetViewer.tsx, apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/SpreadsheetViewer/SpreadsheetDiffViewer.tsx
背景をbg-whiteからbg-backgroundに変更。テーマ対応の色変数を使用。
レンダラー初期化
apps/desktop/src/renderer/index.tsx
ビブランシーストアhydrate()を起動時に実行。

シーケンス図

sequenceDiagram
    participant User as ユーザー
    participant Settings as 設定画面
    participant Store as VibrancyStore
    participant TRPC as TRPC Router
    participant Main as メインプロセス
    participant WM as WindowManager
    participant Window as BrowserWindow

    User->>Settings: ビブランシー設定を変更
    Settings->>Store: setState({enabled, opacity, ...})
    activate Store
    Store->>Store: DOM更新<br/>(data-vibrancy, CSS変数)
    Store->>TRPC: set(state)
    deactivate Store
    
    activate TRPC
    TRPC->>Main: normalizeVibrancyState(state)
    TRPC->>Main: appState.vibrancyStateに永続化
    TRPC->>Main: broadcastVibrancy()
    deactivate TRPC
    
    activate Main
    Main->>WM: 全ウィンドウ取得
    WM->>WM: 各ウィンドウに対して
    Main->>Window: applyVibrancy()<br/>(背景色、vibrancy型設定)
    deactivate Main
    
    activate Window
    Window->>Window: イベント発行<br/>(VIBRANCY_CHANGED)
    deactivate Window
    
    TRPC->>Store: onChanged購読から<br/>イベント受信
    Store->>Settings: 画面更新反映
    Settings->>User: 新しい外観表示
Loading

推定コードレビュー工数

🎯 4 (複雑) | ⏱️ ~60 分

関連している可能性のあるPR

🐰✨ ビブランシーの魔法がやってきた
macOSの窓をぼかして、透明に
ズートピアもかくやと、設定画面で輝く
ウサギがお祝い、コードが踊る
アルファ値のリズムに乗せて 🎨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed タイトルは変更内容の主要な機能(macOS向けウィンドウ透化)と対応プラットフォーム、デフォルト状態を明確に記述しており、PR全体の目的を適切に要約しています。
Description check ✅ Passed 説明は概要、スコープ、実装内容、動的変更の仕組み、既存機能への影響、自己レビュー項目、テスト計画を含む包括的な構成となっており、テンプレートの主要項目をカバーしています。

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/window-vibrancy

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 00c9a3da0e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/desktop/src/renderer/stores/vibrancy/store.ts Outdated
- slider: drive --vibrancy-alpha directly during drag and defer persistence
  to onValueCommit so a single interaction no longer produces dozens of
  app-state writes and IPC broadcasts
- store: coerce enabled=false on unsupported platforms before applying to
  DOM, both on initial hydrate and on subsequent subscription broadcasts
The Appearance/theme store writes --background, --sidebar, --muted, etc.
directly onto documentElement.style via applyUIColors. Inline styles
always win the CSS cascade, so the :root[data-vibrancy="on"] rules in
globals.css were silently shadowed — toggling the slider did nothing.

Fix: the vibrancy store now derives translucent variants from the active
theme via color-mix() and writes them back onto documentElement.style
itself, at the same specificity level. When vibrancy is turned off we
reapply the theme's solid colors to restore the original palette.

Also subscribe to theme changes so switching themes while vibrancy is on
re-derives the overlay against the new palette, and expose a
previewOpacity() action that lets the slider drive live updates without
hitting tRPC on every tick.
- xterm: set allowTransparency on both terminal constructors (main and
  v2 pane) so the WebGL renderer honours rgba background colors
- new useEffectiveTerminalTheme hook that derives a terminal theme
  with an rgba-mixed background when vibrancy is active, and switch
  both terminal consumers over to it
- overlay: drop the +/-0.1 and +/-0.05 deltas so every chrome surface
  shares the same alpha — the old variance made sidebars and cards
  feel much more opaque than the main background
- default opacity: 60 -> 35 so the factory setting already feels like
  vibrancy rather than "slightly translucent"
The previous overlay gave --background an rgba value, so any nested
container with bg-background (the file viewer card, the terminal pane
wrapper, etc) stacked the same translucent tint onto itself and showed
up as a visibly darker rectangle inside the window.

Let the window's own setBackgroundColor(rgba) be the single source of
truth for the base tint and set --background to `transparent` instead.
Chrome-specific surfaces (card, sidebar, muted, popover) stay tinted so
they remain distinct from the transparent body, and get a small alpha
bias so they're still legible at very low opacity settings.

Also convert the blur-strength Select into a slider that snaps across
four internal buckets (subtle/standard/strong/ultra). Feels continuous
to match the opacity slider while still mapping onto the discrete
NSVisualEffectView materials.
- FileViewerContent / SpreadsheetViewer / SpreadsheetDiffViewer: drop
  hardcoded bg-white, bg-[#0d0d0d], and #d0d0d0 in favor of bg-background
  and var(--border) so the file viewer can pick up the vibrancy overlay
- createCodeMirrorTheme: new optional vibrancyOpacity param that mixes
  editor background + gutter background with `transparent` via color-mix,
  plumbed through CodeEditor, CodeMirrorDiffViewer, and ConflictViewer
  via a fresh useVibrancyStore read
- globals.css: defensive .xterm overlay override that forces nested
  xterm viewport / screen / canvas backgrounds to transparent when
  vibrancy is on, so the rgba theme.background reaches the compositor
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (4)
apps/desktop/src/renderer/stores/vibrancy/store.ts (2)

124-140: モジュールレベル変数は HMR で問題になる可能性があります。

hydratePromisesubscriptionEstablishedthemeSubscriptionEstablished がモジュールスコープで定義されています。開発環境での Hot Module Replacement 時に、これらの変数がリセットされずに残り、subscription の二重登録や hydration のスキップが発生する可能性があります。

本番環境では問題ありませんが、開発体験向上のため、HMR cleanup を検討してください。

📝 HMR 対応の例
// 開発環境でのみ HMR cleanup を追加
if (import.meta.hot) {
  import.meta.hot.dispose(() => {
    hydratePromise = null;
    subscriptionEstablished = false;
    themeSubscriptionEstablished = false;
  });
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/stores/vibrancy/store.ts` around lines 124 - 140,
The module-level flags hydratePromise, subscriptionEstablished, and
themeSubscriptionEstablished can persist across HMR updates and cause duplicate
subscriptions or skipped hydration; add an HMR dispose handler that resets these
variables to their initial values so state is cleared on hot reload (use
import.meta.hot.dispose and reset hydratePromise = null, subscriptionEstablished
= false, themeSubscriptionEstablished = false). Place the dispose logic near the
top-level where these symbols and
ensureThemeSubscription/useThemeStore.subscribe are defined so it runs during
module teardown in development only.

172-188: subscription のクリーンアップが欠けています。

electronTrpcClient.vibrancy.onChanged.subscribe の戻り値(unsubscribe 関数)が保持されていません。現在のシングルトン store 設計では実際にはリークしませんが、defensive coding として unsubscribe 関数を保持することを推奨します。

📝 クリーンアップ対応案
+let vibrancyUnsubscribe: (() => void) | null = null;
+
 // Inside hydrate function:
 if (!subscriptionEstablished) {
   subscriptionEstablished = true;
-  electronTrpcClient.vibrancy.onChanged.subscribe(undefined, {
+  vibrancyUnsubscribe = electronTrpcClient.vibrancy.onChanged.subscribe(undefined, {
     onData: (incoming) => {
       // ...
     },
     onError: (err) => {
       console.error("[vibrancy] subscription error:", err);
       subscriptionEstablished = false;
+      vibrancyUnsubscribe = null;
     },
   });
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/stores/vibrancy/store.ts` around lines 172 - 188,
The subscription created by electronTrpcClient.vibrancy.onChanged.subscribe is
not retained so it can't be unsubscribed; capture its unsubscribe function
(e.g., store it in a module-scoped variable like subscriptionUnsubscribe
alongside subscriptionEstablished) when subscribing in the vibrancy store, call
that unsubscribe and set subscriptionEstablished = false in the onError handler
and provide a public cleanup method on the store (e.g.,
dispose/unsubscribe/cleanup) that invokes the stored unsubscribe if present and
clears the variable; update applyToDom/onData flow to still use
set(effectiveIncoming) but ensure the unsubscribe is called from the store's
lifecycle or when tearing down the renderer.
apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/components/VibrancySection/VibrancySection.tsx (1)

151-165: ブラー強度スライダーにドラッグ中のプレビューがありません。

不透明度スライダーには onValueChange でライブプレビューがありますが、ブラー強度スライダーには onValueCommit のみで、ドラッグ中の視覚的フィードバックがありません。離散的な4段階なので大きな問題ではありませんが、一貫性のため検討してください。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/components/VibrancySection/VibrancySection.tsx`
around lines 151 - 165, The vibrancy blur slider (in VibrancySection) only
updates on commit via onValueCommit, so add an onValueChange handler that
mirrors onValueCommit behavior to provide live preview during drag: in the
Slider component add onValueChange={(values) => { const value = values[0]; if
(typeof value !== "number") return; const nextLevel =
sliderValueToBlurLevel(value); if (nextLevel !== blurLevel) { void setState({
blurLevel: nextLevel }); } }} so the temporary slider value is converted with
sliderValueToBlurLevel and applied via setState for live feedback while keeping
the existing onValueCommit logic intact.
apps/desktop/src/renderer/globals.css (1)

141-157: ライトモードの rgb() 関数のフォーマットを修正してください。

Stylelint が / 演算子の後の改行をエラーとして報告しています。CSS Color Level 4 の構文としては有効ですが、一貫性のためダークモードのブロック(Lines 119-139)と同様に rgba() 関数を使用するか、改行なしで記述することを検討してください。

📝 フォーマット修正案
 :root[data-vibrancy="on"].light {
 	--background: rgb(255 255 255 / var(--vibrancy-alpha));
 	--card: rgb(255 255 255 / min(1, calc(var(--vibrancy-alpha) + 0.1)));
 	--popover: rgb(255 255 255 / 0.95);
 	--muted: rgb(247 247 247 / min(1, calc(var(--vibrancy-alpha) + 0.1)));
 	--accent: rgb(247 247 247 / min(1, calc(var(--vibrancy-alpha) + 0.1)));
 	--sidebar: rgb(251 251 251 / max(0, calc(var(--vibrancy-alpha) - 0.05)));
-	--sidebar-accent: rgb(
-		247 247 247 /
-		min(1, calc(var(--vibrancy-alpha) + 0.05))
-	);
+	--sidebar-accent: rgb(247 247 247 / min(1, calc(var(--vibrancy-alpha) + 0.05)));
 	--tertiary: rgb(243 243 243 / max(0, calc(var(--vibrancy-alpha) - 0.05)));
-	--tertiary-active: rgb(
-		233 233 233 /
-		min(1, calc(var(--vibrancy-alpha) + 0.05))
-	);
+	--tertiary-active: rgb(233 233 233 / min(1, calc(var(--vibrancy-alpha) + 0.05)));
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/globals.css` around lines 141 - 157, The rgb(... /
...) color declarations in the :root[data-vibrancy="on"].light block (variables
like --background, --card, --popover, --muted, --accent, --sidebar,
--sidebar-accent, --tertiary, --tertiary-active) must be reformatted to avoid
the line-break after the "/" that Stylelint flags; update them to match the dark
block's style by either converting to rgba(r, g, b, alpha) calls or writing the
rgb(...) with the /alpha on a single line (no newline between the slash and the
alpha expression) so the CSS passes linting and remains consistent with the
dark-mode declarations.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/desktop/src/main/lib/vibrancy/index.ts`:
- Around line 117-123: The code unnecessarily casts null when calling
window.setVibrancy; remove the if/else and the cast so that the value returned
from resolveVibrancyType (typed as "sidebar" | "header" | "content" |
"fullscreen-ui" | null) is passed directly to BrowserWindow.setVibrancy. Replace
the block that checks vibrancyType and uses null as unknown as
Parameters<BrowserWindow["setVibrancy"]>[0] with a single call to
window.setVibrancy(vibrancyType), keeping resolveVibrancyType and vibrancyType
as-is.

In
`@apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/AppearanceSettings.tsx`:
- Line 71: VibrancySection is always rendered because it lacks the same
visibility guard used by other sections; update the render so VibrancySection
only mounts when the search-filtered visibleItems includes "vibrancy" (use the
same visibleItems check pattern used for other sections) — locate the
VibrancySection key="vibrancy" line and wrap it with the visibleItems.contains/
includes conditional used elsewhere in AppearanceSettings so it only renders
when visibleItems indicates it should be visible.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/ConflictViewer/ConflictViewer.tsx`:
- Around line 226-229: activeTheme/vibrancyOpacity changes currently rebuild the
entire EditorView (created via createCodeMirrorTheme), causing editor
re-creation and loss of unsaved content because the file-sync effect only
depends on fileData; instead refactor so the EditorView is created once and
theme updates use a CodeMirror Compartment: create a Compartment for the theme
near where EditorView is instantiated, install the initial theme extension using
createCodeMirrorTheme(activeTheme, ..., { vibrancyOpacity }), and on
vibrancyOpacity or activeTheme change call
compartment.reconfigure(newThemeExtension) (or use view.dispatch to apply the
reconfigure) rather than re-creating the EditorView; keep the fileData sync
effect dependency as-is so it runs after editor is mounted.

---

Nitpick comments:
In `@apps/desktop/src/renderer/globals.css`:
- Around line 141-157: The rgb(... / ...) color declarations in the
:root[data-vibrancy="on"].light block (variables like --background, --card,
--popover, --muted, --accent, --sidebar, --sidebar-accent, --tertiary,
--tertiary-active) must be reformatted to avoid the line-break after the "/"
that Stylelint flags; update them to match the dark block's style by either
converting to rgba(r, g, b, alpha) calls or writing the rgb(...) with the /alpha
on a single line (no newline between the slash and the alpha expression) so the
CSS passes linting and remains consistent with the dark-mode declarations.

In
`@apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/components/VibrancySection/VibrancySection.tsx`:
- Around line 151-165: The vibrancy blur slider (in VibrancySection) only
updates on commit via onValueCommit, so add an onValueChange handler that
mirrors onValueCommit behavior to provide live preview during drag: in the
Slider component add onValueChange={(values) => { const value = values[0]; if
(typeof value !== "number") return; const nextLevel =
sliderValueToBlurLevel(value); if (nextLevel !== blurLevel) { void setState({
blurLevel: nextLevel }); } }} so the temporary slider value is converted with
sliderValueToBlurLevel and applied via setState for live feedback while keeping
the existing onValueCommit logic intact.

In `@apps/desktop/src/renderer/stores/vibrancy/store.ts`:
- Around line 124-140: The module-level flags hydratePromise,
subscriptionEstablished, and themeSubscriptionEstablished can persist across HMR
updates and cause duplicate subscriptions or skipped hydration; add an HMR
dispose handler that resets these variables to their initial values so state is
cleared on hot reload (use import.meta.hot.dispose and reset hydratePromise =
null, subscriptionEstablished = false, themeSubscriptionEstablished = false).
Place the dispose logic near the top-level where these symbols and
ensureThemeSubscription/useThemeStore.subscribe are defined so it runs during
module teardown in development only.
- Around line 172-188: The subscription created by
electronTrpcClient.vibrancy.onChanged.subscribe is not retained so it can't be
unsubscribed; capture its unsubscribe function (e.g., store it in a
module-scoped variable like subscriptionUnsubscribe alongside
subscriptionEstablished) when subscribing in the vibrancy store, call that
unsubscribe and set subscriptionEstablished = false in the onError handler and
provide a public cleanup method on the store (e.g., dispose/unsubscribe/cleanup)
that invokes the stored unsubscribe if present and clears the variable; update
applyToDom/onData flow to still use set(effectiveIncoming) but ensure the
unsubscribe is called from the store's lifecycle or when tearing down the
renderer.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a1aa87af-4039-43a7-97ec-bae50947c65b

📥 Commits

Reviewing files that changed from the base of the PR and between ac93ee1 and 6066d72.

📒 Files selected for processing (28)
  • apps/desktop/src/lib/trpc/routers/index.ts
  • apps/desktop/src/lib/trpc/routers/vibrancy.ts
  • apps/desktop/src/main/lib/app-state/index.ts
  • apps/desktop/src/main/lib/app-state/schemas.ts
  • apps/desktop/src/main/lib/vibrancy/emitter.ts
  • apps/desktop/src/main/lib/vibrancy/index.ts
  • apps/desktop/src/main/lib/window-manager/index.ts
  • apps/desktop/src/main/windows/main.ts
  • apps/desktop/src/renderer/globals.css
  • apps/desktop/src/renderer/index.tsx
  • apps/desktop/src/renderer/lib/terminal/terminal-runtime.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/hooks/useTerminalAppearance/useTerminalAppearance.ts
  • apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/AppearanceSettings.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/components/VibrancySection/VibrancySection.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/appearance/components/AppearanceSettings/components/VibrancySection/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/CodeMirrorDiffViewer/CodeMirrorDiffViewer.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/ConflictViewer/ConflictViewer.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/FileViewerContent/FileViewerContent.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/SpreadsheetViewer/SpreadsheetDiffViewer.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/SpreadsheetViewer/SpreadsheetViewer.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/config.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/components/CodeEditor/CodeEditor.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/components/CodeEditor/createCodeMirrorTheme.ts
  • apps/desktop/src/renderer/stores/index.ts
  • apps/desktop/src/renderer/stores/vibrancy/index.ts
  • apps/desktop/src/renderer/stores/vibrancy/store.ts
  • apps/desktop/src/shared/vibrancy-types.ts

Comment thread apps/desktop/src/main/lib/vibrancy/index.ts Outdated
@MocA-Love MocA-Love marked this pull request as draft April 15, 2026 19:01
Adds a new @superset/macos-window-blur workspace package modelled on
@superset/macos-process-metrics. Its Objective-C++ addon walks the
BrowserWindow's NSView hierarchy to find Electron's NSVisualEffectView
and attaches a CIGaussianBlur filter to the backing CALayer, giving us
a 1-unit continuous blur slider instead of the four NSVisualEffectView
material presets.

- packages/macos-window-blur: new native addon, binding.gyp with
  Cocoa / QuartzCore / CoreImage frameworks, graceful fallback when
  the binary fails to load
- runtime-dependencies.ts: materialize + asarUnpack the addon so
  electron-builder bundles it like the existing macos-process-metrics
- shared/vibrancy-types: add blurRadius field (0-100) and the
  MIN/MAX constants used by the slider
- main/lib/vibrancy: applyVibrancy now also calls setWindowBlurRadius
  via the addon, passing 0 when vibrancy is disabled to clear the
  override. isNativeContinuousBlurSupported gates the UI toggle
- trpc router: getSupported returns nativeBlurSupported, set accepts
  blurRadius inputs
- renderer store: new nativeBlurSupported flag, carries blurRadius
  through hydrate/set/optimistic paths
- VibrancySection: shows a continuous ブラー半径 slider when the native
  addon is available; falls back to the 4-step material selector
  otherwise so the feature stays usable when the .node build was
  skipped
Radix Slider's `value` is fully controlled, so a slider without an
`onValueChange` handler ignores drag input and the thumb appears
pinned to the computed blurLevel bucket. Mirror the opacity-slider
pattern: hold a draft value in local state while dragging and only
commit to the store in `onValueCommit`. Also reflect the drag value in
the label so users see which bucket they're about to land in.
Two bugs discovered after landing the native addon:

1. Slider had no visible effect. NSVisualEffectView's backing layer is
   a CABackdropLayer subclass on macOS 11+, and that class ignores
   `backgroundFilters` because the system manages the backdrop
   separately — instead, `filters` is interpreted as the backdrop
   filter stack on backdrop-style layers. Detect CABackdropLayer via
   runtime class lookup and write the CIGaussianBlur into `filters`
   when the layer is backdrop-style, falling back to backgroundFilters
   otherwise. Also setNeedsDisplay after mutating the filter stack so
   Core Animation re-renders the frame.

2. Value wasn't persisted across restarts. Older on-disk app-state
   files (written before blurRadius existed) were round-tripped back
   through the router as a partial VibrancyState without blurRadius,
   so the mutation ended up writing `undefined` and the next restart
   landed on whatever normalizeVibrancyState defaulted to. Spread the
   stored state over DEFAULT_VIBRANCY_STATE inside getCurrentState so
   callers always see a complete shape.
The previous CIFilter + vev.layer attempt was a silent no-op:
NSVisualEffectView's real gaussian blur lives several sublayers deep
on a CABackdropLayer instance, and that layer's filter stack is
populated with `CAFilter` (private) instances — not CIFilter. Writing
a CIFilter onto an outer CALayer is accepted by the setter but never
rendered by the backdrop pipeline.

Rewrite the addon to:

1. Walk down into the NSVisualEffectView's layer tree (via
   FindLayerByClassName) to locate the actual CABackdropLayer.
2. Look up the private `CAFilter` class at runtime and call
   `+filterWithType:@"gaussianBlur"` through a performSelector shim,
   keeping the binary compilable on SDKs that don't expose CAFilter.
3. Edit the existing gaussianBlur filter in place (creating it when
   absent) instead of replacing the whole filter stack, so we keep
   the system's own filter pipeline intact.
4. Cache the original inputRadius via an associated object so callers
   can pass radius <= 0 to restore the default material feel.
5. Wrap the mutation in a CATransaction with actions disabled plus a
   flush, so Core Animation commits the new radius immediately.

With this in place the slider finally moves the real backdrop radius
instead of shadowing an unused filter on the outer layer.
Add opt-in logging at every layer so we can see what the native blur
addon is actually doing when the slider has no visible effect:

- addon.mm: VDBG macro that writes to stderr when the env var is set,
  covering window/contentView/NSVisualEffectView lookup, the vev.layer
  class name, a sublayer dump when CABackdropLayer can't be found, and
  the final inputRadius read back from the filter after mutation
- packages/macos-window-blur/index.js: log load success/failure plus
  every setWindowBlurRadius call and its boolean result
- main/lib/vibrancy/index.ts: console.log the full state snapshot
  going into applyVibrancy and the addon's return value
- main/windows/main.ts: re-apply vibrancy on did-finish-load so the
  addon runs once the window actually has an on-screen NSVisualEffectView
  (important for the first launch path where no mutation fires)

All logging is gated on `SUPERSET_VIBRANCY_DEBUG=1`.
Drop the SUPERSET_VIBRANCY_DEBUG gate in the addon, the Node loader, and
applyVibrancy so the trace lines fire on every run. Debugging is still
opt-out with a quick revert, but the user does not need to set an env
var before reproducing the native-blur issue.
Debug trace revealed the addon *was* finding the CABackdropLayer and
writing the requested inputRadius onto the existing gaussianBlur
CAFilter, but Core Animation never re-rendered the window. The reason
is that mutating a property on a CAFilter instance after it is already
installed on a layer does not fire any KVO / display-invalidation path,
because CALayer only observes the `filters` array itself.

Fix: replace the existing gaussianBlur filter with a brand-new CAFilter
instance on every call and reassign backdrop.filters so the layer's
property observer runs and the backdrop pipeline re-renders with the
new radius. Also call setNeedsDisplay on the backdrop as a belt-and-
braces trigger.

Also: NSVisualEffectView builds its CABackdropLayer lazily one runloop
tick after setVibrancy is called, so the very first toggle used to
miss the layer and silently fail. Retry setWindowBlurRadius a few
times with exponential backoff from applyVibrancy whenever the initial
call comes back false — subsequent interactions already worked, only
the first frame needed this treatment.
…rwrites

NSVisualEffectView occasionally rewrites its own backdrop filter stack
in response to internal state changes (focus, material refresh, the
lazy CABackdropLayer construction that happens the first frame after
setVibrancy is called). A single call to setWindowBlurRadius can land
before that happens and then get silently reverted, which shows up as
"the slider didn't take effect this time".

Instead of a conditional retry only on failure, always schedule a
short burst of re-applies on the next few runloop ticks (0ms, 16ms,
64ms, 180ms). Each call is cheap, only touches the filters array via
a KVO-firing reassignment, and keeps us ahead of any system overwrite.

Also on the native side:
- Dump the existing filter names/types in the debug trace so future
  regressions can show which filter stack we're mutating.
- After replacing backdrop.filters, call setNeedsDisplay + setNeedsLayout
  and then displayIfNeeded so Core Animation commits the new radius
  synchronously rather than waiting for the next vsync.
Previous burst-retry logic scheduled 4 timers per applyVibrancy call
that each closed over the radius at schedule time. When the user
dragged the blur slider quickly the old radius' retries (up to 180ms
later) would land after a newer radius had already been applied, so
the layer would snap back to an earlier value and the visual wouldn't
match the slider position.

Track one BlurSchedule per window in a WeakMap that carries both the
latest requested radius and the list of still-pending timers. On each
applyVibrancy call we update latestRadius, clearTimeout all prior
retries, and reschedule a fresh burst. Every timer now reads from the
shared latestRadius, so no matter how often the user drags, the
window only ever ends up showing the most recently committed value.

Also stretch the retry schedule to 16/64/180/480/960ms so slower
NSVisualEffectView refresh events (focus changes landing seconds
later) still get corrected.
- native addon: drop the VDBG macro, #include <stdio.h>, and all
  dispatch_block stderr traces so the macOS binary no longer writes to
  stderr on every setWindowBlurRadius call (rv-pr items [2][3][7])
- @superset/macos-window-blur/index.js: remove the success-path
  console.log added while debugging the initial integration
- main/lib/vibrancy/index.ts: delete the vdbg() helper and every
  console.log call site; keep the scheduleNativeBlur retry loop
  otherwise unchanged
- main/windows/main.ts: subscribe to nativeTheme.on("updated") so
  macOS dark/light appearance changes re-run applyVibrancy against
  the latest isDark; the previous implementation pinned the
  BrowserWindow background color to whatever isDark was at the last
  mutation, which left the tint stale on system theme transitions
  when the user picked the "system" theme (rv-pr item [13])
- SpreadsheetViewer / SpreadsheetDiffViewer: replace the remaining
  hardcoded #d0d0d0 / #f5f5f5 / #c0c0c0 / #f0f0f0 / superset-sh#666 / superset-sh#999 /
  #fafafa / #e0e0e0 / #f8f8f8 / superset-sh#888 cells, borders, and text colors
  with var(--border) / var(--muted) / var(--muted-foreground) so the
  spreadsheet grid honours both dark mode and the vibrancy overlay
  (rv-pr item [15] plus the neighbouring SpreadsheetViewer cells
  found while fixing it)
@MocA-Love MocA-Love marked this pull request as ready for review April 15, 2026 20:26
@MocA-Love
Copy link
Copy Markdown
Owner Author

@codex review

@MocA-Love
Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 15, 2026

✅ Actions performed

Full review triggered.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b367cb0a68

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/desktop/src/main/lib/window-manager/index.ts
Comment thread apps/desktop/src/renderer/stores/vibrancy/store.ts Outdated
CodeRabbit / Codex raised five issues against the latest commits:

- vibrancy/index.ts: drop the \`null as unknown as ...\` cast on
  \`window.setVibrancy\`. Electron 30+ types \`setVibrancy\` as
  \`string | null\`, so the raw value from resolveVibrancyType can
  pass through directly and we can collapse the if/else into a
  single call.

- AppearanceSettings.tsx / settings-search catalog: register a new
  \`APPEARANCE_VIBRANCY\` setting id with appropriate search keywords
  and wire \`showVibrancy = isItemVisible(...)\` so the vibrancy
  section participates in the settings search filter like the
  neighbouring sections.

- ConflictViewer.tsx: drop the \`vibrancyOpacity\` wiring entirely.
  The init useEffect's guarded cleanup (view.destroy) ran whenever
  vibrancyOpacity changed, tearing down and re-creating the
  EditorView while the content-sync effect (keyed on \`fileData\`)
  did not re-run — so the re-created editor kept an empty doc and
  unsaved merge-conflict edits could be lost. ConflictViewer is a
  transient merge-assist surface so we can just skip vibrancy here;
  the other two CodeMirror hosts still use proper Compartment
  reconfigure.

- window-manager/index.ts: call applyVibrancy inside the tearoff
  window's did-finish-load handler, mirroring MainWindow. Without
  it, newly opened tearoff windows stuck with the default
  NSVisualEffectView blur until the user bumped the slider, because
  the native addon relies on the NSVisualEffectView being on-screen
  before it can find the CABackdropLayer.

- stores/vibrancy/store.ts: memoize the return value of
  \`useEffectiveTerminalTheme\` via useMemo([base, enabled, opacity]).
  Terminal.tsx runs \`xterm.options.theme = terminalTheme\` inside an
  effect keyed on the theme identity, so a fresh object on every
  render used to force repeated xterm reconfiguration on unrelated
  renders.
@MocA-Love MocA-Love merged commit ffeae54 into main Apr 15, 2026
14 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.

1 participant