docs/PUSH_SETUP.md lists the manual steps left to light up push /
Live Activities end-to-end: APNS key, the one-click Xcode Push
capability, Cloudflare provisioning + secrets (with the bundle-id
topic gotcha: xyz.blakeslee.nucleic-remote, not .remote), host relay
config, and the on-device verification checklist. Also adds
CODE_SIGN_ENTITLEMENTS so the existing entitlements file actually
signs into the app.
Co-Authored-By: Claude Fable 5 <[email protected]>
Brings the phone's ambient surfaces (UX_IOS §5/§8) to maturity:
- Notification pipeline (NotificationRouter): local notifications for
approvals and needs-input transitions while backgrounded; low-risk
approvals are actionable from the banner (Allow requires device
auth, high-risk must open the app's Face ID gate); taps deep-link
to the session; app-icon badge = NEEDS YOU count; resolutions
withdraw the notification (first-responder-wins). The relay's
content-free approval.pending tickle localizes via
Localizable.strings.
- Live Activity: new NucleicRemoteWidgets extension target (lock
screen + Dynamic Island) rendering one aggregate Activity —
N running / M waiting + the most urgent session — started/updated/
ended by LiveActivityManager as session state changes.
- Out-of-band push path: nucleic-edge gains POST /v1/push/register
(admin) so the host can upload tokens for LAN-only pairings; the
host's new PushRelayClient (config-gated on NUCLEIC_RELAY_URL +
NUCLEIC_RELAY_ADMIN_SECRET) mirrors Hello.pushToken to the relay
and wakes non-connected phones when an approval arrives, throttled
per device. Everything stays off until the relay is provisioned.
Worker tests (23) and Swift suites pass apart from the pre-existing
fixture gaps and nvrsion flake. Simulated APNs delivery is blocked in
this environment (notification auth can't be granted headlessly).
Co-Authored-By: Claude Fable 5 <[email protected]>
Four Mac surfaces the iOS remote couldn't render now ride the wire,
forward-compatibly (decodeIfPresent defaults; unknown HostMsg tags
already decode to .unknown):
- DashboardSnapshot gains `usage` (WireSubscriptionUsage — the Mac's
5-hour/weekly quota gauges) and `statusFeeds` (WireStatusFeed —
active provider incidents); the host re-pushes the dashboard when a
poll changes either.
- WireTodo gains `triage` (raw TriageLevel name); the phone renders
the Mac's TriageBadge honoring the encouraging/classic label
setting.
- ClientMsg.fetchDiff / HostMsg.sessionDiff deliver the full worktree
patch on demand (capped at 512 KB with a truncated flag), gated on
the new WireCapabilities.canFetchDiff so a new phone never sends it
to an old host. iOS renders a file list + colored unified patch,
falling back to the diffstat summary against older hosts.
Round-trip and legacy-decode tests cover the new fields; the full
suite passes apart from the pre-existing fake-backend fixture gaps
and the flaky nvrsion lock-domain test (same failure rate on the
base commit).
Co-Authored-By: Claude Fable 5 <[email protected]>
Port the Mac's MarkdownText renderer, AppPalette (color-vision modes +
night-softening), appearance settings (theme/text size/color vision),
and BuildBanner channel strip to the iPhone remote. Add session-row
attention/unseen-completion washes and marker icons, Discard action,
branch/worktree options in the new-chat composer, and a transient
error bubble for host wire errors. Stamp the build channel via
NucleicChannel in Info.plist (ios-release.sh passes NUCLEIC_CHANNEL,
default beta). Demo mode gains NUCLEIC_DEMO_SESSION and skips the
notifications prompt so UI previews are scriptable.
Co-Authored-By: Claude Fable 5 <[email protected]>
Concludes an in-progress merge the auto-lander could not finish with
partial commits. Lands the iOS host_exec command breakdown and the
RemoteStore preview-data missing-return fix.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Squash-promote the nucleic/trunk work (Sessions Auto-Switching, Play
System Sounds Configuration) into dev. Resolved the two divergent
conflicts where dev and trunk had independently implemented the chat
status-sound feature, keeping trunk's canonical version:
- SettingsView: the Sounds settings live under the General tab (trunk's
placement); removed dev's duplicate Sounds section from the Chat tab.
- AppStore: adopt trunk's centralized ChatCueState/announceChatCue
edge-detection (fired from upsertSummary/upsertSummaries), and remove
dev's superseded chatStatusSound(...) helper and its two hand-placed
call sites. Also removed a silent merge-duplicated upsertSummaries.
Nucleic-Promote: 1
Co-authored-by: Nucleic <[email protected]>
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Squash-merge nucleic/trunk into dev (234 commits of accumulated nvrsion work).
Resolved two auto-generated-file conflicts:
- cloud/nucleic-edge/wrangler.jsonc: kept dev's real KV namespace ids and the
xyz.blakeslee.nucleic.remote APNS topic, with binding names corrected to
RELAY_TOKENS/PUSH_TOKENS to match the worker code on both branches.
- Package.resolved: took trunk's newer dependency pins.
Nucleic-Promote: 1
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Desktop channels → xyz.blakeslee.nucleic.desktop.{dev,beta,rc,release} (the stable channel keyword is unchanged; only its bundle-id suffix is "release"). iOS remote → xyz.blakeslee.nucleic.remote, including its Keychain account namespaces, the scanner log subsystem, and the coupled APNS topic.
Correct the Apple Developer Team ID to L7UDTQ6F5W across the Xcode project, ExportOptions, the APNS config + tests, and the signing/cloud docs.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
On-device logs proved the camera is being blocked by device state, not
the app: every retry showed window=true app=active scene=foregroundActive
yet reason-1 (videoDeviceNotAvailableInBackground) persisted for 6s — the
signature of iPhone Mirroring / a locked tethered device, where iOS
disables the camera (the recurring com.apple.PointerUI log is the Mac
pointer driving the phone).
Rather than sit on a black screen after retries are exhausted, report a
new .couldNotStart state explaining the likely cause (locked / mirrored)
with a "Try Again" button that rebuilds the capture session from scratch
(via .id(retryToken)). App-side capture code is correct; this is a
graceful fallback for an environment that withholds the camera.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
On device the camera still reported reason 1 (videoDeviceNotAvailable-
InBackground) even when started from viewDidAppear with the app
foreground-active: the camera assertion isn't granted until the sheet's
presentation transition fully settles (a few hundred ms), and the
back-to-back retries all fired inside that unsettled window.
Add a short delayed retry (0.5s, bounded to 12 attempts) driven by the
interruption notification, plus a proactive +0.6s retry after the view
appears, resetting the counter once the session actually starts. Also log
the window/app/scene activation state to confirm the foreground signals.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Root cause (from on-device logs): the session was started from
viewDidLoad while the pairing sheet was still presenting, so iOS
interrupted it with reason 1 (videoDeviceNotAvailableInBackground) and it
never ran (isRunning=false) — a black preview. The CMVideoFormatDescription
-12710 errors were unrelated noise.
Defer startRunning until the camera can actually run: configure the graph
up front, then start only once the view is on screen and the app is
foreground-active (viewDidAppear + a guarded startSessionIfReady). Recover
on AVCaptureSessionInterruptionEnded / didBecomeActive / runtimeError. All
start triggers funnel through the session queue and no-op if already
running, so it's idempotent.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Black preview persists on a real device though the session reports
running, so instrument the capture lifecycle to pinpoint it: logs the
authorization status, the selected device, isRunning/inputs/outputs after
startRunning, and on AVCaptureSessionDidStartRunning the preview layer's
connection (present/active/enabled) plus the view/layer bounds. Also
observes AVCaptureSessionRuntimeError and ...WasInterrupted.
Filterable via os.Logger subsystem com.nucleic.remote / category scanner
(marker "📷"). To be trimmed once the root cause is fixed. Also sets an
explicit .high preset and gates metadataObjectTypes on availability.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The feed stayed black even when the session reported running: the preview
was a manually-framed sublayer (frame set in viewDidLoad/viewDidLayout,
which raced the async permission callback and could end up zero-sized),
and the session was configured across threads (addInput/Output on main,
startRunning on a queue).
Switch to the canonical AVFoundation pattern: the preview is now the
view's backing layer (CameraPreviewView via layerClass), so it always
fills the view with no frame bookkeeping, and all session configuration +
start/stop run on a dedicated serial queue with delegate/state callbacks
hopped to main.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The QR scanner sat on a silent black screen when the camera couldn't
start: it called AVCaptureDevice.default(for: .video), got nil (e.g. on
the Simulator, which has no camera), and bailed via an early guard with
no UI feedback. It also never requested camera permission (a denied
device stayed black with no recovery) and sized the preview layer only
once in viewDidLoad.
Now it gates on AVCaptureDevice authorization (requesting access when
undetermined, surfacing a Settings link when denied), reports an
"unavailable" state when there's no camera so the UI explains the black
screen, and updates the preview frame in viewDidLayoutSubviews. Camera
runs work on a background queue.
Co-Authored-By: Claude Opus 4.8 <[email protected]>