Fix iOS terminal typing render-grid drift - #7175
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:
📝 Walkthrough📝 Walkthrough🚥 Pre-merge checks | ❌ 20❌ Failed checks (20 inconclusive)
✨ 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 iOS terminal drift caused by fast input ACKs outrunning render-grid delivery. It replaces the previous approach of forcing a full replay on consecutive typing ACKs with a pending-target tracking scheme that drops frames below the latest input ACK, defers replay until the target is met, and bounds retry attempts.
Confidence Score: 5/5Safe to merge. The pending-input drop state machine is internally consistent, all three state variables are cleared in matched pairs across every code path, and retry budgets correctly bound the repair-replay loop. The two main behavioral changes are well-scoped and covered by deterministic regression tests. State machine invariants are cleared together in every exit path. The retry exhaustion path correctly falls back from barrier retries to non-barrier retries before giving up. The full-snapshot reset opens synchronized output before any clearing, preventing the blank-frame flash at the architecture level. No files require special attention. The most complex logic is in MobileShellComposite.swift around the replay response handler, but the retry and drop paths are well-commented and tested. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Input ACK received] --> B{previousPendingSeq set?}
B -- No --> C[Reset retry counter, store targetSeq]
B -- Yes --> D{targetSeq > previousPendingSeq?}
D -- No --> E{droppedSurfaceIDs contains surface?}
E -- Yes --> F[requestTerminalReplayAfterDroppedRenderGrid]
E -- No --> G[return no-op]
D -- Yes --> H[Update pendingSeq, refresh subscription]
I[Render-grid frame arrives] --> J[shouldDropRenderGridBehindPendingInput]
J --> K{pendingSeq AND stateSeq < pendingSeq?}
K -- Yes --> L[Insert into droppedSurfaceIDs, return DROP]
K -- No --> M{droppedSurfaceIDs AND non-full AND non-patch?}
M -- Yes --> N{source == event?}
N -- Yes --> O[requestReplay, return DROP]
N -- No --> P[return DROP, replay handler retries]
M -- No --> Q[return ACCEPT, deliver frame]
Q --> R[markTerminalBytesDelivered]
R --> S{endSeq >= pendingSeq?}
S -- Yes --> T[Clear pendingSeq, droppedSurfaceIDs, retryCounter]
P --> U{prepareTerminalReplayFailureRetry < max?}
U -- Yes --> V[counter+1, request barrier retry]
U -- No --> W{prepareNonBarrierRetry < max?}
W -- Yes --> X[counter+1, request non-barrier retry]
W -- No --> Y[clearBarrier: pending_input_exhausted]
%%{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[Input ACK received] --> B{previousPendingSeq set?}
B -- No --> C[Reset retry counter, store targetSeq]
B -- Yes --> D{targetSeq > previousPendingSeq?}
D -- No --> E{droppedSurfaceIDs contains surface?}
E -- Yes --> F[requestTerminalReplayAfterDroppedRenderGrid]
E -- No --> G[return no-op]
D -- Yes --> H[Update pendingSeq, refresh subscription]
I[Render-grid frame arrives] --> J[shouldDropRenderGridBehindPendingInput]
J --> K{pendingSeq AND stateSeq < pendingSeq?}
K -- Yes --> L[Insert into droppedSurfaceIDs, return DROP]
K -- No --> M{droppedSurfaceIDs AND non-full AND non-patch?}
M -- Yes --> N{source == event?}
N -- Yes --> O[requestReplay, return DROP]
N -- No --> P[return DROP, replay handler retries]
M -- No --> Q[return ACCEPT, deliver frame]
Q --> R[markTerminalBytesDelivered]
R --> S{endSeq >= pendingSeq?}
S -- Yes --> T[Clear pendingSeq, droppedSurfaceIDs, retryCounter]
P --> U{prepareTerminalReplayFailureRetry < max?}
U -- Yes --> V[counter+1, request barrier retry]
U -- No --> W{prepareNonBarrierRetry < max?}
W -- Yes --> X[counter+1, request non-barrier retry]
W -- No --> Y[clearBarrier: pending_input_exhausted]
Reviews (20): Last reviewed commit: "Reset ?1048 save-cursor mode state in th..." | Re-trigger Greptile |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift (1)
6628-6638: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winCoalesce refreshes instead of calling subscribe on every ACK.
terminalSubscriptionRefreshTaskonly suppresses overlapping refreshes;requestTerminalEventSubscriptionstill performs a fullmobile.events.subscribeRPC, and this branch can queue that work on every input ACK while the stream lags. Guard it sotargetSeqonly advances the refresh when it actually moves past the previous pending value, which avoids reissuing subscribe requests during a typing burst.🤖 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.swift` around lines 6628 - 6638, The input-ACK path in MobileShellComposite’s input_seq_wait branch is triggering refreshTerminalEventSubscription too often, causing repeated mobile.events.subscribe work while the stream lags. Update the logic around pendingTerminalByteEndSeqBySurfaceID and refreshTerminalEventSubscription so the refresh is only requested when targetSeq actually increases beyond the previous pending value, and skip the call when the ACK does not advance the pending sequence. Keep the change localized to the canRenderGridAdvancePendingSeq / terminalEventListenerTask check so the coalescing behavior prevents duplicate subscription refreshes during typing bursts.
🤖 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.
Outside diff comments:
In
`@Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift`:
- Around line 6628-6638: The input-ACK path in MobileShellComposite’s
input_seq_wait branch is triggering refreshTerminalEventSubscription too often,
causing repeated mobile.events.subscribe work while the stream lags. Update the
logic around pendingTerminalByteEndSeqBySurfaceID and
refreshTerminalEventSubscription so the refresh is only requested when targetSeq
actually increases beyond the previous pending value, and skip the call when the
ACK does not advance the pending sequence. Keep the change localized to the
canRenderGridAdvancePendingSeq / terminalEventListenerTask check so the
coalescing behavior prevents duplicate subscription refreshes during typing
bursts.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: d5b332d0-f7c4-4f59-bd7d-5ee2ba9af2e6
📒 Files selected for processing (5)
Packages/Shared/CMUXMobileCore/Sources/CMUXMobileCore/MobileTerminalRenderGridReplay.swiftPackages/Shared/CMUXMobileCore/Tests/CMUXMobileCoreTests/MobileTerminalRenderGridTests.swiftPackages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swiftPackages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellRenderGridLivenessTestSupport.swiftPackages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellRenderGridLivenessTests.swift
There was a problem hiding this comment.
2 issues found across 5 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| // update so the client never shows a partially-restored screen. | ||
| bytes.append(Data("\u{1B}c".utf8)) | ||
| bytes.append(Data("\u{1B}[?2026h".utf8)) | ||
| let screenStateReset = "\u{1B}[1\"q\u{1B}[0\"q\u{1B}[999<u\u{1B}[0;1=u\u{0F}\u{1B}(B\u{1B})B\u{1B}*B\u{1B}+B" |
There was a problem hiding this comment.
Kitty keyboard resets in screenStateReset are silently dropped — private marker must lead the parameter string.
\u{1B}[999<u and \u{1B}[0;1=u put the private marker (<, =) after the digits. In Ghostty's VT parser, bytes 0x3C–0x3F are only collected from csi_entry; once the parser is in csi_param (after any digit), they transition to csi_ignore and the whole sequence is discarded:
ghostty/src/terminal/parse_table.zig— csi_entry:range(&result, 0x3C, 0x3F, source, .csi_param, .collect); csi_param:range(&result, 0x3C, 0x3F, source, .csi_ignore, .none)ghostty/src/terminal/stream.zig:1827dispatches kitty push/pop/set offinput.intermediates[0]being>/</=, which only happens with the leading form.
So the emitted bytes are no-ops: the kitty key-flag stack is not reset (RIS used to reset it), and the comment above ("key/input flags") overclaims. Fix is to lead with the marker:
"\u{1B}[<999u\u{1B}[=0;1u"(CSI < 999 u pops the whole kitty stack; CSI = 0;1 u then forces current flags to 0 — matching what ESC c used to guarantee.)
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
Packages/Shared/CMUXMobileCore/Sources/CMUXMobileCore/MobileTerminalRenderGridReplay.swift (1)
87-105: 🩺 Stability & Availability | 🔵 TrivialSynchronized-update prologue design is sound.
Wrapping the entire reset/clear sequence inside
?2026h ... ?2026l(instead of leading withESC c) correctly defers presentation until the full snapshot lands, matching the PR's stated goal. The manual reset subset (DECSASD, key-modifier reset, margins, DECLRMM, DECST8C, kitty keyboard-flag stack pop, DECSCA, G0–G3 charset) plus the documented rationale in the comment gives good confidence this is a deliberate, tested replacement for RIS.This kind of VT-protocol-level change is inherently hard to fully validate with byte-exact unit tests alone (they confirm the bytes emitted, not the actual rendered result). Given the PR's test plan notes no local build/reload was run, consider a manual on-device pass (mirrored terminal reconnect/catch-up scenario) before merge to catch any visual artifacts the string-comparison tests can't detect.
🤖 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/Shared/CMUXMobileCore/Sources/CMUXMobileCore/MobileTerminalRenderGridReplay.swift` around lines 87 - 105, The synchronized-update prologue in fullSnapshotBytes() looks correct, but it still needs real-world validation because byte-for-byte tests won’t catch rendering artifacts. Before merging, verify the new ?2026h/?2026l reset flow on-device in the mirrored reconnect/catch-up path and confirm the full snapshot renders cleanly with no empty-frame flash or visual corruption; if you find issues, adjust the reset sequence in fullSnapshotBytes() accordingly.
🤖 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/Shared/CMUXMobileCore/Tests/CMUXMobileCoreTests/MobileTerminalRenderGridSnapshotReplayTests.swift`:
- Around line 79-102: The fallback path in consumeEscape only skips simple ESC
intermediates, so OSC sequences introduced by ESC ] are being treated as
printable text and leaking into the grid. Update consumeEscape in
MobileTerminalRenderGridSnapshotReplayTests to explicitly recognize OSC and
consume everything through the OSC terminator (ESC \) before returning, while
keeping the existing ESC c and CSI handling intact. Use the existing
consumeEscape, consumeCSI, and isESCIntermediateByte helpers as the main entry
points when locating the fix.
---
Outside diff comments:
In
`@Packages/Shared/CMUXMobileCore/Sources/CMUXMobileCore/MobileTerminalRenderGridReplay.swift`:
- Around line 87-105: The synchronized-update prologue in fullSnapshotBytes()
looks correct, but it still needs real-world validation because byte-for-byte
tests won’t catch rendering artifacts. Before merging, verify the new
?2026h/?2026l reset flow on-device in the mirrored reconnect/catch-up path and
confirm the full snapshot renders cleanly with no empty-frame flash or visual
corruption; if you find issues, adjust the reset sequence in fullSnapshotBytes()
accordingly.
🪄 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: f1c937af-2d4a-46a3-9786-ba9ac99f3f87
⛔ Files ignored due to path filters (1)
.github/swift-file-length-budget.tsvis excluded by!**/*.tsv
📒 Files selected for processing (7)
Packages/Shared/CMUXMobileCore/Sources/CMUXMobileCore/MobileTerminalRenderGridReplay.swiftPackages/Shared/CMUXMobileCore/Tests/CMUXMobileCoreTests/MobileTerminalRenderGridSnapshotReplayTests.swiftPackages/Shared/CMUXMobileCore/Tests/CMUXMobileCoreTests/MobileTerminalRenderGridTests.swiftPackages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+TerminalOutputDelivery.swiftPackages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swiftPackages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellRenderGridInputCatchUpTests.swiftPackages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellRenderGridLivenessTestSupport.swift
There was a problem hiding this comment.
1 issue found across 9 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+TerminalOutputDelivery.swift">
<violation number="1" location="Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+TerminalOutputDelivery.swift:202">
P2: In `retryTerminalReplayAfterAckReset`, `pendingTerminalInputDroppedRenderGridSurfaceIDs` is cleared unconditionally before `prepareTerminalReplayFailureRetry` confirms a retry replay can proceed. If retries are exhausted and the function returns after preserving the barrier, the dropped-frame marker is lost even though no replay ever landed, breaking the PR invariant to keep the marker until a corresponding replay arrives. Consider moving the removal after the retry succeeds, or restoring the marker when the retry preparation fails.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| terminalOutputStreamTokensBySurfaceID[surfaceID] = UUID() | ||
| deliveredTerminalByteEndSeqBySurfaceID.removeValue(forKey: surfaceID) | ||
| pendingTerminalByteEndSeqBySurfaceID.removeValue(forKey: surfaceID) | ||
| pendingTerminalInputDroppedRenderGridSurfaceIDs.remove(surfaceID) |
There was a problem hiding this comment.
P2: In retryTerminalReplayAfterAckReset, pendingTerminalInputDroppedRenderGridSurfaceIDs is cleared unconditionally before prepareTerminalReplayFailureRetry confirms a retry replay can proceed. If retries are exhausted and the function returns after preserving the barrier, the dropped-frame marker is lost even though no replay ever landed, breaking the PR invariant to keep the marker until a corresponding replay arrives. Consider moving the removal after the retry succeeds, or restoring the marker when the retry preparation fails.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+TerminalOutputDelivery.swift, line 202:
<comment>In `retryTerminalReplayAfterAckReset`, `pendingTerminalInputDroppedRenderGridSurfaceIDs` is cleared unconditionally before `prepareTerminalReplayFailureRetry` confirms a retry replay can proceed. If retries are exhausted and the function returns after preserving the barrier, the dropped-frame marker is lost even though no replay ever landed, breaking the PR invariant to keep the marker until a corresponding replay arrives. Consider moving the removal after the retry succeeds, or restoring the marker when the retry preparation fails.</comment>
<file context>
@@ -199,6 +199,7 @@ extension MobileShellComposite {
terminalOutputStreamTokensBySurfaceID[surfaceID] = UUID()
deliveredTerminalByteEndSeqBySurfaceID.removeValue(forKey: surfaceID)
pendingTerminalByteEndSeqBySurfaceID.removeValue(forKey: surfaceID)
+ pendingTerminalInputDroppedRenderGridSurfaceIDs.remove(surfaceID)
terminalReplayBarrierAckStreamTokensBySurfaceID.removeValue(forKey: surfaceID)
terminalReplayBarrierAckCoveredDroppedOutputCountsBySurfaceID.removeValue(forKey: surfaceID)
</file context>
There was a problem hiding this comment.
Intentional: retryTerminalReplayAfterAckReset abandons the whole input catch-up epoch — deliveredTerminalByteEndSeq and pendingTerminalByteEndSeq are removed in the same block, so a kept marker would reference a dead epoch under a rotated stream token. After an exhausted retry the preserved replay barrier is the gate that keeps stale output from delivering (tested by terminalReplayBarrierStaysActiveAfterRetryExhaustionWithoutDroppedOutput); the marker adds nothing behind it, and any later successful replay clears all of this state via markTerminalBytesDelivered.
— Claude Code
There was a problem hiding this comment.
The marker point is fair to call out, but it’s wrong for this path: retryTerminalReplayAfterAckReset drops the whole catch-up epoch, so keeping pendingTerminalInputDroppedRenderGridSurfaceIDs would point at dead state after the stream token rotates. The barrier is the thing that stays active after retry exhaustion and prevents stale output from delivering, so the parent comment doesn’t apply here.
There was a problem hiding this comment.
1 issue found across 4 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+TerminalOutputDelivery.swift">
<violation number="1" location="Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+TerminalOutputDelivery.swift:202">
P2: In `retryTerminalReplayAfterAckReset`, `pendingTerminalInputDroppedRenderGridSurfaceIDs` is cleared unconditionally before `prepareTerminalReplayFailureRetry` confirms a retry replay can proceed. If retries are exhausted and the function returns after preserving the barrier, the dropped-frame marker is lost even though no replay ever landed, breaking the PR invariant to keep the marker until a corresponding replay arrives. Consider moving the removal after the retry succeeds, or restoring the marker when the retry preparation fails.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
There was a problem hiding this comment.
♻️ Duplicate comments (1)
Packages/Shared/CMUXMobileCore/Sources/CMUXMobileCore/MobileTerminalRenderGridReplay.swift (1)
91-91: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winKitty keyboard reset in
screenStateResetis still a silent no-op (marker after digits).
\u{1B}[999<uand\u{1B}[0;1=uplace the private marker (<,=) after the parameter digits. Per the previous review on this exact sequence, Ghostty's CSI parser only collects0x3C-0x3Fmarkers while incsi_entry; once a digit is seen the parser transitions tocsi_param, where those bytes send it tocsi_ignoreand the whole sequence is discarded. So the kitty key-flag stack is never actually reset here, contradicting the "key/input flags" claim in the comment above line 97-100. This string is now load-bearing for the new synchronized restore path (referenced at lines 103, 115, and 138), so the gap persists into this rework.🐛 Proposed fix
- let screenStateReset = "\u{1B}[1\"q\u{1B}[0\"q\u{1B}[999<u\u{1B}[0;1=u\u{0F}\u{1B}(B\u{1B})B\u{1B}*B\u{1B}+B" + let screenStateReset = "\u{1B}[1\"q\u{1B}[0\"q\u{1B}[<999u\u{1B}[=0;1u\u{0F}\u{1B}(B\u{1B})B\u{1B}*B\u{1B}+B"Also applies to: 103-103, 115-115, 138-138
🤖 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/Shared/CMUXMobileCore/Sources/CMUXMobileCore/MobileTerminalRenderGridReplay.swift` at line 91, The kitty reset sequence in screenStateReset is malformed because the private markers are placed after the numeric parameters, so Ghostty discards it and the key/input flags are never reset. Update the reset string in MobileTerminalRenderGridReplay so the kitty CSI markers are emitted in the correct order before any digits, and make sure the synchronized restore path that uses this constant still references the corrected sequence consistently in the surrounding restore logic.
🤖 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.
Duplicate comments:
In
`@Packages/Shared/CMUXMobileCore/Sources/CMUXMobileCore/MobileTerminalRenderGridReplay.swift`:
- Line 91: The kitty reset sequence in screenStateReset is malformed because the
private markers are placed after the numeric parameters, so Ghostty discards it
and the key/input flags are never reset. Update the reset string in
MobileTerminalRenderGridReplay so the kitty CSI markers are emitted in the
correct order before any digits, and make sure the synchronized restore path
that uses this constant still references the corrected sequence consistently in
the surrounding restore logic.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 95b4e425-21b2-4531-996f-89a7b50e04ad
⛔ Files ignored due to path filters (1)
.github/swift-file-length-budget.tsvis excluded by!**/*.tsv
📒 Files selected for processing (5)
Packages/Shared/CMUXMobileCore/Sources/CMUXMobileCore/MobileTerminalRenderGridReplay.swiftPackages/Shared/CMUXMobileCore/Tests/CMUXMobileCoreTests/MobileTerminalRenderGridSnapshotReplayTests.swiftPackages/Shared/CMUXMobileCore/Tests/CMUXMobileCoreTests/MobileTerminalRenderGridTests.swiftPackages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swiftPackages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellRenderGridInputCatchUpTests.swift
…-echo-drift # Conflicts: # .github/swift-file-length-budget.tsv # Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift # Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellRenderGridLivenessTestSupport.swift
The replay failure retry counter is shared with barrier replay failures, and a successful barrier ack clear leaves it at its high-water mark. A surface whose cold-attach replay succeeded only after burning the budget would enter the next typing catch-up already exhausted, so the first dropped render-grid delta could never request the repair replay and non-full deltas stayed dropped until an unrelated full replacement arrived. Clear the counter when a pending input target transitions from none to set, so each catch-up episode gets the full bounded budget. 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).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+TerminalOutputDelivery.swift">
<violation number="1" location="Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+TerminalOutputDelivery.swift:202">
P2: In `retryTerminalReplayAfterAckReset`, `pendingTerminalInputDroppedRenderGridSurfaceIDs` is cleared unconditionally before `prepareTerminalReplayFailureRetry` confirms a retry replay can proceed. If retries are exhausted and the function returns after preserving the barrier, the dropped-frame marker is lost even though no replay ever landed, breaking the PR invariant to keep the marker until a corresponding replay arrives. Consider moving the removal after the retry succeeds, or restoring the marker when the retry preparation fails.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
The pending-input filter runs before the hybrid screen-transition advisory, so a primary render-grid frame that raced the input ACK and landed behind the pending sequence was silently dropped. That frame can be the only signal that the host left the alternate screen, and hybrid transport keeps suppressing raw primary bytes while the tracked screen stays alternate, wedging the surface on stale TUI content until an unrelated frame arrived. Request a replay (bounded by the retry budget) when an event frame dropped behind pending input reports primary while the tracked screen is still alternate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The XTSAVE bank overwrite skipped ?12 (cursor blinking) and ?25 (cursor visibility), so a stale XTSAVE'd ?25l or ?12h on a reused surface survived the full replay and a later XTRESTORE could hide or mis-blink the cursor despite the frame's cursor restore. Force both modes to their Ghostty defaults (?12l, ?25h) right before the bank overwrite so their saved slots are deterministic; the paint sequence and final cursor restore adjust the live values afterwards without touching the bank. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The barrier-episode comment could be misread as describing the general catch-up counter clear; spell out that markTerminalBytesDelivered leaves the counter alone here because no pending input target is set. Use maxTerminalReplayFailureRetries for the scripted failure count instead of a hardcoded 2 so the test tracks the production budget. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The synchronized reset saved the replayed snapshot cursor with a trailing DECSC, so a later bare DECRC or ?1048l restore jumped to the snapshot cursor instead of the default RIS left behind, and a stale DECSC from the reused surface survived on whichever screen the replay did not end on. Save at home with the default pen once per screen inside the reset preamble and stop saving after the cursor restore. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
RIS reset DEC private mode 3, but the manual baseline skipped it entirely, so a reused surface with DECCOLM set or XTSAVE'd kept that geometry mode across a full replay and a later CSI ?3 h/l/r could resize the terminal away from the authoritative remote grid. Emit ?3l right after ?40l — with mode 40 off Ghostty clears the stored value without resizing — and include mode 3 in the saved-bank overwrite so both the live and saved slots return to the RIS baseline. The captured frame's own ?3 stays excluded from replay. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-echo-drift # Conflicts: # .github/swift-file-length-budget.tsv # Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
RIS cleared the whole mode state, but the manual baseline never touched DEC mode 1048, so a reused surface kept its live/XTSAVE'd save-cursor mode value across a full replay where DECRQM could report it and XTRESTORE could act on it. Force ?1048l with the other cursor-mode defaults (its restore action is neutral there: the preamble re-homes and re-saves the cursor immediately after) and add 1048 to the saved-bank overwrite. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…erlap-artifacts Adopt main's typing render-grid drift fix (#7175): the full-snapshot replay's synchronized non-RIS reset prelude and mode baseline supersede this branch's screen-switch-only mode filter (isReplayExcludedMode keeps DECOM and autowrap restored on fulls), and the relocated alternate-screen mode-replay test replaces this branch's copy. Take main's pending-input render-grid gate wholesale. Regenerate the file length budget. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…grid fixes) Re-merge to pick up #7149/#7155/#7175/#7176 and the iOS auth-composition rework. Only .github/swift-file-length-budget.tsv conflicted (regenerated). AppCompositionRoot/ CMUXMobileRootScene auth changes auto-merged cleanly alongside the keyboard-correction preference injection; init + both call sites verified consistent. Core fixes intact (subscription streamID capture, deinit clientHandedOff, TerminalInputDebugLog struct). Co-Authored-By: Claude Opus 4.8 (1M context) <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.
CI's release-build job (twice, deterministically) failed with "the compiler is unable to type-check this expression in reasonable time" on the 7-operand chained string-literal concatenation in appendDefaultModeBaseline, code that landed on main in #7175 and was never exercised by a completed main CI run since (every run was superseded/cancelled). The chain becomes += statements with identical content, which type-check trivially; CMUXMobileCore release build and its 146 package tests pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…le scrolled up (#7196) * Add failing test: iOS terminal input while scrolled up must snap to bottom The iOS Ghostty surface is a display-only mirror: typed bytes go to the Mac and the echo returns in the output stream. When the user scrolls up into local scrollback and then types, the Mac updates at the prompt but the phone stays on old scrollback, so the terminal reads as frozen. In-simulator behavior test mounting a real GhosttySurfaceView + libghostty surface: seed 300 lines, scroll up, simulate typed input via the input proxy, expect the viewport back at the bottom. FAILS on this commit; the fix lands in the next commit. A companion test locks the opposite invariant: passive output must not force the viewport down while the user reads scrollback. Test hooks only in production code: a DEBUG-only debugSkipRenderDispatchForTesting flag skips render dispatch (the scene-less xctest host can never complete a Metal present, which trips the render-stall recovery), mirroring the same hook on the task-ios-terminal-vspace branch. TerminalInputDebugLog moves to its own file to keep GhosttySurfaceView.swift inside the file-length budget. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS terminal: optimistically scroll to bottom on user input All user-produced input on the surface (typing, backspace, escape sequences from arrows/ctrl/hardware keys, paste) converges on the four inputProxy closures. Each now routes through one shared handleUserProducedInput(): reset the cursor blink (as before) and enqueue Ghostty's scroll_to_bottom binding action, so the mirror's viewport follows the user's keystrokes to the prompt where the Mac's echo lands. Passive output still never moves the viewport. The binding action runs on the serial outputQueue (never inline on main) because ghostty_surface_binding_action takes the same internal surface lock as process_output/render_now; enqueuing behind pending process_output also preserves ordering. The enqueue is extracted as enqueueScrollToBottom(), now shared by the initial-output scroll and the DEBUG bottom-scroll stress harness instead of three copies. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Address review: cancel in-flight scroll on input, isolate test hook, lazy debug log Codex P2: a flick still decelerating fought the input snap — coalesced deltas in pendingScrollLines flush on the display-link frame after the scroll_to_bottom enqueue, and UIScrollView momentum kept producing more, so flick-up-then-type could land back in old scrollback. handleUserProducedInput() now drops pending deltas and freezes the scroll mechanics at the current offset before enqueueing the snap. Test-seam policy (Greptile/CodeRabbit/cubic): the debugSkipRenderDispatchForTesting flag moves out of the production class body into the DEBUG-only Debug/ folder as a static member (instance stored properties cannot live in extensions), matching the package's existing debug-isolation pattern and no longer shipping in Release. Input debug logger: message parameter is now an autoclosure so dataSummary/interpolation never run on the typing hot path unless CMUX_INPUT_DEBUG=1, and the body compiles to a no-op in Release so typed user content cannot reach the unified log there. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Coalesce scroll-to-bottom enqueues on the typing path Codex review P2: every keystroke, backspace repeat, and escape sequence enqueued its own lock-taking scroll_to_bottom onto the serial surface queue, so key-repeat during an output/render stall could grow an unbounded backlog of idempotent snaps behind the work the user was waiting on. enqueueScrollToBottom() now sets an in-flight flag and skips while one snap is queued or running; one pending snap is enough because it executes after everything already queued. The flag clears on the completion hop back to main and on the render-pipeline reset path (the queue-regeneration site), so a wedged surface cannot permanently disable the snap. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Scope the render-skip test hook to the suite Codex review P2: makeHarness() set the process-wide debugSkipRenderDispatchForTesting flag and never restored it, so later suites in the same test process would silently stop exercising the real render path; the flag reference also broke non-Debug test builds. Each test now restores the flag in its teardown alongside dismantle, and the suite is gated to DEBUG test configurations (the hook it drives only exists there). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Generation-guard the scroll-to-bottom completion Codex review P2: a scroll_to_bottom completion from pre-recovery queued work could clear scrollToBottomInFlight after a new-generation input had set it, momentarily defeating the coalescing. The completion hop now checks surfaceGeneration like the processOutput completion does, and the reset path (which bumps the generation) remains the owner of clearing the flag across recoveries. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Document TerminalInputAccessoryAction public symbols Aziz documentation policy: the budget extraction moved these public package symbols into a new file, so each case and the output accessor now carry Swift-DocC comments. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Add type-level DocC to TerminalInputAccessoryAction Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fix release-build type-check timeout in mode-baseline literal CI's release-build job (twice, deterministically) failed with "the compiler is unable to type-check this expression in reasonable time" on the 7-operand chained string-literal concatenation in appendDefaultModeBaseline, code that landed on main in #7175 and was never exercised by a completed main CI run since (every run was superseded/cancelled). The chain becomes += statements with identical content, which type-check trivially; CMUXMobileCore release build and its 146 package tests pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
… 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 #7164
Summary
ESC c) with a synchronized soft reset + clear so replay does not flash a blank surface before content lands"\r\n"into oneCharacter), and frames carrying long marker texts are built 40 columns wide so frame validation no longer rejects themorigin/mainand resolve the.github/swift-file-length-budget.tsvconflict with post-merge actual counts (MobileShellComposite.swift7813,WorkspaceUnitTests.swift7404)mobile.terminal.replayforeverOSC 133;Dper screen so replayed cells do not inherit stale semantic prompt/input stateCSI ? … sbatches under the 24-param cap) so a later XTRESTORE cannot resurrect stale modesDECSCUSR 0so a stale primary cursor shape cannot reappear when a replayed TUI exits the alternate screenDECRC/?1048llands on the RIS baseline?3lafter?40l(Ghostty clears the stored value without resizing when mode 40 is off) plus mode 3 in the saved-bank overwriteorigin/mainmerge: integrate Fix iOS render-grid load garble (#7159) #7171's replay-lifecycle extraction and full-replacement staleness with this PR's pending-input catch-up (marker/counter clears woven intoMobileShellComposite+TerminalReplayLifecycle.swift, tests migrated to the sharedrenderGridEventFramefixtures andenqueueReplayRenderGridFrames)Test Plan
swift testinPackages/Shared/CMUXMobileCore: 128 tests passswift testinPackages/iOS/CmuxMobileShell: 335 tests passxcodebuild -scheme CmuxMobileShell -destination 'generic/platform=iOS Simulator' build: succeeds (covers theMobileDebugLogcompile error CI hit at an older head, fixed in 661fee5)python3 scripts/swift_file_length_budget.py: budget respectedNeed help on this PR? Tag
/codesmithwith what you need. Autofix is disabled.Summary by cubic
Fixes #7164. Stabilizes iOS terminal typing echo and removes blank flashes by dropping stale render‑grid behind input, bounding replays, and using a synchronized full‑snapshot restore (no
RIS) with correct mode, color, and cursor resets, including defaulting?1048save‑cursor state.stateSeq; count no‑progress responses (empty, bytes without seq, stale seq, failed non‑barrier) against a bounded retry budget; retry stale replays (barrier and non‑barrier); stop live‑event replay requests after exhaustion and clear the barrier.OSC 8, resetsOSC 133prompt state per screen, restores grapheme mode (?2027) before paint, resets dynamic colors (OSC 10/11/12and fallbacks110/111/112) so clears use the frame default, resets the primary cursor shape before alternate‑screen entry, and resets each screen’s saved cursor to the RIS baseline.?12/?25, save‑cursor?1048, and?3; clear stale DECCOLM without resizing (?40lthen?3l); reset live?1048to its default; then reapply captured modes after a default baseline while skipping geometry, screen‑switch/save‑cursor, reporting, and sync‑output modes.Written for commit 98101ba. Summary will update on new commits.
Summary by CodeRabbit
Bug Fixes
Tests