@agent-native/core 0.182.1 → 0.182.2-nightly-20260917225811

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 (119) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/clips/app/root.tsx +59 -10
  3. package/corpus/templates/clips/changelog/2026-09-17-desktop-popover-closes-when-clicking-away.md +6 -0
  4. package/corpus/templates/clips/changelog/2026-09-17-self-hosted-extension-sign-in.md +6 -0
  5. package/corpus/templates/clips/chrome-extension/src/background.ts +45 -25
  6. package/corpus/templates/clips/chrome-extension/src/content-script.ts +43 -0
  7. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +55 -6
  8. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +107 -19
  9. package/corpus/templates/design/.generated/bridge/hit-test.generated.ts +1 -0
  10. package/corpus/templates/design/actions/add-localhost-screens.ts +54 -22
  11. package/corpus/templates/design/actions/apply-component-prop-edit.ts +174 -14
  12. package/corpus/templates/design/actions/apply-visual-edit.ts +3 -1
  13. package/corpus/templates/design/actions/create-component.ts +173 -12
  14. package/corpus/templates/design/actions/create-design-from-template.ts +50 -35
  15. package/corpus/templates/design/actions/create-design-version.ts +21 -0
  16. package/corpus/templates/design/actions/create-file.ts +35 -33
  17. package/corpus/templates/design/actions/delete-design.ts +29 -28
  18. package/corpus/templates/design/actions/delete-file.ts +154 -23
  19. package/corpus/templates/design/actions/generate-design.ts +28 -16
  20. package/corpus/templates/design/actions/get-component-details.ts +272 -33
  21. package/corpus/templates/design/actions/get-design-version.ts +9 -1
  22. package/corpus/templates/design/actions/go-to-main-component.ts +37 -35
  23. package/corpus/templates/design/actions/index-components.ts +241 -126
  24. package/corpus/templates/design/actions/migrate-board-objects-to-file.ts +12 -9
  25. package/corpus/templates/design/actions/present-design-variants.ts +68 -72
  26. package/corpus/templates/design/actions/rename-component.ts +294 -0
  27. package/corpus/templates/design/actions/rename-screen.ts +10 -10
  28. package/corpus/templates/design/actions/update-file.ts +327 -330
  29. package/corpus/templates/design/actions/update-screen-source.ts +2 -43
  30. package/corpus/templates/design/app/components/design/EditPanel.tsx +17 -1
  31. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +228 -24
  32. package/corpus/templates/design/app/components/design/ResponsiveInteractBar.tsx +17 -1
  33. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +151 -32
  34. package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +4 -0
  35. package/corpus/templates/design/app/components/design/canvas-primitive-style.ts +1 -1
  36. package/corpus/templates/design/app/components/design/code-workbench/CodeWorkbench.tsx +8 -1
  37. package/corpus/templates/design/app/components/design/code-workbench/explorer/FileTree.tsx +55 -33
  38. package/corpus/templates/design/app/components/design/code-workbench/workspace/create-providers.ts +2 -0
  39. package/corpus/templates/design/app/components/design/code-workbench/workspace/inline-provider.ts +4 -2
  40. package/corpus/templates/design/app/components/design/edit-panel/component-section.tsx +273 -60
  41. package/corpus/templates/design/app/components/design/edit-panel/element-classification.ts +9 -2
  42. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +57 -9
  43. package/corpus/templates/design/app/components/design/multi-screen/primitive-drop-target.ts +13 -0
  44. package/corpus/templates/design/app/components/design/multi-screen/types.ts +14 -0
  45. package/corpus/templates/design/app/components/design/types.ts +9 -1
  46. package/corpus/templates/design/app/hooks/use-design-versions.ts +3 -2
  47. package/corpus/templates/design/app/i18n-breakpoints.ts +0 -110
  48. package/corpus/templates/design/app/i18n-data.ts +0 -10
  49. package/corpus/templates/design/app/pages/DesignEditor.tsx +669 -161
  50. package/corpus/templates/design/app/pages/design-editor/clone-and-pen-edit.ts +20 -0
  51. package/corpus/templates/design/app/pages/design-editor/code-layer-state.ts +2 -0
  52. package/corpus/templates/design/app/pages/design-editor/commands/apply-design-editor-command.ts +21 -3
  53. package/corpus/templates/design/app/pages/design-editor/commands/apply-file-content-update.ts +2 -0
  54. package/corpus/templates/design/app/pages/design-editor/commands/create-component.ts +44 -3
  55. package/corpus/templates/design/app/pages/design-editor/commands/delete-files.ts +69 -47
  56. package/corpus/templates/design/app/pages/design-editor/commands/duplicate-screen.ts +157 -52
  57. package/corpus/templates/design/app/pages/design-editor/commands/duplicate-selection.ts +76 -1
  58. package/corpus/templates/design/app/pages/design-editor/commands/redo.ts +152 -44
  59. package/corpus/templates/design/app/pages/design-editor/commands/save-file-content.ts +131 -2
  60. package/corpus/templates/design/app/pages/design-editor/commands/screen-visual-duplicate-change.ts +5 -1
  61. package/corpus/templates/design/app/pages/design-editor/commands/undo.ts +26 -1
  62. package/corpus/templates/design/app/pages/design-editor/commands/visual-duplicate-change.ts +1 -0
  63. package/corpus/templates/design/app/pages/design-editor/editor-helpers.ts +58 -8
  64. package/corpus/templates/design/app/pages/design-editor/editor-state.ts +63 -2
  65. package/corpus/templates/design/app/pages/design-editor/history.ts +3 -0
  66. package/corpus/templates/design/app/pages/design-editor/nudge-intent.ts +74 -22
  67. package/corpus/templates/design/app/pages/design-editor/source-publication.ts +14 -2
  68. package/corpus/templates/design/changelog/2026-09-16-design-components-retain-linked-identities-when-edited-and-d.md +6 -0
  69. package/corpus/templates/design/changelog/2026-09-16-fix-design-nested-layer-dragging-and-screen-duplication-pari.md +6 -0
  70. package/corpus/templates/design/changelog/2026-09-17-design-drag-and-drop-keeps-duplicates-cross-screen-moves-and.md +6 -0
  71. package/corpus/templates/design/changelog/2026-09-17-design-preserves-reusable-component-identity-and-props-acros.md +6 -0
  72. package/corpus/templates/design/changelog/2026-09-17-open-vector-paths-no-longer-show-fills-and-interact-dimensio.md +6 -0
  73. package/corpus/templates/design/changelog/2026-09-17-screen-deletion-is-immediate-and-undoable-with-history-that-.md +6 -0
  74. package/corpus/templates/design/e2e/fixtures/design-component-react/index.html +98 -0
  75. package/corpus/templates/design/e2e/fixtures/design-component-react/inline-component.html +65 -0
  76. package/corpus/templates/design/e2e/helpers.ts +6 -1
  77. package/corpus/templates/design/server/lib/design-data-mutation.ts +9 -5
  78. package/corpus/templates/design/server/lib/design-snapshot.ts +28 -16
  79. package/corpus/templates/design/server/lib/design-versions.ts +208 -130
  80. package/corpus/templates/design/server/lib/fusion-screens.ts +46 -20
  81. package/corpus/templates/design/server/lib/import-design-files.ts +5 -1
  82. package/corpus/templates/design/server/lib/localhost-connection.ts +54 -0
  83. package/corpus/templates/design/server/source-workspace.ts +412 -356
  84. package/corpus/templates/design/shared/component-library.ts +36 -6
  85. package/corpus/templates/design/shared/component-model.ts +12 -2
  86. package/corpus/templates/design/shared/local-jsx-visual-edit.ts +231 -5
  87. package/corpus/templates/design/shared/source-mode.ts +22 -0
  88. package/dist/automation/index.d.ts +1 -1
  89. package/dist/client/AssistantChat.d.ts +4 -1
  90. package/dist/client/AssistantChat.js +26 -3
  91. package/dist/client/chat/agentkit-protocol.js +148 -34
  92. package/dist/client/chat/message-components.js +7 -5
  93. package/dist/client/chat/tool-call-display.js +4 -3
  94. package/dist/client/conversation/AgentConversation.js +4 -2
  95. package/dist/client/tool-display.d.ts +9 -0
  96. package/dist/client/tool-display.js +15 -0
  97. package/dist/collab/awareness.d.ts +2 -2
  98. package/dist/collab/routes.d.ts +1 -1
  99. package/dist/localization/core-messages/ar-SA.js +2 -0
  100. package/dist/localization/core-messages/de-DE.js +2 -0
  101. package/dist/localization/core-messages/en-US.d.ts +2 -0
  102. package/dist/localization/core-messages/en-US.js +2 -0
  103. package/dist/localization/core-messages/es-ES.js +2 -0
  104. package/dist/localization/core-messages/fr-FR.js +2 -0
  105. package/dist/localization/core-messages/hi-IN.js +2 -0
  106. package/dist/localization/core-messages/ja-JP.js +2 -0
  107. package/dist/localization/core-messages/ko-KR.js +2 -0
  108. package/dist/localization/core-messages/pt-BR.js +2 -0
  109. package/dist/localization/core-messages/zh-CN.js +2 -0
  110. package/dist/localization/core-messages/zh-TW.js +2 -0
  111. package/dist/localization/core-messages.d.ts +2 -0
  112. package/dist/notifications/routes.d.ts +3 -3
  113. package/dist/observability/routes.d.ts +3 -3
  114. package/dist/resources/handlers.d.ts +1 -1
  115. package/dist/secrets/routes.d.ts +3 -3
  116. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  117. package/dist/server/core-routes-plugin.d.ts +1 -1
  118. package/package.json +3 -3
  119. package/corpus/templates/design/app/components/design/editor/PendingScreenDeletionDialog.tsx +0 -66
