Skip to content

Fix Copy/Paste Functionality and Resolve Connection Loss on App Switch#92

Open
b-u-g-g wants to merge 5 commits intoAOSSIE-Org:mainfrom
b-u-g-g:main
Open

Fix Copy/Paste Functionality and Resolve Connection Loss on App Switch#92
b-u-g-g wants to merge 5 commits intoAOSSIE-Org:mainfrom
b-u-g-g:main

Conversation

@b-u-g-g
Copy link

@b-u-g-g b-u-g-g commented Feb 15, 2026

Summary

This PR addresses two critical stability and functionality issues: implementing functional Copy and Paste buttons via backend keyboard simulation and resolving WebSocket connection drops when switching between apps on mobile devices.

Problem Statement

1. Copy/Paste Buttons Non-Functional

  • The Copy and Paste buttons in the UI were visible but had no backend implementation to trigger system-level actions.
  • There was no logic to simulate platform-specific keyboard shortcuts (Ctrl+C/V vs. Cmd+C/V).

2. Connection State Lost on App Switch

  • Switching from the Rein app to another application (e.g., WhatsApp) caused the WebSocket to disconnect immediately.
  • The trackpad became unresponsive upon returning to the app due to a lack of automated reconnection or heartbeat mechanisms.

Changes Made

1. Implemented Copy/Paste Button Handlers

  • File: app/server/InputHandler.ts
  • Added keyboard shortcut simulation that detects the host operating system to use the correct modifier key (Key.LeftControl for Windows/Linux or Key.LeftCmd for macOS).
  • Ensured proper sequencing of pressKey and releaseKey events to execute system-level shortcuts reliably.

2. Fixed Connection State Management

  • Heartbeat System (app/server/websocket.ts): * Implemented a server-side check that expects a ping every 10 seconds, terminating the connection only after 35 seconds of silence to prevent premature drops.

  • Auto-Reconnection Logic (app/hooks/useRemoteConnection.ts): * Added event listeners for visibilitychange, focus, and pageshow to detect when a user returns to the foreground.

  • The app now checks connection status immediately upon return and forces a reconnect if the socket is closed.

  • Wake Lock API (app/hooks/useRemoteConnection.ts): * Integrated the Wake Lock API to request that the mobile screen remains active, helping to keep the WebSocket alive while the app is in use.

Testing Done

  • Copy/Paste: Verified that clicking the UI buttons triggers the correct system-level clipboard actions across Windows and macOS.
  • Connection Persistence: Confirmed that switching apps and returning to Rein no longer requires a manual refresh; the trackpad remains responsive.
  • Visibility Handling: Verified that the "App returned to foreground" log triggers correctly and restores the WebSocket state.

Summary by CodeRabbit

  • New Features

    • Functional Copy and Paste buttons added to the trackpad control bar.
    • Connection object exposed for richer remote interactions.
  • Improvements

    • Auto-reconnect on resume from background and faster reconnects (1s).
    • Send operations now warn when not connected.
  • UI Updates

    • Removed L-Click button; copy/paste buttons styled and integrated.
  • Bug Fixes

    • Hidden input autofocus restored.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 15, 2026

Warning

Rate limit exceeded

@b-u-g-g has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 10 minutes and 29 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

Adds functional Copy and Paste controls to the trackpad UI, wires them through the trackpad route to the remote connection, enhances the WebSocket hook with live socket tracking and visibility-aware reconnection, extends server input message types for copy/paste, and refines WebSocket server logging and error handling.

Changes

