@blario/react-native 0.0.0 → 0.1.0

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 (313) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +378 -0
  3. package/dist/BlarProvider.d.ts +74 -0
  4. package/dist/actions/batched.d.ts +1 -0
  5. package/dist/actions/click.d.ts +4 -0
  6. package/dist/actions/commit-watch.d.ts +15 -0
  7. package/dist/actions/events.d.ts +35 -0
  8. package/dist/actions/executor.d.ts +29 -0
  9. package/dist/actions/highlight.d.ts +4 -0
  10. package/dist/actions/interaction-guards.d.ts +9 -0
  11. package/dist/actions/landing.d.ts +11 -0
  12. package/dist/actions/navigation-driver.d.ts +22 -0
  13. package/dist/actions/parse.d.ts +4 -0
  14. package/dist/actions/result.d.ts +4 -0
  15. package/dist/actions/scrolling.d.ts +12 -0
  16. package/dist/actions/services.d.ts +20 -0
  17. package/dist/actions/settle.d.ts +13 -0
  18. package/dist/actions/switch.d.ts +3 -0
  19. package/dist/actions/target.d.ts +25 -0
  20. package/dist/actions/text-input.d.ts +9 -0
  21. package/dist/actions/types.d.ts +59 -0
  22. package/dist/agent/batch-commands.d.ts +17 -0
  23. package/dist/agent/compose.d.ts +23 -0
  24. package/dist/agent/controller.d.ts +28 -0
  25. package/dist/agent/frame-runner.d.ts +9 -0
  26. package/dist/agent/frames.d.ts +180 -0
  27. package/dist/agent/handlers/agent-command.d.ts +5 -0
  28. package/dist/agent/handlers/batch.d.ts +5 -0
  29. package/dist/agent/handlers/consent-screenshot.d.ts +4 -0
  30. package/dist/agent/handlers/context.d.ts +16 -0
  31. package/dist/agent/handlers/dom-state.d.ts +4 -0
  32. package/dist/agent/handlers/element-context.d.ts +2 -0
  33. package/dist/agent/handlers/screenshot.d.ts +3 -0
  34. package/dist/agent/handlers/text-dom.d.ts +5 -0
  35. package/dist/agent/stage.d.ts +13 -0
  36. package/dist/agent-mode/action-description.d.ts +3 -0
  37. package/dist/agent-mode/bubbles.d.ts +10 -0
  38. package/dist/agent-mode/colors.d.ts +12 -0
  39. package/dist/agent-mode/controller.d.ts +92 -0
  40. package/dist/agent-mode/cursor-follow.d.ts +12 -0
  41. package/dist/agent-mode/cursor.d.ts +37 -0
  42. package/dist/agent-mode/frames.d.ts +36 -0
  43. package/dist/agent-mode/haptics.d.ts +6 -0
  44. package/dist/agent-mode/keep-awake.d.ts +19 -0
  45. package/dist/agent-mode/layer-stacking.d.ts +7 -0
  46. package/dist/agent-mode/markdown.d.ts +19 -0
  47. package/dist/agent-mode/panel-geometry.d.ts +6 -0
  48. package/dist/agent-mode/resume.d.ts +13 -0
  49. package/dist/agent-mode/run-memory.d.ts +19 -0
  50. package/dist/agent-mode/spotlight-controller.d.ts +68 -0
  51. package/dist/agent-mode/spotlight.d.ts +6 -0
  52. package/dist/agent-mode/state.d.ts +9 -0
  53. package/dist/agent-mode/status.d.ts +11 -0
  54. package/dist/agent-mode/timers.d.ts +6 -0
  55. package/dist/agent-mode/ui/AgentDock.d.ts +37 -0
  56. package/dist/agent-mode/ui/AgentLayer.d.ts +19 -0
  57. package/dist/agent-mode/ui/AgentLayerHost.d.ts +8 -0
  58. package/dist/agent-mode/ui/AgentPanel.d.ts +19 -0
  59. package/dist/agent-mode/ui/AgentTap.d.ts +10 -0
  60. package/dist/agent-mode/ui/MarkdownText.d.ts +8 -0
  61. package/dist/agent-mode/ui/MessageBubbles.d.ts +9 -0
  62. package/dist/agent-mode/ui/PendingOverlay.d.ts +5 -0
  63. package/dist/agent-mode/ui/SpotlightLayer.d.ts +9 -0
  64. package/dist/agent-mode/ui/SpotlightOverlay.d.ts +7 -0
  65. package/dist/agent-mode/ui/StatusCard.d.ts +14 -0
  66. package/dist/agent-mode/ui/TargetPulse.d.ts +9 -0
  67. package/dist/agent-mode/ui/TouchShield.d.ts +5 -0
  68. package/dist/agent-mode/ui/WarningIcon.d.ts +6 -0
  69. package/dist/agent-mode/ui/full-window-overlay.d.ts +5 -0
  70. package/dist/agent-mode/ui/layer-host.d.ts +2 -0
  71. package/dist/backend/api-client.d.ts +38 -0
  72. package/dist/backend/api-types.d.ts +25 -0
  73. package/dist/backend/app-info.d.ts +14 -0
  74. package/dist/backend/backend-transport.d.ts +70 -0
  75. package/dist/backend/backoff.d.ts +9 -0
  76. package/dist/backend/base64.d.ts +1 -0
  77. package/dist/backend/errors.d.ts +17 -0
  78. package/dist/backend/foreground.d.ts +1 -0
  79. package/dist/backend/health.d.ts +17 -0
  80. package/dist/backend/identity-proof.d.ts +23 -0
  81. package/dist/backend/identity.d.ts +27 -0
  82. package/dist/backend/message-metadata.d.ts +19 -0
  83. package/dist/backend/presence.d.ts +20 -0
  84. package/dist/backend/response-cache.d.ts +9 -0
  85. package/dist/backend/session-change.d.ts +2 -0
  86. package/dist/backend/session-socket.d.ts +49 -0
  87. package/dist/backend/session.d.ts +39 -0
  88. package/dist/backend/socket-url.d.ts +9 -0
  89. package/dist/backend/storage.d.ts +13 -0
  90. package/dist/backend/tab-id.d.ts +2 -0
  91. package/dist/blar.d.ts +37 -0
  92. package/dist/chat/ChatLayer.d.ts +29 -0
  93. package/dist/chat/ChatSheet.d.ts +11 -0
  94. package/dist/chat/ChatView.d.ts +19 -0
  95. package/dist/chat/Launcher.d.ts +14 -0
  96. package/dist/chat/bridge.d.ts +22 -0
  97. package/dist/chat/chat-coordinator.d.ts +57 -0
  98. package/dist/chat/chat-state.d.ts +20 -0
  99. package/dist/chat/navigation.d.ts +5 -0
  100. package/dist/chat/options.d.ts +22 -0
  101. package/dist/chat/webview.d.ts +10 -0
  102. package/dist/chat/widget-bundle.generated.d.ts +2 -0
  103. package/dist/chat/widget-shell.d.ts +24 -0
  104. package/dist/confirmation/port.d.ts +12 -0
  105. package/dist/fiber/locate.d.ts +7 -0
  106. package/dist/fiber/narrow.d.ts +5 -0
  107. package/dist/fiber/reader.d.ts +10 -0
  108. package/dist/fiber/text.d.ts +4 -0
  109. package/dist/fiber/types.d.ts +21 -0
  110. package/dist/host/border-radius.d.ts +1 -0
  111. package/dist/host/instance.d.ts +9 -0
  112. package/dist/host/measure.d.ts +3 -0
  113. package/dist/host/rect.d.ts +22 -0
  114. package/dist/host/visible-area.d.ts +2 -0
  115. package/dist/index.d.ts +10 -0
  116. package/dist/nudge/NudgeBubble.d.ts +15 -0
  117. package/dist/nudge/nudge.d.ts +23 -0
  118. package/dist/optional/expo-haptics.d.ts +1 -0
  119. package/dist/optional/expo-keep-awake.d.ts +1 -0
  120. package/dist/optional/react-native-haptic-feedback.d.ts +1 -0
  121. package/dist/optional/react-native-keep-awake.d.ts +1 -0
  122. package/dist/overlay/AgentOverlay.d.ts +5 -0
  123. package/dist/screenshot/capture-backend.d.ts +6 -0
  124. package/dist/screenshot/screenshotter.d.ts +18 -0
  125. package/dist/screenshot/skia.d.ts +35 -0
  126. package/dist/screenshot/view-shot.d.ts +2 -0
  127. package/dist/snapshot/capture.d.ts +36 -0
  128. package/dist/snapshot/classify.d.ts +11 -0
  129. package/dist/snapshot/collect.d.ts +27 -0
  130. package/dist/snapshot/element-context.d.ts +9 -0
  131. package/dist/snapshot/icon.d.ts +2 -0
  132. package/dist/snapshot/kinds.d.ts +3 -0
  133. package/dist/snapshot/modals.d.ts +2 -0
  134. package/dist/snapshot/navigation.d.ts +14 -0
  135. package/dist/snapshot/ordering.d.ts +2 -0
  136. package/dist/snapshot/properties.d.ts +14 -0
  137. package/dist/snapshot/registry.d.ts +16 -0
  138. package/dist/snapshot/render.d.ts +10 -0
  139. package/dist/snapshot/route-summary.d.ts +3 -0
  140. package/dist/snapshot/scroll.d.ts +15 -0
  141. package/dist/snapshot/text-layout.d.ts +8 -0
  142. package/dist/snapshot/view.d.ts +30 -0
  143. package/dist/snapshot/visibility.d.ts +17 -0
  144. package/dist/snapshot/wrappers.d.ts +11 -0
  145. package/dist/telemetry/SdkErrorBoundary.d.ts +15 -0
  146. package/dist/telemetry/build-info.generated.d.ts +2 -0
  147. package/dist/telemetry/envelope.d.ts +53 -0
  148. package/dist/telemetry/reporter.d.ts +33 -0
  149. package/dist/telemetry/scrub.d.ts +1 -0
  150. package/dist/telemetry/sdk-reporter.d.ts +5 -0
  151. package/dist/transport/connection-options.d.ts +18 -0
  152. package/dist/transport/relay-transport.d.ts +17 -0
  153. package/dist/transport/transport.d.ts +7 -0
  154. package/dist/ui/insets.d.ts +8 -0
  155. package/dist/ui/strings.d.ts +33 -0
  156. package/dist/ui/useInsets.d.ts +2 -0
  157. package/dist/version.d.ts +1 -0
  158. package/package.json +84 -3
  159. package/src/BlarProvider.tsx +462 -0
  160. package/src/actions/batched.ts +16 -0
  161. package/src/actions/click.ts +54 -0
  162. package/src/actions/commit-watch.ts +86 -0
  163. package/src/actions/events.ts +58 -0
  164. package/src/actions/executor.ts +147 -0
  165. package/src/actions/highlight.ts +13 -0
  166. package/src/actions/interaction-guards.ts +44 -0
  167. package/src/actions/landing.ts +26 -0
  168. package/src/actions/navigation-driver.ts +131 -0
  169. package/src/actions/parse.ts +82 -0
  170. package/src/actions/result.ts +14 -0
  171. package/src/actions/scrolling.ts +112 -0
  172. package/src/actions/services.ts +24 -0
  173. package/src/actions/settle.ts +40 -0
  174. package/src/actions/switch.ts +31 -0
  175. package/src/actions/target.ts +77 -0
  176. package/src/actions/text-input.ts +82 -0
  177. package/src/actions/types.ts +52 -0
  178. package/src/agent/batch-commands.ts +33 -0
  179. package/src/agent/compose.ts +67 -0
  180. package/src/agent/controller.ts +106 -0
  181. package/src/agent/frame-runner.ts +27 -0
  182. package/src/agent/frames.ts +193 -0
  183. package/src/agent/handlers/agent-command.ts +74 -0
  184. package/src/agent/handlers/batch.ts +72 -0
  185. package/src/agent/handlers/consent-screenshot.ts +44 -0
  186. package/src/agent/handlers/context.ts +17 -0
  187. package/src/agent/handlers/dom-state.ts +19 -0
  188. package/src/agent/handlers/element-context.ts +9 -0
  189. package/src/agent/handlers/screenshot.ts +24 -0
  190. package/src/agent/handlers/text-dom.ts +17 -0
  191. package/src/agent/stage.ts +22 -0
  192. package/src/agent-mode/action-description.ts +25 -0
  193. package/src/agent-mode/bubbles.ts +31 -0
  194. package/src/agent-mode/colors.ts +40 -0
  195. package/src/agent-mode/controller.ts +459 -0
  196. package/src/agent-mode/cursor-follow.ts +43 -0
  197. package/src/agent-mode/cursor.ts +118 -0
  198. package/src/agent-mode/frames.ts +70 -0
  199. package/src/agent-mode/haptics.ts +49 -0
  200. package/src/agent-mode/keep-awake.ts +107 -0
  201. package/src/agent-mode/layer-stacking.ts +16 -0
  202. package/src/agent-mode/markdown.ts +84 -0
  203. package/src/agent-mode/panel-geometry.ts +27 -0
  204. package/src/agent-mode/resume.ts +16 -0
  205. package/src/agent-mode/run-memory.ts +65 -0
  206. package/src/agent-mode/spotlight-controller.ts +255 -0
  207. package/src/agent-mode/spotlight.ts +14 -0
  208. package/src/agent-mode/state.ts +36 -0
  209. package/src/agent-mode/status.ts +13 -0
  210. package/src/agent-mode/timers.ts +11 -0
  211. package/src/agent-mode/ui/AgentDock.tsx +285 -0
  212. package/src/agent-mode/ui/AgentLayer.tsx +123 -0
  213. package/src/agent-mode/ui/AgentLayerHost.tsx +37 -0
  214. package/src/agent-mode/ui/AgentPanel.tsx +187 -0
  215. package/src/agent-mode/ui/AgentTap.tsx +190 -0
  216. package/src/agent-mode/ui/MarkdownText.tsx +55 -0
  217. package/src/agent-mode/ui/MessageBubbles.tsx +58 -0
  218. package/src/agent-mode/ui/PendingOverlay.tsx +32 -0
  219. package/src/agent-mode/ui/SpotlightLayer.tsx +60 -0
  220. package/src/agent-mode/ui/SpotlightOverlay.tsx +35 -0
  221. package/src/agent-mode/ui/StatusCard.tsx +137 -0
  222. package/src/agent-mode/ui/TargetPulse.tsx +74 -0
  223. package/src/agent-mode/ui/TouchShield.tsx +22 -0
  224. package/src/agent-mode/ui/WarningIcon.tsx +32 -0
  225. package/src/agent-mode/ui/full-window-overlay.ts +16 -0
  226. package/src/agent-mode/ui/layer-host.ts +11 -0
  227. package/src/backend/api-client.ts +210 -0
  228. package/src/backend/api-types.ts +56 -0
  229. package/src/backend/app-info.ts +40 -0
  230. package/src/backend/backend-transport.ts +338 -0
  231. package/src/backend/backoff.ts +16 -0
  232. package/src/backend/base64.ts +25 -0
  233. package/src/backend/errors.ts +32 -0
  234. package/src/backend/foreground.ts +10 -0
  235. package/src/backend/health.ts +64 -0
  236. package/src/backend/identity-proof.ts +151 -0
  237. package/src/backend/identity.ts +31 -0
  238. package/src/backend/message-metadata.ts +28 -0
  239. package/src/backend/presence.ts +95 -0
  240. package/src/backend/response-cache.ts +45 -0
  241. package/src/backend/session-change.ts +14 -0
  242. package/src/backend/session-socket.ts +292 -0
  243. package/src/backend/session.ts +188 -0
  244. package/src/backend/socket-url.ts +25 -0
  245. package/src/backend/storage.ts +49 -0
  246. package/src/backend/tab-id.ts +24 -0
  247. package/src/blar.ts +100 -0
  248. package/src/chat/ChatLayer.tsx +109 -0
  249. package/src/chat/ChatSheet.tsx +82 -0
  250. package/src/chat/ChatView.tsx +127 -0
  251. package/src/chat/Launcher.tsx +72 -0
  252. package/src/chat/bridge.ts +56 -0
  253. package/src/chat/chat-coordinator.ts +189 -0
  254. package/src/chat/chat-state.ts +35 -0
  255. package/src/chat/navigation.ts +15 -0
  256. package/src/chat/options.ts +39 -0
  257. package/src/chat/webview.ts +23 -0
  258. package/src/chat/widget-bundle.generated.ts +2 -0
  259. package/src/chat/widget-shell.ts +170 -0
  260. package/src/confirmation/port.ts +15 -0
  261. package/src/fiber/locate.ts +41 -0
  262. package/src/fiber/narrow.ts +15 -0
  263. package/src/fiber/reader.ts +98 -0
  264. package/src/fiber/text.ts +52 -0
  265. package/src/fiber/types.ts +24 -0
  266. package/src/host/border-radius.ts +11 -0
  267. package/src/host/instance.ts +30 -0
  268. package/src/host/measure.ts +25 -0
  269. package/src/host/rect.ts +45 -0
  270. package/src/host/visible-area.ts +5 -0
  271. package/src/index.ts +10 -0
  272. package/src/nudge/NudgeBubble.tsx +117 -0
  273. package/src/nudge/nudge.ts +100 -0
  274. package/src/optional/expo-haptics.ts +11 -0
  275. package/src/optional/expo-keep-awake.ts +11 -0
  276. package/src/optional/react-native-haptic-feedback.ts +11 -0
  277. package/src/optional/react-native-keep-awake.ts +11 -0
  278. package/src/overlay/AgentOverlay.tsx +16 -0
  279. package/src/screenshot/capture-backend.ts +7 -0
  280. package/src/screenshot/screenshotter.ts +39 -0
  281. package/src/screenshot/skia.ts +107 -0
  282. package/src/screenshot/view-shot.ts +41 -0
  283. package/src/snapshot/capture.ts +163 -0
  284. package/src/snapshot/classify.ts +40 -0
  285. package/src/snapshot/collect.ts +136 -0
  286. package/src/snapshot/element-context.ts +66 -0
  287. package/src/snapshot/icon.ts +21 -0
  288. package/src/snapshot/kinds.ts +10 -0
  289. package/src/snapshot/modals.ts +21 -0
  290. package/src/snapshot/navigation.ts +47 -0
  291. package/src/snapshot/ordering.ts +26 -0
  292. package/src/snapshot/properties.ts +71 -0
  293. package/src/snapshot/registry.ts +33 -0
  294. package/src/snapshot/render.ts +140 -0
  295. package/src/snapshot/route-summary.ts +15 -0
  296. package/src/snapshot/scroll.ts +90 -0
  297. package/src/snapshot/text-layout.ts +59 -0
  298. package/src/snapshot/view.ts +87 -0
  299. package/src/snapshot/visibility.ts +63 -0
  300. package/src/snapshot/wrappers.ts +69 -0
  301. package/src/telemetry/SdkErrorBoundary.tsx +28 -0
  302. package/src/telemetry/build-info.generated.ts +2 -0
  303. package/src/telemetry/envelope.ts +83 -0
  304. package/src/telemetry/reporter.ts +170 -0
  305. package/src/telemetry/scrub.ts +10 -0
  306. package/src/telemetry/sdk-reporter.ts +37 -0
  307. package/src/transport/connection-options.ts +65 -0
  308. package/src/transport/relay-transport.ts +82 -0
  309. package/src/transport/transport.ts +8 -0
  310. package/src/ui/insets.ts +31 -0
  311. package/src/ui/strings.ts +102 -0
  312. package/src/ui/useInsets.ts +10 -0
  313. package/src/version.ts +1 -0
