feat(window): hide taskbar/Dock and pin on top while the assistant is running - #99
Conversation
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>
Self-review: one real side effect found and fixed (86df446)
Guarded it the way One test assertion changed as a result. 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
Still unverifiedEverything about what the shell actually does. |
Code Review ✅ ApprovedHides 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. OptionsAuto-apply is off → Gitar will not commit updates to this branch. Comment with these commands to change the behavior for this request:
Was this helpful? React with 👍 / 👎 | Gitar |
Closes #98
What
Two window surfaces were tied to stealth mode alone. Both now follow
stealth OR running: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. PreviouslydisableStealth()unconditionally undid both.That is why always-on-top moved out of
enableStealth/disableStealthand intoapplySurfaceVisibility()- the one place that reads both inputs and already re-runs on every window event.Three details that fail silently
Level is
screen-saver, not thefloatingdefault. Per the Electron docs, levels fromfloatingtostatusplace the window below the Dock on macOS and below the taskbar on Windows. Onlypop-up-menuand above are actually on top. Stealth already usedscreen-saver; this reuses it.setVisibleOnAllWorkspacesgoes 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. WithoutvisibleOnFullScreenthe 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.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.mjspins the ordering.Deliberate exception
macOS traffic lights still follow
_stealthalone. A merely running window is focusable and interactive, so taking its close and minimise buttons away would strand the user. Covered instealth-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; theshow/restore/maximize/unmaximizeevents andrestoreWindowall 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; andStarting/Stoppingare correctly not "running".stealth-dock.test.mjsgains the Dock-while-running and traffic-lights cases. Note it callsrefreshWindowSurfaces()directly rather than relying onappStateService: 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.mjsregex assertions updated - they pinned the literalsetSkipTaskbar(_stealth).Both new mechanisms verified load-bearing
Rather than assume the tests would catch a regression, I broke each in turn:
appStateServicerefresh hookVerification
pnpm lint, bothtscconfigs,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.mjsexists for the Windows half and is worth a run; the macOS fullscreen behaviour needs a real call to confirm.🤖 Generated with Claude Code