Skip to content

feat(window): show the taskbar button and Dock icon outside stealth mode - #84

Merged
mchl7852 merged 5 commits into
mainfrom
feat/stealth-surface-visibility
Aug 7, 2026
Merged

feat(window): show the taskbar button and Dock icon outside stealth mode#84
mchl7852 merged 5 commits into
mainfrom
feat/stealth-surface-visibility

Conversation

@alpha5611331

Copy link
Copy Markdown
Member

Closes #83

The taskbar button and the Dock icon now follow stealth mode instead of being suppressed all the time, and the Ctrl+Shift+F8 restore shortcut that existed to work around their absence is gone.

Windows taskbar button macOS Dock icon / Cmd+Tab Desktop shortcut
Normal mode shown shown never created
Stealth mode hidden hidden never created

Commits

  1. feat(hotkeys) - remove Ctrl+Shift+F8. restoreWindow() itself is unchanged; relaunching the app still routes to it through the single instance lock.
  2. feat(window) - applySurfaceVisibility() replaces hideFromTaskbar() and drives both surfaces from the stealth flag.
  3. test(stealth) - a manual Windows probe for the part unit tests cannot reach.

Each commit stands on its own; pnpm test:main passes at every one.

Two traps worth knowing about

LSUIElement had 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, alongside dock.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.mjs drives the real compiled service in a real Electron process and reads the taskbar back through UI Automation:

ok  the window starts on the taskbar
ok  cycle 1/2/3: stealth takes the button away      (absent)
ok  cycle 1/2/3: leaving stealth gives it back      (present)
ok  rapid toggles settling in stealth               (absent)
ok  rapid toggles settling in normal mode           (present)

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 via loadMainAs('darwin', ...), since these branches are dead code on the Linux runner CI uses.
  • stealth-surface - updated to pin the runtime policy switch, the absence of LSUIElement, and that no F8 shortcut is registered.

pnpm lint, both tsc configs and vite build pass.

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-closed still quits on every platform, unchanged and still justified: stealth mode can be active when the last window closes.

alpha5611331 and others added 5 commits August 7, 2026 09:46
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>
@mchl7852 mchl7852 self-assigned this Aug 7, 2026
@mchl7852
mchl7852 merged commit bfe2030 into main Aug 7, 2026
1 check passed
@mchl7852
mchl7852 deleted the feat/stealth-surface-visibility branch August 7, 2026 15:15
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.

Taskbar button and Dock icon are hidden in normal mode, not just stealth

2 participants