Skip to content

Fix iOS terminal cold attach first paint - #7172

Merged
austinywang merged 15 commits into
mainfrom
issue-7163-ios-terminal-attach-slow-load
Jul 2, 2026
Merged

austinywang merged 15 commits into
mainfrom
issue-7163-ios-terminal-attach-slow-load

Conversation

@austinywang

@austinywang austinywang commented Jul 2, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #7163

Summary

  • add failing-first iOS coverage for cold terminal attach so a live render-grid delta cannot become the first painted chunk before replay
  • start mounted terminal output streams under a replay barrier, so cold attach holds live bytes/render-grid events until the authoritative replay is applied and acknowledged
  • include the phone's reported viewport dimensions in mobile.terminal.replay, and have the Mac apply those params before capturing the replay frame
  • release only cold-attach replay barriers after replay retries are exhausted, so a failed first replay cannot leave the terminal permanently blank
  • stash the pre-barrier delivered high-water mark as a stale floor so a buffered out-of-order render-grid frame cannot bypass a recovery barrier and establish an outdated baseline; the floor re-bases on the next accepted authoritative delivery so a host-side sequence reset (surface recreate) still recovers

Scope

Validation

  • python3 scripts/swift_file_length_budget.py --budget .github/swift-file-length-budget.tsv
  • git diff --check
  • No local builds/tests per task instruction. CI should validate the two-commit regression shape: commit 1 adds coverage, commit 2 applies the fix.

Summary by CodeRabbit

  • New Features
    • Improved terminal replay recovery for cold-attach and missing render-grid baseline scenarios, including event-driven render-grid delivery decisions.
    • Terminal replay requests now include viewport details and client identity when available.
  • Bug Fixes
    • Refined replay-barrier lifecycle and ack reconciliation to clear the correct per-surface scenario state and retry counters.
    • Improved hybrid delivery behavior so bytes/render-grid updates are withheld or released based on baseline readiness.
  • Tests
    • Added new cold-attach and missing-baseline replay test coverage, including retry budgeting and viewport reporting checks.

@vercel

vercel Bot commented Jul 2, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jul 2, 2026 6:14pm
cmux-staging Building Building Preview, Comment Jul 2, 2026 6:14pm

@coderabbitai

coderabbitai Bot commented Jul 2, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds 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.

Changes

Cold-attach replay and render-grid baseline

Layer / File(s) Summary
Replay state and lifecycle
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift, Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+TerminalOutputDelivery.swift
Adds per-surface cold-attach and baseline replay tracking, initializes and resets the new state, and clears it during replay barrier and terminal output lifecycle changes.
Viewport-aware replay requests
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
Records viewport changes before remote-client gating and includes client ID plus viewport dimensions when building replay requests from tracked viewport state.
Render-grid delivery and barrier resolution
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+TerminalOutputDelivery.swift
Adds replay failure barrier resolution, missing-baseline replay requests, advisory render-grid decisions, baseline waiting branches, and follow-up barrier token reassociation.
Viewport reason tagging
Sources/TerminalController.swift
Adds a reason parameter to viewport reporting and passes distinct replay and viewport reasons into mobile viewport limiting.
Test support updates
Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellRenderGridFrameTestSupport.swift, Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellRenderGridLivenessTestSupport.swift
Adds render-grid and terminal-bytes frame helpers, polling support, and queued replay payload scripting for the new replay paths.
Cold-attach and missing-baseline tests
Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/TerminalColdAttachReplayBarrierTests.swift, Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/TerminalMissingBaselineReplayBudgetTests.swift, Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/TerminalReplayOutputQueueTests.swift, ios/cmuxPackage/Tests/cmuxFeatureTests/cmuxFeatureTests.swift
Adds async tests for cold-attach replay exhaustion, baseline recovery, follow-up replay handling, first-paint ordering, and viewport-bearing replay requests.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related issues

