Skip to content

Commit 06b5eab

Browse files
mcuelenaereclaude
andcommitted
feat(ui): send mouse wheel via binary HID-RPC when available
Mirror the existing rel/abs mouse pattern: when the HID-RPC binary channel is ready, send a WheelReportMessage (0x04) over the unreliable-ordered channel instead of the wheelReport JSON-RPC call. Falls back to JSON-RPC during the WebRTC/handshake window or against older firmware that does not dispatch the binary opcode. Also fix two pre-existing oxlint warnings in the same files (restrict-template-expressions) forced by lint-staged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 59ff82a commit 06b5eab

3 files changed

Lines changed: 41 additions & 5 deletions

File tree

ui/src/hooks/hidRpc.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ export class KeyboardMacroReportMessage extends RpcMessage {
240240
// Ensure the keys are within the KEYS_LENGTH range
241241
const keys = step.keys;
242242
if (keys.length > this.KEYS_LENGTH) {
243-
throw new Error(`Keys ${keys} is not within the hidKeyBufferSize range`);
243+
throw new Error(`Keys ${keys.join(",")} is not within the hidKeyBufferSize range`);
244244
} else if (keys.length < this.KEYS_LENGTH) {
245245
keys.push(...Array(this.KEYS_LENGTH - keys.length).fill(0));
246246
}
@@ -400,6 +400,25 @@ export class KeypressKeepAliveMessage extends RpcMessage {
400400
}
401401
}
402402

403+
export class WheelReportMessage extends RpcMessage {
404+
deltaY: number;
405+
deltaX: number;
406+
407+
constructor(deltaY: number, deltaX: number) {
408+
super(HID_RPC_MESSAGE_TYPES.WheelReport);
409+
this.deltaY = deltaY;
410+
this.deltaX = deltaX;
411+
}
412+
413+
marshal(): Uint8Array {
414+
return new Uint8Array([
415+
this.messageType,
416+
fromInt8ToUint8(this.deltaY),
417+
fromInt8ToUint8(this.deltaX),
418+
]);
419+
}
420+
}
421+
403422
export const messageRegistry = {
404423
[HID_RPC_MESSAGE_TYPES.Handshake]: HandshakeMessage,
405424
[HID_RPC_MESSAGE_TYPES.KeysDownState]: KeysDownStateMessage,

ui/src/hooks/useHidRpc.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
MouseReportMessage,
1616
PointerReportMessage,
1717
RpcMessage,
18+
WheelReportMessage,
1819
unmarshalHidRpcMessage,
1920
} from "./hidRpc";
2021

@@ -273,6 +274,16 @@ export function useHidRpc(onHidRpcMessage?: (payload: RpcMessage) => void) {
273274
[sendMessage],
274275
);
275276

277+
const reportWheelEvent = useCallback(
278+
(deltaY: number, deltaX: number) => {
279+
// Wheel events are motion-like — a single dropped detent self-corrects
280+
// via the next event, so we ride the unreliable-ordered channel for
281+
// lower latency, matching how mouse motion is sent.
282+
sendMessage(new WheelReportMessage(deltaY, deltaX), { useUnreliableChannel: true });
283+
},
284+
[sendMessage],
285+
);
286+
276287
const reportKeyboardMacroEvent = useCallback(
277288
(steps: KeyboardMacroStep[]) => {
278289
sendMessage(new KeyboardMacroReportMessage(false, steps.length, steps));
@@ -314,7 +325,7 @@ export function useHidRpc(onHidRpcMessage?: (payload: RpcMessage) => void) {
314325
};
315326

316327
const errorHandler = (e: Event) => {
317-
console.error(`Error on rpcHidChannel '${rpcHidChannel.label}': ${e}`);
328+
console.error(`Error on rpcHidChannel '${rpcHidChannel.label}': ${e.type}`);
318329
};
319330

320331
rpcHidChannel.addEventListener("message", messageHandler);
@@ -331,6 +342,7 @@ export function useHidRpc(onHidRpcMessage?: (payload: RpcMessage) => void) {
331342
reportKeypressEvent,
332343
reportAbsMouseEvent,
333344
reportRelMouseEvent,
345+
reportWheelEvent,
334346
reportKeyboardMacroEvent,
335347
cancelOngoingKeyboardMacro,
336348
reportKeypressKeepAlive,

ui/src/hooks/useMouse.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export default function useMouse() {
2525

2626
// RPC hooks
2727
const { send } = useJsonRpc();
28-
const { reportAbsMouseEvent, reportRelMouseEvent, rpcHidReady } = useHidRpc();
28+
const { reportAbsMouseEvent, reportRelMouseEvent, reportWheelEvent, rpcHidReady } = useHidRpc();
2929
// Mouse-related
3030

3131
const sendRelMouseMovement = useCallback(
@@ -142,15 +142,20 @@ export default function useMouse() {
142142

143143
if (wheelY === 0 && wheelX === 0) return;
144144

145-
send("wheelReport", { wheelY, wheelX });
145+
if (rpcHidReady) {
146+
reportWheelEvent(wheelY, wheelX);
147+
} else {
148+
// kept for backward compatibility
149+
send("wheelReport", { wheelY, wheelX });
150+
}
146151

147152
// Apply blocking delay based of throttling settings
148153
if (scrollThrottling && !blockWheelEvent) {
149154
setBlockWheelEvent(true);
150155
setTimeout(() => setBlockWheelEvent(false), scrollThrottling);
151156
}
152157
},
153-
[send, blockWheelEvent, scrollThrottling, invertScroll],
158+
[send, reportWheelEvent, rpcHidReady, blockWheelEvent, scrollThrottling, invertScroll],
154159
);
155160

156161
const resetMousePosition = useCallback(() => {

0 commit comments

Comments
 (0)