Cohort / File(s) Summary
ControlBar UI
src/components/Trackpad/ControlBar.tsx
Removed onLeftClick and L-Click button; added onCopy and onPaste props, buttons, and updated button classes/styles.
Trackpad Route
src/routes/trackpad.tsx
Added handleCopy and handlePaste handlers; wired onCopy/onPaste into ControlBar; minor autofocus fix.
Remote connection hook
src/hooks/useRemoteConnection.ts
Added live ws ref exposed in return, visibilitychange-based reconnect, improved logging/error handling, and safer send/sendCombo using live ref.
Server input handling
src/server/InputHandler.ts
Extended InputMessage.type with paste and copy; implemented platform-aware copy/paste actions; switched mouse move to relative deltas; simplified scroll and reduced logging.
WebSocket server
src/server/websocket.ts
Refined startup/connection logs, added raw buffer capture for messages, distinguished JSON parse errors vs runtime errors, and adjusted error/disconnect messages.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant UI as ControlBar (UI)
    participant Route as trackpad.tsx
    participant Hook as useRemoteConnection
    participant WS as WebSocket Client
    participant Server as InputHandler

    User->>UI: Click "Copy"
    UI->>Route: onCopy()
    Route->>Route: handleCopy() logs
    Route->>Hook: send({ type: "copy" })
    Hook->>WS: ws.send(message)
    WS->>Server: receive copy message
    Server->>Server: perform platform-aware copy (Cmd/Ctrl+C)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Poem

🐰 I tapped the copy, then hopped to paste,
Buttons bright where L-Click was displaced,
Sockets watch, they wake with a cheer,
The rabbit types and sends it near,
Small paws, big nets — all synced and chaste 🥕

🚥 Pre-merge checks | ✅ 3 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the two main changes: implementing copy/paste functionality and fixing connection stability issues during app switching.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

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

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

Caution

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

⚠️ Outside diff range comments (2)
src/components/Trackpad/ControlBar.tsx (1)

9-9: 🛠️ Refactor suggestion | 🟠 Major

onLeftClick is still declared and destructured but no longer used in the JSX.

The L-Click button was removed, but onLeftClick remains in the interface (line 9) and destructuring (line 22). This is dead code within the component. Either remove it from the props interface and destructuring, or re-add the L-Click button if it was removed unintentionally.

♻️ Proposed fix
 interface ControlBarProps {
 	scrollMode: boolean;
 	modifier: ModifierState;
 	buffer: string;
 	onToggleScroll: () => void;
-	onLeftClick: () => void;
 	onRightClick: () => void;
 	onKeyboardToggle: () => void;
 	onModifierToggle: () => void;
 	onPaste: () => void;
 	onCopy: () => void;
 }

 export const ControlBar: React.FC<ControlBarProps> = ({
 	scrollMode,
 	modifier,
 	buffer,
 	onToggleScroll,
-	onLeftClick,
 	onRightClick,
 	onKeyboardToggle,
 	onModifierToggle,
 	onPaste,
 	onCopy,
 }) => {

Note: The caller in trackpad.tsx (line 187) still passes onLeftClick={() => handleClick('left')} — that prop should be removed there as well.

Also applies to: 22-22

src/routes/trackpad.tsx (1)

187-187: 🛠️ Refactor suggestion | 🟠 Major

Remove the onLeftClick prop — it's no longer used by ControlBar.

The L-Click button was removed from ControlBar's render, so this prop is dead. Clean it up here as part of the same change.

♻️ Proposed fix
             <ControlBar
                 scrollMode={scrollMode}
                 modifier={modifier}
                 buffer={buffer.join(" + ")}
                 onToggleScroll={() => setScrollMode(!scrollMode)}
-                onLeftClick={() => handleClick('left')}
                 onRightClick={() => handleClick('right')}
                 onKeyboardToggle={focusInput}
                 onModifierToggle={handleModifierState}
                 onPaste={handlePaste}
                 onCopy={handleCopy}
             />
🤖 Fix all issues with AI agents
In `@src/hooks/useRemoteConnection.ts`:
- Around line 63-69: The send and sendCombo callbacks capture a stale React
state variable ws and can miss a freshly reconnected socket; update both
callbacks to reference the live socket reference (currentWs or a useRef like
currentWsRef) instead of the ws state, remove ws from their dependency arrays
(or depend on the ref), and check currentWs.readyState === WebSocket.OPEN before
sending so they always use the most recent connection established by the
reconnect logic (leave setWs as-is for state updates).
- Around line 42-51: The visibility handler handleVisibilityChange currently
calls connect() whenever currentWs.readyState !== WebSocket.OPEN, which can race
with the onclose reconnect timer and create duplicate sockets or abandon a
CONNECTING socket; change the condition to only reconnect when currentWs is
explicitly WebSocket.CLOSED (i.e., if (!currentWs || currentWs.readyState ===
WebSocket.CLOSED) { clearTimeout(reconnectTimer); connect(); }) so it won't
interfere with CONNECTING or CLOSING states—use the existing reconnectTimer
clear to still prevent duplicate onclose timers and reference currentWs,
reconnectTimer, connect, and handleVisibilityChange when applying the change.

In `@src/server/InputHandler.ts`:
- Around line 138-164: In the 'paste' and 'copy' cases inside InputHandler, the
modifier key (computed via Key.LeftControl or Key.LeftCmd) is pressed before
pressing V/C but not guaranteed to be released if keyboard.pressKey(Key.V) or
pressKey(Key.C) throws; update both cases to wrap the press/release sequence in
a try/finally so that after await keyboard.pressKey(modifier) you always call
keyboard.releaseKey(modifier) in the finally block (also ensure you still
press/release the letter key inside the try), mirroring the pattern used in the
zoom handler to prevent leaving the modifier stuck.

In `@src/server/websocket.ts`:
- Around line 67-69: The catch block in the message processing try/catch is
silently swallowing all errors; modify the catch after the receive/parse/handle
flow (where inputHandler.handleMessage is invoked) to distinguish JSON parse
errors from unexpected runtime errors: if the error is a SyntaxError from
JSON.parse, keep silent or log at trace, but for other errors (e.g., from
inputHandler.handleMessage or nut-js) log them at debug/error level including
the error object and context (the raw message or client id) so issues are
observable during debugging.
🧹 Nitpick comments (4)
src/components/Trackpad/ControlBar.tsx (2)

62-97: Add type="button" to all <button> elements.

As flagged by static analysis, buttons without an explicit type default to "submit", which can cause unintended form submissions. Add type="button" to each button.

♻️ Example fix (apply to all 6 buttons)
 <button
+	type="button"
 	className={`btn btn-sm ${scrollMode ? "btn-primary" : "btn-outline"}`}
 	onPointerDown={(e) => handleInteraction(e, onToggleScroll)}
 >

59-99: Unnecessary fragment wrapper.

The component returns a single <div> child. The <>...</> fragment on lines 60/99 serves no purpose and can be removed.

src/server/websocket.ts (1)

76-78: Silent onerror handler.

Same concern — completely silencing WebSocket errors makes production debugging difficult. A one-line log would be beneficial without being noisy.

🔧 Proposed fix
-        ws.onerror = () => {
-            // Silent
+        ws.onerror = (err) => {
+            console.error('[WS] Socket error:', err.message);
         };
src/server/InputHandler.ts (1)

140-141: Misleading variable name and duplicated logic.

isWin is true for both Windows and Linux, making the name misleading. Also, the paste and copy blocks are nearly identical — consider extracting a helper.

♻️ Extract a helper method
+    private async simulateShortcut(key: Key) {
+        const useCtrl = process.platform === 'win32' || process.platform === 'linux';
+        const modifier = useCtrl ? Key.LeftControl : Key.LeftCmd;
+
+        await keyboard.pressKey(modifier);
+        try {
+            await keyboard.pressKey(key);
+            await keyboard.releaseKey(key);
+        } finally {
+            await keyboard.releaseKey(modifier);
+        }
+    }

Then the cases become:

         case 'paste':
-            try { ... } catch { ... }
+            await this.simulateShortcut(Key.V);
             break;
         case 'copy':
-            try { ... } catch { ... }
+            await this.simulateShortcut(Key.C);
             break;

Also applies to: 154-155

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: 2

Caution

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

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

65-82: ⚠️ Potential issue | 🟡 Minor

Zoom handler uses Key.LeftControl unconditionally — won't zoom on macOS.

The paste and copy handlers correctly detect the platform and use Key.LeftCmd on macOS, but the zoom handler on line 76 always uses Key.LeftControl. On macOS, Ctrl+Scroll doesn't trigger system zoom in most apps — it's typically Cmd-based or handled differently. If zoom is intended to work cross-platform, this should mirror the platform detection logic from the copy/paste handlers.

Proposed fix
+                    const isWin = process.platform === 'win32' || process.platform === 'linux';
+                    const zoomModifier = isWin ? Key.LeftControl : Key.LeftCmd;
-                    await keyboard.pressKey(Key.LeftControl);
+                    await keyboard.pressKey(zoomModifier);
                     try {
                         await mouse.scrollDown(amount);
                     } finally {
-                        await keyboard.releaseKey(Key.LeftControl);
+                        await keyboard.releaseKey(zoomModifier);
                     }
🤖 Fix all issues with AI agents
In `@src/hooks/useRemoteConnection.ts`:
- Around line 57-61: The onclose handler in useRemoteConnection schedules a
reconnect (reconnectTimer = setTimeout(connect, 1000)) even during unmount,
causing state updates after unmount; add a disposed flag (e.g., const
disposedRef = useRef(false)) and set disposedRef.current = true in the cleanup
before calling currentWsRef.current?.close(), then update the onclose handler
(in useRemoteConnection) to guard all post-close actions (setStatus, setWs, and
scheduling reconnectTimer) with if (!disposedRef.current) { ... } so no
reconnect or state changes occur after unmount; keep clearing any existing
reconnectTimer as part of cleanup.

In `@src/server/InputHandler.ts`:
- Around line 26-32: The code in InputHandler.ts is using mouse.getPosition()
and constructing a new Point to perform relative moves, which bypasses the
library's helpers; replace the block inside the dx/dy handling so it no longer
calls mouse.getPosition() or constructs new Point, and instead call the nut-js
relative movement helpers via mouse.move(right(msg.dx)) and
mouse.move(down(msg.dy)) (use right/left/up/down as appropriate when dx/dy can
be negative) so the InputHandler mouse.move usage conforms to the library API
and remove the unnecessary position query.
🧹 Nitpick comments (2)
src/server/InputHandler.ts (1)

138-170: Reduce duplication between paste and copy handlers.

The two cases are identical except for the letter key (Key.V vs Key.C). Consider extracting a small helper to keep things DRY and easier to maintain.

♻️ Example helper extraction
+    private async performShortcut(letterKey: Key) {
+        const isWin = process.platform === 'win32' || process.platform === 'linux';
+        const modifier = isWin ? Key.LeftControl : Key.LeftCmd;
+
+        await keyboard.pressKey(modifier);
+        try {
+            await keyboard.pressKey(letterKey);
+            await keyboard.releaseKey(letterKey);
+        } finally {
+            await keyboard.releaseKey(modifier);
+        }
+    }

Then the cases become:

             case 'paste':
-                try {
-                    const isWin = process.platform === 'win32' || process.platform === 'linux';
-                    const modifier = isWin ? Key.LeftControl : Key.LeftCmd;
-                    
-                    await keyboard.pressKey(modifier);
-                    try {
-                        await keyboard.pressKey(Key.V);
-                        await keyboard.releaseKey(Key.V);
-                    } finally {
-                        await keyboard.releaseKey(modifier);
-                    }
-                } catch (error) {
-                    // Silent
-                }
+                try { await this.performShortcut(Key.V); } catch { /* silent */ }
                 break;

             case 'copy':
-                try {
-                    const isWin = process.platform === 'win32' || process.platform === 'linux';
-                    const modifier = isWin ? Key.LeftControl : Key.LeftCmd;
-                    
-                    await keyboard.pressKey(modifier);
-                    try {
-                        await keyboard.pressKey(Key.C);
-                        await keyboard.releaseKey(Key.C);
-                    } finally {
-                        await keyboard.releaseKey(modifier);
-                    }
-                } catch (error) {
-                    // Silent
-                }
+                try { await this.performShortcut(Key.C); } catch { /* silent */ }
                 break;
src/hooks/useRemoteConnection.ts (1)

84-84: Consider removing the unused ws return value to avoid API confusion.

The returned ws state can lag behind currentWsRef.current after reconnection since setWs() is asynchronous. While the hook correctly provides send() and sendCombo() functions that safely use the ref, exporting the ws socket directly creates confusion and risk for future consumers. The only current consumer (trackpad.tsx) explicitly does not destructure ws, indicating it's unused—remove it from the return value to clarify that external code should always use the provided send() and sendCombo() methods instead.

Comment on lines 26 to +32
if (msg.dx !== undefined && msg.dy !== undefined) {
// ========== OPTIMIZED: Use relative move ==========
const currentPos = await mouse.getPosition();

await mouse.setPosition(new Point(
currentPos.x + msg.dx,
currentPos.y + msg.dy
));
await mouse.move([
new Point(currentPos.x + msg.dx, currentPos.y + msg.dy)
]);
// ==================================================
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

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the InputHandler.ts file to see the context
cat -n src/server/InputHandler.ts | head -50

Repository: AOSSIE-Org/Rein

Length of output: 2177


🏁 Script executed:

# Check all uses of mouse.move in the project
rg -n "mouse\.move" --type=ts -C3

Repository: AOSSIE-Org/Rein

Length of output: 1095


🏁 Script executed:

# Look for the nut-tree-fork/nut-js library type definitions or documentation
fd -e "d.ts" -e "package.json" | grep -i nut | head -20

Repository: AOSSIE-Org/Rein

Length of output: 41


🏁 Script executed:

# Check if there's a node_modules directory with nut-tree-fork types
find . -type d -name "@nut-tree*" 2>/dev/null | head -5

Repository: AOSSIE-Org/Rein

Length of output: 41


🏁 Script executed:

# Check package.json for library version and imports
cat package.json | grep -A5 -B5 "nut-tree"

Repository: AOSSIE-Org/Rein

Length of output: 431


🏁 Script executed:

# Search for where right, down, left, up are imported/used
rg -n "import.*\b(right|down|left|up)\b" --type=ts

Repository: AOSSIE-Org/Rein

Length of output: 151


🏁 Script executed:

# Look for any documentation or comments about mouse.move API
rg -n "mouse\.move" -B2 -A2 --type=ts

Repository: AOSSIE-Org/Rein

Length of output: 832


🌐 Web query:

@nut-tree-fork/nut-js mouse.move API documentation relative movement

💡 Result:

mouse.move(...) in nut.js takes a path (Point[] / Promise<Point[]>) and moves the cursor along it; for relative movement, you typically pass one of the built-in relative movement helpers that generate a path from the current cursor position: left(x), right(x), up(x), down(x). Example: await mouse.move(left(10)). [1]

import { mouse, left, right, up, down } from "@nut-tree-fork/nut-js";

await mouse.move(left(100));
await mouse.move(up(50));
await mouse.move(right(100));
await mouse.move(down(50));

This “relative movement” API is documented as the intended way to move relative to the current position (vs. straightTo(point) for absolute targeting). [1][2]


🏁 Script executed:

# Check if there are more complex movement chains in the codebase
rg -n "\.move\(" --type=ts -B1 -A1

Repository: AOSSIE-Org/Rein

Length of output: 603


🏁 Script executed:

# Look for any chaining patterns with right, down, left, up
rg -n "(right|down|left|up)\(" --type=ts -C2

Repository: AOSSIE-Org/Rein

Length of output: 396


Use the library's relative movement helpers instead of manually calculating absolute positions.

The @nut-tree-fork/nut-js library provides right(), down(), left(), and up() helpers specifically for relative movement. The current code manually calculates an absolute position and passes a single-element Point array, which bypasses the intended API.

Replace:

const currentPos = await mouse.getPosition();
await mouse.move([
    new Point(currentPos.x + msg.dx, currentPos.y + msg.dy)
]);

With:

await mouse.move(right(msg.dx));
await mouse.move(down(msg.dy));

This follows the library's documented pattern (see test-input.ts for reference) and eliminates unnecessary position queries.

🤖 Prompt for AI Agents
In `@src/server/InputHandler.ts` around lines 26 - 32, The code in InputHandler.ts
is using mouse.getPosition() and constructing a new Point to perform relative
moves, which bypasses the library's helpers; replace the block inside the dx/dy
handling so it no longer calls mouse.getPosition() or constructs new Point, and
instead call the nut-js relative movement helpers via mouse.move(right(msg.dx))
and mouse.move(down(msg.dy)) (use right/left/up/down as appropriate when dx/dy
can be negative) so the InputHandler mouse.move usage conforms to the library
API and remove the unnecessary position query.

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