Skip to content

feat(widgets): add macOS desktop widgets - #1381

Open
ostiums wants to merge 24 commits into
robinebers:mainfrom
ostiums:widgets/refresh
Open

ostiums wants to merge 24 commits into
robinebers:mainfrom
ostiums:widgets/refresh

Conversation

@ostiums

@ostiums ostiums commented Oct 12, 2026 •

Copy link
Copy Markdown

Approved issue

Fixes #1301

TL;DR

This brings the macOS desktop widgets from #859 to main, with @validatedev's commits kept as they are, including 4e5444a. @validatedev fixed review items 1 and 2 in d65732d and simplified the shared models in 5d60b11. My four commits on top cover the rest: CI builds the widget extension, the stale widget lockfile is gone, and compact countdowns no longer truncate.

What was happening

  • PR CI ran swift build and swift test, which never compile the WidgetKit target in OpenUsageWidgets.xcodeproj.
  • Compact rows put Text(resetAt, style: .relative) next to the headline in an HStack. That text takes whatever width it's offered and has no usable ideal width, so it got cut off ("39 min, 30 se…"). In tinted mode, ProgressView drew the fill and the track in the same tint.
  • The widget project's Package.resolved pinned Sparkle 2.9.4, KeyboardShortcuts 3.0.1 and PostHog 3.64.1, while the root lockfile has 2.10.0, 3.1.0 and 3.90.2.

What this changes

  • 20a4d35: ci.yml builds the OpenUsageWidgets scheme in Release, unsigned, for arm64 and x86_64.
  • 25932ec: I deleted the widget project's Package.resolved and gitignored it. The extension only builds OpenUsageWidgetSupport, which uses none of those packages.
  • d9a8ccd: compact countdowns keep the same row as before. Small widgets show the leading unit ("4 hours", "6 days"). Medium widgets keep the relative style ("4 hrs, 32 min") and switch to minutes in the last 90 minutes, before the relative style adds seconds. Hidden copies of the text reserve its width, so the headline scales down if it has to, not the countdown. The progress fill is widgetAccentable() over a .quaternary track. In a small widget's header, the plan truncates before the provider name.
  • b38c33c: that reserved width covers the text until the next timeline entry. WidgetKit can postpone the requested reload, so the timeline now carries an entry every 30 minutes for 4 hours (9 entries), plus one for the moment the data turns Outdated. All of them come from one bridge read.

