Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -14776,7 +14776,11 @@ public final class MobileShellComposite: MobileTerminalOutputSinking {
return streamToken
}

private func unregisterTerminalOutput(surfaceID: String, streamToken: UUID) {
private func unregisterTerminalOutput(
surfaceID: String,
streamToken: UUID,
releaseViewport: Bool
) {
guard terminalOutputStreamTokensBySurfaceID[surfaceID] == streamToken else { return }
terminalLatencyObserver.surfaceClosed(surfaceID: surfaceID)
terminalLaneOutputReadySurfaceIDs.remove(surfaceID)
Expand Down Expand Up @@ -14833,8 +14837,13 @@ public final class MobileShellComposite: MobileTerminalOutputSinking {
.terminalUnmounted,
correlationID: surfaceID
)
// Tell the Mac this device is no longer viewing the surface so it can unpin and clear its border.
clearTerminalViewport(surfaceID: surfaceID)
if releaseViewport {
// Ownerless and release-gate streams keep the historical contract:
// ending the stream means the viewer lease ended too. Mounted UI
// streams pass false here because UIKit/output-consumer churn is a
// narrower lifetime than presentation ownership.
clearTerminalViewport(surfaceID: surfaceID)
}
}

/// The output byte stream for a terminal surface.
Expand All @@ -14858,6 +14867,26 @@ public final class MobileShellComposite: MobileTerminalOutputSinking {
public func terminalOutputStream(
surfaceID: String,
ownerID: UUID?
) -> AsyncStream<MobileTerminalOutputChunk> {
terminalOutputStream(
surfaceID: surfaceID,
ownerID: ownerID,
releaseViewportOnTermination: true
)
}

/// Opens an owner-aware output stream with explicit viewport-lease
/// termination semantics.
///
/// Mounted UIKit consumers set `releaseViewportOnTermination` to false:
/// their stream can end during a temporary window detach or bounded
/// consumer restart while the presentation still owns the sticky viewport
/// report. Presentation teardown releases that report explicitly through
/// `clearTerminalViewport(surfaceID:)`.
public func terminalOutputStream(
surfaceID: String,
ownerID: UUID?,
releaseViewportOnTermination: Bool
) -> AsyncStream<MobileTerminalOutputChunk> {
AsyncStream { continuation in
let streamToken = registerTerminalOutput(
Expand All @@ -14869,7 +14898,8 @@ public final class MobileShellComposite: MobileTerminalOutputSinking {
Task { @MainActor in
self?.unregisterTerminalOutput(
surfaceID: surfaceID,
streamToken: streamToken
streamToken: streamToken,
releaseViewport: releaseViewportOnTermination
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -513,9 +513,9 @@ struct GhosttySurfaceRepresentable: UIViewRepresentable {
}
}
)
// Drive every output chunk into the libghostty surface. Ending this
// task terminates the stream, which unregisters the surface and
// clears its viewport pin on the Mac (see `terminalOutputStream`).
// Drive every output chunk into the libghostty surface. The output
// stream owns delivery only; this coordinator's presentation owns
// the sticky viewport lease and releases it explicitly on teardown.
outputTaskGeneration &+= 1
let taskGeneration = outputTaskGeneration
let ownerID = UUID()
Expand All @@ -539,7 +539,8 @@ struct GhosttySurfaceRepresentable: UIViewRepresentable {
guard let store else { return }
for await chunk in store.terminalOutputStream(
surfaceID: surfaceID,
ownerID: ownerID
ownerID: ownerID,
releaseViewportOnTermination: false
) {
guard !Task.isCancelled else { return }
guard let self else { return }
Expand Down
Loading