From aefe33f9af16bc19d465ac45b835e05356182eb3 Mon Sep 17 00:00:00 2001 From: Aziz Albahar Date: Sat, 13 Jun 2026 16:58:56 -0700 Subject: [PATCH 1/2] Smooth iOS terminal scrollback with local prefetch --- ...hellComposite+TerminalScrollDelivery.swift | 49 +++++++++++--- .../MobileShellComposite.swift | 34 +++++++--- .../TerminalScrollDelivery.swift | 38 +++++++++++ .../TerminalScrollDeliveryQueueTests.swift | 40 ++++++++++++ ...ttySurfaceView+LocalScrollbackScroll.swift | 26 ++++++++ .../GhosttySurfaceView.swift | 1 + ...minalController+MobileScrollPrefetch.swift | 65 +++++++++++++++++++ Sources/TerminalController.swift | 31 ++------- cmux.xcodeproj/project.pbxproj | 4 ++ 9 files changed, 244 insertions(+), 44 deletions(-) create mode 100644 Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView+LocalScrollbackScroll.swift create mode 100644 Sources/TerminalController+MobileScrollPrefetch.swift diff --git a/Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+TerminalScrollDelivery.swift b/Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+TerminalScrollDelivery.swift index 7e5bb09df81..251f1a7c68c 100644 --- a/Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+TerminalScrollDelivery.swift +++ b/Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+TerminalScrollDelivery.swift @@ -19,7 +19,17 @@ extension MobileShellComposite { /// in flight, newer deltas are summed into the next request instead of /// piling up stale scroll packets. public func scrollTerminal(surfaceID: String, lines: Double, col: Int, row: Int) async { - enqueueTerminalScroll(TerminalScrollDelivery(surfaceID: surfaceID, lines: lines, col: col, row: row)) + var prefetchState = terminalScrollbackPrefetchStatesBySurfaceID[surfaceID] + ?? TerminalScrollbackPrefetchState() + let maxScrollbackRows = prefetchState.rowsToPrefetch(forScrollLines: lines) + terminalScrollbackPrefetchStatesBySurfaceID[surfaceID] = prefetchState + enqueueTerminalScroll(TerminalScrollDelivery( + surfaceID: surfaceID, + lines: lines, + col: col, + row: row, + maxScrollbackRows: maxScrollbackRows + )) } private func enqueueTerminalScroll(_ delivery: TerminalScrollDelivery) { @@ -58,18 +68,37 @@ extension MobileShellComposite { return } do { + var params: [String: Any] = [ + "workspace_id": workspaceID.rawValue, + "surface_id": delivery.surfaceID, + "client_id": clientID, + "delta_lines": delivery.lines, + "col": delivery.col, + "row": delivery.row, + ] + if let maxScrollbackRows = delivery.maxScrollbackRows { + params["max_scrollback_rows"] = maxScrollbackRows + } let request = try MobileCoreRPCClient.requestData( method: "mobile.terminal.scroll", - params: [ - "workspace_id": workspaceID.rawValue, - "surface_id": delivery.surfaceID, - "client_id": clientID, - "delta_lines": delivery.lines, - "col": delivery.col, - "row": delivery.row, - ] + params: params + ) + let data = try await client.sendRequest(request) + guard let maxScrollbackRows = delivery.maxScrollbackRows, + maxScrollbackRows > 0, + remoteClient === client else { + return + } + guard let payload = try? MobileTerminalReplayResponse.decode(data), + let renderGrid = payload.renderGrid, + renderGrid.surfaceID == delivery.surfaceID else { + return + } + deliverAuthoritativeTerminalRenderGrid( + renderGrid, + expectedSurfaceID: delivery.surfaceID, + source: "scroll_prefetch" ) - _ = try await client.sendRequest(request) } catch { terminalScrollDeliveryLog.error("scroll forward failed surface=\(delivery.surfaceID, privacy: .public) error=\(String(describing: error), privacy: .public)") } diff --git a/Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift b/Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift index c59215ca96f..ac7222dde19 100644 --- a/Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift +++ b/Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift @@ -540,6 +540,7 @@ public final class MobileShellComposite: MobileTerminalOutputSinking { var terminalOutputQueuesBySurfaceID: [String: TerminalOutputDeliveryQueue] var terminalScrollQueueTokensBySurfaceID: [String: UUID] var terminalScrollQueuesBySurfaceID: [String: TerminalScrollDeliveryQueue] + var terminalScrollbackPrefetchStatesBySurfaceID: [String: TerminalScrollbackPrefetchState] private var rawTerminalInputBuffer: MobileTerminalInputSendBuffer private var pairingAttemptID: UUID @@ -666,6 +667,7 @@ public final class MobileShellComposite: MobileTerminalOutputSinking { self.terminalOutputQueuesBySurfaceID = [:] self.terminalScrollQueueTokensBySurfaceID = [:] self.terminalScrollQueuesBySurfaceID = [:] + self.terminalScrollbackPrefetchStatesBySurfaceID = [:] self.rawTerminalInputBuffer = MobileTerminalInputSendBuffer() self.pairingAttemptID = UUID() } @@ -3078,6 +3080,7 @@ public final class MobileShellComposite: MobileTerminalOutputSinking { terminalOutputStreamTokensBySurfaceID = terminalOutputStreamTokensBySurfaceID.mapValues { _ in UUID() } terminalScrollQueueTokensBySurfaceID = [:] terminalScrollQueuesBySurfaceID = [:] + terminalScrollbackPrefetchStatesBySurfaceID = [:] terminalOutputTransport = .rawBytes supportedHostCapabilities = [] terminalSubscriptionRefreshTask?.cancel() @@ -4313,6 +4316,26 @@ public final class MobileShellComposite: MobileTerminalOutputSinking { } } + func deliverAuthoritativeTerminalRenderGrid( + _ renderGrid: MobileTerminalRenderGridFrame, + expectedSurfaceID: String? = nil, + source: String + ) { + guard expectedSurfaceID == nil || renderGrid.surfaceID == expectedSurfaceID, + hasTerminalOutputSink(surfaceID: renderGrid.surfaceID) else { + return + } + if let deliveredSeq = deliveredTerminalByteEndSeqBySurfaceID[renderGrid.surfaceID], + deliveredSeq > renderGrid.stateSeq { + MobileDebugLog.anchormux( + "sync.render_grid_stale source=\(source) surface=\(renderGrid.surfaceID) delivered=\(deliveredSeq) frame=\(renderGrid.stateSeq)" + ) + return + } + markTerminalBytesDelivered(surfaceID: renderGrid.surfaceID, endSeq: renderGrid.stateSeq) + deliverTerminalRenderGrid(renderGrid, surfaceID: renderGrid.surfaceID) + } + private static func terminalSnapshotReplacementBytes(_ snapshotBytes: Data) -> Data { var bytes = Data("\u{1B}c\u{1B}[H\u{1B}[2J\u{1B}[3J".utf8) bytes.append(snapshotBytes) @@ -4345,6 +4368,7 @@ public final class MobileShellComposite: MobileTerminalOutputSinking { terminalOutputQueuesBySurfaceID.removeValue(forKey: surfaceID) terminalScrollQueueTokensBySurfaceID.removeValue(forKey: surfaceID) terminalScrollQueuesBySurfaceID.removeValue(forKey: surfaceID) + terminalScrollbackPrefetchStatesBySurfaceID.removeValue(forKey: surfaceID) deliveredTerminalByteEndSeqBySurfaceID.removeValue(forKey: surfaceID) pendingTerminalByteEndSeqBySurfaceID.removeValue(forKey: surfaceID) // Tell the Mac this device is no longer viewing the surface so it stops @@ -4533,18 +4557,10 @@ public final class MobileShellComposite: MobileTerminalOutputSinking { hasTerminalOutputSink(surfaceID: renderGrid.surfaceID) else { return } - if let deliveredSeq = deliveredTerminalByteEndSeqBySurfaceID[renderGrid.surfaceID], - deliveredSeq > renderGrid.stateSeq { - MobileDebugLog.anchormux( - "sync.render_grid_stale surface=\(renderGrid.surfaceID) delivered=\(deliveredSeq) frame=\(renderGrid.stateSeq)" - ) - return - } - markTerminalBytesDelivered(surfaceID: renderGrid.surfaceID, endSeq: renderGrid.stateSeq) #if DEBUG mobileShellLog.info("CMUX_REPLAY live render_grid surface=\(renderGrid.surfaceID, privacy: .public) full=\(renderGrid.full, privacy: .public) spans=\(renderGrid.rowSpans.count, privacy: .public) cleared=\(renderGrid.clearedRows.count, privacy: .public) seq=\(renderGrid.stateSeq, privacy: .public) hasSink=true") #endif - deliverTerminalRenderGrid(renderGrid, surfaceID: renderGrid.surfaceID) + deliverAuthoritativeTerminalRenderGrid(renderGrid, source: "event") } private func handleNotificationDismissedEvent(_ event: MobileEventEnvelope) async { diff --git a/Packages/CmuxMobileShell/Sources/CmuxMobileShell/TerminalScrollDelivery.swift b/Packages/CmuxMobileShell/Sources/CmuxMobileShell/TerminalScrollDelivery.swift index a2bd46c8984..f96d2360692 100644 --- a/Packages/CmuxMobileShell/Sources/CmuxMobileShell/TerminalScrollDelivery.swift +++ b/Packages/CmuxMobileShell/Sources/CmuxMobileShell/TerminalScrollDelivery.swift @@ -5,11 +5,49 @@ struct TerminalScrollDelivery: Equatable, Sendable { var lines: Double var col: Int var row: Int + var maxScrollbackRows: Int? = nil mutating func append(_ delivery: TerminalScrollDelivery) { lines += delivery.lines col = delivery.col row = delivery.row + switch (maxScrollbackRows, delivery.maxScrollbackRows) { + case (.some(let current), .some(let incoming)): + maxScrollbackRows = max(current, incoming) + case (nil, .some(let incoming)): + maxScrollbackRows = incoming + case (.some, nil), (nil, nil): + break + } + } +} + +struct TerminalScrollbackPrefetchState: Equatable, Sendable { + static let defaultWindowRows = 600 + static let defaultRefreshDistanceRows = 120.0 + + var windowRows: Int + var refreshDistanceRows: Double + private var hasPrimedWindow = false + private var accumulatedRowsSincePrefetch = 0.0 + + init( + windowRows: Int = Self.defaultWindowRows, + refreshDistanceRows: Double = Self.defaultRefreshDistanceRows + ) { + self.windowRows = max(0, windowRows) + self.refreshDistanceRows = max(1, refreshDistanceRows) + } + + mutating func rowsToPrefetch(forScrollLines lines: Double) -> Int? { + guard lines != 0, windowRows > 0 else { return nil } + accumulatedRowsSincePrefetch += abs(lines) + guard !hasPrimedWindow || accumulatedRowsSincePrefetch >= refreshDistanceRows else { + return nil + } + hasPrimedWindow = true + accumulatedRowsSincePrefetch = 0 + return windowRows } } diff --git a/Packages/CmuxMobileShell/Tests/CmuxMobileShellTests/TerminalScrollDeliveryQueueTests.swift b/Packages/CmuxMobileShell/Tests/CmuxMobileShellTests/TerminalScrollDeliveryQueueTests.swift index fe35af8b7ee..1a7eecf24c7 100644 --- a/Packages/CmuxMobileShell/Tests/CmuxMobileShellTests/TerminalScrollDeliveryQueueTests.swift +++ b/Packages/CmuxMobileShell/Tests/CmuxMobileShellTests/TerminalScrollDeliveryQueueTests.swift @@ -37,6 +37,46 @@ import Testing #expect(queue.isIdle) } +@Test func terminalScrollQueueCoalescesLargestScrollbackPrefetchWindow() throws { + var queue = TerminalScrollDeliveryQueue() + let inFlight = TerminalScrollDelivery(surfaceID: "surface", lines: 1, col: 1, row: 1) + let firstPending = TerminalScrollDelivery( + surfaceID: "surface", + lines: 2, + col: 2, + row: 2, + maxScrollbackRows: 240 + ) + let latestPending = TerminalScrollDelivery( + surfaceID: "surface", + lines: 3, + col: 3, + row: 3, + maxScrollbackRows: 600 + ) + + #expect(queue.enqueue(inFlight) == inFlight) + #expect(queue.enqueue(firstPending) == nil) + #expect(queue.enqueue(latestPending) == nil) + + let maybeCoalesced = queue.completeInFlight() + let coalesced = try #require(maybeCoalesced) + #expect(coalesced.lines == 5) + #expect(coalesced.col == 3) + #expect(coalesced.row == 3) + #expect(coalesced.maxScrollbackRows == 600) +} + +@Test func terminalScrollbackPrefetchStatePrimesThenRefreshesByDistance() { + var state = TerminalScrollbackPrefetchState(windowRows: 600, refreshDistanceRows: 10) + + #expect(state.rowsToPrefetch(forScrollLines: 0) == nil) + #expect(state.rowsToPrefetch(forScrollLines: 1) == 600) + #expect(state.rowsToPrefetch(forScrollLines: 4) == nil) + #expect(state.rowsToPrefetch(forScrollLines: -5.5) == nil) + #expect(state.rowsToPrefetch(forScrollLines: 0.5) == 600) +} + @Test func terminalScrollQueueResetDropsPendingWork() { var queue = TerminalScrollDeliveryQueue() let inFlight = TerminalScrollDelivery(surfaceID: "surface", lines: 1, col: 1, row: 1) diff --git a/Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView+LocalScrollbackScroll.swift b/Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView+LocalScrollbackScroll.swift new file mode 100644 index 00000000000..b425f293aa8 --- /dev/null +++ b/Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView+LocalScrollbackScroll.swift @@ -0,0 +1,26 @@ +#if canImport(UIKit) +import GhosttyKit +import UIKit + +extension GhosttySurfaceView { + /// Apply the scroll to the phone's local Ghostty mirror immediately. On the + /// primary screen this consumes the preloaded local scrollback window, so a + /// drag/deceleration feels native while the Mac catches up. On alternate + /// screens libghostty turns this into mouse-wheel bytes; the mirror is + /// display-only and drops those bytes, so the authoritative Mac response + /// remains the visible update for TUIs. + func applyLocalScrollbackScroll(lines: Double, col: Int, row: Int) { + guard lines != 0, let surface else { return } + let displayScale = window?.windowScene?.screen.scale ?? traitCollection.displayScale + let scale = max(Double(displayScale), 1) + let size = ghostty_surface_size(surface) + let cellWidthPt = max(Double(size.cell_width_px) / scale, 1) + let cellHeightPt = max(Double(size.cell_height_px) / scale, 1) + let posX = (Double(max(0, col)) + 0.5) * cellWidthPt + let posY = (Double(max(0, row)) + 0.5) * cellHeightPt + ghostty_surface_mouse_pos(surface, posX, posY, GHOSTTY_MODS_NONE) + ghostty_surface_mouse_scroll(surface, 0, lines, 0) + drawForWakeup() + } +} +#endif diff --git a/Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift b/Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift index 2ef2784734e..46b12bfe4e2 100644 --- a/Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift +++ b/Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift @@ -1810,6 +1810,7 @@ public final class GhosttySurfaceView: UIView, TerminalSurfaceHosting { let lines = pendingScrollLines let cell = pendingScrollCell pendingScrollLines = 0 + applyLocalScrollbackScroll(lines: lines, col: cell.col, row: cell.row) delegate?.ghosttySurfaceView(self, didScrollLines: lines, atCol: cell.col, row: cell.row) } diff --git a/Sources/TerminalController+MobileScrollPrefetch.swift b/Sources/TerminalController+MobileScrollPrefetch.swift new file mode 100644 index 00000000000..c0b07f45dfe --- /dev/null +++ b/Sources/TerminalController+MobileScrollPrefetch.swift @@ -0,0 +1,65 @@ +import CMUXMobileCore +import Foundation + +extension TerminalController { + /// Scrollback rows included in a cold-attach render-grid replay snapshot. + /// Live render-grid events carry no scrollback; the phone keeps its own + /// bounded Ghostty scrollback mirror and scrolls that mirror locally while + /// the Mac remains authoritative. + nonisolated static let mobileReplayScrollbackLineBudget = 240 + + /// Larger history window returned only on explicit mobile scroll prefetch + /// requests, keeping ordinary scroll RPCs small. + nonisolated static let mobileScrollPrefetchScrollbackLineBudget = 600 + + func mobileTerminalRenderGridFrame( + terminalPanel: TerminalPanel, + surfaceID: UUID, + seq: UInt64, + scrollbackLines: Int = TerminalController.mobileReplayScrollbackLineBudget + ) -> MobileTerminalRenderGridFrame? { + guard surfaceID == terminalPanel.id else { return nil } + return terminalPanel.surface.mobileRenderGridFrame( + stateSeq: seq, + scrollbackLines: scrollbackLines + )?.frame + } + + func mobileTerminalScrollResponsePayload( + workspaceID: UUID, + terminalPanel: TerminalPanel, + surfaceID: UUID, + params: [String: Any] + ) -> [String: Any] { + var payload: [String: Any] = [ + "workspace_id": workspaceID.uuidString, + "surface_id": surfaceID.uuidString, + ] + let scrollbackRows = mobileScrollPrefetchRows(params: params) + guard scrollbackRows > 0 else { return payload } + let stateSeq = MobileTerminalByteTee.shared.currentSequence(surfaceID: surfaceID) ?? 0 + guard let renderGrid = mobileTerminalRenderGridFrame( + terminalPanel: terminalPanel, + surfaceID: surfaceID, + seq: stateSeq, + scrollbackLines: scrollbackRows + ), + renderGrid.activeScreen == .primary, + let renderGridObject = try? renderGrid.jsonObject() else { + return payload + } + payload["columns"] = renderGrid.columns + payload["rows"] = renderGrid.rows + payload["render_grid"] = renderGridObject + payload["seq"] = renderGrid.stateSeq + return payload + } + + private func mobileScrollPrefetchRows(params: [String: Any]) -> Int { + let requestedRows = (params["max_scrollback_rows"] as? NSNumber)?.intValue ?? 0 + return min( + max(0, requestedRows), + Self.mobileScrollPrefetchScrollbackLineBudget + ) + } +} diff --git a/Sources/TerminalController.swift b/Sources/TerminalController.swift index 770d3d49636..88dbe7e3d0f 100644 --- a/Sources/TerminalController.swift +++ b/Sources/TerminalController.swift @@ -4947,27 +4947,6 @@ class TerminalController { return output } - /// Scrollback rows included in a cold-attach render-grid replay snapshot. - /// Live render-grid events carry no scrollback (the client already has it); - /// only the replay anchor needs history. Kept minimal on purpose: a - /// freshly-attached device gets the live screen immediately, and deeper - /// history is a follow-up (incremental scrollback paging on scroll-to-top). - /// Tune up to trade replay payload size for more attach-time history. - nonisolated static let mobileReplayScrollbackLineBudget = 1 - - private func mobileTerminalRenderGridFrame( - terminalPanel: TerminalPanel, - surfaceID: UUID, - seq: UInt64, - scrollbackLines: Int = TerminalController.mobileReplayScrollbackLineBudget - ) -> MobileTerminalRenderGridFrame? { - guard surfaceID == terminalPanel.id else { return nil } - return terminalPanel.surface.mobileRenderGridFrame( - stateSeq: seq, - scrollbackLines: scrollbackLines - )?.frame - } - private func readPlainTerminalTextForSnapshot( terminalPanel: TerminalPanel, includeScrollback: Bool = false, @@ -14165,10 +14144,12 @@ class TerminalController { terminalPanel.surface.mobileScroll(deltaLines: deltaLines, col: max(0, col), row: max(0, row)) MobileTerminalRenderObserver.shared.noteTerminalBytes(surfaceID: terminalPanel.id) } - return .ok([ - "workspace_id": resolved.workspace.id.uuidString, - "surface_id": surfaceId.uuidString, - ]) + return .ok(mobileTerminalScrollResponsePayload( + workspaceID: resolved.workspace.id, + terminalPanel: terminalPanel, + surfaceID: surfaceId, + params: params + )) } func v2MobileTerminalMouse(params: [String: Any]) -> V2CallResult { diff --git a/cmux.xcodeproj/project.pbxproj b/cmux.xcodeproj/project.pbxproj index ca8b600c27b..acb70bfa4db 100644 --- a/cmux.xcodeproj/project.pbxproj +++ b/cmux.xcodeproj/project.pbxproj @@ -705,6 +705,7 @@ C0DE00000000000000000C62 /* TerminalController+ControlWorkspaceContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE00000000000000000C61 /* TerminalController+ControlWorkspaceContext.swift */; }; C0DE00000000000000000C54 /* TerminalController+ControlWorkspaceGroupContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE00000000000000000C53 /* TerminalController+ControlWorkspaceGroupContext.swift */; }; D7AB00000000000000B001 /* TerminalController+MobileNotificationSync.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7AB00000000000000B000 /* TerminalController+MobileNotificationSync.swift */; }; + D7AB00000000000000B011 /* TerminalController+MobileScrollPrefetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7AB00000000000000B010 /* TerminalController+MobileScrollPrefetch.swift */; }; C7A50B000000000000000012 /* TerminalController+MobileWorkspaceList.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7A50B000000000000000011 /* TerminalController+MobileWorkspaceList.swift */; }; D7AB0000000000000000000B /* TerminalController+MoveTabToNewWorkspace.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7AB0000000000000000000C /* TerminalController+MoveTabToNewWorkspace.swift */; }; A5001007 /* TerminalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001019 /* TerminalController.swift */; }; @@ -1534,6 +1535,7 @@ C0DE00000000000000000C61 /* TerminalController+ControlWorkspaceContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TerminalController+ControlWorkspaceContext.swift"; sourceTree = ""; }; C0DE00000000000000000C53 /* TerminalController+ControlWorkspaceGroupContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TerminalController+ControlWorkspaceGroupContext.swift"; sourceTree = ""; }; D7AB00000000000000B000 /* TerminalController+MobileNotificationSync.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TerminalController+MobileNotificationSync.swift"; sourceTree = ""; }; + D7AB00000000000000B010 /* TerminalController+MobileScrollPrefetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TerminalController+MobileScrollPrefetch.swift"; sourceTree = ""; }; C7A50B000000000000000011 /* TerminalController+MobileWorkspaceList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TerminalController+MobileWorkspaceList.swift"; sourceTree = ""; }; D7AB0000000000000000000C /* TerminalController+MoveTabToNewWorkspace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TerminalController+MoveTabToNewWorkspace.swift"; sourceTree = ""; }; A5001019 /* TerminalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalController.swift; sourceTree = ""; }; @@ -2125,6 +2127,7 @@ C0DE00000000000000000C45 /* TerminalController+ControlNotificationContext.swift */, C0DE00000000000000000C61 /* TerminalController+ControlWorkspaceContext.swift */, D7AB00000000000000B000 /* TerminalController+MobileNotificationSync.swift */, + D7AB00000000000000B010 /* TerminalController+MobileScrollPrefetch.swift */, C0DE00000000000000000C81 /* TerminalController+ControlBrowserPanelContext.swift */, C0DE00000000000000000C6D /* TerminalController+ControlDebugContext.swift */, C0DE00000000000000000C6F /* TerminalController+ControlProjectContext.swift */, @@ -3442,6 +3445,7 @@ C0DE00000000000000000C62 /* TerminalController+ControlWorkspaceContext.swift in Sources */, C0DE00000000000000000C54 /* TerminalController+ControlWorkspaceGroupContext.swift in Sources */, D7AB00000000000000B001 /* TerminalController+MobileNotificationSync.swift in Sources */, + D7AB00000000000000B011 /* TerminalController+MobileScrollPrefetch.swift in Sources */, C7A50B000000000000000012 /* TerminalController+MobileWorkspaceList.swift in Sources */, D7AB0000000000000000000B /* TerminalController+MoveTabToNewWorkspace.swift in Sources */, A5001007 /* TerminalController.swift in Sources */, From 304a7fe5c16f875cfb7b16878084649f482801da Mon Sep 17 00:00:00 2001 From: Aziz Albahar Date: Sat, 13 Jun 2026 20:38:42 -0700 Subject: [PATCH 2/2] Refresh Swift file length budget --- .github/swift-file-length-budget.tsv | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/swift-file-length-budget.tsv b/.github/swift-file-length-budget.tsv index cb3446b955b..6df0c999307 100644 --- a/.github/swift-file-length-budget.tsv +++ b/.github/swift-file-length-budget.tsv @@ -4,7 +4,7 @@ 33285 CLI/cmux.swift 19197 Sources/ContentView.swift 17899 Sources/AppDelegate.swift -14629 Sources/TerminalController.swift +14610 Sources/TerminalController.swift 13572 Sources/Panels/BrowserPanel.swift 12078 Sources/GhosttyTerminalView.swift 12046 cmuxTests/AppDelegateShortcutRoutingTests.swift @@ -21,7 +21,7 @@ 5969 cmuxTests/TerminalAndGhosttyTests.swift 5500 cmuxTests/BrowserConfigTests.swift 5470 Sources/cmuxApp.swift -4922 Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift +4938 Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift 4460 Sources/Panels/FilePreviewPanel.swift 4400 cmuxTests/BrowserPanelTests.swift 4227 Sources/BrowserWindowPortal.swift @@ -29,7 +29,7 @@ 3895 cmuxTests/WindowAndDragTests.swift 3764 cmuxTests/TabManagerUnitTests.swift 3699 cmuxTests/CLIGenericHookPersistenceTests.swift -3663 Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift +3664 Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift 3397 Sources/CmuxConfig.swift 3331 cmuxTests/TabManagerSessionSnapshotTests.swift 3202 Sources/Update/UpdateTitlebarAccessory.swift