fix(desktop): dedupe Linux tray D-Bus calls to stop main-thread freeze (#2459)

The tray controller called setToolTip and setContextMenu unconditionally on
every snapshot push. On Linux both are synchronous D-Bus calls into
plasmashell's StatusNotifierItem host. useTraySync debounced pushes at 120ms
(up to ~8/sec during token streaming), so each push made 2 blocking D-Bus
round-trips even when only dockBadgeCount changed or a token streamed into
an already-listed session — i.e., tooltip and menu content didn't change.
The native block has no JS-level log, is intermittent (depends on plasmashell
load), and eventually freezes the Electron main thread until crash.

setTitle and setImage were already deduped; setToolTip and setContextMenu
were the gap. Mirror the existing lastX pattern:

- tray.mjs: dedupe setToolTip (exact string compare via lastTooltip) and
  setContextMenu (via lastMenuKey, a lightweight signature of menu-affecting
  fields: sessions/approvals/usage). menuKey is a string concat, cheaper than
  buildMenu itself, so skipping buildMenu when the key matches also saves
  work. Cache-after-call ordering matches lastTitle/lastTooltip so a throw
  forces a retry rather than skipping one. destroy() resets both new vars.
- useTraySync.ts: FLUSH_DEBOUNCE_MS 120 -> 500. Tray doesn't need sub-second
  updates; approvals are rare discrete events that flush through the debounce
  and the main app UI stays instant via SSE/stores.

The first change is the real fix; the second is a complement. Either alone
helps; together they eliminate the freeze under streaming.
This commit is contained in:
pablogonzalez
2026-07-27 11:51:22 +03:00
committed by GitHub
parent e52d692ac0
commit 8aaef3dec1
2 changed files with 33 additions and 5 deletions
+3 -3
View File
@@ -39,7 +39,7 @@ import type { QuestionRequest } from '@/types/question';
const TRAY_ACTION_EVENT = 'openchamber:tray-action';
// Event-driven updates do the real work; this is just a slow safety net.
const POLL_INTERVAL_MS = 5000;
const FLUSH_DEBOUNCE_MS = 120;
const FLUSH_DEBOUNCE_MS = 500;
// Pull the full cross-project session list periodically. SSE keeps the active
// directory instant; this catches sessions created in directories this client
// never opened (other worktrees, other projects, the TUI, …).
@@ -468,8 +468,8 @@ export const useTraySync = (): void => {
};
// Coalesce bursts (e.g. token-by-token streaming updates a store rapidly)
// into a single push, while staying near-instant for discrete events like
// a new session appearing.
// into at most one push per FLUSH_DEBOUNCE_MS; discrete events surface within
// that window. The main app UI stays instant via SSE/stores.
const scheduleFlush = () => {
if (disposed || flushTimer !== null) return;
flushTimer = window.setTimeout(() => {