@opengeni/react 1.4.2 → 1.4.5-canary.37458084490001

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 (240) hide show
  1. package/README.md +17 -6
  2. package/dist/artifacts.d.ts +1 -0
  3. package/dist/artifacts.js +22 -15
  4. package/dist/artifacts.js.map +1 -1
  5. package/dist/{browser-viewer-WGJ43YGI.js → browser-viewer-CV23VIHB.js} +8 -6
  6. package/dist/chunk-3ZC6QPOM.js +174 -0
  7. package/dist/chunk-3ZC6QPOM.js.map +1 -0
  8. package/dist/{chunk-YVFRBXST.js → chunk-4BCVCJSU.js} +2 -2
  9. package/dist/{chunk-JHTIMBOL.js → chunk-5JSUNNQN.js} +26 -4
  10. package/dist/{chunk-JHTIMBOL.js.map → chunk-5JSUNNQN.js.map} +1 -1
  11. package/dist/{chunk-MDIXFKA3.js → chunk-6FOTSVWU.js} +303 -109
  12. package/dist/chunk-6FOTSVWU.js.map +1 -0
  13. package/dist/{chunk-IUHHPMAX.js → chunk-7RRBX5EM.js} +14 -85
  14. package/dist/chunk-7RRBX5EM.js.map +1 -0
  15. package/dist/{chunk-L2J3PU7A.js → chunk-7ZB7I3KP.js} +24 -98
  16. package/dist/chunk-7ZB7I3KP.js.map +1 -0
  17. package/dist/{chunk-LVS3MH5T.js → chunk-ATIRUHBZ.js} +128 -10
  18. package/dist/chunk-ATIRUHBZ.js.map +1 -0
  19. package/dist/chunk-BOFEQC6K.js +162 -0
  20. package/dist/chunk-BOFEQC6K.js.map +1 -0
  21. package/dist/{chunk-VCSERYXH.js → chunk-CESBPQP5.js} +309 -162
  22. package/dist/chunk-CESBPQP5.js.map +1 -0
  23. package/dist/{chunk-IYQCWGU5.js → chunk-GHJU7XG7.js} +35 -62
  24. package/dist/chunk-GHJU7XG7.js.map +1 -0
  25. package/dist/{chunk-G6YRCIYJ.js → chunk-HNVWMDNN.js} +55 -60
  26. package/dist/chunk-HNVWMDNN.js.map +1 -0
  27. package/dist/{chunk-PHBFJCEU.js → chunk-I5EGRXXG.js} +110 -7
  28. package/dist/chunk-I5EGRXXG.js.map +1 -0
  29. package/dist/{chunk-PBCZL4JP.js → chunk-INIZSNXT.js} +2 -2
  30. package/dist/{chunk-XS67YG5R.js → chunk-IX3K4THH.js} +24 -3
  31. package/dist/chunk-IX3K4THH.js.map +1 -0
  32. package/dist/chunk-IZV5FTJC.js +55 -0
  33. package/dist/chunk-IZV5FTJC.js.map +1 -0
  34. package/dist/chunk-JDJJMBB3.js +1883 -0
  35. package/dist/chunk-JDJJMBB3.js.map +1 -0
  36. package/dist/{chunk-C5BF4L26.js → chunk-KHF2VRXF.js} +7 -7
  37. package/dist/{chunk-QMMCJGFI.js → chunk-L5OKM2VX.js} +2 -2
  38. package/dist/chunk-LHF6BVM4.js +83 -0
  39. package/dist/chunk-LHF6BVM4.js.map +1 -0
  40. package/dist/chunk-N7S72EXK.js +25 -0
  41. package/dist/chunk-N7S72EXK.js.map +1 -0
  42. package/dist/{chunk-CVYCLFMH.js → chunk-PEHTX3V7.js} +19 -1
  43. package/dist/chunk-PEHTX3V7.js.map +1 -0
  44. package/dist/chunk-R5R5RQQD.js +51 -0
  45. package/dist/chunk-R5R5RQQD.js.map +1 -0
  46. package/dist/chunk-RI42H5IU.js +326 -0
  47. package/dist/chunk-RI42H5IU.js.map +1 -0
  48. package/dist/{chunk-JH7V627O.js → chunk-SHLZMAJN.js} +2 -11
  49. package/dist/chunk-SHLZMAJN.js.map +1 -0
  50. package/dist/{chunk-CREMJMVN.js → chunk-TZQSHCZN.js} +692 -1996
  51. package/dist/chunk-TZQSHCZN.js.map +1 -0
  52. package/dist/chunk-ZIB4H5LN.js +101 -0
  53. package/dist/chunk-ZIB4H5LN.js.map +1 -0
  54. package/dist/client.d.ts +9 -3
  55. package/dist/clipboard.js +1 -1
  56. package/dist/components/browser-viewer.d.ts +6 -0
  57. package/dist/components/composer.d.ts +3 -69
  58. package/dist/components/human-input-form.d.ts +3 -32
  59. package/dist/components/markdown.d.ts +8 -0
  60. package/dist/components/model-policy-picker.d.ts +4 -37
  61. package/dist/components/opper-mark.d.ts +7 -0
  62. package/dist/components/provider-recovery-notice.d.ts +15 -0
  63. package/dist/components/remark-soft-line-breaks.d.ts +38 -0
  64. package/dist/components/sandbox-workspace.d.ts +13 -0
  65. package/dist/components/session-chrome.d.ts +5 -3
  66. package/dist/components/session-conversation.d.ts +15 -3
  67. package/dist/composer-messages.d.ts +74 -0
  68. package/dist/composer-messages.js +9 -0
  69. package/dist/composer.js +15 -10
  70. package/dist/{computer-viewer-46UIN7F3.js → computer-viewer-K4CKYRX6.js} +4 -4
  71. package/dist/connect-setup.d.ts +2 -0
  72. package/dist/connect.d.ts +1 -1
  73. package/dist/connect.js +21 -6
  74. package/dist/connect.js.map +1 -1
  75. package/dist/{embedded-voice-NIL4RDX3.js → embedded-voice-3ZFWWJ6O.js} +6 -4
  76. package/dist/{embedded-voice-NIL4RDX3.js.map → embedded-voice-3ZFWWJ6O.js.map} +1 -1
  77. package/dist/hooks/shared-machines-list.d.ts +4 -0
  78. package/dist/hooks/use-client-config-flags.d.ts +6 -0
  79. package/dist/hooks/use-computer-session.d.ts +2 -0
  80. package/dist/hooks/use-goal.d.ts +2 -1
  81. package/dist/human-input-model.d.ts +40 -0
  82. package/dist/index.d.ts +4 -1
  83. package/dist/index.js +116 -73
  84. package/dist/index.js.map +1 -1
  85. package/dist/interaction.js +5 -5
  86. package/dist/{latest-question-2POFPYNB.js → latest-question-SWC6QEVN.js} +3 -3
  87. package/dist/lib/clipboard.d.ts +7 -0
  88. package/dist/lib/provider-recovery.d.ts +49 -0
  89. package/dist/machines.js +2 -2
  90. package/dist/model-mark-paths.d.ts +34 -0
  91. package/dist/model-mark-paths.js +16 -0
  92. package/dist/model-picker-model.d.ts +111 -0
  93. package/dist/model-picker-model.js +25 -0
  94. package/dist/model-picker-model.js.map +1 -0
  95. package/dist/{model-policy-picker-menu-WRNSRFQC.js → model-policy-picker-menu-NM4RPTY6.js} +6 -4
  96. package/dist/model-policy-picker-menu-NM4RPTY6.js.map +1 -0
  97. package/dist/model-policy.d.ts +10 -0
  98. package/dist/model-policy.js +5 -1
  99. package/dist/organization-model.d.ts +32 -0
  100. package/dist/organization-model.js +65 -0
  101. package/dist/organization-model.js.map +1 -0
  102. package/dist/{platform-activity-row-PJJVATTZ.js → platform-activity-row-NJ47YBXW.js} +7 -84
  103. package/dist/platform-activity-row-NJ47YBXW.js.map +1 -0
  104. package/dist/queue-presentation-model.d.ts +72 -0
  105. package/dist/queue-presentation-model.js +38 -0
  106. package/dist/queue-presentation-model.js.map +1 -0
  107. package/dist/{queue-surface-implementation-67FS7FUI.js → queue-surface-implementation-YPJFQ3O6.js} +24 -252
  108. package/dist/queue-surface-implementation-YPJFQ3O6.js.map +1 -0
  109. package/dist/realtime.js +5 -3
  110. package/dist/{sandbox-files-34CJJNOY.js → sandbox-files-RCVCMXG7.js} +5 -5
  111. package/dist/sandbox-files-RCVCMXG7.js.map +1 -0
  112. package/dist/sandbox-label-model.d.ts +23 -0
  113. package/dist/sandbox-label-model.js +28 -0
  114. package/dist/sandbox-label-model.js.map +1 -0
  115. package/dist/session-attention-model.d.ts +8 -0
  116. package/dist/session-attention-model.js +46 -0
  117. package/dist/session-attention-model.js.map +1 -0
  118. package/dist/session-feedback-model.d.ts +20 -0
  119. package/dist/session-feedback-model.js +56 -0
  120. package/dist/session-feedback-model.js.map +1 -0
  121. package/dist/session-list-model.d.ts +96 -0
  122. package/dist/session-list-model.js +219 -0
  123. package/dist/session-list-model.js.map +1 -0
  124. package/dist/session-status-model.d.ts +19 -0
  125. package/dist/session-ui.js +26 -17
  126. package/dist/session-ui.js.map +1 -1
  127. package/dist/session.js +8 -6
  128. package/dist/testing/session-scenarios.d.ts +10 -0
  129. package/dist/testing/session-scenarios.js +303 -0
  130. package/dist/testing/session-scenarios.js.map +1 -0
  131. package/dist/timeline/genie-copy.d.ts +3 -0
  132. package/dist/timeline/genie-loading.d.ts +11 -0
  133. package/dist/timeline/index.d.ts +1 -1
  134. package/dist/timeline/notice-presentation.d.ts +7 -0
  135. package/dist/timeline/patch-apply-command.d.ts +18 -0
  136. package/dist/timeline/platform-activity-presentation.d.ts +7 -0
  137. package/dist/timeline/presented-image.d.ts +6 -1
  138. package/dist/timeline/question-nav-model.d.ts +23 -0
  139. package/dist/timeline/registry.d.ts +10 -1
  140. package/dist/timeline/retained-loaders.d.ts +1 -1
  141. package/dist/timeline/tool-presentation.d.ts +172 -0
  142. package/dist/timeline/turn-summary-model.d.ts +90 -0
  143. package/dist/timeline/turn-summary.d.ts +4 -74
  144. package/dist/timeline/work-presentation.d.ts +42 -0
  145. package/dist/timeline-model.d.ts +24 -0
  146. package/dist/timeline-model.js +152 -0
  147. package/dist/timeline-model.js.map +1 -0
  148. package/package.json +47 -3
  149. package/src/artifacts.ts +1 -0
  150. package/src/client.ts +9 -3
  151. package/src/components/browser-viewer.tsx +23 -2
  152. package/src/components/claude-mark.tsx +2 -1
  153. package/src/components/composer.tsx +4 -144
  154. package/src/components/computer-viewer.tsx +139 -30
  155. package/src/components/grok-mark.tsx +2 -1
  156. package/src/components/human-input-form.tsx +13 -124
  157. package/src/components/markdown.tsx +19 -1
  158. package/src/components/message-timeline.tsx +45 -99
  159. package/src/components/model-mark.tsx +2 -1
  160. package/src/components/model-policy-picker-menu.tsx +30 -85
  161. package/src/components/model-policy-picker.tsx +15 -148
  162. package/src/components/opper-mark.tsx +18 -0
  163. package/src/components/provider-recovery-notice.tsx +41 -0
  164. package/src/components/queue-surface-implementation.tsx +20 -349
  165. package/src/components/remark-soft-line-breaks.ts +166 -0
  166. package/src/components/sandbox-workspace.tsx +17 -1
  167. package/src/components/session-chrome.tsx +114 -99
  168. package/src/components/session-conversation.tsx +55 -6
  169. package/src/components/session-list.tsx +1 -1
  170. package/src/components/session-status.tsx +17 -16
  171. package/src/composer-messages.ts +150 -0
  172. package/src/connect-setup.tsx +45 -11
  173. package/src/connect.ts +1 -1
  174. package/src/hooks/shared-machines-list.ts +34 -1
  175. package/src/hooks/use-browser-session.ts +445 -168
  176. package/src/hooks/use-client-config-flags.ts +8 -0
  177. package/src/hooks/use-computer-session.ts +334 -98
  178. package/src/hooks/use-goal.ts +66 -10
  179. package/src/human-input-model.ts +147 -0
  180. package/src/index.ts +16 -0
  181. package/src/lib/clipboard.ts +29 -0
  182. package/src/lib/format.ts +7 -0
  183. package/src/lib/provider-recovery.ts +202 -0
  184. package/src/model-mark-paths.ts +53 -0
  185. package/src/model-picker-model.ts +330 -0
  186. package/src/model-policy.ts +43 -2
  187. package/src/organization-model.ts +118 -0
  188. package/src/queue-presentation-model.ts +464 -0
  189. package/src/sandbox-label-model.ts +41 -0
  190. package/src/session-attention-model.ts +59 -0
  191. package/src/session-feedback-model.ts +95 -0
  192. package/src/session-list-model.ts +327 -0
  193. package/src/session-status-model.ts +62 -0
  194. package/src/testing/session-scenarios.ts +326 -0
  195. package/src/timeline/activity-rail.tsx +5 -17
  196. package/src/timeline/genie-copy.ts +19 -0
  197. package/src/timeline/genie-loading.tsx +13 -18
  198. package/src/timeline/index.ts +1 -0
  199. package/src/timeline/notice-presentation.ts +22 -0
  200. package/src/timeline/patch-apply-command.tsx +135 -0
  201. package/src/timeline/platform-activity-presentation.ts +122 -0
  202. package/src/timeline/platform-activity-row.tsx +6 -96
  203. package/src/timeline/presented-image.ts +16 -2
  204. package/src/timeline/question-nav-model.ts +28 -0
  205. package/src/timeline/registry.ts +11 -1
  206. package/src/timeline/retained-loaders.ts +9 -4
  207. package/src/timeline/rolling-activity.tsx +7 -19
  208. package/src/timeline/tool-presentation.ts +1316 -0
  209. package/src/timeline/tool-renderers.tsx +226 -1025
  210. package/src/timeline/turn-summary-model.ts +338 -0
  211. package/src/timeline/turn-summary.tsx +21 -326
  212. package/src/timeline/work-presentation.ts +139 -0
  213. package/src/timeline-model.ts +108 -0
  214. package/styles/compiled.css +8 -0
  215. package/styles/connect.css +4 -0
  216. package/dist/chunk-CREMJMVN.js.map +0 -1
  217. package/dist/chunk-CVYCLFMH.js.map +0 -1
  218. package/dist/chunk-G6YRCIYJ.js.map +0 -1
  219. package/dist/chunk-HXYOSSQO.js +0 -117
  220. package/dist/chunk-HXYOSSQO.js.map +0 -1
  221. package/dist/chunk-IUHHPMAX.js.map +0 -1
  222. package/dist/chunk-IYQCWGU5.js.map +0 -1
  223. package/dist/chunk-JH7V627O.js.map +0 -1
  224. package/dist/chunk-L2J3PU7A.js.map +0 -1
  225. package/dist/chunk-LVS3MH5T.js.map +0 -1
  226. package/dist/chunk-MDIXFKA3.js.map +0 -1
  227. package/dist/chunk-PHBFJCEU.js.map +0 -1
  228. package/dist/chunk-VCSERYXH.js.map +0 -1
  229. package/dist/chunk-XS67YG5R.js.map +0 -1
  230. package/dist/platform-activity-row-PJJVATTZ.js.map +0 -1
  231. package/dist/queue-surface-implementation-67FS7FUI.js.map +0 -1
  232. /package/dist/{browser-viewer-WGJ43YGI.js.map → browser-viewer-CV23VIHB.js.map} +0 -0
  233. /package/dist/{chunk-YVFRBXST.js.map → chunk-4BCVCJSU.js.map} +0 -0
  234. /package/dist/{chunk-PBCZL4JP.js.map → chunk-INIZSNXT.js.map} +0 -0
  235. /package/dist/{chunk-C5BF4L26.js.map → chunk-KHF2VRXF.js.map} +0 -0
  236. /package/dist/{chunk-QMMCJGFI.js.map → chunk-L5OKM2VX.js.map} +0 -0
  237. /package/dist/{computer-viewer-46UIN7F3.js.map → composer-messages.js.map} +0 -0
  238. /package/dist/{model-policy-picker-menu-WRNSRFQC.js.map → computer-viewer-K4CKYRX6.js.map} +0 -0
  239. /package/dist/{latest-question-2POFPYNB.js.map → latest-question-SWC6QEVN.js.map} +0 -0
  240. /package/dist/{sandbox-files-34CJJNOY.js.map → model-mark-paths.js.map} +0 -0
