@agent-native/core 0.90.4 → 0.90.7
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/README.md +34 -7
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +21 -0
- package/corpus/core/docs/content/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/ar-SA/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/de-DE/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/es-ES/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/fr-FR/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/hi-IN/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/ja-JP/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/ko-KR/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/pt-BR/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/zh-CN/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/zh-TW/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/toolkit-agent-ux.mdx +1 -1
- package/corpus/core/docs/content/toolkit-app-adapters.mdx +1 -1
- package/corpus/core/docs/content/toolkit-collaboration-ui.mdx +1 -1
- package/corpus/core/docs/content/toolkit-collaboration.mdx +1 -1
- package/corpus/core/docs/content/toolkit-command-navigation.mdx +2 -2
- package/corpus/core/docs/content/toolkit-comments-review.mdx +1 -1
- package/corpus/core/docs/content/toolkit-history.mdx +1 -1
- package/corpus/core/docs/content/toolkit-observability.mdx +1 -1
- package/corpus/core/docs/content/toolkit-org-team.mdx +1 -1
- package/corpus/core/docs/content/toolkit-resources.mdx +1 -1
- package/corpus/core/docs/content/toolkit-settings.mdx +1 -1
- package/corpus/core/docs/content/toolkit-setup-connections.mdx +1 -1
- package/corpus/core/docs/content/toolkit-sharing-ui.mdx +1 -1
- package/corpus/core/docs/content/toolkit-sharing.mdx +1 -1
- package/corpus/core/docs/content/toolkit-shell-hooks.mdx +1 -1
- package/corpus/core/docs/content/toolkit-ui.mdx +1 -1
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/durable-background.ts +18 -18
- package/corpus/core/src/agent/engine/ai-sdk-engine.ts +8 -2
- package/corpus/core/src/agent/engine/anthropic-engine.ts +11 -1
- package/corpus/core/src/agent/production-agent.ts +22 -11
- package/corpus/core/src/agent/run-manager.ts +17 -6
- package/corpus/core/src/agent/run-store.ts +16 -11
- package/corpus/core/src/cli/design-connect.ts +16 -2
- package/corpus/core/src/client/KeepTabOpenNotice.tsx +20 -15
- package/corpus/core/src/client/RunStuckBanner.tsx +14 -13
- package/corpus/core/src/client/agent-chat-adapter.ts +28 -17
- package/corpus/core/src/client/use-db-sync.ts +99 -1
- package/corpus/core/src/client/use-run-stuck-detection.ts +8 -5
- package/corpus/core/src/collab/client.ts +49 -4
- package/corpus/core/src/deploy/build.ts +103 -0
- package/corpus/core/src/server/core-routes-plugin.ts +137 -8
- package/corpus/templates/clips/changelog/2026-07-07-faster-local-whisper-dictation-start.md +6 -0
- package/corpus/templates/clips/desktop/src-tauri/src/native_speech.rs +311 -31
- package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +2 -0
- package/corpus/templates/content/AGENTS.md +1 -0
- package/corpus/templates/content/actions/_database-source-utils.ts +73 -8
- package/corpus/templates/content/actions/_database-utils.ts +2 -1
- package/corpus/templates/content/actions/prepare-builder-source-review.ts +20 -1
- package/corpus/templates/content/actions/stage-builder-source-bulk-update.ts +663 -0
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +33 -7
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +53 -4
- package/corpus/templates/content/app/components/editor/database/settings.tsx +24 -0
- package/corpus/templates/content/app/hooks/use-content-database.ts +70 -1
- package/corpus/templates/content/app/hooks/use-document-properties.ts +63 -5
- package/corpus/templates/content/app/i18n/zh-TW.ts +3 -0
- package/corpus/templates/content/app/i18n-data.ts +31 -0
- package/corpus/templates/content/changelog/2026-07-01-builder-source-rows-can-be-staged-in-bulk-with-a-reviewable.md +6 -0
- package/corpus/templates/content/changelog/2026-07-07-builder-backed-database-fields-now-edit-like-normal-content-fields.md +6 -0
- package/corpus/templates/content/parity/matrix.md +24 -24
- package/corpus/templates/content/parity/matrix.ts +2 -0
- package/corpus/templates/content/shared/api.ts +59 -4
- package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +9 -0
- package/corpus/templates/design/actions/connect-localhost.ts +26 -3
- package/corpus/templates/design/actions/open-visual-edit.ts +4 -37
- package/corpus/templates/design/actions/read-source-file.ts +28 -0
- package/corpus/templates/design/actions/update-file.ts +76 -7
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +207 -11
- package/corpus/templates/design/app/components/design/EditPanel.tsx +4 -0
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +387 -25
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +1110 -88
- package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +368 -16
- package/corpus/templates/design/app/components/design/edit-panel/fill-gradient-helpers.ts +24 -0
- package/corpus/templates/design/app/components/design/edit-panel/panel-primitives.tsx +27 -6
- package/corpus/templates/design/app/components/design/edit-panel/position-helpers.ts +18 -0
- package/corpus/templates/design/app/components/design/edit-panel/stroke-properties.tsx +5 -4
- package/corpus/templates/design/app/components/design/multi-screen/handle-hit-zones.ts +124 -0
- package/corpus/templates/design/app/components/design/multi-screen/types.ts +23 -0
- package/corpus/templates/design/app/i18n/zh-TW.ts +1 -0
- package/corpus/templates/design/app/i18n-data.ts +19 -0
- package/corpus/templates/design/changelog/2026-07-07-dragging-a-free-floating-element-into-a-layout-column-or-row.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-dragging-a-repeated-list-item-like-a-to-do-card-generated-fr.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-dragging-an-element-onto-a-button-or-chip-s-middle-no-longer.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-elements-dropped-into-another-board-shape-now-stay-exactly-w.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-fixed-drag-reordering-elements-within-a-screen-no-longer-sil.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-fixed-dragging-an-element-from-a-screen-onto-the-canvas-now-.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-fixed-dragging-elements-between-screens-into-ai-generated-co.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-fixed-moving-elements-between-screens-no-longer-bakes-intern.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-fixed-the-cross-screen-drop-indicator-now-points-at-the-corr.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-fixed-the-zoom-menu-no-longer-leaves-the-canvas-unresponsive.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-local-visual-edit-keeps-bridge-access-when-opening-previousl.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-local-visual-edit-now-keeps-the-live-app-connection-authenti.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-text-drawn-on-the-canvas-is-now-readable-white-instead-of-in.md +6 -0
- package/corpus/templates/design/server/source-workspace.ts +27 -3
- package/corpus/templates/design/shared/board-file.ts +359 -0
- package/corpus/templates/design/shared/breakpoint-media.ts +21 -3
- package/corpus/templates/design/shared/code-layer.ts +145 -10
- package/dist/agent/durable-background.d.ts +12 -12
- package/dist/agent/durable-background.js +18 -18
- package/dist/agent/durable-background.js.map +1 -1
- package/dist/agent/engine/ai-sdk-engine.d.ts.map +1 -1
- package/dist/agent/engine/ai-sdk-engine.js +8 -2
- package/dist/agent/engine/ai-sdk-engine.js.map +1 -1
- package/dist/agent/engine/anthropic-engine.d.ts.map +1 -1
- package/dist/agent/engine/anthropic-engine.js +11 -1
- package/dist/agent/engine/anthropic-engine.js.map +1 -1
- package/dist/agent/production-agent.d.ts +2 -2
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +18 -11
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/run-manager.d.ts +7 -1
- package/dist/agent/run-manager.d.ts.map +1 -1
- package/dist/agent/run-manager.js +6 -1
- package/dist/agent/run-manager.js.map +1 -1
- package/dist/agent/run-store.d.ts +1 -1
- package/dist/agent/run-store.d.ts.map +1 -1
- package/dist/agent/run-store.js +15 -10
- package/dist/agent/run-store.js.map +1 -1
- package/dist/cli/design-connect.d.ts.map +1 -1
- package/dist/cli/design-connect.js +12 -2
- package/dist/cli/design-connect.js.map +1 -1
- package/dist/client/KeepTabOpenNotice.d.ts +9 -10
- package/dist/client/KeepTabOpenNotice.d.ts.map +1 -1
- package/dist/client/KeepTabOpenNotice.js +8 -4
- package/dist/client/KeepTabOpenNotice.js.map +1 -1
- package/dist/client/RunStuckBanner.d.ts.map +1 -1
- package/dist/client/RunStuckBanner.js +14 -13
- package/dist/client/RunStuckBanner.js.map +1 -1
- package/dist/client/agent-chat-adapter.d.ts.map +1 -1
- package/dist/client/agent-chat-adapter.js +25 -17
- package/dist/client/agent-chat-adapter.js.map +1 -1
- package/dist/client/use-db-sync.d.ts +18 -0
- package/dist/client/use-db-sync.d.ts.map +1 -1
- package/dist/client/use-db-sync.js +88 -1
- package/dist/client/use-db-sync.js.map +1 -1
- package/dist/client/use-run-stuck-detection.d.ts +1 -1
- package/dist/client/use-run-stuck-detection.d.ts.map +1 -1
- package/dist/client/use-run-stuck-detection.js +6 -4
- package/dist/client/use-run-stuck-detection.js.map +1 -1
- package/dist/collab/client.d.ts.map +1 -1
- package/dist/collab/client.js +43 -4
- package/dist/collab/client.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/deploy/build.d.ts +1 -0
- package/dist/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +57 -0
- package/dist/deploy/build.js.map +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/resources/handlers.d.ts +3 -3
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/core-routes-plugin.d.ts +10 -0
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +103 -9
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/docs/content/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/ar-SA/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/de-DE/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/es-ES/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/fr-FR/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/hi-IN/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/ja-JP/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/ko-KR/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/pt-BR/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/zh-CN/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/zh-TW/agent-native-toolkit.mdx +1 -1
- package/docs/content/toolkit-agent-ux.mdx +1 -1
- package/docs/content/toolkit-app-adapters.mdx +1 -1
- package/docs/content/toolkit-collaboration-ui.mdx +1 -1
- package/docs/content/toolkit-collaboration.mdx +1 -1
- package/docs/content/toolkit-command-navigation.mdx +2 -2
- package/docs/content/toolkit-comments-review.mdx +1 -1
- package/docs/content/toolkit-history.mdx +1 -1
- package/docs/content/toolkit-observability.mdx +1 -1
- package/docs/content/toolkit-org-team.mdx +1 -1
- package/docs/content/toolkit-resources.mdx +1 -1
- package/docs/content/toolkit-settings.mdx +1 -1
- package/docs/content/toolkit-setup-connections.mdx +1 -1
- package/docs/content/toolkit-sharing-ui.mdx +1 -1
- package/docs/content/toolkit-sharing.mdx +1 -1
- package/docs/content/toolkit-shell-hooks.mdx +1 -1
- package/docs/content/toolkit-ui.mdx +1 -1
- package/package.json +1 -1
|
@@ -30,6 +30,34 @@ export default defineAction({
|
|
|
30
30
|
const workspace = await resolveSourceWorkspace(designId, {
|
|
31
31
|
includeContent: true,
|
|
32
32
|
});
|
|
33
|
+
// The board overlay file is a reserved canvas-model document, not a
|
|
34
|
+
// source file the code workbench edits — resolveSourceWorkspace already
|
|
35
|
+
// excludes it from `files`. Asking for "the board's source" (e.g. a UI
|
|
36
|
+
// surface that follows setActiveFileId to whatever screen a cross-screen
|
|
37
|
+
// drop just landed on, including the board) is a legitimate no-op query,
|
|
38
|
+
// not an error: return an empty/readonly placeholder instead of letting
|
|
39
|
+
// findSourceWorkspaceFile throw a 404-as-500 for every such request.
|
|
40
|
+
if (fileId && workspace.boardFileId && fileId === workspace.boardFileId) {
|
|
41
|
+
return {
|
|
42
|
+
designId,
|
|
43
|
+
path: "__board__.html",
|
|
44
|
+
displayName: "__board__.html",
|
|
45
|
+
fileId,
|
|
46
|
+
sourceType: workspace.sourceType,
|
|
47
|
+
backendKind: "virtual-inline",
|
|
48
|
+
readonly: true,
|
|
49
|
+
language: "html",
|
|
50
|
+
content: "",
|
|
51
|
+
versionHash: "",
|
|
52
|
+
updatedAt: null,
|
|
53
|
+
provenance: {
|
|
54
|
+
kind: "design-file" as const,
|
|
55
|
+
designId,
|
|
56
|
+
fileId,
|
|
57
|
+
filename: "__board__.html",
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
33
61
|
const file = findSourceWorkspaceFile(workspace.files, { fileId, path });
|
|
34
62
|
const live = await readLiveSourceFile(file);
|
|
35
63
|
return {
|
|
@@ -33,8 +33,14 @@ function rowsAffected(result: unknown): number | undefined {
|
|
|
33
33
|
* `skippedStaleMirror?: true` is added to the result ONLY in the narrow
|
|
34
34
|
* SQL-mirror-only staleness case: `content` was provided, `syncCollab` was
|
|
35
35
|
* explicitly `false`, a live collaboration document exists for this file,
|
|
36
|
-
* and the caller's `expectedVersionHash`
|
|
37
|
-
*
|
|
36
|
+
* and the caller's `expectedVersionHash` matches NEITHER the live collab
|
|
37
|
+
* text, NOR the `content` being written (an own edit that raced ahead via
|
|
38
|
+
* Yjs), NOR the current SQL mirror content. A caller whose hash matches the
|
|
39
|
+
* current mirror is the mirror column's own lineage (mirror-lineage rescue):
|
|
40
|
+
* it proceeds instead of skipping, advancing the mirror AND diff-merging its
|
|
41
|
+
* content into the live doc.
|
|
42
|
+
* In the genuinely-stale case the content column is intentionally left
|
|
43
|
+
* untouched (the live
|
|
38
44
|
* collab document remains the source of truth) while any `filename`/
|
|
39
45
|
* `fileType` updates in the same call still apply, and the action returns
|
|
40
46
|
* success instead of throwing. The field is omitted (not `false`) in every
|
|
@@ -163,7 +169,24 @@ export default defineAction({
|
|
|
163
169
|
// trying to sync into collab in the first place. Every other
|
|
164
170
|
// expectedVersionHash combination (syncCollab true/default, or no live
|
|
165
171
|
// collab state, or a matching hash, or no hash at all) is UNCHANGED.
|
|
172
|
+
//
|
|
173
|
+
// Own-edit false-positive fix: a single client's own edit reaches the
|
|
174
|
+
// live collab doc via TWO independent, unordered paths — the Yjs
|
|
175
|
+
// update (~80ms client debounce, applied to the server's in-memory doc
|
|
176
|
+
// as soon as its POST lands) and this guarded update-file call (~400ms
|
|
177
|
+
// client debounce). The Yjs path usually wins the race, so by the time
|
|
178
|
+
// this call's hash check runs, `liveContent` often already equals the
|
|
179
|
+
// very `content` this call is trying to write — that is NOT a
|
|
180
|
+
// divergent concurrent edit, it's the same edit having arrived early
|
|
181
|
+
// by a different transport. Comparing hashes first would reject that
|
|
182
|
+
// as "stale" and permanently skip the SQL mirror write (there is no
|
|
183
|
+
// background job that later reconciles design_files.content from the
|
|
184
|
+
// live collab doc — see hasCollabState below), silently losing writes
|
|
185
|
+
// on every edit after the first in a session. Check content equality
|
|
186
|
+
// BEFORE the hash comparison so this exact-match case always proceeds
|
|
187
|
+
// as a normal write instead of hitting either the skip or throw path.
|
|
166
188
|
let skipContentWrite = false;
|
|
189
|
+
let mirrorLineageCollabSync = false;
|
|
167
190
|
if (expectedVersionHash !== undefined && content !== undefined) {
|
|
168
191
|
const collabExists = await hasCollabState(id);
|
|
169
192
|
let liveContent: string;
|
|
@@ -177,10 +200,52 @@ export default defineAction({
|
|
|
177
200
|
.limit(1);
|
|
178
201
|
liveContent = current?.content ?? "";
|
|
179
202
|
}
|
|
180
|
-
if (
|
|
203
|
+
if (
|
|
204
|
+
liveContent !== content &&
|
|
205
|
+
sourceContentHash(liveContent) !== expectedVersionHash
|
|
206
|
+
) {
|
|
181
207
|
if (syncCollab === false && collabExists) {
|
|
182
|
-
|
|
183
|
-
|
|
208
|
+
// Mirror-lineage rescue (sequential-edit data-loss fix, verified
|
|
209
|
+
// live): the client's Yjs transact and its guarded update-file
|
|
210
|
+
// call ride two independent transports, and the Yjs pipe can lag
|
|
211
|
+
// or silently die — reproduced with a live collab doc that never
|
|
212
|
+
// received EITHER of two sequential scrub edits while the HTTP
|
|
213
|
+
// saves advanced the SQL mirror normally. Comparing the caller
|
|
214
|
+
// only against that stale live text mis-classified the SECOND
|
|
215
|
+
// save as a divergent writer and silently dropped it. When the
|
|
216
|
+
// caller's expectedVersionHash matches the CURRENT SQL mirror,
|
|
217
|
+
// the caller is the mirror's own uninterrupted lineage (a plain
|
|
218
|
+
// CAS success against the column this write updates) while the
|
|
219
|
+
// live doc is the diverging party — a dead/lagging client Yjs
|
|
220
|
+
// pipe or a concurrent live-only editor. Do NOT skip, and do NOT
|
|
221
|
+
// silently drop either side: proceed with the mirror write AND
|
|
222
|
+
// push `content` through the collab layer exactly like
|
|
223
|
+
// syncCollab:true does (mirrorLineageCollabSync below). The
|
|
224
|
+
// applyText char-diff merge folds the caller's change into the
|
|
225
|
+
// live doc as a CRDT diff, so no one's edits are dropped: the
|
|
226
|
+
// mirror advances with the caller, and the live doc receives the
|
|
227
|
+
// caller's change as a diff-merge that preserves any other
|
|
228
|
+
// editor's live edits. Only callers matching NEITHER the live
|
|
229
|
+
// text NOR the mirror are genuinely stale and still hit the skip
|
|
230
|
+
// below.
|
|
231
|
+
const [mirrorRow] = await db
|
|
232
|
+
.select({ content: schema.designFiles.content })
|
|
233
|
+
.from(schema.designFiles)
|
|
234
|
+
.where(eq(schema.designFiles.id, id))
|
|
235
|
+
.limit(1);
|
|
236
|
+
if (
|
|
237
|
+
sourceContentHash(mirrorRow?.content ?? "") ===
|
|
238
|
+
expectedVersionHash
|
|
239
|
+
) {
|
|
240
|
+
// Caller is exactly at the persisted mirror's tip — the live
|
|
241
|
+
// collab doc is the lagging party, not the caller. Write the
|
|
242
|
+
// mirror normally and also sync the caller's content into the
|
|
243
|
+
// live doc via the collab diff-merge below.
|
|
244
|
+
mirrorLineageCollabSync = true;
|
|
245
|
+
} else {
|
|
246
|
+
skipContentWrite = true;
|
|
247
|
+
skippedStaleMirror = true;
|
|
248
|
+
}
|
|
184
249
|
} else {
|
|
185
250
|
throw new Error(
|
|
186
251
|
"File changed since it was read. Re-read the file and retry.",
|
|
@@ -263,8 +328,12 @@ export default defineAction({
|
|
|
263
328
|
}
|
|
264
329
|
}
|
|
265
330
|
|
|
266
|
-
// Push content through the collab layer so live editors see the change
|
|
267
|
-
|
|
331
|
+
// Push content through the collab layer so live editors see the change.
|
|
332
|
+
// mirrorLineageCollabSync: a syncCollab:false caller whose hash matched
|
|
333
|
+
// the current SQL mirror (mirror-lineage rescue) also syncs here, so a
|
|
334
|
+
// dead/lagging live doc receives the caller's change as a CRDT
|
|
335
|
+
// diff-merge instead of silently diverging from the mirror.
|
|
336
|
+
if (content !== undefined && (syncCollab || mirrorLineageCollabSync)) {
|
|
268
337
|
const collabExists = await hasCollabState(id);
|
|
269
338
|
if (collabExists) {
|
|
270
339
|
await applyText(id, content, "content", "agent");
|
|
@@ -865,9 +865,16 @@ function snapshotEndpointUrl(bridgeUrl: string, previewUrl: string): string {
|
|
|
865
865
|
return endpoint.toString();
|
|
866
866
|
}
|
|
867
867
|
|
|
868
|
-
function liveEditEndpointUrl(
|
|
868
|
+
export function liveEditEndpointUrl(
|
|
869
|
+
bridgeUrl: string,
|
|
870
|
+
previewUrl: string,
|
|
871
|
+
options?: { includeEditorBridge?: boolean },
|
|
872
|
+
): string {
|
|
869
873
|
const endpoint = new URL("/live-edit", bridgeUrl);
|
|
870
874
|
endpoint.searchParams.set("url", previewUrl);
|
|
875
|
+
if (options?.includeEditorBridge === false) {
|
|
876
|
+
endpoint.searchParams.set("bridge", "0");
|
|
877
|
+
}
|
|
871
878
|
return endpoint.toString();
|
|
872
879
|
}
|
|
873
880
|
|
|
@@ -981,9 +988,38 @@ export function getEmbeddedIframeBackgroundColor(args: {
|
|
|
981
988
|
: (args.embeddedFrameBackground ?? "transparent");
|
|
982
989
|
}
|
|
983
990
|
|
|
991
|
+
/**
|
|
992
|
+
* CSS rule that shifts embedded-frame (board) content so board-coordinate
|
|
993
|
+
* (0,0) lands at the center of the board iframe's oversized surface.
|
|
994
|
+
*
|
|
995
|
+
* The selector is deliberately scoped to DIRECT `<body>` children only.
|
|
996
|
+
* `translate` composes per element, so a blanket
|
|
997
|
+
* `[data-agent-native-node-id]{translate:…}` rule shifted every NESTED
|
|
998
|
+
* node-id-bearing descendant by the surface offset AGAIN (+65536px per
|
|
999
|
+
* nesting level) — any board child nested inside another board primitive
|
|
1000
|
+
* rendered tens of thousands of px outside its parent (i.e. visually
|
|
1001
|
+
* vanished) even when its persisted parent-relative left/top were perfectly
|
|
1002
|
+
* correct, and every rect-based coordinate computation on nested board
|
|
1003
|
+
* content was poisoned by the extra offset.
|
|
1004
|
+
*
|
|
1005
|
+
* Scoping (rather than wrapping content in a single offset container) is the
|
|
1006
|
+
* robust fix here because board elements are DOCUMENTED direct `<body>`
|
|
1007
|
+
* children (see shared/board-file.ts module doc + emptyBoardHtml): every
|
|
1008
|
+
* creation path appends at body level (appendCanvasPrimitiveToHtml,
|
|
1009
|
+
* boardObjectEntryToHtmlFragment, moveNodeBetweenDocuments' body-append),
|
|
1010
|
+
* and several consumers key on that structure with `body > …` selectors
|
|
1011
|
+
* (BOARD_SURFACE_RENDER_STYLE) and depth-1 walks
|
|
1012
|
+
* (backfillBoardPrimitiveMarkers, getBoardContentLayerSignature). A wrapper
|
|
1013
|
+
* element would silently break all of those structural contracts.
|
|
1014
|
+
*
|
|
1015
|
+
* With this rule, the offset applies exactly once — to top-level board
|
|
1016
|
+
* elements — and nested children position purely via parent-relative
|
|
1017
|
+
* left/top inside their (absolutely positioned) parent, like any normal
|
|
1018
|
+
* CSS containing-block hierarchy.
|
|
1019
|
+
*/
|
|
984
1020
|
function embeddedContentOffsetStyle(x: number, y: number): string {
|
|
985
1021
|
if (x === 0 && y === 0) return "";
|
|
986
|
-
return `<style data-agent-native-content-offset>[data-agent-native-node-id]{translate:${Math.round(x)}px ${Math.round(y)}px;}</style>`;
|
|
1022
|
+
return `<style data-agent-native-content-offset>body > [data-agent-native-node-id]{translate:${Math.round(x)}px ${Math.round(y)}px;}</style>`;
|
|
987
1023
|
}
|
|
988
1024
|
|
|
989
1025
|
function injectEmbeddedFrameStyle(content: string, style: string): string {
|
|
@@ -1039,6 +1075,60 @@ export interface IframeContextMenuPayload {
|
|
|
1039
1075
|
info?: ElementInfo | null;
|
|
1040
1076
|
}
|
|
1041
1077
|
|
|
1078
|
+
/**
|
|
1079
|
+
* Creation-race keystroke routing: after the host calls `beginTextEdit(nodeId)`
|
|
1080
|
+
* for a JUST-created text element, there is a window (persist round-trip +
|
|
1081
|
+
* bridge activation) where the browser focus still sits on the HOST document.
|
|
1082
|
+
* Keystrokes typed in that window used to fall into host keyboard shortcuts —
|
|
1083
|
+
* letters switched tools, digits zoomed, and Delete/Backspace DELETED the
|
|
1084
|
+
* selected layer/screen — while the characters themselves were silently lost.
|
|
1085
|
+
*
|
|
1086
|
+
* While a text edit is pending, every host keydown is routed through this
|
|
1087
|
+
* pure helper:
|
|
1088
|
+
* - printable characters are BUFFERED (replayed into the editable once the
|
|
1089
|
+
* bridge reports the session active) and swallowed,
|
|
1090
|
+
* - Backspace edits the buffer, Delete/Enter/Tab/arrows are swallowed so they
|
|
1091
|
+
* can never reach host layer-deletion/navigation,
|
|
1092
|
+
* - Escape clears the pending buffer (user aborted) and is swallowed,
|
|
1093
|
+
* - IME composition and Cmd/Ctrl chords (undo!) pass through untouched.
|
|
1094
|
+
*
|
|
1095
|
+
* Exported for unit tests (DesignCanvas.pending-text-edit.test.ts).
|
|
1096
|
+
*/
|
|
1097
|
+
export type PendingTextEditKeyAction =
|
|
1098
|
+
| { action: "buffer"; char: string }
|
|
1099
|
+
| { action: "drop-last" }
|
|
1100
|
+
| { action: "swallow" }
|
|
1101
|
+
| { action: "clear-and-swallow" }
|
|
1102
|
+
| { action: "pass" };
|
|
1103
|
+
|
|
1104
|
+
export function routePendingTextEditKey(e: {
|
|
1105
|
+
key: string;
|
|
1106
|
+
metaKey?: boolean;
|
|
1107
|
+
ctrlKey?: boolean;
|
|
1108
|
+
altKey?: boolean;
|
|
1109
|
+
isComposing?: boolean;
|
|
1110
|
+
}): PendingTextEditKeyAction {
|
|
1111
|
+
if (e.isComposing) return { action: "pass" };
|
|
1112
|
+
if (e.metaKey || e.ctrlKey) return { action: "pass" };
|
|
1113
|
+
if (e.key === "Escape") return { action: "clear-and-swallow" };
|
|
1114
|
+
if (e.key === "Backspace") return { action: "drop-last" };
|
|
1115
|
+
if (
|
|
1116
|
+
e.key === "Delete" ||
|
|
1117
|
+
e.key === "Enter" ||
|
|
1118
|
+
e.key === "Tab" ||
|
|
1119
|
+
e.key.startsWith("Arrow")
|
|
1120
|
+
) {
|
|
1121
|
+
return { action: "swallow" };
|
|
1122
|
+
}
|
|
1123
|
+
if (e.key.length === 1) return { action: "buffer", char: e.key };
|
|
1124
|
+
return { action: "pass" };
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
/** How long a pending text edit may wait for bridge activation before the
|
|
1128
|
+
* keystroke routing above stands down (matches the bridge's own ~2s
|
|
1129
|
+
* begin-text-edit retry window, plus round-trip slack). */
|
|
1130
|
+
export const PENDING_TEXT_EDIT_TIMEOUT_MS = 3000;
|
|
1131
|
+
|
|
1042
1132
|
export function DesignCanvas({
|
|
1043
1133
|
content,
|
|
1044
1134
|
contentKey,
|
|
@@ -1268,19 +1358,28 @@ export function DesignCanvas({
|
|
|
1268
1358
|
() => contentHash(liveEditBridgeScript),
|
|
1269
1359
|
[liveEditBridgeScript],
|
|
1270
1360
|
);
|
|
1271
|
-
const
|
|
1361
|
+
const hasLiveEditExternalFrame =
|
|
1362
|
+
sourceType === "localhost" && Boolean(bridgeUrl && rawExternalPreviewUrl);
|
|
1363
|
+
const usesLiveEditEditorBridge =
|
|
1272
1364
|
sourceType === "localhost" &&
|
|
1273
1365
|
Boolean(bridgeUrl && rawExternalPreviewUrl) &&
|
|
1274
1366
|
!interactMode &&
|
|
1275
1367
|
!readOnly;
|
|
1276
1368
|
const liveEditBridgeRegistered =
|
|
1277
|
-
|
|
1369
|
+
usesLiveEditEditorBridge &&
|
|
1278
1370
|
registeredLiveEditBridgeKey === liveEditBridgeKey;
|
|
1279
1371
|
const requiresEditableExternalSnapshot = false;
|
|
1280
1372
|
const liveEditExternalPreviewUrl =
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1373
|
+
hasLiveEditExternalFrame &&
|
|
1374
|
+
bridgeUrl &&
|
|
1375
|
+
rawExternalPreviewUrl &&
|
|
1376
|
+
interactMode
|
|
1377
|
+
? liveEditEndpointUrl(bridgeUrl, rawExternalPreviewUrl, {
|
|
1378
|
+
includeEditorBridge: false,
|
|
1379
|
+
})
|
|
1380
|
+
: liveEditBridgeRegistered && bridgeUrl && rawExternalPreviewUrl
|
|
1381
|
+
? liveEditEndpointUrl(bridgeUrl, rawExternalPreviewUrl)
|
|
1382
|
+
: null;
|
|
1284
1383
|
const iframeRenderContent =
|
|
1285
1384
|
activeExternalSnapshotHtml ??
|
|
1286
1385
|
(requiresEditableExternalSnapshot ? "" : renderedContent);
|
|
@@ -1292,7 +1391,7 @@ export function DesignCanvas({
|
|
|
1292
1391
|
const waitingForEditableExternalSnapshot =
|
|
1293
1392
|
requiresEditableExternalSnapshot && !activeExternalSnapshotHtml;
|
|
1294
1393
|
const waitingForLiveEditBridge =
|
|
1295
|
-
|
|
1394
|
+
usesLiveEditEditorBridge && !liveEditBridgeRegistered;
|
|
1296
1395
|
zoomRef.current = zoom;
|
|
1297
1396
|
runtimeReplacementContentRef.current = runtimeReplacementContent;
|
|
1298
1397
|
runtimeReplacementKeyRef.current = runtimeReplacementKey;
|
|
@@ -1302,7 +1401,7 @@ export function DesignCanvas({
|
|
|
1302
1401
|
}, [onExternalContentSnapshot]);
|
|
1303
1402
|
|
|
1304
1403
|
useEffect(() => {
|
|
1305
|
-
if (!
|
|
1404
|
+
if (!usesLiveEditEditorBridge || !bridgeUrl) {
|
|
1306
1405
|
setRegisteredLiveEditBridgeKey(null);
|
|
1307
1406
|
return;
|
|
1308
1407
|
}
|
|
@@ -1345,7 +1444,7 @@ export function DesignCanvas({
|
|
|
1345
1444
|
bridgeToken,
|
|
1346
1445
|
liveEditBridgeKey,
|
|
1347
1446
|
liveEditBridgeScript,
|
|
1348
|
-
|
|
1447
|
+
usesLiveEditEditorBridge,
|
|
1349
1448
|
]);
|
|
1350
1449
|
|
|
1351
1450
|
useEffect(() => {
|
|
@@ -1884,7 +1983,44 @@ export function DesignCanvas({
|
|
|
1884
1983
|
}
|
|
1885
1984
|
return;
|
|
1886
1985
|
}
|
|
1986
|
+
if (e.data.type === "text-edit-pending") {
|
|
1987
|
+
// The bridge is waiting for a just-created node before it can enter
|
|
1988
|
+
// text-edit mode (begin-text-edit arrived first). Arm the host-side
|
|
1989
|
+
// keystroke buffer for the window — repeated pending:true re-posts
|
|
1990
|
+
// for the same node (DesignEditor's T6 retry loop) only extend the
|
|
1991
|
+
// wait, never reset an in-progress buffer.
|
|
1992
|
+
const pendingNodeId =
|
|
1993
|
+
typeof e.data.nodeId === "string" ? e.data.nodeId : "";
|
|
1994
|
+
const currentPending = pendingTextEditRef.current;
|
|
1995
|
+
if (e.data.pending && pendingNodeId) {
|
|
1996
|
+
if (currentPending && currentPending.nodeId === pendingNodeId) {
|
|
1997
|
+
currentPending.startedAt = Date.now();
|
|
1998
|
+
} else {
|
|
1999
|
+
pendingTextEditRef.current = {
|
|
2000
|
+
nodeId: pendingNodeId,
|
|
2001
|
+
buffer: "",
|
|
2002
|
+
startedAt: Date.now(),
|
|
2003
|
+
};
|
|
2004
|
+
}
|
|
2005
|
+
} else if (currentPending?.nodeId === pendingNodeId) {
|
|
2006
|
+
pendingTextEditRef.current = null;
|
|
2007
|
+
}
|
|
2008
|
+
return;
|
|
2009
|
+
}
|
|
1887
2010
|
if (e.data.type === "text-editing-state") {
|
|
2011
|
+
// Creation-race replay: the bridge just armed the session for the
|
|
2012
|
+
// node we were waiting on — flush any keystrokes the host buffered
|
|
2013
|
+
// during the round-trip window into the now-live editable.
|
|
2014
|
+
if (e.data.active && pendingTextEditRef.current) {
|
|
2015
|
+
const pendingBuffer = pendingTextEditRef.current.buffer;
|
|
2016
|
+
pendingTextEditRef.current = null;
|
|
2017
|
+
if (pendingBuffer) {
|
|
2018
|
+
postOneShotBridgeMessage({
|
|
2019
|
+
type: "text-edit-insert-text",
|
|
2020
|
+
text: pendingBuffer,
|
|
2021
|
+
});
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
1888
2024
|
onTextEditingStateChange?.({
|
|
1889
2025
|
active: Boolean(e.data.active),
|
|
1890
2026
|
selector:
|
|
@@ -2075,6 +2211,7 @@ export function DesignCanvas({
|
|
|
2075
2211
|
bridgeUrl,
|
|
2076
2212
|
fusionUrl,
|
|
2077
2213
|
flushPendingOneShotMessages,
|
|
2214
|
+
postOneShotBridgeMessage,
|
|
2078
2215
|
]);
|
|
2079
2216
|
|
|
2080
2217
|
const replayIframeEditorState = useCallback(() => {
|
|
@@ -2374,6 +2511,18 @@ export function DesignCanvas({
|
|
|
2374
2511
|
(nodeId: string) => {
|
|
2375
2512
|
const iframe = iframeRef.current;
|
|
2376
2513
|
if (!iframe?.contentWindow || !nodeId) return;
|
|
2514
|
+
// Arm the creation-race keystroke buffer (see routePendingTextEditKey):
|
|
2515
|
+
// until the bridge reports text-editing-state active for this command,
|
|
2516
|
+
// host keystrokes are buffered/swallowed instead of hitting host
|
|
2517
|
+
// shortcuts, then replayed into the editable. Re-arming for the same
|
|
2518
|
+
// node preserves an in-progress buffer.
|
|
2519
|
+
if (pendingTextEditRef.current?.nodeId !== nodeId) {
|
|
2520
|
+
pendingTextEditRef.current = {
|
|
2521
|
+
nodeId,
|
|
2522
|
+
buffer: "",
|
|
2523
|
+
startedAt: Date.now(),
|
|
2524
|
+
};
|
|
2525
|
+
}
|
|
2377
2526
|
postOneShotBridgeMessage({
|
|
2378
2527
|
type: "begin-text-edit",
|
|
2379
2528
|
nodeId,
|
|
@@ -2383,6 +2532,53 @@ export function DesignCanvas({
|
|
|
2383
2532
|
[postOneShotBridgeMessage],
|
|
2384
2533
|
);
|
|
2385
2534
|
|
|
2535
|
+
// Creation-race keystroke routing (host side). Active only while a
|
|
2536
|
+
// beginTextEdit() command is pending; see routePendingTextEditKey's doc
|
|
2537
|
+
// comment for the exact policy. Capture-phase on window so it runs before
|
|
2538
|
+
// the editor's own shortcut handlers.
|
|
2539
|
+
const pendingTextEditRef = useRef<{
|
|
2540
|
+
nodeId: string;
|
|
2541
|
+
buffer: string;
|
|
2542
|
+
startedAt: number;
|
|
2543
|
+
} | null>(null);
|
|
2544
|
+
useEffect(() => {
|
|
2545
|
+
function onPendingTextEditKeyDown(e: KeyboardEvent) {
|
|
2546
|
+
const pending = pendingTextEditRef.current;
|
|
2547
|
+
if (!pending) return;
|
|
2548
|
+
if (Date.now() - pending.startedAt > PENDING_TEXT_EDIT_TIMEOUT_MS) {
|
|
2549
|
+
pendingTextEditRef.current = null;
|
|
2550
|
+
return;
|
|
2551
|
+
}
|
|
2552
|
+
const routed = routePendingTextEditKey(e);
|
|
2553
|
+
if (routed.action === "pass") return;
|
|
2554
|
+
e.preventDefault();
|
|
2555
|
+
e.stopPropagation();
|
|
2556
|
+
e.stopImmediatePropagation();
|
|
2557
|
+
if (routed.action === "buffer") {
|
|
2558
|
+
pending.buffer += routed.char;
|
|
2559
|
+
} else if (routed.action === "drop-last") {
|
|
2560
|
+
pending.buffer = pending.buffer.slice(0, -1);
|
|
2561
|
+
} else if (routed.action === "clear-and-swallow") {
|
|
2562
|
+
pendingTextEditRef.current = null;
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
function onPendingTextEditPointerDown() {
|
|
2566
|
+
// The user clicked somewhere else in the host — stand down so buffered
|
|
2567
|
+
// keys are never replayed into an edit they've abandoned.
|
|
2568
|
+
pendingTextEditRef.current = null;
|
|
2569
|
+
}
|
|
2570
|
+
window.addEventListener("keydown", onPendingTextEditKeyDown, true);
|
|
2571
|
+
window.addEventListener("pointerdown", onPendingTextEditPointerDown, true);
|
|
2572
|
+
return () => {
|
|
2573
|
+
window.removeEventListener("keydown", onPendingTextEditKeyDown, true);
|
|
2574
|
+
window.removeEventListener(
|
|
2575
|
+
"pointerdown",
|
|
2576
|
+
onPendingTextEditPointerDown,
|
|
2577
|
+
true,
|
|
2578
|
+
);
|
|
2579
|
+
};
|
|
2580
|
+
}, []);
|
|
2581
|
+
|
|
2386
2582
|
const sendStyleChange = useCallback(
|
|
2387
2583
|
(
|
|
2388
2584
|
selector: string,
|
|
@@ -3004,7 +3200,7 @@ export function DesignCanvas({
|
|
|
3004
3200
|
</div>
|
|
3005
3201
|
) : null}
|
|
3006
3202
|
{waitingForEditableExternalSnapshot || waitingForLiveEditBridge ? (
|
|
3007
|
-
<div className="pointer-events-
|
|
3203
|
+
<div className="pointer-events-auto absolute inset-0 z-10 flex items-center justify-center bg-background/85 px-4 text-center text-sm text-muted-foreground">
|
|
3008
3204
|
{waitingForLiveEditBridge ? (
|
|
3009
3205
|
<div className="max-w-[28rem] rounded-md border bg-card px-4 py-3 shadow-sm">
|
|
3010
3206
|
{
|
|
@@ -213,6 +213,7 @@ import {
|
|
|
213
213
|
parseGradientLayer,
|
|
214
214
|
removeFillLayerAtIndex,
|
|
215
215
|
SOLID_FILL_ID,
|
|
216
|
+
solidToGradientPatch,
|
|
216
217
|
splitCssLayers,
|
|
217
218
|
withLayerSizeMarker,
|
|
218
219
|
} from "./edit-panel/fill-gradient-helpers";
|
|
@@ -356,11 +357,14 @@ export {
|
|
|
356
357
|
truncateOpeningTag,
|
|
357
358
|
};
|
|
358
359
|
export {
|
|
360
|
+
averageGradientOpacity,
|
|
359
361
|
buildGradientLayer,
|
|
362
|
+
defaultGradientStops,
|
|
360
363
|
isLayerHiddenBySize,
|
|
361
364
|
joinCssLayers,
|
|
362
365
|
parseGradientLayer,
|
|
363
366
|
removeFillLayerAtIndex,
|
|
367
|
+
solidToGradientPatch,
|
|
364
368
|
splitCssLayers,
|
|
365
369
|
withLayerSizeMarker,
|
|
366
370
|
type FillLayerArrays,
|