@agent-native/core 0.80.10 → 0.81.0
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 +53 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/production-agent.ts +108 -15
- package/corpus/core/src/cli/design-connect.ts +419 -19
- package/corpus/core/src/client/AssistantChat.tsx +123 -26
- package/corpus/core/src/client/ConnectBuilderCard.tsx +1 -0
- package/corpus/core/src/client/chat/index.ts +1 -0
- package/corpus/core/src/client/chat/run-recovery.tsx +9 -7
- package/corpus/core/src/client/chat-view-transition.ts +33 -2
- package/corpus/core/src/client/composer/PromptComposer.tsx +21 -2
- package/corpus/core/src/client/index.ts +1 -0
- package/corpus/core/src/client/org/OrgSwitcher.tsx +26 -4
- package/corpus/core/src/client/sharing/ShareButton.tsx +3 -3
- package/corpus/core/src/client/sse-event-processor.ts +9 -0
- package/corpus/core/src/client/use-agent-chat-home-handoff.ts +31 -3
- package/corpus/core/src/db/client.ts +29 -2
- package/corpus/core/src/provider-api/index.ts +29 -0
- package/corpus/core/src/server/core-routes-plugin.ts +58 -20
- package/corpus/core/src/styles/agent-native.css +54 -0
- package/corpus/core/src/usage/store.ts +2 -2
- package/corpus/templates/analytics/app/components/layout/Layout.tsx +118 -10
- package/corpus/templates/analytics/app/components/layout/layout-route-policy.ts +7 -0
- package/corpus/templates/analytics/app/global.css +8 -0
- package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +7 -1
- package/corpus/templates/analytics/app/lib/chat-handoff.ts +34 -0
- package/corpus/templates/analytics/app/pages/Ask.tsx +9 -18
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +2 -2
- package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +109 -21
- package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +4 -6
- package/corpus/templates/analytics/changelog/2026-06-30-ask-opens-a-fresh-chat-unless-you-were-just-chatting-and-das.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-30-dashboard-email-reports-are-more-reliable-and-use-cleaner.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-30-dashboard-toolbar-controls-use-a-quieter-share-button-and-ve.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-30-session-replay-rows-show-whole-minute-durations-in-the-playl.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-30-session-replays-now-render-captured-styling-and-keep-the-eve.md +6 -0
- package/corpus/templates/analytics/server/jobs/dashboard-report.ts +16 -3
- package/corpus/templates/analytics/server/lib/dashboard-report.ts +31 -14
- package/corpus/templates/assets/.agents/skills/image-generation/SKILL.md +9 -2
- package/corpus/templates/assets/AGENTS.md +4 -0
- package/corpus/templates/assets/actions/_tool-activity.ts +46 -0
- package/corpus/templates/assets/actions/generate-image-batch.ts +26 -5
- package/corpus/templates/assets/actions/generate-image.ts +118 -67
- package/corpus/templates/assets/actions/list-draft-assets.ts +50 -0
- package/corpus/templates/assets/actions/open-asset-picker.ts +9 -0
- package/corpus/templates/assets/actions/rerun-generation-run.ts +52 -31
- package/corpus/templates/assets/app/components/create/RecentDraftsSection.tsx +100 -0
- package/corpus/templates/assets/app/global.css +15 -0
- package/corpus/templates/assets/app/hooks/use-navigation-state.ts +4 -0
- package/corpus/templates/assets/app/i18n/zh-TW.ts +7 -0
- package/corpus/templates/assets/app/i18n-data.ts +61 -0
- package/corpus/templates/assets/app/routes/_index.tsx +4 -0
- package/corpus/templates/assets/app/routes/library.tsx +233 -52
- package/corpus/templates/assets/changelog/2026-06-29-image-generation-can-now-render-requested-text-and-respect-b.md +6 -0
- package/corpus/templates/assets/changelog/2026-06-30-image-generation-no-longer-looks-stuck-while-the-provider-is-still-working.md +6 -0
- package/corpus/templates/assets/changelog/2026-06-30-tagged-presets-now-control-the-image-aspect-ratio.md +6 -0
- package/corpus/templates/assets/changelog/2026-06-30-the-assets-picker-now-supports-a-vertical-embedded-layout-fo.md +6 -0
- package/corpus/templates/assets/server/lib/generation.ts +135 -11
- package/corpus/templates/assets/shared/api.ts +4 -0
- package/corpus/templates/clips/actions/finalize-recording.ts +1 -12
- package/corpus/templates/clips/actions/list-ai-requests.ts +60 -0
- package/corpus/templates/clips/app/hooks/use-auto-title.ts +24 -25
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +10 -5
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +6 -8
- package/corpus/templates/clips/changelog/2026-06-30-extension-save-recovery-avoids-false-upload-failures.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-30-longer-chrome-extension-recordings-now-keep-saving-reliably-.md +6 -0
- package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
- package/corpus/templates/clips/chrome-extension/src/background.ts +19 -10
- package/corpus/templates/clips/chrome-extension/src/content-script.ts +38 -12
- package/corpus/templates/clips/chrome-extension/src/finalize-recovery.ts +224 -0
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +119 -16
- package/corpus/templates/clips/chrome-extension/src/overlay.ts +16 -0
- package/corpus/templates/clips/server/lib/recording-upload-state.ts +48 -0
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +9 -1
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +63 -13
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/status.get.ts +74 -0
- package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +32 -0
- package/corpus/templates/design/AGENTS.md +27 -0
- package/corpus/templates/design/CODE_NATIVE_DESIGN_STUDIO_IMPLEMENTATION_PLAN.md +1 -1
- package/corpus/templates/design/DESIGN-STUDIO-PLAN.md +5 -5
- package/corpus/templates/design/actions/apply-component-prop-edit.ts +76 -25
- package/corpus/templates/design/actions/apply-design-state.ts +17 -4
- package/corpus/templates/design/actions/apply-motion-edit.ts +76 -115
- package/corpus/templates/design/actions/apply-shader-fill.ts +148 -56
- package/corpus/templates/design/actions/capture-design-state.ts +27 -4
- package/corpus/templates/design/actions/connect-localhost.ts +59 -22
- package/corpus/templates/design/actions/delete-design.ts +20 -0
- package/corpus/templates/design/actions/export-zip.ts +23 -8
- package/corpus/templates/design/actions/generate-design.ts +53 -1
- package/corpus/templates/design/actions/get-component-details.ts +21 -26
- package/corpus/templates/design/actions/get-motion-timeline.ts +135 -10
- package/corpus/templates/design/actions/grant-localhost-write-consent.ts +116 -0
- package/corpus/templates/design/actions/import-design-tokens.ts +521 -0
- package/corpus/templates/design/actions/index-design-tokens.ts +18 -5
- package/corpus/templates/design/actions/insert-design-native-asset.ts +268 -0
- package/corpus/templates/design/actions/insert-figma-library-asset.ts +233 -0
- package/corpus/templates/design/actions/list-design-extensions.ts +32 -10
- package/corpus/templates/design/actions/list-design-native-assets.ts +105 -0
- package/corpus/templates/design/actions/list-design-states.ts +19 -1
- package/corpus/templates/design/actions/list-figma-library-assets.ts +292 -0
- package/corpus/templates/design/actions/migrate-board-objects-to-file.ts +238 -0
- package/corpus/templates/design/actions/navigate.ts +18 -3
- package/corpus/templates/design/actions/open-component-source.ts +7 -2
- package/corpus/templates/design/actions/preview-component-prop-edit.ts +32 -26
- package/corpus/templates/design/actions/preview-shader-fill.ts +9 -11
- package/corpus/templates/design/actions/provider-api-catalog.ts +2 -2
- package/corpus/templates/design/actions/provider-api-docs.ts +1 -1
- package/corpus/templates/design/actions/provider-api-request.ts +3 -3
- package/corpus/templates/design/actions/revoke-localhost-write-consent.ts +48 -0
- package/corpus/templates/design/actions/run-design-audit.ts +74 -10
- package/corpus/templates/design/actions/run-design-extension-action.ts +1 -1
- package/corpus/templates/design/actions/view-screen.ts +1 -1
- package/corpus/templates/design/actions/write-local-file.ts +213 -0
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +244 -3197
- package/corpus/templates/design/app/components/design/DesignEditorSkeleton.tsx +30 -20
- package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +795 -289
- package/corpus/templates/design/app/components/design/EditPanel.tsx +635 -234
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +124 -32
- package/corpus/templates/design/app/components/design/LocalhostWriteConsentDialog.tsx +161 -0
- package/corpus/templates/design/app/components/design/MotionDock.tsx +244 -266
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +491 -56
- package/corpus/templates/design/app/components/design/ReviewPanel.tsx +55 -67
- package/corpus/templates/design/app/components/design/StatesPanel.tsx +38 -63
- package/corpus/templates/design/app/components/design/TokensPanel.tsx +264 -8
- package/corpus/templates/design/app/components/design/bridge/codegen.ts +125 -0
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +4037 -0
- package/corpus/templates/design/app/components/design/bridge/embedded-wheel.bridge.ts +62 -0
- package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +272 -0
- package/corpus/templates/design/app/components/design/bridge/nav.bridge.ts +116 -0
- package/corpus/templates/design/app/components/design/bridge/sample.bridge.ts +24 -0
- package/corpus/templates/design/app/components/design/bridge/shader-fill-preview.bridge.ts +78 -0
- package/corpus/templates/design/app/components/design/bridge/tsconfig.json +17 -0
- package/corpus/templates/design/app/components/design/bridge/tweak.bridge.ts +28 -0
- package/corpus/templates/design/app/components/design/bridge/zoom.bridge.ts +43 -0
- package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +45 -16
- package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +88 -9
- package/corpus/templates/design/app/components/editor/PromptDialog.tsx +94 -9
- package/corpus/templates/design/app/components/layout/Layout.tsx +19 -3
- package/corpus/templates/design/app/components/layout/Sidebar.tsx +1 -1
- package/corpus/templates/design/app/global.css +7 -0
- package/corpus/templates/design/app/hooks/use-navigation-state.ts +23 -0
- package/corpus/templates/design/app/i18n/zh-TW.ts +22 -0
- package/corpus/templates/design/app/i18n-data.ts +369 -0
- package/corpus/templates/design/app/lib/pending-generation.ts +1 -0
- package/corpus/templates/design/app/pages/DesignEditor.tsx +3024 -616
- package/corpus/templates/design/app/pages/Index.tsx +5 -1
- package/corpus/templates/design/changelog/2026-06-30-applying-the-one-click-fix-for-a-small-tap-target-accessibil.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-canvas-creation-screen-dragging-undo-redo-and-image-fill-edi.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-component-prop-edits-and-shader-fills-now-preserve-rapid-sam.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-design-can-browse-figma-library-components-and-insert-them-i.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-design-now-has-a-figma-style-left-rail-for-file-agent-assets.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-design-now-has-native-components-embedded-media-and-figma-im.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-design-preview-now-includes-a-publish-app-waitlist-option-fr.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-design-s-left-rail-now-matches-the-sidebar-chrome-and-uses-t.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-design-s-tools-create-menu-now-stays-compact-instead-of-rend.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-design-starter-prompts-now-begin-generating-immediately-and-.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-design-token-edits-now-stay-visible-in-previews-and-token-li.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-design-tools-now-use-a-quieter-figma-style-list-layout.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-drawing-rectangles-on-the-all-screens-canvas-no-longer-flash.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-fixed-code-native-design-studio-edits-so-component-props-mot.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-fixed-visual-editing-hover-overlays-and-spacing-handles-so-s.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-motion-edits-now-save-automatically-without-a-separate-css-a.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-motion-timelines-now-reopen-as-editable-tracks-after-writing.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-new-design-prompts-with-uploaded-screenshots-work-reliably-o.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-screen-previews-in-all-screens-view-now-keep-rounded-outline.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-selection-outlines-now-sit-flush-on-rectangles-drawn-directl.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-share-options-use-a-tighter-design-editor-popover-with-compa.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-the-collapsed-motion-panel-now-stays-tucked-under-the-left-s.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-the-design-inspector-now-shows-states-and-review-in-a-tighte.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-the-sidebar-keeps-the-organization-picker-space-stable-while.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-tokens-can-now-be-imported-from-pasted-notes-code-files-fold.md +6 -0
- package/corpus/templates/design/e2e/global-setup.ts +94 -2
- package/corpus/templates/design/e2e/helpers.ts +168 -65
- package/corpus/templates/design/package.json +3 -1
- package/corpus/templates/design/server/db/schema.ts +23 -0
- package/corpus/templates/design/server/handlers/uploads.ts +22 -1
- package/corpus/templates/design/server/lib/provider-api.ts +7 -4
- package/corpus/templates/design/server/lib/verify-write-grant.ts +108 -0
- package/corpus/templates/design/server/plugins/db.ts +39 -0
- package/corpus/templates/design/server/register-secrets.ts +38 -0
- package/corpus/templates/design/shared/board-file.ts +228 -0
- package/corpus/templates/design/shared/board-objects.ts +288 -0
- package/corpus/templates/design/shared/code-layer.ts +52 -0
- package/corpus/templates/design/shared/component-model.ts +35 -0
- package/corpus/templates/design/shared/motion-compiler.ts +137 -15
- package/corpus/templates/design/shared/motion-timeline.ts +2 -2
- package/corpus/templates/design/shared/resolve-tweaks.ts +32 -9
- package/corpus/templates/design/tsconfig.json +2 -1
- package/corpus/templates/slides/AGENTS.md +9 -0
- package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +4 -0
- package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +4 -24
- package/corpus/templates/slides/app/lib/export-pdf-client.ts +29 -20
- package/corpus/templates/slides/app/lib/export-pptx-client.ts +365 -0
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +12 -9
- package/corpus/templates/slides/app/vite-env.d.ts +18 -0
- package/corpus/templates/slides/changelog/2026-06-30-powerpoint-downloads-now-preserve-the-rendered-slide-layout-.md +6 -0
- package/corpus/templates/slides/changelog/2026-06-30-unavailable-decks-open-in-a-cleaner-full-height-centered-sta.md +6 -0
- package/corpus/templates/slides/package.json +1 -0
- package/corpus/templates/slides/vite.config.ts +1 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +94 -10
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/cli/design-connect.d.ts +25 -0
- package/dist/cli/design-connect.d.ts.map +1 -1
- package/dist/cli/design-connect.js +357 -18
- package/dist/cli/design-connect.js.map +1 -1
- package/dist/client/AssistantChat.d.ts +14 -0
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +115 -22
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/ConnectBuilderCard.d.ts.map +1 -1
- package/dist/client/ConnectBuilderCard.js +1 -0
- package/dist/client/ConnectBuilderCard.js.map +1 -1
- package/dist/client/chat/index.d.ts +1 -1
- package/dist/client/chat/index.d.ts.map +1 -1
- package/dist/client/chat/index.js +1 -1
- package/dist/client/chat/index.js.map +1 -1
- package/dist/client/chat/run-recovery.d.ts.map +1 -1
- package/dist/client/chat/run-recovery.js +5 -5
- package/dist/client/chat/run-recovery.js.map +1 -1
- package/dist/client/chat-view-transition.d.ts +6 -0
- package/dist/client/chat-view-transition.d.ts.map +1 -1
- package/dist/client/chat-view-transition.js +23 -2
- package/dist/client/chat-view-transition.js.map +1 -1
- package/dist/client/composer/PromptComposer.d.ts.map +1 -1
- package/dist/client/composer/PromptComposer.js +3 -2
- package/dist/client/composer/PromptComposer.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/OrgSwitcher.d.ts.map +1 -1
- package/dist/client/org/OrgSwitcher.js +11 -4
- package/dist/client/org/OrgSwitcher.js.map +1 -1
- package/dist/client/sharing/ShareButton.js +3 -3
- package/dist/client/sharing/ShareButton.js.map +1 -1
- package/dist/client/sse-event-processor.d.ts.map +1 -1
- package/dist/client/sse-event-processor.js +10 -0
- package/dist/client/sse-event-processor.js.map +1 -1
- package/dist/client/use-agent-chat-home-handoff.d.ts +8 -2
- package/dist/client/use-agent-chat-home-handoff.d.ts.map +1 -1
- package/dist/client/use-agent-chat-home-handoff.js +20 -6
- package/dist/client/use-agent-chat-home-handoff.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 +2 -2
- package/dist/db/client.d.ts +1 -0
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +28 -2
- package/dist/db/client.js.map +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/provider-api/index.d.ts +5 -4
- package/dist/provider-api/index.d.ts.map +1 -1
- package/dist/provider-api/index.js +28 -0
- package/dist/provider-api/index.js.map +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/core-routes-plugin.d.ts +25 -0
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +43 -18
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/styles/agent-native.css +54 -0
- package/dist/usage/store.js +2 -2
- package/dist/usage/store.js.map +1 -1
- package/package.json +1 -1
|
@@ -6,37 +6,32 @@
|
|
|
6
6
|
* 2. Persist the `motion_timeline` row (insert or update).
|
|
7
7
|
* 3. Compile the tracks into deterministic CSS.
|
|
8
8
|
* 4. Inject/replace the managed `<style data-agent-native-motion>` block inside
|
|
9
|
-
* the design's HTML content
|
|
10
|
-
* (Yjs/collab + SQL, via agentEnterDocument / applyText / seedFromText).
|
|
9
|
+
* the design's durable HTML content.
|
|
11
10
|
* 5. Update `compiledHash` on the row to guard against drift.
|
|
12
11
|
* 6. Return a diff summary (bytes before/after, track count, hash).
|
|
13
12
|
*
|
|
14
13
|
* Never writes unless all steps succeed. Scrubbing/preview is handled by the
|
|
15
14
|
* separate `motion-preview` postMessage path on the frontend — this action is
|
|
16
|
-
* the
|
|
15
|
+
* the durable autosave/persist path for edited timelines.
|
|
17
16
|
*/
|
|
18
17
|
|
|
19
18
|
import { defineAction } from "@agent-native/core";
|
|
20
|
-
import {
|
|
21
|
-
agentEnterDocument,
|
|
22
|
-
agentLeaveDocument,
|
|
23
|
-
applyText,
|
|
24
|
-
getText,
|
|
25
|
-
hasCollabState,
|
|
26
|
-
seedFromText,
|
|
27
|
-
} from "@agent-native/core/collab";
|
|
28
19
|
import {
|
|
29
20
|
getRequestOrgId,
|
|
30
21
|
getRequestUserEmail,
|
|
31
22
|
} from "@agent-native/core/server/request-context";
|
|
32
23
|
import { accessFilter, assertAccess } from "@agent-native/core/sharing";
|
|
33
|
-
import { and, eq } from "drizzle-orm";
|
|
24
|
+
import { and, desc, eq } from "drizzle-orm";
|
|
34
25
|
import { nanoid } from "nanoid";
|
|
35
26
|
import { z } from "zod";
|
|
36
27
|
|
|
37
28
|
import { getDb, schema } from "../server/db/index.js";
|
|
38
29
|
import "../server/db/index.js"; // ensure registerShareableResource runs
|
|
39
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
assertSafeMotionCssProperty,
|
|
32
|
+
assertSafeMotionCssToken,
|
|
33
|
+
compile,
|
|
34
|
+
} from "../shared/motion-compiler.js";
|
|
40
35
|
import type { MotionTrack } from "../shared/motion-timeline.js";
|
|
41
36
|
|
|
42
37
|
// ─── Zod schemas ─────────────────────────────────────────────────────────────
|
|
@@ -85,47 +80,6 @@ const MOTION_STYLE_CLOSE = "</style>";
|
|
|
85
80
|
*/
|
|
86
81
|
const STYLE_CLOSE_RE = /<\s*\/\s*style\b[^>]*>/i;
|
|
87
82
|
|
|
88
|
-
/**
|
|
89
|
-
* Reject `<`, `>`, and any `</style` sequence in a CSS token before it is
|
|
90
|
-
* compiled into the managed `<style>` block. Keyframe `value` / `ease` come
|
|
91
|
-
* straight from the caller and are interpolated raw into the CSS, so an
|
|
92
|
-
* unescaped `</style>` (or angle bracket) would break out of the style block
|
|
93
|
-
* and inject arbitrary markup. Returns the original string when safe; throws on
|
|
94
|
-
* a breakout attempt so the whole atomic write fails loudly rather than
|
|
95
|
-
* persisting a poisoned block.
|
|
96
|
-
*/
|
|
97
|
-
function assertSafeCssToken(value: string, field: string): string {
|
|
98
|
-
if (/[<>]/.test(value) || /<\s*\/\s*style/i.test(value)) {
|
|
99
|
-
throw new Error(
|
|
100
|
-
`Invalid ${field}: "<", ">", and "</style" are not allowed in motion CSS values.`,
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
return value;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Validate that a CSS property name is a safe CSS identifier.
|
|
108
|
-
*
|
|
109
|
-
* Accepts standard and vendor-prefixed property names (e.g. "opacity",
|
|
110
|
-
* "transform", "-webkit-transform") and nothing else. Rejects any string
|
|
111
|
-
* containing `:`, `;`, `{`, `}`, `<`, `/`, whitespace, or other characters
|
|
112
|
-
* that could break out of a CSS declaration or `<style>` block context when
|
|
113
|
-
* the property is interpolated as `${property}: ${value};` inside
|
|
114
|
-
* `@keyframes`.
|
|
115
|
-
*
|
|
116
|
-
* Uses the strict CSS ident regex `^-?[a-zA-Z][a-zA-Z0-9-]*$` which covers
|
|
117
|
-
* every real animatable CSS property while blocking injection payloads.
|
|
118
|
-
*/
|
|
119
|
-
function assertSafeCssProperty(property: string, field: string): string {
|
|
120
|
-
if (!/^-?[a-zA-Z][a-zA-Z0-9-]*$/.test(property)) {
|
|
121
|
-
throw new Error(
|
|
122
|
-
`Invalid ${field}: "${property}" is not a valid CSS property identifier. ` +
|
|
123
|
-
"Only ASCII letters, digits, hyphens, and an optional leading hyphen are allowed.",
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
return property;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
83
|
/**
|
|
130
84
|
* Inject or replace the managed `<style data-agent-native-motion>` block in
|
|
131
85
|
* the HTML content. Inserts before `</head>` when not already present.
|
|
@@ -158,21 +112,6 @@ function injectMotionStyle(html: string, css: string): string {
|
|
|
158
112
|
return block + "\n" + html;
|
|
159
113
|
}
|
|
160
114
|
|
|
161
|
-
async function liveFileContent(
|
|
162
|
-
fileId: string,
|
|
163
|
-
storedContent: string,
|
|
164
|
-
): Promise<string> {
|
|
165
|
-
try {
|
|
166
|
-
if (await hasCollabState(fileId)) {
|
|
167
|
-
const live = await getText(fileId, "content");
|
|
168
|
-
if (typeof live === "string") return live;
|
|
169
|
-
}
|
|
170
|
-
} catch {
|
|
171
|
-
// Best-effort; SQL is the fallback.
|
|
172
|
-
}
|
|
173
|
-
return storedContent;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
115
|
async function persistFileContent(
|
|
177
116
|
fileId: string,
|
|
178
117
|
designId: string,
|
|
@@ -180,29 +119,21 @@ async function persistFileContent(
|
|
|
180
119
|
now: string,
|
|
181
120
|
): Promise<void> {
|
|
182
121
|
const db = getDb();
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
// invoked; this only touches the addressed design row's updatedAt.
|
|
199
|
-
await db
|
|
200
|
-
.update(schema.designs)
|
|
201
|
-
.set({ updatedAt: now })
|
|
202
|
-
.where(eq(schema.designs.id, designId));
|
|
203
|
-
} finally {
|
|
204
|
-
agentLeaveDocument(fileId);
|
|
205
|
-
}
|
|
122
|
+
await db
|
|
123
|
+
.update(schema.designFiles)
|
|
124
|
+
.set({ content, updatedAt: now })
|
|
125
|
+
.where(eq(schema.designFiles.id, fileId));
|
|
126
|
+
|
|
127
|
+
// Keep SQL as the source of truth for this atomic write. The editor adopts
|
|
128
|
+
// the returned HTML content without re-saving it; applying the whole document
|
|
129
|
+
// through an existing collab text snapshot can merge against stale iframe
|
|
130
|
+
// state and duplicate the managed motion stylesheet.
|
|
131
|
+
// guard:allow-unscoped — editor access on this design is asserted in run()
|
|
132
|
+
// before this helper is invoked; this only bumps the addressed design row.
|
|
133
|
+
await db
|
|
134
|
+
.update(schema.designs)
|
|
135
|
+
.set({ updatedAt: now })
|
|
136
|
+
.where(eq(schema.designs.id, designId));
|
|
206
137
|
}
|
|
207
138
|
|
|
208
139
|
// ─── Action ───────────────────────────────────────────────────────────────────
|
|
@@ -213,7 +144,7 @@ export default defineAction({
|
|
|
213
144
|
"Persists the motion_timeline row, compiles tracks to CSS, injects the " +
|
|
214
145
|
"managed <style data-agent-native-motion> block into the design's HTML, " +
|
|
215
146
|
"and updates compiledHash — all in one atomic step. " +
|
|
216
|
-
"This is the
|
|
147
|
+
"This is the durable timeline persist path; preview/scrubbing uses the " +
|
|
217
148
|
"motion-preview postMessage bridge, NOT this action.",
|
|
218
149
|
schema: z.object({
|
|
219
150
|
designId: z.string().describe("Design project ID."),
|
|
@@ -268,6 +199,14 @@ export default defineAction({
|
|
|
268
199
|
.optional()
|
|
269
200
|
.default(false)
|
|
270
201
|
.describe("Include the full patched HTML in the response (large)."),
|
|
202
|
+
currentContent: z
|
|
203
|
+
.string()
|
|
204
|
+
.optional()
|
|
205
|
+
.describe(
|
|
206
|
+
"Current open editor HTML for the target file. When supplied, the " +
|
|
207
|
+
"managed motion CSS is patched into this content instead of the " +
|
|
208
|
+
"last SQL snapshot so in-flight local edits are preserved.",
|
|
209
|
+
),
|
|
271
210
|
}),
|
|
272
211
|
run: async ({
|
|
273
212
|
designId,
|
|
@@ -278,6 +217,7 @@ export default defineAction({
|
|
|
278
217
|
durationMs,
|
|
279
218
|
defaultEase,
|
|
280
219
|
includeContent,
|
|
220
|
+
currentContent: currentContentInput,
|
|
281
221
|
}) => {
|
|
282
222
|
await assertAccess("design", designId, "editor");
|
|
283
223
|
|
|
@@ -319,7 +259,11 @@ export default defineAction({
|
|
|
319
259
|
}
|
|
320
260
|
|
|
321
261
|
const fileId = file.id;
|
|
322
|
-
const
|
|
262
|
+
const resolvedSourceRef = sourceRef ?? fileId;
|
|
263
|
+
const currentContent =
|
|
264
|
+
currentContentInput !== undefined
|
|
265
|
+
? currentContentInput
|
|
266
|
+
: (file.content ?? "");
|
|
323
267
|
|
|
324
268
|
// ── 2. Compile tracks → CSS ─────────────────────────────────────────────
|
|
325
269
|
const typedTracks = tracks as MotionTrack[];
|
|
@@ -328,20 +272,20 @@ export default defineAction({
|
|
|
328
272
|
// keyframe values, and easing strings before they are compiled into the
|
|
329
273
|
// managed <style> block.
|
|
330
274
|
for (const track of typedTracks) {
|
|
331
|
-
|
|
275
|
+
assertSafeMotionCssProperty(track.property, "track.property");
|
|
332
276
|
for (const kf of track.keyframes) {
|
|
333
|
-
|
|
277
|
+
assertSafeMotionCssToken(kf.value, "keyframe value");
|
|
334
278
|
if (kf.ease !== undefined) {
|
|
335
|
-
|
|
279
|
+
assertSafeMotionCssToken(kf.ease, "keyframe ease");
|
|
336
280
|
}
|
|
337
281
|
}
|
|
338
282
|
}
|
|
339
|
-
|
|
283
|
+
assertSafeMotionCssToken(defaultEase, "defaultEase");
|
|
340
284
|
|
|
341
285
|
const { css, hash } = compile({
|
|
342
286
|
id: timelineId ?? "",
|
|
343
287
|
designId,
|
|
344
|
-
sourceRef:
|
|
288
|
+
sourceRef: resolvedSourceRef,
|
|
345
289
|
filePath: null,
|
|
346
290
|
tracks: typedTracks,
|
|
347
291
|
durationMs,
|
|
@@ -360,7 +304,7 @@ export default defineAction({
|
|
|
360
304
|
// Resolve everything that can fail (existence + ownership) BEFORE touching
|
|
361
305
|
// content, so we never persist HTML for a row that can't be written.
|
|
362
306
|
const tracksJson = JSON.stringify(typedTracks);
|
|
363
|
-
|
|
307
|
+
let existingTimelineId = timelineId;
|
|
364
308
|
|
|
365
309
|
let insertOwnerEmail: string | null = null;
|
|
366
310
|
let insertOrgId: string | null = null;
|
|
@@ -384,21 +328,37 @@ export default defineAction({
|
|
|
384
328
|
);
|
|
385
329
|
}
|
|
386
330
|
} else {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
331
|
+
const [existingForSource] = await db
|
|
332
|
+
.select({ id: schema.motionTimeline.id })
|
|
333
|
+
.from(schema.motionTimeline)
|
|
334
|
+
.where(
|
|
335
|
+
and(
|
|
336
|
+
eq(schema.motionTimeline.designId, designId),
|
|
337
|
+
eq(schema.motionTimeline.sourceRef, resolvedSourceRef),
|
|
338
|
+
),
|
|
339
|
+
)
|
|
340
|
+
.orderBy(desc(schema.motionTimeline.updatedAt))
|
|
341
|
+
.limit(1);
|
|
342
|
+
|
|
343
|
+
if (existingForSource) {
|
|
344
|
+
existingTimelineId = existingForSource.id;
|
|
345
|
+
} else {
|
|
346
|
+
// Insert new row — derive ownership from the request context (same
|
|
347
|
+
// pattern as create-design-state and other create actions).
|
|
348
|
+
insertOwnerEmail = getRequestUserEmail() ?? null;
|
|
349
|
+
if (!insertOwnerEmail) throw new Error("no authenticated user");
|
|
350
|
+
insertOrgId = getRequestOrgId() ?? null;
|
|
351
|
+
}
|
|
392
352
|
}
|
|
393
353
|
|
|
354
|
+
const resolvedTimelineId = existingTimelineId ?? nanoid();
|
|
355
|
+
|
|
394
356
|
// ── 5. Persist the motion_timeline row FIRST (atomic SQL portion) ───────
|
|
395
357
|
// The timeline row is written before the HTML so that a failure in the
|
|
396
|
-
// HTML
|
|
397
|
-
// corresponding row.
|
|
398
|
-
// guarantee: if the row write failed after the HTML write, the managed
|
|
399
|
-
// <style> block would be permanently out of sync with the DB state.
|
|
358
|
+
// HTML write step cannot leave the design content mutated without a
|
|
359
|
+
// corresponding row.
|
|
400
360
|
await db.transaction(async (tx) => {
|
|
401
|
-
if (
|
|
361
|
+
if (existingTimelineId) {
|
|
402
362
|
await tx
|
|
403
363
|
.update(schema.motionTimeline)
|
|
404
364
|
.set({
|
|
@@ -406,15 +366,15 @@ export default defineAction({
|
|
|
406
366
|
durationMs,
|
|
407
367
|
defaultEase,
|
|
408
368
|
compiledHash: hash,
|
|
409
|
-
sourceRef:
|
|
369
|
+
sourceRef: resolvedSourceRef,
|
|
410
370
|
updatedAt: now,
|
|
411
371
|
})
|
|
412
|
-
.where(eq(schema.motionTimeline.id,
|
|
372
|
+
.where(eq(schema.motionTimeline.id, existingTimelineId));
|
|
413
373
|
} else {
|
|
414
374
|
await tx.insert(schema.motionTimeline).values({
|
|
415
375
|
id: resolvedTimelineId,
|
|
416
376
|
designId,
|
|
417
|
-
sourceRef:
|
|
377
|
+
sourceRef: resolvedSourceRef,
|
|
418
378
|
filePath: null,
|
|
419
379
|
tracks: tracksJson,
|
|
420
380
|
durationMs,
|
|
@@ -428,8 +388,8 @@ export default defineAction({
|
|
|
428
388
|
}
|
|
429
389
|
});
|
|
430
390
|
|
|
431
|
-
// ── 6. Persist the patched HTML content SECOND
|
|
432
|
-
// Written after the row so a
|
|
391
|
+
// ── 6. Persist the patched HTML content SECOND ─────────────────────────
|
|
392
|
+
// Written after the row so a SQL failure here leaves the timeline row
|
|
433
393
|
// accurate (correct tracks + hash) and the stale HTML can be recompiled on
|
|
434
394
|
// the next apply-motion-edit call via compiledHash drift detection.
|
|
435
395
|
await persistFileContent(fileId, designId, patchedContent, now);
|
|
@@ -438,6 +398,7 @@ export default defineAction({
|
|
|
438
398
|
timelineId: resolvedTimelineId,
|
|
439
399
|
designId,
|
|
440
400
|
fileId,
|
|
401
|
+
sourceRef: resolvedSourceRef,
|
|
441
402
|
trackCount: typedTracks.length,
|
|
442
403
|
compiledHash: hash,
|
|
443
404
|
bytesBefore,
|
|
@@ -19,24 +19,15 @@
|
|
|
19
19
|
* - The descriptor is validated against the preset manifest first, so callers
|
|
20
20
|
* get clear errors before any round-trip.
|
|
21
21
|
*
|
|
22
|
-
* The write
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* the resolved node — no structural inserts, no new owned rows.
|
|
22
|
+
* The write persists durable SQL content directly. The edit is a single
|
|
23
|
+
* inline-style `style.background` change on the resolved node — no structural
|
|
24
|
+
* inserts, no new owned rows.
|
|
26
25
|
*
|
|
27
26
|
* Plan reference: DESIGN-STUDIO-PLAN.md §6.7 + §7 (shader fill apply).
|
|
28
27
|
*/
|
|
29
28
|
|
|
30
29
|
import { defineAction } from "@agent-native/core";
|
|
31
|
-
import {
|
|
32
|
-
agentEnterDocument,
|
|
33
|
-
agentLeaveDocument,
|
|
34
|
-
agentUpdateSelection,
|
|
35
|
-
applyText,
|
|
36
|
-
getText,
|
|
37
|
-
hasCollabState,
|
|
38
|
-
seedFromText,
|
|
39
|
-
} from "@agent-native/core/collab";
|
|
30
|
+
import { agentUpdateSelection } from "@agent-native/core/collab";
|
|
40
31
|
import { accessFilter, assertAccess } from "@agent-native/core/sharing";
|
|
41
32
|
import { and, eq } from "drizzle-orm";
|
|
42
33
|
import { z } from "zod";
|
|
@@ -65,6 +56,15 @@ const PRESET_NAMES = Object.keys(SHADER_PRESET_MAP) as [
|
|
|
65
56
|
ShaderPresetName,
|
|
66
57
|
...ShaderPresetName[],
|
|
67
58
|
];
|
|
59
|
+
const REVISION_CONFLICT_MESSAGE =
|
|
60
|
+
"This file changed since this shader fill was previewed. Refresh the editor and try again.";
|
|
61
|
+
|
|
62
|
+
class ShaderFillRevisionConflictError extends Error {
|
|
63
|
+
constructor() {
|
|
64
|
+
super(REVISION_CONFLICT_MESSAGE);
|
|
65
|
+
this.name = "ShaderFillRevisionConflictError";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
68
|
|
|
69
69
|
const descriptorSchema = z.object({
|
|
70
70
|
preset: z
|
|
@@ -101,6 +101,16 @@ const sourceSchema = z
|
|
|
101
101
|
fileId: z.string().optional(),
|
|
102
102
|
filename: z.string().optional(),
|
|
103
103
|
revision: z.string().optional(),
|
|
104
|
+
currentContent: z
|
|
105
|
+
.string()
|
|
106
|
+
.optional()
|
|
107
|
+
.describe(
|
|
108
|
+
"Current open editor HTML for the target file. When supplied, the " +
|
|
109
|
+
"shader background is patched into this content instead of the " +
|
|
110
|
+
"last SQL snapshot so in-flight local edits are preserved. " +
|
|
111
|
+
"source.revision must also be supplied as the file updatedAt value " +
|
|
112
|
+
"that currentContent was based on.",
|
|
113
|
+
),
|
|
104
114
|
})
|
|
105
115
|
.describe(
|
|
106
116
|
"Design source. Only kind=design-file (HTML) is persisted. Provide " +
|
|
@@ -132,24 +142,11 @@ interface ResolvedDesignFile {
|
|
|
132
142
|
designId: string;
|
|
133
143
|
filename: string;
|
|
134
144
|
content: string;
|
|
145
|
+
updatedAt: string | null;
|
|
146
|
+
expectedUpdatedAt?: string;
|
|
135
147
|
codeLayerSource: CodeLayerSource;
|
|
136
148
|
}
|
|
137
149
|
|
|
138
|
-
async function liveContent(
|
|
139
|
-
fileId: string,
|
|
140
|
-
storedContent: string,
|
|
141
|
-
): Promise<string> {
|
|
142
|
-
try {
|
|
143
|
-
if (await hasCollabState(fileId)) {
|
|
144
|
-
const live = await getText(fileId, "content");
|
|
145
|
-
if (typeof live === "string") return live;
|
|
146
|
-
}
|
|
147
|
-
} catch {
|
|
148
|
-
// Collab reads are best-effort; SQL content remains the fallback.
|
|
149
|
-
}
|
|
150
|
-
return storedContent;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
150
|
/**
|
|
154
151
|
* Resolve the target HTML design file with an access-scoped read, then assert
|
|
155
152
|
* editor access. Mirrors `resolveEditableDesignFile` in apply-visual-edit.ts so
|
|
@@ -160,6 +157,7 @@ async function resolveEditableDesignFile(source: {
|
|
|
160
157
|
fileId?: string;
|
|
161
158
|
filename?: string;
|
|
162
159
|
revision?: string;
|
|
160
|
+
currentContent?: string;
|
|
163
161
|
}): Promise<ResolvedDesignFile> {
|
|
164
162
|
if (!source.fileId && !source.designId) {
|
|
165
163
|
throw new Error(
|
|
@@ -187,6 +185,7 @@ async function resolveEditableDesignFile(source: {
|
|
|
187
185
|
filename: schema.designFiles.filename,
|
|
188
186
|
fileType: schema.designFiles.fileType,
|
|
189
187
|
content: schema.designFiles.content,
|
|
188
|
+
updatedAt: schema.designFiles.updatedAt,
|
|
190
189
|
})
|
|
191
190
|
.from(schema.designFiles)
|
|
192
191
|
.innerJoin(
|
|
@@ -217,11 +216,31 @@ async function resolveEditableDesignFile(source: {
|
|
|
217
216
|
|
|
218
217
|
await assertAccess("design", file.designId, "editor");
|
|
219
218
|
|
|
219
|
+
if (source.currentContent !== undefined && !source.revision) {
|
|
220
|
+
throw new Error(
|
|
221
|
+
"source.revision is required when source.currentContent is provided.",
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
if (
|
|
225
|
+
source.currentContent !== undefined &&
|
|
226
|
+
source.revision &&
|
|
227
|
+
file.updatedAt &&
|
|
228
|
+
source.revision !== file.updatedAt
|
|
229
|
+
) {
|
|
230
|
+
throw new ShaderFillRevisionConflictError();
|
|
231
|
+
}
|
|
232
|
+
|
|
220
233
|
return {
|
|
221
234
|
id: file.id,
|
|
222
235
|
designId: file.designId,
|
|
223
236
|
filename: file.filename,
|
|
224
|
-
|
|
237
|
+
updatedAt: file.updatedAt,
|
|
238
|
+
expectedUpdatedAt:
|
|
239
|
+
source.currentContent !== undefined ? source.revision : undefined,
|
|
240
|
+
content:
|
|
241
|
+
source.currentContent !== undefined
|
|
242
|
+
? source.currentContent
|
|
243
|
+
: (file.content ?? ""),
|
|
225
244
|
codeLayerSource: {
|
|
226
245
|
kind: "design-file",
|
|
227
246
|
designId: file.designId,
|
|
@@ -236,35 +255,65 @@ async function persistDesignFileEdit(file: {
|
|
|
236
255
|
id: string;
|
|
237
256
|
designId: string;
|
|
238
257
|
content: string;
|
|
239
|
-
|
|
258
|
+
expectedUpdatedAt?: string;
|
|
259
|
+
}): Promise<string> {
|
|
240
260
|
await assertAccess("design", file.designId, "editor");
|
|
241
261
|
|
|
242
262
|
const db = getDb();
|
|
243
263
|
const now = new Date().toISOString();
|
|
244
264
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
.
|
|
249
|
-
.
|
|
250
|
-
.
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
265
|
+
if (file.expectedUpdatedAt) {
|
|
266
|
+
const [latest] = await db
|
|
267
|
+
.select({ updatedAt: schema.designFiles.updatedAt })
|
|
268
|
+
.from(schema.designFiles)
|
|
269
|
+
.where(eq(schema.designFiles.id, file.id))
|
|
270
|
+
.limit(1);
|
|
271
|
+
if (!latest) {
|
|
272
|
+
throw new Error("Design HTML file not found.");
|
|
273
|
+
}
|
|
274
|
+
if (latest.updatedAt && latest.updatedAt !== file.expectedUpdatedAt) {
|
|
275
|
+
throw new ShaderFillRevisionConflictError();
|
|
256
276
|
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
await db
|
|
280
|
+
.update(schema.designFiles)
|
|
281
|
+
.set({ content: file.content, updatedAt: now })
|
|
282
|
+
.where(
|
|
283
|
+
file.expectedUpdatedAt
|
|
284
|
+
? and(
|
|
285
|
+
eq(schema.designFiles.id, file.id),
|
|
286
|
+
eq(schema.designFiles.updatedAt, file.expectedUpdatedAt),
|
|
287
|
+
)
|
|
288
|
+
: eq(schema.designFiles.id, file.id),
|
|
289
|
+
);
|
|
257
290
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
.
|
|
265
|
-
|
|
266
|
-
|
|
291
|
+
if (file.expectedUpdatedAt) {
|
|
292
|
+
const [saved] = await db
|
|
293
|
+
.select({
|
|
294
|
+
content: schema.designFiles.content,
|
|
295
|
+
updatedAt: schema.designFiles.updatedAt,
|
|
296
|
+
})
|
|
297
|
+
.from(schema.designFiles)
|
|
298
|
+
.where(eq(schema.designFiles.id, file.id))
|
|
299
|
+
.limit(1);
|
|
300
|
+
if (!saved || saved.updatedAt !== now || saved.content !== file.content) {
|
|
301
|
+
throw new ShaderFillRevisionConflictError();
|
|
302
|
+
}
|
|
267
303
|
}
|
|
304
|
+
|
|
305
|
+
// Keep SQL as the source of truth for this guarded server write. The editor
|
|
306
|
+
// already has the live shader preview applied; feeding a full HTML document
|
|
307
|
+
// back through an existing collab text snapshot can merge against stale
|
|
308
|
+
// iframe state and corrupt the saved source.
|
|
309
|
+
// guard:allow-unscoped — editor access on this design is asserted above
|
|
310
|
+
// before this helper is invoked; this only bumps the addressed design row.
|
|
311
|
+
await db
|
|
312
|
+
.update(schema.designs)
|
|
313
|
+
.set({ updatedAt: now })
|
|
314
|
+
.where(eq(schema.designs.id, file.designId));
|
|
315
|
+
|
|
316
|
+
return now;
|
|
268
317
|
}
|
|
269
318
|
|
|
270
319
|
// ─── Action ──────────────────────────────────────────────────────────────────
|
|
@@ -350,7 +399,25 @@ snippet (WebGL canvas / JSX component), call apply-shader.
|
|
|
350
399
|
};
|
|
351
400
|
}
|
|
352
401
|
|
|
353
|
-
|
|
402
|
+
let file: ResolvedDesignFile;
|
|
403
|
+
try {
|
|
404
|
+
file = await resolveEditableDesignFile(source);
|
|
405
|
+
} catch (error) {
|
|
406
|
+
if (error instanceof ShaderFillRevisionConflictError) {
|
|
407
|
+
return {
|
|
408
|
+
ok: false,
|
|
409
|
+
persisted: false,
|
|
410
|
+
conflict: true,
|
|
411
|
+
descriptor,
|
|
412
|
+
background,
|
|
413
|
+
fallbackCss,
|
|
414
|
+
colors,
|
|
415
|
+
error: error.message,
|
|
416
|
+
note: error.message,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
throw error;
|
|
420
|
+
}
|
|
354
421
|
|
|
355
422
|
const intent: StyleEditIntent = {
|
|
356
423
|
kind: "style",
|
|
@@ -374,13 +441,36 @@ snippet (WebGL canvas / JSX component), call apply-shader.
|
|
|
374
441
|
|
|
375
442
|
const changed =
|
|
376
443
|
patch.result.status === "applied" && patch.result.changed === true;
|
|
444
|
+
let updatedAt: string | undefined;
|
|
377
445
|
|
|
378
446
|
if (changed) {
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
447
|
+
try {
|
|
448
|
+
updatedAt = await persistDesignFileEdit({
|
|
449
|
+
id: file.id,
|
|
450
|
+
designId: file.designId,
|
|
451
|
+
content: patch.content,
|
|
452
|
+
expectedUpdatedAt: file.expectedUpdatedAt,
|
|
453
|
+
});
|
|
454
|
+
} catch (error) {
|
|
455
|
+
if (error instanceof ShaderFillRevisionConflictError) {
|
|
456
|
+
return {
|
|
457
|
+
ok: false,
|
|
458
|
+
persisted: false,
|
|
459
|
+
conflict: true,
|
|
460
|
+
descriptor,
|
|
461
|
+
background,
|
|
462
|
+
fallbackCss,
|
|
463
|
+
colors,
|
|
464
|
+
designId: file.designId,
|
|
465
|
+
fileId: file.id,
|
|
466
|
+
filename: file.filename,
|
|
467
|
+
result: patch.result,
|
|
468
|
+
error: error.message,
|
|
469
|
+
note: error.message,
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
throw error;
|
|
473
|
+
}
|
|
384
474
|
}
|
|
385
475
|
|
|
386
476
|
return {
|
|
@@ -393,7 +483,9 @@ snippet (WebGL canvas / JSX component), call apply-shader.
|
|
|
393
483
|
designId: file.designId,
|
|
394
484
|
fileId: file.id,
|
|
395
485
|
filename: file.filename,
|
|
486
|
+
updatedAt,
|
|
396
487
|
result: patch.result,
|
|
488
|
+
patchedContent: patch.content,
|
|
397
489
|
bytesBefore: file.content.length,
|
|
398
490
|
bytesAfter: patch.content.length,
|
|
399
491
|
note: changed
|
|
@@ -12,6 +12,11 @@ import { getDb, schema } from "../server/db/index.js";
|
|
|
12
12
|
import "../server/db/index.js"; // ensure registerShareableResource runs
|
|
13
13
|
import { hasCapability } from "../shared/design-source-capabilities.js";
|
|
14
14
|
import type { DesignSourceCapabilities } from "../shared/design-source-capabilities.js";
|
|
15
|
+
import {
|
|
16
|
+
normalizeDesignSourceType,
|
|
17
|
+
resolveDescriptorCapabilities,
|
|
18
|
+
type DesignSourceDescriptor,
|
|
19
|
+
} from "../shared/source-mode.js";
|
|
15
20
|
|
|
16
21
|
/**
|
|
17
22
|
* Resolve the capabilities for the design's source type.
|
|
@@ -19,17 +24,35 @@ import type { DesignSourceCapabilities } from "../shared/design-source-capabilit
|
|
|
19
24
|
*/
|
|
20
25
|
function resolveCapabilities(
|
|
21
26
|
designData: string | null,
|
|
22
|
-
): DesignSourceCapabilities
|
|
23
|
-
if (!designData)
|
|
27
|
+
): DesignSourceCapabilities {
|
|
28
|
+
if (!designData) {
|
|
29
|
+
return resolveDescriptorCapabilities({ sourceType: "inline" });
|
|
30
|
+
}
|
|
31
|
+
|
|
24
32
|
try {
|
|
25
33
|
const parsed = JSON.parse(designData) as Record<string, unknown>;
|
|
26
34
|
if (parsed.capabilities && typeof parsed.capabilities === "object") {
|
|
27
35
|
return parsed.capabilities as DesignSourceCapabilities;
|
|
28
36
|
}
|
|
37
|
+
const sourceType = normalizeDesignSourceType(parsed.sourceType) ?? "inline";
|
|
38
|
+
const descriptor: DesignSourceDescriptor =
|
|
39
|
+
sourceType === "fusion"
|
|
40
|
+
? { sourceType, connected: parsed.connected === true }
|
|
41
|
+
: sourceType === "localhost"
|
|
42
|
+
? {
|
|
43
|
+
sourceType,
|
|
44
|
+
connectionId:
|
|
45
|
+
typeof parsed.connectionId === "string"
|
|
46
|
+
? parsed.connectionId
|
|
47
|
+
: "unknown",
|
|
48
|
+
}
|
|
49
|
+
: { sourceType };
|
|
50
|
+
return resolveDescriptorCapabilities(descriptor);
|
|
29
51
|
} catch {
|
|
30
52
|
// ignore parse errors
|
|
31
53
|
}
|
|
32
|
-
|
|
54
|
+
|
|
55
|
+
return resolveDescriptorCapabilities({ sourceType: "inline" });
|
|
33
56
|
}
|
|
34
57
|
|
|
35
58
|
/**
|
|
@@ -167,7 +190,7 @@ export default defineAction({
|
|
|
167
190
|
}
|
|
168
191
|
|
|
169
192
|
const caps = resolveCapabilities(design.data);
|
|
170
|
-
if (
|
|
193
|
+
if (!hasCapability(caps, "captureState")) {
|
|
171
194
|
throw new Error(
|
|
172
195
|
"The design's source does not support captureState. " +
|
|
173
196
|
"Connect Builder or a localhost bridge to enable live captures.",
|