Skip to content

Windows: native quick view, sharp tray icon, start at login - #150

Merged
prakersh merged 3 commits into
onllm-dev:mainfrom
prachimantri20:feat/windows-native-tray
Oct 17, 2026
Merged

prakersh merged 3 commits into
onllm-dev:mainfrom
prachimantri20:feat/windows-native-tray

Conversation

@prachimantri20

Copy link
Copy Markdown
Contributor

Summary

On Windows the tray companion looked broken compared to macOS:

  • Quick view opened as a Microsoft Edge --app window: title bar with min/max/close, taskbar button, fixed 360x680 with a large empty area, no dismiss on focus loss, and a cold browser start on every click.
  • Tray badge looked dotted and blurry.
  • Menu text was blurry at 125%+, rendered in the light theme, and the provider rows looked like actions.
  • No auto-start: onWatch did not come back after a reboot and was not restarted after a crash (onwatch service install was macOS-only).

This PR fixes all four, in three commits.

1. Native quick view and sharp tray icon (feat(menubar))

Root causes

  • The page reports its height through window.webkit.messageHandlers.onwatchResize, which only exists in the macOS WKWebView, so the Edge window never resized. Chromium --app windows always get a standard frame.
  • fyne.io/systray loads icons with LoadImage(..., 0, 0, LR_DEFAULTSIZE), which picks the 32 px large-icon size. The shell then shrank it into the 16-24 px tray slot, so the per-DPI sizes in our .ico were never used. The 7 px pixel font stayed 7 px from 16 to 31 px.
  • The companion was not DPI aware, so Windows bitmap-stretched the menu and windows.

Changes

  • quickview_webview2_windows.go: hosts /menubar in WebView2 inside a borderless WS_EX_TOOLWINDOW popup, the Windows counterpart of popover_darwin.m. It has rounded corners on Windows 11 and is kept warm with a soft refresh on open, like macOS. It sizes itself to the page's onwatchResize, clamped with ClampPopoverHeight and scaled per monitor DPI. It closes on a second click, a click outside, Esc, or the page's close button, and links open in the default browser. It is pure Go (jchv/go-webview2), so CGO_ENABLED=0 release builds are unchanged. The Edge --app window remains the fallback when the WebView2 runtime is missing or ONWATCH_QUICKVIEW_BROWSER is set.
  • menubar.html: a small nativeHandler(name) helper resolves either the WKWebView handler or the WebView2 chrome.webview channel. Messages carry their channel name, and popover_bridge.go routes them (platform-neutral, unit tested).
  • Tray badge: an anti-aliased rounded square with Go Bold digits (golang.org/x/image, pinned to v0.44.0 because later versions need Go 1.26). On Windows it is rendered at exactly SM_CXSMICON for the system DPI and pushed with Shell_NotifyIconW, while systray keeps the multi-size .ico as its fallback.
  • Per-monitor DPI awareness (V2) for the companion process, and dark tray menus that follow the app theme.
  • A status dot on each provider row in the tray menu (Windows, Linux).

2. Start at login and restart after a crash on Windows (feat(service))

  • onwatch service install|uninstall|status now works on Windows. It registers a per-user Task Scheduler task (no admin rights) with a logon trigger, battery restrictions off, and no 72 h run-time limit.
  • Task Scheduler's restart-on-failure does not cover a process that dies after launch, so the task runs onwatch service run: a small supervisor (~13 MB) that keeps the daemon running and restarts it 10 s after a crash, backing off to 5 min. A clean exit (code 0) ends supervision, matching launchd's KeepAlive {SuccessfulExit: false}:
    • terminateProcess on Windows now exits with code 0, and onwatch stop also ends the task, so a stop stays stopped until the next login.
    • onwatch update and dashboard updates restart through the task (/End + /Run), reusing the existing launchd branches, so the new binary stays supervised.
  • The task runs the supervisor under conhost.exe --headless, so no window flashes at logon. The supervisor exits with its console host, so schtasks /End really ends supervision.
  • Fixes found while testing:
    • windowsSessionAvailable relied on SESSIONNAME, which Task Scheduler does not set, so no tray appeared. It now falls back to the process session ID.
    • stopPreviousInstance slept a fixed 500 ms after terminating, but on Windows the port stays bound until the process is gone, so a manual takeover could fail with "Only one usage of each socket address". It now waits for the process to exit.
  • install.ps1 offers auto-start, and re-registers an existing task after an upgrade.

3. Docs

docs/TRAY_LINUX_WINDOWS.md and docs/WINDOWS_SETUP.md (new Start at Login section).

