@opengeni/react 0.57.0 → 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 +654 -22
- 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 +10 -3
- package/dist/realtime.js +229 -83
- 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 +274 -60
- 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
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
InteractionIntervention,
|
|
9
9
|
InteractionSemanticNode,
|
|
10
10
|
} from "@opengeni/sdk/interaction";
|
|
11
|
+
import { interactionControlFailureFromError } from "@opengeni/sdk/interaction";
|
|
11
12
|
import type { DesktopStreamCapability } from "@opengeni/sdk";
|
|
12
13
|
import {
|
|
13
14
|
ChevronDownIcon,
|
|
@@ -24,6 +25,7 @@ import {
|
|
|
24
25
|
} from "lucide-react";
|
|
25
26
|
import {
|
|
26
27
|
type ClipboardEvent,
|
|
28
|
+
type CompositionEvent,
|
|
27
29
|
type KeyboardEvent,
|
|
28
30
|
type MouseEvent,
|
|
29
31
|
type PointerEvent,
|
|
@@ -31,6 +33,7 @@ import {
|
|
|
31
33
|
type WheelEvent,
|
|
32
34
|
useCallback,
|
|
33
35
|
useEffect,
|
|
36
|
+
useLayoutEffect,
|
|
34
37
|
useMemo,
|
|
35
38
|
useRef,
|
|
36
39
|
useState,
|
|
@@ -40,13 +43,16 @@ import {
|
|
|
40
43
|
useComputerFrameStream,
|
|
41
44
|
} from "../hooks/use-computer-frame-stream";
|
|
42
45
|
import { useComputerSession } from "../hooks/use-computer-session";
|
|
46
|
+
import { useComputerInputPosture } from "../hooks/use-computer-input-posture";
|
|
43
47
|
import { useComputerSessions } from "../hooks/use-computer-sessions";
|
|
44
48
|
import { useInteractionInterventions } from "../hooks/use-interaction-interventions";
|
|
45
49
|
import { cn } from "../lib/cn";
|
|
46
50
|
import { copyTextToClipboard } from "../lib/clipboard";
|
|
51
|
+
import { isSourcePlacementChangedError } from "../lib/interaction-errors";
|
|
47
52
|
import type { EmbeddedComputerInteractionClientOverride } from "../session-context";
|
|
48
53
|
import { InteractionInterventionBanner } from "./interaction-intervention-banner";
|
|
49
54
|
import { DesktopViewer } from "./desktop-viewer";
|
|
55
|
+
import { useViewerMenuDismiss } from "./use-viewer-menu-dismiss";
|
|
50
56
|
|
|
51
57
|
export type ComputerViewerNotification = { kind: "error" | "info"; message: string };
|
|
52
58
|
|
|
@@ -62,6 +68,9 @@ export type ComputerViewerProps = EmbeddedComputerInteractionClientOverride & {
|
|
|
62
68
|
/** Host navigation request for one exact ComputerSession. */
|
|
63
69
|
requestedComputerSessionId?: string | null | undefined;
|
|
64
70
|
requestedComputerRequestId?: string | number | null | undefined;
|
|
71
|
+
/** Restore and report the Desktop session selected in this task's dock. */
|
|
72
|
+
initialComputerSessionId?: string | null | undefined;
|
|
73
|
+
onComputerSessionIdChange?: ((computerSessionId: string | null) => void) | undefined;
|
|
65
74
|
};
|
|
66
75
|
|
|
67
76
|
type ComputerSelection = { sessionId: string; pinned: boolean } | null;
|
|
@@ -78,6 +87,8 @@ export function ComputerViewer({
|
|
|
78
87
|
renderEmpty,
|
|
79
88
|
requestedComputerSessionId,
|
|
80
89
|
requestedComputerRequestId,
|
|
90
|
+
initialComputerSessionId,
|
|
91
|
+
onComputerSessionIdChange,
|
|
81
92
|
...override
|
|
82
93
|
}: ComputerViewerProps) {
|
|
83
94
|
const registry = useComputerSessions({ ...override, sessionId, enabled });
|
|
@@ -87,11 +98,26 @@ export function ComputerViewer({
|
|
|
87
98
|
() => registry.relevantSessions.filter((session) => isLiveComputer(session)),
|
|
88
99
|
[registry.relevantSessions],
|
|
89
100
|
);
|
|
101
|
+
const connectedPlacementLoss = useMemo(
|
|
102
|
+
() =>
|
|
103
|
+
registry.relevantSessions.find(
|
|
104
|
+
(session) =>
|
|
105
|
+
session.lifecycle === "lost" &&
|
|
106
|
+
session.failureCode === "source_placement_changed" &&
|
|
107
|
+
session.placement.kind === "connected_machine",
|
|
108
|
+
) ?? null,
|
|
109
|
+
[registry.relevantSessions],
|
|
110
|
+
);
|
|
90
111
|
const recentRelevantFailure = useMemo(
|
|
91
112
|
() =>
|
|
92
113
|
liveRelevant.length === 0
|
|
93
|
-
? (registry.relevantSessions.find(
|
|
94
|
-
|
|
114
|
+
? (registry.relevantSessions.find(
|
|
115
|
+
(session) =>
|
|
116
|
+
["failed", "lost"].includes(session.lifecycle) &&
|
|
117
|
+
!(
|
|
118
|
+
session.failureCode === "source_placement_changed" &&
|
|
119
|
+
session.placement.kind === "connected_machine"
|
|
120
|
+
),
|
|
95
121
|
) ?? null)
|
|
96
122
|
: null,
|
|
97
123
|
[liveRelevant.length, registry.relevantSessions],
|
|
@@ -105,17 +131,46 @@ export function ComputerViewer({
|
|
|
105
131
|
liveRelevant.length > 0 ? liveRelevant : recentRelevantFailure ? [recentRelevantFailure] : [],
|
|
106
132
|
[liveRelevant, recentRelevantFailure],
|
|
107
133
|
);
|
|
108
|
-
const [selection, setSelection] = useState<ComputerSelection>(
|
|
134
|
+
const [selection, setSelection] = useState<ComputerSelection>(() =>
|
|
135
|
+
initialComputerSessionId ? { sessionId: initialComputerSessionId, pinned: true } : null,
|
|
136
|
+
);
|
|
137
|
+
const selectComputerSession = useCallback(
|
|
138
|
+
(next: ComputerSelection) => {
|
|
139
|
+
setSelection(next);
|
|
140
|
+
onComputerSessionIdChange?.(next?.sessionId ?? null);
|
|
141
|
+
},
|
|
142
|
+
[onComputerSessionIdChange],
|
|
143
|
+
);
|
|
109
144
|
const interventions = useInteractionInterventions({
|
|
110
145
|
...override,
|
|
111
146
|
enabled,
|
|
112
147
|
resourceKind: "computer_session",
|
|
113
148
|
});
|
|
114
149
|
const [creating, setCreating] = useState(false);
|
|
150
|
+
const [createError, setCreateError] = useState<Error | null>(null);
|
|
115
151
|
const [showControls, setShowControls] = useState(false);
|
|
116
152
|
const previousSessionIdRef = useRef(sessionId);
|
|
117
153
|
const handledRequestRef = useRef<string | null>(null);
|
|
118
154
|
const seenInterventionIdsRef = useRef(new Set<string>());
|
|
155
|
+
const createInFlightRef = useRef(false);
|
|
156
|
+
const autoCreateSessionRef = useRef<string | null>(null);
|
|
157
|
+
const emptyCatalogConfirmationRef = useRef<string | null>(null);
|
|
158
|
+
const endedGenerationLossRef = useRef(new Set<string>());
|
|
159
|
+
const recreatedPlacementLossRef = useRef(new Set<string>());
|
|
160
|
+
const [suppressGenericCreate, setSuppressGenericCreate] = useState(false);
|
|
161
|
+
const endStaleComputer = registry.end;
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
if (!enabled) return;
|
|
164
|
+
for (const session of registry.sessions) {
|
|
165
|
+
if (!isAttachedChromeGenerationLoss(session)) continue;
|
|
166
|
+
setSuppressGenericCreate(true);
|
|
167
|
+
if (endedGenerationLossRef.current.has(session.id)) continue;
|
|
168
|
+
endedGenerationLossRef.current.add(session.id);
|
|
169
|
+
void endStaleComputer(session.id).catch(() => {
|
|
170
|
+
endedGenerationLossRef.current.delete(session.id);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}, [enabled, endStaleComputer, registry.sessions]);
|
|
119
174
|
|
|
120
175
|
const notifyError = useCallback(
|
|
121
176
|
(cause: unknown, fallback: string) => {
|
|
@@ -131,10 +186,18 @@ export function ComputerViewer({
|
|
|
131
186
|
if (previousSessionIdRef.current === sessionId) return;
|
|
132
187
|
previousSessionIdRef.current = sessionId;
|
|
133
188
|
handledRequestRef.current = null;
|
|
134
|
-
|
|
135
|
-
|
|
189
|
+
autoCreateSessionRef.current = null;
|
|
190
|
+
emptyCatalogConfirmationRef.current = null;
|
|
191
|
+
recreatedPlacementLossRef.current.clear();
|
|
192
|
+
setSuppressGenericCreate(false);
|
|
193
|
+
setCreateError(null);
|
|
194
|
+
setSelection(
|
|
195
|
+
initialComputerSessionId ? { sessionId: initialComputerSessionId, pinned: true } : null,
|
|
196
|
+
);
|
|
197
|
+
}, [initialComputerSessionId, sessionId]);
|
|
136
198
|
|
|
137
199
|
useEffect(() => {
|
|
200
|
+
if (!enabled || registry.loading) return;
|
|
138
201
|
const requested = requestedComputerSessionId
|
|
139
202
|
? (liveSessions.find((session) => session.id === requestedComputerSessionId) ?? null)
|
|
140
203
|
: null;
|
|
@@ -143,7 +206,7 @@ export function ComputerViewer({
|
|
|
143
206
|
: null;
|
|
144
207
|
if (requested && requestKey && handledRequestRef.current !== requestKey) {
|
|
145
208
|
handledRequestRef.current = requestKey;
|
|
146
|
-
|
|
209
|
+
selectComputerSession({ sessionId: requested.id, pinned: true });
|
|
147
210
|
return;
|
|
148
211
|
}
|
|
149
212
|
|
|
@@ -154,15 +217,24 @@ export function ComputerViewer({
|
|
|
154
217
|
if (selection?.pinned && selectedStillLive) return;
|
|
155
218
|
const preferred = relevant[0] ?? null;
|
|
156
219
|
if (!preferred) {
|
|
157
|
-
if (selection)
|
|
220
|
+
if (selection) selectComputerSession(null);
|
|
158
221
|
return;
|
|
159
222
|
}
|
|
160
223
|
if (!selectedStillLive || !selection?.pinned) {
|
|
161
224
|
if (selection?.sessionId !== preferred.id || selection.pinned) {
|
|
162
|
-
|
|
225
|
+
selectComputerSession({ sessionId: preferred.id, pinned: false });
|
|
163
226
|
}
|
|
164
227
|
}
|
|
165
|
-
}, [
|
|
228
|
+
}, [
|
|
229
|
+
liveSessions,
|
|
230
|
+
enabled,
|
|
231
|
+
registry.loading,
|
|
232
|
+
relevant,
|
|
233
|
+
requestedComputerRequestId,
|
|
234
|
+
requestedComputerSessionId,
|
|
235
|
+
selectComputerSession,
|
|
236
|
+
selection,
|
|
237
|
+
]);
|
|
166
238
|
|
|
167
239
|
const selectedRegistrySession = useMemo(
|
|
168
240
|
() => liveSessions.find((session) => session.id === selection?.sessionId) ?? null,
|
|
@@ -185,6 +257,13 @@ export function ComputerViewer({
|
|
|
185
257
|
computerSessionId: selection?.sessionId ?? null,
|
|
186
258
|
enabled: enabled && selection !== null && controllerReady,
|
|
187
259
|
});
|
|
260
|
+
useEffect(() => {
|
|
261
|
+
setShowControls(computer.selectedTarget?.kind === "app");
|
|
262
|
+
}, [sessionId, selection?.sessionId, computer.selectedTarget?.id, computer.selectedTarget?.kind]);
|
|
263
|
+
useEffect(() => {
|
|
264
|
+
if (!isSourcePlacementChangedError(computer.error, "computer_session")) return;
|
|
265
|
+
void refreshRegistry();
|
|
266
|
+
}, [computer.error, refreshRegistry]);
|
|
188
267
|
const act = computer.act;
|
|
189
268
|
const actFromFrame = computer.actFromFrame;
|
|
190
269
|
const refreshComputer = computer.refresh;
|
|
@@ -192,7 +271,12 @@ export function ComputerViewer({
|
|
|
192
271
|
...override,
|
|
193
272
|
computerSessionId: selection?.sessionId ?? null,
|
|
194
273
|
targetId: computer.selectedTarget?.id ?? null,
|
|
195
|
-
enabled:
|
|
274
|
+
enabled:
|
|
275
|
+
enabled &&
|
|
276
|
+
selection !== null &&
|
|
277
|
+
controllerReady &&
|
|
278
|
+
computer.selectedTarget !== null &&
|
|
279
|
+
isComputerFrameTarget(computer.selectedTarget),
|
|
196
280
|
stream: {
|
|
197
281
|
format: "jpeg",
|
|
198
282
|
quality: 78,
|
|
@@ -205,18 +289,69 @@ export function ComputerViewer({
|
|
|
205
289
|
const displayedFrame =
|
|
206
290
|
frames.frame &&
|
|
207
291
|
frames.frame.computerSessionId === selection?.sessionId &&
|
|
208
|
-
frames.frame.targetId === computer.selectedTarget?.id
|
|
292
|
+
frames.frame.targetId === computer.selectedTarget?.id &&
|
|
293
|
+
frames.frame.targetGeneration === computer.selectedTarget?.targetGeneration
|
|
209
294
|
? frames.frame
|
|
210
295
|
: null;
|
|
211
296
|
const rfbStream =
|
|
212
297
|
frames.attachment?.stream.kind === "direct_rfb" ? frames.attachment.stream : null;
|
|
298
|
+
const appTarget = computer.selectedTarget?.kind === "app";
|
|
299
|
+
const appInputPosture = useComputerInputPosture({
|
|
300
|
+
...override,
|
|
301
|
+
computerSessionId: selection?.sessionId ?? null,
|
|
302
|
+
controllerGeneration: computer.session?.controller?.controllerGeneration ?? null,
|
|
303
|
+
enabled: enabled && controllerReady && appTarget,
|
|
304
|
+
});
|
|
305
|
+
const inputPostureRef = useRef({
|
|
306
|
+
sessionId: selection?.sessionId ?? null,
|
|
307
|
+
controllerGeneration: computer.session?.controller?.controllerGeneration ?? null,
|
|
308
|
+
denied: false,
|
|
309
|
+
});
|
|
310
|
+
const postureSessionId = selection?.sessionId ?? null;
|
|
311
|
+
const postureController = computer.session?.controller?.controllerGeneration ?? null;
|
|
312
|
+
if (
|
|
313
|
+
inputPostureRef.current.sessionId !== postureSessionId ||
|
|
314
|
+
inputPostureRef.current.controllerGeneration !== postureController
|
|
315
|
+
) {
|
|
316
|
+
inputPostureRef.current = {
|
|
317
|
+
sessionId: postureSessionId,
|
|
318
|
+
controllerGeneration: postureController,
|
|
319
|
+
denied: false,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
if (
|
|
323
|
+
frames.attachment?.computerSessionId === postureSessionId &&
|
|
324
|
+
frames.attachment.controllerGeneration === postureController
|
|
325
|
+
) {
|
|
326
|
+
inputPostureRef.current.denied = frames.attachment.inputAllowed === false;
|
|
327
|
+
}
|
|
328
|
+
// Keep a known denial while a fresh attachment is pending or a semantic-only
|
|
329
|
+
// target has no stream. Only a fresh posture for this controller clears it.
|
|
330
|
+
const inputDenied = appTarget
|
|
331
|
+
? appInputPosture.inputAllowed !== true
|
|
332
|
+
: inputPostureRef.current.denied;
|
|
333
|
+
const attachmentInputAllowed = frames.attachment !== null && !inputDenied;
|
|
334
|
+
const inputPosturePending = appTarget
|
|
335
|
+
? appInputPosture.pending
|
|
336
|
+
: inputDenied && (frames.state === "attaching" || frames.state === "reconnecting");
|
|
337
|
+
const machineLocked = selectedRegistrySession?.failureCode === "machine_locked";
|
|
338
|
+
const controlUnavailable = computer.controlError !== null;
|
|
339
|
+
const connectionState = controlUnavailable ? "error" : frames.state;
|
|
340
|
+
// RFB has one combined input switch; partial native input stays view-only.
|
|
341
|
+
const rfbInputEnabled =
|
|
342
|
+
rfbStream?.inputAllowed === true &&
|
|
343
|
+
attachmentInputAllowed &&
|
|
344
|
+
!machineLocked &&
|
|
345
|
+
!controlUnavailable &&
|
|
346
|
+
computer.session?.capabilities?.pointerInput === true &&
|
|
347
|
+
computer.session?.capabilities?.keyboardInput === true;
|
|
213
348
|
const rfbCapability = useMemo<DesktopStreamCapability | null>(() => {
|
|
214
349
|
if (!rfbStream || !frames.attachment) return null;
|
|
215
350
|
const bounds = computer.selectedTarget?.bounds;
|
|
216
351
|
return {
|
|
217
352
|
transport: "vnc-ws",
|
|
218
353
|
client: "novnc",
|
|
219
|
-
mode: "interactive",
|
|
354
|
+
mode: rfbInputEnabled ? "interactive" : "read-only",
|
|
220
355
|
url: rfbStream.url,
|
|
221
356
|
token: null,
|
|
222
357
|
expiresAt: frames.attachment.expiresAt,
|
|
@@ -228,8 +363,7 @@ export function ComputerViewer({
|
|
|
228
363
|
sharedSessionIds: [],
|
|
229
364
|
reason: null,
|
|
230
365
|
};
|
|
231
|
-
}, [computer.selectedTarget?.bounds, frames.attachment, rfbStream]);
|
|
232
|
-
const machineLocked = selectedRegistrySession?.failureCode === "machine_locked";
|
|
366
|
+
}, [computer.selectedTarget?.bounds, frames.attachment, rfbInputEnabled, rfbStream]);
|
|
233
367
|
const currentIds = useMemo(() => new Set(relevant.map((session) => session.id)), [relevant]);
|
|
234
368
|
|
|
235
369
|
useEffect(() => {
|
|
@@ -257,54 +391,150 @@ export function ComputerViewer({
|
|
|
257
391
|
outcome === "completed" ? "Agent notified. Continuing work." : "Request cancelled.",
|
|
258
392
|
});
|
|
259
393
|
})
|
|
260
|
-
.catch((cause) => notifyError(cause, "Could not update the
|
|
394
|
+
.catch((cause) => notifyError(cause, "Could not update the desktop request."));
|
|
261
395
|
},
|
|
262
396
|
[interventions, notifyError, onNotify],
|
|
263
397
|
);
|
|
264
398
|
|
|
265
399
|
const createComputer = useCallback(() => {
|
|
266
|
-
if (
|
|
400
|
+
if (createInFlightRef.current) return;
|
|
401
|
+
createInFlightRef.current = true;
|
|
402
|
+
setCreateError(null);
|
|
267
403
|
setCreating(true);
|
|
268
|
-
void createSession({ sessionId, name: "
|
|
404
|
+
void createSession({ sessionId, name: "Desktop" })
|
|
269
405
|
.then((response) => {
|
|
270
|
-
|
|
406
|
+
selectComputerSession({ sessionId: response.session.id, pinned: false });
|
|
271
407
|
})
|
|
272
|
-
.catch((cause) =>
|
|
273
|
-
|
|
274
|
-
|
|
408
|
+
.catch((cause) => {
|
|
409
|
+
const error = cause instanceof Error ? cause : new Error("Could not open a desktop.");
|
|
410
|
+
setCreateError(error);
|
|
411
|
+
notifyError(error, "Could not open a desktop.");
|
|
412
|
+
})
|
|
413
|
+
.finally(() => {
|
|
414
|
+
createInFlightRef.current = false;
|
|
415
|
+
setCreating(false);
|
|
416
|
+
});
|
|
417
|
+
}, [createSession, notifyError, selectComputerSession, sessionId]);
|
|
418
|
+
|
|
419
|
+
useEffect(() => {
|
|
420
|
+
if (
|
|
421
|
+
!enabled ||
|
|
422
|
+
registry.loading ||
|
|
423
|
+
registry.refreshing ||
|
|
424
|
+
liveRelevant.length > 0 ||
|
|
425
|
+
!connectedPlacementLoss ||
|
|
426
|
+
recreatedPlacementLossRef.current.has(connectedPlacementLoss.id)
|
|
427
|
+
) {
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
recreatedPlacementLossRef.current.add(connectedPlacementLoss.id);
|
|
431
|
+
autoCreateSessionRef.current = sessionId;
|
|
432
|
+
emptyCatalogConfirmationRef.current = null;
|
|
433
|
+
createComputer();
|
|
434
|
+
}, [
|
|
435
|
+
connectedPlacementLoss,
|
|
436
|
+
createComputer,
|
|
437
|
+
enabled,
|
|
438
|
+
liveRelevant.length,
|
|
439
|
+
registry.loading,
|
|
440
|
+
registry.refreshing,
|
|
441
|
+
sessionId,
|
|
442
|
+
]);
|
|
443
|
+
|
|
444
|
+
useEffect(() => {
|
|
445
|
+
// Once this task's Desktop has been observed, a later transient empty
|
|
446
|
+
// registry snapshot must not create a second one. This can happen while the
|
|
447
|
+
// lazy surface is disabled/re-enabled or while an invalidation refresh
|
|
448
|
+
// briefly races the association projection. The existing Desktop remains
|
|
449
|
+
// the task's authority; explicit New desktop is still available.
|
|
450
|
+
if (relevant.length > 0) {
|
|
451
|
+
autoCreateSessionRef.current = sessionId;
|
|
452
|
+
emptyCatalogConfirmationRef.current = null;
|
|
453
|
+
}
|
|
454
|
+
}, [relevant.length, sessionId]);
|
|
455
|
+
|
|
456
|
+
useEffect(() => {
|
|
457
|
+
if (
|
|
458
|
+
!enabled ||
|
|
459
|
+
registry.loading ||
|
|
460
|
+
registry.refreshing ||
|
|
461
|
+
registry.error ||
|
|
462
|
+
relevant.length > 0 ||
|
|
463
|
+
suppressGenericCreate ||
|
|
464
|
+
autoCreateSessionRef.current === sessionId
|
|
465
|
+
) {
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
// A lazy surface can momentarily publish an empty settled projection while
|
|
469
|
+
// its initial catalog and a workspace invalidation cross. Confirm the empty
|
|
470
|
+
// result with one authoritative refresh before creating anything. A real
|
|
471
|
+
// empty catalog creates on the following render; an existing association
|
|
472
|
+
// appears and cancels this path.
|
|
473
|
+
if (emptyCatalogConfirmationRef.current !== sessionId) {
|
|
474
|
+
emptyCatalogConfirmationRef.current = sessionId;
|
|
475
|
+
void refreshRegistry();
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
autoCreateSessionRef.current = sessionId;
|
|
479
|
+
createComputer();
|
|
480
|
+
}, [
|
|
481
|
+
createComputer,
|
|
482
|
+
enabled,
|
|
483
|
+
refreshRegistry,
|
|
484
|
+
registry.error,
|
|
485
|
+
registry.loading,
|
|
486
|
+
registry.refreshing,
|
|
487
|
+
relevant.length,
|
|
488
|
+
sessionId,
|
|
489
|
+
suppressGenericCreate,
|
|
490
|
+
]);
|
|
275
491
|
|
|
276
492
|
const perform = useCallback(
|
|
277
493
|
async (action: ComputerAction, frame: ComputerFrame | null): Promise<void> => {
|
|
494
|
+
if (computer.controlError) throw computer.controlError;
|
|
495
|
+
if (inputDenied) throw new Error("Desktop is view only. Refresh to check desktop controls.");
|
|
496
|
+
let receipt;
|
|
278
497
|
if (action.type === "pointer") {
|
|
279
|
-
if (!frame) throw new Error("
|
|
280
|
-
await actFromFrame(action, frame);
|
|
498
|
+
if (!frame) throw new Error("Desktop view is not ready for pointer input.");
|
|
499
|
+
receipt = await actFromFrame(action, frame);
|
|
281
500
|
} else {
|
|
282
|
-
await act(action);
|
|
501
|
+
receipt = await act(action);
|
|
502
|
+
}
|
|
503
|
+
if (receipt.state !== "completed") {
|
|
504
|
+
throw new Error(receipt.error?.message ?? "Desktop input did not complete.");
|
|
283
505
|
}
|
|
284
506
|
},
|
|
285
|
-
[act, actFromFrame],
|
|
507
|
+
[act, actFromFrame, computer.controlError, inputDenied],
|
|
286
508
|
);
|
|
287
509
|
|
|
510
|
+
const reconnect = () => {
|
|
511
|
+
void refreshComputer();
|
|
512
|
+
if (appTarget) appInputPosture.refresh();
|
|
513
|
+
else frames.reconnect();
|
|
514
|
+
};
|
|
515
|
+
|
|
288
516
|
const copyFromRfb = useCallback(
|
|
289
517
|
(event: ClipboardEvent<HTMLDivElement>) => {
|
|
290
|
-
if (!rfbStream || computer.session?.capabilities?.clipboard !== true)
|
|
518
|
+
if (!rfbInputEnabled || !rfbStream || computer.session?.capabilities?.clipboard !== true)
|
|
519
|
+
return;
|
|
291
520
|
event.preventDefault();
|
|
292
521
|
event.stopPropagation();
|
|
293
522
|
void perform({ type: "clipboard", operation: "copy" }, null)
|
|
294
523
|
.then(() => computer.readClipboard())
|
|
295
524
|
.then(async (clipboard) => {
|
|
296
525
|
if (clipboard.text && !(await copyTextToClipboard(clipboard.text))) {
|
|
297
|
-
throw new Error("
|
|
526
|
+
throw new Error("Desktop text could not be copied to the local clipboard");
|
|
298
527
|
}
|
|
299
528
|
})
|
|
300
|
-
.catch((cause) => notifyError(cause, "Could not copy from the
|
|
529
|
+
.catch((cause) => notifyError(cause, "Could not copy from the desktop."));
|
|
301
530
|
},
|
|
302
|
-
[computer, notifyError, perform, rfbStream],
|
|
531
|
+
[computer, notifyError, perform, rfbInputEnabled, rfbStream],
|
|
303
532
|
);
|
|
304
533
|
|
|
305
534
|
const pasteIntoRfb = useCallback(
|
|
306
535
|
(text: string): boolean => {
|
|
307
|
-
if (!rfbStream || computer.session?.capabilities?.clipboard !== true)
|
|
536
|
+
if (!rfbInputEnabled || !rfbStream || computer.session?.capabilities?.clipboard !== true)
|
|
537
|
+
return false;
|
|
308
538
|
// Keep paste on the canonical ComputerSession action path. RFB
|
|
309
539
|
// ClientCutText synchronization varies by server and can acknowledge a
|
|
310
540
|
// local paste without ever updating the remote graphical seat. These two
|
|
@@ -316,49 +546,76 @@ export function ComputerViewer({
|
|
|
316
546
|
await perform({ type: "clipboard", operation: "write", text }, null);
|
|
317
547
|
assertExactComputerClipboard(await computer.readClipboard(), text);
|
|
318
548
|
await perform({ type: "clipboard", operation: "paste" }, null);
|
|
319
|
-
})().catch((cause) => notifyError(cause, "Could not paste into the
|
|
549
|
+
})().catch((cause) => notifyError(cause, "Could not paste into the desktop."));
|
|
320
550
|
return true;
|
|
321
551
|
},
|
|
322
|
-
[computer, notifyError, perform, rfbStream],
|
|
552
|
+
[computer, notifyError, perform, rfbInputEnabled, rfbStream],
|
|
323
553
|
);
|
|
324
554
|
|
|
555
|
+
const hideGenericCreate = suppressGenericCreate && liveRelevant.length === 0;
|
|
556
|
+
const generationLossSelected =
|
|
557
|
+
selectedRegistrySession !== null && isAttachedChromeGenerationLoss(selectedRegistrySession);
|
|
558
|
+
const generationLossFrames = isAttachedChromeGenerationLossError(frames.error);
|
|
559
|
+
|
|
325
560
|
if (!enabled) return null;
|
|
326
561
|
if (registry.loading && liveSessions.length === 0) {
|
|
327
562
|
return (
|
|
328
563
|
<ComputerNotice
|
|
329
564
|
icon={<LoaderCircleIcon className="size-4 animate-spin" />}
|
|
330
|
-
text="Finding workspace
|
|
565
|
+
text="Finding workspace desktops…"
|
|
331
566
|
{...(className ? { className } : {})}
|
|
332
567
|
/>
|
|
333
568
|
);
|
|
334
569
|
}
|
|
335
570
|
if (liveSessions.length === 0) {
|
|
571
|
+
if (hideGenericCreate) {
|
|
572
|
+
return (
|
|
573
|
+
<div className={cn("grid h-full place-items-center bg-og-bg p-6", className)}>
|
|
574
|
+
<div className="max-w-sm text-center">
|
|
575
|
+
<CircleAlertIcon className="mx-auto size-5 text-og-danger" />
|
|
576
|
+
<p className="mt-3 text-og-menu font-medium text-og-fg">
|
|
577
|
+
Chrome reconnected—open a fresh browser/desktop.
|
|
578
|
+
</p>
|
|
579
|
+
<p className="mt-1 text-og-control leading-5 text-og-fg-muted">
|
|
580
|
+
This Chrome profile is live again, but the previous desktop cannot move to the new
|
|
581
|
+
connection. Use Browser → New browser → Connected Chrome.
|
|
582
|
+
</p>
|
|
583
|
+
</div>
|
|
584
|
+
</div>
|
|
585
|
+
);
|
|
586
|
+
}
|
|
336
587
|
if (renderEmpty) return renderEmpty(createComputer, creating);
|
|
588
|
+
const error = createError ?? registry.error;
|
|
337
589
|
return (
|
|
338
590
|
<div className={cn("grid h-full place-items-center bg-og-bg p-6", className)}>
|
|
339
591
|
<div className="max-w-sm text-center">
|
|
340
|
-
<span className="mx-auto grid size-10 place-items-center rounded-og-lg border border-og-border bg-og-surface-1 text-og-muted">
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
<p className="mt-3 text-og-menu font-medium text-og-fg">No computer open</p>
|
|
344
|
-
<p className="mt-1 text-og-control leading-5 text-og-muted">
|
|
345
|
-
Open this agent's desktop, or switch to any computer another workspace agent uses.
|
|
346
|
-
</p>
|
|
347
|
-
<button
|
|
348
|
-
type="button"
|
|
349
|
-
onClick={createComputer}
|
|
350
|
-
disabled={creating}
|
|
351
|
-
className="mt-4 inline-flex h-8 items-center gap-1.5 rounded-og-sm bg-og-accent-deep px-3 text-og-control font-medium text-og-accent-fg transition hover:brightness-110 disabled:opacity-50"
|
|
352
|
-
>
|
|
353
|
-
{creating ? (
|
|
354
|
-
<LoaderCircleIcon className="size-3.5 animate-spin" />
|
|
592
|
+
<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">
|
|
593
|
+
{error ? (
|
|
594
|
+
<CircleAlertIcon className="size-5 text-og-danger" />
|
|
355
595
|
) : (
|
|
356
|
-
<
|
|
596
|
+
<LoaderCircleIcon className="size-5 animate-spin" />
|
|
357
597
|
)}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
598
|
+
</span>
|
|
599
|
+
<p className="mt-3 text-og-menu font-medium text-og-fg">
|
|
600
|
+
{error ? "Desktop didn’t open" : "Opening desktop…"}
|
|
601
|
+
</p>
|
|
602
|
+
<p className="mt-1 text-og-control leading-5 text-og-fg-muted">
|
|
603
|
+
{error?.message ?? "Preparing this agent’s desktop. It will appear here when ready."}
|
|
604
|
+
</p>
|
|
605
|
+
{error ? (
|
|
606
|
+
<button
|
|
607
|
+
type="button"
|
|
608
|
+
onClick={createError ? createComputer : () => void refreshRegistry()}
|
|
609
|
+
disabled={creating || registry.refreshing}
|
|
610
|
+
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"
|
|
611
|
+
>
|
|
612
|
+
{creating || registry.refreshing ? (
|
|
613
|
+
<LoaderCircleIcon className="size-3.5 animate-spin" />
|
|
614
|
+
) : (
|
|
615
|
+
<RotateCcwIcon className="size-3.5" />
|
|
616
|
+
)}
|
|
617
|
+
Try again
|
|
618
|
+
</button>
|
|
362
619
|
) : null}
|
|
363
620
|
</div>
|
|
364
621
|
</div>
|
|
@@ -366,22 +623,46 @@ export function ComputerViewer({
|
|
|
366
623
|
}
|
|
367
624
|
|
|
368
625
|
return (
|
|
369
|
-
<div
|
|
626
|
+
<div
|
|
627
|
+
className={cn(
|
|
628
|
+
"@container/computer-viewer relative flex h-full min-h-0 min-w-0 flex-col overflow-hidden bg-og-bg",
|
|
629
|
+
className,
|
|
630
|
+
)}
|
|
631
|
+
>
|
|
370
632
|
<ComputerToolbar
|
|
371
633
|
sessions={liveSessions}
|
|
372
634
|
relevantSessionIds={currentIds}
|
|
373
635
|
selectedSessionId={selection?.sessionId ?? null}
|
|
374
636
|
creating={creating}
|
|
375
|
-
refreshing={registry.refreshing}
|
|
637
|
+
refreshing={registry.refreshing || inputPosturePending}
|
|
376
638
|
interventionCounts={interventionCounts}
|
|
639
|
+
targets={controllerReady ? computer.targets : []}
|
|
640
|
+
selectedTargetId={computer.selectedTarget?.id ?? null}
|
|
641
|
+
targetsLoading={controllerReady && computer.loading}
|
|
642
|
+
onSelectTarget={(targetId) => {
|
|
643
|
+
void computer
|
|
644
|
+
.selectTarget(targetId)
|
|
645
|
+
.catch((cause) => notifyError(cause, "Could not switch desktop views."));
|
|
646
|
+
}}
|
|
377
647
|
onSelect={(computerSessionId) =>
|
|
378
|
-
|
|
648
|
+
selectComputerSession({ sessionId: computerSessionId, pinned: true })
|
|
379
649
|
}
|
|
380
650
|
onFollow={() => {
|
|
381
|
-
if (relevant[0])
|
|
651
|
+
if (relevant[0]) selectComputerSession({ sessionId: relevant[0].id, pinned: false });
|
|
652
|
+
}}
|
|
653
|
+
onCreate={hideGenericCreate ? undefined : createComputer}
|
|
654
|
+
onRefresh={() => {
|
|
655
|
+
if (appTarget) {
|
|
656
|
+
appInputPosture.refresh();
|
|
657
|
+
} else if (
|
|
658
|
+
(connectionState === "error" || inputDenied) &&
|
|
659
|
+
!generationLossSelected &&
|
|
660
|
+
!generationLossFrames
|
|
661
|
+
) {
|
|
662
|
+
frames.reconnect();
|
|
663
|
+
}
|
|
664
|
+
void Promise.all([refreshRegistry(), refreshComputer()]);
|
|
382
665
|
}}
|
|
383
|
-
onCreate={createComputer}
|
|
384
|
-
onRefresh={() => void Promise.all([refreshRegistry(), refreshComputer()])}
|
|
385
666
|
/>
|
|
386
667
|
<InteractionInterventionBanner
|
|
387
668
|
interventions={selectedInterventions}
|
|
@@ -390,7 +671,7 @@ export function ComputerViewer({
|
|
|
390
671
|
onOpen={(intervention) =>
|
|
391
672
|
void computer
|
|
392
673
|
.selectTarget(intervention.targetId)
|
|
393
|
-
.catch((cause) => notifyError(cause, "Could not open the requested
|
|
674
|
+
.catch((cause) => notifyError(cause, "Could not open the requested desktop view."))
|
|
394
675
|
}
|
|
395
676
|
onResolve={resolveIntervention}
|
|
396
677
|
/>
|
|
@@ -398,6 +679,8 @@ export function ComputerViewer({
|
|
|
398
679
|
<ComputerUnselectedPanel
|
|
399
680
|
peerCount={liveSessions.length}
|
|
400
681
|
creating={creating}
|
|
682
|
+
error={createError}
|
|
683
|
+
generationLoss={suppressGenericCreate}
|
|
401
684
|
onCreate={createComputer}
|
|
402
685
|
/>
|
|
403
686
|
) : !controllerReady ? (
|
|
@@ -409,53 +692,84 @@ export function ComputerViewer({
|
|
|
409
692
|
/>
|
|
410
693
|
) : (
|
|
411
694
|
<>
|
|
412
|
-
<
|
|
413
|
-
targets={computer.targets}
|
|
414
|
-
selectedTargetId={computer.selectedTarget?.id ?? null}
|
|
415
|
-
loading={computer.loading}
|
|
416
|
-
onSelect={(targetId) =>
|
|
417
|
-
void computer
|
|
418
|
-
.selectTarget(targetId)
|
|
419
|
-
.catch((cause) => notifyError(cause, "Could not switch computer views."))
|
|
420
|
-
}
|
|
421
|
-
/>
|
|
422
|
-
<div className="flex min-h-0 flex-1">
|
|
695
|
+
<div className="flex min-h-0 flex-1 flex-col @xl/computer-viewer:flex-row">
|
|
423
696
|
{rfbStream ? (
|
|
424
697
|
<div className="relative min-h-0 flex-1 bg-black" onCopyCapture={copyFromRfb}>
|
|
425
698
|
<DesktopViewer
|
|
426
699
|
capability={rfbCapability}
|
|
427
|
-
interactive
|
|
700
|
+
interactive={rfbInputEnabled}
|
|
428
701
|
showControlToggle={false}
|
|
429
702
|
webSocketProtocols={rfbStream.protocols}
|
|
430
703
|
onPasteText={pasteIntoRfb}
|
|
431
704
|
targetPlatform={computer.session?.platform ?? null}
|
|
432
705
|
className="h-full"
|
|
433
706
|
/>
|
|
707
|
+
{!rfbInputEnabled ? (
|
|
708
|
+
<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">
|
|
709
|
+
{controlUnavailable ? (
|
|
710
|
+
<>
|
|
711
|
+
Desktop controls unavailable{" "}
|
|
712
|
+
<button
|
|
713
|
+
type="button"
|
|
714
|
+
onClick={reconnect}
|
|
715
|
+
className="pointer-events-auto ml-2 font-medium underline"
|
|
716
|
+
>
|
|
717
|
+
Reconnect
|
|
718
|
+
</button>
|
|
719
|
+
</>
|
|
720
|
+
) : (
|
|
721
|
+
"View only · direct input unavailable"
|
|
722
|
+
)}
|
|
723
|
+
</div>
|
|
724
|
+
) : null}
|
|
434
725
|
</div>
|
|
435
726
|
) : (
|
|
436
727
|
<ComputerViewport
|
|
728
|
+
key={JSON.stringify([
|
|
729
|
+
selection?.sessionId,
|
|
730
|
+
computer.selectedTarget?.id,
|
|
731
|
+
computer.selectedTarget?.targetGeneration,
|
|
732
|
+
frames.attachment?.controllerGeneration,
|
|
733
|
+
])}
|
|
437
734
|
frame={machineLocked ? null : displayedFrame}
|
|
438
735
|
observation={computer.observation}
|
|
439
736
|
target={computer.selectedTarget}
|
|
440
737
|
machineLocked={machineLocked}
|
|
441
|
-
|
|
442
|
-
|
|
738
|
+
controlUnavailable={controlUnavailable}
|
|
739
|
+
inputAllowed={!inputDenied}
|
|
740
|
+
connectionState={connectionState}
|
|
741
|
+
connectionError={
|
|
742
|
+
computer.controlError ?? appInputPosture.error ?? frames.error ?? computer.error
|
|
743
|
+
}
|
|
443
744
|
mutating={computer.mutating}
|
|
444
745
|
backgroundActions={computer.session?.capabilities?.backgroundActions === true}
|
|
445
|
-
|
|
746
|
+
backgroundInput={computer.session?.capabilities?.backgroundInput === true}
|
|
747
|
+
clipboardEnabled={
|
|
748
|
+
attachmentInputAllowed && computer.session?.capabilities?.clipboard === true
|
|
749
|
+
}
|
|
750
|
+
pointerInput={
|
|
751
|
+
attachmentInputAllowed && computer.session?.capabilities?.pointerInput === true
|
|
752
|
+
}
|
|
753
|
+
keyboardInput={
|
|
754
|
+
attachmentInputAllowed && computer.session?.capabilities?.keyboardInput === true
|
|
755
|
+
}
|
|
446
756
|
onAction={perform}
|
|
447
757
|
onReadClipboard={computer.readClipboard}
|
|
448
|
-
onReconnect={
|
|
449
|
-
|
|
758
|
+
onReconnect={
|
|
759
|
+
generationLossSelected || generationLossFrames ? () => undefined : reconnect
|
|
760
|
+
}
|
|
761
|
+
onError={(cause) => notifyError(cause, "Desktop input failed.")}
|
|
450
762
|
/>
|
|
451
763
|
)}
|
|
452
764
|
{showControls ? (
|
|
453
765
|
<ComputerSemanticPanel
|
|
454
766
|
observation={computer.observation}
|
|
455
|
-
mutating={computer.mutating}
|
|
767
|
+
mutating={computer.mutating || controlUnavailable || inputDenied}
|
|
768
|
+
controlUnavailable={controlUnavailable}
|
|
769
|
+
inputAllowed={!inputDenied}
|
|
456
770
|
onAction={(action) =>
|
|
457
771
|
void perform(action, null).catch((cause) =>
|
|
458
|
-
notifyError(cause, "
|
|
772
|
+
notifyError(cause, "Desktop action failed."),
|
|
459
773
|
)
|
|
460
774
|
}
|
|
461
775
|
/>
|
|
@@ -464,10 +778,12 @@ export function ComputerViewer({
|
|
|
464
778
|
<ComputerStatusBar
|
|
465
779
|
session={computer.session}
|
|
466
780
|
target={computer.selectedTarget}
|
|
467
|
-
connectionState={
|
|
781
|
+
connectionState={connectionState}
|
|
782
|
+
controlUnavailable={controlUnavailable}
|
|
783
|
+
inputAllowed={!inputDenied}
|
|
468
784
|
refreshing={registry.refreshing}
|
|
469
785
|
showControls={showControls}
|
|
470
|
-
controlCount={
|
|
786
|
+
controlCount={semanticControls(computer.observation).length}
|
|
471
787
|
onToggleControls={() => setShowControls((current) => !current)}
|
|
472
788
|
/>
|
|
473
789
|
</>
|
|
@@ -479,33 +795,50 @@ export function ComputerViewer({
|
|
|
479
795
|
function ComputerUnselectedPanel(props: {
|
|
480
796
|
peerCount: number;
|
|
481
797
|
creating: boolean;
|
|
798
|
+
error: Error | null;
|
|
799
|
+
generationLoss: boolean;
|
|
482
800
|
onCreate: () => void;
|
|
483
801
|
}) {
|
|
484
802
|
return (
|
|
485
803
|
<div className="grid min-h-0 flex-1 place-items-center bg-og-bg p-6 text-center">
|
|
486
804
|
<div className="max-w-sm">
|
|
487
|
-
<span className="mx-auto grid size-10 place-items-center rounded-og-md border border-og-border bg-og-surface-1 text-og-muted">
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
<p className="mt-3 text-og-menu font-medium text-og-fg">No computer for this agent</p>
|
|
491
|
-
<p className="mt-1 text-og-control leading-5 text-og-muted">
|
|
492
|
-
{props.peerCount === 1
|
|
493
|
-
? "One workspace computer is available from the computer menu."
|
|
494
|
-
: `${props.peerCount} workspace computers are available from the computer menu.`}
|
|
495
|
-
</p>
|
|
496
|
-
<button
|
|
497
|
-
type="button"
|
|
498
|
-
disabled={props.creating}
|
|
499
|
-
onClick={props.onCreate}
|
|
500
|
-
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"
|
|
501
|
-
>
|
|
502
|
-
{props.creating ? (
|
|
503
|
-
<LoaderCircleIcon className="size-3.5 animate-spin" />
|
|
805
|
+
<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">
|
|
806
|
+
{props.error || props.generationLoss ? (
|
|
807
|
+
<CircleAlertIcon className="size-4.5 text-og-danger" />
|
|
504
808
|
) : (
|
|
505
|
-
<
|
|
809
|
+
<LoaderCircleIcon className="size-4.5 animate-spin" />
|
|
506
810
|
)}
|
|
507
|
-
|
|
508
|
-
|
|
811
|
+
</span>
|
|
812
|
+
<p className="mt-3 text-og-menu font-medium text-og-fg">
|
|
813
|
+
{props.generationLoss
|
|
814
|
+
? "Chrome reconnected—open a fresh browser/desktop."
|
|
815
|
+
: props.error
|
|
816
|
+
? "Desktop didn’t open"
|
|
817
|
+
: "Opening desktop…"}
|
|
818
|
+
</p>
|
|
819
|
+
<p className="mt-1 text-og-control leading-5 text-og-fg-muted">
|
|
820
|
+
{props.generationLoss
|
|
821
|
+
? "This Chrome profile is live again, but the previous desktop cannot move to the new connection. Use Browser → New browser → Connected Chrome."
|
|
822
|
+
: (props.error?.message ??
|
|
823
|
+
(props.peerCount === 1
|
|
824
|
+
? "Preparing this agent’s desktop. One peer desktop remains available from the menu."
|
|
825
|
+
: `Preparing this agent’s desktop. ${props.peerCount} peer desktops remain available from the menu.`))}
|
|
826
|
+
</p>
|
|
827
|
+
{props.error && !props.generationLoss ? (
|
|
828
|
+
<button
|
|
829
|
+
type="button"
|
|
830
|
+
disabled={props.creating}
|
|
831
|
+
onClick={props.onCreate}
|
|
832
|
+
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"
|
|
833
|
+
>
|
|
834
|
+
{props.creating ? (
|
|
835
|
+
<LoaderCircleIcon className="size-3.5 animate-spin" />
|
|
836
|
+
) : (
|
|
837
|
+
<RotateCcwIcon className="size-3.5" />
|
|
838
|
+
)}
|
|
839
|
+
Try again
|
|
840
|
+
</button>
|
|
841
|
+
) : null}
|
|
509
842
|
</div>
|
|
510
843
|
</div>
|
|
511
844
|
);
|
|
@@ -518,79 +851,116 @@ function ComputerToolbar(props: {
|
|
|
518
851
|
creating: boolean;
|
|
519
852
|
refreshing: boolean;
|
|
520
853
|
interventionCounts: Map<string, number>;
|
|
854
|
+
targets: ComputerTarget[];
|
|
855
|
+
selectedTargetId: string | null;
|
|
856
|
+
targetsLoading: boolean;
|
|
857
|
+
onSelectTarget: (id: string) => void;
|
|
521
858
|
onSelect: (id: string) => void;
|
|
522
859
|
onFollow: () => void;
|
|
523
|
-
onCreate
|
|
860
|
+
onCreate?: (() => void) | undefined;
|
|
524
861
|
onRefresh: () => void;
|
|
525
862
|
}) {
|
|
526
|
-
const detailsRef =
|
|
863
|
+
const detailsRef = useViewerMenuDismiss();
|
|
527
864
|
const selected = props.sessions.find((session) => session.id === props.selectedSessionId);
|
|
528
865
|
const current = props.sessions.filter((session) => props.relevantSessionIds.has(session.id));
|
|
529
866
|
const others = props.sessions.filter((session) => !props.relevantSessionIds.has(session.id));
|
|
530
|
-
const
|
|
531
|
-
props.onSelect(id);
|
|
867
|
+
const closeMenu = (restoreFocus: boolean) => {
|
|
532
868
|
detailsRef.current?.removeAttribute("open");
|
|
869
|
+
if (restoreFocus) detailsRef.current?.querySelector("summary")?.focus();
|
|
870
|
+
};
|
|
871
|
+
const choose = (id: string, restoreFocus: boolean) => {
|
|
872
|
+
props.onSelect(id);
|
|
873
|
+
closeMenu(restoreFocus);
|
|
533
874
|
};
|
|
534
875
|
return (
|
|
535
|
-
<div className="flex h-10 shrink-0 items-center gap-1.5 border-b border-og-border bg-og-surface-
|
|
536
|
-
<details ref={detailsRef} className="
|
|
537
|
-
<summary className="flex h-7 max-w-
|
|
538
|
-
<MonitorIcon className="size-3.5 shrink-0 text-og-muted" />
|
|
539
|
-
<span className="truncate font-medium">
|
|
876
|
+
<div className="flex h-10 shrink-0 items-center gap-1.5 border-b border-og-border bg-og-surface-1 px-2">
|
|
877
|
+
<details ref={detailsRef} className="min-w-0">
|
|
878
|
+
<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">
|
|
879
|
+
<MonitorIcon className="size-3.5 shrink-0 text-og-fg-muted" />
|
|
880
|
+
<span className="truncate font-medium">
|
|
881
|
+
{selected?.name === "Computer" ? "Desktop" : (selected?.name ?? "Desktop")}
|
|
882
|
+
</span>
|
|
540
883
|
{(props.interventionCounts.get(selected?.id ?? "") ?? 0) > 0 ? (
|
|
541
884
|
<span className="size-1.5 shrink-0 rounded-full bg-og-status-waiting" />
|
|
542
885
|
) : null}
|
|
543
|
-
<ChevronDownIcon className="size-3 shrink-0 text-og-subtle" />
|
|
886
|
+
<ChevronDownIcon className="size-3 shrink-0 text-og-fg-subtle" />
|
|
544
887
|
</summary>
|
|
545
|
-
<div className="absolute left-
|
|
546
|
-
<
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
888
|
+
<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">
|
|
889
|
+
<div className="min-h-0 max-h-96 overflow-y-auto overscroll-contain">
|
|
890
|
+
<ComputerSessionGroup
|
|
891
|
+
label="Current agent"
|
|
892
|
+
sessions={current}
|
|
893
|
+
selectedId={props.selectedSessionId}
|
|
894
|
+
interventionCounts={props.interventionCounts}
|
|
895
|
+
onSelect={choose}
|
|
896
|
+
/>
|
|
897
|
+
<ComputerSessionGroup
|
|
898
|
+
label="Other agents"
|
|
899
|
+
sessions={others}
|
|
900
|
+
selectedId={props.selectedSessionId}
|
|
901
|
+
interventionCounts={props.interventionCounts}
|
|
902
|
+
onSelect={choose}
|
|
903
|
+
/>
|
|
904
|
+
</div>
|
|
905
|
+
<div className="mt-1 flex shrink-0 gap-1 border-t border-og-border pt-1">
|
|
561
906
|
{current.length > 0 ? (
|
|
562
|
-
<MenuButton
|
|
907
|
+
<MenuButton
|
|
908
|
+
onClick={(event) => {
|
|
909
|
+
props.onFollow();
|
|
910
|
+
closeMenu(event.detail === 0);
|
|
911
|
+
}}
|
|
912
|
+
>
|
|
913
|
+
Follow agent
|
|
914
|
+
</MenuButton>
|
|
915
|
+
) : null}
|
|
916
|
+
{props.onCreate ? (
|
|
917
|
+
<MenuButton
|
|
918
|
+
onClick={(event) => {
|
|
919
|
+
props.onCreate?.();
|
|
920
|
+
closeMenu(event.detail === 0);
|
|
921
|
+
}}
|
|
922
|
+
disabled={props.creating}
|
|
923
|
+
>
|
|
924
|
+
<PlusIcon className="size-3.5" /> New desktop
|
|
925
|
+
</MenuButton>
|
|
563
926
|
) : null}
|
|
564
|
-
<MenuButton onClick={props.onCreate} disabled={props.creating}>
|
|
565
|
-
<PlusIcon className="size-3.5" /> New computer
|
|
566
|
-
</MenuButton>
|
|
567
927
|
</div>
|
|
568
928
|
</div>
|
|
569
929
|
</details>
|
|
570
|
-
<span className="min-w-0 flex-1 truncate text-og-xs text-og-subtle">
|
|
930
|
+
<span className="min-w-0 flex-1 truncate text-og-xs text-og-fg-subtle">
|
|
571
931
|
{selected ? `${platformLabel(selected)} · ${placementLabel(selected)}` : ""}
|
|
572
932
|
</span>
|
|
933
|
+
<ComputerTargetPicker
|
|
934
|
+
targets={props.targets}
|
|
935
|
+
selectedTargetId={props.selectedTargetId}
|
|
936
|
+
loading={props.targetsLoading}
|
|
937
|
+
onSelect={props.onSelectTarget}
|
|
938
|
+
/>
|
|
573
939
|
<button
|
|
574
940
|
type="button"
|
|
575
941
|
onClick={props.onRefresh}
|
|
576
|
-
|
|
577
|
-
aria-
|
|
942
|
+
disabled={props.refreshing}
|
|
943
|
+
aria-busy={props.refreshing}
|
|
944
|
+
className="grid size-7 place-items-center rounded-og-sm text-og-fg-muted transition hover:bg-og-surface-2 hover:text-og-fg"
|
|
945
|
+
aria-label="Refresh desktops"
|
|
578
946
|
>
|
|
579
947
|
<RefreshCwIcon className={cn("size-3.5", props.refreshing && "animate-spin")} />
|
|
580
948
|
</button>
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
949
|
+
{props.onCreate ? (
|
|
950
|
+
<button
|
|
951
|
+
type="button"
|
|
952
|
+
onClick={props.onCreate}
|
|
953
|
+
disabled={props.creating}
|
|
954
|
+
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"
|
|
955
|
+
aria-label="Open a new desktop"
|
|
956
|
+
>
|
|
957
|
+
{props.creating ? (
|
|
958
|
+
<LoaderCircleIcon className="size-3.5 animate-spin" />
|
|
959
|
+
) : (
|
|
960
|
+
<PlusIcon className="size-3.5" />
|
|
961
|
+
)}
|
|
962
|
+
</button>
|
|
963
|
+
) : null}
|
|
594
964
|
</div>
|
|
595
965
|
);
|
|
596
966
|
}
|
|
@@ -600,12 +970,12 @@ function ComputerSessionGroup(props: {
|
|
|
600
970
|
sessions: ComputerSession[];
|
|
601
971
|
selectedId: string | null;
|
|
602
972
|
interventionCounts: Map<string, number>;
|
|
603
|
-
onSelect: (id: string) => void;
|
|
973
|
+
onSelect: (id: string, restoreFocus: boolean) => void;
|
|
604
974
|
}) {
|
|
605
975
|
if (props.sessions.length === 0) return null;
|
|
606
976
|
return (
|
|
607
977
|
<div className="py-1">
|
|
608
|
-
<p className="px-2 pb-1 text-[10px] font-medium uppercase tracking-[0.12em] text-og-subtle">
|
|
978
|
+
<p className="px-2 pb-1 text-[10px] font-medium uppercase tracking-[0.12em] text-og-fg-subtle">
|
|
609
979
|
{props.label}
|
|
610
980
|
</p>
|
|
611
981
|
{props.sessions.map((session) => {
|
|
@@ -614,7 +984,8 @@ function ComputerSessionGroup(props: {
|
|
|
614
984
|
<button
|
|
615
985
|
key={session.id}
|
|
616
986
|
type="button"
|
|
617
|
-
onClick={() => props.onSelect(session.id)}
|
|
987
|
+
onClick={(event) => props.onSelect(session.id, event.detail === 0)}
|
|
988
|
+
aria-pressed={session.id === props.selectedId}
|
|
618
989
|
className={cn(
|
|
619
990
|
"flex w-full items-center gap-2 rounded-og-sm px-2 py-1.5 text-left transition hover:bg-og-surface-2",
|
|
620
991
|
session.id === props.selectedId && "bg-og-surface-2",
|
|
@@ -628,7 +999,7 @@ function ComputerSessionGroup(props: {
|
|
|
628
999
|
/>
|
|
629
1000
|
<span className="min-w-0 flex-1">
|
|
630
1001
|
<span className="block truncate text-og-control text-og-fg">{session.name}</span>
|
|
631
|
-
<span className="block truncate text-og-xs text-og-subtle">
|
|
1002
|
+
<span className="block truncate text-og-xs text-og-fg-subtle">
|
|
632
1003
|
{platformLabel(session)} · {placementLabel(session)}
|
|
633
1004
|
</span>
|
|
634
1005
|
</span>
|
|
@@ -644,65 +1015,119 @@ function ComputerSessionGroup(props: {
|
|
|
644
1015
|
);
|
|
645
1016
|
}
|
|
646
1017
|
|
|
647
|
-
function MenuButton(props: {
|
|
1018
|
+
function MenuButton(props: {
|
|
1019
|
+
children: ReactNode;
|
|
1020
|
+
onClick: (event: MouseEvent<HTMLButtonElement>) => void;
|
|
1021
|
+
disabled?: boolean;
|
|
1022
|
+
}) {
|
|
648
1023
|
return (
|
|
649
1024
|
<button
|
|
650
1025
|
type="button"
|
|
651
1026
|
onClick={props.onClick}
|
|
652
1027
|
disabled={props.disabled}
|
|
653
|
-
className="flex h-7 flex-1 items-center justify-center gap-1 rounded-og-sm px-2 text-og-control text-og-muted transition hover:bg-og-surface-2 hover:text-og-fg disabled:opacity-50"
|
|
1028
|
+
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"
|
|
654
1029
|
>
|
|
655
1030
|
{props.children}
|
|
656
1031
|
</button>
|
|
657
1032
|
);
|
|
658
1033
|
}
|
|
659
1034
|
|
|
660
|
-
function
|
|
1035
|
+
function ComputerTargetPicker(props: {
|
|
661
1036
|
targets: ComputerTarget[];
|
|
662
1037
|
selectedTargetId: string | null;
|
|
663
1038
|
loading: boolean;
|
|
664
1039
|
onSelect: (id: string) => void;
|
|
665
1040
|
}) {
|
|
666
|
-
const
|
|
1041
|
+
const screens = props.targets.filter((target) => target.kind === "screen");
|
|
1042
|
+
const advancedTargets = props.targets.filter(
|
|
1043
|
+
(target) =>
|
|
1044
|
+
target.kind === "app" || (target.kind === "window" && isRenderableComputerView(target)),
|
|
1045
|
+
);
|
|
1046
|
+
const advancedRef = useViewerMenuDismiss(advancedTargets.length > 0);
|
|
1047
|
+
const selected = props.targets.find((target) => target.id === props.selectedTargetId);
|
|
1048
|
+
const choose = (id: string, restoreFocus = false) => {
|
|
1049
|
+
props.onSelect(id);
|
|
1050
|
+
if (advancedRef.current?.open) {
|
|
1051
|
+
advancedRef.current.removeAttribute("open");
|
|
1052
|
+
if (restoreFocus) advancedRef.current.querySelector("summary")?.focus();
|
|
1053
|
+
}
|
|
1054
|
+
};
|
|
667
1055
|
return (
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
1056
|
+
<>
|
|
1057
|
+
{screens.length > 1 ? (
|
|
1058
|
+
<select
|
|
1059
|
+
aria-label="Screen"
|
|
1060
|
+
value={selected?.kind === "screen" ? selected.id : ""}
|
|
1061
|
+
onChange={(event) => choose(event.currentTarget.value)}
|
|
1062
|
+
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"
|
|
1063
|
+
>
|
|
1064
|
+
<option value="" disabled hidden>
|
|
1065
|
+
Screen
|
|
1066
|
+
</option>
|
|
1067
|
+
{screens.map((screen, index) => (
|
|
1068
|
+
<option key={screen.id} value={screen.id}>
|
|
1069
|
+
{screen.title || `Screen ${index + 1}`}
|
|
1070
|
+
</option>
|
|
1071
|
+
))}
|
|
1072
|
+
</select>
|
|
1073
|
+
) : screens[0] && selected?.kind !== "screen" ? (
|
|
673
1074
|
<button
|
|
674
|
-
key={target.id}
|
|
675
1075
|
type="button"
|
|
676
|
-
onClick={() =>
|
|
677
|
-
className=
|
|
678
|
-
"flex h-7 max-w-56 shrink-0 items-center gap-1.5 rounded-og-sm border px-2 text-og-control transition",
|
|
679
|
-
target.id === props.selectedTargetId
|
|
680
|
-
? "border-og-border-strong bg-og-surface-2 text-og-fg"
|
|
681
|
-
: "border-transparent text-og-muted hover:bg-og-surface-1 hover:text-og-fg",
|
|
682
|
-
)}
|
|
683
|
-
aria-pressed={target.id === props.selectedTargetId}
|
|
1076
|
+
onClick={() => choose(screens[0]!.id)}
|
|
1077
|
+
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"
|
|
684
1078
|
>
|
|
685
|
-
|
|
686
|
-
<MonitorIcon className="size-3.5 shrink-0" />
|
|
687
|
-
) : (
|
|
688
|
-
<PanelsTopLeftIcon className="size-3.5 shrink-0" />
|
|
689
|
-
)}
|
|
690
|
-
<span className="truncate">{target.title || target.applicationId || target.kind}</span>
|
|
691
|
-
{target.focused ? (
|
|
692
|
-
<span
|
|
693
|
-
className="size-1.5 shrink-0 rounded-full bg-og-status-running"
|
|
694
|
-
aria-label="Focused"
|
|
695
|
-
/>
|
|
696
|
-
) : null}
|
|
1079
|
+
<MonitorIcon className="size-3.5" /> Full desktop
|
|
697
1080
|
</button>
|
|
698
|
-
))}
|
|
699
|
-
{props.loading ? (
|
|
700
|
-
<LoaderCircleIcon className="ml-1 size-3.5 animate-spin text-og-muted" />
|
|
701
1081
|
) : null}
|
|
702
|
-
{
|
|
703
|
-
<
|
|
1082
|
+
{advancedTargets.length > 0 ? (
|
|
1083
|
+
<details ref={advancedRef} className="shrink-0">
|
|
1084
|
+
<summary
|
|
1085
|
+
aria-label="Advanced desktop views"
|
|
1086
|
+
title="Advanced desktop views"
|
|
1087
|
+
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"
|
|
1088
|
+
>
|
|
1089
|
+
<PanelsTopLeftIcon className="size-3.5" />
|
|
1090
|
+
<span className="hidden @sm/computer-viewer:inline">Advanced</span>
|
|
1091
|
+
<ChevronDownIcon className="size-3" />
|
|
1092
|
+
</summary>
|
|
1093
|
+
<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">
|
|
1094
|
+
<p className="shrink-0 px-2 py-1 text-og-xs text-og-fg-subtle">Apps and windows</p>
|
|
1095
|
+
<div className="min-h-0 max-h-80 overflow-y-auto overscroll-contain">
|
|
1096
|
+
{advancedTargets.map((target) => (
|
|
1097
|
+
<button
|
|
1098
|
+
key={target.id}
|
|
1099
|
+
type="button"
|
|
1100
|
+
onClick={(event) => choose(target.id, event.detail === 0)}
|
|
1101
|
+
aria-pressed={target.id === props.selectedTargetId}
|
|
1102
|
+
className={cn(
|
|
1103
|
+
"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",
|
|
1104
|
+
target.id === props.selectedTargetId && "bg-og-surface-2",
|
|
1105
|
+
)}
|
|
1106
|
+
>
|
|
1107
|
+
<span className="min-w-0 flex-1">
|
|
1108
|
+
<span className="block truncate text-og-control text-og-fg">
|
|
1109
|
+
{target.title || target.applicationId || target.kind}
|
|
1110
|
+
</span>
|
|
1111
|
+
<span className="block text-og-xs text-og-fg-subtle">
|
|
1112
|
+
{target.kind === "app" ? "App controls" : "Window view"}
|
|
1113
|
+
</span>
|
|
1114
|
+
</span>
|
|
1115
|
+
{target.focused ? (
|
|
1116
|
+
<span
|
|
1117
|
+
className="size-1.5 shrink-0 rounded-full bg-og-status-running"
|
|
1118
|
+
aria-label="Focused"
|
|
1119
|
+
/>
|
|
1120
|
+
) : null}
|
|
1121
|
+
</button>
|
|
1122
|
+
))}
|
|
1123
|
+
</div>
|
|
1124
|
+
</div>
|
|
1125
|
+
</details>
|
|
704
1126
|
) : null}
|
|
705
|
-
|
|
1127
|
+
{props.loading ? (
|
|
1128
|
+
<LoaderCircleIcon className="size-3.5 shrink-0 animate-spin text-og-fg-muted" />
|
|
1129
|
+
) : null}
|
|
1130
|
+
</>
|
|
706
1131
|
);
|
|
707
1132
|
}
|
|
708
1133
|
|
|
@@ -715,30 +1140,41 @@ function isRenderableComputerView(target: ComputerTarget): boolean {
|
|
|
715
1140
|
return target.bounds.width >= 160 && target.bounds.height >= 90;
|
|
716
1141
|
}
|
|
717
1142
|
|
|
1143
|
+
function isComputerFrameTarget(target: ComputerTarget): boolean {
|
|
1144
|
+
return target.kind === "window" || target.kind === "screen";
|
|
1145
|
+
}
|
|
1146
|
+
|
|
718
1147
|
function ComputerLifecyclePanel(props: {
|
|
719
1148
|
session: ComputerSession;
|
|
720
1149
|
creating: boolean;
|
|
721
1150
|
onRefresh: () => Promise<void>;
|
|
722
1151
|
onRetry: () => void;
|
|
723
1152
|
}) {
|
|
1153
|
+
const generationLoss = isAttachedChromeGenerationLoss(props.session);
|
|
724
1154
|
const failed = ["failed", "lost", "repair_required"].includes(props.session.lifecycle);
|
|
725
1155
|
const retryCreatesSession = ["failed", "lost"].includes(props.session.lifecycle);
|
|
726
1156
|
return (
|
|
727
1157
|
<div className="grid min-h-0 flex-1 place-items-center bg-og-bg p-6">
|
|
728
1158
|
<div className="max-w-sm text-center">
|
|
729
1159
|
{failed ? (
|
|
730
|
-
<CircleAlertIcon className="mx-auto size-5 text-og-
|
|
1160
|
+
<CircleAlertIcon className="mx-auto size-5 text-og-danger" />
|
|
731
1161
|
) : (
|
|
732
|
-
<LoaderCircleIcon className="mx-auto size-5 animate-spin text-og-muted" />
|
|
1162
|
+
<LoaderCircleIcon className="mx-auto size-5 animate-spin text-og-fg-muted" />
|
|
733
1163
|
)}
|
|
734
1164
|
<p className="mt-3 text-og-menu font-medium text-og-fg">
|
|
735
|
-
{
|
|
1165
|
+
{generationLoss
|
|
1166
|
+
? "Chrome reconnected—open a fresh browser/desktop."
|
|
1167
|
+
: failed
|
|
1168
|
+
? "Desktop needs attention"
|
|
1169
|
+
: lifecycleLabel(props.session.lifecycle)}
|
|
736
1170
|
</p>
|
|
737
|
-
<p className="mt-1 text-og-control leading-5 text-og-muted">
|
|
738
|
-
{
|
|
739
|
-
"
|
|
1171
|
+
<p className="mt-1 text-og-control leading-5 text-og-fg-muted">
|
|
1172
|
+
{generationLoss
|
|
1173
|
+
? "This Chrome profile is live again, but the previous desktop cannot move to the new connection. Use Browser → New browser → Connected Chrome."
|
|
1174
|
+
: (computerFailureMessage(props.session) ??
|
|
1175
|
+
"The desktop is being prepared on its placement. It will appear here when ready.")}
|
|
740
1176
|
</p>
|
|
741
|
-
{failed ? (
|
|
1177
|
+
{failed && !generationLoss ? (
|
|
742
1178
|
<button
|
|
743
1179
|
type="button"
|
|
744
1180
|
onClick={() => (retryCreatesSession ? props.onRetry() : void props.onRefresh())}
|
|
@@ -763,11 +1199,16 @@ function ComputerViewport(props: {
|
|
|
763
1199
|
observation: ComputerObservation | null;
|
|
764
1200
|
target: ComputerTarget | null;
|
|
765
1201
|
machineLocked: boolean;
|
|
1202
|
+
controlUnavailable: boolean;
|
|
1203
|
+
inputAllowed: boolean;
|
|
766
1204
|
connectionState: string;
|
|
767
1205
|
connectionError: Error | null;
|
|
768
1206
|
mutating: boolean;
|
|
769
1207
|
backgroundActions: boolean;
|
|
1208
|
+
backgroundInput: boolean;
|
|
770
1209
|
clipboardEnabled: boolean;
|
|
1210
|
+
pointerInput: boolean;
|
|
1211
|
+
keyboardInput: boolean;
|
|
771
1212
|
onAction: (action: ComputerAction, frame: ComputerFrame | null) => Promise<void>;
|
|
772
1213
|
onReadClipboard: () => Promise<ComputerClipboard>;
|
|
773
1214
|
onReconnect: () => void;
|
|
@@ -775,6 +1216,7 @@ function ComputerViewport(props: {
|
|
|
775
1216
|
}) {
|
|
776
1217
|
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
|
777
1218
|
const inputRef = useRef<HTMLTextAreaElement | null>(null);
|
|
1219
|
+
const composingRef = useRef(false);
|
|
778
1220
|
const pointerStartRef = useRef<PointerStart | null>(null);
|
|
779
1221
|
const clickTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
780
1222
|
const lastClickRef = useRef<{
|
|
@@ -799,14 +1241,58 @@ function ComputerViewport(props: {
|
|
|
799
1241
|
const readClipboardRef = useRef(props.onReadClipboard);
|
|
800
1242
|
const errorRef = useRef(props.onError);
|
|
801
1243
|
const actionTailRef = useRef<Promise<void>>(Promise.resolve());
|
|
1244
|
+
const actionQueueEpochRef = useRef(0);
|
|
802
1245
|
const queuedFrameRef = useRef<ComputerFrame | null>(null);
|
|
1246
|
+
const currentFrameRef = useRef<ComputerFrame | null>(props.frame);
|
|
1247
|
+
const paintedFrameRef = useRef<ComputerFrame | null>(null);
|
|
1248
|
+
const [paintedFrame, setPaintedFrame] = useState<ComputerFrame | null>(null);
|
|
803
1249
|
const decodingFrameRef = useRef(false);
|
|
804
1250
|
const mountedRef = useRef(true);
|
|
805
1251
|
actionRef.current = props.onAction;
|
|
806
1252
|
readClipboardRef.current = props.onReadClipboard;
|
|
807
1253
|
errorRef.current = props.onError;
|
|
1254
|
+
const streamFailed = props.connectionState === "error";
|
|
808
1255
|
const rawInputEnabled =
|
|
809
|
-
|
|
1256
|
+
props.target?.kind !== "app" &&
|
|
1257
|
+
!streamFailed &&
|
|
1258
|
+
!props.controlUnavailable &&
|
|
1259
|
+
!props.machineLocked &&
|
|
1260
|
+
(props.backgroundInput ||
|
|
1261
|
+
!props.backgroundActions ||
|
|
1262
|
+
props.target?.kind === "screen" ||
|
|
1263
|
+
props.target?.focused === true);
|
|
1264
|
+
|
|
1265
|
+
const clearBufferedInput = useCallback(() => {
|
|
1266
|
+
if (clickTimerRef.current) clearTimeout(clickTimerRef.current);
|
|
1267
|
+
if (wheelRef.current?.timer) clearTimeout(wheelRef.current.timer);
|
|
1268
|
+
if (pendingTextRef.current?.timer) clearTimeout(pendingTextRef.current.timer);
|
|
1269
|
+
clickTimerRef.current = null;
|
|
1270
|
+
wheelRef.current = null;
|
|
1271
|
+
pendingTextRef.current = null;
|
|
1272
|
+
pointerStartRef.current = null;
|
|
1273
|
+
lastClickRef.current = null;
|
|
1274
|
+
composingRef.current = false;
|
|
1275
|
+
if (inputRef.current) inputRef.current.value = "";
|
|
1276
|
+
}, []);
|
|
1277
|
+
|
|
1278
|
+
const pointerInputEnabled = rawInputEnabled && props.pointerInput;
|
|
1279
|
+
const keyboardInputEnabled = rawInputEnabled && props.keyboardInput;
|
|
1280
|
+
|
|
1281
|
+
useLayoutEffect(() => {
|
|
1282
|
+
const canvas = canvasRef.current;
|
|
1283
|
+
const viewport = canvas?.parentElement;
|
|
1284
|
+
if (!canvas || !viewport) return;
|
|
1285
|
+
const fit = () => fitComputerCanvas(canvas);
|
|
1286
|
+
fit();
|
|
1287
|
+
if (typeof ResizeObserver === "undefined") {
|
|
1288
|
+
window.addEventListener("resize", fit);
|
|
1289
|
+
return () => window.removeEventListener("resize", fit);
|
|
1290
|
+
}
|
|
1291
|
+
// The canvas is absolute, so fitting it cannot resize the observed dock.
|
|
1292
|
+
const observer = new ResizeObserver(fit);
|
|
1293
|
+
observer.observe(viewport);
|
|
1294
|
+
return () => observer.disconnect();
|
|
1295
|
+
}, []);
|
|
810
1296
|
|
|
811
1297
|
const paintQueuedFrames = useCallback(() => {
|
|
812
1298
|
if (decodingFrameRef.current) return;
|
|
@@ -824,8 +1310,15 @@ function ComputerViewport(props: {
|
|
|
824
1310
|
const bitmap = await createImageBitmap(blob);
|
|
825
1311
|
try {
|
|
826
1312
|
const canvas = canvasRef.current;
|
|
827
|
-
if (
|
|
1313
|
+
if (
|
|
1314
|
+
mountedRef.current &&
|
|
1315
|
+
canvas &&
|
|
1316
|
+
currentFrameRef.current &&
|
|
1317
|
+
sameComputerTarget(frame, currentFrameRef.current)
|
|
1318
|
+
) {
|
|
828
1319
|
paintCanvas(canvas, bitmap, frame.width, frame.height);
|
|
1320
|
+
paintedFrameRef.current = frame;
|
|
1321
|
+
setPaintedFrame(frame);
|
|
829
1322
|
}
|
|
830
1323
|
} finally {
|
|
831
1324
|
bitmap.close();
|
|
@@ -835,11 +1328,23 @@ function ComputerViewport(props: {
|
|
|
835
1328
|
objectUrl = URL.createObjectURL(blob);
|
|
836
1329
|
const image = await loadImage(objectUrl);
|
|
837
1330
|
const canvas = canvasRef.current;
|
|
838
|
-
if (
|
|
1331
|
+
if (
|
|
1332
|
+
mountedRef.current &&
|
|
1333
|
+
canvas &&
|
|
1334
|
+
currentFrameRef.current &&
|
|
1335
|
+
sameComputerTarget(frame, currentFrameRef.current)
|
|
1336
|
+
) {
|
|
839
1337
|
paintCanvas(canvas, image, frame.width, frame.height);
|
|
1338
|
+
paintedFrameRef.current = frame;
|
|
1339
|
+
setPaintedFrame(frame);
|
|
840
1340
|
}
|
|
841
1341
|
} catch (cause) {
|
|
842
|
-
if (
|
|
1342
|
+
if (
|
|
1343
|
+
mountedRef.current &&
|
|
1344
|
+
currentFrameRef.current &&
|
|
1345
|
+
sameComputerTarget(frame, currentFrameRef.current)
|
|
1346
|
+
)
|
|
1347
|
+
errorRef.current(cause);
|
|
843
1348
|
} finally {
|
|
844
1349
|
if (objectUrl) URL.revokeObjectURL(objectUrl);
|
|
845
1350
|
}
|
|
@@ -851,40 +1356,64 @@ function ComputerViewport(props: {
|
|
|
851
1356
|
})();
|
|
852
1357
|
}, []);
|
|
853
1358
|
|
|
854
|
-
|
|
1359
|
+
useLayoutEffect(() => {
|
|
855
1360
|
mountedRef.current = true;
|
|
856
1361
|
return () => {
|
|
857
1362
|
mountedRef.current = false;
|
|
1363
|
+
actionQueueEpochRef.current += 1;
|
|
858
1364
|
queuedFrameRef.current = null;
|
|
1365
|
+
clearBufferedInput();
|
|
859
1366
|
};
|
|
860
|
-
}, []);
|
|
1367
|
+
}, [clearBufferedInput]);
|
|
861
1368
|
|
|
862
|
-
|
|
863
|
-
if (!props.frame)
|
|
1369
|
+
useLayoutEffect(() => {
|
|
1370
|
+
if (currentFrameRef.current && !props.frame) {
|
|
1371
|
+
actionQueueEpochRef.current += 1;
|
|
1372
|
+
clearBufferedInput();
|
|
1373
|
+
}
|
|
1374
|
+
currentFrameRef.current = props.frame;
|
|
1375
|
+
if (!props.frame) {
|
|
1376
|
+
queuedFrameRef.current = null;
|
|
1377
|
+
paintedFrameRef.current = null;
|
|
1378
|
+
setPaintedFrame(null);
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
864
1381
|
queuedFrameRef.current = props.frame;
|
|
865
1382
|
paintQueuedFrames();
|
|
866
|
-
}, [paintQueuedFrames, props.frame]);
|
|
1383
|
+
}, [clearBufferedInput, paintQueuedFrames, props.frame]);
|
|
867
1384
|
|
|
868
|
-
|
|
869
|
-
(
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
[],
|
|
875
|
-
);
|
|
1385
|
+
useLayoutEffect(() => {
|
|
1386
|
+
if (!rawInputEnabled || !props.pointerInput || !props.keyboardInput) {
|
|
1387
|
+
actionQueueEpochRef.current += 1;
|
|
1388
|
+
clearBufferedInput();
|
|
1389
|
+
}
|
|
1390
|
+
}, [clearBufferedInput, props.keyboardInput, props.pointerInput, rawInputEnabled]);
|
|
876
1391
|
|
|
877
1392
|
const enqueue = useCallback(
|
|
878
|
-
(
|
|
1393
|
+
(
|
|
1394
|
+
action: ComputerAction,
|
|
1395
|
+
frame: ComputerFrame | null,
|
|
1396
|
+
after?: (isCurrent: () => boolean) => Promise<void>,
|
|
1397
|
+
) => {
|
|
1398
|
+
const epoch = actionQueueEpochRef.current;
|
|
1399
|
+
const dispatch = actionRef.current;
|
|
1400
|
+
const isCurrent = () => mountedRef.current && epoch === actionQueueEpochRef.current;
|
|
879
1401
|
actionTailRef.current = actionTailRef.current
|
|
880
1402
|
.catch(() => undefined)
|
|
881
1403
|
.then(async () => {
|
|
882
|
-
|
|
883
|
-
await
|
|
1404
|
+
if (!isCurrent()) return;
|
|
1405
|
+
await dispatch(action, frame);
|
|
1406
|
+
if (!isCurrent()) return;
|
|
1407
|
+
await after?.(isCurrent);
|
|
884
1408
|
})
|
|
885
|
-
.catch((cause) =>
|
|
1409
|
+
.catch((cause) => {
|
|
1410
|
+
if (!isCurrent()) return;
|
|
1411
|
+
actionQueueEpochRef.current += 1;
|
|
1412
|
+
clearBufferedInput();
|
|
1413
|
+
errorRef.current(cause);
|
|
1414
|
+
});
|
|
886
1415
|
},
|
|
887
|
-
[],
|
|
1416
|
+
[clearBufferedInput],
|
|
888
1417
|
);
|
|
889
1418
|
|
|
890
1419
|
const point = useCallback(
|
|
@@ -919,21 +1448,45 @@ function ComputerViewport(props: {
|
|
|
919
1448
|
);
|
|
920
1449
|
}, [enqueue]);
|
|
921
1450
|
|
|
1451
|
+
const flushPendingWheel = useCallback(() => {
|
|
1452
|
+
const batch = wheelRef.current;
|
|
1453
|
+
if (!batch) return;
|
|
1454
|
+
if (batch.timer) clearTimeout(batch.timer);
|
|
1455
|
+
wheelRef.current = null;
|
|
1456
|
+
enqueue(
|
|
1457
|
+
{
|
|
1458
|
+
type: "pointer",
|
|
1459
|
+
frameId: batch.frame.frameId,
|
|
1460
|
+
action: "scroll",
|
|
1461
|
+
x: batch.x,
|
|
1462
|
+
y: batch.y,
|
|
1463
|
+
deltaX: batch.deltaX,
|
|
1464
|
+
deltaY: batch.deltaY,
|
|
1465
|
+
},
|
|
1466
|
+
batch.frame,
|
|
1467
|
+
);
|
|
1468
|
+
}, [enqueue]);
|
|
1469
|
+
|
|
922
1470
|
const pointerDown = (event: PointerEvent<HTMLCanvasElement>) => {
|
|
923
|
-
|
|
1471
|
+
const frame = paintedFrameRef.current;
|
|
1472
|
+
if (!frame || props.mutating || !rawInputEnabled || event.button !== 0) return;
|
|
1473
|
+
// Preserve the keyboard sink's focus through the canvas pointer default action.
|
|
1474
|
+
event.preventDefault();
|
|
1475
|
+
if (keyboardInputEnabled) inputRef.current?.focus({ preventScroll: true });
|
|
1476
|
+
if (!pointerInputEnabled) return;
|
|
1477
|
+
flushPendingWheel();
|
|
924
1478
|
flushPendingText();
|
|
925
1479
|
pointerStartRef.current = {
|
|
926
1480
|
x: event.clientX,
|
|
927
1481
|
y: event.clientY,
|
|
928
1482
|
pointerId: event.pointerId,
|
|
929
|
-
frame
|
|
1483
|
+
frame,
|
|
930
1484
|
};
|
|
931
1485
|
event.currentTarget.setPointerCapture?.(event.pointerId);
|
|
932
|
-
inputRef.current?.focus({ preventScroll: true });
|
|
933
1486
|
};
|
|
934
1487
|
|
|
935
1488
|
const pointerUp = (event: PointerEvent<HTMLCanvasElement>) => {
|
|
936
|
-
if (!
|
|
1489
|
+
if (!pointerInputEnabled) return;
|
|
937
1490
|
const start = pointerStartRef.current;
|
|
938
1491
|
pointerStartRef.current = null;
|
|
939
1492
|
if (!start || start.pointerId !== event.pointerId) return;
|
|
@@ -998,9 +1551,10 @@ function ComputerViewport(props: {
|
|
|
998
1551
|
|
|
999
1552
|
const contextMenu = (event: MouseEvent<HTMLCanvasElement>) => {
|
|
1000
1553
|
event.preventDefault();
|
|
1001
|
-
if (!
|
|
1002
|
-
const frame =
|
|
1554
|
+
if (!pointerInputEnabled) return;
|
|
1555
|
+
const frame = paintedFrameRef.current;
|
|
1003
1556
|
if (!frame) return;
|
|
1557
|
+
flushPendingWheel();
|
|
1004
1558
|
flushPendingText();
|
|
1005
1559
|
flushPendingClick();
|
|
1006
1560
|
const at = point(frame, event.clientX, event.clientY);
|
|
@@ -1020,45 +1574,43 @@ function ComputerViewport(props: {
|
|
|
1020
1574
|
};
|
|
1021
1575
|
|
|
1022
1576
|
const wheel = (event: WheelEvent<HTMLCanvasElement>) => {
|
|
1023
|
-
if (!
|
|
1024
|
-
const frame =
|
|
1577
|
+
if (!pointerInputEnabled) return;
|
|
1578
|
+
const frame = paintedFrameRef.current;
|
|
1025
1579
|
if (!frame) return;
|
|
1026
1580
|
const at = point(frame, event.clientX, event.clientY);
|
|
1027
1581
|
if (!at) return;
|
|
1028
1582
|
flushPendingText();
|
|
1029
1583
|
flushPendingClick();
|
|
1030
1584
|
event.preventDefault();
|
|
1031
|
-
|
|
1032
|
-
if (
|
|
1585
|
+
let pending = wheelRef.current;
|
|
1586
|
+
if (
|
|
1587
|
+
pending &&
|
|
1588
|
+
(!sameComputerTarget(pending.frame, frame) ||
|
|
1589
|
+
Math.hypot(pending.x - at.x, pending.y - at.y) > 6)
|
|
1590
|
+
) {
|
|
1591
|
+
flushPendingWheel();
|
|
1592
|
+
pending = null;
|
|
1593
|
+
}
|
|
1033
1594
|
wheelRef.current = {
|
|
1034
1595
|
x: at.x,
|
|
1035
1596
|
y: at.y,
|
|
1036
|
-
deltaX: (pending
|
|
1037
|
-
deltaY: (pending
|
|
1597
|
+
deltaX: (pending?.deltaX ?? 0) + event.deltaX,
|
|
1598
|
+
deltaY: (pending?.deltaY ?? 0) + event.deltaY,
|
|
1038
1599
|
frame,
|
|
1039
|
-
timer: setTimeout(
|
|
1040
|
-
const batch = wheelRef.current;
|
|
1041
|
-
wheelRef.current = null;
|
|
1042
|
-
if (batch) {
|
|
1043
|
-
enqueue(
|
|
1044
|
-
{
|
|
1045
|
-
type: "pointer",
|
|
1046
|
-
frameId: batch.frame.frameId,
|
|
1047
|
-
action: "scroll",
|
|
1048
|
-
x: batch.x,
|
|
1049
|
-
y: batch.y,
|
|
1050
|
-
deltaX: batch.deltaX,
|
|
1051
|
-
deltaY: batch.deltaY,
|
|
1052
|
-
},
|
|
1053
|
-
batch.frame,
|
|
1054
|
-
);
|
|
1055
|
-
}
|
|
1056
|
-
}, 45),
|
|
1600
|
+
timer: pending?.timer ?? setTimeout(flushPendingWheel, 45),
|
|
1057
1601
|
};
|
|
1058
1602
|
};
|
|
1059
1603
|
|
|
1060
1604
|
const keyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
|
|
1061
|
-
if (!
|
|
1605
|
+
if (!keyboardInputEnabled) return;
|
|
1606
|
+
if (
|
|
1607
|
+
composingRef.current ||
|
|
1608
|
+
event.nativeEvent.isComposing ||
|
|
1609
|
+
event.nativeEvent.keyCode === 229
|
|
1610
|
+
) {
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
flushPendingWheel();
|
|
1062
1614
|
const command = event.metaKey || event.ctrlKey;
|
|
1063
1615
|
if (
|
|
1064
1616
|
props.clipboardEnabled &&
|
|
@@ -1078,33 +1630,43 @@ function ComputerViewport(props: {
|
|
|
1078
1630
|
};
|
|
1079
1631
|
|
|
1080
1632
|
const copy = (event: ClipboardEvent<HTMLTextAreaElement>) => {
|
|
1081
|
-
if (!props.clipboardEnabled || !
|
|
1633
|
+
if (!props.clipboardEnabled || !keyboardInputEnabled) return;
|
|
1082
1634
|
event.preventDefault();
|
|
1635
|
+
flushPendingWheel();
|
|
1083
1636
|
flushPendingText();
|
|
1084
1637
|
flushPendingClick();
|
|
1085
|
-
|
|
1086
|
-
|
|
1638
|
+
const readClipboard = readClipboardRef.current;
|
|
1639
|
+
enqueue({ type: "clipboard", operation: "copy" }, null, async (isCurrent) => {
|
|
1640
|
+
const clipboard = await readClipboard();
|
|
1641
|
+
if (!isCurrent()) return;
|
|
1087
1642
|
if (!clipboard.text) return;
|
|
1088
1643
|
if (!(await copyTextToClipboard(clipboard.text))) {
|
|
1089
|
-
throw new Error("
|
|
1644
|
+
throw new Error("Desktop text could not be copied to the local clipboard");
|
|
1090
1645
|
}
|
|
1091
1646
|
});
|
|
1092
1647
|
};
|
|
1093
1648
|
|
|
1094
1649
|
const paste = (event: ClipboardEvent<HTMLTextAreaElement>) => {
|
|
1095
|
-
if (!props.clipboardEnabled || !
|
|
1650
|
+
if (!props.clipboardEnabled || !keyboardInputEnabled) return;
|
|
1096
1651
|
event.preventDefault();
|
|
1652
|
+
flushPendingWheel();
|
|
1097
1653
|
flushPendingText();
|
|
1098
1654
|
flushPendingClick();
|
|
1099
1655
|
const text = event.clipboardData.getData("text/plain");
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1656
|
+
const readClipboard = readClipboardRef.current;
|
|
1657
|
+
const dispatch = actionRef.current;
|
|
1658
|
+
enqueue({ type: "clipboard", operation: "write", text }, null, async (isCurrent) => {
|
|
1659
|
+
const clipboard = await readClipboard();
|
|
1660
|
+
if (!isCurrent()) return;
|
|
1661
|
+
assertExactComputerClipboard(clipboard, text);
|
|
1662
|
+
await dispatch({ type: "clipboard", operation: "paste" }, null);
|
|
1103
1663
|
});
|
|
1104
1664
|
};
|
|
1105
1665
|
|
|
1106
|
-
const input = (value: string) => {
|
|
1107
|
-
if (
|
|
1666
|
+
const input = (value: string, nativeComposing = false) => {
|
|
1667
|
+
if (composingRef.current || nativeComposing) return;
|
|
1668
|
+
if (!value || !keyboardInputEnabled) return;
|
|
1669
|
+
flushPendingWheel();
|
|
1108
1670
|
flushPendingClick();
|
|
1109
1671
|
const pending = pendingTextRef.current;
|
|
1110
1672
|
if (pending) {
|
|
@@ -1120,7 +1682,17 @@ function ComputerViewport(props: {
|
|
|
1120
1682
|
if (inputRef.current) inputRef.current.value = "";
|
|
1121
1683
|
};
|
|
1122
1684
|
|
|
1123
|
-
const
|
|
1685
|
+
const compositionEnd = (event: CompositionEvent<HTMLTextAreaElement>) => {
|
|
1686
|
+
composingRef.current = false;
|
|
1687
|
+
input(event.currentTarget.value || event.data);
|
|
1688
|
+
};
|
|
1689
|
+
|
|
1690
|
+
const showCanvas =
|
|
1691
|
+
props.frame !== null &&
|
|
1692
|
+
paintedFrame !== null &&
|
|
1693
|
+
sameComputerTarget(props.frame, paintedFrame) &&
|
|
1694
|
+
!props.machineLocked &&
|
|
1695
|
+
!streamFailed;
|
|
1124
1696
|
return (
|
|
1125
1697
|
<div className="relative min-h-0 flex-1 overflow-hidden bg-black">
|
|
1126
1698
|
<canvas
|
|
@@ -1128,7 +1700,7 @@ function ComputerViewport(props: {
|
|
|
1128
1700
|
className={cn(
|
|
1129
1701
|
"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",
|
|
1130
1702
|
!showCanvas && "invisible",
|
|
1131
|
-
!
|
|
1703
|
+
!pointerInputEnabled && "cursor-default",
|
|
1132
1704
|
)}
|
|
1133
1705
|
onPointerDown={pointerDown}
|
|
1134
1706
|
onPointerUp={pointerUp}
|
|
@@ -1137,26 +1709,34 @@ function ComputerViewport(props: {
|
|
|
1137
1709
|
}}
|
|
1138
1710
|
onContextMenu={contextMenu}
|
|
1139
1711
|
onWheel={wheel}
|
|
1140
|
-
aria-label={`Interactive ${props.target?.kind ?? "
|
|
1712
|
+
aria-label={`Interactive ${props.target?.kind ?? "desktop"} view`}
|
|
1141
1713
|
/>
|
|
1142
1714
|
<textarea
|
|
1143
1715
|
ref={inputRef}
|
|
1144
1716
|
defaultValue=""
|
|
1145
|
-
onInput={(event) =>
|
|
1717
|
+
onInput={(event) =>
|
|
1718
|
+
input(event.currentTarget.value, (event.nativeEvent as InputEvent).isComposing)
|
|
1719
|
+
}
|
|
1720
|
+
onCompositionStart={() => {
|
|
1721
|
+
composingRef.current = true;
|
|
1722
|
+
}}
|
|
1723
|
+
onCompositionEnd={compositionEnd}
|
|
1146
1724
|
onKeyDown={keyDown}
|
|
1147
1725
|
onCopy={copy}
|
|
1148
1726
|
onPaste={paste}
|
|
1149
1727
|
className="pointer-events-none absolute left-1/2 top-1/2 size-px resize-none overflow-hidden opacity-0"
|
|
1150
|
-
aria-label="
|
|
1728
|
+
aria-label="Desktop keyboard input"
|
|
1151
1729
|
autoCapitalize="off"
|
|
1152
1730
|
autoCorrect="off"
|
|
1153
1731
|
spellCheck={false}
|
|
1154
|
-
disabled={!
|
|
1732
|
+
disabled={!keyboardInputEnabled}
|
|
1155
1733
|
/>
|
|
1156
1734
|
{!showCanvas ? (
|
|
1157
1735
|
<ComputerViewportFallback
|
|
1158
1736
|
observation={props.observation}
|
|
1159
1737
|
machineLocked={props.machineLocked}
|
|
1738
|
+
controlUnavailable={props.controlUnavailable}
|
|
1739
|
+
inputAllowed={props.inputAllowed}
|
|
1160
1740
|
connectionState={props.connectionState}
|
|
1161
1741
|
error={props.connectionError}
|
|
1162
1742
|
onAction={(action) => enqueue(action, null)}
|
|
@@ -1168,17 +1748,25 @@ function ComputerViewport(props: {
|
|
|
1168
1748
|
<LoaderCircleIcon className="size-3 animate-spin" /> Acting
|
|
1169
1749
|
</div>
|
|
1170
1750
|
) : null}
|
|
1171
|
-
{showCanvas && !rawInputEnabled ? (
|
|
1172
|
-
<div className="absolute bottom-3 left-1/2 flex -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">
|
|
1173
|
-
<span>
|
|
1174
|
-
|
|
1751
|
+
{showCanvas && (!rawInputEnabled || !props.pointerInput || !props.keyboardInput) ? (
|
|
1752
|
+
<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">
|
|
1753
|
+
<span>
|
|
1754
|
+
{!rawInputEnabled && (props.pointerInput || props.keyboardInput)
|
|
1755
|
+
? "Background view · use app controls"
|
|
1756
|
+
: !props.pointerInput && !props.keyboardInput
|
|
1757
|
+
? "View only · mouse and keyboard unavailable"
|
|
1758
|
+
: props.keyboardInput
|
|
1759
|
+
? "Keyboard only · mouse unavailable"
|
|
1760
|
+
: "Mouse only · keyboard unavailable"}
|
|
1761
|
+
</span>
|
|
1762
|
+
{!rawInputEnabled && props.target && (props.pointerInput || props.keyboardInput) ? (
|
|
1175
1763
|
<button
|
|
1176
1764
|
type="button"
|
|
1177
1765
|
disabled={props.mutating}
|
|
1178
1766
|
onClick={() => enqueue({ type: "focus", targetId: props.target!.id }, null)}
|
|
1179
|
-
className="rounded-full bg-white/10 px-2 py-0.5 font-medium text-white transition hover:bg-white/20 disabled:opacity-50"
|
|
1767
|
+
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"
|
|
1180
1768
|
>
|
|
1181
|
-
|
|
1769
|
+
Bring to front
|
|
1182
1770
|
</button>
|
|
1183
1771
|
) : null}
|
|
1184
1772
|
</div>
|
|
@@ -1189,13 +1777,15 @@ function ComputerViewport(props: {
|
|
|
1189
1777
|
|
|
1190
1778
|
function assertExactComputerClipboard(clipboard: ComputerClipboard, expected: string): void {
|
|
1191
1779
|
if (clipboard.truncated || clipboard.text !== expected) {
|
|
1192
|
-
throw new Error("
|
|
1780
|
+
throw new Error("Desktop clipboard did not accept the exact pasted text");
|
|
1193
1781
|
}
|
|
1194
1782
|
}
|
|
1195
1783
|
|
|
1196
1784
|
function ComputerViewportFallback(props: {
|
|
1197
1785
|
observation: ComputerObservation | null;
|
|
1198
1786
|
machineLocked: boolean;
|
|
1787
|
+
controlUnavailable: boolean;
|
|
1788
|
+
inputAllowed: boolean;
|
|
1199
1789
|
connectionState: string;
|
|
1200
1790
|
error: Error | null;
|
|
1201
1791
|
onAction: (action: ComputerAction) => void;
|
|
@@ -1205,36 +1795,60 @@ function ComputerViewportFallback(props: {
|
|
|
1205
1795
|
return (
|
|
1206
1796
|
<div className="absolute inset-0 grid place-items-center bg-og-bg p-6">
|
|
1207
1797
|
<div className="max-w-sm text-center">
|
|
1208
|
-
<LockKeyholeIcon className="mx-auto size-6 text-og-muted" />
|
|
1798
|
+
<LockKeyholeIcon className="mx-auto size-6 text-og-fg-muted" />
|
|
1209
1799
|
<p className="mt-3 text-og-menu font-medium text-og-fg">Machine locked</p>
|
|
1210
|
-
<p className="mt-1 text-og-control leading-5 text-og-muted">
|
|
1800
|
+
<p className="mt-1 text-og-control leading-5 text-og-fg-muted">
|
|
1211
1801
|
Live frames and input stay private until someone unlocks the machine.
|
|
1212
1802
|
</p>
|
|
1213
1803
|
</div>
|
|
1214
1804
|
</div>
|
|
1215
1805
|
);
|
|
1216
1806
|
}
|
|
1217
|
-
const
|
|
1807
|
+
const appControls = props.observation?.target.kind === "app";
|
|
1808
|
+
const controlFailure = interactionControlFailureFromError(props.error);
|
|
1809
|
+
const interactive = semanticControls(props.observation)
|
|
1218
1810
|
.filter((node) => semanticAction(node) !== null)
|
|
1219
1811
|
.slice(0, 10);
|
|
1220
1812
|
return (
|
|
1221
1813
|
<div className="absolute inset-0 grid place-items-center bg-og-bg p-6">
|
|
1222
|
-
<div className="w-full max-w-md rounded-og-lg border border-og-border bg-og-surface-
|
|
1814
|
+
<div className="w-full max-w-md rounded-og-lg border border-og-border bg-og-surface-1 p-4 shadow-lg">
|
|
1223
1815
|
<div className="flex items-center gap-2">
|
|
1224
1816
|
{props.error ? (
|
|
1225
|
-
<CircleAlertIcon className="size-4 text-og-
|
|
1817
|
+
<CircleAlertIcon className="size-4 text-og-danger" />
|
|
1818
|
+
) : appControls ? (
|
|
1819
|
+
<PanelsTopLeftIcon className="size-4 text-og-fg-muted" />
|
|
1226
1820
|
) : (
|
|
1227
|
-
<LoaderCircleIcon className="size-4 animate-spin text-og-muted" />
|
|
1821
|
+
<LoaderCircleIcon className="size-4 animate-spin text-og-fg-muted" />
|
|
1228
1822
|
)}
|
|
1229
1823
|
<p className="text-og-menu font-medium text-og-fg">
|
|
1230
|
-
{props.error
|
|
1231
|
-
? "
|
|
1232
|
-
:
|
|
1824
|
+
{isAttachedChromeGenerationLossError(props.error)
|
|
1825
|
+
? "Chrome reconnected—open a fresh browser/desktop."
|
|
1826
|
+
: !props.inputAllowed
|
|
1827
|
+
? "View only"
|
|
1828
|
+
: props.controlUnavailable
|
|
1829
|
+
? "Desktop controls unavailable"
|
|
1830
|
+
: props.error
|
|
1831
|
+
? "Live view disconnected"
|
|
1832
|
+
: appControls
|
|
1833
|
+
? "App controls"
|
|
1834
|
+
: computerConnectionLabel(props.connectionState)}
|
|
1233
1835
|
</p>
|
|
1234
1836
|
</div>
|
|
1235
|
-
{
|
|
1837
|
+
{props.error ? (
|
|
1838
|
+
<p className="mt-2 text-og-control leading-5 text-og-fg-muted">
|
|
1839
|
+
{isAttachedChromeGenerationLossError(props.error)
|
|
1840
|
+
? "Chrome reconnected—open a fresh browser/desktop. Use Browser → New browser → Connected Chrome."
|
|
1841
|
+
: (controlFailure?.message ?? props.error.message)}
|
|
1842
|
+
</p>
|
|
1843
|
+
) : null}
|
|
1844
|
+
{!props.inputAllowed ? (
|
|
1845
|
+
<p className="mt-2 text-og-control leading-5 text-og-fg-muted">
|
|
1846
|
+
Refresh to check desktop controls.
|
|
1847
|
+
</p>
|
|
1848
|
+
) : null}
|
|
1849
|
+
{interactive.length > 0 && !props.controlUnavailable && props.inputAllowed ? (
|
|
1236
1850
|
<div className="mt-3 border-t border-og-border pt-3">
|
|
1237
|
-
<p className="mb-2 text-og-xs text-og-subtle">
|
|
1851
|
+
<p className="mb-2 text-og-xs text-og-fg-subtle">App controls remain available</p>
|
|
1238
1852
|
<div className="flex flex-wrap gap-1.5">
|
|
1239
1853
|
{interactive.map((node) => (
|
|
1240
1854
|
<button
|
|
@@ -1252,13 +1866,13 @@ function ComputerViewportFallback(props: {
|
|
|
1252
1866
|
</div>
|
|
1253
1867
|
</div>
|
|
1254
1868
|
) : null}
|
|
1255
|
-
{props.error ? (
|
|
1869
|
+
{props.error && !isAttachedChromeGenerationLossError(props.error) ? (
|
|
1256
1870
|
<button
|
|
1257
1871
|
type="button"
|
|
1258
1872
|
onClick={props.onReconnect}
|
|
1259
1873
|
className="mt-3 text-og-control font-medium text-og-accent hover:underline"
|
|
1260
1874
|
>
|
|
1261
|
-
Reconnect
|
|
1875
|
+
{controlFailure?.retryable === false ? "Try again" : "Reconnect"}
|
|
1262
1876
|
</button>
|
|
1263
1877
|
) : null}
|
|
1264
1878
|
</div>
|
|
@@ -1269,18 +1883,31 @@ function ComputerViewportFallback(props: {
|
|
|
1269
1883
|
function ComputerSemanticPanel(props: {
|
|
1270
1884
|
observation: ComputerObservation | null;
|
|
1271
1885
|
mutating: boolean;
|
|
1886
|
+
controlUnavailable: boolean;
|
|
1887
|
+
inputAllowed: boolean;
|
|
1272
1888
|
onAction: (action: ComputerAction) => void;
|
|
1273
1889
|
}) {
|
|
1274
|
-
const nodes =
|
|
1890
|
+
const nodes = semanticControls(props.observation).slice(0, 100);
|
|
1275
1891
|
return (
|
|
1276
|
-
<aside className="w-
|
|
1277
|
-
<div className="mb-2 flex items-center gap-1.5 px-1 text-og-xs font-medium uppercase tracking-[0.1em] text-og-subtle">
|
|
1278
|
-
<KeyboardIcon className="size-3" />
|
|
1892
|
+
<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">
|
|
1893
|
+
<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">
|
|
1894
|
+
<KeyboardIcon className="size-3" /> App controls
|
|
1279
1895
|
</div>
|
|
1280
|
-
{
|
|
1281
|
-
<p className="px-1 py-2 text-og-control leading-5 text-og-muted">
|
|
1282
|
-
|
|
1896
|
+
{!props.inputAllowed ? (
|
|
1897
|
+
<p className="px-1 py-2 text-og-control leading-5 text-og-fg-muted">
|
|
1898
|
+
View only · refresh to check desktop controls.
|
|
1899
|
+
</p>
|
|
1900
|
+
) : props.controlUnavailable ? (
|
|
1901
|
+
<p className="px-1 py-2 text-og-control leading-5 text-og-fg-muted">
|
|
1902
|
+
Reconnect to use app controls.
|
|
1283
1903
|
</p>
|
|
1904
|
+
) : null}
|
|
1905
|
+
{nodes.length === 0 ? (
|
|
1906
|
+
props.controlUnavailable ? null : (
|
|
1907
|
+
<p className="px-1 py-2 text-og-control leading-5 text-og-fg-muted">
|
|
1908
|
+
This view has no accessibility controls. Use the live image instead.
|
|
1909
|
+
</p>
|
|
1910
|
+
)
|
|
1284
1911
|
) : (
|
|
1285
1912
|
<div className="space-y-0.5">
|
|
1286
1913
|
{nodes.map((node) => (
|
|
@@ -1318,6 +1945,7 @@ function ComputerSemanticControl(props: {
|
|
|
1318
1945
|
className="rounded-og-sm px-2 py-1.5 hover:bg-og-surface-2"
|
|
1319
1946
|
onSubmit={(event) => {
|
|
1320
1947
|
event.preventDefault();
|
|
1948
|
+
if (props.mutating) return;
|
|
1321
1949
|
props.onAction({
|
|
1322
1950
|
type: "semantic",
|
|
1323
1951
|
locator: { kind: "ref", ref: node.ref },
|
|
@@ -1326,7 +1954,7 @@ function ComputerSemanticControl(props: {
|
|
|
1326
1954
|
});
|
|
1327
1955
|
}}
|
|
1328
1956
|
>
|
|
1329
|
-
<label className="block truncate text-[10px] uppercase text-og-subtle">
|
|
1957
|
+
<label className="block truncate text-[10px] uppercase text-og-fg-subtle">
|
|
1330
1958
|
{node.name || node.identifier || node.role}
|
|
1331
1959
|
</label>
|
|
1332
1960
|
<div className="mt-1 flex gap-1">
|
|
@@ -1358,7 +1986,7 @@ function ComputerSemanticControl(props: {
|
|
|
1358
1986
|
}}
|
|
1359
1987
|
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"
|
|
1360
1988
|
>
|
|
1361
|
-
<span className="mt-0.5 text-[10px] uppercase text-og-subtle">{node.role}</span>
|
|
1989
|
+
<span className="mt-0.5 text-[10px] uppercase text-og-fg-subtle">{node.role}</span>
|
|
1362
1990
|
<span className="min-w-0 flex-1 truncate text-og-control text-og-fg">
|
|
1363
1991
|
{node.name || semanticValue(node) || node.identifier || "Unnamed control"}
|
|
1364
1992
|
</span>
|
|
@@ -1370,6 +1998,8 @@ function ComputerStatusBar(props: {
|
|
|
1370
1998
|
session: ComputerSession | null;
|
|
1371
1999
|
target: ComputerTarget | null;
|
|
1372
2000
|
connectionState: string;
|
|
2001
|
+
controlUnavailable: boolean;
|
|
2002
|
+
inputAllowed: boolean;
|
|
1373
2003
|
refreshing: boolean;
|
|
1374
2004
|
showControls: boolean;
|
|
1375
2005
|
controlCount: number;
|
|
@@ -1377,7 +2007,7 @@ function ComputerStatusBar(props: {
|
|
|
1377
2007
|
}) {
|
|
1378
2008
|
const screen = props.target?.kind === "screen";
|
|
1379
2009
|
return (
|
|
1380
|
-
<div className="flex h-8 shrink-0 items-center gap-2 border-t border-og-border bg-og-surface-
|
|
2010
|
+
<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">
|
|
1381
2011
|
<span
|
|
1382
2012
|
className={cn(
|
|
1383
2013
|
"size-1.5 rounded-full",
|
|
@@ -1385,28 +2015,48 @@ function ComputerStatusBar(props: {
|
|
|
1385
2015
|
)}
|
|
1386
2016
|
/>
|
|
1387
2017
|
<span>
|
|
1388
|
-
{props.
|
|
2018
|
+
{!props.inputAllowed
|
|
2019
|
+
? "View only"
|
|
2020
|
+
: props.controlUnavailable
|
|
2021
|
+
? "Controls unavailable"
|
|
2022
|
+
: props.target?.kind === "app"
|
|
2023
|
+
? "App controls"
|
|
2024
|
+
: props.connectionState === "live"
|
|
2025
|
+
? "Live"
|
|
2026
|
+
: computerConnectionLabel(props.connectionState)}
|
|
1389
2027
|
</span>
|
|
1390
2028
|
<span className="min-w-0 flex-1 truncate">
|
|
1391
|
-
{
|
|
1392
|
-
? "
|
|
1393
|
-
: props.
|
|
1394
|
-
? "
|
|
1395
|
-
:
|
|
2029
|
+
{!props.inputAllowed
|
|
2030
|
+
? "Refresh to check desktop controls"
|
|
2031
|
+
: props.controlUnavailable
|
|
2032
|
+
? "Reconnect to use desktop input"
|
|
2033
|
+
: screen
|
|
2034
|
+
? "Full screen · input may move pointer and focus"
|
|
2035
|
+
: props.target?.kind === "app"
|
|
2036
|
+
? props.session?.capabilities?.backgroundActions
|
|
2037
|
+
? "App · controls work in the background"
|
|
2038
|
+
: "App controls"
|
|
2039
|
+
: props.session?.capabilities?.backgroundInput
|
|
2040
|
+
? "Window · clicks and typing stay in the background"
|
|
2041
|
+
: props.session?.capabilities?.backgroundActions
|
|
2042
|
+
? "Window · app controls work in the background"
|
|
2043
|
+
: (props.target?.kind ?? "Desktop")}
|
|
1396
2044
|
</span>
|
|
1397
2045
|
{screen ? <MousePointer2Icon className="size-3" aria-hidden /> : null}
|
|
1398
2046
|
{props.refreshing ? <LoaderCircleIcon className="size-3 animate-spin" /> : null}
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
2047
|
+
{props.controlCount > 0 || props.showControls ? (
|
|
2048
|
+
<button
|
|
2049
|
+
type="button"
|
|
2050
|
+
onClick={props.onToggleControls}
|
|
2051
|
+
aria-pressed={props.showControls}
|
|
2052
|
+
className={cn(
|
|
2053
|
+
"rounded px-1.5 py-0.5 transition hover:bg-og-surface-2 hover:text-og-fg",
|
|
2054
|
+
props.showControls && "bg-og-surface-2 text-og-fg",
|
|
2055
|
+
)}
|
|
2056
|
+
>
|
|
2057
|
+
Controls {props.controlCount}
|
|
2058
|
+
</button>
|
|
2059
|
+
) : null}
|
|
1410
2060
|
</div>
|
|
1411
2061
|
);
|
|
1412
2062
|
}
|
|
@@ -1415,7 +2065,7 @@ function ComputerNotice(props: { icon: ReactNode; text: string; className?: stri
|
|
|
1415
2065
|
return (
|
|
1416
2066
|
<div
|
|
1417
2067
|
className={cn(
|
|
1418
|
-
"grid h-full place-items-center bg-og-bg text-og-control text-og-muted",
|
|
2068
|
+
"grid h-full place-items-center bg-og-bg text-og-control text-og-fg-muted",
|
|
1419
2069
|
props.className,
|
|
1420
2070
|
)}
|
|
1421
2071
|
>
|
|
@@ -1427,11 +2077,37 @@ function ComputerNotice(props: { icon: ReactNode; text: string; className?: stri
|
|
|
1427
2077
|
);
|
|
1428
2078
|
}
|
|
1429
2079
|
|
|
1430
|
-
|
|
2080
|
+
const structuralRoles = new Set([
|
|
2081
|
+
"frame",
|
|
2082
|
+
"panel",
|
|
2083
|
+
"section",
|
|
2084
|
+
"document",
|
|
2085
|
+
"document web",
|
|
2086
|
+
"static",
|
|
2087
|
+
"static text",
|
|
2088
|
+
"heading",
|
|
2089
|
+
"paragraph",
|
|
2090
|
+
"notification",
|
|
2091
|
+
"separator",
|
|
2092
|
+
"tool bar",
|
|
2093
|
+
"page tab list",
|
|
2094
|
+
]);
|
|
2095
|
+
|
|
2096
|
+
function semanticControls(observation: ComputerObservation | null): InteractionSemanticNode[] {
|
|
1431
2097
|
if (observation?.semantic?.kind !== "snapshot") return [];
|
|
1432
2098
|
const result: InteractionSemanticNode[] = [];
|
|
1433
2099
|
const visit = (node: InteractionSemanticNode) => {
|
|
1434
|
-
|
|
2100
|
+
const role = node.role.trim().toLowerCase();
|
|
2101
|
+
const label = node.name?.trim() || node.identifier?.trim();
|
|
2102
|
+
if (
|
|
2103
|
+
node.actions.includes("set_value") ||
|
|
2104
|
+
(label &&
|
|
2105
|
+
label.toLowerCase() !== role &&
|
|
2106
|
+
!structuralRoles.has(role) &&
|
|
2107
|
+
semanticAction(node) !== null)
|
|
2108
|
+
) {
|
|
2109
|
+
result.push(node);
|
|
2110
|
+
}
|
|
1435
2111
|
node.children?.forEach(visit);
|
|
1436
2112
|
};
|
|
1437
2113
|
observation.semantic.roots.forEach(visit);
|
|
@@ -1463,18 +2139,33 @@ function paintCanvas(
|
|
|
1463
2139
|
width: number,
|
|
1464
2140
|
height: number,
|
|
1465
2141
|
): void {
|
|
2142
|
+
const dimensionsChanged = canvas.width !== width || canvas.height !== height;
|
|
1466
2143
|
canvas.width = width;
|
|
1467
2144
|
canvas.height = height;
|
|
1468
2145
|
const context = canvas.getContext("2d", { alpha: false });
|
|
1469
|
-
if (!context) throw new Error("
|
|
2146
|
+
if (!context) throw new Error("Desktop canvas is unavailable.");
|
|
1470
2147
|
context.drawImage(image, 0, 0, width, height);
|
|
2148
|
+
if (dimensionsChanged) fitComputerCanvas(canvas);
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
function fitComputerCanvas(canvas: HTMLCanvasElement): void {
|
|
2152
|
+
const viewport = canvas.parentElement;
|
|
2153
|
+
if (!viewport || canvas.width <= 0 || canvas.height <= 0) return;
|
|
2154
|
+
const scale = Math.min(
|
|
2155
|
+
viewport.clientWidth / canvas.width,
|
|
2156
|
+
viewport.clientHeight / canvas.height,
|
|
2157
|
+
);
|
|
2158
|
+
// Hidden docks retain their previous fit until their content box is visible.
|
|
2159
|
+
if (!Number.isFinite(scale) || scale <= 0) return;
|
|
2160
|
+
canvas.style.width = `${canvas.width * scale}px`;
|
|
2161
|
+
canvas.style.height = `${canvas.height * scale}px`;
|
|
1471
2162
|
}
|
|
1472
2163
|
|
|
1473
2164
|
function loadImage(url: string): Promise<HTMLImageElement> {
|
|
1474
2165
|
return new Promise((resolve, reject) => {
|
|
1475
2166
|
const image = new Image();
|
|
1476
2167
|
image.onload = () => resolve(image);
|
|
1477
|
-
image.onerror = () => reject(new Error("
|
|
2168
|
+
image.onerror = () => reject(new Error("Desktop frame image could not be decoded."));
|
|
1478
2169
|
image.src = url;
|
|
1479
2170
|
});
|
|
1480
2171
|
}
|
|
@@ -1528,9 +2219,13 @@ export function computerKey(
|
|
|
1528
2219
|
}
|
|
1529
2220
|
|
|
1530
2221
|
function sameFrameFence(left: ComputerFrame, right: ComputerFrame): boolean {
|
|
2222
|
+
return left.frameId === right.frameId && sameComputerTarget(left, right);
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
function sameComputerTarget(left: ComputerFrame, right: ComputerFrame): boolean {
|
|
1531
2226
|
return (
|
|
1532
|
-
left.frameId === right.frameId &&
|
|
1533
2227
|
left.computerSessionId === right.computerSessionId &&
|
|
2228
|
+
left.controllerGeneration === right.controllerGeneration &&
|
|
1534
2229
|
left.targetId === right.targetId &&
|
|
1535
2230
|
left.targetGeneration === right.targetGeneration
|
|
1536
2231
|
);
|
|
@@ -1545,14 +2240,31 @@ function computerFailureMessage(session: ComputerSession): string | null {
|
|
|
1545
2240
|
case "machine_locked":
|
|
1546
2241
|
return "Unlock the connected Mac, then try again.";
|
|
1547
2242
|
case "controller_heartbeat_expired":
|
|
1548
|
-
return "The
|
|
2243
|
+
return "The desktop connection was lost. Try opening it again.";
|
|
2244
|
+
case "controller_transition_expired":
|
|
2245
|
+
return session.placement.kind === "attached_device"
|
|
2246
|
+
? "Chrome reconnected—open a fresh browser/desktop. Use Browser → New browser → Connected Chrome."
|
|
2247
|
+
: "The desktop connection expired. Open a new desktop.";
|
|
1549
2248
|
case null:
|
|
1550
2249
|
return null;
|
|
1551
2250
|
default:
|
|
1552
|
-
return "The
|
|
2251
|
+
return "The desktop could not be opened. Try again.";
|
|
1553
2252
|
}
|
|
1554
2253
|
}
|
|
1555
2254
|
|
|
2255
|
+
function isAttachedChromeGenerationLoss(session: ComputerSession): boolean {
|
|
2256
|
+
return (
|
|
2257
|
+
session.lifecycle === "lost" &&
|
|
2258
|
+
session.failureCode === "controller_transition_expired" &&
|
|
2259
|
+
session.placement.kind === "attached_device"
|
|
2260
|
+
);
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
function isAttachedChromeGenerationLossError(error: Error | null): boolean {
|
|
2264
|
+
if (!error) return false;
|
|
2265
|
+
return /placement instance changed|controller authority changed/i.test(error.message);
|
|
2266
|
+
}
|
|
2267
|
+
|
|
1556
2268
|
function countInterventions(
|
|
1557
2269
|
interventions: readonly InteractionIntervention[],
|
|
1558
2270
|
): Map<string, number> {
|
|
@@ -1573,7 +2285,7 @@ function interventionTitle(intervention: InteractionIntervention): string {
|
|
|
1573
2285
|
case "confirmation":
|
|
1574
2286
|
return "Action needed";
|
|
1575
2287
|
case "other":
|
|
1576
|
-
return "
|
|
2288
|
+
return "Desktop needs your help";
|
|
1577
2289
|
}
|
|
1578
2290
|
}
|
|
1579
2291
|
|
|
@@ -1586,7 +2298,7 @@ function platformLabel(session: ComputerSession): string {
|
|
|
1586
2298
|
case "windows":
|
|
1587
2299
|
return "Windows";
|
|
1588
2300
|
default:
|
|
1589
|
-
return "
|
|
2301
|
+
return "Desktop";
|
|
1590
2302
|
}
|
|
1591
2303
|
}
|
|
1592
2304
|
|
|
@@ -1599,31 +2311,31 @@ function placementLabel(session: ComputerSession): string {
|
|
|
1599
2311
|
case "attached_device":
|
|
1600
2312
|
return "Your machine";
|
|
1601
2313
|
case "external_provider":
|
|
1602
|
-
return "Cloud
|
|
2314
|
+
return "Cloud desktop";
|
|
1603
2315
|
}
|
|
1604
2316
|
}
|
|
1605
2317
|
|
|
1606
2318
|
function lifecycleLabel(lifecycle: ComputerSession["lifecycle"]): string {
|
|
1607
2319
|
switch (lifecycle) {
|
|
1608
2320
|
case "starting":
|
|
1609
|
-
return "Opening
|
|
2321
|
+
return "Opening desktop…";
|
|
1610
2322
|
case "suspending":
|
|
1611
|
-
return "Pausing
|
|
2323
|
+
return "Pausing desktop…";
|
|
1612
2324
|
case "suspended":
|
|
1613
|
-
return "
|
|
2325
|
+
return "Desktop paused";
|
|
1614
2326
|
case "restoring":
|
|
1615
|
-
return "Restoring
|
|
2327
|
+
return "Restoring desktop…";
|
|
1616
2328
|
case "ending":
|
|
1617
|
-
return "Closing
|
|
2329
|
+
return "Closing desktop…";
|
|
1618
2330
|
default:
|
|
1619
|
-
return "Connecting to
|
|
2331
|
+
return "Connecting to desktop…";
|
|
1620
2332
|
}
|
|
1621
2333
|
}
|
|
1622
2334
|
|
|
1623
2335
|
function computerConnectionLabel(state: string): string {
|
|
1624
2336
|
switch (state) {
|
|
1625
2337
|
case "attaching":
|
|
1626
|
-
return "Opening
|
|
2338
|
+
return "Opening desktop view…";
|
|
1627
2339
|
case "connecting":
|
|
1628
2340
|
return "Connecting…";
|
|
1629
2341
|
case "reconnecting":
|
|
@@ -1631,6 +2343,6 @@ function computerConnectionLabel(state: string): string {
|
|
|
1631
2343
|
case "error":
|
|
1632
2344
|
return "Disconnected";
|
|
1633
2345
|
default:
|
|
1634
|
-
return "Waiting for
|
|
2346
|
+
return "Waiting for desktop…";
|
|
1635
2347
|
}
|
|
1636
2348
|
}
|