Skip to content

Use Ghostty's queued text path for deferred terminal sends - #3124

Closed
lawrencecchen wants to merge 6 commits into
mainfrom
task-ghostty-large-text-insert-api
Closed

lawrencecchen wants to merge 6 commits into
mainfrom
task-ghostty-large-text-insert-api

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Apr 23, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • replace AppDelegate.sendTextWhenReady with direct TerminalSurface.sendText routing
  • use Ghostty's built-in ghostty_surface_text path and deferred queue instead of app-level observers and a 3 second timeout
  • add regression coverage that proves the preferred terminal target queues text immediately when the runtime surface is still missing

Testing

  • ./scripts/setup.sh
  • ./scripts/reload.sh --tag txtapi
  • local unit tests not run, repo policy prefers CI for tests

Issues

  • Related: task "Figure out the right Ghostty API to insert a lot of text and remove the sendTextWhenReady code smell"

Summary by cubic

Route all terminal text inserts through Ghostty’s queued text API to remove app-level deferral logic and avoid 3s timeouts. Adds a more robust ReactGrab pasteback extractor, defers beforeSend until text dispatch, and treats empty links as plain text. Addresses the Linear task to use the right Ghostty API for large text inserts and remove the sendTextWhenReady code smell.

  • New Features

    • ReactGrab pasteback extracts readable content from selected elements (headings, paragraphs, links, code) with markdown-style formatting and a safe fallback.
    • Preserves tabs/newlines and filters control characters during pasteback.
  • Bug Fixes

    • beforeSend runs only when text is sent to a live surface; queued sends defer the callback. Added tests for extractor formatting (headings/links/code, indentation) and queued-callback behavior.
    • Empty anchor href values are ignored during pasteback and rendered as plain text.

Written for commit 004a2c5. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Injected pasteback extractor that converts copied HTML into normalized, readable plain-text/markdown-like output and tightens scalar filtering (preserves tabs/newlines).
  • Bug Fixes

    • Terminal text routing now resolves target panels immediately, delegates queuing to the terminal surface, and consolidates routing/debug logging. Dispatch completion callbacks are invoked on actual send.
  • Tests

    • Added tests for pasteback extraction formatting and terminal send/queue behavior.
  • Chores

    • Added a debug helper to inspect pending terminal input state.

@vercel

vercel Bot commented Apr 23, 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 Apr 23, 2026 3:49am

@coderabbitai

coderabbitai Bot commented Apr 23, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Replaces delayed sendTextWhenReady with sendTextToTerminalSurface that resolves a target TerminalPanel synchronously and routes text into TerminalPanel.sendText(..., onDispatch:), delegating immediate write vs. queuing and dispatch-callback invocation to TerminalSurface. Adds pending-input debug snapshot, tighter pasteback extraction, and tests for queuing and extractor formatting.

Changes

Cohort / File(s) Summary
Terminal Text Routing
Sources/AppDelegate.swift
Removes sendTextWhenReady and adds @discardableResult static func sendTextToTerminalSurface(...)->Bool. Resolves target panel immediately and calls terminalPanel.sendText(..., onDispatch:); returns false if no target.
TerminalSurface / Ghostty view
Sources/GhosttyTerminalView.swift
Extends TerminalSurface.sendText(_:, onDispatch:) to accept an optional dispatch callback, attaches it to queued PendingSocketInput items, invokes it after writing queued or immediate data, and adds @MainActor debugPendingSocketInputSnapshot() (debug-only).
TerminalPanel API
Sources/Panels/TerminalPanel.swift
Updates TerminalPanel.sendText(_:, onDispatch:) signature to forward the dispatch callback into TerminalSurface.
React pasteback extractor & bridge
Sources/Panels/ReactGrab.swift
Adds JS pasteback content extractor injected into the bridge; normalizes/extracts DOM text (headings, lists, links, code fences), tightens unicode scalar filtering (blocks dangerous C0/C1 except tab/newline), and uses extractor output for onCopySuccess with try/catch fallback.
Tests — routing & pasteback
cmuxTests/ShortcutAndCommandPaletteTests.swift, cmuxTests/BrowserPanelTests.swift
Adds test asserting queued behavior when preferred surface is missing (and that beforeSend is not called) and tests validating ReactGrab pasteback extraction/formatting, preserved tabs/blocks, and empty-anchor handling.

