Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 71 additions & 46 deletions src/components/Trackpad/ControlBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ interface ControlBarProps {
scrollMode: boolean;
modifier: ModifierState;
buffer: string;
latency: number | null;
onToggleScroll: () => void;
onLeftClick: () => void;
onRightClick: () => void;
onKeyboardToggle: () => void;
onModifierToggle: () => void;
Expand All @@ -16,8 +16,8 @@ export const ControlBar: React.FC<ControlBarProps> = ({
scrollMode,
modifier,
buffer,
latency,
onToggleScroll,
onLeftClick,
onRightClick,
onKeyboardToggle,
onModifierToggle,
Expand Down Expand Up @@ -52,50 +52,75 @@ export const ControlBar: React.FC<ControlBarProps> = ({
}
};

const getLatencyColor = (ms: number) => {
if (ms < 50) return "text-success";
if (ms < 150) return "text-warning";
return "text-error";
};
return (
<div className="bg-base-200 p-2 grid grid-cols-5 gap-2 shrink-0">
<button
className={`btn btn-sm ${scrollMode ? "btn-primary" : "btn-outline"}`}
onPointerDown={(e) => handleInteraction(e, onToggleScroll)}
>
{scrollMode ? "Scroll" : "Cursor"}
</button>
<button
className="btn btn-sm btn-outline"
>
Copy
</button>
<button
className="btn btn-sm btn-outline"
>
Paste
</button>
{/*
<button
className="btn btn-sm btn-outline"
onPointerDown={(e) => handleInteraction(e, onLeftClick)}
>
L-Click
</button>
*/}
<button
className="btn btn-sm btn-outline"
onPointerDown={(e) => handleInteraction(e, onRightClick)}
>
R-Click
</button>
<button
className={`btn btn-sm ${getModifierButtonClass()}`}
onPointerDown={(e) => handleInteraction(e, onModifierToggle)}
>
{getModifierLabel()}
</button>
<button
className="btn btn-sm btn-secondary"
onPointerDown={(e) => handleInteraction(e, onKeyboardToggle)}
>
Keyboard
</button>
<div className="bg-base-200 p-2 shrink-0">
<div className="flex justify-between items-center mb-2 px-1">
<span className="text-xs font-mono opacity-70">
{latency !== null ? (
<span className={getLatencyColor(latency)}>Ping: {latency}ms</span>
) : (
"Ping: ---"
)}
</span>
</div>
<div className="grid grid-cols-6 gap-2">
<button
type="button"
className={`btn btn-sm ${scrollMode ? "btn-primary" : "btn-outline"}`}
onPointerDown={(e) => handleInteraction(e, onToggleScroll)}
>
{scrollMode ? "Scroll" : "Cursor"}
</button>
<button
type="button"
className="btn btn-sm btn-outline btn-disabled"
disabled
>
Copy
</button>
<button
type="button"
className="btn btn-sm btn-outline btn-disabled"
disabled
>
Paste
</button>
{/*
<button
type="button"
className="btn btn-sm btn-outline"
onPointerDown={(e) => handleInteraction(e, onLeftClick)}
>
L-Click
</button>
*/}
<button
type="button"
className="btn btn-sm btn-outline"
onPointerDown={(e) => handleInteraction(e, onRightClick)}
>
R-Click
</button>
<button
type="button"
className={`btn btn-sm ${getModifierButtonClass()}`}
onPointerDown={(e) => handleInteraction(e, onModifierToggle)}
>
{getModifierLabel()}
</button>
<button
type="button"
className="btn btn-sm btn-secondary"
onPointerDown={(e) => handleInteraction(e, onKeyboardToggle)}
>
Keyboard
</button>
</div>
</div>
);
};
};
192 changes: 112 additions & 80 deletions src/hooks/useRemoteConnection.ts
Original file line number Diff line number Diff line change
@@ -1,83 +1,115 @@
import { useState, useEffect, useCallback, useRef } from 'react';
import { useState, useEffect, useCallback, useRef } from "react";
import type { RemoteMessage } from "@/types";

export const useRemoteConnection = () => {
const wsRef = useRef<WebSocket | null>(null);
const [status, setStatus] = useState<'connecting' | 'connected' | 'disconnected'>('disconnected');

useEffect(() => {
let isMounted = true;
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const host = window.location.host;
const wsUrl = `${protocol}//${host}/ws`;

let reconnectTimer: NodeJS.Timeout;

const connect = () => {
if (!isMounted) return;

// Close any existing socket before creating a new one
if (wsRef.current) {
wsRef.current.onopen = null;
wsRef.current.onclose = null;
wsRef.current.onerror = null;
wsRef.current.close();
wsRef.current = null;
}

console.log(`Connecting to ${wsUrl}`);
setStatus('connecting');
const socket = new WebSocket(wsUrl);

socket.onopen = () => {
if (isMounted) setStatus('connected');
};
socket.onclose = () => {
if (isMounted) {
setStatus('disconnected');
reconnectTimer = setTimeout(connect, 3000);
}
};
socket.onerror = (e) => {
console.error("WS Error", e);
socket.close();
};

wsRef.current = socket;
};

// Defer to next tick so React Strict Mode's immediate unmount
// sets isMounted=false before any socket is created
const initialTimer = setTimeout(connect, 0);

return () => {
isMounted = false;
clearTimeout(initialTimer);
clearTimeout(reconnectTimer);
if (wsRef.current) {
// Nullify handlers to prevent cascading error/close events
wsRef.current.onopen = null;
wsRef.current.onclose = null;
wsRef.current.onerror = null;
wsRef.current.close();
wsRef.current = null;
}
};
}, []);

const send = useCallback((msg: any) => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
wsRef.current.send(JSON.stringify(msg));
}
}, []);

