@omercnet/paseo-shared-browser 1.1.0-next.204.1 → 1.1.0-next.210.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -29,6 +29,8 @@ import {
29
29
  type BrowserInputEvent,
30
30
  type BrowserState,
31
31
  captureBrowserRpc,
32
+ containedRect,
33
+ createImageSizeLoader,
32
34
  DEVICE_PRESETS,
33
35
  type DevicePresetId,
34
36
  detachBrowserRpc,
@@ -41,6 +43,7 @@ import {
41
43
  releaseControlRpc,
42
44
  resizeBrowserRpc,
43
45
  sendBrowserInputRpc,
46
+ toViewportPoint,
44
47
  } from "../shared/browser";
45
48
 
46
49
  const SPACE = {
@@ -88,12 +91,7 @@ interface Size {
88
91
  height: number;
89
92
  }
90
93
 
91
- interface DisplayRect extends Size {
92
- x: number;
93
- y: number;
94
- }
95
-
96
- interface DisplayPoint extends Size {
94
+ interface DisplayPoint {
97
95
  x: number;
98
96
  y: number;
99
97
  }
@@ -115,21 +113,6 @@ const SPECIAL_KEYS: readonly { key: SpecialKey; label: string }[] = [
115
113
  { key: "Space", label: "Space" },
116
114
  ];
117
115
 
118
- function containedRect(container: Size, image: Size): DisplayRect | null {
119
- if (container.width <= 0 || container.height <= 0 || image.width <= 0 || image.height <= 0) {
120
- return null;
121
- }
122
- const scale = Math.min(container.width / image.width, container.height / image.height);
123
- const width = image.width * scale;
124
- const height = image.height * scale;
125
- return {
126
- x: (container.width - width) / 2,
127
- y: (container.height - height) / 2,
128
- width,
129
- height,
130
- };
131
- }
132
-
133
116
  function errorMessage(error: unknown): string {
134
117
  if (error instanceof Error && error.message.trim()) return error.message;
135
118
  return "The shared browser request failed.";
@@ -1253,16 +1236,34 @@ export function SharedBrowserPanel({
1253
1236
  const currentFrame = frame && state && isFrameCurrent(frame, state) ? frame : null;
1254
1237
  const canSendInput = canControl && Boolean(currentFrame) && !inputMutation.isPending;
1255
1238
 
1239
+ const frameUri = useMemo(
1240
+ () => (currentFrame ? `data:${currentFrame.mimeType};base64,${currentFrame.dataBase64}` : null),
1241
+ [currentFrame],
1242
+ );
1243
+ const [imageSize, setImageSize] = useState<{ uri: string; size: Size } | null>(null);
1244
+ const loadImageSize = useMemo(
1245
+ () =>
1246
+ createImageSizeLoader(
1247
+ (uri, success, failure) => Image.getSize(uri, success, failure),
1248
+ (uri, size) => setImageSize(size ? { uri, size } : null),
1249
+ ),
1250
+ [],
1251
+ );
1252
+ useEffect(() => {
1253
+ if (frameUri) loadImageSize(frameUri);
1254
+ }, [frameUri, loadImageSize]);
1255
+ // The bitmap can disagree with the frame metadata; its own aspect ratio decides the rectangle.
1256
1256
  const displayRect = useMemo(
1257
1257
  () =>
1258
1258
  currentFrame
1259
- ? containedRect(containerSize, { width: currentFrame.width, height: currentFrame.height })
1259
+ ? containedRect(
1260
+ containerSize,
1261
+ imageSize?.uri === frameUri
1262
+ ? imageSize.size
1263
+ : { width: currentFrame.width, height: currentFrame.height },
1264
+ )
1260
1265
  : null,
1261
- [containerSize, currentFrame],
1262
- );
1263
- const frameUri = useMemo(
1264
- () => (currentFrame ? `data:${currentFrame.mimeType};base64,${currentFrame.dataBase64}` : null),
1265
- [currentFrame],
1266
+ [containerSize, currentFrame, frameUri, imageSize],
1266
1267
  );
1267
1268
 
1268
1269
  const controlContext = useCallback(() => {
@@ -1329,7 +1330,7 @@ export function SharedBrowserPanel({
1329
1330
  if (!displayRect) return null;
1330
1331
  const x = Math.min(displayRect.width, Math.max(0, event.nativeEvent.locationX));
1331
1332
  const y = Math.min(displayRect.height, Math.max(0, event.nativeEvent.locationY));
1332
- return { x, y, width: displayRect.width, height: displayRect.height };
1333
+ return { x, y };
1333
1334
  },
1334
1335
  [displayRect],
1335
1336
  );
@@ -1338,17 +1339,18 @@ export function SharedBrowserPanel({
1338
1339
  (event: GestureResponderEvent) => {
1339
1340
  const start = dragStartRef.current;
1340
1341
  const end = pointFromEvent(event);
1342
+ const viewport = stateRef.current?.viewport;
1341
1343
  dragStartRef.current = null;
1342
- if (!start || !end) return;
1344
+ if (!start || !end || !displayRect || !viewport) return;
1345
+ const toViewport = (point: DisplayPoint) => toViewportPoint(point, displayRect, viewport);
1343
1346
  lastPointRef.current = { x: end.x, y: end.y };
1344
1347
  const distance = Math.hypot(end.x - start.x, end.y - start.y);
1345
1348
  if (distance >= DRAG_THRESHOLD) {
1346
1349
  if (swipeMode === "scroll") {
1347
- const current = stateRef.current;
1348
- const scale = current ? current.viewport.width / end.width : 1;
1350
+ const scale = viewport.width / displayRect.width;
1349
1351
  sendEvent({
1350
1352
  kind: "scroll",
1351
- point: { ...start, width: end.width, height: end.height },
1353
+ point: toViewport(start),
1352
1354
  deltaX: clampScrollDelta((start.x - end.x) * scale),
1353
1355
  deltaY: clampScrollDelta((start.y - end.y) * scale),
1354
1356
  });
@@ -1356,20 +1358,20 @@ export function SharedBrowserPanel({
1356
1358
  }
1357
1359
  sendEvent({
1358
1360
  kind: "drag",
1359
- start: { ...start, width: end.width, height: end.height },
1360
- end,
1361
+ start: toViewport(start),
1362
+ end: toViewport(end),
1361
1363
  button: interactionMode === "right" ? "right" : "left",
1362
1364
  });
1363
1365
  return;
1364
1366
  }
1365
1367
  sendEvent({
1366
1368
  kind: "click",
1367
- point: end,
1369
+ point: toViewport(end),
1368
1370
  button: interactionMode === "right" ? "right" : "left",
1369
1371
  clickCount: interactionMode === "double" ? 2 : 1,
1370
1372
  });
1371
1373
  },
1372
- [interactionMode, pointFromEvent, sendEvent, swipeMode],
1374
+ [displayRect, interactionMode, pointFromEvent, sendEvent, swipeMode],
1373
1375
  );
1374
1376
 
1375
1377
  const panResponder = useMemo(
@@ -1474,13 +1476,14 @@ export function SharedBrowserPanel({
1474
1476
 
1475
1477
  const scroll = useCallback(
1476
1478
  (deltaX: number, deltaY: number) => {
1477
- if (!displayRect) return;
1479
+ const viewport = stateRef.current?.viewport;
1480
+ if (!displayRect || !viewport) return;
1478
1481
  const previous = lastPointRef.current;
1479
1482
  const x = previous && previous.x <= displayRect.width ? previous.x : displayRect.width / 2;
1480
1483
  const y = previous && previous.y <= displayRect.height ? previous.y : displayRect.height / 2;
1481
1484
  sendEvent({
1482
1485
  kind: "scroll",
1483
- point: { x, y, width: displayRect.width, height: displayRect.height },
1486
+ point: toViewportPoint({ x, y }, displayRect, viewport),
1484
1487
  deltaX,
1485
1488
  deltaY,
1486
1489
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omercnet/paseo-shared-browser",
3
- "version": "1.1.0-next.204.1",
3
+ "version": "1.1.0-next.210.1",
4
4
  "description": "One real Chromium session per Paseo workspace, shared live across every connected client.",
5
5
  "keywords": [
6
6
  "browser",
@@ -43,8 +43,8 @@
43
43
  "check:write": "biome check --write .",
44
44
  "typecheck": "tsc --noEmit",
45
45
  "prepare:runtime": "node scripts/prepare-runtime.mjs",
46
- "test": "vitest run tests/contracts.test.ts tests/lease.test.ts tests/runtime-lifecycle.test.ts tests/agent-access.test.ts tests/composer-pills.test.ts tests/sidebar.test.ts",
47
- "test:unit": "vitest run tests/contracts.test.ts tests/lease.test.ts tests/runtime-lifecycle.test.ts tests/agent-access.test.ts tests/composer-pills.test.ts tests/sidebar.test.ts",
46
+ "test": "vitest run tests/contracts.test.ts tests/lease.test.ts tests/runtime-lifecycle.test.ts tests/agent-access.test.ts tests/composer-pills.test.ts tests/sidebar.test.ts tests/pointer-mapping.test.ts",
47
+ "test:unit": "vitest run tests/contracts.test.ts tests/lease.test.ts tests/runtime-lifecycle.test.ts tests/agent-access.test.ts tests/composer-pills.test.ts tests/sidebar.test.ts tests/pointer-mapping.test.ts",
48
48
  "test:smoke": "vitest run tests/browser.smoke.test.ts --testTimeout=30000"
49
49
  },
50
50
  "dependencies": {
package/shared/browser.ts CHANGED
@@ -365,3 +365,70 @@ export function mapDisplayedPoint(
365
365
  y: Math.min(viewport.height - 1, Math.max(0, (point.y / point.height) * viewport.height)),
366
366
  };
367
367
  }
368
+
369
+ export interface FrameSize {
370
+ width: number;
371
+ height: number;
372
+ }
373
+
374
+ export interface FrameRect extends FrameSize {
375
+ x: number;
376
+ y: number;
377
+ }
378
+
379
+ export function containedRect(container: FrameSize, image: FrameSize): FrameRect | null {
380
+ if (container.width <= 0 || container.height <= 0 || image.width <= 0 || image.height <= 0) {
381
+ return null;
382
+ }
383
+ const scale = Math.min(container.width / image.width, container.height / image.height);
384
+ const width = image.width * scale;
385
+ const height = image.height * scale;
386
+ return {
387
+ x: (container.width - width) / 2,
388
+ y: (container.height - height) / 2,
389
+ width,
390
+ height,
391
+ };
392
+ }
393
+
394
+ /** Converts a point in the displayed image to the canonical viewport, scaling both axes by viewport.width / rect.width. */
395
+ export function toViewportPoint(
396
+ point: MappedPoint,
397
+ rect: FrameSize,
398
+ viewport: Viewport,
399
+ ): MappedPoint & FrameSize {
400
+ const scale = viewport.width / rect.width;
401
+ return {
402
+ x: point.x * scale,
403
+ y: Math.min(viewport.height, point.y * scale),
404
+ width: viewport.width,
405
+ height: viewport.height,
406
+ };
407
+ }
408
+
409
+ type GetImageSize = (
410
+ uri: string,
411
+ success: (width: number, height: number) => void,
412
+ failure: () => void,
413
+ ) => void;
414
+
415
+ /** Looks up the intrinsic size once per distinct URI and drops results superseded by a newer URI. */
416
+ export function createImageSizeLoader(
417
+ getSize: GetImageSize,
418
+ onSize: (uri: string, size: FrameSize | null) => void,
419
+ ): (uri: string) => void {
420
+ let current: string | null = null;
421
+ return (uri) => {
422
+ if (uri === current) return;
423
+ current = uri;
424
+ getSize(
425
+ uri,
426
+ (width, height) => {
427
+ if (current === uri) onSize(uri, { width, height });
428
+ },
429
+ () => {
430
+ if (current === uri) onSize(uri, null);
431
+ },
432
+ );
433
+ };
434
+ }