@agent-native/core 0.84.32 → 0.84.34

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 (45) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +12 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/client/use-action.ts +5 -1
  5. package/corpus/core/src/server/ssr-handler.ts +12 -0
  6. package/corpus/core/src/vite/action-types-plugin.ts +6 -2
  7. package/corpus/core/src/vite/client.ts +152 -17
  8. package/corpus/templates/design/actions/import-design-source.ts +158 -0
  9. package/corpus/templates/design/actions/navigate.ts +3 -2
  10. package/corpus/templates/design/actions/view-screen.ts +1 -1
  11. package/corpus/templates/design/app/components/design/DesignImportPanel.tsx +519 -0
  12. package/corpus/templates/design/app/hooks/use-navigation-state.ts +28 -5
  13. package/corpus/templates/design/app/i18n/zh-TW.ts +40 -0
  14. package/corpus/templates/design/app/i18n-data.ts +508 -0
  15. package/corpus/templates/design/app/pages/DesignEditor.tsx +27 -0
  16. package/corpus/templates/design/changelog/2026-07-01-design-can-import-figma-paste-fig-files-and-standalone-html.md +6 -0
  17. package/corpus/templates/design/package.json +3 -0
  18. package/corpus/templates/design/server/handlers/import-design-file.ts +168 -0
  19. package/corpus/templates/design/server/lib/figma-import/clipboard.ts +122 -0
  20. package/corpus/templates/design/server/lib/figma-import/decode.ts +455 -0
  21. package/corpus/templates/design/server/lib/figma-import/processor.ts +116 -0
  22. package/corpus/templates/design/server/lib/figma-import/render-html.ts +397 -0
  23. package/corpus/templates/design/server/lib/figma-import/types.ts +60 -0
  24. package/corpus/templates/design/server/lib/import-design-files.ts +314 -0
  25. package/corpus/templates/design/server/routes/api/import-design-file.post.ts +1 -0
  26. package/dist/client/use-action.d.ts +5 -1
  27. package/dist/client/use-action.d.ts.map +1 -1
  28. package/dist/client/use-action.js.map +1 -1
  29. package/dist/collab/awareness.d.ts +2 -2
  30. package/dist/collab/awareness.d.ts.map +1 -1
  31. package/dist/collab/routes.d.ts +1 -1
  32. package/dist/notifications/routes.d.ts +3 -3
  33. package/dist/observability/routes.d.ts +3 -3
  34. package/dist/resources/handlers.d.ts +2 -2
  35. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  36. package/dist/server/ssr-handler.d.ts.map +1 -1
  37. package/dist/server/ssr-handler.js +7 -0
  38. package/dist/server/ssr-handler.js.map +1 -1
  39. package/dist/vite/action-types-plugin.d.ts.map +1 -1
  40. package/dist/vite/action-types-plugin.js +6 -2
  41. package/dist/vite/action-types-plugin.js.map +1 -1
  42. package/dist/vite/client.d.ts.map +1 -1
  43. package/dist/vite/client.js +132 -11
  44. package/dist/vite/client.js.map +1 -1
  45. package/package.json +1 -1
package/corpus/README.md CHANGED
@@ -28,4 +28,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
28
28
  ## Generated Counts
29
29
 
30
30
  - core files: 2043
31
- - template files: 4983
31
+ - template files: 4994
@@ -1,5 +1,17 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.84.34
4
+
5
+ ### Patch Changes
6
+
7
+ - 56f3d91: Fix hosted Google Analytics / Tag Manager injection by baking the measurement id into Nitro server bundles and merging the required GA/GTM script, connect, and image hosts into existing stricter document CSPs.
8
+
9
+ ## 0.84.33
10
+
11
+ ### Patch Changes
12
+
13
+ - 8cc9620: Keep Nitro/Vite dev servers responsive when native file watchers hit EMFILE or ENOSPC by falling back to polling instead of replacing failed watches with no-ops.
14
+
3
15
  ## 0.84.32
