The relay Worker (nucleic-edge at relay.nucleic.blakeslee.xyz) is deployed, so land the
formerly deploy-gated client side of the data path:
- NucleicProtocol/Sync/RelayTransport.swift: RelayAPI (one base URL for REST + WS,
membership -> connection token trade), RelayWebSocket (ordered sends, ping keepalive,
ping-confirmed connect), RelayFrameChannel (client leg, WireFraming inside WS binary,
presence fail-fast when the room has no host), RelayPresence.
- NucleicCore/Sync/RelayAccess.swift: X25519 PoP enrollment (RelayEnrollment), room
credential in the login Keychain (separate from the push credential), membership minting
with re-enroll-on-401.
- NucleicCore/Sync/RelayListener.swift: host SyncListener demuxing the room socket into
per-tag virtual FrameChannels via RelayEnvelope; presence-driven reaping; backoff redial;
injectable RelayRoomSocket seam for tests.
- Wire: additive HostMsg.relayMembership(WireRelayMembership) pushed after every hello
(SyncHost.register -> AppStore mint) so devices paired before the relay adopt it and the
~90-day token refreshes on each connect; the pairing QR also carries a bootstrap
membership so first contact can ride the relay. Old clients ignore the unknown tag.
- AppStore: .relay joins the listener composite behind the Connection-methods checkbox
(failure degrades to a status row), advertises relayRoomID in PeerAddresses, mints the
QR bootstrap in beginPairing.
- Desktop UI: the Nucleic Private Relay toggle is enabled (was "coming soon"); the
LAN-only banner offers it alongside Tailnet.
- iOS: relay is the last dial candidate in HostConnection pair + reconnect (10s handshake
watchdog); PairedHost persists relayRoomID/relayMembershipToken/relayURL; the
relayMembership push updates the registry in place; Settings shows Relay in Transports.
Tests: RelayTransportTests, RelayListenerTests, SyncHostTests relay push + QR bootstrap.
Full suite green (783 core + 113 protocol + 2 new); iOS simulator build succeeds. Live
smoke test against the deployed Worker passed end-to-end (PoP enroll, both token tiers,
two-socket frame round-trip through the Room DO with correct envelope tags).
Known limits: host revoke-on-unpair not wired (endpoint is admin-only); PeerClient
(Mac<->Mac) doesn't dial the relay yet.
Co-Authored-By: Claude Fable 5 <[email protected]>
The iOS [HostID: HostConnection] multiplexer: connect to all paired Macs at once, mirror the active
one, instant host switching, cross-host badge/Live Activity. Compile + demo-verified; the live
multi-connection path needs a two-Mac test (may be rolled back).
Resolves the RemoteStore.swift overlap with dev's Tailnet-node-name/auth-key change by taking the
multiplexer version (which moved the tailnet config into HostConnection); dev's authKey removal is
re-applied there.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
# Conflicts:
# ios/NucleicRemote/NucleicRemote/Models/RemoteStore.swift
Completes the multiplexer rewire begun in f335eb4. RemoteStore no longer runs an inline single
connection — it owns `connections: [HostID: HostConnection]` and connects every paired Mac at once,
so all your Macs are live simultaneously.
- reconnect() connects/reconnects all paired hosts (dropping since-unpaired ones); pair() adds a new
connection and makes it active while the others keep running; unpair() drops the active Mac and
switches to a remaining one.
- The flat @Published state (connectivity/sessions/dashboard/capabilities/…) mirrors the *active*
connection via callbacks, so switchHost() is now instant — every Mac is already connected, so it
just re-points at the target's live state (no reconnect).
- Aggregate concerns merge across hosts: the app-icon badge counts needs-you across ALL Macs, Live
Activity summarizes all live sessions, and an approval answered from a notification is broadcast to
every connection (the owner resolves it; the notification carries no hostID yet). Intents (send /
open / approvals) route to the active connection.
Removed ~450 lines of connection machine from RemoteStore (now in HostConnection); kept
friendlyTransportError/showError/dismissError. Demo bypasses connections (seeds state directly).
Compiles (iOS Simulator BUILD SUCCEEDED) and the demo host switcher is runtime-verified (screenshots:
switching Macs swaps the session list + tab badge). The LIVE multi-connection path — simultaneous
dials, reconnect/teardown, the shared embedded tailnet node — is compile-verified only; it needs two
real Macs to validate.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Begins the simultaneous multiplexer. HostConnection.swift is the per-host connection engine lifted
out of RemoteStore: one instance owns a single Mac's SyncClient, LAN→tailnet candidate chain,
reconnect backoff, tailnet-node lifecycle, and event stream, and keeps that Mac's projection
(connectivity, sessions, dashboard, capabilities, meshPeers). It talks back to its owner through a
Callbacks struct so aggregate concerns — the badge, Live Activity, notifications, and the single open
transcript — can be merged across hosts by the coordinator.
Not yet wired: RemoteStore still runs its own inline single-connection machine. The next step swaps
RemoteStore onto a `[HostID: HostConnection]` map (connect all paired Macs, aggregate their sessions,
route intents by host) — the behavior-critical part, to be verified with two real Macs. Compiles
(iOS Simulator BUILD SUCCEEDED); no behavior change yet (the engine is unused until the rewire).
Also: RemoteStore.friendlyTransportError made non-private so the engine shares it.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The safe, verifiable slice of iOS multi-host: switch which paired Mac the phone views, reusing the
existing (proven) single-connection reconnect — no rewrite of the connection state machine. The
simultaneous [HostID: HostConnection] multiplexer stays deferred (it needs two real Macs to verify).
RemoteStore: an `activeHostID` (the paired Mac the flat projection reflects); `hostChoices` (paired
registry live, mock hosts in demo); `switchHost(to:)` — live re-points via reconnect(), demo swaps
the mock host preserving in-demo edits; `reconnect()`/`pair()` set the active host; `unpair()` now
forgets the *active* Mac and switches to a remaining one if any (identical for a single host). Demo
seeds two mock Macs ("Andrew's Mac", "Studio Mac") with distinct sessions/dashboards.
SessionsView: a host-switcher menu in the toolbar, shown only when >1 Mac.
Verified in the iOS Simulator (demo mode): the switcher lists both Macs, and switching swaps the
whole session projection + the tab badge (screenshots taken). Single-host behavior is unchanged
(one host ⇒ switcher hidden ⇒ flat state exactly as before).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
First step of Phase 3 (iOS multi-host), per docs/MESH_TRANSFER.md. Converts IdentityStore from a
single `nucleic.pairedHost` slot to a `nucleic.pairedHosts` registry (ordered [PairedHost], keyed by
fingerprint = hostID), with a one-time migration of the legacy single value on first read (then the
old key is removed) so an upgrade keeps its Mac.
New registry API — pairedHosts(), pairedHost(id:), upsertPairedHost(_:), removePairedHost(id:) — for
the coming RemoteStore multiplexer. A single-host bridge keeps every current caller unchanged and
behavior identical: loadPairedHost() returns the active (most-recently-paired) host, savePairedHost
upserts + makes active, clearPairedHost removes the active. RemoteStore is untouched and still holds
one connection.
Settings gains a "Paired Macs" list (the visible artifact): each registered Mac with its fingerprint,
an "Active" marker, and a per-host remove (removing the active one unpairs the live connection;
removing another just forgets it) — this also gives a removal path now that pairing a new Mac keeps
the old one in the registry instead of overwriting it.
Verified with an iOS Simulator build (BUILD SUCCEEDED). Remaining Phase 3: the RemoteStore
[HostID: HostConnection] multiplexer (simultaneous connections, (hostID, sessionID) keying),
host-qualified notifications/Live Activity, host switcher UI, demo N-hosts, and the two-Mac
tailnet spike.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The P4/P5 mesh work added ClientMsg cases (addressUpdate, transferOffer/Chunk/Commit/Cancel)
and SyncClient.Event cases (transferAccept/Reject/Ready/Committed/ChunkAck) but never updated
the iOS RemoteStore's two exhaustive switches — the demo-simulator ClientMsg handler and the
event handler — so NucleicRemote failed to compile ("switch must be exhaustive"). The app target
isn't built by `swift build`, so this landed unnoticed on the branch.
Both are inert on the phone: it's never a transfer source/destination and demo has no peer Macs,
so the new cases join the passive `break`. Verified with an iOS Simulator build (BUILD SUCCEEDED),
which also confirms this session's moved/arrived-provenance rendering edits compile.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Completes Phase 5 of the multi-device mesh / session-transfer program (docs/MESH_TRANSFER.md)
except the two-Mac memory-carry spike. All additive + capability-gated; SyncProtocol stays v1.
- Moved-session visibility: additive SessionSummary.movedTo (MovedDestination), decode-defaulted.
A moved session no longer silently vanishes — the source keeps a read-only "Moved to <Mac>"
tombstone under Archived (name resolved live from paired Macs), surfaced on relaunch without
rebuilding a runnable controller, and sent on the wire so phones see it too.
- Relaunch recovery driven from launch (+ on every peer reconnect, single-flight):
AppStore.recoverInterruptedTransfers clears abandoned pre-tombstone locks, discards orphaned
inbound staging, and re-drives a tombstoned commit via SessionTransferCoordinator.recoverTombstoned
(bounded, idempotent; a dest that lost staging leaves the lock, never revives the source).
- Bulk "Hand off active sessions…": transferableSessions + moveSessionsToPeer (sequential, rollup
error) behind a "Hand off…" button → HandoffSheet checklist in RemoteAccessView.
- Arrived-from provenance (mirror of moved-to): GRDB v24 arrived_from_device_id/arrived_at; the
importer stamps them at staging; additive SessionSummary.arrivedFrom (ArrivedFrom); a subtle
"Arrived from <Mac>" marker on the sidebar (live name) + iOS row (host-baked name).
- Stranded-arrival "Activate anyway": the importer persists the staged Session to the staging dir
at .ready, so a destination that relaunches before commit can recoverableInboundTransfers() and
activateRecoveredTransfer()/clearInboundStaging(). AppStore surfaces pendingArrivedTransfers with
activate/discard, shown in a new "Interrupted arrivals" section. (A .ready lock with no manifest
is now cleared as unrecoverable.)
Tests: +6 core, +2 protocol across WireMessageTests, SessionTransferTests, AppStoreTests,
AppStoreSyncBridgeTests. Full package builds; Swift suites green. iOS NucleicRemote edits reviewed
but not compiled here (separate Xcode target).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The mesh/session-transfer work added `ClientMsg.listPeers`, but the phone's
demo-mode `demoHandle` switch (which lists its cases explicitly, no default)
was never updated — leaving the NucleicRemote target non-exhaustive and failing
to build (pre-existing on dev, surfaced by the iPad-port merge). Demo mode has
no real mesh peers, so treat `.listPeers` as passive alongside the other
read-only requests.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The desktop CommandStepsCard breaks a shell pipeline (a rm/git chain) into a
step list with the destructive delete flagged, instead of a raw blob. Rather
than duplicate a parser, deliver that value by reusing the existing, tested
HostCommandSummary + HostCommandBreakdown (a general command parser whose
Invocation already carries a `destructive` flag for rm/rmdir) on the two paths
that still showed raw text:
- ToolCallCard.details: a shell tool whose command has >1 invocation or is
destructive now renders the compact step breakdown (deletes glyphed/tinted in
red) with the literal command under "Show command"; simple one-liners keep the
plain input block.
- ApprovalCardView: the parsed breakdown (with its sudo/deletes risk banner) now
covers any Bash pipeline / destructive approval, not just host_exec.
Demo transcript gains a destructive cleanup pipeline (rm -rf && git worktree
prune && git branch -D) so the step list is exercisable offline.
Verified in the iPad simulator: the pipeline expands to a step list with
`rm -rf` flagged in red.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Port the desktop GitBlockCard's commit rendering (+ CommandDisclosure) to the
remote transcript: a Bash `git commit` now shows the commit subject as a
headline and the message body as Markdown, with the raw command one tap away
under "Show command" -- instead of a raw `git commit -F - <<'EOF' ...` blob.
- GitCommitCard.swift: the card + the CommandDisclosure helper (portable
SwiftUI, mirroring Sources/NucleicApp/{GitBlockCard,CommandDisclosure}).
- GitCommitSummary.parse (in HostCommandSummary.swift) extracts the message
from -m/--message args or a `-F -` heredoc body, reusing the file-private
shell Lexer already used for host-exec summaries; skips env prefixes and git
global flags, and only fires for real `git commit` segments.
- Hooked into ToolCallCard.details, gated to shell tools so non-shell input is
never misread as a commit.
- Demo transcript gains a git-commit call so the card is exercisable offline.
SummaryCard is intentionally not ported -- it's the host-only Apple-Intelligence
recap, which a remote has no way to generate.
Verified in the iPad simulator (demo mode); parser covered by a standalone test.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Grow the universal NucleicRemote app into a width-adaptive shell so a
regular-width iPad renders the macOS sidebar+detail IA while the iPhone
keeps its TabView -- both over the same RemoteStore projection (one host
authority, N renderers).
Phase 1 (adaptive shell):
- AdaptiveRootView branches on horizontalSizeClass: CompactRootView (the
existing iPhone TabView, moved verbatim) vs SplitRootView
(NavigationSplitView) on regular width.
- SplitSidebar: Home/Projects/To-dos/Settings destinations + sessions
grouped under their projects (attention-sorted), connection chip footer.
- SplitDetail selects a destination or a session; a selected session reuses
SessionDetailView keyed .id(sessionID) so switching drives open/close.
- RemoteStore.closeOpen(_:) is now id-guarded so a split-view A->B switch
(onAppear(B) before onDisappear(A)) can't tear down B's fresh subscription.
- IdentityStore.deviceID idiom-tags the prefix (ipad-/iphone-) for new
installs so the host lists a paired iPad correctly.
Phase 2 (width tuning + diff):
- readableColumn() caps+centers Home and the transcript on wide layouts;
a no-op at phone/portrait width.
- SessionDiffView switches on available width (GeometryReader): a Mac-style
two-pane diff (file list + selected file's patch) on wide/landscape, the
phone stack otherwise. UnifiedPatch splits the combined patch per file.
Read-only, same wire, no protocol change.
- Demo diff fixture now carries both files' patches.
iPhone layout and behavior unchanged. Builds clean; verified in the iPad
simulator (demo mode).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Tailscale (Tailnet) sync transport: transport picker in Settings ▸ Remote,
interactive browser login, and LAN↔tailnet fallback.
Semantic merge fixes (both sides compiled alone but not together):
- handleTransportFailure (dev's friendly transport errors) now checks
connectivity.isLive — .connected gained a transport payload on the branch —
and stays silent while the branch's connect chain still has candidates to
try, so a LAN probe failing over to the tailnet doesn't flash a red error.
- Chain-exhaustion paths keep a friendlier transport-level failure message
when onFailed already surfaced one instead of clobbering it with the
generic handshake error.
Verified on the merge: swift build + 37 sync/transport/store tests green
(incl. dev's FilePairedDeviceStore + the now-fixed contract tests), iOS
simulator build green.
Co-Authored-By: Claude Fable 5 <[email protected]>
Browser login — the auth key is now optional on both platforms. When the
embedded node starts with no key, TailnetNode asks the backend (LocalAPI
backendStatus — IPN state, deliberately not filesystem heuristics: tsnet
writes logs and a machine key on every start, registered or not) whether a
login is needed, triggers login-interactive, surfaces the auth URL through a
new statusStream()/.needsLogin, and waits for Running (4-minute deadline,
generation-fenced against stop/restart). The Mac auto-opens the login page
from Settings ▸ Remote and shows a re-open button; the iPhone auto-opens only
during user-initiated pairing (a background reconnect that suddenly needs a
login must not eject the user to Safari — Settings ▸ Tailscale carries the
link). The remote-access toggle now reflects the in-flight start instead of
snapping off for the whole login window, and toggling off mid-start is
honored at both commit points (before and after host.start).
LAN↔tailnet fallback — picking Tailnet now keeps LAN on too: the host runs
both listeners under a new CompositeSyncListener (merged accept stream; one
child ending doesn't end the rest) and the pairing QR carries both hints.
The phone builds an ordered candidate chain — LAN first (QR hint or Bonjour),
tailnet second — and walks it on pair and reconnect, so a phone that leaves
the Mac's Wi‑Fi rolls over to the tailnet and rolls back when it returns.
A per-channel 4s connect guard (readiness-checking, bound to exactly its
channel) keeps a stale LAN hint from hanging the chain; a stale-client guard
in consume() keeps a replaced client's tail events from advancing it; chain
exhaustion during pairing lands in a terminal failure instead of spinning on
"Connecting…"; routine pre-fallback handshake errors no longer flash the red
error bubble. "Connected · LAN / Tailnet" shows whichever transport won.
Multi-agent review: 11 confirmed findings (incl. the login gate being dead
code via tsnet's eager state-dir writes, and two connect-timeout races), all
fixed and re-verified. Suite green (24 sync-related tests incl. 3 new
CompositeSyncListener tests); macOS + iOS builds clean.
Co-Authored-By: Claude Fable 5 <[email protected]>
Settings ▸ Remote gains a "Connect via" picker — LAN (default), Tailscale
(tailnet), or Relay (disabled, coming soon). On Tailnet, both devices run an
embedded tsnet node via TailscaleKit (tailscale/libtailscale) and sync frames
flow over the user's tailnet, so the phone can connect from anywhere the
tailnet reaches; Noise E2EE runs above the transport unchanged.
- NucleicTailnet (new target, macOS + iOS): TailnetNode wraps TailscaleKit's
node lifecycle (auth-key login, generation-fenced start/stop since up() is
un-cancellable) and drops to the framework's public C API for the data path
— tailscale_dial/listen/accept hand back full-duplex socketpair fds, wrapped
by FDFrameChannel (DispatchIO) into the shared FrameChannel seam. The Swift
wrapper's one-way connection actors can't carry a bidirectional stream.
- Host: TailnetListener adopts SyncListener; startSyncServer is single-flight
and honors toggle-off/picker changes at the commit point; pairing QRs carry
transport + tailnet IP/port hints (PairingPayload additive optional fields,
forward/backward compatible over CBOR).
- iPhone: pair/reconnect dial over whichever transport the pairing recorded;
Settings gains a Tailscale auth-key field (Keychain, committed on editing
end); connectivity chip shows "Connected · Tailnet".
- TailscaleKit has no SwiftPM distribution: scripts/build-tailscalekit.sh
builds a pinned libtailscale commit into an untracked local xcframework;
Package.swift links it only when present (everything builds without it, the
picker then reports Tailscale support as not built in), and the script
clears SwiftPM's content-keyed manifest cache so the toggle is picked up.
- iOS floor 17.0 → 18.1 (TailscaleKit requires the iOS 18 Swift runtime);
package-app.sh embeds the framework in the .app like Sparkle.
703-test suite: no new failures (the 7 fake-claude/fake-grok staging issues
reproduce identically on an untouched checkout — pre-existing, tracked
separately). New coverage: FDFrameChannel over socketpairs, pairing-payload
version-skew both directions, transport-setting resolution.
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]>
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]>
Bring NucleicRemote closer to desktop parity in two areas (the core
sync loop was already at parity — shared protocol, control scope).
Transcript fidelity (iOS): a client-side TranscriptProjection coalesces
streaming text by messageID and folds each tool call's lifecycle
(start/deltas/complete/result/fileChange) into one expandable card —
fixing the duplicate started+completed rows. Adds Markdown bubbles, the
gold Orchestra card for Task/Agent spawns, and the previously-dropped
usage/cost, rate-limit, file-change, turn-boundary and session-started
rows, plus a context-window % header badge.
Mid-session controls + model catalog (protocol/host/iOS): project the
host ModelCatalog over the wire as WireModelCatalog (in Welcome); add 5
control-scope setters (setSessionModel/Effort/Auto/AutoShip/ShipBranch)
backed by the existing AppStore.mutateSession + SessionController hooks;
enrich WireSessionSummary with model/effort/auto/autoShip/shipBranch/
contextInputTokens (all forward-compatible). The composer gains a model
picker and a catalog-driven effort menu (per-backend caps: Codex→xhigh,
Grok→auto), and the session header gains a model/effort/auto/autoship
control bar.
Tests: CBOR round-trips for the new messages, Welcome.modelCatalog, the
new summary fields, forward-compat decode of old bytes, and the setters
reaching the host. Verified in the Simulator (NUCLEIC_DEMO=1).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Design system: Palette (teal accent + the Mac's categorical status colors, status→color
refined by turn disposition), StatusStyle glyphs/labels, a card surface modifier, app-wide
teal tint.
Fixes the reported bug: Sessions now hides archived chats and uses disposition to split
NEEDS YOU from DONE (a finished-the-work turn no longer shows as needing you). Rows gain
status dots, favorite star, approval-count badge, diffstat; swipe to favorite/archive/delete.
New surfaces, full control-scope parity with the Mac:
- Home: greeting + day-streak flame, GitHub-style activity grid, stat cards, quick to-dos,
and a start-chat composer (project picker + Auto).
- Projects: per-project session counts → project detail with a scoped composer.
- To-dos: capture, group by project, complete/dispatch/delete.
- Session detail: a control menu (rename, favorite, interrupt, integrate merge/squash/rebase,
archive, delete).
RemoteStore handles the dashboard event + exposes all control intents; claims control scope.
A NUCLEIC_DEMO/NUCLEIC_TAB env seam seeds mock state for offline UI preview.
iOS app builds for the iOS 27 simulator; Home + Sessions verified rendering via screenshots.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
A real iOS Xcode app (ios/NucleicRemote) linking the NucleicProtocol SwiftPM
library as a local package. Builds for the iOS 27 simulator and launches to the
pairing screen.
- Transport: NWFrameChannel (NWConnection) + LANDiscovery (Bonjour _nucleic._tcp).
- Engine: drives NucleicProtocol.SyncClient (Noise XXpsk0 pair / IK reconnect,
hello/welcome, HostMsg→Event stream).
- State: RemoteStore (ObservableObject) — the single on-device projection of host
state; IdentityStore persists the device identity (Keychain) + pinned host.
- UI (UX_IOS): attention-first SessionsView, SessionDetailView (transcript/diff +
status-driven action area / composer), ApprovalCardView with Face ID gate on
high-risk approvals + allow-always menu, PairingScannerView (AVFoundation QR),
SettingsView, connection chip. Same status glyphs/semantics as the Mac.
Add-iPhone QR display + server start live on the macOS side (follow-up); push /
Live Activity are M5 (needs the relay). gitignore keeps this .xcodeproj despite
the blanket *.xcodeproj rule.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>