Skip to content

Fix React Grab Cmd-Shift-G terminal round-trip - #2615

Merged
lawrencecchen merged 16 commits into
mainfrom
task-cmd-shift-g-react-grab-roundtrip
Apr 6, 2026
Merged

lawrencecchen merged 16 commits into
mainfrom
task-cmd-shift-g-react-grab-roundtrip

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Apr 6, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • make React Grab use Cmd+Shift+G and move terminal find previous off that chord
  • let Cmd+Shift+G from a terminal route to the only browser pane, start React Grab there, and remember the originating terminal for pasteback
  • after React Grab copies an element, refocus the original terminal and paste the copied content back, and document the shortcut behavior

Testing

  • xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux-unit -destination 'platform=macOS' -derivedDataPath /tmp/cmux-task-cmd-shift-g-react-grab-roundtrip-red test -only-testing:cmuxTests/BrowserDeveloperToolsShortcutDefaultsTests/testDefaultShortcutForToggleReactGrabUsesCommandShiftG (fails on commit 08d17ac8)
  • xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux-unit -destination 'platform=macOS' -derivedDataPath /tmp/cmux-task-cmd-shift-g-react-grab-roundtrip test -only-testing:cmuxTests/BrowserPanelReactGrabBridgeTests -only-testing:cmuxTests/ReactGrabShortcutRouteTests -only-testing:cmuxTests/BrowserDeveloperToolsShortcutDefaultsTests
  • ./scripts/reload.sh --tag task-cmd-shift-g-react-grab-roundtrip

Task

  • Plain-text task reference: "cmd shift g is not working. also i want to be able to press cmd shift g from a terminal. and then iff ther's a single browser pane open, then it will focus it and trigger react grab. then after user grabs an element, automatically refocus back to original terminal and trigger a paste of the grabbed element"

Summary by cubic

Remaps React Grab to Cmd+Shift+G and moves Terminal “Find Previous” to Option+Cmd+G. From a terminal, Cmd+Shift+G focuses the only browser pane, starts React Grab, then returns to the original terminal and pastes the copied selection.

  • New Features

    • Updates shortcuts; adds terminal find commands to the command palette and updates web/data/cmux-shortcuts.ts.
    • Routes Cmd+Shift+G from a terminal to the only browser, clears split zoom, focuses the WebView, activates React Grab, and saves the terminal for pasteback.
    • Adds a WebView bridge that emits state changes and copySuccess with a round‑trip token; posts reactGrabDidCopySelection so AppDelegate can refocus the exact terminal and paste.
  • Bug Fixes

    • Hardens pasteback: use the saved terminal target with no fallback, wait for the correct surface, track focus/first‑responder, strip invisible Unicode, and add timing logs; failed toggles beep.
    • Preserves the pasteback target across React Grab deactivation and page/URL changes; refreshes active round‑trip tokens so mid‑session copies still paste back, and hides session tokens from page scripts.
    • Handles deferred WebView focus by re‑requesting focus after activation.

Written for commit 483de84. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Terminal find controls added to the command palette (find, next, previous, hide).
    • Browser React Grab supports copy-from-webview with targeted return to a terminal panel.
  • Changes

    • Keyboard shortcuts updated: Find Previous → ⌥⌘G; Toggle React Grab → ⌘⇧G.
    • React Grab activation now routes and focuses the correct panel, with audible feedback on failure.
  • Bug Fixes

    • Improved focus/readiness and observer cleanup for more reliable delayed pasteback.
  • Tests

    • Added tests covering React Grab routing, bridge messages, and pasteback behaviors.

@vercel

vercel Bot commented Apr 6, 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 6, 2026 8:34am

@coderabbitai

coderabbitai Bot commented Apr 6, 2026 •

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds focus‑aware React Grab routing and typed JS bridge messages: browser panels can arm return‑to‑terminal round trips, post structured pasteback notifications, AppDelegate routes pasted content to a preferred terminal panel and waits for readiness; also swaps two shortcut defaults and adds tests for routing and bridge behavior.

Changes

Cohort / File(s) Summary
App delegate & pasteback routing
Sources/AppDelegate.swift, Sources/TerminalController.swift
Add NotificationCenter observer for .reactGrabDidCopySelection, new handleReactGrabDidCopySelection(_:), extend sendTextWhenReady to accept preferredPanelId, add resolveTerminalPanelForTextSend(...), and declare Notification.Name.reactGrabDidCopySelection.
React Grab core & routing
Sources/Panels/ReactGrab.swift, Sources/TabManager.swift
Introduce typed bridge messages (ReactGrabBridgeMessage), route/snapshot types and resolveReactGrabShortcutRoute, make bridge injection idempotent, add toggleReactGrabFromCurrentFocus() that routes and arms/clears round trips.
BrowserPanel focus & round‑trip
Sources/Panels/BrowserPanel.swift, Sources/Panels/BrowserPanelView.swift
Add pendingReactGrabReturnTargetPanelId, requestExplicitWebViewFocus(), arm/clear round‑trip APIs, handleReactGrabBridgeMessage, ensureReactGrabActive(), and call resetReactGrabState(preserveRoundTrip:reason:) / clear round‑trip before manual start.
Command palette & shortcuts
Sources/ContentView.swift, Sources/cmuxApp.swift, Sources/KeyboardShortcutSettings.swift, web/data/cmux-shortcuts.ts
Swap default bindings for toggleReactGrab and findPrevious, update hint/resolver and command handlers to call toggleReactGrabFromCurrentFocus() and play NSSound.beep() on failure; update web shortcut metadata.
Tests
cmuxTests/BrowserPanelTests.swift, cmuxTests/ShortcutAndCommandPaletteTests.swift, cmuxTests/BrowserConfigTests.swift
Add bridge round‑trip unit tests and routing tests; update default shortcut unit test expectation for toggleReactGrab.
UI glue / small updates
Sources/Panels/BrowserPanelView.swift, Sources/ContentView.swift
Adjust panel.resetReactGrabState(...) call site to preserve round trip on URL changes and clear round‑trip before manual toolbar toggle.

Sequence Diagram

sequenceDiagram
    participant User as User
    participant TM as TabManager
    participant BP as BrowserPanel
    participant AD as AppDelegate
    participant TP as TerminalPanel

    User->>TM: toggleReactGrabFromCurrentFocus()
    TM->>TM: resolveReactGrabShortcutRoute()
    TM->>BP: ensure focused browser & requestExplicitWebViewFocus()
    BP-->>TM: focus result (true/false)
    alt route includes terminal return
      TM->>BP: armReactGrabRoundTrip(returnTo: terminalId)
      TM->>BP: ensureReactGrabActive() / inject bridge
    else no terminal return
      TM->>BP: toggleOrInjectReactGrab()
    end
    BP->>BP: JS posts {type:'copySuccess', content}
    BP->>BP: handleReactGrabBridgeMessage(.copySuccess)
    BP->>AD: post .reactGrabDidCopySelection (workspace, browserId, returnPanelId, content)
    AD->>TM: resolve workspace & panels
    AD->>TP: focus target panel (preferredPanelId)
    AD->>TP: sendTextWhenReady(content, preferredPanelId)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I tunneled from web to shell and back with glee,
I arm a round‑trip, copy, post, then flee;
The bridge now speaks in types, precise and neat,
I hop your HTML home — pasteback complete!
Shortcuts swapped, I wink and tap my feet.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.44% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main change: implementing React Grab with Cmd+Shift+G keyboard shortcut including terminal round-trip functionality.
Description check ✅ Passed The PR description covers the main changes, testing approach, and task reference, but lacks details on manual verification and missing demo video.

✏️ 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-cmd-shift-g-react-grab-roundtrip

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.

@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: a84026f4b3

ℹ️ 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 12 files

You’re at about 96% of the monthly review limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.

@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: 3

🧹 Nitpick comments (2)
Sources/AppDelegate.swift (1)

6675-6676: Refocus the originating window before pasteback to complete round-trip UX.

focusTab(...) updates panel focus, but it doesn’t guarantee the source terminal window becomes key/frontmost. In cross-window React Grab flows, this can paste “silently” while the user still sees the browser window.

🔧 Proposed fix
-        manager.focusTab(workspaceId, surfaceId: returnPanelId, suppressFlash: true)
-        sendTextWhenReady(content, to: workspace)
+        if let originWindowId = windowId(for: manager) {
+            _ = focusMainWindow(windowId: originWindowId)
+        }
+        manager.focusTab(workspaceId, surfaceId: returnPanelId, suppressFlash: true)
+        sendTextWhenReady(content, to: workspace)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/AppDelegate.swift` around lines 6675 - 6676, After calling
manager.focusTab(workspaceId, surfaceId: returnPanelId, suppressFlash: true),
ensure the originating terminal/window is made key/frontmost before performing
the paste by activating the app/window and waiting for it to be key; then call
sendTextWhenReady(content, to: workspace). Concretely: after focusTab, invoke
the window-activation API (e.g., NSApp.activate(ignoringOtherApps: true) or the
existing manager/window helper that makes the workspace window key) and only
once the window is frontmost/key proceed to call sendTextWhenReady using the
same workspaceId/returnPanelId/content values to guarantee the paste targets the
intended window.
Sources/Panels/ReactGrab.swift (1)

139-151: Consider making type field required instead of defaulting to "stateChange".

Line 140 defaults missing type to "stateChange" for backward compatibility. If all bridge messages now include an explicit type (per the updated JS on lines 259, 262), consider returning nil when type is absent to catch malformed messages early.

🔧 Optional: Strict type requirement
     init?(body: [String: Any]) {
-        let type = body["type"] as? String ?? "stateChange"
+        guard let type = body["type"] as? String else { return nil }
         switch type {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Panels/ReactGrab.swift` around lines 139 - 151, The initializer
init?(body:) currently defaults type to "stateChange" which masks missing/
malformed messages; change it to require the "type" key by guarding let type =
body["type"] as? String else { return nil } and then switch on that type string
to construct the enum cases .stateChange(isActive:) and .copySuccess(content:),
returning nil for unknown types—this ensures absent or malformed "type" values
are rejected rather than silently treated as stateChange.
🤖 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/BrowserPanel.swift`:
- Around line 3277-3308: The method requestExplicitWebViewFocus() currently
calls suppressOmnibarAutofocus(for:) before confirming focus success, allowing a
stale suppression window when the method returns false; change the flow so that
endSuppressWebViewFocusForAddressBar() and clearWebViewFocusSuppression()
remain, but only call suppressOmnibarAutofocus(for:) after focus actually
succeeds (both the immediate window.makeFirstResponder(webView) path and the
async retry path where noteWebViewFocused() is invoked), ensuring suppression is
only applied when noteWebViewFocused()/window.makeFirstResponder(webView) have
succeeded.

In `@Sources/Panels/ReactGrab.swift`:
- Around line 208-229: In handleReactGrabBridgeMessage, the .copySuccess branch
posts raw HTML content (content) to the ReactGrab pasteback notification, which
may include BiDi/zero-width characters; before sending, sanitize the string by
passing it through the same filtered(_:) sanitizer used in
BrowserPickerMessageHandler (or equivalent) and use the filtered result in the
NotificationCenter.post userInfo for ReactGrabPastebackNotificationKey.content;
keep the rest of the logic (clearing round trip, using
pendingReactGrabReturnTargetPanelId and other keys) intact.

In `@Sources/TabManager.swift`:
- Around line 3345-3352: Before calling workspace.focusPanel(browserPanel.id)
save the current focused panel id (workspace.focusedPanelId) into a local
variable, then attempt to focus the browser and call
browserPanel.requestExplicitWebViewFocus(); if that call fails (the guard
branch) call browserPanel.clearReactGrabRoundTrip() and then restore the prior
focus by calling workspace.focusPanel(previousFocusedId) (only if
previousFocusedId is non-nil and different from browserPanel.id) before
returning false so focus isn't left changed on failure.

---

Nitpick comments:
In `@Sources/AppDelegate.swift`:
- Around line 6675-6676: After calling manager.focusTab(workspaceId, surfaceId:
returnPanelId, suppressFlash: true), ensure the originating terminal/window is
made key/frontmost before performing the paste by activating the app/window and
waiting for it to be key; then call sendTextWhenReady(content, to: workspace).
Concretely: after focusTab, invoke the window-activation API (e.g.,
NSApp.activate(ignoringOtherApps: true) or the existing manager/window helper
that makes the workspace window key) and only once the window is frontmost/key
proceed to call sendTextWhenReady using the same
workspaceId/returnPanelId/content values to guarantee the paste targets the
intended window.

In `@Sources/Panels/ReactGrab.swift`:
- Around line 139-151: The initializer init?(body:) currently defaults type to
"stateChange" which masks missing/ malformed messages; change it to require the
"type" key by guarding let type = body["type"] as? String else { return nil }
and then switch on that type string to construct the enum cases
.stateChange(isActive:) and .copySuccess(content:), returning nil for unknown
types—this ensures absent or malformed "type" values are rejected rather than
silently treated as stateChange.
🪄 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: 9261935c-d5bc-4fe1-9a78-bc21369e0faa

📥 Commits

Reviewing files that changed from the base of the PR and between 97af97b and a84026f.

📒 Files selected for processing (12)
  • Sources/AppDelegate.swift
  • Sources/ContentView.swift
  • Sources/KeyboardShortcutSettings.swift
  • Sources/Panels/BrowserPanel.swift
  • Sources/Panels/ReactGrab.swift
  • Sources/TabManager.swift
  • Sources/TerminalController.swift
  • Sources/cmuxApp.swift
  • cmuxTests/BrowserConfigTests.swift
  • cmuxTests/BrowserPanelTests.swift
  • cmuxTests/ShortcutAndCommandPaletteTests.swift
  • web/data/cmux-shortcuts.ts

Comment thread Sources/Panels/BrowserPanel.swift
Comment thread Sources/Panels/ReactGrab.swift
Comment thread Sources/TabManager.swift Outdated
@greptile-apps

greptile-apps Bot commented Apr 6, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Remaps React Grab to ⌘⇧G (moving Terminal Find Previous to ⌥⌘G) and adds a terminal→browser round-trip: Cmd+Shift+G from a focused terminal focuses the sole browser pane, activates React Grab, and after copySuccess refocuses the original terminal and pastes back the copied content.

  • P1 — silent content drop: handleReactGrabDidCopySelection calls focusTab(…, surfaceId: returnPanelId, …) then immediately sendTextWhenReady(content, to: workspace), which resolves the destination via tab.focusedTerminalPanel rather than returnPanelId. If focus state lags, the content enters a retry loop that only wakes on panel or surface-ready changes — not on focus changes — and is silently dropped after 3 s. Targeting workspace.terminalPanel(for: returnPanelId) directly is more robust.

Confidence Score: 4/5

Mostly safe to merge; one P1 concern around sendTextWhenReady silently dropping content if focus state lags after focusTab

The feature works correctly in the common synchronous path and has solid unit test coverage. The P1 concern is that sendTextWhenReady uses focusedTerminalPanel instead of returnPanelId directly — if focus hasn't converged when the ready-poll fallback runs, the content is silently dropped. All other changes are clean, consistent, and well-tested.

Sources/AppDelegate.swift — the handleReactGrabDidCopySelection handler

Important Files Changed

Filename Overview
Sources/AppDelegate.swift Adds handleReactGrabDidCopySelection observer/handler; uses sendTextWhenReady which targets focusedTerminalPanel (focus-dependent) rather than returnPanelId directly
Sources/Panels/ReactGrab.swift Introduces ReactGrabBridgeMessage enum, round-trip arm/clear state, handleReactGrabBridgeMessage, ensureReactGrabActive, and refactored JS bridge with deduplication guard and onCopySuccess hook
Sources/TabManager.swift Replaces toggleReactGrabFocusedBrowser with toggleReactGrabFromCurrentFocus that resolves terminal→browser route, arms round-trip, focuses browser, and dispatches inject/toggle asynchronously
Sources/Panels/BrowserPanel.swift Adds pendingReactGrabReturnTargetPanelId storage and requestExplicitWebViewFocus helper that suppresses omnibar auto-focus competition before programmatic WebView focus
Sources/KeyboardShortcutSettings.swift Swaps defaults: findPrevious ← ⌥⌘G, toggleReactGrab ← ⌘⇧G, resolving the chord collision
Sources/ContentView.swift Updates Find Previous shortcut hint to ⌥⌘G; adds missing command→action mappings for terminal find commands in the command palette
Sources/TerminalController.swift Registers the .reactGrabDidCopySelection notification name alongside existing app-wide notification names
Sources/cmuxApp.swift Updates menu button to call toggleReactGrabFromCurrentFocus with NSSound.beep() fallback, consistent with other browser menu actions
cmuxTests/BrowserPanelTests.swift Adds runtime behavioral tests for bridge message handler and round-trip state machine through real BrowserPanel instances
cmuxTests/ShortcutAndCommandPaletteTests.swift Adds four unit tests for resolveReactGrabShortcutRoute covering all routing cases
cmuxTests/BrowserConfigTests.swift Updates test name and assertions to verify ⌘⇧G default for toggleReactGrab via the runtime defaultShortcut property
web/data/cmux-shortcuts.ts Updates shortcut combos and adds bilingual note describing context-sensitive routing; updates findPrevious to ⌥⌘G

Sequence Diagram

sequenceDiagram
    participant User
    participant AppDelegate
    participant TabManager
    participant BrowserPanel
    participant ReactGrabJS as React Grab (JS)
    participant Terminal

    User->>AppDelegate: Cmd+Shift+G (terminal focused)
    AppDelegate->>TabManager: toggleReactGrabFromCurrentFocus()
    TabManager->>TabManager: resolveReactGrabShortcutRoute(panels)
    TabManager->>BrowserPanel: armReactGrabRoundTrip(returnTo: terminalId)
    TabManager->>BrowserPanel: requestExplicitWebViewFocus()
    BrowserPanel-->>TabManager: true
    TabManager->>BrowserPanel: ensureReactGrabActive() [async Task]
    BrowserPanel->>ReactGrabJS: evaluateJavaScript(inject + activate)
    ReactGrabJS-->>BrowserPanel: stateChange(isActive: true)
    User->>ReactGrabJS: Selects element
    ReactGrabJS-->>BrowserPanel: copySuccess(content)
    BrowserPanel->>BrowserPanel: clearReactGrabRoundTrip()
    BrowserPanel->>AppDelegate: post .reactGrabDidCopySelection
    AppDelegate->>TabManager: focusTab(workspaceId, surfaceId: returnPanelId)
    AppDelegate->>Terminal: sendTextWhenReady(content)
    Terminal-->>User: Pasted content
Loading

Reviews (1): Last reviewed commit: "fix React Grab terminal round-trip" | Re-trigger Greptile

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.

1 issue found across 2 files (changes from recent commits).

You’re at about 96% of the monthly review limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.

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/AppDelegate.swift">

<violation number="1" location="Sources/AppDelegate.swift:6684">
P2: When a preferred return panel is provided, falling back to `focusedTerminalPanel` can paste React Grab content into the wrong terminal. If the preferred panel is unavailable, this path should fail instead of rerouting.</violation>
</file>

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

Comment thread Sources/AppDelegate.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: 45457947d1

ℹ️ 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 Outdated

@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

guard let scriptSource = await ReactGrabScriptLoader.fetch() else {
#if DEBUG
dlog("reactGrab.inject.fetchFailed")
#endif
return

P2 Badge Clear armed pasteback target when React Grab injection fails

When Cmd+Shift+G is started from a terminal, pendingReactGrabReturnTargetPanelId is armed before injection, but this failure path returns early without clearing it. If script fetch fails (e.g., offline/hash mismatch) or injection previously armed the target and never became active, the stale target can persist and a later copySuccess from a browser-triggered React Grab session will paste into an old terminal unexpectedly. Clear the pending round-trip target on injection failure (and similarly on eval failure) so failed launches cannot leave latent pasteback state behind.

ℹ️ 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".

@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

♻️ Duplicate comments (2)
Sources/Panels/ReactGrab.swift (2)

254-262: ⚠️ Potential issue | 🟡 Minor

Filter invisible scalars out of the pasteback payload.

content is still forwarded verbatim in the notification payload. Even after authenticating the bridge, strip BiDi override and zero-width characters here so downstream pasteback never sees visually deceptive input.

Based on learnings, this repo already filters web-derived text with a shared dangerousScalars pattern before publishing it.

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

In `@Sources/Panels/ReactGrab.swift` around lines 254 - 262, The pasteback payload
currently forwards `content` verbatim in the NotificationCenter.post call;
sanitize it first by stripping BiDi override and zero-width characters using the
repository's shared dangerousScalars pattern (the same filter used for
web-derived text) and use the sanitized value instead of `content` when
populating ReactGrabPastebackNotificationKey.content; update the code around the
NotificationCenter.post in ReactGrab.swift (the block referencing workspaceId,
id, returnPanelId, and content) to compute a `sanitizedContent` and pass that
into the userInfo payload.

161-166: ⚠️ Potential issue | 🔴 Critical

Gate copySuccess messages on recent native input, and sanitize content before pasteback.

JavaScript in the injected react-grab bridge can forge copySuccess messages at any time. When pendingReactGrabReturnTargetPanelId is armed, the handler immediately posts the untrusted content to the pasteback notification chain without any native-input gating or content filtering.

Unlike BrowserPickerMessageHandler (which gates via NSEvent.addLocalMonitorForEvents and sanitizes web text for BiDi overrides, zero-width characters, and control characters), ReactGrab applies neither safeguard:

  • Trust boundary: Add a one-shot timestamp check similar to BrowserPickerMessageHandler—record the arm time and reject messages older than 1.0s.
  • Content sanitization: Filter content for dangerous scalars before posting the notification. Apply the same filtered(_:) + prefix(200) pattern used in other web-to-native boundaries.

Also applies to: 234–262

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

In `@Sources/Panels/ReactGrab.swift` around lines 161 - 166, The ReactGrab bridge
currently accepts untrusted copySuccess messages immediately; add a one-shot
native-input gating check and sanitize the content before posting pasteback
notifications: in userContentController(_:didReceive:) where you parse
ReactGrabBridgeMessage, record and check a one-shot timestamp (e.g.,
pendingReactGrabReturnArmedAt) and reject any copySuccess messages older than
1.0s, and before posting the notification for
pendingReactGrabReturnTargetPanelId sanitize the message content using the
existing filtered(_:) helper and then limit to prefix(200); apply the same
timestamp gating and filtered(_:) + prefix(200) sanitization to the other
copySuccess handling block in the file (the block around lines 234–262) so all
web-to-native pastebacks use the same defenses.
🤖 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/AppDelegate.swift`:
- Around line 6732-6738: The function resolveTerminalPanelForTextSend currently
falls back to tab.focusedTerminalPanel even when a preferredPanelId was supplied
but no matching panel exists; change its logic so that when a preferredPanelId
is provided you attempt tab.terminalPanel(for: preferredPanelId) and if that
returns nil you return nil (do not fall back), and only use
tab.focusedTerminalPanel when preferredPanelId is nil; update the
resolveTerminalPanelForTextSend implementation accordingly to enforce this
behavior.

---

Duplicate comments:
In `@Sources/Panels/ReactGrab.swift`:
- Around line 254-262: The pasteback payload currently forwards `content`
verbatim in the NotificationCenter.post call; sanitize it first by stripping
BiDi override and zero-width characters using the repository's shared
dangerousScalars pattern (the same filter used for web-derived text) and use the
sanitized value instead of `content` when populating
ReactGrabPastebackNotificationKey.content; update the code around the
NotificationCenter.post in ReactGrab.swift (the block referencing workspaceId,
id, returnPanelId, and content) to compute a `sanitizedContent` and pass that
into the userInfo payload.
- Around line 161-166: The ReactGrab bridge currently accepts untrusted
copySuccess messages immediately; add a one-shot native-input gating check and
sanitize the content before posting pasteback notifications: in
userContentController(_:didReceive:) where you parse ReactGrabBridgeMessage,
record and check a one-shot timestamp (e.g., pendingReactGrabReturnArmedAt) and
reject any copySuccess messages older than 1.0s, and before posting the
notification for pendingReactGrabReturnTargetPanelId sanitize the message
content using the existing filtered(_:) helper and then limit to prefix(200);
apply the same timestamp gating and filtered(_:) + prefix(200) sanitization to
the other copySuccess handling block in the file (the block around lines
234–262) so all web-to-native pastebacks use the same defenses.
🪄 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: ed5d4c23-ebfd-4f60-a080-69b03c8d4552

📥 Commits

Reviewing files that changed from the base of the PR and between 4545794 and 5eb2016.

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

Comment thread Sources/AppDelegate.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.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Sources/Panels/ReactGrab.swift (1)

279-313: ⚠️ Potential issue | 🟠 Major

Don't re-evaluate the react-grab bundle on every re-activation.

toggleOrInjectReactGrab() and ensureReactGrabActive() both route back through injectReactGrab() whenever isReactGrabActive is false. In injectReactGrab(), the JavaScript IIFE checks for an existing window.__REACT_GRAB__ API and returns early (line 313) if already installed, but scriptSource is appended outside the IIFE (line 323), meaning the third-party bundle is always re-evaluated regardless of the fast path. Each re-activation re-runs the bundle unnecessarily and risks side effects if the bundle is not fully idempotent. Move scriptSource evaluation inside the IIFE so it only runs on first install, and invoke only activate() on subsequent toggles.

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

In `@Sources/Panels/ReactGrab.swift` around lines 279 - 313, The IIFE in
injectReactGrab() currently returns early when window.__REACT_GRAB__ exists but
scriptSource is appended outside the IIFE so the third‑party bundle is
re-evaluated on every toggle; to fix, move the evaluation of scriptSource inside
the IIFE and only append/execute it when installing the bridge (i.e., before or
as part of installBridge when window.__REACT_GRAB__ is absent), leaving the
early-return path to only call window.__REACT_GRAB__.activate() on subsequent
calls from toggleOrInjectReactGrab() / ensureReactGrabActive(); refer to
injectReactGrab(), window.__REACT_GRAB__, scriptSource, installBridge and
activate() to locate and update the logic.
🤖 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/BrowserPanelView.swift`:
- Around line 825-826: The code currently calls panel.clearReactGrabRoundTrip()
immediately before Task { await panel.toggleOrInjectReactGrab() }, which can
erase a round-trip target armed via armReactGrabRoundTrip(returnTo:) and cause
copySuccess to have no return panel; change the flow so you do not clear the
round-trip state before toggleOrInjectReactGrab() runs—either remove the
unconditional call to panel.clearReactGrabRoundTrip() here and let
toggleOrInjectReactGrab() clear it when appropriate, or move the clear into the
completion path inside toggleOrInjectReactGrab() (or after confirming no return
panel is needed) so that armReactGrabRoundTrip(returnTo:) remains valid and
copySuccess/pasteback can find the return panel.

In `@Sources/Panels/ReactGrab.swift`:
- Around line 216-250: The pendingReactGrabReturnTargetPanelId is not being
cleared on canceled grabs so stale return targets can be used later; update
handleReactGrabBridgeMessage (and the ReactGrabBridgeMessage enum if needed) to
explicitly clear/disarm pendingReactGrabReturnTargetPanelId on a cancel pathway
(e.g., when receiving a cancel message or when stateChange becomes false), and
also ensure any browser-only activation path clears stale
pendingReactGrabReturnTargetPanelId before arming a new target (use
clearReactGrabRoundTrip() where appropriate); add a regression test that
simulates a canceled terminal-launched grab followed by a browser-initiated copy
to assert no stale paste occurs.

---

Outside diff comments:
In `@Sources/Panels/ReactGrab.swift`:
- Around line 279-313: The IIFE in injectReactGrab() currently returns early
when window.__REACT_GRAB__ exists but scriptSource is appended outside the IIFE
so the third‑party bundle is re-evaluated on every toggle; to fix, move the
evaluation of scriptSource inside the IIFE and only append/execute it when
installing the bridge (i.e., before or as part of installBridge when
window.__REACT_GRAB__ is absent), leaving the early-return path to only call
window.__REACT_GRAB__.activate() on subsequent calls from
toggleOrInjectReactGrab() / ensureReactGrabActive(); refer to injectReactGrab(),
window.__REACT_GRAB__, scriptSource, installBridge and activate() to locate and
update the logic.
🪄 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: 8be353b2-7ed3-4ed2-9385-02728b28d77d

📥 Commits

Reviewing files that changed from the base of the PR and between 5eb2016 and 489fb0c.

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

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

@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: c28b8a0d91

ℹ️ 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

@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

♻️ Duplicate comments (1)
Sources/Panels/ReactGrab.swift (1)

242-270: ⚠️ Potential issue | 🟡 Minor

Sanitize web-derived pasteback content before posting notification.

copySuccess currently forwards raw content into pasteback. Please apply the same invisible-scalar filtering pattern used elsewhere for web-originated text before posting.

🔧 Proposed fix
 extension BrowserPanel {
+    private func sanitizeReactGrabPastebackContent(_ input: String) -> String {
+        let dangerousScalars: Set<UInt32> = [
+            0x202A, 0x202B, 0x202C, 0x202D, 0x202E, 0x2066, 0x2067, 0x2068, 0x2069,
+            0x200B, 0x200C, 0x200D, 0x200E, 0x200F, 0xFEFF,
+        ]
+        let filteredScalars = input.unicodeScalars.filter { scalar in
+            let v = scalar.value
+            if dangerousScalars.contains(v) { return false }
+            return !CharacterSet.controlCharacters.contains(scalar)
+        }
+        return String(String.UnicodeScalarView(filteredScalars))
+    }
+
     func handleReactGrabBridgeMessage(_ message: ReactGrabBridgeMessage) {
         switch message {
         case .stateChange(let isActive):
             isReactGrabActive = isActive
@@
         case .copySuccess(let content):
             guard let returnPanelId = pendingReactGrabReturnTargetPanelId else {
@@
             clearReactGrabRoundTrip(reason: "copySuccess")
+            let sanitizedContent = sanitizeReactGrabPastebackContent(content)
             NotificationCenter.default.post(
                 name: .reactGrabDidCopySelection,
                 object: nil,
                 userInfo: [
                     ReactGrabPastebackNotificationKey.workspaceId: workspaceId,
                     ReactGrabPastebackNotificationKey.browserPanelId: id,
                     ReactGrabPastebackNotificationKey.returnPanelId: returnPanelId,
-                    ReactGrabPastebackNotificationKey.content: content,
+                    ReactGrabPastebackNotificationKey.content: sanitizedContent,
                 ]
             )
         }
     }
 }

Based on learnings: “BrowserPickerMessageHandler uses dangerousScalars filtering for web-derived text (sanitizeWebText/filtered(_)) to strip BiDi/zero-width/control hazards.”

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

In `@Sources/Panels/ReactGrab.swift` around lines 242 - 270, The .copySuccess
branch forwards raw web-derived content into the pasteback notification; run the
content through the existing web-sanitization helper (e.g., sanitizeWebText(...)
or the filtered(_:) call that applies dangerousScalars filtering) before using
it in debug logs, calling clearReactGrabRoundTrip, or populating
NotificationCenter userInfo (ReactGrabPastebackNotificationKey.content). Update
the code in the case .copySuccess (around pendingReactGrabReturnTargetPanelId
handling) to replace raw content with the sanitized result so all downstream
consumers receive the filtered text.
🤖 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/TabManager.swift`:
- Around line 3345-3349: The code focuses the routed browser panel then
immediately calls browserPanel.requestExplicitWebViewFocus(), but if the browser
is in a hidden split-zoomed pane the panel must be revealed first; insert a call
to clear the split-zoom before focusing. Specifically, before calling
workspace.focusPanel(browserPanel.id) (and prior to
browserPanel.requestExplicitWebViewFocus()), invoke the split-zoom clearing
helper (e.g., workspace.clearSplitZoom()) so the browser pane is unhidden, then
call workspace.focusPanel(browserPanel.id) and then
browserPanel.requestExplicitWebViewFocus().

---

Duplicate comments:
In `@Sources/Panels/ReactGrab.swift`:
- Around line 242-270: The .copySuccess branch forwards raw web-derived content
into the pasteback notification; run the content through the existing
web-sanitization helper (e.g., sanitizeWebText(...) or the filtered(_:) call
that applies dangerousScalars filtering) before using it in debug logs, calling
clearReactGrabRoundTrip, or populating NotificationCenter userInfo
(ReactGrabPastebackNotificationKey.content). Update the code in the case
.copySuccess (around pendingReactGrabReturnTargetPanelId handling) to replace
raw content with the sanitized result so all downstream consumers receive the
filtered text.
🪄 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: ff00101e-b0e6-43d2-a30a-91e1b5e1126f

📥 Commits

Reviewing files that changed from the base of the PR and between 489fb0c and c28b8a0.

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

Comment thread Sources/TabManager.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: 430884bec4

ℹ️ 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 commented Apr 6, 2026

Copy link
Copy Markdown

This review could not be run because your cubic account has exceeded the monthly review limit. If you need help restoring access, please contact contact@cubic.dev.

@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: 4d64aed8ad

ℹ️ 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 Outdated
@lawrencecchen
lawrencecchen merged commit 145d109 into main Apr 6, 2026
17 of 18 checks passed
@lawrencecchen
lawrencecchen deleted the task-cmd-shift-g-react-grab-roundtrip branch April 6, 2026 08:37

@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: 483de84e26

ℹ️ 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 on lines +353 to +357
try {
Object.defineProperty(window, updaterName, {
value: syncSessionToken,
writable: false,
configurable: false,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Install React Grab bridge without page-patched intrinsics

Fresh evidence for the pasteback-auth issue: this bridge is installed in page JS context and calls Object.defineProperty(window, updaterName, ...) before refreshSessionToken(), so a malicious page can monkey-patch Object.defineProperty first, wrap syncSessionToken, and capture the native session token when it is synced. With that token, page JS can forge cmuxReactGrab copySuccess messages and force arbitrary text into the armed terminal.

Useful? React with 👍 / 👎.

Comment on lines +56 to +58
let browserPanels = panels.filter { $0.panelType == .browser }
guard browserPanels.count == 1, let browserPanel = browserPanels.first else {
return nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Route terminal shortcut by browser pane, not browser tab count

The terminal path currently requires exactly one browser panel in resolveReactGrabShortcutRoute, but the caller builds snapshots from workspace.panels.values (all tabs), so a workspace with one browser pane containing multiple browser tabs is treated as ambiguous and Cmd+Shift+G from terminal wrongly fails. This contradicts the intended "only browser pane" behavior and blocks valid round-trip routing.

Useful? React with 👍 / 👎.

This branch was successfully deployed

1 active deployment
Preview — 483de84e Deployed Apr 6, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant