Skip to content

iOS: fix View as Text showing empty - #6736

Closed
lawrencecchen wants to merge 1 commit into
mainfrom
fix-ios-view-as-text
Closed

lawrencecchen wants to merge 1 commit into
mainfrom
fix-ios-view-as-text

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jun 24, 2026 •

Copy link
Copy Markdown
Contributor

Summary

On the iPhone, "View as Text" showed the empty state "No terminal text available" even when the terminal clearly had visible output. Two causes:

  1. The capture's registry pick is visibility-scoped (it requires the surface to be window-attached, not hidden, and opaque). It ran from the sheet's own .task, i.e. after the sheet had begun presenting. Presenting a sheet can transiently drop the presenter's window/alpha, so the one live surface could be filtered out and the read returned nil. The resolve now happens in openTextSheetFromMenu, at tap time, while the terminal surface is still fully on screen. copyableTerminalText does its visibility-scoped pick and FIFO-enqueues the off-main read synchronously before returning the awaitable, so the surface can no longer be filtered out by a presentation-induced window/alpha drop. The sheet just awaits that capture.

  2. surfaceText returns a non-nil empty string ("") for a zero-byte range, not nil. So surfaceText(SCREEN) ?? surfaceText(VIEWPORT) never fell back when the SCREEN read succeeded but came back empty. The new resolvedText treats a nil OR empty SCREEN as "try VIEWPORT", and only yields nil when neither has content.

Both decisions are extracted into CopyableTerminalTextSelection (pure, no UIKit/GhosttyKit) so the candidate predicate and the SCREEN-vs-viewport fallback are unit-testable. A concise DEBUG-only anchormux diagnostic records the requested id, every candidate's state (id / surface / window / hidden / alpha), and the SCREEN/VIEWPORT read outcomes, so the path is self-explaining in the dogfood log.

No new user-facing strings, so no localization changes.

Testing

  • swift test on CmuxMobileTerminalKit (host build): 103 tests pass, including the new CopyableTerminalTextSelection suite covering id-scoped match, excluded-when-no-surface, off-screen exclusion, deterministic lowest-keyed pick, and empty-SCREEN-falls-back-to-viewport.
  • xcodebuild build for iOS Simulator on both CmuxMobileTerminal and CmuxMobileShellUI: BUILD SUCCEEDED.
  • Not yet dogfooded on a real device; needs a live "View as Text" tap on a phone with terminal output.

Issues

Plain-text bug (no GitHub issue): iPhone "View as Text" showed "No terminal text available" even when the terminal had visible output.


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Note

Low Risk
Localized iOS copy-sheet bugfix with preserved fallback path; no auth, networking, or data-model changes.

Overview
Fixes iPhone View as Text incorrectly showing No terminal text available when the terminal clearly had visible output.

Timing: Terminal text capture now starts in openTextSheetFromMenu at menu tap (while the surface is still window-attached), and the sheet awaits that pre-started Task instead of calling copyableTerminalText from .task after presentation. Sheet presentation can briefly drop the presenter’s window/alpha; the visibility-scoped registry pick used to run too late and filter out the live surface.

Read fallback: SCREEN reads that succeed with "" no longer block VIEWPORT fallback (screen ?? viewport treated empty SCREEN as success). CopyableTerminalTextSelection.resolvedText treats nil or empty SCREEN as “try VIEWPORT.”

Testability: Surface eligibility and SCREEN/VIEWPORT resolution move into pure CopyableTerminalTextSelection in CmuxMobileTerminalKit, wired from GhosttySurfaceRegistry, with unit tests. DEBUG anchormux logs record pick candidates and read outcomes.

Reviewed by Cursor Bugbot for commit 23d3fea. Bugbot is set up for automated code reviews on this repo. Configure here.


Summary by cubic

Fixes iPhone “View as Text” showing empty when the terminal has visible output. The capture now starts at tap time and empty SCREEN reads fall back to VIEWPORT, so text reliably loads.

  • Bug Fixes

    • Start the capture in openTextSheetFromMenu and pass it to TerminalTextSheetView; avoids losing the visibility-scoped pick during sheet presentation.
    • Fall back when SCREEN is nil or empty using CopyableTerminalTextSelection.resolvedText; only show empty when both SCREEN and VIEWPORT have no content.
  • Refactors

    • Extracted pick/fallback logic to CopyableTerminalTextSelection in CmuxMobileTerminalKit with unit tests; integrated into GhosttySurfaceView in CmuxMobileTerminal.
    • Added DEBUG anchormux logs for candidate states and read outcomes.

