@agent-native/core 0.92.6 → 0.92.8
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 +18 -0
- package/corpus/core/docs/content/a2a-protocol.mdx +12 -0
- package/corpus/core/docs/content/locales/ar-SA/a2a-protocol.mdx +12 -0
- package/corpus/core/docs/content/locales/ar-SA/pr-visual-recap.mdx +3 -3
- package/corpus/core/docs/content/locales/ar-SA/pure-agent-apps.mdx +4 -0
- package/corpus/core/docs/content/locales/de-DE/a2a-protocol.mdx +12 -0
- package/corpus/core/docs/content/locales/de-DE/pr-visual-recap.mdx +3 -3
- package/corpus/core/docs/content/locales/de-DE/pure-agent-apps.mdx +4 -0
- package/corpus/core/docs/content/locales/es-ES/a2a-protocol.mdx +12 -0
- package/corpus/core/docs/content/locales/es-ES/pr-visual-recap.mdx +3 -3
- package/corpus/core/docs/content/locales/es-ES/pure-agent-apps.mdx +4 -0
- package/corpus/core/docs/content/locales/fr-FR/a2a-protocol.mdx +12 -0
- package/corpus/core/docs/content/locales/fr-FR/pr-visual-recap.mdx +3 -3
- package/corpus/core/docs/content/locales/fr-FR/pure-agent-apps.mdx +4 -0
- package/corpus/core/docs/content/locales/hi-IN/a2a-protocol.mdx +12 -0
- package/corpus/core/docs/content/locales/hi-IN/pr-visual-recap.mdx +3 -3
- package/corpus/core/docs/content/locales/hi-IN/pure-agent-apps.mdx +4 -0
- package/corpus/core/docs/content/locales/ja-JP/a2a-protocol.mdx +12 -0
- package/corpus/core/docs/content/locales/ja-JP/pr-visual-recap.mdx +3 -3
- package/corpus/core/docs/content/locales/ja-JP/pure-agent-apps.mdx +4 -0
- package/corpus/core/docs/content/locales/ko-KR/a2a-protocol.mdx +12 -0
- package/corpus/core/docs/content/locales/ko-KR/pr-visual-recap.mdx +3 -3
- package/corpus/core/docs/content/locales/ko-KR/pure-agent-apps.mdx +4 -0
- package/corpus/core/docs/content/locales/pt-BR/a2a-protocol.mdx +12 -0
- package/corpus/core/docs/content/locales/pt-BR/pr-visual-recap.mdx +3 -3
- package/corpus/core/docs/content/locales/pt-BR/pure-agent-apps.mdx +4 -0
- package/corpus/core/docs/content/locales/zh-CN/a2a-protocol.mdx +12 -0
- package/corpus/core/docs/content/locales/zh-CN/pr-visual-recap.mdx +3 -3
- package/corpus/core/docs/content/locales/zh-CN/pure-agent-apps.mdx +4 -0
- package/corpus/core/docs/content/locales/zh-TW/a2a-protocol.mdx +12 -0
- package/corpus/core/docs/content/locales/zh-TW/pr-visual-recap.mdx +3 -3
- package/corpus/core/docs/content/locales/zh-TW/pure-agent-apps.mdx +4 -0
- package/corpus/core/docs/content/pr-visual-recap.mdx +3 -3
- package/corpus/core/docs/content/pure-agent-apps.mdx +22 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/engine/builtin.ts +1 -1
- package/corpus/core/src/agent/model-config.ts +30 -30
- package/corpus/core/src/agent/production-agent.ts +129 -7
- package/corpus/core/src/cli/design-connect.ts +438 -83
- package/corpus/core/src/cli/recap.ts +2 -2
- package/corpus/core/src/cli/sync-builder-starter-manifest.ts +1 -1
- package/corpus/core/src/client/AgentPanel.tsx +8 -1
- package/corpus/core/src/client/agent-chat-adapter.ts +12 -7
- package/corpus/core/src/client/composer/TiptapComposer.tsx +1 -1
- package/corpus/core/src/client/index.ts +4 -0
- package/corpus/core/src/client/org/TeamPage.tsx +41 -26
- package/corpus/core/src/client/use-action.ts +113 -1
- package/corpus/core/src/db/client.ts +12 -2
- package/corpus/core/src/db/create-get-db.ts +4 -1
- package/corpus/core/src/scripts/agent-engines/manage-agent-engine.ts +1 -1
- package/corpus/core/src/scripts/agent-engines/set-agent-engine.ts +1 -1
- package/corpus/core/src/server/transcribe-voice.ts +1 -1
- package/corpus/core/src/usage/store.ts +13 -4
- package/corpus/core/src/vite/client.ts +38 -1
- package/corpus/templates/analytics/AGENTS.md +3 -0
- package/corpus/templates/analytics/actions/get-data-program.ts +78 -0
- package/corpus/templates/analytics/actions/list-data-programs.ts +49 -0
- package/corpus/templates/analytics/app/global.css +11 -0
- package/corpus/templates/analytics/app/hooks/use-dashboard-chat-context.ts +163 -17
- package/corpus/templates/analytics/app/i18n/zh-TW.ts +2 -0
- package/corpus/templates/analytics/app/i18n-data.ts +20 -0
- package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/ChartCard.tsx +96 -3
- package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +3 -1
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +61 -4
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +40 -2
- package/corpus/templates/analytics/changelog/2026-07-09-analytics-no-longer-forgets-successful-data-queries-when-a-l.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-09-dashboard-charts-keep-consistent-spacing-when-they-stack-ver.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-09-dashboard-charts-tables-and-extensions-can-be-selected-and-d.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-09-long-running-analytics-requests-now-recover-automatically-wh.md +6 -0
- package/corpus/templates/analytics/server/db/index.ts +6 -0
- package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +1 -7
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +102 -15
- package/corpus/templates/content/app/hooks/use-content-database.ts +52 -0
- package/corpus/templates/content/app/hooks/use-document-properties.ts +50 -6
- package/corpus/templates/content/app/i18n/zh-TW.ts +2 -0
- package/corpus/templates/content/app/i18n-data.ts +28 -0
- package/corpus/templates/content/changelog/2026-07-09-notion-conflict-warnings-no-longer-flash-during-normal-synce.md +6 -0
- package/corpus/templates/content/changelog/2026-07-09-property-menus-and-delete-confirmations-now-open-visibly-and.md +6 -0
- package/corpus/templates/content/server/lib/notion-sync.ts +130 -28
- package/corpus/templates/design/actions/add-breakpoint.ts +48 -68
- package/corpus/templates/design/actions/add-localhost-screens.ts +496 -61
- package/corpus/templates/design/actions/apply-component-prop-edit.ts +2 -18
- package/corpus/templates/design/actions/apply-design-token-edit.ts +51 -82
- package/corpus/templates/design/actions/apply-tweaks.ts +63 -41
- package/corpus/templates/design/actions/capture-design-state.ts +2 -2
- package/corpus/templates/design/actions/connect-localhost.ts +58 -12
- package/corpus/templates/design/actions/create-component.ts +2 -18
- package/corpus/templates/design/actions/create-design-branch.ts +38 -18
- package/corpus/templates/design/actions/create-fusion-app.ts +15 -11
- package/corpus/templates/design/actions/delete-file.ts +19 -34
- package/corpus/templates/design/actions/deploy-design-preview.ts +59 -27
- package/corpus/templates/design/actions/deploy-fusion-app.ts +22 -15
- package/corpus/templates/design/actions/export-pdf.ts +2 -1
- package/corpus/templates/design/actions/export-zip.ts +9 -3
- package/corpus/templates/design/actions/generate-design.ts +107 -70
- package/corpus/templates/design/actions/get-component-details.ts +2 -18
- package/corpus/templates/design/actions/get-design-branch-diff.ts +2 -3
- package/corpus/templates/design/actions/get-design-surface-index.ts +2 -18
- package/corpus/templates/design/actions/get-design.ts +2 -1
- package/corpus/templates/design/actions/grant-localhost-write-consent.ts +7 -1
- package/corpus/templates/design/actions/import-design-tokens.ts +114 -76
- package/corpus/templates/design/actions/index-components.ts +2 -18
- package/corpus/templates/design/actions/list-design-source-capabilities.ts +23 -28
- package/corpus/templates/design/actions/list-local-files.ts +9 -2
- package/corpus/templates/design/actions/migrate-board-objects-to-file.ts +82 -98
- package/corpus/templates/design/actions/open-component-source.ts +2 -18
- package/corpus/templates/design/actions/open-visual-edit.ts +19 -1
- package/corpus/templates/design/actions/present-design-variants.ts +70 -70
- package/corpus/templates/design/actions/preview-component-prop-edit.ts +4 -14
- package/corpus/templates/design/actions/read-local-file.ts +9 -2
- package/corpus/templates/design/actions/remove-breakpoint.ts +40 -61
- package/corpus/templates/design/actions/rename-screen.ts +425 -0
- package/corpus/templates/design/actions/request-localhost-write-consent.ts +12 -2
- package/corpus/templates/design/actions/revoke-localhost-write-consent.ts +9 -2
- package/corpus/templates/design/actions/sync-fusion-app.ts +56 -39
- package/corpus/templates/design/actions/update-design.ts +376 -61
- package/corpus/templates/design/actions/update-file.ts +455 -188
- package/corpus/templates/design/actions/write-local-file.ts +10 -2
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +685 -608
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +43 -15
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +576 -2015
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +62 -14
- package/corpus/templates/design/app/components/design/bridge/embedded-wheel.bridge.ts +263 -25
- package/corpus/templates/design/app/components/design/design-canvas/annotation-submit.ts +34 -0
- package/corpus/templates/design/app/components/design/design-canvas/coordinate-transforms.ts +40 -0
- package/corpus/templates/design/app/components/design/design-canvas/creation.ts +47 -0
- package/corpus/templates/design/app/components/design/design-canvas/element-payload.ts +27 -0
- package/corpus/templates/design/app/components/design/design-canvas/embedded-frame.ts +62 -0
- package/corpus/templates/design/app/components/design/design-canvas/external-preview.ts +137 -0
- package/corpus/templates/design/app/components/design/design-canvas/file-drop.ts +10 -0
- package/corpus/templates/design/app/components/design/design-canvas/hit-test.ts +23 -0
- package/corpus/templates/design/app/components/design/design-canvas/iframe-events.ts +23 -0
- package/corpus/templates/design/app/components/design/design-canvas/iframe-pan.ts +177 -0
- package/corpus/templates/design/app/components/design/design-canvas/motion-types.ts +7 -0
- package/corpus/templates/design/app/components/design/design-canvas/pending-text-edit.ts +62 -0
- package/corpus/templates/design/app/components/design/index.ts +2 -2
- package/corpus/templates/design/app/components/design/multi-screen/canvas-tools.ts +100 -0
- package/corpus/templates/design/app/components/design/multi-screen/coordinate-transforms.ts +81 -0
- package/corpus/templates/design/app/components/design/multi-screen/cross-screen-drop.ts +23 -13
- package/corpus/templates/design/app/components/design/multi-screen/culling.ts +238 -19
- package/corpus/templates/design/app/components/design/multi-screen/draft-primitives.ts +424 -0
- package/corpus/templates/design/app/components/design/multi-screen/frame-geometry.ts +341 -0
- package/corpus/templates/design/app/components/design/multi-screen/iframe-targeting.ts +52 -0
- package/corpus/templates/design/app/components/design/multi-screen/overview-layout.ts +62 -0
- package/corpus/templates/design/app/components/design/multi-screen/primitive-drop-target.ts +283 -0
- package/corpus/templates/design/app/components/design/multi-screen/screen-content-cache.ts +272 -0
- package/corpus/templates/design/app/components/design/multi-screen/types.ts +5 -1
- package/corpus/templates/design/app/components/design/multi-screen/wheel-gesture-state.ts +12 -0
- package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +325 -84
- package/corpus/templates/design/app/hooks/use-navigation-state.ts +15 -1
- package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +119 -47
- package/corpus/templates/design/app/i18n/zh-TW.ts +9 -0
- package/corpus/templates/design/app/i18n-data.ts +223 -0
- package/corpus/templates/design/app/lib/design-save-outbox.ts +379 -0
- package/corpus/templates/design/app/lib/png-clipboard.ts +113 -0
- package/corpus/templates/design/app/pages/design-editor/canvas-primitives.ts +229 -0
- package/corpus/templates/design/app/pages/design-editor/code-layer-state.ts +918 -0
- package/corpus/templates/design/app/pages/design-editor/collab-sync.ts +43 -0
- package/corpus/templates/design/app/pages/design-editor/cross-screen-text-color.ts +336 -0
- package/corpus/templates/design/app/pages/design-editor/data-operations.ts +270 -0
- package/corpus/templates/design/app/pages/design-editor/dom-guards.ts +24 -0
- package/corpus/templates/design/app/pages/design-editor/dom-utils.ts +11 -0
- package/corpus/templates/design/app/pages/design-editor/editor-session.ts +20 -0
- package/corpus/templates/design/app/pages/design-editor/editor-state.ts +408 -0
- package/corpus/templates/design/app/pages/design-editor/export-capture.ts +115 -0
- package/corpus/templates/design/app/pages/design-editor/geometry-persistence.ts +93 -0
- package/corpus/templates/design/app/pages/design-editor/history.ts +169 -0
- package/corpus/templates/design/app/pages/design-editor/layout-operations.ts +185 -0
- package/corpus/templates/design/app/pages/design-editor/motion-state.ts +309 -0
- package/corpus/templates/design/app/pages/design-editor/overview-camera.ts +438 -0
- package/corpus/templates/design/app/pages/design-editor/pending-edits.ts +510 -0
- package/corpus/templates/design/app/pages/design-editor/portable-style.ts +115 -0
- package/corpus/templates/design/app/pages/design-editor/selection-state.ts +337 -0
- package/corpus/templates/design/app/pages/design-editor/style-utils.ts +5 -0
- package/corpus/templates/design/app/pages/design-editor/tool-state.ts +92 -0
- package/corpus/templates/design/app/pages/design-editor/tweak-save.ts +87 -0
- package/corpus/templates/design/app/pages/design-editor/types.ts +36 -0
- package/corpus/templates/design/app/routes/design.$id.tsx +11 -1
- package/corpus/templates/design/changelog/2026-07-09-design-edits-canvas-positions-tools-and-tab-context-now-pers.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-figma-style-shortcuts-drawing-annotations-and-cross-screen-d.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-hand-and-scale-tools-now-keep-their-active-toolbar-identity-.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-large-multi-screen-designs-now-stay-responsive-during-long-c.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-layer-drags-now-land-exactly-where-the-insertion-indicator-s.md +6 -0
- package/corpus/templates/design/server/db/index.ts +2 -52
- package/corpus/templates/design/server/db/schema.ts +16 -0
- package/corpus/templates/design/server/lib/design-data-access.ts +70 -0
- package/corpus/templates/design/server/lib/design-data-mutation.ts +249 -0
- package/corpus/templates/design/server/lib/fusion-screens.ts +89 -76
- package/corpus/templates/design/server/lib/import-design-files.ts +76 -35
- package/corpus/templates/design/server/lib/verify-write-grant.ts +7 -2
- package/corpus/templates/design/server/plugins/db.ts +19 -0
- package/corpus/templates/design/server/source-workspace.ts +12 -13
- package/corpus/templates/design/shared/resolve-tweaks.ts +15 -0
- package/corpus/templates/design/shared/screen-rename.ts +42 -0
- package/corpus/templates/design/shared/source-mode.ts +84 -4
- package/dist/agent/engine/builder-engine.d.ts +1 -1
- package/dist/agent/engine/builder-engine.d.ts.map +1 -1
- package/dist/agent/engine/builtin.js +1 -1
- package/dist/agent/engine/builtin.js.map +1 -1
- package/dist/agent/model-config.d.ts +9 -9
- package/dist/agent/model-config.d.ts.map +1 -1
- package/dist/agent/model-config.js +30 -30
- package/dist/agent/model-config.js.map +1 -1
- package/dist/agent/production-agent.d.ts +60 -1
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +106 -7
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/cli/design-connect.d.ts +30 -1
- package/dist/cli/design-connect.d.ts.map +1 -1
- package/dist/cli/design-connect.js +359 -75
- package/dist/cli/design-connect.js.map +1 -1
- package/dist/cli/recap.js +2 -2
- package/dist/cli/recap.js.map +1 -1
- package/dist/cli/sync-builder-starter-manifest.js +1 -1
- package/dist/cli/sync-builder-starter-manifest.js.map +1 -1
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +6 -1
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/agent-chat-adapter.d.ts.map +1 -1
- package/dist/client/agent-chat-adapter.js +10 -5
- package/dist/client/agent-chat-adapter.js.map +1 -1
- package/dist/client/composer/TiptapComposer.js +1 -1
- package/dist/client/composer/TiptapComposer.js.map +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/org/TeamPage.d.ts.map +1 -1
- package/dist/client/org/TeamPage.js +6 -5
- package/dist/client/org/TeamPage.js.map +1 -1
- package/dist/client/use-action.d.ts +32 -0
- package/dist/client/use-action.d.ts.map +1 -1
- package/dist/client/use-action.js +68 -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/db/client.d.ts +3 -0
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +7 -2
- package/dist/db/client.js.map +1 -1
- package/dist/db/create-get-db.d.ts.map +1 -1
- package/dist/db/create-get-db.js +2 -2
- package/dist/db/create-get-db.js.map +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +5 -5
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/scripts/agent-engines/manage-agent-engine.js +1 -1
- package/dist/scripts/agent-engines/manage-agent-engine.js.map +1 -1
- package/dist/scripts/agent-engines/set-agent-engine.js +1 -1
- package/dist/scripts/agent-engines/set-agent-engine.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/transcribe-voice.js +1 -1
- package/dist/server/transcribe-voice.js.map +1 -1
- package/dist/usage/store.d.ts.map +1 -1
- package/dist/usage/store.js +13 -4
- package/dist/usage/store.js.map +1 -1
- package/dist/vite/client.d.ts.map +1 -1
- package/dist/vite/client.js +36 -1
- package/dist/vite/client.js.map +1 -1
- package/docs/content/a2a-protocol.mdx +12 -0
- package/docs/content/locales/ar-SA/a2a-protocol.mdx +12 -0
- package/docs/content/locales/ar-SA/pr-visual-recap.mdx +3 -3
- package/docs/content/locales/ar-SA/pure-agent-apps.mdx +4 -0
- package/docs/content/locales/de-DE/a2a-protocol.mdx +12 -0
- package/docs/content/locales/de-DE/pr-visual-recap.mdx +3 -3
- package/docs/content/locales/de-DE/pure-agent-apps.mdx +4 -0
- package/docs/content/locales/es-ES/a2a-protocol.mdx +12 -0
- package/docs/content/locales/es-ES/pr-visual-recap.mdx +3 -3
- package/docs/content/locales/es-ES/pure-agent-apps.mdx +4 -0
- package/docs/content/locales/fr-FR/a2a-protocol.mdx +12 -0
- package/docs/content/locales/fr-FR/pr-visual-recap.mdx +3 -3
- package/docs/content/locales/fr-FR/pure-agent-apps.mdx +4 -0
- package/docs/content/locales/hi-IN/a2a-protocol.mdx +12 -0
- package/docs/content/locales/hi-IN/pr-visual-recap.mdx +3 -3
- package/docs/content/locales/hi-IN/pure-agent-apps.mdx +4 -0
- package/docs/content/locales/ja-JP/a2a-protocol.mdx +12 -0
- package/docs/content/locales/ja-JP/pr-visual-recap.mdx +3 -3
- package/docs/content/locales/ja-JP/pure-agent-apps.mdx +4 -0
- package/docs/content/locales/ko-KR/a2a-protocol.mdx +12 -0
- package/docs/content/locales/ko-KR/pr-visual-recap.mdx +3 -3
- package/docs/content/locales/ko-KR/pure-agent-apps.mdx +4 -0
- package/docs/content/locales/pt-BR/a2a-protocol.mdx +12 -0
- package/docs/content/locales/pt-BR/pr-visual-recap.mdx +3 -3
- package/docs/content/locales/pt-BR/pure-agent-apps.mdx +4 -0
- package/docs/content/locales/zh-CN/a2a-protocol.mdx +12 -0
- package/docs/content/locales/zh-CN/pr-visual-recap.mdx +3 -3
- package/docs/content/locales/zh-CN/pure-agent-apps.mdx +4 -0
- package/docs/content/locales/zh-TW/a2a-protocol.mdx +12 -0
- package/docs/content/locales/zh-TW/pr-visual-recap.mdx +3 -3
- package/docs/content/locales/zh-TW/pure-agent-apps.mdx +4 -0
- package/docs/content/pr-visual-recap.mdx +3 -3
- package/docs/content/pure-agent-apps.mdx +22 -0
- package/package.json +2 -2
|
@@ -12,6 +12,12 @@ export const designs = table("designs", {
|
|
|
12
12
|
title: text("title").notNull(),
|
|
13
13
|
description: text("description"),
|
|
14
14
|
data: text("data").notNull(),
|
|
15
|
+
// Monotonic per-client sequence numbers for path-addressed data writes.
|
|
16
|
+
// Kept outside `data` so editor/export payloads stay free of transport
|
|
17
|
+
// bookkeeping while late keepalive requests can be rejected atomically.
|
|
18
|
+
dataOperationRevisions: text("data_operation_revisions")
|
|
19
|
+
.notNull()
|
|
20
|
+
.default("{}"),
|
|
15
21
|
projectType: text("project_type").notNull().default("prototype"),
|
|
16
22
|
designSystemId: text("design_system_id"),
|
|
17
23
|
createdAt: text("created_at").default(now()),
|
|
@@ -43,6 +49,12 @@ export const designFiles = table("design_files", {
|
|
|
43
49
|
designId: text("design_id").notNull(),
|
|
44
50
|
filename: text("filename").notNull(),
|
|
45
51
|
content: text("content").notNull(),
|
|
52
|
+
// Last accepted browser content-save operation. This transport metadata
|
|
53
|
+
// stays beside (rather than inside) the document so a late unload
|
|
54
|
+
// keepalive can be rejected without parsing or rewriting user content.
|
|
55
|
+
contentOperationSource: text("content_operation_source"),
|
|
56
|
+
contentOperationRevision: integer("content_operation_revision"),
|
|
57
|
+
contentOperationResultHash: text("content_operation_result_hash"),
|
|
46
58
|
fileType: text("file_type").notNull().default("html"),
|
|
47
59
|
createdAt: text("created_at").default(now()),
|
|
48
60
|
updatedAt: text("updated_at").default(now()),
|
|
@@ -75,6 +87,10 @@ export const designLocalhostConnections = table(
|
|
|
75
87
|
.notNull()
|
|
76
88
|
.default("connected"),
|
|
77
89
|
lastSeenAt: text("last_seen_at"),
|
|
90
|
+
/** Read-only credential used by browser preview/bridge-registration calls.
|
|
91
|
+
* It is one-way derived from the filesystem token when not supplied by a
|
|
92
|
+
* newer bridge, so leaking it cannot grant source-file access. */
|
|
93
|
+
previewToken: text("preview_token"),
|
|
78
94
|
bridgeToken: text("bridge_token"),
|
|
79
95
|
ownerEmail: text("owner_email").notNull(),
|
|
80
96
|
orgId: text("org_id"),
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const EDITOR_ROLES = new Set(["owner", "admin", "editor"]);
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Public visibility is read-only. Ownership and explicit share grants are
|
|
5
|
+
* resolved separately by the framework, so they can still upgrade the caller
|
|
6
|
+
* to owner/admin/editor without making an anonymous public link writable.
|
|
7
|
+
*/
|
|
8
|
+
export function publicDesignAccessRole(): "viewer" {
|
|
9
|
+
return "viewer";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function removeLocalhostCredentials(
|
|
13
|
+
value: unknown,
|
|
14
|
+
ancestors: WeakSet<object>,
|
|
15
|
+
allowPreviewToken: boolean,
|
|
16
|
+
): unknown {
|
|
17
|
+
if (!value || typeof value !== "object") return value;
|
|
18
|
+
if (ancestors.has(value)) {
|
|
19
|
+
throw new Error("Design data contains a circular reference");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
ancestors.add(value);
|
|
23
|
+
try {
|
|
24
|
+
if (Array.isArray(value)) {
|
|
25
|
+
return value.map((child) =>
|
|
26
|
+
removeLocalhostCredentials(child, ancestors, allowPreviewToken),
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return Object.fromEntries(
|
|
31
|
+
Object.entries(value as Record<string, unknown>)
|
|
32
|
+
.filter(
|
|
33
|
+
([key]) =>
|
|
34
|
+
key !== "bridgeToken" &&
|
|
35
|
+
(allowPreviewToken || key !== "previewToken"),
|
|
36
|
+
)
|
|
37
|
+
.map(([key, child]) => [
|
|
38
|
+
key,
|
|
39
|
+
removeLocalhostCredentials(child, ancestors, allowPreviewToken),
|
|
40
|
+
]),
|
|
41
|
+
);
|
|
42
|
+
} finally {
|
|
43
|
+
ancestors.delete(value);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Filter persisted design data before sending it to a caller.
|
|
49
|
+
*
|
|
50
|
+
* Filesystem bridge tokens unlock privileged loopback endpoints and are never
|
|
51
|
+
* returned through design data, including to owners/editors. The separate
|
|
52
|
+
* read-only preview token is available only to callers with an editor role;
|
|
53
|
+
* anonymous/public viewers fail closed rather than gaining access to a local
|
|
54
|
+
* development server. Invalid persisted JSON also fails closed to null.
|
|
55
|
+
*/
|
|
56
|
+
export function designDataForAccessRole(data: unknown, role: unknown): unknown {
|
|
57
|
+
const allowPreviewToken = typeof role === "string" && EDITOR_ROLES.has(role);
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
if (typeof data === "string") {
|
|
61
|
+
const parsed = JSON.parse(data) as unknown;
|
|
62
|
+
return JSON.stringify(
|
|
63
|
+
removeLocalhostCredentials(parsed, new WeakSet(), allowPreviewToken),
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
return removeLocalhostCredentials(data, new WeakSet(), allowPreviewToken);
|
|
67
|
+
} catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { assertAccess } from "@agent-native/core/sharing";
|
|
2
|
+
import { and, eq, isNull } from "drizzle-orm";
|
|
3
|
+
|
|
4
|
+
import { getDb, schema } from "../db/index.js";
|
|
5
|
+
|
|
6
|
+
const DEFAULT_MAX_ATTEMPTS = 8;
|
|
7
|
+
const CONFLICT_BACKOFF_MS = 8;
|
|
8
|
+
const designDataLocks = new Map<string, Promise<unknown>>();
|
|
9
|
+
|
|
10
|
+
export type DesignDataRecord = Record<string, unknown>;
|
|
11
|
+
|
|
12
|
+
export class DesignDataMutationConflictError extends Error {
|
|
13
|
+
constructor(designId: string, attempts: number) {
|
|
14
|
+
super(
|
|
15
|
+
`Failed to update design "${designId}" after ${attempts} concurrent write conflicts. Please retry.`,
|
|
16
|
+
);
|
|
17
|
+
this.name = "DesignDataMutationConflictError";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class InvalidDesignDataError extends Error {
|
|
22
|
+
constructor(designId: string) {
|
|
23
|
+
super(
|
|
24
|
+
`Design "${designId}" has invalid data JSON. Refusing to overwrite it; repair or restore the design data before retrying.`,
|
|
25
|
+
);
|
|
26
|
+
this.name = "InvalidDesignDataError";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parseDesignData(
|
|
31
|
+
designId: string,
|
|
32
|
+
serialized: string | null,
|
|
33
|
+
): DesignDataRecord {
|
|
34
|
+
// A small number of legacy rows predate the current NOT NULL schema. Treat
|
|
35
|
+
// SQL NULL as the old empty-data sentinel, while still refusing malformed
|
|
36
|
+
// non-null JSON so a corrupt blob is never silently discarded.
|
|
37
|
+
if (serialized === null) return {};
|
|
38
|
+
try {
|
|
39
|
+
const parsed: unknown = JSON.parse(serialized);
|
|
40
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
41
|
+
return parsed as DesignDataRecord;
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
// The dedicated error below explains why the write is refused.
|
|
45
|
+
}
|
|
46
|
+
throw new InvalidDesignDataError(designId);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function serializeDesignData(designId: string, data: DesignDataRecord): string {
|
|
50
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) {
|
|
51
|
+
throw new InvalidDesignDataError(designId);
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const serialized = JSON.stringify(data);
|
|
55
|
+
if (typeof serialized === "string") return serialized;
|
|
56
|
+
} catch {
|
|
57
|
+
// The dedicated error below keeps circular/non-serializable transforms
|
|
58
|
+
// from turning into a partial or ambiguous write.
|
|
59
|
+
}
|
|
60
|
+
throw new InvalidDesignDataError(designId);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function nextUpdatedAt(current: string | null, now: Date): string {
|
|
64
|
+
const currentMs = current ? Date.parse(current) : Number.NaN;
|
|
65
|
+
const nextMs = Number.isFinite(currentMs)
|
|
66
|
+
? Math.max(now.getTime(), currentMs + 1)
|
|
67
|
+
: now.getTime();
|
|
68
|
+
return new Date(nextMs).toISOString();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function conflictDelay(attempt: number): Promise<void> {
|
|
72
|
+
return new Promise((resolve) => {
|
|
73
|
+
setTimeout(resolve, Math.min(64, CONFLICT_BACKOFF_MS * (attempt + 1)));
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isRetryableTransactionConflict(error: unknown): boolean {
|
|
78
|
+
const code =
|
|
79
|
+
error && typeof error === "object" && "code" in error
|
|
80
|
+
? String((error as { code?: unknown }).code ?? "")
|
|
81
|
+
: "";
|
|
82
|
+
return (
|
|
83
|
+
code === "SQLITE_BUSY" ||
|
|
84
|
+
code === "SQLITE_LOCKED" ||
|
|
85
|
+
code === "40001" || // Postgres serialization failure
|
|
86
|
+
code === "40P01" // Postgres deadlock detected
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function withDesignDataLock<T>(
|
|
91
|
+
designId: string,
|
|
92
|
+
callback: () => Promise<T>,
|
|
93
|
+
): Promise<T> {
|
|
94
|
+
const previous = designDataLocks.get(designId) ?? Promise.resolve();
|
|
95
|
+
const next = previous.then(callback, callback);
|
|
96
|
+
designDataLocks.set(designId, next);
|
|
97
|
+
const cleanup = () => {
|
|
98
|
+
if (designDataLocks.get(designId) === next) {
|
|
99
|
+
designDataLocks.delete(designId);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
next.then(cleanup, cleanup);
|
|
103
|
+
return next;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
interface MutateDesignDataOptions {
|
|
107
|
+
designId: string;
|
|
108
|
+
mutate: (
|
|
109
|
+
current: DesignDataRecord,
|
|
110
|
+
context: { updatedAt: string },
|
|
111
|
+
) => DesignDataRecord;
|
|
112
|
+
/**
|
|
113
|
+
* Proves the caller's intent is present in the committed row. This is
|
|
114
|
+
* deliberately intent-based rather than whole-object equality: a sibling
|
|
115
|
+
* writer may safely add unrelated keys immediately after our commit.
|
|
116
|
+
*/
|
|
117
|
+
isApplied: (persisted: DesignDataRecord) => boolean;
|
|
118
|
+
maxAttempts?: number;
|
|
119
|
+
now?: () => Date;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Atomically mutate the designs.data JSON record without losing sibling keys.
|
|
124
|
+
*
|
|
125
|
+
* The conditional UPDATE is portable Drizzle query-builder SQL (no RETURNING,
|
|
126
|
+
* dialect-specific JSON operator, or driver result-shape assumption). The
|
|
127
|
+
* read, compare-and-swap, and confirmation read live in one transaction. A
|
|
128
|
+
* post-commit read then proves the requested intent survived before success is
|
|
129
|
+
* reported. Explicit property deletion performed by `mutate` is preserved
|
|
130
|
+
* because the complete transformed object is the CAS candidate.
|
|
131
|
+
*
|
|
132
|
+
* Isolation assumptions: local better-sqlite3 transactions use the framework's
|
|
133
|
+
* BEGIN IMMEDIATE + top-level queue, so no sibling writer can enter between
|
|
134
|
+
* the read and CAS. Postgres may let a sibling commit after the read, but its
|
|
135
|
+
* conditional UPDATE is re-evaluated after the row-lock wait; the confirmation
|
|
136
|
+
* read detects a lost CAS and triggers a retry. Other supported drivers get the
|
|
137
|
+
* same guarded predicate plus confirmation/post-commit verification rather
|
|
138
|
+
* than relying on a driver-specific affected-row result.
|
|
139
|
+
*/
|
|
140
|
+
async function mutateDesignDataUnlocked({
|
|
141
|
+
designId,
|
|
142
|
+
mutate,
|
|
143
|
+
isApplied,
|
|
144
|
+
maxAttempts = DEFAULT_MAX_ATTEMPTS,
|
|
145
|
+
now = () => new Date(),
|
|
146
|
+
}: MutateDesignDataOptions): Promise<{
|
|
147
|
+
data: DesignDataRecord;
|
|
148
|
+
updatedAt: string;
|
|
149
|
+
}> {
|
|
150
|
+
// Re-assert at the shared write boundary. Callers also check before doing
|
|
151
|
+
// parse/index work so unauthorized requests fail early, but this helper must
|
|
152
|
+
// remain independently scoped if a new action adopts it later.
|
|
153
|
+
await assertAccess("design", designId, "editor");
|
|
154
|
+
const db = getDb();
|
|
155
|
+
|
|
156
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
157
|
+
let committed: { data: DesignDataRecord; updatedAt: string } | undefined;
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
committed = await db.transaction(async (tx) => {
|
|
161
|
+
const [currentRow] = await tx
|
|
162
|
+
.select({
|
|
163
|
+
data: schema.designs.data,
|
|
164
|
+
updatedAt: schema.designs.updatedAt,
|
|
165
|
+
})
|
|
166
|
+
.from(schema.designs)
|
|
167
|
+
.where(eq(schema.designs.id, designId));
|
|
168
|
+
|
|
169
|
+
if (!currentRow) {
|
|
170
|
+
throw new Error(`Design "${designId}" not found.`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const updatedAt = nextUpdatedAt(currentRow.updatedAt, now());
|
|
174
|
+
const currentData = parseDesignData(designId, currentRow.data);
|
|
175
|
+
const nextData = mutate(currentData, { updatedAt });
|
|
176
|
+
const nextSerialized = serializeDesignData(designId, nextData);
|
|
177
|
+
const revisionConditions = [
|
|
178
|
+
eq(schema.designs.id, designId),
|
|
179
|
+
currentRow.data === null
|
|
180
|
+
? isNull(schema.designs.data)
|
|
181
|
+
: eq(schema.designs.data, currentRow.data),
|
|
182
|
+
];
|
|
183
|
+
revisionConditions.push(
|
|
184
|
+
currentRow.updatedAt === null
|
|
185
|
+
? isNull(schema.designs.updatedAt)
|
|
186
|
+
: eq(schema.designs.updatedAt, currentRow.updatedAt),
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
await tx
|
|
190
|
+
.update(schema.designs)
|
|
191
|
+
.set({ data: nextSerialized, updatedAt })
|
|
192
|
+
.where(and(...revisionConditions));
|
|
193
|
+
|
|
194
|
+
// Avoid driver-specific rowsAffected/rowCount/RETURNING contracts.
|
|
195
|
+
// Inside the same transaction the row remains locked after a winning
|
|
196
|
+
// UPDATE, so exact equality proves this CAS attempt wrote its candidate.
|
|
197
|
+
const [confirmed] = await tx
|
|
198
|
+
.select({ data: schema.designs.data })
|
|
199
|
+
.from(schema.designs)
|
|
200
|
+
.where(eq(schema.designs.id, designId));
|
|
201
|
+
|
|
202
|
+
if (!confirmed) {
|
|
203
|
+
throw new Error(`Design "${designId}" not found after update.`);
|
|
204
|
+
}
|
|
205
|
+
if (confirmed.data !== nextSerialized) {
|
|
206
|
+
throw new DesignDataMutationConflictError(designId, attempt + 1);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return { data: nextData, updatedAt };
|
|
210
|
+
});
|
|
211
|
+
} catch (error) {
|
|
212
|
+
if (
|
|
213
|
+
!(error instanceof DesignDataMutationConflictError) &&
|
|
214
|
+
!isRetryableTransactionConflict(error)
|
|
215
|
+
) {
|
|
216
|
+
throw error;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (committed) {
|
|
221
|
+
const [persistedRow] = await db
|
|
222
|
+
.select({ data: schema.designs.data })
|
|
223
|
+
.from(schema.designs)
|
|
224
|
+
.where(eq(schema.designs.id, designId));
|
|
225
|
+
if (!persistedRow) {
|
|
226
|
+
throw new Error(`Design "${designId}" not found after commit.`);
|
|
227
|
+
}
|
|
228
|
+
const persistedData = parseDesignData(designId, persistedRow.data);
|
|
229
|
+
if (isApplied(persistedData)) {
|
|
230
|
+
return { data: persistedData, updatedAt: committed.updatedAt };
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (attempt < maxAttempts - 1) await conflictDelay(attempt);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
throw new DesignDataMutationConflictError(designId, maxAttempts);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function mutateDesignData(
|
|
241
|
+
options: MutateDesignDataOptions,
|
|
242
|
+
): Promise<{ data: DesignDataRecord; updatedAt: string }> {
|
|
243
|
+
// Serialize same-process calls before entering a backend transaction. This
|
|
244
|
+
// avoids overlapping libSQL/SQLite transactions on one client; the CAS
|
|
245
|
+
// remains necessary for multi-instance and cross-process writers.
|
|
246
|
+
return withDesignDataLock(options.designId, () =>
|
|
247
|
+
mutateDesignDataUnlocked(options),
|
|
248
|
+
);
|
|
249
|
+
}
|
|
@@ -29,8 +29,8 @@ import {
|
|
|
29
29
|
parseCanvasFrameGeometryById,
|
|
30
30
|
type CanvasFramePlacement,
|
|
31
31
|
} from "../../shared/canvas-frames.js";
|
|
32
|
-
import { parseDesignDataBlob } from "../../shared/full-app.js";
|
|
33
32
|
import { getDb, schema } from "../db/index.js";
|
|
33
|
+
import { mutateDesignData } from "./design-data-mutation.js";
|
|
34
34
|
|
|
35
35
|
/** Default iframe viewport, mirroring add-localhost-screens' defaults. */
|
|
36
36
|
export const DEFAULT_FUSION_SCREEN_WIDTH = 1280;
|
|
@@ -117,18 +117,6 @@ export async function upsertFusionScreens(args: {
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
const db = getDb();
|
|
120
|
-
const [design] = await db
|
|
121
|
-
.select({ data: schema.designs.data })
|
|
122
|
-
.from(schema.designs)
|
|
123
|
-
.where(eq(schema.designs.id, designId))
|
|
124
|
-
.limit(1);
|
|
125
|
-
const prevData = parseDesignDataBlob(design?.data);
|
|
126
|
-
const existingCanvasFrames = parseCanvasFrameGeometryById(
|
|
127
|
-
prevData.canvasFrames,
|
|
128
|
-
);
|
|
129
|
-
const existingMetadata = isRecord(prevData.screenMetadata)
|
|
130
|
-
? { ...(prevData.screenMetadata as Record<string, unknown>) }
|
|
131
|
-
: {};
|
|
132
120
|
const existingFiles = await db
|
|
133
121
|
.select()
|
|
134
122
|
.from(schema.designFiles)
|
|
@@ -140,22 +128,6 @@ export async function upsertFusionScreens(args: {
|
|
|
140
128
|
const now = new Date().toISOString();
|
|
141
129
|
|
|
142
130
|
const results: FusionScreenResult[] = [];
|
|
143
|
-
const placements: CanvasFramePlacement[] = [];
|
|
144
|
-
|
|
145
|
-
// Only place screens that don't already have canvas geometry so re-syncs
|
|
146
|
-
// never reset positions the user has arranged. New screens start after the
|
|
147
|
-
// right-most existing frame.
|
|
148
|
-
const existingGeometries = Object.values(existingCanvasFrames);
|
|
149
|
-
let nextX = startX;
|
|
150
|
-
if (existingGeometries.length > 0) {
|
|
151
|
-
nextX =
|
|
152
|
-
Math.max(
|
|
153
|
-
startX,
|
|
154
|
-
...existingGeometries.map(
|
|
155
|
-
(frame) => (frame.x ?? 0) + (frame.width ?? width),
|
|
156
|
-
),
|
|
157
|
-
) + gap;
|
|
158
|
-
}
|
|
159
131
|
|
|
160
132
|
for (let index = 0; index < paths.length; index += 1) {
|
|
161
133
|
const path = paths[index]!;
|
|
@@ -190,57 +162,98 @@ export async function upsertFusionScreens(args: {
|
|
|
190
162
|
}
|
|
191
163
|
|
|
192
164
|
results.push({ fileId, filename, path, url, title, width, height });
|
|
193
|
-
if (!existingCanvasFrames[fileId]) {
|
|
194
|
-
placements.push({
|
|
195
|
-
fileId,
|
|
196
|
-
filename,
|
|
197
|
-
x: nextX,
|
|
198
|
-
y: startY,
|
|
199
|
-
width,
|
|
200
|
-
height,
|
|
201
|
-
z: placements.length,
|
|
202
|
-
});
|
|
203
|
-
nextX += width + gap;
|
|
204
|
-
}
|
|
205
165
|
}
|
|
206
166
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
167
|
+
let placedFrames: Array<{
|
|
168
|
+
fileId: string;
|
|
169
|
+
filename?: string;
|
|
170
|
+
frame: CanvasFramePlacement;
|
|
171
|
+
}> = [];
|
|
172
|
+
await mutateDesignData({
|
|
173
|
+
designId,
|
|
174
|
+
mutate: (current, { updatedAt }) => {
|
|
175
|
+
const currentFrames = parseCanvasFrameGeometryById(current.canvasFrames);
|
|
176
|
+
const existingGeometries = Object.values(currentFrames);
|
|
177
|
+
let nextX = startX;
|
|
178
|
+
if (existingGeometries.length > 0) {
|
|
179
|
+
nextX =
|
|
180
|
+
Math.max(
|
|
181
|
+
startX,
|
|
182
|
+
...existingGeometries.map(
|
|
183
|
+
(frame) => (frame.x ?? 0) + (frame.width ?? width),
|
|
184
|
+
),
|
|
185
|
+
) + gap;
|
|
186
|
+
}
|
|
212
187
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
188
|
+
const placements: CanvasFramePlacement[] = [];
|
|
189
|
+
for (const screen of results) {
|
|
190
|
+
if (currentFrames[screen.fileId]) continue;
|
|
191
|
+
placements.push({
|
|
192
|
+
fileId: screen.fileId,
|
|
193
|
+
filename: screen.filename,
|
|
194
|
+
x: nextX,
|
|
195
|
+
y: startY,
|
|
196
|
+
width,
|
|
197
|
+
height,
|
|
198
|
+
z: placements.length,
|
|
199
|
+
});
|
|
200
|
+
nextX += width + gap;
|
|
201
|
+
}
|
|
202
|
+
const mergedFrames = mergeCanvasFramePlacements({
|
|
203
|
+
existing: current.canvasFrames,
|
|
204
|
+
placements,
|
|
205
|
+
resolveFileId: (placement) => placement.fileId,
|
|
206
|
+
});
|
|
207
|
+
placedFrames = mergedFrames.placedFrames;
|
|
231
208
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
209
|
+
const metadata = isRecord(current.screenMetadata)
|
|
210
|
+
? { ...current.screenMetadata }
|
|
211
|
+
: {};
|
|
212
|
+
for (const screen of results) {
|
|
213
|
+
// Preserve user-adjusted title/dimensions on refresh; only URL-backed
|
|
214
|
+
// source fields track the current container preview.
|
|
215
|
+
const candidate = metadata[screen.fileId];
|
|
216
|
+
const previous: Record<string, unknown> = isRecord(candidate)
|
|
217
|
+
? candidate
|
|
218
|
+
: {};
|
|
219
|
+
metadata[screen.fileId] = {
|
|
220
|
+
...previous,
|
|
221
|
+
sourceType: "fusion",
|
|
222
|
+
previewState: "live",
|
|
223
|
+
title: previous.title ?? screen.title,
|
|
224
|
+
width: previous.width ?? screen.width,
|
|
225
|
+
height: previous.height ?? screen.height,
|
|
226
|
+
url: screen.url,
|
|
227
|
+
previewUrl: screen.url,
|
|
228
|
+
path: screen.path,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
...current,
|
|
237
234
|
canvasFrames: mergedFrames.canvasFrames,
|
|
238
|
-
screenMetadata:
|
|
239
|
-
updatedAt
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
235
|
+
screenMetadata: metadata,
|
|
236
|
+
updatedAt,
|
|
237
|
+
};
|
|
238
|
+
},
|
|
239
|
+
isApplied: (current) => {
|
|
240
|
+
const frames = parseCanvasFrameGeometryById(current.canvasFrames);
|
|
241
|
+
const metadata = isRecord(current.screenMetadata)
|
|
242
|
+
? current.screenMetadata
|
|
243
|
+
: {};
|
|
244
|
+
return results.every((screen) => {
|
|
245
|
+
const entry = metadata[screen.fileId];
|
|
246
|
+
return (
|
|
247
|
+
Boolean(frames[screen.fileId]) &&
|
|
248
|
+
isRecord(entry) &&
|
|
249
|
+
entry.sourceType === "fusion" &&
|
|
250
|
+
entry.url === screen.url &&
|
|
251
|
+
entry.previewUrl === screen.url &&
|
|
252
|
+
entry.path === screen.path
|
|
253
|
+
);
|
|
254
|
+
});
|
|
255
|
+
},
|
|
256
|
+
});
|
|
244
257
|
|
|
245
|
-
return { screens: results, placedFrames
|
|
258
|
+
return { screens: results, placedFrames };
|
|
246
259
|
}
|