@opengeni/react 7.4.0 → 7.5.0-canary.36919050519001

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.
Files changed (210) hide show
  1. package/README.md +124 -27
  2. package/dist/artifacts-document.js +3 -2
  3. package/dist/artifacts-presentation.js +3 -2
  4. package/dist/artifacts-spreadsheet.js +2 -2
  5. package/dist/artifacts.d.ts +1 -0
  6. package/dist/artifacts.js +516 -1413
  7. package/dist/artifacts.js.map +1 -1
  8. package/dist/{browser-viewer-2E6R2Y4W.js → browser-viewer-Z7S7QEJL.js} +6 -5
  9. package/dist/chat-interactive-block-UGBTNB3L.js +11 -0
  10. package/dist/{chunk-Y3QTZN2F.js → chunk-2VFNCX4E.js} +6 -6
  11. package/dist/{chunk-UDD6DLLT.js → chunk-3IJJEYW4.js} +15 -3
  12. package/dist/chunk-3IJJEYW4.js.map +1 -0
  13. package/dist/chunk-4RVYOY4O.js +379 -0
  14. package/dist/chunk-4RVYOY4O.js.map +1 -0
  15. package/dist/chunk-5CVXZA3Y.js +292 -0
  16. package/dist/chunk-5CVXZA3Y.js.map +1 -0
  17. package/dist/{chunk-5YK2ZDIG.js → chunk-5J4FOW6Q.js} +2 -10
  18. package/dist/chunk-5J4FOW6Q.js.map +1 -0
  19. package/dist/{chunk-KVERXCFY.js → chunk-5KYZIQFY.js} +40 -3
  20. package/dist/chunk-5KYZIQFY.js.map +1 -0
  21. package/dist/{chunk-KNDIXZN6.js → chunk-7UAMYXJY.js} +25 -18
  22. package/dist/{chunk-KNDIXZN6.js.map → chunk-7UAMYXJY.js.map} +1 -1
  23. package/dist/{chunk-SEEVCT36.js → chunk-A4UJB2MM.js} +2 -2
  24. package/dist/{chunk-X4HIN6TV.js → chunk-AFLPWUQ7.js} +71 -162
  25. package/dist/chunk-AFLPWUQ7.js.map +1 -0
  26. package/dist/chunk-B3V6TG77.js +41 -0
  27. package/dist/chunk-B3V6TG77.js.map +1 -0
  28. package/dist/{chunk-XEJEOAOV.js → chunk-B7WMS3CS.js} +51 -40
  29. package/dist/chunk-B7WMS3CS.js.map +1 -0
  30. package/dist/{chunk-S5JQ4R22.js → chunk-BDKR6BAR.js} +54 -24
  31. package/dist/chunk-BDKR6BAR.js.map +1 -0
  32. package/dist/{chunk-4MDLEHXL.js → chunk-DYWBUX5Q.js} +24 -12
  33. package/dist/chunk-DYWBUX5Q.js.map +1 -0
  34. package/dist/{chunk-YZ3YYMOM.js → chunk-E3G7SAYZ.js} +252 -624
  35. package/dist/chunk-E3G7SAYZ.js.map +1 -0
  36. package/dist/chunk-JT4JW7BC.js +54 -0
  37. package/dist/chunk-JT4JW7BC.js.map +1 -0
  38. package/dist/{chunk-5AZSK6MV.js → chunk-LWNLETQT.js} +5 -5
  39. package/dist/{chunk-USXVQACU.js → chunk-M7LBYP63.js} +5 -4
  40. package/dist/{chunk-USXVQACU.js.map → chunk-M7LBYP63.js.map} +1 -1
  41. package/dist/{chunk-L4OSXQ6H.js → chunk-NBOHNRLJ.js} +1289 -1060
  42. package/dist/chunk-NBOHNRLJ.js.map +1 -0
  43. package/dist/chunk-RHREZ7BO.js +11 -0
  44. package/dist/chunk-RHREZ7BO.js.map +1 -0
  45. package/dist/chunk-SCOTPE2I.js +589 -0
  46. package/dist/chunk-SCOTPE2I.js.map +1 -0
  47. package/dist/chunk-WEHMPOXK.js +39 -0
  48. package/dist/chunk-WEHMPOXK.js.map +1 -0
  49. package/dist/chunk-WRHIZYZF.js +988 -0
  50. package/dist/chunk-WRHIZYZF.js.map +1 -0
  51. package/dist/{chunk-3E2AITTZ.js → chunk-WUISZ3WU.js} +32 -28
  52. package/dist/{chunk-3E2AITTZ.js.map → chunk-WUISZ3WU.js.map} +1 -1
  53. package/dist/chunk-XCHWDJ22.js +107 -0
  54. package/dist/chunk-XCHWDJ22.js.map +1 -0
  55. package/dist/{chunk-DUEBTL3N.js → chunk-Y2PRMYLC.js} +4 -3
  56. package/dist/{chunk-DUEBTL3N.js.map → chunk-Y2PRMYLC.js.map} +1 -1
  57. package/dist/chunk-YBXQDQFX.js +1772 -0
  58. package/dist/chunk-YBXQDQFX.js.map +1 -0
  59. package/dist/chunk-YV4L3ILF.js +9 -0
  60. package/dist/chunk-YV4L3ILF.js.map +1 -0
  61. package/dist/{chunk-V2ABIVYJ.js → chunk-ZXSEDZP7.js} +70 -6
  62. package/dist/chunk-ZXSEDZP7.js.map +1 -0
  63. package/dist/components/artifacts/artifact-chrome.d.ts +120 -0
  64. package/dist/components/artifacts/artifact-sandbox.d.ts +27 -0
  65. package/dist/components/artifacts/chat-interactive-block.d.ts +29 -0
  66. package/dist/components/artifacts/deferred-chat-media.d.ts +15 -0
  67. package/dist/components/artifacts/editable-artifact-ui.d.ts +2 -1
  68. package/dist/components/artifacts/editable-artifact-view.d.ts +46 -0
  69. package/dist/components/artifacts/index.d.ts +7 -0
  70. package/dist/components/artifacts/latest-task-runner.d.ts +15 -0
  71. package/dist/components/artifacts/session-artifact-viewer.d.ts +39 -0
  72. package/dist/components/artifacts/site-view.d.ts +23 -0
  73. package/dist/components/browser-select-control.d.ts +3 -1
  74. package/dist/components/code-editor.d.ts +1 -4
  75. package/dist/components/desktop-viewer.d.ts +1 -1
  76. package/dist/components/message-timeline.d.ts +20 -3
  77. package/dist/components/open-geni-links.d.ts +16 -0
  78. package/dist/components/session-conversation.d.ts +20 -1
  79. package/dist/components/timeline-pill-geometry.d.ts +8 -0
  80. package/dist/components/timeline-pill-placement.d.ts +24 -0
  81. package/dist/components/usage-limit-notice.d.ts +49 -0
  82. package/dist/components/usage-member-list.d.ts +91 -0
  83. package/dist/components/usage-meter.d.ts +73 -0
  84. package/dist/composer.js +5 -5
  85. package/dist/{computer-viewer-WMJE6OA3.js → computer-viewer-KJWLH3GI.js} +7 -5
  86. package/dist/connect.js +7 -6
  87. package/dist/connect.js.map +1 -1
  88. package/dist/conversation-timeline.d.ts +1 -1
  89. package/dist/desktop.d.ts +7 -0
  90. package/dist/desktop.js +26 -0
  91. package/dist/desktop.js.map +1 -0
  92. package/dist/editor.d.ts +7 -0
  93. package/dist/editor.js +21 -0
  94. package/dist/editor.js.map +1 -0
  95. package/dist/hooks/latest-question.d.ts +1 -1
  96. package/dist/hooks/use-composer.d.ts +4 -0
  97. package/dist/hooks/use-desktop-stream.d.ts +1 -1
  98. package/dist/hooks/use-usage.d.ts +40 -0
  99. package/dist/index.d.ts +7 -2
  100. package/dist/index.js +460 -1386
  101. package/dist/index.js.map +1 -1
  102. package/dist/interaction.js +8 -6
  103. package/dist/interaction.js.map +1 -1
  104. package/dist/{latest-question-QB4EUCW5.js → latest-question-IS445HOW.js} +3 -3
  105. package/dist/lib/format.d.ts +6 -1
  106. package/dist/lib/workbench-peers.d.ts +36 -0
  107. package/dist/machines.js +2 -2
  108. package/dist/{markdown-table-layout-PFUEZ2U4.js → markdown-table-layout-QXSLK4BY.js} +37 -5
  109. package/dist/markdown-table-layout-QXSLK4BY.js.map +1 -0
  110. package/dist/{queue-surface-implementation-IM2HD3SK.js → queue-surface-implementation-2CAQAIHM.js} +2 -2
  111. package/dist/realtime.js +3 -3
  112. package/dist/{sandbox-files-PXPIZAFG.js → sandbox-files-AEJTWGT4.js} +8 -5
  113. package/dist/sandbox-files-AEJTWGT4.js.map +1 -0
  114. package/dist/session-ui.d.ts +9 -2
  115. package/dist/session-ui.js +39 -20
  116. package/dist/session-ui.js.map +1 -1
  117. package/dist/session.js +4 -4
  118. package/dist/sites.js +4 -289
  119. package/dist/sites.js.map +1 -1
  120. package/dist/terminal.d.ts +8 -0
  121. package/dist/terminal.js +31 -0
  122. package/dist/terminal.js.map +1 -0
  123. package/dist/timeline/allowance-exhausted-row.d.ts +24 -0
  124. package/dist/timeline/genie-loading.d.ts +3 -1
  125. package/dist/timeline/types.d.ts +8 -1
  126. package/dist/{timeline-annotation-selection-ODLVF4OI.js → timeline-annotation-selection-DMLMM2TX.js} +6 -1
  127. package/dist/timeline-annotation-selection-DMLMM2TX.js.map +1 -0
  128. package/dist/timeline-pill-geometry-Z23V243M.js +110 -0
  129. package/dist/timeline-pill-geometry-Z23V243M.js.map +1 -0
  130. package/dist/usage/allowance-copy.d.ts +25 -0
  131. package/dist/usage/summary.d.ts +35 -0
  132. package/dist/usage.d.ts +13 -0
  133. package/dist/usage.js +1073 -0
  134. package/dist/usage.js.map +1 -0
  135. package/package.json +19 -3
  136. package/src/artifacts.ts +37 -0
  137. package/src/components/artifacts/artifact-chrome.tsx +413 -0
  138. package/src/components/artifacts/artifact-sandbox.tsx +185 -0
  139. package/src/components/artifacts/chat-interactive-block.tsx +260 -0
  140. package/src/components/artifacts/deferred-chat-media.tsx +67 -0
  141. package/src/components/artifacts/editable-artifact-ui.tsx +3 -1
  142. package/src/components/artifacts/editable-artifact-view.tsx +187 -0
  143. package/src/components/artifacts/editable-artifact-workbench.tsx +5 -2
  144. package/src/components/artifacts/editable-document.tsx +34 -36
  145. package/src/components/artifacts/editable-presentation.tsx +21 -16
  146. package/src/components/artifacts/index.ts +41 -0
  147. package/src/components/artifacts/latest-task-runner.ts +49 -0
  148. package/src/components/artifacts/session-artifact-viewer.tsx +376 -0
  149. package/src/components/artifacts/site-view.tsx +116 -0
  150. package/src/components/browser-select-control.tsx +4 -1
  151. package/src/components/browser-viewer.tsx +81 -22
  152. package/src/components/code-editor.tsx +13 -16
  153. package/src/components/computer-viewer.tsx +51 -23
  154. package/src/components/desktop-viewer.tsx +1 -1
  155. package/src/components/markdown-table-layout.ts +49 -5
  156. package/src/components/markdown.tsx +5 -2
  157. package/src/components/message-timeline.tsx +557 -516
  158. package/src/components/open-geni-links.tsx +24 -0
  159. package/src/components/sandbox-terminal.tsx +48 -11
  160. package/src/components/session-chrome.tsx +22 -3
  161. package/src/components/session-conversation.tsx +195 -102
  162. package/src/components/timeline-annotation-selection.tsx +7 -0
  163. package/src/components/timeline-pill-geometry.ts +169 -0
  164. package/src/components/timeline-pill-placement.ts +99 -0
  165. package/src/components/usage-limit-notice.tsx +179 -0
  166. package/src/components/usage-member-list.tsx +792 -0
  167. package/src/components/usage-meter.tsx +382 -0
  168. package/src/conversation-timeline.ts +10 -2
  169. package/src/desktop.ts +24 -0
  170. package/src/editor.ts +13 -0
  171. package/src/hooks/use-available-models.ts +1 -1
  172. package/src/hooks/use-composer.ts +57 -2
  173. package/src/hooks/use-desktop-stream.ts +19 -28
  174. package/src/hooks/use-usage.ts +100 -0
  175. package/src/index.ts +22 -0
  176. package/src/lib/format.ts +32 -6
  177. package/src/lib/workbench-peers.ts +62 -0
  178. package/src/session-ui.ts +22 -0
  179. package/src/terminal.ts +26 -0
  180. package/src/timeline/activity-rail.tsx +5 -3
  181. package/src/timeline/allowance-exhausted-row.tsx +86 -0
  182. package/src/timeline/genie-loading.tsx +16 -6
  183. package/src/timeline/projection.ts +78 -4
  184. package/src/timeline/types.ts +8 -0
  185. package/src/usage/allowance-copy.ts +64 -0
  186. package/src/usage/summary.ts +93 -0
  187. package/src/usage.ts +50 -0
  188. package/styles/compiled.css +325 -0
  189. package/dist/chunk-4MDLEHXL.js.map +0 -1
  190. package/dist/chunk-5YK2ZDIG.js.map +0 -1
  191. package/dist/chunk-KVERXCFY.js.map +0 -1
  192. package/dist/chunk-L4OSXQ6H.js.map +0 -1
  193. package/dist/chunk-S5JQ4R22.js.map +0 -1
  194. package/dist/chunk-UDD6DLLT.js.map +0 -1
  195. package/dist/chunk-UGMKPUEM.js +0 -299
  196. package/dist/chunk-UGMKPUEM.js.map +0 -1
  197. package/dist/chunk-V2ABIVYJ.js.map +0 -1
  198. package/dist/chunk-X4HIN6TV.js.map +0 -1
  199. package/dist/chunk-XEJEOAOV.js.map +0 -1
  200. package/dist/chunk-YZ3YYMOM.js.map +0 -1
  201. package/dist/markdown-table-layout-PFUEZ2U4.js.map +0 -1
  202. package/dist/timeline-annotation-selection-ODLVF4OI.js.map +0 -1
  203. /package/dist/{browser-viewer-2E6R2Y4W.js.map → browser-viewer-Z7S7QEJL.js.map} +0 -0
  204. /package/dist/{computer-viewer-WMJE6OA3.js.map → chat-interactive-block-UGBTNB3L.js.map} +0 -0
  205. /package/dist/{chunk-Y3QTZN2F.js.map → chunk-2VFNCX4E.js.map} +0 -0
  206. /package/dist/{chunk-SEEVCT36.js.map → chunk-A4UJB2MM.js.map} +0 -0
  207. /package/dist/{chunk-5AZSK6MV.js.map → chunk-LWNLETQT.js.map} +0 -0
  208. /package/dist/{sandbox-files-PXPIZAFG.js.map → computer-viewer-KJWLH3GI.js.map} +0 -0
  209. /package/dist/{latest-question-QB4EUCW5.js.map → latest-question-IS445HOW.js.map} +0 -0
  210. /package/dist/{queue-surface-implementation-IM2HD3SK.js.map → queue-surface-implementation-2CAQAIHM.js.map} +0 -0
@@ -208,6 +208,11 @@ export function BrowserViewer({
208
208
  () => registry.relevantSessions.filter((session) => isLiveBrowser(session)),
209
209
  [registry.relevantSessions],
210
210
  );
211
+ const latestRelevantBrowser = registry.relevantSessions[0];
212
+ const unavailableBrowser =
213
+ latestRelevantBrowser?.lifecycle === "lost" || latestRelevantBrowser?.lifecycle === "failed"
214
+ ? latestRelevantBrowser
215
+ : undefined;
211
216
  const [selection, setSelection] = useState<BrowserSelection>(() =>
212
217
  initialBrowserSessionId ? { sessionId: initialBrowserSessionId, pinned: true } : null,
213
218
  );
@@ -769,7 +774,8 @@ export function BrowserViewer({
769
774
  );
770
775
  }
771
776
  if (liveSessions.length === 0) {
772
- if (renderEmpty) return <>{renderEmpty(() => createBrowser(), creating)}</>;
777
+ if (renderEmpty && !unavailableBrowser)
778
+ return <>{renderEmpty(() => createBrowser(), creating)}</>;
773
779
  return (
774
780
  <div
775
781
  className={cn("flex h-full min-h-0 items-center justify-center bg-og-bg p-6", className)}
@@ -778,16 +784,22 @@ export function BrowserViewer({
778
784
  <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">
779
785
  <Globe2Icon className="size-4.5" />
780
786
  </span>
781
- <p className="mt-3 text-og-menu font-medium text-og-fg">
782
- {attachedGenerationLoss
783
- ? "Chrome reconnected—open a fresh browser/desktop."
784
- : "No browser open"}
785
- </p>
786
- <p className="mt-1 text-og-control leading-5 text-og-fg-muted">
787
- {attachedGenerationLoss
788
- ? "This Chrome profile is live again, but the previous browser cannot move to the new connection. Use Browser → New browser → Connected Chrome."
789
- : "A browser appears here when this agent—or another agent in the workspace—opens one."}
790
- </p>
787
+ {unavailableBrowser && !attachedGenerationLoss ? (
788
+ <BrowserUnavailableNotice session={unavailableBrowser} />
789
+ ) : (
790
+ <>
791
+ <p className="mt-3 text-og-menu font-medium text-og-fg">
792
+ {attachedGenerationLoss
793
+ ? "Chrome reconnected—open a fresh browser/desktop."
794
+ : "No browser open"}
795
+ </p>
796
+ <p className="mt-1 text-og-control leading-5 text-og-fg-muted">
797
+ {attachedGenerationLoss
798
+ ? "This Chrome profile is live again, but the previous browser cannot move to the new connection. Use Browser → New browser → Connected Chrome."
799
+ : "A browser appears here when this agent—or another agent in the workspace—opens one."}
800
+ </p>
801
+ </>
802
+ )}
791
803
  <BrowserLaunchMenu
792
804
  attachedBridges={onlineAttachedBridges}
793
805
  attachedDevices={attached.devices}
@@ -855,6 +867,7 @@ export function BrowserViewer({
855
867
  />
856
868
  {!selectedRegistrySession ? (
857
869
  <BrowserUnselectedPanel
870
+ unavailableBrowser={unavailableBrowser}
858
871
  peerCount={liveSessions.length}
859
872
  creating={creating}
860
873
  onCreate={() => createBrowser()}
@@ -1186,6 +1199,7 @@ function BrowserToolbar(props: {
1186
1199
  }
1187
1200
 
1188
1201
  function BrowserUnselectedPanel(props: {
1202
+ unavailableBrowser: BrowserSession | undefined;
1189
1203
  peerCount: number;
1190
1204
  creating: boolean;
1191
1205
  onCreate: () => void;
@@ -1196,7 +1210,11 @@ function BrowserUnselectedPanel(props: {
1196
1210
  <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">
1197
1211
  <Globe2Icon className="size-4.5" />
1198
1212
  </span>
1199
- <p className="mt-3 text-og-menu font-medium text-og-fg">No browser for this agent</p>
1213
+ {props.unavailableBrowser ? (
1214
+ <BrowserUnavailableNotice session={props.unavailableBrowser} />
1215
+ ) : (
1216
+ <p className="mt-3 text-og-menu font-medium text-og-fg">No browser for this agent</p>
1217
+ )}
1200
1218
  <p className="mt-1 text-og-control leading-5 text-og-fg-muted">
1201
1219
  {props.peerCount === 1
1202
1220
  ? "One workspace browser is available from the browser menu."
@@ -1216,6 +1234,25 @@ function BrowserUnselectedPanel(props: {
1216
1234
  );
1217
1235
  }
1218
1236
 
1237
+ function BrowserUnavailableNotice({ session }: { session: BrowserSession }) {
1238
+ const reason =
1239
+ session.failureCode === "provider_deadline_rotation"
1240
+ ? "The computer running it reached its time limit."
1241
+ : session.failureCode === "source_placement_changed"
1242
+ ? "This chat moved to another computer."
1243
+ : session.lifecycle === "failed"
1244
+ ? "It stopped unexpectedly."
1245
+ : "The connection to it was lost.";
1246
+ return (
1247
+ <div role="status">
1248
+ <p className="mt-3 text-og-menu font-medium text-og-fg">Browser unavailable</p>
1249
+ <p className="mt-1 break-words text-og-control leading-5 text-og-fg-muted">
1250
+ {session.name}. {reason} Open a new browser to continue.
1251
+ </p>
1252
+ </div>
1253
+ );
1254
+ }
1255
+
1219
1256
  function BrowserSessionGroup(props: {
1220
1257
  label: string;
1221
1258
  sessions: BrowserSession[];
@@ -1719,7 +1756,8 @@ function BrowserProfileMenu(props: {
1719
1756
  ) : null}
1720
1757
  {canSave ? (
1721
1758
  <p className="mt-2 text-[10px] leading-4 text-og-fg-subtle">
1722
- Saving briefly restarts this browser. Other open browsers are unchanged.
1759
+ Saving restarts this browser. Save unfinished work on its pages first; unsaved form
1760
+ entries may be lost. Other open browsers are unchanged.
1723
1761
  </p>
1724
1762
  ) : null}
1725
1763
  </div>
@@ -1910,6 +1948,7 @@ function BrowserViewport(props: {
1910
1948
  controllerGeneration: string;
1911
1949
  targetId: string;
1912
1950
  fencedInputBatches?: true | undefined;
1951
+ focusedInputObservations?: true | undefined;
1913
1952
  expiresAt: string;
1914
1953
  } | null;
1915
1954
  activityLabel?: string | undefined;
@@ -1962,6 +2001,7 @@ function BrowserViewport(props: {
1962
2001
  } | null>(null);
1963
2002
  const actionRef = useRef(props.onAction);
1964
2003
  const readClipboardRef = useRef(props.onReadClipboard);
2004
+ const observeInputRef = useRef(props.onObserveForInput);
1965
2005
  const errorRef = useRef(props.onError);
1966
2006
  const actionTailRef = useRef<Promise<void>>(Promise.resolve());
1967
2007
  const actionQueueEpochRef = useRef(0);
@@ -1978,6 +2018,7 @@ function BrowserViewport(props: {
1978
2018
  const mountedRef = useRef(true);
1979
2019
  actionRef.current = props.onAction;
1980
2020
  readClipboardRef.current = props.onReadClipboard;
2021
+ observeInputRef.current = props.onObserveForInput;
1981
2022
  errorRef.current = props.onError;
1982
2023
 
1983
2024
  const clearBufferedInput = useCallback(() => {
@@ -2154,8 +2195,23 @@ function BrowserViewport(props: {
2154
2195
  // frame from that render even when its input fence contains no bytes.
2155
2196
  const receipt = await actionRef.current(dispatchedAction, frame);
2156
2197
  if (!mountedRef.current || epoch !== actionQueueEpochRef.current) return;
2157
- if (selectAnchor && receipt.observation && inputSequence === inputSequenceRef.current) {
2158
- setSelectPopup({ observation: receipt.observation, anchor: selectAnchor });
2198
+ // Alt+Down explicitly opens a native dropdown from the keyboard.
2199
+ // Existing controllers return focused-input metadata for clicks only,
2200
+ // so use the ordinary authorized read for this explicit gesture.
2201
+ const keyboardSelect =
2202
+ dispatchedAction.type === "press" && dispatchedAction.key === "Alt+ArrowDown";
2203
+ if ((selectAnchor || keyboardSelect) && inputSequence === inputSequenceRef.current) {
2204
+ const observation = keyboardSelect
2205
+ ? await observeInputRef.current()
2206
+ : receipt.observation;
2207
+ if (
2208
+ mountedRef.current &&
2209
+ epoch === actionQueueEpochRef.current &&
2210
+ inputSequence === inputSequenceRef.current &&
2211
+ observation
2212
+ ) {
2213
+ setSelectPopup({ observation, anchor: selectAnchor ?? null });
2214
+ }
2159
2215
  }
2160
2216
  await after?.(receipt);
2161
2217
  })
@@ -2448,6 +2504,7 @@ function BrowserViewport(props: {
2448
2504
  onPaste={paste}
2449
2505
  className="pointer-events-none absolute left-1/2 top-1/2 size-px resize-none overflow-hidden opacity-0"
2450
2506
  aria-label="Browser keyboard input"
2507
+ aria-keyshortcuts="Alt+ArrowDown"
2451
2508
  autoCapitalize="off"
2452
2509
  autoCorrect="off"
2453
2510
  spellCheck={false}
@@ -2467,6 +2524,7 @@ function BrowserViewport(props: {
2467
2524
  ) : null}
2468
2525
  {showCanvas ? (
2469
2526
  <BrowserSelectControl
2527
+ manualFallback={props.inputBatchAttachment?.focusedInputObservations !== true}
2470
2528
  activation={selectPopup}
2471
2529
  onDismiss={() => inputRef.current?.focus({ preventScroll: true })}
2472
2530
  observe={async () => {
@@ -2740,13 +2798,14 @@ function BrowserDiagnosticsDrawer(props: {
2740
2798
  id="browser-page-title"
2741
2799
  className="text-[10px] font-semibold uppercase tracking-[0.14em] text-og-fg-subtle"
2742
2800
  >
2743
- Current page
2801
+ Tab diagnostics
2744
2802
  </h3>
2803
+ <p className="mt-1 text-og-xs text-og-fg-subtle">Includes earlier pages in this tab.</p>
2745
2804
  <dl className="mt-2 grid grid-cols-2 gap-2">
2746
- <DiagnosticCount label="Console errors" value={diagnostics?.consoleErrorCount ?? 0} />
2747
- <DiagnosticCount label="Page errors" value={diagnostics?.pageErrorCount ?? 0} />
2748
- <DiagnosticCount label="Failed requests" value={diagnostics?.failedRequestCount ?? 0} />
2749
- <DiagnosticCount label="Downloads" value={diagnostics?.downloadCount ?? 0} />
2805
+ <DiagnosticCount label="Console errors" value={diagnostics?.consoleErrorCount} />
2806
+ <DiagnosticCount label="Page errors" value={diagnostics?.pageErrorCount} />
2807
+ <DiagnosticCount label="Failed requests" value={diagnostics?.failedRequestCount} />
2808
+ <DiagnosticCount label="Downloads" value={diagnostics?.downloadCount} />
2750
2809
  </dl>
2751
2810
  {session?.failureCode ? (
2752
2811
  <p className="mt-2 rounded-og-sm border border-og-status-error/30 bg-og-status-error/5 px-2.5 py-2 text-og-xs text-og-danger">
@@ -3044,7 +3103,7 @@ function DiagnosticFact(props: { label: string; value: string }) {
3044
3103
  );
3045
3104
  }
3046
3105
 
3047
- function DiagnosticCount(props: { label: string; value: number }) {
3106
+ function DiagnosticCount(props: { label: string; value: number | undefined }) {
3048
3107
  return (
3049
3108
  <div className="rounded-og-sm border border-og-border bg-og-bg px-2.5 py-2">
3050
3109
  <dt className="text-[10px] text-og-fg-subtle">{props.label}</dt>
@@ -3054,7 +3113,7 @@ function DiagnosticCount(props: { label: string; value: number }) {
3054
3113
  props.value ? "text-og-fg" : "text-og-fg-muted",
3055
3114
  )}
3056
3115
  >
3057
- {props.value}
3116
+ {props.value ?? "Unavailable"}
3058
3117
  </dd>
3059
3118
  </div>
3060
3119
  );
@@ -8,8 +8,10 @@ import {
8
8
  useMemo,
9
9
  useRef,
10
10
  useState,
11
+ useSyncExternalStore,
11
12
  } from "react";
12
13
  import { cn } from "../lib/cn";
14
+ import { codeEditorPeers, type CodeEditorLanguage } from "../lib/workbench-peers";
13
15
 
14
16
  // --- Lazy CodeMirror surface ----------------------------------------------
15
17
  //
@@ -42,19 +44,8 @@ type EditorBundle = {
42
44
  languageExtension: unknown | null;
43
45
  };
44
46
 
45
- /** Language grammar loaders, keyed by the extension class we infer from the path. */
46
- const LANGUAGE_LOADERS: Record<string, () => Promise<unknown>> = {
47
- javascript: async () =>
48
- (await import("@codemirror/lang-javascript")).javascript({ jsx: true, typescript: true }),
49
- json: async () => (await import("@codemirror/lang-json")).json(),
50
- python: async () => (await import("@codemirror/lang-python")).python(),
51
- markdown: async () => (await import("@codemirror/lang-markdown")).markdown(),
52
- css: async () => (await import("@codemirror/lang-css")).css(),
53
- html: async () => (await import("@codemirror/lang-html")).html(),
54
- };
55
-
56
47
  /** Map a filename to a grammar key (or null for plain text — still fully editable). */
57
- export function languageForPath(path: string): keyof typeof LANGUAGE_LOADERS | null {
48
+ export function languageForPath(path: string): string | null {
58
49
  const name = path.split("/").pop() ?? path;
59
50
  const ext = name.includes(".") ? (name.split(".").pop() ?? "").toLowerCase() : "";
60
51
  switch (ext) {
@@ -182,6 +173,11 @@ export function CodeEditor({
182
173
  }: CodeEditorProps) {
183
174
  const [failed, setFailed] = useState(false);
184
175
  const [bundle, setBundle] = useState<EditorBundle | null>(null);
176
+ const peerRevision = useSyncExternalStore(
177
+ codeEditorPeers.subscribe,
178
+ codeEditorPeers.revision,
179
+ codeEditorPeers.revision,
180
+ );
185
181
 
186
182
  // The live buffer and its compare-and-swap baseline are one state value so a
187
183
  // remote refresh cannot update one without the other.
@@ -233,12 +229,13 @@ export function CodeEditor({
233
229
 
234
230
  // Resolve the lazy bundle (editor + keymap/Prec helpers + language grammar)
235
231
  // once, re-resolving the grammar when the file's language class changes.
236
- const langKey = useMemo(() => languageForPath(path), [path]);
232
+ const langKey = useMemo(() => languageForPath(path) as CodeEditorLanguage | null, [path]);
237
233
  useEffect(() => {
238
234
  let cancelled = false;
239
235
  void (async () => {
240
236
  try {
241
- const cmMod = (await import("@uiw/react-codemirror")) as unknown as {
237
+ const peers = await codeEditorPeers.load();
238
+ const cmMod = peers.module as {
242
239
  default: ReactCodeMirrorComponent;
243
240
  keymap: { of: (binds: unknown[]) => unknown };
244
241
  Prec: { highest: (ext: unknown) => unknown };
@@ -248,7 +245,7 @@ export function CodeEditor({
248
245
  let languageExtension: unknown | null = null;
249
246
  if (langKey) {
250
247
  try {
251
- languageExtension = (await LANGUAGE_LOADERS[langKey]?.()) ?? null;
248
+ languageExtension = (await peers.languages?.[langKey]?.()) ?? null;
252
249
  } catch {
253
250
  languageExtension = null;
254
251
  }
@@ -283,7 +280,7 @@ export function CodeEditor({
283
280
  return () => {
284
281
  cancelled = true;
285
282
  };
286
- }, [langKey]);
283
+ }, [langKey, peerRevision]);
287
284
 
288
285
  // Keep the freshest save closure reachable from the (stable) keymap binding.
289
286
  const saveRef = useRef<() => void>(() => {});
@@ -289,13 +289,19 @@ export function ComputerViewer({
289
289
  : null;
290
290
  const rfbStream =
291
291
  frames.attachment?.stream.kind === "direct_rfb" ? frames.attachment.stream : null;
292
+ const machineLocked = selectedRegistrySession?.failureCode === "machine_locked";
293
+ // RFB has one combined input switch; partial native input stays view-only.
294
+ const rfbInputEnabled =
295
+ !machineLocked &&
296
+ computer.session?.capabilities?.pointerInput === true &&
297
+ computer.session?.capabilities?.keyboardInput === true;
292
298
  const rfbCapability = useMemo<DesktopStreamCapability | null>(() => {
293
299
  if (!rfbStream || !frames.attachment) return null;
294
300
  const bounds = computer.selectedTarget?.bounds;
295
301
  return {
296
302
  transport: "vnc-ws",
297
303
  client: "novnc",
298
- mode: "interactive",
304
+ mode: rfbInputEnabled ? "interactive" : "read-only",
299
305
  url: rfbStream.url,
300
306
  token: null,
301
307
  expiresAt: frames.attachment.expiresAt,
@@ -307,8 +313,7 @@ export function ComputerViewer({
307
313
  sharedSessionIds: [],
308
314
  reason: null,
309
315
  };
310
- }, [computer.selectedTarget?.bounds, frames.attachment, rfbStream]);
311
- const machineLocked = selectedRegistrySession?.failureCode === "machine_locked";
316
+ }, [computer.selectedTarget?.bounds, frames.attachment, rfbInputEnabled, rfbStream]);
312
317
  const currentIds = useMemo(() => new Set(relevant.map((session) => session.id)), [relevant]);
313
318
 
314
319
  useEffect(() => {
@@ -452,7 +457,8 @@ export function ComputerViewer({
452
457
 
453
458
  const copyFromRfb = useCallback(
454
459
  (event: ClipboardEvent<HTMLDivElement>) => {
455
- if (!rfbStream || computer.session?.capabilities?.clipboard !== true) return;
460
+ if (!rfbInputEnabled || !rfbStream || computer.session?.capabilities?.clipboard !== true)
461
+ return;
456
462
  event.preventDefault();
457
463
  event.stopPropagation();
458
464
  void perform({ type: "clipboard", operation: "copy" }, null)
@@ -464,12 +470,13 @@ export function ComputerViewer({
464
470
  })
465
471
  .catch((cause) => notifyError(cause, "Could not copy from the desktop."));
466
472
  },
467
- [computer, notifyError, perform, rfbStream],
473
+ [computer, notifyError, perform, rfbInputEnabled, rfbStream],
468
474
  );
469
475
 
470
476
  const pasteIntoRfb = useCallback(
471
477
  (text: string): boolean => {
472
- if (!rfbStream || computer.session?.capabilities?.clipboard !== true) return false;
478
+ if (!rfbInputEnabled || !rfbStream || computer.session?.capabilities?.clipboard !== true)
479
+ return false;
473
480
  // Keep paste on the canonical ComputerSession action path. RFB
474
481
  // ClientCutText synchronization varies by server and can acknowledge a
475
482
  // local paste without ever updating the remote graphical seat. These two
@@ -484,7 +491,7 @@ export function ComputerViewer({
484
491
  })().catch((cause) => notifyError(cause, "Could not paste into the desktop."));
485
492
  return true;
486
493
  },
487
- [computer, notifyError, perform, rfbStream],
494
+ [computer, notifyError, perform, rfbInputEnabled, rfbStream],
488
495
  );
489
496
 
490
497
  const hideGenericCreate = suppressGenericCreate && liveRelevant.length === 0;
@@ -623,13 +630,18 @@ export function ComputerViewer({
623
630
  <div className="relative min-h-0 flex-1 bg-black" onCopyCapture={copyFromRfb}>
624
631
  <DesktopViewer
625
632
  capability={rfbCapability}
626
- interactive
633
+ interactive={rfbInputEnabled}
627
634
  showControlToggle={false}
628
635
  webSocketProtocols={rfbStream.protocols}
629
636
  onPasteText={pasteIntoRfb}
630
637
  targetPlatform={computer.session?.platform ?? null}
631
638
  className="h-full"
632
639
  />
640
+ {!rfbInputEnabled ? (
641
+ <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">
642
+ View only · direct input unavailable
643
+ </div>
644
+ ) : null}
633
645
  </div>
634
646
  ) : (
635
647
  <ComputerViewport
@@ -648,6 +660,8 @@ export function ComputerViewer({
648
660
  mutating={computer.mutating}
649
661
  backgroundActions={computer.session?.capabilities?.backgroundActions === true}
650
662
  clipboardEnabled={computer.session?.capabilities?.clipboard === true}
663
+ pointerInput={computer.session?.capabilities?.pointerInput === true}
664
+ keyboardInput={computer.session?.capabilities?.keyboardInput === true}
651
665
  onAction={perform}
652
666
  onReadClipboard={computer.readClipboard}
653
667
  onReconnect={
@@ -1011,6 +1025,8 @@ function ComputerViewport(props: {
1011
1025
  mutating: boolean;
1012
1026
  backgroundActions: boolean;
1013
1027
  clipboardEnabled: boolean;
1028
+ pointerInput: boolean;
1029
+ keyboardInput: boolean;
1014
1030
  onAction: (action: ComputerAction, frame: ComputerFrame | null) => Promise<void>;
1015
1031
  onReadClipboard: () => Promise<ComputerClipboard>;
1016
1032
  onReconnect: () => void;
@@ -1070,6 +1086,9 @@ function ComputerViewport(props: {
1070
1086
  if (inputRef.current) inputRef.current.value = "";
1071
1087
  }, []);
1072
1088
 
1089
+ const pointerInputEnabled = rawInputEnabled && props.pointerInput;
1090
+ const keyboardInputEnabled = rawInputEnabled && props.keyboardInput;
1091
+
1073
1092
  const paintQueuedFrames = useCallback(() => {
1074
1093
  if (decodingFrameRef.current) return;
1075
1094
  decodingFrameRef.current = true;
@@ -1159,11 +1178,11 @@ function ComputerViewport(props: {
1159
1178
  }, [clearBufferedInput, paintQueuedFrames, props.frame]);
1160
1179
 
1161
1180
  useLayoutEffect(() => {
1162
- if (!rawInputEnabled) {
1181
+ if (!rawInputEnabled || !props.pointerInput || !props.keyboardInput) {
1163
1182
  actionQueueEpochRef.current += 1;
1164
1183
  clearBufferedInput();
1165
1184
  }
1166
- }, [clearBufferedInput, rawInputEnabled]);
1185
+ }, [clearBufferedInput, props.keyboardInput, props.pointerInput, rawInputEnabled]);
1167
1186
 
1168
1187
  const enqueue = useCallback(
1169
1188
  (
@@ -1248,6 +1267,8 @@ function ComputerViewport(props: {
1248
1267
  if (!frame || props.mutating || !rawInputEnabled || event.button !== 0) return;
1249
1268
  // Preserve the keyboard sink's focus through the canvas pointer default action.
1250
1269
  event.preventDefault();
1270
+ if (keyboardInputEnabled) inputRef.current?.focus({ preventScroll: true });
1271
+ if (!pointerInputEnabled) return;
1251
1272
  flushPendingWheel();
1252
1273
  flushPendingText();
1253
1274
  pointerStartRef.current = {
@@ -1257,11 +1278,10 @@ function ComputerViewport(props: {
1257
1278
  frame,
1258
1279
  };
1259
1280
  event.currentTarget.setPointerCapture?.(event.pointerId);
1260
- inputRef.current?.focus({ preventScroll: true });
1261
1281
  };
1262
1282
 
1263
1283
  const pointerUp = (event: PointerEvent<HTMLCanvasElement>) => {
1264
- if (!rawInputEnabled) return;
1284
+ if (!pointerInputEnabled) return;
1265
1285
  const start = pointerStartRef.current;
1266
1286
  pointerStartRef.current = null;
1267
1287
  if (!start || start.pointerId !== event.pointerId) return;
@@ -1326,7 +1346,7 @@ function ComputerViewport(props: {
1326
1346
 
1327
1347
  const contextMenu = (event: MouseEvent<HTMLCanvasElement>) => {
1328
1348
  event.preventDefault();
1329
- if (!rawInputEnabled) return;
1349
+ if (!pointerInputEnabled) return;
1330
1350
  const frame = paintedFrameRef.current;
1331
1351
  if (!frame) return;
1332
1352
  flushPendingWheel();
@@ -1349,7 +1369,7 @@ function ComputerViewport(props: {
1349
1369
  };
1350
1370
 
1351
1371
  const wheel = (event: WheelEvent<HTMLCanvasElement>) => {
1352
- if (!rawInputEnabled) return;
1372
+ if (!pointerInputEnabled) return;
1353
1373
  const frame = paintedFrameRef.current;
1354
1374
  if (!frame) return;
1355
1375
  const at = point(frame, event.clientX, event.clientY);
@@ -1377,7 +1397,7 @@ function ComputerViewport(props: {
1377
1397
  };
1378
1398
 
1379
1399
  const keyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
1380
- if (!rawInputEnabled) return;
1400
+ if (!keyboardInputEnabled) return;
1381
1401
  flushPendingWheel();
1382
1402
  const command = event.metaKey || event.ctrlKey;
1383
1403
  if (
@@ -1398,7 +1418,7 @@ function ComputerViewport(props: {
1398
1418
  };
1399
1419
 
1400
1420
  const copy = (event: ClipboardEvent<HTMLTextAreaElement>) => {
1401
- if (!props.clipboardEnabled || !rawInputEnabled) return;
1421
+ if (!props.clipboardEnabled || !keyboardInputEnabled) return;
1402
1422
  event.preventDefault();
1403
1423
  flushPendingWheel();
1404
1424
  flushPendingText();
@@ -1415,7 +1435,7 @@ function ComputerViewport(props: {
1415
1435
  };
1416
1436
 
1417
1437
  const paste = (event: ClipboardEvent<HTMLTextAreaElement>) => {
1418
- if (!props.clipboardEnabled || !rawInputEnabled) return;
1438
+ if (!props.clipboardEnabled || !keyboardInputEnabled) return;
1419
1439
  event.preventDefault();
1420
1440
  flushPendingWheel();
1421
1441
  flushPendingText();
@@ -1432,7 +1452,7 @@ function ComputerViewport(props: {
1432
1452
  };
1433
1453
 
1434
1454
  const input = (value: string) => {
1435
- if (!value || !rawInputEnabled) return;
1455
+ if (!value || !keyboardInputEnabled) return;
1436
1456
  flushPendingWheel();
1437
1457
  flushPendingClick();
1438
1458
  const pending = pendingTextRef.current;
@@ -1462,7 +1482,7 @@ function ComputerViewport(props: {
1462
1482
  className={cn(
1463
1483
  "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",
1464
1484
  !showCanvas && "invisible",
1465
- !rawInputEnabled && "cursor-default",
1485
+ !pointerInputEnabled && "cursor-default",
1466
1486
  )}
1467
1487
  onPointerDown={pointerDown}
1468
1488
  onPointerUp={pointerUp}
@@ -1485,7 +1505,7 @@ function ComputerViewport(props: {
1485
1505
  autoCapitalize="off"
1486
1506
  autoCorrect="off"
1487
1507
  spellCheck={false}
1488
- disabled={!rawInputEnabled}
1508
+ disabled={!keyboardInputEnabled}
1489
1509
  />
1490
1510
  {!showCanvas ? (
1491
1511
  <ComputerViewportFallback
@@ -1502,10 +1522,18 @@ function ComputerViewport(props: {
1502
1522
  <LoaderCircleIcon className="size-3 animate-spin" /> Acting
1503
1523
  </div>
1504
1524
  ) : null}
1505
- {showCanvas && !rawInputEnabled ? (
1525
+ {showCanvas && (!rawInputEnabled || !props.pointerInput || !props.keyboardInput) ? (
1506
1526
  <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">
1507
- <span>Background view · use native controls</span>
1508
- {props.target ? (
1527
+ <span>
1528
+ {!rawInputEnabled && (props.pointerInput || props.keyboardInput)
1529
+ ? "Background view · use native controls"
1530
+ : !props.pointerInput && !props.keyboardInput
1531
+ ? "View only · mouse and keyboard unavailable"
1532
+ : props.keyboardInput
1533
+ ? "Keyboard only · mouse unavailable"
1534
+ : "Mouse only · keyboard unavailable"}
1535
+ </span>
1536
+ {!rawInputEnabled && props.target && (props.pointerInput || props.keyboardInput) ? (
1509
1537
  <button
1510
1538
  type="button"
1511
1539
  disabled={props.mutating}
@@ -30,7 +30,7 @@ export type DesktopViewerProps = {
30
30
  /** Render the built-in Watching ⇄ Take control toggle (default true). */
31
31
  showControlToggle?: boolean | undefined;
32
32
  scaleViewport?: boolean | undefined;
33
- /** Custom RFB factory (tests / a WebRTC swap). Defaults to lazy @novnc/novnc. */
33
+ /** Custom RFB factory; otherwise enableDesktopViewer() supplies lazy noVNC. */
34
34
  rfbFactory?: DesktopRfbFactory | undefined;
35
35
  /** Authenticated controller protocols for proxied RFB transports. */
36
36
  webSocketProtocols?: string[] | undefined;
@@ -16,6 +16,37 @@ export function markdownTableWidth({
16
16
  return Math.max(columnWidth, Math.min(preferredWidth, available));
17
17
  }
18
18
 
19
+ function measurementCopy(source: Node, document: Document): Node {
20
+ if (source.nodeType === Node.ELEMENT_NODE) {
21
+ const element = source as Element;
22
+ // Inert prevents focus, not custom-element construction, media loading or
23
+ // embedded browsing contexts. Do not clone those host-rendered surfaces.
24
+ if (
25
+ element.localName.includes("-") ||
26
+ ["iframe", "object", "embed", "video", "audio", "canvas", "script"].includes(
27
+ element.localName,
28
+ )
29
+ ) {
30
+ const bounds = element.getBoundingClientRect();
31
+ const placeholder = document.createElement("span");
32
+ placeholder.style.display = "inline-block";
33
+ placeholder.style.width = `${bounds.width}px`;
34
+ placeholder.style.height = `${bounds.height}px`;
35
+ return placeholder;
36
+ }
37
+ }
38
+ const copy = document.importNode(source, false);
39
+ if (copy.nodeType === Node.ELEMENT_NODE) {
40
+ const element = copy as Element;
41
+ element.removeAttribute("id");
42
+ for (const attribute of [...element.attributes]) {
43
+ if (attribute.name.startsWith("on")) element.removeAttribute(attribute.name);
44
+ }
45
+ }
46
+ for (const child of source.childNodes) copy.appendChild(measurementCopy(child, document));
47
+ return copy;
48
+ }
49
+
19
50
  /** Loaded only for assistant tables. Keep prose and nested scroll owners intact. */
20
51
  export function observeMarkdownTableLayout(wrapper: HTMLDivElement, table: HTMLTableElement) {
21
52
  const body = wrapper.parentElement;
@@ -24,6 +55,8 @@ export function observeMarkdownTableLayout(wrapper: HTMLDivElement, table: HTMLT
24
55
  ?.closest<HTMLElement>("[data-og-timeline-scroller]");
25
56
  if (!body?.classList.contains("og-markdown-body") || !scroller) return;
26
57
 
58
+ // Build the static copy without a browsing context before inserting its probe.
59
+ const measurementDocument = table.ownerDocument.implementation.createHTMLDocument("");
27
60
  let width: number | null = null;
28
61
  const reset = () => {
29
62
  width = null;
@@ -50,14 +83,25 @@ export function observeMarkdownTableLayout(wrapper: HTMLDivElement, table: HTMLT
50
83
  const right =
51
84
  panel.left + scroller.clientLeft + scroller.clientWidth - parseFloat(panelStyle.paddingRight);
52
85
 
53
- // Measure the original table, preserving its wrapping and TSV copy DOM.
54
- const previousWidth = table.style.width;
86
+ // Even a temporary max-content width on the live table changes its height
87
+ // during forced layout. Browser scroll anchoring can retain that displacement
88
+ // after the width is restored. Measure a copy without touching live geometry.
89
+ const probe = table.ownerDocument.createElement("div");
90
+ probe.setAttribute("aria-hidden", "true");
91
+ probe.inert = true;
92
+ // A zero-sized, contained box cannot extend the scroller's overflow area,
93
+ // including inside transformed ancestors. Keep the same inherited styling.
94
+ probe.style.cssText =
95
+ "position:absolute;width:0;height:0;overflow:hidden;contain:layout size paint;visibility:hidden;pointer-events:none";
96
+ const copy = measurementCopy(table, measurementDocument) as HTMLTableElement;
97
+ copy.style.width = "max-content";
98
+ probe.append(copy);
55
99
  let preferred: number;
56
100
  try {
57
- table.style.width = "max-content";
58
- preferred = table.getBoundingClientRect().width;
101
+ table.parentElement!.append(probe);
102
+ preferred = copy.getBoundingClientRect().width;
59
103
  } finally {
60
- table.style.width = previousWidth;
104
+ probe.remove();
61
105
  }
62
106
  const next = markdownTableWidth({
63
107
  columnLeft: column.left,
@@ -391,12 +391,15 @@ function ActionMarkdownLink({
391
391
  type="button"
392
392
  className={cn(
393
393
  MARKDOWN_LINK_CLASS,
394
- "inline-flex min-h-7 cursor-pointer items-center disabled:cursor-wait disabled:opacity-70 pointer-coarse:min-h-11",
394
+ "inline-flex min-h-7 cursor-pointer items-center aria-disabled:cursor-wait aria-disabled:opacity-70 pointer-coarse:min-h-11",
395
395
  )}
396
- disabled={state === "loading"}
396
+ // Not `disabled`: disabling the focused control drops focus to the page,
397
+ // so a host viewer could not return focus to the link that opened it.
398
+ aria-disabled={state === "loading" || undefined}
397
399
  aria-busy={state === "loading"}
398
400
  title={state === "error" ? "Couldn't open this file. Select to retry." : title}
399
401
  onClick={() => {
402
+ if (state === "loading") return;
400
403
  setState("loading");
401
404
  void Promise.resolve()
402
405
  .then(onOpen)