Heads-up

  • I tested on macOS 26 (arm64) with free Personal Team signing only. The production-signed, notarized install and the macOS 15 runtime are still unchecked, the two gaps @validatedev listed in macOS desktop widgets (finish #859) #1301.
  • The PR adds about 2,560 lines, so the size check will warn. Most of that is the original widget commit.
  • With the lockfile gone, Xcode resolves the root package's dependencies to the newest versions their from: ranges allow. They're resolved but not built for the extension.
  • Large widgets are unchanged and still show "Resets 14 min, 8 secs".

Tests

  • swift test: 1626 tests, 3 skipped, 0 failures, plus the 4 CLI tests. My new test is WidgetTimelineScheduleTests.testEntriesStayEvenlySpacedPastTheRequestedReload.
  • The CI command xcodebuild -project OpenUsageWidgets.xcodeproj -scheme OpenUsageWidgets -configuration Release -destination 'generic/platform=macOS' CODE_SIGNING_ALLOWED=NO ARCHS='arm64 x86_64' ONLY_ACTIVE_ARCH=NO build passes, and lipo -archs on the built extension prints x86_64 arm64.
  • By hand, I ran small, medium and large Claude and Codex widgets on live data, in default and tinted mode.

Screenshots

Before, from the #859 comment:

Default Tinted
Widgets before this change, default rendering Widgets before this change, tinted rendering

After:

Default Tinted
Widgets after this change, default rendering Widgets after this change, tinted rendering

validatedev and others added 14 commits July 6, 2026 15:50
…port

# Conflicts:
#	Package.swift
#	README.md
#	Sources/OpenUsage/App/OpenUsageApp.swift
#	Sources/OpenUsage/Stores/LayoutStore.swift
#	Sources/OpenUsage/Views/DashboardView.swift
#	docs/architecture.md
#	script/build_and_run.sh
#	script/release.sh
The widget extension kept a hardcoded provider list, so providers added
after it (Ollama, OpenCode) and extra Claude/Codex account cards could
not be chosen. The picker now lists what the host app exports, and each
record carries an iconID so account cards keep their provider mark.

Selection rules live in OpenUsageWidgetSupport so they are unit-tested.
A saved provider the app no longer exports still resolves, so the widget
shows Provider Unavailable instead of silently switching to another
provider.
WidgetKit keeps the extension process running after the app quits, so a
rebuilt dev bundle kept rendering desktop widgets with the previous
build's code. Kill the staged extension alongside the app; matching the
dist path leaves an installed release untouched.
@github-actions

Copy link
Copy Markdown

This pull request adds 2,430 lines.

Pull requests with more than 1,000 added lines are rarely merged because they are difficult to review and often extend beyond the approved issue. Please consider breaking this into a smaller, more surgical contribution.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

Two widget recovery paths remain broken at 5ebc6ecb: recovery clicks for missing/removed providers do not open the popover, and unconfigured widgets hide corrupt/unsupported bridge errors. The fixes described in the PR body are not present in the fetched head.

Reviewed changes Reviewed the complete host-to-widget implementation and its integration.

  • Shared data bridge: Adds versioned presentation records, atomic App Group persistence, observation and debounced WidgetKit reloads.
  • Widget extension: Adds provider configuration, three widget sizes, health states and stale-data timelines.
  • Navigation: Queues startup URLs and routes known providers to the dashboard or Customize.
  • Packaging and tests: Builds and signs nested extensions in dev/release bundles, with bridge/navigation tests and documentation. Shell syntax validation passed; macOS builds and runtime tests could not run on Linux.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using gpt-6.1-sol | 𝕏

Comment thread Sources/OpenUsage/App/OpenUsageApp.swift Outdated
Comment thread Widgets/OpenUsageWidgets/ProviderUsageTimelineProvider.swift Outdated

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes Reviewed the recovery fixes in d65732dd against the previous reviewed head, 5ebc6ecb.

  • Fixed recovery navigation: Routed widgets with missing or removed providers to the dashboard while retaining validation of unsupported URL shapes.
  • Prioritized bridge errors: Extracted shared entry selection so corrupt and unsupported bridge states display correctly before a provider is configured, while preserving configured IDs and existing default selection.
  • Added regression coverage: Covered recovery URLs, unconfigured bridge errors, configured IDs and loaded-bridge selection, and updated the widget behavior documentation.

Swift 6 strict-concurrency type-checking passed for the shared widget support sources. Full macOS tests, Xcode builds and WidgetKit runtime checks could not be run in this Linux environment.

Pullfrog  | View workflow run | Using gpt-6.1-sol | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes Reviewed the welcome-screen and widget-launch integration in 7cf37f14 against the previously reviewed head, d65732dd.

  • Preserved queued destinations: Opened the welcome flow's default dashboard before routing queued widget links, so a disabled provider's Customize destination remains selected.
  • Integrated startup sequencing: Retained incoming links while welcome setup is pending, surfaced the welcome window on widget clicks, and started periodic refresh when the dashboard becomes ready.
  • Added regression coverage: Covered welcome completion with a queued disabled-provider link and without queued links, and documented first-launch widget routing and refresh timing.

Validation was static: traced the startup flow through controller initialization, the dashboard handler and deep-link navigation. macOS/AppKit tests and WidgetKit runtime checks could not run on this Linux runner.

Pullfrog  | View workflow run | Using gpt-6.1-sol | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes Reviewed 5d60b112 against the previously reviewed head, 7cf37f14, focusing on behavior preserved by the widget model refactor.

  • Unified shared models: Removed duplicate extension presentation types and the generic selection protocol; the reader, picker, timeline and views now use shared bridge records directly.
  • Simplified timeline state: Carried the shared selection and freshness flag through timeline entries, preserving bridge-error precedence, stale transitions and provider click-through destinations.
  • Consolidated presentation: Extracted common header titles and health labels, retained host-side chart exclusion, removed unused dependencies and simplified test setup.
  • Updated documentation: Described the extension's direct rendering of shared display-ready records.

Swift 6 strict-concurrency type-checking passed for the shared support sources. macOS tests, WidgetKit target compilation and runtime rendering could not be verified on this Linux runner.

Pullfrog  | View workflow run | Using gpt-6.1-sol | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes Reviewed the merge in 0fb7e316 against the previously reviewed head, 5d60b112, focusing on integration with the incoming dashboard changes from main.

  • Integrated dashboard expansion: Moved the expanded card's caret below On Demand metrics and quick links, retaining the separate metric-reorder boundary and provider scroll targets used by widget links.
  • Updated drag previews: Carried separator position, links and expansion state into lifted provider cards.
  • Preserved widget behavior: Retained deep-link expansion and saved metric placement as the source of widget primary/secondary rows; the bridge and extension sources are unchanged.

Validation was static. macOS builds, tests and runtime rendering could not be verified on this Linux runner.

Pullfrog  | View workflow run | Using gpt-6.1-sol | 𝕏

swift build only compiles the shared support library. Add an unsigned
universal xcodebuild of the OpenUsageWidgets scheme.
The widget project points at the root package, so Xcode resolves Sparkle,
KeyboardShortcuts and PostHog, but the extension builds only
OpenUsageWidgetSupport, which uses none of them. Its lockfile pinned
versions it never builds and drifted from the root one.
Small and medium widgets truncated the reset countdown ("49 min, 26 se…").
Small widgets now show the leading unit, medium widgets switch to minutes
near the reset, and hidden static copies reserve the countdown's width so
the headline scales down instead. A small widget's header truncates the
plan before the provider name.

The progress bar's fill is now accentable, so tinted widgets keep fill and
track apart.
Compact countdowns reserve width for the text they show until the next
timeline entry, assuming entries 30 minutes apart. WidgetKit can postpone
the requested reload, so the timeline now carries entries every 30 minutes
for four hours, built from one bridge read.

The relative countdown's sizing text comes from the localized abbreviated
components style instead of hand-built English units.
@github-actions github-actions Bot added the ci label Oct 13, 2026

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes Reviewed the four new commits since 0fb7e316, focusing on compact countdown layout, timeline scheduling and extension build coverage.

  • Added extension CI: Built the separate WidgetKit scheme unsigned in Release for both architectures.
  • Removed stale dependency pins: Deleted and ignored the widget project's nested lockfile.
  • Adjusted compact presentation: Reserved countdown width, used a leading unit on small widgets and minutes near reset on medium widgets, prioritized provider names over plans, and separated accented progress fills from their tracks.
  • Extended timeline coverage: Generated 30-minute entries through four hours from one bridge read, retained the stale transition, and added schedule regression coverage.

Swift 6 strict-concurrency type-checking passed for all four shared support sources. Full macOS tests, Xcode builds and runtime rendering could not be run on this Linux runner.

Pullfrog  | View workflow run | Using gpt-6.1-sol | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes Reviewed the merge in 59b1f8fc against the previously reviewed head, b38c33c6, focusing on integration with widget startup routing.

  • Integrated startup timing: Retained the monotonic launch-duration measurement and lifecycle log after status item creation, without changing queued widget URL delivery or welcome-screen destinations.
  • Preserved widget behavior: Kept the bridge, extension rendering, countdown scheduling and packaging unchanged; the remaining range-diff change is documentation attribution context from main.

Validation was static. macOS builds, tests and WidgetKit runtime checks could not be run on this Linux runner.

Pullfrog  | View workflow run | Using gpt-6.1-sol | 𝕏

@ostiums

ostiums commented Oct 19, 2026 •

Copy link
Copy Markdown
Author

Hi @validatedev! The CI workflow for this PR is waiting on approval (action_required for b38c33c and 59b1f8f), since it runs from a fork. Could you approve the run? It's the first time the new Build widget extension step would run on your runners. Locally, the same xcodebuild command passes for arm64 and x86_64, and swift test passes.

@validatedev

Copy link
Copy Markdown
Collaborator

@ostiums sure!

@validatedev

Copy link
Copy Markdown
Collaborator

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ⚠️ Failed 2026-10-19T12:07:28.432529Z 246beca Manual request
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

An unknown error occurred
ℹ️ 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".

@validatedev

Copy link
Copy Markdown
Collaborator

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

An unknown error occurred
ℹ️ 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".

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

macOS desktop widgets (finish #859)

2 participants