Written for commit 23d3fea. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • “View as Text” now captures terminal text at the moment you tap it, helping preserve the text you intended to view.
    • The text sheet now shows a more reliable snapshot when the live terminal changes or disappears.
  • Bug Fixes

    • Improved text selection so the app can fall back to a visible viewport capture when the main screen capture is empty.
    • Better handling of which terminal window is used for text capture, reducing missed or blank results.

Two causes made the sheet show "No terminal text available" with visible
output:

1. The registry pick for the capture is visibility-scoped (window/hidden/
   alpha). It ran from the sheet's own .task, after presentation, when the
   presenter's window/alpha can transiently drop, so the one live surface
   could be filtered out and the read returned nil. Resolve and FIFO-enqueue
   the read now happen in openTextSheetFromMenu, at tap time, while the
   surface is still fully on screen; the sheet just awaits that capture.

2. surfaceText returns a non-nil empty string for a zero-byte range, so
   screen ?? viewport never fell back when SCREEN read empty-but-ok. The new
   resolvedText treats a nil OR empty SCREEN as "try VIEWPORT".

Extract both decisions into CopyableTerminalTextSelection (pure, no UIKit/
GhosttyKit) and unit-test the id-scoped pick, the no-surface/off-screen
exclusions, and the empty-SCREEN-falls-back-to-viewport rule. Add a concise
DEBUG-only anchormux diagnostic recording the requested id, every candidate's
state, and the SCREEN/VIEWPORT read outcomes so the path is self-explaining in
the dogfood log.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 24, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jun 24, 2026 6:26am
cmux-staging Building Building Preview, Comment Jun 24, 2026 6:26am

@coderabbitai

coderabbitai Bot commented Jun 24, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 24c7cdc7-2bd8-4592-9009-8f6527f905eb

📥 Commits

Reviewing files that changed from the base of the PR and between d65cbf2 and 23d3fea.

📒 Files selected for processing (5)
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalTextSheetView.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView.swift
  • Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceRegistry.swift
  • Packages/iOS/CmuxMobileTerminalKit/Sources/CmuxMobileTerminalKit/CopyableTerminalTextSelection.swift
  • Packages/iOS/CmuxMobileTerminalKit/Tests/CmuxMobileTerminalKitTests/CopyableTerminalTextSelectionTests.swift

📝 Walkthrough

Walkthrough

A new pure CopyableTerminalTextSelection enum is added to extract surface-selection and screen/viewport fallback logic into a testable model. GhosttySurfaceRegistry.copyableTerminalText is refactored to use this model. WorkspaceDetailView spawns a text-capture Task at menu-tap time and passes it to TerminalTextSheetView, which prefers awaiting the pre-armed capture over a later registry-based resolve.

Changes

View as Text tap-time capture refactor

Layer / File(s) Summary
CopyableTerminalTextSelection model and tests
Packages/iOS/CmuxMobileTerminalKit/Sources/CmuxMobileTerminalKit/CopyableTerminalTextSelection.swift, Packages/iOS/CmuxMobileTerminalKit/Tests/CmuxMobileTerminalKitTests/CopyableTerminalTextSelectionTests.swift
Introduces CopyableTerminalTextSelection enum with Candidate value type, isEligible predicate, deterministic chosenIndex selector, and resolvedText screen/viewport fallback; full test suite covering all three behaviors.
GhosttySurfaceRegistry selection and read refactor
Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceRegistry.swift
Adds imports for CmuxMobileDiagnostics and CmuxMobileTerminalKit; replaces inline matchingView predicate with CopyableTerminalTextSelection.chosenIndex over a mapped candidate list; reads SCREEN and VIEWPORT unconditionally and resolves final text via CopyableTerminalTextSelection.resolvedText; adds DEBUG candidate logging.
Tap-time capture wiring
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView.swift, Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalTextSheetView.swift
WorkspaceDetailView adds @State textSheetCapture and spawns the capture task in openTextSheetFromMenu at tap time, passing it into TerminalTextSheetView via capture:; TerminalTextSheetView adds the capture property and loadSnapshot prefers await capture.value before falling back to a registry-based resolve.

Sequence Diagram

