Skip to content

feat(window): hide taskbar/Dock and pin on top while the assistant is running - #99

Merged
alpha5611331 merged 2 commits into
mainfrom
feat/running-window-surface
Aug 13, 2026
Merged

feat(window): hide taskbar/Dock and pin on top while the assistant is running#99
alpha5611331 merged 2 commits into
mainfrom
feat/running-window-surface

Conversation

@alpha5611331

Copy link
Copy Markdown
Member

Closes #98

What

Two window surfaces were tied to stealth mode alone. Both now follow stealth OR running:

  • Taskbar button (Windows) / Dock icon (macOS) - hidden while a session is live, since that is exactly when a screen share is most likely to be up
  • Always-on-top - pinned while a session is live, so the window does not fall behind the call the moment the call takes focus

Getting either behaviour previously meant entering full stealth, which also makes the window click-through, non-focusable and semi-transparent. That is not what someone wants who is simply running a session on a second monitor.

The design decision worth reviewing

The two inputs are independent, not nested. shouldHideSurfaces() is _stealth || isAssistantRunning(), and everything reads it. The consequence that matters: leaving stealth mid-session no longer hands the taskbar button back or drops the pin. Previously disableStealth() unconditionally undid both.

That is why always-on-top moved out of enableStealth/disableStealth and into applySurfaceVisibility() - the one place that reads both inputs and already re-runs on every window event.

Three details that fail silently

  1. Level is screen-saver, not the floating default. Per the Electron docs, levels from floating to status place the window below the Dock on macOS and below the taskbar on Windows. Only pop-up-menu and above are actually on top. Stealth already used screen-saver; this reuses it.

  2. setVisibleOnAllWorkspaces goes with the pin. On macOS an always-on-top window still vanishes when the user switches to a fullscreen Space - which is how most people run a video call. Without visibleOnFullScreen the pin does nothing in precisely the case it exists for. It is now released symmetrically too: entering stealth used to set it and never unset it, so the window followed the user around their desktop afterwards.

  3. Z-order is applied before setSkipTaskbar. Changing the level re-registers the window with the shell and hands the button back, so the taskbar call has to be the later of the two. test/running-surface.test.mjs pins the ordering.

Deliberate exception

macOS traffic lights still follow _stealth alone. A merely running window is focusable and interactive, so taking its close and minimise buttons away would strand the user. Covered in stealth-dock.test.mjs, the only test that runs as darwin.

Tests

New test/running-surface.test.mjs (26 checks) covers: running alone hides and pins; the show/restore/maximize/unmaximize events and restoreWindow all respect the running input; leaving stealth mid-session keeps both; stopping releases both; stealth alone still works unchanged; repeated identical state updates do no work; and Starting/Stopping are correctly not "running".

stealth-dock.test.mjs gains the Dock-while-running and traffic-lights cases. Note it calls refreshWindowSurfaces() directly rather than relying on appStateService: the singleton is shared with the darwin copy, but its own import of window-control carries no query string, so its refresh drives the default instance. That the state change triggers a refresh at all is covered in the new file.

stealth-surface.test.mjs regex assertions updated - they pinned the literal setSkipTaskbar(_stealth).

Both new mechanisms verified load-bearing

Rather than assume the tests would catch a regression, I broke each in turn:

break result
remove the appStateService refresh hook 8 checks fail
swap the z-order and taskbar call order ordering check fails

Verification

pnpm lint, both tsc configs, pnpm build, pnpm test:main - all clean.

Not verified on a real desktop. Every claim here about what the shell actually does - the taskbar button disappearing, the window floating over a fullscreen Zoom, the Dock icon going - rests on unit tests against a stubbed window. test/manual/taskbar-probe.mjs exists for the Windows half and is worth a run; the macOS fullscreen behaviour needs a real call to confirm.

🤖 Generated with Claude Code

The taskbar button, the Dock icon and always-on-top were tied to stealth
mode alone. Both are wrong for a running session outside stealth: a
running assistant is exactly when a screen share is most likely live, and
the window falls behind the call the moment the call takes focus, which
is when the suggestions most need to be readable.

Getting either behaviour used to mean entering full stealth, which also
makes the window click-through, non-focusable and semi-transparent.

Drive both from shouldHideSurfaces() - `_stealth || isAssistantRunning()`
- instead. The two inputs are independent, not nested: leaving stealth
mid-session no longer hands the taskbar button back or drops the pin.
That is why always-on-top moved out of enableStealth/disableStealth and
into applySurfaceVisibility, which is the one place that reads both.

Three details that are easy to get wrong and fail silently:

- Level is 'screen-saver'. Levels from 'floating' to 'status' put the
  window *below* the Dock and taskbar; only 'pop-up-menu' and above are
  actually on top.
- setVisibleOnAllWorkspaces goes with it. On macOS an always-on-top
  window still vanishes when the user switches to a fullscreen Space,
  which is how most people run a call - so without visibleOnFullScreen
  the pin does nothing in the case it exists for. Now released
  symmetrically; entering stealth used to set it and never unset it.