@@ -0,0 +1,112 @@
1
+ import { measureHost } from '../host/measure.ts';
2
+ import { type Band, scrollDeltaIntoBand } from '../host/rect.ts';
3
+ import { scrollInstanceOf } from '../snapshot/classify.ts';
4
+ import { maxScrollOffsetOf, measureScrollGeometry, scrollOffsetOf, type ScrollGeometry, viewportExtentOf } from '../snapshot/scroll.ts';
5
+ import { failed, failedFromException, succeeded } from './result.ts';
6
+ import type { ResolvedTarget } from './target.ts';
7
+ import type { AgentAction, HandlerResult } from './types.ts';
8
+
9
+ type ScrollAction = Extract<AgentAction, { type: 'scroll' }>;
10
+
11
+ const PAGE_FRACTION = 0.8;
12
+ const VISIBLE_MARGIN = 24;
13
+ const MOVEMENT_TOLERANCE = 1;
14
+
15
+ function clamp(value: number, minimum: number, maximum: number): number {
16
+ return Math.min(Math.max(value, minimum), maximum);
17
+ }
18
+
19
+ function applyOffset(target: ResolvedTarget, geometry: ScrollGeometry, offset: number, animated: boolean): HandlerResult {
20
+ const instance = scrollInstanceOf(target.fiber);
21
+ if (instance === null) {
22
+ return failed('element_not_interactable', 'the scroll container has no scrollTo');
23
+ }
24
+ try {
25
+ instance.scrollTo(geometry.horizontal ? { x: offset, animated } : { y: offset, animated });
26
+ return succeeded();
27
+ } catch (error) {
28
+ return failedFromException(error);
29
+ }
30
+ }
31
+
32
+ export async function scrollContainer(target: ResolvedTarget, action: ScrollAction): Promise<HandlerResult> {
33
+ if (target.element.kind !== 'scroll') {
34
+ return failed('element_not_interactable', 'scroll needs a scroll container (scr-N)');
35
+ }
36
+ const geometry = await measureScrollGeometry(target.fiber);
37
+ if (geometry === null) {
38
+ return failed('element_not_found', 'the scroll container could not be measured');
39
+ }
40
+ const isHorizontalAction = action.direction === 'left' || action.direction === 'right';
41
+ if (isHorizontalAction !== geometry.horizontal) {
42
+ return failed('element_not_interactable', `the container scrolls ${geometry.horizontal ? 'horizontally' : 'vertically'}`);
43
+ }
44
+ const forward = action.direction === 'down' || action.direction === 'right';
45
+ const distance = action.amount ?? Math.round(viewportExtentOf(geometry) * PAGE_FRACTION);
46
+ const current = scrollOffsetOf(geometry);
47
+ const next = clamp(current + (forward ? distance : -distance), 0, maxScrollOffsetOf(geometry));
48
+ if (Math.abs(next - current) < MOVEMENT_TOLERANCE) {
49
+ return succeeded('already at the edge of the container');
50
+ }
51
+ return applyOffset(target, geometry, next, true);
52
+ }
53
+
54
+ function nearestScrollAncestor(target: ResolvedTarget): ResolvedTarget['fiber'] | null {
55
+ for (let index = target.ancestors.length - 1; index >= 0; index -= 1) {
56
+ const ancestor = target.ancestors[index];
57
+ if (ancestor !== undefined && scrollInstanceOf(ancestor) !== null) {
58
+ return ancestor;
59
+ }
60
+ }
61
+ return null;
62
+ }
63
+
64
+ function offsetDeltaToReveal(start: number, end: number, viewportStart: number, viewportEnd: number): number {
65
+ const startShortfall = start - (viewportStart + VISIBLE_MARGIN);
66
+ if (startShortfall < 0) {
67
+ return startShortfall;
68
+ }
69
+ const endOverflow = end - (viewportEnd - VISIBLE_MARGIN);
70
+ return endOverflow > 0 ? Math.min(endOverflow, startShortfall) : 0;
71
+ }
72
+
73
+ export async function scrollIntoView(target: ResolvedTarget, options: { animated: boolean } = { animated: true }): Promise<HandlerResult> {
74
+ const container = nearestScrollAncestor(target);
75
+ if (container === null || target.host === null) {
76
+ return succeeded('the element is not inside a scroll container');
77
+ }
78
+ const [geometry, rect] = await Promise.all([measureScrollGeometry(container), measureHost(target.host)]);
79
+ if (geometry === null || rect === null) {
80
+ return failed('element_not_found', 'the element or its scroll container could not be measured');
81
+ }
82
+ const { viewport, horizontal } = geometry;
83
+ const delta = horizontal
84
+ ? offsetDeltaToReveal(rect.x, rect.x + rect.width, viewport.x, viewport.x + viewport.width)
85
+ : offsetDeltaToReveal(rect.y, rect.y + rect.height, viewport.y, viewport.y + viewport.height);
86
+ if (Math.abs(delta) < MOVEMENT_TOLERANCE) {
87
+ return succeeded('the element is already in view');
88
+ }
89
+ const next = clamp(scrollOffsetOf(geometry) + delta, 0, maxScrollOffsetOf(geometry));
90
+ return applyOffset({ ...target, fiber: container }, geometry, next, options.animated);
91
+ }
92
+
93
+ export async function scrollIntoBand(target: ResolvedTarget, band: Band): Promise<HandlerResult> {
94
+ const container = nearestScrollAncestor(target);
95
+ if (container === null || target.host === null) {
96
+ return succeeded('the element is not inside a scroll container');
97
+ }
98
+ const [geometry, rect] = await Promise.all([measureScrollGeometry(container), measureHost(target.host)]);
99
+ if (geometry === null || rect === null) {
100
+ return failed('element_not_found', 'the element or its scroll container could not be measured');
101
+ }
102
+ if (geometry.horizontal) {
103
+ return succeeded('the container scrolls sideways');
104
+ }
105
+ const { viewport } = geometry;
106
+ const keepsInside = clamp(scrollDeltaIntoBand(rect, band), rect.y + rect.height - (viewport.y + viewport.height), rect.y - viewport.y);
107
+ const next = clamp(scrollOffsetOf(geometry) + keepsInside, 0, maxScrollOffsetOf(geometry));
108
+ if (Math.abs(next - scrollOffsetOf(geometry)) < MOVEMENT_TOLERANCE) {
109
+ return succeeded('the element is already in place');
110
+ }
111
+ return applyOffset({ ...target, fiber: container }, geometry, next, true);
112
+ }
@@ -0,0 +1,24 @@
1
+ import type { RegisteredElement } from '../snapshot/registry.ts';
2
+ import type { CursorPort } from './landing.ts';
3
+ import { NavigationDriver } from './navigation-driver.ts';
4
+ import { TargetResolver, type TargetView } from './target.ts';
5
+
6
+ export interface ReadingRequest extends TargetView {
7
+ text: string | null;
8
+ }
9
+
10
+ export interface ReadingPort {
11
+ read: (request: ReadingRequest) => Promise<boolean>;
12
+ }
13
+
14
+ export interface InputMemory {
15
+ last: RegisteredElement | null;
16
+ }
17
+
18
+ export interface ActionServices {
19
+ resolver: TargetResolver;
20
+ navigation: NavigationDriver;
21
+ reading: ReadingPort;
22
+ cursor: CursorPort;
23
+ inputMemory: InputMemory;
24
+ }
@@ -0,0 +1,40 @@
1
+ export interface SettleOptions {
2
+ minWaitMs: number;
3
+ quietMs: number;
4
+ capMs: number;
5
+ pollMs: number;
6
+ }
7
+
8
+ export interface Clock {
9
+ now: () => number;
10
+ sleep: (milliseconds: number) => Promise<void>;
11
+ }
12
+
13
+ export const DEFAULT_SETTLE: SettleOptions = { minWaitMs: 300, quietMs: 150, capMs: 2000, pollMs: 75 };
14
+
15
+ export const SYSTEM_CLOCK: Clock = {
16
+ now: () => Date.now(),
17
+ sleep: (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
18
+ };
19
+
20
+ export async function waitForSettle(
21
+ sample: () => Promise<string>,
22
+ options: SettleOptions = DEFAULT_SETTLE,
23
+ clock: Clock = SYSTEM_CLOCK,
24
+ ): Promise<string> {
25
+ const startedAt = clock.now();
26
+ await clock.sleep(options.minWaitMs);
27
+ let latest = await sample();
28
+ let stableSince = clock.now();
29
+ while (clock.now() - startedAt < options.capMs) {
30
+ await clock.sleep(options.pollMs);
31
+ const current = await sample();
32
+ if (current !== latest) {
33
+ latest = current;
34
+ stableSince = clock.now();
35
+ } else if (clock.now() - stableSince >= options.quietMs) {
36
+ break;
37
+ }
38
+ }
39
+ return latest;
40
+ }
@@ -0,0 +1,31 @@
1
+ import { isFunction } from '../fiber/narrow.ts';
2
+ import { runBatched } from './batched.ts';
3
+ import { checkInteractable } from './interaction-guards.ts';
4
+ import { failed, failedFromException, succeeded } from './result.ts';
5
+ import type { ResolvedTarget } from './target.ts';
6
+ import type { HandlerResult } from './types.ts';
7
+
8
+ export function toggleSwitch(target: ResolvedTarget, desired: boolean | null): HandlerResult {
9
+ if (target.element.kind !== 'switch') {
10
+ return failed('element_not_interactable', 'check needs a switch or checkbox');
11
+ }
12
+ const verdict = checkInteractable(target.fiber, target.ancestors);
13
+ if (!verdict.allowed) {
14
+ return failed('element_not_interactable', verdict.reason);
15
+ }
16
+ const onValueChange = target.props.onValueChange;
17
+ if (!isFunction(onValueChange)) {
18
+ return failed('element_not_interactable', 'the switch no longer has an onValueChange handler');
19
+ }
20
+ const current = target.props.value === true;
21
+ const next = desired ?? !current;
22
+ if (next === current) {
23
+ return succeeded(`already ${current ? 'on' : 'off'}`);
24
+ }
25
+ try {
26
+ runBatched(() => onValueChange(next));
27
+ return succeeded();
28
+ } catch (error) {
29
+ return failedFromException(error);
30
+ }
31
+ }
@@ -0,0 +1,77 @@
1
+ import { currentRootFiber, locateCurrent } from '../fiber/locate.ts';
2
+ import { firstHostDescendant, propsOf } from '../fiber/reader.ts';
3
+ import { collectText } from '../fiber/text.ts';
4
+ import { explicitLabelOf, placeholderOf } from '../snapshot/properties.ts';
5
+ import { borderRadiusOf } from '../host/border-radius.ts';
6
+ import { measureHost } from '../host/measure.ts';
7
+ import type { Band, Rect } from '../host/rect.ts';
8
+ import type { Fiber, Props } from '../fiber/types.ts';
9
+ import { ElementRegistry, type RegisteredElement } from '../snapshot/registry.ts';
10
+ import { scrollIntoBand } from './scrolling.ts';
11
+
12
+ export interface ResolvedTarget {
13
+ element: RegisteredElement;
14
+ fiber: Fiber;
15
+ ancestors: Fiber[];
16
+ props: Props;
17
+ host: Fiber | null;
18
+ }
19
+
20
+ export class TargetResolver {
21
+ constructor(
22
+ private readonly registry: ElementRegistry,
23
+ private readonly getAnchorFiber: () => Fiber | null,
24
+ ) {}
25
+
26
+ resolveAgentId(agentId: string): ResolvedTarget | null {
27
+ const element = this.registry.find(agentId);
28
+ return element === null ? null : this.resolveElement(element);
29
+ }
30
+
31
+ resolveElement(element: RegisteredElement): ResolvedTarget | null {
32
+ const anchor = this.getAnchorFiber();
33
+ const root = anchor === null ? null : currentRootFiber(anchor);
34
+ if (root === null) {
35
+ return null;
36
+ }
37
+ const located = locateCurrent(root, element.fiber);
38
+ if (located === null) {
39
+ return null;
40
+ }
41
+ return {
42
+ element,
43
+ fiber: located.fiber,
44
+ ancestors: located.ancestors,
45
+ props: propsOf(located.fiber),
46
+ host: firstHostDescendant(located.fiber),
47
+ };
48
+ }
49
+ }
50
+
51
+ export interface TargetView {
52
+ measure: () => Promise<Rect | null>;
53
+ radius: number;
54
+ reveal?: (band: Band) => Promise<void>;
55
+ }
56
+
57
+ export function measureTarget(target: ResolvedTarget): () => Promise<Rect | null> {
58
+ const host = target.host;
59
+ return host === null ? () => Promise.resolve(target.element.rect) : () => measureHost(host);
60
+ }
61
+
62
+ export function viewOf(target: ResolvedTarget): TargetView {
63
+ return {
64
+ measure: measureTarget(target),
65
+ radius: target.host === null ? 0 : borderRadiusOf(propsOf(target.host).style),
66
+ reveal: async (band) => {
67
+ await scrollIntoBand(target, band);
68
+ },
69
+ };
70
+ }
71
+
72
+ const NAME_LIMIT = 80;
73
+
74
+ export function nameOf(target: ResolvedTarget): string | null {
75
+ const name = explicitLabelOf(target.props) ?? (collectText(target.fiber, NAME_LIMIT) || placeholderOf(target.props));
76
+ return name === null || name.length === 0 ? null : name.slice(0, NAME_LIMIT);
77
+ }
@@ -0,0 +1,82 @@
1
+ import { findNodeHandle, Platform, UIManager } from 'react-native';
2
+
3
+ import { isFunction } from '../fiber/narrow.ts';
4
+ import { propsOf } from '../fiber/reader.ts';
5
+ import { hostInstanceOf } from '../host/instance.ts';
6
+ import { runBatched } from './batched.ts';
7
+ import { buildSubmitEvent, buildTextChangeEvent } from './events.ts';
8
+ import { checkEditable } from './interaction-guards.ts';
9
+ import { failed, failedFromException, succeeded } from './result.ts';
10
+ import type { ActionServices } from './services.ts';
11
+ import type { ResolvedTarget } from './target.ts';
12
+ import type { AgentAction, HandlerResult } from './types.ts';
13
+
14
+ type TypeAction = Extract<AgentAction, { type: 'type' }>;
15
+
16
+ function currentTextOf(target: ResolvedTarget): string {
17
+ const { value, defaultValue } = target.props;
18
+ if (typeof value === 'string') {
19
+ return value;
20
+ }
21
+ return typeof defaultValue === 'string' ? defaultValue : '';
22
+ }
23
+
24
+ function isControlled(target: ResolvedTarget): boolean {
25
+ return typeof target.props.value === 'string';
26
+ }
27
+
28
+ export function typeText(target: ResolvedTarget, action: TypeAction, services: ActionServices): HandlerResult {
29
+ if (target.element.kind !== 'text_input') {
30
+ return failed('element_not_interactable', 'type needs a text input');
31
+ }
32
+ const verdict = checkEditable(target.fiber, target.ancestors);
33
+ if (!verdict.allowed) {
34
+ return failed('element_not_interactable', verdict.reason);
35
+ }
36
+ const next = action.clear_first ? action.value : currentTextOf(target) + action.value;
37
+ const instance = target.host === null ? null : hostInstanceOf(target.host);
38
+ try {
39
+ instance?.focus?.();
40
+ if (!isControlled(target)) {
41
+ if (Platform.OS === 'android') {
42
+ const tag = findNodeHandle(instance as Parameters<typeof findNodeHandle>[0]);
43
+ const eventCount = target.host === null ? undefined : propsOf(target.host).mostRecentEventCount;
44
+ if (tag === null || typeof eventCount !== 'number' || !Number.isInteger(eventCount) || eventCount < 0) {
45
+ return failed('element_not_interactable', 'the native text input is unavailable');
46
+ }
47
+ UIManager.dispatchViewManagerCommand(tag, 'setTextAndSelection', [eventCount, next, next.length, next.length]);
48
+ } else {
49
+ instance?.setNativeProps?.({ text: next });
50
+ }
51
+ }
52
+ const { onChange, onChangeText } = target.props;
53
+ runBatched(() => {
54
+ if (isFunction(onChange)) {
55
+ onChange(buildTextChangeEvent(next));
56
+ }
57
+ if (isFunction(onChangeText)) {
58
+ onChangeText(next);
59
+ }
60
+ });
61
+ } catch (error) {
62
+ return failedFromException(error);
63
+ }
64
+ services.inputMemory.last = target.element;
65
+ return succeeded();
66
+ }
67
+
68
+ export function submitInput(target: ResolvedTarget): HandlerResult {
69
+ if (target.element.kind !== 'text_input') {
70
+ return failed('element_not_interactable', 'Enter needs a text input');
71
+ }
72
+ const onSubmitEditing = target.props.onSubmitEditing;
73
+ if (!isFunction(onSubmitEditing)) {
74
+ return failed('element_not_interactable', 'the input has no onSubmitEditing handler');
75
+ }
76
+ try {
77
+ runBatched(() => onSubmitEditing(buildSubmitEvent(currentTextOf(target))));
78
+ return succeeded();
79
+ } catch (error) {
80
+ return failedFromException(error);
81
+ }
82
+ }
@@ -0,0 +1,52 @@
1
+ import type { UnknownRecord } from '../fiber/narrow.ts';
2
+
3
+ export type ScrollDirectionName = 'up' | 'down' | 'left' | 'right';
4
+
5
+ export type AgentAction =
6
+ | { type: 'click' }
7
+ | { type: 'type'; value: string; clear_first: boolean }
8
+ | { type: 'check'; checked: boolean | null }
9
+ | { type: 'press_key'; key: string }
10
+ | { type: 'scroll'; direction: ScrollDirectionName; amount: number | null }
11
+ | { type: 'scroll_to' }
12
+ | { type: 'navigate'; route: string; params: UnknownRecord | null }
13
+ | { type: 'highlight' };
14
+
15
+ export type ActionErrorCode =
16
+ | 'element_not_found'
17
+ | 'element_not_interactable'
18
+ | 'unsupported_action'
19
+ | 'execution_error'
20
+ | 'user_cancelled'
21
+ | 'timeout';
22
+
23
+ export type RiskLevel = 'safe' | 'reversible' | 'irreversible' | 'destructive';
24
+
25
+ export type FailedResult = { ok: false; error: ActionErrorCode; message: string };
26
+
27
+ export type HandlerResult = { ok: true; message?: string } | FailedResult;
28
+
29
+ export interface ActionOutcome {
30
+ success: boolean;
31
+ no_op: boolean;
32
+ error?: ActionErrorCode;
33
+ message?: string;
34
+ }
35
+
36
+ export interface PageContentRequest {
37
+ viewportOnly: boolean;
38
+ maxChars: number;
39
+ }
40
+
41
+ export interface AgentCommandData {
42
+ command_id: string;
43
+ action: AgentAction;
44
+ agent_id: string | null;
45
+ risk_level: RiskLevel;
46
+ confirmation_required: boolean;
47
+ teaching_explanation: string | null;
48
+ include_page_content: boolean;
49
+ page_content_options: PageContentRequest;
50
+ dom_settle_ms: number;
51
+ }
52
+
@@ -0,0 +1,33 @@
1
+ import { parseAgentCommand, parsePageContentRequest } from '../actions/parse.ts';
2
+ import type { AgentCommandData, PageContentRequest } from '../actions/types.ts';
3
+ import { isRecord, nonEmptyString } from '../fiber/narrow.ts';
4
+
5
+ export type BatchEntry =
6
+ | { kind: 'action'; command: AgentCommandData }
7
+ | { kind: 'dom_state'; commandId: string }
8
+ | { kind: 'text_dom'; commandId: string; options: PageContentRequest }
9
+ | { kind: 'invalid'; commandId: string; reason: string };
10
+
11
+ function parseEntry(raw: unknown): BatchEntry {
12
+ if (!isRecord(raw)) {
13
+ return { kind: 'invalid', commandId: '', reason: 'malformed batch entry' };
14
+ }
15
+ const commandId = nonEmptyString(raw.command_id) ?? '';
16
+ switch (raw.type) {
17
+ case 'request_dom_state':
18
+ return { kind: 'dom_state', commandId };
19
+ case 'request_text_dom':
20
+ return { kind: 'text_dom', commandId, options: parsePageContentRequest(raw.options) };
21
+ case 'agent_command': {
22
+ const command = parseAgentCommand(raw);
23
+ return command === null ? { kind: 'invalid', commandId, reason: 'unsupported or malformed action' } : { kind: 'action', command };
24
+ }
25
+ default:
26
+ return { kind: 'invalid', commandId, reason: `unknown batch entry "${String(raw.type)}"` };
27
+ }
28
+ }
29
+
30
+ export function parseBatchEntries(data: unknown): BatchEntry[] {
31
+ const commands = isRecord(data) && Array.isArray(data.commands) ? data.commands : [];
32
+ return commands.map(parseEntry);
33
+ }
@@ -0,0 +1,67 @@
1
+ import { ActionExecutor } from '../actions/executor.ts';
2
+ import { CommitWatch } from '../actions/commit-watch.ts';
3
+ import { NavigationDriver, type NavigationRefLike } from '../actions/navigation-driver.ts';
4
+ import type { CursorPort } from '../actions/landing.ts';
5
+ import type { ActionServices, ReadingPort } from '../actions/services.ts';
6
+ import { TargetResolver } from '../actions/target.ts';
7
+ import type { Fiber } from '../fiber/types.ts';
8
+ import type { Size } from '../host/rect.ts';
9
+ import type { ConfirmationPort } from '../confirmation/port.ts';
10
+ import type { AgentStage } from './stage.ts';
11
+ import { Screenshotter } from '../screenshot/screenshotter.ts';
12
+ import { SnapshotCapturer } from '../snapshot/capture.ts';
13
+ import type { ScreenRef } from '../snapshot/navigation.ts';
14
+ import { ElementContextReader } from '../snapshot/element-context.ts';
15
+ import { ElementRegistry } from '../snapshot/registry.ts';
16
+ import type { AgentTransport } from '../transport/transport.ts';
17
+ import { AgentController } from './controller.ts';
18
+
19
+ export interface AgentEnvironment {
20
+ transport: AgentTransport;
21
+ getAnchorFiber: () => Fiber | null;
22
+ getViewport: () => Size;
23
+ getNavigationRef: () => NavigationRefLike | null;
24
+ getCaptureTarget: () => unknown;
25
+ reading: ReadingPort;
26
+ cursor: CursorPort;
27
+ confirmation: ConfirmationPort;
28
+ stage: AgentStage;
29
+ onCommit?: () => void;
30
+ onSettled?: () => void;
31
+ }
32
+
33
+ export function createAgentController(environment: AgentEnvironment): AgentController {
34
+ const registry = new ElementRegistry();
35
+ const navigation: NavigationDriver = new NavigationDriver({
36
+ readFocusedScreens: (): ScreenRef[] => capturer.readFocusedScreens(),
37
+ getNavigationRef: environment.getNavigationRef,
38
+ });
39
+ const capturer: SnapshotCapturer = new SnapshotCapturer({
40
+ getAnchorFiber: environment.getAnchorFiber,
41
+ registry,
42
+ getViewport: environment.getViewport,
43
+ getExtraRoutes: (): string[] => navigation.listRefRoutePaths(),
44
+ });
45
+ const commitWatch = new CommitWatch(environment.getAnchorFiber, environment.onCommit);
46
+ const services: ActionServices = {
47
+ resolver: new TargetResolver(registry, environment.getAnchorFiber),
48
+ navigation,
49
+ reading: environment.reading,
50
+ cursor: environment.cursor,
51
+ inputMemory: { last: null },
52
+ };
53
+ return new AgentController({
54
+ transport: environment.transport,
55
+ commitWatch,
56
+ getViewport: environment.getViewport,
57
+ onSettled: environment.onSettled,
58
+ context: {
59
+ capturer,
60
+ executor: new ActionExecutor(capturer, services, commitWatch),
61
+ screenshotter: new Screenshotter(environment.getCaptureTarget),
62
+ elementContext: new ElementContextReader(services.resolver, environment.getViewport),
63
+ confirmation: environment.confirmation,
64
+ stage: environment.stage,
65
+ },
66
+ });
67
+ }
@@ -0,0 +1,106 @@
1
+ import type { CommitWatch } from '../actions/commit-watch.ts';
2
+ import { readAppInfo } from '../backend/app-info.ts';
3
+ import { buildMessageMetadata, type MessageMetadata } from '../backend/message-metadata.ts';
4
+ import type { Size } from '../host/rect.ts';
5
+ import { reportSdkError } from '../telemetry/sdk-reporter.ts';
6
+ import type { AgentTransport } from '../transport/transport.ts';
7
+ import { runFrame } from './frame-runner.ts';
8
+ import { type OutboundFrame, parseInboundFrame, type RoutedFrame } from './frames.ts';
9
+ import { handleAgentCommand } from './handlers/agent-command.ts';
10
+ import { handleBatch } from './handlers/batch.ts';
11
+ import type { HandlerContext } from './handlers/context.ts';
12
+ import { handleDomState } from './handlers/dom-state.ts';
13
+ import { handleElementContext } from './handlers/element-context.ts';
14
+ import { handleScreenshot } from './handlers/screenshot.ts';
15
+ import { handleTextDom, readTextDom } from './handlers/text-dom.ts';
16
+
17
+ export interface ControllerDependencies {
18
+ transport: AgentTransport;
19
+ commitWatch: CommitWatch;
20
+ getViewport: () => Size;
21
+ context: Omit<HandlerContext, 'send'>;
22
+ onSettled?: () => void;
23
+ }
24
+
25
+
26
+ export class AgentController {
27
+ private queue: Promise<void> = Promise.resolve();
28
+
29
+ private readonly context: HandlerContext;
30
+
31
+ constructor(private readonly dependencies: ControllerDependencies) {
32
+ this.context = { ...dependencies.context, send: (frame) => this.send(frame) };
33
+ }
34
+
35
+ start(): void {
36
+ this.dependencies.transport.onMessage((raw) => this.receive(raw));
37
+ this.dependencies.transport.start();
38
+ }
39
+
40
+ stop(): void {
41
+ this.dependencies.transport.stop();
42
+ this.dependencies.commitWatch.release();
43
+ }
44
+
45
+ async readMessageMetadata(options: { includeTextDom: boolean; tabId: string }): Promise<MessageMetadata> {
46
+ const snapshot = await this.context.capturer.capture('peek');
47
+ const textDom = options.includeTextDom ? await readTextDom(this.context, { viewportOnly: false, maxChars: 0 }) : null;
48
+ return buildMessageMetadata({
49
+ screen: snapshot.screen,
50
+ tabId: options.tabId,
51
+ viewport: this.dependencies.getViewport(),
52
+ appInfo: readAppInfo(),
53
+ textDom,
54
+ });
55
+ }
56
+
57
+ private receive(raw: string): void {
58
+ const frame = parseInboundFrame(raw);
59
+ if (frame === null || frame.kind === 'unknown') {
60
+ return;
61
+ }
62
+ if (frame.kind === 'ping') {
63
+ this.send({ type: 'pong' });
64
+ return;
65
+ }
66
+ this.send({ type: 'command_ack', command_id: frame.commandId });
67
+ this.queue = this.queue
68
+ .then(() => runFrame(frame, { stage: this.context.stage, handle: (next) => this.handle(next), onSettled: this.dependencies.onSettled }))
69
+ .catch((error: unknown) => reportSdkError('AgentController.receive', error));
70
+ }
71
+
72
+ private async handle(frame: RoutedFrame): Promise<void> {
73
+ try {
74
+ await this.route(frame);
75
+ } catch (error) {
76
+ reportSdkError('AgentController.handle', error);
77
+ this.send({
78
+ type: 'sdk_error',
79
+ command_id: frame.commandId,
80
+ error: error instanceof Error ? error.message : String(error),
81
+ code: 'execution_error',
82
+ });
83
+ }
84
+ }
85
+
86
+ private route(frame: RoutedFrame): Promise<void> {
87
+ switch (frame.kind) {
88
+ case 'agent_command':
89
+ return handleAgentCommand(this.context, frame.commandId, frame.data);
90
+ case 'agent_command_batch':
91
+ return handleBatch(this.context, frame.commandId, frame.data);
92
+ case 'request_dom_state':
93
+ return handleDomState(this.context, frame.commandId);
94
+ case 'request_text_dom':
95
+ return handleTextDom(this.context, frame.commandId, frame.options);
96
+ case 'request_screenshot':
97
+ return handleScreenshot(this.context, frame.commandId, frame.options);
98
+ case 'request_element_context':
99
+ return handleElementContext(this.context, frame.commandId, frame.agentIds);
100
+ }
101
+ }
102
+
103
+ private send(frame: OutboundFrame): void {
104
+ this.dependencies.transport.send(frame);
105
+ }
106
+ }
@@ -0,0 +1,27 @@
1
+ import { parseAgentCommand } from '../actions/parse.ts';
2
+ import type { RoutedFrame } from './frames.ts';
3
+ import type { Admission, AgentStage } from './stage.ts';
4
+
5
+ export function admissionFor(frame: RoutedFrame): Admission | null {
6
+ if (frame.kind === 'agent_command') {
7
+ return { kind: 'command', keepText: parseAgentCommand(frame.data)?.teaching_explanation ?? null };
8
+ }
9
+ return frame.kind === 'agent_command_batch' ? { kind: 'batch', keepText: null } : null;
10
+ }
11
+
12
+ export interface FrameRunnerOptions {
13
+ stage: AgentStage;
14
+ handle: (frame: RoutedFrame) => Promise<void>;
15
+ onSettled?: () => void;
16
+ }
17
+
18
+ export async function runFrame(frame: RoutedFrame, options: FrameRunnerOptions): Promise<void> {
19
+ options.stage.received();
20
+ const admission = admissionFor(frame);
21
+ if (admission !== null && !(await options.stage.admit(admission))) {
22
+ return;
23
+ }
24
+ await options.handle(frame);
25
+ options.stage.settled();
26
+ options.onSettled?.();
27
+ }