@@ -83,6 +83,16 @@ export function useBrowserSession(options: UseBrowserSessionOptions): UseBrowser
83
83
  const { client, workspaceId } = useEmbeddedBrowserInteraction(options);
84
84
  const browserSessionId = options.browserSessionId;
85
85
  const enabled = (options.enabled ?? true) && browserSessionId !== null;
86
+ const sourceRef = useRef({ client, workspaceId, browserSessionId, enabled });
87
+ if (
88
+ sourceRef.current.client !== client ||
89
+ sourceRef.current.workspaceId !== workspaceId ||
90
+ sourceRef.current.browserSessionId !== browserSessionId ||
91
+ sourceRef.current.enabled !== enabled
92
+ ) {
93
+ sourceRef.current = { client, workspaceId, browserSessionId, enabled };
94
+ }
95
+ const source = sourceRef.current;
86
96
  const semanticObservationEnabled = options.semanticObservationEnabled ?? true;
87
97
  const semanticObservationEnabledRef = useRef(semanticObservationEnabled);
88
98
  semanticObservationEnabledRef.current = semanticObservationEnabled;
@@ -93,6 +103,7 @@ export function useBrowserSession(options: UseBrowserSessionOptions): UseBrowser
93
103
  const pageLive = usePageLiveActivity();
