Reported from the shipped Windows app. Three faults in one call, and the first is why it failed rather than merely misbehaved. runtime.Show is implemented by Wails as a bare mainWindow.Show(), while runtime.WindowShow wraps the identical work in runtime.LockOSThread. Win32 window operations have to run on the thread owning the window's message pump, and the tray's handler runs on the SYSTRAY's goroutine, which is never that thread. An unlocked Win32 call from an arbitrary goroutine is the bug. Two more that would each have been enough on their own: - Showing is not un-minimising. Hidden and minimised are different states and Show only fixes the first, so a window the user minimised stayed minimised. - Windows refuses the foreground to a process that does not already hold it, so the window came back BEHIND whatever was being looked at. Clicking a tray icon is by definition a moment when this app is not in front, so that is not an edge case here - it is every time. The always-on-top flip is the ordinary way to ask, and it is why this now runs in a goroutine rather than on the menu loop, which must not sleep. The same four calls fix OnSecondInstanceLaunch, which had the same shape and is reached far more often: double-clicking the desktop icon while the app is already running. OnBeforeClose used runtime.Hide against a reopen that used WindowShow - different calls on Windows, one thread-locked and one not. Paired now. And a Mac build, because the question came up and the answer turned out to be yes. Wails' darwin frontend references UTType without linking UniformTypeIdentifiers, so the build failed at the LINK step after compiling everything - which reads like a broken toolchain rather than one missing flag. There was no Mac version because of that, not because of a design limit. darwin_link.go declares the framework in source rather than leaving it as a CGO_LDFLAGS incantation, for the same reason deploy.sh now finds Go itself. Verified: plain `go build` produces a 16 MB arm64 binary on this Mac, and the Windows build is unchanged. Worth knowing for whoever edits that file: the comment directly above `import "C"` is cgo's C preamble, not documentation. The first attempt put the explanation there and the prose was compiled as C. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KGcjxF1cNLcuwc3DAPcnfj
Behavision desktop
The store-facing app: a tray icon, a window, and the supervisor for the Python recognition engine.
Why one process, not three
The tray, the window and the supervisor all need the same state, and a user who quits the tray expects recognition to stop. Splitting them means two things can disagree about whether the engine is running.
It is deliberately not a Windows service. A service runs in session 0 and cannot draw a tray icon — that is Windows session isolation, not a library limitation. Spawning a child process also needs no elevation, while controlling a service does, so this design never triggers UAC at runtime.
Layout
main.go wails.Run, window options
app.go the methods bound to the frontend
tray.go fyne.io/systray — wails v2 has no tray of its own
icons.go tray icons generated at run time, not embedded
internal/local client for the engine on 127.0.0.1:8010
internal/cloud client for https://mcp.loyaly.ai
frontend/ React + Vite
The supervisor, durable spool, broker client and path resolution come from
../agent/pkg/* — the same tested code the headless agent runs, imported
rather than copied.
Build
cd frontend && npm install && npm run build # then, from this directory:
wails build -platform windows/amd64
wails build needs the Wails CLI:
go install github.com/wailsapp/wails/v2/cmd/wails@v2.9.2
Without it, go build still type-checks everything provided
frontend/dist exists — the embed directive requires it.
What the frontend talks to
Nothing is imported from generated bindings. src/bridge.js calls
window.go.main.App.* directly, so npm run build works without running
wails generate, and there is one place that handles "the engine is not
running yet" — the state every screen has to survive on a fresh install.