@anvia/react-ui 1.2.0 → 1.2.2

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/dist/index.js CHANGED
@@ -9,6 +9,15 @@ import {
9
9
  ComposerPrimitive,
10
10
  ThreadPrimitive
11
11
  } from "./chunk-Y2KAMPVJ.js";
12
+ import {
13
+ CompletionPrimitive
14
+ } from "./chunk-HGOPGZNQ.js";
15
+ import {
16
+ ImagePrimitive
17
+ } from "./chunk-DLBNFBXQ.js";
18
+ import {
19
+ HumanInputPrimitive
20
+ } from "./chunk-N5XQU36W.js";
12
21
  import {
13
22
  MessagePrimitive
14
23
  } from "./chunk-W2AHKMRS.js";
@@ -19,16 +28,7 @@ import {
19
28
  import {
20
29
  AttachmentPrimitive
21
30
  } from "./chunk-22EZJUPM.js";
22
- import {
23
- CompletionPrimitive
24
- } from "./chunk-HGOPGZNQ.js";
25
- import {
26
- HumanInputPrimitive
27
- } from "./chunk-N5XQU36W.js";
28
31
  import "./chunk-CKA4CE26.js";
29
- import {
30
- ImagePrimitive
31
- } from "./chunk-DLBNFBXQ.js";
32
32
  import {
33
33
  renderPrimitive
34
34
  } from "./chunk-T3ZFZ3NP.js";
@@ -183,7 +183,7 @@ var SandboxDesktop = forwardRef(
183
183
  const [status, setStatus] = useState({ state: "loading" });
184
184
  const [control, setControl] = useState(void 0);
185
185
  const [notice, setNotice] = useState(void 0);
186
- const viewOnly = readOnly || control?.readOnly === true || control?.held === true && !control.you;
186
+ const viewOnly = readOnly || control?.readOnly === true || control?.you !== true;
187
187
  const latest = useRef({
188
188
  ticket,
189
189
  reconnect,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/sandbox-desktop/desktop.tsx","../../src/sandbox-desktop/vnc-channel.ts","../../src/sandbox-desktop/connection.ts"],"sourcesContent":["import type RFB from \"@novnc/novnc\";\nimport {\n forwardRef,\n useEffect,\n useImperativeHandle,\n useRef,\n useState,\n type CSSProperties,\n type ReactNode,\n} from \"react\";\n\nimport { type PrimitiveProps } from \"../primitives\";\nimport type { RelayReconnectOptions, RelayStatus } from \"../relay/connection\";\nimport { DefaultStatus } from \"../relay/status\";\nimport { type TicketSource, resolveTicket } from \"../relay/ticket\";\nimport { DesktopConnection, type SandboxDesktopControl } from \"./connection\";\n\nexport interface SandboxDesktopHandle {\n focus(): void;\n /** Reconnect now, after the viewer gave up. */\n reconnect(): void;\n /** Take control, which pauses the agent. */\n takeOver(): void;\n /** Hand control back. */\n release(): void;\n}\n\nexport interface SandboxDesktopNotice {\n code: string;\n message: string;\n}\n\n/** What `renderControls` is given to draw its own take-over bar. */\nexport interface SandboxDesktopControlsState {\n status: RelayStatus;\n /** Undefined until the relay has said who has control. */\n control: SandboxDesktopControl | undefined;\n /** This viewer cannot send input, because of the prop or because the ticket says so. */\n readOnly: boolean;\n /** The latest failed request, such as taking over while someone else holds control. */\n notice: SandboxDesktopNotice | undefined;\n takeOver(): void;\n release(): void;\n}\n\nexport type SandboxDesktopProps = Omit<PrimitiveProps<\"div\">, \"children\" | \"onError\"> & {\n /**\n * The hub's desktop endpoint (`hub.handleDesktopUpgrade`): `ws:`/`wss:`, `http:`/`https:`, or a\n * path relative to this page.\n */\n url: string;\n /**\n * A one-time ticket, or a function returning one. Pass a function: tickets are single use, so\n * each reconnect needs a fresh one. It travels as a WebSocket subprotocol, not in the URL.\n */\n ticket?: TicketSource;\n /** Watch only. The server enforces it too; this also stops the viewer sending input. */\n readOnly?: boolean;\n /** Scale the desktop to fit the element. Default true. */\n scaleToFit?: boolean;\n /**\n * JPEG quality asked of the server, 0 (smallest, blockiest) to 9 (best, most data). Default 6.\n * Lower it for a slow link; it applies to the live connection.\n */\n qualityLevel?: number;\n /**\n * How hard the server compresses, 0 (fastest, most data) to 9 (slowest, least data). Default 2,\n * which keeps the server's CPU low. Raise it for a slow link; it applies to the live connection.\n */\n compressionLevel?: number;\n reconnect?: RelayReconnectOptions;\n onStatusChange?: (status: RelayStatus) => void;\n /** Called whenever the relay reports who has control. */\n onControlChange?: (control: SandboxDesktopControl) => void;\n /** Replace the default status line. Return null to show nothing. */\n renderStatus?: (status: RelayStatus, retry: () => void) => ReactNode;\n /** Replace the default take-over bar. Return null to show nothing. */\n renderControls?: (state: SandboxDesktopControlsState) => ReactNode;\n};\n\nconst defaultQualityLevel = 6;\nconst defaultCompressionLevel = 2;\n\n/** A level noVNC accepts: a whole number from 0 to 9. Anything else falls back to the default. */\nfunction level(value: number | undefined, fallback: number): number {\n if (value === undefined || !Number.isFinite(value)) return fallback;\n return Math.min(9, Math.max(0, Math.round(value)));\n}\n\nconst rootStyle: CSSProperties = { position: \"relative\", width: \"100%\", height: \"100%\" };\nconst screenStyle: CSSProperties = { width: \"100%\", height: \"100%\" };\n\n/**\n * A viewer for a remote sandbox's desktop, drawn with noVNC. noVNC is loaded on demand, so it costs\n * nothing until a desktop mounts. It connects to the hub's desktop relay, reconnects with a fresh\n * ticket when the link drops, and lets a person take over control from the agent.\n */\nexport const SandboxDesktop = forwardRef<SandboxDesktopHandle, SandboxDesktopProps>(\n function SandboxDesktop(\n {\n url,\n ticket,\n readOnly = false,\n scaleToFit = true,\n qualityLevel,\n compressionLevel,\n reconnect,\n onStatusChange,\n onControlChange,\n renderStatus,\n renderControls,\n style,\n ...rest\n },\n ref,\n ) {\n const screen = useRef<HTMLDivElement>(null);\n const connection = useRef<DesktopConnection | undefined>(undefined);\n const viewer = useRef<RFB | undefined>(undefined);\n const [status, setStatus] = useState<RelayStatus>({ state: \"loading\" });\n const [control, setControl] = useState<SandboxDesktopControl | undefined>(undefined);\n const [notice, setNotice] = useState<SandboxDesktopNotice | undefined>(undefined);\n\n // The viewer may send input unless it is read-only, or someone else holds control.\n const viewOnly =\n readOnly || control?.readOnly === true || (control?.held === true && !control.you);\n\n // The latest props, read by long-lived callbacks without restarting the connection.\n const latest = useRef({\n ticket,\n reconnect,\n onStatusChange,\n onControlChange,\n scaleToFit,\n viewOnly,\n quality: level(qualityLevel, defaultQualityLevel),\n compression: level(compressionLevel, defaultCompressionLevel),\n });\n latest.current = {\n ticket,\n reconnect,\n onStatusChange,\n onControlChange,\n scaleToFit,\n viewOnly,\n quality: level(qualityLevel, defaultQualityLevel),\n compression: level(compressionLevel, defaultCompressionLevel),\n };\n\n useImperativeHandle(\n ref,\n () => ({\n focus: () => viewer.current?.focus(),\n reconnect: () => connection.current?.retry(),\n takeOver: () => connection.current?.takeOver(),\n release: () => connection.current?.release(),\n }),\n [],\n );\n\n useEffect(() => {\n if (viewer.current !== undefined) viewer.current.viewOnly = viewOnly;\n }, [viewOnly]);\n useEffect(() => {\n if (viewer.current !== undefined) viewer.current.scaleViewport = scaleToFit;\n }, [scaleToFit]);\n useEffect(() => {\n if (viewer.current !== undefined) viewer.current.qualityLevel = latest.current.quality;\n }, [qualityLevel]);\n useEffect(() => {\n if (viewer.current !== undefined) {\n viewer.current.compressionLevel = latest.current.compression;\n }\n }, [compressionLevel]);\n\n useEffect(() => {\n const container = screen.current;\n if (container === null) return undefined;\n let disposed = false;\n let teardown: (() => void) | undefined;\n\n const publish = (next: RelayStatus) => {\n if (disposed) return;\n setStatus(next);\n // Who has control is unknown while the link is down.\n if (next.state !== \"open\") setControl(undefined);\n latest.current.onStatusChange?.(next);\n };\n publish({ state: \"loading\" });\n\n void (async () => {\n const { default: Rfb } = await import(\"@novnc/novnc\");\n if (disposed) return;\n\n const link = new DesktopConnection({\n url,\n getTicket: resolveTicket(() => latest.current.ticket),\n reconnect: latest.current.reconnect,\n onStatus: publish,\n onChannel: (channel) => {\n // noVNC clients are single-use: each attachment gets a new one.\n viewer.current?.disconnect();\n const rfb = new Rfb(container, channel);\n rfb.scaleViewport = latest.current.scaleToFit;\n rfb.clipViewport = false;\n // The desktop's size is set through the hub's API, never by a viewer.\n rfb.resizeSession = false;\n rfb.viewOnly = latest.current.viewOnly;\n // Set outright, not left to noVNC's defaults, so what a viewer asks of the server is\n // stated here and can be changed.\n rfb.qualityLevel = latest.current.quality;\n rfb.compressionLevel = latest.current.compression;\n viewer.current = rfb;\n },\n onControl: (next) => {\n if (disposed) return;\n setControl(next);\n setNotice(undefined);\n latest.current.onControlChange?.(next);\n },\n onNotice: (next) => {\n if (!disposed) setNotice(next);\n },\n });\n connection.current = link;\n link.start();\n teardown = () => {\n link.dispose();\n viewer.current?.disconnect();\n viewer.current = undefined;\n connection.current = undefined;\n };\n })().catch((error: unknown) => {\n publish({\n state: \"failed\",\n message: error instanceof Error ? error.message : \"Could not load the desktop viewer.\",\n });\n });\n\n return () => {\n disposed = true;\n teardown?.();\n };\n }, [url]);\n\n const retry = () => connection.current?.retry();\n const takeOver = () => connection.current?.takeOver();\n const release = () => connection.current?.release();\n const statusNode =\n renderStatus === undefined ? (\n <DefaultStatus status={status} retry={retry} />\n ) : (\n renderStatus(status, retry)\n );\n const controlsState: SandboxDesktopControlsState = {\n status,\n control,\n readOnly: readOnly || control?.readOnly === true,\n notice,\n takeOver,\n release,\n };\n const controlsNode =\n renderControls === undefined ? (\n <DefaultControls {...controlsState} />\n ) : (\n renderControls(controlsState)\n );\n\n return (\n <div\n {...rest}\n data-state={status.state}\n data-readonly={viewOnly ? \"\" : undefined}\n data-control={control?.you === true ? \"you\" : control?.held === true ? \"other\" : \"none\"}\n style={{ ...rootStyle, ...style }}\n >\n <div ref={screen} style={screenStyle} />\n {controlsNode}\n {statusNode}\n </div>\n );\n },\n);\n\nconst controlsStyle: CSSProperties = {\n position: \"absolute\",\n top: 8,\n left: 8,\n zIndex: 1,\n display: \"flex\",\n gap: 8,\n alignItems: \"center\",\n padding: \"2px 8px\",\n borderRadius: 4,\n fontSize: 12,\n background: \"rgba(0, 0, 0, 0.7)\",\n color: \"#fff\",\n};\n\nfunction DefaultControls({\n status,\n control,\n readOnly,\n notice,\n takeOver,\n release,\n}: SandboxDesktopControlsState) {\n if (status.state !== \"open\" || control === undefined) return null;\n const who = control.you\n ? \"You have control\"\n : control.held\n ? `${control.holder ?? \"Someone\"} has control`\n : readOnly\n ? \"View only\"\n : \"The agent has control\";\n return (\n <div data-sandbox-desktop-controls=\"\" style={controlsStyle}>\n <span>{who}</span>\n {control.you ? (\n <button type=\"button\" onClick={release}>\n Release\n </button>\n ) : !readOnly && !control.held ? (\n <button type=\"button\" onClick={takeOver}>\n Take over\n </button>\n ) : null}\n {notice === undefined ? null : <span role=\"alert\">{notice.message}</span>}\n </div>\n );\n}\n","/** What a {@link VncChannel} needs from the socket it wraps. */\nexport interface VncLink {\n send(data: ArrayBufferLike | ArrayBufferView): void;\n close(): void;\n}\n\n// WebSocket ready states, written out so this works wherever noVNC's own checks do.\nconst OPEN = 1;\nconst CLOSED = 3;\n\n/**\n * A WebSocket-shaped channel for noVNC to attach to. The hub's desktop relay mixes binary VNC frames\n * with JSON control frames on one socket, and noVNC understands only the former, so the link\n * hands this channel just the binary ones.\n *\n * The VNC server speaks first, so frames can arrive before noVNC has attached. They wait here\n * until it installs its `onmessage`.\n *\n * noVNC checks a channel for these property names (`send`, `close`, `binaryType`, `onerror`,\n * `onmessage`, `onopen`, `protocol`, `readyState`), as it does for a WebSocket or an\n * RTCDataChannel, so they must all be present.\n */\nexport class VncChannel {\n binaryType = \"arraybuffer\";\n protocol: string;\n onerror: ((event: unknown) => void) | null = null;\n onopen: ((event: unknown) => void) | null = null;\n onclose: ((event: unknown) => void) | null = null;\n private handler: ((event: { data: ArrayBuffer }) => void) | null = null;\n private backlog: ArrayBuffer[] = [];\n private state = OPEN;\n\n constructor(\n private readonly link: VncLink,\n protocol = \"\",\n ) {\n this.protocol = protocol;\n }\n\n get readyState(): number {\n return this.state;\n }\n\n get onmessage(): ((event: { data: ArrayBuffer }) => void) | null {\n return this.handler;\n }\n\n set onmessage(handler: ((event: { data: ArrayBuffer }) => void) | null) {\n this.handler = handler;\n if (handler === null) return;\n for (const data of this.backlog.splice(0)) handler({ data });\n }\n\n send(data: ArrayBufferLike | ArrayBufferView): void {\n if (this.state === OPEN) this.link.send(data);\n }\n\n /** noVNC is done with the connection. */\n close(): void {\n if (this.state !== OPEN) return;\n this.state = CLOSED;\n this.link.close();\n }\n\n /** A VNC frame arrived from the relay. */\n receive(data: ArrayBuffer): void {\n if (this.handler === null) this.backlog.push(data);\n else this.handler({ data });\n }\n\n /** The relay closed the socket. */\n closed(event: { code: number; reason: string }): void {\n if (this.state === CLOSED) return;\n this.state = CLOSED;\n this.backlog = [];\n this.onclose?.(event);\n }\n}\n","import {\n type RelayProtocol,\n type RelayReconnectOptions,\n type RelaySession,\n type RelayStatus,\n RelayConnection,\n} from \"../relay/connection\";\nimport { VncChannel } from \"./vnc-channel\";\n\n/** Subprotocol the hub's `handleDesktopUpgrade` selects. */\nexport const desktopSubprotocol = \"anvia.desktop.v1\";\n\n/** Who is driving the desktop, as the relay reports it to this viewer. */\nexport interface SandboxDesktopControl {\n /** Someone has taken control, which pauses the agent. */\n held: boolean;\n /** Who. Present when `held`. */\n holder?: string;\n /** When the hold lapses unless renewed. */\n expiresAt?: string;\n /** This viewer is the holder. */\n you: boolean;\n /** This viewer may only watch. */\n readOnly: boolean;\n}\n\nexport interface DesktopConnectionOptions {\n url: string;\n getTicket?: (() => string | Promise<string>) | undefined;\n reconnect?: RelayReconnectOptions | undefined;\n onStatus: (status: RelayStatus) => void;\n /** The relay accepted this attachment. Attach a VNC client to the channel. */\n onChannel: (channel: VncChannel, reconnected: boolean) => void;\n onControl: (control: SandboxDesktopControl) => void;\n /** A request over the control channel failed, such as taking over while someone else holds it. */\n onNotice?: ((notice: { code: string; message: string }) => void) | undefined;\n}\n\n/** Errors worth another attempt: the sandbox may simply not be reachable yet. */\nconst retryableCodes = new Set([\"sandbox_unavailable\", \"timeout\"]);\n/** The hub closes a socket it could not set up with `<error code>: <message>`. */\nconst closeReason = /^([a-z_]+): (.*)$/s;\n/** The relay closes with this code when the viewer sent something it could not account for. */\nconst policyViolation = 1008;\n\ntype ControlMessage = { type: \"control\" } & SandboxDesktopControl;\ntype ErrorMessage = { type: \"error\"; code?: string; message?: string };\n\n/**\n * One browser link to the hub's desktop relay, over a {@link RelayConnection}. It hands each\n * accepted attachment to the owner as a channel for a VNC client, and speaks the relay's small\n * JSON control protocol (`takeover`, `release`, and the `control` updates that come back).\n */\nexport class DesktopConnection {\n private readonly relay: RelayConnection;\n\n constructor(options: DesktopConnectionOptions) {\n let channel: VncChannel | undefined;\n let accepted = false;\n const protocol: RelayProtocol = {\n subprotocol: desktopSubprotocol,\n label: \"desktop\",\n open: (socket) => {\n accepted = false;\n channel = new VncChannel(\n { send: (data) => socket.send(data), close: () => socket.close(1000) },\n socket.protocol,\n );\n },\n message: (event, session: RelaySession) => {\n // The first thing the relay sends means it set the viewer up; a failure closes instead.\n if (!accepted && channel !== undefined) {\n accepted = true;\n options.onChannel(channel, session.ready());\n }\n if (typeof event.data !== \"string\") {\n channel?.receive(event.data as ArrayBuffer);\n return;\n }\n const message = parseText(event.data);\n if (message?.type === \"control\") {\n const { type: _type, ...control } = message;\n options.onControl(control);\n } else if (message?.type === \"error\") {\n options.onNotice?.({\n code: message.code ?? \"protocol\",\n message: message.message ?? \"The request failed.\",\n });\n }\n },\n close: (event, session) => {\n channel?.closed(event);\n const match = closeReason.exec(event.reason);\n if (match !== null && !retryableCodes.has(match[1] ?? \"\")) {\n session.fatal(match[2] ?? \"The desktop could not be opened.\");\n } else if (event.code === policyViolation) {\n session.fatal(event.reason || \"The desktop relay refused what the viewer sent.\");\n }\n },\n };\n this.relay = new RelayConnection({\n url: options.url,\n getTicket: options.getTicket,\n onStatus: options.onStatus,\n reconnect: options.reconnect,\n protocol,\n });\n }\n\n start(): void {\n this.relay.start();\n }\n\n retry(): void {\n this.relay.retry();\n }\n\n /** Ask for control, which pauses the agent's inputs. The answer arrives as a `control` update. */\n takeOver(): void {\n this.relay.sendJson({ type: \"takeover\" });\n }\n\n release(): void {\n this.relay.sendJson({ type: \"release\" });\n }\n\n dispose(): void {\n this.relay.dispose();\n }\n}\n\nfunction parseText(text: string): ControlMessage | ErrorMessage | undefined {\n try {\n const value: unknown = JSON.parse(text);\n if (typeof value !== \"object\" || value === null) return undefined;\n const type = (value as { type?: unknown }).type;\n return type === \"control\" || type === \"error\"\n ? (value as ControlMessage | ErrorMessage)\n : undefined;\n } catch {\n return undefined;\n }\n}\n"],"mappings":"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","names":["SandboxDesktop"]}
1
+ {"version":3,"sources":["../../src/sandbox-desktop/desktop.tsx","../../src/sandbox-desktop/vnc-channel.ts","../../src/sandbox-desktop/connection.ts"],"sourcesContent":["import type RFB from \"@novnc/novnc\";\nimport {\n forwardRef,\n useEffect,\n useImperativeHandle,\n useRef,\n useState,\n type CSSProperties,\n type ReactNode,\n} from \"react\";\n\nimport { type PrimitiveProps } from \"../primitives\";\nimport type { RelayReconnectOptions, RelayStatus } from \"../relay/connection\";\nimport { DefaultStatus } from \"../relay/status\";\nimport { type TicketSource, resolveTicket } from \"../relay/ticket\";\nimport { DesktopConnection, type SandboxDesktopControl } from \"./connection\";\n\nexport interface SandboxDesktopHandle {\n focus(): void;\n /** Reconnect now, after the viewer gave up. */\n reconnect(): void;\n /** Take control, which pauses the agent. */\n takeOver(): void;\n /** Hand control back. */\n release(): void;\n}\n\nexport interface SandboxDesktopNotice {\n code: string;\n message: string;\n}\n\n/** What `renderControls` is given to draw its own take-over bar. */\nexport interface SandboxDesktopControlsState {\n status: RelayStatus;\n /** Undefined until the relay has said who has control. */\n control: SandboxDesktopControl | undefined;\n /** This viewer cannot send input, because of the prop or because the ticket says so. */\n readOnly: boolean;\n /** The latest failed request, such as taking over while someone else holds control. */\n notice: SandboxDesktopNotice | undefined;\n takeOver(): void;\n release(): void;\n}\n\nexport type SandboxDesktopProps = Omit<PrimitiveProps<\"div\">, \"children\" | \"onError\"> & {\n /**\n * The hub's desktop endpoint (`hub.handleDesktopUpgrade`): `ws:`/`wss:`, `http:`/`https:`, or a\n * path relative to this page.\n */\n url: string;\n /**\n * A one-time ticket, or a function returning one. Pass a function: tickets are single use, so\n * each reconnect needs a fresh one. It travels as a WebSocket subprotocol, not in the URL.\n */\n ticket?: TicketSource;\n /** Watch only. The server enforces it too; this also stops the viewer sending input. */\n readOnly?: boolean;\n /** Scale the desktop to fit the element. Default true. */\n scaleToFit?: boolean;\n /**\n * JPEG quality asked of the server, 0 (smallest, blockiest) to 9 (best, most data). Default 6.\n * Lower it for a slow link; it applies to the live connection.\n */\n qualityLevel?: number;\n /**\n * How hard the server compresses, 0 (fastest, most data) to 9 (slowest, least data). Default 2,\n * which keeps the server's CPU low. Raise it for a slow link; it applies to the live connection.\n */\n compressionLevel?: number;\n reconnect?: RelayReconnectOptions;\n onStatusChange?: (status: RelayStatus) => void;\n /** Called whenever the relay reports who has control. */\n onControlChange?: (control: SandboxDesktopControl) => void;\n /** Replace the default status line. Return null to show nothing. */\n renderStatus?: (status: RelayStatus, retry: () => void) => ReactNode;\n /** Replace the default take-over bar. Return null to show nothing. */\n renderControls?: (state: SandboxDesktopControlsState) => ReactNode;\n};\n\nconst defaultQualityLevel = 6;\nconst defaultCompressionLevel = 2;\n\n/** A level noVNC accepts: a whole number from 0 to 9. Anything else falls back to the default. */\nfunction level(value: number | undefined, fallback: number): number {\n if (value === undefined || !Number.isFinite(value)) return fallback;\n return Math.min(9, Math.max(0, Math.round(value)));\n}\n\nconst rootStyle: CSSProperties = { position: \"relative\", width: \"100%\", height: \"100%\" };\nconst screenStyle: CSSProperties = { width: \"100%\", height: \"100%\" };\n\n/**\n * A viewer for a remote sandbox's desktop, drawn with noVNC. noVNC is loaded on demand, so it costs\n * nothing until a desktop mounts. It connects to the hub's desktop relay, reconnects with a fresh\n * ticket when the link drops, and lets a person take over control from the agent.\n */\nexport const SandboxDesktop = forwardRef<SandboxDesktopHandle, SandboxDesktopProps>(\n function SandboxDesktop(\n {\n url,\n ticket,\n readOnly = false,\n scaleToFit = true,\n qualityLevel,\n compressionLevel,\n reconnect,\n onStatusChange,\n onControlChange,\n renderStatus,\n renderControls,\n style,\n ...rest\n },\n ref,\n ) {\n const screen = useRef<HTMLDivElement>(null);\n const connection = useRef<DesktopConnection | undefined>(undefined);\n const viewer = useRef<RFB | undefined>(undefined);\n const [status, setStatus] = useState<RelayStatus>({ state: \"loading\" });\n const [control, setControl] = useState<SandboxDesktopControl | undefined>(undefined);\n const [notice, setNotice] = useState<SandboxDesktopNotice | undefined>(undefined);\n\n // The viewer sends input only while this viewer holds control, so the agent's input and a\n // person's never mix. Until the relay says who has control, nobody does.\n const viewOnly = readOnly || control?.readOnly === true || control?.you !== true;\n\n // The latest props, read by long-lived callbacks without restarting the connection.\n const latest = useRef({\n ticket,\n reconnect,\n onStatusChange,\n onControlChange,\n scaleToFit,\n viewOnly,\n quality: level(qualityLevel, defaultQualityLevel),\n compression: level(compressionLevel, defaultCompressionLevel),\n });\n latest.current = {\n ticket,\n reconnect,\n onStatusChange,\n onControlChange,\n scaleToFit,\n viewOnly,\n quality: level(qualityLevel, defaultQualityLevel),\n compression: level(compressionLevel, defaultCompressionLevel),\n };\n\n useImperativeHandle(\n ref,\n () => ({\n focus: () => viewer.current?.focus(),\n reconnect: () => connection.current?.retry(),\n takeOver: () => connection.current?.takeOver(),\n release: () => connection.current?.release(),\n }),\n [],\n );\n\n useEffect(() => {\n if (viewer.current !== undefined) viewer.current.viewOnly = viewOnly;\n }, [viewOnly]);\n useEffect(() => {\n if (viewer.current !== undefined) viewer.current.scaleViewport = scaleToFit;\n }, [scaleToFit]);\n useEffect(() => {\n if (viewer.current !== undefined) viewer.current.qualityLevel = latest.current.quality;\n }, [qualityLevel]);\n useEffect(() => {\n if (viewer.current !== undefined) {\n viewer.current.compressionLevel = latest.current.compression;\n }\n }, [compressionLevel]);\n\n useEffect(() => {\n const container = screen.current;\n if (container === null) return undefined;\n let disposed = false;\n let teardown: (() => void) | undefined;\n\n const publish = (next: RelayStatus) => {\n if (disposed) return;\n setStatus(next);\n // Who has control is unknown while the link is down.\n if (next.state !== \"open\") setControl(undefined);\n latest.current.onStatusChange?.(next);\n };\n publish({ state: \"loading\" });\n\n void (async () => {\n const { default: Rfb } = await import(\"@novnc/novnc\");\n if (disposed) return;\n\n const link = new DesktopConnection({\n url,\n getTicket: resolveTicket(() => latest.current.ticket),\n reconnect: latest.current.reconnect,\n onStatus: publish,\n onChannel: (channel) => {\n // noVNC clients are single-use: each attachment gets a new one.\n viewer.current?.disconnect();\n const rfb = new Rfb(container, channel);\n rfb.scaleViewport = latest.current.scaleToFit;\n rfb.clipViewport = false;\n // The desktop's size is set through the hub's API, never by a viewer.\n rfb.resizeSession = false;\n rfb.viewOnly = latest.current.viewOnly;\n // Set outright, not left to noVNC's defaults, so what a viewer asks of the server is\n // stated here and can be changed.\n rfb.qualityLevel = latest.current.quality;\n rfb.compressionLevel = latest.current.compression;\n viewer.current = rfb;\n },\n onControl: (next) => {\n if (disposed) return;\n setControl(next);\n setNotice(undefined);\n latest.current.onControlChange?.(next);\n },\n onNotice: (next) => {\n if (!disposed) setNotice(next);\n },\n });\n connection.current = link;\n link.start();\n teardown = () => {\n link.dispose();\n viewer.current?.disconnect();\n viewer.current = undefined;\n connection.current = undefined;\n };\n })().catch((error: unknown) => {\n publish({\n state: \"failed\",\n message: error instanceof Error ? error.message : \"Could not load the desktop viewer.\",\n });\n });\n\n return () => {\n disposed = true;\n teardown?.();\n };\n }, [url]);\n\n const retry = () => connection.current?.retry();\n const takeOver = () => connection.current?.takeOver();\n const release = () => connection.current?.release();\n const statusNode =\n renderStatus === undefined ? (\n <DefaultStatus status={status} retry={retry} />\n ) : (\n renderStatus(status, retry)\n );\n const controlsState: SandboxDesktopControlsState = {\n status,\n control,\n readOnly: readOnly || control?.readOnly === true,\n notice,\n takeOver,\n release,\n };\n const controlsNode =\n renderControls === undefined ? (\n <DefaultControls {...controlsState} />\n ) : (\n renderControls(controlsState)\n );\n\n return (\n <div\n {...rest}\n data-state={status.state}\n data-readonly={viewOnly ? \"\" : undefined}\n data-control={control?.you === true ? \"you\" : control?.held === true ? \"other\" : \"none\"}\n style={{ ...rootStyle, ...style }}\n >\n <div ref={screen} style={screenStyle} />\n {controlsNode}\n {statusNode}\n </div>\n );\n },\n);\n\nconst controlsStyle: CSSProperties = {\n position: \"absolute\",\n top: 8,\n left: 8,\n zIndex: 1,\n display: \"flex\",\n gap: 8,\n alignItems: \"center\",\n padding: \"2px 8px\",\n borderRadius: 4,\n fontSize: 12,\n background: \"rgba(0, 0, 0, 0.7)\",\n color: \"#fff\",\n};\n\nfunction DefaultControls({\n status,\n control,\n readOnly,\n notice,\n takeOver,\n release,\n}: SandboxDesktopControlsState) {\n if (status.state !== \"open\" || control === undefined) return null;\n const who = control.you\n ? \"You have control\"\n : control.held\n ? `${control.holder ?? \"Someone\"} has control`\n : readOnly\n ? \"View only\"\n : \"The agent has control\";\n return (\n <div data-sandbox-desktop-controls=\"\" style={controlsStyle}>\n <span>{who}</span>\n {control.you ? (\n <button type=\"button\" onClick={release}>\n Release\n </button>\n ) : !readOnly && !control.held ? (\n <button type=\"button\" onClick={takeOver}>\n Take over\n </button>\n ) : null}\n {notice === undefined ? null : <span role=\"alert\">{notice.message}</span>}\n </div>\n );\n}\n","/** What a {@link VncChannel} needs from the socket it wraps. */\nexport interface VncLink {\n send(data: ArrayBufferLike | ArrayBufferView): void;\n close(): void;\n}\n\n// WebSocket ready states, written out so this works wherever noVNC's own checks do.\nconst OPEN = 1;\nconst CLOSED = 3;\n\n/**\n * A WebSocket-shaped channel for noVNC to attach to. The hub's desktop relay mixes binary VNC frames\n * with JSON control frames on one socket, and noVNC understands only the former, so the link\n * hands this channel just the binary ones.\n *\n * The VNC server speaks first, so frames can arrive before noVNC has attached. They wait here\n * until it installs its `onmessage`.\n *\n * noVNC checks a channel for these property names (`send`, `close`, `binaryType`, `onerror`,\n * `onmessage`, `onopen`, `protocol`, `readyState`), as it does for a WebSocket or an\n * RTCDataChannel, so they must all be present.\n */\nexport class VncChannel {\n binaryType = \"arraybuffer\";\n protocol: string;\n onerror: ((event: unknown) => void) | null = null;\n onopen: ((event: unknown) => void) | null = null;\n onclose: ((event: unknown) => void) | null = null;\n private handler: ((event: { data: ArrayBuffer }) => void) | null = null;\n private backlog: ArrayBuffer[] = [];\n private state = OPEN;\n\n constructor(\n private readonly link: VncLink,\n protocol = \"\",\n ) {\n this.protocol = protocol;\n }\n\n get readyState(): number {\n return this.state;\n }\n\n get onmessage(): ((event: { data: ArrayBuffer }) => void) | null {\n return this.handler;\n }\n\n set onmessage(handler: ((event: { data: ArrayBuffer }) => void) | null) {\n this.handler = handler;\n if (handler === null) return;\n for (const data of this.backlog.splice(0)) handler({ data });\n }\n\n send(data: ArrayBufferLike | ArrayBufferView): void {\n if (this.state === OPEN) this.link.send(data);\n }\n\n /** noVNC is done with the connection. */\n close(): void {\n if (this.state !== OPEN) return;\n this.state = CLOSED;\n this.link.close();\n }\n\n /** A VNC frame arrived from the relay. */\n receive(data: ArrayBuffer): void {\n if (this.handler === null) this.backlog.push(data);\n else this.handler({ data });\n }\n\n /** The relay closed the socket. */\n closed(event: { code: number; reason: string }): void {\n if (this.state === CLOSED) return;\n this.state = CLOSED;\n this.backlog = [];\n this.onclose?.(event);\n }\n}\n","import {\n type RelayProtocol,\n type RelayReconnectOptions,\n type RelaySession,\n type RelayStatus,\n RelayConnection,\n} from \"../relay/connection\";\nimport { VncChannel } from \"./vnc-channel\";\n\n/** Subprotocol the hub's `handleDesktopUpgrade` selects. */\nexport const desktopSubprotocol = \"anvia.desktop.v1\";\n\n/** Who is driving the desktop, as the relay reports it to this viewer. */\nexport interface SandboxDesktopControl {\n /** Someone has taken control, which pauses the agent. */\n held: boolean;\n /** Who. Present when `held`. */\n holder?: string;\n /** When the hold lapses unless renewed. */\n expiresAt?: string;\n /** This viewer is the holder. */\n you: boolean;\n /** This viewer may only watch. */\n readOnly: boolean;\n}\n\nexport interface DesktopConnectionOptions {\n url: string;\n getTicket?: (() => string | Promise<string>) | undefined;\n reconnect?: RelayReconnectOptions | undefined;\n onStatus: (status: RelayStatus) => void;\n /** The relay accepted this attachment. Attach a VNC client to the channel. */\n onChannel: (channel: VncChannel, reconnected: boolean) => void;\n onControl: (control: SandboxDesktopControl) => void;\n /** A request over the control channel failed, such as taking over while someone else holds it. */\n onNotice?: ((notice: { code: string; message: string }) => void) | undefined;\n}\n\n/** Errors worth another attempt: the sandbox may simply not be reachable yet. */\nconst retryableCodes = new Set([\"sandbox_unavailable\", \"timeout\"]);\n/** The hub closes a socket it could not set up with `<error code>: <message>`. */\nconst closeReason = /^([a-z_]+): (.*)$/s;\n/** The relay closes with this code when the viewer sent something it could not account for. */\nconst policyViolation = 1008;\n\ntype ControlMessage = { type: \"control\" } & SandboxDesktopControl;\ntype ErrorMessage = { type: \"error\"; code?: string; message?: string };\n\n/**\n * One browser link to the hub's desktop relay, over a {@link RelayConnection}. It hands each\n * accepted attachment to the owner as a channel for a VNC client, and speaks the relay's small\n * JSON control protocol (`takeover`, `release`, and the `control` updates that come back).\n */\nexport class DesktopConnection {\n private readonly relay: RelayConnection;\n\n constructor(options: DesktopConnectionOptions) {\n let channel: VncChannel | undefined;\n let accepted = false;\n const protocol: RelayProtocol = {\n subprotocol: desktopSubprotocol,\n label: \"desktop\",\n open: (socket) => {\n accepted = false;\n channel = new VncChannel(\n { send: (data) => socket.send(data), close: () => socket.close(1000) },\n socket.protocol,\n );\n },\n message: (event, session: RelaySession) => {\n // The first thing the relay sends means it set the viewer up; a failure closes instead.\n if (!accepted && channel !== undefined) {\n accepted = true;\n options.onChannel(channel, session.ready());\n }\n if (typeof event.data !== \"string\") {\n channel?.receive(event.data as ArrayBuffer);\n return;\n }\n const message = parseText(event.data);\n if (message?.type === \"control\") {\n const { type: _type, ...control } = message;\n options.onControl(control);\n } else if (message?.type === \"error\") {\n options.onNotice?.({\n code: message.code ?? \"protocol\",\n message: message.message ?? \"The request failed.\",\n });\n }\n },\n close: (event, session) => {\n channel?.closed(event);\n const match = closeReason.exec(event.reason);\n if (match !== null && !retryableCodes.has(match[1] ?? \"\")) {\n session.fatal(match[2] ?? \"The desktop could not be opened.\");\n } else if (event.code === policyViolation) {\n session.fatal(event.reason || \"The desktop relay refused what the viewer sent.\");\n }\n },\n };\n this.relay = new RelayConnection({\n url: options.url,\n getTicket: options.getTicket,\n onStatus: options.onStatus,\n reconnect: options.reconnect,\n protocol,\n });\n }\n\n start(): void {\n this.relay.start();\n }\n\n retry(): void {\n this.relay.retry();\n }\n\n /** Ask for control, which pauses the agent's inputs. The answer arrives as a `control` update. */\n takeOver(): void {\n this.relay.sendJson({ type: \"takeover\" });\n }\n\n release(): void {\n this.relay.sendJson({ type: \"release\" });\n }\n\n dispose(): void {\n this.relay.dispose();\n }\n}\n\nfunction parseText(text: string): ControlMessage | ErrorMessage | undefined {\n try {\n const value: unknown = JSON.parse(text);\n if (typeof value !== \"object\" || value === null) return undefined;\n const type = (value as { type?: unknown }).type;\n return type === \"control\" || type === \"error\"\n ? (value as ControlMessage | ErrorMessage)\n : undefined;\n } catch {\n return undefined;\n }\n}\n"],"mappings":";;;;;;;;AACA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;;;ACFP,IAAM,OAAO;AACb,IAAM,SAAS;AAcR,IAAM,aAAN,MAAiB;AAAA,EAUtB,YACmB,MACjB,WAAW,IACX;AAFiB;AAGjB,SAAK,WAAW;AAAA,EAClB;AAAA,EAJmB;AAAA,EAVnB,aAAa;AAAA,EACb;AAAA,EACA,UAA6C;AAAA,EAC7C,SAA4C;AAAA,EAC5C,UAA6C;AAAA,EACrC,UAA2D;AAAA,EAC3D,UAAyB,CAAC;AAAA,EAC1B,QAAQ;AAAA,EAShB,IAAI,aAAqB;AACvB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,IAAI,YAA6D;AAC/D,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,IAAI,UAAU,SAA0D;AACtE,SAAK,UAAU;AACf,QAAI,YAAY,KAAM;AACtB,eAAW,QAAQ,KAAK,QAAQ,OAAO,CAAC,EAAG,SAAQ,EAAE,KAAK,CAAC;AAAA,EAC7D;AAAA,EAEA,KAAK,MAA+C;AAClD,QAAI,KAAK,UAAU,KAAM,MAAK,KAAK,KAAK,IAAI;AAAA,EAC9C;AAAA;AAAA,EAGA,QAAc;AACZ,QAAI,KAAK,UAAU,KAAM;AACzB,SAAK,QAAQ;AACb,SAAK,KAAK,MAAM;AAAA,EAClB;AAAA;AAAA,EAGA,QAAQ,MAAyB;AAC/B,QAAI,KAAK,YAAY,KAAM,MAAK,QAAQ,KAAK,IAAI;AAAA,QAC5C,MAAK,QAAQ,EAAE,KAAK,CAAC;AAAA,EAC5B;AAAA;AAAA,EAGA,OAAO,OAA+C;AACpD,QAAI,KAAK,UAAU,OAAQ;AAC3B,SAAK,QAAQ;AACb,SAAK,UAAU,CAAC;AAChB,SAAK,UAAU,KAAK;AAAA,EACtB;AACF;;;ACnEO,IAAM,qBAAqB;AA6BlC,IAAM,iBAAiB,oBAAI,IAAI,CAAC,uBAAuB,SAAS,CAAC;AAEjE,IAAM,cAAc;AAEpB,IAAM,kBAAkB;AAUjB,IAAM,oBAAN,MAAwB;AAAA,EACZ;AAAA,EAEjB,YAAY,SAAmC;AAC7C,QAAI;AACJ,QAAI,WAAW;AACf,UAAM,WAA0B;AAAA,MAC9B,aAAa;AAAA,MACb,OAAO;AAAA,MACP,MAAM,CAAC,WAAW;AAChB,mBAAW;AACX,kBAAU,IAAI;AAAA,UACZ,EAAE,MAAM,CAAC,SAAS,OAAO,KAAK,IAAI,GAAG,OAAO,MAAM,OAAO,MAAM,GAAI,EAAE;AAAA,UACrE,OAAO;AAAA,QACT;AAAA,MACF;AAAA,MACA,SAAS,CAAC,OAAO,YAA0B;AAEzC,YAAI,CAAC,YAAY,YAAY,QAAW;AACtC,qBAAW;AACX,kBAAQ,UAAU,SAAS,QAAQ,MAAM,CAAC;AAAA,QAC5C;AACA,YAAI,OAAO,MAAM,SAAS,UAAU;AAClC,mBAAS,QAAQ,MAAM,IAAmB;AAC1C;AAAA,QACF;AACA,cAAM,UAAU,UAAU,MAAM,IAAI;AACpC,YAAI,SAAS,SAAS,WAAW;AAC/B,gBAAM,EAAE,MAAM,OAAO,GAAG,QAAQ,IAAI;AACpC,kBAAQ,UAAU,OAAO;AAAA,QAC3B,WAAW,SAAS,SAAS,SAAS;AACpC,kBAAQ,WAAW;AAAA,YACjB,MAAM,QAAQ,QAAQ;AAAA,YACtB,SAAS,QAAQ,WAAW;AAAA,UAC9B,CAAC;AAAA,QACH;AAAA,MACF;AAAA,MACA,OAAO,CAAC,OAAO,YAAY;AACzB,iBAAS,OAAO,KAAK;AACrB,cAAM,QAAQ,YAAY,KAAK,MAAM,MAAM;AAC3C,YAAI,UAAU,QAAQ,CAAC,eAAe,IAAI,MAAM,CAAC,KAAK,EAAE,GAAG;AACzD,kBAAQ,MAAM,MAAM,CAAC,KAAK,kCAAkC;AAAA,QAC9D,WAAW,MAAM,SAAS,iBAAiB;AACzC,kBAAQ,MAAM,MAAM,UAAU,iDAAiD;AAAA,QACjF;AAAA,MACF;AAAA,IACF;AACA,SAAK,QAAQ,IAAI,gBAAgB;AAAA,MAC/B,KAAK,QAAQ;AAAA,MACb,WAAW,QAAQ;AAAA,MACnB,UAAU,QAAQ;AAAA,MAClB,WAAW,QAAQ;AAAA,MACnB;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEA,QAAc;AACZ,SAAK,MAAM,MAAM;AAAA,EACnB;AAAA,EAEA,QAAc;AACZ,SAAK,MAAM,MAAM;AAAA,EACnB;AAAA;AAAA,EAGA,WAAiB;AACf,SAAK,MAAM,SAAS,EAAE,MAAM,WAAW,CAAC;AAAA,EAC1C;AAAA,EAEA,UAAgB;AACd,SAAK,MAAM,SAAS,EAAE,MAAM,UAAU,CAAC;AAAA,EACzC;AAAA,EAEA,UAAgB;AACd,SAAK,MAAM,QAAQ;AAAA,EACrB;AACF;AAEA,SAAS,UAAU,MAAyD;AAC1E,MAAI;AACF,UAAM,QAAiB,KAAK,MAAM,IAAI;AACtC,QAAI,OAAO,UAAU,YAAY,UAAU,KAAM,QAAO;AACxD,UAAM,OAAQ,MAA6B;AAC3C,WAAO,SAAS,aAAa,SAAS,UACjC,QACD;AAAA,EACN,QAAQ;AACN,WAAO;AAAA,EACT;AACF;;;AF4GQ,cAoBF,YApBE;AA1KR,IAAM,sBAAsB;AAC5B,IAAM,0BAA0B;AAGhC,SAAS,MAAM,OAA2B,UAA0B;AAClE,MAAI,UAAU,UAAa,CAAC,OAAO,SAAS,KAAK,EAAG,QAAO;AAC3D,SAAO,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,KAAK,MAAM,KAAK,CAAC,CAAC;AACnD;AAEA,IAAM,YAA2B,EAAE,UAAU,YAAY,OAAO,QAAQ,QAAQ,OAAO;AACvF,IAAM,cAA6B,EAAE,OAAO,QAAQ,QAAQ,OAAO;AAO5D,IAAM,iBAAiB;AAAA,EAC5B,SAASA,gBACP;AAAA,IACE;AAAA,IACA;AAAA,IACA,WAAW;AAAA,IACX,aAAa;AAAA,IACb;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,GAAG;AAAA,EACL,GACA,KACA;AACA,UAAM,SAAS,OAAuB,IAAI;AAC1C,UAAM,aAAa,OAAsC,MAAS;AAClE,UAAM,SAAS,OAAwB,MAAS;AAChD,UAAM,CAAC,QAAQ,SAAS,IAAI,SAAsB,EAAE,OAAO,UAAU,CAAC;AACtE,UAAM,CAAC,SAAS,UAAU,IAAI,SAA4C,MAAS;AACnF,UAAM,CAAC,QAAQ,SAAS,IAAI,SAA2C,MAAS;AAIhF,UAAM,WAAW,YAAY,SAAS,aAAa,QAAQ,SAAS,QAAQ;AAG5E,UAAM,SAAS,OAAO;AAAA,MACpB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,SAAS,MAAM,cAAc,mBAAmB;AAAA,MAChD,aAAa,MAAM,kBAAkB,uBAAuB;AAAA,IAC9D,CAAC;AACD,WAAO,UAAU;AAAA,MACf;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,SAAS,MAAM,cAAc,mBAAmB;AAAA,MAChD,aAAa,MAAM,kBAAkB,uBAAuB;AAAA,IAC9D;AAEA;AAAA,MACE;AAAA,MACA,OAAO;AAAA,QACL,OAAO,MAAM,OAAO,SAAS,MAAM;AAAA,QACnC,WAAW,MAAM,WAAW,SAAS,MAAM;AAAA,QAC3C,UAAU,MAAM,WAAW,SAAS,SAAS;AAAA,QAC7C,SAAS,MAAM,WAAW,SAAS,QAAQ;AAAA,MAC7C;AAAA,MACA,CAAC;AAAA,IACH;AAEA,cAAU,MAAM;AACd,UAAI,OAAO,YAAY,OAAW,QAAO,QAAQ,WAAW;AAAA,IAC9D,GAAG,CAAC,QAAQ,CAAC;AACb,cAAU,MAAM;AACd,UAAI,OAAO,YAAY,OAAW,QAAO,QAAQ,gBAAgB;AAAA,IACnE,GAAG,CAAC,UAAU,CAAC;AACf,cAAU,MAAM;AACd,UAAI,OAAO,YAAY,OAAW,QAAO,QAAQ,eAAe,OAAO,QAAQ;AAAA,IACjF,GAAG,CAAC,YAAY,CAAC;AACjB,cAAU,MAAM;AACd,UAAI,OAAO,YAAY,QAAW;AAChC,eAAO,QAAQ,mBAAmB,OAAO,QAAQ;AAAA,MACnD;AAAA,IACF,GAAG,CAAC,gBAAgB,CAAC;AAErB,cAAU,MAAM;AACd,YAAM,YAAY,OAAO;AACzB,UAAI,cAAc,KAAM,QAAO;AAC/B,UAAI,WAAW;AACf,UAAI;AAEJ,YAAM,UAAU,CAAC,SAAsB;AACrC,YAAI,SAAU;AACd,kBAAU,IAAI;AAEd,YAAI,KAAK,UAAU,OAAQ,YAAW,MAAS;AAC/C,eAAO,QAAQ,iBAAiB,IAAI;AAAA,MACtC;AACA,cAAQ,EAAE,OAAO,UAAU,CAAC;AAE5B,YAAM,YAAY;AAChB,cAAM,EAAE,SAAS,IAAI,IAAI,MAAM,OAAO,cAAc;AACpD,YAAI,SAAU;AAEd,cAAM,OAAO,IAAI,kBAAkB;AAAA,UACjC;AAAA,UACA,WAAW,cAAc,MAAM,OAAO,QAAQ,MAAM;AAAA,UACpD,WAAW,OAAO,QAAQ;AAAA,UAC1B,UAAU;AAAA,UACV,WAAW,CAAC,YAAY;AAEtB,mBAAO,SAAS,WAAW;AAC3B,kBAAM,MAAM,IAAI,IAAI,WAAW,OAAO;AACtC,gBAAI,gBAAgB,OAAO,QAAQ;AACnC,gBAAI,eAAe;AAEnB,gBAAI,gBAAgB;AACpB,gBAAI,WAAW,OAAO,QAAQ;AAG9B,gBAAI,eAAe,OAAO,QAAQ;AAClC,gBAAI,mBAAmB,OAAO,QAAQ;AACtC,mBAAO,UAAU;AAAA,UACnB;AAAA,UACA,WAAW,CAAC,SAAS;AACnB,gBAAI,SAAU;AACd,uBAAW,IAAI;AACf,sBAAU,MAAS;AACnB,mBAAO,QAAQ,kBAAkB,IAAI;AAAA,UACvC;AAAA,UACA,UAAU,CAAC,SAAS;AAClB,gBAAI,CAAC,SAAU,WAAU,IAAI;AAAA,UAC/B;AAAA,QACF,CAAC;AACD,mBAAW,UAAU;AACrB,aAAK,MAAM;AACX,mBAAW,MAAM;AACf,eAAK,QAAQ;AACb,iBAAO,SAAS,WAAW;AAC3B,iBAAO,UAAU;AACjB,qBAAW,UAAU;AAAA,QACvB;AAAA,MACF,GAAG,EAAE,MAAM,CAAC,UAAmB;AAC7B,gBAAQ;AAAA,UACN,OAAO;AAAA,UACP,SAAS,iBAAiB,QAAQ,MAAM,UAAU;AAAA,QACpD,CAAC;AAAA,MACH,CAAC;AAED,aAAO,MAAM;AACX,mBAAW;AACX,mBAAW;AAAA,MACb;AAAA,IACF,GAAG,CAAC,GAAG,CAAC;AAER,UAAM,QAAQ,MAAM,WAAW,SAAS,MAAM;AAC9C,UAAM,WAAW,MAAM,WAAW,SAAS,SAAS;AACpD,UAAM,UAAU,MAAM,WAAW,SAAS,QAAQ;AAClD,UAAM,aACJ,iBAAiB,SACf,oBAAC,iBAAc,QAAgB,OAAc,IAE7C,aAAa,QAAQ,KAAK;AAE9B,UAAM,gBAA6C;AAAA,MACjD;AAAA,MACA;AAAA,MACA,UAAU,YAAY,SAAS,aAAa;AAAA,MAC5C;AAAA,MACA;AAAA,MACA;AAAA,IACF;AACA,UAAM,eACJ,mBAAmB,SACjB,oBAAC,mBAAiB,GAAG,eAAe,IAEpC,eAAe,aAAa;AAGhC,WACE;AAAA,MAAC;AAAA;AAAA,QACE,GAAG;AAAA,QACJ,cAAY,OAAO;AAAA,QACnB,iBAAe,WAAW,KAAK;AAAA,QAC/B,gBAAc,SAAS,QAAQ,OAAO,QAAQ,SAAS,SAAS,OAAO,UAAU;AAAA,QACjF,OAAO,EAAE,GAAG,WAAW,GAAG,MAAM;AAAA,QAEhC;AAAA,8BAAC,SAAI,KAAK,QAAQ,OAAO,aAAa;AAAA,UACrC;AAAA,UACA;AAAA;AAAA;AAAA,IACH;AAAA,EAEJ;AACF;AAEA,IAAM,gBAA+B;AAAA,EACnC,UAAU;AAAA,EACV,KAAK;AAAA,EACL,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,KAAK;AAAA,EACL,YAAY;AAAA,EACZ,SAAS;AAAA,EACT,cAAc;AAAA,EACd,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,OAAO;AACT;AAEA,SAAS,gBAAgB;AAAA,EACvB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAgC;AAC9B,MAAI,OAAO,UAAU,UAAU,YAAY,OAAW,QAAO;AAC7D,QAAM,MAAM,QAAQ,MAChB,qBACA,QAAQ,OACN,GAAG,QAAQ,UAAU,SAAS,iBAC9B,WACE,cACA;AACR,SACE,qBAAC,SAAI,iCAA8B,IAAG,OAAO,eAC3C;AAAA,wBAAC,UAAM,eAAI;AAAA,IACV,QAAQ,MACP,oBAAC,YAAO,MAAK,UAAS,SAAS,SAAS,qBAExC,IACE,CAAC,YAAY,CAAC,QAAQ,OACxB,oBAAC,YAAO,MAAK,UAAS,SAAS,UAAU,uBAEzC,IACE;AAAA,IACH,WAAW,SAAY,OAAO,oBAAC,UAAK,MAAK,SAAS,iBAAO,SAAQ;AAAA,KACpE;AAEJ;","names":["SandboxDesktop"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anvia/react-ui",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "Composable, headless React UI primitives for Anvia applications.",
5
5
  "author": "anvia",
6
6
  "license": "MIT",
@@ -88,7 +88,7 @@
88
88
  "@tiptap/pm": "^3.32.0",
89
89
  "@tiptap/react": "^3.32.0",
90
90
  "@tiptap/suggestion": "^3.32.0",
91
- "marked": "^18.1.1",
91
+ "marked": "^18.1.2",
92
92
  "react-markdown": "^10.1.0",
93
93
  "remark-gfm": "^4.0.1"
94
94
  },
@@ -99,7 +99,7 @@
99
99
  "@xterm/addon-fit": ">=0.10.0",
100
100
  "@novnc/novnc": ">=1.5.0",
101
101
  "@anvia/client": "^1.6.0",
102
- "@anvia/graph": "^1.1.5",
102
+ "@anvia/graph": "^1.1.6",
103
103
  "@anvia/react": "^1.1.5"
104
104
  },
105
105
  "peerDependenciesMeta": {
@@ -123,17 +123,17 @@
123
123
  "@types/react": "^19.3.0",
124
124
  "@types/react-dom": "^19.3.0",
125
125
  "@vitest/coverage-v8": "4.1.11",
126
- "@xterm/addon-fit": "^0.10.0",
127
- "@xterm/xterm": "^5.5.0",
126
+ "@xterm/addon-fit": "^0.11.0",
127
+ "@xterm/xterm": "^6.0.0",
128
128
  "happy-dom": "20.9.0",
129
- "react": "19.3.0",
130
- "react-dom": "19.3.0",
129
+ "react": "19.3.1",
130
+ "react-dom": "19.3.1",
131
131
  "tsup": "^8.5.0",
132
132
  "typescript": "^5.9.3",
133
133
  "vitest": "^4.1.11",
134
- "@anvia/react": "1.1.5",
135
134
  "@anvia/client": "1.6.0",
136
- "@anvia/graph": "1.1.5"
135
+ "@anvia/graph": "1.1.6",
136
+ "@anvia/react": "1.1.5"
137
137
  },
138
138
  "scripts": {
139
139
  "build": "tsup src/index.ts src/attachment/index.ts src/chat/index.ts src/completion/index.ts src/graph-explorer/index.ts src/human-input/index.ts src/image/index.ts src/message/index.ts src/sandbox-desktop/index.ts src/sandbox-terminal/index.ts src/selection-toolbar/index.ts src/shared.ts src/stream/index.ts src/thread-list/index.ts --format esm --dts --sourcemap --clean --external react --external react-dom --external @xterm/xterm --external @xterm/addon-fit --external @novnc/novnc",