Conversation
Add Sparkle framework (from: "2.7.0") to Package.swift and wire it into the Mori executable target. This enables the Sparkle auto-update infrastructure for future update checking and in-app updates.
Create Sources/Mori/Update/ with stub files for the Sparkle update system: UpdateState, UpdateViewModel, UpdateDriver, UpdateDelegate, UpdateController, UpdateBadge, UpdatePill, UpdatePopoverView, and UpdateAccessoryView. These will be implemented in Phase 2 and 3.
…e script - Add SUPublicEDKey (placeholder) and SUFeedURL to Info.plist generation - Embed Sparkle.framework from SPM artifacts into Contents/Frameworks/ - Copy Sparkle XPC services to Contents/XPCServices/ - Add XPC service signing to sign.sh (inside-out before main app)
Document the Sparkle auto-update system: EdDSA key generation and export, public/private key storage locations, update flow overview, appcast feed hosting, and key regeneration procedure.
Add state enum with associated data structs for permission request, checking, update available, downloading, extracting, installing, not found, and error states. Includes cancel/confirm helpers, manual Equatable conformance, and Mori-specific ReleaseNotes linking to GitHub releases.
ObservableObject with @published state for Combine sink support. Computed properties for text, maxWidthText, iconName, description, badge, iconColor, backgroundColor, and foregroundColor matching each update lifecycle state.
SPUUserDriver implementation that translates all Sparkle lifecycle callbacks into UpdateState transitions on the view model. Falls back to SPUStandardUserDriver when no visible window is available for unobtrusive badge rendering.
SPUUpdaterDelegate extension on UpdateDriver providing the appcast feed URL (GitHub Pages), handling install-on-quit state transition, and invalidating restorable state before relaunch.
…orce-install @mainactor controller owning SPUUpdater with startUpdater(), checkForUpdates(), and installUpdate() methods. Force-install uses Combine $state.sink to auto-confirm each update step. Wires retry callback to UpdateDriver for error recovery.
…rk itself Inside-out signing requires deepest nested bundles to be signed first. Sparkle.framework contains XPC services that must be signed before the framework wrapping them.
SwiftUI badge view showing state-appropriate visual indicators: progress ring for downloading/extracting, rotating animation for checking state, and static SF Symbol icons for other states.
Pill-shaped button wrapping UpdateBadge + text label with state-dependent colors. Shows popover on click, auto-dismisses .notFound after 5 seconds. Fixed text width prevents resizing during progress updates.
Full popover content for all update states: permission request, checking with spinner, update available with version/size/date and action buttons, downloading/extracting with progress bars, installing with restart prompt, not found confirmation, and error with retry option.
NSTitlebarAccessoryViewController subclass hosting UpdatePill via NSHostingView, positioned trailing in the titlebar for unobtrusive update badge display.
…sory Create UpdateController after window is shown (so hasUnobtrusiveTarget has a visible window), start the updater, and add the UpdatePill as a trailing titlebar accessory via MainWindowController.addUpdateAccessory().
Add "Check for Updates" as a searchable action in the command palette with the arrow.triangle.2.circlepath icon.
Address Phase 2 reviewer feedback: hasUnobtrusiveTarget now checks for MainWindowController specifically instead of any window with a content view controller. The willClose notification handler also filters to only react when Mori's main window is closing.
Downloads Sparkle 2.9.0 tools, signs release archives with EdDSA, and generates appcast.xml for the auto-update feed. Supports merging with existing appcast.xml to preserve previous release entries.
Adds two new steps to the release pipeline: generate appcast.xml with EdDSA signatures using Sparkle tools, then publish it to the gh-pages branch for the auto-update feed. Uses SPARKLE_PRIVATE_KEY secret.
Creates scripts/setup-gh-pages.sh that initializes an orphan gh-pages branch with a redirect index.html and empty appcast.xml placeholder. Run once before the first release to set up the update feed endpoint.
…update docs Documents the full release pipeline flow, appcast generation process, SPARKLE_PRIVATE_KEY secret setup, gh-pages branch initialization, manual appcast generation, and troubleshooting tips.
Instead of a separate gh-pages branch, the appcast is now committed to vaayne/homebrew-tap alongside the cask update. This reuses existing CI infra and supports multiple products in the tap repo with namespaced filenames.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 794aeeb12a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| <key>NSPrincipalClass</key> | ||
| <string>NSApplication</string> | ||
| <key>SUPublicEDKey</key> | ||
| <string>PLACEHOLDER_EDDSA_PUBLIC_KEY</string> |
There was a problem hiding this comment.
Fail builds when SUPublicEDKey is still placeholder
The bundle script always writes PLACEHOLDER_EDDSA_PUBLIC_KEY into Info.plist, while the release workflow signs appcast.xml with the real SPARKLE_PRIVATE_KEY. If this placeholder is not manually edited before a tag, every shipped app will reject updates because the embedded public key cannot verify the feed signatures. This should be injected from CI (or hard-fail when still placeholder) to avoid silently publishing non-updatable builds.
Useful? React with 👍 / 👎.
| HStack { | ||
| Spacer() | ||
| Button(String.localized("OK")) { | ||
| notFound.acknowledgement() |
There was a problem hiding this comment.
Prevent double-calling not-found acknowledgement
In the “No Updates Found” popover path, pressing OK calls notFound.acknowledgement() but does not transition state away from .notFound; the auto-dismiss task in UpdatePill still fires after 5 seconds and calls the same acknowledgement closure again. Sparkle acknowledgement callbacks are intended as one-shot completions, so this can trigger duplicate completion behavior when the popover stays open through a no-update check.
Useful? React with 👍 / 👎.
| items.append(.action( | ||
| id: "action.check-for-updates", | ||
| title: .localized("Check for Updates"), | ||
| subtitle: .localized("Check for available Mori updates") |
There was a problem hiding this comment.
Localize the new update command subtitle
The new command palette subtitle uses .localized("Check for available Mori updates"), but that key was not added to either en.lproj or zh-Hans.lproj. Per the repo guideline in /workspace/mori/AGENTS.md (“All new user-facing strings … add entries to both … Localizable.strings”), this introduces a user-visible i18n regression where the raw English key/fallback appears in non-English locales.
Useful? React with 👍 / 👎.
…n string - NotFoundView OK button now sets state to .idle before calling acknowledgement(), preventing the auto-dismiss task from calling it again after 5s - Added missing "Check for available Mori updates" to en + zh-Hans Localizable.strings
- Prevent double-reply via callOnce wrapper on Sparkle reply closures - Add @mainactor to UpdateDriver for thread-safety enforcement - Replace DispatchQueue.main.asyncAfter with Task.sleep for consistency - Defer installCancellable cleanup to avoid cancelling mid-callback - Remove duplicate feed URL override (rely on SUFeedURL in Info.plist) - Simplify ReleaseNotes single-case enum to computed URL? property - Mark UpdateController, UpdateDriver, UpdateViewModel as final - Add clarifying comment on Installing.isAutoUpdate dual code paths
Extract normalizedProgress on Downloading/Extracting to eliminate duplicated clamping logic across 3 files. Throttle download progress to ~100 UI updates instead of ~1,900. Cache hasUnobtrusiveTarget via window lifecycle notifications. Centralize NotFound dismissal, fix NotFoundView leaf-view pattern, remove dead guard in installUpdate sink, cache textWidth measurement, extract GitHub URL constant.
Summary
appcast.xmlwith EdDSA signatures and publishes togh-pagesbranchSources/Mori/Update/, 3 new scripts, updated bundle/sign scriptsNew Files
Sources/Mori/Update/UpdateState.swiftSources/Mori/Update/UpdateViewModel.swiftSources/Mori/Update/UpdateDriver.swiftSources/Mori/Update/UpdateDelegate.swiftSources/Mori/Update/UpdateController.swiftSources/Mori/Update/UpdateBadge.swiftSources/Mori/Update/UpdatePill.swiftSources/Mori/Update/UpdatePopoverView.swiftSources/Mori/Update/UpdateAccessoryView.swiftscripts/generate-appcast.shscripts/setup-gh-pages.shdocs/auto-update.mdPre-release checklist
generate_keys)PLACEHOLDER_EDDSA_PUBLIC_KEYinscripts/bundle.shSPARKLE_PRIVATE_KEYGitHub secretscripts/setup-gh-pages.shand push gh-pages branchTest plan
swift build --product Moriscripts/generate-appcast.shproduces valid appcast.xml (after key setup)