Possibly related PRs

  • manaflow-ai/cmux#6035: Both PRs modify iOS MobileShellComposite terminal output ACK handling, especially the terminalOutputDidProcess path in MobileShellComposite+TerminalOutputDelivery.swift.
  • manaflow-ai/cmux#7098: Both PRs change iOS terminal replay-barrier delivery and ack handling in MobileShellComposite and MobileShellComposite+TerminalOutputDelivery.swift.

Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (2 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Swift Concurrency ❌ Error FAIL: Sources/WorkspaceContentView.swift turns TmuxWorkspacePaneOverlayModel into ObservableObject with @Published state, introducing new Combine app-state tracking in production UI code. Replace it with Observation (@Observable) or plain value state passed through SwiftUI; avoid new Combine-backed app-state models in this diff.
Cmux Full Internationalization ❌ Error v2MobileTerminalReplay adds the new user-facing error "Invalid mobile viewport report" without a localized API or string-catalog entry. Localize the new error with String(localized:defaultValue:) (or equivalent) and add matching catalog entries for every supported locale in the affected surface.
Description check ⚠️ Warning The PR description covers summary, scope, and validation, but it omits required template sections like Testing, Demo Video, Review Trigger, and Checklist. Add the missing template sections, especially Testing, Demo Video, Review Trigger, and Checklist, and fill them with the requested details.
✅ Passed checks (22 passed)
Check name Status Explanation
Title check ✅ Passed It clearly summarizes the main change: iOS terminal cold-attach first paint handling.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed PASS: The touched production types are already @MainActor, and the new replay/viewport state stays on that actor; no new isolation hazards were introduced.
Cmux Swift Blocking Runtime ✅ Passed PASS: production diffs add no blocking/sync primitives; the only new Task.sleep is the test-only pollUntil helper, which the policy explicitly allows.
Cmux Browser Automation Off-Main ✅ Passed Browser waits/callbacks stay in socketWorkerMethods and worker router; only direct focus/open commands remain main-actor, with policy tests covering routing.
Cmux Expensive Synchronous Load ✅ Passed Touched replay/viewport code only mutates in-memory state and params; no new RestorableAgentSessionIndex.load/Data(contentsOf:)/JSONDecoder call was added on a main/interactive path.
Cmux Cache Substitution Correctness ✅ Passed PASS: the replay path still captures from live Ghostty surface; viewport dims are event-driven reports with reset/TTL handling, not an unguarded cache swap.
Cmux No Hacky Sleeps ✅ Passed The diff adds no sleeps/polling/timers; touched non-Swift runtime hunks are API removals, not wait-based race fixes.
Cmux Algorithmic Complexity ✅ Passed The new production paths are mostly O(1) map/set updates; the only scans are per-surface viewport-report recomputations over tiny client sets, not scalable full-collection rescans.
Cmux Swift @Concurrent ✅ Passed No touched Swift code adds/misuses @concurrent or nonisolated async; new async work is actor-isolated (@MainActor/actor) or UI-bound test helpers.
Cmux Swift File And Package Boundaries ✅ Passed Touched production files are existing oversized glue or a focused terminal-output package extension; new large files are test-only.
Cmux Swiftpm Lockfiles ✅ Passed No SwiftPM/Xcode lockfile or dependency files are in scope, and ios/cmuxPackage/.gitignore does not ignore Package.resolved.
Cmux Swift Logging ✅ Passed No print/debugPrint/dump/NSLog or file/stdout logging was added; the new logs use existing MobileDebugLog.anchormux with internal, non-sensitive state.
Cmux User-Facing Error Privacy ✅ Passed The PR only adds a generic invalid mobile viewport report error and internal replay-barrier/debug logic; no vendor names, raw upstream messages, or secrets are exposed.
Cmux Swiftui State Layout ✅ Passed Changed files are model/tests only; the stateful type is already @Observable, and no ObservableObject/@Published/GeometryReader/List/ForEach patterns were added.
Cmux Architecture Rethink ✅ Passed Replay/barrier state stays within MobileShellComposite’s single owner; no new production timing/observer workaround was introduced, and polling is test-only.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed Only terminal replay/render-grid code and tests changed; no user-visible NSWindow/NSPanel/NSWindowController/WindowGroup or cmuxAuxiliaryWindowIdentifiers changes.
Cmux Source Artifacts ✅ Passed Only submodule pointers changed in ghostty and vendor/bonsplit; no logs, caches, temp, build, or other artifact paths were added.
Cmux No Test Or Debug Seam In Production Source ✅ Passed Touched Sources files only add production replay/viewport logic; no new DEBUG/test-only accessors or seam-like members were added.
Cmux No Ambient Global State ✅ Passed New helpers/state live inside MobileShellComposite or existing methods; no new production file-scope func, mutable global, or singleton was added.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-7163-ios-terminal-attach-slow-load

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 3 files

Re-trigger cubic

@greptile-apps

greptile-apps Bot commented Jul 2, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This 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.

  • Cold-attach barrier: requestColdAttachTerminalReplay now marks each barrier token in terminalColdAttachReplayBarrierTokensBySurfaceID, and resolveTerminalReplayFailureBarrier force-clears cold-attach and missing-baseline barriers once retries are exhausted instead of leaving them active (which previously caused a permanently blank terminal).
  • Stale-floor guard: stashTerminalPreBarrierDeliveredEndSeq captures the pre-barrier high-water mark so buffered out-of-order render-grid frames cannot bypass the recovery barrier; the floor is released once a live delivery catches up or the barrier resolves with an empty result.
  • Viewport in replay: mobile.terminal.replay now accepts client_id/viewport_columns/viewport_rows and calls applyMobileViewportReport before capturing the frame, with a sticky-flag preservation fix to avoid downgrading a previously sticky report.

Confidence Score: 5/5

Safe 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

Filename Overview
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+TerminalOutputDelivery.swift Adds deliverAuthoritativeTerminalRenderGrid, stale-floor staleness check, missing-baseline gating, and bypassLiveBaselineBarrier logic. The nil == nil bypass concern from a prior round was fixed upstream; the new logic requires activeReplayBarrierToken != nil.
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+TerminalReplayLifecycle.swift Adds resolveTerminalReplayFailureBarrier (clears cold-attach/missing-baseline barriers on exhaustion instead of preserving them), requestTerminalReplayForMissingRenderGridBaseline, stash/restore/rebase helpers for the pre-barrier floor, and proper per-surface-scenario cleanup in beginTerminalReplayBarrier and clearTerminalReplayBarrierIfCurrent.
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift Adds five new per-surface dictionaries for the barrier/floor/baseline tracking, widens TerminalOutputTransport and related properties from private to internal (justified: code moved to extension files), adds viewport snapshot to requestTerminalReplay, and correctly resets all new state in init/reset/register/unregister paths.
Sources/TerminalController.swift mobile.terminal.replay now validates and applies viewport report fields before capturing the replay frame; applyMobileViewportReport preserves an existing sticky flag rather than overwriting it with false — a correct behavioural fix.
Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/TerminalColdAttachReplayBarrierTests.swift New test file covering cold-attach barrier exhaustion (raw bytes and render-grid transports), follow-up replay exhaustion, partial-frame budget isolation, full-frame baseline establishment, and missing-baseline transient/recovery paths.
Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/TerminalMissingBaselineReplayBudgetTests.swift New test file covering missing-baseline budget exhaustion, replay-count preservation through follow-up barriers, and budget reset on baseline delivery.
Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/TerminalReplayOutputQueueTests.swift New test verifying that a full render-grid event outside an active barrier does not reset the output queue/stream token — regression test for the nil == nil bypass fix.
Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/TerminalStaleFloorByteChannelTests.swift New test verifying that byte chunks whose end-seq falls at or below the stale floor are rejected, and that chunks overlapping the floor are trimmed and delivered correctly.
Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/TerminalStaleFrameRecoveryBarrierTests.swift New test verifying pre-barrier stash, floor gating for render-grid frames, floor restoration on empty barrier release, and rebase on surface-rebuilding resets.
ios/cmuxPackage/Tests/cmuxFeatureTests/cmuxFeatureTests.swift Adds integration tests for viewport dimension reporting in mobile.terminal.replay: verifies that client_id, viewport_columns, and viewport_rows are forwarded on cold attach.

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]
Loading
%%{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]
Loading

