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]>
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]>
Lengthens the glow pulse period from 0.9s to 2.6s (macOS + iOS), keeping the bold
amplitude — the easeInOut lingers at the baseline, so each pulse now lands
deliberately with a clear gap rather than strobing. The effort-label sparkle keeps
time with it.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Swaps the Orchestra accent from violet to a warm gold (AppTheme.orchestra +
iOS Palette.orchestra), so the composer glow, effort label, and subagent cards all
read gold. Tunes the glow harder — a ~0.9s pulse (was 1.8s) with a thicker stroke
and a bigger outer bloom that swells on the beat — so an Orchestra turn announces
itself. Reduce Motion still holds the glow steady. Against a Control project's
lavender accent, the gold glow stands out cleanly.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Orchestra is a Nucleic Control capability now:
- Core: SessionController withholds the fan-out consent unless the session's project
is under Control (resolvedEffort still maps the sentinel to xhigh, so a stray
selection never reaches a backend verbatim). Covered by a new SessionController test.
- macOS: the effort menu shows Orchestra disabled with a 'Requires Nucleic Control'
note + tooltip off-control; effectiveEffort demotes a carried-over selection so the
label/glow never show it active where it can't run.
- Home composer themes lavender (the Control accent) for a Control project — the
Auto/Merge accents and a persistent ring on the field, tracking the project picker.
- Remote: WireProject carries isNucleicControlled (decode-tolerant) so the iOS effort
picker gates Orchestra the same way.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Renames the orchestration mode's user-facing brand and all code identifiers
(UltracodeStyle→OrchestraStyle, isUltracode→isOrchestra, AppTheme.ultracode→
.orchestra, etc.). The canonical effort sentinel becomes "orchestra"; the legacy
"ultracode" token is still recognized so a persisted/in-flight session keeps
working. Comment referencing Claude Code's own ultracode mode kept accurate.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
dev received an intermediate snapshot of this branch that had the accent
scoping machinery (AppPalette.make(controlled:), controlAccent, RootView's
contextProjectID gate) but NOT the reverts of the base accent back to teal.
Result: the standard palette's base accent AND controlAccent were both
lavender, so control and non-control projects rendered identically lavender.
Re-assert the defaults so the gate is actually visible:
- macOS AppTheme: standard base accent -> teal (0.04,0.52,0.50); controlAccent
stays lavender (0.45,0.32,0.82). Non-control/home now render teal; only
Nucleic Control projects get lavender.
- iOS NucleicRemote: Palette.accent + AccentColor.colorset back to flat teal
(remote has no Control concept).
Merges current dev first so this branch is strictly ahead of it — autoshipping
it back to dev will now override dev's lavender base with teal.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Make the lavender/purple accent a signal that you are operating inside a
Nucleic Control project; every other project keeps the standard teal tint.
- AppPalette.make gains a 'controlled' flag. When true (and in standard
color-vision mode) it swaps the teal accent for a cached, nightSoftened
lavender (controlAccent); otherwise it returns the unchanged palette.
- RootView derives 'controlled' from store.contextProjectID ->
Project.isNucleicControlled, so the tint follows the active project and
reverts to teal on the home dashboard / non-control projects. Tracked
through Observation, so it flips live as you navigate.
- Color-vision (deut/prot/trit) palettes keep their tuned accents; the
swap only applies in standard mode.
- iOS NucleicRemote reverts to the standard teal: the remote has no
Nucleic Control concept (the wire protocol doesn't carry it), so it
can't distinguish controlled projects and stays teal everywhere.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Switch the standard brand accent from deep teal to lavender/purple across
macOS and iOS, with a distinct shade per appearance:
- macOS (AppTheme standard palette): accent -> (0.45, 0.32, 0.82). ~5.4:1
with white labels in light; nightSoftened() derives the muted dark shade.
- iOS (Palette.accent): now an appearance-aware UIColor — deep violet in
light, brighter lavender (0.66, 0.55, 0.95) in dark so it lifts off the
near-black background.
- iOS AccentColor.colorset: add a dark luminosity variant to match.
Accessibility color-vision palettes (deuteranopia/protanopia/tritanopia)
are intentionally left tuned for distinguishability; tritanopia already
renders violet.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The first pass put the detail in a ScrollView with fixedSize(horizontal:)
and a fixed maxHeight, so the command was forced onto one long line that
scrolled horizontally and the box always claimed full height — a tall box
with a lot of empty space.
Replace it with an ApprovalDetailBox (macOS + iOS) that wraps the text and
measures its content height via a PreferenceKey, sizing the box to fit and
only scrolling vertically once the content exceeds the max height. No more
horizontal scroll; short commands sit in a snug box.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Permission requests only ever displayed a summary that was hard-capped:
RiskClassifier.title() slices to 80 chars, and the iOS card used a lossy
compactSummary clipped to 8 lines. The full tool input was present on the
request but never shown, so a long Bash command, file path, or URL was cut
off with "…" and the user couldn't see what they were granting.
- Add RiskClassifier.detail(toolName:input:): the full, untruncated content
(full command/path/url/query), falling back to the pretty-printed input so
nothing about an unrecognized tool is hidden.
- Add JSONValue.prettyString() for indented, multi-line display.
- macOS ApprovalBar: render the detail in a bounded, scrollable, selectable
monospaced block (shown only when it adds beyond the already-shown title).
- iOS ApprovalCardView: replace lossy compactSummary/lineLimit(8) with the
full content in a bounded scroll view via JSONValue.approvalDetail.
- Tests: assert detail is full and untruncated, with pretty-input fallback.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Animate the Mac streak lightning bolt; intensity (speed, scale, brightness,
rotation jitter, glow) ramps up with streak length.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
# Conflicts:
# Sources/NucleicApp/HomeView.swift
Surface actively-running sessions on the phone home dashboard: a fourth
"In progress" stat card and a list pinned above the to-dos. Both share one
definition (live sessions with status .running), and list rows link into the
session transcript.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Add a `.note` AgentEvent kind — a system-generated log line — and emit one
into the open session's transcript whenever a control in the header Git menu
is used (merge/squash/rebase outcome, open in Terminal, reveal in Finder,
copy branch/worktree path). Renders as a muted note on both macOS and iOS.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The macOS ApprovalBar put the title and action buttons on one row, so a
long file-path title got truncated. Stack title above the buttons so it
wraps to as many lines as needed. Bump the iOS card's input summary cap
from 3 to 8 lines. Both made text-selectable.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Adds a per-project "Allow host build/run" sandbox capability that lets a
containerized agent request to build and run executables on the host
machine, escaping the Linux sandbox — e.g. compiling and running a macOS
binary the container can't.
- New `host_exec` MCP tool on the approval server, advertised only when the
session opts in. Pre-allowed via --allowedTools so the call reaches our
handler directly rather than Claude's permission path: the handler is the
sole gate, so `auto` mode can never auto-approve it.
- Every host command surfaces an explicit approval (risk .hostExec) and runs
on the host via /bin/zsh -lc in the session worktree only after approval.
An explicit "Allow for Session" choice grants the rest of the session;
auto-approve never sets that — only a deliberate user choice does.
- ProjectSandbox.allowHostExec (off by default) with tolerant decoding so
rows persisted before the field default to false instead of dropping the
whole sandbox config.
- Threaded allowHostExec through RunSpec/ResumeSpec/SessionController; Mac
Project Settings toggle; Mac ApprovalBar "Allow for Session" button; iOS
risk styling/biometric gate for .hostExec.
- Tests: host_exec advertised/served only when registered + refused
otherwise; allowHostExec round-trip and legacy-JSON default-to-false.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
iPhone client gains full control-scope parity with the Mac (start chats, manage
to-dos, manage/integrate sessions), the desktop design language (teal palette,
status colors, cards), and Home/Projects/To-Dos surfaces. Fixes archived chats
leaking into Needs You. See nucleic-m4-state.
# Conflicts:
# ios/NucleicRemote/NucleicRemote.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
Adds DEVELOPMENT_TEAM (3XPAYVD6BP) so the app signs to a physical iPhone, and
records the resolved SwiftTerm / swift-argument-parser package pins. Generated by
opening the project in Xcode.
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]>