sequenceDiagram
  participant User
  participant WorkspaceDetailView
  participant GhosttySurfaceRegistry
  participant CopyableTerminalTextSelection
  participant TerminalTextSheetView

  User->>WorkspaceDetailView: tap "View as Text"
  WorkspaceDetailView->>GhosttySurfaceRegistry: Task { copyableTerminalText(surfaceID:) }
  Note over WorkspaceDetailView: stores in textSheetCapture
  GhosttySurfaceRegistry->>CopyableTerminalTextSelection: chosenIndex(candidates, surfaceID)
  GhosttySurfaceRegistry->>CopyableTerminalTextSelection: resolvedText(screen, viewport)
  WorkspaceDetailView->>TerminalTextSheetView: present sheet(capture: textSheetCapture)
  TerminalTextSheetView->>TerminalTextSheetView: loadSnapshot() — await capture.value
  TerminalTextSheetView-->>User: display captured text
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • manaflow-ai/cmux#5875: Introduced the original "View as Text" sheet flow including TerminalTextSheetView.loadSnapshot, WorkspaceDetailView tap wiring, and GhosttySurfaceRegistry.copyableTerminalText that this PR directly refactors.

Poem

🐇 Hop hop, I tap the menu with glee,
A Task is born before the sheet can see!
Candidate lists, a chosen one found,
Screen and viewport both unwound.
Non-empty screen wins every time—
The rabbit's text capture is sublime! 🌿


Important

Pre-merge checks failed

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

❌ Failed checks (2 errors)