Reviews (16): Last reviewed commit: "Align replay tests with the merged cold-..." | Re-trigger Greptile

Comment thread Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift Outdated
Comment thread ios/cmuxPackage/Tests/cmuxFeatureTests/cmuxFeatureTests.swift Outdated
@austinywang
austinywang force-pushed the issue-7163-ios-terminal-attach-slow-load branch 9 times, most recently from 1b9d268 to 8005dd7 Compare July 2, 2026 06:40

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 8850142 and 1b9d268.

📒 Files selected for processing (5)
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+TerminalOutputDelivery.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/TerminalColdAttachReplayBarrierTests.swift
  • Sources/TerminalController.swift
  • ios/cmuxPackage/Tests/cmuxFeatureTests/cmuxFeatureTests.swift

@austinywang
austinywang force-pushed the issue-7163-ios-terminal-attach-slow-load branch from 8005dd7 to 68bd6fa Compare July 2, 2026 06:48

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/TerminalColdAttachReplayBarrierTests.swift (1)

72-80: 🎯 Functional Correctness | 🟠 Major

Negative 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

📥 Commits

Reviewing files that changed from the base of the PR and between 1b9d268 and 8005dd7.

📒 Files selected for processing (5)
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+TerminalOutputDelivery.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/TerminalColdAttachReplayBarrierTests.swift
  • Sources/TerminalController.swift
  • ios/cmuxPackage/Tests/cmuxFeatureTests/cmuxFeatureTests.swift