const sendCombo = useCallback((msg: string[]) => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
wsRef.current.send(JSON.stringify({
type: "combo",
keys: msg,
}));
}
}, []);

return { status, send, sendCombo };
const wsRef = useRef<WebSocket | null>(null);
const [status, setStatus] = useState<"connecting" | "connected" | "disconnected">("disconnected");
const [latency, setLatency] = useState<number | null>(null);

useEffect(() => {
let isMounted = true;
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const host = window.location.host;
const wsUrl = `${protocol}//${host}/ws`;

let reconnectTimer: NodeJS.Timeout;
let heartbeatTimer: NodeJS.Timeout;
let reconnectDelay = 1000;
const MAX_RECONNECT_DELAY = 30000;

const connect = () => {
if (!isMounted) return;

if (wsRef.current) {
wsRef.current.onopen = null;
wsRef.current.onclose = null;
wsRef.current.onerror = null;
wsRef.current.close();
wsRef.current = null;
}

console.log(`Connecting to ${wsUrl}`);
setStatus("connecting");
const socket = new WebSocket(wsUrl);
wsRef.current = socket;

socket.onopen = () => {
if (!isMounted) return;
setStatus("connected");
reconnectDelay = 1000;
heartbeatTimer = setInterval(() => {
if (socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({ type: "ping", timestamp: Date.now() }));
}
}, 3000);
};

socket.onmessage = (event) => {
try {
const msg = JSON.parse(event.data);
if (msg.type === "pong") {
const ts = Number(msg.timestamp);
const rtt = Date.now() - ts;
if (Number.isFinite(ts) && Number.isFinite(rtt) && rtt >= 0 && rtt < 60000) {
setLatency(rtt);
}
}
} catch (e) {
console.error("Error parsing WS message", e);
}
};

socket.onclose = () => {
if (!isMounted) return;
setStatus("disconnected");
setLatency(null);
wsRef.current = null;
clearInterval(heartbeatTimer);
const delay = reconnectDelay;
reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_DELAY);
reconnectTimer = setTimeout(connect, delay);
};

socket.onerror = (e) => {
console.error("WS Error", e);
socket.close();
};
};

const initialTimer = setTimeout(connect, 0);

return () => {
isMounted = false;
clearTimeout(initialTimer);
clearTimeout(reconnectTimer);
clearInterval(heartbeatTimer);

if (wsRef.current) {
wsRef.current.onopen = null;
wsRef.current.onclose = null;
wsRef.current.onerror = null;
wsRef.current.close();
wsRef.current = null;
}
};
}, []);

const send = useCallback((msg: RemoteMessage) => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
wsRef.current.send(JSON.stringify(msg));
}
}, []);

const sendCombo = useCallback((msg: string[]) => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
wsRef.current.send(
JSON.stringify({
type: "combo",
keys: msg,
}),
);
}
}, []);
Comment on lines +103 to +112
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, let's find and examine the RemoteMessage type definition
echo "=== Looking for RemoteMessage type definition ==="
rg -n --type=ts --type=tsx "type RemoteMessage|export type RemoteMessage" -A 10

echo -e "\n=== Checking sendCombo implementation in useRemoteConnection.ts ==="
sed -n '103,112p' src/hooks/useRemoteConnection.ts

