Fix iOS terminal cold attach first paint - #7172
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds cold-attach and missing-baseline replay tracking for iOS terminal delivery, threads viewport metadata into replay requests, tags viewport reports with distinct reasons, and expands tests for replay ordering and baseline recovery. ChangesCold-attach replay and render-grid baseline
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related issues
Possibly related PRs
Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (2 errors, 1 warning)
✅ Passed checks (22 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryThis PR fixes a blank-first-paint bug on iOS cold terminal attach by wrapping output streams in a replay barrier until the authoritative replay is applied, and includes viewport dimensions in replay requests so the Mac captures the frame at the correct grid size.
Confidence Score: 5/5Safe to merge. The barrier lifecycle, stale-floor guard, and exhaustion-release path are all logically consistent with each other and validated by the new test suite. All three production Swift files touch closely coordinated state machines (replay barriers, stale-floor seq tracking, missing-baseline budgets) and the logic holds up across the full retry/exhaustion/recovery path. The visibility widening of TerminalOutputTransport and related properties is justified by code moving to a separate extension file. The sticky-flag preservation in TerminalController.swift is a correct narrowing of a pre-existing overwrite bug. The new test coverage is comprehensive: cold-attach exhaustion, follow-up exhaustion, partial-frame budget isolation, full-frame bypass, transient failure recovery, stale-floor trimming, and output-queue regression. No correctness issues were found. No files require special attention. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[terminalOutputStream called\ncold attach] --> B[registerTerminalOutput\nclears all per-surface state]
B --> C[requestColdAttachTerminalReplay]
C --> D{host supports\nterminal.replay.v1?}
D -- yes --> E[beginTerminalReplayBarrier\nmarks terminalColdAttachReplayBarrierTokensBySurfaceID]
D -- no --> F[requestTerminalReplay\nno barrier]
E --> G[mobile.terminal.replay RPC\nwith viewport dimensions]
G -- success --> H{accepted and has seq?}
H -- yes --> I[rebaseTerminalReplayStaleFloor\nmark bytes delivered\nclear barrier on ack]
H -- no/empty --> J[clearTerminalReplayBarrierIfCurrent\nrestorePreBarrierBaseline if needed]
G -- failure --> K{retries < max 2?}
K -- yes --> L[prepareTerminalReplayFailureRetry\nretry same barrier]
L --> G
K -- no --> M[resolveTerminalReplayFailureBarrier]
M --> N{cold-attach or\nmissing-baseline barrier?}
N -- yes --> O[clearTerminalReplayBarrierIfCurrent\nreleases live output]
N -- no --> P[preserveTerminalReplayBarrierIfCurrent\nbarrier stays active]
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A[terminalOutputStream called\ncold attach] --> B[registerTerminalOutput\nclears all per-surface state]
B --> C[requestColdAttachTerminalReplay]
C --> D{host supports\nterminal.replay.v1?}
D -- yes --> E[beginTerminalReplayBarrier\nmarks terminalColdAttachReplayBarrierTokensBySurfaceID]
D -- no --> F[requestTerminalReplay\nno barrier]
E --> G[mobile.terminal.replay RPC\nwith viewport dimensions]
G -- success --> H{accepted and has seq?}
H -- yes --> I[rebaseTerminalReplayStaleFloor\nmark bytes delivered\nclear barrier on ack]
H -- no/empty --> J[clearTerminalReplayBarrierIfCurrent\nrestorePreBarrierBaseline if needed]
G -- failure --> K{retries < max 2?}
K -- yes --> L[prepareTerminalReplayFailureRetry\nretry same barrier]
L --> G
K -- no --> M[resolveTerminalReplayFailureBarrier]
M --> N{cold-attach or\nmissing-baseline barrier?}
N -- yes --> O[clearTerminalReplayBarrierIfCurrent\nreleases live output]
N -- no --> P[preserveTerminalReplayBarrierIfCurrent\nbarrier stays active]
Reviews (16): Last reviewed commit: "Align replay tests with the merged cold-..." | Re-trigger Greptile |
1b9d268 to
8005dd7
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite`+TerminalOutputDelivery.swift:
- Around line 31-33: Route advisory replay handling through the missing-baseline
barrier instead of starting a replay directly from the alternate-screen path. In
MobileShellComposite+TerminalOutputDelivery, update the replay flow around
beginTerminalReplayBarrier/clearTerminalReplayBarrierIfCurrent so live deltas
stay blocked until the authoritative primary-screen replay or baseline resolves,
and ensure replay failures increment and respect the missing-baseline retry cap
even when deliveredTerminalByteEndSeqBySurfaceID already has an alternate-screen
seq.
In
`@Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/TerminalColdAttachReplayBarrierTests.swift`:
- Around line 73-80: The test in TerminalColdAttachReplayBarrierTests is using
negative polling to prove nothing happened, which makes it timing-dependent
instead of causal. Update the assertions around the replay/barrier flow to wait
on an explicit drain/completion signal from the router, transport, or collector
after the render-grid event is delivered, then assert the delivered line is
absent and the replay/request count has not increased. Apply the same
causality-based fix to the other affected assertions in the same test.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 765c1d8f-7e98-460a-80a4-b58906d2d140
📒 Files selected for processing (5)
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+TerminalOutputDelivery.swiftPackages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swiftPackages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/TerminalColdAttachReplayBarrierTests.swiftSources/TerminalController.swiftios/cmuxPackage/Tests/cmuxFeatureTests/cmuxFeatureTests.swift
8005dd7 to
68bd6fa
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/TerminalColdAttachReplayBarrierTests.swift (1)
72-80: 🎯 Functional Correctness | 🟠 MajorNegative assertions still proven only by poll-timeout, not a causal signal.
pollUntil(attempts: 50) { collector.lines.contains(...) }followed by#expect(!delivered, ...)proves "nothing happened" purely by exhausting a fixed attempt/time budget. This is timing-dependent: a slow CI runner could still be mid-delivery at attempt 50 and pass for the wrong reason, and a race could theoretically resolve after the window closes. This is the same pattern already flagged on this file (previously at lines 73-80/162-165); it now also appears at 407-410 (partialAlternateRenderGridWaitingForBaselineSuppressesRawBytes).As per path instructions, tests must "assert on causality, not latency" and must not rely on fixed/bounded polling as proof of absence before an assertion; wait for an explicit drain/settle signal from the router/collector after the event is delivered, then assert the count/state did not change.
Also applies to: 162-165, 407-410
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/TerminalColdAttachReplayBarrierTests.swift` around lines 72 - 80, The negative assertions in TerminalColdAttachReplayBarrierTests are still based on poll timeout rather than a causal settle signal. In the affected test cases around partialDelivered/partialRequestedReplay and partialAlternateRenderGridWaitingForBaselineSuppressesRawBytes, replace the bounded poll-and-negate pattern with waiting for an explicit router/collector drain or settle condition after the triggering event, then assert that the expected replay/count/state did not change. Use the existing pollUntil, router.count(of:), and collector.lines checks only after the event has fully settled so the tests assert causality, not latency.Source: Path instructions
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@ios/cmuxPackage/Tests/cmuxFeatureTests/cmuxFeatureTests.swift`:
- Around line 2552-2556: The first-paint wait in the test loop uses a `where`
clause that only skips the body, so it always burns the full 3-second budget
instead of exiting early. Update the polling logic in `cmuxFeatureTests` to
break out as soon as `collector.lines` becomes non-empty, matching the
early-return behavior used by `waitForRequestCount` and keeping the same 10ms
polling cadence only until the condition is met.
In
`@Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite`+TerminalOutputDelivery.swift:
- Around line 51-55: `markTerminalBytesDelivered` is clearing the replay barrier
token too early, which can cause `terminalOutputDidProcess` to lose the
missing-baseline identity before it re-associates follow-up barriers. Update the
token lifecycle so `terminalRenderGridBaselineReplayBarrierTokensBySurfaceID` is
preserved until the replay barrier ack is processed, and keep it in sync with
the related state in `terminalOutputDidProcess` and any follow-up handling
around the bypassed full render-grid frame.
---
Duplicate comments:
In
`@Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/TerminalColdAttachReplayBarrierTests.swift`:
- Around line 72-80: The negative assertions in
TerminalColdAttachReplayBarrierTests are still based on poll timeout rather than
a causal settle signal. In the affected test cases around
partialDelivered/partialRequestedReplay and
partialAlternateRenderGridWaitingForBaselineSuppressesRawBytes, replace the
bounded poll-and-negate pattern with waiting for an explicit router/collector
drain or settle condition after the triggering event, then assert that the
expected replay/count/state did not change. Use the existing pollUntil,
router.count(of:), and collector.lines checks only after the event has fully
settled so the tests assert causality, not latency.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 06a435df-6c7f-40ea-851f-64b767ea9472
📒 Files selected for processing (5)
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+TerminalOutputDelivery.swiftPackages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swiftPackages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/TerminalColdAttachReplayBarrierTests.swiftSources/TerminalController.swiftios/cmuxPackage/Tests/cmuxFeatureTests/cmuxFeatureTests.swift
3059c02 to
07e34ed
Compare
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (1)
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+TerminalOutputDelivery.swift (1)
128-130: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winRoute advisory baseline replays through the barrier helper.
Line 129 starts a replay without creating a replay barrier, so later live render-grid/byte output can still interleave before the authoritative primary-screen replay is applied. Use the missing-baseline helper so this path shares the same barrier token, retry budget, and failure-release behavior.
Proposed fix
if requestReplay { - requestTerminalReplay(surfaceID: renderGrid.surfaceID) + requestTerminalReplayForMissingRenderGridBaseline(surfaceID: renderGrid.surfaceID) }As per path instructions, terminal attach/replay barrier decisions are correctness-critical state and should not fall back to best-effort replay paths.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite`+TerminalOutputDelivery.swift around lines 128 - 130, The replay path in MobileShellComposite+TerminalOutputDelivery currently calls requestTerminalReplay directly without establishing the replay barrier, which can let later live output interleave before the authoritative baseline is applied. Update this requestReplay branch to use the missing-baseline barrier helper so it shares the same barrier token, retry budget, and failure-release handling as other replay entry points. Keep the fix localized around requestTerminalReplay and the barrier helper used for baseline replay coordination.Source: Path instructions
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/TerminalColdAttachReplayBarrierTests.swift`:
- Around line 378-381: The replay-barrier test is proving absence with timeout
polling instead of using the causal completion signal. In the
`TerminalColdAttachReplayBarrierTests` flow, once `barrierCleared` settles the
recovery barrier, read the follow-up replay count directly and assert the
invariant from that settled state rather than using `pollUntil` to confirm
`!followUpRequested`; keep the same pattern in the nearby raw-bytes path only if
a real drain/flush signal is added later.
In
`@Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/TerminalMissingBaselineReplayBudgetTests.swift`:
- Around line 56-60: The negative assertion in
TerminalMissingBaselineReplayBudgetTests is still relying on a bounded poll
timeout via pollUntil(attempts:) instead of a logical completion condition.
Update the test around extraReplayRequested and the collector.lines check to
wait for an explicit invariant/signaling state from the replay flow or router
rather than assuming 50 attempts is enough, following the same approach used in
TerminalColdAttachReplayBarrierTests. Keep the final assertions tied to a
deterministic completion signal so the test proves the absence of extra replay
requests without timing dependence.
In `@Sources/TerminalController.swift`:
- Line 13650: Fail closed in the mobile.terminal.replay path when viewport
metadata is malformed: update the handler that calls applyMobileViewportReport
so it distinguishes truly absent viewport fields from present-but-unusable ones,
and returns invalid_params whenever any viewport field is supplied but cannot be
parsed or applied. Keep the current backward-compatible no-op only when all
viewport metadata is missing, and ensure the replay sizing logic in
TerminalController uses the validated viewport before capture so a bad first
frame is never emitted.
---
Duplicate comments:
In
`@Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite`+TerminalOutputDelivery.swift:
- Around line 128-130: The replay path in
MobileShellComposite+TerminalOutputDelivery currently calls
requestTerminalReplay directly without establishing the replay barrier, which
can let later live output interleave before the authoritative baseline is
applied. Update this requestReplay branch to use the missing-baseline barrier
helper so it shares the same barrier token, retry budget, and failure-release
handling as other replay entry points. Keep the fix localized around
requestTerminalReplay and the barrier helper used for baseline replay
coordination.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: f2f1cf28-1951-400a-8227-8f40eaaf5cb9
📒 Files selected for processing (8)
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+TerminalOutputDelivery.swiftPackages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swiftPackages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellRenderGridFrameTestSupport.swiftPackages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellRenderGridLivenessTestSupport.swiftPackages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/TerminalColdAttachReplayBarrierTests.swiftPackages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/TerminalMissingBaselineReplayBudgetTests.swiftSources/TerminalController.swiftios/cmuxPackage/Tests/cmuxFeatureTests/cmuxFeatureTests.swift
deee7fe to
fc852c5
Compare
fc852c5 to
a76fc2f
Compare
Releasing a follow-up replay barrier after failed/empty replays left the surface baseline-less with the missing-baseline budget exhausted, so every later render-grid delta was dropped until an incidental full frame arrived — a stalled mirror in the recovery path. The local surface still shows exactly the pre-barrier content, so the stashed stale floor IS the truthful delivered state: restore it as the live baseline on any barrier release that delivered nothing, and drop the floor-arbitration nudge the restore obsoletes (the nil-baseline-with-floor state is now confined to an armed barrier). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
c477868 to
f8f7bed
Compare
The alternate-screen baseline flag was cleared whenever any replay barrier began, so a self-heal barrier that released empty left an intact alt-screen surface flagged baseline-less: the next alternate delta was gated into the replay budget and a hybrid TUI stalled right after the recovery path ran. A barrier only pauses delivery — the surface keeps its content — so the flag now survives barriers and is cleared only by the surface-destroying reset paths, which also drop the stale floor a rebuilt surface can no longer truthfully restore. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
1 issue found across 3 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
The missing-baseline gate trusted the speculatively tracked screen, which the gate itself writes before any alternate frame is delivered: after an empty baseline replay restored the sequence baseline, the next alternate delta saw tracked==alternate with a live sequence and painted onto a surface still showing the primary screen (a delta VT patch cannot switch screens). Gate both screen directions on the delivered alternate-baseline flag instead, stop wiping that flag for gated deltas, and treat a restored baseline as undelivered for the replay budget so an empty-answering host cannot be hammered once per gated delta. The barrier release/preserve pair moves next to the ack machinery it pairs with in the delivery extension. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A compatibility host can answer a replay with a raw byte tail carrying no sequence; re-basing the stale floor on that acceptance defeated the ack path that restores the floor as the baseline for exactly this case, leaving the surface baseline-less after the barrier cleared. Only a sequence-carrying acceptance re-bases the floor. Byte-channel floor tests move to their own file. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
terminalOutputNeedsReplay is reached from the render-pipeline reset, which rebuilds the local surface blank before requesting the replay — yet its barrier kept the intact-surface bookkeeping: the stale floor was stashed and restored (and the alternate baseline survived) after an empty/failed replay, making the store claim content the rebuilt surface no longer shows and drop or gate the live output that should repaint it. Drop the floor and alternate baseline at arm time, matching terminalOutputDidReset; the intact-surface preserve/restore tests move to the follow-up-barrier shape that actually keeps the surface visible. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…al-attach-slow-load # Conflicts: # .github/swift-file-length-budget.tsv # Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+TerminalOutputDelivery.swift # Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift # Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellRenderGridLivenessTestSupport.swift
Integrate the pre-barrier stale floor, baseline restore, missing-baseline budget, cold-attach barrier classification, and alternate-baseline preservation into the replay lifecycle extension #7171 introduced: barriers stash the delivered high-water mark (and hand it back on an empty release via one shared helper), capability-gated cold attaches and pending upgrades mark their barriers as cold-attach, and the delivery gates keep both the full-replacement observation and the floor rejection. Test fixtures collapse to one definition per helper. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Render grids re-emit at unchanged byte sequences, so a buffered full frame at exactly the stashed floor is equally pre-barrier content: it could bypass the recovery barrier, cancel the authoritative replay, and re-establish the outdated baseline. The floor comparison now includes equality, while the live delivered mark keeps strict ordering so steady-state same-sequence re-emits (resize repaints) still deliver. Also fix the optional-String compile error the new CmuxMobileShell package-test CI lane surfaced in the budget tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The CmuxMobileShell package tests never executed in CI until main added the swift-test lane, hiding fixture frames whose row spans exceed the 16-column grid (MobileTerminalRenderGridFrame rejects such spans at init). Default the fixture builders to 80 columns — every test text fits — and pin the one width-sensitive viewport-policy assertion to an explicit 16-column frame. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The liveness tests assert the alternate viewport policy at 16 columns; after the fixture default widened to 80, those alternate frames must carry their grid explicitly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ack calls from tests raced the async event pipeline, clearing barriers before the pre-ack delta was consumed; poll the barrier's dropped-output count as the causal drain signal first. The same-seq staleness test's scaffolding assumed a baseline-less partial paints — the exact stray-fragment behavior the merged gate eliminates — so it now asserts the gate while keeping its purpose: the same-sequence authoritative replay still applies. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rd-autocomplete-autocorrect-is-disa Re-merge to pick up #7172 (iOS terminal cold attach first paint). Only .github/swift-file-length-budget.tsv conflicted (regenerated via scripts/swift_file_length_budget.py --write-budget); MobileShellComposite.swift auto-merged cleanly, preserving the secondary-subscription streamID capture and the SecondaryMacSubscription deinit ownership fix. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…erlap-artifacts Adopt main's cold-attach first-paint rework (#7172) wholesale, dropping this branch's interim in-flight-skip band-aid, and port this branch's viewport machinery over main's changes: the pre-ACK pending token cleanup stays in beginTerminalReplayBarrier, the extension-file updateTerminalViewport gains main's reportTerminalViewport letterbox call, and applyMobileViewportReport keeps generation gating while adopting main's sticky-report preservation. Regenerate the file length budget. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…m gap) OpenCode still showed a bottom letterbox even on latest main (with #7150/#7175/ #7172): the capture set the font PER-AGENT right before each screenshot, and OpenCode doesn't repaint the newly-added bottom rows after that late resize (its pure-black bg exposes the terminal-default gray in the unpainted rows; claude/ codex/pi hide it because their bg matches the default). Set the font ONCE before opening any terminal so each surface opens at its final grid with no resize-while-shown, and give it a longer settle to fully paint.
… screenshots (#6697) * iOS: public App Store lane (com.cmux.app), privacy manifest, fastlane screenshots Prep cmux iOS for a public App Store release alongside the existing dev.cmux.app.beta dogfood channel. - PrivacyInfo.xcprivacy wired into the app target: NSPrivacyTracking=false, UserDefaults (CA92.1) + file-timestamp (DDA9.1) reasons, product-interaction analytics label. No Sentry/IDFA in iOS. - upload-testflight.sh + cloud-testflight.sh: new appstore lane (com.cmux.app, on-device name "cmux", cmux Distribution profile), sharing the existing release entitlements and cmux-ios URL scheme. - web/services/apns/routePolicy.ts: route com.cmux.app to production APNs (+ test). - MobileBuildType: document/test com.cmux.app as a prod bundle id. - ios/fastlane: snapshot config (en-US + ja; iPhone 6.9" + iPad 13") driving the CMUX_UITEST_MOCK_DATA DEBUG state via a SnapshotUITests case. - .github/workflows/ios-screenshots.yml: capture screenshots in CI on a DEBUG build (no signing), resolving the required iPhone/iPad classes at runtime; optional upload to App Store Connect on workflow_dispatch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios-screenshots: read SNAPSHOT_DEVICES from GITHUB_ENV (set -u fix) The resolve step exported SNAPSHOT_DEVICES to $GITHUB_ENV (for later steps) but then echoed $SNAPSHOT_DEVICES, which is unset in the current shell, so set -u aborted the step. Confirm by grepping the env file instead. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios-screenshots: pick devices from available simulators, skip RAM variants Resolver picked the device TYPE 'iPad Pro 13-inch (M5) (16GB)', which has no pre-created simulator, so fastlane errored 'not in list of available simulators'. Resolve against available simulator DEVICES and exclude RAM-variant (GB) names; prefer iPhone NN Pro Max + iPad Pro/Air 13-inch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios-screenshots: drive devices/languages from Fastfile, not Snapfile The Snapfile's devices([...]) overrode the action's devices param, so CI's runtime-resolved simulators were ignored and fastlane looked for the stale 'iPhone 16 Pro Max'. Move devices+languages to the Fastfile (env-overridable, SNAPSHOT_DEVICES/SNAPSHOT_LANGUAGES) as the single source. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios-screenshots: capture via standalone preview screens (real images) CMUX_UITEST_MOCK_DATA alone lands on the add-device screen, so snapshots were empty (0 images). Use the standalone preview hooks that render real UI with no sign-in/pairing: WORKSPACE_LIST_PREVIEW + TERMINAL_PREVIEW (+ fake keyboard), settling on window/foreground instead of identifiers the preview views do not expose. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios-screenshots: populated terminal, clean status bar, dismiss banner Make the captured screenshots presentable for the App Store: - TerminalLayoutPreviewView feeds a sample ANSI agent-session transcript when CMUX_UITEST_TERMINAL_PREVIEW_CONTENT=1, so the terminal shot shows real content instead of a blank surface (blank layout preview unchanged). - SnapshotUITests enables that flag, drops the debug zoom overlay, and swipes away the one-time 'Ready for Apple Intelligence' notification banner. - Fastfile capture uses override_status_bar for a clean 9:41 status bar. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios-screenshots: feed terminal sample content on didResize updateUIView never re-ran with a non-zero size, so the sample transcript was never fed and the terminal shot came out blank. Feed it from the surface's first didResize (grid sized = can render). Also trigger the screenshots workflow on preview-view changes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * iOS: localize app into 12 more languages (machine translation) Add zh-Hans, zh-Hant, ko, de, fr, es, pt-BR, it, ru, nl, tr, pl to all iOS xcstrings (app, agent chat UI, InfoPlist permission strings) and the project knownRegions, alongside the existing en + ja. Translations are a machine-translation first pass (placeholders/format specifiers preserved, brand/tech terms kept) and should get native review before public release. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios: localized App Store listing metadata (14 locales) Store the App Store listing copy (description, keywords, promotional text, URLs; en-US also name/subtitle) for en-US + ja, zh-Hans, zh-Hant, ko, de-DE, fr-FR, es-ES, pt-BR, it, ru, nl-NL, tr, pl. Applied to App Store Connect and kept here so the listing is reproducible via fastlane deliver and the machine-translated copy is reviewable before public release. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios-screenshots: realistic framed shots (agents, keyboard, notifications, frameit) Make App Store screenshots realistic and on-message: - TerminalPreviewTranscripts: Claude Code / Codex / OpenCode / pi sample sessions, selected via CMUX_UITEST_TERMINAL_TRANSCRIPT. - ScreenshotKeyboardView: drawn dark iOS keyboard overlaid in the reserved keyboard region (CMUX_UITEST_SCREENSHOT_KEYBOARD=1); the simulator won't render the system keyboard in CI. Device-aware height (iPhone vs iPad). - ScreenshotNotificationBanner: iOS push banner over the workspace list (CMUX_UITEST_NOTIFICATION_BANNER=1) to show agent notifications. - SnapshotUITests: 7 screens (workspaces, notifications, 4 agents w/ keyboard, full Ghostty terminal). - frameit pipeline: tranquil gradient background, Framefile.json, localized title.strings (prepare_frames.py from titles.*.json), framed in the lane after capture. Workflow installs imagemagick; deliver uploads the framed images. Dynamic island comes from the frameit device frame. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: re-trigger screenshots workflow Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios-screenshots: fix MainActor setupSnapshot call; add localized frame titles setupSnapshot is @mainactor; calling it from nonisolated setUpWithError failed to compile, so the snapshot test never ran (0 screenshots, frameit found nothing). Call it from the @mainactor test method. Also add titles.json (localized screenshot captions, 13 locales) consumed by prepare_frames.py. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios-screenshots: real frames (frameit) + real recorded agent TUIs Make everything real: - Device frame: fastlane frameit photographic frames (font supplied via the Framefile fonts array; ImageMagick has no default font). iPhone 17 Pro Max frames natively; iPad captures are resized 2064x2752 -> 2048x2732 in prepare_frames.py so frameit's real 12.9 iPad Pro frame applies. - Terminal content: replay REAL recorded sessions from the actual claude/codex/ opencode/pi CLIs (tmux capture-pane), base64-embedded in TerminalPreviewTranscripts. record_sessions.sh + embed_sessions.py reproduce them; the screenshot CI replays the committed fixtures (no agent auth needed on the runner). - Removed the drawn fake keyboard (ScreenshotKeyboardView) and the custom PIL compositor (compose_frames.py); terminal shots show the full real terminal, keyboard down. - SnapshotUITests: 6 screens (workspaces, notifications, claude, codex, opencode, pi). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios-screenshots: grid probe + font override + 39-locale titles - Add CMUX_UITEST_TERMINAL_FONT_SIZE override and a 'probe' transcript that prints the live cols x rows + ruler, to measure the exact iOS terminal grid and re-record agent fixtures at matching width (fixes OpenCode/Pi wrap). - titles.json localized to all 39 App Store locales; titles.en trimmed to the 6 current screens. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios-screenshots: auto-fit terminal width, real 76-col agent sessions, dark mode, real notif icon, landscape iPad - Terminal auto-fits font to CMUX_UITEST_TERMINAL_TARGET_COLS=76 (via setLiveFontSize on first didResize), so one 76-col fixture fills the width edge-to-edge on both iPhone (portrait) and iPad (landscape). Measured grids: iPhone 93x88, iPad 205 cols @ font 8 -> fixed-width fixtures couldn't fill both. - Re-recorded all 4 agents (claude/codex/opencode/pi) at 76 cols with richer prompts that fill the screen (fixes narrow + OpenCode/Pi breakage). - Capture in dark mode (Fastfile dark_mode: true). - Notification banner uses the real embedded cmux app icon + tighter iOS styling. - iPad captured in landscape and composited bezel-less (latest real screen, no dated 2020 frame); iPhone keeps the real frameit frame. Removed grid probe. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios-screenshots: generate HTML gallery in CI (screenshots/preview/index.html) frame lane now builds a self-contained preview gallery referencing the framed PNGs in place; CI uploads it inside the screenshots artifact (automatable, not a local /tmp one-off). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios-screenshots: install Pillow in CI for the iPad compositor compose_ipad.py needs PIL; the runner didn't have it (ModuleNotFoundError), failing the frame step after the iPhone frames succeeded. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios-screenshots: iPad compositor via ImageMagick (fast, no Pillow) Rewrite compose_ipad.py to use magick (the C lib frameit already requires) instead of Pillow: faster on the large landscape iPad images and removes the extra Python dependency from CI. Validated locally (rounded screen + soft shadow + caption on the tranquil background). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios-screenshots: gallery at screenshots root with subdir+URL-encoded img refs Browsers block file:// access to parent dirs (../) and don't auto-encode spaces in filenames, so the preview images showed broken. Write index.html at the screenshots root referencing <locale>/<file> (a subdir) with URL-encoded paths. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios-screenshots: premium composition (stitch real frame, large device, bold header) Self-review: frameit shrank the tall device under the title leaving big dead margins, and the header read weak. Replace with compose_shots.py: stitch the screenshot into the real iPhone 17 Pro Max frame and place it LARGE (bleeding off the bottom) under a bold 2-line header; iPad is a large bezel-less landscape screen with a bold header. Removed frameit action + prepare_frames/compose_ipad/ Framefile. Uses the same frameit frame PNG (downloaded on demand) + ImageMagick. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios-screenshots: SF Pro header + nature backgrounds Per review: switch the header to Apple's SF Pro (SFNS, heavy weight; Unicode fallback for CJK/RTL) and replace the gradient with tranquil nature photos (mountains for iPhone portrait, lake+mountains for iPad landscape), darkened with a top gradient so the device + header pop. Compositor picks bg by orientation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios-screenshots: round capture corners before stitching (clean frame top) The square simulator capture poked its corners past the frame's rounded screen opening, leaving square artifacts at the top corners. Round the capture to the screen corner radius before compositing under the device frame. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios screenshots: opening-mask frame stitch + agent logos in headers - compose_shots.py: mask each iPhone screenshot to the device frame's real screen opening (extracted from the frame alpha) so the screen follows the bezel's exact rounded corners (fixes the square-corner artifacts poking past the frame). Render headers in bold SF Pro with the agent logo (Claude/Codex/ OpenCode/pi) smushed in before the localized title. - logos/: agent mark PNGs used in the headers. - Fastfile: accept the machine-store secret names (ASC_KEY_ID / ASC_ISSUER_ID / ASC_PRIVATE_KEY_PATH) in addition to the ASC_API_* names so upload_screenshots works from ~/.secrets without manual remapping. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios screenshots: real terminal titlebar, smaller inline logos, brighter bg, pi title - TerminalLayoutPreviewView: wrap the capture in a NavigationStack with the real terminal nav bar (back chevron + centered workspace name + chat/terminal icons, mirroring WorkspaceDetailView). The terminal no longer bleeds under the status bar / Dynamic Island; there is a proper cmux titlebar below the safe area, so the device-frame stitch reads cleanly at the top. SnapshotUITests passes a believable per-agent workspace name via CMUX_UITEST_TERMINAL_TITLE. - compose_shots.py: shrink the header agent logo to ~cap height and tighten the gap so it sits inline before the title instead of as a large badge. - titles: Pi screen now names pi (mirrors each locale's Claude title, Claude Code -> pi) instead of 'Ship from anywhere', with the pi logo inline. - bg_portrait/bg_landscape: brighter nature photos (sunlit meadow + sky) with a top gradient kept for white-header legibility. - propagate_locales.py + frame lane: capture shoots only en-US+ja (the localized app UI); fan those raws out to all ~39 App Store locales before framing so the whole pipeline is reproducible in CI. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios screenshots: unified Dynamic Island, per-screen bg variation, raw toggle - compose_shots.py: paint a single rounded Dynamic Island over the frame's physical cutouts (the frame PNG draws a pill + a separate camera hole; iOS shows one unified black pill). Fixes the weird double-cutout header. - Per-screen backgrounds: 6 bright, clean nature scenes (sky gradients + ocean, all Pexels License = free for commercial use, no attribution) under frame_assets/backgrounds/{p,l}; each screen (01..06) gets its own, so the listing has varied backdrops instead of one repeated photo. iPhone + iPad of the same screen share a theme. - generate_preview.py: add a 'Show original (unframed) captures' toggle so the raw screenshots can be inspected next to the framed ones. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios screenshots: terminal preview fills header with terminal color, real glass nav chrome The screenshot preview only extended the terminal background (#272822 Monokai) under the horizontal + bottom safe areas, so the status-bar / nav-bar region fell back to black — which is NOT what the running app shows. WorkspaceDetailView fills the whole window (including under the top) with the terminal color and uses mobileTerminalNavigationChrome() (translucent Liquid Glass on iOS 26, material on iOS 18). Mirror both here so the captured header matches the real device instead of showing a black band. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios screenshots: glass-pill nav title in terminal preview Put the preview's nav title on a Liquid Glass pill (mobileGlassNavigationTitle), matching WorkspaceDetailView.glassTitle, so it stays legible over terminal text now that the bar background is cleared on iOS 26. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios screenshots: bigger device, smaller title, horizon-free backgrounds - compose_iphone: enlarge the device (0.885 -> 0.95 width), raise it slightly, and shrink the header (120 -> 104pt, wider box) so titles like 'OpenCode, pi, any agent' fit on one line and the device is the prominent element. - backgrounds 03/04/05: replace the ocean/beach photos (visible horizon band behind the device read as 'weird', esp. OpenCode) with clean horizon-free skies; keeps the bright per-screen variation without a busy seam. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios screenshots: strip OpenCode's explicit bg so it renders uniformly The OpenCode fixture painted its content on its own near-black (#0a0a0a) and let bold headings reset to the terminal default bg, which on the mobile terminal is Monokai #272822 — so heading/emphasis spans showed as olive boxes against the dark content (the 'weird rendering'). The other agents never set an explicit bg, so they render cleanly. Strip OpenCode's background SGR codes so it renders uniformly on the terminal background like the others. Also add the transcripts file to the screenshot workflow triggers so fixture changes re-capture. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios screenshots: OpenCode keeps its dark card, made uniform (no olive boxes) Stripping OpenCode's background lost its distinctive near-black card. Instead, pin the background to OpenCode's own #0a0a0a everywhere (re-assert it after every reset / default-bg) so the card stays dark and uniform with no fallback to the Monokai #272822 default (which caused the olive boxes). The status panel's #1e1e1e is set explicitly and survives. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios screenshots: render OpenCode on a matching #0a0a0a terminal background Definitive fix for OpenCode's olive boxes. The boxes were cells that fell back to the Monokai #272822 default (heading resets AND line-ends the agent didn't pad to the 76-col display width); ANSI surgery on the fixture couldn't cover every case. Instead set libghostty's default background to #0a0a0a for the OpenCode shot via CMUX_UITEST_TERMINAL_BG (each screenshot is its own app launch, so it's scoped to that one shot), and match the preview chrome fill to it. Now every cell — painted, reset, or unpadded — is #0a0a0a, so OpenCode is a clean uniform dark card with no boxes, and the original (unedited) capture fixture is restored. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios screenshots: re-record OpenCode with a full-screen response The previous OpenCode capture was sparse (a short answer + a large empty 'Build' panel), so it left a big empty area at the bottom while the other agents fill the screen. Re-recorded with a richer prompt captured after completion, so OpenCode now shows a dense full-screen response (entry point + readability + #Preview) matching the fill of claude/codex/pi. Renders on the #0a0a0a terminal background added previously, so no boxes and no surgery needed. * ios screenshots: real notification (not a drawn banner) Replace the hand-drawn ScreenshotNotificationBanner with a REAL local notification: in the workspace-list preview, request notification authorization and schedule a genuine UNNotificationRequest, so the system renders the actual banner (real blur/fonts, the app's real icon, and the 'cmux' display name — lowercase). The snapshot UITest taps the springboard 'Allow' prompt and captures the real NotificationShortLookView instead of swiping it away. Deletes the fake banner view + its embedded icon. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios screenshots: auto-derive terminal bg from transcript (no hardcoded color) Replace the hardcoded per-agent #0a0a0a override with auto-derivation: scan each transcript for its dominant explicit background color and render the terminal on it (OpenCode -> its near-black card; claude/codex/pi -> terminal default). The preview sets CMUX_UITEST_TERMINAL_BG from the derived value before the surface is created, so libghostty's default background matches and no reset/unpadded cell falls back to Monokai. Removes the magic constant from the snapshot test. * ios screenshots: capture the real notification banner at fixed timing The foreground notification banner renders correctly (verified: real icon, 'cmux', over the workspace list) but is a transient system overlay (~5s) that isn't reliably queryable, so the UITest was snapshotting after it dismissed. Fire the notification ~0.6s after the auth grant and snapshot at a fixed 2.5s (inside the banner's visible window) instead of waiting on an element. * ios screenshots: real Mac-streamed capture orchestration (WIP) Adds the orchestration for capturing the live Mac-streamed agent terminals on a paired simulator (capture-streamed.py) + the recorded agent sessions as raw .ans files for deterministic Mac-side content. Proven end to end (live OpenCode terminal streamed to the sim, framed, approved). Known blockers being worked: the device's workspace list only resyncs grouped workspaces after a pairing reconnect, and the device mirrors a workspace's own streamed surface (so the workspace must be created with its --command), plus local machine contention drops the tagged Mac app/pairing intermittently. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: Blacksmith streamed-screenshot validation workflow (WIP, manual) Validates the real Mac-streamed screenshot pipeline headless on Blacksmith: build + run the desktop Mac app, pair an iOS sim, capture the live streamed terminal. De-risks running the desktop app + paired sim in CI (cmux streams terminal content rendered on-device, so the Mac's GUI rendering should not matter). Gated on dogfood account secrets (CMUX_DOGFOOD_STACK_EMAIL/PASSWORD) for sim sign-in — not yet configured in CI. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: trigger streamed-validate via push-to-self on the feature branch workflow_dispatch needs the workflow on the default branch (unavailable here), so fire the validation by pushing edits to the workflow file on the feature branch. * ci: streamed-validate stands up dev web backend (Postgres + Next.js) on Blacksmith Run the dev web app locally on the runner (local Postgres via db-local.sh + bun dev, dev Stack project) so the DEBUG sim signs into localhost:3000 + the dev project where the dedicated CI account lives — no prod, no email verification. Dev secret bundle provided via CMUXTERM_DEV_ENV_B64 (dogfood creds swapped to the dedicated CI account). Then build the Mac + iOS apps, pair the sim, capture one streamed terminal. * ci: streamed-validate uses native Postgres + next dev (no Docker) Blacksmith macOS has no Docker, so run native Postgres (cmux:cmux@localhost:13000) + drizzle migrate, and run 'next dev --port 3000' directly instead of bun dev / dev-local.sh (which call the Docker db-local.sh). Detach the server with setsid so it survives into the pairing step. * ci: detach web server with nohup (macOS has no setsid) Native Postgres + drizzle migrate work; the server step failed only because 'setsid' doesn't exist on macOS. Use nohup + disown, and dump webdev.log on failure for faster diagnosis. * ci: run streamed-validate on GitHub-hosted macos-26 (Blacksmith queue dead) The Blacksmith run sat queued 24h with no runner and was auto-cancelled, so use a GitHub-hosted macOS runner (separate pool) to actually execute the validation. macos-26 has Xcode 26 / iOS 26 sims. (Burns paid macOS minutes.) * ios-streamed-validate: run on Blacksmith macos-26, not paid GitHub-hosted Use the same Blacksmith iOS lane as test-ios.yml (vars.MACOS_RUNNER_IOS || blacksmith-6vcpu-macos-26). The desktop Mac app link failure (undefined libc symbols) was a GitHub-hosted macos-26 toolchain quirk; Blacksmith macos-26 is the proven fleet that builds cmux releases, and the prebuilt GhosttyKit is SHA-pinned for the current ghostty submodule so no from-source build runs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios-streamed-validate: pin Blacksmith macos-26 + Xcode 26 (not Depot/16.4) The vars.MACOS_RUNNER_IOS override routes the iOS lane to a Depot macOS image whose default Xcode is 16.4; cmux's iOS targets need Xcode 26's Swift toolchain (Swift 6 actor-isolation + interpolation errors otherwise) and the iPhone 17 sim only exists on iOS 26. Pin blacksmith-6vcpu-macos-26 directly and select the newest Xcode 26 explicitly instead of the default symlink. The desktop Mac app already builds clean on this lane; this unblocks the iOS sim build + the in-CI pairing/capture. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios-streamed-validate: mirror the Blacksmith fleet build recipe (fix Xcode 26 link) The Mac build failed under Xcode 26 with undefined libc symbols (_abort/_free/_malloc_size/...): the AArch64 GlobalISel codegen path miscompiles under -O/wholemodule. Adopt reload-build.yml's exact fleet recipe: - Select Xcode via scripts/select-ci-xcode.sh (ranks by macOS SDK, picks 26.x, aligns xcode-select) instead of the default Xcode.app symlink. - Provision GhosttyKit via download-prebuilt-ghosttykit.sh (SHA-pinned), the path ci.yml/reload-build use. - Build the Mac app with --swift-frontend-workaround (disables GlobalISel). - Add CMUX_SKIP_ZIG_BUILD=1 + SWIFT_BACKTRACE env. The iOS sim build forces -O/wholemodule too, so it needs the same workaround. ios/scripts/reload.sh had no escape hatch, so add --swift-frontend-workaround (also via CMUX_SWIFT_FRONTEND_WORKAROUND=1), mirroring scripts/reload.sh, and apply it to both the simulator and device xcodebuild invocations. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios-streamed-validate: make the streamed capture actually run (idb, sign-in, pairing) The capture step passed only because of '|| true'; it produced no screenshot. Three real fixes: - idb: install fb-idb into a pinned-Python venv (/tmp/idbvenv) and put it on GITHUB_PATH. The runner's default python3 (3.14) is too new for fb-idb, so the 'idb' CLI was missing (FileNotFoundError) and all device navigation failed. - sign-in: inject the DEDICATED CI screenshot account (secrets CMUX_DOGFOOD_STACK_EMAIL/PASSWORD, @cmux.com, not a personal account) as env; dev-secrets reads CMUX_DOGFOOD_STACK_* from the environment first, so mobile-dev-launch signs the device in instead of erroring 'no credentials'. - pairing order: launch the tagged Mac app and wait for its debug socket BEFORE mobile-dev-launch --ensure-mac, which mints the pairing ticket from the running Mac app; otherwise the device is signed-in-only with no workspaces to stream. Also drop '|| true' on capture and assert a *.png exists, so the step fails loudly if no real streamed shot is produced; use --detach so the sim launch returns instead of blocking on --console-pty. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios-streamed-validate: capture device-state diagnostics on capture failure Sign-in + pairing + Mac workspace creation now all succeed, but the device shows 'workspace not visible' (likely the new workspace was created after the device paired and the dev workspace list didn't resync). Before guessing the fix across CI iterations, capture hard evidence: a device screenshot + accessibility tree right after pairing and again post-capture, plus the Mac workspace list, into _diag/ artifacts. Keep the top-level *.png assert (diagnostics live in _diag/). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios-streamed-validate: install idb_companion (facebook/fb tap) + /usr/local/bin symlink The device pairs and shows the workspace list fine; the capture failed only because idb ui describe-all threw FileNotFoundError: '/usr/local/bin/idb_companion'. Two causes: (1) 'brew install idb-companion' => 'No available formula' (it lives in the facebook/fb tap), so the companion was never installed; (2) the python idb client spawns it from the hardcoded /usr/local/bin path while arm64 brew installs to /opt/homebrew/bin. Install from the tap and symlink to /usr/local/bin, and assert idb_companion in the capture prereqs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios-streamed-validate: real claude agent content in the streamed shot The streamed terminals run real agents on the runner, but the agents weren't installed (zsh: command not found). Install claude/codex/opencode/pi and symlink them into /usr/local/bin (cmux workspace shells launch from the GUI Mac app and lack ~/.bun/bin + ~/.local/bin on PATH). Propagate agent auth into the GUI session with launchctl setenv BEFORE (open uses launchd env, not the step env), so workspace terminals inherit CLAUDE_CODE_OAUTH_TOKEN (claude, native) and DEEPSEEK_API_KEY (the others, via DeepSeek's OpenAI-compatible API). Validate the real-agent path with claude first (strongest auth, no provider config): launch with --permission-mode plan (clean read-only UI) and pre-seed ~/.claude.json so fresh-$HOME onboarding + folder-trust don't block. pi switched to --provider deepseek. Capture --agents claude; opencode/codex provider config to follow once claude proves the path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios-streamed-validate: trust claude sandbox by realpath (/private/tmp) The claude shot proved auth propagation works (no login screen) but stalled on the folder-trust prompt: the pre-seed keyed /tmp/cmux-stream-claude, while macOS resolves /tmp -> /private/tmp and claude keys its trust map by the realpath it sees (/private/tmp/cmux-stream-claude, visible in the screenshot). Seed both spellings so the trust prompt is pre-accepted and claude answers the prompt. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios-streamed-validate: test the DeepSeek-backed agent path (pi) claude renders a real session now (trust cleared) but the CLAUDE_CODE_OAUTH_TOKEN account is over its monthly spend limit, so it shows the rate-limit screen. Verify the DeepSeek-backed path independently by capturing pi (--provider deepseek, reads DEEPSEEK_API_KEY propagated via launchctl) before fanning out to opencode/codex. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios-streamed-validate: replay recorded agent sessions (claude) instead of live Live agents in CI hit auth/billing limits (the CLAUDE_CODE_OAUTH_TOKEN account is over its monthly spend limit) and are nondeterministic. Instead, record a real session locally with a funded account, commit the transcript, seed it into the agent's on-disk session store at the matching project cwd (/private/tmp/cmux-stream-claude), and resume it read-only in CI. claude: recorded via 'claude -p' locally (genuine answer w/ #Preview code block, no account/email/token fields in the jsonl), committed as ios/fastlane/streamed-sessions/claude/<sessionId>.jsonl, copied into ~/.claude/projects/-private-tmp-cmux-stream-claude/ in CI, launched with 'claude --continue'. Resuming renders the transcript without an API call, so the over-budget token still works. opencode/codex/pi transcripts to follow once this proves out. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios-streamed-validate: resume claude by session id, seed both cwd encodings claude --continue returned 'No conversation found' even though the jsonl seeded correctly: --continue relies on the ~/.claude.json history index (not seeded), and the CI cwd may encode as -tmp- (logical) vs -private-tmp- (resolved symlink). Switch to 'claude --resume <session-id>' (opens the jsonl by id) and seed the transcript into both project-dir encodings. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios-streamed-validate: real recorded sessions for all 4 agents (claude/codex/opencode/pi) claude --resume <id> proved the record->resume approach renders a real session in the streamed capture. Extend to all four: - codex: seed rollout jsonl into ~/.codex/sessions/<date>/, resume by id. - pi: seed cwd-keyed jsonl (both /tmp spellings), resume via --session <uuid>. - opencode: sessions live in a sqlite DB, so import the exported JSON, resume via --session <id>. All transcripts recorded locally with funded accounts (genuine answers with #Preview code blocks), scanned clean of secrets. Capture all four in one run. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios-streamed-validate: reliable back-navigation between agent captures Only claude captured (1/4): the nav-bar back chevron has no accessibility label, so navigate_back (which searched for '<'/'Back'/'chevron') failed and the device stayed on claude's terminal, so codex/opencode/pi workspace rows were never found. Tap the back chevron by its known nav-bar position with an iOS edge-swipe pop fallback, and verify we returned to the list (>=2 'Terminal' rows) before moving to the next agent. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ios-streamed-validate: give codex a DeepSeek provider so resume renders (4/4) 3/4 agents rendered real resumed sessions (claude, opencode, pi). codex showed the 'Sign in with ChatGPT' onboarding because it has no CI login, which blocks codex resume. Write ~/.codex/config.toml with a DeepSeek (OpenAI-compatible) provider keyed on DEEPSEEK_API_KEY so codex is authenticated and resumes the seeded rollout. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * capture-streamed: screencapture the Mac per agent (theme diagnostic) To debug the white bands on the phone (likely the Mac streaming a light/default theme), grab the CI Mac's cmux window right after each workspace is created (foreground), into _diag/mac-<agent>.png, so we can view Mac vs phone side by side for the same agent. * streamed shots: force dark theme on CI Mac + codex press-enter nudge White bands on the phone: the fresh CI runner boots in Light Mode, so cmux resolves a LIGHT terminal theme and streams a white background; unpainted cells render white on the phone. Force cmux to dark (appearanceMode=dark for the cistream bundle + system AppleInterfaceStyle=Dark) before launch — temporary CI screenshot setup. codex: send one Enter after 'codex resume' to advance past the 'Press enter to continue' welcome to the resumed session. * streamed shots: codex uses fake OpenAI auth (real codex, no DeepSeek) Per feedback: fake auth values are enough to pass codex's login gate, and resume makes no API call, so seed a dummy ~/.codex/auth.json instead of a DeepSeek provider. Codex authenticates as itself and renders the real recorded gpt-5 session with no DeepSeek label. * streamed shots: drop DeepSeek, fake per-provider auth so each agent shows its real model All three (codex/opencode/pi) fell back to DeepSeek because DEEPSEEK_API_KEY was the only provider in the GUI env. Remove it entirely and seed a fake credential for each agent's OWN recorded provider so it restores its real model with no DeepSeek label: - codex: fake ~/.codex/auth.json (openai apikey) -> gpt-5 - pi: fake ~/.pi/agent/auth.json (openai-codex oauth) -> gpt-5.5 - opencode: fake ~/.local/share/opencode/auth.json (zai) -> glm-5.2 Resume makes no API call, so the fake keys are never validated. * capture-streamed: set terminal font once up front (fix OpenCode bottom gap) OpenCode still showed a bottom letterbox even on latest main (with #7150/#7175/ #7172): the capture set the font PER-AGENT right before each screenshot, and OpenCode doesn't repaint the newly-added bottom rows after that late resize (its pure-black bg exposes the terminal-default gray in the unpainted rows; claude/ codex/pi hide it because their bg matches the default). Set the font ONCE before opening any terminal so each surface opens at its final grid with no resize-while-shown, and give it a longer settle to fully paint. * capture-streamed: per-agent set_font + long settle (consistent size AND fills) Setting the font once up front left each agent at whatever size its surface opened with, so OpenCode rendered larger (scaled-up narrow grid) than the others. Restore the per-agent focused set_font (all four at the same size) but keep the long 6s settle so OpenCode fully repaints the resized grid and fills the height (a short settle was the original cause of its bottom gap, not the resize itself). * capture-streamed: log each agent's Mac PTY grid (diagnose OpenCode scaling) OpenCode still renders ~20% larger than the others at the same set_font, which points to its terminal grid being narrower (scaled up to fill width) rather than a font-value difference. Log the read-screen cols x rows per agent so we can see the actual grids and target the real cause. * capture-streamed: smaller font for OpenCode so it matches the others' size OpenCode's TUI negotiates a narrower grid that the phone scales up, so its glyphs look ~1.3x larger than claude/codex/pi at the same font. Add a per-agent font override and set OpenCode to 11 (vs 15) to compensate; grid diagnostic stays so we can dial it in. * streamed shots: opencode layout=stretch (full width), revert font hack Root cause of OpenCode looking larger/narrower: its TUI defaults to layout=auto, which caps content to a narrow readable column, so on the phone it's narrower than claude/codex/pi (scaled up or big margins). The per-agent font hack made it worse (clipped + margins). Revert the font override and seed ~/.config/opencode/opencode.json with layout=stretch so OpenCode uses the full terminal width and matches the others. Grid diagnostic stays to verify. * iOS streamed capture: claude theme dark-ansi so it renders on cmux Monokai bg The 'dark' theme paints claude's own #1e1e1e background, overriding cmux's Monokai (#272822) terminal default; codex/pi never paint a full-screen bg so they already show Monokai. dark-ansi uses the terminal's ANSI palette + background, so claude matches. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * iOS: add dispatch-only App Store upload workflow + --marketing-version CI plumbing for public com.cmux.app builds, mirroring the beta lane's signing/ ASC setup but with the prod 'cmux Distribution' profile and no schedule (App Store builds are cut deliberately, one per submission). Gated to main so only reviewed code ships. Needs the new IOS_PROD_PROVISIONING_PROFILE_BASE64 secret (set from ASC profile VF3CDPFLX9, app id 7WLXT3NR37.com.cmux.app, aps=production). upload-testflight.sh gains --marketing-version <X.Y[.Z]> to stamp an explicit version train (mutually exclusive with the beta --auto-version bump) so an appstore build lands in the exact ASC store-version train (1.0) and is attachable, instead of auto-bumping to 1.0.4. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * screenshots: drop pi from OpenCode header (pi has its own shot) 05-Opencode said 'OpenCode, pi, any agent' but pi is the very next screenshot (06-Pi). Reworded to 'OpenCode and any agent' (and each locale's equivalent) so the two shots don't overlap. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Fix App Store lane alias guard * Address screenshot preview policy findings * Localize screenshot notification fixture * Limit app-declared iOS locales * Guard screenshot workflow secrets * Address App Store screenshot PR feedback * Fix streamed simulator lookup * Handle localized notification permission in screenshots * Fix screenshot preview task modifier * Address remaining screenshot review feedback * Fix terminal preview keyboard height parse * Restrict streamed validation secrets to main * Preserve raw App Store screenshot captures * Parallelize App Store screenshot framing --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixes #7163
Summary
mobile.terminal.replay, and have the Mac apply those params before capturing the replay frameScope
origin/issue-7159-ios-load-garble) after it became available. That branch fixes stale same-sequence render-grid replay replacement withterminalFullReplacementSeqBySurfaceID; this PR keeps to the cold-attach first-paint path withterminalColdAttachReplayBarrierTokensBySurfaceIDplus viewport-sized replay capture. The fixes touch nearby code but keep distinct state and behavior.Validation
python3 scripts/swift_file_length_budget.py --budget .github/swift-file-length-budget.tsvgit diff --checkSummary by CodeRabbit