@arbidocs/blocks 0.5.126 → 0.5.127
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/README.md +19 -0
- package/dist/{ConnectedCallDock-733O5M36.cjs → ConnectedCallDock-XJOYVVAH.cjs} +5 -5
- package/dist/{ConnectedCallDock-733O5M36.cjs.map → ConnectedCallDock-XJOYVVAH.cjs.map} +1 -1
- package/dist/{ConnectedCallRoom-TAUGSRGV.cjs → ConnectedCallRoom-CUDYAPQK.cjs} +5 -5
- package/dist/{ConnectedCallRoom-TAUGSRGV.cjs.map → ConnectedCallRoom-CUDYAPQK.cjs.map} +1 -1
- package/dist/{ConnectedNativeDocxComparison-HDE522YC.cjs → ConnectedNativeDocxComparison-JUF2TI2P.cjs} +9 -9
- package/dist/{ConnectedNativeDocxComparison-HDE522YC.cjs.map → ConnectedNativeDocxComparison-JUF2TI2P.cjs.map} +1 -1
- package/dist/ConnectedPresentation-SI3L77J4.cjs +1 -1
- package/dist/{ConnectedSlidesTab-JSPU6EY5.cjs → ConnectedSlidesTab-E76OVY34.cjs} +3 -3
- package/dist/{ConnectedSlidesTab-JSPU6EY5.cjs.map → ConnectedSlidesTab-E76OVY34.cjs.map} +1 -1
- package/dist/{ConnectedStreamedWordArtifact-4FHPOAFZ.cjs → ConnectedStreamedWordArtifact-Q64DRRQD.cjs} +9 -9
- package/dist/{ConnectedStreamedWordArtifact-4FHPOAFZ.cjs.map → ConnectedStreamedWordArtifact-Q64DRRQD.cjs.map} +1 -1
- package/dist/{DocumentReviewCard-BDNXP3D4.cjs → DocumentReviewCard-M53XFLI2.cjs} +4 -4
- package/dist/{DocumentReviewCard-BDNXP3D4.cjs.map → DocumentReviewCard-M53XFLI2.cjs.map} +1 -1
- package/dist/{FilesViewerDialog-WQRNSKWM.cjs → FilesViewerDialog-V6FSRQTP.cjs} +8 -8
- package/dist/{FilesViewerDialog-WQRNSKWM.cjs.map → FilesViewerDialog-V6FSRQTP.cjs.map} +1 -1
- package/dist/{NativeDocxEditor-SUDULUN3.cjs → NativeDocxEditor-2PTEK6ND.cjs} +6 -6
- package/dist/{NativeDocxEditor-SUDULUN3.cjs.map → NativeDocxEditor-2PTEK6ND.cjs.map} +1 -1
- package/dist/WorkspaceTableView-3XYAV4BV.cjs +1 -1
- package/dist/agents/index.cjs +1 -1
- package/dist/ai-IEIR7SC2.cjs +1 -1
- package/dist/calls/index.cjs +2 -2
- package/dist/{chunk-Q6N5D3R5.cjs → chunk-23I5EUZA.cjs} +45 -15
- package/dist/chunk-23I5EUZA.cjs.map +1 -0
- package/dist/{chunk-OHHKBURG.cjs → chunk-2SU37RZA.cjs} +378 -11
- package/dist/chunk-2SU37RZA.cjs.map +1 -0
- package/dist/{chunk-6B4CCDDB.cjs → chunk-ESVRVZ53.cjs} +5 -5
- package/dist/{chunk-6B4CCDDB.cjs.map → chunk-ESVRVZ53.cjs.map} +1 -1
- package/dist/{chunk-KA3F6TIH.cjs → chunk-IWHP7FAB.cjs} +12 -5
- package/dist/chunk-IWHP7FAB.cjs.map +1 -0
- package/dist/{chunk-FB3JXSQT.cjs → chunk-JVLOIIA7.cjs} +8 -8
- package/dist/{chunk-FB3JXSQT.cjs.map → chunk-JVLOIIA7.cjs.map} +1 -1
- package/dist/{chunk-FGOJBOZX.cjs → chunk-M6WVIGYP.cjs} +4 -4
- package/dist/{chunk-FGOJBOZX.cjs.map → chunk-M6WVIGYP.cjs.map} +1 -1
- package/dist/{chunk-I4X72WUT.cjs → chunk-MMSHIIKG.cjs} +6 -6
- package/dist/{chunk-I4X72WUT.cjs.map → chunk-MMSHIIKG.cjs.map} +1 -1
- package/dist/{chunk-LXM5QXII.cjs → chunk-MXBXZXXQ.cjs} +3 -3
- package/dist/{chunk-LXM5QXII.cjs.map → chunk-MXBXZXXQ.cjs.map} +1 -1
- package/dist/{chunk-463QM764.cjs → chunk-NG2EE7HR.cjs} +3 -3
- package/dist/{chunk-463QM764.cjs.map → chunk-NG2EE7HR.cjs.map} +1 -1
- package/dist/chunk-NTDQQLDD.cjs +18 -0
- package/dist/{chunk-OALQGO5C.cjs.map → chunk-NTDQQLDD.cjs.map} +1 -1
- package/dist/{chunk-XNB27POJ.cjs → chunk-Q6IKXWPG.cjs} +4 -4
- package/dist/{chunk-XNB27POJ.cjs.map → chunk-Q6IKXWPG.cjs.map} +1 -1
- package/dist/{chunk-LOB2ATS5.cjs → chunk-VLQCLQAS.cjs} +5 -5
- package/dist/{chunk-LOB2ATS5.cjs.map → chunk-VLQCLQAS.cjs.map} +1 -1
- package/dist/{chunk-4KEQ62SL.cjs → chunk-YP5EBGLU.cjs} +6 -6
- package/dist/{chunk-4KEQ62SL.cjs.map → chunk-YP5EBGLU.cjs.map} +1 -1
- package/dist/{chunk-CMPHJRVV.cjs → chunk-ZYGAXD3N.cjs} +3 -3
- package/dist/{chunk-CMPHJRVV.cjs.map → chunk-ZYGAXD3N.cjs.map} +1 -1
- package/dist/connect/docx.cjs +13 -13
- package/dist/connect/index.cjs +233 -287
- package/dist/connect/index.cjs.map +1 -1
- package/dist/connect/viewer.cjs +6 -6
- package/dist/connection/remote-control/ConnectedRemoteControlConsent.js +6 -3
- package/dist/connection/remote-control/ConnectedRemoteControlConsent.js.map +2 -2
- package/dist/connection/remote-control/ConnectedRemoteControlIndicator.js +12 -2
- package/dist/connection/remote-control/ConnectedRemoteControlIndicator.js.map +2 -2
- package/dist/connection/remote-control/ConnectedRemoteControlSwitch.js +1 -1
- package/dist/connection/remote-control/ConnectedRemoteControlSwitch.js.map +1 -1
- package/dist/connection/remote-control/browser-diagnostics.d.ts +7 -0
- package/dist/connection/remote-control/browser-diagnostics.js +109 -0
- package/dist/connection/remote-control/browser-diagnostics.js.map +7 -0
- package/dist/connection/remote-control/remoteControl.d.ts +9 -1
- package/dist/connection/remote-control/remoteControl.js +130 -10
- package/dist/connection/remote-control/remoteControl.js.map +2 -2
- package/dist/connection/remote-control/screen-sharing.d.ts +11 -0
- package/dist/connection/remote-control/screen-sharing.js +78 -0
- package/dist/connection/remote-control/screen-sharing.js.map +7 -0
- package/dist/connection/report-problem/screen-capture.d.ts +1 -0
- package/dist/connection/report-problem/screen-capture.js +2 -1
- package/dist/connection/report-problem/screen-capture.js.map +2 -2
- package/dist/docx/NativeDocxEditor.js +14 -12
- package/dist/docx/NativeDocxEditor.js.map +2 -2
- package/dist/docx/index.cjs +38 -38
- package/dist/docx/useVersionAutosave.d.ts +10 -0
- package/dist/docx/useVersionAutosave.js +36 -0
- package/dist/docx/useVersionAutosave.js.map +7 -0
- package/dist/embed/index.cjs +9 -9
- package/dist/facts/index.cjs +43 -43
- package/dist/index.cjs +97 -97
- package/dist/settings/index.cjs +1 -1
- package/dist/shell/RemoteControlPill.d.ts +2 -1
- package/dist/shell/RemoteControlPill.js +10 -3
- package/dist/shell/RemoteControlPill.js.map +2 -2
- package/dist/shell/index.cjs +21 -21
- package/dist/studio/index.cjs +1 -1
- package/dist/workflow-canvas/index.cjs +12 -12
- package/node_modules/@arbidocs/coedit/package.json +1 -1
- package/package.json +5 -5
- package/dist/chunk-KA3F6TIH.cjs.map +0 -1
- package/dist/chunk-OALQGO5C.cjs +0 -18
- package/dist/chunk-OHHKBURG.cjs.map +0 -1
- package/dist/chunk-Q6N5D3R5.cjs.map +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/connection/remote-control/remoteControl.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Remote-control orchestration \u2014 the frontend side of \"a backend agent drives the UI over the\n * websocket\" (the message and the session state: `@arbidocs/react`'s remote-control module).\n *\n * This is the SEPARATE, gated entry point for websocket-arriving runs. It is deliberately distinct\n * from a site's `GuidedRuns.run` so that local \"Show me\" tips (which run directly) are NEVER gated\n * by consent \u2014 GUARANTEE #4. Only runs that arrive here, from the socket, pass through the gate.\n *\n * Gate logic (`handleMessage`):\n * - the site has not turned remote control on (`guided.remoteControl`), or the consent prompt or\n * the guided cursor is not on screen \u2192 refuse: nothing runs, nothing is asked.\n * - remote control `enabled` this session \u2192 run immediately.\n * - otherwise \u2192 stash the request in `pending` (the consent prompt opens); run NOTHING until the\n * user grants (GUARANTEE #2).\n *\n * What a remote command can run: a `sequence_id` the site registered (`GuidedRuns.isSequenceId`)\n * or inline `steps` whose every step has a target, through the one guided runner \u2014 nothing else\n * (no action dispatch). In a remote run the runner acts only on what the user can see, caps waits\n * and pauses at 5 s, and never acts inside an off-limits surface (remote control's own controls,\n * so a one-off grant cannot grant itself the session; credential and endpoint forms) nor on a\n * label whose control is inside one; no guided run types into a secret field.\n *\n * Every sign-in, sign-out, session expiry and change of user drops the grant, any pending request\n * and the run in flight (`useRemoteControlSessionReset`, mounted with the handler and the cursor),\n * so nothing carries over to the next person in the tab.\n *\n * While a run executes, `controllingAgent` is set (labels the pill) and cleared afterwards.\n * Turning remote control off (`disable`) cancels any in-flight run immediately (GUARANTEE #5).\n */\nimport { useEffect, useMemo, useRef } from 'react'\nimport { logger } from '@arbidocs/client'\nimport {\n remoteControlActions,\n remoteControlStateOf,\n toRemoteControlRequest,\n useArbiHost,\n useArbiSession,\n useArbiSessionStore,\n type ArbiWsHandlers,\n type GuidedStep,\n type RemoteControlMessage,\n type RemoteControlRequest,\n type RemoteControlSession,\n} from '@arbidocs/react'\nimport { cancelGuidedRun } from '../../guided/runner'\nimport type { GuidedRuns } from '../../guided/sequences'\nimport { useGuidedRuns } from '../../guided/useGuidedRuns'\nimport { isSurfaceMounted } from '../../guided/surfaces'\n\nexport interface RemoteControl {\n /** The websocket entry point, for a `remote_control` message. Gated (see the file header). */\n handleMessage(msg: RemoteControlMessage): void\n /**\n * Grant the pending consent and run it. `forSession: true` keeps remote control enabled so\n * later websocket runs go without re-prompting; `false` runs the one pending run and leaves it\n * disabled (\"just this once\").\n */\n grantAndRun(opts: { forSession: boolean }): void\n /** Deny the pending consent \u2014 clears it and runs nothing. */\n deny(): void\n /**\n * Turn remote control OFF and revoke immediately (GUARANTEE #5): clears the enabled flag, the\n * pending prompt and the pill, and cancels any in-flight run so the guided cursor stops at once.\n */\n disable(): void\n /** Turn remote control ON directly \u2014 the user's own explicit choice. Runs nothing by itself. */\n enable(): void\n}\n\n/** Remote control on a session's state, running the site's guided runs. */\nexport function createRemoteControl(\n session: RemoteControlSession,\n guided: Pick<GuidedRuns, 'isSequenceId' | 'run' | 'remoteControl'>\n): RemoteControl {\n const actions = remoteControlActions(session)\n const state = () => remoteControlStateOf(session.getState())\n\n /** Resolve a request to the concrete run input for the runner. */\n const resolveRun = (req: RemoteControlRequest): string | GuidedStep[] | null => {\n if (req.steps && req.steps.length > 0) return req.steps\n if (req.sequenceId && guided.isSequenceId(req.sequenceId)) return req.sequenceId\n return null\n }\n\n /** Execute a request, labelling the pill for its duration. The ONLY place a websocket-arriving\n * run reaches the runner. */\n const runRemoteRequest = async (req: RemoteControlRequest): Promise<void> => {\n const run = resolveRun(req)\n if (!run) return\n actions.setControllingAgent({ name: req.agentName, extId: req.agentExtId })\n try {\n await guided.run(run, { remote: true })\n } finally {\n // Only clear the pill if THIS run is still the active one \u2014 a newer run or a disable will\n // have replaced/cleared it already.\n const current = state().controllingAgent\n if (current && current.name === req.agentName && current.extId === req.agentExtId) {\n actions.setControllingAgent(null)\n }\n }\n }\n\n return {\n handleMessage(msg) {\n if (!guided.remoteControl) {\n logger.warn('[RemoteControl] Off on this site \u2014 ignoring a request')\n return\n }\n if (!isSurfaceMounted('consent') || !isSurfaceMounted('cursor')) {\n logger.warn('[RemoteControl] No consent prompt or cursor on screen \u2014 ignoring a request')\n return\n }\n const req = toRemoteControlRequest(msg, guided.isSequenceId)\n if (!req) return\n if (state().enabled) {\n logger.info('[RemoteControl] Enabled \u2014 running requested action from', req.agentName)\n void runRemoteRequest(req)\n return\n }\n logger.info('[RemoteControl] Consent required \u2014 prompting for', req.agentName)\n actions.request(req)\n },\n grantAndRun(opts) {\n const pending = state().pending\n actions.grant(opts)\n if (pending) void runRemoteRequest(pending)\n },\n deny: () => actions.deny(),\n disable() {\n actions.setEnabled(false)\n cancelGuidedRun()\n },\n enable: () => actions.setEnabled(true),\n }\n}\n\n/** Remote control on the host's session and guided runs. */\nexport function useRemoteControlActions(): RemoteControl {\n const session = useArbiSessionStore()\n const guided = useGuidedRuns()\n return useMemo(() => createRemoteControl(session, guided), [session, guided])\n}\n\n/**\n * Drop remote control's grant, its pending request and the guided run in flight on every sign-in,\n * sign-out and session expiry of the host's `Arbi`, and whenever the signed-in user changes (a\n * host that signs in its own way), so nothing carries over to the next person in the tab.\n */\nexport function useRemoteControlSessionReset(): void {\n const session = useArbiSessionStore()\n const { arbi } = useArbiHost()\n const userId = useArbiSession().user?.external_id ?? null\n useEffect(() => {\n const reset = () => {\n remoteControlActions(session).reset()\n cancelGuidedRun()\n }\n if (!arbi) return\n // A host's stand-in `Arbi` (a demo, a test) may lack some of them.\n const off = [\n arbi.onSigningOut?.(reset),\n arbi.onSessionExpired?.(reset),\n arbi.onSignedIn?.(reset),\n ]\n return () => off.forEach((unsubscribe) => unsubscribe?.())\n }, [arbi, session])\n const lastUser = useRef(userId)\n useEffect(() => {\n if (lastUser.current === userId) return\n lastUser.current = userId\n remoteControlActions(session).reset()\n cancelGuidedRun()\n }, [userId, session])\n}\n\n/**\n * The live connection's handler for remote control (`ArbiWebSocketProvider` `handlers`): an\n * `agt-\u2026` agent asking to drive the UI goes to the consent-gated entry point. None while the\n * site has not turned remote control on (`guided.remoteControl`). It also drops the grant and\n * the run in flight at every sign-in and sign-out (`useRemoteControlSessionReset`).\n */\nexport function useRemoteControlHandlers(): ArbiWsHandlers {\n useRemoteControlSessionReset()\n const remote = useRemoteControlActions()\n const { remoteControl } = useGuidedRuns()\n return useMemo(\n () =>\n remoteControl\n ? {\n remote_control: (message) =>\n remote.handleMessage(message as unknown as RemoteControlMessage),\n }\n : {},\n [remote, remoteControl]\n )\n}\n"],
|
|
5
|
-
"mappings": ";AA6BA,SAAS,WAAW,SAAS,cAAc;AAC3C,SAAS,cAAc;AACvB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAMK;AACP,SAAS,uBAAuB;AAEhC,SAAS,qBAAqB;AAC9B,SAAS,wBAAwB;
|
|
4
|
+
"sourcesContent": ["/**\n * Remote-control orchestration \u2014 the frontend side of \"a backend agent drives the UI over the\n * websocket\" (the message and the session state: `@arbidocs/react`'s remote-control module).\n *\n * This is the SEPARATE, gated entry point for websocket-arriving runs. It is deliberately distinct\n * from a site's `GuidedRuns.run` so that local \"Show me\" tips (which run directly) are NEVER gated\n * by consent \u2014 GUARANTEE #4. Only runs that arrive here, from the socket, pass through the gate.\n *\n * Gate logic (`handleMessage`):\n * - the site has not turned remote control on (`guided.remoteControl`), or the consent prompt or\n * the guided cursor is not on screen \u2192 refuse: nothing runs, nothing is asked.\n * - remote control `enabled` this session \u2192 run immediately.\n * - otherwise \u2192 stash the request in `pending` (the consent prompt opens); run NOTHING until the\n * user grants (GUARANTEE #2).\n *\n * What a remote command can run: a `sequence_id` the site registered (`GuidedRuns.isSequenceId`)\n * or inline `steps` whose every step has a target, through the one guided runner \u2014 nothing else\n * (no action dispatch). In a remote run the runner acts only on what the user can see, caps waits\n * and pauses at 5 s, and never acts inside an off-limits surface (remote control's own controls,\n * so a one-off grant cannot grant itself the session; credential and endpoint forms) nor on a\n * label whose control is inside one; no guided run types into a secret field.\n *\n * Every sign-in, sign-out, session expiry and change of user drops the grant, any pending request\n * and the run in flight (`useRemoteControlSessionReset`, mounted with the handler and the cursor),\n * so nothing carries over to the next person in the tab.\n *\n * While a run executes, `controllingAgent` is set (labels the pill) and cleared afterwards.\n * Turning remote control off (`disable`) cancels any in-flight run immediately (GUARANTEE #5).\n */\nimport { useEffect, useMemo, useRef } from 'react'\nimport { logger } from '@arbidocs/client'\nimport {\n remoteControlActions,\n remoteControlStateOf,\n toRemoteControlRequest,\n useArbiHost,\n useArbiSession,\n useArbiSessionStore,\n useArbiSessionState,\n type ArbiWsHandlers,\n type GuidedStep,\n type RemoteControlMessage,\n type RemoteControlRequest,\n type RemoteControlSession,\n} from '@arbidocs/react'\nimport { cancelGuidedRun } from '../../guided/runner'\nimport type { GuidedRuns } from '../../guided/sequences'\nimport { useGuidedRuns } from '../../guided/useGuidedRuns'\nimport { isSurfaceMounted } from '../../guided/surfaces'\nimport { createScreenSharing, type ScreenSharing } from './screen-sharing'\n\nexport interface RemoteControl {\n /** The websocket entry point, for a `remote_control` message. Gated (see the file header). */\n handleMessage(msg: RemoteControlMessage): void\n /**\n * Grant the pending consent and run it. `forSession: true` keeps remote control enabled so\n * later websocket runs go without re-prompting; `false` runs the one pending run and leaves it\n * disabled (\"just this once\").\n */\n grantAndRun(opts: { forSession: boolean }): void\n /** Deny the pending consent \u2014 clears it and runs nothing. */\n deny(): void\n /**\n * Turn remote control OFF and revoke immediately (GUARANTEE #5): clears the enabled flag, the\n * pending prompt and the pill, and cancels any in-flight run so the guided cursor stops at once.\n */\n disable(): void\n /** Turn remote control ON directly \u2014 the user's own explicit choice. Runs nothing by itself. */\n enable(): void\n}\n\nexport interface ScreenTransport {\n reply(\n requestId: string,\n result: {\n status: 'captured' | 'denied' | 'stopped' | 'unavailable' | 'action_finished'\n screenshot_png?: string\n diagnostics?: string\n }\n ): Promise<void>\n diagnose?(): Promise<string>\n}\nconst screenRuntimes = new WeakMap<\n RemoteControlSession,\n { sharing: ScreenSharing; generation: number; setup: string }\n>()\nfunction screenRuntime(session: RemoteControlSession) {\n let runtime = screenRuntimes.get(session)\n if (!runtime) {\n runtime = { sharing: createScreenSharing(), generation: 0, setup: '' }\n screenRuntimes.set(session, runtime)\n }\n return runtime\n}\nfunction stopScreen(session: RemoteControlSession) {\n const runtime = screenRuntimes.get(session)\n if (!runtime) return\n runtime.generation++\n runtime.sharing.stop()\n runtime.setup = ''\n const actions = remoteControlActions(session)\n if (remoteControlStateOf(session.getState()).sharing) actions.setSharing(false)\n}\n\n/** Remote control on a session's state, running the site's guided runs. */\nexport function createRemoteControl(\n session: RemoteControlSession,\n guided: Pick<GuidedRuns, 'isSequenceId' | 'run' | 'remoteControl'>,\n screenTransport?: ScreenTransport\n): RemoteControl {\n const actions = remoteControlActions(session)\n const state = () => remoteControlStateOf(session.getState())\n\n const runtime = screenRuntime(session)\n const reply = async (\n req: RemoteControlRequest,\n status: 'denied' | 'stopped' | 'unavailable' | 'action_finished'\n ) => {\n if ((req.screenRequestId || req.actionRequestId) && screenTransport) {\n await screenTransport\n .reply((req.screenRequestId || req.actionRequestId)!, { status })\n .catch(() => {})\n }\n }\n const disable = () => {\n const pending = state().pending\n if (pending) void reply(pending, 'stopped')\n stopScreen(session)\n actions.setEnabled(false)\n cancelGuidedRun()\n }\n const inspect = async (req: RemoteControlRequest, keepSharing: boolean) => {\n if (!req.screenRequestId || !screenTransport) return\n const workspace = session.getState().workspaceId\n if (workspace !== req.workspaceExtId) {\n await reply(req, 'unavailable')\n return\n }\n const token = runtime.generation\n const current = () =>\n token === runtime.generation && session.getState().workspaceId === workspace\n actions.setControllingAgent({ name: req.agentName, extId: req.agentExtId })\n try {\n if (!runtime.sharing.active) {\n // Called directly from the consent button: getDisplayMedia needs this user gesture.\n await runtime.sharing.start(disable)\n if (!current()) {\n runtime.sharing.stop()\n await reply(req, 'stopped')\n return\n }\n actions.setSharing(true)\n if (screenTransport.diagnose)\n runtime.setup = await screenTransport.diagnose().catch(() => 'Setup checks unavailable.')\n }\n if (!current()) {\n await reply(req, 'stopped')\n return\n }\n const frame = await runtime.sharing.frame()\n if (!current()) {\n await reply(req, 'stopped')\n return\n }\n await screenTransport.reply(req.screenRequestId, {\n status: 'captured',\n ...frame,\n diagnostics: `${frame.diagnostics}\\n${runtime.setup}`.slice(0, 24000),\n })\n } catch {\n await reply(req, current() ? 'unavailable' : 'stopped')\n if (current()) disable()\n } finally {\n if (current()) {\n actions.setControllingAgent(null)\n if (!keepSharing) disable()\n }\n }\n }\n\n /** Resolve a request to the concrete run input for the runner. */\n const resolveRun = (req: RemoteControlRequest): string | GuidedStep[] | null => {\n if (req.steps && req.steps.length > 0) return req.steps\n if (req.sequenceId && guided.isSequenceId(req.sequenceId)) return req.sequenceId\n return null\n }\n\n /** Execute a request, labelling the pill for its duration. The ONLY place a websocket-arriving\n * run reaches the runner. */\n const runRemoteRequest = async (\n req: RemoteControlRequest,\n keepSharing = state().enabled\n ): Promise<void> => {\n if (req.screenRequestId) {\n await inspect(req, keepSharing)\n return\n }\n const run = resolveRun(req)\n if (!run) return\n const token = runtime.generation\n actions.setControllingAgent({ name: req.agentName, extId: req.agentExtId })\n try {\n await guided.run(run, { remote: true })\n if (req.actionRequestId)\n await reply(req, token === runtime.generation ? 'action_finished' : 'stopped')\n } catch {\n await reply(req, 'unavailable')\n } finally {\n // Only clear the pill if THIS run is still the active one \u2014 a newer run or a disable will\n // have replaced/cleared it already.\n const current = state().controllingAgent\n if (current && current.name === req.agentName && current.extId === req.agentExtId) {\n actions.setControllingAgent(null)\n }\n }\n }\n\n return {\n handleMessage(msg) {\n if (!guided.remoteControl) {\n logger.warn('[RemoteControl] Off on this site \u2014 ignoring a request')\n return\n }\n if (!isSurfaceMounted('consent') || !isSurfaceMounted('cursor')) {\n logger.warn('[RemoteControl] No consent prompt or cursor on screen \u2014 ignoring a request')\n return\n }\n const req = toRemoteControlRequest(msg, guided.isSequenceId)\n if (!req) return\n if (\n (req.screenRequestId || req.actionRequestId) &&\n (!screenTransport || session.getState().workspaceId !== req.workspaceExtId)\n ) {\n void reply(req, 'unavailable')\n return\n }\n if (state().enabled && (!req.screenRequestId || runtime.sharing.active)) {\n logger.info('[RemoteControl] Enabled \u2014 running requested action from', req.agentName)\n void runRemoteRequest(req)\n return\n }\n logger.info('[RemoteControl] Consent required \u2014 prompting for', req.agentName)\n if (state().pending) void reply(state().pending!, 'denied')\n actions.request(req)\n },\n grantAndRun(opts) {\n const pending = state().pending\n const keepSharing = opts.forSession || state().enabled\n actions.grant(opts)\n if (pending) void runRemoteRequest(pending, keepSharing)\n },\n deny: () => {\n const pending = state().pending\n actions.deny()\n if (pending) void reply(pending, 'denied')\n },\n disable,\n enable: () => actions.setEnabled(true),\n }\n}\n\n/** Remote control on the host's session and guided runs. */\nexport function useRemoteControlActions(): RemoteControl {\n const session = useArbiSessionStore()\n const guided = useGuidedRuns()\n const { arbi } = useArbiHost()\n return useMemo(\n () =>\n createRemoteControl(\n session,\n guided,\n arbi\n ? {\n reply: (id, result) => arbi.remoteControl.replyScreen(id, result),\n diagnose: async () => JSON.stringify(await arbi.diagnose()),\n }\n : undefined\n ),\n [session, guided, arbi]\n )\n}\n\n/**\n * Drop remote control's grant, its pending request and the guided run in flight on every sign-in,\n * sign-out and session expiry of the host's `Arbi`, and whenever the signed-in user changes (a\n * host that signs in its own way), so nothing carries over to the next person in the tab.\n */\nexport function useRemoteControlSessionReset(): void {\n const session = useArbiSessionStore()\n const { arbi } = useArbiHost()\n const userId = useArbiSession().user?.external_id ?? null\n useEffect(() => {\n const reset = () => {\n stopScreen(session)\n remoteControlActions(session).reset()\n cancelGuidedRun()\n }\n if (!arbi) return\n // A host's stand-in `Arbi` (a demo, a test) may lack some of them.\n const off = [\n arbi.onSigningOut?.(reset),\n arbi.onSessionExpired?.(reset),\n arbi.onSignedIn?.(reset),\n ]\n return () => off.forEach((unsubscribe) => unsubscribe?.())\n }, [arbi, session])\n const lastUser = useRef(userId)\n useEffect(() => {\n if (lastUser.current === userId) return\n lastUser.current = userId\n stopScreen(session)\n remoteControlActions(session).reset()\n cancelGuidedRun()\n }, [userId, session])\n}\n\n/**\n * The live connection's handler for remote control (`ArbiWebSocketProvider` `handlers`): an\n * `agt-\u2026` agent asking to drive the UI goes to the consent-gated entry point. None while the\n * site has not turned remote control on (`guided.remoteControl`). It also drops the grant and\n * the run in flight at every sign-in and sign-out (`useRemoteControlSessionReset`).\n */\nexport function useRemoteControlHandlers(): ArbiWsHandlers {\n useRemoteControlSessionReset()\n const remote = useRemoteControlActions()\n const { remoteControl } = useGuidedRuns()\n const session = useArbiSessionStore()\n const workspaceId = useArbiSessionState((s) => s.workspaceId)\n // The handler owns the stream lifecycle. A route/auth host teardown always stops sharing.\n useEffect(\n () => () => {\n stopScreen(session)\n remoteControlActions(session).setEnabled(false)\n cancelGuidedRun()\n },\n [session, workspaceId]\n )\n return useMemo(\n () =>\n remoteControl\n ? {\n remote_control: (message) =>\n remote.handleMessage(message as unknown as RemoteControlMessage),\n }\n : {},\n [remote, remoteControl]\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** A retained browser-authorized surface. Fresh frames are taken only when the agent requests one. */
|
|
2
|
+
export interface ScreenSharing {
|
|
3
|
+
readonly active: boolean;
|
|
4
|
+
start(onEnded: () => void): Promise<void>;
|
|
5
|
+
frame(): Promise<{
|
|
6
|
+
screenshot_png: string;
|
|
7
|
+
diagnostics: string;
|
|
8
|
+
}>;
|
|
9
|
+
stop(): void;
|
|
10
|
+
}
|
|
11
|
+
export declare function createScreenSharing(): ScreenSharing;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { grabFrame, ScreenCaptureError } from "../report-problem/screen-capture.js";
|
|
3
|
+
import { collectBrowserDiagnostics } from "./browser-diagnostics.js";
|
|
4
|
+
function createScreenSharing() {
|
|
5
|
+
let stream = null;
|
|
6
|
+
let diagnostics = null;
|
|
7
|
+
let generation = 0;
|
|
8
|
+
const stop = () => {
|
|
9
|
+
generation++;
|
|
10
|
+
const previous = stream;
|
|
11
|
+
stream = null;
|
|
12
|
+
previous?.getTracks().forEach((track) => track.stop());
|
|
13
|
+
diagnostics?.stop();
|
|
14
|
+
diagnostics = null;
|
|
15
|
+
};
|
|
16
|
+
return {
|
|
17
|
+
get active() {
|
|
18
|
+
return !!stream?.getVideoTracks().some((track) => track.readyState === "live");
|
|
19
|
+
},
|
|
20
|
+
async start(onEnded) {
|
|
21
|
+
const token = ++generation;
|
|
22
|
+
if (!navigator.mediaDevices?.getDisplayMedia)
|
|
23
|
+
throw new ScreenCaptureError(
|
|
24
|
+
"unsupported",
|
|
25
|
+
"Screen sharing is unavailable in this browser."
|
|
26
|
+
);
|
|
27
|
+
const next = await navigator.mediaDevices.getDisplayMedia({
|
|
28
|
+
video: { displaySurface: "browser" },
|
|
29
|
+
audio: false,
|
|
30
|
+
preferCurrentTab: true,
|
|
31
|
+
selfBrowserSurface: "include",
|
|
32
|
+
surfaceSwitching: "exclude"
|
|
33
|
+
});
|
|
34
|
+
if (token !== generation) {
|
|
35
|
+
next.getTracks().forEach((track) => track.stop());
|
|
36
|
+
throw new ScreenCaptureError("cancelled", "Sharing stopped.");
|
|
37
|
+
}
|
|
38
|
+
stream = next;
|
|
39
|
+
diagnostics = collectBrowserDiagnostics();
|
|
40
|
+
next.getVideoTracks().forEach(
|
|
41
|
+
(track) => track.addEventListener(
|
|
42
|
+
"ended",
|
|
43
|
+
() => {
|
|
44
|
+
if (stream === next) {
|
|
45
|
+
stop();
|
|
46
|
+
onEnded();
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
{ once: true }
|
|
50
|
+
)
|
|
51
|
+
);
|
|
52
|
+
},
|
|
53
|
+
async frame() {
|
|
54
|
+
if (!stream) throw new ScreenCaptureError("cancelled", "Sharing stopped.");
|
|
55
|
+
const token = generation;
|
|
56
|
+
const blob = await grabFrame(stream);
|
|
57
|
+
if (token !== generation) throw new ScreenCaptureError("cancelled", "Sharing stopped.");
|
|
58
|
+
if (blob.size > 8 * 1024 * 1024)
|
|
59
|
+
throw new ScreenCaptureError(
|
|
60
|
+
"failed",
|
|
61
|
+
"The shared screen image is too large. Share a smaller window."
|
|
62
|
+
);
|
|
63
|
+
const screenshot_png = await new Promise((resolve, reject) => {
|
|
64
|
+
const reader = new FileReader();
|
|
65
|
+
reader.onload = () => resolve(String(reader.result).split(",")[1]);
|
|
66
|
+
reader.onerror = () => reject(new Error("Could not read the screen frame."));
|
|
67
|
+
reader.readAsDataURL(blob);
|
|
68
|
+
});
|
|
69
|
+
if (token !== generation) throw new ScreenCaptureError("cancelled", "Sharing stopped.");
|
|
70
|
+
return { screenshot_png, diagnostics: diagnostics?.snapshot() ?? "" };
|
|
71
|
+
},
|
|
72
|
+
stop
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export {
|
|
76
|
+
createScreenSharing
|
|
77
|
+
};
|
|
78
|
+
//# sourceMappingURL=screen-sharing.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/connection/remote-control/screen-sharing.ts"],
|
|
4
|
+
"sourcesContent": ["import { grabFrame, ScreenCaptureError } from '../report-problem/screen-capture'\nimport { collectBrowserDiagnostics } from './browser-diagnostics'\n\n/** A retained browser-authorized surface. Fresh frames are taken only when the agent requests one. */\nexport interface ScreenSharing {\n readonly active: boolean\n start(onEnded: () => void): Promise<void>\n frame(): Promise<{ screenshot_png: string; diagnostics: string }>\n stop(): void\n}\n\nexport function createScreenSharing(): ScreenSharing {\n let stream: MediaStream | null = null\n let diagnostics: ReturnType<typeof collectBrowserDiagnostics> | null = null\n let generation = 0\n const stop = () => {\n generation++\n const previous = stream\n stream = null\n previous?.getTracks().forEach((track) => track.stop())\n diagnostics?.stop()\n diagnostics = null\n }\n return {\n get active() {\n return !!stream?.getVideoTracks().some((track) => track.readyState === 'live')\n },\n async start(onEnded) {\n const token = ++generation\n if (!navigator.mediaDevices?.getDisplayMedia)\n throw new ScreenCaptureError(\n 'unsupported',\n 'Screen sharing is unavailable in this browser.'\n )\n const next = await navigator.mediaDevices.getDisplayMedia({\n video: { displaySurface: 'browser' },\n audio: false,\n preferCurrentTab: true,\n selfBrowserSurface: 'include',\n surfaceSwitching: 'exclude',\n } as DisplayMediaStreamOptions)\n if (token !== generation) {\n next.getTracks().forEach((track) => track.stop())\n throw new ScreenCaptureError('cancelled', 'Sharing stopped.')\n }\n stream = next\n diagnostics = collectBrowserDiagnostics()\n next.getVideoTracks().forEach((track) =>\n track.addEventListener(\n 'ended',\n () => {\n if (stream === next) {\n stop()\n onEnded()\n }\n },\n { once: true }\n )\n )\n },\n async frame() {\n if (!stream) throw new ScreenCaptureError('cancelled', 'Sharing stopped.')\n const token = generation\n const blob = await grabFrame(stream)\n if (token !== generation) throw new ScreenCaptureError('cancelled', 'Sharing stopped.')\n if (blob.size > 8 * 1024 * 1024)\n throw new ScreenCaptureError(\n 'failed',\n 'The shared screen image is too large. Share a smaller window.'\n )\n const screenshot_png = await new Promise<string>((resolve, reject) => {\n const reader = new FileReader()\n reader.onload = () => resolve(String(reader.result).split(',')[1])\n reader.onerror = () => reject(new Error('Could not read the screen frame.'))\n reader.readAsDataURL(blob)\n })\n if (token !== generation) throw new ScreenCaptureError('cancelled', 'Sharing stopped.')\n return { screenshot_png, diagnostics: diagnostics?.snapshot() ?? '' }\n },\n stop,\n }\n}\n"],
|
|
5
|
+
"mappings": ";AAAA,SAAS,WAAW,0BAA0B;AAC9C,SAAS,iCAAiC;AAUnC,SAAS,sBAAqC;AACnD,MAAI,SAA6B;AACjC,MAAI,cAAmE;AACvE,MAAI,aAAa;AACjB,QAAM,OAAO,MAAM;AACjB;AACA,UAAM,WAAW;AACjB,aAAS;AACT,cAAU,UAAU,EAAE,QAAQ,CAAC,UAAU,MAAM,KAAK,CAAC;AACrD,iBAAa,KAAK;AAClB,kBAAc;AAAA,EAChB;AACA,SAAO;AAAA,IACL,IAAI,SAAS;AACX,aAAO,CAAC,CAAC,QAAQ,eAAe,EAAE,KAAK,CAAC,UAAU,MAAM,eAAe,MAAM;AAAA,IAC/E;AAAA,IACA,MAAM,MAAM,SAAS;AACnB,YAAM,QAAQ,EAAE;AAChB,UAAI,CAAC,UAAU,cAAc;AAC3B,cAAM,IAAI;AAAA,UACR;AAAA,UACA;AAAA,QACF;AACF,YAAM,OAAO,MAAM,UAAU,aAAa,gBAAgB;AAAA,QACxD,OAAO,EAAE,gBAAgB,UAAU;AAAA,QACnC,OAAO;AAAA,QACP,kBAAkB;AAAA,QAClB,oBAAoB;AAAA,QACpB,kBAAkB;AAAA,MACpB,CAA8B;AAC9B,UAAI,UAAU,YAAY;AACxB,aAAK,UAAU,EAAE,QAAQ,CAAC,UAAU,MAAM,KAAK,CAAC;AAChD,cAAM,IAAI,mBAAmB,aAAa,kBAAkB;AAAA,MAC9D;AACA,eAAS;AACT,oBAAc,0BAA0B;AACxC,WAAK,eAAe,EAAE;AAAA,QAAQ,CAAC,UAC7B,MAAM;AAAA,UACJ;AAAA,UACA,MAAM;AACJ,gBAAI,WAAW,MAAM;AACnB,mBAAK;AACL,sBAAQ;AAAA,YACV;AAAA,UACF;AAAA,UACA,EAAE,MAAM,KAAK;AAAA,QACf;AAAA,MACF;AAAA,IACF;AAAA,IACA,MAAM,QAAQ;AACZ,UAAI,CAAC,OAAQ,OAAM,IAAI,mBAAmB,aAAa,kBAAkB;AACzE,YAAM,QAAQ;AACd,YAAM,OAAO,MAAM,UAAU,MAAM;AACnC,UAAI,UAAU,WAAY,OAAM,IAAI,mBAAmB,aAAa,kBAAkB;AACtF,UAAI,KAAK,OAAO,IAAI,OAAO;AACzB,cAAM,IAAI;AAAA,UACR;AAAA,UACA;AAAA,QACF;AACF,YAAM,iBAAiB,MAAM,IAAI,QAAgB,CAAC,SAAS,WAAW;AACpE,cAAM,SAAS,IAAI,WAAW;AAC9B,eAAO,SAAS,MAAM,QAAQ,OAAO,OAAO,MAAM,EAAE,MAAM,GAAG,EAAE,CAAC,CAAC;AACjE,eAAO,UAAU,MAAM,OAAO,IAAI,MAAM,kCAAkC,CAAC;AAC3E,eAAO,cAAc,IAAI;AAAA,MAC3B,CAAC;AACD,UAAI,UAAU,WAAY,OAAM,IAAI,mBAAmB,aAAa,kBAAkB;AACtF,aAAO,EAAE,gBAAgB,aAAa,aAAa,SAAS,KAAK,GAAG;AAAA,IACtE;AAAA,IACA;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -11,4 +11,5 @@ export declare class ScreenCaptureError extends Error {
|
|
|
11
11
|
export declare function canCaptureScreen(): boolean;
|
|
12
12
|
/** Capture the current tab as a PNG blob. Throws ScreenCaptureError. */
|
|
13
13
|
export declare function captureCurrentTab(): Promise<Blob>;
|
|
14
|
+
export declare function grabFrame(stream: MediaStream): Promise<Blob>;
|
|
14
15
|
export declare function canvasToBlob(canvas: HTMLCanvasElement): Promise<Blob>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/connection/report-problem/screen-capture.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Capture one still frame of the current tab with the browser's screen-capture API. It sees\n * exactly what is on screen, including PDFs, canvases and embedded viewers, at the cost of one\n * browser prompt (\"share this tab\").\n */\n\nexport type ScreenCaptureErrorCode = 'unsupported' | 'cancelled' | 'failed'\n\nexport class ScreenCaptureError extends Error {\n constructor(\n readonly code: ScreenCaptureErrorCode,\n message: string\n ) {\n super(message)\n this.name = 'ScreenCaptureError'\n }\n}\n\nexport function canCaptureScreen(): boolean {\n return typeof navigator !== 'undefined' && !!navigator.mediaDevices?.getDisplayMedia\n}\n\n/** Capture the current tab as a PNG blob. Throws ScreenCaptureError. */\nexport async function captureCurrentTab(): Promise<Blob> {\n if (!canCaptureScreen()) {\n throw new ScreenCaptureError('unsupported', \"This browser can't capture the screen.\")\n }\n let stream: MediaStream\n try {\n stream = await navigator.mediaDevices.getDisplayMedia({\n video: { displaySurface: 'browser' },\n audio: false,\n // Chromium hints: offer this tab first, and don't let the person switch surfaces mid-capture.\n preferCurrentTab: true,\n selfBrowserSurface: 'include',\n surfaceSwitching: 'exclude',\n } as DisplayMediaStreamOptions)\n } catch (error) {\n const name = error instanceof DOMException ? error.name : ''\n if (name === 'NotAllowedError' || name === 'AbortError') {\n throw new ScreenCaptureError('cancelled', 'Screen capture was cancelled.')\n }\n throw new ScreenCaptureError('failed', \"Couldn't start screen capture.\")\n }\n try {\n return await grabFrame(stream)\n } finally {\n stream.getTracks().forEach((track) => track.stop())\n }\n}\n\
|
|
5
|
-
"mappings": ";AAQO,MAAM,2BAA2B,MAAM;AAAA,EAC5C,YACW,MACT,SACA;AACA,UAAM,OAAO;AAHJ;AAIT,SAAK,OAAO;AAAA,EACd;AAAA,EALW;AAMb;AAEO,SAAS,mBAA4B;AAC1C,SAAO,OAAO,cAAc,eAAe,CAAC,CAAC,UAAU,cAAc;AACvE;AAGA,eAAsB,oBAAmC;AACvD,MAAI,CAAC,iBAAiB,GAAG;AACvB,UAAM,IAAI,mBAAmB,eAAe,wCAAwC;AAAA,EACtF;AACA,MAAI;AACJ,MAAI;AACF,aAAS,MAAM,UAAU,aAAa,gBAAgB;AAAA,MACpD,OAAO,EAAE,gBAAgB,UAAU;AAAA,MACnC,OAAO;AAAA;AAAA,MAEP,kBAAkB;AAAA,MAClB,oBAAoB;AAAA,MACpB,kBAAkB;AAAA,IACpB,CAA8B;AAAA,EAChC,SAAS,OAAO;AACd,UAAM,OAAO,iBAAiB,eAAe,MAAM,OAAO;AAC1D,QAAI,SAAS,qBAAqB,SAAS,cAAc;AACvD,YAAM,IAAI,mBAAmB,aAAa,+BAA+B;AAAA,IAC3E;AACA,UAAM,IAAI,mBAAmB,UAAU,gCAAgC;AAAA,EACzE;AACA,MAAI;AACF,WAAO,MAAM,UAAU,MAAM;AAAA,EAC/B,UAAE;AACA,WAAO,UAAU,EAAE,QAAQ,CAAC,UAAU,MAAM,KAAK,CAAC;AAAA,EACpD;AACF;AAEA,
|
|
4
|
+
"sourcesContent": ["/**\n * Capture one still frame of the current tab with the browser's screen-capture API. It sees\n * exactly what is on screen, including PDFs, canvases and embedded viewers, at the cost of one\n * browser prompt (\"share this tab\").\n */\n\nexport type ScreenCaptureErrorCode = 'unsupported' | 'cancelled' | 'failed'\n\nexport class ScreenCaptureError extends Error {\n constructor(\n readonly code: ScreenCaptureErrorCode,\n message: string\n ) {\n super(message)\n this.name = 'ScreenCaptureError'\n }\n}\n\nexport function canCaptureScreen(): boolean {\n return typeof navigator !== 'undefined' && !!navigator.mediaDevices?.getDisplayMedia\n}\n\n/** Capture the current tab as a PNG blob. Throws ScreenCaptureError. */\nexport async function captureCurrentTab(): Promise<Blob> {\n if (!canCaptureScreen()) {\n throw new ScreenCaptureError('unsupported', \"This browser can't capture the screen.\")\n }\n let stream: MediaStream\n try {\n stream = await navigator.mediaDevices.getDisplayMedia({\n video: { displaySurface: 'browser' },\n audio: false,\n // Chromium hints: offer this tab first, and don't let the person switch surfaces mid-capture.\n preferCurrentTab: true,\n selfBrowserSurface: 'include',\n surfaceSwitching: 'exclude',\n } as DisplayMediaStreamOptions)\n } catch (error) {\n const name = error instanceof DOMException ? error.name : ''\n if (name === 'NotAllowedError' || name === 'AbortError') {\n throw new ScreenCaptureError('cancelled', 'Screen capture was cancelled.')\n }\n throw new ScreenCaptureError('failed', \"Couldn't start screen capture.\")\n }\n try {\n return await grabFrame(stream)\n } finally {\n stream.getTracks().forEach((track) => track.stop())\n }\n}\n\nexport async function grabFrame(stream: MediaStream): Promise<Blob> {\n const video = document.createElement('video')\n video.muted = true\n video.playsInline = true\n video.srcObject = stream\n await video.play()\n // The first frames can be blank while the capture warms up: wait for real dimensions and one\n // painted frame.\n for (let i = 0; i < 50 && (!video.videoWidth || !video.videoHeight); i++) {\n await new Promise((resolve) => setTimeout(resolve, 40))\n }\n await new Promise((resolve) => requestAnimationFrame(() => resolve(null)))\n const canvas = document.createElement('canvas')\n canvas.width = video.videoWidth\n canvas.height = video.videoHeight\n const ctx = canvas.getContext('2d')\n if (!ctx || !canvas.width) throw new ScreenCaptureError('failed', \"Couldn't read the screen.\")\n ctx.drawImage(video, 0, 0)\n video.srcObject = null\n return canvasToBlob(canvas)\n}\n\nexport function canvasToBlob(canvas: HTMLCanvasElement): Promise<Blob> {\n return new Promise((resolve, reject) =>\n canvas.toBlob(\n (blob) =>\n blob ? resolve(blob) : reject(new ScreenCaptureError('failed', \"Couldn't save the image.\")),\n 'image/png'\n )\n )\n}\n"],
|
|
5
|
+
"mappings": ";AAQO,MAAM,2BAA2B,MAAM;AAAA,EAC5C,YACW,MACT,SACA;AACA,UAAM,OAAO;AAHJ;AAIT,SAAK,OAAO;AAAA,EACd;AAAA,EALW;AAMb;AAEO,SAAS,mBAA4B;AAC1C,SAAO,OAAO,cAAc,eAAe,CAAC,CAAC,UAAU,cAAc;AACvE;AAGA,eAAsB,oBAAmC;AACvD,MAAI,CAAC,iBAAiB,GAAG;AACvB,UAAM,IAAI,mBAAmB,eAAe,wCAAwC;AAAA,EACtF;AACA,MAAI;AACJ,MAAI;AACF,aAAS,MAAM,UAAU,aAAa,gBAAgB;AAAA,MACpD,OAAO,EAAE,gBAAgB,UAAU;AAAA,MACnC,OAAO;AAAA;AAAA,MAEP,kBAAkB;AAAA,MAClB,oBAAoB;AAAA,MACpB,kBAAkB;AAAA,IACpB,CAA8B;AAAA,EAChC,SAAS,OAAO;AACd,UAAM,OAAO,iBAAiB,eAAe,MAAM,OAAO;AAC1D,QAAI,SAAS,qBAAqB,SAAS,cAAc;AACvD,YAAM,IAAI,mBAAmB,aAAa,+BAA+B;AAAA,IAC3E;AACA,UAAM,IAAI,mBAAmB,UAAU,gCAAgC;AAAA,EACzE;AACA,MAAI;AACF,WAAO,MAAM,UAAU,MAAM;AAAA,EAC/B,UAAE;AACA,WAAO,UAAU,EAAE,QAAQ,CAAC,UAAU,MAAM,KAAK,CAAC;AAAA,EACpD;AACF;AAEA,eAAsB,UAAU,QAAoC;AAClE,QAAM,QAAQ,SAAS,cAAc,OAAO;AAC5C,QAAM,QAAQ;AACd,QAAM,cAAc;AACpB,QAAM,YAAY;AAClB,QAAM,MAAM,KAAK;AAGjB,WAAS,IAAI,GAAG,IAAI,OAAO,CAAC,MAAM,cAAc,CAAC,MAAM,cAAc,KAAK;AACxE,UAAM,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,EAAE,CAAC;AAAA,EACxD;AACA,QAAM,IAAI,QAAQ,CAAC,YAAY,sBAAsB,MAAM,QAAQ,IAAI,CAAC,CAAC;AACzE,QAAM,SAAS,SAAS,cAAc,QAAQ;AAC9C,SAAO,QAAQ,MAAM;AACrB,SAAO,SAAS,MAAM;AACtB,QAAM,MAAM,OAAO,WAAW,IAAI;AAClC,MAAI,CAAC,OAAO,CAAC,OAAO,MAAO,OAAM,IAAI,mBAAmB,UAAU,2BAA2B;AAC7F,MAAI,UAAU,OAAO,GAAG,CAAC;AACzB,QAAM,YAAY;AAClB,SAAO,aAAa,MAAM;AAC5B;AAEO,SAAS,aAAa,QAA0C;AACrE,SAAO,IAAI;AAAA,IAAQ,CAAC,SAAS,WAC3B,OAAO;AAAA,MACL,CAAC,SACC,OAAO,QAAQ,IAAI,IAAI,OAAO,IAAI,mBAAmB,UAAU,0BAA0B,CAAC;AAAA,MAC5F;AAAA,IACF;AAAA,EACF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useVersionAutosave, useVersionSaveQueue } from "./useVersionAutosave.js";
|
|
3
4
|
import { normalizeDocxExport } from "./export.js";
|
|
4
5
|
import { WordDraftStream } from "./streamWordDocument.js";
|
|
5
6
|
import {
|
|
@@ -498,7 +499,8 @@ function NativeDocxEditor({
|
|
|
498
499
|
setEditRevision(0);
|
|
499
500
|
setCheckpoint(null);
|
|
500
501
|
}, [documentId]);
|
|
501
|
-
|
|
502
|
+
const saveVersion = useVersionSaveQueue(persistVersion);
|
|
503
|
+
async function persistVersion(autosave = false) {
|
|
502
504
|
if (!ready || savingRef.current || stream) return false;
|
|
503
505
|
if (snapshot || !ready.canEdit) return true;
|
|
504
506
|
if (offline) return false;
|
|
@@ -530,14 +532,16 @@ function NativeDocxEditor({
|
|
|
530
532
|
await adapter.onVersionSaved?.(result.document_ext_id, savedVersion);
|
|
531
533
|
onSaved?.(result.document_ext_id);
|
|
532
534
|
if (revision.current !== atRevision) {
|
|
533
|
-
|
|
535
|
+
if (!autosave)
|
|
536
|
+
adapter.notify?.("The document changed while saving. Please try again.", "error");
|
|
534
537
|
return false;
|
|
535
538
|
}
|
|
536
539
|
setCheckpoint({ version: savedVersion, autosave: autosave && !isDraft, revision: atRevision });
|
|
537
540
|
if (!autosave) adapter.notify?.(isDraft ? "Word note saved" : "DOCX version saved", "success");
|
|
538
541
|
return savedVersion ?? true;
|
|
539
542
|
} catch (e) {
|
|
540
|
-
|
|
543
|
+
if (!autosave)
|
|
544
|
+
adapter.notify?.(e instanceof Error ? e.message : "Could not save this document.", "error");
|
|
541
545
|
setStatus("Save failed \xB7 your draft is retained");
|
|
542
546
|
return false;
|
|
543
547
|
} finally {
|
|
@@ -552,15 +556,13 @@ function NativeDocxEditor({
|
|
|
552
556
|
saveRef.current = null;
|
|
553
557
|
};
|
|
554
558
|
});
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
return () => clearTimeout(timer);
|
|
563
|
-
}, [editRevision, documentId, compact, snapshot, offline, stream]);
|
|
559
|
+
useVersionAutosave({
|
|
560
|
+
documentId,
|
|
561
|
+
revision: editRevision,
|
|
562
|
+
savedRevision: checkpoint?.revision ?? 0,
|
|
563
|
+
enabled: !!ready?.canEdit && !compact && !snapshot && !offline && !stream && !saving,
|
|
564
|
+
save: () => saveVersion(true)
|
|
565
|
+
});
|
|
564
566
|
useEffect(() => {
|
|
565
567
|
if (compact || snapshot) return;
|
|
566
568
|
return adapter.registerTurnFlush?.(documentId, async () => {
|