4
16
 
5
17
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.84.32",
3
+ "version": "0.84.34",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -60,7 +60,11 @@ function defaultActionQueryRetry(
60
60
  * it maps action names to their parameter and return types, enabling
61
61
  * end-to-end type safety for `useActionQuery` and `useActionMutation`.
62
62
  */
63
- export interface ActionRegistry {}
63
+ declare global {
64
+ interface AgentNativeActionRegistry {}
65
+ }
66
+
67
+ export interface ActionRegistry extends AgentNativeActionRegistry {}
64
68
 
65
69
  /** Resolves to the union of registered action names, or `string` if no registry exists. */
66
70
  type ActionName = keyof ActionRegistry extends never
@@ -402,6 +402,15 @@ function appendToExistingCspDirective(
402
402
  existing.tokens = appendCspTokens(existing.tokens, additions);
403
403
  }
404
404
 
405
+ function ensureCspDirective(
406
+ directives: CspDirective[],
407
+ name: string,
408
+ tokens: readonly string[],
409
+ ): void {
410
+ if (findCspDirective(directives, name)) return;
411
+ directives.push({ name, tokens: [...tokens] });
412
+ }
413
+
405
414
  function hasStrictNonceScriptPolicy(tokens: readonly string[]): boolean {
406
415
  return tokens.some(
407
416
  (token) => token === "'strict-dynamic'" || token.startsWith("'nonce-"),
@@ -480,6 +489,9 @@ function augmentExistingEnforcedCspForFrameworkScripts(
480
489
  }
481
490
  }
482
491
 
492
+ ensureCspDirective(directives, "object-src", ["'none'"]);
493
+ ensureCspDirective(directives, "base-uri", ["'self'"]);
494
+
483
495
  return serializeCsp(directives);
484
496
  }
485
497
 
@@ -280,12 +280,16 @@ type ActionEntry<T> = T extends { default: { run: (...args: infer A) => infer R
280
280
  }
281
281
  : { result: any; params: Record<string, any> };
282
282
 
283
- declare module "@agent-native/core/client" {
284
- interface ActionRegistry {
283
+ declare global {
284
+ interface AgentNativeActionRegistry {
285
285
  ${typeEntries.join("\n")}
286
286
  }
287
287
  }
288
288
 
289
+ declare module "@agent-native/core/client" {
290
+ interface ActionRegistry extends AgentNativeActionRegistry {}
291
+ }
292
+
289
293
  export {};
290
294
  `;
291
295
 
@@ -1,3 +1,4 @@
1
+ import { EventEmitter } from "events";
1
2
  import fs from "fs";
2
3
  import type { IncomingMessage, ServerResponse } from "http";
3
4
  import { createRequire, syncBuiltinESMExports } from "module";
@@ -36,6 +37,137 @@ const require = createRequire(import.meta.url);
36
37
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
37
38
  let nitroFsWatchGuardInstalled = false;
38
39
 
40
+ type FsWatchArgs = [fs.PathLike, ...any[]];
41
+
42
+ function isFileWatchLimitError(
43
+ error: NodeJS.ErrnoException | undefined,
44
+ ): boolean {
45
+ return error?.code === "EMFILE" || error?.code === "ENOSPC";
46
+ }
47
+
48
+ function watchPollingIntervalMs(): number {
49
+ const raw = Number(process.env.CHOKIDAR_INTERVAL ?? 1000);
50
+ return Number.isFinite(raw) && raw > 0 ? raw : 1000;
51
+ }
52
+
53
+ function fsWatchListener(args: FsWatchArgs): fs.WatchListener<string> | null {
54
+ const maybeOptionsOrListener = args[1];
55
+ const maybeListener = args[2];
56
+ if (typeof maybeOptionsOrListener === "function")
57
+ return maybeOptionsOrListener as fs.WatchListener<string>;
58
+ if (typeof maybeListener === "function")
59
+ return maybeListener as fs.WatchListener<string>;
60
+ return null;
61
+ }
62
+
63
+ function fsWatchPersistent(args: FsWatchArgs): boolean {
64
+ const options = args[1];
65
+ if (!options || typeof options === "function") return true;
66
+ if (typeof options === "string" || Buffer.isBuffer(options)) return true;
67
+ return options.persistent !== false;
68
+ }
69
+
70
+ function directEntrySnapshot(
71
+ target: string,
72
+ ): Map<string, { mtimeMs: number; size: number; directory: boolean }> {
73
+ const snapshot = new Map<
74
+ string,
75
+ { mtimeMs: number; size: number; directory: boolean }
76
+ >();
77
+ const stat = fs.statSync(target);
78
+ if (!stat.isDirectory()) {
79
+ snapshot.set(path.basename(target), {
80
+ mtimeMs: stat.mtimeMs,
81
+ size: stat.size,
82
+ directory: false,
83
+ });
84
+ return snapshot;
85
+ }
86
+ for (const entry of fs.readdirSync(target, { withFileTypes: true })) {
87
+ const entryPath = path.join(target, entry.name);
88
+ try {
89
+ const entryStat = fs.statSync(entryPath);
90
+ snapshot.set(entry.name, {
91
+ mtimeMs: entryStat.mtimeMs,
92
+ size: entryStat.size,
93
+ directory: entryStat.isDirectory(),
94
+ });
95
+ } catch {
96
+ // The entry may have disappeared between readdir and stat.
97
+ }
98
+ }
99
+ return snapshot;
100
+ }
101
+
102
+ function createPollingFsWatcher(args: FsWatchArgs): fs.FSWatcher {
103
+ const target = String(args[0]);
104
+ const listener = fsWatchListener(args);
105
+ const emitter = new EventEmitter() as fs.FSWatcher;
106
+ let closed = false;
107
+ let previous = directEntrySnapshot(target);
108
+
109
+ if (listener) emitter.on("change", listener);
110
+
111
+ const emitChange = (eventName: "change" | "rename", filename: string) => {
112
+ emitter.emit("change", eventName, filename);
113
+ };
114
+
115
+ const timer = setInterval(() => {
116
+ if (closed) return;
117
+ let next: typeof previous;
118
+ try {
119
+ next = directEntrySnapshot(target);
120
+ } catch (error) {
121
+ emitter.emit("error", error);
122
+ return;
123
+ }
124
+
125
+ for (const [filename, current] of next) {
126
+ const old = previous.get(filename);
127
+ if (!old) {
128
+ emitChange("rename", filename);
129
+ continue;
130
+ }
131
+ if (
132
+ old.mtimeMs !== current.mtimeMs ||
133
+ old.size !== current.size ||
134
+ old.directory !== current.directory
135
+ ) {
136
+ emitChange("change", filename);
137
+ }
138
+ }
139
+ for (const filename of previous.keys()) {
140
+ if (!next.has(filename)) emitChange("rename", filename);
141
+ }
142
+ previous = next;
143
+ }, watchPollingIntervalMs());
144
+
145
+ if (!fsWatchPersistent(args)) timer.unref();
146
+
147
+ emitter.close = () => {
148
+ if (closed) return;
149
+ closed = true;
150
+ clearInterval(timer);
151
+ emitter.removeAllListeners();
152
+ };
153
+ emitter.ref = () => {
154
+ timer.ref();
155
+ return emitter;
156
+ };
157
+ emitter.unref = () => {
158
+ timer.unref();
159
+ return emitter;
160
+ };
161
+
162
+ return emitter;
163
+ }
164
+
165
+ function warnNitroFsWatchFallback(target: unknown, err: NodeJS.ErrnoException) {
166
+ console.warn(
167
+ `[agent-native] Falling back to polling Nitro fs.watch for ${String(target)}: ${err.message}`,
168
+ );
169
+ }
170
+
39
171
  function installNitroFsWatchGuard(): void {
40
172
  if (nitroFsWatchGuardInstalled) return;
41
173
  nitroFsWatchGuardInstalled = true;
@@ -49,29 +181,32 @@ function installNitroFsWatchGuard(): void {
49
181
  watcher = originalWatch(...args);
50
182
  } catch (error) {
51
183
  const err = error as NodeJS.ErrnoException;
52
- if (err.code !== "EMFILE" && err.code !== "ENOSPC") throw error;
53
- console.warn(
54
- `[agent-native] Disabled Nitro fs.watch for ${String(args[0])}: ${err.message}`,
55
- );
56
- return {
57
- close() {},
58
- on() {
59
- return this as fs.FSWatcher;
60
- },
61
- } as unknown as fs.FSWatcher;
184
+ if (!isFileWatchLimitError(err)) throw error;
185
+ warnNitroFsWatchFallback(args[0], err);
186
+ return createPollingFsWatcher(args as FsWatchArgs);
62
187
  }
63
188
 
64
189
  const originalEmit = watcher.emit.bind(watcher);
190
+ const originalClose = watcher.close.bind(watcher);
191
+ let pollingFallback: fs.FSWatcher | undefined;
192
+
193
+ watcher.close = (() => {
194
+ pollingFallback?.close();
195
+ return originalClose();
196
+ }) as fs.FSWatcher["close"];
197
+
65
198
  watcher.emit = ((eventName: string | symbol, ...eventArgs: any[]) => {
66
199
  const err = eventArgs[0] as NodeJS.ErrnoException | undefined;
67
- if (
68
- eventName === "error" &&
69
- (err?.code === "EMFILE" || err?.code === "ENOSPC")
70
- ) {
71
- console.warn(
72
- `[agent-native] Disabled Nitro fs.watch for ${String(args[0])}: ${err.message}`,
73
- );
200
+ if (eventName === "error" && isFileWatchLimitError(err) && err) {
201
+ warnNitroFsWatchFallback(args[0], err);
74
202
  watcher.close();
203
+ pollingFallback = createPollingFsWatcher(args as FsWatchArgs);
204
+ pollingFallback.on("change", (changeEvent, filename) => {
205
+ originalEmit("change", changeEvent, filename);
206
+ });
207
+ pollingFallback.on("error", (pollingError) => {
208
+ originalEmit("error", pollingError);
209
+ });
75
210
  return false;
76
211
  }
77
212
  return originalEmit(eventName, ...eventArgs);
@@ -0,0 +1,158 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import { assertAccess } from "@agent-native/core/sharing";
3
+ import { z } from "zod";
4
+
5
+ import { parseFigmaClipboardHtml } from "../server/lib/figma-import/clipboard.js";
6
+ import { importFigmaBuffer } from "../server/lib/figma-import/processor.js";
7
+ import {
8
+ normalizeImportedHtmlDocument,
9
+ resolveImportDesignId,
10
+ saveImportedDesignFiles,
11
+ type ImportedDesignFile,
12
+ } from "../server/lib/import-design-files.js";
13
+
14
+ const MAX_HTML_IMPORT_BYTES = 2 * 1024 * 1024;
15
+
16
+ function ensureHtmlSize(content: string) {
17
+ if (Buffer.byteLength(content, "utf8") > MAX_HTML_IMPORT_BYTES) {
18
+ throw new Error("HTML import content is too large (max 2 MB).");
19
+ }
20
+ }
21
+
22
+ function baseFilename(originalName: string | undefined, fallback: string) {
23
+ return (originalName?.trim() || fallback).replace(/\.[^.]+$/, "") + ".html";
24
+ }
25
+
26
+ export default defineAction({
27
+ description:
28
+ "Import Figma clipboard HTML or standalone HTML into the current Design project as one or more editable screens.",
29
+ schema: z.object({
30
+ designId: z
31
+ .string()
32
+ .optional()
33
+ .describe("Design id. Defaults to the active editor navigation state."),
34
+ sourceType: z.enum(["figma-paste-html", "html-string"]),
35
+ content: z
36
+ .string()
37
+ .max(
38
+ MAX_HTML_IMPORT_BYTES,
39
+ "HTML import content is too large (max 2 MB).",
40
+ ),
41
+ originalName: z.string().optional(),
42
+ }),
43
+ run: async ({ designId, sourceType, content, originalName }) => {
44
+ ensureHtmlSize(content);
45
+ const resolvedDesignId = await resolveImportDesignId(designId);
46
+ await assertAccess("design", resolvedDesignId, "editor");
47
+
48
+ if (sourceType === "html-string") {
49
+ const saved = await saveImportedDesignFiles({
50
+ designId: resolvedDesignId,
51
+ sourceType: "html-import",
52
+ files: [
53
+ {
54
+ filename: baseFilename(originalName, "imported-html"),
55
+ fileType: "html",
56
+ content: normalizeImportedHtmlDocument(content, "HTML source"),
57
+ source: { sourceType: "html-string", originalName },
58
+ },
59
+ ],
60
+ });
61
+ return {
62
+ ...saved,
63
+ stats: { sourceKind: "html-string", frameCount: saved.files.length },
64
+ };
65
+ }
66
+
67
+ const parsed = parseFigmaClipboardHtml(content);
68
+ const warnings: string[] = [];
69
+ if (parsed.buffer && parsed.hasFigmaBuffer) {
70
+ try {
71
+ const imported = await importFigmaBuffer({
72
+ buffer: parsed.buffer,
73
+ filename: originalName ?? "figma-paste.fig",
74
+ sourceKind: "figma-paste",
75
+ selection: { nodeId: parsed.meta?.selectedNodeId },
76
+ meta: parsed.meta,
77
+ });
78
+ const files: ImportedDesignFile[] = imported.files.map((file) => ({
79
+ filename: file.filename,
80
+ fileType: "html",
81
+ content: file.content,
82
+ source: file.source,
83
+ preferredFrame: {
84
+ title:
85
+ typeof file.source?.frameName === "string"
86
+ ? file.source.frameName
87
+ : undefined,
88
+ width: file.width,
89
+ height: file.height,
90
+ },
91
+ }));
92
+ const saved = await saveImportedDesignFiles({
93
+ designId: resolvedDesignId,
94
+ sourceType: "figma-paste",
95
+ files,
96
+ warnings: [...warnings, ...imported.warnings],
97
+ });
98
+ return { ...saved, stats: imported.stats };
99
+ } catch (error) {
100
+ warnings.push(
101
+ error instanceof Error
102
+ ? `Figma binary payload could not be decoded: ${error.message}`
103
+ : "Figma binary payload could not be decoded.",
104
+ );
105
+ }
106
+ } else if (parsed.buffer) {
107
+ warnings.push(
108
+ "The clipboard included a Figma buffer, but it was not a supported fig-kiwi payload.",
109
+ );
110
+ }
111
+
112
+ if (!parsed.fallbackHtml) {
113
+ throw new Error(
114
+ "No importable Figma frame data or visible HTML was found in the clipboard.",
115
+ );
116
+ }
117
+ const saved = await saveImportedDesignFiles({
118
+ designId: resolvedDesignId,
119
+ sourceType: "figma-paste-fallback",
120
+ files: [
121
+ {
122
+ filename: baseFilename(originalName, "figma-paste"),
123
+ fileType: "html",
124
+ content: normalizeImportedHtmlDocument(
125
+ parsed.fallbackHtml,
126
+ "Figma clipboard fallback HTML",
127
+ ),
128
+ source: {
129
+ sourceType: "figma-paste-fallback",
130
+ selectedNodeId: parsed.meta?.selectedNodeId,
131
+ fileKey: parsed.meta?.fileKey,
132
+ },
133
+ },
134
+ ],
135
+ warnings,
136
+ });
137
+ return {
138
+ ...saved,
139
+ stats: {
140
+ sourceKind: "figma-paste",
141
+ format: "html-fallback",
142
+ frameCount: saved.files.length,
143
+ imageCount: 0,
144
+ selectedNodeId: parsed.meta?.selectedNodeId,
145
+ },
146
+ };
147
+ },
148
+ link: ({ result }) => {
149
+ if (!result || typeof result !== "object") return null;
150
+ const designId = (result as { designId?: string }).designId;
151
+ if (!designId) return null;
152
+ return {
153
+ url: `/design/${designId}`,
154
+ label: "Open overview",
155
+ view: "editor",
156
+ };
157
+ },
158
+ });
@@ -18,7 +18,7 @@
18
18
  * --designId Design ID (for editor/present views)
19
19
  * --editorView Editor mode for designs: single or overview
20
20
  * --inspectorTab Inspector tab for designs: design or tweaks (extensions opens Tools for compatibility)
21
- * --leftPanel Left editor panel: file, agent, assets, tools, tokens, or code
21
+ * --leftPanel Left editor panel: file, agent, assets, import, tools, tokens, or code
22
22
  * --fileId Screen/file id to focus in the design editor
23
23
  * --filename Screen filename to focus in the design editor
24
24
  * --tool Design editor tool to activate
@@ -51,6 +51,7 @@ const designLeftPanelSchema = z.enum([
51
51
  "file",
52
52
  "agent",
53
53
  "assets",
54
+ "import",
54
55
  "tools",
55
56
  "tokens",
56
57
  "code",
@@ -58,7 +59,7 @@ const designLeftPanelSchema = z.enum([
58
59
 
59
60
  export default defineAction({
60
61
  description:
61
- "Navigate the UI to a specific view or path. Views: list, editor, design-systems, present, settings. Use --designId with editor/present views and --designSystemId with design-systems. For designs, use editorView=overview to show the infinite screens canvas, or editorView=single with fileId/filename/screen to focus a screen. Use leftPanel=file|agent|assets|tools|tokens|code to focus the left rail, including the wide Code workspace. Legacy inspectorTab=extensions opens Tools. Use tool to activate a design editor tool.",
62
+ "Navigate the UI to a specific view or path. Views: list, editor, design-systems, present, settings. Use --designId with editor/present views and --designSystemId with design-systems. For designs, use editorView=overview to show the infinite screens canvas, or editorView=single with fileId/filename/screen to focus a screen. Use leftPanel=file|agent|assets|import|tools|tokens|code to focus the left rail, including Import and the wide Code workspace. Legacy inspectorTab=extensions opens Tools. Use tool to activate a design editor tool.",
62
63
  schema: z
63
64
  .object({
64
65
  view: z
@@ -136,7 +136,7 @@ function resolveActiveCodeFile(
136
136
 
137
137
  export default defineAction({
138
138
  description:
139
- "See what the user is currently looking at on screen. Returns the current navigation state including which design is open, which view they are on (list, editor, design-systems, present, settings), active/focused design screen, selected element, active inspector tab (design or tweaks), active left rail panel (file, agent, assets, tools, tokens, or code), active code file metadata, overview canvas state, plus any pending question overlay. Always call this first before taking any action.",
139
+ "See what the user is currently looking at on screen. Returns the current navigation state including which design is open, which view they are on (list, editor, design-systems, present, settings), active/focused design screen, selected element, active inspector tab (design or tweaks), active left rail panel (file, agent, assets, import, tools, tokens, or code), active code file metadata, overview canvas state, plus any pending question overlay. Always call this first before taking any action.",
140
140
  schema: z.object({}),
141
141
  http: false,
142
142
  run: async () => {