@omercnet/paseo-shared-browser 3.0.0-next.283.1 → 3.0.0-next.292.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.
package/OVERVIEW.md CHANGED
@@ -13,7 +13,7 @@ New agents whose provider accepts external MCP servers receive a local MCP serve
13
13
 
14
14
  ## Setup
15
15
 
16
- - Paseo ^0.12.0, and Node.js 24 or newer on the daemon host.
16
+ - Paseo ^0.13.0, and Node.js 24 or newer on the daemon host.
17
17
  - Chromium is downloaded when the plugin is installed and stored in the Paseo data directory. On Linux ARM64 nothing is downloaded, and you install a non-Snap Chromium at `/usr/bin/chromium` first. `PASEO_SHARED_BROWSER_CHROMIUM_EXECUTABLE` points the plugin at a different Chromium.
18
18
 
19
19
  ## Limits
package/README.md CHANGED
@@ -183,7 +183,9 @@ User-supplied `AGENT_BROWSER_*` variables are deliberately ignored.
183
183
  within the pane, scroll long lists, and dismiss with an outside press, Escape or native Back.
184
184
  - The monitor menu offers **Fit to panel** (default) and **Actual size (100%)**. Actual size
185
185
  uses browser layout pixels, not the larger JPEG dimensions of sharper phone captures.
186
- This choice affects only the current viewer, without resizing the shared browser.
186
+ This choice affects only the current viewer, without resizing the shared browser. Each
187
+ Shared Browser tab remembers its choice and its selected browser page across app reloads;
188
+ a remembered page that no longer exists falls back to the default page.
187
189
  Desktop local scrollbars reveal overflow while wheel events on a controlled frame go to
188
190
  the remote page. Phone viewers can pan while observing; controlling sends swipes to the
189
191
  page instead. Fitting images stay centered without empty scroll ranges; a scrollbar on
@@ -422,7 +424,7 @@ runs the actual Paseo compiler on both plugin entries.
422
424
  Release Please maintains the version, changelog, component tag, and GitHub release from
423
425
  Conventional Commits in the monorepo.
424
426
 
425
- Both the Paseo daemon and app must satisfy `^0.12.0`. Encoded video is opt-in on the daemon and
427
+ Both the Paseo daemon and app must satisfy `^0.13.0`. Encoded video is opt-in on the daemon and
426
428
  requires WebCodecs in a web client; JPEG is always available. The client surface uses React
427
429
  Native primitives and works in desktop, web, iOS, and Android Paseo clients.
428
430
 
@@ -1,6 +1,6 @@
1
1
  /** Public themed panel controls and notices. Leaves own only focus/hover styling, never browser authority. */
2
2
  import type { PluginWorkspacePanelProps } from "@getpaseo/plugin/client";
3
- import { Icon, TextInput } from "@getpaseo/plugin/client/react-native";
3
+ import { Icon, TextInput } from "@getpaseo/plugin/client/ui";
4
4
  import { type ReactNode, useCallback, useState } from "react";
5
5
  import {
6
6
  ActivityIndicator,
@@ -33,7 +33,7 @@ vi.mock("react", () => ({
33
33
  hooks.effects.push(effect);
34
34
  },
35
35
  }));