Check name Status Explanation Resolution
Cmux Algorithmic Complexity ❌ Error copyableTerminalText sorts and rescans all registered views on each “View as Text” tap (O(n log n)+O(n)) with no bound or benchmark. Use a one-pass eligible-min lookup or cached indexed order; if sorting is intentional, add a documented upper bound plus profiling/benchmark note.
Cmux No Ambient Global State ❌ Error FAIL: CopyableTerminalTextSelection is a new caseless enum namespace of only static funcs at `Packages/iOS/CmuxMobileTerminalKit/Sources/CmuxMobileTerminalKit/CopyableTerminalTextSelection.swift:... Move the selection/fallback logic onto a constructable instance type and inject it at the registry/sheet seam, or keep it private/fileprivate if it’s only local.
✅ Passed checks (23 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: fixing the iPhone View as Text empty-state bug.
Description check ✅ Passed The description is detailed and covers the bug, fix, testing, and issue context, so it is mostly complete.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed No new actor-isolation debt: the new selection model is pure/nonisolated, UI views stay on MainActor, and only private @unchecked Sendable handles were added with safety notes.
Cmux Swift Blocking Runtime ✅ Passed The touched Swift code adds no semaphores, sleeps, main-syncs, polling, or manual locks; the async bridging via withCheckedContinuation/outputQueue.async is non-blocking.
Cmux Browser Automation Off-Main ✅ Passed PR only updates ghostty and vendor submodule pointers; the ghostty range touches surface/embedded text code, not browser socket automation routing or waits.
Cmux Expensive Synchronous Load ✅ Passed Touched code only adds async terminal-text capture and pure selection/fallback; no agent-history load, JSON/JSONL parse, or directory scan was added to a main-actor/interactive path.
Cmux Cache Substitution Correctness ✅ Passed Tap-time capture is transient, not persisted; cold nil falls back to a fresh resolve, and the call sites document why stale-after-tap is acceptable for this sheet.
Cmux No Hacky Sleeps ✅ Passed Swift-only diff adds no sleeps/timers/polling; it just captures text at tap time and refactors pure selection/fallback logic.
Cmux Swift Concurrency ✅ Passed The diff adds only stored, caller-owned Tasks in the SwiftUI sheet flow; the other async work is a third-party/UI boundary, not disallowed legacy async.
Cmux Swift @Concurrent ✅ Passed The new async paths explicitly hop off UI work via Task/outputQueue/Task.detached, and no changed function adds invalid or missing @concurrent usage.
Cmux Swift File And Package Boundaries ✅ Passed Testable selection/fallback logic moved into small CmuxMobileTerminalKit; app-target edits are thin UI/Ghostty glue, and the 896-line view only gained 21 lines.
Cmux Swiftpm Lockfiles ✅ Passed Only submodule gitlink updates in vendored third-party dirs (ghostty, vendor/bonsplit); no cmux-owned Package.swift, .gitignore, workflow, or Package.resolved files changed.
Cmux Swift Logging ✅ Passed PASS: The only new diagnostics are MobileDebugLog.anchormux(...) calls wrapped in #if DEBUG; no new production print/NSLog/file logging or sensitive data exposure.
Cmux User-Facing Error Privacy ✅ Passed Touched UI copy is generic/localized; added anchormux logging is DEBUG-gated, and no user-facing text exposes vendor/provider/internal details.
Cmux Full Internationalization ✅ Passed The PR only adds tests, comments, and DEBUG logs; touched user-facing SwiftUI text uses existing localized keys already present in Localizable.xcstrings, with no catalog edits.
Cmux Swiftui State Layout ✅ Passed Only adds @State/task plumbing and event-driven capture; no new ObservableObject/@published, GeometryReader, lazy-row store refs, or render-time state writes.
Cmux Architecture Rethink ✅ Passed Small local correctness fix with clear owner/invariant; no sleeps, polling, locks, observers, or split ownership added.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed Only sheets, registry logic, and tests changed; no user-visible NSWindow/NSPanel/WindowGroup or cmuxAuxiliaryWindowIdentifiers/close-shortcut routing was introduced.
Cmux Source Artifacts ✅ Passed Diff only adds hand-written source and test files under Sources/ and Tests/, which the rule explicitly allows.
Cmux No Test Or Debug Seam In Production Source ✅ Passed PASS: GhosttySurfaceRegistry adds only DEBUG logging in production source; the test-observable selection/fallback logic was moved into CopyableTerminalTextSelection and covered under Tests/.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-ios-view-as-text

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@greptile-apps

greptile-apps Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes two independent bugs that caused "View as Text" to show an empty sheet on iPhone even when the terminal had visible output. The surface registry pick (which requires the view to be window-attached and opaque) was previously invoked from inside the sheet's own .task, by which time the sheet presentation had transiently dropped the presenter's window/alpha and excluded the only live surface; the pick is now triggered at menu-tap time. Additionally, surfaceText can return a non-nil empty string for a zero-byte range, so the prior screen ?? viewport chain never fell back to the viewport when SCREEN succeeded but was empty; the new resolvedText treats nil-or-empty SCREEN as a signal to try VIEWPORT.

  • CopyableTerminalTextSelection extracts the eligibility predicate and SCREEN/VIEWPORT resolution into a UIKit-free type in CmuxMobileTerminalKit, and a new test suite covers the key edge cases.
  • openTextSheetFromMenu now creates a Task immediately (while the surface is still visible) and passes it to TerminalTextSheetView, which awaits it instead of re-resolving from the registry post-presentation.

Confidence Score: 4/5

Safe to merge; the two bug fixes are well-reasoned and the new test suite covers the core decision points.

The fix correctly identifies and addresses both root causes. The only findings are: a caseless-enum static namespace that violates the no-ambient-global-state style rule, an eager double-read of VIEWPORT even when SCREEN succeeds, and a missing task cancel on sheet dismiss. None of these affect the correctness of the shipped fix.

CopyableTerminalTextSelection.swift — the caseless enum namespace structure should be revisited before the pattern spreads to other extracted-logic files.

Important Files Changed

Filename Overview
Packages/iOS/CmuxMobileTerminalKit/Sources/CmuxMobileTerminalKit/CopyableTerminalTextSelection.swift New pure-logic file extracting surface-pick eligibility and SCREEN/VIEWPORT fallback decisions; well-documented and testable, but structured as a caseless enum static namespace which violates the no-ambient-global-state rule.
Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceRegistry.swift Refactored registry pick to delegate to CopyableTerminalTextSelection; adds DEBUG-only anchormux diagnostics and now reads both SCREEN and VIEWPORT eagerly before resolving — minor unnecessary extra lock acquisition when SCREEN succeeds.
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView.swift Moves surface capture to tap time via a new @State Task; onDismiss clears the reference but does not cancel the task before release, leaving the background read running if the user dismisses mid-load.
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalTextSheetView.swift Adds optional capture parameter; loadSnapshot correctly prefers the pre-armed task and falls back gracefully when capture is nil or surfaceID is nil — logic is clean and the fallback path is preserved.
Packages/iOS/CmuxMobileTerminalKit/Tests/CmuxMobileTerminalKitTests/CopyableTerminalTextSelectionTests.swift Comprehensive test suite covering id-scoped match, dismantling-view exclusion, off-screen exclusion, deterministic lowest-key pick, nil/empty SCREEN fallback, and honest empty state — all cases well chosen.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant User
    participant WDV as WorkspaceDetailView
    participant GSV as GhosttySurfaceView
    participant OQ as outputQueue
    participant Sheet as TerminalTextSheetView

    User->>WDV: tap View as Text
    note over WDV: surface fully window-attached
    WDV->>GSV: copyableTerminalText on MainActor
    GSV->>GSV: pick eligible candidate
    GSV->>OQ: enqueue SCREEN and VIEWPORT reads
    GSV-->>WDV: Task capture returned
    WDV->>Sheet: present with capture
    note over Sheet: sheet begins presenting
    Sheet->>Sheet: .task loadSnapshot
    Sheet->>GSV: await capture.value
    OQ-->>Sheet: resolvedText result
    Sheet->>Sheet: capped snapshot computed off main
    Sheet-->>User: selectable terminal text shown
    User->>Sheet: dismiss
    Sheet->>WDV: onDismiss clears textSheetCapture
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant User
    participant WDV as WorkspaceDetailView
    participant GSV as GhosttySurfaceView
    participant OQ as outputQueue
    participant Sheet as TerminalTextSheetView

    User->>WDV: tap View as Text
    note over WDV: surface fully window-attached
    WDV->>GSV: copyableTerminalText on MainActor
    GSV->>GSV: pick eligible candidate
    GSV->>OQ: enqueue SCREEN and VIEWPORT reads
    GSV-->>WDV: Task capture returned
    WDV->>Sheet: present with capture
    note over Sheet: sheet begins presenting
    Sheet->>Sheet: .task loadSnapshot
    Sheet->>GSV: await capture.value
    OQ-->>Sheet: resolvedText result
    Sheet->>Sheet: capped snapshot computed off main
    Sheet-->>User: selectable terminal text shown
    User->>Sheet: dismiss
    Sheet->>WDV: onDismiss clears textSheetCapture
Loading

Reviews (1): Last reviewed commit: "iOS: fix View as Text showing empty when..." | Re-trigger Greptile

/// 2. `surfaceText` returns a non-nil empty string when the range has zero bytes,
/// so `screen ?? viewport` never fell back when SCREEN read empty-but-ok. The
/// `resolvedText` decision below treats nil OR empty SCREEN as "try VIEWPORT".
public enum CopyableTerminalTextSelection {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Caseless enum used as static-function namespace

CopyableTerminalTextSelection is a caseless enum whose entire API is three public static funcs — this is the namespace-enum antipattern flagged by the cmux no-ambient-global-state rule. The comment block in the rule explicitly calls out "a caseless enum or empty struct used purely as a static func/static let namespace." The fix that keeps it host-testable without UIKit is top-level free functions in CmuxMobileTerminalKit (pass for private/fileprivate file-scope pure helpers) or a concrete struct with an initializer — either form lets the test suite reach them via @testable import without requiring a static-namespace enum.

Rule Used: Flag new ambient global state in production Swift:... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +132 to +134
let screen = surfaceText(handle.surface, pointTag: GHOSTTY_POINT_SCREEN)
let viewport = surfaceText(handle.surface, pointTag: GHOSTTY_POINT_VIEWPORT)
let text = CopyableTerminalTextSelection.resolvedText(screen: screen, viewport: viewport)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Both the SCREEN and VIEWPORT reads are issued eagerly even when SCREEN has content. The old code's lazy ?? chain only read VIEWPORT when SCREEN returned nil. The problem the PR fixes (empty SCREEN never falling back) could be addressed without reading VIEWPORT unconditionally: read VIEWPORT only when SCREEN is absent or empty. This avoids a second ghostty_surface_read_text call (and the lock acquisition it implies) on the common case where SCREEN succeeds with content.

Suggested change
let screen = surfaceText(handle.surface, pointTag: GHOSTTY_POINT_SCREEN)
let viewport = surfaceText(handle.surface, pointTag: GHOSTTY_POINT_VIEWPORT)
let text = CopyableTerminalTextSelection.resolvedText(screen: screen, viewport: viewport)
let screen = surfaceText(handle.surface, pointTag: GHOSTTY_POINT_SCREEN)
let viewport: String? = (screen?.isEmpty ?? true)
? surfaceText(handle.surface, pointTag: GHOSTTY_POINT_VIEWPORT)
: nil
let text = CopyableTerminalTextSelection.resolvedText(screen: screen, viewport: viewport)

}
.sheet(isPresented: $isTextSheetPresented) {
TerminalTextSheetView(surfaceID: textSheetSurfaceID)
.sheet(isPresented: $isTextSheetPresented, onDismiss: { textSheetCapture = nil }) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 The onDismiss callback releases the task reference but does not call cancel() on it. If the user dismisses the sheet before the outputQueue read completes, the unstructured Task continues running until it finishes (the surface read acquires the surface lock), and capture.value in the sheet's already-cancelled .task is simply dropped. For a user-triggered, bounded read this is harmless, but explicitly cancelling is the safer idiom and signals intent to cooperating callers.

Suggested change
.sheet(isPresented: $isTextSheetPresented, onDismiss: { textSheetCapture = nil }) {
.sheet(isPresented: $isTextSheetPresented, onDismiss: {
textSheetCapture?.cancel()
textSheetCapture = nil
}) {

@lawrencecchen lawrencecchen added the stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening. label Sep 23, 2026
@github-project-automation github-project-automation Bot moved this from Todo to Done in cmux backlog Sep 23, 2026

This branch was successfully deployed

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

Labels

stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants