@opengeni/react 1.0.1 → 1.0.2-canary.37191796234001
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 +651 -21
- package/dist/accounts.d.ts +64 -0
- package/dist/accounts.js +822 -0
- package/dist/accounts.js.map +1 -0
- package/dist/approvals.d.ts +2 -1
- package/dist/artifacts-document.js +3 -2
- package/dist/artifacts-presentation.js +3 -2
- package/dist/artifacts-spreadsheet.js +2 -2
- package/dist/artifacts.d.ts +4 -1
- package/dist/artifacts.js +518 -42
- package/dist/artifacts.js.map +1 -1
- package/dist/browser-viewer-GTR5XQ7X.js +16 -0
- package/dist/capability-catalog-row.d.ts +20 -0
- package/dist/capability-logo-fallback.d.ts +3 -0
- package/dist/chat-interactive-block-UGBTNB3L.js +11 -0
- package/dist/chat-interactive-block-UGBTNB3L.js.map +1 -0
- package/dist/{chunk-5UQPNLIO.js → chunk-23J2ZNXT.js} +4117 -3896
- package/dist/chunk-23J2ZNXT.js.map +1 -0
- package/dist/chunk-447JLCBU.js +209 -0
- package/dist/chunk-447JLCBU.js.map +1 -0
- package/dist/chunk-4RVYOY4O.js +379 -0
- package/dist/chunk-4RVYOY4O.js.map +1 -0
- package/dist/chunk-5CVXZA3Y.js +292 -0
- package/dist/chunk-5CVXZA3Y.js.map +1 -0
- package/dist/chunk-5J4FOW6Q.js +108 -0
- package/dist/chunk-5J4FOW6Q.js.map +1 -0
- package/dist/chunk-A2PI6UMY.js +41 -0
- package/dist/chunk-A2PI6UMY.js.map +1 -0
- package/dist/{chunk-7WCLAEFV.js → chunk-A4UJB2MM.js} +37 -27
- package/dist/chunk-A4UJB2MM.js.map +1 -0
- package/dist/chunk-B3V6TG77.js +41 -0
- package/dist/chunk-B3V6TG77.js.map +1 -0
- package/dist/chunk-BRM6FVZA.js +302 -0
- package/dist/chunk-BRM6FVZA.js.map +1 -0
- package/dist/chunk-C5X5AJXK.js +182 -0
- package/dist/chunk-C5X5AJXK.js.map +1 -0
- package/dist/chunk-CR7U7HAR.js +453 -0
- package/dist/chunk-CR7U7HAR.js.map +1 -0
- package/dist/{chunk-R3AZOAB5.js → chunk-DYWBUX5Q.js} +29 -123
- package/dist/chunk-DYWBUX5Q.js.map +1 -0
- package/dist/chunk-E3G7SAYZ.js +3124 -0
- package/dist/chunk-E3G7SAYZ.js.map +1 -0
- package/dist/chunk-ELCE5SXV.js +359 -0
- package/dist/chunk-ELCE5SXV.js.map +1 -0
- package/dist/chunk-ERGYJIBB.js +21 -0
- package/dist/chunk-ERGYJIBB.js.map +1 -0
- package/dist/chunk-FFA2OVOV.js +319 -0
- package/dist/chunk-FFA2OVOV.js.map +1 -0
- package/dist/chunk-FZCP527Y.js +116 -0
- package/dist/chunk-FZCP527Y.js.map +1 -0
- package/dist/chunk-GIMRELBW.js +14 -0
- package/dist/chunk-GIMRELBW.js.map +1 -0
- package/dist/{chunk-6KLX2RH5.js → chunk-H2NJQORO.js} +1069 -79
- package/dist/chunk-H2NJQORO.js.map +1 -0
- package/dist/chunk-I7IWRQLN.js +1608 -0
- package/dist/chunk-I7IWRQLN.js.map +1 -0
- package/dist/{chunk-HA46CYQK.js → chunk-IQUCRAT7.js} +792 -537
- package/dist/chunk-IQUCRAT7.js.map +1 -0
- package/dist/chunk-IWIDV7SB.js +1 -0
- package/dist/chunk-IWIDV7SB.js.map +1 -0
- package/dist/chunk-J2QWVSQY.js +312 -0
- package/dist/chunk-J2QWVSQY.js.map +1 -0
- package/dist/chunk-JFNEV4LU.js +2733 -0
- package/dist/chunk-JFNEV4LU.js.map +1 -0
- package/dist/chunk-JT4JW7BC.js +54 -0
- package/dist/chunk-JT4JW7BC.js.map +1 -0
- package/dist/chunk-K3HQAKQX.js +24 -0
- package/dist/chunk-K3HQAKQX.js.map +1 -0
- package/dist/{chunk-C5ZPSSS5.js → chunk-NEXLLTP4.js} +2 -1
- package/dist/{chunk-C5ZPSSS5.js.map → chunk-NEXLLTP4.js.map} +1 -1
- package/dist/chunk-NZUI76EM.js +371 -0
- package/dist/chunk-NZUI76EM.js.map +1 -0
- package/dist/{chunk-PEI6RANI.js → chunk-PNY3JKBJ.js} +309 -74
- package/dist/chunk-PNY3JKBJ.js.map +1 -0
- package/dist/{chunk-IGC6A2ZI.js → chunk-Q5QBEZIQ.js} +777 -246
- package/dist/chunk-Q5QBEZIQ.js.map +1 -0
- package/dist/chunk-RHREZ7BO.js +11 -0
- package/dist/chunk-RHREZ7BO.js.map +1 -0
- package/dist/chunk-SCOTPE2I.js +589 -0
- package/dist/chunk-SCOTPE2I.js.map +1 -0
- package/dist/{chunk-B6WPZSTT.js → chunk-SJZDKVW2.js} +1331 -1163
- package/dist/chunk-SJZDKVW2.js.map +1 -0
- package/dist/chunk-TMJRG4YC.js +12803 -0
- package/dist/chunk-TMJRG4YC.js.map +1 -0
- package/dist/chunk-TMYGL6X5.js +742 -0
- package/dist/chunk-TMYGL6X5.js.map +1 -0
- package/dist/chunk-UGLEYEMP.js +33 -0
- package/dist/chunk-UGLEYEMP.js.map +1 -0
- package/dist/chunk-UK4TINKT.js +27 -0
- package/dist/chunk-UK4TINKT.js.map +1 -0
- package/dist/chunk-UTRTG6LH.js +220 -0
- package/dist/chunk-UTRTG6LH.js.map +1 -0
- package/dist/chunk-WEHMPOXK.js +39 -0
- package/dist/chunk-WEHMPOXK.js.map +1 -0
- package/dist/chunk-WRHIZYZF.js +988 -0
- package/dist/chunk-WRHIZYZF.js.map +1 -0
- package/dist/{chunk-HUIKIDM5.js → chunk-WUISZ3WU.js} +223 -115
- package/dist/chunk-WUISZ3WU.js.map +1 -0
- package/dist/chunk-XCHWDJ22.js +107 -0
- package/dist/chunk-XCHWDJ22.js.map +1 -0
- package/dist/chunk-XOAMRDDK.js +481 -0
- package/dist/chunk-XOAMRDDK.js.map +1 -0
- package/dist/chunk-XUW2UWQM.js +299 -0
- package/dist/chunk-XUW2UWQM.js.map +1 -0
- package/dist/{chunk-KCGXPP5V.js → chunk-Y2PRMYLC.js} +11 -6
- package/dist/chunk-Y2PRMYLC.js.map +1 -0
- package/dist/chunk-YBXQDQFX.js +1772 -0
- package/dist/chunk-YBXQDQFX.js.map +1 -0
- package/dist/{chunk-KZ73RL76.js → chunk-YQEAA6LT.js} +7 -3
- package/dist/chunk-YQEAA6LT.js.map +1 -0
- package/dist/chunk-YV4L3ILF.js +9 -0
- package/dist/chunk-YV4L3ILF.js.map +1 -0
- package/dist/{chunk-LLOLUHVG.js → chunk-YWFG7ZX7.js} +1358 -2746
- package/dist/chunk-YWFG7ZX7.js.map +1 -0
- package/dist/chunk-ZYRGEKWS.js +216 -0
- package/dist/chunk-ZYRGEKWS.js.map +1 -0
- package/dist/client.d.ts +7 -4
- package/dist/clipboard.js +1 -0
- package/dist/commands/types.d.ts +2 -0
- package/dist/components/anthropic-mark.d.ts +3 -0
- package/dist/components/approval-surface.d.ts +15 -0
- package/dist/components/artifacts/artifact-chrome.d.ts +120 -0
- package/dist/components/artifacts/artifact-sandbox.d.ts +27 -0
- package/dist/components/artifacts/artifact-surface.d.ts +3 -1
- package/dist/components/artifacts/chat-interactive-block.d.ts +29 -0
- package/dist/components/artifacts/deferred-chat-media.d.ts +15 -0
- package/dist/components/artifacts/document-editor.d.ts +4 -2
- package/dist/components/artifacts/editable-artifact-ui.d.ts +2 -1
- package/dist/components/artifacts/editable-artifact-view.d.ts +46 -0
- package/dist/components/artifacts/editable-document.d.ts +1 -1
- package/dist/components/artifacts/editable-presentation.d.ts +2 -2
- package/dist/components/artifacts/editable-spreadsheet.d.ts +2 -1
- package/dist/components/artifacts/index.d.ts +8 -1
- package/dist/components/artifacts/inline-html-document.d.ts +2 -0
- package/dist/components/artifacts/latest-task-runner.d.ts +15 -0
- package/dist/components/artifacts/presentation-editor.d.ts +6 -2
- package/dist/components/artifacts/published-html-artifact-frame.d.ts +48 -3
- package/dist/components/artifacts/session-artifact-viewer.d.ts +39 -0
- package/dist/components/artifacts/site-view.d.ts +23 -0
- package/dist/components/artifacts/spreadsheet-general-display.d.ts +24 -0
- package/dist/components/artifacts/visualization-assets.gen.d.ts +2 -0
- package/dist/components/browser-select-control.d.ts +16 -0
- package/dist/components/browser-viewer.d.ts +10 -1
- package/dist/components/chat-composer.d.ts +21 -3
- package/dist/components/child-session-link.d.ts +5 -0
- package/dist/components/claude-mark.d.ts +3 -0
- package/dist/components/code-editor.d.ts +1 -4
- package/dist/components/composer.d.ts +45 -4
- package/dist/components/computer-viewer.d.ts +4 -1
- package/dist/components/copy-button.d.ts +3 -1
- package/dist/components/desktop-viewer.d.ts +1 -1
- package/dist/components/enrollment-consent.d.ts +6 -3
- package/dist/components/file-browser.d.ts +10 -1
- package/dist/components/fleet-tile.d.ts +2 -2
- package/dist/components/grok-mark.d.ts +26 -0
- package/dist/components/human-input-form.d.ts +21 -2
- package/dist/components/human-input-surface.d.ts +6 -3
- package/dist/components/machine-card.d.ts +4 -1
- package/dist/components/machines/machine-detail.d.ts +4 -1
- package/dist/components/machines/machine-operation-policy.d.ts +11 -0
- package/dist/components/machines-dashboard.d.ts +3 -1
- package/dist/components/markdown-table-layout.d.ts +12 -0
- package/dist/components/markdown.d.ts +47 -3
- package/dist/components/message-timeline.d.ts +72 -9
- package/dist/components/model-mark.d.ts +29 -0
- package/dist/components/model-policy-picker-menu.d.ts +9 -0
- package/dist/components/model-policy-picker.d.ts +62 -13
- package/dist/components/open-geni-chat.d.ts +55 -0
- package/dist/components/open-geni-links.d.ts +68 -0
- package/dist/components/openrouter-mark.d.ts +6 -0
- package/dist/components/pierre-file.d.ts +1 -1
- package/dist/components/preview-loading.d.ts +2 -0
- package/dist/components/queue-item-content.d.ts +9 -0
- package/dist/components/sandbox-files.d.ts +13 -2
- package/dist/components/sandbox-workspace.d.ts +43 -3
- package/dist/components/session-capability-frame.d.ts +35 -0
- package/dist/components/session-chrome.d.ts +66 -8
- package/dist/components/session-commands-panel.d.ts +6 -0
- package/dist/components/session-commands.d.ts +6 -0
- package/dist/components/session-connection-request.d.ts +8 -0
- package/dist/components/session-conversation.d.ts +86 -0
- package/dist/components/session-list.d.ts +36 -0
- package/dist/components/session-mcp-capability-card.d.ts +41 -0
- package/dist/components/timeline-anchor.d.ts +24 -0
- package/dist/components/timeline-annotation-chrome.d.ts +25 -0
- package/dist/components/timeline-annotation-layout.d.ts +42 -0
- package/dist/components/timeline-annotation-markers.d.ts +8 -0
- package/dist/components/timeline-annotation-reveal-context.d.ts +4 -0
- package/dist/components/timeline-annotation-shared.d.ts +31 -0
- package/dist/components/timeline-annotations-dialog.d.ts +3 -2
- package/dist/components/timeline-annotations.d.ts +13 -3
- package/dist/components/timeline-reading-progress.d.ts +11 -0
- package/dist/components/timeline-search.d.ts +34 -0
- package/dist/components/timeline-settlement.d.ts +16 -0
- package/dist/components/tip-follow.d.ts +16 -0
- package/dist/components/usage-limit-notice.d.ts +49 -0
- package/dist/components/usage-member-list.d.ts +91 -0
- package/dist/components/usage-meter.d.ts +73 -0
- package/dist/components/use-viewer-menu-dismiss.d.ts +2 -0
- package/dist/components/user-message-body.d.ts +10 -1
- package/dist/components/workspace-dock.d.ts +6 -2
- package/dist/composer.d.ts +5 -4
- package/dist/composer.js +15 -9
- package/dist/computer-viewer-ENHVAZ2Q.js +19 -0
- package/dist/computer-viewer-ENHVAZ2Q.js.map +1 -0
- package/dist/connect-account-identity.d.ts +8 -0
- package/dist/connect-accounts.d.ts +15 -0
- package/dist/connect-chooser.d.ts +18 -0
- package/dist/connect-panel.d.ts +18 -0
- package/dist/connect-setup.d.ts +16 -0
- package/dist/connect.d.ts +21 -0
- package/dist/connect.js +2129 -0
- package/dist/connect.js.map +1 -0
- package/dist/connection-catalog.d.ts +38 -0
- package/dist/connection-discovery.d.ts +12 -0
- package/dist/connection-installed.d.ts +17 -0
- package/dist/connection-logo.d.ts +9 -0
- package/dist/connection-service-logo.d.ts +8 -0
- package/dist/connection-service-presentation.d.ts +13 -0
- package/dist/connection-skeleton.d.ts +5 -0
- package/dist/connection-type-picker.d.ts +6 -0
- package/dist/conversation-timeline.d.ts +5 -0
- package/dist/desktop.d.ts +7 -0
- package/dist/desktop.js +26 -0
- package/dist/desktop.js.map +1 -0
- package/dist/device-authorization.d.ts +23 -0
- package/dist/diffs.d.ts +8 -0
- package/dist/diffs.js +14 -0
- package/dist/diffs.js.map +1 -0
- package/dist/discovery-cache.d.ts +5 -0
- package/dist/editor.d.ts +7 -0
- package/dist/editor.js +21 -0
- package/dist/editor.js.map +1 -0
- package/dist/embedded-session-client.d.ts +23 -0
- package/dist/file-node-visibility.d.ts +15 -0
- package/dist/{fleet-decision-row-GGCAT4E4.js → fleet-decision-row-YZ3ZKWMM.js} +2 -1
- package/dist/fleet-decision-row-YZ3ZKWMM.js.map +1 -0
- package/dist/hooks/channel-cache.d.ts +23 -0
- package/dist/hooks/latest-question.d.ts +25 -0
- package/dist/hooks/shared-machines-list.d.ts +14 -0
- package/dist/hooks/use-attached-browsers.d.ts +2 -1
- package/dist/hooks/use-browser-frame-stream.d.ts +1 -1
- package/dist/hooks/use-browser-session.d.ts +12 -1
- package/dist/hooks/use-channels.d.ts +1 -0
- package/dist/hooks/use-codex-accounts.d.ts +25 -7
- package/dist/hooks/use-composer.d.ts +45 -9
- package/dist/hooks/use-computer-frame-stream.d.ts +1 -1
- package/dist/hooks/use-computer-input-posture.d.ts +13 -0
- package/dist/hooks/use-computer-session.d.ts +2 -0
- package/dist/hooks/use-connect.d.ts +22 -0
- package/dist/hooks/use-desktop-stream.d.ts +1 -1
- package/dist/hooks/use-file-attachments.d.ts +21 -2
- package/dist/hooks/use-machine-chip.d.ts +3 -3
- package/dist/hooks/use-machines.d.ts +19 -1
- package/dist/hooks/use-sandbox-files.d.ts +5 -1
- package/dist/hooks/use-session-background-commands.d.ts +14 -0
- package/dist/hooks/use-session-events.d.ts +30 -8
- package/dist/hooks/use-session-lineage.d.ts +4 -0
- package/dist/hooks/use-session-mcp-approval-policy.d.ts +2 -2
- package/dist/hooks/use-session.d.ts +6 -0
- package/dist/hooks/use-turn-queue.d.ts +12 -0
- package/dist/hooks/use-usage.d.ts +40 -0
- package/dist/hooks/use-workspace-edit.d.ts +4 -0
- package/dist/hooks/use-workspace-sessions.d.ts +27 -5
- package/dist/human-input.d.ts +3 -1
- package/dist/identity-link-accounts.d.ts +11 -0
- package/dist/identity-link-consent.d.ts +18 -0
- package/dist/index.d.ts +60 -17
- package/dist/index.js +2237 -6200
- package/dist/index.js.map +1 -1
- package/dist/interaction.d.ts +1 -1
- package/dist/interaction.js +35 -184
- package/dist/interaction.js.map +1 -1
- package/dist/latest-question-755OW5DS.js +117 -0
- package/dist/latest-question-755OW5DS.js.map +1 -0
- package/dist/lib/error-message.d.ts +7 -0
- package/dist/lib/format.d.ts +36 -2
- package/dist/lib/host-theme.d.ts +70 -0
- package/dist/lib/interaction-errors.d.ts +6 -0
- package/dist/lib/menu-styles.d.ts +22 -0
- package/dist/lib/pierre-diffs-loader.d.ts +20 -0
- package/dist/lib/workbench-peers.d.ts +36 -0
- package/dist/lib/xterm-theme.d.ts +7 -5
- package/dist/machines.d.ts +4 -2
- package/dist/machines.js +14 -4
- package/dist/markdown-table-layout-QXSLK4BY.js +106 -0
- package/dist/markdown-table-layout-QXSLK4BY.js.map +1 -0
- package/dist/mcp-connection-status.d.ts +14 -0
- package/dist/model-policy-picker-menu-PWRI7R2X.js +14 -0
- package/dist/model-policy-picker-menu-PWRI7R2X.js.map +1 -0
- package/dist/model-policy.d.ts +19 -3
- package/dist/model-policy.js +9 -1
- package/dist/older-history.d.ts +30 -0
- package/dist/platform-activity-row-627BFZ46.js +182 -0
- package/dist/platform-activity-row-627BFZ46.js.map +1 -0
- package/dist/plugin-details.d.ts +14 -0
- package/dist/plugin-discovery.d.ts +25 -0
- package/dist/provider.d.ts +13 -1
- package/dist/{queue-surface-implementation-2KVJTYXH.js → queue-surface-implementation-ZHFI5JBP.js} +131 -44
- package/dist/queue-surface-implementation-ZHFI5JBP.js.map +1 -0
- package/dist/realtime/realtime-control.d.ts +6 -3
- package/dist/realtime.js +133 -74
- package/dist/realtime.js.map +1 -1
- package/dist/sandbox-files-BBC47SLD.js +20 -0
- package/dist/sandbox-files-BBC47SLD.js.map +1 -0
- package/dist/session-auth-recommendation.d.ts +5 -0
- package/dist/session-capability-policy.d.ts +12 -0
- package/dist/session-ui.d.ts +30 -2
- package/dist/session-ui.js +85 -7
- package/dist/session-ui.js.map +1 -1
- package/dist/session.d.ts +8 -2
- package/dist/session.js +30 -19
- package/dist/sites-ui.d.ts +34 -0
- package/dist/sites.d.ts +3 -0
- package/dist/sites.js +11 -0
- package/dist/sites.js.map +1 -0
- package/dist/skill-discovery.d.ts +47 -0
- package/dist/terminal.d.ts +8 -0
- package/dist/terminal.js +31 -0
- package/dist/terminal.js.map +1 -0
- package/dist/timeline/activity-rail.d.ts +11 -2
- package/dist/timeline/allowance-exhausted-row.d.ts +24 -0
- package/dist/timeline/compaction-copy.d.ts +10 -0
- package/dist/timeline/entrance.d.ts +13 -1
- package/dist/timeline/genie-loading.d.ts +34 -0
- package/dist/timeline/index.d.ts +6 -2
- package/dist/timeline/knowledge-receipt.d.ts +17 -0
- package/dist/timeline/platform-activity-row.d.ts +14 -0
- package/dist/timeline/presented-image.d.ts +6 -0
- package/dist/timeline/projection.d.ts +31 -2
- package/dist/timeline/registry.d.ts +2 -0
- package/dist/timeline/retained-image.d.ts +18 -0
- package/dist/timeline/rolling-activity.d.ts +11 -0
- package/dist/timeline/screenshot-lightbox.d.ts +16 -1
- package/dist/timeline/shared.d.ts +8 -3
- package/dist/timeline/startup-preference.d.ts +3 -0
- package/dist/timeline/startup-timings.d.ts +4 -0
- package/dist/timeline/tool-display-name.d.ts +2 -1
- package/dist/timeline/turn-summary.d.ts +32 -3
- package/dist/timeline/types.d.ts +141 -7
- package/dist/timeline-annotation-markers-GNDHTGBD.js +165 -0
- package/dist/timeline-annotation-markers-GNDHTGBD.js.map +1 -0
- package/dist/timeline-annotation-selection-DMLMM2TX.js +241 -0
- package/dist/timeline-annotation-selection-DMLMM2TX.js.map +1 -0
- package/dist/timeline-annotations-dialog-ELRDBHTS.js +273 -0
- package/dist/timeline-annotations-dialog-ELRDBHTS.js.map +1 -0
- package/dist/types/machines.d.ts +2 -2
- package/dist/usage/allowance-copy.d.ts +25 -0
- package/dist/usage/summary.d.ts +35 -0
- package/dist/usage.d.ts +13 -0
- package/dist/usage.js +1074 -0
- package/dist/usage.js.map +1 -0
- package/package.json +44 -4
- package/src/accounts.tsx +1072 -0
- package/src/approvals.ts +8 -1
- package/src/artifacts.ts +42 -0
- package/src/assets/README.md +8 -0
- package/src/assets/gmail.ico +0 -0
- package/src/assets/outlook.ico +0 -0
- package/src/capability-catalog-row.tsx +94 -0
- package/src/capability-logo-fallback.ts +22 -0
- package/src/client.ts +22 -13
- package/src/commands/registry.ts +18 -15
- package/src/commands/types.ts +2 -0
- package/src/components/anthropic-mark.tsx +10 -0
- package/src/components/approval-surface.tsx +171 -37
- package/src/components/artifacts/artifact-chrome.tsx +413 -0
- package/src/components/artifacts/artifact-sandbox.tsx +185 -0
- package/src/components/artifacts/artifact-surface.tsx +39 -19
- package/src/components/artifacts/chat-interactive-block.tsx +260 -0
- package/src/components/artifacts/deferred-chat-media.tsx +67 -0
- package/src/components/artifacts/document-editor.tsx +141 -16
- package/src/components/artifacts/editable-artifact-ui.tsx +4 -2
- package/src/components/artifacts/editable-artifact-view.tsx +187 -0
- package/src/components/artifacts/editable-artifact-workbench.tsx +5 -2
- package/src/components/artifacts/editable-document.tsx +37 -36
- package/src/components/artifacts/editable-presentation.tsx +36 -18
- package/src/components/artifacts/editable-spreadsheet.tsx +5 -2
- package/src/components/artifacts/index.ts +44 -0
- package/src/components/artifacts/inline-html-document.ts +24 -0
- package/src/components/artifacts/latest-task-runner.ts +49 -0
- package/src/components/artifacts/presentation-editor.tsx +323 -67
- package/src/components/artifacts/published-html-artifact-frame.tsx +338 -5
- package/src/components/artifacts/session-artifact-viewer.tsx +376 -0
- package/src/components/artifacts/site-view.tsx +116 -0
- package/src/components/artifacts/spreadsheet-canvas.ts +5 -10
- package/src/components/artifacts/spreadsheet-general-display.ts +43 -0
- package/src/components/artifacts/spreadsheet-grid.tsx +12 -13
- package/src/components/artifacts/visualization-assets/base.css +1015 -0
- package/src/components/artifacts/visualization-assets/helpers.html +349 -0
- package/src/components/artifacts/visualization-assets.gen.ts +3 -0
- package/src/components/browser-select-control.tsx +312 -0
- package/src/components/browser-viewer.tsx +1054 -314
- package/src/components/chat-composer.tsx +131 -85
- package/src/components/child-session-link.tsx +27 -0
- package/src/components/claude-mark.tsx +10 -0
- package/src/components/code-editor.tsx +13 -16
- package/src/components/composer-transcription-control.tsx +7 -5
- package/src/components/composer.tsx +376 -49
- package/src/components/computer-viewer.tsx +1041 -329
- package/src/components/copy-button.tsx +13 -5
- package/src/components/desktop-viewer.tsx +1 -1
- package/src/components/enrollment-consent.tsx +65 -7
- package/src/components/enrollment-device-flow.tsx +1 -1
- package/src/components/file-browser.tsx +158 -34
- package/src/components/fleet-tile.tsx +8 -16
- package/src/components/generated-video-player.tsx +4 -2
- package/src/components/grok-mark.tsx +33 -0
- package/src/components/human-input-form.tsx +455 -114
- package/src/components/human-input-surface.tsx +18 -7
- package/src/components/interaction-intervention-banner.tsx +2 -2
- package/src/components/machine-card.tsx +119 -4
- package/src/components/machine-dock-bar.tsx +1 -1
- package/src/components/machine-input-display.ts +23 -2
- package/src/components/machines/machine-detail.tsx +52 -2
- package/src/components/machines/machine-operation-policy.tsx +179 -0
- package/src/components/machines-dashboard.tsx +8 -17
- package/src/components/markdown-table-layout.ts +132 -0
- package/src/components/markdown.tsx +473 -44
- package/src/components/message-timeline.tsx +2498 -663
- package/src/components/model-mark.tsx +90 -0
- package/src/components/model-picker.tsx +15 -4
- package/src/components/model-policy-picker-menu.tsx +424 -0
- package/src/components/model-policy-picker.tsx +337 -381
- package/src/components/open-geni-chat.tsx +319 -0
- package/src/components/open-geni-links.tsx +179 -0
- package/src/components/openrouter-mark.tsx +13 -0
- package/src/components/pierre-diff.tsx +41 -16
- package/src/components/pierre-file.tsx +36 -11
- package/src/components/preview-loading.tsx +154 -0
- package/src/components/queue-item-content.ts +14 -0
- package/src/components/queue-surface-implementation.tsx +132 -42
- package/src/components/queue-surface-state.tsx +3 -1
- package/src/components/sandbox-files.tsx +267 -35
- package/src/components/sandbox-terminal.tsx +48 -11
- package/src/components/sandbox-workspace.tsx +337 -135
- package/src/components/session-capability-frame.tsx +192 -0
- package/src/components/session-chrome.tsx +1027 -328
- package/src/components/session-commands-panel.tsx +126 -0
- package/src/components/session-commands.tsx +18 -0
- package/src/components/session-connection-request.tsx +97 -0
- package/src/components/session-conversation.tsx +512 -0
- package/src/components/session-list.tsx +250 -0
- package/src/components/session-mcp-capability-card.tsx +680 -0
- package/src/components/session-status.tsx +1 -1
- package/src/components/timeline-anchor.tsx +119 -0
- package/src/components/timeline-annotation-chrome.tsx +196 -0
- package/src/components/timeline-annotation-layout.ts +268 -0
- package/src/components/timeline-annotation-markers.tsx +185 -0
- package/src/components/timeline-annotation-reveal-context.ts +9 -0
- package/src/components/timeline-annotation-selection.tsx +106 -54
- package/src/components/timeline-annotation-shared.ts +245 -0
- package/src/components/timeline-annotations-dialog.tsx +185 -128
- package/src/components/timeline-annotations.tsx +189 -45
- package/src/components/timeline-reading-progress.ts +80 -0
- package/src/components/timeline-search.tsx +168 -0
- package/src/components/timeline-settlement.ts +60 -0
- package/src/components/tip-follow.ts +70 -7
- package/src/components/tooltip.tsx +38 -5
- package/src/components/usage-limit-notice.tsx +179 -0
- package/src/components/usage-member-list.tsx +792 -0
- package/src/components/usage-meter.tsx +382 -0
- package/src/components/use-viewer-menu-dismiss.ts +33 -0
- package/src/components/user-message-body.tsx +194 -49
- package/src/components/workspace-dock.tsx +141 -175
- package/src/composer.ts +6 -1
- package/src/connect-account-identity.tsx +34 -0
- package/src/connect-accounts.tsx +233 -0
- package/src/connect-chooser.tsx +336 -0
- package/src/connect-panel.tsx +99 -0
- package/src/connect-setup.tsx +307 -0
- package/src/connect.ts +60 -0
- package/src/connection-assets.d.ts +4 -0
- package/src/connection-catalog.tsx +162 -0
- package/src/connection-discovery.tsx +235 -0
- package/src/connection-installed.tsx +51 -0
- package/src/connection-logo.tsx +37 -0
- package/src/connection-service-logo.tsx +55 -0
- package/src/connection-service-presentation.ts +46 -0
- package/src/connection-skeleton.tsx +16 -0
- package/src/connection-type-picker.tsx +36 -0
- package/src/conversation-timeline.ts +92 -0
- package/src/desktop.ts +24 -0
- package/src/device-authorization.tsx +98 -0
- package/src/diffs.ts +16 -0
- package/src/discovery-cache.ts +42 -0
- package/src/editor.ts +13 -0
- package/src/embedded-session-client.ts +104 -0
- package/src/file-node-visibility.ts +66 -0
- package/src/hooks/channel-cache.ts +104 -0
- package/src/hooks/internal.ts +3 -2
- package/src/hooks/latest-question.ts +164 -0
- package/src/hooks/shared-machines-list.ts +239 -0
- package/src/hooks/use-attached-browsers.ts +5 -1
- package/src/hooks/use-available-models.ts +20 -14
- package/src/hooks/use-browser-frame-stream.ts +25 -3
- package/src/hooks/use-browser-session.ts +271 -71
- package/src/hooks/use-browser-sessions.ts +7 -1
- package/src/hooks/use-channels.ts +32 -10
- package/src/hooks/use-codex-accounts.ts +78 -21
- package/src/hooks/use-composer.ts +1034 -320
- package/src/hooks/use-computer-frame-stream.ts +39 -16
- package/src/hooks/use-computer-input-posture.ts +94 -0
- package/src/hooks/use-computer-session.ts +110 -32
- package/src/hooks/use-computer-sessions.ts +8 -1
- package/src/hooks/use-connect.ts +23 -0
- package/src/hooks/use-desktop-stream.ts +19 -28
- package/src/hooks/use-file-attachments.ts +118 -10
- package/src/hooks/use-goal.ts +36 -9
- package/src/hooks/use-human-input.ts +31 -4
- package/src/hooks/use-last-started-turn-policy.ts +15 -13
- package/src/hooks/use-machine-chip.ts +23 -6
- package/src/hooks/use-machines.ts +104 -6
- package/src/hooks/use-sandbox-files.ts +253 -150
- package/src/hooks/use-sandbox-git.ts +58 -29
- package/src/hooks/use-session-background-commands.ts +67 -0
- package/src/hooks/use-session-capabilities.ts +21 -2
- package/src/hooks/use-session-events.ts +759 -433
- package/src/hooks/use-session-lineage.ts +28 -6
- package/src/hooks/use-session-mcp-approval-policy.ts +13 -5
- package/src/hooks/use-session.ts +75 -16
- package/src/hooks/use-slash-commands.ts +6 -11
- package/src/hooks/use-turn-queue.ts +113 -13
- package/src/hooks/use-usage.ts +100 -0
- package/src/hooks/use-voice-input.ts +12 -1
- package/src/hooks/use-workspace-capture.ts +3 -2
- package/src/hooks/use-workspace-edit.ts +23 -5
- package/src/hooks/use-workspace-sessions.ts +99 -11
- package/src/human-input.ts +12 -1
- package/src/identity-link-accounts.tsx +148 -0
- package/src/identity-link-consent.tsx +192 -0
- package/src/index.ts +157 -12
- package/src/interaction.ts +1 -0
- package/src/lib/error-message.ts +37 -0
- package/src/lib/format.ts +150 -14
- package/src/lib/host-theme.ts +511 -0
- package/src/lib/interaction-errors.ts +33 -0
- package/src/lib/menu-styles.ts +44 -0
- package/src/lib/pierre-diffs-loader.ts +53 -0
- package/src/lib/workbench-peers.ts +62 -0
- package/src/lib/xterm-theme.ts +23 -21
- package/src/machines.ts +9 -1
- package/src/mcp-connection-status.ts +35 -0
- package/src/model-policy.ts +223 -69
- package/src/older-history.ts +87 -0
- package/src/plugin-details.tsx +185 -0
- package/src/plugin-discovery.tsx +196 -0
- package/src/provider.tsx +55 -14
- package/src/realtime/dropdown-menu.tsx +19 -12
- package/src/realtime/realtime-control.tsx +133 -46
- package/src/session-auth-recommendation.ts +48 -0
- package/src/session-capability-policy.ts +83 -0
- package/src/session-context.ts +1 -0
- package/src/session-ui.ts +66 -2
- package/src/session.ts +18 -1
- package/src/sites-ui.tsx +402 -0
- package/src/sites.ts +3 -0
- package/src/skill-discovery.tsx +208 -0
- package/src/terminal.ts +26 -0
- package/src/timeline/activity-rail.tsx +171 -80
- package/src/timeline/allowance-exhausted-row.tsx +86 -0
- package/src/timeline/compaction-copy.ts +42 -0
- package/src/timeline/entrance.tsx +28 -3
- package/src/timeline/fleet-decision-projection.ts +2 -1
- package/src/timeline/fleet-decision-row.tsx +1 -0
- package/src/timeline/genie-loading.tsx +136 -0
- package/src/timeline/index.ts +11 -0
- package/src/timeline/knowledge-receipt.tsx +100 -0
- package/src/timeline/parsers.ts +2 -2
- package/src/timeline/platform-activity-row.tsx +216 -0
- package/src/timeline/presented-image.ts +49 -0
- package/src/timeline/projection.ts +1600 -84
- package/src/timeline/registry.ts +15 -3
- package/src/timeline/retained-image.ts +97 -0
- package/src/timeline/rolling-activity.tsx +92 -0
- package/src/timeline/screenshot-lightbox.tsx +115 -17
- package/src/timeline/shared.tsx +47 -15
- package/src/timeline/startup-preference.ts +33 -0
- package/src/timeline/startup-timings.tsx +145 -0
- package/src/timeline/tool-display-name.ts +8 -6
- package/src/timeline/tool-renderers.tsx +497 -128
- package/src/timeline/turn-summary.tsx +208 -44
- package/src/timeline/types.ts +186 -7
- package/src/types/machines.ts +10 -0
- package/src/usage/allowance-copy.ts +64 -0
- package/src/usage/summary.ts +93 -0
- package/src/usage.ts +50 -0
- package/styles/compiled.css +2098 -757
- package/styles/connect.css +284 -0
- package/styles/connect.d.ts +1 -0
- package/styles/effective-tokens.css +1 -0
- package/styles/index.css +201 -9
- package/styles/responsive.css +27 -1
- package/styles/session-capability.css +44 -0
- package/styles/tokens.css +135 -65
- package/dist/chunk-5UQPNLIO.js.map +0 -1
- package/dist/chunk-6KLX2RH5.js.map +0 -1
- package/dist/chunk-7WCLAEFV.js.map +0 -1
- package/dist/chunk-B6WPZSTT.js.map +0 -1
- package/dist/chunk-FK6VG4LL.js +0 -90
- package/dist/chunk-FK6VG4LL.js.map +0 -1
- package/dist/chunk-HA46CYQK.js.map +0 -1
- package/dist/chunk-HUIKIDM5.js.map +0 -1
- package/dist/chunk-IGC6A2ZI.js.map +0 -1
- package/dist/chunk-KCGXPP5V.js.map +0 -1
- package/dist/chunk-KZ73RL76.js.map +0 -1
- package/dist/chunk-LAKLF4PA.js +0 -141
- package/dist/chunk-LAKLF4PA.js.map +0 -1
- package/dist/chunk-LLOLUHVG.js.map +0 -1
- package/dist/chunk-MKPIAGVL.js +0 -465
- package/dist/chunk-MKPIAGVL.js.map +0 -1
- package/dist/chunk-PEI6RANI.js.map +0 -1
- package/dist/chunk-R3AZOAB5.js.map +0 -1
- package/dist/chunk-WSK7G5LE.js +0 -170
- package/dist/chunk-WSK7G5LE.js.map +0 -1
- package/dist/chunk-XBZWOM2K.js +0 -218
- package/dist/chunk-XBZWOM2K.js.map +0 -1
- package/dist/chunk-Y7OEDUJ5.js +0 -8810
- package/dist/chunk-Y7OEDUJ5.js.map +0 -1
- package/dist/fleet-decision-row-GGCAT4E4.js.map +0 -1
- package/dist/hooks/use-packs.d.ts +0 -32
- package/dist/interactive-workbench-viewers-TOHBXJEE.js +0 -15
- package/dist/queue-surface-implementation-2KVJTYXH.js.map +0 -1
- package/dist/timeline-annotation-selection-3L7LHGG2.js +0 -187
- package/dist/timeline-annotation-selection-3L7LHGG2.js.map +0 -1
- package/dist/timeline-annotations-dialog-U7EVLR75.js +0 -196
- package/dist/timeline-annotations-dialog-U7EVLR75.js.map +0 -1
- package/src/hooks/use-packs.ts +0 -157
- /package/dist/{interactive-workbench-viewers-TOHBXJEE.js.map → browser-viewer-GTR5XQ7X.js.map} +0 -0
|
@@ -0,0 +1,2733 @@
|
|
|
1
|
+
import {
|
|
2
|
+
InteractionInterventionBanner,
|
|
3
|
+
isInteractionControlUnavailable,
|
|
4
|
+
isNonRetryableInteractionError,
|
|
5
|
+
isSourcePlacementChangedError,
|
|
6
|
+
useInteractionInterventions,
|
|
7
|
+
useInteractionInvalidation,
|
|
8
|
+
useViewerMenuDismiss
|
|
9
|
+
} from "./chunk-NZUI76EM.js";
|
|
10
|
+
import {
|
|
11
|
+
copyTextToClipboard
|
|
12
|
+
} from "./chunk-JVYQRRC4.js";
|
|
13
|
+
import {
|
|
14
|
+
DesktopViewer
|
|
15
|
+
} from "./chunk-DYWBUX5Q.js";
|
|
16
|
+
import {
|
|
17
|
+
STREAM_CLOSE_REASON_NORMAL,
|
|
18
|
+
STREAM_KIND_COMPUTER,
|
|
19
|
+
STREAM_ROLE_CLIENT,
|
|
20
|
+
decodeStreamFrame,
|
|
21
|
+
decodeStreamOpenAck,
|
|
22
|
+
encodeStreamClose,
|
|
23
|
+
encodeStreamOpen
|
|
24
|
+
} from "./chunk-5J4FOW6Q.js";
|
|
25
|
+
import {
|
|
26
|
+
usePageLiveActivity
|
|
27
|
+
} from "./chunk-YQEAA6LT.js";
|
|
28
|
+
import {
|
|
29
|
+
useEmbeddedComputerInteraction
|
|
30
|
+
} from "./chunk-NEXLLTP4.js";
|
|
31
|
+
import {
|
|
32
|
+
cn
|
|
33
|
+
} from "./chunk-22KP6LR5.js";
|
|
34
|
+
|
|
35
|
+
// src/components/computer-viewer.tsx
|
|
36
|
+
import { interactionControlFailureFromError } from "@opengeni/sdk/interaction";
|
|
37
|
+
import {
|
|
38
|
+
ChevronDownIcon,
|
|
39
|
+
CircleAlertIcon,
|
|
40
|
+
KeyboardIcon,
|
|
41
|
+
LoaderCircleIcon,
|
|
42
|
+
LockKeyholeIcon,
|
|
43
|
+
MonitorIcon,
|
|
44
|
+
MousePointer2Icon,
|
|
45
|
+
PanelsTopLeftIcon,
|
|
46
|
+
PlusIcon,
|
|
47
|
+
RefreshCwIcon,
|
|
48
|
+
RotateCcwIcon
|
|
49
|
+
} from "lucide-react";
|
|
50
|
+
import {
|
|
51
|
+
useCallback as useCallback5,
|
|
52
|
+
useEffect as useEffect5,
|
|
53
|
+
useLayoutEffect,
|
|
54
|
+
useMemo as useMemo2,
|
|
55
|
+
useRef as useRef5,
|
|
56
|
+
useState as useState5
|
|
57
|
+
} from "react";
|
|
58
|
+
|
|
59
|
+
// src/hooks/use-computer-frame-stream.ts
|
|
60
|
+
import {
|
|
61
|
+
computerFrameSocketUrl,
|
|
62
|
+
decodeComputerFrameMessage
|
|
63
|
+
} from "@opengeni/sdk/interaction";
|
|
64
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
65
|
+
var RELAY_TAG_OPEN = 1;
|
|
66
|
+
var RELAY_TAG_OPEN_ACK = 2;
|
|
67
|
+
var RELAY_TAG_FRAME = 3;
|
|
68
|
+
var RELAY_TAG_CLOSE = 4;
|
|
69
|
+
var MAX_AUTOMATIC_RECONNECTS_WITHOUT_A_FRAME = 2;
|
|
70
|
+
function useComputerFrameStream(options) {
|
|
71
|
+
const { client, workspaceId } = useEmbeddedComputerInteraction(options);
|
|
72
|
+
const enabled = options.enabled ?? true;
|
|
73
|
+
const [nonce, setNonce] = useState(0);
|
|
74
|
+
const [result, setResult] = useState({
|
|
75
|
+
state: "idle",
|
|
76
|
+
frame: null,
|
|
77
|
+
attachment: null,
|
|
78
|
+
error: null
|
|
79
|
+
});
|
|
80
|
+
const latestRef = useRef({
|
|
81
|
+
key: "",
|
|
82
|
+
sequence: -1
|
|
83
|
+
});
|
|
84
|
+
const factoryRef = useRef(options.webSocketFactory);
|
|
85
|
+
const streamRef = useRef(options.stream);
|
|
86
|
+
factoryRef.current = options.webSocketFactory;
|
|
87
|
+
streamRef.current = options.stream;
|
|
88
|
+
const reconnect = useCallback(() => setNonce((value) => value + 1), []);
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
const computerSessionId = options.computerSessionId;
|
|
91
|
+
const targetId = options.targetId;
|
|
92
|
+
if (!enabled || !computerSessionId || !targetId || typeof window === "undefined") {
|
|
93
|
+
setResult({ state: "idle", frame: null, attachment: null, error: null });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
latestRef.current = { key: "", sequence: -1 };
|
|
97
|
+
setResult({
|
|
98
|
+
state: "attaching",
|
|
99
|
+
frame: null,
|
|
100
|
+
attachment: null,
|
|
101
|
+
error: null
|
|
102
|
+
});
|
|
103
|
+
let disposed = false;
|
|
104
|
+
let socket = null;
|
|
105
|
+
let socketHandlers = null;
|
|
106
|
+
let reconnectTimer = null;
|
|
107
|
+
let expiryTimer = null;
|
|
108
|
+
let failures = 0;
|
|
109
|
+
let controllerGeneration = null;
|
|
110
|
+
let activeAttachment = null;
|
|
111
|
+
let activeStream = null;
|
|
112
|
+
let attachmentExpiresAt = 0;
|
|
113
|
+
let lastRelaySequence = null;
|
|
114
|
+
let relayAccepted = false;
|
|
115
|
+
let pendingFrame = null;
|
|
116
|
+
let decodingFrame = false;
|
|
117
|
+
const attachmentAbort = new AbortController();
|
|
118
|
+
const clearSocket = (terminateProducer = false) => {
|
|
119
|
+
if (!socket) return;
|
|
120
|
+
const current = socket;
|
|
121
|
+
const handlers = socketHandlers;
|
|
122
|
+
if (terminateProducer && activeStream?.kind === "relay" && socket.readyState === 1) {
|
|
123
|
+
try {
|
|
124
|
+
socket.send(
|
|
125
|
+
relayDatagram(
|
|
126
|
+
RELAY_TAG_CLOSE,
|
|
127
|
+
encodeStreamClose({
|
|
128
|
+
channelId: activeStream.channel.channelId,
|
|
129
|
+
reason: STREAM_CLOSE_REASON_NORMAL,
|
|
130
|
+
message: "viewer detached"
|
|
131
|
+
})
|
|
132
|
+
)
|
|
133
|
+
);
|
|
134
|
+
} catch {
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (handlers) {
|
|
138
|
+
current.removeEventListener("open", handlers.open);
|
|
139
|
+
current.removeEventListener("message", handlers.message);
|
|
140
|
+
current.removeEventListener("error", handlers.error);
|
|
141
|
+
current.removeEventListener("close", handlers.close);
|
|
142
|
+
}
|
|
143
|
+
if (current.readyState === 0 || current.readyState === 1) {
|
|
144
|
+
current.close(1e3, "viewer detached");
|
|
145
|
+
}
|
|
146
|
+
socket = null;
|
|
147
|
+
socketHandlers = null;
|
|
148
|
+
pendingFrame = null;
|
|
149
|
+
};
|
|
150
|
+
const clearRecoveryTimers = () => {
|
|
151
|
+
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
152
|
+
if (expiryTimer) clearTimeout(expiryTimer);
|
|
153
|
+
reconnectTimer = null;
|
|
154
|
+
expiryTimer = null;
|
|
155
|
+
};
|
|
156
|
+
const scheduleReconnect = () => {
|
|
157
|
+
if (disposed || reconnectTimer) return;
|
|
158
|
+
if (failures >= MAX_AUTOMATIC_RECONNECTS_WITHOUT_A_FRAME) {
|
|
159
|
+
clearRecoveryTimers();
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const delay = Math.min(5e3, 250 * 2 ** Math.min(failures, 5));
|
|
163
|
+
failures += 1;
|
|
164
|
+
setResult((current) => ({ ...current, state: "reconnecting" }));
|
|
165
|
+
reconnectTimer = setTimeout(() => {
|
|
166
|
+
reconnectTimer = null;
|
|
167
|
+
connectSocket();
|
|
168
|
+
}, delay);
|
|
169
|
+
};
|
|
170
|
+
const fail = (cause, reconnectAutomatically = true) => {
|
|
171
|
+
if (disposed) return;
|
|
172
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
173
|
+
setResult((current) => ({ ...current, state: "error", error }));
|
|
174
|
+
clearSocket();
|
|
175
|
+
if (reconnectAutomatically) scheduleReconnect();
|
|
176
|
+
else clearRecoveryTimers();
|
|
177
|
+
};
|
|
178
|
+
const onOpen = (source) => {
|
|
179
|
+
if (disposed || source !== socket) return;
|
|
180
|
+
if (activeStream?.kind === "relay") {
|
|
181
|
+
const body = encodeStreamOpen({
|
|
182
|
+
channel: { ...activeStream.channel, kind: STREAM_KIND_COMPUTER },
|
|
183
|
+
token: activeStream.token,
|
|
184
|
+
role: STREAM_ROLE_CLIENT,
|
|
185
|
+
resumeFromSeq: lastRelaySequence === null ? "0" : (BigInt(lastRelaySequence) + 1n).toString()
|
|
186
|
+
});
|
|
187
|
+
try {
|
|
188
|
+
source.send(relayDatagram(RELAY_TAG_OPEN, body));
|
|
189
|
+
} catch (error) {
|
|
190
|
+
fail(error);
|
|
191
|
+
}
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
setResult((current) => ({ ...current, state: "live", error: null }));
|
|
195
|
+
};
|
|
196
|
+
const drainLatestFrame = async () => {
|
|
197
|
+
if (decodingFrame) return;
|
|
198
|
+
decodingFrame = true;
|
|
199
|
+
try {
|
|
200
|
+
while (pendingFrame) {
|
|
201
|
+
if (disposed) break;
|
|
202
|
+
const pending = pendingFrame;
|
|
203
|
+
pendingFrame = null;
|
|
204
|
+
const { bytes, source } = pending;
|
|
205
|
+
const frame = await decodeComputerFrameMessage(bytes);
|
|
206
|
+
if (disposed || source !== socket) continue;
|
|
207
|
+
if (frame.computerSessionId !== computerSessionId || frame.targetId !== targetId) {
|
|
208
|
+
throw new Error("desktop frame belongs to another resource");
|
|
209
|
+
}
|
|
210
|
+
if (!controllerGeneration || frame.controllerGeneration !== controllerGeneration) {
|
|
211
|
+
throw new Error("desktop frame belongs to a stale controller");
|
|
212
|
+
}
|
|
213
|
+
const key = `${computerSessionId}:${targetId}:${frame.controllerGeneration}:${frame.targetGeneration}`;
|
|
214
|
+
if (latestRef.current.key === key && frame.sequence <= latestRef.current.sequence)
|
|
215
|
+
continue;
|
|
216
|
+
latestRef.current = { key, sequence: frame.sequence };
|
|
217
|
+
failures = 0;
|
|
218
|
+
setResult((current) => ({
|
|
219
|
+
...current,
|
|
220
|
+
state: "live",
|
|
221
|
+
frame,
|
|
222
|
+
error: null
|
|
223
|
+
}));
|
|
224
|
+
}
|
|
225
|
+
} catch (cause) {
|
|
226
|
+
fail(cause);
|
|
227
|
+
} finally {
|
|
228
|
+
decodingFrame = false;
|
|
229
|
+
if (!disposed && pendingFrame) void drainLatestFrame();
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
const onMessage = (source, event) => {
|
|
233
|
+
void (async () => {
|
|
234
|
+
try {
|
|
235
|
+
const bytes = await messageBytes(event.data);
|
|
236
|
+
if (disposed || source !== socket) return;
|
|
237
|
+
let frameBytes = bytes;
|
|
238
|
+
if (activeStream?.kind === "relay") {
|
|
239
|
+
if (bytes.length < 1) throw new Error("desktop relay returned an empty message");
|
|
240
|
+
const tag = bytes[0];
|
|
241
|
+
const body = bytes.subarray(1);
|
|
242
|
+
if (tag === RELAY_TAG_OPEN_ACK) {
|
|
243
|
+
const ack = decodeStreamOpenAck(body);
|
|
244
|
+
if (!ack.accepted) {
|
|
245
|
+
activeAttachment = null;
|
|
246
|
+
activeStream = null;
|
|
247
|
+
attachmentExpiresAt = 0;
|
|
248
|
+
throw new Error(ack.error?.message ?? "desktop stream was rejected by the relay");
|
|
249
|
+
}
|
|
250
|
+
relayAccepted = true;
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (tag === RELAY_TAG_CLOSE) {
|
|
254
|
+
activeAttachment = null;
|
|
255
|
+
activeStream = null;
|
|
256
|
+
attachmentExpiresAt = 0;
|
|
257
|
+
lastRelaySequence = null;
|
|
258
|
+
relayAccepted = false;
|
|
259
|
+
fail(new Error("Desktop frame source ended."), false);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (tag !== RELAY_TAG_FRAME || !relayAccepted) return;
|
|
263
|
+
const relayFrame = decodeStreamFrame(body);
|
|
264
|
+
lastRelaySequence = relayFrame.seq;
|
|
265
|
+
frameBytes = relayFrame.data;
|
|
266
|
+
}
|
|
267
|
+
pendingFrame = { bytes: frameBytes, source };
|
|
268
|
+
void drainLatestFrame();
|
|
269
|
+
} catch (cause) {
|
|
270
|
+
fail(cause);
|
|
271
|
+
}
|
|
272
|
+
})();
|
|
273
|
+
};
|
|
274
|
+
const onError = (source) => {
|
|
275
|
+
if (source !== socket) return;
|
|
276
|
+
fail(new Error("Desktop view lost connection."));
|
|
277
|
+
};
|
|
278
|
+
const onClose = (source) => {
|
|
279
|
+
if (disposed || source !== socket) return;
|
|
280
|
+
fail(new Error("Desktop view lost connection."));
|
|
281
|
+
};
|
|
282
|
+
const connectSocket = () => {
|
|
283
|
+
if (disposed) return;
|
|
284
|
+
clearSocket();
|
|
285
|
+
if (!activeAttachment || !activeStream || attachmentExpiresAt <= Date.now() + 1e3) {
|
|
286
|
+
void attach();
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (activeStream.kind === "direct_rfb") {
|
|
290
|
+
setResult((current) => ({ ...current, state: "live", error: null }));
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
relayAccepted = false;
|
|
294
|
+
setResult((current) => ({
|
|
295
|
+
...current,
|
|
296
|
+
state: "connecting",
|
|
297
|
+
error: null
|
|
298
|
+
}));
|
|
299
|
+
const createSocket = factoryRef.current ?? ((url, protocols) => new WebSocket(url, protocols));
|
|
300
|
+
const openedSocket = createSocket(
|
|
301
|
+
activeStream.kind === "relay" ? activeStream.url : computerFrameSocketUrl(activeAttachment, streamRef.current),
|
|
302
|
+
activeStream.kind === "direct_websocket" ? [...activeStream.protocols] : []
|
|
303
|
+
);
|
|
304
|
+
socket = openedSocket;
|
|
305
|
+
socketHandlers = {
|
|
306
|
+
open: () => onOpen(openedSocket),
|
|
307
|
+
message: (event) => onMessage(openedSocket, event),
|
|
308
|
+
error: () => onError(openedSocket),
|
|
309
|
+
close: () => onClose(openedSocket)
|
|
310
|
+
};
|
|
311
|
+
openedSocket.binaryType = "arraybuffer";
|
|
312
|
+
openedSocket.addEventListener("open", socketHandlers.open);
|
|
313
|
+
openedSocket.addEventListener("message", socketHandlers.message);
|
|
314
|
+
openedSocket.addEventListener("error", socketHandlers.error);
|
|
315
|
+
openedSocket.addEventListener("close", socketHandlers.close);
|
|
316
|
+
};
|
|
317
|
+
const attach = async () => {
|
|
318
|
+
if (disposed) return;
|
|
319
|
+
clearSocket();
|
|
320
|
+
if (expiryTimer) clearTimeout(expiryTimer);
|
|
321
|
+
setResult((current) => ({
|
|
322
|
+
...current,
|
|
323
|
+
state: current.attachment ? "reconnecting" : "attaching",
|
|
324
|
+
error: null
|
|
325
|
+
}));
|
|
326
|
+
try {
|
|
327
|
+
const attachment = await client.attachComputerSession(
|
|
328
|
+
workspaceId,
|
|
329
|
+
computerSessionId,
|
|
330
|
+
{
|
|
331
|
+
targetId,
|
|
332
|
+
expiresInSeconds: 120,
|
|
333
|
+
...streamRef.current ? { stream: streamRef.current } : {}
|
|
334
|
+
},
|
|
335
|
+
{ signal: attachmentAbort.signal, timeoutMs: 15e3 }
|
|
336
|
+
);
|
|
337
|
+
if (disposed) return;
|
|
338
|
+
if (attachment.computerSessionId !== computerSessionId || attachment.targetId !== targetId) {
|
|
339
|
+
throw new Error("desktop attachment does not match the requested resource");
|
|
340
|
+
}
|
|
341
|
+
controllerGeneration = attachment.controllerGeneration;
|
|
342
|
+
latestRef.current = { key: "", sequence: -1 };
|
|
343
|
+
activeAttachment = attachment;
|
|
344
|
+
activeStream = attachment.stream;
|
|
345
|
+
attachmentExpiresAt = Date.parse(attachment.expiresAt);
|
|
346
|
+
lastRelaySequence = null;
|
|
347
|
+
relayAccepted = false;
|
|
348
|
+
setResult((current) => ({
|
|
349
|
+
...current,
|
|
350
|
+
state: "connecting",
|
|
351
|
+
attachment: {
|
|
352
|
+
computerSessionId: attachment.computerSessionId,
|
|
353
|
+
controllerGeneration: attachment.controllerGeneration,
|
|
354
|
+
targetId: attachment.targetId,
|
|
355
|
+
inputAllowed: attachment.inputAllowed,
|
|
356
|
+
stream: attachment.stream,
|
|
357
|
+
expiresAt: attachment.expiresAt
|
|
358
|
+
},
|
|
359
|
+
error: null
|
|
360
|
+
}));
|
|
361
|
+
const refreshIn = Number.isFinite(attachmentExpiresAt) ? Math.max(1e3, attachmentExpiresAt - Date.now() - 5e3) : 6e4;
|
|
362
|
+
expiryTimer = setTimeout(() => {
|
|
363
|
+
if (disposed) return;
|
|
364
|
+
clearSocket(true);
|
|
365
|
+
activeAttachment = null;
|
|
366
|
+
activeStream = null;
|
|
367
|
+
attachmentExpiresAt = 0;
|
|
368
|
+
void attach();
|
|
369
|
+
}, refreshIn);
|
|
370
|
+
connectSocket();
|
|
371
|
+
} catch (cause) {
|
|
372
|
+
if (attachmentAbort.signal.aborted || disposed) return;
|
|
373
|
+
fail(cause, !isPlacementGenerationLossError(cause));
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
void attach();
|
|
377
|
+
return () => {
|
|
378
|
+
disposed = true;
|
|
379
|
+
pendingFrame = null;
|
|
380
|
+
attachmentAbort.abort();
|
|
381
|
+
clearRecoveryTimers();
|
|
382
|
+
clearSocket(true);
|
|
383
|
+
};
|
|
384
|
+
}, [client, enabled, nonce, options.computerSessionId, options.targetId, workspaceId]);
|
|
385
|
+
return { ...result, reconnect };
|
|
386
|
+
}
|
|
387
|
+
function relayDatagram(tag, body) {
|
|
388
|
+
const message = new Uint8Array(body.length + 1);
|
|
389
|
+
message[0] = tag;
|
|
390
|
+
message.set(body, 1);
|
|
391
|
+
return message.buffer;
|
|
392
|
+
}
|
|
393
|
+
async function messageBytes(value) {
|
|
394
|
+
if (value instanceof ArrayBuffer) return new Uint8Array(value);
|
|
395
|
+
if (ArrayBuffer.isView(value)) {
|
|
396
|
+
return new Uint8Array(value.buffer, value.byteOffset, value.byteLength).slice();
|
|
397
|
+
}
|
|
398
|
+
if (typeof Blob !== "undefined" && value instanceof Blob) {
|
|
399
|
+
return new Uint8Array(await value.arrayBuffer());
|
|
400
|
+
}
|
|
401
|
+
throw new Error("desktop frame stream returned a non-binary message");
|
|
402
|
+
}
|
|
403
|
+
function isPlacementGenerationLossError(cause) {
|
|
404
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
405
|
+
return /placement instance changed|controller authority changed/i.test(message);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// src/hooks/use-computer-session.ts
|
|
409
|
+
import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
|
|
410
|
+
function useComputerSession(options) {
|
|
411
|
+
const { client, workspaceId } = useEmbeddedComputerInteraction(options);
|
|
412
|
+
const computerSessionId = options.computerSessionId;
|
|
413
|
+
const enabled = (options.enabled ?? true) && computerSessionId !== null;
|
|
414
|
+
const pageLive = usePageLiveActivity();
|
|
415
|
+
const pollIntervalMs = Math.max(750, options.pollIntervalMs ?? 2e3);
|
|
416
|
+
const [state, setState] = useState2(
|
|
417
|
+
() => emptyState(computerSessionId, enabled)
|
|
418
|
+
);
|
|
419
|
+
const visible = state.computerSessionId === computerSessionId ? state : emptyState(computerSessionId, enabled);
|
|
420
|
+
const refreshBlocked = isNonRetryableInteractionError(visible.error);
|
|
421
|
+
const selectedTargetIdRef = useRef2(visible.selectedTargetId);
|
|
422
|
+
selectedTargetIdRef.current = visible.selectedTargetId;
|
|
423
|
+
const targetsRef = useRef2({
|
|
424
|
+
computerSessionId,
|
|
425
|
+
targets: visible.targets
|
|
426
|
+
});
|
|
427
|
+
const observationRef = useRef2({ computerSessionId, observation: visible.observation });
|
|
428
|
+
if (targetsRef.current.computerSessionId !== computerSessionId) {
|
|
429
|
+
targetsRef.current = { computerSessionId, targets: visible.targets };
|
|
430
|
+
observationRef.current = {
|
|
431
|
+
computerSessionId,
|
|
432
|
+
observation: visible.observation
|
|
433
|
+
};
|
|
434
|
+
} else {
|
|
435
|
+
targetsRef.current.targets = visible.targets;
|
|
436
|
+
observationRef.current.observation = visible.observation;
|
|
437
|
+
}
|
|
438
|
+
const requestRef = useRef2({
|
|
439
|
+
id: 0,
|
|
440
|
+
controller: null
|
|
441
|
+
});
|
|
442
|
+
const mutationRef = useRef2({
|
|
443
|
+
computerSessionId,
|
|
444
|
+
count: 0
|
|
445
|
+
});
|
|
446
|
+
const mountedRef = useRef2(true);
|
|
447
|
+
const invalidateRefresh = useCallback2(() => {
|
|
448
|
+
const id = requestRef.current.id + 1;
|
|
449
|
+
requestRef.current.controller?.abort();
|
|
450
|
+
requestRef.current = { id, controller: null };
|
|
451
|
+
}, []);
|
|
452
|
+
const refresh = useCallback2(async () => {
|
|
453
|
+
if (!enabled || !computerSessionId) return;
|
|
454
|
+
const id = requestRef.current.id + 1;
|
|
455
|
+
requestRef.current.controller?.abort();
|
|
456
|
+
const controller = new AbortController();
|
|
457
|
+
requestRef.current = { id, controller };
|
|
458
|
+
let discoveryCompleted = false;
|
|
459
|
+
try {
|
|
460
|
+
const [session, targetResponse] = await Promise.all([
|
|
461
|
+
client.getComputerSession(workspaceId, computerSessionId, {
|
|
462
|
+
signal: controller.signal
|
|
463
|
+
}),
|
|
464
|
+
client.listComputerTargets(workspaceId, computerSessionId, {
|
|
465
|
+
signal: controller.signal
|
|
466
|
+
})
|
|
467
|
+
]);
|
|
468
|
+
discoveryCompleted = true;
|
|
469
|
+
if (!mountedRef.current || requestRef.current.id !== id) return;
|
|
470
|
+
const targets = sortComputerTargets(targetResponse.targets);
|
|
471
|
+
const selected = chooseTarget(targets, selectedTargetIdRef.current);
|
|
472
|
+
const previous = observationRef.current.observation;
|
|
473
|
+
const retainedObservation = selected && previous?.target.id === selected.id && previous.target.controllerGeneration === selected.controllerGeneration && previous.target.targetGeneration === selected.targetGeneration ? previous : null;
|
|
474
|
+
selectedTargetIdRef.current = selected?.id ?? null;
|
|
475
|
+
targetsRef.current = { computerSessionId, targets };
|
|
476
|
+
observationRef.current = { computerSessionId, observation: retainedObservation };
|
|
477
|
+
setState(
|
|
478
|
+
(current) => current.computerSessionId === computerSessionId ? {
|
|
479
|
+
...current,
|
|
480
|
+
session,
|
|
481
|
+
targets,
|
|
482
|
+
selectedTargetId: selected?.id ?? null,
|
|
483
|
+
observation: retainedObservation,
|
|
484
|
+
loading: false,
|
|
485
|
+
error: null,
|
|
486
|
+
controlError: null
|
|
487
|
+
} : current
|
|
488
|
+
);
|
|
489
|
+
const observation = selected ? await client.observeComputerTarget(workspaceId, computerSessionId, selected.id, {
|
|
490
|
+
signal: controller.signal
|
|
491
|
+
}) : null;
|
|
492
|
+
if (!mountedRef.current || requestRef.current.id !== id) return;
|
|
493
|
+
selectedTargetIdRef.current = selected?.id ?? null;
|
|
494
|
+
targetsRef.current = { computerSessionId, targets };
|
|
495
|
+
observationRef.current = { computerSessionId, observation };
|
|
496
|
+
setState(
|
|
497
|
+
(current) => current.computerSessionId === computerSessionId ? {
|
|
498
|
+
...current,
|
|
499
|
+
session,
|
|
500
|
+
targets,
|
|
501
|
+
selectedTargetId: selected?.id ?? null,
|
|
502
|
+
observation,
|
|
503
|
+
loading: false,
|
|
504
|
+
error: null
|
|
505
|
+
} : current
|
|
506
|
+
);
|
|
507
|
+
} catch (cause) {
|
|
508
|
+
if (controller.signal.aborted || !mountedRef.current || requestRef.current.id !== id) return;
|
|
509
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
510
|
+
setState(
|
|
511
|
+
(current) => current.computerSessionId === computerSessionId ? {
|
|
512
|
+
...current,
|
|
513
|
+
loading: false,
|
|
514
|
+
error,
|
|
515
|
+
controlError: isInteractionControlUnavailable(
|
|
516
|
+
error,
|
|
517
|
+
discoveryCompleted ? "observation" : "control"
|
|
518
|
+
) ? error : current.controlError
|
|
519
|
+
} : current
|
|
520
|
+
);
|
|
521
|
+
} finally {
|
|
522
|
+
if (requestRef.current.id === id) requestRef.current = { id, controller: null };
|
|
523
|
+
}
|
|
524
|
+
}, [client, computerSessionId, enabled, workspaceId]);
|
|
525
|
+
useEffect2(() => {
|
|
526
|
+
mountedRef.current = true;
|
|
527
|
+
if (!enabled) {
|
|
528
|
+
invalidateRefresh();
|
|
529
|
+
setState(emptyState(computerSessionId, false));
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
setState(
|
|
533
|
+
(current) => current.computerSessionId === computerSessionId ? { ...current, loading: true } : emptyState(computerSessionId, true)
|
|
534
|
+
);
|
|
535
|
+
void refresh();
|
|
536
|
+
return () => {
|
|
537
|
+
mountedRef.current = false;
|
|
538
|
+
invalidateRefresh();
|
|
539
|
+
};
|
|
540
|
+
}, [computerSessionId, enabled, invalidateRefresh, refresh]);
|
|
541
|
+
useEffect2(() => {
|
|
542
|
+
if (!enabled || !pageLive || visible.mutating || refreshBlocked) return;
|
|
543
|
+
const timer = setInterval(() => {
|
|
544
|
+
if (!requestRef.current.controller) void refresh();
|
|
545
|
+
}, pollIntervalMs);
|
|
546
|
+
return () => clearInterval(timer);
|
|
547
|
+
}, [enabled, pageLive, pollIntervalMs, refresh, refreshBlocked, visible.mutating]);
|
|
548
|
+
useEffect2(() => {
|
|
549
|
+
if (!enabled || !computerSessionId || !pageLive || refreshBlocked) return;
|
|
550
|
+
let disposed = false;
|
|
551
|
+
const timer = setInterval(() => {
|
|
552
|
+
void client.heartbeatComputerSession(workspaceId, computerSessionId).catch(() => {
|
|
553
|
+
if (!disposed) void refresh();
|
|
554
|
+
});
|
|
555
|
+
}, 3e4);
|
|
556
|
+
return () => {
|
|
557
|
+
disposed = true;
|
|
558
|
+
clearInterval(timer);
|
|
559
|
+
};
|
|
560
|
+
}, [client, computerSessionId, enabled, pageLive, refresh, refreshBlocked, workspaceId]);
|
|
561
|
+
const runMutation = useCallback2(
|
|
562
|
+
async (scopeComputerSessionId, operation) => {
|
|
563
|
+
invalidateRefresh();
|
|
564
|
+
if (mutationRef.current.computerSessionId !== scopeComputerSessionId) {
|
|
565
|
+
mutationRef.current = {
|
|
566
|
+
computerSessionId: scopeComputerSessionId,
|
|
567
|
+
count: 0
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
mutationRef.current.count += 1;
|
|
571
|
+
setState(
|
|
572
|
+
(current) => current.computerSessionId === scopeComputerSessionId ? { ...current, mutating: true, error: null } : current
|
|
573
|
+
);
|
|
574
|
+
try {
|
|
575
|
+
const result = await operation();
|
|
576
|
+
setState(
|
|
577
|
+
(current) => current.computerSessionId === scopeComputerSessionId ? { ...current, controlError: null } : current
|
|
578
|
+
);
|
|
579
|
+
return result;
|
|
580
|
+
} catch (cause) {
|
|
581
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
582
|
+
setState(
|
|
583
|
+
(current) => current.computerSessionId === scopeComputerSessionId ? {
|
|
584
|
+
...current,
|
|
585
|
+
error,
|
|
586
|
+
controlError: isInteractionControlUnavailable(error) ? error : current.controlError
|
|
587
|
+
} : current
|
|
588
|
+
);
|
|
589
|
+
throw error;
|
|
590
|
+
} finally {
|
|
591
|
+
if (mutationRef.current.computerSessionId === scopeComputerSessionId) {
|
|
592
|
+
mutationRef.current.count = Math.max(0, mutationRef.current.count - 1);
|
|
593
|
+
const mutating = mutationRef.current.count > 0;
|
|
594
|
+
setState(
|
|
595
|
+
(current) => current.computerSessionId === scopeComputerSessionId ? { ...current, mutating } : current
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
},
|
|
600
|
+
[invalidateRefresh]
|
|
601
|
+
);
|
|
602
|
+
const selectTarget = useCallback2(
|
|
603
|
+
async (targetId) => {
|
|
604
|
+
if (!computerSessionId) throw new Error("No desktop is selected.");
|
|
605
|
+
const target = targetsRef.current.targets.find((candidate) => candidate.id === targetId);
|
|
606
|
+
if (!target) throw new Error("The selected desktop target is no longer available.");
|
|
607
|
+
invalidateRefresh();
|
|
608
|
+
selectedTargetIdRef.current = target.id;
|
|
609
|
+
observationRef.current = { computerSessionId, observation: null };
|
|
610
|
+
setState(
|
|
611
|
+
(current) => current.computerSessionId === computerSessionId ? {
|
|
612
|
+
...current,
|
|
613
|
+
selectedTargetId: target.id,
|
|
614
|
+
observation: null,
|
|
615
|
+
loading: true,
|
|
616
|
+
error: null
|
|
617
|
+
} : current
|
|
618
|
+
);
|
|
619
|
+
try {
|
|
620
|
+
const observation = await client.observeComputerTarget(
|
|
621
|
+
workspaceId,
|
|
622
|
+
computerSessionId,
|
|
623
|
+
target.id
|
|
624
|
+
);
|
|
625
|
+
if (selectedTargetIdRef.current !== target.id) return target;
|
|
626
|
+
observationRef.current = { computerSessionId, observation };
|
|
627
|
+
setState(
|
|
628
|
+
(current) => current.computerSessionId === computerSessionId && current.selectedTargetId === target.id ? { ...current, observation, loading: false, error: null } : current
|
|
629
|
+
);
|
|
630
|
+
return observation.target;
|
|
631
|
+
} catch (cause) {
|
|
632
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
633
|
+
setState(
|
|
634
|
+
(current) => current.computerSessionId === computerSessionId && current.selectedTargetId === target.id ? {
|
|
635
|
+
...current,
|
|
636
|
+
loading: false,
|
|
637
|
+
error,
|
|
638
|
+
controlError: isInteractionControlUnavailable(error, "observation") ? error : current.controlError
|
|
639
|
+
} : current
|
|
640
|
+
);
|
|
641
|
+
throw error;
|
|
642
|
+
}
|
|
643
|
+
},
|
|
644
|
+
[client, computerSessionId, invalidateRefresh, workspaceId]
|
|
645
|
+
);
|
|
646
|
+
const dispatchAction = useCallback2(
|
|
647
|
+
async (action, operationId, frame) => {
|
|
648
|
+
if (!computerSessionId) throw new Error("No desktop is selected.");
|
|
649
|
+
if (frame && frame.computerSessionId !== computerSessionId) {
|
|
650
|
+
throw new Error("The displayed desktop frame belongs to another desktop session.");
|
|
651
|
+
}
|
|
652
|
+
const currentObservation = observationRef.current.computerSessionId === computerSessionId ? observationRef.current.observation : null;
|
|
653
|
+
const focusTarget = action.type === "focus" ? targetsRef.current.targets.find((candidate) => candidate.id === action.targetId) ?? null : null;
|
|
654
|
+
const frameTarget = frame ? targetsRef.current.targets.find((candidate) => candidate.id === frame.targetId) ?? (currentObservation?.target.id === frame.targetId ? currentObservation.target : null) : null;
|
|
655
|
+
const inputTarget = (action.type === "keyboard" || action.type === "clipboard") && targetsRef.current.computerSessionId === computerSessionId ? targetsRef.current.targets.find(
|
|
656
|
+
(candidate) => candidate.id === selectedTargetIdRef.current
|
|
657
|
+
) : null;
|
|
658
|
+
const target = frameTarget ?? focusTarget ?? currentObservation?.target ?? inputTarget ?? null;
|
|
659
|
+
if (!target) throw new Error("The desktop target is not ready for input.");
|
|
660
|
+
if (frame && frame.targetId !== selectedTargetIdRef.current) {
|
|
661
|
+
throw new Error("The displayed desktop frame is no longer selected.");
|
|
662
|
+
}
|
|
663
|
+
if (action.type === "pointer") {
|
|
664
|
+
const expectedFrameId = frame?.frameId ?? currentObservation?.frameId ?? null;
|
|
665
|
+
if (!expectedFrameId || action.frameId !== expectedFrameId) {
|
|
666
|
+
throw new Error("Pointer input must reference the exact displayed desktop frame.");
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
if (action.type === "semantic" && !currentObservation) {
|
|
670
|
+
throw new Error("The desktop accessibility tree is not ready for input.");
|
|
671
|
+
}
|
|
672
|
+
return await runMutation(computerSessionId, async () => {
|
|
673
|
+
const receipt = await client.actInComputer(workspaceId, computerSessionId, {
|
|
674
|
+
operationId,
|
|
675
|
+
targetId: target.id,
|
|
676
|
+
expectedTargetGeneration: frame?.targetGeneration ?? target.targetGeneration,
|
|
677
|
+
expectedObservationId: action.type === "pointer" ? null : currentObservation?.observationId ?? null,
|
|
678
|
+
expectedFrameId: action.type === "pointer" ? action.frameId : null,
|
|
679
|
+
action
|
|
680
|
+
});
|
|
681
|
+
if (receipt.observation) {
|
|
682
|
+
const observation = receipt.observation;
|
|
683
|
+
observationRef.current = { computerSessionId, observation };
|
|
684
|
+
selectedTargetIdRef.current = observation.target.id;
|
|
685
|
+
setState(
|
|
686
|
+
(current) => current.computerSessionId === computerSessionId ? {
|
|
687
|
+
...current,
|
|
688
|
+
observation,
|
|
689
|
+
selectedTargetId: observation.target.id,
|
|
690
|
+
targets: replaceTarget(current.targets, observation.target)
|
|
691
|
+
} : current
|
|
692
|
+
);
|
|
693
|
+
} else {
|
|
694
|
+
void refresh();
|
|
695
|
+
}
|
|
696
|
+
return receipt;
|
|
697
|
+
});
|
|
698
|
+
},
|
|
699
|
+
[client, computerSessionId, refresh, runMutation, workspaceId]
|
|
700
|
+
);
|
|
701
|
+
const act = useCallback2(
|
|
702
|
+
async (action, operationId = crypto.randomUUID()) => await dispatchAction(action, operationId, null),
|
|
703
|
+
[dispatchAction]
|
|
704
|
+
);
|
|
705
|
+
const actFromFrame = useCallback2(
|
|
706
|
+
async (action, frame, operationId = crypto.randomUUID()) => await dispatchAction(action, operationId, frame),
|
|
707
|
+
[dispatchAction]
|
|
708
|
+
);
|
|
709
|
+
const readClipboard = useCallback2(async () => {
|
|
710
|
+
if (!computerSessionId) throw new Error("No desktop is selected.");
|
|
711
|
+
try {
|
|
712
|
+
return await client.readComputerClipboard(workspaceId, computerSessionId);
|
|
713
|
+
} catch (cause) {
|
|
714
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
715
|
+
setState(
|
|
716
|
+
(current) => current.computerSessionId === computerSessionId ? {
|
|
717
|
+
...current,
|
|
718
|
+
error,
|
|
719
|
+
controlError: isInteractionControlUnavailable(error) ? error : current.controlError
|
|
720
|
+
} : current
|
|
721
|
+
);
|
|
722
|
+
throw error;
|
|
723
|
+
}
|
|
724
|
+
}, [client, computerSessionId, workspaceId]);
|
|
725
|
+
return {
|
|
726
|
+
session: visible.session,
|
|
727
|
+
targets: visible.targets,
|
|
728
|
+
selectedTarget: visible.targets.find((target) => target.id === visible.selectedTargetId) ?? null,
|
|
729
|
+
observation: visible.observation,
|
|
730
|
+
loading: visible.loading,
|
|
731
|
+
mutating: visible.mutating,
|
|
732
|
+
error: visible.error,
|
|
733
|
+
controlError: visible.controlError,
|
|
734
|
+
refresh,
|
|
735
|
+
readClipboard,
|
|
736
|
+
selectTarget,
|
|
737
|
+
act,
|
|
738
|
+
actFromFrame
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
function emptyState(computerSessionId, loading) {
|
|
742
|
+
return {
|
|
743
|
+
computerSessionId,
|
|
744
|
+
session: null,
|
|
745
|
+
targets: [],
|
|
746
|
+
selectedTargetId: null,
|
|
747
|
+
observation: null,
|
|
748
|
+
loading,
|
|
749
|
+
mutating: false,
|
|
750
|
+
error: null,
|
|
751
|
+
controlError: null
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
function chooseTarget(targets, preferredId) {
|
|
755
|
+
const preferred = targets.find((target) => target.id === preferredId);
|
|
756
|
+
if (preferred) return preferred;
|
|
757
|
+
return targets.find((target) => target.focused && target.kind === "screen") ?? targets.find((target) => target.kind === "screen") ?? targets.find((target) => target.focused && target.kind === "window") ?? targets.find((target) => target.kind === "window") ?? targets.find((target) => target.focused && target.kind === "app") ?? targets.find((target) => target.kind === "app") ?? targets[0] ?? null;
|
|
758
|
+
}
|
|
759
|
+
function sortComputerTargets(targets) {
|
|
760
|
+
const rank = { window: 0, app: 1, screen: 2 };
|
|
761
|
+
return [...targets].sort(
|
|
762
|
+
(left, right) => Number(right.focused) - Number(left.focused) || rank[left.kind] - rank[right.kind] || left.title.localeCompare(right.title) || left.id.localeCompare(right.id)
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
function replaceTarget(targets, target) {
|
|
766
|
+
return sortComputerTargets([
|
|
767
|
+
...targets.filter((candidate) => candidate.id !== target.id),
|
|
768
|
+
target
|
|
769
|
+
]);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// src/hooks/use-computer-input-posture.ts
|
|
773
|
+
import { useCallback as useCallback3, useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
|
|
774
|
+
function useComputerInputPosture(options) {
|
|
775
|
+
const { client, workspaceId } = useEmbeddedComputerInteraction(options);
|
|
776
|
+
const { computerSessionId, controllerGeneration, enabled } = options;
|
|
777
|
+
const [nonce, setNonce] = useState3(0);
|
|
778
|
+
const [state, setState] = useState3(null);
|
|
779
|
+
const key = JSON.stringify([workspaceId, computerSessionId, controllerGeneration, enabled]);
|
|
780
|
+
const sourceRef = useRef3({ client, key });
|
|
781
|
+
if (sourceRef.current.client !== client || sourceRef.current.key !== key) {
|
|
782
|
+
sourceRef.current = { client, key };
|
|
783
|
+
}
|
|
784
|
+
const source = sourceRef.current;
|
|
785
|
+
const visible = state?.source === source && state.nonce === nonce ? state : null;
|
|
786
|
+
const refresh = useCallback3(() => setNonce((value) => value + 1), []);
|
|
787
|
+
useEffect3(() => {
|
|
788
|
+
if (!enabled || !computerSessionId || !controllerGeneration) return;
|
|
789
|
+
const controller = new AbortController();
|
|
790
|
+
let disposed = false;
|
|
791
|
+
setState({ source, nonce, inputAllowed: null, pending: true, error: null });
|
|
792
|
+
void (async () => {
|
|
793
|
+
try {
|
|
794
|
+
if (!client.getComputerInputPosture) {
|
|
795
|
+
throw new Error("Desktop input availability could not be verified.");
|
|
796
|
+
}
|
|
797
|
+
const posture = await client.getComputerInputPosture(workspaceId, computerSessionId, {
|
|
798
|
+
signal: controller.signal,
|
|
799
|
+
timeoutMs: 15e3
|
|
800
|
+
});
|
|
801
|
+
if (disposed || sourceRef.current !== source) return;
|
|
802
|
+
if (posture.computerSessionId !== computerSessionId || posture.controllerGeneration !== controllerGeneration || typeof posture.inputAllowed !== "boolean") {
|
|
803
|
+
throw new Error("Desktop input availability belongs to a different controller.");
|
|
804
|
+
}
|
|
805
|
+
setState({
|
|
806
|
+
source,
|
|
807
|
+
nonce,
|
|
808
|
+
inputAllowed: posture.inputAllowed,
|
|
809
|
+
pending: false,
|
|
810
|
+
error: null
|
|
811
|
+
});
|
|
812
|
+
} catch (cause) {
|
|
813
|
+
if (disposed || controller.signal.aborted || sourceRef.current !== source) return;
|
|
814
|
+
setState({
|
|
815
|
+
source,
|
|
816
|
+
nonce,
|
|
817
|
+
inputAllowed: null,
|
|
818
|
+
pending: false,
|
|
819
|
+
error: cause instanceof Error ? cause : new Error(String(cause))
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
})();
|
|
823
|
+
return () => {
|
|
824
|
+
disposed = true;
|
|
825
|
+
controller.abort();
|
|
826
|
+
};
|
|
827
|
+
}, [client, computerSessionId, controllerGeneration, enabled, nonce, source, workspaceId]);
|
|
828
|
+
return {
|
|
829
|
+
inputAllowed: visible?.inputAllowed ?? null,
|
|
830
|
+
pending: enabled && (visible?.pending ?? true),
|
|
831
|
+
error: visible?.error ?? null,
|
|
832
|
+
refresh
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// src/hooks/use-computer-sessions.ts
|
|
837
|
+
import { useCallback as useCallback4, useEffect as useEffect4, useMemo, useRef as useRef4, useState as useState4 } from "react";
|
|
838
|
+
function useComputerSessions(options = {}) {
|
|
839
|
+
const {
|
|
840
|
+
client,
|
|
841
|
+
workspaceId,
|
|
842
|
+
registerSessionReconciler,
|
|
843
|
+
workspaceInteractionEvent,
|
|
844
|
+
workspaceInteractionConnectionState
|
|
845
|
+
} = useEmbeddedComputerInteraction(options);
|
|
846
|
+
const enabled = options.enabled ?? true;
|
|
847
|
+
const [state, setState] = useState4(() => emptyState2(workspaceId, enabled));
|
|
848
|
+
const requestRef = useRef4({
|
|
849
|
+
id: 0,
|
|
850
|
+
controller: null
|
|
851
|
+
});
|
|
852
|
+
const mountedRef = useRef4(true);
|
|
853
|
+
const visible = state.workspaceId === workspaceId ? state : emptyState2(workspaceId, enabled);
|
|
854
|
+
const cancelLoad = useCallback4(() => {
|
|
855
|
+
const id = requestRef.current.id + 1;
|
|
856
|
+
requestRef.current.controller?.abort();
|
|
857
|
+
requestRef.current = { id, controller: null };
|
|
858
|
+
}, []);
|
|
859
|
+
const load = useCallback4(
|
|
860
|
+
async (foreground) => {
|
|
861
|
+
if (!enabled) return;
|
|
862
|
+
const id = requestRef.current.id + 1;
|
|
863
|
+
requestRef.current.controller?.abort();
|
|
864
|
+
const controller = new AbortController();
|
|
865
|
+
requestRef.current = { id, controller };
|
|
866
|
+
setState((current) => ({
|
|
867
|
+
...current.workspaceId === workspaceId ? current : emptyState2(workspaceId, true),
|
|
868
|
+
active: true,
|
|
869
|
+
loading: foreground,
|
|
870
|
+
refreshing: !foreground,
|
|
871
|
+
error: foreground || current.workspaceId !== workspaceId ? null : current.error
|
|
872
|
+
}));
|
|
873
|
+
try {
|
|
874
|
+
const response = await client.listComputerSessions(workspaceId, {
|
|
875
|
+
signal: controller.signal
|
|
876
|
+
});
|
|
877
|
+
if (!mountedRef.current || requestRef.current.id !== id) return;
|
|
878
|
+
setState({
|
|
879
|
+
workspaceId,
|
|
880
|
+
active: true,
|
|
881
|
+
revision: response.revision,
|
|
882
|
+
sessions: sortComputerSessions(response.sessions),
|
|
883
|
+
loading: false,
|
|
884
|
+
refreshing: false,
|
|
885
|
+
error: null
|
|
886
|
+
});
|
|
887
|
+
} catch (cause) {
|
|
888
|
+
if (controller.signal.aborted || !mountedRef.current || requestRef.current.id !== id)
|
|
889
|
+
return;
|
|
890
|
+
setState((current) => ({
|
|
891
|
+
...current.workspaceId === workspaceId ? current : emptyState2(workspaceId, true),
|
|
892
|
+
loading: false,
|
|
893
|
+
refreshing: false,
|
|
894
|
+
error: cause instanceof Error ? cause : new Error(String(cause))
|
|
895
|
+
}));
|
|
896
|
+
} finally {
|
|
897
|
+
if (requestRef.current.id === id) requestRef.current = { id, controller: null };
|
|
898
|
+
}
|
|
899
|
+
},
|
|
900
|
+
[client, enabled, workspaceId]
|
|
901
|
+
);
|
|
902
|
+
const refresh = useCallback4(async () => await load(false), [load]);
|
|
903
|
+
useEffect4(() => {
|
|
904
|
+
mountedRef.current = true;
|
|
905
|
+
if (!enabled) {
|
|
906
|
+
cancelLoad();
|
|
907
|
+
setState(emptyState2(workspaceId, false));
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
void load(true);
|
|
911
|
+
return () => {
|
|
912
|
+
mountedRef.current = false;
|
|
913
|
+
cancelLoad();
|
|
914
|
+
};
|
|
915
|
+
}, [cancelLoad, enabled, load, workspaceId]);
|
|
916
|
+
useInteractionInvalidation({
|
|
917
|
+
workspaceId,
|
|
918
|
+
key: workspaceId,
|
|
919
|
+
enabled,
|
|
920
|
+
event: workspaceInteractionEvent,
|
|
921
|
+
connectionState: workspaceInteractionConnectionState,
|
|
922
|
+
knownRevision: visible.revision,
|
|
923
|
+
refresh,
|
|
924
|
+
fallbackPollIntervalMs: options.pollIntervalMs
|
|
925
|
+
});
|
|
926
|
+
useEffect4(() => {
|
|
927
|
+
if (!enabled || !options.sessionId) return;
|
|
928
|
+
return registerSessionReconciler(
|
|
929
|
+
options.sessionId,
|
|
930
|
+
`computer-sessions:${workspaceId}`,
|
|
931
|
+
refresh
|
|
932
|
+
);
|
|
933
|
+
}, [enabled, options.sessionId, refresh, registerSessionReconciler, workspaceId]);
|
|
934
|
+
const create = useCallback4(
|
|
935
|
+
async (request) => {
|
|
936
|
+
const response = await client.createComputerSession(workspaceId, {
|
|
937
|
+
...request,
|
|
938
|
+
operationId: request.operationId ?? crypto.randomUUID()
|
|
939
|
+
});
|
|
940
|
+
setState((current) => mergeSession(current, workspaceId, response.session));
|
|
941
|
+
return response;
|
|
942
|
+
},
|
|
943
|
+
[client, workspaceId]
|
|
944
|
+
);
|
|
945
|
+
const end = useCallback4(
|
|
946
|
+
async (computerSessionId, operationId = crypto.randomUUID()) => {
|
|
947
|
+
const response = await client.endComputerSession(workspaceId, computerSessionId, {
|
|
948
|
+
operationId
|
|
949
|
+
});
|
|
950
|
+
setState((current) => mergeSession(current, workspaceId, response.session));
|
|
951
|
+
return response;
|
|
952
|
+
},
|
|
953
|
+
[client, workspaceId]
|
|
954
|
+
);
|
|
955
|
+
const relevantSessions = useMemo(
|
|
956
|
+
() => options.sessionId ? visible.sessions.filter(
|
|
957
|
+
(session) => session.associations.some((association) => association.sessionId === options.sessionId)
|
|
958
|
+
) : [],
|
|
959
|
+
[options.sessionId, visible.sessions]
|
|
960
|
+
);
|
|
961
|
+
return {
|
|
962
|
+
revision: visible.revision,
|
|
963
|
+
sessions: visible.sessions,
|
|
964
|
+
relevantSessions,
|
|
965
|
+
// A disabled registry is deliberately empty. When its surface becomes
|
|
966
|
+
// visible again, expose that transition as loading in the same render so
|
|
967
|
+
// consumers cannot mistake the stale empty snapshot for a completed list.
|
|
968
|
+
loading: enabled ? !visible.active || visible.loading : false,
|
|
969
|
+
refreshing: visible.refreshing,
|
|
970
|
+
error: visible.error,
|
|
971
|
+
refresh,
|
|
972
|
+
create,
|
|
973
|
+
end
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
function emptyState2(workspaceId, loading) {
|
|
977
|
+
return {
|
|
978
|
+
workspaceId,
|
|
979
|
+
active: loading,
|
|
980
|
+
revision: 0,
|
|
981
|
+
sessions: [],
|
|
982
|
+
loading,
|
|
983
|
+
refreshing: false,
|
|
984
|
+
error: null
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
function sortComputerSessions(sessions) {
|
|
988
|
+
return [...sessions].sort((left, right) => {
|
|
989
|
+
const live = Number(isLiveComputer(right)) - Number(isLiveComputer(left));
|
|
990
|
+
return live || timestamp(right.lastUsedAt) - timestamp(left.lastUsedAt) || left.id.localeCompare(right.id);
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
function isLiveComputer(session) {
|
|
994
|
+
return !["ending", "ended", "failed", "lost"].includes(session.lifecycle);
|
|
995
|
+
}
|
|
996
|
+
function timestamp(value) {
|
|
997
|
+
const parsed = Date.parse(value);
|
|
998
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
999
|
+
}
|
|
1000
|
+
function mergeSession(current, workspaceId, session) {
|
|
1001
|
+
if (current.workspaceId !== workspaceId) return current;
|
|
1002
|
+
const sessions = current.sessions.filter((candidate) => candidate.id !== session.id);
|
|
1003
|
+
sessions.push(session);
|
|
1004
|
+
return { ...current, sessions: sortComputerSessions(sessions), error: null };
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// src/components/computer-viewer.tsx
|
|
1008
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
1009
|
+
function ComputerViewer({
|
|
1010
|
+
sessionId,
|
|
1011
|
+
enabled = true,
|
|
1012
|
+
className,
|
|
1013
|
+
onNotify,
|
|
1014
|
+
webSocketFactory,
|
|
1015
|
+
renderEmpty,
|
|
1016
|
+
requestedComputerSessionId,
|
|
1017
|
+
requestedComputerRequestId,
|
|
1018
|
+
initialComputerSessionId,
|
|
1019
|
+
onComputerSessionIdChange,
|
|
1020
|
+
...override
|
|
1021
|
+
}) {
|
|
1022
|
+
const registry = useComputerSessions({ ...override, sessionId, enabled });
|
|
1023
|
+
const createSession = registry.create;
|
|
1024
|
+
const refreshRegistry = registry.refresh;
|
|
1025
|
+
const liveRelevant = useMemo2(
|
|
1026
|
+
() => registry.relevantSessions.filter((session) => isLiveComputer2(session)),
|
|
1027
|
+
[registry.relevantSessions]
|
|
1028
|
+
);
|
|
1029
|
+
const connectedPlacementLoss = useMemo2(
|
|
1030
|
+
() => registry.relevantSessions.find(
|
|
1031
|
+
(session) => session.lifecycle === "lost" && session.failureCode === "source_placement_changed" && session.placement.kind === "connected_machine"
|
|
1032
|
+
) ?? null,
|
|
1033
|
+
[registry.relevantSessions]
|
|
1034
|
+
);
|
|
1035
|
+
const recentRelevantFailure = useMemo2(
|
|
1036
|
+
() => liveRelevant.length === 0 ? registry.relevantSessions.find(
|
|
1037
|
+
(session) => ["failed", "lost"].includes(session.lifecycle) && !(session.failureCode === "source_placement_changed" && session.placement.kind === "connected_machine")
|
|
1038
|
+
) ?? null : null,
|
|
1039
|
+
[liveRelevant.length, registry.relevantSessions]
|
|
1040
|
+
);
|
|
1041
|
+
const liveSessions = useMemo2(() => {
|
|
1042
|
+
const sessions = registry.sessions.filter((session) => isLiveComputer2(session));
|
|
1043
|
+
return recentRelevantFailure ? [...sessions, recentRelevantFailure] : sessions;
|
|
1044
|
+
}, [recentRelevantFailure, registry.sessions]);
|
|
1045
|
+
const relevant = useMemo2(
|
|
1046
|
+
() => liveRelevant.length > 0 ? liveRelevant : recentRelevantFailure ? [recentRelevantFailure] : [],
|
|
1047
|
+
[liveRelevant, recentRelevantFailure]
|
|
1048
|
+
);
|
|
1049
|
+
const [selection, setSelection] = useState5(
|
|
1050
|
+
() => initialComputerSessionId ? { sessionId: initialComputerSessionId, pinned: true } : null
|
|
1051
|
+
);
|
|
1052
|
+
const selectComputerSession = useCallback5(
|
|
1053
|
+
(next) => {
|
|
1054
|
+
setSelection(next);
|
|
1055
|
+
onComputerSessionIdChange?.(next?.sessionId ?? null);
|
|
1056
|
+
},
|
|
1057
|
+
[onComputerSessionIdChange]
|
|
1058
|
+
);
|
|
1059
|
+
const interventions = useInteractionInterventions({
|
|
1060
|
+
...override,
|
|
1061
|
+
enabled,
|
|
1062
|
+
resourceKind: "computer_session"
|
|
1063
|
+
});
|
|
1064
|
+
const [creating, setCreating] = useState5(false);
|
|
1065
|
+
const [createError, setCreateError] = useState5(null);
|
|
1066
|
+
const [showControls, setShowControls] = useState5(false);
|
|
1067
|
+
const previousSessionIdRef = useRef5(sessionId);
|
|
1068
|
+
const handledRequestRef = useRef5(null);
|
|
1069
|
+
const seenInterventionIdsRef = useRef5(/* @__PURE__ */ new Set());
|
|
1070
|
+
const createInFlightRef = useRef5(false);
|
|
1071
|
+
const autoCreateSessionRef = useRef5(null);
|
|
1072
|
+
const emptyCatalogConfirmationRef = useRef5(null);
|
|
1073
|
+
const endedGenerationLossRef = useRef5(/* @__PURE__ */ new Set());
|
|
1074
|
+
const recreatedPlacementLossRef = useRef5(/* @__PURE__ */ new Set());
|
|
1075
|
+
const [suppressGenericCreate, setSuppressGenericCreate] = useState5(false);
|
|
1076
|
+
const endStaleComputer = registry.end;
|
|
1077
|
+
useEffect5(() => {
|
|
1078
|
+
if (!enabled) return;
|
|
1079
|
+
for (const session of registry.sessions) {
|
|
1080
|
+
if (!isAttachedChromeGenerationLoss(session)) continue;
|
|
1081
|
+
setSuppressGenericCreate(true);
|
|
1082
|
+
if (endedGenerationLossRef.current.has(session.id)) continue;
|
|
1083
|
+
endedGenerationLossRef.current.add(session.id);
|
|
1084
|
+
void endStaleComputer(session.id).catch(() => {
|
|
1085
|
+
endedGenerationLossRef.current.delete(session.id);
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
}, [enabled, endStaleComputer, registry.sessions]);
|
|
1089
|
+
const notifyError = useCallback5(
|
|
1090
|
+
(cause, fallback) => {
|
|
1091
|
+
onNotify?.({
|
|
1092
|
+
kind: "error",
|
|
1093
|
+
message: cause instanceof Error ? cause.message : fallback
|
|
1094
|
+
});
|
|
1095
|
+
},
|
|
1096
|
+
[onNotify]
|
|
1097
|
+
);
|
|
1098
|
+
useEffect5(() => {
|
|
1099
|
+
if (previousSessionIdRef.current === sessionId) return;
|
|
1100
|
+
previousSessionIdRef.current = sessionId;
|
|
1101
|
+
handledRequestRef.current = null;
|
|
1102
|
+
autoCreateSessionRef.current = null;
|
|
1103
|
+
emptyCatalogConfirmationRef.current = null;
|
|
1104
|
+
recreatedPlacementLossRef.current.clear();
|
|
1105
|
+
setSuppressGenericCreate(false);
|
|
1106
|
+
setCreateError(null);
|
|
1107
|
+
setSelection(
|
|
1108
|
+
initialComputerSessionId ? { sessionId: initialComputerSessionId, pinned: true } : null
|
|
1109
|
+
);
|
|
1110
|
+
}, [initialComputerSessionId, sessionId]);
|
|
1111
|
+
useEffect5(() => {
|
|
1112
|
+
if (!enabled || registry.loading) return;
|
|
1113
|
+
const requested = requestedComputerSessionId ? liveSessions.find((session) => session.id === requestedComputerSessionId) ?? null : null;
|
|
1114
|
+
const requestKey = requested ? `${requestedComputerRequestId ?? "default"}:${requested.id}` : null;
|
|
1115
|
+
if (requested && requestKey && handledRequestRef.current !== requestKey) {
|
|
1116
|
+
handledRequestRef.current = requestKey;
|
|
1117
|
+
selectComputerSession({ sessionId: requested.id, pinned: true });
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
const selectedStillLive = liveSessions.some((session) => session.id === selection?.sessionId);
|
|
1121
|
+
if (selection?.pinned && selectedStillLive) return;
|
|
1122
|
+
const preferred = relevant[0] ?? null;
|
|
1123
|
+
if (!preferred) {
|
|
1124
|
+
if (selection) selectComputerSession(null);
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
if (!selectedStillLive || !selection?.pinned) {
|
|
1128
|
+
if (selection?.sessionId !== preferred.id || selection.pinned) {
|
|
1129
|
+
selectComputerSession({ sessionId: preferred.id, pinned: false });
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
}, [
|
|
1133
|
+
liveSessions,
|
|
1134
|
+
enabled,
|
|
1135
|
+
registry.loading,
|
|
1136
|
+
relevant,
|
|
1137
|
+
requestedComputerRequestId,
|
|
1138
|
+
requestedComputerSessionId,
|
|
1139
|
+
selectComputerSession,
|
|
1140
|
+
selection
|
|
1141
|
+
]);
|
|
1142
|
+
const selectedRegistrySession = useMemo2(
|
|
1143
|
+
() => liveSessions.find((session) => session.id === selection?.sessionId) ?? null,
|
|
1144
|
+
[liveSessions, selection?.sessionId]
|
|
1145
|
+
);
|
|
1146
|
+
const interventionCounts = useMemo2(
|
|
1147
|
+
() => countInterventions(interventions.interventions),
|
|
1148
|
+
[interventions.interventions]
|
|
1149
|
+
);
|
|
1150
|
+
const selectedInterventions = useMemo2(
|
|
1151
|
+
() => interventions.interventions.filter(
|
|
1152
|
+
(intervention) => intervention.resourceId === selection?.sessionId
|
|
1153
|
+
),
|
|
1154
|
+
[interventions.interventions, selection?.sessionId]
|
|
1155
|
+
);
|
|
1156
|
+
const controllerReady = selectedRegistrySession?.lifecycle === "active";
|
|
1157
|
+
const computer = useComputerSession({
|
|
1158
|
+
...override,
|
|
1159
|
+
computerSessionId: selection?.sessionId ?? null,
|
|
1160
|
+
enabled: enabled && selection !== null && controllerReady
|
|
1161
|
+
});
|
|
1162
|
+
useEffect5(() => {
|
|
1163
|
+
setShowControls(computer.selectedTarget?.kind === "app");
|
|
1164
|
+
}, [sessionId, selection?.sessionId, computer.selectedTarget?.id, computer.selectedTarget?.kind]);
|
|
1165
|
+
useEffect5(() => {
|
|
1166
|
+
if (!isSourcePlacementChangedError(computer.error, "computer_session")) return;
|
|
1167
|
+
void refreshRegistry();
|
|
1168
|
+
}, [computer.error, refreshRegistry]);
|
|
1169
|
+
const act = computer.act;
|
|
1170
|
+
const actFromFrame = computer.actFromFrame;
|
|
1171
|
+
const refreshComputer = computer.refresh;
|
|
1172
|
+
const frames = useComputerFrameStream({
|
|
1173
|
+
...override,
|
|
1174
|
+
computerSessionId: selection?.sessionId ?? null,
|
|
1175
|
+
targetId: computer.selectedTarget?.id ?? null,
|
|
1176
|
+
enabled: enabled && selection !== null && controllerReady && computer.selectedTarget !== null && isComputerFrameTarget(computer.selectedTarget),
|
|
1177
|
+
stream: {
|
|
1178
|
+
format: "jpeg",
|
|
1179
|
+
quality: 78,
|
|
1180
|
+
maxWidth: 1920,
|
|
1181
|
+
maxHeight: 1200,
|
|
1182
|
+
everyNthFrame: 1
|
|
1183
|
+
},
|
|
1184
|
+
...webSocketFactory ? { webSocketFactory } : {}
|
|
1185
|
+
});
|
|
1186
|
+
const displayedFrame = frames.frame && frames.frame.computerSessionId === selection?.sessionId && frames.frame.targetId === computer.selectedTarget?.id && frames.frame.targetGeneration === computer.selectedTarget?.targetGeneration ? frames.frame : null;
|
|
1187
|
+
const rfbStream = frames.attachment?.stream.kind === "direct_rfb" ? frames.attachment.stream : null;
|
|
1188
|
+
const appTarget = computer.selectedTarget?.kind === "app";
|
|
1189
|
+
const appInputPosture = useComputerInputPosture({
|
|
1190
|
+
...override,
|
|
1191
|
+
computerSessionId: selection?.sessionId ?? null,
|
|
1192
|
+
controllerGeneration: computer.session?.controller?.controllerGeneration ?? null,
|
|
1193
|
+
enabled: enabled && controllerReady && appTarget
|
|
1194
|
+
});
|
|
1195
|
+
const inputPostureRef = useRef5({
|
|
1196
|
+
sessionId: selection?.sessionId ?? null,
|
|
1197
|
+
controllerGeneration: computer.session?.controller?.controllerGeneration ?? null,
|
|
1198
|
+
denied: false
|
|
1199
|
+
});
|
|
1200
|
+
const postureSessionId = selection?.sessionId ?? null;
|
|
1201
|
+
const postureController = computer.session?.controller?.controllerGeneration ?? null;
|
|
1202
|
+
if (inputPostureRef.current.sessionId !== postureSessionId || inputPostureRef.current.controllerGeneration !== postureController) {
|
|
1203
|
+
inputPostureRef.current = {
|
|
1204
|
+
sessionId: postureSessionId,
|
|
1205
|
+
controllerGeneration: postureController,
|
|
1206
|
+
denied: false
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
if (frames.attachment?.computerSessionId === postureSessionId && frames.attachment.controllerGeneration === postureController) {
|
|
1210
|
+
inputPostureRef.current.denied = frames.attachment.inputAllowed === false;
|
|
1211
|
+
}
|
|
1212
|
+
const inputDenied = appTarget ? appInputPosture.inputAllowed !== true : inputPostureRef.current.denied;
|
|
1213
|
+
const attachmentInputAllowed = frames.attachment !== null && !inputDenied;
|
|
1214
|
+
const inputPosturePending = appTarget ? appInputPosture.pending : inputDenied && (frames.state === "attaching" || frames.state === "reconnecting");
|
|
1215
|
+
const machineLocked = selectedRegistrySession?.failureCode === "machine_locked";
|
|
1216
|
+
const controlUnavailable = computer.controlError !== null;
|
|
1217
|
+
const connectionState = controlUnavailable ? "error" : frames.state;
|
|
1218
|
+
const rfbInputEnabled = rfbStream?.inputAllowed === true && attachmentInputAllowed && !machineLocked && !controlUnavailable && computer.session?.capabilities?.pointerInput === true && computer.session?.capabilities?.keyboardInput === true;
|
|
1219
|
+
const rfbCapability = useMemo2(() => {
|
|
1220
|
+
if (!rfbStream || !frames.attachment) return null;
|
|
1221
|
+
const bounds = computer.selectedTarget?.bounds;
|
|
1222
|
+
return {
|
|
1223
|
+
transport: "vnc-ws",
|
|
1224
|
+
client: "novnc",
|
|
1225
|
+
mode: rfbInputEnabled ? "interactive" : "read-only",
|
|
1226
|
+
url: rfbStream.url,
|
|
1227
|
+
token: null,
|
|
1228
|
+
expiresAt: frames.attachment.expiresAt,
|
|
1229
|
+
resolution: [bounds?.width ?? 1440, bounds?.height ?? 900],
|
|
1230
|
+
unredacted: true,
|
|
1231
|
+
requiresAcknowledgment: false,
|
|
1232
|
+
acknowledged: true,
|
|
1233
|
+
shared: false,
|
|
1234
|
+
sharedSessionIds: [],
|
|
1235
|
+
reason: null
|
|
1236
|
+
};
|
|
1237
|
+
}, [computer.selectedTarget?.bounds, frames.attachment, rfbInputEnabled, rfbStream]);
|
|
1238
|
+
const currentIds = useMemo2(() => new Set(relevant.map((session) => session.id)), [relevant]);
|
|
1239
|
+
useEffect5(() => {
|
|
1240
|
+
for (const intervention of interventions.interventions) {
|
|
1241
|
+
if (seenInterventionIdsRef.current.has(intervention.id)) continue;
|
|
1242
|
+
seenInterventionIdsRef.current.add(intervention.id);
|
|
1243
|
+
onNotify?.({
|
|
1244
|
+
kind: "info",
|
|
1245
|
+
message: `${interventionTitle(intervention)}: ${intervention.reason}`
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
}, [interventions.interventions, onNotify]);
|
|
1249
|
+
const resolveIntervention = useCallback5(
|
|
1250
|
+
(intervention, outcome) => {
|
|
1251
|
+
void interventions.resolve(intervention.id, {
|
|
1252
|
+
expectedVersion: intervention.version,
|
|
1253
|
+
outcome
|
|
1254
|
+
}).then(() => {
|
|
1255
|
+
onNotify?.({
|
|
1256
|
+
kind: "info",
|
|
1257
|
+
message: outcome === "completed" ? "Agent notified. Continuing work." : "Request cancelled."
|
|
1258
|
+
});
|
|
1259
|
+
}).catch((cause) => notifyError(cause, "Could not update the desktop request."));
|
|
1260
|
+
},
|
|
1261
|
+
[interventions, notifyError, onNotify]
|
|
1262
|
+
);
|
|
1263
|
+
const createComputer = useCallback5(() => {
|
|
1264
|
+
if (createInFlightRef.current) return;
|
|
1265
|
+
createInFlightRef.current = true;
|
|
1266
|
+
setCreateError(null);
|
|
1267
|
+
setCreating(true);
|
|
1268
|
+
void createSession({ sessionId, name: "Desktop" }).then((response) => {
|
|
1269
|
+
selectComputerSession({ sessionId: response.session.id, pinned: false });
|
|
1270
|
+
}).catch((cause) => {
|
|
1271
|
+
const error = cause instanceof Error ? cause : new Error("Could not open a desktop.");
|
|
1272
|
+
setCreateError(error);
|
|
1273
|
+
notifyError(error, "Could not open a desktop.");
|
|
1274
|
+
}).finally(() => {
|
|
1275
|
+
createInFlightRef.current = false;
|
|
1276
|
+
setCreating(false);
|
|
1277
|
+
});
|
|
1278
|
+
}, [createSession, notifyError, selectComputerSession, sessionId]);
|
|
1279
|
+
useEffect5(() => {
|
|
1280
|
+
if (!enabled || registry.loading || registry.refreshing || liveRelevant.length > 0 || !connectedPlacementLoss || recreatedPlacementLossRef.current.has(connectedPlacementLoss.id)) {
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
recreatedPlacementLossRef.current.add(connectedPlacementLoss.id);
|
|
1284
|
+
autoCreateSessionRef.current = sessionId;
|
|
1285
|
+
emptyCatalogConfirmationRef.current = null;
|
|
1286
|
+
createComputer();
|
|
1287
|
+
}, [
|
|
1288
|
+
connectedPlacementLoss,
|
|
1289
|
+
createComputer,
|
|
1290
|
+
enabled,
|
|
1291
|
+
liveRelevant.length,
|
|
1292
|
+
registry.loading,
|
|
1293
|
+
registry.refreshing,
|
|
1294
|
+
sessionId
|
|
1295
|
+
]);
|
|
1296
|
+
useEffect5(() => {
|
|
1297
|
+
if (relevant.length > 0) {
|
|
1298
|
+
autoCreateSessionRef.current = sessionId;
|
|
1299
|
+
emptyCatalogConfirmationRef.current = null;
|
|
1300
|
+
}
|
|
1301
|
+
}, [relevant.length, sessionId]);
|
|
1302
|
+
useEffect5(() => {
|
|
1303
|
+
if (!enabled || registry.loading || registry.refreshing || registry.error || relevant.length > 0 || suppressGenericCreate || autoCreateSessionRef.current === sessionId) {
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
if (emptyCatalogConfirmationRef.current !== sessionId) {
|
|
1307
|
+
emptyCatalogConfirmationRef.current = sessionId;
|
|
1308
|
+
void refreshRegistry();
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
autoCreateSessionRef.current = sessionId;
|
|
1312
|
+
createComputer();
|
|
1313
|
+
}, [
|
|
1314
|
+
createComputer,
|
|
1315
|
+
enabled,
|
|
1316
|
+
refreshRegistry,
|
|
1317
|
+
registry.error,
|
|
1318
|
+
registry.loading,
|
|
1319
|
+
registry.refreshing,
|
|
1320
|
+
relevant.length,
|
|
1321
|
+
sessionId,
|
|
1322
|
+
suppressGenericCreate
|
|
1323
|
+
]);
|
|
1324
|
+
const perform = useCallback5(
|
|
1325
|
+
async (action, frame) => {
|
|
1326
|
+
if (computer.controlError) throw computer.controlError;
|
|
1327
|
+
if (inputDenied) throw new Error("Desktop is view only. Refresh to check desktop controls.");
|
|
1328
|
+
let receipt;
|
|
1329
|
+
if (action.type === "pointer") {
|
|
1330
|
+
if (!frame) throw new Error("Desktop view is not ready for pointer input.");
|
|
1331
|
+
receipt = await actFromFrame(action, frame);
|
|
1332
|
+
} else {
|
|
1333
|
+
receipt = await act(action);
|
|
1334
|
+
}
|
|
1335
|
+
if (receipt.state !== "completed") {
|
|
1336
|
+
throw new Error(receipt.error?.message ?? "Desktop input did not complete.");
|
|
1337
|
+
}
|
|
1338
|
+
},
|
|
1339
|
+
[act, actFromFrame, computer.controlError, inputDenied]
|
|
1340
|
+
);
|
|
1341
|
+
const reconnect = () => {
|
|
1342
|
+
void refreshComputer();
|
|
1343
|
+
if (appTarget) appInputPosture.refresh();
|
|
1344
|
+
else frames.reconnect();
|
|
1345
|
+
};
|
|
1346
|
+
const copyFromRfb = useCallback5(
|
|
1347
|
+
(event) => {
|
|
1348
|
+
if (!rfbInputEnabled || !rfbStream || computer.session?.capabilities?.clipboard !== true)
|
|
1349
|
+
return;
|
|
1350
|
+
event.preventDefault();
|
|
1351
|
+
event.stopPropagation();
|
|
1352
|
+
void perform({ type: "clipboard", operation: "copy" }, null).then(() => computer.readClipboard()).then(async (clipboard) => {
|
|
1353
|
+
if (clipboard.text && !await copyTextToClipboard(clipboard.text)) {
|
|
1354
|
+
throw new Error("Desktop text could not be copied to the local clipboard");
|
|
1355
|
+
}
|
|
1356
|
+
}).catch((cause) => notifyError(cause, "Could not copy from the desktop."));
|
|
1357
|
+
},
|
|
1358
|
+
[computer, notifyError, perform, rfbInputEnabled, rfbStream]
|
|
1359
|
+
);
|
|
1360
|
+
const pasteIntoRfb = useCallback5(
|
|
1361
|
+
(text) => {
|
|
1362
|
+
if (!rfbInputEnabled || !rfbStream || computer.session?.capabilities?.clipboard !== true)
|
|
1363
|
+
return false;
|
|
1364
|
+
void (async () => {
|
|
1365
|
+
await perform({ type: "clipboard", operation: "write", text }, null);
|
|
1366
|
+
assertExactComputerClipboard(await computer.readClipboard(), text);
|
|
1367
|
+
await perform({ type: "clipboard", operation: "paste" }, null);
|
|
1368
|
+
})().catch((cause) => notifyError(cause, "Could not paste into the desktop."));
|
|
1369
|
+
return true;
|
|
1370
|
+
},
|
|
1371
|
+
[computer, notifyError, perform, rfbInputEnabled, rfbStream]
|
|
1372
|
+
);
|
|
1373
|
+
const hideGenericCreate = suppressGenericCreate && liveRelevant.length === 0;
|
|
1374
|
+
const generationLossSelected = selectedRegistrySession !== null && isAttachedChromeGenerationLoss(selectedRegistrySession);
|
|
1375
|
+
const generationLossFrames = isAttachedChromeGenerationLossError(frames.error);
|
|
1376
|
+
if (!enabled) return null;
|
|
1377
|
+
if (registry.loading && liveSessions.length === 0) {
|
|
1378
|
+
return /* @__PURE__ */ jsx(
|
|
1379
|
+
ComputerNotice,
|
|
1380
|
+
{
|
|
1381
|
+
icon: /* @__PURE__ */ jsx(LoaderCircleIcon, { className: "size-4 animate-spin" }),
|
|
1382
|
+
text: "Finding workspace desktops\u2026",
|
|
1383
|
+
...className ? { className } : {}
|
|
1384
|
+
}
|
|
1385
|
+
);
|
|
1386
|
+
}
|
|
1387
|
+
if (liveSessions.length === 0) {
|
|
1388
|
+
if (hideGenericCreate) {
|
|
1389
|
+
return /* @__PURE__ */ jsx("div", { className: cn("grid h-full place-items-center bg-og-bg p-6", className), children: /* @__PURE__ */ jsxs("div", { className: "max-w-sm text-center", children: [
|
|
1390
|
+
/* @__PURE__ */ jsx(CircleAlertIcon, { className: "mx-auto size-5 text-og-danger" }),
|
|
1391
|
+
/* @__PURE__ */ jsx("p", { className: "mt-3 text-og-menu font-medium text-og-fg", children: "Chrome reconnected\u2014open a fresh browser/desktop." }),
|
|
1392
|
+
/* @__PURE__ */ jsx("p", { className: "mt-1 text-og-control leading-5 text-og-fg-muted", children: "This Chrome profile is live again, but the previous desktop cannot move to the new connection. Use Browser \u2192 New browser \u2192 Connected Chrome." })
|
|
1393
|
+
] }) });
|
|
1394
|
+
}
|
|
1395
|
+
if (renderEmpty) return renderEmpty(createComputer, creating);
|
|
1396
|
+
const error = createError ?? registry.error;
|
|
1397
|
+
return /* @__PURE__ */ jsx("div", { className: cn("grid h-full place-items-center bg-og-bg p-6", className), children: /* @__PURE__ */ jsxs("div", { className: "max-w-sm text-center", children: [
|
|
1398
|
+
/* @__PURE__ */ jsx("span", { className: "mx-auto grid size-10 place-items-center rounded-og-lg border border-og-border bg-og-surface-1 text-og-fg-muted", children: error ? /* @__PURE__ */ jsx(CircleAlertIcon, { className: "size-5 text-og-danger" }) : /* @__PURE__ */ jsx(LoaderCircleIcon, { className: "size-5 animate-spin" }) }),
|
|
1399
|
+
/* @__PURE__ */ jsx("p", { className: "mt-3 text-og-menu font-medium text-og-fg", children: error ? "Desktop didn\u2019t open" : "Opening desktop\u2026" }),
|
|
1400
|
+
/* @__PURE__ */ jsx("p", { className: "mt-1 text-og-control leading-5 text-og-fg-muted", children: error?.message ?? "Preparing this agent\u2019s desktop. It will appear here when ready." }),
|
|
1401
|
+
error ? /* @__PURE__ */ jsxs(
|
|
1402
|
+
"button",
|
|
1403
|
+
{
|
|
1404
|
+
type: "button",
|
|
1405
|
+
onClick: createError ? createComputer : () => void refreshRegistry(),
|
|
1406
|
+
disabled: creating || registry.refreshing,
|
|
1407
|
+
className: "mt-4 inline-flex h-8 items-center gap-1.5 rounded-og-sm border border-og-primary-border bg-og-primary text-og-primary-fg px-3 text-og-control font-medium transition hover:bg-og-primary-hover disabled:opacity-50",
|
|
1408
|
+
children: [
|
|
1409
|
+
creating || registry.refreshing ? /* @__PURE__ */ jsx(LoaderCircleIcon, { className: "size-3.5 animate-spin" }) : /* @__PURE__ */ jsx(RotateCcwIcon, { className: "size-3.5" }),
|
|
1410
|
+
"Try again"
|
|
1411
|
+
]
|
|
1412
|
+
}
|
|
1413
|
+
) : null
|
|
1414
|
+
] }) });
|
|
1415
|
+
}
|
|
1416
|
+
return /* @__PURE__ */ jsxs(
|
|
1417
|
+
"div",
|
|
1418
|
+
{
|
|
1419
|
+
className: cn(
|
|
1420
|
+
"@container/computer-viewer relative flex h-full min-h-0 min-w-0 flex-col overflow-hidden bg-og-bg",
|
|
1421
|
+
className
|
|
1422
|
+
),
|
|
1423
|
+
children: [
|
|
1424
|
+
/* @__PURE__ */ jsx(
|
|
1425
|
+
ComputerToolbar,
|
|
1426
|
+
{
|
|
1427
|
+
sessions: liveSessions,
|
|
1428
|
+
relevantSessionIds: currentIds,
|
|
1429
|
+
selectedSessionId: selection?.sessionId ?? null,
|
|
1430
|
+
creating,
|
|
1431
|
+
refreshing: registry.refreshing || inputPosturePending,
|
|
1432
|
+
interventionCounts,
|
|
1433
|
+
targets: controllerReady ? computer.targets : [],
|
|
1434
|
+
selectedTargetId: computer.selectedTarget?.id ?? null,
|
|
1435
|
+
targetsLoading: controllerReady && computer.loading,
|
|
1436
|
+
onSelectTarget: (targetId) => {
|
|
1437
|
+
void computer.selectTarget(targetId).catch((cause) => notifyError(cause, "Could not switch desktop views."));
|
|
1438
|
+
},
|
|
1439
|
+
onSelect: (computerSessionId) => selectComputerSession({ sessionId: computerSessionId, pinned: true }),
|
|
1440
|
+
onFollow: () => {
|
|
1441
|
+
if (relevant[0]) selectComputerSession({ sessionId: relevant[0].id, pinned: false });
|
|
1442
|
+
},
|
|
1443
|
+
onCreate: hideGenericCreate ? void 0 : createComputer,
|
|
1444
|
+
onRefresh: () => {
|
|
1445
|
+
if (appTarget) {
|
|
1446
|
+
appInputPosture.refresh();
|
|
1447
|
+
} else if ((connectionState === "error" || inputDenied) && !generationLossSelected && !generationLossFrames) {
|
|
1448
|
+
frames.reconnect();
|
|
1449
|
+
}
|
|
1450
|
+
void Promise.all([refreshRegistry(), refreshComputer()]);
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
),
|
|
1454
|
+
/* @__PURE__ */ jsx(
|
|
1455
|
+
InteractionInterventionBanner,
|
|
1456
|
+
{
|
|
1457
|
+
interventions: selectedInterventions,
|
|
1458
|
+
activeTargetId: computer.selectedTarget?.id ?? null,
|
|
1459
|
+
mutating: interventions.mutating,
|
|
1460
|
+
onOpen: (intervention) => void computer.selectTarget(intervention.targetId).catch((cause) => notifyError(cause, "Could not open the requested desktop view.")),
|
|
1461
|
+
onResolve: resolveIntervention
|
|
1462
|
+
}
|
|
1463
|
+
),
|
|
1464
|
+
!selectedRegistrySession ? /* @__PURE__ */ jsx(
|
|
1465
|
+
ComputerUnselectedPanel,
|
|
1466
|
+
{
|
|
1467
|
+
peerCount: liveSessions.length,
|
|
1468
|
+
creating,
|
|
1469
|
+
error: createError,
|
|
1470
|
+
generationLoss: suppressGenericCreate,
|
|
1471
|
+
onCreate: createComputer
|
|
1472
|
+
}
|
|
1473
|
+
) : !controllerReady ? /* @__PURE__ */ jsx(
|
|
1474
|
+
ComputerLifecyclePanel,
|
|
1475
|
+
{
|
|
1476
|
+
session: selectedRegistrySession,
|
|
1477
|
+
creating,
|
|
1478
|
+
onRefresh: registry.refresh,
|
|
1479
|
+
onRetry: createComputer
|
|
1480
|
+
}
|
|
1481
|
+
) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1482
|
+
/* @__PURE__ */ jsxs("div", { className: "flex min-h-0 flex-1 flex-col @xl/computer-viewer:flex-row", children: [
|
|
1483
|
+
rfbStream ? /* @__PURE__ */ jsxs("div", { className: "relative min-h-0 flex-1 bg-black", onCopyCapture: copyFromRfb, children: [
|
|
1484
|
+
/* @__PURE__ */ jsx(
|
|
1485
|
+
DesktopViewer,
|
|
1486
|
+
{
|
|
1487
|
+
capability: rfbCapability,
|
|
1488
|
+
interactive: rfbInputEnabled,
|
|
1489
|
+
showControlToggle: false,
|
|
1490
|
+
webSocketProtocols: rfbStream.protocols,
|
|
1491
|
+
onPasteText: pasteIntoRfb,
|
|
1492
|
+
targetPlatform: computer.session?.platform ?? null,
|
|
1493
|
+
className: "h-full"
|
|
1494
|
+
}
|
|
1495
|
+
),
|
|
1496
|
+
!rfbInputEnabled ? /* @__PURE__ */ jsx("div", { className: "pointer-events-none absolute bottom-3 left-1/2 -translate-x-1/2 rounded-full border border-white/10 bg-black/70 px-3 py-1 text-[11px] text-white/80 backdrop-blur", children: controlUnavailable ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1497
|
+
"Desktop controls unavailable",
|
|
1498
|
+
" ",
|
|
1499
|
+
/* @__PURE__ */ jsx(
|
|
1500
|
+
"button",
|
|
1501
|
+
{
|
|
1502
|
+
type: "button",
|
|
1503
|
+
onClick: reconnect,
|
|
1504
|
+
className: "pointer-events-auto ml-2 font-medium underline",
|
|
1505
|
+
children: "Reconnect"
|
|
1506
|
+
}
|
|
1507
|
+
)
|
|
1508
|
+
] }) : "View only \xB7 direct input unavailable" }) : null
|
|
1509
|
+
] }) : /* @__PURE__ */ jsx(
|
|
1510
|
+
ComputerViewport,
|
|
1511
|
+
{
|
|
1512
|
+
frame: machineLocked ? null : displayedFrame,
|
|
1513
|
+
observation: computer.observation,
|
|
1514
|
+
target: computer.selectedTarget,
|
|
1515
|
+
machineLocked,
|
|
1516
|
+
controlUnavailable,
|
|
1517
|
+
inputAllowed: !inputDenied,
|
|
1518
|
+
connectionState,
|
|
1519
|
+
connectionError: computer.controlError ?? appInputPosture.error ?? frames.error ?? computer.error,
|
|
1520
|
+
mutating: computer.mutating,
|
|
1521
|
+
backgroundActions: computer.session?.capabilities?.backgroundActions === true,
|
|
1522
|
+
backgroundInput: computer.session?.capabilities?.backgroundInput === true,
|
|
1523
|
+
clipboardEnabled: attachmentInputAllowed && computer.session?.capabilities?.clipboard === true,
|
|
1524
|
+
pointerInput: attachmentInputAllowed && computer.session?.capabilities?.pointerInput === true,
|
|
1525
|
+
keyboardInput: attachmentInputAllowed && computer.session?.capabilities?.keyboardInput === true,
|
|
1526
|
+
onAction: perform,
|
|
1527
|
+
onReadClipboard: computer.readClipboard,
|
|
1528
|
+
onReconnect: generationLossSelected || generationLossFrames ? () => void 0 : reconnect,
|
|
1529
|
+
onError: (cause) => notifyError(cause, "Desktop input failed.")
|
|
1530
|
+
},
|
|
1531
|
+
JSON.stringify([
|
|
1532
|
+
selection?.sessionId,
|
|
1533
|
+
computer.selectedTarget?.id,
|
|
1534
|
+
computer.selectedTarget?.targetGeneration,
|
|
1535
|
+
frames.attachment?.controllerGeneration
|
|
1536
|
+
])
|
|
1537
|
+
),
|
|
1538
|
+
showControls ? /* @__PURE__ */ jsx(
|
|
1539
|
+
ComputerSemanticPanel,
|
|
1540
|
+
{
|
|
1541
|
+
observation: computer.observation,
|
|
1542
|
+
mutating: computer.mutating || controlUnavailable || inputDenied,
|
|
1543
|
+
controlUnavailable,
|
|
1544
|
+
inputAllowed: !inputDenied,
|
|
1545
|
+
onAction: (action) => void perform(action, null).catch(
|
|
1546
|
+
(cause) => notifyError(cause, "Desktop action failed.")
|
|
1547
|
+
)
|
|
1548
|
+
}
|
|
1549
|
+
) : null
|
|
1550
|
+
] }),
|
|
1551
|
+
/* @__PURE__ */ jsx(
|
|
1552
|
+
ComputerStatusBar,
|
|
1553
|
+
{
|
|
1554
|
+
session: computer.session,
|
|
1555
|
+
target: computer.selectedTarget,
|
|
1556
|
+
connectionState,
|
|
1557
|
+
controlUnavailable,
|
|
1558
|
+
inputAllowed: !inputDenied,
|
|
1559
|
+
refreshing: registry.refreshing,
|
|
1560
|
+
showControls,
|
|
1561
|
+
controlCount: semanticControls(computer.observation).length,
|
|
1562
|
+
onToggleControls: () => setShowControls((current) => !current)
|
|
1563
|
+
}
|
|
1564
|
+
)
|
|
1565
|
+
] })
|
|
1566
|
+
]
|
|
1567
|
+
}
|
|
1568
|
+
);
|
|
1569
|
+
}
|
|
1570
|
+
function ComputerUnselectedPanel(props) {
|
|
1571
|
+
return /* @__PURE__ */ jsx("div", { className: "grid min-h-0 flex-1 place-items-center bg-og-bg p-6 text-center", children: /* @__PURE__ */ jsxs("div", { className: "max-w-sm", children: [
|
|
1572
|
+
/* @__PURE__ */ jsx("span", { className: "mx-auto grid size-10 place-items-center rounded-og-md border border-og-border bg-og-surface-1 text-og-fg-muted", children: props.error || props.generationLoss ? /* @__PURE__ */ jsx(CircleAlertIcon, { className: "size-4.5 text-og-danger" }) : /* @__PURE__ */ jsx(LoaderCircleIcon, { className: "size-4.5 animate-spin" }) }),
|
|
1573
|
+
/* @__PURE__ */ jsx("p", { className: "mt-3 text-og-menu font-medium text-og-fg", children: props.generationLoss ? "Chrome reconnected\u2014open a fresh browser/desktop." : props.error ? "Desktop didn\u2019t open" : "Opening desktop\u2026" }),
|
|
1574
|
+
/* @__PURE__ */ jsx("p", { className: "mt-1 text-og-control leading-5 text-og-fg-muted", children: props.generationLoss ? "This Chrome profile is live again, but the previous desktop cannot move to the new connection. Use Browser \u2192 New browser \u2192 Connected Chrome." : props.error?.message ?? (props.peerCount === 1 ? "Preparing this agent\u2019s desktop. One peer desktop remains available from the menu." : `Preparing this agent\u2019s desktop. ${props.peerCount} peer desktops remain available from the menu.`) }),
|
|
1575
|
+
props.error && !props.generationLoss ? /* @__PURE__ */ jsxs(
|
|
1576
|
+
"button",
|
|
1577
|
+
{
|
|
1578
|
+
type: "button",
|
|
1579
|
+
disabled: props.creating,
|
|
1580
|
+
onClick: props.onCreate,
|
|
1581
|
+
className: "mt-4 inline-flex h-8 items-center gap-1.5 rounded-og-sm border border-og-border bg-og-surface-1 px-3 text-og-control font-medium text-og-fg transition hover:bg-og-surface-2 disabled:cursor-not-allowed disabled:opacity-50",
|
|
1582
|
+
children: [
|
|
1583
|
+
props.creating ? /* @__PURE__ */ jsx(LoaderCircleIcon, { className: "size-3.5 animate-spin" }) : /* @__PURE__ */ jsx(RotateCcwIcon, { className: "size-3.5" }),
|
|
1584
|
+
"Try again"
|
|
1585
|
+
]
|
|
1586
|
+
}
|
|
1587
|
+
) : null
|
|
1588
|
+
] }) });
|
|
1589
|
+
}
|
|
1590
|
+
function ComputerToolbar(props) {
|
|
1591
|
+
const detailsRef = useViewerMenuDismiss();
|
|
1592
|
+
const selected = props.sessions.find((session) => session.id === props.selectedSessionId);
|
|
1593
|
+
const current = props.sessions.filter((session) => props.relevantSessionIds.has(session.id));
|
|
1594
|
+
const others = props.sessions.filter((session) => !props.relevantSessionIds.has(session.id));
|
|
1595
|
+
const closeMenu = (restoreFocus) => {
|
|
1596
|
+
detailsRef.current?.removeAttribute("open");
|
|
1597
|
+
if (restoreFocus) detailsRef.current?.querySelector("summary")?.focus();
|
|
1598
|
+
};
|
|
1599
|
+
const choose = (id, restoreFocus) => {
|
|
1600
|
+
props.onSelect(id);
|
|
1601
|
+
closeMenu(restoreFocus);
|
|
1602
|
+
};
|
|
1603
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex h-10 shrink-0 items-center gap-1.5 border-b border-og-border bg-og-surface-1 px-2", children: [
|
|
1604
|
+
/* @__PURE__ */ jsxs("details", { ref: detailsRef, className: "min-w-0", children: [
|
|
1605
|
+
/* @__PURE__ */ jsxs("summary", { className: "flex h-7 max-w-32 cursor-pointer list-none items-center gap-2 rounded-og-sm px-2 text-og-control text-og-fg transition hover:bg-og-surface-2 @sm/computer-viewer:max-w-52 [&::-webkit-details-marker]:hidden", children: [
|
|
1606
|
+
/* @__PURE__ */ jsx(MonitorIcon, { className: "size-3.5 shrink-0 text-og-fg-muted" }),
|
|
1607
|
+
/* @__PURE__ */ jsx("span", { className: "truncate font-medium", children: selected?.name === "Computer" ? "Desktop" : selected?.name ?? "Desktop" }),
|
|
1608
|
+
(props.interventionCounts.get(selected?.id ?? "") ?? 0) > 0 ? /* @__PURE__ */ jsx("span", { className: "size-1.5 shrink-0 rounded-full bg-og-status-waiting" }) : null,
|
|
1609
|
+
/* @__PURE__ */ jsx(ChevronDownIcon, { className: "size-3 shrink-0 text-og-fg-subtle" })
|
|
1610
|
+
] }),
|
|
1611
|
+
/* @__PURE__ */ jsxs("div", { className: "absolute left-2 top-10 z-30 flex max-h-[calc(100%-3rem)] w-72 max-w-[calc(100%-1rem)] flex-col overflow-hidden rounded-og-md border border-og-border bg-og-surface-1 p-1 shadow-xl", children: [
|
|
1612
|
+
/* @__PURE__ */ jsxs("div", { className: "min-h-0 max-h-96 overflow-y-auto overscroll-contain", children: [
|
|
1613
|
+
/* @__PURE__ */ jsx(
|
|
1614
|
+
ComputerSessionGroup,
|
|
1615
|
+
{
|
|
1616
|
+
label: "Current agent",
|
|
1617
|
+
sessions: current,
|
|
1618
|
+
selectedId: props.selectedSessionId,
|
|
1619
|
+
interventionCounts: props.interventionCounts,
|
|
1620
|
+
onSelect: choose
|
|
1621
|
+
}
|
|
1622
|
+
),
|
|
1623
|
+
/* @__PURE__ */ jsx(
|
|
1624
|
+
ComputerSessionGroup,
|
|
1625
|
+
{
|
|
1626
|
+
label: "Other agents",
|
|
1627
|
+
sessions: others,
|
|
1628
|
+
selectedId: props.selectedSessionId,
|
|
1629
|
+
interventionCounts: props.interventionCounts,
|
|
1630
|
+
onSelect: choose
|
|
1631
|
+
}
|
|
1632
|
+
)
|
|
1633
|
+
] }),
|
|
1634
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-1 flex shrink-0 gap-1 border-t border-og-border pt-1", children: [
|
|
1635
|
+
current.length > 0 ? /* @__PURE__ */ jsx(
|
|
1636
|
+
MenuButton,
|
|
1637
|
+
{
|
|
1638
|
+
onClick: (event) => {
|
|
1639
|
+
props.onFollow();
|
|
1640
|
+
closeMenu(event.detail === 0);
|
|
1641
|
+
},
|
|
1642
|
+
children: "Follow agent"
|
|
1643
|
+
}
|
|
1644
|
+
) : null,
|
|
1645
|
+
props.onCreate ? /* @__PURE__ */ jsxs(
|
|
1646
|
+
MenuButton,
|
|
1647
|
+
{
|
|
1648
|
+
onClick: (event) => {
|
|
1649
|
+
props.onCreate?.();
|
|
1650
|
+
closeMenu(event.detail === 0);
|
|
1651
|
+
},
|
|
1652
|
+
disabled: props.creating,
|
|
1653
|
+
children: [
|
|
1654
|
+
/* @__PURE__ */ jsx(PlusIcon, { className: "size-3.5" }),
|
|
1655
|
+
" New desktop"
|
|
1656
|
+
]
|
|
1657
|
+
}
|
|
1658
|
+
) : null
|
|
1659
|
+
] })
|
|
1660
|
+
] })
|
|
1661
|
+
] }),
|
|
1662
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-og-xs text-og-fg-subtle", children: selected ? `${platformLabel(selected)} \xB7 ${placementLabel(selected)}` : "" }),
|
|
1663
|
+
/* @__PURE__ */ jsx(
|
|
1664
|
+
ComputerTargetPicker,
|
|
1665
|
+
{
|
|
1666
|
+
targets: props.targets,
|
|
1667
|
+
selectedTargetId: props.selectedTargetId,
|
|
1668
|
+
loading: props.targetsLoading,
|
|
1669
|
+
onSelect: props.onSelectTarget
|
|
1670
|
+
}
|
|
1671
|
+
),
|
|
1672
|
+
/* @__PURE__ */ jsx(
|
|
1673
|
+
"button",
|
|
1674
|
+
{
|
|
1675
|
+
type: "button",
|
|
1676
|
+
onClick: props.onRefresh,
|
|
1677
|
+
disabled: props.refreshing,
|
|
1678
|
+
"aria-busy": props.refreshing,
|
|
1679
|
+
className: "grid size-7 place-items-center rounded-og-sm text-og-fg-muted transition hover:bg-og-surface-2 hover:text-og-fg",
|
|
1680
|
+
"aria-label": "Refresh desktops",
|
|
1681
|
+
children: /* @__PURE__ */ jsx(RefreshCwIcon, { className: cn("size-3.5", props.refreshing && "animate-spin") })
|
|
1682
|
+
}
|
|
1683
|
+
),
|
|
1684
|
+
props.onCreate ? /* @__PURE__ */ jsx(
|
|
1685
|
+
"button",
|
|
1686
|
+
{
|
|
1687
|
+
type: "button",
|
|
1688
|
+
onClick: props.onCreate,
|
|
1689
|
+
disabled: props.creating,
|
|
1690
|
+
className: "hidden size-7 place-items-center rounded-og-sm text-og-fg-muted transition hover:bg-og-surface-2 hover:text-og-fg disabled:opacity-40 @sm/computer-viewer:grid",
|
|
1691
|
+
"aria-label": "Open a new desktop",
|
|
1692
|
+
children: props.creating ? /* @__PURE__ */ jsx(LoaderCircleIcon, { className: "size-3.5 animate-spin" }) : /* @__PURE__ */ jsx(PlusIcon, { className: "size-3.5" })
|
|
1693
|
+
}
|
|
1694
|
+
) : null
|
|
1695
|
+
] });
|
|
1696
|
+
}
|
|
1697
|
+
function ComputerSessionGroup(props) {
|
|
1698
|
+
if (props.sessions.length === 0) return null;
|
|
1699
|
+
return /* @__PURE__ */ jsxs("div", { className: "py-1", children: [
|
|
1700
|
+
/* @__PURE__ */ jsx("p", { className: "px-2 pb-1 text-[10px] font-medium uppercase tracking-[0.12em] text-og-fg-subtle", children: props.label }),
|
|
1701
|
+
props.sessions.map((session) => {
|
|
1702
|
+
const interventionCount = props.interventionCounts.get(session.id) ?? 0;
|
|
1703
|
+
return /* @__PURE__ */ jsxs(
|
|
1704
|
+
"button",
|
|
1705
|
+
{
|
|
1706
|
+
type: "button",
|
|
1707
|
+
onClick: (event) => props.onSelect(session.id, event.detail === 0),
|
|
1708
|
+
"aria-pressed": session.id === props.selectedId,
|
|
1709
|
+
className: cn(
|
|
1710
|
+
"flex w-full items-center gap-2 rounded-og-sm px-2 py-1.5 text-left transition hover:bg-og-surface-2",
|
|
1711
|
+
session.id === props.selectedId && "bg-og-surface-2"
|
|
1712
|
+
),
|
|
1713
|
+
children: [
|
|
1714
|
+
/* @__PURE__ */ jsx(
|
|
1715
|
+
"span",
|
|
1716
|
+
{
|
|
1717
|
+
className: cn(
|
|
1718
|
+
"size-1.5 rounded-full",
|
|
1719
|
+
session.lifecycle === "active" ? "bg-og-status-running" : "bg-og-muted"
|
|
1720
|
+
)
|
|
1721
|
+
}
|
|
1722
|
+
),
|
|
1723
|
+
/* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
|
|
1724
|
+
/* @__PURE__ */ jsx("span", { className: "block truncate text-og-control text-og-fg", children: session.name }),
|
|
1725
|
+
/* @__PURE__ */ jsxs("span", { className: "block truncate text-og-xs text-og-fg-subtle", children: [
|
|
1726
|
+
platformLabel(session),
|
|
1727
|
+
" \xB7 ",
|
|
1728
|
+
placementLabel(session)
|
|
1729
|
+
] })
|
|
1730
|
+
] }),
|
|
1731
|
+
interventionCount > 0 ? /* @__PURE__ */ jsx("span", { className: "rounded-full bg-og-status-waiting/10 px-1.5 py-0.5 text-[10px] font-medium text-og-status-waiting", children: interventionCount }) : null
|
|
1732
|
+
]
|
|
1733
|
+
},
|
|
1734
|
+
session.id
|
|
1735
|
+
);
|
|
1736
|
+
})
|
|
1737
|
+
] });
|
|
1738
|
+
}
|
|
1739
|
+
function MenuButton(props) {
|
|
1740
|
+
return /* @__PURE__ */ jsx(
|
|
1741
|
+
"button",
|
|
1742
|
+
{
|
|
1743
|
+
type: "button",
|
|
1744
|
+
onClick: props.onClick,
|
|
1745
|
+
disabled: props.disabled,
|
|
1746
|
+
className: "flex h-7 flex-1 items-center justify-center gap-1 rounded-og-sm px-2 text-og-control text-og-fg-muted transition hover:bg-og-surface-2 hover:text-og-fg disabled:opacity-50",
|
|
1747
|
+
children: props.children
|
|
1748
|
+
}
|
|
1749
|
+
);
|
|
1750
|
+
}
|
|
1751
|
+
function ComputerTargetPicker(props) {
|
|
1752
|
+
const screens = props.targets.filter((target) => target.kind === "screen");
|
|
1753
|
+
const advancedTargets = props.targets.filter(
|
|
1754
|
+
(target) => target.kind === "app" || target.kind === "window" && isRenderableComputerView(target)
|
|
1755
|
+
);
|
|
1756
|
+
const advancedRef = useViewerMenuDismiss(advancedTargets.length > 0);
|
|
1757
|
+
const selected = props.targets.find((target) => target.id === props.selectedTargetId);
|
|
1758
|
+
const choose = (id, restoreFocus = false) => {
|
|
1759
|
+
props.onSelect(id);
|
|
1760
|
+
if (advancedRef.current?.open) {
|
|
1761
|
+
advancedRef.current.removeAttribute("open");
|
|
1762
|
+
if (restoreFocus) advancedRef.current.querySelector("summary")?.focus();
|
|
1763
|
+
}
|
|
1764
|
+
};
|
|
1765
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1766
|
+
screens.length > 1 ? /* @__PURE__ */ jsxs(
|
|
1767
|
+
"select",
|
|
1768
|
+
{
|
|
1769
|
+
"aria-label": "Screen",
|
|
1770
|
+
value: selected?.kind === "screen" ? selected.id : "",
|
|
1771
|
+
onChange: (event) => choose(event.currentTarget.value),
|
|
1772
|
+
className: "h-7 min-w-0 max-w-28 rounded-og-sm border border-og-border bg-og-surface-1 px-1.5 text-og-control text-og-fg outline-hidden focus:border-og-accent @sm/computer-viewer:max-w-40",
|
|
1773
|
+
children: [
|
|
1774
|
+
/* @__PURE__ */ jsx("option", { value: "", disabled: true, hidden: true, children: "Screen" }),
|
|
1775
|
+
screens.map((screen, index) => /* @__PURE__ */ jsx("option", { value: screen.id, children: screen.title || `Screen ${index + 1}` }, screen.id))
|
|
1776
|
+
]
|
|
1777
|
+
}
|
|
1778
|
+
) : screens[0] && selected?.kind !== "screen" ? /* @__PURE__ */ jsxs(
|
|
1779
|
+
"button",
|
|
1780
|
+
{
|
|
1781
|
+
type: "button",
|
|
1782
|
+
onClick: () => choose(screens[0].id),
|
|
1783
|
+
className: "flex h-7 shrink-0 items-center gap-1.5 rounded-og-sm px-2 text-og-control text-og-fg-muted transition hover:bg-og-surface-2 hover:text-og-fg",
|
|
1784
|
+
children: [
|
|
1785
|
+
/* @__PURE__ */ jsx(MonitorIcon, { className: "size-3.5" }),
|
|
1786
|
+
" Full desktop"
|
|
1787
|
+
]
|
|
1788
|
+
}
|
|
1789
|
+
) : null,
|
|
1790
|
+
advancedTargets.length > 0 ? /* @__PURE__ */ jsxs("details", { ref: advancedRef, className: "shrink-0", children: [
|
|
1791
|
+
/* @__PURE__ */ jsxs(
|
|
1792
|
+
"summary",
|
|
1793
|
+
{
|
|
1794
|
+
"aria-label": "Advanced desktop views",
|
|
1795
|
+
title: "Advanced desktop views",
|
|
1796
|
+
className: "flex h-7 cursor-pointer list-none items-center gap-1.5 rounded-og-sm px-2 text-og-control text-og-fg-muted transition hover:bg-og-surface-2 hover:text-og-fg [&::-webkit-details-marker]:hidden",
|
|
1797
|
+
children: [
|
|
1798
|
+
/* @__PURE__ */ jsx(PanelsTopLeftIcon, { className: "size-3.5" }),
|
|
1799
|
+
/* @__PURE__ */ jsx("span", { className: "hidden @sm/computer-viewer:inline", children: "Advanced" }),
|
|
1800
|
+
/* @__PURE__ */ jsx(ChevronDownIcon, { className: "size-3" })
|
|
1801
|
+
]
|
|
1802
|
+
}
|
|
1803
|
+
),
|
|
1804
|
+
/* @__PURE__ */ jsxs("div", { className: "absolute right-2 top-10 z-30 flex max-h-[calc(100%-3rem)] w-64 max-w-[calc(100%-1rem)] flex-col overflow-hidden rounded-og-md border border-og-border bg-og-surface-1 p-1 shadow-xl", children: [
|
|
1805
|
+
/* @__PURE__ */ jsx("p", { className: "shrink-0 px-2 py-1 text-og-xs text-og-fg-subtle", children: "Apps and windows" }),
|
|
1806
|
+
/* @__PURE__ */ jsx("div", { className: "min-h-0 max-h-80 overflow-y-auto overscroll-contain", children: advancedTargets.map((target) => /* @__PURE__ */ jsxs(
|
|
1807
|
+
"button",
|
|
1808
|
+
{
|
|
1809
|
+
type: "button",
|
|
1810
|
+
onClick: (event) => choose(target.id, event.detail === 0),
|
|
1811
|
+
"aria-pressed": target.id === props.selectedTargetId,
|
|
1812
|
+
className: cn(
|
|
1813
|
+
"flex w-full min-w-0 items-center gap-2 rounded-og-sm px-2 py-1.5 text-left transition hover:bg-og-surface-2",
|
|
1814
|
+
target.id === props.selectedTargetId && "bg-og-surface-2"
|
|
1815
|
+
),
|
|
1816
|
+
children: [
|
|
1817
|
+
/* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
|
|
1818
|
+
/* @__PURE__ */ jsx("span", { className: "block truncate text-og-control text-og-fg", children: target.title || target.applicationId || target.kind }),
|
|
1819
|
+
/* @__PURE__ */ jsx("span", { className: "block text-og-xs text-og-fg-subtle", children: target.kind === "app" ? "App controls" : "Window view" })
|
|
1820
|
+
] }),
|
|
1821
|
+
target.focused ? /* @__PURE__ */ jsx(
|
|
1822
|
+
"span",
|
|
1823
|
+
{
|
|
1824
|
+
className: "size-1.5 shrink-0 rounded-full bg-og-status-running",
|
|
1825
|
+
"aria-label": "Focused"
|
|
1826
|
+
}
|
|
1827
|
+
) : null
|
|
1828
|
+
]
|
|
1829
|
+
},
|
|
1830
|
+
target.id
|
|
1831
|
+
)) })
|
|
1832
|
+
] })
|
|
1833
|
+
] }) : null,
|
|
1834
|
+
props.loading ? /* @__PURE__ */ jsx(LoaderCircleIcon, { className: "size-3.5 shrink-0 animate-spin text-og-fg-muted" }) : null
|
|
1835
|
+
] });
|
|
1836
|
+
}
|
|
1837
|
+
function isRenderableComputerView(target) {
|
|
1838
|
+
if (target.kind === "screen") return true;
|
|
1839
|
+
if (target.kind !== "window" || !target.bounds) return false;
|
|
1840
|
+
return target.bounds.width >= 160 && target.bounds.height >= 90;
|
|
1841
|
+
}
|
|
1842
|
+
function isComputerFrameTarget(target) {
|
|
1843
|
+
return target.kind === "window" || target.kind === "screen";
|
|
1844
|
+
}
|
|
1845
|
+
function ComputerLifecyclePanel(props) {
|
|
1846
|
+
const generationLoss = isAttachedChromeGenerationLoss(props.session);
|
|
1847
|
+
const failed = ["failed", "lost", "repair_required"].includes(props.session.lifecycle);
|
|
1848
|
+
const retryCreatesSession = ["failed", "lost"].includes(props.session.lifecycle);
|
|
1849
|
+
return /* @__PURE__ */ jsx("div", { className: "grid min-h-0 flex-1 place-items-center bg-og-bg p-6", children: /* @__PURE__ */ jsxs("div", { className: "max-w-sm text-center", children: [
|
|
1850
|
+
failed ? /* @__PURE__ */ jsx(CircleAlertIcon, { className: "mx-auto size-5 text-og-danger" }) : /* @__PURE__ */ jsx(LoaderCircleIcon, { className: "mx-auto size-5 animate-spin text-og-fg-muted" }),
|
|
1851
|
+
/* @__PURE__ */ jsx("p", { className: "mt-3 text-og-menu font-medium text-og-fg", children: generationLoss ? "Chrome reconnected\u2014open a fresh browser/desktop." : failed ? "Desktop needs attention" : lifecycleLabel(props.session.lifecycle) }),
|
|
1852
|
+
/* @__PURE__ */ jsx("p", { className: "mt-1 text-og-control leading-5 text-og-fg-muted", children: generationLoss ? "This Chrome profile is live again, but the previous desktop cannot move to the new connection. Use Browser \u2192 New browser \u2192 Connected Chrome." : computerFailureMessage(props.session) ?? "The desktop is being prepared on its placement. It will appear here when ready." }),
|
|
1853
|
+
failed && !generationLoss ? /* @__PURE__ */ jsxs(
|
|
1854
|
+
"button",
|
|
1855
|
+
{
|
|
1856
|
+
type: "button",
|
|
1857
|
+
onClick: () => retryCreatesSession ? props.onRetry() : void props.onRefresh(),
|
|
1858
|
+
disabled: props.creating,
|
|
1859
|
+
className: "mt-4 inline-flex h-8 items-center gap-1.5 rounded-og-sm border border-og-border bg-og-surface-1 px-3 text-og-control font-medium text-og-fg transition hover:bg-og-surface-2",
|
|
1860
|
+
children: [
|
|
1861
|
+
props.creating ? /* @__PURE__ */ jsx(LoaderCircleIcon, { className: "size-3.5 animate-spin" }) : /* @__PURE__ */ jsx(RotateCcwIcon, { className: "size-3.5" }),
|
|
1862
|
+
retryCreatesSession ? "Try again" : "Check again"
|
|
1863
|
+
]
|
|
1864
|
+
}
|
|
1865
|
+
) : null
|
|
1866
|
+
] }) });
|
|
1867
|
+
}
|
|
1868
|
+
function ComputerViewport(props) {
|
|
1869
|
+
const canvasRef = useRef5(null);
|
|
1870
|
+
const inputRef = useRef5(null);
|
|
1871
|
+
const composingRef = useRef5(false);
|
|
1872
|
+
const pointerStartRef = useRef5(null);
|
|
1873
|
+
const clickTimerRef = useRef5(null);
|
|
1874
|
+
const lastClickRef = useRef5(null);
|
|
1875
|
+
const wheelRef = useRef5(null);
|
|
1876
|
+
const pendingTextRef = useRef5(null);
|
|
1877
|
+
const actionRef = useRef5(props.onAction);
|
|
1878
|
+
const readClipboardRef = useRef5(props.onReadClipboard);
|
|
1879
|
+
const errorRef = useRef5(props.onError);
|
|
1880
|
+
const actionTailRef = useRef5(Promise.resolve());
|
|
1881
|
+
const actionQueueEpochRef = useRef5(0);
|
|
1882
|
+
const queuedFrameRef = useRef5(null);
|
|
1883
|
+
const currentFrameRef = useRef5(props.frame);
|
|
1884
|
+
const paintedFrameRef = useRef5(null);
|
|
1885
|
+
const [paintedFrame, setPaintedFrame] = useState5(null);
|
|
1886
|
+
const decodingFrameRef = useRef5(false);
|
|
1887
|
+
const mountedRef = useRef5(true);
|
|
1888
|
+
actionRef.current = props.onAction;
|
|
1889
|
+
readClipboardRef.current = props.onReadClipboard;
|
|
1890
|
+
errorRef.current = props.onError;
|
|
1891
|
+
const streamFailed = props.connectionState === "error";
|
|
1892
|
+
const rawInputEnabled = props.target?.kind !== "app" && !streamFailed && !props.controlUnavailable && !props.machineLocked && (props.backgroundInput || !props.backgroundActions || props.target?.kind === "screen" || props.target?.focused === true);
|
|
1893
|
+
const clearBufferedInput = useCallback5(() => {
|
|
1894
|
+
if (clickTimerRef.current) clearTimeout(clickTimerRef.current);
|
|
1895
|
+
if (wheelRef.current?.timer) clearTimeout(wheelRef.current.timer);
|
|
1896
|
+
if (pendingTextRef.current?.timer) clearTimeout(pendingTextRef.current.timer);
|
|
1897
|
+
clickTimerRef.current = null;
|
|
1898
|
+
wheelRef.current = null;
|
|
1899
|
+
pendingTextRef.current = null;
|
|
1900
|
+
pointerStartRef.current = null;
|
|
1901
|
+
lastClickRef.current = null;
|
|
1902
|
+
composingRef.current = false;
|
|
1903
|
+
if (inputRef.current) inputRef.current.value = "";
|
|
1904
|
+
}, []);
|
|
1905
|
+
const pointerInputEnabled = rawInputEnabled && props.pointerInput;
|
|
1906
|
+
const keyboardInputEnabled = rawInputEnabled && props.keyboardInput;
|
|
1907
|
+
useLayoutEffect(() => {
|
|
1908
|
+
const canvas = canvasRef.current;
|
|
1909
|
+
const viewport = canvas?.parentElement;
|
|
1910
|
+
if (!canvas || !viewport) return;
|
|
1911
|
+
const fit = () => fitComputerCanvas(canvas);
|
|
1912
|
+
fit();
|
|
1913
|
+
if (typeof ResizeObserver === "undefined") {
|
|
1914
|
+
window.addEventListener("resize", fit);
|
|
1915
|
+
return () => window.removeEventListener("resize", fit);
|
|
1916
|
+
}
|
|
1917
|
+
const observer = new ResizeObserver(fit);
|
|
1918
|
+
observer.observe(viewport);
|
|
1919
|
+
return () => observer.disconnect();
|
|
1920
|
+
}, []);
|
|
1921
|
+
const paintQueuedFrames = useCallback5(() => {
|
|
1922
|
+
if (decodingFrameRef.current) return;
|
|
1923
|
+
decodingFrameRef.current = true;
|
|
1924
|
+
void (async () => {
|
|
1925
|
+
try {
|
|
1926
|
+
while (mountedRef.current) {
|
|
1927
|
+
const frame = queuedFrameRef.current;
|
|
1928
|
+
queuedFrameRef.current = null;
|
|
1929
|
+
if (!frame) break;
|
|
1930
|
+
let objectUrl = null;
|
|
1931
|
+
try {
|
|
1932
|
+
const blob = new Blob([frame.data.slice().buffer], { type: frame.mediaType });
|
|
1933
|
+
if (typeof createImageBitmap === "function") {
|
|
1934
|
+
const bitmap = await createImageBitmap(blob);
|
|
1935
|
+
try {
|
|
1936
|
+
const canvas2 = canvasRef.current;
|
|
1937
|
+
if (mountedRef.current && canvas2 && currentFrameRef.current && sameComputerTarget(frame, currentFrameRef.current)) {
|
|
1938
|
+
paintCanvas(canvas2, bitmap, frame.width, frame.height);
|
|
1939
|
+
paintedFrameRef.current = frame;
|
|
1940
|
+
setPaintedFrame(frame);
|
|
1941
|
+
}
|
|
1942
|
+
} finally {
|
|
1943
|
+
bitmap.close();
|
|
1944
|
+
}
|
|
1945
|
+
continue;
|
|
1946
|
+
}
|
|
1947
|
+
objectUrl = URL.createObjectURL(blob);
|
|
1948
|
+
const image = await loadImage(objectUrl);
|
|
1949
|
+
const canvas = canvasRef.current;
|
|
1950
|
+
if (mountedRef.current && canvas && currentFrameRef.current && sameComputerTarget(frame, currentFrameRef.current)) {
|
|
1951
|
+
paintCanvas(canvas, image, frame.width, frame.height);
|
|
1952
|
+
paintedFrameRef.current = frame;
|
|
1953
|
+
setPaintedFrame(frame);
|
|
1954
|
+
}
|
|
1955
|
+
} catch (cause) {
|
|
1956
|
+
if (mountedRef.current && currentFrameRef.current && sameComputerTarget(frame, currentFrameRef.current))
|
|
1957
|
+
errorRef.current(cause);
|
|
1958
|
+
} finally {
|
|
1959
|
+
if (objectUrl) URL.revokeObjectURL(objectUrl);
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
} finally {
|
|
1963
|
+
decodingFrameRef.current = false;
|
|
1964
|
+
if (mountedRef.current && queuedFrameRef.current) paintQueuedFrames();
|
|
1965
|
+
}
|
|
1966
|
+
})();
|
|
1967
|
+
}, []);
|
|
1968
|
+
useLayoutEffect(() => {
|
|
1969
|
+
mountedRef.current = true;
|
|
1970
|
+
return () => {
|
|
1971
|
+
mountedRef.current = false;
|
|
1972
|
+
actionQueueEpochRef.current += 1;
|
|
1973
|
+
queuedFrameRef.current = null;
|
|
1974
|
+
clearBufferedInput();
|
|
1975
|
+
};
|
|
1976
|
+
}, [clearBufferedInput]);
|
|
1977
|
+
useLayoutEffect(() => {
|
|
1978
|
+
if (currentFrameRef.current && !props.frame) {
|
|
1979
|
+
actionQueueEpochRef.current += 1;
|
|
1980
|
+
clearBufferedInput();
|
|
1981
|
+
}
|
|
1982
|
+
currentFrameRef.current = props.frame;
|
|
1983
|
+
if (!props.frame) {
|
|
1984
|
+
queuedFrameRef.current = null;
|
|
1985
|
+
paintedFrameRef.current = null;
|
|
1986
|
+
setPaintedFrame(null);
|
|
1987
|
+
return;
|
|
1988
|
+
}
|
|
1989
|
+
queuedFrameRef.current = props.frame;
|
|
1990
|
+
paintQueuedFrames();
|
|
1991
|
+
}, [clearBufferedInput, paintQueuedFrames, props.frame]);
|
|
1992
|
+
useLayoutEffect(() => {
|
|
1993
|
+
if (!rawInputEnabled || !props.pointerInput || !props.keyboardInput) {
|
|
1994
|
+
actionQueueEpochRef.current += 1;
|
|
1995
|
+
clearBufferedInput();
|
|
1996
|
+
}
|
|
1997
|
+
}, [clearBufferedInput, props.keyboardInput, props.pointerInput, rawInputEnabled]);
|
|
1998
|
+
const enqueue = useCallback5(
|
|
1999
|
+
(action, frame, after) => {
|
|
2000
|
+
const epoch = actionQueueEpochRef.current;
|
|
2001
|
+
const dispatch = actionRef.current;
|
|
2002
|
+
const isCurrent = () => mountedRef.current && epoch === actionQueueEpochRef.current;
|
|
2003
|
+
actionTailRef.current = actionTailRef.current.catch(() => void 0).then(async () => {
|
|
2004
|
+
if (!isCurrent()) return;
|
|
2005
|
+
await dispatch(action, frame);
|
|
2006
|
+
if (!isCurrent()) return;
|
|
2007
|
+
await after?.(isCurrent);
|
|
2008
|
+
}).catch((cause) => {
|
|
2009
|
+
if (!isCurrent()) return;
|
|
2010
|
+
actionQueueEpochRef.current += 1;
|
|
2011
|
+
clearBufferedInput();
|
|
2012
|
+
errorRef.current(cause);
|
|
2013
|
+
});
|
|
2014
|
+
},
|
|
2015
|
+
[clearBufferedInput]
|
|
2016
|
+
);
|
|
2017
|
+
const point = useCallback5(
|
|
2018
|
+
(frame, clientX, clientY) => computerPoint(canvasRef.current, frame, clientX, clientY),
|
|
2019
|
+
[]
|
|
2020
|
+
);
|
|
2021
|
+
const flushPendingText = useCallback5(() => {
|
|
2022
|
+
const pending = pendingTextRef.current;
|
|
2023
|
+
if (!pending) return;
|
|
2024
|
+
clearTimeout(pending.timer);
|
|
2025
|
+
pendingTextRef.current = null;
|
|
2026
|
+
enqueue({ type: "keyboard", action: "type", value: pending.text }, null);
|
|
2027
|
+
}, [enqueue]);
|
|
2028
|
+
const flushPendingClick = useCallback5(() => {
|
|
2029
|
+
const pending = lastClickRef.current;
|
|
2030
|
+
if (!pending) return;
|
|
2031
|
+
if (clickTimerRef.current) clearTimeout(clickTimerRef.current);
|
|
2032
|
+
clickTimerRef.current = null;
|
|
2033
|
+
lastClickRef.current = null;
|
|
2034
|
+
enqueue(
|
|
2035
|
+
{
|
|
2036
|
+
type: "pointer",
|
|
2037
|
+
frameId: pending.frame.frameId,
|
|
2038
|
+
action: "click",
|
|
2039
|
+
x: pending.x,
|
|
2040
|
+
y: pending.y
|
|
2041
|
+
},
|
|
2042
|
+
pending.frame
|
|
2043
|
+
);
|
|
2044
|
+
}, [enqueue]);
|
|
2045
|
+
const flushPendingWheel = useCallback5(() => {
|
|
2046
|
+
const batch = wheelRef.current;
|
|
2047
|
+
if (!batch) return;
|
|
2048
|
+
if (batch.timer) clearTimeout(batch.timer);
|
|
2049
|
+
wheelRef.current = null;
|
|
2050
|
+
enqueue(
|
|
2051
|
+
{
|
|
2052
|
+
type: "pointer",
|
|
2053
|
+
frameId: batch.frame.frameId,
|
|
2054
|
+
action: "scroll",
|
|
2055
|
+
x: batch.x,
|
|
2056
|
+
y: batch.y,
|
|
2057
|
+
deltaX: batch.deltaX,
|
|
2058
|
+
deltaY: batch.deltaY
|
|
2059
|
+
},
|
|
2060
|
+
batch.frame
|
|
2061
|
+
);
|
|
2062
|
+
}, [enqueue]);
|
|
2063
|
+
const pointerDown = (event) => {
|
|
2064
|
+
const frame = paintedFrameRef.current;
|
|
2065
|
+
if (!frame || props.mutating || !rawInputEnabled || event.button !== 0) return;
|
|
2066
|
+
event.preventDefault();
|
|
2067
|
+
if (keyboardInputEnabled) inputRef.current?.focus({ preventScroll: true });
|
|
2068
|
+
if (!pointerInputEnabled) return;
|
|
2069
|
+
flushPendingWheel();
|
|
2070
|
+
flushPendingText();
|
|
2071
|
+
pointerStartRef.current = {
|
|
2072
|
+
x: event.clientX,
|
|
2073
|
+
y: event.clientY,
|
|
2074
|
+
pointerId: event.pointerId,
|
|
2075
|
+
frame
|
|
2076
|
+
};
|
|
2077
|
+
event.currentTarget.setPointerCapture?.(event.pointerId);
|
|
2078
|
+
};
|
|
2079
|
+
const pointerUp = (event) => {
|
|
2080
|
+
if (!pointerInputEnabled) return;
|
|
2081
|
+
const start = pointerStartRef.current;
|
|
2082
|
+
pointerStartRef.current = null;
|
|
2083
|
+
if (!start || start.pointerId !== event.pointerId) return;
|
|
2084
|
+
const from = point(start.frame, start.x, start.y);
|
|
2085
|
+
const to = point(start.frame, event.clientX, event.clientY);
|
|
2086
|
+
if (!from || !to) return;
|
|
2087
|
+
if (Math.hypot(event.clientX - start.x, event.clientY - start.y) > 5) {
|
|
2088
|
+
flushPendingClick();
|
|
2089
|
+
enqueue(
|
|
2090
|
+
{
|
|
2091
|
+
type: "pointer",
|
|
2092
|
+
frameId: start.frame.frameId,
|
|
2093
|
+
action: "drag",
|
|
2094
|
+
x: from.x,
|
|
2095
|
+
y: from.y,
|
|
2096
|
+
endX: to.x,
|
|
2097
|
+
endY: to.y
|
|
2098
|
+
},
|
|
2099
|
+
start.frame
|
|
2100
|
+
);
|
|
2101
|
+
return;
|
|
2102
|
+
}
|
|
2103
|
+
const now = Date.now();
|
|
2104
|
+
const previous = lastClickRef.current;
|
|
2105
|
+
if (previous && now - previous.at < 280 && Math.hypot(previous.x - to.x, previous.y - to.y) < 6 && sameFrameFence(previous.frame, start.frame)) {
|
|
2106
|
+
if (clickTimerRef.current) clearTimeout(clickTimerRef.current);
|
|
2107
|
+
clickTimerRef.current = null;
|
|
2108
|
+
lastClickRef.current = null;
|
|
2109
|
+
enqueue(
|
|
2110
|
+
{
|
|
2111
|
+
type: "pointer",
|
|
2112
|
+
frameId: start.frame.frameId,
|
|
2113
|
+
action: "double_click",
|
|
2114
|
+
x: to.x,
|
|
2115
|
+
y: to.y
|
|
2116
|
+
},
|
|
2117
|
+
start.frame
|
|
2118
|
+
);
|
|
2119
|
+
return;
|
|
2120
|
+
}
|
|
2121
|
+
lastClickRef.current = { at: now, x: to.x, y: to.y, frame: start.frame };
|
|
2122
|
+
clickTimerRef.current = setTimeout(() => {
|
|
2123
|
+
clickTimerRef.current = null;
|
|
2124
|
+
lastClickRef.current = null;
|
|
2125
|
+
enqueue(
|
|
2126
|
+
{
|
|
2127
|
+
type: "pointer",
|
|
2128
|
+
frameId: start.frame.frameId,
|
|
2129
|
+
action: "click",
|
|
2130
|
+
x: to.x,
|
|
2131
|
+
y: to.y
|
|
2132
|
+
},
|
|
2133
|
+
start.frame
|
|
2134
|
+
);
|
|
2135
|
+
}, 280);
|
|
2136
|
+
};
|
|
2137
|
+
const contextMenu = (event) => {
|
|
2138
|
+
event.preventDefault();
|
|
2139
|
+
if (!pointerInputEnabled) return;
|
|
2140
|
+
const frame = paintedFrameRef.current;
|
|
2141
|
+
if (!frame) return;
|
|
2142
|
+
flushPendingWheel();
|
|
2143
|
+
flushPendingText();
|
|
2144
|
+
flushPendingClick();
|
|
2145
|
+
const at = point(frame, event.clientX, event.clientY);
|
|
2146
|
+
if (at) {
|
|
2147
|
+
enqueue(
|
|
2148
|
+
{
|
|
2149
|
+
type: "pointer",
|
|
2150
|
+
frameId: frame.frameId,
|
|
2151
|
+
action: "click",
|
|
2152
|
+
x: at.x,
|
|
2153
|
+
y: at.y,
|
|
2154
|
+
button: "right"
|
|
2155
|
+
},
|
|
2156
|
+
frame
|
|
2157
|
+
);
|
|
2158
|
+
}
|
|
2159
|
+
};
|
|
2160
|
+
const wheel = (event) => {
|
|
2161
|
+
if (!pointerInputEnabled) return;
|
|
2162
|
+
const frame = paintedFrameRef.current;
|
|
2163
|
+
if (!frame) return;
|
|
2164
|
+
const at = point(frame, event.clientX, event.clientY);
|
|
2165
|
+
if (!at) return;
|
|
2166
|
+
flushPendingText();
|
|
2167
|
+
flushPendingClick();
|
|
2168
|
+
event.preventDefault();
|
|
2169
|
+
let pending = wheelRef.current;
|
|
2170
|
+
if (pending && (!sameComputerTarget(pending.frame, frame) || Math.hypot(pending.x - at.x, pending.y - at.y) > 6)) {
|
|
2171
|
+
flushPendingWheel();
|
|
2172
|
+
pending = null;
|
|
2173
|
+
}
|
|
2174
|
+
wheelRef.current = {
|
|
2175
|
+
x: at.x,
|
|
2176
|
+
y: at.y,
|
|
2177
|
+
deltaX: (pending?.deltaX ?? 0) + event.deltaX,
|
|
2178
|
+
deltaY: (pending?.deltaY ?? 0) + event.deltaY,
|
|
2179
|
+
frame,
|
|
2180
|
+
timer: pending?.timer ?? setTimeout(flushPendingWheel, 45)
|
|
2181
|
+
};
|
|
2182
|
+
};
|
|
2183
|
+
const keyDown = (event) => {
|
|
2184
|
+
if (!keyboardInputEnabled) return;
|
|
2185
|
+
if (composingRef.current || event.nativeEvent.isComposing || event.nativeEvent.keyCode === 229) {
|
|
2186
|
+
return;
|
|
2187
|
+
}
|
|
2188
|
+
flushPendingWheel();
|
|
2189
|
+
const command = event.metaKey || event.ctrlKey;
|
|
2190
|
+
if (props.clipboardEnabled && command && !event.altKey && ["c", "v"].includes(event.key.toLowerCase())) {
|
|
2191
|
+
flushPendingText();
|
|
2192
|
+
return;
|
|
2193
|
+
}
|
|
2194
|
+
const key = computerKey(event);
|
|
2195
|
+
if (!key) return;
|
|
2196
|
+
event.preventDefault();
|
|
2197
|
+
flushPendingText();
|
|
2198
|
+
flushPendingClick();
|
|
2199
|
+
enqueue({ type: "keyboard", action: "press", value: key }, null);
|
|
2200
|
+
};
|
|
2201
|
+
const copy = (event) => {
|
|
2202
|
+
if (!props.clipboardEnabled || !keyboardInputEnabled) return;
|
|
2203
|
+
event.preventDefault();
|
|
2204
|
+
flushPendingWheel();
|
|
2205
|
+
flushPendingText();
|
|
2206
|
+
flushPendingClick();
|
|
2207
|
+
const readClipboard = readClipboardRef.current;
|
|
2208
|
+
enqueue({ type: "clipboard", operation: "copy" }, null, async (isCurrent) => {
|
|
2209
|
+
const clipboard = await readClipboard();
|
|
2210
|
+
if (!isCurrent()) return;
|
|
2211
|
+
if (!clipboard.text) return;
|
|
2212
|
+
if (!await copyTextToClipboard(clipboard.text)) {
|
|
2213
|
+
throw new Error("Desktop text could not be copied to the local clipboard");
|
|
2214
|
+
}
|
|
2215
|
+
});
|
|
2216
|
+
};
|
|
2217
|
+
const paste = (event) => {
|
|
2218
|
+
if (!props.clipboardEnabled || !keyboardInputEnabled) return;
|
|
2219
|
+
event.preventDefault();
|
|
2220
|
+
flushPendingWheel();
|
|
2221
|
+
flushPendingText();
|
|
2222
|
+
flushPendingClick();
|
|
2223
|
+
const text = event.clipboardData.getData("text/plain");
|
|
2224
|
+
const readClipboard = readClipboardRef.current;
|
|
2225
|
+
const dispatch = actionRef.current;
|
|
2226
|
+
enqueue({ type: "clipboard", operation: "write", text }, null, async (isCurrent) => {
|
|
2227
|
+
const clipboard = await readClipboard();
|
|
2228
|
+
if (!isCurrent()) return;
|
|
2229
|
+
assertExactComputerClipboard(clipboard, text);
|
|
2230
|
+
await dispatch({ type: "clipboard", operation: "paste" }, null);
|
|
2231
|
+
});
|
|
2232
|
+
};
|
|
2233
|
+
const input = (value, nativeComposing = false) => {
|
|
2234
|
+
if (composingRef.current || nativeComposing) return;
|
|
2235
|
+
if (!value || !keyboardInputEnabled) return;
|
|
2236
|
+
flushPendingWheel();
|
|
2237
|
+
flushPendingClick();
|
|
2238
|
+
const pending = pendingTextRef.current;
|
|
2239
|
+
if (pending) {
|
|
2240
|
+
clearTimeout(pending.timer);
|
|
2241
|
+
pending.text += value;
|
|
2242
|
+
pending.timer = setTimeout(flushPendingText, 16);
|
|
2243
|
+
} else {
|
|
2244
|
+
pendingTextRef.current = {
|
|
2245
|
+
text: value,
|
|
2246
|
+
timer: setTimeout(flushPendingText, 16)
|
|
2247
|
+
};
|
|
2248
|
+
}
|
|
2249
|
+
if (inputRef.current) inputRef.current.value = "";
|
|
2250
|
+
};
|
|
2251
|
+
const compositionEnd = (event) => {
|
|
2252
|
+
composingRef.current = false;
|
|
2253
|
+
input(event.currentTarget.value || event.data);
|
|
2254
|
+
};
|
|
2255
|
+
const showCanvas = props.frame !== null && paintedFrame !== null && sameComputerTarget(props.frame, paintedFrame) && !props.machineLocked && !streamFailed;
|
|
2256
|
+
return /* @__PURE__ */ jsxs("div", { className: "relative min-h-0 flex-1 overflow-hidden bg-black", children: [
|
|
2257
|
+
/* @__PURE__ */ jsx(
|
|
2258
|
+
"canvas",
|
|
2259
|
+
{
|
|
2260
|
+
ref: canvasRef,
|
|
2261
|
+
className: cn(
|
|
2262
|
+
"absolute inset-0 m-auto max-h-full max-w-full touch-none focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-og-accent",
|
|
2263
|
+
!showCanvas && "invisible",
|
|
2264
|
+
!pointerInputEnabled && "cursor-default"
|
|
2265
|
+
),
|
|
2266
|
+
onPointerDown: pointerDown,
|
|
2267
|
+
onPointerUp: pointerUp,
|
|
2268
|
+
onPointerCancel: () => {
|
|
2269
|
+
pointerStartRef.current = null;
|
|
2270
|
+
},
|
|
2271
|
+
onContextMenu: contextMenu,
|
|
2272
|
+
onWheel: wheel,
|
|
2273
|
+
"aria-label": `Interactive ${props.target?.kind ?? "desktop"} view`
|
|
2274
|
+
}
|
|
2275
|
+
),
|
|
2276
|
+
/* @__PURE__ */ jsx(
|
|
2277
|
+
"textarea",
|
|
2278
|
+
{
|
|
2279
|
+
ref: inputRef,
|
|
2280
|
+
defaultValue: "",
|
|
2281
|
+
onInput: (event) => input(event.currentTarget.value, event.nativeEvent.isComposing),
|
|
2282
|
+
onCompositionStart: () => {
|
|
2283
|
+
composingRef.current = true;
|
|
2284
|
+
},
|
|
2285
|
+
onCompositionEnd: compositionEnd,
|
|
2286
|
+
onKeyDown: keyDown,
|
|
2287
|
+
onCopy: copy,
|
|
2288
|
+
onPaste: paste,
|
|
2289
|
+
className: "pointer-events-none absolute left-1/2 top-1/2 size-px resize-none overflow-hidden opacity-0",
|
|
2290
|
+
"aria-label": "Desktop keyboard input",
|
|
2291
|
+
autoCapitalize: "off",
|
|
2292
|
+
autoCorrect: "off",
|
|
2293
|
+
spellCheck: false,
|
|
2294
|
+
disabled: !keyboardInputEnabled
|
|
2295
|
+
}
|
|
2296
|
+
),
|
|
2297
|
+
!showCanvas ? /* @__PURE__ */ jsx(
|
|
2298
|
+
ComputerViewportFallback,
|
|
2299
|
+
{
|
|
2300
|
+
observation: props.observation,
|
|
2301
|
+
machineLocked: props.machineLocked,
|
|
2302
|
+
controlUnavailable: props.controlUnavailable,
|
|
2303
|
+
inputAllowed: props.inputAllowed,
|
|
2304
|
+
connectionState: props.connectionState,
|
|
2305
|
+
error: props.connectionError,
|
|
2306
|
+
onAction: (action) => enqueue(action, null),
|
|
2307
|
+
onReconnect: props.onReconnect
|
|
2308
|
+
}
|
|
2309
|
+
) : null,
|
|
2310
|
+
props.mutating ? /* @__PURE__ */ jsxs("div", { className: "pointer-events-none absolute right-3 top-3 flex items-center gap-1.5 rounded-full border border-white/10 bg-black/65 px-2.5 py-1 text-[11px] text-white/80 backdrop-blur", children: [
|
|
2311
|
+
/* @__PURE__ */ jsx(LoaderCircleIcon, { className: "size-3 animate-spin" }),
|
|
2312
|
+
" Acting"
|
|
2313
|
+
] }) : null,
|
|
2314
|
+
showCanvas && (!rawInputEnabled || !props.pointerInput || !props.keyboardInput) ? /* @__PURE__ */ jsxs("div", { className: "absolute bottom-3 left-1/2 flex w-max max-w-[calc(100%-1.5rem)] -translate-x-1/2 items-center gap-2 rounded-full border border-white/10 bg-black/70 py-1 pl-3 pr-1 text-[11px] text-white/80 backdrop-blur", children: [
|
|
2315
|
+
/* @__PURE__ */ jsx("span", { children: !rawInputEnabled && (props.pointerInput || props.keyboardInput) ? "Background view \xB7 use app controls" : !props.pointerInput && !props.keyboardInput ? "View only \xB7 mouse and keyboard unavailable" : props.keyboardInput ? "Keyboard only \xB7 mouse unavailable" : "Mouse only \xB7 keyboard unavailable" }),
|
|
2316
|
+
!rawInputEnabled && props.target && (props.pointerInput || props.keyboardInput) ? /* @__PURE__ */ jsx(
|
|
2317
|
+
"button",
|
|
2318
|
+
{
|
|
2319
|
+
type: "button",
|
|
2320
|
+
disabled: props.mutating,
|
|
2321
|
+
onClick: () => enqueue({ type: "focus", targetId: props.target.id }, null),
|
|
2322
|
+
className: "shrink-0 whitespace-nowrap rounded-full bg-white/10 px-2 py-0.5 font-medium text-white transition hover:bg-white/20 disabled:opacity-50",
|
|
2323
|
+
children: "Bring to front"
|
|
2324
|
+
}
|
|
2325
|
+
) : null
|
|
2326
|
+
] }) : null
|
|
2327
|
+
] });
|
|
2328
|
+
}
|
|
2329
|
+
function assertExactComputerClipboard(clipboard, expected) {
|
|
2330
|
+
if (clipboard.truncated || clipboard.text !== expected) {
|
|
2331
|
+
throw new Error("Desktop clipboard did not accept the exact pasted text");
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
function ComputerViewportFallback(props) {
|
|
2335
|
+
if (props.machineLocked) {
|
|
2336
|
+
return /* @__PURE__ */ jsx("div", { className: "absolute inset-0 grid place-items-center bg-og-bg p-6", children: /* @__PURE__ */ jsxs("div", { className: "max-w-sm text-center", children: [
|
|
2337
|
+
/* @__PURE__ */ jsx(LockKeyholeIcon, { className: "mx-auto size-6 text-og-fg-muted" }),
|
|
2338
|
+
/* @__PURE__ */ jsx("p", { className: "mt-3 text-og-menu font-medium text-og-fg", children: "Machine locked" }),
|
|
2339
|
+
/* @__PURE__ */ jsx("p", { className: "mt-1 text-og-control leading-5 text-og-fg-muted", children: "Live frames and input stay private until someone unlocks the machine." })
|
|
2340
|
+
] }) });
|
|
2341
|
+
}
|
|
2342
|
+
const appControls = props.observation?.target.kind === "app";
|
|
2343
|
+
const controlFailure = interactionControlFailureFromError(props.error);
|
|
2344
|
+
const interactive = semanticControls(props.observation).filter((node) => semanticAction(node) !== null).slice(0, 10);
|
|
2345
|
+
return /* @__PURE__ */ jsx("div", { className: "absolute inset-0 grid place-items-center bg-og-bg p-6", children: /* @__PURE__ */ jsxs("div", { className: "w-full max-w-md rounded-og-lg border border-og-border bg-og-surface-1 p-4 shadow-lg", children: [
|
|
2346
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
2347
|
+
props.error ? /* @__PURE__ */ jsx(CircleAlertIcon, { className: "size-4 text-og-danger" }) : appControls ? /* @__PURE__ */ jsx(PanelsTopLeftIcon, { className: "size-4 text-og-fg-muted" }) : /* @__PURE__ */ jsx(LoaderCircleIcon, { className: "size-4 animate-spin text-og-fg-muted" }),
|
|
2348
|
+
/* @__PURE__ */ jsx("p", { className: "text-og-menu font-medium text-og-fg", children: isAttachedChromeGenerationLossError(props.error) ? "Chrome reconnected\u2014open a fresh browser/desktop." : !props.inputAllowed ? "View only" : props.controlUnavailable ? "Desktop controls unavailable" : props.error ? "Live view disconnected" : appControls ? "App controls" : computerConnectionLabel(props.connectionState) })
|
|
2349
|
+
] }),
|
|
2350
|
+
props.error ? /* @__PURE__ */ jsx("p", { className: "mt-2 text-og-control leading-5 text-og-fg-muted", children: isAttachedChromeGenerationLossError(props.error) ? "Chrome reconnected\u2014open a fresh browser/desktop. Use Browser \u2192 New browser \u2192 Connected Chrome." : controlFailure?.message ?? props.error.message }) : null,
|
|
2351
|
+
!props.inputAllowed ? /* @__PURE__ */ jsx("p", { className: "mt-2 text-og-control leading-5 text-og-fg-muted", children: "Refresh to check desktop controls." }) : null,
|
|
2352
|
+
interactive.length > 0 && !props.controlUnavailable && props.inputAllowed ? /* @__PURE__ */ jsxs("div", { className: "mt-3 border-t border-og-border pt-3", children: [
|
|
2353
|
+
/* @__PURE__ */ jsx("p", { className: "mb-2 text-og-xs text-og-fg-subtle", children: "App controls remain available" }),
|
|
2354
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1.5", children: interactive.map((node) => /* @__PURE__ */ jsx(
|
|
2355
|
+
"button",
|
|
2356
|
+
{
|
|
2357
|
+
type: "button",
|
|
2358
|
+
onClick: () => {
|
|
2359
|
+
const action = semanticAction(node);
|
|
2360
|
+
if (action) props.onAction(action);
|
|
2361
|
+
},
|
|
2362
|
+
className: "rounded-og-sm border border-og-border bg-og-surface-1 px-2 py-1 text-og-control text-og-fg transition hover:bg-og-surface-2",
|
|
2363
|
+
children: node.name || node.role
|
|
2364
|
+
},
|
|
2365
|
+
node.ref
|
|
2366
|
+
)) })
|
|
2367
|
+
] }) : null,
|
|
2368
|
+
props.error && !isAttachedChromeGenerationLossError(props.error) ? /* @__PURE__ */ jsx(
|
|
2369
|
+
"button",
|
|
2370
|
+
{
|
|
2371
|
+
type: "button",
|
|
2372
|
+
onClick: props.onReconnect,
|
|
2373
|
+
className: "mt-3 text-og-control font-medium text-og-accent hover:underline",
|
|
2374
|
+
children: controlFailure?.retryable === false ? "Try again" : "Reconnect"
|
|
2375
|
+
}
|
|
2376
|
+
) : null
|
|
2377
|
+
] }) });
|
|
2378
|
+
}
|
|
2379
|
+
function ComputerSemanticPanel(props) {
|
|
2380
|
+
const nodes = semanticControls(props.observation).slice(0, 100);
|
|
2381
|
+
return /* @__PURE__ */ jsxs("aside", { className: "box-border max-h-[40%] w-full shrink-0 overflow-y-auto border-t border-og-border bg-og-surface-1 p-2 @xl/computer-viewer:max-h-none @xl/computer-viewer:w-64 @xl/computer-viewer:border-t-0 @xl/computer-viewer:border-l", children: [
|
|
2382
|
+
/* @__PURE__ */ jsxs("div", { className: "mb-2 flex items-center gap-1.5 px-1 text-og-xs font-medium uppercase tracking-[0.1em] text-og-fg-subtle", children: [
|
|
2383
|
+
/* @__PURE__ */ jsx(KeyboardIcon, { className: "size-3" }),
|
|
2384
|
+
" App controls"
|
|
2385
|
+
] }),
|
|
2386
|
+
!props.inputAllowed ? /* @__PURE__ */ jsx("p", { className: "px-1 py-2 text-og-control leading-5 text-og-fg-muted", children: "View only \xB7 refresh to check desktop controls." }) : props.controlUnavailable ? /* @__PURE__ */ jsx("p", { className: "px-1 py-2 text-og-control leading-5 text-og-fg-muted", children: "Reconnect to use app controls." }) : null,
|
|
2387
|
+
nodes.length === 0 ? props.controlUnavailable ? null : /* @__PURE__ */ jsx("p", { className: "px-1 py-2 text-og-control leading-5 text-og-fg-muted", children: "This view has no accessibility controls. Use the live image instead." }) : /* @__PURE__ */ jsx("div", { className: "space-y-0.5", children: nodes.map((node) => /* @__PURE__ */ jsx(
|
|
2388
|
+
ComputerSemanticControl,
|
|
2389
|
+
{
|
|
2390
|
+
node,
|
|
2391
|
+
mutating: props.mutating,
|
|
2392
|
+
onAction: props.onAction
|
|
2393
|
+
},
|
|
2394
|
+
node.ref
|
|
2395
|
+
)) })
|
|
2396
|
+
] });
|
|
2397
|
+
}
|
|
2398
|
+
function ComputerSemanticControl(props) {
|
|
2399
|
+
const { node } = props;
|
|
2400
|
+
const editable = node.actions.includes("set_value");
|
|
2401
|
+
const action = semanticAction(node);
|
|
2402
|
+
const observedValue = semanticValue(node) ?? "";
|
|
2403
|
+
const [value, setValue] = useState5(() => observedValue);
|
|
2404
|
+
useEffect5(() => {
|
|
2405
|
+
setValue(observedValue);
|
|
2406
|
+
}, [node.ref, observedValue]);
|
|
2407
|
+
if (editable) {
|
|
2408
|
+
return /* @__PURE__ */ jsxs(
|
|
2409
|
+
"form",
|
|
2410
|
+
{
|
|
2411
|
+
className: "rounded-og-sm px-2 py-1.5 hover:bg-og-surface-2",
|
|
2412
|
+
onSubmit: (event) => {
|
|
2413
|
+
event.preventDefault();
|
|
2414
|
+
if (props.mutating) return;
|
|
2415
|
+
props.onAction({
|
|
2416
|
+
type: "semantic",
|
|
2417
|
+
locator: { kind: "ref", ref: node.ref },
|
|
2418
|
+
action: "set_value",
|
|
2419
|
+
value
|
|
2420
|
+
});
|
|
2421
|
+
},
|
|
2422
|
+
children: [
|
|
2423
|
+
/* @__PURE__ */ jsx("label", { className: "block truncate text-[10px] uppercase text-og-fg-subtle", children: node.name || node.identifier || node.role }),
|
|
2424
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-1 flex gap-1", children: [
|
|
2425
|
+
/* @__PURE__ */ jsx(
|
|
2426
|
+
"input",
|
|
2427
|
+
{
|
|
2428
|
+
value,
|
|
2429
|
+
disabled: props.mutating,
|
|
2430
|
+
onChange: (event) => setValue(event.currentTarget.value),
|
|
2431
|
+
className: "min-w-0 flex-1 rounded-og-sm border border-og-border bg-og-surface-1 px-1.5 py-1 text-og-control text-og-fg outline-hidden focus:border-og-accent",
|
|
2432
|
+
"aria-label": `Set ${node.name || node.identifier || node.role}`
|
|
2433
|
+
}
|
|
2434
|
+
),
|
|
2435
|
+
/* @__PURE__ */ jsx(
|
|
2436
|
+
"button",
|
|
2437
|
+
{
|
|
2438
|
+
type: "submit",
|
|
2439
|
+
disabled: props.mutating,
|
|
2440
|
+
className: "rounded-og-sm border border-og-border bg-og-surface-1 px-2 text-og-control font-medium text-og-fg transition hover:bg-og-surface-2 disabled:opacity-50",
|
|
2441
|
+
children: "Set"
|
|
2442
|
+
}
|
|
2443
|
+
)
|
|
2444
|
+
] })
|
|
2445
|
+
]
|
|
2446
|
+
}
|
|
2447
|
+
);
|
|
2448
|
+
}
|
|
2449
|
+
return /* @__PURE__ */ jsxs(
|
|
2450
|
+
"button",
|
|
2451
|
+
{
|
|
2452
|
+
type: "button",
|
|
2453
|
+
disabled: !action || props.mutating,
|
|
2454
|
+
onClick: () => {
|
|
2455
|
+
if (action) props.onAction(action);
|
|
2456
|
+
},
|
|
2457
|
+
className: "flex w-full items-start gap-2 rounded-og-sm px-2 py-1.5 text-left transition hover:bg-og-surface-2 disabled:cursor-default disabled:hover:bg-transparent",
|
|
2458
|
+
children: [
|
|
2459
|
+
/* @__PURE__ */ jsx("span", { className: "mt-0.5 text-[10px] uppercase text-og-fg-subtle", children: node.role }),
|
|
2460
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-og-control text-og-fg", children: node.name || semanticValue(node) || node.identifier || "Unnamed control" })
|
|
2461
|
+
]
|
|
2462
|
+
}
|
|
2463
|
+
);
|
|
2464
|
+
}
|
|
2465
|
+
function ComputerStatusBar(props) {
|
|
2466
|
+
const screen = props.target?.kind === "screen";
|
|
2467
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex h-8 shrink-0 items-center gap-2 border-t border-og-border bg-og-surface-1 px-2 text-og-xs text-og-fg-subtle", children: [
|
|
2468
|
+
/* @__PURE__ */ jsx(
|
|
2469
|
+
"span",
|
|
2470
|
+
{
|
|
2471
|
+
className: cn(
|
|
2472
|
+
"size-1.5 rounded-full",
|
|
2473
|
+
props.connectionState === "live" ? "bg-og-status-running" : "bg-og-muted"
|
|
2474
|
+
)
|
|
2475
|
+
}
|
|
2476
|
+
),
|
|
2477
|
+
/* @__PURE__ */ jsx("span", { children: !props.inputAllowed ? "View only" : props.controlUnavailable ? "Controls unavailable" : props.target?.kind === "app" ? "App controls" : props.connectionState === "live" ? "Live" : computerConnectionLabel(props.connectionState) }),
|
|
2478
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: !props.inputAllowed ? "Refresh to check desktop controls" : props.controlUnavailable ? "Reconnect to use desktop input" : screen ? "Full screen \xB7 input may move pointer and focus" : props.target?.kind === "app" ? props.session?.capabilities?.backgroundActions ? "App \xB7 controls work in the background" : "App controls" : props.session?.capabilities?.backgroundInput ? "Window \xB7 clicks and typing stay in the background" : props.session?.capabilities?.backgroundActions ? "Window \xB7 app controls work in the background" : props.target?.kind ?? "Desktop" }),
|
|
2479
|
+
screen ? /* @__PURE__ */ jsx(MousePointer2Icon, { className: "size-3", "aria-hidden": true }) : null,
|
|
2480
|
+
props.refreshing ? /* @__PURE__ */ jsx(LoaderCircleIcon, { className: "size-3 animate-spin" }) : null,
|
|
2481
|
+
props.controlCount > 0 || props.showControls ? /* @__PURE__ */ jsxs(
|
|
2482
|
+
"button",
|
|
2483
|
+
{
|
|
2484
|
+
type: "button",
|
|
2485
|
+
onClick: props.onToggleControls,
|
|
2486
|
+
"aria-pressed": props.showControls,
|
|
2487
|
+
className: cn(
|
|
2488
|
+
"rounded px-1.5 py-0.5 transition hover:bg-og-surface-2 hover:text-og-fg",
|
|
2489
|
+
props.showControls && "bg-og-surface-2 text-og-fg"
|
|
2490
|
+
),
|
|
2491
|
+
children: [
|
|
2492
|
+
"Controls ",
|
|
2493
|
+
props.controlCount
|
|
2494
|
+
]
|
|
2495
|
+
}
|
|
2496
|
+
) : null
|
|
2497
|
+
] });
|
|
2498
|
+
}
|
|
2499
|
+
function ComputerNotice(props) {
|
|
2500
|
+
return /* @__PURE__ */ jsx(
|
|
2501
|
+
"div",
|
|
2502
|
+
{
|
|
2503
|
+
className: cn(
|
|
2504
|
+
"grid h-full place-items-center bg-og-bg text-og-control text-og-fg-muted",
|
|
2505
|
+
props.className
|
|
2506
|
+
),
|
|
2507
|
+
children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
2508
|
+
props.icon,
|
|
2509
|
+
props.text
|
|
2510
|
+
] })
|
|
2511
|
+
}
|
|
2512
|
+
);
|
|
2513
|
+
}
|
|
2514
|
+
var structuralRoles = /* @__PURE__ */ new Set([
|
|
2515
|
+
"frame",
|
|
2516
|
+
"panel",
|
|
2517
|
+
"section",
|
|
2518
|
+
"document",
|
|
2519
|
+
"document web",
|
|
2520
|
+
"static",
|
|
2521
|
+
"static text",
|
|
2522
|
+
"heading",
|
|
2523
|
+
"paragraph",
|
|
2524
|
+
"notification",
|
|
2525
|
+
"separator",
|
|
2526
|
+
"tool bar",
|
|
2527
|
+
"page tab list"
|
|
2528
|
+
]);
|
|
2529
|
+
function semanticControls(observation) {
|
|
2530
|
+
if (observation?.semantic?.kind !== "snapshot") return [];
|
|
2531
|
+
const result = [];
|
|
2532
|
+
const visit = (node) => {
|
|
2533
|
+
const role = node.role.trim().toLowerCase();
|
|
2534
|
+
const label = node.name?.trim() || node.identifier?.trim();
|
|
2535
|
+
if (node.actions.includes("set_value") || label && label.toLowerCase() !== role && !structuralRoles.has(role) && semanticAction(node) !== null) {
|
|
2536
|
+
result.push(node);
|
|
2537
|
+
}
|
|
2538
|
+
node.children?.forEach(visit);
|
|
2539
|
+
};
|
|
2540
|
+
observation.semantic.roots.forEach(visit);
|
|
2541
|
+
return result;
|
|
2542
|
+
}
|
|
2543
|
+
function semanticAction(node) {
|
|
2544
|
+
const invoke = ["invoke", "press", "click"].some((action) => node.actions.includes(action));
|
|
2545
|
+
if (invoke) {
|
|
2546
|
+
return {
|
|
2547
|
+
type: "semantic",
|
|
2548
|
+
locator: { kind: "ref", ref: node.ref },
|
|
2549
|
+
action: "invoke"
|
|
2550
|
+
};
|
|
2551
|
+
}
|
|
2552
|
+
return null;
|
|
2553
|
+
}
|
|
2554
|
+
function semanticValue(node) {
|
|
2555
|
+
return typeof node.value === "string" ? node.value : null;
|
|
2556
|
+
}
|
|
2557
|
+
function paintCanvas(canvas, image, width, height) {
|
|
2558
|
+
const dimensionsChanged = canvas.width !== width || canvas.height !== height;
|
|
2559
|
+
canvas.width = width;
|
|
2560
|
+
canvas.height = height;
|
|
2561
|
+
const context = canvas.getContext("2d", { alpha: false });
|
|
2562
|
+
if (!context) throw new Error("Desktop canvas is unavailable.");
|
|
2563
|
+
context.drawImage(image, 0, 0, width, height);
|
|
2564
|
+
if (dimensionsChanged) fitComputerCanvas(canvas);
|
|
2565
|
+
}
|
|
2566
|
+
function fitComputerCanvas(canvas) {
|
|
2567
|
+
const viewport = canvas.parentElement;
|
|
2568
|
+
if (!viewport || canvas.width <= 0 || canvas.height <= 0) return;
|
|
2569
|
+
const scale = Math.min(
|
|
2570
|
+
viewport.clientWidth / canvas.width,
|
|
2571
|
+
viewport.clientHeight / canvas.height
|
|
2572
|
+
);
|
|
2573
|
+
if (!Number.isFinite(scale) || scale <= 0) return;
|
|
2574
|
+
canvas.style.width = `${canvas.width * scale}px`;
|
|
2575
|
+
canvas.style.height = `${canvas.height * scale}px`;
|
|
2576
|
+
}
|
|
2577
|
+
function loadImage(url) {
|
|
2578
|
+
return new Promise((resolve, reject) => {
|
|
2579
|
+
const image = new Image();
|
|
2580
|
+
image.onload = () => resolve(image);
|
|
2581
|
+
image.onerror = () => reject(new Error("Desktop frame image could not be decoded."));
|
|
2582
|
+
image.src = url;
|
|
2583
|
+
});
|
|
2584
|
+
}
|
|
2585
|
+
function computerPoint(canvas, frame, clientX, clientY) {
|
|
2586
|
+
if (!canvas) return null;
|
|
2587
|
+
const rect = canvas.getBoundingClientRect();
|
|
2588
|
+
if (rect.width <= 0 || rect.height <= 0) return null;
|
|
2589
|
+
return {
|
|
2590
|
+
x: Math.max(0, Math.min(frame.width - 1, (clientX - rect.left) / rect.width * frame.width)),
|
|
2591
|
+
y: Math.max(0, Math.min(frame.height - 1, (clientY - rect.top) / rect.height * frame.height))
|
|
2592
|
+
};
|
|
2593
|
+
}
|
|
2594
|
+
function computerKey(event) {
|
|
2595
|
+
if (["Alt", "AltGraph", "Control", "Meta", "Shift"].includes(event.key)) return null;
|
|
2596
|
+
const special = /* @__PURE__ */ new Set([
|
|
2597
|
+
"Enter",
|
|
2598
|
+
"Tab",
|
|
2599
|
+
"Escape",
|
|
2600
|
+
"Backspace",
|
|
2601
|
+
"Delete",
|
|
2602
|
+
"ArrowUp",
|
|
2603
|
+
"ArrowDown",
|
|
2604
|
+
"ArrowLeft",
|
|
2605
|
+
"ArrowRight",
|
|
2606
|
+
"Home",
|
|
2607
|
+
"End",
|
|
2608
|
+
"PageUp",
|
|
2609
|
+
"PageDown"
|
|
2610
|
+
]);
|
|
2611
|
+
const modified = event.altKey || event.ctrlKey || event.metaKey;
|
|
2612
|
+
if (!modified && !special.has(event.key)) return null;
|
|
2613
|
+
const parts = [];
|
|
2614
|
+
if (event.ctrlKey) parts.push("Control");
|
|
2615
|
+
if (event.altKey) parts.push("Alt");
|
|
2616
|
+
if (event.metaKey) parts.push("Meta");
|
|
2617
|
+
if (event.shiftKey) parts.push("Shift");
|
|
2618
|
+
parts.push(event.key === " " ? "Space" : event.key);
|
|
2619
|
+
return parts.join("+");
|
|
2620
|
+
}
|
|
2621
|
+
function sameFrameFence(left, right) {
|
|
2622
|
+
return left.frameId === right.frameId && sameComputerTarget(left, right);
|
|
2623
|
+
}
|
|
2624
|
+
function sameComputerTarget(left, right) {
|
|
2625
|
+
return left.computerSessionId === right.computerSessionId && left.controllerGeneration === right.controllerGeneration && left.targetId === right.targetId && left.targetGeneration === right.targetGeneration;
|
|
2626
|
+
}
|
|
2627
|
+
function isLiveComputer2(session) {
|
|
2628
|
+
return !["ending", "ended", "failed", "lost"].includes(session.lifecycle);
|
|
2629
|
+
}
|
|
2630
|
+
function computerFailureMessage(session) {
|
|
2631
|
+
switch (session.failureCode) {
|
|
2632
|
+
case "machine_locked":
|
|
2633
|
+
return "Unlock the connected Mac, then try again.";
|
|
2634
|
+
case "controller_heartbeat_expired":
|
|
2635
|
+
return "The desktop connection was lost. Try opening it again.";
|
|
2636
|
+
case "controller_transition_expired":
|
|
2637
|
+
return session.placement.kind === "attached_device" ? "Chrome reconnected\u2014open a fresh browser/desktop. Use Browser \u2192 New browser \u2192 Connected Chrome." : "The desktop connection expired. Open a new desktop.";
|
|
2638
|
+
case null:
|
|
2639
|
+
return null;
|
|
2640
|
+
default:
|
|
2641
|
+
return "The desktop could not be opened. Try again.";
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
function isAttachedChromeGenerationLoss(session) {
|
|
2645
|
+
return session.lifecycle === "lost" && session.failureCode === "controller_transition_expired" && session.placement.kind === "attached_device";
|
|
2646
|
+
}
|
|
2647
|
+
function isAttachedChromeGenerationLossError(error) {
|
|
2648
|
+
if (!error) return false;
|
|
2649
|
+
return /placement instance changed|controller authority changed/i.test(error.message);
|
|
2650
|
+
}
|
|
2651
|
+
function countInterventions(interventions) {
|
|
2652
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2653
|
+
for (const intervention of interventions) {
|
|
2654
|
+
counts.set(intervention.resourceId, (counts.get(intervention.resourceId) ?? 0) + 1);
|
|
2655
|
+
}
|
|
2656
|
+
return counts;
|
|
2657
|
+
}
|
|
2658
|
+
function interventionTitle(intervention) {
|
|
2659
|
+
switch (intervention.kind) {
|
|
2660
|
+
case "manual_login":
|
|
2661
|
+
return "Sign in needed";
|
|
2662
|
+
case "mfa":
|
|
2663
|
+
return "Verification needed";
|
|
2664
|
+
case "external_action":
|
|
2665
|
+
case "confirmation":
|
|
2666
|
+
return "Action needed";
|
|
2667
|
+
case "other":
|
|
2668
|
+
return "Desktop needs your help";
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
function platformLabel(session) {
|
|
2672
|
+
switch (session.platform) {
|
|
2673
|
+
case "macos":
|
|
2674
|
+
return "Mac";
|
|
2675
|
+
case "linux":
|
|
2676
|
+
return "Linux";
|
|
2677
|
+
case "windows":
|
|
2678
|
+
return "Windows";
|
|
2679
|
+
default:
|
|
2680
|
+
return "Desktop";
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2683
|
+
function placementLabel(session) {
|
|
2684
|
+
switch (session.placement.kind) {
|
|
2685
|
+
case "sandbox_group":
|
|
2686
|
+
return "Agent sandbox";
|
|
2687
|
+
case "connected_machine":
|
|
2688
|
+
return "Connected machine";
|
|
2689
|
+
case "attached_device":
|
|
2690
|
+
return "Your machine";
|
|
2691
|
+
case "external_provider":
|
|
2692
|
+
return "Cloud desktop";
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
function lifecycleLabel(lifecycle) {
|
|
2696
|
+
switch (lifecycle) {
|
|
2697
|
+
case "starting":
|
|
2698
|
+
return "Opening desktop\u2026";
|
|
2699
|
+
case "suspending":
|
|
2700
|
+
return "Pausing desktop\u2026";
|
|
2701
|
+
case "suspended":
|
|
2702
|
+
return "Desktop paused";
|
|
2703
|
+
case "restoring":
|
|
2704
|
+
return "Restoring desktop\u2026";
|
|
2705
|
+
case "ending":
|
|
2706
|
+
return "Closing desktop\u2026";
|
|
2707
|
+
default:
|
|
2708
|
+
return "Connecting to desktop\u2026";
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
function computerConnectionLabel(state) {
|
|
2712
|
+
switch (state) {
|
|
2713
|
+
case "attaching":
|
|
2714
|
+
return "Opening desktop view\u2026";
|
|
2715
|
+
case "connecting":
|
|
2716
|
+
return "Connecting\u2026";
|
|
2717
|
+
case "reconnecting":
|
|
2718
|
+
return "Reconnecting\u2026";
|
|
2719
|
+
case "error":
|
|
2720
|
+
return "Disconnected";
|
|
2721
|
+
default:
|
|
2722
|
+
return "Waiting for desktop\u2026";
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
export {
|
|
2727
|
+
useComputerSessions,
|
|
2728
|
+
useComputerSession,
|
|
2729
|
+
useComputerFrameStream,
|
|
2730
|
+
ComputerViewer,
|
|
2731
|
+
computerKey
|
|
2732
|
+
};
|
|
2733
|
+
//# sourceMappingURL=chunk-JFNEV4LU.js.map
|