Skip to content

Add copy and paste shortcuts to trackpad toolbar#102

Open
Nakshatra480 wants to merge 1 commit intoAOSSIE-Org:mainfrom
Nakshatra480:feature/copy-paste-buttons
Open

Add copy and paste shortcuts to trackpad toolbar#102
Nakshatra480 wants to merge 1 commit intoAOSSIE-Org:mainfrom
Nakshatra480:feature/copy-paste-buttons

Conversation

@Nakshatra480
Copy link

@Nakshatra480 Nakshatra480 commented Feb 17, 2026

Closes #97

Summary

This PR wires up the existing Copy and Paste buttons in the trackpad toolbar so they send real keyboard shortcuts to the host machine. Previously these buttons were just placeholders with no functionality.

What’s changed

  • Extended ControlBar props to accept two new callbacks: onCopy and onPaste.
  • Hooked the Copy and Paste buttons to the existing handleInteraction helper in ControlBar, keeping behavior consistent with other buttons.
  • In TrackpadPage:
    • Added small helpers getCopyCombo() and getPasteCombo() that return ["control", "c"] and ["control", "v"].
    • Connected onCopy and onPaste to sendCombo(getCopyCombo()) and sendCombo(getPasteCombo()).

All keyboard events still go through the existing useRemoteConnection + sendCombo path and are handled on the server via the existing InputHandler and KEY_MAP.

Implementation details

  • Reused the existing WebSocket message shape (type: "combo") and combo handling logic on the server.
  • Kept the implementation minimal and localized to the trackpad route and toolbar:
    • No new dependencies.
    • No changes to server behavior or configuration.
  • Kept the code style and patterns consistent with the surrounding React components.

Testing

  • npm run build
    • Build completes successfully for client, SSR, and Nitro targets.
  • Manual checks:
    • Opened the remote trackpad.
    • Focused a text input on the host.
    • Verified:
      • Selecting text + pressing Copy sends Ctrl + C.
      • Moving the cursor and pressing Paste sends Ctrl + V.

Notes

  • The shortcuts currently use control for compatibility across platforms. If needed, a follow-up can make the modifier platform-aware (e.g. meta on macOS) without changing this UI wiring.

Summary by CodeRabbit

  • New Features

    • Copy and Paste buttons in the trackpad control bar are now functional and perform clipboard actions with the remote system.
  • Enhancements

    • Trackpad control bar wiring updated to sync clipboard contents for paste and return copied text from the remote.
    • Minor UI/layout adjustments to the control bar and behavior tweaks for modifier/key handling.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 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

Walkthrough

Adds clipboard copy/paste support across UI, client connection hook, WebSocket handling, and server input handling; ControlBar gets onCopy/onPaste props used to trigger clipboard actions via the remote connection.

Changes