Sequence Diagram(s)

sequenceDiagram
    participant Caller as AppDelegate
    participant Panel as TerminalPanel
    participant Surface as TerminalSurface

    Caller->>Panel: sendTextToTerminalSurface(text, preferredPanelId?, beforeSend?)
    Panel->>Panel: resolve target panel (by id or active)
    alt panel found
        Panel->>Surface: terminalPanel.sendText(text, onDispatch: beforeSend)
        alt surface ready
            Surface->>Surface: writeTextData(...)
            Surface->>Panel: invoke onDispatch()
            Panel-->>Caller: return true
        else surface not ready
            Surface->>Surface: enqueue PendingSocketInput(text, onDispatch)
            Panel-->>Caller: return false
        end
    else no panel found
        Panel-->>Caller: return false
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐇
I hopped from caller down the lane,
Routed text to shell again,
If surface naps I tuck it neat,
Then tap your callback when we meet,
A tiny hop, a tidy beat.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning The PR description covers the main changes and testing approach, but lacks key details expected by the template structure. Add a 'Demo Video' section (or note if not applicable), request bot reviews by including the review trigger block, and complete the checklist with actual checkmarks for tested items and resolved comments.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title directly matches the main objective of the PR, which is replacing the old sendTextWhenReady mechanism with Ghostty's queued text path for terminal sends.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 task-ghostty-large-text-insert-api

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 and usage tips.

@greptile-apps

greptile-apps Bot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR replaces the complex sendTextWhenReady observer-based mechanism (NotificationCenter observers, Combine pipeline, 3-second timeout) with a direct call to TerminalSurface.sendText, which already has a built-in deferred queue (pendingSocketInputQueue) that flushes when the runtime surface attaches. The refactor removes ~160 lines of coordination logic and adds a regression test proving queuing works when the surface is absent.

Confidence Score: 5/5

Safe to merge — clean simplification with no P0/P1 issues and regression coverage for the deferred-surface path

All findings are at P2 or below. The refactor correctly delegates deferred sends to the existing pendingSocketInputQueue mechanism, eliminates observer leaks and the 3-second timeout, and the new test verifies the queue behaviour. Caller semantics for beforeSend are preserved for all production call sites.

No files require special attention

Important Files Changed

Filename Overview
Sources/AppDelegate.swift Replaces sendTextWhenReady instance method with static sendTextToTerminalSurface, removing all observer/timeout logic and delegating deferred sends to TerminalSurface.pendingSocketInputQueue
Sources/GhosttyTerminalView.swift Adds debugPendingSocketInputSnapshot() test-only helper inside #if DEBUG, exposing queue depth/key count/byte count for assertion in the new test
cmuxTests/ShortcutAndCommandPaletteTests.swift Adds testSendTextToTerminalSurfaceQueuesImmediateTextOnPreferredTerminalWhenSurfaceIsMissing which verifies beforeSend fires, the queue gains exactly one item, and bytes matches the UTF-8 payload size

Sequence Diagram

sequenceDiagram
    participant Caller as Caller (AppDelegate)
    participant STTS as sendTextToTerminalSurface (static)
    participant TP as TerminalPanel
    participant TS as TerminalSurface
    participant Q as pendingSocketInputQueue

    Caller->>STTS: sendTextToTerminalSurface(text, in: workspace)
    STTS->>STTS: resolveTerminalPanelForTextSend()
    alt No terminal panel found
        STTS-->>Caller: return false
    else Terminal panel found
        STTS->>STTS: beforeSend?()
        STTS->>TP: sendText(text)
        TP->>TS: surface.sendText(text)
        alt surface != nil (live)
            TS->>TS: writeTextData(data, to: surface)
        else surface == nil (not ready)
            TS->>Q: enqueuePendingSocketInput(.text(data))
            TS->>TS: requestBackgroundSurfaceStartIfNeeded()
            Note over Q,TS: Queue flushed via flushPendingSocketInputIfNeeded() when surface attaches
        end
        STTS-->>Caller: return true
    end
Loading

Reviews (1): Last reviewed commit: "Use TerminalSurface queue for deferred t..." | Re-trigger Greptile

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
cmuxTests/ShortcutAndCommandPaletteTests.swift (1)