- Within applySurfaceVisibility the z-order call runs before
  setSkipTaskbar, since changing it re-registers the window with the
  shell. running-surface.test.mjs pins the ordering and fails if the two
  are swapped.

macOS traffic lights deliberately still follow stealth alone: a merely
running window is focusable and interactive, so removing its close and
minimise buttons would strand the user.

Verified both new mechanisms are load-bearing by breaking each in turn -
removing the appStateService hook fails 8 checks, swapping the call order
fails the ordering check.

Closes #98

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Self-review catch. applyAlwaysOnTop ran on every show/restore/maximize/
unmaximize with no no-op guard, so while a session was running each of
those re-issued the pin. That is not free and not invisible: on Windows
re-issuing HWND_TOPMOST re-raises the window to the front of the topmost
band, shuffling it over whatever the user just brought forward, and on
macOS Electron re-runs the whole level lookup and Cocoa call with no
early return of its own.

Guard it the way applyDockVisibility already guards the Dock, and reset
the tracked value in setWindowReference - carrying it across windows
would read the first real call on a new window as a no-op and leave it
unpinned.

The "leaving stealth mid-session" assertion moved from "last call was a
pin" to "never issued a release". The old form was written against the
unguarded code and would now demand exactly the redundant re-issue this
removes; the window being pinned throughout is the thing that matters.

Both halves verified load-bearing: dropping the guard fails the new
window-event check, dropping the reset fails the startup check.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@alpha5611331

Copy link
Copy Markdown
Member Author

Self-review: one real side effect found and fixed (86df446)

applyAlwaysOnTop had no no-op guard. It runs on every show/restore/maximize/unmaximize, so while a session was running each of those re-issued the pin. That is neither free nor invisible:

  • Windows: re-issuing HWND_TOPMOST re-raises the window to the front of the topmost band, so a window event would shuffle it over whatever the user had just brought forward.
  • macOS: the Electron source has no early return here - every call re-runs the full level lookup and the Cocoa SetWindowLevel.

Guarded it the way applyDockVisibility already guards the Dock, and reset the tracked value in setWindowReference - carrying it across windows would read the first real call on a new window as a no-op and leave it unpinned.

One test assertion changed as a result. leaving stealth mid-session keeps the window pinned checked that the last call was a pin; with the guard the correct behaviour is to issue no call at all, since the window is already pinned for the running session. It now asserts the window is never released, which is both the thing that actually matters and no longer coupled to the implementation.

Both halves verified load-bearing by breaking each in turn: dropping the guard fails the new window-event check, dropping the reset fails the startup check.

Checked and clear

  • The import cycle is pre-existing and safe. app-state.service already imported getWindowReference from window-control.service, which imports appStateService back. Adding refreshWindowSurfaces runs in the same direction. Nothing resolves at module-evaluation time - AppStateService's constructor calls setPlaceholderState(), which writes state directly rather than going through updateState, so no refresh fires during init. isAssistantRunning() is additionally wrapped in try/catch, which would absorb a TDZ error and fall back to "not running".
  • No alwaysOnTop in the BrowserWindow options, so the release call at startup has nothing to undo.
  • Everything still reading _stealth alone is meant to. restoreWindow() picks showInactive over show only in stealth (a running window is focusable, so plain show is right); toggleOpacity is stealth-only; setWindowButtonVisibility is the documented exception; the window:stealth-changed broadcast is unchanged, so the control panel does not vanish merely because a session started.
  • No event-storm path. show/restore/maximize/unmaximize are user-driven, and updateState only refreshes on an actual running-state transition - re-reporting the same running state changes nothing pins that.

Still unverified

Everything about what the shell actually does. test/manual/taskbar-probe.mjs covers the Windows taskbar half by hand; the macOS fullscreen-Space behaviour needs a real call to confirm.

@gitar-bot

gitar-bot Bot commented Aug 13, 2026

Copy link
Copy Markdown
Code Review ✅ Approved

Hides the taskbar icon and pins the assistant window on top while a session is running. Cleanly separates surface visibility logic from stealth mode with robust test coverage and no issues found.

Options

Auto-apply is off → Gitar will not commit updates to this branch.
Display: compact → Showing less information.

Comment with these commands to change the behavior for this request:

Auto-apply Compact
gitar auto-apply:on         
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

@alpha5611331
alpha5611331 merged commit 6e64440 into main Aug 13, 2026
2 checks passed
@alpha5611331
alpha5611331 deleted the feat/running-window-surface branch August 13, 2026 20:08

@anton-karlovskiy anton-karlovskiy left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM

@chmm195 chmm195 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM

@kevinkamto kevinkamto left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM

anton-karlovskiy

This comment was marked as duplicate.

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.

Window drops behind the call and keeps a taskbar button while the assistant is running

4 participants