94
104
  const pollIntervalMs = Math.max(750, options.pollIntervalMs ?? 2_000);
95
105
  const [state, setState] = useState<{
106
+ source: typeof source;
96
107
  browserSessionId: string | null;
97
108
  session: BrowserSession | null;
98
109
  targets: BrowserTarget[];
@@ -102,14 +113,19 @@ export function useBrowserSession(options: UseBrowserSessionOptions): UseBrowser
102
113
  mutating: boolean;
103
114
  error: Error | null;
104
115
  inputFailure: BrowserInputFailure | null;
105
- }>(() => emptyState(browserSessionId, enabled));
116
+ actionResultOrder: number;
117
+ }>(() => ({ ...emptyState(browserSessionId, enabled), source }));
106
118
  const visible =
107
- state.browserSessionId === browserSessionId ? state : emptyState(browserSessionId, enabled);
119
+ state.source === source && state.browserSessionId === browserSessionId
120
+ ? state
121
+ : { ...emptyState(browserSessionId, enabled), source };
108
122
  const refreshBlocked = isNonRetryableInteractionError(visible.error);
109
123
  const inputFailureRef = useRef(visible.inputFailure);
110
124
  inputFailureRef.current = visible.inputFailure;
111
125
  const selectedTargetIdRef = useRef<string | null>(visible.selectedTargetId);
112
126
  selectedTargetIdRef.current = visible.selectedTargetId;
127
+ const selectionRevisionRef = useRef(0);
128
+ const actionResultOrderRef = useRef({ next: 0, settled: 0 });
113
129
  const selectedTargetRef = useRef<BrowserTarget | null>(
114
130
  visible.targets.find((target) => target.id === visible.selectedTargetId) ?? null,
115
131
  );
@@ -134,12 +150,20 @@ export function useBrowserSession(options: UseBrowserSessionOptions): UseBrowser
134
150
  });
135
151
  const mutationRef = useRef<{
136
152
  browserSessionId: string | null;
153
+ source: typeof source;
137
154
  count: number;
155
+ needsInventoryReconcile: boolean;
138
156
  }>({
139
157
  browserSessionId,
158
+ source,
140
159
  count: 0,
160
+ needsInventoryReconcile: false,
141
161
  });
142
162
  const mountedRef = useRef(true);
163
+ const isCurrentSource = useCallback(
164
+ () => mountedRef.current && sourceRef.current === source,
165
+ [source],
166
+ );
143
167
 
144
168
  const invalidateRefresh = useCallback(() => {
145
169
  const id = requestRef.current.id + 1;
@@ -148,8 +172,8 @@ export function useBrowserSession(options: UseBrowserSessionOptions): UseBrowser
148
172
  }, []);
149
173
 
150
174
  const refreshInventory = useCallback(
151
- async (checkInput = false) => {
152
- if (!enabled || !browserSessionId) return;
175
+ async (checkInput = false, observeSelectedTarget = true) => {
176
+ if (!enabled || !browserSessionId || !isCurrentSource()) return;
153
177
  const checkedInputFailure = inputFailureRef.current;
154
178
  const id = requestRef.current.id + 1;
155
179
  requestRef.current.controller?.abort();
@@ -164,9 +188,10 @@ export function useBrowserSession(options: UseBrowserSessionOptions): UseBrowser
164
188
  signal: controller.signal,
165
189
  }),
166
190
  ]);
167
- if (!mountedRef.current || requestRef.current.id !== id) return;
191
+ if (!isCurrentSource() || requestRef.current.id !== id) return;
168
192
  const selected = chooseTarget(targetResponse.targets, selectedTargetIdRef.current);