237-253: Use a payload above the large-text threshold.

This verifies missing-surface queueing, but the 21-byte payload does not cover the large text path this PR is targeting. Use a payload over 256 bytes so the regression protects deferred large inserts too. Based on learnings, prefer ghostty_surface_text for large socket text payloads; threshold is pasteTextThreshold = 256, while short text goes through sendTextEvent.

Suggested test adjustment
         var beforeSendCalled = false
-        let payload = "<button>Save</button>"
+        let payload = String(repeating: "<button>Save</button>", count: 20)
+        XCTAssertGreaterThan(payload.utf8.count, 256)
         XCTAssertTrue(
             AppDelegate.sendTextToTerminalSurface(
                 payload,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmuxTests/ShortcutAndCommandPaletteTests.swift` around lines 237 - 253, The
test uses a 21-byte payload so it never exercises the large-text path; change
the payload used in the AppDelegate.sendTextToTerminalSurface call to be larger
than pasteTextThreshold (256 bytes) and use a large-text style string such as
repeating "ghostty_surface_text" (or repeat "A") until payload.utf8.count > 256
so the code path that queues large socket text is exercised; keep the rest of
the assertions (beforeSendCalled,
terminalPanel.surface.debugPendingSocketInputSnapshot checks) but update
expected snapshot.bytes to match the new payload.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@cmuxTests/ShortcutAndCommandPaletteTests.swift`:
- Around line 237-253: The test uses a 21-byte payload so it never exercises the
large-text path; change the payload used in the
AppDelegate.sendTextToTerminalSurface call to be larger than pasteTextThreshold
(256 bytes) and use a large-text style string such as repeating
"ghostty_surface_text" (or repeat "A") until payload.utf8.count > 256 so the
code path that queues large socket text is exercised; keep the rest of the
assertions (beforeSendCalled,
terminalPanel.surface.debugPendingSocketInputSnapshot checks) but update
expected snapshot.bytes to match the new payload.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7b7ea453-6e8e-4c8d-8a59-73c8e8748ef9

📥 Commits

Reviewing files that changed from the base of the PR and between 7102fdf and 7b6be78.

📒 Files selected for processing (3)
  • Sources/AppDelegate.swift
  • Sources/GhosttyTerminalView.swift
  • cmuxTests/ShortcutAndCommandPaletteTests.swift

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7b6be78420

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Sources/AppDelegate.swift Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No issues found across 3 files

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/Panels/ReactGrab.swift`:
- Around line 87-267: The pasteback extractor can carry C0/C1 control scalars
into notifications; update the Swift-side sanitizer (the filtered(_:) routine
used by BrowserPickerMessageHandler and any pasteback handling) to strip all
control characters except \n and \t and also remove known
dangerousScalars/zero-width/BiDi characters (mirror TerminalController's
filtering logic), and ensure ReactGrabPastebackContentExtractor's fallback
content is passed through that sanitizer before notifying (references:
ReactGrabPastebackContentExtractor, invocationScript, installerSource,
filtered(_:), and TerminalController.dangerousScalars).
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4f17fd54-4dc7-4be2-b7b2-ce7cf7953924

📥 Commits

Reviewing files that changed from the base of the PR and between 7b6be78 and 7291ea2.

📒 Files selected for processing (6)
  • Sources/AppDelegate.swift
  • Sources/GhosttyTerminalView.swift
  • Sources/Panels/ReactGrab.swift
  • Sources/Panels/TerminalPanel.swift
  • cmuxTests/BrowserPanelTests.swift
  • cmuxTests/ShortcutAndCommandPaletteTests.swift
🚧 Files skipped from review as they are similar to previous changes (2)
  • Sources/GhosttyTerminalView.swift
  • Sources/AppDelegate.swift

Comment thread Sources/Panels/ReactGrab.swift

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7291ea2df3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Sources/Panels/ReactGrab.swift

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

2 issues found across 6 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="Sources/Panels/ReactGrab.swift">

<violation number="1" location="Sources/Panels/ReactGrab.swift:181">
P2: Consecutive identical blocks are being deduplicated, which can silently drop valid repeated content from pasteback output.</violation>

<violation number="2" location="Sources/Panels/ReactGrab.swift:200">
P1: Avoid passing PRE/CODE output through the whitespace normalizer; it collapses indentation and can corrupt pasted code snippets.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread Sources/Panels/ReactGrab.swift Outdated
Comment thread Sources/Panels/ReactGrab.swift Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/Panels/ReactGrab.swift`:
- Around line 188-192: The code currently falls back to element.href when
getAttribute('href') is falsy, causing empty href attributes to resolve to the
document URL; change the logic in the inline link handling (the block using
hasBlockChild, renderInline, textFromNode) to check element.hasAttribute('href')
first: if hasAttribute('href') use the raw getAttribute('href') value (even if
empty) and if not present then fall back to element.href, then treat an empty
string href as "no link" and return linkText instead of resolving to the
document URL.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 658dd5a6-0b68-43c2-b820-8a030a5e02e3

📥 Commits

Reviewing files that changed from the base of the PR and between 7291ea2 and 55f7c34.

📒 Files selected for processing (2)
  • Sources/Panels/ReactGrab.swift
  • cmuxTests/BrowserPanelTests.swift

Comment thread Sources/Panels/ReactGrab.swift

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
cmuxTests/BrowserPanelTests.swift (1)

392-501: Add one fallback-path regression for the extractor.

These runtime tests cover the formatted-content cases well, but none forces the fallbackContentLiteral branch when no readable blocks are produced. A small empty-elements case would protect the safe fallback behavior called out by this PR.

🧪 Optional fallback coverage
+    func testPastebackExtractorFallsBackWhenNoReadableElementsAreFound() async throws {
+        let panel = BrowserPanel(workspaceId: UUID())
+        let fallbackLiteral = try XCTUnwrap(cmuxJavaScriptStringLiteral("fallback\ntext"))
+
+        let result = try await panel.evaluateJavaScript(
+            """
+            \(ReactGrabPastebackContentExtractor.invocationScript(
+                elementsExpression: "[]",
+                fallbackContentLiteral: fallbackLiteral
+            ))
+            """
+        ) as? String
+
+        XCTAssertEqual(result, "fallback\ntext")
+    }

Based on learnings, tests in **/*Test*.swift should verify observable runtime behavior through executable paths rather than source-code shape.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmuxTests/BrowserPanelTests.swift` around lines 392 - 501, Add a regression
test that forces the extractor's fallback path by creating an element that
produces no readable blocks and asserting the returned string equals the
provided fallback; specifically add a new async test (e.g.,
testPastebackExtractorUsesFallbackWhenNoReadableBlocks) in
BrowserPanelTests.swift that constructs an empty target element HTML, obtains
htmlLiteral and a fallbackLiteral via cmuxJavaScriptStringLiteral, invokes
ReactGrabPastebackContentExtractor.invocationScript(elementsExpression:
"[document.getElementById('target')]", fallbackContentLiteral: fallbackLiteral)
through panel.evaluateJavaScript, and XCTAssertEqual the result to the
fallbackLiteral's unescaped value (the expected fallback string).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@cmuxTests/BrowserPanelTests.swift`:
- Around line 392-501: Add a regression test that forces the extractor's
fallback path by creating an element that produces no readable blocks and
asserting the returned string equals the provided fallback; specifically add a
new async test (e.g., testPastebackExtractorUsesFallbackWhenNoReadableBlocks) in
BrowserPanelTests.swift that constructs an empty target element HTML, obtains
htmlLiteral and a fallbackLiteral via cmuxJavaScriptStringLiteral, invokes
ReactGrabPastebackContentExtractor.invocationScript(elementsExpression:
"[document.getElementById('target')]", fallbackContentLiteral: fallbackLiteral)
through panel.evaluateJavaScript, and XCTAssertEqual the result to the
fallbackLiteral's unescaped value (the expected fallback string).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 657f61a2-eba9-48eb-96d0-c7cf6f30908f

📥 Commits

Reviewing files that changed from the base of the PR and between 55f7c34 and 004a2c5.

📒 Files selected for processing (2)
  • Sources/Panels/ReactGrab.swift
  • cmuxTests/BrowserPanelTests.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • Sources/Panels/ReactGrab.swift

@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 — 004a2c55 Deployed Apr 23, 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