echo -e "\n=== Searching for all 'combo' references in the codebase ==="
rg -n '"combo"' --type=ts --type=tsx

echo -e "\n=== Searching for RemoteMessage usage context ==="
rg -n "RemoteMessage" --type=ts --type=tsx -B 2 -A 2

Repository: AOSSIE-Org/Rein

Length of output: 604


🏁 Script executed:

#!/bin/bash
# Find the types file
echo "=== Locating types file ==="
find . -name "types.*" -type f | grep -E "\.(ts|tsx|js|jsx)$"

echo -e "\n=== Searching for RemoteMessage definition ==="
rg -n "type RemoteMessage|RemoteMessage =" -A 15

echo -e "\n=== Searching for combo references ==="
rg -n "combo" -C 2

echo -e "\n=== Examining src/types.tsx or similar ==="
if [ -f src/types.tsx ]; then
  cat -n src/types.tsx
elif [ -f src/types.ts ]; then
  cat -n src/types.ts
fi

Repository: AOSSIE-Org/Rein

Length of output: 3073


sendCombo sends "combo" type that is missing from the RemoteMessage union.

The sendCombo function constructs and sends { type: "combo", keys: msg } directly, bypassing the RemoteMessage type defined in src/types.tsx, which only includes: move, scroll, click, key, text, zoom. Meanwhile, the server-side InputMessage in src/server/InputHandler.ts correctly includes "combo" in its type union (line 5) and handles it (lines 110–128).

Add "combo" to RemoteMessage in src/types.tsx to align client and server types:

Suggested type addition
export type RemoteMessage =
	| { type: "move"; dx: number; dy: number }
	| { type: "scroll"; dx?: number; dy?: number }
	| { type: "click"; button: "left" | "right" | "middle"; press: boolean }
	| { type: "key"; key: string }
	| { type: "text"; text: string }
	| { type: "zoom"; delta: number }
	| { type: "combo"; keys: string[] };
🤖 Prompt for AI Agents
In `@src/hooks/useRemoteConnection.ts` around lines 103 - 112, The client sends
messages with type "combo" from sendCombo but RemoteMessage in src/types.tsx
lacks that variant; add a new union member {| type: "combo"; keys: string[] |}
to RemoteMessage so it matches the server-side InputMessage and satisfies
TypeScript checks (update any imports/usages of RemoteMessage if needed, e.g.,
where sendCombo or WebSocket message typing is validated). Ensure the added
variant follows the same naming and shape used in sendCombo so
serialization/handlers remain consistent.


return { status, latency, send, sendCombo };
};
3 changes: 2 additions & 1 deletion src/hooks/useTrackpadGesture.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useRef, useState } from 'react';
import { TOUCH_MOVE_THRESHOLD, TOUCH_TIMEOUT, PINCH_THRESHOLD, calculateAccelerationMult } from '../utils/math';
import type { RemoteMessage } from '@/types';

interface TrackedTouch {
identifier: number;
Expand All @@ -17,7 +18,7 @@ const getTouchDistance = (a: TrackedTouch, b: TrackedTouch): number => {
};

export const useTrackpadGesture = (
send: (msg: any) => void,
send: (msg: RemoteMessage) => void,
scrollMode: boolean,
sensitivity: number = 1.5,
invertScroll: boolean = false,
Expand Down
8 changes: 5 additions & 3 deletions src/routes/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,17 +104,18 @@ function SettingsPage() {
{/* Left Column: Settings Form */}
<div className="w-full flex-1 space-y-8">
<div className="form-control w-full">
<label className="label">
<label className="label" htmlFor="ip-input">
<span className="label-text">Server IP (for Remote)</span>
</label>
<input
type="text"
id="ip-input"
placeholder="192.168.1.X"
className="input input-bordered w-full"
value={ip}
onChange={(e) => setIp(e.target.value)}
/>
<label className="label">
<label className="label" htmlFor="ip-input">
<span className="label-text-alt opacity-50">This Computer's LAN IP</span>
</label>
</div>
Expand Down Expand Up @@ -163,11 +164,12 @@ function SettingsPage() {
</div>

<div className="form-control w-full">
<label className="label">
<label className="label" htmlFor="port-input">
<span className="label-text">Port</span>
</label>
<input
type="text"
id="port-input"
placeholder={String(CONFIG.FRONTEND_PORT)}
className="input input-bordered w-full"
value={frontendPort}
Expand Down
Loading