@agent-native/core 0.84.30 → 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.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +24 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/client/session-replay.ts +5 -0
- package/corpus/core/src/client/use-action.ts +5 -1
- package/corpus/core/src/server/ssr-handler.ts +12 -0
- package/corpus/core/src/vite/action-types-plugin.ts +6 -2
- package/corpus/core/src/vite/client.ts +152 -17
- package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +13 -1
- package/corpus/templates/analytics/changelog/2026-07-01-session-replays-keep-inlined-css-without-live-resource-loa.md +6 -0
- package/corpus/templates/design/AGENTS.md +18 -0
- package/corpus/templates/design/actions/apply-source-edit.ts +87 -0
- package/corpus/templates/design/actions/import-design-source.ts +158 -0
- package/corpus/templates/design/actions/list-source-files.ts +52 -0
- package/corpus/templates/design/actions/navigate.ts +4 -2
- package/corpus/templates/design/actions/preview-source-edit.ts +85 -0
- package/corpus/templates/design/actions/read-source-file.ts +55 -0
- package/corpus/templates/design/actions/resolve-selection-source.ts +101 -0
- package/corpus/templates/design/actions/view-screen.ts +43 -1
- package/corpus/templates/design/app/components/design/CodeWorkbenchHost.tsx +630 -0
- package/corpus/templates/design/app/components/design/DesignImportPanel.tsx +519 -0
- package/corpus/templates/design/app/hooks/use-navigation-state.ts +32 -6
- package/corpus/templates/design/app/i18n/zh-TW.ts +40 -0
- package/corpus/templates/design/app/i18n-data.ts +508 -0
- package/corpus/templates/design/app/pages/DesignEditor.tsx +171 -9
- package/corpus/templates/design/changelog/2026-07-01-design-can-import-figma-paste-fig-files-and-standalone-html.md +6 -0
- package/corpus/templates/design/changelog/2026-07-01-design-code-workspace.md +6 -0
- package/corpus/templates/design/package.json +3 -0
- package/corpus/templates/design/server/handlers/import-design-file.ts +168 -0
- package/corpus/templates/design/server/lib/figma-import/clipboard.ts +122 -0
- package/corpus/templates/design/server/lib/figma-import/decode.ts +455 -0
- package/corpus/templates/design/server/lib/figma-import/processor.ts +116 -0
- package/corpus/templates/design/server/lib/figma-import/render-html.ts +397 -0
- package/corpus/templates/design/server/lib/figma-import/types.ts +60 -0
- package/corpus/templates/design/server/lib/import-design-files.ts +314 -0
- package/corpus/templates/design/server/routes/api/import-design-file.post.ts +1 -0
- package/corpus/templates/design/server/source-workspace.ts +215 -0
- package/corpus/templates/design/shared/design-source-capabilities.ts +5 -5
- package/corpus/templates/design/shared/source-workspace.ts +149 -0
- package/dist/client/session-replay.d.ts +1 -0
- package/dist/client/session-replay.d.ts.map +1 -1
- package/dist/client/session-replay.js +2 -0
- package/dist/client/session-replay.js.map +1 -1
- package/dist/client/use-action.d.ts +5 -1
- package/dist/client/use-action.d.ts.map +1 -1
- package/dist/client/use-action.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +3 -3
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/ssr-handler.d.ts.map +1 -1
- package/dist/server/ssr-handler.js +7 -0
- package/dist/server/ssr-handler.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/vite/action-types-plugin.d.ts.map +1 -1
- package/dist/vite/action-types-plugin.js +6 -2
- package/dist/vite/action-types-plugin.js.map +1 -1
- package/dist/vite/client.d.ts.map +1 -1
- package/dist/vite/client.js +132 -11
- package/dist/vite/client.js.map +1 -1
- package/package.json +1 -1
package/corpus/README.md
CHANGED
package/corpus/core/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
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
|
+
|
|
15
|
+
## 0.84.32
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- da8ac0a: Keep tail-resume reconnect content display-only, recover zero-byte action-prep stalls, and polish Design screen tool labels.
|
|
20
|
+
|
|
21
|
+
## 0.84.31
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- 3190dea: Inline rrweb stylesheet snapshots by default so Analytics session replay captures can play back styled pages without live CSS fetches.
|
|
26
|
+
|
|
3
27
|
## 0.84.30
|
|
4
28
|
|
|
5
29
|
### Patch Changes
|
package/corpus/core/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.84.
|
|
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": {
|
|
@@ -19,6 +19,7 @@ interface RrwebRecordOptions {
|
|
|
19
19
|
emit: (event: ReplayEvent) => void;
|
|
20
20
|
checkoutEveryNth?: number;
|
|
21
21
|
checkoutEveryNms?: number;
|
|
22
|
+
inlineStylesheet?: boolean;
|
|
22
23
|
blockClass?: string | RegExp;
|
|
23
24
|
blockSelector?: string;
|
|
24
25
|
ignoreClass?: string | RegExp;
|
|
@@ -82,6 +83,7 @@ export interface SessionReplayOptions {
|
|
|
82
83
|
maxBatchBytes?: number;
|
|
83
84
|
checkoutEveryNth?: number;
|
|
84
85
|
checkoutEveryNms?: number;
|
|
86
|
+
inlineStylesheet?: boolean;
|
|
85
87
|
blockSelector?: string;
|
|
86
88
|
ignoreSelector?: string;
|
|
87
89
|
maskTextClass?: string | RegExp;
|
|
@@ -135,6 +137,7 @@ interface NormalizedSessionReplayOptions {
|
|
|
135
137
|
maxBatchBytes: number;
|
|
136
138
|
checkoutEveryNth?: number;
|
|
137
139
|
checkoutEveryNms?: number;
|
|
140
|
+
inlineStylesheet: boolean;
|
|
138
141
|
blockSelector: string;
|
|
139
142
|
ignoreSelector: string;
|
|
140
143
|
maskTextClass: string | RegExp;
|
|
@@ -509,6 +512,7 @@ function normalizeOptions(
|
|
|
509
512
|
),
|
|
510
513
|
checkoutEveryNth: options.checkoutEveryNth,
|
|
511
514
|
checkoutEveryNms: options.checkoutEveryNms,
|
|
515
|
+
inlineStylesheet: options.inlineStylesheet ?? true,
|
|
512
516
|
blockSelector: options.blockSelector || DEFAULT_BLOCK_SELECTOR,
|
|
513
517
|
ignoreSelector: options.ignoreSelector || DEFAULT_IGNORE_SELECTOR,
|
|
514
518
|
maskTextClass: options.maskTextClass || DEFAULT_MASK_TEXT_CLASS,
|
|
@@ -1010,6 +1014,7 @@ async function startSessionReplayRecorder(
|
|
|
1010
1014
|
sampling: normalized.eventSampling,
|
|
1011
1015
|
checkoutEveryNth: normalized.checkoutEveryNth,
|
|
1012
1016
|
checkoutEveryNms: normalized.checkoutEveryNms,
|
|
1017
|
+
inlineStylesheet: normalized.inlineStylesheet,
|
|
1013
1018
|
blockSelector: normalized.blockSelector,
|
|
1014
1019
|
ignoreSelector: normalized.ignoreSelector,
|
|
1015
1020
|
maskTextClass: normalized.maskTextClass,
|
|
@@ -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
|
-
|
|
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
|
|
284
|
-
interface
|
|
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
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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);
|
|
@@ -1243,7 +1243,14 @@ function sanitizeCssText(value: string): string {
|
|
|
1243
1243
|
if (!containsStylesheetNetworkLoad(value)) return value;
|
|
1244
1244
|
return value
|
|
1245
1245
|
.replace(/@import\s+(?:url\s*\()?[^;{}]+;?/gi, "")
|
|
1246
|
-
.replace(/\burl\s*\((?:\\.|[^\\)])
|
|
1246
|
+
.replace(/\burl\s*\(\s*((?:\\.|[^\\)])*)\)/gi, sanitizeCssUrlToken);
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
function sanitizeCssUrlToken(match: string, rawValue: string): string {
|
|
1250
|
+
const urlValue = rawValue.trim();
|
|
1251
|
+
const unquoted = urlValue.replace(/^(['"])(.*)\1$/, "$2").trim();
|
|
1252
|
+
if (/^(?:data:|blob:|#)/i.test(unquoted)) return match;
|
|
1253
|
+
return "none";
|
|
1247
1254
|
}
|
|
1248
1255
|
|
|
1249
1256
|
function sanitizeAttributes(attributes: AnyRecord): AnyRecord {
|
|
@@ -1258,6 +1265,11 @@ function sanitizeAttributes(attributes: AnyRecord): AnyRecord {
|
|
|
1258
1265
|
if (style.trim()) next[key] = style;
|
|
1259
1266
|
continue;
|
|
1260
1267
|
}
|
|
1268
|
+
if (normalized === "_csstext") {
|
|
1269
|
+
const cssText = sanitizeCssText(String(value));
|
|
1270
|
+
if (cssText.trim()) next[key] = cssText;
|
|
1271
|
+
continue;
|
|
1272
|
+
}
|
|
1261
1273
|
next[key] = value;
|
|
1262
1274
|
}
|
|
1263
1275
|
return next;
|
|
@@ -191,6 +191,24 @@ patterns live in `.agents/skills/`.
|
|
|
191
191
|
mode can list and resolve local routes now, but file reads/writes remain a
|
|
192
192
|
bridge contract until explicit permission controls are hardened.
|
|
193
193
|
|
|
194
|
+
## Code Workspace
|
|
195
|
+
|
|
196
|
+
- The editor left rail has a wide `code` workspace panel. Open it with
|
|
197
|
+
`navigate --view editor --designId <id> --leftPanel code` and optionally pass
|
|
198
|
+
`fileId`, `filename`, or `screen` to focus a file.
|
|
199
|
+
- Use `list-source-files` to inspect the source workspace. For the MVP the
|
|
200
|
+
backend is `virtual-inline` over SQL-backed `design_files`, exposed as
|
|
201
|
+
`designfs://<designId>/`.
|
|
202
|
+
- Use `read-source-file` for file contents and preserve its `versionHash` before
|
|
203
|
+
writing. Do not return full file content from `view-screen`; it reports only
|
|
204
|
+
active code file metadata and dirty state.
|
|
205
|
+
- Use `preview-source-edit` to show a diff without saving, then
|
|
206
|
+
`apply-source-edit` with the prior `versionHash` to save either a full replace
|
|
207
|
+
or exact replace. These actions update the same inline file state as the UI.
|
|
208
|
+
- Use `resolve-selection-source` when the user has a canvas element selected and
|
|
209
|
+
you need the best matching inline file location/snippet. Localhost/container
|
|
210
|
+
source reads and writes remain future backend work.
|
|
211
|
+
|
|
194
212
|
## Localhost Source Actions
|
|
195
213
|
|
|
196
214
|
- `connect-localhost`: registers or refreshes a localhost source connection
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { defineAction } from "@agent-native/core";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
findSourceWorkspaceFile,
|
|
6
|
+
readLiveSourceFile,
|
|
7
|
+
resolveSourceWorkspace,
|
|
8
|
+
writeInlineSourceFile,
|
|
9
|
+
} from "../server/source-workspace.js";
|
|
10
|
+
import {
|
|
11
|
+
applySourceEdit,
|
|
12
|
+
previewSourceDiff,
|
|
13
|
+
} from "../shared/source-workspace.js";
|
|
14
|
+
|
|
15
|
+
const sourceEditSchema = z.discriminatedUnion("kind", [
|
|
16
|
+
z.object({
|
|
17
|
+
kind: z.literal("full-replace"),
|
|
18
|
+
content: z.string().describe("Complete replacement file content"),
|
|
19
|
+
}),
|
|
20
|
+
z.object({
|
|
21
|
+
kind: z.literal("exact-replace"),
|
|
22
|
+
search: z.string().min(1).describe("Unique exact text to replace"),
|
|
23
|
+
replace: z.string().describe("Replacement text"),
|
|
24
|
+
}),
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
export default defineAction({
|
|
28
|
+
description:
|
|
29
|
+
"Apply a source-file edit through the shared Design source surface. In the " +
|
|
30
|
+
"MVP this writes inline design_files only and rejects stale version hashes.",
|
|
31
|
+
schema: z
|
|
32
|
+
.object({
|
|
33
|
+
designId: z.string().describe("Design project ID"),
|
|
34
|
+
path: z
|
|
35
|
+
.string()
|
|
36
|
+
.optional()
|
|
37
|
+
.describe("Source path/filename, such as index.html"),
|
|
38
|
+
fileId: z.string().optional().describe("Design file ID"),
|
|
39
|
+
edit: sourceEditSchema,
|
|
40
|
+
expectedVersionHash: z
|
|
41
|
+
.string()
|
|
42
|
+
.optional()
|
|
43
|
+
.describe("Hash returned by read-source-file or preview-source-edit"),
|
|
44
|
+
})
|
|
45
|
+
.refine((args) => args.path || args.fileId, {
|
|
46
|
+
message: "Provide either path or fileId.",
|
|
47
|
+
path: ["path"],
|
|
48
|
+
}),
|
|
49
|
+
run: async ({ designId, path, fileId, edit, expectedVersionHash }) => {
|
|
50
|
+
const workspace = await resolveSourceWorkspace(designId, {
|
|
51
|
+
includeContent: true,
|
|
52
|
+
});
|
|
53
|
+
if (workspace.sourceType !== "inline") {
|
|
54
|
+
throw new Error("Only inline Design files are editable in this MVP.");
|
|
55
|
+
}
|
|
56
|
+
const file = findSourceWorkspaceFile(workspace.files, { fileId, path });
|
|
57
|
+
const live = await readLiveSourceFile(file);
|
|
58
|
+
if (
|
|
59
|
+
expectedVersionHash !== undefined &&
|
|
60
|
+
expectedVersionHash !== live.versionHash
|
|
61
|
+
) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
"Source file changed since it was read. Re-read the file and retry.",
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const next = applySourceEdit(live.content, edit);
|
|
68
|
+
const write = await writeInlineSourceFile({
|
|
69
|
+
designId,
|
|
70
|
+
file,
|
|
71
|
+
content: next.content,
|
|
72
|
+
expectedVersionHash: expectedVersionHash ?? live.versionHash,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
designId,
|
|
77
|
+
path: file.filename,
|
|
78
|
+
fileId: file.id,
|
|
79
|
+
backendKind: "virtual-inline",
|
|
80
|
+
changed: write.changed,
|
|
81
|
+
editsApplied: next.editsApplied,
|
|
82
|
+
versionHash: write.versionHash,
|
|
83
|
+
updatedAt: write.updatedAt,
|
|
84
|
+
diff: previewSourceDiff(live.content, next.content),
|
|
85
|
+
};
|
|
86
|
+
},
|
|
87
|
+
});
|
|
@@ -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
|
+
});
|