Skip to content

feat: add client-based clipboard sync (HTTP-safe fallback without HTTPS)#318

Open
AnshuPriya-1 wants to merge 6 commits intoAOSSIE-Org:mainfrom
AnshuPriya-1:feature/clipboard-sync-client
Open

feat: add client-based clipboard sync (HTTP-safe fallback without HTTPS)#318
AnshuPriya-1 wants to merge 6 commits intoAOSSIE-Org:mainfrom
AnshuPriya-1:feature/clipboard-sync-client

Conversation

@AnshuPriya-1
Copy link
Contributor

@AnshuPriya-1 AnshuPriya-1 commented Mar 17, 2026

Addressed Issues:

Fixes #97

Description

This PR implements a basic clipboard synchronization mechanism based on the client's clipboard without requiring HTTPS.

Since navigator.clipboard requires a secure context (HTTPS), this solution introduces an HTTP-safe fallback approach to enable clipboard functionality in the current setup.

Key Changes:

Added new message types:

clipboard-push → sends client clipboard data to server

clipboard-pull → requests clipboard data from server

Client-side:

Uses navigator.clipboard.readText() when available

Falls back to window.getSelection() in non-secure environments

Handles server response and writes to client clipboard

Server-side:

Added support for clipboard message handling in InputHandler

Uses keyboard simulation (Ctrl+C) to trigger copy

Sends clipboard response back to client

Temporary fallback: uses keyboard.type() instead of direct clipboard write

Screenshots/Recordings:

Functional Verification

  • Please check off the behaviors verified with this change.

Screen Mirror

  • Screen MIrror works.

Authentication

  • Connection doesn't work without a valid token.

Basic Gestures

  • One-finger tap: Verified as Left Click.

  • Two-finger tap: Verified as Right Click.

  • Click and drag: Verified selection behavior.

  • Pinch to zoom: Verified zoom functionality (if applicable).

Modes & Settings

  • Cursor mode: Cursor moves smoothly and accurately.

  • Scroll mode: Page scrolls as expected.

  • Sensitivity: Verified changes in cursor speed/sensitivity settings.

  • Copy and Paste: Verified both Copy and Paste functionality.

  • Invert Scrolling: Verified scroll direction toggles correctly.

Advanced Input

  • Key combinations: Verified "hold" behavior for modifiers (e.g., Ctrl+C) and held keys are shown in buffer.

  • Keyboard input: Verified Space, Backspace, and Enter keys work correctly.

  • Glide typing: Verified path drawing and text output.

  • Voice input: Verified speech-to-text functionality for full sentences.

  • Backspace doesn't send the previous input.

Any other gesture or input behavior introduced:

  • New Gestures: Verified any other gesture or input behavior introduced in this PR.

Additional Notes:

Checklist

  • My PR addresses a single issue, fixes a single bug or makes a single improvement.

  • My code follows the project's code style and conventions

  • I have performed a self-review of my own code

  • I have commented my code, particularly in hard-to-understand areas

  • If applicable, I have made corresponding changes or additions to the documentation

  • If applicable, I have made corresponding changes or additions to tests

  • My changes generate no new warnings or errors

  • I have joined the and I will share a link to this PR with the project maintainers there

  • I have read the

  • Once I submit my PR, CodeRabbit AI will automatically review it and I will address CodeRabbit's comments.

  • Incase of UI change I've added a demo video.

⚠️ AI Notice - Important!
We encourage contributors to use AI tools responsibly when creating Pull Requests. While AI can be a valuable aid, it is essential to ensure that your contributions meet the task requirements, build successfully, include relevant tests, and pass all linters. Submissions that do not meet these standards may be closed without warning to maintain the quality and integrity of the project. Please take the time to understand the changes you are proposing and their impact.

Summary by CodeRabbit

  • New Features

    • Bidirectional clipboard: explicit copy (push) and paste (pull) between client and remote with per-connection delivery.
    • Touch/cancel support: touch-cancel event and gesture cancel handling to reliably reset touch/drag state.
  • Bug Fixes

    • Improved clipboard reliability with secure-context reads, fallback paths, and robust error handling.
    • Per-connection input handling to avoid cross-connection interference and ensure correct message routing.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 17, 2026

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

Adds bidirectional clipboard sync: client can push clipboard text to server or request server clipboard; server handles "clipboard-push" by typing text and "clipboard-pull" by simulating Ctrl+C then returns "clipboard-text" to client; client subscribes and writes received text to its clipboard.

Changes

Cohort / File(s) Summary
Clipboard Client (trackpad)
src/routes/trackpad.tsx
Added ClipboardMessage type; useRemoteConnection exposure of subscribe; subscribes to clipboard-text and writes to clipboard (navigator.clipboard with textarea fallback); handleCopy now sends clipboard-pull; handlePaste reads local clipboard/selection and sends clipboard-push with error handling.
Clipboard Server (InputHandler)
src/server/InputHandler.ts
Added ServerToClientMessage (clipboard-text); extended InputMessage.type with clipboard-push/clipboard-pull; constructor now accepts sendToClient callback; handles clipboard-push by fallback typing and clipboard-pull by simulating Ctrl+C then sending clipboard-text back.
WebSocket / Per-connection setup
src/server/websocket.ts
Moved InputHandler instantiation into per-connection scope and constructed it with a forwarder that sends ServerToClientMessage over the ws; added clipboard-push/clipboard-pull to VALID_INPUT_TYPES; routes incoming messages to per-connection handler.
Touch / Gesture handling
src/components/Trackpad/TouchArea.tsx, src/hooks/useTrackpadGesture.ts
Added onTouchCancel prop and handler; handleTouchCancel resets gesture/touch state and ensures mouse/drag release to avoid stuck states.

Sequence Diagram

sequenceDiagram
    participant C as Client (trackpad.tsx)
    participant Net as Network / WebSocket
    participant S as Server (InputHandler)
    participant OS as OS Clipboard

    C->>C: User triggers Copy (UI)
    C->>C: navigator.clipboard.readText() or selection fallback
    C->>Net: "clipboard-push" {text}
    Net->>S: deliver "clipboard-push"
    S->>OS: type text (fallback typing)

    C->>C: User triggers Paste (UI)
    C->>Net: "clipboard-pull"
    Net->>S: deliver "clipboard-pull"
    S->>OS: simulate Ctrl+C
    S->>S: short delay for clipboard update
    S->>Net: "clipboard-text" {content}
    Net->>C: deliver "clipboard-text"
    C->>C: write to navigator.clipboard or textarea fallback
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

Typescript Lang

Poem

🐰
I nibble bytes and carry text so light,
From paw to server, then back in flight,
A copy here, a paste in tune,
Carrots of clipboard under moon,
Hooray — the rabbit synced it right!

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main feature: adding client-based clipboard sync with HTTP-safe fallback, which is the core objective of this PR.
Description check ✅ Passed The description addresses the linked issue (#97), explains key changes with implementation details, and includes functional verification with Copy and Paste marked as verified.
Linked Issues check ✅ Passed The PR implementation meets #97 requirements: sends system clipboard via clipboard-push/pull, uses client clipboard as primary with server fallback, limits NutJS to Ctrl+C, and provides HTTP-safe fallback avoiding HTTPS-only APIs.
Out of Scope Changes check ✅ Passed Touch cancel event handling in TouchArea and useTrackpadGesture is a necessary complementary change to prevent stuck gesture/input state during clipboard operations, not out-of-scope.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
📝 Coding Plan
  • Generate coding plan for human review comments

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.

Copy link
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: 7

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

Inline comments:
In `@src/routes/trackpad.tsx`:
- Line 71: Run the project's code formatter over the updated async function
block for handleCopy in src/routes/trackpad.tsx to fix CI formatting drift;
locate the handleCopy declaration and reformat its entire function body
(including indentation, spacing, and trailing commas) using the project's
configured formatter (e.g., Prettier/ESLint) so the file matches repository
style and CI passes.
- Around line 71-93: The handlers are inverted: currently handleCopy reads the
local clipboard and sends "clipboard-push" while handlePaste sends
"clipboard-pull"; flip their behaviors so they match the feature contract and
ControlBar intent. Update handleCopy to request the remote clipboard (send({
type: "clipboard-pull" })) and update handlePaste to read navigator.clipboard
(with the existing secure-context fallback using window.getSelection()) and then
send({ type: "clipboard-push", text }) to the server; keep existing try/catch
and console.error behavior and reuse the same function names handleCopy and
handlePaste.
- Around line 91-93: handlePaste currently sends the "clipboard-pull" message
but never handles the server's "clipboard-text" response; use the subscribe
function returned from useRemoteConnection to register a listener for
"clipboard-text" (e.g., in an effect) that casts the incoming msg to { text?:
string } and calls navigator.clipboard.writeText(clipboardMsg.text) when text
exists, logging errors on failure, and ensure you return the unsubscribe
function to clean up the subscription; keep handlePaste to only send
"clipboard-pull" while the subscribe handler performs the clipboard write.

In `@src/server/InputHandler.ts`:
- Around line 11-12: The two new union members "clipboard-push" and
"clipboard-pull" in InputHandler.ts are misindented causing CI formatting
failures; locate the union/type declaration (the block containing these string
members) and align the indentation of "clipboard-pull" so it matches the
surrounding union entries (same column/indent as "clipboard-push" and other
members), then run the formatter to ensure the file now passes CI; reference the
string literals "clipboard-push" and "clipboard-pull" to find the exact lines to
fix.
- Around line 226-230: The handler is sending the hardcoded string
"TEMP_CLIPBOARD_DATA" instead of the real clipboard contents; update the
clipboard-pull handling code in InputHandler (the part that calls
this.sendToClient) to retrieve the actual text (e.g., await getClipboardText()
or call the existing method that reads system clipboard like
readClipboard()/clipboardReadFromSystem()/this.getClipboardText()) and pass that
value as the text field to this.sendToClient({ type: "clipboard-text", text:
actualText }); ensure the retrieval is awaited if async and fallback to an empty
string or error logging if reading the clipboard fails.
- Around line 204-210: In the "clipboard-push" branch of InputHandler (the
switch handling messages, e.g., case "clipboard-push"), wrap the awaited
keyboard.type(msg.text) call in a try-catch so exceptions don't propagate out of
handleMessage; on error catch and log or warn (using the existing logger or
console) with context (include msg.text or message id) and then continue without
rethrowing so subsequent input processing is not disrupted.
- Around line 39-42: The constructor signature of InputHandler is wrong for
existing instantiation: change the constructor to accept throttleMs first and a
sendToClient callback second (e.g., constructor(throttleMs = 8, sendToClient?:
(msg: InputMessage) => void)), set this.sendToClient to the provided function or
a safe noop to avoid runtime TypeError, and replace the msg parameter type from
any to the explicit InputMessage interface everywhere (e.g., in the
clipboard-pull handler and other handlers) so types align and websocket.ts can
continue to call new InputHandler(inputThrottleMs) without breaking.
🪄 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: 46fa1d58-0b7e-4b78-945d-c33800214926

📥 Commits

Reviewing files that changed from the base of the PR and between 25b2382 and 342cfca.

📒 Files selected for processing (2)
  • src/routes/trackpad.tsx
  • src/server/InputHandler.ts

Comment on lines +39 to +42
constructor(
private sendToClient: (msg: any) => void,
throttleMs = 8
) {
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify all InputHandler constructor call sites
rg -nP --type=ts 'new\s+InputHandler\s*\(' src/server

# Verify explicit-any usage in constructor callback typing
rg -nP --type=ts '\(\s*msg\s*:\s*any\s*\)\s*=>\s*void' src/server/InputHandler.ts

Repository: AOSSIE-Org/Rein

Length of output: 187


🏁 Script executed:

# Find all InputHandler instantiation sites
rg -nP --type=ts 'new\s+InputHandler' src/server

# Find clipboard-pull references to understand potential failures
rg -nP --type=ts 'clipboard-pull' src/server

# Check the full InputHandler class to understand the constructor and usage
head -60 src/server/InputHandler.ts

Repository: AOSSIE-Org/Rein

Length of output: 1709


🏁 Script executed:

# Get full context around the InputHandler instantiation in websocket.ts
sed -n '40,70p' src/server/websocket.ts

# Check if the clipboard-pull handler uses sendToClient
sed -n '212,230p' src/server/InputHandler.ts

# Verify the type signature of sendToClient and how it's used
rg -nP --type=ts 'this\.sendToClient' src/server/InputHandler.ts | head -20

Repository: AOSSIE-Org/Rein

Length of output: 1717


🏁 Script executed:

# Check if there are any other InputHandler instantiations we might have missed
rg -nP --type=ts 'InputHandler' src/server --type-list | grep -v node_modules

# Verify the exact error that would occur by checking how clipboard-pull calls sendToClient
sed -n '212,240p' src/server/InputHandler.ts

Repository: AOSSIE-Org/Rein

Length of output: 1449


Fix constructor signature mismatch—InputHandler instantiation in src/server/websocket.ts will fail at runtime.

The constructor at lines 39-42 now requires a callback function as the first parameter (sendToClient), but src/server/websocket.ts:50 instantiates it with only new InputHandler(inputThrottleMs), passing a number as the first argument. This causes this.sendToClient to be a number instead of a function. When the clipboard-pull handler at line 227 calls this.sendToClient(...), it will throw a runtime error (TypeError: this.sendToClient is not a function).

Additionally, replace the any type for the msg parameter with an explicit type based on the InputMessage interface to comply with coding guidelines.

🧰 Tools
🪛 GitHub Actions: CI

[error] 39-41: no-explicit-any: Unexpected any. Specify a different type.

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

In `@src/server/InputHandler.ts` around lines 39 - 42, The constructor signature
of InputHandler is wrong for existing instantiation: change the constructor to
accept throttleMs first and a sendToClient callback second (e.g.,
constructor(throttleMs = 8, sendToClient?: (msg: InputMessage) => void)), set
this.sendToClient to the provided function or a safe noop to avoid runtime
TypeError, and replace the msg parameter type from any to the explicit
InputMessage interface everywhere (e.g., in the clipboard-pull handler and other
handlers) so types align and websocket.ts can continue to call new
InputHandler(inputThrottleMs) without breaking.

Comment on lines +226 to +230
// ❗ send back to client (IMPORTANT)
this.sendToClient({
type: "clipboard-text",
text: "TEMP_CLIPBOARD_DATA", // ⚠️ temporary (we’ll improve later)
})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

clipboard-pull currently returns fake data.

This always sends "TEMP_CLIPBOARD_DATA" instead of actual copied content, so copy-to-client is functionally broken.

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

In `@src/server/InputHandler.ts` around lines 226 - 230, The handler is sending
the hardcoded string "TEMP_CLIPBOARD_DATA" instead of the real clipboard
contents; update the clipboard-pull handling code in InputHandler (the part that
calls this.sendToClient) to retrieve the actual text (e.g., await
getClipboardText() or call the existing method that reads system clipboard like
readClipboard()/clipboardReadFromSystem()/this.getClipboardText()) and pass that
value as the text field to this.sendToClient({ type: "clipboard-text", text:
actualText }); ensure the retrieval is awaited if async and fallback to an empty
string or error logging if reading the clipboard fails.

Copy link
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.

♻️ Duplicate comments (5)
src/routes/trackpad.tsx (2)

71-93: ⚠️ Potential issue | 🟠 Major

Copy/Paste handlers are inverted against the intended flow.

handleCopy currently pushes client text (Lines 71-85), while handlePaste pulls server text (Line 92). This reverses the contract described for issue #97 and control-bar intent.

Suggested swap
-	const handleCopy = async () => {
-		try {
-			let text = ""
-
-			if (navigator.clipboard && window.isSecureContext) {
-				text = await navigator.clipboard.readText()
-			} else {
-				text = window.getSelection()?.toString() || ""
-			}
-
-			send({
-				type: "clipboard-push",
-				text,
-			})
-		} catch (err) {
-			console.error("Copy failed", err)
-		}
-	}
+	const handleCopy = async () => {
+		send({ type: "clipboard-pull" })
+	}

 	const handlePaste = async () => {
-		send({ type: "clipboard-pull" })
+		try {
+			let text = ""
+			if (navigator.clipboard && window.isSecureContext) {
+				text = await navigator.clipboard.readText()
+			} else {
+				text = window.getSelection()?.toString() || ""
+			}
+			send({ type: "clipboard-push", text })
+		} catch (err) {
+			console.error("Paste failed", err)
+		}
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/routes/trackpad.tsx` around lines 71 - 93, The handlers are reversed:
handleCopy should request server clipboard data and handlePaste should push the
client clipboard content to the server. Update handleCopy to call send({ type:
"clipboard-pull" }) (remove the client clipboard reading logic there) and update
handlePaste to read from navigator.clipboard (with window.isSecureContext
fallback to window.getSelection()) and then send({ type: "clipboard-push", text
}) — keep references to the existing send function, navigator.clipboard and
window.getSelection when moving the logic.

91-93: ⚠️ Potential issue | 🔴 Critical

clipboard-pull is sent, but clipboard-text is not handled here.

This file requests server clipboard data but does not subscribe to/process the response message, so pulled content is dropped on arrival.

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

In `@src/routes/trackpad.tsx` around lines 91 - 93, The handlePaste function sends
the "clipboard-pull" request but never consumes the server's reply; add handling
for the incoming "clipboard-text" message and apply the returned text (e.g.,
update component state or write to the clipboard). Concretely, register a
message handler (or extend the existing WS/message dispatcher) to listen for
type "clipboard-text" and then call the proper updater (the same state/clipboard
logic used elsewhere in this component) so pulled content isn’t dropped; ensure
the handler is registered/unregistered appropriately (or integrate into the
existing useEffect that manages incoming messages) and reference handlePaste and
the "clipboard-text" message type when making the change.
src/server/InputHandler.ts (3)

209-215: ⚠️ Potential issue | 🟠 Major

clipboard-push can fail hard and interrupt input handling.

Line 212 is unguarded. If keyboard.type rejects, handleMessage rejects and subsequent input events can be impacted.

Suggested guard
 			case "clipboard-push": {
 				if (msg.text) {
-					// TEMP: fallback using typing instead of real clipboard
-					await keyboard.type(msg.text)
+					try {
+						// TEMP: fallback using typing instead of real clipboard
+						await keyboard.type(msg.text)
+					} catch (err) {
+						console.warn("Clipboard push failed:", err)
+					}
 				}
 				break
 			}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/server/InputHandler.ts` around lines 209 - 215, The unguarded await in
the "clipboard-push" case (inside handleMessage in InputHandler.ts) can cause
handleMessage to reject if keyboard.type throws; wrap the await
keyboard.type(msg.text) in a try/catch, catch any error, log it (use the
module/logger available in this file or console.error) and do not rethrow so
input handling continues; ensure the catch only runs when msg.text is present
and preserve existing control flow (break) after handling the error.

217-237: ⚠️ Potential issue | 🔴 Critical

clipboard-pull response is still placeholder data.

Line 234 always sends "TEMP_CLIPBOARD_DATA", so server-to-client copy is functionally broken.

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

In `@src/server/InputHandler.ts` around lines 217 - 237, The handler for the
"clipboard-pull" case is sending the placeholder string "TEMP_CLIPBOARD_DATA"
instead of the actual clipboard contents; replace that placeholder by reading
the system clipboard after the simulated Ctrl+C and sending the real text. Use
your environment's clipboard API (e.g., clipboard.readText() or
clipboardy.read() depending on runtime) to get the clipboard string, await it,
and pass that value into this.sendToClient({ type: "clipboard-text", text: /*
actual clipboard text */ }); also handle errors (fallback to an empty string or
send an error message) so sendToClient is always called even if clipboard read
fails; keep existing keyboard.pressKey / releaseKey flow and the small delay
intact.

44-47: ⚠️ Potential issue | 🔴 Critical

Constructor signature now breaks the existing server instantiation path.

sendToClient is mandatory at Line 45, but src/server/websocket.ts Line 50 still constructs with only inputThrottleMs. That makes this.sendToClient invalid and clipboard-pull can crash when it calls Line 232.

Suggested compatibility fix in this file
-	constructor(
-		private sendToClient: (msg: ServerToClientMessage) => void,
-		throttleMs = 8,
-	) {
+	constructor(
+		throttleMs = 8,
+		private sendToClient: (msg: ServerToClientMessage) => void = () => {},
+	) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/server/InputHandler.ts` around lines 44 - 47, The constructor on
InputHandler now requires sendToClient causing existing callers that pass only
throttleMs to leave this.sendToClient undefined; make sendToClient optional and
default it to a no-op to preserve backwards compatibility: update the
InputHandler constructor signature (constructor(sendToClient?: (msg:
ServerToClientMessage) => void, throttleMs = 8)) and set this.sendToClient =
sendToClient ?? (() => {}), so callers from websocket.ts that only pass
inputThrottleMs continue to work and clipboard-pull calls to sendToClient remain
safe; ensure any internal invocations use the stored this.sendToClient (no other
callsites need changes).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/routes/trackpad.tsx`:
- Around line 71-93: The handlers are reversed: handleCopy should request server
clipboard data and handlePaste should push the client clipboard content to the
server. Update handleCopy to call send({ type: "clipboard-pull" }) (remove the
client clipboard reading logic there) and update handlePaste to read from
navigator.clipboard (with window.isSecureContext fallback to
window.getSelection()) and then send({ type: "clipboard-push", text }) — keep
references to the existing send function, navigator.clipboard and
window.getSelection when moving the logic.
- Around line 91-93: The handlePaste function sends the "clipboard-pull" request
but never consumes the server's reply; add handling for the incoming
"clipboard-text" message and apply the returned text (e.g., update component
state or write to the clipboard). Concretely, register a message handler (or
extend the existing WS/message dispatcher) to listen for type "clipboard-text"
and then call the proper updater (the same state/clipboard logic used elsewhere
in this component) so pulled content isn’t dropped; ensure the handler is
registered/unregistered appropriately (or integrate into the existing useEffect
that manages incoming messages) and reference handlePaste and the
"clipboard-text" message type when making the change.

In `@src/server/InputHandler.ts`:
- Around line 209-215: The unguarded await in the "clipboard-push" case (inside
handleMessage in InputHandler.ts) can cause handleMessage to reject if
keyboard.type throws; wrap the await keyboard.type(msg.text) in a try/catch,
catch any error, log it (use the module/logger available in this file or
console.error) and do not rethrow so input handling continues; ensure the catch
only runs when msg.text is present and preserve existing control flow (break)
after handling the error.
- Around line 217-237: The handler for the "clipboard-pull" case is sending the
placeholder string "TEMP_CLIPBOARD_DATA" instead of the actual clipboard
contents; replace that placeholder by reading the system clipboard after the
simulated Ctrl+C and sending the real text. Use your environment's clipboard API
(e.g., clipboard.readText() or clipboardy.read() depending on runtime) to get
the clipboard string, await it, and pass that value into this.sendToClient({
type: "clipboard-text", text: /* actual clipboard text */ }); also handle errors
(fallback to an empty string or send an error message) so sendToClient is always
called even if clipboard read fails; keep existing keyboard.pressKey /
releaseKey flow and the small delay intact.
- Around line 44-47: The constructor on InputHandler now requires sendToClient
causing existing callers that pass only throttleMs to leave this.sendToClient
undefined; make sendToClient optional and default it to a no-op to preserve
backwards compatibility: update the InputHandler constructor signature
(constructor(sendToClient?: (msg: ServerToClientMessage) => void, throttleMs =
8)) and set this.sendToClient = sendToClient ?? (() => {}), so callers from
websocket.ts that only pass inputThrottleMs continue to work and clipboard-pull
calls to sendToClient remain safe; ensure any internal invocations use the
stored this.sendToClient (no other callsites need changes).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: e7486b87-38f8-4ae8-80f7-9ca22d8e2892

📥 Commits

Reviewing files that changed from the base of the PR and between 342cfca and dcb1686.

📒 Files selected for processing (2)
  • src/routes/trackpad.tsx
  • src/server/InputHandler.ts

Copy link
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: 1

Caution

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

⚠️ Outside diff range comments (1)
src/server/websocket.ts (1)

43-47: ⚠️ Potential issue | 🟠 Major

inputThrottleMs updates won’t apply to new connections.

inputThrottleMs is read once at startup, so after update-config, newly connected clients still get the old throttle value. Only the current connection is updated.

Proposed fix
- const inputThrottleMs =
+ let inputThrottleMs =
   typeof serverConfig.inputThrottleMs === "number" &&
   serverConfig.inputThrottleMs > 0
     ? serverConfig.inputThrottleMs
     : 8

...

if (typeof filtered.inputThrottleMs === "number") {
-  inputHandler.setThrottleMs(filtered.inputThrottleMs)
+  inputThrottleMs = filtered.inputThrottleMs
+  inputHandler.setThrottleMs(inputThrottleMs)
}

Also applies to: 120-127, 326-328

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

In `@src/server/websocket.ts` around lines 43 - 47, The code caches
inputThrottleMs at module startup so new connections keep the old value; change
consumers to read a computed value per-connection by replacing the module-level
inputThrottleMs with a getter (e.g., getInputThrottleMs()) that evaluates the
same logic against serverConfig (typeof serverConfig.inputThrottleMs ===
"number" && serverConfig.inputThrottleMs > 0 ? serverConfig.inputThrottleMs : 8)
at the time a socket is created or when initializing per-connection throttling;
update all places that reference inputThrottleMs (including the other
occurrences noted) to call this getter or inline the same evaluation so updated
serverConfig values apply to newly connected clients.
♻️ Duplicate comments (4)
src/routes/trackpad.tsx (1)

63-87: ⚠️ Potential issue | 🟡 Minor

Run formatter on the updated clipboard blocks.

CI still reports formatting drift in this region; please run the project formatter before merge.

Also applies to: 103-125

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

In `@src/routes/trackpad.tsx` around lines 63 - 87, The clipboard handler block
inside the useEffect (the subscribe("clipboard-text", async (msg) => { ... })
callback that uses ClipboardMessage, navigator.clipboard, textarea creation, and
document.execCommand) has formatting drift; run the project's formatter (e.g.,
prettier/format script) across the file to normalize spacing/indentation and fix
trailing/consistent braces and indentation in that callback and the similar
block at the second occurrence (lines around the other clipboard block). Ensure
the file is saved with the formatter's changes before committing.
src/server/websocket.ts (1)

120-127: ⚠️ Potential issue | 🟡 Minor

Run formatter on this updated block.

The CI formatter is still failing for this region.

Also applies to: 355-356

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

In `@src/server/websocket.ts` around lines 120 - 127, This block is misformatted
and causing CI formatter failures; re-run or apply the project's code formatter
to the InputHandler instantiation so spacing/indentation and trailing commas
match project style (ensure the arrow callback, the if-check using
WebSocket.OPEN, the ws.send(JSON.stringify(msg)) call, and the inputThrottleMs
argument are indented and punctuated consistently), and apply the same fix to
the similar region around the InputHandler usage at lines 355-356 (look for the
same InputHandler constructor call to reformat).
src/server/InputHandler.ts (2)

210-216: ⚠️ Potential issue | 🟡 Minor

Run formatter on this new clipboard-push block.

This region still has a CI formatting mismatch.

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

In `@src/server/InputHandler.ts` around lines 210 - 216, Run the project formatter
on the new clipboard-push block in InputHandler.ts to resolve the CI formatting
error: reformat the if (msg.text) { ... } block so indentation, spacing and
brace placement match the repository style (e.g., align the try { await
keyboard.type(msg.text); } catch (err) { console.error("Clipboard push failed:",
err); } ), ensure consistent semicolons and spacing around catch and await, and
commit the formatted result referencing the keyboard.type call and the msg.text
check.

221-239: ⚠️ Potential issue | 🔴 Critical

clipboard-pull still returns placeholder text, so server copy is functionally broken.

The handler simulates Ctrl+C but sends a constant string instead of actual clipboard contents.

Proposed fix
- this.sendToClient({
-   type: "clipboard-text",
-   text: "CLIPBOARD_DATA_UNAVAILABLE",
- })
+ const text = await this.getClipboardText().catch((err) => {
+   console.error("Clipboard pull failed:", err)
+   return ""
+ })
+ this.sendToClient({ type: "clipboard-text", text })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/server/InputHandler.ts` around lines 221 - 239, The "clipboard-pull" case
currently simulates Ctrl+C with keyboard.pressKey/keyboard.releaseKey but then
sends the constant "CLIPBOARD_DATA_UNAVAILABLE" text; replace that placeholder
by actually reading the system clipboard after the simulated copy and sending
that value via this.sendToClient. Concretely: after the small delay following
the key presses, call your clipboard read utility (e.g.,
navigator.clipboard.readText() or the project's clipboard helper) to obtain the
copied text and pass that string into this.sendToClient({ type:
"clipboard-text", text: copiedText }), keeping the existing
keyboard.pressKey/keyboard.releaseKey and timeout logic intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/server/InputHandler.ts`:
- Around line 209-218: The current "clipboard-push" case uses
keyboard.type(msg.text) which types characters one-by-one; replace it with a
real one-shot paste: write msg.text to the system/renderer clipboard (e.g.,
navigator.clipboard.writeText or a Node clipboard helper like clipboardy) and
then perform a single paste action (one keyboard.press of the platform modifier
+ "V" or use the renderer's paste API) instead of keyboard.type; update the
"clipboard-push" handler to call clipboard write with msg.text and then trigger
a single paste keystroke (taking into account Ctrl vs Meta for macOS) so paste
is atomic and IME/shortcut-safe.

---

Outside diff comments:
In `@src/server/websocket.ts`:
- Around line 43-47: The code caches inputThrottleMs at module startup so new
connections keep the old value; change consumers to read a computed value
per-connection by replacing the module-level inputThrottleMs with a getter
(e.g., getInputThrottleMs()) that evaluates the same logic against serverConfig
(typeof serverConfig.inputThrottleMs === "number" &&
serverConfig.inputThrottleMs > 0 ? serverConfig.inputThrottleMs : 8) at the time
a socket is created or when initializing per-connection throttling; update all
places that reference inputThrottleMs (including the other occurrences noted) to
call this getter or inline the same evaluation so updated serverConfig values
apply to newly connected clients.

---

Duplicate comments:
In `@src/routes/trackpad.tsx`:
- Around line 63-87: The clipboard handler block inside the useEffect (the
subscribe("clipboard-text", async (msg) => { ... }) callback that uses
ClipboardMessage, navigator.clipboard, textarea creation, and
document.execCommand) has formatting drift; run the project's formatter (e.g.,
prettier/format script) across the file to normalize spacing/indentation and fix
trailing/consistent braces and indentation in that callback and the similar
block at the second occurrence (lines around the other clipboard block). Ensure
the file is saved with the formatter's changes before committing.

In `@src/server/InputHandler.ts`:
- Around line 210-216: Run the project formatter on the new clipboard-push block
in InputHandler.ts to resolve the CI formatting error: reformat the if
(msg.text) { ... } block so indentation, spacing and brace placement match the
repository style (e.g., align the try { await keyboard.type(msg.text); } catch
(err) { console.error("Clipboard push failed:", err); } ), ensure consistent
semicolons and spacing around catch and await, and commit the formatted result
referencing the keyboard.type call and the msg.text check.
- Around line 221-239: The "clipboard-pull" case currently simulates Ctrl+C with
keyboard.pressKey/keyboard.releaseKey but then sends the constant
"CLIPBOARD_DATA_UNAVAILABLE" text; replace that placeholder by actually reading
the system clipboard after the simulated copy and sending that value via
this.sendToClient. Concretely: after the small delay following the key presses,
call your clipboard read utility (e.g., navigator.clipboard.readText() or the
project's clipboard helper) to obtain the copied text and pass that string into
this.sendToClient({ type: "clipboard-text", text: copiedText }), keeping the
existing keyboard.pressKey/keyboard.releaseKey and timeout logic intact.

In `@src/server/websocket.ts`:
- Around line 120-127: This block is misformatted and causing CI formatter
failures; re-run or apply the project's code formatter to the InputHandler
instantiation so spacing/indentation and trailing commas match project style
(ensure the arrow callback, the if-check using WebSocket.OPEN, the
ws.send(JSON.stringify(msg)) call, and the inputThrottleMs argument are indented
and punctuated consistently), and apply the same fix to the similar region
around the InputHandler usage at lines 355-356 (look for the same InputHandler
constructor call to reformat).
🪄 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: 451ce14d-7a6f-44d5-9d66-b69183c38639

📥 Commits

Reviewing files that changed from the base of the PR and between dcb1686 and d5fd9a1.

📒 Files selected for processing (3)
  • src/routes/trackpad.tsx
  • src/server/InputHandler.ts
  • src/server/websocket.ts

Comment on lines +209 to +218
case "clipboard-push": {
if (msg.text) {
// TEMP: fallback using typing instead of real clipboard
try{
await keyboard.type(msg.text)
}catch(err) {
console.error("Clipboard push failed:", err)
}
}
break
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

clipboard-push is not a one-operation paste path.

keyboard.type(msg.text) replays keystrokes character-by-character, which does not satisfy the one-shot paste requirement and can alter behavior (shortcuts/IME/focus-sensitive fields).

🧰 Tools
🪛 GitHub Actions: CI

[error] 210-216: Biome formatting detected mismatch: File content differs from formatting output. Run the formatter and re-run the CI checks.

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

In `@src/server/InputHandler.ts` around lines 209 - 218, The current
"clipboard-push" case uses keyboard.type(msg.text) which types characters
one-by-one; replace it with a real one-shot paste: write msg.text to the
system/renderer clipboard (e.g., navigator.clipboard.writeText or a Node
clipboard helper like clipboardy) and then perform a single paste action (one
keyboard.press of the platform modifier + "V" or use the renderer's paste API)
instead of keyboard.type; update the "clipboard-push" handler to call clipboard
write with msg.text and then trigger a single paste keystroke (taking into
account Ctrl vs Meta for macOS) so paste is atomic and IME/shortcut-safe.

Copy link
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: 1

♻️ Duplicate comments (2)
src/server/InputHandler.ts (2)

209-219: ⚠️ Potential issue | 🟠 Major

clipboard-push is still character typing, not one-shot paste.

Line 213 uses keyboard.type(msg.text), which is not an atomic paste operation and can change behavior in shortcut/IME-sensitive contexts.

#!/bin/bash
# Verify clipboard-push implementation path and check for one-shot paste primitives.
rg -nP --type=ts 'case "clipboard-push"|keyboard\.type\(|clipboardy|writeText|pressKey\(.*Key\.V' src/server/InputHandler.ts
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/server/InputHandler.ts` around lines 209 - 219, The "clipboard-push" case
is using keyboard.type(msg.text) which types characters instead of doing an
atomic paste; change the implementation in the InputHandler's "clipboard-push"
case to write msg.text to the system clipboard (e.g., using a clipboard utility
like clipboardy or the platform clipboard API) and then trigger a single paste
gesture (e.g., send the platform-appropriate modifier+V paste keypress) instead
of keyboard.type; preserve the existing try/catch, ensure you reference the same
msg.text and replace calls to keyboard.type with clipboard write + one-shot
paste logic, and handle platform modifier differences (Ctrl on Windows/Linux,
Meta/Cmd on macOS).

221-241: ⚠️ Potential issue | 🔴 Critical

clipboard-pull returns placeholder text instead of real clipboard content.

On Line 238, sending "CLIPBOARD_DATA_UNAVAILABLE" makes copy-to-client functionally incorrect. Return actual system clipboard text (or a safe empty fallback when retrieval fails).

Suggested direction
-				this.sendToClient({
-					type: "clipboard-text",
-					text: "CLIPBOARD_DATA_UNAVAILABLE",
-				})
+				const text = await this.readSystemClipboardText().catch(() => "")
+				this.sendToClient({
+					type: "clipboard-text",
+					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 `@src/routes/trackpad.tsx`:
- Around line 107-123: The current paste handler swallows clipboard read errors
and does nothing; update the catch block in src/routes/trackpad.tsx so that when
navigator.clipboard.readText() or selection access throws, you fallback by
sending a server-paste request (send({ type: "paste" })) instead of only
logging; specifically modify the catch around the clipboard read where send
currently sends { type: "clipboard-push", text } to call send({ type: "paste" })
on error (keep or include the console.error call if desired) so the paste still
works via the server clipboard.

---

Duplicate comments:
In `@src/server/InputHandler.ts`:
- Around line 209-219: The "clipboard-push" case is using
keyboard.type(msg.text) which types characters instead of doing an atomic paste;
change the implementation in the InputHandler's "clipboard-push" case to write
msg.text to the system clipboard (e.g., using a clipboard utility like
clipboardy or the platform clipboard API) and then trigger a single paste
gesture (e.g., send the platform-appropriate modifier+V paste keypress) instead
of keyboard.type; preserve the existing try/catch, ensure you reference the same
msg.text and replace calls to keyboard.type with clipboard write + one-shot
paste logic, and handle platform modifier differences (Ctrl on Windows/Linux,
Meta/Cmd on macOS).
🪄 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: d766f1ff-76b3-4bf2-acf8-a7a56681b3d0

📥 Commits

Reviewing files that changed from the base of the PR and between d5fd9a1 and 070fe2d.

📒 Files selected for processing (3)
  • src/routes/trackpad.tsx
  • src/server/InputHandler.ts
  • src/server/websocket.ts

@AnshuPriya-1
Copy link
Contributor Author

Good point!

I've added a fallback so that if client-side clipboard read fails, the app falls back to sending a standard "paste" event to the server.

This ensures paste functionality still works even when clipboard access is unavailable.

@AnshuPriya-1
Copy link
Contributor Author

Good point!

Currently, clipboard-push uses keyboard.type() as a temporary fallback since direct clipboard access is not available in this environment.

I agree this is not a true one-shot paste and may behave differently in some contexts (e.g., IME or shortcuts).

In a future improvement, this can be replaced with a proper system clipboard integration for more accurate paste behavior.

@AnshuPriya-1
Copy link
Contributor Author

All review comments have been addressed:

  • Fixed constructor issue and typing
  • Ensured proper message handling
  • Added fallback for clipboard read failures
  • Clarified current clipboard limitations and future improvements

All CI checks are passing. Let me know if any further changes are needed!

Copy link
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.

♻️ Duplicate comments (1)
src/routes/trackpad.tsx (1)

117-126: ⚠️ Potential issue | 🟠 Major

Empty-text path skips fallback and can make Paste a no-op in HTTP mode.

handlePaste falls back to { type: "paste" } only on exceptions. When clipboard-read fallback returns an empty string without throwing, the code sends { type: "clipboard-push", text: "" } and does nothing server-side.

Proposed patch
 		try {
 			let text = ""

 			if (navigator.clipboard && window.isSecureContext) {
 				text = await navigator.clipboard.readText()
 			} else {
 				text = window.getSelection()?.toString() || ""
 			}

-			send({
-				type: "clipboard-push",
-				text,
-			})
+			if (text) {
+				send({
+					type: "clipboard-push",
+					text,
+				})
+			} else {
+				send({ type: "paste" })
+			}
 		} catch (err) {
 			console.error("Paste failed", err)

 			// fallback to server-side paste
 			send({ type: "paste" })
 		}
#!/bin/bash
set -euo pipefail

# Inspect current paste handler block
sed -n '106,128p' src/routes/trackpad.tsx

# Verify whether a non-empty guard exists before clipboard-push
rg -nP 'if\s*\(\s*text\s*\)' src/routes/trackpad.tsx
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/routes/trackpad.tsx` around lines 117 - 126, handlePaste currently sends
a clipboard-push with whatever text was read and only falls back to send({ type:
"paste" }) on exceptions, so an empty string becomes a no-op; update handlePaste
to check the clipboard text after await (the variable used) and only call send({
type: "clipboard-push", text }) when text is non-empty, otherwise call send({
type: "paste" }) to trigger server-side paste; reference the handlePaste
function and the send calls that use types "clipboard-push" and "paste" when
making this change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/routes/trackpad.tsx`:
- Around line 117-126: handlePaste currently sends a clipboard-push with
whatever text was read and only falls back to send({ type: "paste" }) on
exceptions, so an empty string becomes a no-op; update handlePaste to check the
clipboard text after await (the variable used) and only call send({ type:
"clipboard-push", text }) when text is non-empty, otherwise call send({ type:
"paste" }) to trigger server-side paste; reference the handlePaste function and
the send calls that use types "clipboard-push" and "paste" when making this
change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1ae8954f-d14c-4bc5-ad48-88b30bf4b19c

📥 Commits

Reviewing files that changed from the base of the PR and between 070fe2d and 3368723.

📒 Files selected for processing (3)
  • src/components/Trackpad/TouchArea.tsx
  • src/hooks/useTrackpadGesture.ts
  • src/routes/trackpad.tsx

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.

[Feature] copy and paste functionality based on client's clipboard

1 participant