Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
9b84ba3
Add regression for repaired input event subscription
Jun 26, 2026
ba1a1d2
Replay after repaired input subscription
Jun 26, 2026
8b204d3
Merge remote-tracking branch 'origin/main' into issue-5911-ios-ci-ren…
Jun 26, 2026
7111dad
Refresh Swift file length budget
Jun 26, 2026
d74b973
Add regression for global input repair replay
Jun 26, 2026
6137690
Replay all mounted surfaces after input repair
Jun 26, 2026
217a11a
Merge remote-tracking branch 'origin/main' into issue-5911-ios-ci-ren…
Jun 27, 2026
caca4e9
Add regression for empty-list subscription repair
Jun 27, 2026
6e59027
Repair empty-list subscription refreshes
Jun 27, 2026
0f12da4
Merge remote-tracking branch 'origin/main' into issue-5911-ios-ci-ren…
Jun 27, 2026
1a00bcd
Gate iOS glass APIs by compiler
Jun 27, 2026
7420df5
Gate chat scroll edge APIs by compiler
Jun 27, 2026
61c2972
Fix chat UI older compiler build
Jun 27, 2026
b3e3317
Use window lifecycle for keyboard tap cleanup
Jun 27, 2026
70699ec
Gate chat composer glass container
Jun 27, 2026
deef8e5
Allow composer icon labels in nonisolated closures
Jun 27, 2026
2834052
Allow hardware key commands in static tables
Jun 27, 2026
317bc03
Isolate hardware key command creation
Jun 27, 2026
2aaec53
Make Ghostty surface queue hops sendable
Jun 27, 2026
2f3f01e
Avoid isolated deinit in mobile shell
Jun 27, 2026
b4b68cf
Gate mobile shell glass containers
Jun 27, 2026
52aa765
Avoid sending notification settings to main actor
Jun 27, 2026
5e47ca7
Merge remote-tracking branch 'origin/main' into issue-5911-ios-ci-ren…
austinywang Jun 30, 2026
5746089
Merge remote-tracking branch 'origin/main' into issue-5911-ios-ci-ren…
austinywang Jul 2, 2026
da8bdf3
Add failing test: repaired replay must supersede in-flight cold-attac…
austinywang Jul 2, 2026
01602a0
Supersede in-flight replay when replaying a repaired subscription
austinywang Jul 2, 2026
27661fe
Refresh Swift file length budget
austinywang Jul 2, 2026
56b1775
Restore actor-isolated deinit on Swift 6.2+ toolchains
austinywang Jul 2, 2026
e45854d
Fail fast when TerminalOutputCollector.waitForLineCount undershoots
austinywang Jul 2, 2026
f2911ff
Merge remote-tracking branch 'origin/main' into issue-5911-ios-ci-ren…
austinywang Jul 2, 2026
4cd0fa2
Merge origin/main into issue-5911 (adopt replay-lifecycle refactor + …
austinywang Jul 2, 2026
1da9a30
Match origin/main's ungated isolated deinit in MobileShellComposite
austinywang Jul 2, 2026
1164e3e
Merge remote-tracking branch 'origin/main' into issue-5911-ios-ci-ren…
austinywang Jul 3, 2026
1b90e6b
Avoid repeated workspace scans during repaired replay
austinywang Jul 3, 2026
12419d4
Merge remote-tracking branch 'origin/main' into issue-5911-ios-ci-ren…
austinywang Jul 4, 2026
b59b924
Avoid render-grid resync on pending duplicate input acks
austinywang Jul 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions .github/swift-file-length-budget.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
9497 cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift
8032 Sources/Panels/BrowserPanelView.swift
8016 CLI/cmux_open.swift
7743 Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
7792 Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
7405 cmuxTests/WorkspaceUnitTests.swift
7218 cmuxTests/WorkspaceRemoteConnectionTests.swift
6359 cmuxTests/SessionPersistenceTests.swift
Expand All @@ -24,8 +24,8 @@
4483 Sources/cmuxApp.swift
4482 Sources/Panels/FilePreviewPanel.swift
4367 cmuxTests/BrowserPanelTests.swift
4283 Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift
4121 Sources/BrowserWindowPortal.swift
3964 Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift
3934 Sources/Feed/FeedPanelView.swift
3926 cmuxTests/TabManagerUnitTests.swift
3896 cmuxTests/WindowAndDragTests.swift
Expand Down Expand Up @@ -58,7 +58,7 @@
1656 Sources/FileExplorerView.swift
1652 cmuxTests/CMUXCLIErrorOutputRegressionTests.swift
1649 cmuxTests/TerminalControllerSocketSecurityTests.swift
1604 Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/TerminalInputTextView.swift
1603 Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/TerminalInputTextView.swift
1560 cmuxTests/TextBoxMentionCompletionTests.swift
1500 cmuxUITests/MultiWindowNotificationsUITests.swift
1499 cmuxTests/OmnibarAndToolsTests.swift
Expand Down Expand Up @@ -116,9 +116,10 @@
847 cmuxTests/AgentSessionAutoResumeSettingsTests.swift
845 cmuxTests/SSHStartupSignalLifecycleTests.swift
841 Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/TerminalOutputDeliveryQueueTests.swift
839 Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellRenderGridLivenessTests.swift
834 Sources/MainWindowFocusController.swift
830 Sources/TaskManagerTypes.swift
825 Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalComposerView.swift
823 Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalComposerView.swift
822 Sources/WorkspaceContentView.swift
810 Packages/macOS/CmuxSwiftRender/Tests/CmuxSwiftRenderTests/SwiftViewInterpreterTests.swift
803 Packages/iOS/CmuxMobilePairedMac/Sources/CmuxMobilePairedMac/MobilePairedMacStore.swift
Expand All @@ -137,6 +138,7 @@
752 cmuxUITests/CloseWorkspaceCmdDUITests.swift
739 cmuxTests/CLICodexHookTimeoutRegressionTests.swift
738 Packages/macOS/CMUXProjectModel/Sources/CMUXProjectModel/XcodeProjectAdapter.swift
735 Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellRenderGridLivenessTestSupport.swift
718 Packages/Shared/CmuxAuthRuntime/Sources/CmuxAuthRuntime/Coordinator/AuthCoordinator.swift
716 Sources/TaskManagerSnapshot.swift
715 Sources/AppleScriptSupport.swift
Expand All @@ -157,11 +159,11 @@
680 Sources/FileExplorerSearchController.swift
677 Packages/macOS/CmuxRemoteSession/Sources/CmuxRemoteSession/Session/RemoteSessionCoordinator+Bootstrap.swift
672 cmuxTests/SessionPersistenceResumeBindingTests.swift
671 Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellRenderGridInputCatchUpTests.swift
668 cmuxTests/FeedCoordinatorTests.swift
668 cmuxTests/SettingsWindowPresenterTests.swift
664 Sources/CmuxTopSnapshot.swift
663 Sources/PortScanner.swift
660 Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellRenderGridInputCatchUpTests.swift
655 Packages/macOS/CmuxRemoteSession/Sources/CmuxRemoteSession/Session/RemoteSessionCoordinator.swift
655 Packages/macOS/CmuxTerminal/Sources/CmuxTerminal/Surface/TerminalSurface+RuntimeLifecycle.swift
650 Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Import/Detection/BrowserInstalledBrowserDetector.swift
Expand All @@ -170,13 +172,11 @@
644 Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/AgentLaunchSanitizerTests.swift
642 cmuxTests/RemoteTmuxControlParserTests.swift
641 cmuxTests/CommandPaletteNucleoFFITests.swift
637 Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellRenderGridLivenessTestSupport.swift
635 cmuxUITests/RightSidebarChromeHeightUITests.swift
630 Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutWhenClause.swift
620 Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/KeyboardShortcutsSection.swift
620 cmuxTests/FinderFileDropRegressionTests.swift
620 cmuxTests/TerminalNotificationQueueTests.swift
617 Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellRenderGridLivenessTests.swift
617 Sources/SettingsNavigation.swift
608 cmuxUITests/FeedSidebarUITests.swift
607 Packages/macOS/CmuxTerminal/Sources/CmuxTerminal/Surface/TerminalSurface.swift
Expand Down Expand Up @@ -241,9 +241,9 @@
518 Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/CMUXMobileRootView.swift
518 Packages/macOS/CmuxSwiftRender/Tests/CmuxSwiftRenderTests/Corpus/stress-git-review-queue-command-deck.swift
516 Sources/TerminalImageTransfer.swift
515 Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Transcript/ChatTranscriptTableView.swift
514 Packages/macOS/CmuxSwiftRender/Sources/CmuxSwiftRender/ExpressionEvaluator.swift
514 cmuxUITests/UpdatePillUITests.swift
513 Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Transcript/ChatTranscriptTableView.swift
509 Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentLaunchSanitizerAdditionalPolicies.swift
508 Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/GhosttySurfaceRepresentable.swift
507 Sources/TerminalControllerTopSupport.swift
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,12 @@ public struct ChatComposerView: View {
@State private var attachments: [ChatComposerAttachment] = []
@State private var dictation = ComposerDictationController()
#endif

@Environment(\.chatTheme) private var theme

@ScaledMetric(relativeTo: .title) private var sendButtonSize: CGFloat = 36
private let controlHeight: CGFloat = 40

private static let maxAttachmentDimension: CGFloat = 2048
private static let jpegQuality: CGFloat = 0.85
private static let hardStopWindow: TimeInterval = 2

public init(
agentState: ChatAgentState,
agentKind: ChatAgentKind,
Expand Down Expand Up @@ -99,13 +95,17 @@ public struct ChatComposerView: View {
#if os(iOS)
@ViewBuilder
private var composerSurface: some View {
#if compiler(>=6.2)
if #available(iOS 26.0, *) {
GlassEffectContainer {
composerStack
}
} else {
composerStack
}
#else
composerStack
#endif
}
#endif

Expand Down Expand Up @@ -375,6 +375,7 @@ public struct ChatComposerView: View {
isDraftFocused = true
}

@MainActor
private var attachButton: some View {
PhotosPicker(selection: $pickedItems, maxSelectionCount: 4, matching: .images) {
MobileComposerIconLabel(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ struct KeyboardDismissTap: UIViewRepresentable {
/// in `didMoveToWindow` — the only reliable "I'm in a window now" hook
/// (relying on `updateUIView` timing missed the attach when no input
/// changed after mount, so the first version never fired).
@MainActor
final class TapInstallerView: UIView {
/// Region (window coords) whose taps dismiss the keyboard.
var dismissRegion: CGRect = .zero
Expand Down Expand Up @@ -77,10 +78,6 @@ struct KeyboardDismissTap: UIViewRepresentable {
installedWindow = window
}

deinit {
installedWindow?.removeGestureRecognizer(recognizer)
}

@objc private func handleTap() {
// Resign whoever holds the keyboard, app-wide; robust regardless
// of which window/responder owns it.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,7 @@ private struct ChatTranscriptTableConfiguration {
return items
}

@MainActor
@ViewBuilder
func view(for item: ChatTranscriptTableItem, tableWidth: CGFloat) -> some View {
itemView(for: item)
Expand All @@ -407,6 +408,7 @@ private struct ChatTranscriptTableConfiguration {
)
}

@MainActor
@ViewBuilder
private func itemView(for item: ChatTranscriptTableItem) -> some View {
switch item {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,11 +167,11 @@ public struct ChatFileEditCardView: View {
private func diffLineAccessibilityLabel(_ line: String) -> String {
if line.hasPrefix("+") {
return String(localized: "chat.diff.added.accessibility",
defaultValue: "Added: \(line.dropFirst())", bundle: .module)
defaultValue: "Added: \(String(line.dropFirst()))", bundle: .module)
}
if line.hasPrefix("-") {
return String(localized: "chat.diff.removed.accessibility",
defaultValue: "Removed: \(line.dropFirst())", bundle: .module)
defaultValue: "Removed: \(String(line.dropFirst()))", bundle: .module)
}
if line.hasPrefix("@@") {
return String(localized: "chat.diff.hunk.accessibility",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6290,22 +6290,69 @@ public final class MobileShellComposite: MobileTerminalOutputSinking {
}
}

private func refreshTerminalEventSubscription(reason: String) {
private func refreshTerminalEventSubscription(
reason: String,
replaySurfaceIDsIfRepaired: [String] = []
) {
guard let client = remoteClient, connectionState == .connected else { return }
guard runtime?.supportsServerPushEvents ?? true else { return }
guard terminalSubscriptionRefreshTask == nil else { return }
Comment thread
austinywang marked this conversation as resolved.
terminalSubscriptionRefreshTask = Task { @MainActor [weak self] in
defer { self?.terminalSubscriptionRefreshTask = nil }
guard let self else { return }
let topics = self.terminalOutputTransport.eventTopics
_ = await self.requestTerminalEventSubscription(
let ack = await self.requestTerminalEventSubscription(
client: client,
reason: reason,
topics: topics
)
guard !Task.isCancelled,
self.remoteClient === client,
self.connectionState == .connected else {
return
}
guard case .subscribed(let alreadySubscribed) = ack,
alreadySubscribed == false else {
return
}
let replaySurfaceIDs = replaySurfaceIDsIfRepaired.isEmpty
? Array(self.terminalByteContinuationsBySurfaceID.keys)
: replaySurfaceIDsIfRepaired
MobileDebugLog.anchormux("sync.subscribe_repaired reason=\(reason) surfaces=\(replaySurfaceIDs.count)")
self.replayAfterRepairedTerminalEventSubscription(
surfaceIDs: replaySurfaceIDs
)
}
}

private func replayAfterRepairedTerminalEventSubscription(surfaceIDs: [String]) {
var workspaceIDsBySurfaceID: [String: MobileWorkspacePreview.ID] = [:]
for workspace in workspaces {
for terminal in workspace.terminals where workspaceIDsBySurfaceID[terminal.id.rawValue] == nil {
workspaceIDsBySurfaceID[terminal.id.rawValue] = workspace.id
}
}
for surfaceID in surfaceIDs where hasTerminalOutputSink(surfaceID: surfaceID) {
// A repaired subscription means events were missed during the gap,
// so this catch-up replay must reflect that gap. `requestTerminalReplay`
// no-ops while a replay for the surface is already in flight, which
// would silently drop the catch-up behind an older (e.g. cold-attach)
// replay whose snapshot predates the missed events, leaving the surface
// stuck behind the input response sequence. Supersede any in-flight
// replay first so the fresh request always goes out; it re-adopts the
// active replay barrier token, if any.
cancelTerminalReplayInFlight(surfaceID: surfaceID)
requestTerminalReplay(
surfaceID: surfaceID,
resolvedWorkspaceID: workspaceIDsBySurfaceID[surfaceID]
)
}
// The same registration carries `workspace.updated`, so workspace
// create/rename/delete events emitted during the gap were missed too;
// re-fetch the authoritative list.
scheduleWorkspaceListRefreshFromEvent()
}

private func startTerminalRefreshPolling() {
guard let client = remoteClient else { return }
guard runtime?.supportsServerPushEvents ?? true else { return }
Expand Down Expand Up @@ -6612,13 +6659,9 @@ public final class MobileShellComposite: MobileTerminalOutputSinking {
// host-side condition), so no listener restart is needed.
MobileDebugLog.anchormux("sync.liveness probe_repaired silentMs=\(Int(silent * 1000))")
mobileShellLog.info("liveness probe reinstalled a lost event subscription, replaying mounted surfaces")
for surfaceID in self.terminalByteContinuationsBySurfaceID.keys {
self.requestTerminalReplay(surfaceID: surfaceID)
}
// The same registration carries `workspace.updated`, so
// workspace create/rename/delete events emitted during the
// gap were missed too; re-fetch the authoritative list.
self.scheduleWorkspaceListRefreshFromEvent()
self.replayAfterRepairedTerminalEventSubscription(
surfaceIDs: Array(self.terminalByteContinuationsBySurfaceID.keys)
)
} else {
MobileDebugLog.anchormux("sync.liveness probe_ok silentMs=\(Int(silent * 1000))")
}
Expand Down Expand Up @@ -6723,6 +6766,8 @@ public final class MobileShellComposite: MobileTerminalOutputSinking {
"sync.input_seq_replay_after_drop surface=\(surfaceID) local=\(localSeq) pending=\(targetSeq) remote=\(remoteSeq)"
)
requestTerminalReplayAfterDroppedRenderGrid(surfaceID: surfaceID, source: "input_ack")
} else if localSeq < previousPendingSeq {
MobileDebugLog.anchormux("sync.input_seq_waiting surface=\(surfaceID) local=\(localSeq) pending=\(previousPendingSeq) remote=\(remoteSeq)")
}
return
}
Expand All @@ -6736,7 +6781,10 @@ public final class MobileShellComposite: MobileTerminalOutputSinking {
}
pendingTerminalByteEndSeqBySurfaceID[surfaceID] = targetSeq
MobileDebugLog.anchormux("sync.input_seq_wait surface=\(surfaceID) local=\(localSeq) pending=\(targetSeq) remote=\(remoteSeq)")
refreshTerminalEventSubscription(reason: "input_seq_wait")
refreshTerminalEventSubscription(
reason: "input_seq_wait",
replaySurfaceIDsIfRepaired: Array(terminalByteContinuationsBySurfaceID.keys)
)
return
}
MobileDebugLog.anchormux("sync.input_seq_behind surface=\(surfaceID) local=\(localSeq) remote=\(remoteSeq)")
Expand Down Expand Up @@ -6910,7 +6958,8 @@ public final class MobileShellComposite: MobileTerminalOutputSinking {
func requestTerminalReplay(
surfaceID: String,
replayBarrierToken: UUID? = nil,
coveredReplayBarrierDroppedOutputCount: UInt64? = nil
coveredReplayBarrierDroppedOutputCount: UInt64? = nil,
resolvedWorkspaceID: MobileWorkspacePreview.ID? = nil
) {
if let replayBarrierToken, terminalReplayBarrierTokensBySurfaceID[surfaceID] != replayBarrierToken { return }; let replayBarrierTokenForRequest = replayBarrierToken
?? terminalReplayBarrierTokensBySurfaceID[surfaceID]
Expand Down Expand Up @@ -6938,7 +6987,7 @@ public final class MobileShellComposite: MobileTerminalOutputSinking {
#endif
return
}
guard let workspaceID = workspaceID(forTerminalID: surfaceID) else {
guard let workspaceID = resolvedWorkspaceID ?? workspaceID(forTerminalID: surfaceID) else {
clearTerminalReplayBarrierIfCurrent(
surfaceID: surfaceID,
token: replayBarrierTokenForRequest,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,21 @@ import Testing
)
#expect(firstRefreshSent, "the first ahead-of-render-grid ACK should refresh the event subscription")
let subscribeCountAfterFirstAck = await router.count(of: "mobile.events.subscribe")
let hostStatusCountAfterFirstAck = await router.count(of: "mobile.host.status")

await store.submitTerminalRawInput(Data("b".utf8), surfaceID: "live-terminal")
let inputSent = try await pollUntil { await router.count(of: "terminal.input") >= 2 }
#expect(inputSent)
let restartSent = await router.waitForCount(
of: "mobile.host.status",
atLeast: hostStatusCountAfterFirstAck + 1,
timeoutNanoseconds: 500_000_000,
recordIssueOnTimeout: false
)
#expect(
!restartSent,
"duplicate ACKs for the same pending sequence must not restart the render-grid event stream"
)
let duplicateRefreshSent = await router.waitForCount(
of: "mobile.events.subscribe",
atLeast: subscribeCountAfterFirstAck + 1,
Expand Down
Loading
Loading