169
193
  selectedTargetIdRef.current = selected?.id ?? null;
194
+ selectedTargetRef.current = selected;
170
195
  const previousObservation =
171
196
  observationRef.current.browserSessionId === browserSessionId
172
197
  ? observationRef.current.observation
@@ -175,8 +200,13 @@ export function useBrowserSession(options: UseBrowserSessionOptions): UseBrowser
175
200
  selected && previousObservation && sameObservationTarget(previousObservation, selected)
176
201
  ? previousObservation
177
202
  : null;
178
- observationRef.current = { browserSessionId, observation: retainedObservation };
203
+ observationRef.current = {
204
+ browserSessionId,
205
+ observation: retainedObservation,
206
+ };
179
207
  setState((current) =>
208
+ isCurrentSource() &&
209
+ current.source === source &&
180
210
  current.browserSessionId === browserSessionId
181
211
  ? {
182
212
  ...current,
@@ -198,7 +228,7 @@ export function useBrowserSession(options: UseBrowserSessionOptions): UseBrowser
198
228
  }
199
229
  : current,
200
230
  );
201
- if (!selected || !semanticObservationEnabledRef.current) return;
231
+ if (!selected || !observeSelectedTarget || !semanticObservationEnabledRef.current) return;
202
232
  try {
203
233
  const observation = await client.observeBrowserTarget(
204
234
  workspaceId,
@@ -206,17 +236,36 @@ export function useBrowserSession(options: UseBrowserSessionOptions): UseBrowser
206
236
  selected.id,
207
237
  { signal: controller.signal },
208
238
  );
209
- if (!mountedRef.current || requestRef.current.id !== id) return;
239
+ if (!isCurrentSource() || requestRef.current.id !== id) return;
240
+ const currentSelected = selectedTargetRef.current;
241
+ if (
242
+ observationRef.current.browserSessionId !== browserSessionId ||
243
+ currentSelected?.id !== selected.id ||
244
+ observation.browserSessionId !== browserSessionId ||
245
+ observation.target.id !== selected.id ||
246
+ observation.target.controllerGeneration !== currentSelected.controllerGeneration ||
247
+ observation.target.targetGeneration !== currentSelected.targetGeneration
248
+ )
249
+ return;
250
+ // Navigation may complete between the inventory and its selected-page
251
+ // observation. Keep both the selected target and input fence coherent.
252
+ selectedTargetRef.current = observation.target;
210
253
  observationRef.current = { browserSessionId, observation };
211
254
  lastObservationAtRef.current = Date.now();
212
255
  setState((current) =>
256
+ isCurrentSource() &&
257
+ current.source === source &&
213
258
  current.browserSessionId === browserSessionId &&
214
259
  current.selectedTargetId === observation.target.id
215
- ? { ...current, observation }
260
+ ? {
261
+ ...current,
262
+ observation,
263
+ targets: replaceTarget(current.targets, observation.target),
264
+ }
216
265
  : current,
217
266
  );
218
267
  } catch (cause) {
219
- if (controller.signal.aborted || !mountedRef.current || requestRef.current.id !== id)
268
+ if (controller.signal.aborted || !isCurrentSource() || requestRef.current.id !== id)
220
269
  return;
221
270
  // Target inventory and the media plane remain authoritative for the
222
271
  // human-facing browser. A failed semantic snapshot must not blank or
@@ -224,9 +273,10 @@ export function useBrowserSession(options: UseBrowserSessionOptions): UseBrowser
224
273
  void cause;
225
274
  }
226
275
  } catch (cause) {
227
- if (controller.signal.aborted || !mountedRef.current || requestRef.current.id !== id)
228
- return;
276
+ if (controller.signal.aborted || !isCurrentSource() || requestRef.current.id !== id) return;
229
277
  setState((current) =>
278
+ isCurrentSource() &&
279
+ current.source === source &&
230
280
  current.browserSessionId === browserSessionId
231
281
  ? {
232
282
  ...current,
@@ -241,13 +291,13 @@ export function useBrowserSession(options: UseBrowserSessionOptions): UseBrowser
241
291
  }
242
292
  }
243
293
  },
244
- [browserSessionId, client, enabled, workspaceId],
294
+ [browserSessionId, client, enabled, isCurrentSource, source, workspaceId],
245
295
  );
246
296
 
247
297
  const refresh = useCallback(async () => await refreshInventory(true), [refreshInventory]);
248
298
 
249
299
  const refreshSemantic = useCallback(async (): Promise<void> => {
250
- if (!enabled || !browserSessionId) return;
300
+ if (!enabled || !browserSessionId || !isCurrentSource()) return;
251
301
  const target = selectedTargetRef.current;
252
302
  if (!target) return;
253
303
  const id = requestRef.current.id + 1;
@@ -261,12 +311,15 @@ export function useBrowserSession(options: UseBrowserSessionOptions): UseBrowser
261
311
  target.id,
262
312
  { signal: controller.signal },
263
313
  );
264
- if (!mountedRef.current || requestRef.current.id !== id) return;
314
+ if (!isCurrentSource() || requestRef.current.id !== id) return;
265
315
  if (!sameObservationTarget(observation, selectedTargetRef.current)) return;
266
316
  observationRef.current = { browserSessionId, observation };
267
317
  lastObservationAtRef.current = Date.now();
268
318
  setState((current) =>
269
- current.browserSessionId === browserSessionId && current.selectedTargetId === target.id
319
+ isCurrentSource() &&
320
+ current.source === source &&
321
+ current.browserSessionId === browserSessionId &&
322
+ current.selectedTargetId === target.id
270
323
  ? { ...current, observation }
271
324
  : current,
272
325
  );
@@ -275,26 +328,28 @@ export function useBrowserSession(options: UseBrowserSessionOptions): UseBrowser
275
328
  } finally {
276
329
  if (requestRef.current.id === id) requestRef.current = { id, controller: null };
277
330
  }
278
- }, [browserSessionId, client, enabled, workspaceId]);
331
+ }, [browserSessionId, client, enabled, isCurrentSource, source, workspaceId]);
279
332
 
280
333
  useEffect(() => {
281
334
  mountedRef.current = true;
282
335
  if (!enabled) {
283
336
  invalidateRefresh();
284
- setState(emptyState(browserSessionId, false));
337
+ setState({ ...emptyState(browserSessionId, false), source });
285
338
  return;
286
339
  }
287
340
  setState((current) =>
341
+ isCurrentSource() &&
342
+ current.source === source &&
288
343
  current.browserSessionId === browserSessionId
289
344
  ? { ...current, loading: true }
290
- : emptyState(browserSessionId, true),
345
+ : { ...emptyState(browserSessionId, true), source },
291
346
  );
292
347
  void refreshInventory();
293
348
  return () => {
294
349
  mountedRef.current = false;
295
350
  invalidateRefresh();
296
351
  };
297
- }, [browserSessionId, enabled, invalidateRefresh, refreshInventory]);
352
+ }, [browserSessionId, enabled, invalidateRefresh, isCurrentSource, refreshInventory, source]);
298
353
 
299
354
  useEffect(() => {
300
355
  const previous = previousSemanticObservationRef.current;
@@ -321,11 +376,23 @@ export function useBrowserSession(options: UseBrowserSessionOptions): UseBrowser
321
376
  }
322
377
  observationRef.current = { browserSessionId, observation: null };
323
378
  setState((current) =>
324
- current.browserSessionId === browserSessionId ? { ...current, observation: null } : current,
379
+ isCurrentSource() &&
380
+ current.source === source &&
381
+ current.browserSessionId === browserSessionId
382
+ ? { ...current, observation: null }
383
+ : current,
325
384
  );
326
385
  void refreshSemantic();
327
386
  }
328
- }, [browserSessionId, enabled, pollIntervalMs, refreshSemantic, semanticObservationEnabled]);
387
+ }, [
388
+ browserSessionId,
389
+ enabled,
390
+ isCurrentSource,
391
+ pollIntervalMs,
392
+ refreshSemantic,
393
+ semanticObservationEnabled,
394
+ source,
395
+ ]);
329
396
 
330
397
  useEffect(() => {
331
398
  if (!enabled || !pageLive || visible.mutating || refreshBlocked) return;
@@ -339,6 +406,7 @@ export function useBrowserSession(options: UseBrowserSessionOptions): UseBrowser
339
406
  if (!enabled || !browserSessionId || !pageLive || refreshBlocked) return;
340
407
  let disposed = false;
341
408
  const heartbeat = () => {
409
+ if (!isCurrentSource()) return;
342
410
  void client.heartbeatBrowserSession(workspaceId, browserSessionId).catch(() => {
343
411
  if (!disposed) void refreshInventory();
344
412
  });
@@ -348,167 +416,282 @@ export function useBrowserSession(options: UseBrowserSessionOptions): UseBrowser
348
416
  disposed = true;
349
417
  clearInterval(timer);
350
418
  };
351
- }, [browserSessionId, client, enabled, pageLive, refreshInventory, refreshBlocked, workspaceId]);
419
+ }, [
420
+ browserSessionId,
421
+ client,
422
+ enabled,
423
+ isCurrentSource,
424
+ pageLive,
425
+ refreshInventory,
426
+ refreshBlocked,
427
+ workspaceId,
428
+ ]);
352
429
 
353
430
  const runMutation = useCallback(
354
- async <T>(scopeBrowserSessionId: string, operation: () => Promise<T>): Promise<T> => {
431
+ async <T>(
432
+ scopeBrowserSessionId: string,
433
+ operation: (selectionRevision: number, readRequestId: number) => Promise<T>,
434
+ selectionRead = false,
435
+ changesInventory = false,
436
+ ): Promise<T> => {
437
+ if (!enabled || !isCurrentSource()) {
438
+ throw new Error("The browser source is no longer selected.");
439
+ }
440
+ const selectionRevision = ++selectionRevisionRef.current;
355
441
  invalidateRefresh();
356
- if (mutationRef.current.browserSessionId !== scopeBrowserSessionId) {
442
+ const readRequestId = requestRef.current.id;
443
+ if (
444
+ mutationRef.current.browserSessionId !== scopeBrowserSessionId ||
445
+ mutationRef.current.source !== source
446
+ ) {
357
447
  mutationRef.current = {
358
448
  browserSessionId: scopeBrowserSessionId,
449
+ source,
359
450
  count: 0,
451
+ needsInventoryReconcile: false,
360
452
  };
361
453
  }
362
454
  mutationRef.current.count += 1;
363
455
  setState((current) =>
456
+ isCurrentSource() &&
457
+ current.source === source &&
364
458
  current.browserSessionId === scopeBrowserSessionId
365
459
  ? { ...current, mutating: true, error: null }
366
460
  : current,
367
461
  );
368
462
  try {
369
- return await operation();
463
+ return await operation(selectionRevision, readRequestId);
370
464
  } catch (cause) {
371
465
  const error = cause instanceof Error ? cause : new Error(String(cause));
372
- setState((current) =>
373
- current.browserSessionId === scopeBrowserSessionId ? { ...current, error } : current,
374
- );
466
+ // Selection observations share refresh ordering; physical mutations
467
+ // keep their existing outcome admission.
468
+ if (!selectionRead || requestRef.current.id === readRequestId) {
469
+ setState((current) =>
470
+ isCurrentSource() &&
471
+ selectionRevisionRef.current === selectionRevision &&
472
+ current.source === source &&
473
+ current.browserSessionId === scopeBrowserSessionId
474
+ ? { ...current, error }
475
+ : current,
476
+ );
477
+ }
375
478
  throw error;
376
479
  } finally {
377
- if (mutationRef.current.browserSessionId === scopeBrowserSessionId) {
480
+ if (
481
+ mutationRef.current.browserSessionId === scopeBrowserSessionId &&
482
+ mutationRef.current.source === source
483
+ ) {
484
+ if (
485
+ changesInventory &&
486
+ (selectionRevisionRef.current !== selectionRevision ||
487
+ requestRef.current.id !== readRequestId)
488
+ ) {
489
+ mutationRef.current.needsInventoryReconcile = true;
490
+ }
378
491
  mutationRef.current.count = Math.max(0, mutationRef.current.count - 1);
379
492
  const mutating = mutationRef.current.count > 0;
380
493
  setState((current) =>
381
- current.browserSessionId === scopeBrowserSessionId ? { ...current, mutating } : current,
494
+ isCurrentSource() &&
495
+ current.source === source &&
496
+ current.browserSessionId === scopeBrowserSessionId
497
+ ? { ...current, mutating }
498
+ : current,
382
499
  );
500
+ if (!mutating && mutationRef.current.needsInventoryReconcile && isCurrentSource()) {
501
+ mutationRef.current.needsInventoryReconcile = false;
502
+ // A superseded tab mutation still changed the real inventory. Wait
503
+ // for all selections to settle, then reconcile without replacing
504
+ // the latest selected-page observation or cancelling its read.
505
+ void refreshInventory(false, false);
506
+ }
383
507
  }
384
508
  }
385
509
  },
386
- [invalidateRefresh],
510
+ [enabled, invalidateRefresh, isCurrentSource, refreshInventory, source],
387
511
  );
388
512
 
389
513
  const selectTarget = useCallback(
390
514
  async (targetId: string): Promise<BrowserTarget> => {
391
515
  if (!browserSessionId) throw new Error("No BrowserSession is selected.");
392
- return await runMutation(browserSessionId, async () => {
393
- let targets = visible.targets;
394
- let target: BrowserTarget;
395
- let observation: BrowserObservation;
396
- try {
397
- observation = await client.selectBrowserTarget(workspaceId, browserSessionId, targetId);
398
- target = observation.target;
399
- } catch (cause) {
400
- if (!isMissingBrowserTarget(cause)) throw cause;
401
- // A physical/attached tab may disappear between inventory and click.
402
- // Reconcile once from the authoritative controller inventory and move
403
- // to its live selected/first page instead of leaving a dead tab ID in
404
- // React state. Session-level 404s are deliberately not swallowed.
405
- const response = await client.listBrowserTargets(workspaceId, browserSessionId);
406
- targets = response.targets;
407
- const fallback = chooseTarget(targets, null);
408
- if (!fallback) {
409
- selectedTargetIdRef.current = null;
410
- observationRef.current = { browserSessionId, observation: null };
411
- setState((current) =>
412
- current.browserSessionId === browserSessionId
413
- ? {
414
- ...current,
415
- targets,
416
- selectedTargetId: null,
417
- observation: null,
418
- }
419
- : current,
516
+ return await runMutation(
517
+ browserSessionId,
518
+ async (selectionRevision, readRequestId) => {
519
+ const isCurrentSelection = () =>
520
+ isCurrentSource() && selectionRevisionRef.current === selectionRevision;
521
+ // Once admitted, a queued observation survives immediate input.
522
+ const isCurrentSelectionRead = () =>
523
+ isCurrentSelection() && requestRef.current.id === readRequestId;
524
+ let targets = visible.targets;
525
+ let target: BrowserTarget;
526
+ let observation: BrowserObservation;
527
+ try {
528
+ observation = await client.selectBrowserTarget(workspaceId, browserSessionId, targetId);
529
+ target = observation.target;
530
+ } catch (cause) {
531
+ if (!isCurrentSelectionRead() || !isMissingBrowserTarget(cause)) throw cause;
532
+ // A physical/attached tab may disappear between inventory and click.
533
+ // Reconcile once from the authoritative controller inventory and move
534
+ // to its live selected/first page instead of leaving a dead tab ID in
535
+ // React state. Session-level 404s are deliberately not swallowed.
536
+ const response = await client.listBrowserTargets(workspaceId, browserSessionId);
537
+ if (!isCurrentSelectionRead()) throw cause;
538
+ targets = response.targets;
539
+ const fallback = chooseTarget(targets, null);
540
+ if (!fallback) {
541
+ selectedTargetIdRef.current = null;
542
+ selectedTargetRef.current = null;
543
+ observationRef.current = { browserSessionId, observation: null };
544
+ setState((current) =>
545
+ isCurrentSelection() &&
546
+ current.source === source &&
547
+ current.browserSessionId === browserSessionId
548
+ ? {
549
+ ...current,
550
+ targets,
551
+ selectedTargetId: null,
552
+ observation: null,
553
+ }
554
+ : current,
555
+ );
556
+ throw cause;
557
+ }
558
+ observation = await client.selectBrowserTarget(
559
+ workspaceId,
560
+ browserSessionId,
561
+ fallback.id,
420
562
  );
421
- throw cause;
563
+ target = observation.target;
422
564
  }
423
- observation = await client.selectBrowserTarget(
424
- workspaceId,
425
- browserSessionId,
426
- fallback.id,
565
+ if (!isCurrentSelectionRead()) return target;
566
+ selectedTargetIdRef.current = target.id;
567
+ selectedTargetRef.current = target;
568
+ observationRef.current = { browserSessionId, observation };
569
+ lastObservationAtRef.current = Date.now();
570
+ setState((current) =>
571
+ isCurrentSelection() &&
572
+ current.source === source &&
573
+ current.browserSessionId === browserSessionId
574
+ ? {
575
+ ...current,
576
+ targets: replaceTarget(targets, target).map((candidate) => ({
577
+ ...candidate,
578
+ selected: candidate.id === target.id,
579
+ })),
580
+ selectedTargetId: target.id,
581
+ observation,
582
+ }
583
+ : current,
427
584
  );
428
- target = observation.target;
429
- }
430
- selectedTargetIdRef.current = target.id;
431
- observationRef.current = { browserSessionId, observation };
432
- lastObservationAtRef.current = Date.now();
433
- setState((current) =>
434
- current.browserSessionId === browserSessionId
435
- ? {
436
- ...current,
437
- targets: targets.map((candidate) => ({
438
- ...candidate,
439
- selected: candidate.id === target.id,
440
- })),
441
- selectedTargetId: target.id,
442
- observation,
443
- }
444
- : current,
445
- );
446
- return target;
447
- });
585
+ return target;
586
+ },
587
+ true,
588
+ );
448
589
  },
449
- [browserSessionId, client, runMutation, visible.targets, workspaceId],
590
+ [browserSessionId, client, isCurrentSource, runMutation, source, visible.targets, workspaceId],
450
591
  );
451
592
 
452
593
  const openTarget = useCallback(
453
594
  async (url?: string): Promise<BrowserTarget> => {
454
595
  if (!browserSessionId) throw new Error("No BrowserSession is selected.");
455
- return await runMutation(browserSessionId, async () => {
456
- const observation = await client.openBrowserTarget(
457
- workspaceId,
458
- browserSessionId,
459
- url === undefined ? {} : { url },
460
- );
461
- const target = observation.target;
462
- selectedTargetIdRef.current = target.id;
463
- observationRef.current = { browserSessionId, observation };
464
- lastObservationAtRef.current = Date.now();
465
- setState((current) =>
466
- current.browserSessionId === browserSessionId
467
- ? {
468
- ...current,
469
- targets: [
470
- ...current.targets.map((candidate) => ({
471
- ...candidate,
472
- selected: false,
473
- })),
474
- target,
475
- ],
476
- selectedTargetId: target.id,
477
- observation,
478
- }
479
- : current,
480
- );
481
- return target;
482
- });
596
+ return await runMutation(
597
+ browserSessionId,
598
+ async (selectionRevision, readRequestId) => {
599
+ const isCurrentSelection = () =>
600
+ isCurrentSource() && selectionRevisionRef.current === selectionRevision;
601
+ const isCurrentSelectionRead = () =>
602
+ isCurrentSelection() && requestRef.current.id === readRequestId;
603
+ const observation = await client.openBrowserTarget(
604
+ workspaceId,
605
+ browserSessionId,
606
+ url === undefined ? {} : { url },
607
+ );
608
+ const target = observation.target;
609
+ if (!isCurrentSource()) return target;
610
+ if (!isCurrentSelectionRead()) return target;
611
+ selectedTargetIdRef.current = target.id;
612
+ selectedTargetRef.current = target;
613
+ observationRef.current = { browserSessionId, observation };
614
+ lastObservationAtRef.current = Date.now();
615
+ setState((current) =>
616
+ isCurrentSelection() &&
617
+ current.source === source &&
618
+ current.browserSessionId === browserSessionId
619
+ ? {
620
+ ...current,
621
+ targets: [
622
+ ...current.targets.map((candidate) => ({
623
+ ...candidate,
624
+ selected: false,
625
+ })),
626
+ target,
627
+ ],
628
+ selectedTargetId: target.id,
629
+ observation,
630
+ }
631
+ : current,
632
+ );
633
+ return target;
634
+ },
635
+ false,
636
+ true,
637
+ );
483
638
  },
484
- [browserSessionId, client, runMutation, workspaceId],
639
+ [browserSessionId, client, isCurrentSource, runMutation, source, workspaceId],
485
640
  );
486
641
 
487
642
  const closeTarget = useCallback(
488
643
  async (targetId: string): Promise<void> => {
489
644
  if (!browserSessionId) throw new Error("No BrowserSession is selected.");
490
- await runMutation(browserSessionId, async () => {
491
- const response = await client.closeBrowserTarget(workspaceId, browserSessionId, targetId);
492
- const selected = chooseTarget(response.targets, null);
493
- const observation = selected
494
- ? await client.observeBrowserTarget(workspaceId, browserSessionId, selected.id)
495
- : null;
496
- selectedTargetIdRef.current = selected?.id ?? null;
497
- observationRef.current = { browserSessionId, observation };
498
- if (observation) lastObservationAtRef.current = Date.now();
499
- setState((current) =>
500
- current.browserSessionId === browserSessionId
501
- ? {
502
- ...current,
503
- targets: response.targets,
504
- selectedTargetId: selected?.id ?? null,
505
- observation,
506
- }
507
- : current,
508
- );
509
- });
645
+ await runMutation(
646
+ browserSessionId,
647
+ async (selectionRevision, readRequestId) => {
648
+ const isCurrentSelection = () =>
649
+ isCurrentSource() && selectionRevisionRef.current === selectionRevision;
650
+ const isCurrentSelectionRead = () =>
651
+ isCurrentSelection() && requestRef.current.id === readRequestId;
652
+ const response = await client.closeBrowserTarget(workspaceId, browserSessionId, targetId);
653
+ if (!isCurrentSource()) return;
654
+ if (!isCurrentSelectionRead()) return;
655
+ const selected = chooseTarget(response.targets, selectedTargetIdRef.current);
656
+ const observed = selected
657
+ ? await client.observeBrowserTarget(workspaceId, browserSessionId, selected.id)
658
+ : null;
659
+ if (!isCurrentSource()) return;
660
+ if (!isCurrentSelectionRead()) return;
661
+ const observation =
662
+ observed &&
663
+ selected &&
664
+ observed.browserSessionId === browserSessionId &&
665
+ observed.target.id === selected.id &&
666
+ observed.target.controllerGeneration === selected.controllerGeneration &&
667
+ observed.target.targetGeneration === selected.targetGeneration
668
+ ? observed
669
+ : null;
670
+ const currentTarget = observation?.target ?? selected;
671
+ selectedTargetIdRef.current = currentTarget?.id ?? null;
672
+ selectedTargetRef.current = currentTarget;
673
+ observationRef.current = { browserSessionId, observation };
674
+ if (observation) lastObservationAtRef.current = Date.now();
675
+ setState((current) =>
676
+ isCurrentSelection() &&
677
+ current.source === source &&
678
+ current.browserSessionId === browserSessionId
679
+ ? {
680
+ ...current,
681
+ targets: currentTarget
682
+ ? replaceTarget(response.targets, currentTarget)
683
+ : response.targets,
684
+ selectedTargetId: currentTarget?.id ?? null,
685
+ observation,
686
+ }
687
+ : current,
688
+ );
689
+ },
690
+ false,
691
+ true,
692
+ );
510
693
  },
511
- [browserSessionId, client, runMutation, workspaceId],
694
+ [browserSessionId, client, isCurrentSource, runMutation, source, workspaceId],
512
695
  );
513
696
 
514
697
  const dispatchAction = useCallback(
@@ -516,7 +699,10 @@ export function useBrowserSession(options: UseBrowserSessionOptions): UseBrowser
516
699
  action: BrowserAction | BrowserActionBatch,
517
700
  operationId: string,
518
701
  frame:
519
- | (Pick<BrowserFrame, "browserSessionId" | "targetId" | "targetGeneration"> & {
702
+ | (Pick<
703
+ BrowserFrame,
704
+ "browserSessionId" | "controllerGeneration" | "targetId" | "targetGeneration"
705
+ > & {
520
706
  frameId: string | null;
521
707
  documentGeneration: string | null;
522
708
  })
@@ -524,6 +710,12 @@ export function useBrowserSession(options: UseBrowserSessionOptions): UseBrowser
524
710
  observationMode: "none" | "input" = "none",
525
711
  ): Promise<BrowserActionReceipt> => {
526
712
  if (!browserSessionId) throw new Error("No BrowserSession is selected.");
713
+ if (!enabled || !isCurrentSource()) {
714
+ throw new Error("The browser source is no longer selected.");
715
+ }
716
+ if (!selectedTargetRef.current) {
717
+ throw new Error("The browser page is not ready for input.");
718
+ }
527
719
  if (frame && frame.browserSessionId !== browserSessionId) {
528
720
  throw new Error("The displayed browser frame belongs to another BrowserSession.");
529
721
  }
@@ -538,7 +730,7 @@ export function useBrowserSession(options: UseBrowserSessionOptions): UseBrowser
538
730
  expectedDocumentGeneration: frame.documentGeneration,
539
731
  expectedFrameId: frame.frameId,
540
732
  }
541
- : currentObservation
733
+ : currentObservation && sameObservationTarget(currentObservation, selectedTargetRef.current)
542
734
  ? {
543
735
  targetId: currentObservation.target.id,
544
736
  expectedTargetGeneration: currentObservation.target.targetGeneration,
@@ -549,6 +741,32 @@ export function useBrowserSession(options: UseBrowserSessionOptions): UseBrowser
549
741
  if (!fence) {
550
742
  throw new Error("The browser page is not ready for input.");
551
743
  }
744
+ const expectedControllerGeneration =
745
+ frame?.controllerGeneration ?? currentObservation!.target.controllerGeneration;
746
+ const selectionRevision = selectionRevisionRef.current;
747
+ const resultOrder = ++actionResultOrderRef.current.next;
748
+ let resultDocumentGeneration = fence.expectedDocumentGeneration;
749
+ const isCurrentResultView = () => {
750
+ const selected = selectedTargetRef.current;
751
+ return (
752
+ isCurrentSource() &&
753
+ selectionRevisionRef.current === selectionRevision &&
754
+ selected?.browserSessionId === browserSessionId &&
755
+ selected.id === fence.targetId &&
756
+ selected.controllerGeneration === expectedControllerGeneration &&
757
+ selected.targetGeneration === fence.expectedTargetGeneration &&
758
+ (selected.documentGeneration === fence.expectedDocumentGeneration ||
759
+ selected.documentGeneration === resultDocumentGeneration)
760
+ );
761
+ };
762
+ const canProjectResult = () =>
763
+ isCurrentResultView() && resultOrder >= actionResultOrderRef.current.settled;
764
+ const settleResult = () => {
765
+ if (!canProjectResult()) return false;
766
+ actionResultOrderRef.current.settled = resultOrder;
767
+ invalidateRefresh();
768
+ return true;
769
+ };
552
770
  try {
553
771
  const receipt = await client.actInBrowser(workspaceId, browserSessionId, {
554
772
  operationId,
@@ -556,42 +774,90 @@ export function useBrowserSession(options: UseBrowserSessionOptions): UseBrowser
556
774
  observationMode,
557
775
  action,
558
776
  });
559
- if (receipt.state !== "completed") {
560
- const inputFailure: BrowserInputFailure = {
561
- operationId: receipt.operationId,
562
- state: receipt.state,
563
- error: receipt.error,
564
- };
565
- setState((current) =>
566
- current.browserSessionId === browserSessionId ? { ...current, inputFailure } : current,
567
- );
568
- }
569
- if (receipt.observation) {
777
+ resultDocumentGeneration =
778
+ receipt.observation?.target.documentGeneration ?? fence.expectedDocumentGeneration;
779
+ const settled =
780
+ receipt.state === "completed" ||
781
+ receipt.state === "failed" ||
782
+ receipt.state === "outcome_unknown";
783
+ // Keep the operation result available to its caller, but an older
784
+ // delivery cannot replace a newer settled view or input failure.
785
+ if (!canProjectResult() || (settled && !settleResult())) return receipt;
786
+ const receivedObservation = receipt.observation;
787
+ const selectedTarget = selectedTargetRef.current;
788
+ let projectedObservation: BrowserObservation | null = null;
789
+ if (
790
+ settled &&
791
+ receivedObservation &&
792
+ canProjectResult() &&
793
+ receipt.browserSessionId === browserSessionId &&
794
+ receipt.controllerGeneration === expectedControllerGeneration &&
795
+ receipt.targetId === fence.targetId &&
796
+ observationRef.current.browserSessionId === browserSessionId &&
797
+ receivedObservation.browserSessionId === browserSessionId &&
798
+ selectedTarget?.browserSessionId === browserSessionId &&
799
+ selectedTarget.id === fence.targetId &&
800
+ receivedObservation.target.id === fence.targetId &&
801
+ selectedTarget.controllerGeneration === expectedControllerGeneration &&
802
+ receivedObservation.target.controllerGeneration === expectedControllerGeneration &&
803
+ selectedTarget.targetGeneration === fence.expectedTargetGeneration &&
804
+ receivedObservation.target.targetGeneration === fence.expectedTargetGeneration &&
805
+ (selectedTarget.documentGeneration === fence.expectedDocumentGeneration ||
806
+ selectedTarget.documentGeneration === receivedObservation.target.documentGeneration)
807
+ ) {
808
+ // A receipt updates its own selected page only. Its operation outcome
809
+ // remains available even when the human has moved to another page.
810
+ projectedObservation = receivedObservation;
570
811
  observationRef.current = {
571
812
  browserSessionId,
572
- observation: receipt.observation,
813
+ observation: receivedObservation,
573
814
  };
815
+ selectedTargetRef.current = receivedObservation.target;
574
816
  lastObservationAtRef.current = Date.now();
575
- setState((current) =>
576
- current.browserSessionId === browserSessionId
577
- ? {
578
- ...current,
579
- observation: receipt.observation,
580
- targets: replaceTarget(current.targets, receipt.observation!.target),
581
- }
582
- : current,
583
- );
584
817
  }
818
+ setState((current) =>
819
+ isCurrentResultView() &&
820
+ current.source === source &&
821
+ current.browserSessionId === browserSessionId &&
822
+ current.actionResultOrder <= resultOrder
823
+ ? {
824
+ ...current,
825
+ actionResultOrder: settled ? resultOrder : current.actionResultOrder,
826
+ error: receipt.state === "completed" ? null : current.error,
827
+ inputFailure:
828
+ receipt.state === "completed"
829
+ ? current.inputFailure
830
+ : {
831
+ operationId: receipt.operationId,
832
+ state: receipt.state,
833
+ error: receipt.error,
834
+ },
835
+ ...(projectedObservation
836
+ ? {
837
+ observation: projectedObservation,
838
+ targets: replaceTarget(current.targets, projectedObservation.target),
839
+ }
840
+ : {}),
841
+ }
842
+ : current,
843
+ );
585
844
  return receipt;
586
845
  } catch (cause) {
587
846
  const error = cause instanceof Error ? cause : new Error(String(cause));
588
- setState((current) =>
589
- current.browserSessionId === browserSessionId ? { ...current, error } : current,
590
- );
847
+ if (settleResult()) {
848
+ setState((current) =>
849
+ isCurrentResultView() &&
850
+ current.source === source &&
851
+ current.browserSessionId === browserSessionId &&
852
+ current.actionResultOrder <= resultOrder
853
+ ? { ...current, error, actionResultOrder: resultOrder }
854
+ : current,
855
+ );
856
+ }
591
857
  throw error;
592
858
  }
593
859
  },
594
- [browserSessionId, client, workspaceId],
860
+ [browserSessionId, client, enabled, invalidateRefresh, isCurrentSource, source, workspaceId],
595
861
  );
596
862
 
597
863
  const act = useCallback(
@@ -615,17 +881,20 @@ export function useBrowserSession(options: UseBrowserSessionOptions): UseBrowser
615
881
 
616
882
  const observeForInput = useCallback(async (): Promise<BrowserObservation> => {
617
883
  const target = selectedTargetRef.current;
884
+ if (!enabled || !isCurrentSource()) {
885
+ throw new Error("The browser source is no longer selected.");
886
+ }
618
887
  if (!browserSessionId || !target) throw new Error("No browser tab is selected.");
619
888
  const observation = await client.observeBrowserTarget(workspaceId, browserSessionId, target.id);
620
889
  if (
621
- !mountedRef.current ||
890
+ !isCurrentSource() ||
622
891
  observation.browserSessionId !== browserSessionId ||
623
892
  !sameObservationTarget(observation, selectedTargetRef.current)
624
893
  ) {
625
894
  throw new Error("The browser page changed. Open the options again.");
626
895
  }
627
896
  return observation;
628
- }, [browserSessionId, client, workspaceId]);
897
+ }, [browserSessionId, client, enabled, isCurrentSource, workspaceId]);
629
898
 
630
899
  const actFromObservation = useCallback(
631
900
  async (action: BrowserAction, observation: BrowserObservation) => {
@@ -634,6 +903,7 @@ export function useBrowserSession(options: UseBrowserSessionOptions): UseBrowser
634
903
  }
635
904
  return await dispatchAction(action, crypto.randomUUID(), {
636
905
  browserSessionId: observation.browserSessionId,
906
+ controllerGeneration: observation.target.controllerGeneration,
637
907
  targetId: observation.target.id,
638
908
  targetGeneration: observation.target.targetGeneration,
639
909
  documentGeneration: observation.target.documentGeneration,
@@ -645,6 +915,9 @@ export function useBrowserSession(options: UseBrowserSessionOptions): UseBrowser
645
915
 
646
916
  const diagnostics = useCallback(
647
917
  async (diagnosticOptions: BrowserDiagnosticsOptions = {}): Promise<BrowserDiagnosticBatch> => {
918
+ if (!enabled || !isCurrentSource()) {
919
+ throw new Error("The browser source is no longer selected.");
920
+ }
648
921
  const targetId = selectedTargetIdRef.current;
649
922
  if (!browserSessionId || !targetId) {
650
923
  throw new Error("No browser tab is selected.");
@@ -656,17 +929,20 @@ export function useBrowserSession(options: UseBrowserSessionOptions): UseBrowser
656
929
  diagnosticOptions,
657
930
  );
658
931
  },
659
- [browserSessionId, client, workspaceId],
932
+ [browserSessionId, client, enabled, isCurrentSource, workspaceId],
660
933
  );
661
934
 
662
935
  const readClipboard = useCallback(async (): Promise<BrowserClipboard> => {
936
+ if (!enabled || !isCurrentSource()) {
937
+ throw new Error("The browser source is no longer selected.");
938
+ }
663
939
  if (!browserSessionId) throw new Error("No BrowserSession is selected.");
664
940
  const clipboard = await client.readBrowserClipboard(workspaceId, browserSessionId);
665
- if (clipboard.browserSessionId !== browserSessionId) {
941
+ if (!isCurrentSource() || clipboard.browserSessionId !== browserSessionId) {
666
942
  throw new Error("Browser clipboard belongs to another BrowserSession.");
667
943
  }
668
944
  return clipboard;
669
- }, [browserSessionId, client, workspaceId]);
945
+ }, [browserSessionId, client, enabled, isCurrentSource, workspaceId]);
670
946
 
671
947
  return {
672
948
  session: visible.session,
@@ -702,6 +978,7 @@ function emptyState(browserSessionId: string | null, loading: boolean) {
702
978
  mutating: false,
703
979
  error: null as Error | null,
704
980
  inputFailure: null as BrowserInputFailure | null,
981
+ actionResultOrder: 0,
705
982
  };
706
983
  }
707
984