36
- vi.mock("@getpaseo/plugin/client/react-native", () => {
36
+ vi.mock("@getpaseo/plugin/client/ui", () => {
37
37
  const Modal = Object.assign(() => null, { Content: () => null });
38
38
  return { Modal, TextInput: () => null };
39
39
  });
@@ -1,6 +1,6 @@
1
1
  /** Explicit Compose draft UI, with original control/document ownership pinned until Done. */
2
2
  import type { PluginWorkspacePanelProps } from "@getpaseo/plugin/client";
3
- import { Modal, TextInput } from "@getpaseo/plugin/client/react-native";
3
+ import { Modal, TextInput } from "@getpaseo/plugin/client/ui";
4
4
  import { useEffect, useRef, useState } from "react";
5
5
  import { Text, View } from "react-native";
6
6
  import { ControlButton } from "./browser-chrome";
@@ -1,5 +1,5 @@
1
1
  import type { PluginWorkspacePanelProps } from "@getpaseo/plugin/client";
2
- import { Icon } from "@getpaseo/plugin/client/react-native";
2
+ import { Icon } from "@getpaseo/plugin/client/ui";
3
3
  import { useCallback, useState } from "react";
4
4
  import { Pressable, Text, type TextStyle, type ViewStyle } from "react-native";
5
5
 
@@ -1,6 +1,6 @@
1
1
  /** Confirm a one-time control request before the selected tab action continues. */
2
2
  import type { PluginWorkspacePanelProps } from "@getpaseo/plugin/client";
3
- import { Icon, Modal } from "@getpaseo/plugin/client/react-native";
3
+ import { Icon, Modal } from "@getpaseo/plugin/client/ui";
4
4
  import type { BrowserState } from "../shared/browser";
5
5
  import { ControlButton } from "./browser-chrome";
6
6
  import type { ControlRequest } from "./browser-control-request";
@@ -0,0 +1,23 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { browserPanelStateSchema, DEFAULT_BROWSER_PANEL_STATE } from "./browser-panel-state";
3
+
4
+ describe("persisted browser panel state", () => {
5
+ it("round-trips a remembered tab and display mode", () => {
6
+ const state = { scaleMode: "actual", selectedTabId: "page-2" };
7
+ expect(browserPanelStateSchema.parse(state)).toEqual(state);
8
+ });
9
+
10
+ it("accepts its own default", () => {
11
+ expect(browserPanelStateSchema.safeParse(DEFAULT_BROWSER_PANEL_STATE).success).toBe(true);
12
+ });
13
+
14
+ it("rejects state that a newer or older plugin version could have written", () => {
15
+ expect(browserPanelStateSchema.safeParse(undefined).success).toBe(false);
16
+ expect(
17
+ browserPanelStateSchema.safeParse({ scaleMode: "zoom", selectedTabId: null }).success,
18
+ ).toBe(false);
19
+ expect(browserPanelStateSchema.safeParse({ scaleMode: "fit", selectedTabId: 3 }).success).toBe(
20
+ false,
21
+ );
22
+ });
23
+ });
@@ -0,0 +1,14 @@
1
+ /** Per-tab state Paseo persists across tab restoration and app reloads. */
2
+ import { z } from "zod";
3
+ import { browserTabIdSchema } from "../shared/browser";
4
+
5
+ export const browserPanelStateSchema = z.object({
6
+ scaleMode: z.enum(["fit", "actual"]),
7
+ selectedTabId: browserTabIdSchema.nullable(),
8
+ });
9
+ export type BrowserPanelState = z.infer<typeof browserPanelStateSchema>;
10
+
11
+ export const DEFAULT_BROWSER_PANEL_STATE: BrowserPanelState = {
12
+ scaleMode: "fit",
13
+ selectedTabId: null,
14
+ };
@@ -1,6 +1,6 @@
1
1
  /** Grouped resolution choices with independent host favorites and capture quality. */
2
2
  import type { PluginHostProps } from "@getpaseo/plugin/client";
3
- import { Icon } from "@getpaseo/plugin/client/react-native";
3
+ import { Icon } from "@getpaseo/plugin/client/ui";
4
4
  import { Pressable, StyleSheet, Text, View } from "react-native";
5
5
  import type { BrowserDisplayPreferences } from "../shared/browser-display-preferences";
6
6
  import type { DevicePresetId } from "../shared/device-presets";
@@ -4,7 +4,7 @@
4
4
  * host internals. Placement stays inside the pane, including compact clients.
5
5
  */
6
6
  import type { PluginHostProps } from "@getpaseo/plugin/client";
7
- import { Icon } from "@getpaseo/plugin/client/react-native";
7
+ import { Icon } from "@getpaseo/plugin/client/ui";
8
8
  import {
9
9
  type ReactNode,
10
10
  type RefObject,
@@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest";
3
3
  import {
4
4
  createBrowserViewerRecovery,
5
5
  isBrowserClosedError,
6
+ isBrowserTabUnavailableError,
6
7
  isExpiredBrowserViewerError,
7
8
  } from "./browser-viewer-recovery";
8
9
 
@@ -109,3 +110,26 @@ it("the actual capture query observer discards old token errors when its token k
109
110
  client.clear();
110
111
  }
111
112
  });
113
+
114
+ describe("remembered tab recovery", () => {
115
+ it("recognizes only the unavailable-tab rejection", () => {
116
+ const unavailable = Object.assign(
117
+ new Error(
118
+ "Request failed: Workspace runtime identity failed: Browser tab is no longer available requestType=plugin.rpc.invoke.request code=handler_error",
119
+ ),
120
+ { code: "handler_error" },
121
+ );
122
+ expect(isBrowserTabUnavailableError(unavailable)).toBe(true);
123
+ expect(isBrowserTabUnavailableError(new Error("Browser tab is no longer available"))).toBe(
124
+ true,
125
+ );
126
+ expect(isBrowserTabUnavailableError(expired)).toBe(false);
127
+ expect(isBrowserTabUnavailableError(new Error("Browser is closed"))).toBe(false);
128
+ expect(
129
+ isBrowserTabUnavailableError(
130
+ new Error("Could not parse: Browser tab is no longer available"),
131
+ ),
132
+ ).toBe(false);
133
+ expect(isBrowserTabUnavailableError(null)).toBe(false);
134
+ });
135
+ });
@@ -5,6 +5,11 @@
5
5
  */
6
6
  const VIEWER_EXPIRED_MESSAGE = "Viewer token is invalid or expired";
7
7
  const BROWSER_CLOSED_MESSAGE = "Browser is closed";
8
+ const BROWSER_TAB_UNAVAILABLE_MESSAGE = "Browser tab is no longer available";
9
+ // The supervisor wraps runtime rejections as "Workspace runtime <operation> failed: <message>".
10
+ const UNAVAILABLE_TAB_PATTERN = new RegExp(
11
+ `^(?:Workspace runtime \\w+ failed: )?${BROWSER_TAB_UNAVAILABLE_MESSAGE}(?: requestType=|$)`,
12
+ );
8
13
 
9
14
  /** Recognize the server's explicit close fence without confusing it with a transport failure. */
10
15
  export function isBrowserClosedError(error: unknown): boolean {
@@ -76,3 +81,18 @@ export function createBrowserViewerRecovery() {
76
81
  return true;
77
82
  };
78
83
  }
84
+
85
+ /**
86
+ * A remembered tab can outlive its page; the server then rejects attaching to it. The runtime
87
+ * wraps that rejection ("Workspace runtime identity failed: ..."), so match the phrase.
88
+ */
89
+ export function isBrowserTabUnavailableError(error: unknown): boolean {
90
+ if (!error || typeof error !== "object") return false;
91
+ const value = error as { message?: unknown; code?: unknown };
92
+ if (typeof value.message !== "string") return false;
93
+ if (value.code !== undefined && value.code !== "handler_error") return false;
94
+ const message = value.message.startsWith("Request failed: ")
95
+ ? value.message.slice("Request failed: ".length)
96
+ : value.message;
97
+ return UNAVAILABLE_TAB_PATTERN.test(message);
98
+ }
@@ -4,8 +4,8 @@
4
4
  * presentation models live in focused counterpart hooks; this module never
5
5
  * replays published mutations or substitutes visible pixels for input authority.
6
6
  */
7
- import { type PluginWorkspacePanelProps, useRpc } from "@getpaseo/plugin/client";
8
- import { Icon, Modal } from "@getpaseo/plugin/client/react-native";
7
+ import { type PluginWorkspacePanelProps, useRpc, useTabState } from "@getpaseo/plugin/client";
8
+ import { Icon, Modal } from "@getpaseo/plugin/client/ui";
9
9
  import { useQuery, useQueryClient } from "@tanstack/react-query";
10
10
  import {
11
11
  type ReactNode,
@@ -53,7 +53,7 @@ import {
53
53
  type CaptureDensityMode,
54
54
  getAutomaticCaptureDensity,
55
55
  } from "./browser-auto-capture-density";
56
- import { type BrowserCanvasDisplayMode, getBrowserCanvasLayout } from "./browser-canvas-layout";
56
+ import { getBrowserCanvasLayout } from "./browser-canvas-layout";
57
57
  import { BrowserCanvasViewport } from "./browser-canvas-viewport";
58
58
  import {
59
59
  CanvasPlaceholder,
@@ -75,6 +75,7 @@ import { type EmulationSelection, matchingResolutionPresetId } from "./browser-e
75
75
  import { getFillViewportResolution } from "./browser-fill-viewport";
76
76
  import type { FrameCandidate } from "./browser-frame-buffer";
77
77
  import { BrowserFrameImage } from "./browser-frame-image";
78
+ import { browserPanelStateSchema, DEFAULT_BROWSER_PANEL_STATE } from "./browser-panel-state";
78
79
  import { createStyles, DIMENSION, SPACE } from "./browser-panel-styles";
79
80
  import { BrowserResolutionPicker } from "./browser-resolution-picker";
80
81
  import { BrowserTabs } from "./browser-tabs";
@@ -86,7 +87,11 @@ import {
86
87
  } from "./browser-toolbar-menu";
87
88
  import { isFrameCurrent, isVideoSourceCurrent } from "./browser-video-currency";
88
89
  import { BrowserVideoSurface } from "./browser-video-surface";
89
- import { isBrowserClosedError, isExpiredBrowserViewerError } from "./browser-viewer-recovery";
90
+ import {
91
+ isBrowserClosedError,
92
+ isBrowserTabUnavailableError,
93
+ isExpiredBrowserViewerError,
94
+ } from "./browser-viewer-recovery";
90
95
  import { createFrameLifecycle } from "./frame-lifecycle";
91
96
  import { useBrowserAutoCaptureDensity } from "./use-browser-auto-capture-density";
92
97
  import { useBrowserCanvasInput } from "./use-browser-canvas-input";
@@ -188,6 +193,7 @@ export function SharedBrowserPanel({
188
193
  host,
189
194
  layout,
190
195
  workspaceId,
196
+ tab,
191
197
  }: PluginWorkspacePanelProps) {
192
198
  const queryClient = useQueryClient();
193
199
  const styles = useMemo(() => createStyles(theme, layout.compact), [theme, layout.compact]);
@@ -245,7 +251,38 @@ export function SharedBrowserPanel({
245
251
  const [operationError, setOperationError] = useState<string | null>(null);
246
252
  const [reconnecting, setReconnecting] = useState(false);
247
253
  const [browserClosed, setBrowserClosed] = useState(false);
248
- const [selectedTabId, setSelectedTabId] = useState<string | null>(null);
254
+ const [panelState, writePanelState] = useTabState(
255
+ tab,
256
+ browserPanelStateSchema,
257
+ DEFAULT_BROWSER_PANEL_STATE,
258
+ );
259
+ // Writes replace the whole persisted value; merge into the latest one so two
260
+ // updates in the same tick (e.g. Fill viewport) cannot drop each other.
261
+ const panelStateRef = useRef(panelState);
262
+ panelStateRef.current = panelState;
263
+ const updatePanelState = useCallback(
264
+ (patch: Partial<typeof panelState>) => {
265
+ const next = { ...panelStateRef.current, ...patch };
266
+ if (
267
+ next.scaleMode === panelStateRef.current.scaleMode &&
268
+ next.selectedTabId === panelStateRef.current.selectedTabId
269
+ ) {
270
+ return;
271
+ }
272
+ panelStateRef.current = next;
273
+ writePanelState(next);
274
+ },
275
+ [writePanelState],
276
+ );
277
+ const { selectedTabId, scaleMode } = panelState;
278
+ const setSelectedTabId = useCallback(
279
+ (tabId: string | null) => updatePanelState({ selectedTabId: tabId }),
280
+ [updatePanelState],
281
+ );
282
+ const setScaleMode = useCallback(
283
+ (mode: typeof scaleMode) => updatePanelState({ scaleMode: mode }),
284
+ [updatePanelState],
285
+ );
249
286
  const selectedTabIdRef = useRef(selectedTabId);
250
287
  selectedTabIdRef.current = selectedTabId;
251
288
  const [tabMutationPending, setTabMutationPending] = useState(false);
@@ -268,7 +305,6 @@ export function SharedBrowserPanel({
268
305
  const [keysSubmenuOpen, setKeysSubmenuOpen] = useState(false);
269
306
  const keysAnchorRef = useRef<View | null>(null);
270
307
  const [toolbarMenu, setToolbarMenu] = useState<"display" | "actions" | null>(null);
271
- const [scaleMode, setScaleMode] = useState<BrowserCanvasDisplayMode>("fit");
272
308
  const paneRef = useRef<View | null>(null);
273
309
  const displayAnchorRef = useRef<View | null>(null);
274
310
  const actionsAnchorRef = useRef<View | null>(null);
@@ -426,6 +462,12 @@ export function SharedBrowserPanel({
426
462
  if (isBrowserClosedError(attachQuery.error)) setBrowserClosed(true);
427
463
  }, [attachQuery.error]);
428
464
 
465
+ // A restored selection can name a page that no longer exists; without the
466
+ // attach there is no viewer to list tabs with, so fall back to the default page.
467
+ useEffect(() => {
468
+ if (selectedTabId && isBrowserTabUnavailableError(attachQuery.error)) setSelectedTabId(null);
469
+ }, [attachQuery.error, selectedTabId, setSelectedTabId]);
470
+
429
471
  useEffect(() => {
430
472
  if (!closedView) return;
431
473
  setControlRequest(null);
@@ -457,13 +499,21 @@ export function SharedBrowserPanel({
457
499
  setOperationError(null);
458
500
  setRuntimeNotice(null);
459
501
  setBrowserClosed(false);
460
- setSelectedTabId(null);
461
502
  setTabCloseConfirmationOpen(false);
462
503
  setControlRequest(null);
463
504
  setPendingControlAction(null);
464
505
  setCloseConfirmationOpen(false);
465
506
  }, [reset, workspaceId, host.id, inputLifecycle]);
466
507
 
508
+ // The remembered tab belongs to this host and workspace. Keep it on mount,
509
+ // where it was just restored, and clear it only when the scope really changes.
510
+ const selectionScopeRef = useRef(panelScope);
511
+ useEffect(() => {
512
+ if (selectionScopeRef.current === panelScope) return;
513
+ selectionScopeRef.current = panelScope;
514
+ setSelectedTabId(null);
515
+ }, [panelScope, setSelectedTabId]);
516
+
467
517
  useEffect(() => {
468
518
  // A tab selection changes the viewer's page and frame authority, even
469
519
  // though the underlying Chromium process and saved profile stay shared.
package/index.client.tsx CHANGED
@@ -7,7 +7,6 @@ export default function contribute(client: PluginClientContext) {
7
7
  id: "shared-browser",
8
8
  title: "Shared Browser",
9
9
  icon: "PanelsTopLeft",
10
- context: "workspace",
11
10
  locations: ["workspace", "explorer"],
12
11
  Component: SharedBrowserPanel,
13
12
  });
@@ -8,7 +8,7 @@
8
8
  "name": "@omercnet/paseo-shared-browser",
9
9
  "version": "3.0.0",
10
10
  "dependencies": {
11
- "@getpaseo/plugin": "0.12.0",
11
+ "@getpaseo/plugin": "0.13.0",
12
12
  "@modelcontextprotocol/sdk": "1.30.0",
13
13
  "agent-browser": "0.39.1",
14
14
  "esbuild": "0.28.2",
@@ -441,28 +441,30 @@
441
441
  }
442
442
  },
443
443
  "node_modules/@getpaseo/client": {
444
- "version": "0.12.0",
445
- "resolved": "https://registry.npmjs.org/@getpaseo/client/-/client-0.12.0.tgz",
446
- "integrity": "sha512-FrszuQkG73mbU5nJcaWKjW2K1mDt6k1veCa2acV9V8HphPD7BtQLPiiUnXsSUdYeP8S54duGbuYfZgaVyUvd5g==",
444
+ "version": "0.13.0",
445
+ "resolved": "https://registry.npmjs.org/@getpaseo/client/-/client-0.13.0.tgz",
446
+ "integrity": "sha512-379gl/OHIowATqKBZugupxX6HaOPWQhkOeVnlMQNWnZ5oKbyfCTEPGSkzA8Kotvv1Uue2u+j8z2WnLXyZ9ChYQ==",
447
447
  "peer": true,
448
448
  "dependencies": {
449
- "@getpaseo/protocol": "0.12.0",
450
- "@getpaseo/relay": "0.12.0",
449
+ "@getpaseo/protocol": "0.13.0",
450
+ "@getpaseo/relay": "0.13.0",
451
+ "ws": "^8.20.0",
451
452
  "zod": "^4.4.3"
452
453
  }
453
454
  },
454
455
  "node_modules/@getpaseo/plugin": {
455
- "version": "0.12.0",
456
- "resolved": "https://registry.npmjs.org/@getpaseo/plugin/-/plugin-0.12.0.tgz",
457
- "integrity": "sha512-LSwpGU3o6/SRAfzIIq9BA6aqEO6LRmbJTyyCRl+jWCsguL++klMpf6HBbAZJtQOlv5YuF21gjKZvLcSkngpxFg==",
456
+ "version": "0.13.0",
457
+ "resolved": "https://registry.npmjs.org/@getpaseo/plugin/-/plugin-0.13.0.tgz",
458
+ "integrity": "sha512-8aGH0lf5kF9yjaM0dVsm8Awu5eB2jHhmbX9ljgf6+jttNWX3XRbRNeylg3whPoKORGrJNlpuLx3XvJMC5KSCaQ==",
458
459
  "dependencies": {
459
460
  "@agentclientprotocol/sdk": "^1.4.0",
461
+ "i18next": "^26.3.0",
460
462
  "use-sync-external-store": "^1.6.0"
461
463
  },
462
464
  "peerDependencies": {
463
- "@getpaseo/client": "0.12.0",
464
- "@getpaseo/protocol": "0.12.0",
465
- "react": "~19.1.0",
465
+ "@getpaseo/client": "0.13.0",
466
+ "@getpaseo/protocol": "0.13.0",
467
+ "react": ">=19.1.0 <20",
466
468
  "react-native": ">=0.81.5",
467
469
  "zod": "^4.4.3"
468
470
  },
@@ -473,9 +475,9 @@
473
475
  }
474
476
  },
475
477
  "node_modules/@getpaseo/protocol": {
476
- "version": "0.12.0",
477
- "resolved": "https://registry.npmjs.org/@getpaseo/protocol/-/protocol-0.12.0.tgz",
478
- "integrity": "sha512-05jivVDeG18jWTpvQZ3yETNxH/AfHpHy9CgnP7O5ZxQLUr3onJIAqaGNzv15dPnGf4fU342UHQBFAE2t2Yc7fQ==",
478
+ "version": "0.13.0",
479
+ "resolved": "https://registry.npmjs.org/@getpaseo/protocol/-/protocol-0.13.0.tgz",
480
+ "integrity": "sha512-g5Hp6QP5hV7KTCrSRBVye2+qhptwnhhLXRn6PHaS1L8PgmeRh5LvrE8PZq3HRq8B1WxflVDuV2WRB2Z3FNcvJA==",
479
481
  "peer": true,
480
482
  "dependencies": {
481
483
  "semver": "^7.8.5",
@@ -483,11 +485,14 @@
483
485
  }
484
486
  },
485
487
  "node_modules/@getpaseo/relay": {
486
- "version": "0.12.0",
487
- "resolved": "https://registry.npmjs.org/@getpaseo/relay/-/relay-0.12.0.tgz",
488
- "integrity": "sha512-ochmc4uIZWlSgMr9aJIEqnyiFrhcW+TjOWxnTYX4LCyiJMvLrhN4DJpZlReAqyWmSgEybjjRBq+7NBH64FIV3A==",
488
+ "version": "0.13.0",
489
+ "resolved": "https://registry.npmjs.org/@getpaseo/relay/-/relay-0.13.0.tgz",
490
+ "integrity": "sha512-Yd/RR+2hStTEtmkutwE21drjVmgz+RnQWyjmoh1++dPysp2oNsrimurnbfN0yoBJ5xXWlyuvn8Sr7CziTpkv7Q==",
489
491
  "peer": true,
490
492
  "dependencies": {
493
+ "@noble/ciphers": "^2.4.0",
494
+ "@noble/curves": "^2.4.0",
495
+ "@noble/hashes": "^2.4.0",
491
496
  "base64-js": "^1.5.1",
492
497
  "tweetnacl": "^1.0.3",
493
498
  "ws": "^8.14.2"
@@ -545,6 +550,48 @@
545
550
  }
546
551
  }
547
552
  },
553
+ "node_modules/@noble/ciphers": {
554
+ "version": "2.4.0",
555
+ "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.4.0.tgz",
556
+ "integrity": "sha512-AnjFn0Jv92laAkvMrghlFZq4qQCIN/4DxFV/eooqtC2YTjB7kBeLMS2T9KJX4Dn+ZVXLOwK0lSgqDtx9gvxtiw==",
557
+ "license": "MIT",
558
+ "peer": true,
559
+ "engines": {
560
+ "node": ">= 20.19.0"
561
+ },
562
+ "funding": {
563
+ "url": "https://paulmillr.com/funding/"
564
+ }
565
+ },
566
+ "node_modules/@noble/curves": {
567
+ "version": "2.4.0",
568
+ "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.4.0.tgz",
569
+ "integrity": "sha512-P4/62zrgfH33CneE3Dn4WhJVA22YUU0eR51wKIan4NVRvwsA0YnPTwWGpNbpuacSujmSFLvyzpyuR30+fbq2Ew==",
570
+ "license": "MIT",
571
+ "peer": true,
572
+ "dependencies": {
573
+ "@noble/hashes": "2.4.0"
574
+ },
575
+ "engines": {
576
+ "node": ">= 20.19.0"
577
+ },
578
+ "funding": {
579
+ "url": "https://paulmillr.com/funding/"
580
+ }
581
+ },
582
+ "node_modules/@noble/hashes": {
583
+ "version": "2.4.0",
584
+ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.4.0.tgz",
585
+ "integrity": "sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==",
586
+ "license": "MIT",
587
+ "peer": true,
588
+ "engines": {
589
+ "node": ">= 20.19.0"
590
+ },
591
+ "funding": {
592
+ "url": "https://paulmillr.com/funding/"
593
+ }
594
+ },
548
595
  "node_modules/accepts": {
549
596
  "version": "2.0.0",
550
597
  "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
@@ -627,18 +674,18 @@
627
674
  "peer": true
628
675
  },
629
676
  "node_modules/body-parser": {
630
- "version": "2.3.0",
631
- "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz",
632
- "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==",
677
+ "version": "2.4.0",
678
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.4.0.tgz",
679
+ "integrity": "sha512-Ifk1KcLrxaGt1occQagQNFNNOqRt1LFuehEnHHzB8Z6cRgIxRU1TBaiIsUiCdA+gy5PL7Y1IX31DNKkRmxtXjQ==",
633
680
  "license": "MIT",
634
681
  "dependencies": {
635
682
  "bytes": "^3.1.2",
636
- "content-type": "^2.0.0",
683
+ "content-type": "^2.1.0",
637
684
  "debug": "^4.4.3",
638
685
  "http-errors": "^2.0.1",
639
- "iconv-lite": "^0.7.2",
686
+ "iconv-lite": "^0.7.3",
640
687
  "on-finished": "^2.4.1",
641
- "qs": "^6.15.2",
688
+ "qs": "^6.16.1",
642
689
  "raw-body": "^3.0.2",
643
690
  "type-is": "^2.1.0"
644
691
  },
@@ -1016,9 +1063,9 @@
1016
1063
  "license": "MIT"
1017
1064
  },
1018
1065
  "node_modules/fast-uri": {
1019
- "version": "3.1.8",
1020
- "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz",
1021
- "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==",
1066
+ "version": "3.1.9",
1067
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.9.tgz",
1068
+ "integrity": "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==",
1022
1069
  "funding": [
1023
1070
  {
1024
1071
  "type": "github",
@@ -1181,6 +1228,34 @@
1181
1228
  "url": "https://opencollective.com/express"
1182
1229
  }
1183
1230
  },
1231
+ "node_modules/i18next": {
1232
+ "version": "26.5.8",
1233
+ "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.5.8.tgz",
1234
+ "integrity": "sha512-EQx2RyhwINMekbykhZpAJQ2ILeJqnYOpRjWEkSVv45pR8yeVbb0thr2zxQ+dOo365OIb7h06BKPtt16WE8Wliw==",
1235
+ "funding": [
1236
+ {
1237
+ "type": "individual",
1238
+ "url": "https://www.locize.com/i18next"
1239
+ },
1240
+ {
1241
+ "type": "individual",
1242
+ "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
1243
+ },
1244
+ {
1245
+ "type": "individual",
1246
+ "url": "https://www.locize.com"
1247
+ }
1248
+ ],
1249
+ "license": "MIT",
1250
+ "peerDependencies": {
1251
+ "typescript": "^5 || ^6 || ^7"
1252
+ },
1253
+ "peerDependenciesMeta": {
1254
+ "typescript": {
1255
+ "optional": true
1256
+ }
1257
+ }
1258
+ },
1184
1259
  "node_modules/iconv-lite": {
1185
1260
  "version": "0.7.3",
1186
1261
  "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz",
@@ -1234,9 +1309,9 @@
1234
1309
  "license": "ISC"
1235
1310
  },
1236
1311
  "node_modules/jose": {
1237
- "version": "6.2.12",
1238
- "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.12.tgz",
1239
- "integrity": "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==",
1312
+ "version": "6.2.13",
1313
+ "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.13.tgz",
1314
+ "integrity": "sha512-7TIg10ZE1x82YediN+lcrPNvGMKge9yFHQD+Qho/b8y2adfugU3AsvvHfxV0fzyFllT6l9xCjf4RWY9g/Z9PJQ==",
1240
1315
  "license": "MIT",
1241
1316
  "funding": {
1242
1317
  "url": "https://github.com/sponsors/panva"
@@ -1370,15 +1445,19 @@
1370
1445
  }
1371
1446
  },
1372
1447
  "node_modules/on-finished": {
1373
- "version": "2.4.1",
1374
- "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
1375
- "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
1448
+ "version": "2.4.2",
1449
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.2.tgz",
1450
+ "integrity": "sha512-ATWUv387i+nLD49EiDoOft5scf32Vsdz3ktrQJG9bi7jRFmaXTssvyvj1EmeKVVgvesbqUtvQQvaS2TpTukAZQ==",
1376
1451
  "license": "MIT",
1377
1452
  "dependencies": {
1378
1453
  "ee-first": "1.1.1"
1379
1454
  },
1380
1455
  "engines": {
1381
1456
  "node": ">= 0.8"
1457
+ },
1458
+ "funding": {
1459
+ "type": "opencollective",
1460
+ "url": "https://opencollective.com/express"
1382
1461
  }
1383
1462
  },
1384
1463
  "node_modules/once": {
@@ -1489,9 +1568,9 @@
1489
1568
  }
1490
1569
  },
1491
1570
  "node_modules/react": {
1492
- "version": "19.1.10",
1493
- "resolved": "https://registry.npmjs.org/react/-/react-19.1.10.tgz",
1494
- "integrity": "sha512-xLes4RnWjC02mmkdHO2GPNjO0le6RMBPLYWKMaMf4jUIMFjTqrYBobxWAh8rU0pjfefJ6OLdef9zbYj+7iXH9Q==",
1571
+ "version": "19.3.1",
1572
+ "resolved": "https://registry.npmjs.org/react/-/react-19.3.1.tgz",
1573
+ "integrity": "sha512-QNhWdt7nrgVWWQG58QuGsAR3jv3Hbe4mb4vz7w2csnHavciZJgda1CPNaldF6eVxv90ud3jUXpKlHL69gO38yw==",
1495
1574
  "license": "MIT",
1496
1575
  "peer": true,
1497
1576
  "engines": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omercnet/paseo-shared-browser",
3
- "version": "3.0.0-next.283.1",
3
+ "version": "3.0.0-next.292.1",
4
4
  "description": "One real Chromium session per Paseo workspace, shared live across every connected client.",
5
5
  "keywords": [
6
6
  "browser",
@@ -54,7 +54,7 @@
54
54
  "test:video": "vitest run tests/video.smoke.test.ts --testTimeout=30000"
55
55
  },
56
56
  "dependencies": {
57
- "@getpaseo/plugin": "0.12.0",
57
+ "@getpaseo/plugin": "0.13.0",
58
58
  "@modelcontextprotocol/sdk": "1.30.0",
59
59
  "agent-browser": "0.39.1",
60
60
  "esbuild": "0.28.2",
@@ -62,7 +62,7 @@
62
62
  },
63
63
  "devDependencies": {
64
64
  "@biomejs/biome": "^2.5.10",
65
- "@getpaseo/server": "0.12.0",
65
+ "@getpaseo/server": "0.13.0",
66
66
  "@tanstack/react-query": "^5.102.3",
67
67
  "@types/node": "^24.10.1",
68
68
  "@types/react": "^19.2.18",
package/paseo-plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "shared-browser",
3
3
  "description": "One real Chromium session per Paseo workspace, shared live across every connected client.",
4
- "requirements": { "paseo": "^0.12.0" },
4
+ "requirements": { "paseo": "^0.13.0" },
5
5
  "build": [["node", "scripts/prepare-dependencies.mjs"], ["npm", "run", "prepare:runtime"]]
6
6
  }