feat(window): show the taskbar button and Dock icon outside stealth mode - #84
Merged
Conversation
The shortcut existed only to work around the window being kept off the taskbar and the Dock, which leaves a minimized window with no button to click. It burns a system-wide accelerator on a self-inflicted problem, and nothing on screen suggests it exists. Relaunching the app stays the route back: the single instance lock already routes to restoreWindow(). restoreWindow itself is unchanged. Refs #83 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The app hid itself from the taskbar and the Dock at all times, but only stealth mode needs those surfaces gone - it is what hides the window from screen capture, and a labelled taskbar button is the first thing a screen share gives the app away with. Outside stealth there is nothing to hide from, and everyone paid for it with an unrecoverable minimize. applySurfaceVisibility() drives both from the stealth flag and replaces hideFromTaskbar(). It runs after every transition and on the window events that re-register the button, because hiding it is shell registration (ITaskbarList::DeleteTab) rather than a window style and does not survive setFocusable or the z-order change. LSUIElement is dropped from the packaged Info.plist: it pins the app to accessory from launch, leaving no Dock icon to give back. The activation policy is switched at runtime instead, alongside dock.show()/hide() - the policy is what moves the app, dock.show() alone does not lift an accessory app back out. macOS drops a Dock call made within a second of the previous one, with no error, so mashing the stealth hotkey would strand the icon on screen for the rest of the session. Calls that land inside that window schedule a re-assert; no-ops are skipped so window events cannot spend the budget a real toggle needs. The activation policy carries no such limit and is applied immediately, so the icon still goes at once in that case. stealth-dock.test.mjs loads a second copy of the service through loadMainAs(darwin, ...) - these branches are dead code on the Linux runner CI uses, which is exactly where they would rot unnoticed. Refs #83 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
stealth-toggle.test.mjs can only prove setSkipTaskbar is called with the right argument. Whether the shell acts on it is the part that broke before, and it fails with no exception and nothing logged. Drives the real compiled service in a real Electron process and reads the taskbar back through UI Automation. Counts the windows a button reports rather than the buttons themselves: Windows groups every Electron app under one button, so a second Electron app on the machine would otherwise mask ours. Windows only and needs a desktop session, so it is run by hand and kept out of test/run.mjs - CI is headless Linux. Refs #83 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #83
The taskbar button and the Dock icon now follow stealth mode instead of being suppressed all the time, and the
Ctrl+Shift+F8restore shortcut that existed to work around their absence is gone.Commits
feat(hotkeys)- removeCtrl+Shift+F8.restoreWindow()itself is unchanged; relaunching the app still routes to it through the single instance lock.feat(window)-applySurfaceVisibility()replaceshideFromTaskbar()and drives both surfaces from the stealth flag.test(stealth)- a manual Windows probe for the part unit tests cannot reach.Each commit stands on its own;
pnpm test:mainpasses at every one.Two traps worth knowing about
LSUIElementhad to go. It pins the packaged mac app to accessory from launch, so there would be no Dock icon to give back. The activation policy is switched at runtime instead, alongsidedock.show()/dock.hide()- the policy is what actually moves the app,dock.show()alone does not lift an accessory app back out.macOS drops a Dock call made within one second of the previous one, silently (docs). My first pass had this bug: toggling stealth twice quickly - mashing the hotkey - left the Dock icon on screen for the rest of the session, no error, nothing logged, in the one mode where that matters most. Now no-op calls are skipped so window events cannot spend the budget a real toggle needs, and a call landing inside the window schedules a re-assert. The activation policy carries no such limit and is applied immediately, so the icon still goes at once even in the swallowed case.
Testing
Live Windows taskbar, 9/9. Unit tests can only prove we pass the right argument to
setSkipTaskbar; whether the shell acts on it is what actually broke before, and it fails without an exception.test/manual/taskbar-probe.mjsdrives the real compiled service in a real Electron process and reads the taskbar back through UI Automation:Two false readings on the way there, in case anyone repeats this: screenshotting the taskbar is useless if another Electron app is running (same icon), and counting taskbar buttons is wrong because Windows groups all Electron apps under one button (shared AppUserModelID). Counting the windows that button reports is the reliable signal.
Automated (
pnpm test:main, 118 checks):stealth-toggle- 10 full cycles with a window event between each, asserting the button tracks the mode every time. One pass in each direction proves little here; the original bug only appeared after switching.stealth-dock(new) - 5 rapid cycles, the swallowed-call re-assert, no-op suppression, and that the re-assert does not re-arm itself. Loads a second copy of the service vialoadMainAs('darwin', ...), since these branches are dead code on the Linux runner CI uses.stealth-surface- updated to pin the runtime policy switch, the absence ofLSUIElement, and that no F8 shortcut is registered.pnpm lint, bothtscconfigs andvite buildpass.Not covered
The macOS Dock is verified at the call level only - the stub records what we ask for, not what macOS does with it, and the rate-limit repair rests on Electron's documented behaviour. Someone with a Mac should confirm the icon comes and goes across repeated toggles, including two fast ones inside a second.
window-all-closedstill quits on every platform, unchanged and still justified: stealth mode can be active when the last window closes.