feat: run through go-widgets/application (bundle, working tray, no freeze) - #272
Merged
Conversation
…eeze) Replace the reader's private window glue and the non-working go-macos/statusitem tray with go-widgets/application v0.1.0, the cross-platform app-lifecycle sibling to go-widgets/window. - Delete internal/window: its Handler/Config contract and run loop were extracted into go-widgets/application. windowapp and the cmd import application (aliased as `window`, so every type reference is unchanged). - present_reader.go builds an application.Spec (name, identifier, the 32px appicon.Tray, and a go-widgets/tray "Quit" menu) and calls application.Run, which owns the run loop and attaches the tray to it. A bare executable can NOT own a status item, which is why the #271 statusitem tray never appeared; the tray now works once the binary runs inside the .app bundle. - Drop the go-macos/statusitem code and dependency. - Freeze fix: app.ActivateAfterVault reads the vault on a background goroutine (so the keychain prompt never blocks the render thread) and applies the result — reactivating the authenticated providers, importing a browser session — on the render thread via the existing post queue. The window no longer beach-balls behind the password panel. Replaces HydrateAndActivate. - application.Run fires onReady after the first frame, so windowapp no longer carries its own SetOnReady/frame-counter. - cmd/bundle assembles "News Reader.app" via go-macos/appbundle (icns from the 512px appicon.Icon); nr-build runs it so the Dock tile + name + tray exist. Build tooling, excluded from the coverage gate like the example plugins. Full suite green, 100% coverage on the gated packages, -race clean, gofmt + vet clean; builds for darwin, linux, windows. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ady) The tray never appeared because application v0.1.0 attached it before the window loop and NSApplication existed; v0.1.1 attaches it from onReady, once both are up. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
internal/window was removed when the reader moved to go-widgets/application; the separate 'go test ./internal/window/...' step now finds no package and fails. The native window loop is covered in go-widgets/application's own CI. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
tray v0.6.0 links its NSStatusItem/DBus/win32 backend per-platform without a build tag (the gate now selects by file shape), so the reader gets a working tray from a plain 'go build' — no -tags tray_native. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Unlocking the vault prompted once per secret (one keychain item per account field), so a user with several sign-ins was asked several times — and with Biometric unlock on, that was several Touch ID prompts. Now every secret lives in a single JSON blob under one keychain account (vaultAccount): hydrateSecrets reads it with ONE store.Get, pushSecrets writes it with ONE store.Set. A pre-blob install is migrated on first load — the old per-ref items are read once (the only launch that prompts per item), folded into the blob, and deleted; idempotent thereafter. Biometric gate, the unsigned-build fallback, the file fallback, and plaintext-on-disk migration are all preserved. So startup is one prompt — one Touch ID with biometric unlock on. 100% coverage (TestHydrateReadsVaultOnce proves the single Get; TestMigrateFromPerRefItems the one-time migration; corrupt-blob + write-error paths covered); gofmt + vet clean; builds darwin/linux/windows. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Touch ID was fully wired but the BiometricUnlock preference defaulted OFF, so a signed build still prompted for the keychain password and never offered Touch ID unless the user found the toggle. Make it a tri-state *bool (like the other opt-in-by-default settings): unset means DefaultBiometricUnlock (ON), an explicit false persists an opt-out. Default() seeds it and Normalize() backfills an unset field — so an existing settings file (which never wrote the old false-with- omitempty) becomes Touch ID on, and a fresh install offers it without hunting for a switch. On an unsigned build the write silently falls back to an ungated item, so on-by-default is safe everywhere. Read through Settings.BiometricUnlockEnabled(); the scene projects an explicit value once the user has interacted. 100% coverage, gofmt + vet clean, builds darwin/linux/windows. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… build) The keychain SecAccessControl user-presence route to Touch ID needs the data-protection keychain, whose keychain-access-groups entitlement AMFI rejects on a self-signed app (it then fails to launch), so it always fell back to a password. Gate the vault READ behind a LocalAuthentication (LAContext) prompt instead: it asks the Secure Enclave for a Touch ID (or device-password) attestation with NO keychain entitlement — only that the process is a .app bundle, which the reader now is. - internal/biometric: a portable Authenticate seam. darwin uses go-macos/localauthentication (PolicyOwner, so a Touch-ID-less Mac falls back to the device password); it skips gating when no owner check is available so a user is never locked out. Off macOS it is a no-op. - app.ActivateAfterVault prompts once, on its background goroutine, before HydrateSecrets when biometric unlock is on; a denial opens the reader without the sign-ins rather than failing. With the single consolidated vault item that is ONE Touch ID at startup. - Secrets now always store ungated (the app stops enabling the broken keychain user-presence path); the LAContext gate is the biometric. 100% coverage on the gated packages (the darwin LAContext call is a native boundary in a _darwin.go file, not compiled on the linux gate); gofmt + vet clean; builds darwin/linux/windows. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… password Touch ID appeared, then a keychain password prompt — because the keychain grants silent access only to the app signature on the item's ACL, and the item was first written before the signature was stable. Prompting Touch ID before that grant is in place stacks two prompts. Hold the biometric gate until the vault has been unlocked once under this app (new internal BiometricPrimed flag): the first unlock goes through the keychain's own grant (a password / "Always Allow", which now sticks under the stable signature), then the flag is set and persisted. From the next launch the keychain read is silent and Touch ID is the ONLY prompt. Settings.HasStoredSecret gates the priming on there actually being a secret to protect. 100% coverage (priming run, primed run allow/deny, disabled, HasStoredSecret); gofmt + vet clean; builds darwin/linux/windows. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ration Legacy per-item secrets whose ACL trusts a code signature that no longer exists read back errSecUserCanceled (-128), so migratePerRefItems recovered nothing and — guarded by `len(recovered)==0` — wrote no blob, leaving the per-item read path live: every launch re-prompted for the dead items (the "3 password prompts each time" bug). Write the blob whenever a migration was ATTEMPTED (there are secret-carrying accounts), even when nothing was recovered: its presence alone retires the per-item path, so an unreadable legacy item is left behind unread instead of re-prompting forever. Recovered secrets still go in and their old items are still deleted. Regression guard: TestMigrateBrokenLegacyItemsStillWritesBlob. 100% coverage; gofmt + vet clean. Co-Authored-By: Claude Opus 4.8 <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.
Rewires the reader onto the new
go-widgets/applicationv0.1.0, the cross-platform app-lifecycle sibling togo-widgets/window, and fixes the three launch complaints: no Dock icon, a tray that never appeared, and the app freezing behind the vault password panel.Why the old approach was wrong
The
go-macos/statusitemtray shipped in #271 cannot appear from a bare executable — "a status item is asked for by nobody" outside a.app. The real fix is to run inside a bundle, which also gives the Dock tile and the app name.Changes
internal/window— itsHandler/Configcontract and run loop were extracted intogo-widgets/application.windowappandcmdimportapplication(aliasedwindow, so type references are unchanged).application.Run—present_reader.gobuilds anapplication.Spec(name, identifier, the 32pxappicon.Tray, ago-widgets/tray"Quit" menu) and callsapplication.Run, which owns the run loop and attaches the tray to it. Dropsgo-macos/statusitem.app.ActivateAfterVaultreads the vault on a background goroutine (the keychain prompt no longer blocks the render thread) and applies the result — reactivating the authenticated providers, importing a browser session — on the render thread via the existingpostqueue. ReplacesHydrateAndActivate.onReadynow comes fromapplication(fired after the first frame), sowindowappdrops its ownSetOnReady/frame-counter.cmd/bundleassemblesNews Reader.appviago-macos/appbundle(icns from the 512pxappicon.Icon); nr-build runs it so the Dock tile + name + working tray exist. Build tooling, excluded from the coverage gate like the example plugins.Tests
Full suite green, 100% coverage on the gated packages,
-raceclean, gofmt + vet clean; builds for darwin, linux, windows. The.appassembly was verified (validContents/{MacOS, Resources/News Reader.icns, Info.plist}).🤖 Generated with Claude Code