local-operator-ui 0.32.12 → 0.33.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 (84) hide show
  1. package/out/main/index.js +863 -41
  2. package/out/renderer/assets/{_basePickBy-7JY2-dci.js → _basePickBy-D28vC22o.js} +1 -1
  3. package/out/renderer/assets/{_baseUniq-Cv-zhYu-.js → _baseUniq-o_JUYW6i.js} +1 -1
  4. package/out/renderer/assets/{agent-details-page-CfV-mkCC.js → agent-details-page-ChzjnPRU.js} +1 -1
  5. package/out/renderer/assets/{agent-hub-page-Ia-raLux.js → agent-hub-page-D4sGuq6W.js} +1 -1
  6. package/out/renderer/assets/{agents-page-D_di29Ib.js → agents-page-DLbb0HLY.js} +3 -3
  7. package/out/renderer/assets/{arc-mehlPZo4.js → arc-C7H_y-lB.js} +1 -1
  8. package/out/renderer/assets/{architectureDiagram-IEHRJDOE-C3QXzi20.js → architectureDiagram-IEHRJDOE-ByV1G-th.js} +1 -1
  9. package/out/renderer/assets/{blockDiagram-JOT3LUYC-bIxOuelr.js → blockDiagram-JOT3LUYC-CTmNa-ju.js} +1 -1
  10. package/out/renderer/assets/{book-open-CTP21S75.js → book-open-DYd3X6Ks.js} +1 -1
  11. package/out/renderer/assets/browser-page-DWoUyFr9.js +1 -0
  12. package/out/renderer/assets/{browser-webauthn-prompt-DbCbUVyd.js → browser-webauthn-prompt-pJFYpzxg.js} +1 -1
  13. package/out/renderer/assets/{c4Diagram-VJAJSXHY-DnNDU7mE.js → c4Diagram-VJAJSXHY-BdU1Z3Zd.js} +1 -1
  14. package/out/renderer/assets/channel-C6MsVi8e.js +1 -0
  15. package/out/renderer/assets/{chunk-4BMEZGHF-BFZfF77h.js → chunk-4BMEZGHF-C1KSNvam.js} +1 -1
  16. package/out/renderer/assets/{chunk-A2AXSNBT-W6q4u1lS.js → chunk-A2AXSNBT-BeyZ24d5.js} +1 -1
  17. package/out/renderer/assets/{chunk-AEK57VVT-DIGgPf6O.js → chunk-AEK57VVT-BNvqJbtU.js} +1 -1
  18. package/out/renderer/assets/{chunk-D6G4REZN-GcOHsSY0.js → chunk-D6G4REZN-C-nv9uoV.js} +1 -1
  19. package/out/renderer/assets/{chunk-RZ5BOZE2-zy5B_FO_.js → chunk-RZ5BOZE2-CjHsbcIj.js} +1 -1
  20. package/out/renderer/assets/{chunk-XZIHB7SX-DjjVXveD.js → chunk-XZIHB7SX-BcA9mNMe.js} +1 -1
  21. package/out/renderer/assets/classDiagram-GIVACNV2-BJPo82xK.js +1 -0
  22. package/out/renderer/assets/classDiagram-v2-COTLJTTW-BJPo82xK.js +1 -0
  23. package/out/renderer/assets/clone-p5PCMoIy.js +1 -0
  24. package/out/renderer/assets/{console-mirror-zvZtREj7.js → console-mirror-BsrFX_mx.js} +1 -1
  25. package/out/renderer/assets/{consoleCapture-rT_Z-gkA.js → consoleCapture-BhdNznhO.js} +1 -1
  26. package/out/renderer/assets/{conversation-input-sync-tkg-UMLM.js → conversation-input-sync-CRJ3UpLx.js} +241 -241
  27. package/out/renderer/assets/{dagre-OKDRZEBW-HMEjzbYe.js → dagre-OKDRZEBW-b_ooMjhL.js} +1 -1
  28. package/out/renderer/assets/{diagram-SSKATNLV-BCvRLJnV.js → diagram-SSKATNLV-BQ61bPZw.js} +1 -1
  29. package/out/renderer/assets/{diagram-VNBRO52H-lNYbg8cU.js → diagram-VNBRO52H-BJPjbqDG.js} +1 -1
  30. package/out/renderer/assets/{erDiagram-Q7BY3M3F-BRa2ybF7.js → erDiagram-Q7BY3M3F-DWqkTt0O.js} +1 -1
  31. package/out/renderer/assets/{error-boundary-BD3BOwfn.js → error-boundary-BytCV_16.js} +1 -1
  32. package/out/renderer/assets/{flowDiagram-4HSFHLVR-DuLNLp0l.js → flowDiagram-4HSFHLVR-DYJ_zZRJ.js} +1 -1
  33. package/out/renderer/assets/{ganttDiagram-APWFNJXF-iRL47hZI.js → ganttDiagram-APWFNJXF-BcQIW2kK.js} +1 -1
  34. package/out/renderer/assets/{gitGraphDiagram-7IBYFJ6S-DNjw7t2p.js → gitGraphDiagram-7IBYFJ6S-BozzPR7Y.js} +1 -1
  35. package/out/renderer/assets/{graph-ShouXqtj.js → graph-2oj8R69e.js} +1 -1
  36. package/out/renderer/assets/{index-uiwTD_ic.js → index-BpyyqcP2.js} +198 -198
  37. package/out/renderer/assets/{index-DbJ7zdT7.js → index-DFCe_aj6.js} +1 -1
  38. package/out/renderer/assets/index-DxRO8eiH.css +1 -0
  39. package/out/renderer/assets/{index-Dvt8lY3g.js → index-HFzeSJx1.js} +1 -1
  40. package/out/renderer/assets/{infoDiagram-PH2N3AL5-F7BSfuVf.js → infoDiagram-PH2N3AL5-CmFzwS1O.js} +1 -1
  41. package/out/renderer/assets/{installer-DHyvaAUM.js → installer-DjcPDTZm.js} +1 -1
  42. package/out/renderer/assets/{journeyDiagram-U35MCT3I-CU-YRANp.js → journeyDiagram-U35MCT3I-DVz3LHSl.js} +1 -1
  43. package/out/renderer/assets/{kanban-definition-NDS4AKOZ-DRmsosUr.js → kanban-definition-NDS4AKOZ-D5k-En4m.js} +1 -1
  44. package/out/renderer/assets/{layout-C6BStS_D.js → layout-Hn2ZMp7l.js} +1 -1
  45. package/out/renderer/assets/{legacy-agents-page-B_aoiSaQ.js → legacy-agents-page-CZNoRN6U.js} +1 -1
  46. package/out/renderer/assets/{mermaid.core-Bt1YaOoC.js → mermaid.core-Cp1rt_e9.js} +5 -5
  47. package/out/renderer/assets/{mesh-page-mHXuzwDk.js → mesh-page-CQahM_X_.js} +1 -1
  48. package/out/renderer/assets/{mindmap-definition-ALO5MXBD-DfNYX5CI.js → mindmap-definition-ALO5MXBD-cEPXOHRg.js} +1 -1
  49. package/out/renderer/assets/{mini-zSO_9Fot.js → mini-Dc6PF53Y.js} +1 -1
  50. package/out/renderer/assets/{org-surface-gate-C5LZBFNS.js → org-surface-gate-C1m8ERFA.js} +1 -1
  51. package/out/renderer/assets/{page-header-BUHGwAKD.js → page-header-BK4_E8_S.js} +1 -1
  52. package/out/renderer/assets/{pieDiagram-IB7DONF6-m1DWaK9u.js → pieDiagram-IB7DONF6-CNw8zo8M.js} +1 -1
  53. package/out/renderer/assets/{projects-page-DQP9Q9s4.js → projects-page-B1NTfBlT.js} +1 -1
  54. package/out/renderer/assets/{quadrantDiagram-7GDLP6J5-Dnz_PF1v.js → quadrantDiagram-7GDLP6J5-DUvurVgl.js} +1 -1
  55. package/out/renderer/assets/{radar-MK3ICKWK-C3WVpnm1.js → radar-MK3ICKWK-Bzpnihrv.js} +1 -1
  56. package/out/renderer/assets/{radient-auth-buttons-D6DSx6pD.js → radient-auth-buttons-Dw-mDyUk.js} +1 -1
  57. package/out/renderer/assets/{requirementDiagram-KVF5MWMF-DukYMtDO.js → requirementDiagram-KVF5MWMF-Cz10GD-t.js} +1 -1
  58. package/out/renderer/assets/{sankeyDiagram-QLVOVGJD-D9HNsvQD.js → sankeyDiagram-QLVOVGJD-DhQ1QL8x.js} +1 -1
  59. package/out/renderer/assets/{schedules-page-DXBfewzG.js → schedules-page-pGHMkDcb.js} +1 -1
  60. package/out/renderer/assets/{sequenceDiagram-X6HHIX6F-Cl5J0Bmw.js → sequenceDiagram-X6HHIX6F-Dt97gRJT.js} +1 -1
  61. package/out/renderer/assets/{settings-page-BUJ7AIxs.js → settings-page-DN34EQgz.js} +1 -1
  62. package/out/renderer/assets/{settings-section-C9gHPe8E.js → settings-section-DbH2vofx.js} +1 -1
  63. package/out/renderer/assets/{stateDiagram-DGXRK772-SksA-fBz.js → stateDiagram-DGXRK772-DiHxs26f.js} +1 -1
  64. package/out/renderer/assets/stateDiagram-v2-YXO3MK2T-TW6Z0SoU.js +1 -0
  65. package/out/renderer/assets/{timeline-definition-BDJGKUSR-ivyTu1t2.js → timeline-definition-BDJGKUSR-BeuVfVtI.js} +1 -1
  66. package/out/renderer/assets/ui-preferences-store-33sdgyd1.js +1 -0
  67. package/out/renderer/assets/{use-download-agent-mutation-BhrrtNCc.js → use-download-agent-mutation-Bs5MsvaK.js} +1 -1
  68. package/out/renderer/assets/{use-memberships-query-CIfvCO8h.js → use-memberships-query-DUTZNAaq.js} +1 -1
  69. package/out/renderer/assets/{use-session-snapshot--Gg-KOK1.js → use-session-snapshot-U8KyPEV9.js} +1 -1
  70. package/out/renderer/assets/{xychartDiagram-VJFVF3MP-DzbeHzyh.js → xychartDiagram-VJFVF3MP-C81pe8RF.js} +1 -1
  71. package/out/renderer/console-capture.html +5 -5
  72. package/out/renderer/index.html +7 -7
  73. package/out/renderer/installer.html +4 -4
  74. package/out/renderer/mini.html +7 -7
  75. package/package.json +1 -1
  76. package/out/renderer/assets/browser-page-bOF5M855.js +0 -1
  77. package/out/renderer/assets/channel-GEfpXdbc.js +0 -1
  78. package/out/renderer/assets/classDiagram-GIVACNV2-CmewmNKx.js +0 -1
  79. package/out/renderer/assets/classDiagram-v2-COTLJTTW-CmewmNKx.js +0 -1
  80. package/out/renderer/assets/clone-tqHKmaIi.js +0 -1
  81. package/out/renderer/assets/index-D9v4KyH5.css +0 -1
  82. package/out/renderer/assets/stateDiagram-v2-YXO3MK2T-BVtZR151.js +0 -1
  83. package/out/renderer/assets/ui-preferences-store-BWxVGilA.js +0 -1
  84. /package/out/renderer/assets/{index-Gll2cP3b.js → index-DDd7NnXP.js} +0 -0
package/out/main/index.js CHANGED
@@ -18758,6 +18758,8 @@ function deadline(op, ms, what) {
18758
18758
  }
18759
18759
  const PROTOCOL_VERSION = "1.3";
18760
18760
  const ALREADY_ATTACHED = /another debugger|already attached/i;
18761
+ const STALE_SESSION = /no session|session with given id|no target|target closed/i;
18762
+ const FRAME_DETACHED = "frame_detached";
18761
18763
  class CdpPool {
18762
18764
  constructor(options = {}) {
18763
18765
  this.options = options;
@@ -18765,6 +18767,19 @@ class CdpPool {
18765
18767
  attachments = /* @__PURE__ */ new Map();
18766
18768
  /** Per-view CDP event subscribers, by webContents id. See `subscribe`. */
18767
18769
  subscribers = /* @__PURE__ */ new Map();
18770
+ /**
18771
+ * Child-frame sessions, by webContents id then frameId (an out-of-process
18772
+ * iframe's frameId IS its targetId — measured, ARCH-1). Attached on demand and
18773
+ * cached rather than auto-attached: a standing `Target.setAutoAttach` attaches
18774
+ * to every ad frame on every driven page and, with `waitForDebuggerOnStart`,
18775
+ * can hang a page whose resume is missed. Dropped on `detachedFromTarget`, on
18776
+ * a stale-session error, and with the page's own attachment — a child session
18777
+ * never outlives its parent.
18778
+ */
18779
+ frames = /* @__PURE__ */ new Map();
18780
+ /** Attaches in flight, by `<webContentsId>:<frameId>`, shared by concurrent
18781
+ * callers (see `attachFrame`). */
18782
+ frameAttaches = /* @__PURE__ */ new Map();
18768
18783
  /**
18769
18784
  * Observe this view's CDP events for as long as the returned function has not
18770
18785
  * been called.
@@ -18814,6 +18829,7 @@ class CdpPool {
18814
18829
  const existing = this.attachments.get(contents.id);
18815
18830
  if (existing && existing.detachedReason === null) return;
18816
18831
  if (existing) this.releaseListeners(existing);
18832
+ this.frames.delete(contents.id);
18817
18833
  if (!contents.getURL()) {
18818
18834
  await deadline(
18819
18835
  contents.loadURL("about:blank"),
@@ -18839,7 +18855,14 @@ class CdpPool {
18839
18855
  );
18840
18856
  }
18841
18857
  }
18842
- const messageListener = (_event, method, params) => {
18858
+ const messageListener = (_event, method, params, sessionId2) => {
18859
+ if (sessionId2) return;
18860
+ if (method === "Target.detachedFromTarget") {
18861
+ this.dropFrameSession(
18862
+ contents.id,
18863
+ String(params?.sessionId ?? "")
18864
+ );
18865
+ }
18843
18866
  this.routeEvent(
18844
18867
  contents.id,
18845
18868
  method,
@@ -18848,6 +18871,7 @@ class CdpPool {
18848
18871
  };
18849
18872
  const detachListener = (_event, reason) => {
18850
18873
  attachment.detachedReason = reason || "detached";
18874
+ this.frames.delete(contents.id);
18851
18875
  this.options.log?.(
18852
18876
  `[browser] debugger detached from view ${contents.id}: ${reason}`
18853
18877
  );
@@ -18911,10 +18935,86 @@ class CdpPool {
18911
18935
  contents,
18912
18936
  method,
18913
18937
  params,
18914
- options.deadlineMs ?? CDP_DEADLINE_MS
18938
+ options.deadlineMs ?? CDP_DEADLINE_MS,
18939
+ options.sessionId
18915
18940
  );
18916
18941
  }
18917
- async sendTo(contents, method, params, deadlineMs) {
18942
+ /**
18943
+ * The flattened session for one child frame, attached on first use.
18944
+ *
18945
+ * Attached from the PAGE session even for a grandchild (measured: the top
18946
+ * session reaches any frame target by id). Focus emulation goes to the child
18947
+ * for the reason `attach` gives it to the page: `Input.insertText` on an
18948
+ * unfocused target delivers but never replies. `Fetch` is NEVER enabled on a
18949
+ * child: a pause nobody continues would hang the frame.
18950
+ */
18951
+ async attachFrame(contents, frameId) {
18952
+ const cached2 = this.frames.get(contents.id)?.get(frameId);
18953
+ if (cached2) return cached2;
18954
+ const key = `${contents.id}:${frameId}`;
18955
+ const pending = this.frameAttaches.get(key);
18956
+ if (pending) return pending;
18957
+ const attaching = this.openFrameSession(contents, frameId).finally(() => {
18958
+ this.frameAttaches.delete(key);
18959
+ });
18960
+ this.frameAttaches.set(key, attaching);
18961
+ return attaching;
18962
+ }
18963
+ async openFrameSession(contents, frameId) {
18964
+ let attached;
18965
+ try {
18966
+ attached = await this.send(
18967
+ contents,
18968
+ "Target.attachToTarget",
18969
+ { targetId: frameId, flatten: true }
18970
+ );
18971
+ } catch (error) {
18972
+ if (error instanceof BridgeCommandError) throw error;
18973
+ throw new BridgeCommandError(
18974
+ "element_not_found",
18975
+ "that frame is gone or still loading; take a new snapshot and retry",
18976
+ { reason: FRAME_DETACHED }
18977
+ );
18978
+ }
18979
+ const sessionId2 = attached?.sessionId;
18980
+ if (!sessionId2) {
18981
+ throw new BridgeCommandError(
18982
+ "element_not_found",
18983
+ "that frame could not be attached to; take a new snapshot and retry",
18984
+ { reason: FRAME_DETACHED }
18985
+ );
18986
+ }
18987
+ try {
18988
+ await this.send(
18989
+ contents,
18990
+ "Emulation.setFocusEmulationEnabled",
18991
+ { enabled: true },
18992
+ { sessionId: sessionId2 }
18993
+ );
18994
+ } catch (error) {
18995
+ if (!(error instanceof BridgeCommandError && error.data?.reason === FRAME_DETACHED)) {
18996
+ await this.send(contents, "Target.detachFromTarget", {
18997
+ sessionId: sessionId2
18998
+ }).catch(() => void 0);
18999
+ }
19000
+ throw error;
19001
+ }
19002
+ let sessions = this.frames.get(contents.id);
19003
+ if (!sessions) {
19004
+ sessions = /* @__PURE__ */ new Map();
19005
+ this.frames.set(contents.id, sessions);
19006
+ }
19007
+ sessions.set(frameId, sessionId2);
19008
+ return sessionId2;
19009
+ }
19010
+ dropFrameSession(webContentsId, sessionId2) {
19011
+ const sessions = this.frames.get(webContentsId);
19012
+ if (!sessions || !sessionId2) return;
19013
+ for (const [frameId, cached2] of sessions) {
19014
+ if (cached2 === sessionId2) sessions.delete(frameId);
19015
+ }
19016
+ }
19017
+ async sendTo(contents, method, params, deadlineMs, sessionId2) {
18918
19018
  const attachment = this.attachments.get(contents.id);
18919
19019
  if (!attachment) {
18920
19020
  throw new BridgeCommandError(
@@ -18930,11 +19030,29 @@ class CdpPool {
18930
19030
  { reason: attachment.detachedReason }
18931
19031
  );
18932
19032
  }
18933
- return deadline(
18934
- contents.debugger.sendCommand(method, params),
18935
- deadlineMs,
18936
- method
18937
- );
19033
+ if (!sessionId2) {
19034
+ return deadline(
19035
+ contents.debugger.sendCommand(method, params),
19036
+ deadlineMs,
19037
+ method
19038
+ );
19039
+ }
19040
+ try {
19041
+ return await deadline(
19042
+ contents.debugger.sendCommand(method, params, sessionId2),
19043
+ deadlineMs,
19044
+ method
19045
+ );
19046
+ } catch (error) {
19047
+ const message = error instanceof Error ? error.message : String(error);
19048
+ if (!STALE_SESSION.test(message)) throw error;
19049
+ this.dropFrameSession(contents.id, sessionId2);
19050
+ throw new BridgeCommandError(
19051
+ "element_not_found",
19052
+ "that frame navigated or closed while it was being driven; retry",
19053
+ { reason: FRAME_DETACHED }
19054
+ );
19055
+ }
18938
19056
  }
18939
19057
  /** Remove this attachment's debugger listeners. Called before the record is
18940
19058
  * replaced by a re-`attach` and before it is dropped, so a view is never left
@@ -18957,6 +19075,7 @@ class CdpPool {
18957
19075
  if (attachment) this.releaseListeners(attachment);
18958
19076
  this.attachments.delete(webContentsId);
18959
19077
  this.subscribers.delete(webContentsId);
19078
+ this.frames.delete(webContentsId);
18960
19079
  stopLogCapture(webContentsId);
18961
19080
  }
18962
19081
  /** Detach cleanly: release the debugger session and the log buffer. */
@@ -18965,6 +19084,7 @@ class CdpPool {
18965
19084
  if (attachment) this.releaseListeners(attachment);
18966
19085
  this.attachments.delete(webContentsId);
18967
19086
  this.subscribers.delete(webContentsId);
19087
+ this.frames.delete(webContentsId);
18968
19088
  stopLogCapture(webContentsId);
18969
19089
  if (!attachment) return;
18970
19090
  try {
@@ -19080,15 +19200,15 @@ async function withOriginGate(ctx, view, requester, approved, operation) {
19080
19200
  const contents = view.webContents;
19081
19201
  const blocked = [];
19082
19202
  const seen = /* @__PURE__ */ new Set();
19083
- const frameTree = await resolveMainFrameId(ctx, contents);
19084
- const mainFrameId = frameTree.mainFrameId;
19203
+ const frameTree2 = await resolveMainFrameId(ctx, contents);
19204
+ const mainFrameId = frameTree2.mainFrameId;
19085
19205
  if (mainFrameId === null) {
19086
19206
  ctx.log(
19087
19207
  `[browser] could not read the frame tree for ${requester}; this navigation is gated on every Document request, which may refuse cross-origin frames the page needs${// The domain's state travels with the fallback because it is not
19088
19208
  // inferable from the failure: an enable that resolved and a read that
19089
19209
  // did not left `Page` armed, an enable that never resolved did not
19090
19210
  // (agent review round 2, R3).
19091
- frameTree.pageEnabled ? " (the Page domain may still be enabled)" : ""}`
19211
+ frameTree2.pageEnabled ? " (the Page domain may still be enabled)" : ""}`
19092
19212
  );
19093
19213
  }
19094
19214
  let unattributed = 0;
@@ -19160,7 +19280,7 @@ async function withOriginGate(ctx, view, requester, approved, operation) {
19160
19280
  await ctx.cdp.send(contents, "Fetch.disable", {});
19161
19281
  } catch {
19162
19282
  }
19163
- if (frameTree.pageEnabled) {
19283
+ if (frameTree2.pageEnabled) {
19164
19284
  try {
19165
19285
  await ctx.cdp.send(contents, "Page.disable", {});
19166
19286
  } catch {
@@ -19191,6 +19311,399 @@ function pageOf(view) {
19191
19311
  title: view.webContents.isDestroyed() ? "" : view.webContents.getTitle()
19192
19312
  };
19193
19313
  }
19314
+ const FRAME_HOP = ">>>";
19315
+ const MAX_FRAME_DEPTH = 3;
19316
+ const MAX_FRAMES_SEARCHED = 20;
19317
+ function sendIn(ctx, contents, sessionId2, method, params) {
19318
+ return sessionId2 ? ctx.cdp.send(contents, method, params, { sessionId: sessionId2 }) : ctx.cdp.send(contents, method, params);
19319
+ }
19320
+ function parseFrameHops(target2) {
19321
+ if (!target2.includes(FRAME_HOP)) return null;
19322
+ const hops = splitTopLevel(target2);
19323
+ if (hops.length === 1) return null;
19324
+ if (hops.some((hop) => hop === "")) {
19325
+ throw new BridgeCommandError(
19326
+ "element_not_found",
19327
+ `${target2} has an empty side of '${FRAME_HOP}'; write it as <iframe-selector> ${FRAME_HOP} <selector>`
19328
+ );
19329
+ }
19330
+ if (hops.length - 1 > MAX_FRAME_DEPTH) {
19331
+ throw new BridgeCommandError(
19332
+ "element_not_found",
19333
+ `${target2} hops through ${hops.length - 1} frames; at most ${MAX_FRAME_DEPTH} are followed`
19334
+ );
19335
+ }
19336
+ return hops;
19337
+ }
19338
+ function splitTopLevel(target2) {
19339
+ const parts = [];
19340
+ let quote = null;
19341
+ let depth = 0;
19342
+ let start = 0;
19343
+ for (let i = 0; i < target2.length; i += 1) {
19344
+ const ch = target2[i];
19345
+ if (ch === "\\") {
19346
+ i += 1;
19347
+ continue;
19348
+ }
19349
+ if (quote) {
19350
+ if (ch === quote) quote = null;
19351
+ continue;
19352
+ }
19353
+ if (ch === '"' || ch === "'") quote = ch;
19354
+ else if (ch === "[" || ch === "(") depth += 1;
19355
+ else if ((ch === "]" || ch === ")") && depth > 0) depth -= 1;
19356
+ else if (depth === 0 && target2.startsWith(FRAME_HOP, i)) {
19357
+ parts.push(target2.slice(start, i).trim());
19358
+ i += FRAME_HOP.length - 1;
19359
+ start = i + 1;
19360
+ }
19361
+ }
19362
+ parts.push(target2.slice(start).trim());
19363
+ return parts;
19364
+ }
19365
+ function assertFrameNotDenied(ctx, scope) {
19366
+ if (!isDeniedFrame(ctx, scope)) return;
19367
+ throw new BridgeCommandError(
19368
+ "origin_not_allowed",
19369
+ `the user denied ${scope.origin} for agent access, and that element is inside a frame from it`,
19370
+ { origin: scope.origin, reason: "denied" }
19371
+ );
19372
+ }
19373
+ function isDeniedRefusal(error) {
19374
+ return error instanceof BridgeCommandError && error.code === "origin_not_allowed" && error.data?.reason === "denied";
19375
+ }
19376
+ function isDeniedFrame(ctx, scope) {
19377
+ if (!scope.origin) return false;
19378
+ let url;
19379
+ try {
19380
+ url = new URL(scope.origin);
19381
+ } catch {
19382
+ return false;
19383
+ }
19384
+ return ctx.approvals?.refusalReason(url) === "denied";
19385
+ }
19386
+ function isFatalWalkError(error) {
19387
+ return error instanceof BridgeCommandError && (error.code === "debugger_conflict" || error.code === "tab_closed");
19388
+ }
19389
+ async function topScope(ctx, contents) {
19390
+ const document = await ctx.cdp.send(
19391
+ contents,
19392
+ "DOM.getDocument",
19393
+ { depth: 0 }
19394
+ );
19395
+ const rootNodeId = document?.root?.nodeId;
19396
+ if (rootNodeId === void 0) {
19397
+ throw new BridgeCommandError("internal", "the page has no document to query");
19398
+ }
19399
+ return { rootNodeId, hops: [], path: [] };
19400
+ }
19401
+ async function queryIn(ctx, contents, scope, selector) {
19402
+ const queried = await sendIn(
19403
+ ctx,
19404
+ contents,
19405
+ scope.sessionId,
19406
+ "DOM.querySelector",
19407
+ { nodeId: scope.rootNodeId, selector }
19408
+ );
19409
+ return queried?.nodeId ?? 0;
19410
+ }
19411
+ const FRAME_NODE_NAMES = /* @__PURE__ */ new Set(["IFRAME", "FRAME"]);
19412
+ function originOf(url) {
19413
+ if (!url) return void 0;
19414
+ try {
19415
+ return new URL(url).origin;
19416
+ } catch {
19417
+ return void 0;
19418
+ }
19419
+ }
19420
+ const PLAIN_ID = /^[A-Za-z_][\w-]*$/;
19421
+ function frameSelectorFor(attributes = []) {
19422
+ const attrs = /* @__PURE__ */ new Map();
19423
+ for (let i = 0; i + 1 < attributes.length; i += 2) {
19424
+ attrs.set(attributes[i], attributes[i + 1]);
19425
+ }
19426
+ const id2 = attrs.get("id");
19427
+ if (id2 && PLAIN_ID.test(id2)) return `iframe#${id2}`;
19428
+ for (const key of ["name", "title", "src"]) {
19429
+ const value = attrs.get(key);
19430
+ if (value) return `iframe[${key}=${JSON.stringify(value)}]`;
19431
+ }
19432
+ return "iframe";
19433
+ }
19434
+ async function enterFrame(ctx, contents, parent, iframeNodeId) {
19435
+ const described = await sendIn(
19436
+ ctx,
19437
+ contents,
19438
+ parent.sessionId,
19439
+ "DOM.describeNode",
19440
+ { nodeId: iframeNodeId, depth: 1 }
19441
+ );
19442
+ const node = described?.node;
19443
+ if (!node || !FRAME_NODE_NAMES.has(String(node.nodeName).toUpperCase())) {
19444
+ throw new BridgeCommandError(
19445
+ "element_not_found",
19446
+ "that element is not an iframe, so there is no frame to step into",
19447
+ { reason: NOT_A_FRAME }
19448
+ );
19449
+ }
19450
+ const hops = [
19451
+ ...parent.hops,
19452
+ { sessionId: parent.sessionId, nodeId: iframeNodeId }
19453
+ ];
19454
+ const path2 = [...parent.path, frameSelectorFor(node.attributes)];
19455
+ const inProcess = node.contentDocument?.backendNodeId;
19456
+ if (inProcess !== void 0) {
19457
+ assertFrameNotDenied(ctx, {
19458
+ origin: originOf(node.contentDocument?.documentURL)
19459
+ });
19460
+ const pushed = await sendIn(
19461
+ ctx,
19462
+ contents,
19463
+ parent.sessionId,
19464
+ "DOM.pushNodesByBackendIdsToFrontend",
19465
+ { backendNodeIds: [inProcess] }
19466
+ );
19467
+ const rootNodeId = pushed?.nodeIds?.[0];
19468
+ if (!rootNodeId) {
19469
+ throw new BridgeCommandError(
19470
+ "element_not_found",
19471
+ "that frame has no document yet; retry once it has loaded"
19472
+ );
19473
+ }
19474
+ return {
19475
+ sessionId: parent.sessionId,
19476
+ frameTargetId: parent.frameTargetId,
19477
+ frameId: node.frameId,
19478
+ rootNodeId,
19479
+ origin: originOf(node.contentDocument?.documentURL),
19480
+ hops,
19481
+ path: path2
19482
+ };
19483
+ }
19484
+ const frameId = node.frameId;
19485
+ if (!frameId) {
19486
+ throw new BridgeCommandError(
19487
+ "element_not_found",
19488
+ "that frame has no document yet; retry once it has loaded"
19489
+ );
19490
+ }
19491
+ const rooted = await childRoot(ctx, contents, frameId);
19492
+ assertFrameNotDenied(ctx, { origin: rooted.origin });
19493
+ return {
19494
+ sessionId: rooted.sessionId,
19495
+ frameTargetId: frameId,
19496
+ frameId,
19497
+ rootNodeId: rooted.rootNodeId,
19498
+ origin: rooted.origin,
19499
+ hops,
19500
+ path: path2
19501
+ };
19502
+ }
19503
+ async function childRoot(ctx, contents, frameId) {
19504
+ for (let attempt = 0; ; attempt += 1) {
19505
+ try {
19506
+ const sessionId2 = await ctx.cdp.attachFrame(contents, frameId);
19507
+ const document = await ctx.cdp.send(contents, "DOM.getDocument", { depth: 0 }, { sessionId: sessionId2 });
19508
+ const rootNodeId = document?.root?.nodeId;
19509
+ if (rootNodeId === void 0) {
19510
+ throw new BridgeCommandError(
19511
+ "element_not_found",
19512
+ "that frame has no document yet; retry once it has loaded"
19513
+ );
19514
+ }
19515
+ return {
19516
+ sessionId: sessionId2,
19517
+ rootNodeId,
19518
+ origin: originOf(document?.root?.documentURL)
19519
+ };
19520
+ } catch (error) {
19521
+ if (attempt === 0 && isFrameDetached(error)) continue;
19522
+ throw error;
19523
+ }
19524
+ }
19525
+ }
19526
+ const NOT_A_FRAME = "not_a_frame";
19527
+ function isNotAFrame(error) {
19528
+ return error instanceof BridgeCommandError && error.data?.reason === NOT_A_FRAME;
19529
+ }
19530
+ function isFrameDetached(error) {
19531
+ return error instanceof BridgeCommandError && error.data?.reason === FRAME_DETACHED;
19532
+ }
19533
+ async function resolveHops(ctx, contents, hops) {
19534
+ let scope = await topScope(ctx, contents);
19535
+ for (const hop of hops.slice(0, -1)) {
19536
+ const iframe = await queryIn(ctx, contents, scope, hop);
19537
+ if (!iframe) {
19538
+ throw new BridgeCommandError(
19539
+ "element_not_found",
19540
+ `frame selector ${hop} matched nothing`
19541
+ );
19542
+ }
19543
+ scope = await enterFrame(ctx, contents, scope, iframe).catch((error) => {
19544
+ if (isNotAFrame(error)) {
19545
+ throw new BridgeCommandError(
19546
+ "element_not_found",
19547
+ `${hop} is not an iframe, so '${FRAME_HOP}' cannot step into it`
19548
+ );
19549
+ }
19550
+ throw error;
19551
+ });
19552
+ }
19553
+ const selector = hops[hops.length - 1];
19554
+ const nodeId = await queryIn(ctx, contents, scope, selector);
19555
+ if (!nodeId) {
19556
+ throw new BridgeCommandError(
19557
+ "element_not_found",
19558
+ `selector ${selector} matched nothing inside ${hops.slice(0, -1).join(` ${FRAME_HOP} `)}`
19559
+ );
19560
+ }
19561
+ return { scope, nodeId };
19562
+ }
19563
+ async function searchFrames(ctx, contents, selector) {
19564
+ const matches = [];
19565
+ const walk = await listFrames(ctx, contents);
19566
+ for (const scope of walk.frames) {
19567
+ const nodeId = await queryIn(ctx, contents, scope, selector).catch(
19568
+ (error) => {
19569
+ if (isFatalWalkError(error)) throw error;
19570
+ return 0;
19571
+ }
19572
+ );
19573
+ if (nodeId) matches.push({ scope, nodeId });
19574
+ }
19575
+ return { matches, walk };
19576
+ }
19577
+ async function listFrames(ctx, contents) {
19578
+ const walk = {
19579
+ frames: [],
19580
+ truncated: false,
19581
+ skipped: 0,
19582
+ denied: 0
19583
+ };
19584
+ let level = [await topScope(ctx, contents)];
19585
+ for (let depth = 0; depth < MAX_FRAME_DEPTH && level.length; depth += 1) {
19586
+ const next = [];
19587
+ for (const parent of level) {
19588
+ const listed = await sendIn(
19589
+ ctx,
19590
+ contents,
19591
+ parent.sessionId,
19592
+ "DOM.querySelectorAll",
19593
+ { nodeId: parent.rootNodeId, selector: "iframe, frame" }
19594
+ );
19595
+ for (const iframe of listed?.nodeIds ?? []) {
19596
+ if (walk.frames.length + walk.skipped >= MAX_FRAMES_SEARCHED) {
19597
+ walk.truncated = true;
19598
+ return walk;
19599
+ }
19600
+ let scope;
19601
+ try {
19602
+ scope = await enterFrame(ctx, contents, parent, iframe);
19603
+ } catch (error) {
19604
+ if (isFatalWalkError(error)) throw error;
19605
+ if (isDeniedRefusal(error)) {
19606
+ walk.denied += 1;
19607
+ continue;
19608
+ }
19609
+ if (error instanceof BridgeCommandError) {
19610
+ walk.skipped += 1;
19611
+ continue;
19612
+ }
19613
+ throw error;
19614
+ }
19615
+ walk.frames.push(scope);
19616
+ next.push(scope);
19617
+ }
19618
+ }
19619
+ level = next;
19620
+ }
19621
+ return walk;
19622
+ }
19623
+ function candidatePath(scope, selector) {
19624
+ return [...scope.path, selector].join(` ${FRAME_HOP} `);
19625
+ }
19626
+ const EDITABLE_SELECTOR = 'input:not([type=hidden]):not([type=button]):not([type=submit]):not([type=reset]):not([type=checkbox]):not([type=radio]):not([type=file]):not([type=image]):not([type=range]):not([type=color]):not([disabled]):not([readonly]), textarea:not([disabled]):not([readonly]), [contenteditable]:not([contenteditable="false"])';
19627
+ const FRAME_EDITABLE_FUNCTION_SOURCE = `function (selector, mode) {
19628
+ const all = Array.from(this.querySelectorAll(selector));
19629
+ if (mode === 'active') {
19630
+ const active = this.activeElement;
19631
+ return active && all.indexOf(active) !== -1 ? active : null;
19632
+ }
19633
+ if (mode === 'only') return all.length === 1 ? all[0] : null;
19634
+ return all.map(function (el) {
19635
+ const tag = String(el.tagName || '').toLowerCase();
19636
+ if (el.id && /^[A-Za-z_][\\w-]*$/.test(el.id)) return '#' + el.id;
19637
+ const name = el.getAttribute && el.getAttribute('name');
19638
+ if (name) return tag + '[name=' + JSON.stringify(name) + ']';
19639
+ // A real selector, not a label: the n-th element of its tag under its parent,
19640
+ // walked up to the document so the path is unique and addressable.
19641
+ const steps = [];
19642
+ for (let node = el; node && node.nodeType === 1 && node.tagName; node = node.parentElement) {
19643
+ const name2 = String(node.tagName).toLowerCase();
19644
+ let nth = 1;
19645
+ for (let sib = node.previousElementSibling; sib; sib = sib.previousElementSibling) {
19646
+ if (sib.tagName === node.tagName) nth += 1;
19647
+ }
19648
+ steps.unshift(name2 + ':nth-of-type(' + nth + ')');
19649
+ if (name2 === 'html') break;
19650
+ }
19651
+ return steps.length ? steps.join(' > ') : tag;
19652
+ });
19653
+ }`;
19654
+ async function editableInFrame(ctx, contents, scope, target2) {
19655
+ const documentObject = await sendIn(
19656
+ ctx,
19657
+ contents,
19658
+ scope.sessionId,
19659
+ "DOM.resolveNode",
19660
+ { nodeId: scope.rootNodeId }
19661
+ );
19662
+ const documentId = documentObject?.object?.objectId;
19663
+ if (!documentId) {
19664
+ throw new BridgeCommandError(
19665
+ "element_not_found",
19666
+ `could not read the document inside ${target2}`
19667
+ );
19668
+ }
19669
+ const pick = (mode, returnByValue) => sendIn(
19670
+ ctx,
19671
+ contents,
19672
+ scope.sessionId,
19673
+ "Runtime.callFunctionOn",
19674
+ {
19675
+ objectId: documentId,
19676
+ functionDeclaration: FRAME_EDITABLE_FUNCTION_SOURCE,
19677
+ arguments: [{ value: EDITABLE_SELECTOR }, { value: mode }],
19678
+ returnByValue
19679
+ }
19680
+ );
19681
+ let objectId = (await pick("active", false))?.result?.objectId;
19682
+ if (!objectId) objectId = (await pick("only", false))?.result?.objectId;
19683
+ if (!objectId) {
19684
+ const listed = (await pick("list", true))?.result?.value;
19685
+ const fields = Array.isArray(listed) ? listed.map(String) : [];
19686
+ if (fields.length === 0) {
19687
+ throw new BridgeCommandError(
19688
+ "element_not_found",
19689
+ `${target2} is not an editable field (no value setter, not contenteditable), so nothing was typed; it is an iframe and its document holds no editable field either`
19690
+ );
19691
+ }
19692
+ throw new BridgeCommandError(
19693
+ "element_not_found",
19694
+ `${target2} is an iframe holding ${fields.length} editable fields, so nothing was typed; name one: ${fields.map((field) => candidatePath(scope, field)).join(", ")}`,
19695
+ { candidates: fields.map((field) => candidatePath(scope, field)) }
19696
+ );
19697
+ }
19698
+ const requested = await sendIn(
19699
+ ctx,
19700
+ contents,
19701
+ scope.sessionId,
19702
+ "DOM.requestNode",
19703
+ { objectId }
19704
+ );
19705
+ return { nodeId: requested?.nodeId ?? 0, objectId };
19706
+ }
19194
19707
  const LANDMARKS = /* @__PURE__ */ new Set(["banner", "main", "navigation", "complementary", "contentinfo", "form", "region"]);
19195
19708
  const INTERACTIVE = /* @__PURE__ */ new Set(["button", "link", "textbox", "checkbox", "radio", "combobox", "menuitem", "tab", "switch", "slider", "spinbutton"]);
19196
19709
  function compactAX(nodes, epoch) {
@@ -19321,7 +19834,16 @@ async function snapshot(ctx, params) {
19321
19834
  await ctx.cdp.send(contents, "Accessibility.disable", {}).catch(() => {
19322
19835
  });
19323
19836
  }
19324
- const { snapshot: rendered, refs } = compactAX(nodes, record.epoch);
19837
+ const { snapshot: topRendered, refs: topRefs } = compactAX(
19838
+ nodes,
19839
+ record.epoch
19840
+ );
19841
+ const refs = { ...topRefs };
19842
+ let rendered = topRendered;
19843
+ if (nodes.some(isFrameNode)) {
19844
+ const blocks = await frameSnapshots(ctx, record, nodes, refs);
19845
+ if (blocks.length) rendered = [rendered, ...blocks].join("\n");
19846
+ }
19325
19847
  record.refs = refs;
19326
19848
  ctx.registry.touch(record);
19327
19849
  return {
@@ -19331,6 +19853,88 @@ async function snapshot(ctx, params) {
19331
19853
  ...pageOf(record.view)
19332
19854
  };
19333
19855
  }
19856
+ function isFrameNode(node) {
19857
+ return node.role?.value === "Iframe" && node.backendDOMNodeId !== void 0;
19858
+ }
19859
+ const REF_LABEL = /\[e(\d+)\]/g;
19860
+ async function frameSnapshots(ctx, record, topNodes, refs) {
19861
+ const contents = record.view.webContents;
19862
+ const blocks = [];
19863
+ let entered = 0;
19864
+ let level = [
19865
+ { scope: await topScope(ctx, contents), nodes: topNodes }
19866
+ ];
19867
+ for (let depth = 0; depth < MAX_FRAME_DEPTH && level.length; depth += 1) {
19868
+ const next = [];
19869
+ for (const { scope: parent, nodes } of level) {
19870
+ for (const frameNode of nodes.filter(isFrameNode)) {
19871
+ if (entered >= MAX_FRAMES_SEARCHED) return blocks;
19872
+ entered += 1;
19873
+ try {
19874
+ const pushed = await sendIn(
19875
+ ctx,
19876
+ contents,
19877
+ parent.sessionId,
19878
+ "DOM.pushNodesByBackendIdsToFrontend",
19879
+ { backendNodeIds: [frameNode.backendDOMNodeId] }
19880
+ );
19881
+ const iframeNodeId = pushed?.nodeIds?.[0];
19882
+ if (!iframeNodeId) continue;
19883
+ const scope = await enterFrame(ctx, contents, parent, iframeNodeId);
19884
+ const frameNodes = await frameTree(ctx, contents, scope);
19885
+ const { snapshot: inner, refs: innerRefs } = compactAX(
19886
+ frameNodes,
19887
+ record.epoch
19888
+ );
19889
+ const offset = Object.keys(refs).length;
19890
+ for (const [name, ref] of Object.entries(innerRefs)) {
19891
+ refs[`e${Number(name.slice(1)) + offset}`] = {
19892
+ ...ref,
19893
+ ...scope.frameTargetId ? { frameId: scope.frameTargetId } : {},
19894
+ frameOrigin: scope.origin ?? ""
19895
+ };
19896
+ }
19897
+ const indent = " ".repeat(depth);
19898
+ const body = inner.replace(REF_LABEL, (_m, n) => `[e${Number(n) + offset}]`).split("\n").filter(Boolean).map((line) => `${indent} ${line}`);
19899
+ blocks.push(
19900
+ `${indent}- frame ${scope.path.join(` ${FRAME_HOP} `)}${scope.origin ? ` (${scope.origin})` : ""}:`,
19901
+ ...body
19902
+ );
19903
+ next.push({ scope, nodes: frameNodes });
19904
+ } catch (error) {
19905
+ if (error instanceof BridgeCommandError && error.code !== "debugger_conflict" && error.code !== "tab_closed")
19906
+ continue;
19907
+ throw error;
19908
+ }
19909
+ }
19910
+ }
19911
+ level = next;
19912
+ }
19913
+ return blocks;
19914
+ }
19915
+ async function frameTree(ctx, contents, scope) {
19916
+ const params = scope.frameTargetId === scope.frameId || !scope.frameId ? {} : { frameId: scope.frameId };
19917
+ await sendIn(ctx, contents, scope.sessionId, "Accessibility.enable", {});
19918
+ try {
19919
+ const tree = await sendIn(
19920
+ ctx,
19921
+ contents,
19922
+ scope.sessionId,
19923
+ "Accessibility.getFullAXTree",
19924
+ params
19925
+ );
19926
+ return Array.isArray(tree?.nodes) ? tree.nodes : [];
19927
+ } finally {
19928
+ await sendIn(
19929
+ ctx,
19930
+ contents,
19931
+ scope.sessionId,
19932
+ "Accessibility.disable",
19933
+ {}
19934
+ ).catch(() => {
19935
+ });
19936
+ }
19937
+ }
19334
19938
  const METRICS_EXPRESSION = `(() => {
19335
19939
  const de = document.scrollingElement || document.documentElement;
19336
19940
  const x = window.scrollX, y = window.scrollY;
@@ -19550,11 +20154,13 @@ const READ_VALUE_FUNCTION = `function () {
19550
20154
  return String(this.textContent || '');
19551
20155
  }`;
19552
20156
  const SNAPSHOT_REF = /^e\d+$/;
20157
+ function frameFields(node) {
20158
+ return node.frame ? { frame_origin: node.frame.origin ?? "" } : {};
20159
+ }
19553
20160
  async function resolveNode(ctx, record, target2) {
19554
20161
  const view = record.view;
19555
20162
  const contents = view.webContents;
19556
20163
  const ref = SNAPSHOT_REF.test(target2) ? record.refs[target2] : void 0;
19557
- let nodeId;
19558
20164
  if (ref) {
19559
20165
  if (ref.epoch !== record.epoch) {
19560
20166
  throw new BridgeCommandError(
@@ -19562,6 +20168,7 @@ async function resolveNode(ctx, record, target2) {
19562
20168
  "the page navigated since that snapshot; take a new snapshot and retry"
19563
20169
  );
19564
20170
  }
20171
+ if (ref.frameId) return resolveFrameRef(ctx, contents, target2, ref);
19565
20172
  await ctx.cdp.send(contents, "DOM.getDocument", { depth: 0 });
19566
20173
  const pushed = await ctx.cdp.send(
19567
20174
  contents,
@@ -19572,10 +20179,72 @@ async function resolveNode(ctx, record, target2) {
19572
20179
  if (pushedId === void 0) {
19573
20180
  throw new BridgeCommandError("element_not_found", "snapshot ref is stale");
19574
20181
  }
19575
- nodeId = pushedId;
19576
- } else {
20182
+ return finishTop(ctx, contents, target2, pushedId, ref.frameOrigin);
20183
+ }
20184
+ const hops = parseFrameHops(target2);
20185
+ if (hops) {
20186
+ const hit = await resolveHops(ctx, contents, hops);
20187
+ return finishInFrame(ctx, contents, target2, hit.scope, hit.nodeId);
20188
+ }
20189
+ let nodeId;
20190
+ try {
19577
20191
  nodeId = await nodeIdForSelector(ctx, view, target2);
20192
+ } catch (error) {
20193
+ if (!(error instanceof BridgeCommandError) || !TOP_MISS.test(error.message)) {
20194
+ throw error;
20195
+ }
20196
+ const { matches, walk } = await searchFrames(ctx, contents, target2);
20197
+ if (matches.length === 0) {
20198
+ if (!walk.truncated && walk.skipped === 0 && walk.denied === 0)
20199
+ throw error;
20200
+ throw new BridgeCommandError(
20201
+ "element_not_found",
20202
+ `selector ${target2} matched nothing in the page itself or the frames searched (${walkNote(walk)}); address the frame explicitly as <iframe-selector> ${FRAME_HOP} <selector>`
20203
+ );
20204
+ }
20205
+ if (matches.length === 1 && (walk.truncated || walk.skipped > 0)) {
20206
+ const candidate = candidatePath(matches[0].scope, target2);
20207
+ throw new BridgeCommandError(
20208
+ "element_not_found",
20209
+ `selector ${target2} matched nothing in the page itself and one element in the frames searched, but not every frame was searched (${walkNote(walk)}), so nothing was done; if this is the one, name it: ${candidate}`,
20210
+ { candidates: [candidate] }
20211
+ );
20212
+ }
20213
+ if (matches.length > 1) {
20214
+ const candidates = matches.map(
20215
+ (match) => candidatePath(match.scope, target2)
20216
+ );
20217
+ throw new BridgeCommandError(
20218
+ "element_not_found",
20219
+ `selector ${target2} matched nothing in the page itself and ${matches.length} elements inside its frames, so nothing was done; name one: ${candidates.join(", ")}`,
20220
+ { candidates }
20221
+ );
20222
+ }
20223
+ return finishInFrame(
20224
+ ctx,
20225
+ contents,
20226
+ target2,
20227
+ matches[0].scope,
20228
+ matches[0].nodeId
20229
+ );
19578
20230
  }
20231
+ return finishTop(ctx, contents, target2, nodeId);
20232
+ }
20233
+ function walkNote(walk) {
20234
+ const notes = [];
20235
+ if (walk.truncated) notes.push(`stopped at ${MAX_FRAMES_SEARCHED} frames`);
20236
+ if (walk.skipped > 0)
20237
+ notes.push(`${walk.skipped} frame(s) could not be entered`);
20238
+ if (walk.denied > 0)
20239
+ notes.push(
20240
+ `${walk.denied} frame(s) from an origin the user denied were not searched`
20241
+ );
20242
+ return notes.join("; ") || "every frame searched";
20243
+ }
20244
+ const TOP_MISS = /^selector .* matched nothing$/s;
20245
+ async function finishTop(ctx, contents, target2, nodeId, frameOrigin) {
20246
+ if (frameOrigin !== void 0)
20247
+ assertFrameNotDenied(ctx, { origin: frameOrigin });
19579
20248
  await ctx.cdp.send(contents, "DOM.scrollIntoViewIfNeeded", { nodeId });
19580
20249
  const resolved = await ctx.cdp.send(
19581
20250
  contents,
@@ -19589,7 +20258,83 @@ async function resolveNode(ctx, record, target2) {
19589
20258
  `could not resolve ${target2}`
19590
20259
  );
19591
20260
  }
19592
- return { nodeId, objectId };
20261
+ if (frameOrigin === void 0) return { nodeId, objectId };
20262
+ return {
20263
+ nodeId,
20264
+ objectId,
20265
+ frame: { rootNodeId: 0, origin: frameOrigin, hops: [], path: [] }
20266
+ };
20267
+ }
20268
+ async function finishInFrame(ctx, contents, target2, scope, nodeId) {
20269
+ assertFrameNotDenied(ctx, scope);
20270
+ for (const hop of scope.hops) {
20271
+ await sendIn(ctx, contents, hop.sessionId, "DOM.scrollIntoViewIfNeeded", {
20272
+ nodeId: hop.nodeId
20273
+ }).catch(() => void 0);
20274
+ }
20275
+ await sendIn(ctx, contents, scope.sessionId, "DOM.scrollIntoViewIfNeeded", {
20276
+ nodeId
20277
+ }).catch(() => void 0);
20278
+ const resolved = await sendIn(
20279
+ ctx,
20280
+ contents,
20281
+ scope.sessionId,
20282
+ "DOM.resolveNode",
20283
+ { nodeId }
20284
+ );
20285
+ const objectId = resolved?.object?.objectId;
20286
+ if (!objectId) {
20287
+ throw new BridgeCommandError(
20288
+ "element_not_found",
20289
+ `could not resolve ${target2}`
20290
+ );
20291
+ }
20292
+ return { nodeId, objectId, sessionId: scope.sessionId, frame: scope };
20293
+ }
20294
+ async function resolveFrameRef(ctx, contents, target2, ref) {
20295
+ assertFrameNotDenied(ctx, { origin: ref.frameOrigin });
20296
+ let rooted;
20297
+ try {
20298
+ rooted = await childRoot(ctx, contents, String(ref.frameId));
20299
+ } catch (error) {
20300
+ if (isFrameDetached(error)) {
20301
+ throw new BridgeCommandError("element_not_found", "snapshot ref is stale");
20302
+ }
20303
+ throw error;
20304
+ }
20305
+ assertFrameNotDenied(ctx, { origin: rooted.origin });
20306
+ await ctx.cdp.send(contents, "DOM.getFrameOwner", {
20307
+ frameId: ref.frameId
20308
+ }).then(
20309
+ (owner) => owner?.backendNodeId ? ctx.cdp.send(contents, "DOM.scrollIntoViewIfNeeded", {
20310
+ backendNodeId: owner.backendNodeId
20311
+ }) : void 0
20312
+ ).catch(() => void 0);
20313
+ const pushed = await ctx.cdp.send(
20314
+ contents,
20315
+ "DOM.pushNodesByBackendIdsToFrontend",
20316
+ { backendNodeIds: [ref.backendNodeId] },
20317
+ { sessionId: rooted.sessionId }
20318
+ );
20319
+ const nodeId = pushed?.nodeIds?.[0];
20320
+ if (!nodeId) {
20321
+ throw new BridgeCommandError("element_not_found", "snapshot ref is stale");
20322
+ }
20323
+ return finishInFrame(
20324
+ ctx,
20325
+ contents,
20326
+ target2,
20327
+ {
20328
+ sessionId: rooted.sessionId,
20329
+ frameTargetId: ref.frameId,
20330
+ frameId: ref.frameId,
20331
+ rootNodeId: rooted.rootNodeId,
20332
+ origin: ref.frameOrigin ?? rooted.origin,
20333
+ hops: [],
20334
+ path: []
20335
+ },
20336
+ nodeId
20337
+ );
19593
20338
  }
19594
20339
  function targetOf(params) {
19595
20340
  const ref = stringParam(params, "ref");
@@ -19616,7 +20361,7 @@ async function click(ctx, params) {
19616
20361
  };
19617
20362
  contents.on("did-start-navigation", onStart);
19618
20363
  try {
19619
- await ctx.cdp.send(contents, "Runtime.callFunctionOn", {
20364
+ await sendIn(ctx, contents, node.sessionId, "Runtime.callFunctionOn", {
19620
20365
  objectId: node.objectId,
19621
20366
  functionDeclaration: CLICK_FUNCTION,
19622
20367
  returnByValue: true
@@ -19630,26 +20375,78 @@ async function click(ctx, params) {
19630
20375
  }
19631
20376
  ctx.registry.touch(record);
19632
20377
  const after = pageOf(record.view);
19633
- return { navigated: navigationSeen || after.url !== before, ...after };
20378
+ return {
20379
+ navigated: navigationSeen || after.url !== before,
20380
+ ...frameFields(node),
20381
+ ...after
20382
+ };
19634
20383
  }
19635
20384
  async function type(ctx, params) {
19636
20385
  const record = ctx.registry.requireSurface(params.tab);
19637
20386
  const contents = record.view.webContents;
19638
- const node = await resolveNode(ctx, record, targetOf(params));
20387
+ const target2 = targetOf(params);
20388
+ const node = await resolveNode(ctx, record, target2);
19639
20389
  const text = typeof params.text === "string" ? params.text : "";
19640
- await ctx.cdp.send(contents, "Runtime.callFunctionOn", {
20390
+ const typed = await typeInto(ctx, contents, node, text, target2);
20391
+ if (typed) {
20392
+ ctx.registry.touch(record);
20393
+ return { ...typed, ...frameFields(node), ...pageOf(record.view) };
20394
+ }
20395
+ const frame = await iframeScopeOf(ctx, contents, node);
20396
+ if (frame) {
20397
+ assertFrameNotDenied(ctx, frame);
20398
+ const field = await editableInFrame(ctx, contents, frame, target2);
20399
+ const inner = {
20400
+ nodeId: field.nodeId,
20401
+ objectId: field.objectId,
20402
+ sessionId: frame.sessionId,
20403
+ frame
20404
+ };
20405
+ const typedInFrame = await typeInto(ctx, contents, inner, text, target2);
20406
+ if (typedInFrame) {
20407
+ ctx.registry.touch(record);
20408
+ return {
20409
+ ...typedInFrame,
20410
+ ...frameFields(inner),
20411
+ ...pageOf(record.view)
20412
+ };
20413
+ }
20414
+ }
20415
+ throw new BridgeCommandError(
20416
+ "element_not_found",
20417
+ `${target2} is not an editable field (no value setter, not contenteditable), so nothing was typed${frame ? "; it is an iframe and its document holds no field that took the text either" : `; if the field lives inside an iframe, it cannot be targeted from the top document; address it as <iframe-selector> ${FRAME_HOP} <selector>`}`
20418
+ );
20419
+ }
20420
+ const NON_ALPHANUMERIC = /[^\p{L}\p{N}]+/gu;
20421
+ function foldForComparison(value) {
20422
+ return value.normalize("NFKC").toLowerCase().replace(NON_ALPHANUMERIC, "");
20423
+ }
20424
+ function holdsTypedText(readBack2, text) {
20425
+ const folded = foldForComparison(text);
20426
+ if (folded === "") return readBack2.includes(text);
20427
+ return foldForComparison(readBack2).includes(folded);
20428
+ }
20429
+ const SETTLE_BEFORE_REREAD_MS = 100;
20430
+ const MAX_QUOTED_READBACK = 80;
20431
+ function quoteReadBack(value) {
20432
+ if (value.length <= MAX_QUOTED_READBACK) return `'${value}'`;
20433
+ return `'${value.slice(0, MAX_QUOTED_READBACK)}…' (${value.length} chars)`;
20434
+ }
20435
+ async function typeInto(ctx, contents, node, text, target2) {
20436
+ await sendIn(ctx, contents, node.sessionId, "Runtime.callFunctionOn", {
19641
20437
  objectId: node.objectId,
19642
20438
  functionDeclaration: FOCUS_AND_SELECT_FUNCTION,
19643
20439
  returnByValue: true
19644
20440
  });
19645
- await ctx.cdp.send(contents, "Input.insertText", { text });
19646
- const readBack2 = await conversationReadBack(ctx, node.objectId, contents);
19647
- if (readBack2.includes(text)) {
19648
- ctx.registry.touch(record);
19649
- return { value: readBack2, via: "insert_text", ...pageOf(record.view) };
20441
+ await sendIn(ctx, contents, node.sessionId, "Input.insertText", { text });
20442
+ const readBack2 = await conversationReadBack(ctx, node, contents);
20443
+ if (holdsTypedText(readBack2, text)) {
20444
+ return { value: readBack2, via: "insert_text" };
19650
20445
  }
19651
- const set2 = await ctx.cdp.send(
20446
+ const set2 = await sendIn(
20447
+ ctx,
19652
20448
  contents,
20449
+ node.sessionId,
19653
20450
  "Runtime.callFunctionOn",
19654
20451
  {
19655
20452
  objectId: node.objectId,
@@ -19659,26 +20456,44 @@ async function type(ctx, params) {
19659
20456
  }
19660
20457
  );
19661
20458
  const setValue = set2?.result?.value;
19662
- if (setValue === null) {
19663
- throw new BridgeCommandError(
19664
- "element_not_found",
19665
- `${targetOf(params)} is not an editable field (no value setter, not contenteditable), so nothing was typed; if the field lives inside an iframe, it cannot be targeted from the top document`
19666
- );
20459
+ if (setValue === null) return null;
20460
+ await sleep(SETTLE_BEFORE_REREAD_MS);
20461
+ const settled = await conversationReadBack(ctx, node, contents);
20462
+ if (holdsTypedText(settled, text)) {
20463
+ return {
20464
+ value: settled,
20465
+ via: "value_setter",
20466
+ insert_text_readback: readBack2
20467
+ };
19667
20468
  }
19668
- ctx.registry.touch(record);
19669
- return {
19670
- value: String(setValue ?? readBack2),
19671
- via: "value_setter",
19672
- insert_text_readback: readBack2,
19673
- ...pageOf(record.view)
20469
+ throw new BridgeCommandError(
20470
+ "element_not_found",
20471
+ `${target2} took the value-setter write but does not hold the text (read back ${quoteReadBack(settled)}), so nothing was reported typed`
20472
+ );
20473
+ }
20474
+ async function iframeScopeOf(ctx, contents, node) {
20475
+ const parent = node.frame ?? {
20476
+ rootNodeId: 0,
20477
+ hops: [],
20478
+ path: []
19674
20479
  };
20480
+ try {
20481
+ return await enterFrame(ctx, contents, parent, node.nodeId);
20482
+ } catch (error) {
20483
+ if (isNotAFrame(error)) {
20484
+ return null;
20485
+ }
20486
+ throw error;
20487
+ }
19675
20488
  }
19676
- async function conversationReadBack(ctx, objectId, contents) {
19677
- const out = await ctx.cdp.send(
20489
+ async function conversationReadBack(ctx, node, contents) {
20490
+ const out = await sendIn(
20491
+ ctx,
19678
20492
  contents,
20493
+ node.sessionId,
19679
20494
  "Runtime.callFunctionOn",
19680
20495
  {
19681
- objectId,
20496
+ objectId: node.objectId,
19682
20497
  functionDeclaration: READ_VALUE_FUNCTION,
19683
20498
  returnByValue: true
19684
20499
  }
@@ -20661,6 +21476,13 @@ async function upload(ctx, params) {
20661
21476
  const paths = uploadPaths(params);
20662
21477
  const contents = record.view.webContents;
20663
21478
  const node = await resolveNode(ctx, record, selector);
21479
+ if (node.frame) {
21480
+ throw new BridgeCommandError(
21481
+ "element_not_found",
21482
+ `${selector} resolved to an element inside a frame${node.frame.origin ? ` (${node.frame.origin})` : ""}; upload into a frame is not supported yet, so nothing was attached`,
21483
+ { selector, frame_origin: node.frame.origin ?? "" }
21484
+ );
21485
+ }
20664
21486
  const accept = await acceptsOf(ctx, record, node.nodeId);
20665
21487
  await ctx.cdp.send(contents, "DOM.setFileInputFiles", {
20666
21488
  files: paths,