The host surfaces (design §5.2/§5.2a). A bound host wears a tinted chip that says
what a click will do. The card menu grows "Connect with ▸" — a ONE-OFF that never
rebinds, with a checkmark on the binding and an explicit "Set Default Profile" as
its last item for the users who do want to rebind from there — plus "Pin as Card ▸"
and "Copy Link".
A pinned host+profile combo becomes its own card next to its host: same record,
same live status, the profile as the prominent subtitle, one click to connect. It
is an extra grid ENTRY, never a duplicated host record — duplicating would fork
pairing, Wake-on-LAN and renames. Its menu carries only connect-shaped actions;
edit, pair, forget and remove stay on the primary card, where the thing they act on
lives. On the gamepad carousel and tvOS the same pins are tiles, which is the whole
point: focus-and-press is what those surfaces do well, and menus are not.
The edit sheet binds and pins; both rows vanish when no profiles exist, so a user
who never makes one sees exactly today's sheet. A dangling binding renders as
"Default settings (profile deleted)" and is cleaned up on save.
The speed test finally writes where the tested host reads. Unbound: the global, as
before. Bound to a profile that overrides bitrate: that override. Bound to one that
inherits it: both are offered, because either is defensible and guessing would
silently pick one. Every button names its target, and the probe now runs at the
mode that host would actually stream — the measurement is the streaming path.
Shortcuts gains `ProfileEntity` over the App-Group catalog and a Profile parameter
on Connect, still round-tripping through the URL router rather than opening a second
connect path. Connect and Wake drop their iOS wall — AppIntents is real on macOS and
tvOS, and "Stream Desktop with Work" from Spotlight was the ask; only the phrases
provider stays iOS-gated, since it bundles the LiveActivityIntent. The deep-link
observer moved out with them: an intent that posts to nobody is a shortcut that
silently does nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
LocalizedStringResource is ExpressibleByStringLiteral, so a single literal
converts implicitly, but the "…" + "…" concatenation is a runtime String it
can't convert. Collapsed to one literal.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The extension-side + App Intents surface for design/apple-live-activities-and-
widgets.md. The iOS-framework code (WidgetKit/ActivityKit/AppIntents) can't be
compiled by the macOS `swift build` CI target and needs the Xcode widget-
extension target that only exists once created in the GUI — see the checklist in
the memory note. What macOS DID verify: HostEntity (AppIntents is available on
macOS), the shared attribute/notification plumbing, and that nothing regressed
(142 tests green).
Shared (PunktfunkShared):
- PunktfunkSessionAttributes (ActivityAttributes) — the one type app + extension
share; gated os(iOS) (ActivityKit imports on macOS but its types are
unavailable, so canImport would wrongly admit it).
- EndStreamIntent (LiveActivityIntent) — posts .punktfunkEndActiveSession.
- HostEntity + HostEntityQuery (AppEntity over the shared store) — the intent /
widget-config parameter type; canImport(AppIntents), so macOS type-checks it.
- New notifications: end-active-session, open-deep-link.
M1 widget extension sources (Sources/PunktfunkWidgets/, NOT a SwiftPM target —
`swift build` ignores the dir):
- PunktfunkWidgetBundle (@main): HostsWidget + PunktfunkSessionLiveActivity.
- HostsWidget (kind "PunktfunkHosts"): reads the shared-suite store, sorts by
recency, deep-links each host; small/medium/accessory families; empty state.
- SessionLiveActivity: Lock-Screen banner + Dynamic Island (elapsed timer,
mode line, background countdown, End button).
M3 controller (app, iOS): SessionActivityController owns the Activity lifecycle
(request/update/end + launch orphan-sweep + staleDate); ContentView drives it
from the model's phase/isBackgrounded/backgroundDeadline (which SessionModel now
publishes), keeping ActivityKit out of the cross-platform model.
M4 (app, iOS): ConnectToHost/WakeHost intents + AppShortcutsProvider; Connect
routes via .punktfunkOpenDeepLink into the same onOpenURL router (one set of
guards); Wake reuses the WoL path; End surfaced to Shortcuts too.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>