@agent-native/core 0.181.0 → 0.181.1-nightly-20260916144018

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 (58) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/clips/app/components/player/create-account-dialog.tsx +2 -1
  3. package/corpus/templates/clips/changelog/2026-09-15-google-sign-up-now-opens-reliably-in-embedded-browsers.md +6 -0
  4. package/corpus/templates/content/actions/resolve-preview-document-draft.ts +320 -60
  5. package/corpus/templates/content/app/components/editor/DocumentBlockFields.tsx +3 -1
  6. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +349 -104
  7. package/corpus/templates/content/app/components/editor/DocumentReconcileRecovery.tsx +109 -0
  8. package/corpus/templates/content/app/components/editor/PageDraftRecovery.tsx +31 -153
  9. package/corpus/templates/content/app/components/editor/RecoveryComparison.tsx +230 -0
  10. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +46 -19
  11. package/corpus/templates/content/app/components/editor/editor-draft-save.ts +13 -0
  12. package/corpus/templates/content/app/components/editor/useDocumentReconcileRecovery.ts +248 -0
  13. package/corpus/templates/content/app/components/sidebar/NotionButton.tsx +2 -1
  14. package/corpus/templates/content/app/i18n-data.ts +13 -0
  15. package/corpus/templates/content/changelog/2026-09-15-oauth-connections-now-open-reliably-in-embedded-browsers.md +6 -0
  16. package/corpus/templates/content/docs/solutions/2026-09-02-content-suggested-edits-parity-shape.md +84 -2
  17. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +27 -7
  18. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +45 -15
  19. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +35 -8
  20. package/corpus/templates/design/app/components/design/design-canvas/element-payload.ts +14 -0
  21. package/corpus/templates/design/app/pages/DesignEditor.tsx +18 -5
  22. package/corpus/templates/design/app/pages/design-editor/commands/record-pending-live-structure-edit.ts +9 -4
  23. package/corpus/templates/design/app/pages/design-editor/commands/screen-visual-structure-change.ts +3 -0
  24. package/corpus/templates/design/app/pages/design-editor/commands/start-sidebar-resize.ts +7 -2
  25. package/corpus/templates/design/app/pages/design-editor/commands/visual-structure-change.ts +2 -0
  26. package/corpus/templates/design/app/pages/design-editor/pending-edits.ts +4 -0
  27. package/corpus/templates/design/app/pages/design-editor/pending-structure-verification.ts +197 -147
  28. package/corpus/templates/design/changelog/2026-09-16-design-editor-agent-chat-uses-shared-conversation-tabs-and-s.md +6 -0
  29. package/corpus/templates/design/changelog/2026-09-16-visual-edits-stay-applied-after-replacements.md +5 -0
  30. package/corpus/templates/mail/app/components/GoogleConnectBanner.tsx +2 -1
  31. package/corpus/templates/mail/changelog/2026-09-15-google-connections-now-open-reliably-in-embedded-browsers.md +6 -0
  32. package/dist/client/AgentPanel.d.ts +1 -1
  33. package/dist/client/AgentPanel.js +4 -4
  34. package/dist/client/auth/AuthPage.js +6 -1
  35. package/dist/client/oauth-popup.d.ts +7 -0
  36. package/dist/client/oauth-popup.js +14 -0
  37. package/dist/client/ocean/OceanBackground.d.ts +3 -1
  38. package/dist/client/ocean/OceanBackground.js +4 -3
  39. package/dist/client/resources/mcp-integration-catalog.js +7 -3
  40. package/dist/client/settings/useBuilderStatus.js +4 -3
  41. package/dist/collab/routes.d.ts +1 -1
  42. package/dist/collab/struct-routes.d.ts +1 -1
  43. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  44. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  45. package/dist/notifications/routes.d.ts +3 -3
  46. package/dist/observability/routes.d.ts +3 -3
  47. package/dist/org/app-roles.d.ts +8 -0
  48. package/dist/org/app-roles.js +7 -0
  49. package/dist/org/index.d.ts +1 -1
  50. package/dist/org/index.js +1 -1
  51. package/dist/provider-api/actions/custom-provider-registration.d.ts +4 -4
  52. package/dist/secrets/routes.d.ts +6 -6
  53. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  54. package/dist/server/auth.js +3 -0
  55. package/dist/server/core-routes-plugin.d.ts +3 -0
  56. package/dist/server/core-routes-plugin.js +17 -0
  57. package/dist/server/realtime-token.d.ts +1 -1
  58. package/package.json +5 -4
@@ -2,9 +2,9 @@ import {
2
2
  buildCodeLayerProjection,
3
3
  type CodeLayerNode,
4
4
  } from "@shared/code-layer";
5
+ import { parse, type DefaultTreeAdapterMap } from "parse5";
5
6
 
6
7
  import {
7
- bridgeSourceIdForCodeLayerNode,
8
8
  collapsedElementText,
9
9
  resolveCodeLayerTargetFromBridge,
10
10
  } from "./code-layer-state";
@@ -20,6 +20,8 @@ export type RuntimeStructureVerificationFailure =
20
20
  | "ambiguous-subject"
21
21
  | "subject-still-present"
22
22
  | "missing-replacement"
23
+ | "missing-replacement-evidence"
24
+ | "replacement-context-changed"
23
25
  | "ambiguous-replacement"
24
26
  | "missing-anchor"
25
27
  | "ambiguous-anchor"
@@ -37,7 +39,6 @@ type RuntimeStructureNodeRole = "subject" | "replacement" | "anchor";
37
39
  interface RuntimeStructureNodeResolution {
38
40
  node?: CodeLayerNode;
39
41
  failure?: RuntimeStructureVerificationFailure;
40
- matchedBy?: "identity" | "selector" | "signature";
41
42
  }
42
43
 
43
44
  function runtimeStructureResolutionFailure(
@@ -85,16 +86,11 @@ function resolveRuntimeStructureNode(args: {
85
86
  args.sourceId ?? undefined,
86
87
  );
87
88
  if (direct.status === "resolved") {
88
- const matchedBy =
89
- args.sourceId &&
90
- bridgeSourceIdForCodeLayerNode(direct.node) === args.sourceId
91
- ? "identity"
92
- : "selector";
93
89
  if (
94
90
  !args.signature ||
95
91
  runtimeStructureNodeMatchesSignature(direct.node, args.signature)
96
92
  ) {
97
- return { node: direct.node, matchedBy };
93
+ return { node: direct.node };
98
94
  }
99
95
  // A live identity is stronger evidence than a stale content signature.
100
96
  // Do not let an unrelated sibling with the old signature validate this
@@ -108,75 +104,31 @@ function resolveRuntimeStructureNode(args: {
108
104
  : "missing-anchor",
109
105
  };
110
106
  }
111
-
112
- if (!args.signature) {
107
+ if (direct.status === "ambiguous") {
113
108
  return {
114
- failure: runtimeStructureResolutionFailure(
115
- direct.status === "ambiguous" ? "ambiguous" : "absent",
116
- args.role,
117
- ),
109
+ failure: runtimeStructureResolutionFailure("ambiguous", args.role),
118
110
  };
119
111
  }
120
112
 
121
- const matches = args.projection.nodes.filter((node) =>
122
- runtimeStructureNodeMatchesSignature(node, args.signature!),
123
- );
124
- if (matches.length === 1) {
125
- return { node: matches[0], matchedBy: "signature" };
126
- }
127
- return {
128
- failure: runtimeStructureResolutionFailure(
129
- matches.length > 1 ? "ambiguous" : "absent",
130
- args.role,
131
- ),
132
- };
133
- }
134
-
135
- function resolveRuntimeStructureNodeByIdentity(args: {
136
- projection: { nodes: CodeLayerNode[] };
137
- selector?: string;
138
- sourceId?: string | null;
139
- role: RuntimeStructureNodeRole;
140
- }): RuntimeStructureNodeResolution {
141
- const direct = resolveCodeLayerTargetFromBridge(
142
- args.projection,
143
- args.selector,
144
- args.sourceId ?? undefined,
145
- );
146
- if (direct.status === "resolved") {
147
- return {
148
- node: direct.node,
149
- matchedBy:
150
- args.sourceId &&
151
- bridgeSourceIdForCodeLayerNode(direct.node) === args.sourceId
152
- ? "identity"
153
- : "selector",
154
- };
155
- }
156
- return {
157
- failure: runtimeStructureResolutionFailure(
158
- direct.status === "ambiguous" ? "ambiguous" : "absent",
159
- args.role,
160
- ),
161
- };
113
+ return resolveRuntimeStructureNodeBySignature(args);
162
114
  }
163
115
 
164
116
  function resolveRuntimeStructureNodeBySignature(args: {
165
117
  projection: { nodes: CodeLayerNode[] };
166
118
  signature?: RuntimeStructureNodeSignature;
167
119
  role: RuntimeStructureNodeRole;
168
- excludedNodeIds?: ReadonlySet<string>;
120
+ excludedNode?: CodeLayerNode;
169
121
  }): RuntimeStructureNodeResolution {
170
122
  if (!args.signature) {
171
123
  return { failure: runtimeStructureResolutionFailure("absent", args.role) };
172
124
  }
173
125
  const matches = args.projection.nodes.filter(
174
126
  (node) =>
175
- !args.excludedNodeIds?.has(node.id) &&
127
+ node !== args.excludedNode &&
176
128
  runtimeStructureNodeMatchesSignature(node, args.signature!),
177
129
  );
178
130
  if (matches.length === 1) {
179
- return { node: matches[0], matchedBy: "signature" };
131
+ return { node: matches[0] };
180
132
  }
181
133
  return {
182
134
  failure: runtimeStructureResolutionFailure(
@@ -186,18 +138,142 @@ function resolveRuntimeStructureNodeBySignature(args: {
186
138
  };
187
139
  }
188
140
 
189
- function resolveRuntimeStructureNodeForPresence(args: {
190
- projection: { nodes: CodeLayerNode[] };
191
- selector?: string;
192
- sourceId?: string | null;
193
- signature?: RuntimeStructureNodeSignature;
194
- role: RuntimeStructureNodeRole;
195
- }): RuntimeStructureNodeResolution {
196
- const identity = resolveRuntimeStructureNodeByIdentity(args);
197
- if (identity.node || identity.failure?.startsWith("ambiguous")) {
198
- return identity;
141
+ function verifyRuntimeStructureSubjectAbsent(
142
+ projection: { nodes: CodeLayerNode[] },
143
+ edit: PendingLiveStructureEdit,
144
+ replacement?: CodeLayerNode,
145
+ ): RuntimeStructureVerificationResult {
146
+ // Absence must inspect the old identity alone: a positional selector may
147
+ // now point at the replacement or an unrelated sibling.
148
+ const subject = resolveCodeLayerTargetFromBridge(
149
+ projection,
150
+ edit.sourceId ? undefined : edit.selector,
151
+ edit.sourceId ?? undefined,
152
+ );
153
+ if (subject.status === "ambiguous") {
154
+ return { ok: false, failure: "ambiguous-subject" };
199
155
  }
200
- return resolveRuntimeStructureNodeBySignature(args);
156
+ if (
157
+ subject.status === "resolved" &&
158
+ (edit.sourceId ||
159
+ replacement === undefined ||
160
+ (subject.node === replacement &&
161
+ (!edit.subjectSignature ||
162
+ runtimeStructureNodeMatchesSignature(
163
+ subject.node,
164
+ edit.subjectSignature,
165
+ ))))
166
+ ) {
167
+ return { ok: false, failure: "subject-still-present" };
168
+ }
169
+
170
+ // Signature-only fallback cannot distinguish unchanged old content. A
171
+ // unique identity or selector independently resolves the replacement;
172
+ // captured document evidence still has to prove the resulting structure.
173
+ const replacementTarget = resolveCodeLayerTargetFromBridge(
174
+ projection,
175
+ edit.replacementSourceId ? undefined : edit.replacementSelector,
176
+ edit.replacementSourceId ?? undefined,
177
+ );
178
+ const signature = resolveRuntimeStructureNodeBySignature({
179
+ projection,
180
+ signature: edit.subjectSignature,
181
+ role: "subject",
182
+ excludedNode:
183
+ replacementTarget.status === "resolved" &&
184
+ replacementTarget.node === replacement
185
+ ? replacement
186
+ : undefined,
187
+ });
188
+ if (signature.failure === "ambiguous-subject") {
189
+ return { ok: false, failure: signature.failure };
190
+ }
191
+ return signature.node
192
+ ? { ok: false, failure: "subject-still-present" }
193
+ : { ok: true };
194
+ }
195
+
196
+ // These attributes are injected by serializeRuntimeLayerSnapshot, not authored
197
+ // semantic state. Keep this list exact so other data-* attributes remain evidence.
198
+ const runtimeSnapshotMetadataAttributes = new Set([
199
+ "style",
200
+ "data-agent-native-node-id",
201
+ "data-agent-native-node-rewrite-proposal",
202
+ "data-agent-native-group-runtime-state",
203
+ "data-agent-native-runtime-hidden",
204
+ "data-agent-native-runtime-locked",
205
+ "data-agent-native-previous-display",
206
+ "data-agent-native-text-editing",
207
+ "data-an-runtime-layer-snapshot",
208
+ "data-an-pending-node-id",
209
+ "data-an-state-preview",
210
+ "data-an-state-preview-key",
211
+ "data-an-vector-logical-width",
212
+ "data-an-vector-stroke-defs",
213
+ "data-an-vector-stroke-geometry",
214
+ "data-an-vector-stroke-original-overflow",
215
+ "data-an-vector-stroke-original-overflow-priority",
216
+ "data-an-vector-stroke-overlay",
217
+ "data-an-vector-stroke-position",
218
+ "data-source-framework",
219
+ "data-source-file",
220
+ "data-source-line",
221
+ "data-source-method",
222
+ "data-source-column",
223
+ "data-component-name",
224
+ "data-source-owner-file",
225
+ "data-source-owner-line",
226
+ "data-source-owner-column",
227
+ "data-source-owner-component",
228
+ "data-source-owner-method",
229
+ "data-source-owner-key",
230
+ "data-source-unavailable",
231
+ ]);
232
+
233
+ /** Full visual-tree evidence: sibling order, nesting, content and semantic
234
+ * attributes must match; runtime identities and computed styles may change. */
235
+ export function runtimeStructureSnapshotSignature(
236
+ snapshotHtml: string,
237
+ ): string {
238
+ function structure(node: DefaultTreeAdapterMap["node"]): unknown[] {
239
+ if ("value" in node && node.nodeName === "#text") {
240
+ const text = collapsedElementText(node.value);
241
+ return text ? [text] : [];
242
+ }
243
+ if ("tagName" in node && node.tagName === "head") return [];
244
+ const children =
245
+ "childNodes" in node ? node.childNodes.flatMap(structure) : [];
246
+ if (!("tagName" in node) || node.tagName === "html") return children;
247
+ return [
248
+ [
249
+ node.namespaceURI,
250
+ node.tagName,
251
+ normalizeRuntimeStructureClasses(
252
+ (
253
+ node.attrs.find((attribute) => attribute.name === "class")?.value ??
254
+ ""
255
+ ).split(/\s+/),
256
+ ),
257
+ node.attrs
258
+ .filter(
259
+ (attribute) =>
260
+ attribute.namespace ||
261
+ (attribute.name !== "class" &&
262
+ !runtimeSnapshotMetadataAttributes.has(attribute.name)),
263
+ )
264
+ .map((attribute) =>
265
+ JSON.stringify([
266
+ attribute.namespace ?? "",
267
+ attribute.name,
268
+ attribute.value,
269
+ ]),
270
+ )
271
+ .sort(),
272
+ children,
273
+ ],
274
+ ];
275
+ }
276
+ return JSON.stringify(structure(parse(snapshotHtml)));
201
277
  }
202
278
 
203
279
  function runtimeStructureNodeForNoOp(
@@ -229,9 +305,9 @@ export function verifyPendingStructureRuntime(
229
305
  ): RuntimeStructureVerificationResult {
230
306
  const projection = buildCodeLayerProjection(snapshotHtml);
231
307
  if (edit.replaced) {
232
- const replacementResolution = resolveRuntimeStructureNodeForPresence({
308
+ const replacementResolution = resolveRuntimeStructureNode({
233
309
  projection,
234
- selector: edit.replacementSelector,
310
+ selector: edit.replacementSourceId ? undefined : edit.replacementSelector,
235
311
  sourceId: edit.replacementSourceId,
236
312
  signature: edit.replacementSignature,
237
313
  role: "replacement",
@@ -243,83 +319,22 @@ export function verifyPendingStructureRuntime(
243
319
  };
244
320
  }
245
321
 
246
- const subjectIdentityResolution = resolveRuntimeStructureNodeByIdentity({
322
+ const absence = verifyRuntimeStructureSubjectAbsent(
247
323
  projection,
248
- selector: edit.selector,
249
- sourceId: edit.sourceId,
250
- role: "subject",
251
- });
252
- if (subjectIdentityResolution.failure === "ambiguous-subject") {
253
- return { ok: false, failure: "ambiguous-subject" };
254
- }
255
- if (subjectIdentityResolution.node) {
256
- const sameNode =
257
- subjectIdentityResolution.node.id === replacementResolution.node.id;
258
- const replacementIsProvenByPosition =
259
- sameNode &&
260
- subjectIdentityResolution.matchedBy === "selector" &&
261
- replacementResolution.matchedBy === "selector";
262
- if (!replacementIsProvenByPosition) {
263
- return { ok: false, failure: "subject-still-present" };
264
- }
265
- }
266
-
267
- const subjectMatches = projection.nodes.filter((node) =>
268
- edit.subjectSignature
269
- ? runtimeStructureNodeMatchesSignature(node, edit.subjectSignature)
270
- : false,
324
+ edit,
325
+ replacementResolution.node,
271
326
  );
272
- const replacementIsUniquelyLocated =
273
- replacementResolution.matchedBy === "identity" ||
274
- replacementResolution.matchedBy === "selector";
275
- const remainingSubjectMatches = replacementIsUniquelyLocated
276
- ? subjectMatches.filter(
277
- (node) => node.id !== replacementResolution.node!.id,
278
- )
279
- : subjectMatches;
280
- const replacementIsProvenByPosition =
281
- subjectIdentityResolution.node?.id === replacementResolution.node.id &&
282
- subjectIdentityResolution.matchedBy === "selector" &&
283
- replacementResolution.matchedBy === "selector";
284
- if (!replacementIsProvenByPosition) {
285
- if (remainingSubjectMatches.length > 1) {
286
- return { ok: false, failure: "ambiguous-subject" };
287
- }
288
- if (remainingSubjectMatches.length === 1) {
289
- return { ok: false, failure: "subject-still-present" };
290
- }
327
+ if (!absence.ok) return absence;
328
+ if (!edit.replacementSnapshotSignature) {
329
+ return { ok: false, failure: "missing-replacement-evidence" };
291
330
  }
292
- return { ok: true };
331
+ return runtimeStructureSnapshotSignature(snapshotHtml) ===
332
+ edit.replacementSnapshotSignature
333
+ ? { ok: true }
334
+ : { ok: false, failure: "replacement-context-changed" };
293
335
  }
294
- // A removal proves itself by ABSENCE. Running it through the anchor/order
295
- // checks below would report "missing-subject" for the exact outcome it
296
- // asked for, and the apply flow would sit in awaiting-runtime until it
297
- // timed out on a source write that actually succeeded.
298
336
  if (edit.removed) {
299
- const subjectIdentityResolution = resolveRuntimeStructureNodeByIdentity({
300
- projection,
301
- selector: edit.selector,
302
- sourceId: edit.sourceId,
303
- role: "subject",
304
- });
305
- if (subjectIdentityResolution.failure === "ambiguous-subject") {
306
- return { ok: false, failure: "ambiguous-subject" };
307
- }
308
- if (subjectIdentityResolution.node) {
309
- return { ok: false, failure: "subject-still-present" };
310
- }
311
- const subjectSignatureResolution = resolveRuntimeStructureNodeBySignature({
312
- projection,
313
- signature: edit.subjectSignature,
314
- role: "subject",
315
- });
316
- if (subjectSignatureResolution.failure === "ambiguous-subject") {
317
- return { ok: false, failure: "ambiguous-subject" };
318
- }
319
- if (subjectSignatureResolution.node) {
320
- return { ok: false, failure: "subject-still-present" };
321
- }
322
- return { ok: true };
337
+ return verifyRuntimeStructureSubjectAbsent(projection, edit);
323
338
  }
324
339
 
325
340
  const subjectResolution = resolveRuntimeStructureNode({
@@ -433,14 +448,30 @@ export function isPendingStructureDropNoOp(
433
448
  );
434
449
  }
435
450
 
451
+ function replacementSnapshotsByScreen(
452
+ edits: readonly PendingLiveStructureEdit[],
453
+ ) {
454
+ // Later replacement gestures include the earlier optimistic replacements.
455
+ // Verify the composed screen without discarding each edit's identity checks.
456
+ return new Map(
457
+ edits
458
+ .filter((edit) => edit.replaced)
459
+ .map((edit) => [edit.screenId, edit.replacementSnapshotSignature]),
460
+ );
461
+ }
462
+
436
463
  export function verifyPendingStructuresRuntime(
437
464
  snapshots: Record<string, { html: string } | undefined>,
438
465
  edits: readonly PendingLiveStructureEdit[],
439
466
  ): RuntimeStructureVerificationResult {
467
+ const replacementSnapshots = replacementSnapshotsByScreen(edits);
440
468
  for (const edit of edits) {
441
469
  const snapshot = snapshots[edit.screenId];
442
470
  if (!snapshot) return { ok: false, failure: "missing-subject" };
443
- const result = verifyPendingStructureRuntime(snapshot.html, edit);
471
+ const result = verifyPendingStructureRuntime(snapshot.html, {
472
+ ...edit,
473
+ replacementSnapshotSignature: replacementSnapshots.get(edit.screenId),
474
+ });
444
475
  if (!result.ok) return result;
445
476
  }
446
477
  return { ok: true };
@@ -453,11 +484,30 @@ export function partitionPendingStructuresRuntime(
453
484
  verified: PendingLiveStructureEdit[];
454
485
  remaining: PendingLiveStructureEdit[];
455
486
  } {
487
+ const replacementSnapshots = replacementSnapshotsByScreen(edits);
488
+ const results = edits.map((edit) => {
489
+ const snapshot = snapshots[edit.screenId];
490
+ return {
491
+ edit,
492
+ ok: Boolean(
493
+ snapshot &&
494
+ verifyPendingStructureRuntime(snapshot.html, {
495
+ ...edit,
496
+ replacementSnapshotSignature: replacementSnapshots.get(edit.screenId),
497
+ }).ok,
498
+ ),
499
+ };
500
+ });
501
+ // Keep the shared evidence until every replacement using it verifies.
502
+ const blockedScreens = new Set(
503
+ results
504
+ .filter(({ edit, ok }) => edit.replaced && !ok)
505
+ .map(({ edit }) => edit.screenId),
506
+ );
456
507
  const verified: PendingLiveStructureEdit[] = [];
457
508
  const remaining: PendingLiveStructureEdit[] = [];
458
- for (const edit of edits) {
459
- const snapshot = snapshots[edit.screenId];
460
- if (snapshot && verifyPendingStructureRuntime(snapshot.html, edit).ok) {
509
+ for (const { edit, ok } of results) {
510
+ if (ok && (!edit.replaced || !blockedScreens.has(edit.screenId))) {
461
511
  verified.push(edit);
462
512
  } else {
463
513
  remaining.push(edit);
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-09-16
4
+ ---
5
+
6
+ Design editor agent chat uses shared conversation tabs and stays usable at narrow widths
@@ -0,0 +1,5 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-09-16
4
+ ---
5
+ Visual edits stay applied when the page structure shifts after a replacement.
@@ -6,6 +6,7 @@ import {
6
6
  } from "@agent-native/core/client/host";
7
7
  import { useT } from "@agent-native/core/client/i18n";
8
8
  import { startWorkspaceProviderOAuth } from "@agent-native/core/client/integrations";
9
+ import { openOAuthPopup } from "@agent-native/core/client/oauth-popup";
9
10
  import {
10
11
  IconMail,
11
12
  IconX,
@@ -178,7 +179,7 @@ export function GoogleConnectBanner({
178
179
  desktop: "1",
179
180
  flow_id: flowId,
180
181
  });
181
- const popup = window.open("", "_blank");
182
+ const popup = openOAuthPopup();
182
183
  if (!popup) {
183
184
  setDesktopAuthIssue({
184
185
  code: "popup_blocked",
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-09-15
4
+ ---
5
+
6
+ Google connections now open reliably in embedded browsers
@@ -55,7 +55,7 @@ export declare function shouldShowAgentPanelSidebarChatTabs(tabs: MultiTabAssist
55
55
  export declare function shouldShowAgentPanelPageNewChatButton(tabs: MultiTabAssistantChatHeaderProps["tabs"], activeTabId: string, activeTabMessageCount: number): boolean;
56
56
  export declare function shouldShowAgentPanelPageHeader(tabs: MultiTabAssistantChatHeaderProps["tabs"], activeTabId: string, activeTabMessageCount: number): boolean;
57
57
  export declare function shouldShowAgentPanelCliTabBar(cliTabs: string[]): boolean;
58
- export declare function shouldShowAgentPanelModeButtons(isSidebar: boolean): boolean;
58
+ export declare function shouldShowAgentPanelModeButtons(isSidebar: boolean, chatOnly?: boolean): boolean;
59
59
  export declare function shouldShowAgentPanelFullViewAction(agentPageHref: string | undefined, mode: PanelMode, isSidebar?: boolean, currentPath?: string): boolean;
60
60
  export declare function resolveAgentPanelFullViewAction(agentPageHref: string | undefined, onFullViewRequest: (() => void) | undefined, mode: PanelMode, isSidebar?: boolean, currentPath?: string): {
61
61
  readonly kind: "callback";
@@ -342,8 +342,8 @@ export function shouldShowAgentPanelPageHeader(tabs, activeTabId, activeTabMessa
342
342
  export function shouldShowAgentPanelCliTabBar(cliTabs) {
343
343
  return cliTabs.length > 1;
344
344
  }
345
- export function shouldShowAgentPanelModeButtons(isSidebar) {
346
- return !isSidebar;
345
+ export function shouldShowAgentPanelModeButtons(isSidebar, chatOnly = false) {
346
+ return !isSidebar && !chatOnly;
347
347
  }
348
348
  export function shouldShowAgentPanelFullViewAction(agentPageHref, mode, isSidebar = false, currentPath) {
349
349
  return (Boolean(agentPageHref) &&
@@ -1018,7 +1018,7 @@ function AgentPanelInner({ defaultMode = "chat", className, style, apiUrl, empty
1018
1018
  borderRadius: "0 6px 6px 0",
1019
1019
  background: "linear-gradient(to right, transparent, hsl(var(--accent)) 40%)",
1020
1020
  }, children: _jsx(IconX, { size: 10 }) })] }, tab.id));
1021
- }) })) : shouldShowAgentPanelModeButtons(Boolean(onCollapse)) ? (renderModeButtons(mode)) : null }), _jsx("div", { className: "flex items-center gap-0.5", children: renderHeaderActions({
1021
+ }) })) : shouldShowAgentPanelModeButtons(Boolean(onCollapse), chatOnly) ? (renderModeButtons(mode)) : null }), _jsx("div", { className: "flex items-center gap-0.5", children: renderHeaderActions({
1022
1022
  activeChatSessionId: activeTabId,
1023
1023
  activeTabId,
1024
1024
  activeTabMessageCount,
@@ -1119,7 +1119,7 @@ function AgentPanelInner({ defaultMode = "chat", className, style, apiUrl, empty
1119
1119
  switchMode,
1120
1120
  t,
1121
1121
  ]);
1122
- return (_jsxs(ThinkingDisplayProvider, { value: assistantChatProps.thinkingDisplay, children: [_jsx(AgentPanelSettingsNavigation, { onOpenSettings: onOpenSettings }), _jsxs("div", { className: cn("agent-panel-root agent-kit-density flex flex-1 flex-col min-h-0 h-full antialiased", className), style: {
1122
+ return (_jsxs(ThinkingDisplayProvider, { value: assistantChatProps.thinkingDisplay, children: [_jsx(AgentPanelSettingsNavigation, { onOpenSettings: onOpenSettings }), _jsxs("div", { className: cn("agent-panel-root agent-kit-density flex flex-1 flex-col min-h-0 min-w-0 h-full antialiased", className), style: {
1123
1123
  ...AGENT_PANEL_ROOT_STYLE,
1124
1124
  ...style,
1125
1125
  // The chat view-transition container otherwise traps fixed onboarding
@@ -7,6 +7,7 @@ import { AUTH_SIGNUP_INVITE_ONLY_CODE } from "../../shared/auth-copy.js";
7
7
  import { isQaTestEmail } from "../../shared/qa-test-email.js";
8
8
  import { signInJourney, } from "../../shared/sign-in-journey.js";
9
9
  import { isSyntheticTrafficValue } from "../../shared/test-traffic.js";
10
+ import { openOAuthPopup } from "../oauth-popup.js";
10
11
  import { OceanBackground } from "../ocean/OceanBackground.js";
11
12
  const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
12
13
  const TAB_STORAGE_KEY = "an.onboarding.tab";
@@ -1180,7 +1181,10 @@ export function AuthPage(props) {
1180
1181
  // redirect and hit that same 403 the moment the popup failed to open.
1181
1182
  const redirectFallbackUnsafe = isInFrame();
1182
1183
  try {
1183
- popup = window.open("", "_blank", "width=640,height=760");
1184
+ popup = openOAuthPopup({
1185
+ initialUrl: new URL(`${runtimeAppBasePath}/_agent-native/oauth/popup`, window.location.origin).href,
1186
+ features: "width=640,height=760",
1187
+ });
1184
1188
  if (!popup) {
1185
1189
  if (redirectFallbackUnsafe) {
1186
1190
  setGoogleBusy(false);
@@ -1277,6 +1281,7 @@ export function AuthPage(props) {
1277
1281
  googleViaIdentitySso,
1278
1282
  resolveGoogleFlow,
1279
1283
  resumeHref,
1284
+ runtimeAppBasePath,
1280
1285
  setNotice,
1281
1286
  showGoogle,
1282
1287
  startOAuthExchange,
@@ -0,0 +1,7 @@
1
+ export interface OpenOAuthPopupOptions {
2
+ /** HTTP(S) URL loaded synchronously while the user gesture is active. */
3
+ initialUrl?: string;
4
+ features?: string;
5
+ }
6
+ export declare function oauthPopupWaitingUrl(): string;
7
+ export declare function openOAuthPopup({ initialUrl, features, }?: OpenOAuthPopupOptions): Window | null;
@@ -0,0 +1,14 @@
1
+ import { agentNativePath } from "./api-path.js";
2
+ export function oauthPopupWaitingUrl() {
3
+ if (typeof window === "undefined")
4
+ return "";
5
+ return new URL(agentNativePath("/_agent-native/oauth/popup"), window.location.href).href;
6
+ }
7
+ export function openOAuthPopup({ initialUrl, features, } = {}) {
8
+ if (typeof window === "undefined")
9
+ return null;
10
+ const url = new URL(initialUrl || oauthPopupWaitingUrl(), window.location.href);
11
+ if (url.protocol !== "http:" && url.protocol !== "https:")
12
+ return null;
13
+ return window.open(url.href, "_blank", features);
14
+ }
@@ -1,4 +1,6 @@
1
+ /** @jsxRuntime classic */
2
+ import * as React from "react";
1
3
  export declare function OceanBackground({ className, frameRate, }: {
2
4
  className: string;
3
5
  frameRate?: number;
4
- }): import("react").JSX.Element;
6
+ }): React.JSX.Element;
@@ -1,4 +1,5 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ /** @jsxRuntime classic */
2
+ import * as React from "react";
2
3
  import { useCallback, useEffect, useState } from "react";
3
4
  import { StarfieldBackground } from "../StarfieldBackground.js";
4
5
  import { HeroOceanBackground } from "./hero-ocean-background.js";
@@ -41,7 +42,7 @@ export function OceanBackground({ className, frameRate = 30, }) {
41
42
  }, []);
42
43
  const handleOceanError = useCallback(() => setBackground("fallback"), []);
43
44
  if (background === "ocean") {
44
- return (_jsx(HeroOceanBackground, { className: className, frameRate: frameRate, onError: handleOceanError }));
45
+ return (React.createElement(HeroOceanBackground, { className: className, frameRate: frameRate, onError: handleOceanError }));
45
46
  }
46
- return _jsx(StarfieldBackground, { className: className, frameRate: frameRate });
47
+ return React.createElement(StarfieldBackground, { className: className, frameRate: frameRate });
47
48
  }
@@ -1,6 +1,8 @@
1
1
  import { normalizeMcpIntegrationsConfig, } from "../../shared/mcp-integration-config.js";
2
2
  import { hostMatches, MCP_LINK_HOSTS, normalizeMcpUrl, } from "../../shared/mcp-provider-hosts.js";
3
3
  import { mergeDefinitionsById } from "../../shared/merge-by-id.js";
4
+ import { agentNativePath } from "../api-path.js";
5
+ import { openOAuthPopup } from "../oauth-popup.js";
4
6
  import { markMcpConnectionPending } from "./mcp-connection-refresh.js";
5
7
  import { mcpIntegrationLogo } from "./mcp-integration-logos.js";
6
8
  export const DEFAULT_MCP_INTEGRATIONS = [
@@ -877,17 +879,19 @@ export function buildMcpOAuthStartUrl({ name, url, description, scope, returnUrl
877
879
  ? { tracking_integration_id: trackingIntegrationId }
878
880
  : {}),
879
881
  });
880
- return `/_agent-native/mcp/servers/oauth/start?${params.toString()}`;
882
+ return `${agentNativePath("/_agent-native/mcp/servers/oauth/start")}?${params.toString()}`;
881
883
  }
882
884
  export function navigateToMcpOAuthStart(url) {
883
885
  if (typeof window === "undefined")
884
886
  return false;
885
887
  try {
886
- const popup = window.open("about:blank", "_blank");
888
+ const popup = openOAuthPopup({
889
+ initialUrl: url,
890
+ features: "width=640,height=760",
891
+ });
887
892
  if (!popup)
888
893
  return false;
889
894
  popup.opener = null;
890
- popup.location.replace(url);
891
895
  // The callback redirects the popup, not this window, so this marker is the
892
896
  // only thing that tells the opener its cached server list is now suspect.
893
897
  markMcpConnectionPending();
@@ -4,6 +4,7 @@ import { trackEvent } from "../analytics.js";
4
4
  import { agentNativePath } from "../api-path.js";
5
5
  import { getCallbackOrigin } from "../frame.js";
6
6
  import { openMcpAppHostLink } from "../mcp-app-host.js";
7
+ import { oauthPopupWaitingUrl } from "../oauth-popup.js";
7
8
  import { scheduleAfterPaint } from "../use-after-paint.js";
8
9
  import { usePollLoop } from "../use-poll-loop.js";
9
10
  /**
@@ -677,8 +678,8 @@ export function useBuilderConnectFlow(opts = {}) {
677
678
  ? statusConnectUrl
678
679
  : null;
679
680
  // popupUrl props and statusConnectUrl are signed URLs minted before the
680
- // click. In web browsers, always refresh inside an about:blank popup so a
681
- // server/package restart cannot leave the user with a stale signed state.
681
+ // click. In web browsers, always refresh inside the inert HTTP waiting
682
+ // page so a server/package restart cannot leave the user with stale state.
682
683
  // Desktop keeps the direct path because the Electron shell owns the popup.
683
684
  const signedPropUrl = hasSignedConnectToken(popupUrl) ? popupUrl : null;
684
685
  const fallbackUrl = new URL(agentNativePath("/_agent-native/builder/connect"), origin).href;
@@ -710,7 +711,7 @@ export function useBuilderConnectFlow(opts = {}) {
710
711
  }
711
712
  else {
712
713
  const opened = openBuilderConnectPopup({
713
- url: "about:blank",
714
+ url: oauthPopupWaitingUrl(),
714
715
  source: clickTrackingSource,
715
716
  flow: clickTrackingFlow,
716
717
  features: "width=600,height=700",