@@ -25,7 +25,13 @@ import { z } from "zod";
25
25
 
26
26
  import { getDb, schema } from "../server/db/index.js";
27
27
  import "../server/db/index.js"; // ensure registerShareableResource runs
28
+ import {
29
+ fetchLocalhostSnapshot,
30
+ resolveLocalhostBridgeConnection,
31
+ resolveLocalhostConnectionScope,
32
+ } from "../server/lib/localhost-connection.js";
28
33
  import { resolveSourceCapabilities } from "../shared/capability-resolver.js";
34
+ import { sanitizeMarkup } from "../shared/capture-sanitize.js";
29
35
  import { buildCodeLayerProjection } from "../shared/code-layer.js";
30
36
  import type { CodeLayerNode, CodeLayerSource } from "../shared/code-layer.js";
31
37
  import {
@@ -39,9 +45,14 @@ import {
39
45
  componentNodeIdMatches,
40
46
  extractProps,
41
47
  type ComponentInstance,
48
+ instanceFromNode,
42
49
  } from "../shared/component-model.js";
43
50
  import { hasCapability } from "../shared/design-source-capabilities.js";
44
- import { designSourceTypeFromData } from "../shared/source-mode.js";
51
+ import { isStandaloneHttpUrl } from "../shared/html-content.js";
52
+ import {
53
+ designConnectionIdFromData,
54
+ designSourceTypeFromData,
55
+ } from "../shared/source-mode.js";
45
56
 
46
57
  export function canRestoreComponentMain(
47
58
  node: Pick<CodeLayerNode, "dataAttributes">,
@@ -65,6 +76,126 @@ function parseJson<T>(raw: string | null | undefined, fallback: T): T {
65
76
  }
66
77
  }
67
78
 
79
+ function isRecord(value: unknown): value is Record<string, unknown> {
80
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
81
+ }
82
+
83
+ function metadataForFile(
84
+ data: Record<string, unknown>,
85
+ fileId: string,
86
+ ): Record<string, unknown> {
87
+ for (const key of ["screenMetadata", "localhostScreens"]) {
88
+ const metadata = data[key];
89
+ const entry = isRecord(metadata) ? metadata[fileId] : undefined;
90
+ if (isRecord(entry)) return entry;
91
+ }
92
+ return {};
93
+ }
94
+
95
+ function stringValue(
96
+ attributes: Record<string, string>,
97
+ names: readonly string[],
98
+ ): string | undefined {
99
+ for (const name of names) {
100
+ const value = attributes[name]?.trim();
101
+ if (value) return value;
102
+ }
103
+ return undefined;
104
+ }
105
+
106
+ function positiveIntegerValue(
107
+ attributes: Record<string, string>,
108
+ names: readonly string[],
109
+ ): number | undefined {
110
+ const raw = stringValue(attributes, names);
111
+ if (!raw || !/^\d+$/.test(raw)) return undefined;
112
+ const value = Number(raw);
113
+ return Number.isSafeInteger(value) && value > 0 ? value : undefined;
114
+ }
115
+
116
+ function liveComponentNameFor(
117
+ node: CodeLayerNode,
118
+ sourceType: ReturnType<typeof designSourceTypeFromData>,
119
+ ): string | null {
120
+ return (
121
+ componentNameFor(node) ??
122
+ (sourceType !== "inline"
123
+ ? (stringValue(node.dataAttributes, ["data-component-name"]) ?? null)
124
+ : null)
125
+ );
126
+ }
127
+
128
+ function liveInstanceForNode(
129
+ node: CodeLayerNode,
130
+ name: string,
131
+ ): NonNullable<ReturnType<typeof instanceFromNode>> {
132
+ const existing = instanceFromNode(node);
133
+ if (existing) return existing;
134
+ const stableNodeId =
135
+ stringValue(node.dataAttributes, ["data-agent-native-node-id"]) ?? node.id;
136
+ const alpineDataRaw = node.attributes["x-data"];
137
+ return {
138
+ instanceId: stableNodeId,
139
+ name,
140
+ props: extractProps(node),
141
+ alpineData: typeof alpineDataRaw === "string" ? alpineDataRaw : undefined,
142
+ selector: node.selector,
143
+ nodeId: stableNodeId,
144
+ componentId:
145
+ stringValue(node.dataAttributes, [COMPONENT_ID_ATTR]) ?? undefined,
146
+ componentRef:
147
+ stringValue(node.dataAttributes, [COMPONENT_REF_ATTR]) ?? undefined,
148
+ };
149
+ }
150
+
151
+ function sourceLocationForNode(
152
+ node: CodeLayerNode,
153
+ indexRow:
154
+ | {
155
+ filePath?: string | null;
156
+ exportName?: string | null;
157
+ }
158
+ | undefined,
159
+ canResolveToFile: boolean,
160
+ componentName: string,
161
+ ) {
162
+ if (!canResolveToFile) return undefined;
163
+ const filePath =
164
+ stringValue(node.dataAttributes, [
165
+ "data-source-file",
166
+ "data-agent-native-source-file",
167
+ "data-agent-native-source-path",
168
+ ]) ?? indexRow?.filePath;
169
+ if (!filePath) return undefined;
170
+ const line = positiveIntegerValue(node.dataAttributes, [
171
+ "data-source-line",
172
+ "data-agent-native-source-line",
173
+ ]);
174
+ const column = positiveIntegerValue(node.dataAttributes, [
175
+ "data-source-column",
176
+ "data-agent-native-source-column",
177
+ ]);
178
+ return {
179
+ filePath,
180
+ exportName: indexRow?.exportName ?? undefined,
181
+ ...(line ? { line } : {}),
182
+ ...(column ? { column } : {}),
183
+ componentName:
184
+ stringValue(node.dataAttributes, ["data-component-name"]) ??
185
+ componentName,
186
+ };
187
+ }
188
+
189
+ export class ComponentDetailsLiveProjectionError extends Error {
190
+ readonly statusCode = 424;
191
+ readonly code = "LIVE_PROJECTION_UNAVAILABLE";
192
+
193
+ constructor(message: string) {
194
+ super(message);
195
+ this.name = "ComponentDetailsLiveProjectionError";
196
+ }
197
+ }
198
+
68
199
  // ─── Action ───────────────────────────────────────────────────────────────────
69
200
 
70
201
  export default defineAction({
@@ -86,10 +217,36 @@ export default defineAction({
86
217
  .string()
87
218
  .optional()
88
219
  .describe("Design file id. Defaults to index.html."),
220
+ runtime: z
221
+ .object({
222
+ name: z.string().min(1),
223
+ nodeId: z.string().min(1),
224
+ selector: z.string().min(1),
225
+ props: z.array(
226
+ z.object({ name: z.string().min(1), value: z.string() }),
227
+ ),
228
+ literalProps: z
229
+ .array(z.object({ name: z.string().min(1), value: z.string() }))
230
+ .optional(),
231
+ alpineData: z.string().nullable().optional(),
232
+ componentId: z.string().optional(),
233
+ componentRef: z.string().optional(),
234
+ isMain: z.boolean().optional(),
235
+ sourceLocation: z
236
+ .object({
237
+ filePath: z.string().min(1),
238
+ exportName: z.string().optional(),
239
+ })
240
+ .optional(),
241
+ })
242
+ .optional()
243
+ .describe(
244
+ "Runtime component metadata from a URL-backed preview. This keeps the inspector on the live DOM projection when the SQL file stores only a route URL.",
245
+ ),
89
246
  }),
90
247
  readOnly: true,
91
248
  http: { method: "GET" },
92
- run: async ({ designId, nodeId, fileId }) => {
249
+ run: async ({ designId, nodeId, fileId, runtime }) => {
93
250
  // ── Access check ────────────────────────────────────────────────────────
94
251
  const access = await resolveAccess("design", designId);
95
252
  if (!access) throw new Error("Design not found");
@@ -114,6 +271,62 @@ export default defineAction({
114
271
  ? "Prop write-back requires the bridge applyEdit capability. Preview controls remain available until source write hardening is enabled."
115
272
  : undefined;
116
273
 
274
+ // URL-backed files store the route URL in SQL rather than the rendered
275
+ // DOM. Use the accepted runtime projection for the inspector instead of
276
+ // pretending that URL is an HTML source document.
277
+ if (runtime) {
278
+ const [indexRow] = await db
279
+ .select()
280
+ .from(schema.componentIndex)
281
+ .where(
282
+ and(
283
+ eq(schema.componentIndex.designId, designId),
284
+ eq(schema.componentIndex.name, runtime.name),
285
+ ),
286
+ )
287
+ .limit(1);
288
+ const persistedProps = parseJson<unknown[]>(indexRow?.props, []);
289
+ const persistedVariants = parseJson<Record<string, string[]>>(
290
+ indexRow?.variants,
291
+ {},
292
+ );
293
+ const persistedStories = parseJson<unknown[]>(indexRow?.stories, []);
294
+ const instance: ComponentInstance = {
295
+ instanceId: runtime.nodeId,
296
+ name: runtime.name,
297
+ props: runtime.props,
298
+ alpineData: runtime.alpineData ?? undefined,
299
+ selector: runtime.selector,
300
+ nodeId: runtime.nodeId,
301
+ componentId: runtime.componentId,
302
+ componentRef: runtime.componentRef,
303
+ };
304
+ return {
305
+ designId,
306
+ nodeId,
307
+ sourceType,
308
+ instance,
309
+ name: runtime.name,
310
+ isMain:
311
+ runtime.isMain ??
312
+ Boolean(runtime.componentId && !runtime.componentRef),
313
+ canRestore: false,
314
+ observedProps: runtime.props,
315
+ literalProps: runtime.literalProps,
316
+ persistedProps,
317
+ persistedVariants,
318
+ persistedStories,
319
+ sourceLocation: runtime.sourceLocation,
320
+ capabilities: {
321
+ canResolveToFile,
322
+ hasFullIndex,
323
+ canEditProps,
324
+ ctaRequired,
325
+ ctaMessage,
326
+ },
327
+ };
328
+ }
329
+
117
330
  // ── Fetch design file ────────────────────────────────────────────────────
118
331
  const conditions = [
119
332
  accessFilter(schema.designs, schema.designShares, undefined, "viewer", {
@@ -131,6 +344,7 @@ export default defineAction({
131
344
  designId: schema.designFiles.designId,
132
345
  filename: schema.designFiles.filename,
133
346
  content: schema.designFiles.content,
347
+ data: schema.designs.data,
134
348
  })
135
349
  .from(schema.designFiles)
136
350
  .innerJoin(
@@ -142,11 +356,51 @@ export default defineAction({
142
356
 
143
357
  if (!file) throw new Error("Design HTML file not found.");
144
358
 
145
- // Use the durable SQL source for component prop reads. A connected editor
146
- // can briefly hold an older Yjs text snapshot while server-side prop writes
147
- // have already updated SQL; preferring collab here makes the inspector
148
- // rehydrate stale props even though the canvas renders the saved source.
149
- const html = file.content ?? "";
359
+ // Inline screens use durable SQL. URL-backed screens must be projected from
360
+ // the live bridge because the stored value is only a route URL.
361
+ let html = file.content ?? "";
362
+ if (isStandaloneHttpUrl(html)) {
363
+ if (sourceType !== "localhost") {
364
+ throw new ComponentDetailsLiveProjectionError(
365
+ `Component details for URL-backed screen "${file.filename}" require a localhost bridge snapshot; source type "${sourceType}" has no live component mapping.`,
366
+ );
367
+ }
368
+ const designData = parseJson<Record<string, unknown>>(file.data, {});
369
+ const metadata = metadataForFile(designData, file.id);
370
+ const connectionId =
371
+ (typeof metadata.connectionId === "string" &&
372
+ metadata.connectionId.trim()) ||
373
+ designConnectionIdFromData(designData);
374
+ if (!connectionId) {
375
+ throw new ComponentDetailsLiveProjectionError(
376
+ `URL-backed screen "${file.filename}" has no localhost connection metadata. Reopen visual edit or reconnect the localhost app before requesting component details.`,
377
+ );
378
+ }
379
+ const { ownerEmail, orgId } = await resolveLocalhostConnectionScope({
380
+ designId,
381
+ });
382
+ const connection = await resolveLocalhostBridgeConnection({
383
+ connectionId,
384
+ ownerEmail,
385
+ orgId,
386
+ });
387
+ const previewToken =
388
+ typeof metadata.previewToken === "string"
389
+ ? metadata.previewToken
390
+ : null;
391
+ html = sanitizeMarkup(
392
+ await fetchLocalhostSnapshot({
393
+ bridgeUrl: connection.bridgeUrl,
394
+ previewToken,
395
+ url: html,
396
+ }),
397
+ ).trim();
398
+ if (!html) {
399
+ throw new ComponentDetailsLiveProjectionError(
400
+ `The localhost bridge returned an empty snapshot for "${file.filename}". Reload the live frame and retry.`,
401
+ );
402
+ }
403
+ }
150
404
 
151
405
  // ── Projection lookup ────────────────────────────────────────────────────
152
406
  const codeLayerSource: CodeLayerSource = {
@@ -169,28 +423,16 @@ export default defineAction({
169
423
  );
170
424
  }
171
425
 
172
- const name = componentNameFor(node);
426
+ const name = liveComponentNameFor(node, sourceType);
173
427
  if (!name) {
174
428
  throw new Error(
175
- `Node "${nodeId}" does not carry a data-agent-native-component attribute and is not a component root.`,
429
+ `Node "${nodeId}" does not carry component provenance and is not a component root.`,
176
430
  );
177
431
  }
178
432
 
179
433
  // ── Simple props from attributes ─────────────────────────────────────────
180
434
  const observedProps = extractProps(node);
181
- const alpineData =
182
- typeof node.attributes["x-data"] === "string"
183
- ? node.attributes["x-data"]
184
- : undefined;
185
-
186
- const instance: ComponentInstance = {
187
- instanceId: node.id,
188
- name,
189
- props: observedProps,
190
- alpineData,
191
- selector: node.selector,
192
- nodeId,
193
- };
435
+ const instance = liveInstanceForNode(node, name);
194
436
 
195
437
  // ── Lookup persisted component_index row ─────────────────────────────────
196
438
  const [indexRow] = await db
@@ -212,17 +454,14 @@ export default defineAction({
212
454
  const persistedStories = parseJson<unknown[]>(indexRow?.stories, []);
213
455
 
214
456
  // ── Source location (real-app only) ──────────────────────────────────────
215
- // The bridge `resolveNodeToFile` op maps a node id to a source file + span.
216
- // For inline designs the capability is `available` but resolves to the
217
- // design file itself (no external source file). We surface whatever we
218
- // have from the index row.
219
- const sourceLocation =
220
- canResolveToFile && indexRow?.filePath
221
- ? {
222
- filePath: indexRow.filePath,
223
- exportName: indexRow.exportName ?? undefined,
224
- }
225
- : undefined;
457
+ // Live bridge provenance is preferred; indexed metadata remains the
458
+ // fallback when the bridge did not stamp a source span.
459
+ const sourceLocation = sourceLocationForNode(
460
+ node,
461
+ indexRow,
462
+ canResolveToFile,
463
+ name,
464
+ );
226
465
 
227
466
  return {
228
467
  designId,
@@ -28,6 +28,9 @@ function parseChatContext(raw: string | null): DesignVersionChatContext | null {
28
28
  context[key] = candidate;
29
29
  }
30
30
  }
31
+ if ((value as { surface?: unknown }).surface === "editor") {
32
+ context.surface = "editor";
33
+ }
31
34
  return Object.keys(context).length > 0 ? context : null;
32
35
  }
33
36
 
@@ -77,7 +80,12 @@ export default defineAction({
77
80
  designId,
78
81
  label: version.label,
79
82
  createdAt: version.createdAt,
80
- source: chatContext ? ("chat" as const) : ("legacy" as const),
83
+ source:
84
+ chatContext?.surface === "editor"
85
+ ? ("editor" as const)
86
+ : chatContext
87
+ ? ("chat" as const)
88
+ : ("legacy" as const),
81
89
  fileCount: version.fileCount ?? snapshot.files.length,
82
90
  chatContext,
83
91
  designTitle: snapshot.designTitle ?? null,
@@ -1,26 +1,9 @@
1
1
  /**
2
2
  * go-to-main-component — Figma's "Go to main component".
3
3
  *
4
- * DESIGN NOTE — mapping onto this codebase's data model: there is no
5
- * persisted "main component" concept anywhere here. A component is just a
6
- * name (`data-agent-native-component="Name"`) that happens to be stamped on
7
- * more than one element; every instance is an independently-duplicated copy
8
- * of HTML (see `shared/component-model.ts`, `component_index` schema — it
9
- * stores props/variants/runtime-selectors metadata, never a canonical
10
- * definition). A true "main component" would require a data-model addition
11
- * (e.g. an `isMain`/definition pointer on `component_index`) — out of scope
12
- * here; flagged for a follow-up if promoting a specific instance as
13
- * authoritative becomes a real need.
14
- *
15
- * Closest tractable equivalent: treat the EARLIEST instance of the same
16
- * component name — scanning every design file in `createdAt` order, document
17
- * order within each file — as the "main" analogue, mirroring the common case
18
- * where a component's original occurrence predates its copies. If the given
19
- * instance already IS that earliest one, this returns `isMain: true` instead
20
- * of navigating (nothing to jump to). Otherwise it writes a `navigate`
21
- * app-state command (same mechanism as `navigate` / `open-component-source`)
22
- * so the editor selects the target instance, including switching design
23
- * files when the main instance lives on a different screen.
4
+ * A canonical linked component has a persisted opaque id on its main root and
5
+ * references. Resolve that id first. Legacy name-only annotations still use
6
+ * the earliest same-name instance as their compatibility fallback.
24
7
  *
25
8
  * Navigation-only + inline/Alpine designs only (real-app sources return a
26
9
  * CTA). Because this writes the transient `navigate` application-state
@@ -43,6 +26,8 @@ import {
43
26
  scanComponentLibrary,
44
27
  } from "../shared/component-library.js";
45
28
  import {
29
+ COMPONENT_ID_ATTR,
30
+ COMPONENT_REF_ATTR,
46
31
  componentNameFor,
47
32
  componentNodeIdMatches,
48
33
  } from "../shared/component-model.js";
@@ -66,12 +51,11 @@ async function liveContent(
66
51
  export default defineAction({
67
52
  description:
68
53
  "Resolve the 'main' instance of a component (Figma's Go to main " +
69
- "component). This codebase has no separate component-definition markup " +
70
- "— components are structurally duplicated HTML matched by name — so the " +
71
- "earliest instance found across the design's files stands in for the " +
72
- "main component. Returns isMain=true when the given instance already IS " +
73
- "that earliest one; otherwise navigates the editor to it (which may " +
74
- "switch design files).",
54
+ "component). Canonical linked components resolve by their persisted " +
55
+ "component id; legacy name-only annotations fall back to the earliest " +
56
+ "same-name instance across the design's files. Returns isMain=true when " +
57
+ "the selected instance is already the main; otherwise navigates the " +
58
+ "editor to it.",
75
59
  schema: z.object({
76
60
  designId: z.string().describe("Design project ID"),
77
61
  nodeId: z
@@ -154,9 +138,12 @@ export default defineAction({
154
138
  }
155
139
 
156
140
  const componentName = componentNameFor(node);
157
- if (!componentName) {
141
+ const componentId =
142
+ node.dataAttributes[COMPONENT_ID_ATTR]?.trim() ||
143
+ node.dataAttributes[COMPONENT_REF_ATTR]?.trim();
144
+ if (!componentName && !componentId) {
158
145
  throw new Error(
159
- `Node "${nodeId}" is not a component root (no data-agent-native-component attribute).`,
146
+ `Node "${nodeId}" is not a component root (no component name or linked identity).`,
160
147
  );
161
148
  }
162
149
 
@@ -190,16 +177,31 @@ export default defineAction({
190
177
  );
191
178
 
192
179
  const entries = scanComponentLibrary(filesForScan);
193
- const matches = entriesForComponent(entries, componentName);
180
+ const matches = componentId
181
+ ? entries.filter(
182
+ (entry) =>
183
+ entry.componentId === componentId ||
184
+ entry.componentRef === componentId,
185
+ )
186
+ : entriesForComponent(entries, componentName!);
194
187
 
195
188
  if (matches.length === 0) {
196
189
  // Shouldn't happen (the current node itself matches), but guard anyway.
197
190
  throw new Error(
198
- `No instances of component "${componentName}" found across the design's files.`,
191
+ `No instances of component "${componentName ?? componentId}" found across the design's files.`,
192
+ );
193
+ }
194
+ const resolvedComponentName = componentName ?? matches[0]?.name;
195
+ if (!resolvedComponentName) {
196
+ throw new Error(
197
+ `Component identity "${componentId}" has no named canonical root.`,
199
198
  );
200
199
  }
201
200
 
202
- const main = matches[0];
201
+ const main = componentId
202
+ ? (matches.find((entry) => entry.componentId === componentId) ??
203
+ matches[0])
204
+ : matches[0];
203
205
  // Compare against the caller's own stable `nodeId` param (a durable
204
206
  // data-agent-native-node-id), not `node.id` (an ephemeral id scoped to
205
207
  // this projection call) — `scanComponentLibrary` resolves the same
@@ -210,7 +212,7 @@ export default defineAction({
210
212
  return {
211
213
  designId,
212
214
  nodeId,
213
- componentName,
215
+ componentName: resolvedComponentName,
214
216
  sourceType,
215
217
  ctaRequired: false,
216
218
  isMain: true,
@@ -218,8 +220,8 @@ export default defineAction({
218
220
  navigated: false,
219
221
  note:
220
222
  matches.length === 1
221
- ? `"${componentName}" has only one instance — this is it.`
222
- : `This is the earliest instance of "${componentName}" across the design.`,
223
+ ? `"${resolvedComponentName}" has only one instance — this is it.`
224
+ : `This is the earliest instance of "${resolvedComponentName}" across the design.`,
223
225
  };
224
226
  }
225
227
 
@@ -237,7 +239,7 @@ export default defineAction({
237
239
  return {
238
240
  designId,
239
241
  nodeId,
240
- componentName,
242
+ componentName: resolvedComponentName,
241
243
  sourceType,
242
244
  ctaRequired: false,
243
245
  isMain: false,