Comment thread ios/cmuxPackage/Tests/cmuxFeatureTests/cmuxFeatureTests.swift
@austinywang
austinywang force-pushed the issue-7163-ios-terminal-attach-slow-load branch 6 times, most recently from 3059c02 to 07e34ed Compare July 2, 2026 07:40

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+TerminalOutputDelivery.swift (1)

128-130: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Route 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

📥 Commits

Reviewing files that changed from the base of the PR and between 8005dd7 and 3059c02.

📒 Files selected for processing (8)
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+TerminalOutputDelivery.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellRenderGridFrameTestSupport.swift
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellRenderGridLivenessTestSupport.swift
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/TerminalColdAttachReplayBarrierTests.swift
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/TerminalMissingBaselineReplayBudgetTests.swift
  • Sources/TerminalController.swift
  • ios/cmuxPackage/Tests/cmuxFeatureTests/cmuxFeatureTests.swift

Comment thread Sources/TerminalController.swift
@austinywang
austinywang force-pushed the issue-7163-ios-terminal-attach-slow-load branch 3 times, most recently from deee7fe to fc852c5 Compare July 2, 2026 08:08
@austinywang
austinywang force-pushed the issue-7163-ios-terminal-attach-slow-load branch from fc852c5 to a76fc2f Compare July 2, 2026 08:20
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>
@austinywang
austinywang force-pushed the issue-7163-ios-terminal-attach-slow-load branch from c477868 to f8f7bed Compare July 2, 2026 10:48
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>

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

austinywang and others added 3 commits July 2, 2026 04:14
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>
austinywang and others added 6 commits July 2, 2026 07:32
…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>
@austinywang
austinywang merged commit 23a56fe into main Jul 2, 2026
36 checks passed
@austinywang
austinywang deleted the issue-7163-ios-terminal-attach-slow-load branch July 2, 2026 18:18
austinywang added a commit that referenced this pull request Jul 2, 2026
…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>
austinywang added a commit that referenced this pull request Jul 2, 2026
…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>
lawrencecchen added a commit that referenced this pull request Jul 3, 2026
…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.
lawrencecchen added a commit that referenced this pull request Jul 14, 2026
… 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>

This branch was successfully deployed

1 active deployment
Preview – cmux — 2b353243 Deployed Jul 2, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant