Skip to content

fix(terminal): use Electron clipboard IPC for Ctrl+V paste on Windows#2008

Open
octo-patch wants to merge 2 commits intoAndyMik90:developfrom
octo-patch:fix/issue-1911-windows-clipboard-paste
Open

fix(terminal): use Electron clipboard IPC for Ctrl+V paste on Windows#2008
octo-patch wants to merge 2 commits intoAndyMik90:developfrom
octo-patch:fix/issue-1911-windows-clipboard-paste

Conversation

@octo-patch
Copy link
Copy Markdown

@octo-patch octo-patch commented Apr 12, 2026

Fixes #1911

Problem

On Windows, pressing Ctrl+V in the agent terminal briefly shows the pasted text and then clears it. This happens because navigator.clipboard.readText() in Electron's renderer process requires the clipboard-read permission, which can be silently denied on Windows. The text that briefly appears comes from xterm.js's native paste event handler, which fires independently of our keyboard event handler. Our async clipboard read either fails silently or races with xterm's internal handling, causing the paste to vanish.

Solution

Add a TERMINAL_READ_CLIPBOARD IPC channel that reads clipboard text from the Electron main process using electron.clipboard.readText(). The main process has unconditional clipboard access on all platforms without requiring browser-level permissions.

Changes:

  • ipc.ts: Add TERMINAL_READ_CLIPBOARD channel constant
  • terminal-handlers.ts: Register the IPC handler using electron.clipboard
  • terminal-api.ts: Expose readClipboardText() in the TerminalAPI preload interface
  • useXterm.ts: Use window.electronAPI.readClipboardText() with a fallback to navigator.clipboard.readText()

Testing

  • Ctrl+V in the terminal now pastes clipboard content reliably on Windows
  • Ctrl+Shift+V on Linux is unaffected (separate code path)
  • CMD+V on macOS is unaffected (handled by xterm natively)
  • Fallback to navigator.clipboard ensures the fix doesn't break non-Electron web contexts

Summary by CodeRabbit

New Features

  • Clipboard Integration: Clipboard paste operations now processed through the application with intelligent fallback support, improving reliability and error handling
  • Automated PR Creation: Enhanced workflow with automatic change staging, commit creation, and validation to ensure modifications are properly captured before pushing to the repository

octo-patch and others added 2 commits April 11, 2026 10:00
…tion (fixes AndyMik90#1928)

When the agent writes files to the worktree but does not call `git commit`,
the subsequent `gh pr create` fails with a cryptic GraphQL error:
'Head sha can\'t be blank, Base sha can\'t be blank, No commits between
main and <branch>'.

Fix:
- Auto-stage and commit any uncommitted changes in the worktree before
  pushing (handles agents that skip the commit step)
- After pushing, verify at least one commit exists ahead of the base branch
  and return a clear, actionable error message if none are found
…ndyMik90#1911)

On Windows, navigator.clipboard.readText() in the Electron renderer may be
silently denied due to the clipboard-read permission not being granted.
This caused Ctrl+V in the agent terminal to briefly show pasted text and
then clear it, with no feedback to the user.

Add a TERMINAL_READ_CLIPBOARD IPC channel that reads clipboard text from the
main process using Electron's clipboard module, which has full clipboard access
without renderer permission restrictions. The renderer falls back to
navigator.clipboard.readText() if the IPC call fails.
@CLAassistant
Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 12, 2026

📝 Walkthrough

Walkthrough

This pull request adds clipboard text reading via IPC for the renderer process and updates PR creation logic with auto-commit functionality and commit count validation. Changes span the main process IPC handlers, preload API, renderer paste handling, shared IPC constants, and PR creation workflow.

Changes

Cohort / File(s) Summary
Clipboard IPC Infrastructure
apps/desktop/src/shared/constants/ipc.ts, apps/desktop/src/main/ipc-handlers/terminal-handlers.ts, apps/desktop/src/preload/api/terminal-api.ts
Added TERMINAL_READ_CLIPBOARD IPC channel constant, implemented main process handler to return clipboard text via clipboard.readText(), and exposed readClipboardText() method in TerminalAPI interface.
Terminal Paste Handling
apps/desktop/src/renderer/components/terminal/useXterm.ts
Updated paste logic to use IPC-based clipboard reading as primary path with fallback to navigator.clipboard.readText() on failure; maintains existing 1MB truncation behavior.
PR Creation Workflow
apps/desktop/src/main/ai/runners/github/pr-creator.ts
Added autoCommitWorktreeChanges() function to stage and commit worktree changes, introduced early effectiveBase computation, and added post-push commit count validation to guard against empty PRs.

Sequence Diagram

sequenceDiagram
    participant Renderer as Renderer Process
    participant Preload as Preload Bridge
    participant Main as Main Process
    participant Clipboard as System Clipboard
    participant Navigator as Navigator API

    Renderer->>Renderer: Paste event triggered
    Renderer->>Preload: readClipboardText()
    Preload->>Main: IPC invoke (TERMINAL_READ_CLIPBOARD)
    Main->>Clipboard: clipboard.readText()
    alt Success
        Clipboard-->>Main: text
        Main-->>Preload: text
        Preload-->>Renderer: Promise resolves
        Renderer->>Renderer: Paste text to terminal
    else IPC Fails
        Main-->>Preload: error
        Preload-->>Renderer: Promise rejects
        Renderer->>Navigator: fallback navigator.clipboard.readText()
        Navigator-->>Renderer: text
        Renderer->>Renderer: Paste text to terminal
    end
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested Labels

bug, area/frontend, size/M

Poem

🐰 Clipboards now hop through the IPC wire,
No more paste-and-vanish on Windows' desire!
Main process reads where renderer cannot see,
A fallback awaits if IPC disagrees,
Paste persists—automation's delight! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR includes out-of-scope changes: the autoCommitWorktreeChanges() function and pre-PR validation logic in pr-creator.ts are unrelated to the clipboard/paste fix. Remove the autoCommitWorktreeChanges() changes and PR validation logic from pr-creator.ts as they are not part of issue #1911; preserve only the clipboard IPC fixes across the other four files.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: using Electron clipboard IPC to fix Ctrl+V paste on Windows, matching the primary objective across multiple modified files.
Linked Issues check ✅ Passed The PR successfully addresses issue #1911 by implementing the TERMINAL_READ_CLIPBOARD IPC channel and clipboard-read fallback mechanism to resolve the Windows Ctrl+V paste issue.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

navigator.clipboard.readText()
// Use Electron's main-process clipboard API via IPC, which is reliable on all
// platforms including Windows (where navigator.clipboard.readText() may silently
// fail due to renderer permission restrictions, causing paste to appear and vanish).
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: A synchronous TypeError occurs if window.electronAPI is undefined, preventing the fallback to navigator.clipboard and breaking paste functionality in non-Electron contexts.
Severity: HIGH

Suggested Fix

Use optional chaining (?.) on window.electronAPI when calling readClipboardText(). This will prevent a synchronous error and allow the .catch() block to execute the fallback logic as intended.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: apps/desktop/src/renderer/components/terminal/useXterm.ts#L174

Potential issue: In non-Electron contexts where `window.electronAPI` is `undefined`, the
direct property access in `window.electronAPI.readClipboardText()` will throw a
synchronous `TypeError`. This error is not handled by the `.catch()` block, which only
catches promise rejections. As a result, the fallback logic that uses
`navigator.clipboard.readText()` is unreachable. This breaks the clipboard paste
functionality in any environment where the Electron IPC API is not available, which
contradicts the stated goal of supporting non-Electron web contexts.

Did we get this right? 👍 / 👎 to inform future reviews.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces automatic staging and committing of uncommitted changes during the PR creation process and adds a check to verify that the branch is ahead of the base. It also implements a main-process IPC handler for clipboard access to resolve permission issues on Windows. Feedback was provided to improve the robustness of the commit count check by handling cases where the remote is not named 'origin'.

Comment on lines +372 to +388
try {
const commitCount = execFileSync(
gitPath,
['rev-list', '--count', `origin/${effectiveBase}..HEAD`],
{ cwd: worktreePath, encoding: 'utf-8', stdio: 'pipe' },
).trim();
if (commitCount === '0') {
return {
success: false,
error: `No commits found between '${effectiveBase}' and '${branchName}'. ` +
'The agent may not have committed any changes. ' +
'Please verify the implementation completed successfully before creating a PR.',
};
}
} catch {
// Unable to count commits — proceed and let `gh pr create` surface the error
}
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.

medium

The rev-list check assumes the remote is always named origin and that the base branch can be reached via origin/${effectiveBase}. This will fail if the remote has a different name (e.g., upstream) or if the base branch already includes a remote prefix.

Consider using a fallback pattern similar to the one used in gatherPRContext to improve robustness.

  try {
    let commitCount: string;
    try {
      commitCount = execFileSync(
        gitPath,
        ['rev-list', '--count', `origin/${effectiveBase}..HEAD`],
        { cwd: worktreePath, encoding: 'utf-8', stdio: 'pipe' },
      ).trim();
    } catch {
      // Fallback to using the base branch directly if origin/ prefix fails
      commitCount = execFileSync(
        gitPath,
        ['rev-list', '--count', `${baseBranch}..HEAD`],
        { cwd: worktreePath, encoding: 'utf-8', stdio: 'pipe' },
      ).trim();
    }

    if (commitCount === '0') {
      return {
        success: false,
        error: `No commits found between '${effectiveBase}' and '${branchName}'. ` +
          'The agent may not have committed any changes. ' +
          'Please verify the implementation completed successfully before creating a PR.',
      };
    }
  } catch {
    // Unable to count commits — proceed and let `gh pr create` surface the error
  }

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

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)
apps/desktop/src/main/ai/runners/github/pr-creator.ts (1)

391-398: 🧹 Nitpick | 🔵 Trivial

Use effectiveBase consistently for context generation and PR prompt metadata.

You normalize the base branch at Line 349-352, but context gathering and AI prompt generation still use baseBranch. If baseBranch is origin/*, this triggers avoidable fallback exceptions in gatherPRContext and can make PR metadata inconsistent.

Proposed refactor
-  const { diffSummary, commitLog } = gatherPRContext(worktreePath, gitPath, baseBranch);
+  const { diffSummary, commitLog } = gatherPRContext(worktreePath, gitPath, effectiveBase);

   const aiBody = await generatePRBody(
     specId,
     title,
-    baseBranch,
+    effectiveBase,
     branchName,
     diffSummary,
     commitLog,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/ai/runners/github/pr-creator.ts` around lines 391 -
398, The code normalizes the base branch into effectiveBase but continues to
pass the raw baseBranch into context and prompt generation; update calls that
use baseBranch—specifically gatherPRContext(...) and generatePRBody(... where
aiBody is produced)—to use effectiveBase instead (retain worktreePath, gitPath,
branchName, specId, title as-is) so context gathering and AI prompt metadata
consistently reference the normalized base branch and avoid fallback/exceptions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/desktop/src/main/ai/runners/github/pr-creator.ts`:
- Around line 354-358: autoCommitWorktreeChanges(...) is being called and its
result ignored, which can let PRs be created without new worktree changes;
change the call to await and handle failures: call await
autoCommitWorktreeChanges(worktreePath, gitPath, specId) and if it returns a
falsy value or throws, log an error including specId and either abort PR
creation (return/throw) or surface the error to the caller so the push/PR step
does not proceed with stale commits; update any surrounding control flow in the
PR creator to propagate the failure instead of silently continuing.

In `@apps/desktop/src/renderer/components/terminal/useXterm.ts`:
- Around line 172-176: The call to window.electronAPI.readClipboardText() can
throw synchronously when the preload API is absent; update the paste logic in
useXterm.ts to first guard that window.electronAPI exists and that
readClipboardText is a function before invoking it, and fall back to
navigator.clipboard.readText() (or other existing fallback path) if the guard
fails; apply the same protective check/try-catch pattern to the other clipboard
invocation in the same module (the later readClipboardText usage) so both places
avoid synchronous exceptions in web/test contexts.

---

Outside diff comments:
In `@apps/desktop/src/main/ai/runners/github/pr-creator.ts`:
- Around line 391-398: The code normalizes the base branch into effectiveBase
but continues to pass the raw baseBranch into context and prompt generation;
update calls that use baseBranch—specifically gatherPRContext(...) and
generatePRBody(... where aiBody is produced)—to use effectiveBase instead
(retain worktreePath, gitPath, branchName, specId, title as-is) so context
gathering and AI prompt metadata consistently reference the normalized base
branch and avoid fallback/exceptions.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: f4524bd3-d7c0-4be1-b5c7-23829f997e83

📥 Commits

Reviewing files that changed from the base of the PR and between cba7a02 and 5fda3e6.

📒 Files selected for processing (5)
  • apps/desktop/src/main/ai/runners/github/pr-creator.ts
  • apps/desktop/src/main/ipc-handlers/terminal-handlers.ts
  • apps/desktop/src/preload/api/terminal-api.ts
  • apps/desktop/src/renderer/components/terminal/useXterm.ts
  • apps/desktop/src/shared/constants/ipc.ts

Comment on lines +354 to +358
// Step 1a: Auto-commit any uncommitted changes in the worktree before pushing.
// This handles the case where the agent wrote files but didn't run `git commit`.
// A failure here is non-fatal — we still attempt the push in case prior commits exist.
autoCommitWorktreeChanges(worktreePath, gitPath, specId);

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.

⚠️ Potential issue | 🟠 Major

Handle auto-commit errors instead of discarding them.

autoCommitWorktreeChanges(...) can fail, but its return value is ignored. That can silently create a PR from stale commits while newly generated worktree changes remain uncommitted.

Proposed fix
-  autoCommitWorktreeChanges(worktreePath, gitPath, specId);
+  const autoCommitError = autoCommitWorktreeChanges(worktreePath, gitPath, specId);
+  if (autoCommitError) {
+    return {
+      success: false,
+      error: `Failed to auto-commit worktree changes: ${autoCommitError}`,
+    };
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/ai/runners/github/pr-creator.ts` around lines 354 -
358, autoCommitWorktreeChanges(...) is being called and its result ignored,
which can let PRs be created without new worktree changes; change the call to
await and handle failures: call await autoCommitWorktreeChanges(worktreePath,
gitPath, specId) and if it returns a falsy value or throws, log an error
including specId and either abort PR creation (return/throw) or surface the
error to the caller so the push/PR step does not proceed with stale commits;
update any surrounding control flow in the PR creator to propagate the failure
instead of silently continuing.

Comment on lines +172 to 176
// Use Electron's main-process clipboard API via IPC, which is reliable on all
// platforms including Windows (where navigator.clipboard.readText() may silently
// fail due to renderer permission restrictions, causing paste to appear and vanish).
window.electronAPI.readClipboardText()
.then((text) => {
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.

⚠️ Potential issue | 🟠 Major

Guard window.electronAPI before invoking clipboard IPC.

This call can throw synchronously when preload API is unavailable, which bypasses the .catch fallback and breaks paste in web/test contexts.

💡 Proposed fix
     const MAX_PASTE_BYTES = 1_048_576; // 1 MB
     const handlePasteFromClipboard = (): void => {
+      const pasteText = (text: string) => {
+        if (!text) return;
+        if (text.length > MAX_PASTE_BYTES) {
+          console.warn(`[useXterm] Paste truncated from ${text.length} to ${MAX_PASTE_BYTES} bytes`);
+          xterm.paste(text.slice(0, MAX_PASTE_BYTES));
+        } else {
+          xterm.paste(text);
+        }
+      };
+
+      const readFromNavigator = () =>
+        navigator.clipboard.readText()
+          .then(pasteText)
+          .catch((err) => {
+            console.error('[useXterm] Failed to read clipboard:', err);
+          });
+
       // Use Electron's main-process clipboard API via IPC, which is reliable on all
       // platforms including Windows (where navigator.clipboard.readText() may silently
       // fail due to renderer permission restrictions, causing paste to appear and vanish).
-      window.electronAPI.readClipboardText()
-        .then((text) => {
-          if (text) {
-            if (text.length > MAX_PASTE_BYTES) {
-              console.warn(`[useXterm] Paste truncated from ${text.length} to ${MAX_PASTE_BYTES} bytes`);
-              xterm.paste(text.slice(0, MAX_PASTE_BYTES));
-            } else {
-              xterm.paste(text);
-            }
-          }
-        })
-        .catch(() => {
-          // Fall back to navigator.clipboard if IPC unavailable
-          navigator.clipboard.readText()
-            .then((text) => {
-              if (text) {
-                xterm.paste(text.length > MAX_PASTE_BYTES ? text.slice(0, MAX_PASTE_BYTES) : text);
-              }
-            })
-            .catch((err) => {
-              console.error('[useXterm] Failed to read clipboard:', err);
-            });
-        });
+      if (window.electronAPI?.readClipboardText) {
+        window.electronAPI.readClipboardText()
+          .then(pasteText)
+          .catch(() => {
+            // Fall back to navigator.clipboard if IPC is unavailable or fails
+            readFromNavigator();
+          });
+      } else {
+        readFromNavigator();
+      }
     };

Also applies to: 186-196

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

In `@apps/desktop/src/renderer/components/terminal/useXterm.ts` around lines 172 -
176, The call to window.electronAPI.readClipboardText() can throw synchronously
when the preload API is absent; update the paste logic in useXterm.ts to first
guard that window.electronAPI exists and that readClipboardText is a function
before invoking it, and fall back to navigator.clipboard.readText() (or other
existing fallback path) if the guard fails; apply the same protective
check/try-catch pattern to the other clipboard invocation in the same module
(the later readClipboardText usage) so both places avoid synchronous exceptions
in web/test contexts.

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.

Windows agent terminal claude code bug

2 participants