Cohort / File(s) Summary
ControlBar component
src/components/Trackpad/ControlBar.tsx
Added public props onCopy: () => void and onPaste: () => void; Copy/Paste buttons now call these handlers on pointer down.
Trackpad route / wiring
src/routes/trackpad.tsx
Wired ControlBar to new handlers that call remote clipboard API (sendClipboard/sendCombo), passing current clipboardText for paste.
Remote connection hook
src/hooks/useRemoteConnection.ts
Added clipboardText state and sendClipboard(action, text?); updated sendCombo signature to accept readonly string[]; hook now returns { status, send, sendCombo, clipboardText, sendClipboard }.
Server input handling
src/server/InputHandler.ts
Added 'clipboard' InputMessage variant with `action?: 'copy'
WebSocket server
src/server/websocket.ts
Now captures handleMessage result and sends back {"type":"clipboard-content","text": ...} when a string is returned (clipboard content), enabling client-side clipboardText updates.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant ControlBar
    participant TrackpadPage
    participant RemoteConn
    participant WebSocket
    participant Server
    User->>ControlBar: pointer down "Copy"/"Paste"
    ControlBar->>TrackpadPage: onCopy() / onPaste()
    TrackpadPage->>RemoteConn: sendClipboard('copy') / sendClipboard('paste', clipboardText)
    RemoteConn->>WebSocket: WS send { type: "input", ... , clipboard/action }
    WebSocket->>Server: deliver message
    Server->>Server: handleMessage -> (simulate Ctrl/Cmd+C or write paste text)
    alt copy returns clipboard text
        Server-->>WebSocket: send { type: "clipboard-content", text }
        WebSocket-->>RemoteConn: WS message
        RemoteConn-->>TrackpadPage: update clipboardText
        TrackpadPage-->>ControlBar: (future paste uses clipboardText)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐇 I nibble keys and gently clap,
Copy hops out, Paste follows my map,
Messages scurry through wire and wood,
Bytes turned carrots, all working good,
A twitching tail, a successful tap!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

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.
Linked Issues check ❓ Inconclusive The PR implements copy and paste functionality, but uses keyboard shortcuts instead of the suggested non-key-combination approach with clipboard APIs, conflicting with #97's scalability preference. Address the divergence: consider either aligning implementation with #97's preference for clipboard-based approach or documenting why key combinations were chosen over the nutjs clipboard method.
✅ 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 'Add copy and paste shortcuts to trackpad toolbar' directly and accurately describes the primary change—implementing functional copy and paste buttons in the trackpad toolbar.
Out of Scope Changes check ✅ Passed All changes are directly aligned with implementing copy and paste functionality for the trackpad toolbar; no unrelated modifications detected.

✏️ 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.

🧹 Nitpick comments (1)
src/routes/trackpad.tsx (1)

156-162: Consider replacing helper functions with constants.

getCopyCombo and getPasteCombo return the same static arrays every time. Plain constants (e.g. at module or component scope) would be simpler and signal immutability more clearly.

♻️ Suggested simplification
-    const getCopyCombo = () => {
-        return ["control", "c"];
-    };
-
-    const getPasteCombo = () => {
-        return ["control", "v"];
-    };
+    const COPY_COMBO = ["control", "c"] as const;
+    const PASTE_COMBO = ["control", "v"] as const;

Then update the call sites:

-                onCopy={() => sendCombo(getCopyCombo())}
-                onPaste={() => sendCombo(getPasteCombo())}
+                onCopy={() => sendCombo([...COPY_COMBO])}
+                onPaste={() => sendCombo([...PASTE_COMBO])}

(Or pass directly if sendCombo doesn't mutate its argument.)

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

In `@src/routes/trackpad.tsx` around lines 156 - 162, Replace the two trivial
helper functions getCopyCombo and getPasteCombo with constant arrays (e.g.
COPY_COMBO and PASTE_COMBO) at module or component scope and update their call
sites (places that call getCopyCombo/getPasteCombo or pass them into sendCombo)
to use the constants directly; ensure you use frozen/readonly arrays if desired
to signal immutability and only replace calls where sendCombo or other code does
not mutate the array.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/routes/trackpad.tsx`:
- Around line 156-162: Replace the two trivial helper functions getCopyCombo and
getPasteCombo with constant arrays (e.g. COPY_COMBO and PASTE_COMBO) at module
or component scope and update their call sites (places that call
getCopyCombo/getPasteCombo or pass them into sendCombo) to use the constants
directly; ensure you use frozen/readonly arrays if desired to signal
immutability and only replace calls where sendCombo or other code does not
mutate the array.

@Nakshatra480 Nakshatra480 force-pushed the feature/copy-paste-buttons branch from 5f5978d to fc24303 Compare February 17, 2026 05:40
@Nakshatra480 Nakshatra480 marked this pull request as draft February 17, 2026 05:41
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: 3

🧹 Nitpick comments (2)
src/routes/trackpad.tsx (1)

156-157: Hoist COPY_COMBO and PASTE_COMBO outside the component.

These are static constants and don't depend on props, state, or closures. Defining them inside the component body means they're re-created on every render. Move them to module scope.

Proposed fix
+const COPY_COMBO = ["control", "c"] as const;
+const PASTE_COMBO = ["control", "v"] as const;
+
 function TrackpadPage() {
     ...
-    const COPY_COMBO = ["control", "c"] as const;
-    const PASTE_COMBO = ["control", "v"] as const;
-
     return (
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/routes/trackpad.tsx` around lines 156 - 157, COPY_COMBO and PASTE_COMBO
are defined inside the component and are recreated every render; hoist them to
module scope by moving the const declarations for COPY_COMBO and PASTE_COMBO out
of the Trackpad component (or whatever component function contains them) to the
top of the file (near imports) so they become true static constants and are not
redefined on each render.
src/components/Trackpad/ControlBar.tsx (1)

95-102: Remove commented-out L-Click button.

Commented-out code adds noise. If this removal is intentional, delete it entirely; if it's temporary, track it with an issue instead.

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

In `@src/components/Trackpad/ControlBar.tsx` around lines 95 - 102, Remove the
commented-out L-Click button markup from the ControlBar component to eliminate
dead code noise: delete the entire commented block that contains the button
using className "btn btn-sm btn-outline" and the onPointerDown handler that
calls handleInteraction with onLeftClick so only active UI remains; if this was
meant to be tracked, create an issue instead of leaving the commented code.
🤖 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/components/Trackpad/ControlBar.tsx`:
- Line 8: The ControlBar component's latency prop is currently typed as number |
null but upstream may pass undefined; update the prop type for latency in
ControlBar (the latency prop/interface) to number | null | undefined or add a
default (e.g., normalize undefined to null inside the ControlBar props handling)
so the component accepts the hook's initial undefined value; adjust any
consumers or destructuring in ControlBar to handle undefined safely (use
null-coalescing or guards) and keep types consistent with the upstream hook that
provides latency.
- Around line 77-120: Buttons in ControlBar.tsx are missing explicit type
attributes which defaults them to "submit" and can trigger form submissions;
update every <button> element that uses handleInteraction with handlers
onToggleScroll, onCopy, onPaste, onRightClick, onModifierToggle, and
onKeyboardToggle to include type="button" (also for the modifier button that
uses getModifierButtonClass/getModifierLabel) so none of these control buttons
act as form submitters.

In `@src/routes/trackpad.tsx`:
- Line 36: The trackpad.tsx runtime bug comes from destructuring a non-existent
latency from useRemoteConnection; remove latency from the destructure (keep
const { status, send, sendCombo } = useRemoteConnection()) and pass a safe
fallback to ControlBar (e.g., latency={null}) until you implement latency
tracking, or alternatively implement latency measurement inside
useRemoteConnection (add a latency state, measure ping in useRemoteConnection,
and return latency alongside status/send/sendCombo) and update the hook
signature accordingly.

---

Nitpick comments:
In `@src/components/Trackpad/ControlBar.tsx`:
- Around line 95-102: Remove the commented-out L-Click button markup from the
ControlBar component to eliminate dead code noise: delete the entire commented
block that contains the button using className "btn btn-sm btn-outline" and the
onPointerDown handler that calls handleInteraction with onLeftClick so only
active UI remains; if this was meant to be tracked, create an issue instead of
leaving the commented code.

In `@src/routes/trackpad.tsx`:
- Around line 156-157: COPY_COMBO and PASTE_COMBO are defined inside the
component and are recreated every render; hoist them to module scope by moving
the const declarations for COPY_COMBO and PASTE_COMBO out of the Trackpad
component (or whatever component function contains them) to the top of the file
(near imports) so they become true static constants and are not redefined on
each render.

@Nakshatra480 Nakshatra480 force-pushed the feature/copy-paste-buttons branch from fc24303 to c5c9959 Compare February 17, 2026 05:47
@Nakshatra480 Nakshatra480 marked this pull request as ready for review February 17, 2026 05:48
@Nakshatra480 Nakshatra480 force-pushed the feature/copy-paste-buttons branch from c5c9959 to 22263a0 Compare February 17, 2026 05:56
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.

🧹 Nitpick comments (1)
src/routes/trackpad.tsx (1)

15-16: Consider marking combo constants as const for immutability.

These module-level arrays are intended to be fixed values. Adding as const prevents accidental mutation and narrows the type.

Suggested change
-const COPY_COMBO = ["control", "c"];
-const PASTE_COMBO = ["control", "v"];
+const COPY_COMBO = ["control", "c"] as const;
+const PASTE_COMBO = ["control", "v"] as const;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/routes/trackpad.tsx` around lines 15 - 16, COPY_COMBO and PASTE_COMBO are
declared as mutable arrays; change them to readonly tuple literal types by
appending "as const" to each declaration so they become immutable and their
types are narrowed (e.g., COPY_COMBO and PASTE_COMBO should be declared as
["control","c"] as const and ["control","v"] as const), update any code that
expects a mutable array if necessary to use readonly accessors.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/routes/trackpad.tsx`:
- Around line 15-16: COPY_COMBO and PASTE_COMBO are declared as mutable arrays;
change them to readonly tuple literal types by appending "as const" to each
declaration so they become immutable and their types are narrowed (e.g.,
COPY_COMBO and PASTE_COMBO should be declared as ["control","c"] as const and
["control","v"] as const), update any code that expects a mutable array if
necessary to use readonly accessors.

@Nakshatra480 Nakshatra480 force-pushed the feature/copy-paste-buttons branch from 22263a0 to 30878cc Compare February 17, 2026 06:00
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.

🧹 Nitpick comments (1)
src/routes/trackpad.tsx (1)

15-16: Clean constant definitions; note macOS caveat.

Using as const here is good — it produces readonly tuples that align with the updated sendCombo signature. As acknowledged in the PR description, "control" won't trigger copy/paste on macOS (needs "meta"). A small helper like getPlatformModifier() checking navigator.platform or navigator.userAgentData would be a natural follow-up.

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

In `@src/routes/trackpad.tsx` around lines 15 - 16, COPY_COMBO and PASTE_COMBO are
defined with a hardcoded "control" modifier which won't trigger copy/paste on
macOS; add a small helper (e.g., getPlatformModifier) that detects platform via
navigator.platform or navigator.userAgentData and returns "meta" for macOS and
"control" otherwise, then replace uses of the hardcoded tuples (COPY_COMBO,
PASTE_COMBO) or compute them using that helper so the tuples passed to sendCombo
match the platform-specific modifier; ensure the tuples remain readonly to match
sendCombo's signature.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/routes/trackpad.tsx`:
- Around line 15-16: COPY_COMBO and PASTE_COMBO are defined with a hardcoded
"control" modifier which won't trigger copy/paste on macOS; add a small helper
(e.g., getPlatformModifier) that detects platform via navigator.platform or
navigator.userAgentData and returns "meta" for macOS and "control" otherwise,
then replace uses of the hardcoded tuples (COPY_COMBO, PASTE_COMBO) or compute
them using that helper so the tuples passed to sendCombo match the
platform-specific modifier; ensure the tuples remain readonly to match
sendCombo's signature.

@Nakshatra480 Nakshatra480 force-pushed the feature/copy-paste-buttons branch from 30878cc to b6fe2b2 Compare February 17, 2026 06:07
@Nakshatra480
Copy link
Author

Hi @imxade the copy and paste shortcuts are properly working now, can you pls review this PR.
Thanks

@PinJinx
Copy link
Contributor

PinJinx commented Feb 18, 2026

@Nakshatra480
As we discussed on the server, could you handle this functionality on the backend using the nutjs clipboard component?

Sending key combinations might trigger unintended scenarios. For example, pressing copy in a terminal (Ctrl + C) could terminate a running process.

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

🧹 Nitpick comments (2)
src/hooks/useRemoteConnection.ts (1)

48-55: Consider guarding setClipboardText with isMounted.

The onopen handler (line 33) checks isMounted before updating state, but onmessage does not. If a message arrives in the narrow window between cleanup setting isMounted = false and the socket being closed, this could trigger a state update after unmount. Additionally, cleanup nullifies onopen/onclose/onerror but not onmessage.

Not a practical issue in React 18 (no warning), but worth aligning for consistency.

Suggested fix
             socket.onmessage = (event) => {
                 try {
                     const data = JSON.parse(event.data);
-                    if (data.type === 'clipboard-content' && typeof data.text === 'string') {
+                    if (isMounted && data.type === 'clipboard-content' && typeof data.text === 'string') {
                         setClipboardText(data.text);
                     }
                 } catch { /* ignore non-JSON or irrelevant messages */ }
             };

And in the cleanup (around line 68), add wsRef.current.onmessage = null; alongside the other handler nullifications.

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

In `@src/hooks/useRemoteConnection.ts` around lines 48 - 55, The onmessage handler
may call setClipboardText after unmount; update socket.onmessage to check the
existing isMounted flag before calling setClipboardText (same pattern used in
the onopen handler) and ensure the cleanup also nulls the handler by setting
wsRef.current.onmessage = null alongside onopen/onclose/onerror so no late
messages can trigger state updates; reference socket.onmessage,
setClipboardText, isMounted and wsRef.current.onmessage to locate and modify the
code.
src/server/websocket.ts (1)

80-80: No validation on the action field before passing to InputHandler.

The msg is cast to InputMessage without verifying that action is actually 'copy' or 'paste' for clipboard-type messages. A malformed message like { type: 'clipboard', action: 'drop-tables' } reaches handleMessage and falls through harmlessly (no-op), but more dangerous is a message like { type: 'clipboard', action: 'paste', text: '<huge string>' } — the clipboard.setContent() call has no size bound. Consider adding basic validation at the WebSocket layer, especially for the clipboard type.

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

In `@src/server/websocket.ts` at line 80, The WebSocket message is cast to
InputMessage without validating the clipboard-specific fields; update the
message handling in websocket.ts (before calling inputHandler.handleMessage) to
validate that for messages with type === 'clipboard' the action is one of 'copy'
or 'paste' and that for 'paste' the text length is bounded (introduce a
MAX_CLIPBOARD_LENGTH constant and reject/trim messages exceeding it); return an
error or ignore invalid clipboard messages instead of passing malformed input to
inputHandler.handleMessage (reference InputMessage, the clipboard type/action
values, inputHandler.handleMessage, and clipboard.setContent when applying the
size check).
🤖 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 182-183: The paste handler currently re-sends the server-provided
clipboardText by calling sendClipboard('paste', clipboardText), which prevents
pasting content copied on the mobile device; change the onPaste flow in the
Trackpad component to read the device clipboard via
navigator.clipboard.readText() (fallback to clipboardText if unavailable or
permission denied) and then call sendClipboard('paste', <readText>); update any
related logic around sendClipboard and clipboardText (e.g., the onCopy handler
and state usage) to treat clipboardText as the server-origin copy cache only,
not the primary source for paste operations.

In `@src/server/InputHandler.ts`:
- Around line 160-161: Replace the fragile fixed await new Promise(resolve =>
setTimeout(resolve, 100)) delay with a short polling loop that reads
clipboard.getContent repeatedly until it changes or a timeout/maximum attempts
is reached: before triggering the simulated Ctrl+C, capture initialContent =
await clipboard.getContent(); after triggering the copy, poll in a loop (e.g.,
sleep 30–100ms between attempts) calling clipboard.getContent() and compare to
initialContent, returning as soon as it differs or when maxAttempts/timeout
elapses (return the last value); update the code in InputHandler around the
clipboard.getContent usage to implement this retry/polling strategy and add a
brief comment documenting the known limitation if the clipboard never updates.
- Around line 155-170: In the 'clipboard' branch of InputHandler.ts (case
'clipboard'), wrap the key-press operations in try/finally so modifier and
letter keys are always released even if clipboard.getContent() or
clipboard.setContent() throws: call keyboard.pressKey(modifier, Key.C/V) then in
a try block perform clipboard.getContent() (for copy) or clipboard.setContent()
and any paste logic, capture the copy result before finally, and in the finally
block call keyboard.releaseKey(modifier, Key.C/V) to release both keys; do the
same pattern for the paste path (press then try the clipboard operation, finally
release), mirroring the try/finally protection used in the existing combo
handling.

---

Duplicate comments:
In `@src/components/Trackpad/ControlBar.tsx`:
- Around line 13-14: The new onCopy and onPaste props are wired correctly but
the toolbar's <button> elements (including those that call onCopy, onPaste and
other handlers via handleInteraction in ControlBar) are missing explicit
type="button", which can cause unexpected form submissions; update every
<button> in the ControlBar component (the elements that invoke onCopy, onPaste
and those using handleInteraction) to include type="button". Ensure you add
type="button" to the buttons around the copy/paste handlers and the other
toolbar buttons referenced in the same component so all buttons behave as
non-submit controls.

---

Nitpick comments:
In `@src/hooks/useRemoteConnection.ts`:
- Around line 48-55: The onmessage handler may call setClipboardText after
unmount; update socket.onmessage to check the existing isMounted flag before
calling setClipboardText (same pattern used in the onopen handler) and ensure
the cleanup also nulls the handler by setting wsRef.current.onmessage = null
alongside onopen/onclose/onerror so no late messages can trigger state updates;
reference socket.onmessage, setClipboardText, isMounted and
wsRef.current.onmessage to locate and modify the code.

In `@src/server/websocket.ts`:
- Line 80: The WebSocket message is cast to InputMessage without validating the
clipboard-specific fields; update the message handling in websocket.ts (before
calling inputHandler.handleMessage) to validate that for messages with type ===
'clipboard' the action is one of 'copy' or 'paste' and that for 'paste' the text
length is bounded (introduce a MAX_CLIPBOARD_LENGTH constant and reject/trim
messages exceeding it); return an error or ignore invalid clipboard messages
instead of passing malformed input to inputHandler.handleMessage (reference
InputMessage, the clipboard type/action values, inputHandler.handleMessage, and
clipboard.setContent when applying the size check).

Comment on lines 182 to 183
onCopy={() => sendClipboard('copy')}
onPaste={() => sendClipboard('paste', clipboardText)}
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

Paste sends server-side clipboard content, not the mobile device's clipboard.

The current flow round-trips clipboard through the server:

  1. Copy: server simulates Ctrl+C → reads host clipboard → sends content to client → stored in clipboardText
  2. Paste: client sends clipboardText back to server → writes to host clipboard → simulates Ctrl+V

This means "paste" can only paste what was previously "copied" through this app — not content from the mobile device's own clipboard. If the user copies text on their phone (outside the app) and taps Paste, the host clipboard gets overwritten with stale clipboardText (or empty string on first use).

Consider using the Clipboard API (navigator.clipboard.readText()) to read from the mobile device's clipboard on paste, which would make the feature more intuitive. This aligns with the reviewer comment in issue #97 about using the nut-js clipboard component more directly.

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

In `@src/routes/trackpad.tsx` around lines 182 - 183, The paste handler currently
re-sends the server-provided clipboardText by calling sendClipboard('paste',
clipboardText), which prevents pasting content copied on the mobile device;
change the onPaste flow in the Trackpad component to read the device clipboard
via navigator.clipboard.readText() (fallback to clipboardText if unavailable or
permission denied) and then call sendClipboard('paste', <readText>); update any
related logic around sendClipboard and clipboardText (e.g., the onCopy handler
and state usage) to treat clipboardText as the server-origin copy cache only,
not the primary source for paste operations.

Comment on lines 155 to 170
case 'clipboard': {
const modifier = process.platform === 'darwin' ? Key.LeftSuper : Key.LeftControl;
if (msg.action === 'copy') {
await keyboard.pressKey(modifier, Key.C);
await keyboard.releaseKey(modifier, Key.C);
await new Promise(resolve => setTimeout(resolve, 100));
return await clipboard.getContent();
} else if (msg.action === 'paste') {
if (msg.text) {
await clipboard.setContent(msg.text);
}
await keyboard.pressKey(modifier, Key.V);
await keyboard.releaseKey(modifier, Key.V);
}
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

Missing try/finally for key release — modifier keys can get stuck.

If keyboard.releaseKey or clipboard.getContent()/clipboard.setContent() throws, the modifier key (and C/V) remain pressed on the host machine. The existing combo case (lines 134-149) already guards against this with try/finally. The clipboard case should do the same.

Proposed fix
         case 'clipboard': {
             const modifier = process.platform === 'darwin' ? Key.LeftSuper : Key.LeftControl;
             if (msg.action === 'copy') {
-                await keyboard.pressKey(modifier, Key.C);
-                await keyboard.releaseKey(modifier, Key.C);
-                await new Promise(resolve => setTimeout(resolve, 100));
-                return await clipboard.getContent();
+                try {
+                    await keyboard.pressKey(modifier, Key.C);
+                } finally {
+                    await keyboard.releaseKey(modifier, Key.C);
+                }
+                await new Promise(resolve => setTimeout(resolve, 100));
+                return await clipboard.getContent();
             } else if (msg.action === 'paste') {
                 if (msg.text) {
                     await clipboard.setContent(msg.text);
                 }
-                await keyboard.pressKey(modifier, Key.V);
-                await keyboard.releaseKey(modifier, Key.V);
+                try {
+                    await keyboard.pressKey(modifier, Key.V);
+                } finally {
+                    await keyboard.releaseKey(modifier, Key.V);
+                }
             }
             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 155 - 170, In the 'clipboard' branch
of InputHandler.ts (case 'clipboard'), wrap the key-press operations in
try/finally so modifier and letter keys are always released even if
clipboard.getContent() or clipboard.setContent() throws: call
keyboard.pressKey(modifier, Key.C/V) then in a try block perform
clipboard.getContent() (for copy) or clipboard.setContent() and any paste logic,
capture the copy result before finally, and in the finally block call
keyboard.releaseKey(modifier, Key.C/V) to release both keys; do the same pattern
for the paste path (press then try the clipboard operation, finally release),
mirroring the try/finally protection used in the existing combo handling.

Comment on lines 160 to 161
await new Promise(resolve => setTimeout(resolve, 100));
return await clipboard.getContent();
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 | 🟡 Minor

The 100ms delay before reading clipboard is a fragile heuristic.

After simulating Ctrl+C, the target application writes to the clipboard asynchronously. 100ms may not be sufficient for large selections or slow applications, leading to stale/empty clipboard reads. There's no reliable way to "wait until clipboard changes," but consider:

  • Polling the clipboard a few times with short intervals until content changes (or a timeout is reached).
  • Documenting this known limitation.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/server/InputHandler.ts` around lines 160 - 161, Replace the fragile fixed
await new Promise(resolve => setTimeout(resolve, 100)) delay with a short
polling loop that reads clipboard.getContent repeatedly until it changes or a
timeout/maximum attempts is reached: before triggering the simulated Ctrl+C,
capture initialContent = await clipboard.getContent(); after triggering the
copy, poll in a loop (e.g., sleep 30–100ms between attempts) calling
clipboard.getContent() and compare to initialContent, returning as soon as it
differs or when maxAttempts/timeout elapses (return the last value); update the
code in InputHandler around the clipboard.getContent usage to implement this
retry/polling strategy and add a brief comment documenting the known limitation
if the clipboard never updates.

@Nakshatra480 Nakshatra480 force-pushed the feature/copy-paste-buttons branch from aba46b3 to db8292f Compare February 18, 2026 09:26
@Nakshatra480 Nakshatra480 marked this pull request as draft February 18, 2026 10:04
@PinJinx
Copy link
Contributor

PinJinx commented Feb 18, 2026

@Nakshatra480 I’ve reviewed your PR and the functionality looks good 👍
Just update your branch with the recent commits and fix any issues that may arise. Also Currently there are two PRs addressing the same issue, please coordinate with @b-u-g-g so we can merge a single PR for addressing this feature.

@Nakshatra480 Nakshatra480 force-pushed the feature/copy-paste-buttons branch from db8292f to 8863ae0 Compare February 18, 2026 15:03
- Add clipboard case to InputHandler with platform-aware modifier keys
- Use try/finally to prevent stuck keys on copy and paste operations
- Poll clipboard for changes after copy (up to 500ms)
- Validate clipboard messages and enforce 1MB text limit in websocket
- Forward clipboard content from server to client via WebSocket
- Add clipboardText state and sendClipboard to useRemoteConnection hook
- Wire onCopy/onPaste handlers in ControlBar and trackpad page
- Add type='button' to all ControlBar buttons
- Read device clipboard on paste with server-cache fallback

Closes AOSSIE-Org#97
@Nakshatra480 Nakshatra480 force-pushed the feature/copy-paste-buttons branch from 8863ae0 to 8cb10d4 Compare February 18, 2026 15:09
@Nakshatra480 Nakshatra480 marked this pull request as ready for review February 18, 2026 15:10
@Nakshatra480
Copy link
Author

@Nakshatra480 I’ve reviewed your PR and the functionality looks good 👍 Just update your branch with the recent commits and fix any issues that may arise. Also Currently there are two PRs addressing the same issue, please coordinate with @b-u-g-g so we can merge a single PR for addressing this feature.

I have resolved the merge conflicts, now review the PR.
thanks

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

2 participants