@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
|
@@ -32,7 +32,11 @@ import { getDb, schema } from "../server/db/index.js";
|
|
|
32
32
|
import "../server/db/index.js"; // ensure registerShareableResource runs
|
|
33
33
|
import { buildCodeLayerProjection } from "../shared/code-layer.js";
|
|
34
34
|
import type { CodeLayerSource } from "../shared/code-layer.js";
|
|
35
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
componentNameFor,
|
|
37
|
+
componentNodeIdMatches,
|
|
38
|
+
extractProps,
|
|
39
|
+
} from "../shared/component-model.js";
|
|
36
40
|
import { normalizeDesignSourceType } from "../shared/source-mode.js";
|
|
37
41
|
|
|
38
42
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
@@ -62,7 +66,15 @@ async function liveContent(
|
|
|
62
66
|
export interface ComponentPropPreviewMessage {
|
|
63
67
|
/** Matches the existing parent→iframe postMessage type vocabulary. */
|
|
64
68
|
type: "style-change" | "replace-document-content" | "select-element";
|
|
65
|
-
|
|
69
|
+
selector?: string;
|
|
70
|
+
nodeId?: string;
|
|
71
|
+
attributeOverrides?: Record<string, string>;
|
|
72
|
+
classEdit?: {
|
|
73
|
+
kind: "class";
|
|
74
|
+
operation: "replace";
|
|
75
|
+
from: string;
|
|
76
|
+
to: string;
|
|
77
|
+
};
|
|
66
78
|
}
|
|
67
79
|
|
|
68
80
|
// ─── Action ───────────────────────────────────────────────────────────────────
|
|
@@ -156,7 +168,9 @@ export default defineAction({
|
|
|
156
168
|
source: codeLayerSource,
|
|
157
169
|
});
|
|
158
170
|
|
|
159
|
-
const node = projection.nodes.find((n) =>
|
|
171
|
+
const node = projection.nodes.find((n) =>
|
|
172
|
+
componentNodeIdMatches(n, nodeId),
|
|
173
|
+
);
|
|
160
174
|
if (!node) {
|
|
161
175
|
throw new Error(
|
|
162
176
|
`Node "${nodeId}" not found. Run get-code-layer-projection to list current ids.`,
|
|
@@ -182,35 +196,29 @@ export default defineAction({
|
|
|
182
196
|
// attribute without a full HTML replace.
|
|
183
197
|
messages.push({
|
|
184
198
|
type: "style-change",
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
attributeOverrides: { "x-data": edit.value },
|
|
189
|
-
},
|
|
199
|
+
selector: node.selector,
|
|
200
|
+
nodeId,
|
|
201
|
+
attributeOverrides: { "x-data": edit.value },
|
|
190
202
|
});
|
|
191
203
|
} else if (edit.kind === "attribute") {
|
|
192
204
|
messages.push({
|
|
193
205
|
type: "style-change",
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
attributeOverrides: { [edit.attribute]: edit.value },
|
|
198
|
-
},
|
|
206
|
+
selector: node.selector,
|
|
207
|
+
nodeId,
|
|
208
|
+
attributeOverrides: { [edit.attribute]: edit.value },
|
|
199
209
|
});
|
|
200
210
|
} else if (edit.kind === "classReplace") {
|
|
201
211
|
// Communicate as a responsive-class intent so the bridge applies it via
|
|
202
212
|
// the existing deterministic class-patch path.
|
|
203
213
|
messages.push({
|
|
204
214
|
type: "style-change",
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
to: edit.to,
|
|
213
|
-
},
|
|
215
|
+
selector: node.selector,
|
|
216
|
+
nodeId,
|
|
217
|
+
classEdit: {
|
|
218
|
+
kind: "class",
|
|
219
|
+
operation: "replace",
|
|
220
|
+
from: edit.from,
|
|
221
|
+
to: edit.to,
|
|
214
222
|
},
|
|
215
223
|
});
|
|
216
224
|
}
|
|
@@ -218,10 +226,8 @@ export default defineAction({
|
|
|
218
226
|
// Also emit a select-element message so the canvas highlights the node.
|
|
219
227
|
messages.push({
|
|
220
228
|
type: "select-element",
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
nodeId,
|
|
224
|
-
},
|
|
229
|
+
selector: node.selector,
|
|
230
|
+
nodeId,
|
|
225
231
|
});
|
|
226
232
|
|
|
227
233
|
const sourceType =
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* preview-shader-fill — preview-only action.
|
|
3
3
|
*
|
|
4
4
|
* Returns the CSS mesh-gradient approximation for a shader fill so the caller
|
|
5
|
-
* can apply it to the selected node via the
|
|
6
|
-
*
|
|
5
|
+
* can apply it to the selected node via the `shader-fill-preview` bridge
|
|
6
|
+
* message **without persisting anything**.
|
|
7
7
|
*
|
|
8
8
|
* Deliberately lightweight:
|
|
9
9
|
* - No DB access.
|
|
@@ -84,11 +84,11 @@ export default defineAction({
|
|
|
84
84
|
Preview a CSS mesh-gradient shader fill on the selected design node without persisting anything.
|
|
85
85
|
|
|
86
86
|
Returns:
|
|
87
|
-
- previewCss — a CSS \`background\` value for the live preview (inject via
|
|
87
|
+
- previewCss — a CSS \`background\` value for the live preview (inject via the shader-fill-preview bridge message).
|
|
88
88
|
- fallbackCss — a simpler static CSS \`background\` for export / PDF / SSR contexts.
|
|
89
89
|
- fallbackBlock — a complete CSS rule block (selector + fallback background) ready to embed.
|
|
90
90
|
- descriptor — the resolved and validated ShaderDescriptor.
|
|
91
|
-
- bridgeMessage — a ready-to-use JSON bridge payload for the iframe
|
|
91
|
+
- bridgeMessage — a ready-to-use JSON bridge payload for the iframe shader-fill-preview message.
|
|
92
92
|
|
|
93
93
|
Call this before apply-shader-fill. The preview is purely CSS — no WebGL, no canvas, no writes.
|
|
94
94
|
|
|
@@ -139,14 +139,12 @@ support; it has no effect on the static CSS preview output today.
|
|
|
139
139
|
const fallbackBlock = buildShaderFillFallbackBlock(selector, descriptor);
|
|
140
140
|
|
|
141
141
|
// Build a ready-to-post bridge payload for the iframe.
|
|
142
|
-
// The `
|
|
142
|
+
// The `shader-fill-preview` message type is handled by DesignCanvas.tsx.
|
|
143
143
|
const bridgeMessage = {
|
|
144
|
-
type: "
|
|
145
|
-
nodeId: target?.nodeId ?? null,
|
|
144
|
+
type: "shader-fill-preview",
|
|
146
145
|
selector: target?.selector ?? null,
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
},
|
|
146
|
+
nodeId: target?.nodeId ?? null,
|
|
147
|
+
css: previewCss,
|
|
150
148
|
// Metadata so the client can attach a "preview only" badge.
|
|
151
149
|
_preview: true,
|
|
152
150
|
_source: "shader-fill",
|
|
@@ -167,7 +165,7 @@ support; it has no effect on the static CSS preview output today.
|
|
|
167
165
|
JSON.stringify(bridgeMessage, null, 2),
|
|
168
166
|
``,
|
|
169
167
|
`Apply steps:`,
|
|
170
|
-
`1. Inject the previewCss via a bridge "
|
|
168
|
+
`1. Inject the previewCss via a bridge "shader-fill-preview" message (no write, no persist).`,
|
|
171
169
|
`2. If the user approves, call apply-shader-fill — but note that action is currently GATED`,
|
|
172
170
|
` and will return a clear not-yet-available result until runtime rendering +`,
|
|
173
171
|
` source-write + diff proof are all in place.`,
|
|
@@ -10,7 +10,7 @@ const ProviderSchema = z.enum(DESIGN_PROVIDER_API_IDS);
|
|
|
10
10
|
|
|
11
11
|
export default defineAction({
|
|
12
12
|
description:
|
|
13
|
-
"List raw HTTP API capabilities for Design-connected providers. Use before provider-api-request when design-token
|
|
13
|
+
"List raw HTTP API capabilities for Design-connected providers such as GitHub and Figma. Use before provider-api-request when design-token, repository, or Figma-library convenience actions are too narrow. Returns provider base URLs, auth style, credential key names, docs/spec URLs, placeholders, and examples; never returns secret values.",
|
|
14
14
|
schema: z.object({
|
|
15
15
|
provider: ProviderSchema.optional().describe(
|
|
16
16
|
"Optional provider id to inspect. Omit to list every Design provider API escape hatch.",
|
|
@@ -22,7 +22,7 @@ export default defineAction({
|
|
|
22
22
|
return {
|
|
23
23
|
providers: await listProviderApiCatalog(provider),
|
|
24
24
|
guidance:
|
|
25
|
-
"Design actions like import-github are workflow shortcuts, not capability limits. When
|
|
25
|
+
"Design actions like import-github and list-figma-library-assets are workflow shortcuts, not capability limits. When GitHub or Figma can answer the question with a better endpoint, query, body, pagination mode, metadata field, or API version, inspect docs/spec URLs and call provider-api-request directly. Auth uses saved scoped secrets such as GITHUB_TOKEN or FIGMA_ACCESS_TOKEN and never exposes token values.",
|
|
26
26
|
};
|
|
27
27
|
},
|
|
28
28
|
});
|
|
@@ -25,7 +25,7 @@ const WebContentSearchSchema = z.object({
|
|
|
25
25
|
|
|
26
26
|
export default defineAction({
|
|
27
27
|
description:
|
|
28
|
-
"Inspect provider API docs/spec metadata, or fetch any public API documentation page, OpenAPI spec, changelog, or web page. Use this before provider-api-request when the exact GitHub endpoint, payload shape, pagination, or API version is uncertain.",
|
|
28
|
+
"Inspect provider API docs/spec metadata, or fetch any public API documentation page, OpenAPI spec, changelog, or web page. Use this before provider-api-request when the exact GitHub/Figma endpoint, payload shape, pagination, or API version is uncertain.",
|
|
29
29
|
schema: z.object({
|
|
30
30
|
provider: ProviderSchema.describe(
|
|
31
31
|
"Provider whose API docs/spec to inspect.",
|
|
@@ -71,8 +71,8 @@ const PaginationSchema = z
|
|
|
71
71
|
export default defineAction({
|
|
72
72
|
description:
|
|
73
73
|
"Make an arbitrary authenticated HTTP request to a Design-connected provider API. " +
|
|
74
|
-
"Use this as the flexible escape hatch when a convenience action cannot express the needed GitHub endpoint, file
|
|
75
|
-
"The request is constrained to the provider host, uses
|
|
74
|
+
"Use this as the flexible escape hatch when a convenience action cannot express the needed GitHub endpoint, Figma file/library endpoint, request body, pagination mode, payload shape, or API version. " +
|
|
75
|
+
"The request is constrained to the provider host, uses saved scoped credentials such as GITHUB_TOKEN or FIGMA_ACCESS_TOKEN automatically, blocks private/internal URLs, and redacts secrets from responses. " +
|
|
76
76
|
"\n\nSTAGING MODE (preferred for large responses): Pass stageAs to write response items into a scratch dataset instead of returning the raw body. " +
|
|
77
77
|
"Returns { dataset, rowCount, columns, sampleRows } so only a compact summary enters context. Use query-staged-dataset to aggregate, filter, and project the data without re-fetching. " +
|
|
78
78
|
"\n\nPAGINATION: When stageAs is set, pass pagination config to fetch all pages server-side into the same dataset. For GitHub page pagination, use pageParam='page', pageSize from per_page, and itemsPath when the response object contains an items array.",
|
|
@@ -83,7 +83,7 @@ export default defineAction({
|
|
|
83
83
|
.string()
|
|
84
84
|
.min(1)
|
|
85
85
|
.describe(
|
|
86
|
-
"Provider API path such as /repos/{owner}/{repo}/contents/{path}, /search/code, /
|
|
86
|
+
"Provider API path such as /repos/{owner}/{repo}/contents/{path}, /search/code, /files/{fileKey}/components, /images/{fileKey}, or a full URL on an allowed provider host. Use placeholders from provider-api-catalog when provided.",
|
|
87
87
|
),
|
|
88
88
|
query: z
|
|
89
89
|
.unknown()
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { defineAction } from "@agent-native/core";
|
|
2
|
+
import { getRequestUserEmail } from "@agent-native/core/server/request-context";
|
|
3
|
+
import { assertAccess } from "@agent-native/core/sharing";
|
|
4
|
+
import { and, eq } from "drizzle-orm";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
|
|
7
|
+
import { getDb, schema } from "../server/db/index.js";
|
|
8
|
+
|
|
9
|
+
export default defineAction({
|
|
10
|
+
description:
|
|
11
|
+
"Revoke a previously granted localhost write-consent for a design + connection. " +
|
|
12
|
+
"After revocation the agent can no longer write local files until the user re-grants consent. " +
|
|
13
|
+
"Requires editor access on the design.",
|
|
14
|
+
schema: z.object({
|
|
15
|
+
designId: z.string().describe("Design ID."),
|
|
16
|
+
connectionId: z
|
|
17
|
+
.string()
|
|
18
|
+
.describe("Localhost connection ID whose grant should be revoked."),
|
|
19
|
+
}),
|
|
20
|
+
run: async ({ designId, connectionId }) => {
|
|
21
|
+
await assertAccess("design", designId, "editor");
|
|
22
|
+
|
|
23
|
+
const ownerEmail = getRequestUserEmail();
|
|
24
|
+
if (!ownerEmail) throw new Error("no authenticated user");
|
|
25
|
+
|
|
26
|
+
const db = getDb();
|
|
27
|
+
|
|
28
|
+
const deleted = await db
|
|
29
|
+
.delete(schema.designLocalhostWriteGrants)
|
|
30
|
+
.where(
|
|
31
|
+
and(
|
|
32
|
+
eq(schema.designLocalhostWriteGrants.designId, designId),
|
|
33
|
+
eq(schema.designLocalhostWriteGrants.connectionId, connectionId),
|
|
34
|
+
eq(schema.designLocalhostWriteGrants.ownerEmail, ownerEmail),
|
|
35
|
+
),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
// `deleted` shape varies by dialect; check rows affected via the object.
|
|
39
|
+
const rowsAffected =
|
|
40
|
+
(deleted as unknown as { rowsAffected?: number })?.rowsAffected ?? 0;
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
designId,
|
|
44
|
+
connectionId,
|
|
45
|
+
revoked: rowsAffected > 0,
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
});
|
|
@@ -108,6 +108,8 @@ function checkMissingLabels(html: string): A11yFinding[] {
|
|
|
108
108
|
let idx = 0;
|
|
109
109
|
for (const m of html.matchAll(inputPattern)) {
|
|
110
110
|
const tag = m[0];
|
|
111
|
+
const inputStart = m.index ?? 0;
|
|
112
|
+
const inputEnd = inputStart + tag.length;
|
|
111
113
|
const typeMatch = tag.match(/\btype\s*=\s*(?:"([^"]*?)"|'([^']*?)')/i);
|
|
112
114
|
const type = (typeMatch?.[1] ?? typeMatch?.[2] ?? "text").toLowerCase();
|
|
113
115
|
// Hidden and submit/button/image inputs don't need visible labels
|
|
@@ -123,8 +125,9 @@ function checkMissingLabels(html: string): A11yFinding[] {
|
|
|
123
125
|
const hasExplicitLabel = inputId
|
|
124
126
|
? new RegExp(`for\\s*=\\s*(?:"${inputId}"|'${inputId}')`, "i").test(html)
|
|
125
127
|
: false;
|
|
128
|
+
const hasImplicitLabel = isWrappedByLabel(html, inputStart, inputEnd);
|
|
126
129
|
|
|
127
|
-
if (!hasAriaLabel && !hasExplicitLabel) {
|
|
130
|
+
if (!hasAriaLabel && !hasExplicitLabel && !hasImplicitLabel) {
|
|
128
131
|
findings.push({
|
|
129
132
|
id: `missing-label:input-${idx}`,
|
|
130
133
|
severity: "error" as A11ySeverity,
|
|
@@ -143,8 +146,53 @@ function checkMissingLabels(html: string): A11yFinding[] {
|
|
|
143
146
|
return findings;
|
|
144
147
|
}
|
|
145
148
|
|
|
146
|
-
|
|
147
|
-
|
|
149
|
+
function isWrappedByLabel(
|
|
150
|
+
html: string,
|
|
151
|
+
inputStart: number,
|
|
152
|
+
inputEnd: number,
|
|
153
|
+
): boolean {
|
|
154
|
+
const labelOpen = html.lastIndexOf("<label", inputStart);
|
|
155
|
+
if (labelOpen === -1) return false;
|
|
156
|
+
const labelCloseBeforeInput = html.lastIndexOf("</label", inputStart);
|
|
157
|
+
if (labelCloseBeforeInput > labelOpen) return false;
|
|
158
|
+
const labelCloseAfterInput = html.indexOf("</label", inputEnd);
|
|
159
|
+
return labelCloseAfterInput !== -1;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Whether an interactive element already declares a minimum size that meets the
|
|
164
|
+
* 44px tap-target floor. This recognises exactly what the inline auto-fix adds
|
|
165
|
+
* (`min-h-[44px] min-w-[44px]`) plus equivalents — arbitrary `min-h`/`min-w`
|
|
166
|
+
* values in px/rem/em ≥ 44px, the Tailwind spacing scale (`min-h-11` = 44px on a
|
|
167
|
+
* 4px step), and full-bleed minimums (`min-h-full` / `min-h-screen`). Without
|
|
168
|
+
* this, a fixed element keeps its original tiny `h-4` class and the audit would
|
|
169
|
+
* re-flag it forever, so the audit↔fix loop would never converge.
|
|
170
|
+
*/
|
|
171
|
+
function hasAdequateMinTapSize(tag: string): boolean {
|
|
172
|
+
// Arbitrary values: min-h-[44px], min-w-[2.75rem], etc.
|
|
173
|
+
const arbitraryPattern = /\bmin-(?:h|w)-\[([\d.]+)(px|rem|em)\]/gi;
|
|
174
|
+
let m: RegExpExecArray | null;
|
|
175
|
+
while ((m = arbitraryPattern.exec(tag)) !== null) {
|
|
176
|
+
const value = Number.parseFloat(m[1] ?? "");
|
|
177
|
+
if (!Number.isFinite(value)) continue;
|
|
178
|
+
const px = m[2]?.toLowerCase() === "px" ? value : value * 16;
|
|
179
|
+
if (px >= 44) return true;
|
|
180
|
+
}
|
|
181
|
+
// Tailwind spacing scale: min-h-11 / min-w-11 = 2.75rem = 44px (4px per step).
|
|
182
|
+
const scalePattern = /\bmin-(?:h|w)-(\d+)\b/gi;
|
|
183
|
+
while ((m = scalePattern.exec(tag)) !== null) {
|
|
184
|
+
if (Number.parseInt(m[1] ?? "", 10) * 4 >= 44) return true;
|
|
185
|
+
}
|
|
186
|
+
// Full-bleed minimums always clear the tap floor.
|
|
187
|
+
return /\bmin-(?:h|w)-(?:full|screen)\b/.test(tag);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Check interactive elements that are likely too small for touch targets
|
|
192
|
+
* (< ~44px heuristic via Tailwind class). Exported for unit tests that assert
|
|
193
|
+
* the audit↔fix loop converges (a fixed element must stop being flagged).
|
|
194
|
+
*/
|
|
195
|
+
export function checkTapTargets(html: string): A11yFinding[] {
|
|
148
196
|
const findings: A11yFinding[] = [];
|
|
149
197
|
// Heuristic: buttons/links with explicit tiny size classes (h-4, h-5, w-4, w-5, size-4, size-5)
|
|
150
198
|
// and no explicit larger override or sr-only are flagged.
|
|
@@ -158,7 +206,8 @@ function checkTapTargets(html: string): A11yFinding[] {
|
|
|
158
206
|
if (
|
|
159
207
|
tinyPattern.test(tag) &&
|
|
160
208
|
!largePattern.test(tag) &&
|
|
161
|
-
!srOnlyPattern.test(tag)
|
|
209
|
+
!srOnlyPattern.test(tag) &&
|
|
210
|
+
!hasAdequateMinTapSize(tag)
|
|
162
211
|
) {
|
|
163
212
|
findings.push({
|
|
164
213
|
id: `tap-target:interactive-${idx}`,
|
|
@@ -170,7 +219,7 @@ function checkTapTargets(html: string): A11yFinding[] {
|
|
|
170
219
|
nodeId: extractNodeId(tag),
|
|
171
220
|
selector: extractSelector(tag, "button"),
|
|
172
221
|
wcag: "2.5.5",
|
|
173
|
-
fixAvailable:
|
|
222
|
+
fixAvailable: true,
|
|
174
223
|
});
|
|
175
224
|
}
|
|
176
225
|
idx++;
|
|
@@ -272,7 +321,7 @@ function checkFocusVisibility(html: string): A11yFinding[] {
|
|
|
272
321
|
nodeId: extractNodeId(tag),
|
|
273
322
|
selector: extractSelector(tag, tag.match(/^<([a-z]+)/i)?.[1] ?? "*"),
|
|
274
323
|
wcag: "2.4.7",
|
|
275
|
-
fixAvailable:
|
|
324
|
+
fixAvailable: true,
|
|
276
325
|
});
|
|
277
326
|
}
|
|
278
327
|
inlineIdx++;
|
|
@@ -286,10 +335,23 @@ function checkContrastHint(html: string): A11yFinding[] {
|
|
|
286
335
|
// Static analysis cannot compute real contrast ratios without a DOM/CSS
|
|
287
336
|
// cascade resolver. We flag the presence of explicit low-opacity text colors
|
|
288
337
|
// as a human-review hint — the UI will show these as "info" prompts.
|
|
289
|
-
const
|
|
290
|
-
|
|
338
|
+
const tagPattern = /<([a-z][a-z0-9:-]*)\b[^>]*>/gi;
|
|
339
|
+
const textColorPattern = /\btext-(?:white|black|gray-\d+)\b/i;
|
|
340
|
+
const lowOpacityPattern = /\b(?:opacity-[0-3]\d|text-opacity-[0-3]\d)\b/i;
|
|
291
341
|
let idx = 0;
|
|
292
|
-
for (const m of html.matchAll(
|
|
342
|
+
for (const m of html.matchAll(tagPattern)) {
|
|
343
|
+
const tag = m[0];
|
|
344
|
+
const classMatch = tag.match(/\bclass\s*=\s*(?:"([^"]*?)"|'([^']*?)')/i);
|
|
345
|
+
const className = classMatch?.[1] ?? classMatch?.[2] ?? "";
|
|
346
|
+
if (
|
|
347
|
+
!textColorPattern.test(className) ||
|
|
348
|
+
!lowOpacityPattern.test(className)
|
|
349
|
+
) {
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
const tagName = m[1] ?? "*";
|
|
353
|
+
const nodeId = extractNodeId(tag);
|
|
354
|
+
const selector = extractSelector(tag, tagName);
|
|
293
355
|
findings.push({
|
|
294
356
|
id: `contrast:low-opacity-${idx}`,
|
|
295
357
|
severity: "info" as A11ySeverity,
|
|
@@ -298,6 +360,8 @@ function checkContrastHint(html: string): A11yFinding[] {
|
|
|
298
360
|
"Text element has a low-opacity modifier — verify contrast ratio meets 4.5:1 minimum.",
|
|
299
361
|
detail:
|
|
300
362
|
"Low-opacity text can fail WCAG 1.4.3. Run a live contrast check in the browser.",
|
|
363
|
+
nodeId,
|
|
364
|
+
selector,
|
|
301
365
|
wcag: "1.4.3",
|
|
302
366
|
fixAvailable: false,
|
|
303
367
|
});
|
|
@@ -353,7 +417,7 @@ export default defineAction({
|
|
|
353
417
|
),
|
|
354
418
|
}),
|
|
355
419
|
readOnly: true,
|
|
356
|
-
http: { method: "
|
|
420
|
+
http: { method: "POST" },
|
|
357
421
|
run: async ({ designId, fileId, filename }) => {
|
|
358
422
|
const db = getDb();
|
|
359
423
|
|
|
@@ -146,7 +146,7 @@ const CAPABILITY_ROUTES: Record<string, CapabilityRoute> = {
|
|
|
146
146
|
"design.motion-presets:apply": {
|
|
147
147
|
action: "apply-motion-edit",
|
|
148
148
|
paramHint:
|
|
149
|
-
"Call apply-motion-edit with { designId, fileId?, timeline: { tracks, durationMs } } for an atomic
|
|
149
|
+
"Call apply-motion-edit with { designId, fileId?, timeline: { tracks, durationMs } } for an atomic managed CSS timeline save.",
|
|
150
150
|
readOnly: false,
|
|
151
151
|
},
|
|
152
152
|
"design.motion-presets:write-source": {
|
|
@@ -95,7 +95,7 @@ function resolveActiveScreen(
|
|
|
95
95
|
|
|
96
96
|
export default defineAction({
|
|
97
97
|
description:
|
|
98
|
-
"See what the user is currently looking at on screen. Returns the current navigation state including which design is open, which view they are on (list, editor, design-systems, present, templates, settings), active/focused design screen, selected element, active inspector tab (design
|
|
98
|
+
"See what the user is currently looking at on screen. Returns the current navigation state including which design is open, which view they are on (list, editor, design-systems, present, templates, settings), active/focused design screen, selected element, active inspector tab (design or tweaks), active left rail panel (file, agent, assets, tools, or tokens), overview canvas state, plus any pending question overlay. Always call this first before taking any action.",
|
|
99
99
|
schema: z.object({}),
|
|
100
100
|
http: false,
|
|
101
101
|
run: async () => {
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* write-local-file — write or patch a local file through the design bridge.
|
|
3
|
+
*
|
|
4
|
+
* Security gates (in order):
|
|
5
|
+
* 1. assertAccess: the caller must have editor access to the design.
|
|
6
|
+
* 2. File extension: only .html, .htm, and .css files are allowed.
|
|
7
|
+
* 3. verifyWriteGrant: a valid (non-expired) user-approved write-consent grant
|
|
8
|
+
* must exist. The agent CANNOT bypass this check.
|
|
9
|
+
* 4. Path confinement: assertPathInside ensures the target stays inside
|
|
10
|
+
* rootPath (pre-bridge check; bridge also validates with realpath).
|
|
11
|
+
* 5. Bridge token: the X-Bridge-Token header is set to the per-grant token
|
|
12
|
+
* minted at consent time so the bridge can reject unauthorized calls.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
|
|
17
|
+
import { defineAction } from "@agent-native/core";
|
|
18
|
+
import { getRequestUserEmail } from "@agent-native/core/server/request-context";
|
|
19
|
+
import { assertAccess } from "@agent-native/core/sharing";
|
|
20
|
+
import { and, eq } from "drizzle-orm";
|
|
21
|
+
import { z } from "zod";
|
|
22
|
+
|
|
23
|
+
import { getDb, schema } from "../server/db/index.js";
|
|
24
|
+
import { verifyWriteGrant } from "../server/lib/verify-write-grant.js";
|
|
25
|
+
|
|
26
|
+
/** File extensions the agent is permitted to write via the bridge. */
|
|
27
|
+
const ALLOWED_EXTENSIONS = new Set([".html", ".htm", ".css"]);
|
|
28
|
+
|
|
29
|
+
function assertAllowedExtension(relPath: string): void {
|
|
30
|
+
const ext = path.extname(relPath).toLowerCase();
|
|
31
|
+
if (!ALLOWED_EXTENSIONS.has(ext)) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`File "${relPath}" has extension "${ext}" which is not allowed. ` +
|
|
34
|
+
"Only .html, .htm, and .css files may be written through the bridge.",
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isLoopbackHostname(hostname: string): boolean {
|
|
40
|
+
const normalized = hostname.toLowerCase();
|
|
41
|
+
if (
|
|
42
|
+
normalized === "localhost" ||
|
|
43
|
+
normalized === "::1" ||
|
|
44
|
+
normalized === "[::1]"
|
|
45
|
+
) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
const parts = normalized.split(".");
|
|
49
|
+
return (
|
|
50
|
+
parts.length === 4 &&
|
|
51
|
+
parts[0] === "127" &&
|
|
52
|
+
parts.every((part) => /^\d+$/.test(part) && Number(part) <= 255)
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function normalizeBridgeUrl(value: string): string {
|
|
57
|
+
const parsed = new URL(value.trim());
|
|
58
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
59
|
+
throw new Error("bridgeUrl must be an http(s) URL");
|
|
60
|
+
}
|
|
61
|
+
if (parsed.username || parsed.password) {
|
|
62
|
+
throw new Error("bridgeUrl must not include credentials");
|
|
63
|
+
}
|
|
64
|
+
if (parsed.pathname !== "/" && parsed.pathname !== "") {
|
|
65
|
+
throw new Error("bridgeUrl must not include a path");
|
|
66
|
+
}
|
|
67
|
+
if (!isLoopbackHostname(parsed.hostname)) {
|
|
68
|
+
throw new Error("bridgeUrl must use localhost or a loopback IP address");
|
|
69
|
+
}
|
|
70
|
+
parsed.search = "";
|
|
71
|
+
parsed.hash = "";
|
|
72
|
+
parsed.pathname = "";
|
|
73
|
+
return parsed.toString().replace(/\/$/, "");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export default defineAction({
|
|
77
|
+
description:
|
|
78
|
+
"Write or patch a local file (HTML or CSS only) via the localhost design bridge. " +
|
|
79
|
+
"The user MUST have already granted write consent via grant-localhost-write-consent; " +
|
|
80
|
+
"this action will reject the request if no valid grant exists. " +
|
|
81
|
+
"Pass content for a full file write, or {search, replace} for a targeted patch. " +
|
|
82
|
+
"Requires editor access on the design.",
|
|
83
|
+
schema: z.object({
|
|
84
|
+
designId: z.string().describe("Design ID."),
|
|
85
|
+
connectionId: z
|
|
86
|
+
.string()
|
|
87
|
+
.describe("Localhost connection ID (must have an active write grant)."),
|
|
88
|
+
relPath: z
|
|
89
|
+
.string()
|
|
90
|
+
.describe(
|
|
91
|
+
"Path to the file relative to the connection rootPath. " +
|
|
92
|
+
"Only .html, .htm, and .css files are accepted.",
|
|
93
|
+
),
|
|
94
|
+
content: z
|
|
95
|
+
.string()
|
|
96
|
+
.optional()
|
|
97
|
+
.describe(
|
|
98
|
+
"Full replacement file content. Use for new files or complete rewrites.",
|
|
99
|
+
),
|
|
100
|
+
patch: z
|
|
101
|
+
.object({
|
|
102
|
+
search: z
|
|
103
|
+
.string()
|
|
104
|
+
.describe("Exact text to search for (must appear exactly once)."),
|
|
105
|
+
replace: z.string().describe("Replacement text."),
|
|
106
|
+
})
|
|
107
|
+
.optional()
|
|
108
|
+
.describe(
|
|
109
|
+
"Search-and-replace patch. Use for targeted edits. " +
|
|
110
|
+
"Mutually exclusive with content.",
|
|
111
|
+
),
|
|
112
|
+
}),
|
|
113
|
+
run: async ({ designId, connectionId, relPath, content, patch }) => {
|
|
114
|
+
// --- Gate 1: access ---
|
|
115
|
+
await assertAccess("design", designId, "editor");
|
|
116
|
+
|
|
117
|
+
const ownerEmail = getRequestUserEmail();
|
|
118
|
+
if (!ownerEmail) throw new Error("no authenticated user");
|
|
119
|
+
|
|
120
|
+
// --- Gate 2: extension whitelist ---
|
|
121
|
+
assertAllowedExtension(relPath);
|
|
122
|
+
|
|
123
|
+
// --- Gate 3: valid write-consent grant ---
|
|
124
|
+
const grant = await verifyWriteGrant({
|
|
125
|
+
designId,
|
|
126
|
+
connectionId,
|
|
127
|
+
ownerEmail,
|
|
128
|
+
targetPath: relPath,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// --- Gate 4: exactly one of content/patch must be provided ---
|
|
132
|
+
if (content === undefined && patch === undefined) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
"Either content (full file write) or patch (search/replace) must be provided.",
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
if (content !== undefined && patch !== undefined) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
"content and patch are mutually exclusive. Provide one or the other.",
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// --- Resolve bridge URL ---
|
|
144
|
+
const db = getDb();
|
|
145
|
+
const [connection] = await db
|
|
146
|
+
.select({ bridgeUrl: schema.designLocalhostConnections.bridgeUrl })
|
|
147
|
+
.from(schema.designLocalhostConnections)
|
|
148
|
+
.where(
|
|
149
|
+
and(
|
|
150
|
+
eq(schema.designLocalhostConnections.id, connectionId),
|
|
151
|
+
eq(schema.designLocalhostConnections.ownerEmail, ownerEmail),
|
|
152
|
+
),
|
|
153
|
+
)
|
|
154
|
+
.limit(1);
|
|
155
|
+
|
|
156
|
+
if (!connection?.bridgeUrl) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
`No bridge URL found for connection "${connectionId}". ` +
|
|
159
|
+
"Ensure the design bridge is running (npx @agent-native/core@latest design connect).",
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const bridgeUrl = normalizeBridgeUrl(connection.bridgeUrl);
|
|
164
|
+
const headers: Record<string, string> = {
|
|
165
|
+
"Content-Type": "application/json",
|
|
166
|
+
"X-Bridge-Token": grant.bridgeToken,
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
if (content !== undefined) {
|
|
170
|
+
// Full file write
|
|
171
|
+
const res = await fetch(`${bridgeUrl}/write-file`, {
|
|
172
|
+
method: "POST",
|
|
173
|
+
headers,
|
|
174
|
+
body: JSON.stringify({ relPath, content }),
|
|
175
|
+
});
|
|
176
|
+
if (!res.ok) {
|
|
177
|
+
const errText = await res.text().catch(() => res.statusText);
|
|
178
|
+
throw new Error(`Bridge write-file failed (${res.status}): ${errText}`);
|
|
179
|
+
}
|
|
180
|
+
return { designId, relPath, operation: "write" as const, written: true };
|
|
181
|
+
} else {
|
|
182
|
+
// Search-and-replace patch — first read, then apply-edit
|
|
183
|
+
const readRes = await fetch(`${bridgeUrl}/read-file`, {
|
|
184
|
+
method: "POST",
|
|
185
|
+
headers,
|
|
186
|
+
body: JSON.stringify({ relPath }),
|
|
187
|
+
});
|
|
188
|
+
if (!readRes.ok) {
|
|
189
|
+
const errText = await readRes.text().catch(() => readRes.statusText);
|
|
190
|
+
throw new Error(
|
|
191
|
+
`Bridge read-file failed (${readRes.status}): ${errText}`,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const applyRes = await fetch(`${bridgeUrl}/apply-edit`, {
|
|
196
|
+
method: "POST",
|
|
197
|
+
headers,
|
|
198
|
+
body: JSON.stringify({
|
|
199
|
+
relPath,
|
|
200
|
+
search: patch!.search,
|
|
201
|
+
replace: patch!.replace,
|
|
202
|
+
}),
|
|
203
|
+
});
|
|
204
|
+
if (!applyRes.ok) {
|
|
205
|
+
const errText = await applyRes.text().catch(() => applyRes.statusText);
|
|
206
|
+
throw new Error(
|
|
207
|
+
`Bridge apply-edit failed (${applyRes.status}): ${errText}`,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
return { designId, relPath, operation: "patch" as const, written: true };
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
});
|