Skip to content

feat: expose containers and machines to Spotlight and Shortcuts - #188

Merged
henrywang merged 2 commits into
mainfrom
feat/167-spotlight-app-intents
Oct 10, 2026
Merged

henrywang merged 2 commits into
mainfrom
feat/167-spotlight-app-intents

Conversation

@henrywang

Copy link
Copy Markdown
Owner

Closes #167

What

  • ContainerEntity and MachineEntity (AppEntity + IndexedEntity), searchable by name or image. Utility machines (the internal default VM) are excluded, as in the sidebar.
  • Six intents: Start, Stop and Open for containers and machines. Open reuses the menu bar's selection path (MenuBarBridge.pendingIntent = .selectCompute), so it lands on the item's detail pane.
  • SpotlightIndexer indexes both kinds and re-sends only what changed, by fingerprint (the service replaces its arrays every poll). On first connect after launch it clears and re-indexes, so entries for items deleted while Berthly was not running don't linger. It does not run under unit or UI tests.
  • BerthlyApp.init now creates the service and menu bar bridge and registers them with AppDependencyManager, so intents can reach them outside the view tree.

Scope decisions (differ from the issue)

  • No OS gating. The issue assumed IndexedEntity was macOS 27-only; it is macOS 15+. Every API used is macOS 15 or older, so the feature ships on macOS 26 and 27 alike (agreed with the maintainer). The macOS 27-only IndexedEntityQuery reindex callback is not adopted, since the indexer already re-sends everything on launch.
  • Machines added, which the issue omitted. Images and volumes are left for later.
  • No AppIntentsTesting coverage. It is macOS 27 / Xcode 27 only and cannot run on the macOS 26 gate. Calling perform() directly in unit tests crashes the host, because @Parameter/@Dependency assert outside the intent runtime. So each intent delegates to a plain ComputeIntentActions function, which is unit-tested.

Verification

  • 12 new unit tests (entity lookup, index diff, start/stop/open actions); full BerthlyTests passes; SecondaryViewTests passes, confirming the app still launches after the init change; swiftlint lint --strict clean.
  • Build generates App Intents metadata for all 6 actions and both entities.
  • Manually verified on macOS 27: containers appear in Spotlight and Return opens the container's detail pane; all six actions are listed in Shortcuts, and Start Container ran against a real container.
  • Not run live: Stop and the machine actions (same code path as Start Container, logic unit-tested), and the macOS 26 runtime path (the macOS 26 CI legs are the check).

Note for local testing

macOS registers an app's actions by bundle ID. With several copies of app.berthly.Berthly registered (old DerivedData builds, dist/export, an installed release without intents), the indexer can read the wrong copy and the actions silently don't appear. Unregister stale copies with lsregister -u and check linkd in /usr/bin/log for Checking app.berthly.Berthly at .../extract.actionsdata. Shipped releases are a single copy carrying the metadata, so users are not affected.

🤖 Generated with Claude Code

Adds ContainerEntity and MachineEntity (IndexedEntity) with Start, Stop and Open intents, and a SpotlightIndexer that keeps the index in step with the service. All APIs are macOS 15+, so this ships on macOS 26 and 27 with no gating; the macOS 27 IndexedEntityQuery reindex callback is not adopted. Indexing is skipped under tests so test hosts never write into the system Spotlight index.
Replaces the duplicated per-kind snapshot/diff/index blocks with one generic helper, drops the unused Equatable on IndexDiff, and builds snapshots with uniquingKeysWith so a duplicate id cannot trap.
@henrywang
henrywang merged commit 764bf42 into main Oct 10, 2026
13 of 14 checks passed
@henrywang
henrywang deleted the feat/167-spotlight-app-intents branch October 10, 2026 05:25
henrywang added a commit that referenced this pull request Oct 10, 2026
Closes #189

## What
- **New `BerthlyUITests/AppIntentsSystemTests.swift`:** two tests that
drive Berthly's intents through the system, the path Shortcuts and
Spotlight use.
- Open Container: resolves the mock `worker` container through Berthly's
entity query, runs the intent, and checks the detail pane shows
"worker".
- Start Container: starts the stopped `worker`, then checks the detail
pane shows the Stop button.
- **Guards:** `#if canImport(AppIntentsTesting)`, because the framework
ships only with Xcode 27, plus `@available(macOS 27, *)`.
- **Safety:** each test launches Berthly in mock mode first. The system
routes intents to the running instance, so a system-launched instance
never acts on a real daemon.
- **`project.pbxproj`:** sets `DEVELOPMENT_TEAM` on the `BerthlyUITests`
target only.

## Why the signing change (spike result)
`AppIntentsTesting` answers only a test runner signed by the same
development team as the app. Apple's WWDC26 session "Validate your App
Intents adoption with AppIntentsTesting" states this, and I verified it
locally:

| Runner signing | Result |
|---|---|
| Team-signed | both tests pass |
| Ad-hoc (the previous default) |
`AppIntentsServicesSecurityErrorDomain` code 800, "Your app does not
have permission to perform this" |
| Everything ad-hoc, as `ci.yml` builds | same error |

## CI behavior
- **macOS 26 leg:** compiles the file out (Xcode 26).
- **macOS 27 leg:** builds ad-hoc, so the tests catch the code 800 error
and `XCTSkip`. The run stays green.

So like `BerthlyE2ETests`, these run only on a developer's Mac, with no
extra arguments: a normal ⌘U or `xcodebuild test` on Xcode 27 / macOS 27
includes them. Running them on CI would need a development certificate
in CI secrets, which is not part of this PR.

They stay in `BerthlyUITests`, not `BerthlyE2ETests`. They use mock
data, and `scripts/e2e.sh` re-signs its runner ad-hoc, which would make
them skip.

## Verification
- Local, project signing (team runner): both tests pass.
- Local, CI-style ad-hoc signing: both skip, and the run succeeds.
- Full `BerthlyUITests` with the team-signed runner: 84 tests, 0
failures.
- `swiftlint lint --strict` clean.

## Local gotcha
If stale copies of `app.berthly.Berthly` are registered with
LaunchServices (old DerivedData builds, an installed release), macOS may
not see the intents, and the tests fail on "exactly one container named
worker". Unregister the stale copies with `lsregister -u` (see the PR
#188 description).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
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.

macOS 27: Spotlight indexing + App Intents for containers, images, and volumes

1 participant