Testing

  • New and updated unit tests:
    • the message bridge and DPI math
    • badge rendering (text fits and grows at every size, anti-aliasing)
    • the Task Scheduler XML and schtasks calls
    • the supervisor loop and backoff
    • Windows session detection
    • terminateProcess exiting with code 0
    • the page bridge (string checks, plus the Node resize test extended to WebView2)
  • go test -cover ./... and -tags menubar for menubar, service, cmd/onwatch and web on Windows 11: everything passes except two failures that also fail on main on Windows: TestResolveExeKeepsStableSymlink (symlinks need a privilege) and the TestDaemonChildRun_DebugModeAllProviders* TempDir cleanup (a test-spawned Antigravity CLI keeps a file open). -race could not run locally (no C toolchain); CI covers it.
  • Cross-builds: CGO_ENABLED=0 with -tags menubar for windows/amd64, windows/arm64, linux/amd64 and linux/arm64, plus untagged builds. go vet for darwin untagged (the darwin tray needs cgo and a Mac).
  • Manually on Windows 11 at 125%:
    • Quick view: opens anchored above the taskbar, fits its content, closes on an outside click.
    • Tray: badge is pixel-exact at 20 px; dark menu with status dots.
    • Auto-start lifecycle: install, simulated logon (schtasks /Run), kill the daemon (restarted after 10 s), onwatch stop (stays stopped), /End + /Run (exactly one daemon after takeover), uninstall (daemon keeps running). No visible windows at any point.
  • flake.nix vendorHash was updated by computing the NAR hash of go mod vendor output locally. The same script reproduces the current hash on main; CI's nix job confirms it.

Notes for review

  • go-webview2's edge package calls runtime.LockOSThread() and CoInitializeEx in init(). Since the Windows release is one binary built with -tags menubar, the daemon's main goroutine is pinned to the main thread too. That's harmless (nothing else uses COM), but worth knowing.
  • The exact-size tray icon addresses the icon fyne.io/systray registered: its hidden SystrayClass window, uID 100. Both are fixed in the pinned v1.12.0. If a future version changes them, the call fails quietly and the systray icon stays.
  • Dark menus use uxtheme's unnamed SetPreferredAppMode/FlushMenuThemes exports (ordinals 135/136), guarded to Windows 10 1903+, as Chromium and Windows Terminal do.
  • The Linux status dots use MenuItem.SetIcon over dbusmenu and are untested on a real Linux desktop. macOS only gains no-op hooks.

🤖 Generated with Claude Code

pmncoind and others added 3 commits October 17, 2026 20:42
The Windows quick view was Microsoft Edge in --app mode: a normal titled
window with min/max/close buttons, a taskbar button, a fixed 360x680 size
(the page's resize reports only reached the macOS WKWebView), no dismiss on
focus loss, and a cold browser start on every click.

- Host /menubar in WebView2 inside a borderless tool window, the Windows
  counterpart of the macOS popover: no title bar, taskbar or Alt-Tab entry,
  rounded corners on Windows 11, kept warm and soft-refreshed on open,
  sized to its content, closed by a second click, a click elsewhere, Esc or
  the page's close button. Links open in the default browser. The page and
  host share the macOS channel names (onwatchResize, onwatchAction) through
  chrome.webview. Pure Go (jchv/go-webview2), so CGO_ENABLED=0 builds keep
  working; the Edge --app window remains the fallback without the runtime.
- Render the tray badge at the exact small-icon size for the display
  scale. fyne.io/systray loads icons with LR_DEFAULTSIZE (the 32 px large
  icon), which the shell shrank into the tray slot - the dotted, blurry
  badge. The badge is now an anti-aliased rounded square with Go Bold
  digits that grow with the icon instead of a 7 px pixel font.
- Make the companion per-monitor DPI aware, so the menu and quick view are
  crisp at 125% and above, and let the tray menu follow the dark theme.
- Mark provider rows in the tray menu with a status dot (Windows, Linux).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`onwatch service install` now works on Windows. It registers a per-user
Task Scheduler task (no administrator rights) that starts at logon, runs on
battery and has no run-time limit. Task Scheduler cannot restart a process
that dies after launch, so the task runs `onwatch service run`, a small
supervisor that keeps the daemon running and restarts it after a crash with
a 10s-5m backoff. A clean exit ends supervision, matching launchd's
KeepAlive {SuccessfulExit: false}:

- `onwatch stop` terminates with exit code 0 on Windows and ends the task,
  so it stays stopped until the next login.
- `onwatch update` and dashboard updates restart through the task, so the
  new binary stays supervised.
- The task runs under a headless conhost, so nothing flashes at logon.
- A Task Scheduler start has no SESSIONNAME; fall back to the process's
  session ID so the tray companion still starts.
- Wait for a stopped instance to exit before starting the new one: on
  Windows the port stays bound until then and the new daemon failed to
  start.
- install.ps1 offers auto-start, and re-registers an existing task after an
  upgrade.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@prakersh
prakersh merged commit 37b5689 into onllm-dev:main Oct 17, 2026
21 checks passed
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.

2 participants