@1agh/maude 0.42.0 → 0.44.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/apps/studio/.ai/cache/_stats.json +7 -0
- package/apps/studio/acp/bootstrap-brief.ts +12 -2
- package/apps/studio/acp/bridge.ts +44 -10
- package/apps/studio/acp/env.ts +23 -0
- package/apps/studio/acp/login-state.ts +263 -0
- package/apps/studio/acp/plugin-bootstrap.ts +31 -25
- package/apps/studio/acp/probe.ts +152 -1
- package/apps/studio/annotations-layer.tsx +1451 -324
- package/apps/studio/annotations-model.ts +58 -0
- package/apps/studio/api.ts +568 -24
- package/apps/studio/bin/_audio-search.mjs +140 -0
- package/apps/studio/bin/_import-asset-pdf-worker.mjs +34 -0
- package/apps/studio/bin/_import-asset.mjs +815 -0
- package/apps/studio/bin/_import-brand.mjs +635 -0
- package/apps/studio/bin/_import-tokens-alias-resolver.mjs +95 -0
- package/apps/studio/bin/_import-tokens.mjs +1201 -0
- package/apps/studio/bin/_ingest-footage.mjs +386 -0
- package/apps/studio/bin/_probe-footage-playwright.mjs +269 -0
- package/apps/studio/bin/_transcribe.mjs +419 -0
- package/apps/studio/bin/_transcribe.test.mjs +80 -0
- package/apps/studio/bin/_video-playwright.mjs +141 -71
- package/apps/studio/bin/audio-search.sh +28 -0
- package/apps/studio/bin/generate.sh +154 -0
- package/apps/studio/bin/import-asset.sh +34 -0
- package/apps/studio/bin/import-brand.sh +29 -0
- package/apps/studio/bin/import-tokens.sh +33 -0
- package/apps/studio/bin/ingest-footage.sh +27 -0
- package/apps/studio/bin/photo-adjust.sh +163 -0
- package/apps/studio/bin/photo-bg-remove.sh +258 -0
- package/apps/studio/bin/probe-footage.sh +28 -0
- package/apps/studio/bin/read-annotations.mjs +125 -2
- package/apps/studio/bin/transcribe.sh +39 -0
- package/apps/studio/canvas-edit.ts +656 -12
- package/apps/studio/canvas-lib.tsx +637 -10
- package/apps/studio/canvas-pipeline.ts +65 -7
- package/apps/studio/canvas-shell.tsx +156 -16
- package/apps/studio/client/app.jsx +2385 -699
- package/apps/studio/client/export-center.jsx +35 -1
- package/apps/studio/client/generate-dialog.jsx +352 -0
- package/apps/studio/client/github.js +18 -0
- package/apps/studio/client/inspector-controls.jsx +781 -0
- package/apps/studio/client/panels/BrandUploadPanel.jsx +233 -0
- package/apps/studio/client/panels/ChatPanel.jsx +15 -33
- package/apps/studio/client/panels/GitPanel.jsx +8 -0
- package/apps/studio/client/panels/IntroVideoDialog.jsx +44 -0
- package/apps/studio/client/panels/OnboardingWizard.jsx +12 -0
- package/apps/studio/client/panels/ReadinessList.jsx +227 -7
- package/apps/studio/client/panels/RepoBranchSwitcher.jsx +133 -21
- package/apps/studio/client/panels/SettingsPanel.jsx +828 -0
- package/apps/studio/client/panels/SetupChecklist.jsx +223 -0
- package/apps/studio/client/panels/StickerPicker.jsx +160 -0
- package/apps/studio/client/panels/TimelinePanel.jsx +15 -2
- package/apps/studio/client/photo-knobs.jsx +432 -0
- package/apps/studio/client/styles/3-shell-maude.css +376 -28
- package/apps/studio/client/styles/4-components.css +223 -0
- package/apps/studio/client/styles/6-acp-chat.css +86 -0
- package/apps/studio/client/tour/quick-setup-tour.js +39 -0
- package/apps/studio/commands/annotation-strokes-command.ts +13 -3
- package/apps/studio/commands/edit-source-command.ts +31 -4
- package/apps/studio/config.schema.json +70 -0
- package/apps/studio/context-menu.tsx +42 -8
- package/apps/studio/context.ts +16 -0
- package/apps/studio/design-setup-readiness.ts +115 -0
- package/apps/studio/dist/client.bundle.js +5572 -21
- package/apps/studio/dist/comment-mount.js +2 -2
- package/apps/studio/dist/runtime/.min-sizes.json +1 -0
- package/apps/studio/dist/runtime/@imgly_background-removal.js +5543 -0
- package/apps/studio/dist/runtime/pixi-js.js +519 -519
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/dom-selection.ts +25 -0
- package/apps/studio/draw/palette.ts +34 -0
- package/apps/studio/exporters/jobs.ts +47 -5
- package/apps/studio/exporters/video.ts +24 -4
- package/apps/studio/footage/schema.test.ts +287 -0
- package/apps/studio/footage/schema.ts +662 -0
- package/apps/studio/footage-store.ts +235 -0
- package/apps/studio/fs-watch.ts +10 -0
- package/apps/studio/generation/adapters/elevenlabs.ts +370 -0
- package/apps/studio/generation/adapters/gemini.ts +542 -0
- package/apps/studio/generation/adapters/groq.test.ts +80 -0
- package/apps/studio/generation/adapters/groq.ts +247 -0
- package/apps/studio/generation/audio-library.test.ts +95 -0
- package/apps/studio/generation/audio-library.ts +156 -0
- package/apps/studio/generation/captions.test.ts +78 -0
- package/apps/studio/generation/captions.ts +132 -0
- package/apps/studio/generation/download.ts +76 -0
- package/apps/studio/generation/elevenlabs.test.ts +223 -0
- package/apps/studio/generation/gemini.test.ts +350 -0
- package/apps/studio/generation/jobs.test.ts +128 -0
- package/apps/studio/generation/jobs.ts +243 -0
- package/apps/studio/generation/keys.test.ts +75 -0
- package/apps/studio/generation/keys.ts +162 -0
- package/apps/studio/generation/prefs.test.ts +95 -0
- package/apps/studio/generation/prefs.ts +82 -0
- package/apps/studio/generation/registry.ts +88 -0
- package/apps/studio/generation/types.test.ts +123 -0
- package/apps/studio/generation/types.ts +266 -0
- package/apps/studio/generation/whisper-models.test.ts +152 -0
- package/apps/studio/generation/whisper-models.ts +257 -0
- package/apps/studio/generation/whisper-spike-results.md +61 -0
- package/apps/studio/git/endpoints.ts +68 -7
- package/apps/studio/git/service.ts +199 -21
- package/apps/studio/github/service.ts +67 -0
- package/apps/studio/handoff.ts +3 -2
- package/apps/studio/http.ts +1039 -22
- package/apps/studio/input-router.tsx +10 -0
- package/apps/studio/inspect.ts +53 -1
- package/apps/studio/media/intro.mp4 +0 -0
- package/apps/studio/media-commit-chain.ts +119 -0
- package/apps/studio/paths.ts +20 -0
- package/apps/studio/photo/filters.ts +285 -0
- package/apps/studio/photo/pipeline.ts +607 -0
- package/apps/studio/photo/schema.ts +367 -0
- package/apps/studio/photo-store.ts +154 -0
- package/apps/studio/readiness.ts +108 -20
- package/apps/studio/runtime-bundle.ts +29 -6
- package/apps/studio/scaffold-design.ts +1145 -0
- package/apps/studio/server.ts +18 -2
- package/apps/studio/stickers/figjam-doodle/manifest.json +104 -0
- package/apps/studio/stickers/figjam-doodle/slice1.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice10.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice11.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice12.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice13.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice14.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice15.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice16.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice17.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice18.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice19.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice2.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice20.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice21.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice22.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice23.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice24.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice3.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice4.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice5.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice6.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice7.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice8.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice9.png +0 -0
- package/apps/studio/stickers/life-style/best.png +0 -0
- package/apps/studio/stickers/life-style/come-on.png +0 -0
- package/apps/studio/stickers/life-style/cut.png +0 -0
- package/apps/studio/stickers/life-style/detox.png +0 -0
- package/apps/studio/stickers/life-style/just.png +0 -0
- package/apps/studio/stickers/life-style/manifest.json +68 -0
- package/apps/studio/stickers/life-style/mirror.png +0 -0
- package/apps/studio/stickers/life-style/nice.png +0 -0
- package/apps/studio/stickers/life-style/objects.png +0 -0
- package/apps/studio/stickers/life-style/ok.png +0 -0
- package/apps/studio/stickers/life-style/queen.png +0 -0
- package/apps/studio/stickers/life-style/star.png +0 -0
- package/apps/studio/stickers/life-style/sun.png +0 -0
- package/apps/studio/stickers/life-style/water.png +0 -0
- package/apps/studio/stickers/life-style/yeah.png +0 -0
- package/apps/studio/stickers/life-style/you-can.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/check-the-deets.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/cut-it.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/dope.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/fresh.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/hot.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/idea.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/keep-exploring.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/killed-it.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/love-it.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/manifest.json +88 -0
- package/apps/studio/stickers/opposing-thoughts/not-sure.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/ok.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/pure-gold.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/save-for-later.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/steal-this.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/take-a-peek.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/this-or-that.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/thoughts.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/vibes.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/winner.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/wip.png +0 -0
- package/apps/studio/stickers/project-status/group-100.png +0 -0
- package/apps/studio/stickers/project-status/group-101.png +0 -0
- package/apps/studio/stickers/project-status/group-102.png +0 -0
- package/apps/studio/stickers/project-status/group-103.png +0 -0
- package/apps/studio/stickers/project-status/group-104.png +0 -0
- package/apps/studio/stickers/project-status/group-105.png +0 -0
- package/apps/studio/stickers/project-status/group-106.png +0 -0
- package/apps/studio/stickers/project-status/group-107.png +0 -0
- package/apps/studio/stickers/project-status/group-108.png +0 -0
- package/apps/studio/stickers/project-status/group-109.png +0 -0
- package/apps/studio/stickers/project-status/group-110.png +0 -0
- package/apps/studio/stickers/project-status/group-111.png +0 -0
- package/apps/studio/stickers/project-status/group-112.png +0 -0
- package/apps/studio/stickers/project-status/group-113.png +0 -0
- package/apps/studio/stickers/project-status/group-114.png +0 -0
- package/apps/studio/stickers/project-status/group-115.png +0 -0
- package/apps/studio/stickers/project-status/group-117.png +0 -0
- package/apps/studio/stickers/project-status/group-118.png +0 -0
- package/apps/studio/stickers/project-status/group-119.png +0 -0
- package/apps/studio/stickers/project-status/group-120.png +0 -0
- package/apps/studio/stickers/project-status/group-121.png +0 -0
- package/apps/studio/stickers/project-status/group-122.png +0 -0
- package/apps/studio/stickers/project-status/group-41.png +0 -0
- package/apps/studio/stickers/project-status/group-42.png +0 -0
- package/apps/studio/stickers/project-status/group-43.png +0 -0
- package/apps/studio/stickers/project-status/group-44.png +0 -0
- package/apps/studio/stickers/project-status/group-45.png +0 -0
- package/apps/studio/stickers/project-status/group-46.png +0 -0
- package/apps/studio/stickers/project-status/group-47.png +0 -0
- package/apps/studio/stickers/project-status/group-48.png +0 -0
- package/apps/studio/stickers/project-status/group-49.png +0 -0
- package/apps/studio/stickers/project-status/group-50.png +0 -0
- package/apps/studio/stickers/project-status/group-51.png +0 -0
- package/apps/studio/stickers/project-status/group-52.png +0 -0
- package/apps/studio/stickers/project-status/group-53.png +0 -0
- package/apps/studio/stickers/project-status/group-54.png +0 -0
- package/apps/studio/stickers/project-status/group-55.png +0 -0
- package/apps/studio/stickers/project-status/group-56.png +0 -0
- package/apps/studio/stickers/project-status/group-57.png +0 -0
- package/apps/studio/stickers/project-status/group-58.png +0 -0
- package/apps/studio/stickers/project-status/group-59.png +0 -0
- package/apps/studio/stickers/project-status/group-60.png +0 -0
- package/apps/studio/stickers/project-status/group-61.png +0 -0
- package/apps/studio/stickers/project-status/group-62.png +0 -0
- package/apps/studio/stickers/project-status/group-63.png +0 -0
- package/apps/studio/stickers/project-status/group-64.png +0 -0
- package/apps/studio/stickers/project-status/group-65.png +0 -0
- package/apps/studio/stickers/project-status/group-66.png +0 -0
- package/apps/studio/stickers/project-status/group-67.png +0 -0
- package/apps/studio/stickers/project-status/group-68.png +0 -0
- package/apps/studio/stickers/project-status/group-69.png +0 -0
- package/apps/studio/stickers/project-status/group-70.png +0 -0
- package/apps/studio/stickers/project-status/group-71.png +0 -0
- package/apps/studio/stickers/project-status/group-72.png +0 -0
- package/apps/studio/stickers/project-status/group-73.png +0 -0
- package/apps/studio/stickers/project-status/group-74.png +0 -0
- package/apps/studio/stickers/project-status/group-75.png +0 -0
- package/apps/studio/stickers/project-status/group-76.png +0 -0
- package/apps/studio/stickers/project-status/group-77.png +0 -0
- package/apps/studio/stickers/project-status/group-78.png +0 -0
- package/apps/studio/stickers/project-status/group-79.png +0 -0
- package/apps/studio/stickers/project-status/group-80.png +0 -0
- package/apps/studio/stickers/project-status/group-81.png +0 -0
- package/apps/studio/stickers/project-status/group-82.png +0 -0
- package/apps/studio/stickers/project-status/group-83.png +0 -0
- package/apps/studio/stickers/project-status/group-84.png +0 -0
- package/apps/studio/stickers/project-status/group-85.png +0 -0
- package/apps/studio/stickers/project-status/group-86.png +0 -0
- package/apps/studio/stickers/project-status/group-87.png +0 -0
- package/apps/studio/stickers/project-status/group-88.png +0 -0
- package/apps/studio/stickers/project-status/group-89.png +0 -0
- package/apps/studio/stickers/project-status/group-90.png +0 -0
- package/apps/studio/stickers/project-status/group-91.png +0 -0
- package/apps/studio/stickers/project-status/group-92.png +0 -0
- package/apps/studio/stickers/project-status/group-93.png +0 -0
- package/apps/studio/stickers/project-status/group-94.png +0 -0
- package/apps/studio/stickers/project-status/group-96.png +0 -0
- package/apps/studio/stickers/project-status/group-97.png +0 -0
- package/apps/studio/stickers/project-status/group-98.png +0 -0
- package/apps/studio/stickers/project-status/group-99.png +0 -0
- package/apps/studio/stickers/project-status/manifest.json +328 -0
- package/apps/studio/test/_helpers.ts +15 -2
- package/apps/studio/test/acp-bootstrap-brief.test.ts +11 -0
- package/apps/studio/test/acp-bridge.test.ts +34 -1
- package/apps/studio/test/acp-commands.test.ts +2 -1
- package/apps/studio/test/acp-env.test.ts +30 -0
- package/apps/studio/test/acp-plugin-bootstrap.test.ts +23 -57
- package/apps/studio/test/acp-session-plugins.test.ts +72 -1
- package/apps/studio/test/annotation-strokes-command.test.ts +17 -0
- package/apps/studio/test/annotations-layer.test.ts +174 -0
- package/apps/studio/test/asset-api.test.ts +3 -3
- package/apps/studio/test/canvas-edit.test.ts +17 -6
- package/apps/studio/test/canvas-origin-gate.test.ts +50 -0
- package/apps/studio/test/csp-canvas-shell.test.ts +11 -0
- package/apps/studio/test/csrf-write-guard.test.ts +23 -1
- package/apps/studio/test/design-setup-readiness.test.ts +155 -0
- package/apps/studio/test/dynamic-text-edit.test.ts +162 -0
- package/apps/studio/test/edit-source-command.test.ts +23 -0
- package/apps/studio/test/element-structural-edit.test.ts +154 -0
- package/apps/studio/test/fixtures/fake-claude-auth.mjs +13 -0
- package/apps/studio/test/footage-store.test.ts +100 -0
- package/apps/studio/test/generate-route.test.ts +106 -0
- package/apps/studio/test/git-branches.test.ts +97 -0
- package/apps/studio/test/github-api.test.ts +56 -1
- package/apps/studio/test/import-asset-browser.test.ts +64 -0
- package/apps/studio/test/import-asset-route.test.ts +71 -0
- package/apps/studio/test/import-asset.test.ts +399 -0
- package/apps/studio/test/import-brand-browser.test.ts +88 -0
- package/apps/studio/test/import-brand-route.test.ts +120 -0
- package/apps/studio/test/import-brand.test.ts +206 -0
- package/apps/studio/test/import-tokens.test.ts +722 -0
- package/apps/studio/test/input-router.test.ts +33 -0
- package/apps/studio/test/inspect-script-syntax.test.ts +53 -0
- package/apps/studio/test/media-commit-chain.test.ts +210 -0
- package/apps/studio/test/photo-bg-remove-validation.test.ts +71 -0
- package/apps/studio/test/photo-canvas-bundle.test.ts +61 -0
- package/apps/studio/test/photo-edit-api.test.ts +201 -0
- package/apps/studio/test/photo-filters.test.ts +149 -0
- package/apps/studio/test/photo-pipeline.test.ts +106 -0
- package/apps/studio/test/photo-taxonomy.test.ts +96 -0
- package/apps/studio/test/read-annotations.test.ts +164 -0
- package/apps/studio/test/readiness.test.ts +13 -11
- package/apps/studio/test/scaffold-design.test.ts +122 -0
- package/apps/studio/test/shell-importmap.test.ts +49 -0
- package/apps/studio/test/text-editability-stamp.test.ts +131 -0
- package/apps/studio/test/timeline-parse.test.ts +57 -0
- package/apps/studio/test/tool-palette-insert-anchor.test.ts +5 -3
- package/apps/studio/test/tour-overlay.test.tsx +16 -0
- package/apps/studio/test/undo-stack.test.ts +33 -0
- package/apps/studio/test/video-asset.test.ts +5 -5
- package/apps/studio/test/video-render-bridge.test.ts +23 -1
- package/apps/studio/text-caret.ts +239 -0
- package/apps/studio/tool-palette.tsx +49 -21
- package/apps/studio/ui-prefs.ts +130 -0
- package/apps/studio/undo-stack.ts +94 -2
- package/apps/studio/use-annotation-resize.tsx +4 -4
- package/apps/studio/use-canvas-media-drop.tsx +40 -1
- package/apps/studio/use-chrome-visibility.tsx +8 -2
- package/apps/studio/use-selection-set.tsx +21 -0
- package/apps/studio/video-comp.tsx +48 -7
- package/apps/studio/whats-new.json +149 -0
- package/apps/studio/ws.ts +5 -0
- package/cli/bin/maude.mjs +6 -6
- package/cli/commands/design.mjs +67 -3
- package/cli/lib/gitignore-block.mjs +2 -0
- package/cli/lib/pkg-root.mjs +107 -0
- package/cli/lib/pkg-root.test.mjs +79 -0
- package/cli/lib/reconstruct-toolset.test.mjs +194 -0
- package/cli/lib/update-check.mjs +8 -0
- package/package.json +13 -11
- package/plugins/design/dependencies.json +18 -0
- package/plugins/design/templates/_shell.html +1 -0
package/apps/studio/api.ts
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
insertArtboard,
|
|
24
24
|
insertClip,
|
|
25
25
|
insertElement,
|
|
26
|
+
insertElementIntoArtboard,
|
|
26
27
|
type MovePosition,
|
|
27
28
|
moveElement,
|
|
28
29
|
removeAttribute,
|
|
@@ -33,10 +34,19 @@ import {
|
|
|
33
34
|
retimeSequence,
|
|
34
35
|
retimeSequenceByClip,
|
|
35
36
|
editText as runEditText,
|
|
37
|
+
setArtboardHug,
|
|
38
|
+
setArtboardStyle,
|
|
36
39
|
toggleClipHidden,
|
|
37
40
|
} from './canvas-edit.ts';
|
|
38
41
|
import type { Context } from './context.ts';
|
|
42
|
+
import {
|
|
43
|
+
type AudioMatch,
|
|
44
|
+
type Candidate,
|
|
45
|
+
rankMatches,
|
|
46
|
+
sanitizeReuseText,
|
|
47
|
+
} from './generation/audio-library.ts';
|
|
39
48
|
import { createHistory } from './history.ts';
|
|
49
|
+
import { STICKERS_DIR } from './paths.ts';
|
|
40
50
|
|
|
41
51
|
// Directories that never hold user-facing canvases. Exported so the
|
|
42
52
|
// external-canvas watcher (`canvas-list-watch.ts`) shares one source instead of
|
|
@@ -255,6 +265,32 @@ export interface Api {
|
|
|
255
265
|
saveAsset(bytes: Uint8Array): Promise<SaveAssetResult>;
|
|
256
266
|
/** Stage F1 — list content-addressed image/video assets for the AssetPicker. */
|
|
257
267
|
listAssets(): Promise<{ ok: true; assets: AssetListing[] }>;
|
|
268
|
+
/** feature-ai-media-generation (Task 1.2) — read a content-addressed
|
|
269
|
+
* `assets/<sha8>.<ext>` source's bytes + sniffed mime for the image-edit /
|
|
270
|
+
* image-to-video generation flows. Contained to <designRoot>/assets/;
|
|
271
|
+
* null for an unknown/contained-out path. */
|
|
272
|
+
readAssetBytes(rel: unknown): Promise<{ bytes: Uint8Array; mime: string } | null>;
|
|
273
|
+
/** feature-ai-media-generation (Task 2.6) — write a caption sidecar
|
|
274
|
+
* (`assets/<sha8>.srt|.vtt`) next to a content-addressed source, so a cloud
|
|
275
|
+
* STT result lands where the local whisper path also writes it. Contained to
|
|
276
|
+
* <designRoot>/assets/; text byte-capped; format allowlisted. */
|
|
277
|
+
writeCaptionSidecar(
|
|
278
|
+
sourceRel: unknown,
|
|
279
|
+
format: unknown,
|
|
280
|
+
text: unknown
|
|
281
|
+
): Promise<{ ok: boolean; path?: string; error?: string }>;
|
|
282
|
+
/** feature-ai-media-generation (Task 2.5) — write the audio-intent sidecar
|
|
283
|
+
* (`assets/<sha8>.audio.json`) for reuse-before-you-pay search. */
|
|
284
|
+
writeAudioIntent(
|
|
285
|
+
assetRel: unknown,
|
|
286
|
+
meta: { kind?: string; prompt?: string; provider?: string; model?: string; at?: string }
|
|
287
|
+
): Promise<{ ok: boolean; path?: string; error?: string }>;
|
|
288
|
+
/** feature-ai-media-generation (Task 2.5) — keyword-search the project's own
|
|
289
|
+
* generated audio by recorded intent; ranked reuse candidates. */
|
|
290
|
+
searchAudioLibrary(query: unknown, limit?: number): Promise<AudioMatch[]>;
|
|
291
|
+
/** Phase 4 (feature-whiteboard-annotation-improvements) — the bundled sticker
|
|
292
|
+
* catalogue (MAUDE's own, not the served project's) for the StickerPicker. */
|
|
293
|
+
listStickers(): Promise<{ ok: true; packs: StickerPack[] }>;
|
|
258
294
|
/** DDR-148 — streaming variant for the HTTP route (100 MB video without a
|
|
259
295
|
* full in-RAM buffer). Sniffs + caps + content-addresses like saveAsset. */
|
|
260
296
|
saveAssetFromStream(stream: ReadableStream<Uint8Array>): Promise<SaveAssetResult>;
|
|
@@ -288,7 +324,13 @@ export interface Api {
|
|
|
288
324
|
idIndex?: unknown;
|
|
289
325
|
}): Promise<EditOpResult>;
|
|
290
326
|
// Phase 12 (DDR-103) — inline text-content edit (POST /_api/edit-text). Main-origin only.
|
|
291
|
-
editText(input: {
|
|
327
|
+
editText(input: {
|
|
328
|
+
canvas?: unknown;
|
|
329
|
+
id?: unknown;
|
|
330
|
+
text?: unknown;
|
|
331
|
+
occurrence?: unknown;
|
|
332
|
+
before?: unknown;
|
|
333
|
+
}): Promise<EditOpResult>;
|
|
292
334
|
// Phase 12.2 (DDR-104) — custom HTML attribute edit (POST /_api/edit-attr). Main-origin
|
|
293
335
|
// only. The CSS panel's "custom HTML attribute" escape hatch (data-*, aria-*, role, …);
|
|
294
336
|
// writes a plain JSX attribute via editAttribute's non-`style.` path.
|
|
@@ -396,10 +438,16 @@ export interface Api {
|
|
|
396
438
|
}): Promise<
|
|
397
439
|
{ ok: true; deletedId: string; seq?: number } | { ok: false; status: number; error: string }
|
|
398
440
|
>;
|
|
399
|
-
/**
|
|
441
|
+
/**
|
|
442
|
+
* Insert a synthesized div/text/image relative to a reference element, OR —
|
|
443
|
+
* when the artboard has no element to anchor on yet — as a direct child of
|
|
444
|
+
* `artboardId` (the tool-palette "+ Element" empty-artboard fallback).
|
|
445
|
+
* Exactly one of `refId` / `artboardId` must be provided.
|
|
446
|
+
*/
|
|
400
447
|
insertElementOp(input: {
|
|
401
448
|
canvas?: unknown;
|
|
402
449
|
refId?: unknown;
|
|
450
|
+
artboardId?: unknown;
|
|
403
451
|
position?: unknown;
|
|
404
452
|
kind?: unknown;
|
|
405
453
|
src?: unknown;
|
|
@@ -424,6 +472,22 @@ export interface Api {
|
|
|
424
472
|
width?: unknown;
|
|
425
473
|
height?: unknown;
|
|
426
474
|
}): Promise<{ ok: true; seq?: number } | { ok: false; status: number; error: string }>;
|
|
475
|
+
/** Toggle an artboard's Hug/Fixed height sizing mode (CSS-panel control). */
|
|
476
|
+
setArtboardHugOp(input: {
|
|
477
|
+
canvas?: unknown;
|
|
478
|
+
artboardId?: unknown;
|
|
479
|
+
fixed?: unknown;
|
|
480
|
+
freezeHeight?: unknown;
|
|
481
|
+
}): Promise<{ ok: true; seq?: number } | { ok: false; status: number; error: string }>;
|
|
482
|
+
/** Set artboard "more settings" — background / padding / layout / gap. */
|
|
483
|
+
setArtboardStyleOp(input: {
|
|
484
|
+
canvas?: unknown;
|
|
485
|
+
artboardId?: unknown;
|
|
486
|
+
background?: unknown;
|
|
487
|
+
padding?: unknown;
|
|
488
|
+
layout?: unknown;
|
|
489
|
+
gap?: unknown;
|
|
490
|
+
}): Promise<{ ok: true; seq?: number } | { ok: false; status: number; error: string }>;
|
|
427
491
|
/** Duplicate an element (Cmd+D) — a copy as the next sibling, whole-file undo. */
|
|
428
492
|
duplicateElementOp(input: {
|
|
429
493
|
canvas?: unknown;
|
|
@@ -472,8 +536,19 @@ export { ASSET_IMAGE_HREF_RE, sanitizeAnnotationSvg } from './annotations-model.
|
|
|
472
536
|
|
|
473
537
|
import { sanitizeAnnotationSvg } from './annotations-model.ts';
|
|
474
538
|
|
|
475
|
-
/**
|
|
476
|
-
|
|
539
|
+
/**
|
|
540
|
+
* Phase 23 — per-file ceiling for a still image. Raised 10 MB → 50 MB (still
|
|
541
|
+
* well under the video cap / MAX_REQUEST_BODY headroom) after a real drone
|
|
542
|
+
* photo tripped the old ceiling. Overridable via `MAUDE_ASSET_MAX_IMAGE_BYTES`
|
|
543
|
+
* (bytes), mirroring {@link ASSET_MAX_VIDEO_BYTES}'s override. The route lives
|
|
544
|
+
* on the (untrusted) canvas origin (DDR-088), and images stream through the
|
|
545
|
+
* same category-capped writer as video/audio (`saveAssetFromStream`), so
|
|
546
|
+
* raising this doesn't reintroduce the memory-amplification risk DDR-088 capped.
|
|
547
|
+
*/
|
|
548
|
+
export const ASSET_MAX_BYTES = (() => {
|
|
549
|
+
const env = Number(process.env.MAUDE_ASSET_MAX_IMAGE_BYTES);
|
|
550
|
+
return Number.isFinite(env) && env > 0 ? env : 50 * 1024 * 1024;
|
|
551
|
+
})();
|
|
477
552
|
|
|
478
553
|
/**
|
|
479
554
|
* Phase 23 security review (DDR-088 follow-up) — aggregate per-server-instance
|
|
@@ -560,11 +635,29 @@ export interface AssetListing {
|
|
|
560
635
|
path: string;
|
|
561
636
|
name: string;
|
|
562
637
|
ext: string;
|
|
563
|
-
kind: 'image' | 'video';
|
|
638
|
+
kind: 'image' | 'video' | 'audio';
|
|
564
639
|
size: number;
|
|
565
640
|
mtimeMs: number;
|
|
566
641
|
}
|
|
567
642
|
|
|
643
|
+
/** Phase 4 (feature-whiteboard-annotation-improvements) — one bundled sticker. */
|
|
644
|
+
export interface StickerItem {
|
|
645
|
+
file: string;
|
|
646
|
+
keywords: string[];
|
|
647
|
+
/** Servable path — `/_stickers/<pack>/<file>` (main-origin static route). */
|
|
648
|
+
url: string;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/** A bundled sticker pack — one `apps/studio/stickers/<slug>/manifest.json`. */
|
|
652
|
+
export interface StickerPack {
|
|
653
|
+
slug: string;
|
|
654
|
+
name: string;
|
|
655
|
+
author: string;
|
|
656
|
+
attributionUrl: string;
|
|
657
|
+
license: string;
|
|
658
|
+
stickers: StickerItem[];
|
|
659
|
+
}
|
|
660
|
+
|
|
568
661
|
/** DDR-148 — media category, decides which per-file cap applies. */
|
|
569
662
|
export type AssetCategory = 'image' | 'video' | 'audio';
|
|
570
663
|
|
|
@@ -576,7 +669,7 @@ export interface AssetTypeInfo {
|
|
|
576
669
|
|
|
577
670
|
/**
|
|
578
671
|
* DDR-148 — per-file ceiling for time-based media (video + audio). Images keep
|
|
579
|
-
* the tighter {@link ASSET_MAX_BYTES}
|
|
672
|
+
* the tighter {@link ASSET_MAX_BYTES} cap. Overridable via
|
|
580
673
|
* `MAUDE_ASSET_MAX_VIDEO_BYTES` (bytes) for power users. The route lives on the
|
|
581
674
|
* (untrusted) canvas origin, so this cap + the session budget + the streamed
|
|
582
675
|
* write are the trust mitigation, exactly like the image caps (DDR-088).
|
|
@@ -595,7 +688,9 @@ const UNSUPPORTED_ASSET_MSG =
|
|
|
595
688
|
'unsupported media type — png/jpeg/gif/webp images or mp4/mov/webm/mp3/wav/m4a media only (SVG/script rejected)';
|
|
596
689
|
|
|
597
690
|
function capError(category?: AssetCategory): string {
|
|
598
|
-
if (category === 'image')
|
|
691
|
+
if (category === 'image') {
|
|
692
|
+
return `image exceeds the ${Math.round(ASSET_MAX_BYTES / (1024 * 1024))} MB cap`;
|
|
693
|
+
}
|
|
599
694
|
const mb = Math.round(ASSET_MAX_VIDEO_BYTES / (1024 * 1024));
|
|
600
695
|
return `media exceeds the ${mb} MB cap`;
|
|
601
696
|
}
|
|
@@ -1316,8 +1411,9 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
1316
1411
|
// optional (DDR Task 9):
|
|
1317
1412
|
// • magic-byte sniff → true type ∈ {png,jpg,gif,webp}; a header lie or an
|
|
1318
1413
|
// SVG (script-bearing vector) is rejected — bytes decide, name is ignored.
|
|
1319
|
-
// •
|
|
1320
|
-
// SVG-text gate in
|
|
1414
|
+
// • per-category ceiling ({@link ASSET_MAX_BYTES} for images; assets get
|
|
1415
|
+
// their OWN cap; never routed through the 1 MB SVG-text gate in
|
|
1416
|
+
// saveAnnotations).
|
|
1321
1417
|
// • content-addressed name `assets/<sha8-of-bytes>.<ext>` → identical drops
|
|
1322
1418
|
// dedupe → a malicious canvas can't fill the disk with N copies of one
|
|
1323
1419
|
// image, and orphan-on-delete is safe (shared content survives).
|
|
@@ -1331,8 +1427,8 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
1331
1427
|
* request body chunk-by-chunk so a 100 MB video never lands as a single
|
|
1332
1428
|
* ArrayBuffer in RAM (the memory-amplification vector DDR-088 §review flagged
|
|
1333
1429
|
* for the untrusted canvas origin — worse now that the per-file cap is 100 MB,
|
|
1334
|
-
* not
|
|
1335
|
-
* streaming (image
|
|
1430
|
+
* not a few MB). Sniffs the type from the head, applies the CATEGORY cap while
|
|
1431
|
+
* streaming (image {@link ASSET_MAX_BYTES} · video/audio {@link ASSET_MAX_VIDEO_BYTES}), writes
|
|
1336
1432
|
* to a temp file, then content-addresses (sha8) → dedupe-rename. Nothing is
|
|
1337
1433
|
* ever parsed — only magic bytes are read. `saveAsset(bytes)` wraps this so
|
|
1338
1434
|
* both the route and any programmatic caller share ONE tested path.
|
|
@@ -1482,6 +1578,10 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
1482
1578
|
const assetsDir = path.join(paths.designRoot, 'assets');
|
|
1483
1579
|
const IMG = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.avif', '.svg']);
|
|
1484
1580
|
const VID = new Set(['.mp4', '.webm', '.mov', '.m4v', '.ogg']);
|
|
1581
|
+
// feature-ai-media-generation Phase 2 — generated audio (ElevenLabs music /
|
|
1582
|
+
// SFX / voiceover) is content-addressed under assets/ like any other media;
|
|
1583
|
+
// surface it in the picker so a generated track can be dropped into an EDL.
|
|
1584
|
+
const AUD = new Set(['.mp3', '.wav', '.m4a', '.aac', '.flac', '.oga', '.opus']);
|
|
1485
1585
|
let entries: string[] = [];
|
|
1486
1586
|
try {
|
|
1487
1587
|
entries = await readdir(assetsDir);
|
|
@@ -1491,7 +1591,7 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
1491
1591
|
const out: AssetListing[] = [];
|
|
1492
1592
|
for (const name of entries) {
|
|
1493
1593
|
const ext = path.extname(name).toLowerCase();
|
|
1494
|
-
const kind = IMG.has(ext) ? 'image' : VID.has(ext) ? 'video' : null;
|
|
1594
|
+
const kind = IMG.has(ext) ? 'image' : VID.has(ext) ? 'video' : AUD.has(ext) ? 'audio' : null;
|
|
1495
1595
|
if (!kind) continue;
|
|
1496
1596
|
let size = 0;
|
|
1497
1597
|
let mtimeMs = 0;
|
|
@@ -1512,6 +1612,253 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
1512
1612
|
return { ok: true, assets: out.slice(0, 500) };
|
|
1513
1613
|
}
|
|
1514
1614
|
|
|
1615
|
+
// feature-ai-media-generation (Task 1.2) — read a content-addressed source
|
|
1616
|
+
// asset's bytes + authoritative (magic-byte-SNIFFED, not extension-trusted)
|
|
1617
|
+
// mime, for the image-edit / image-to-video generation flows (Nano Banana
|
|
1618
|
+
// maskless edit reads the source into an inlineData part). The provider adapter
|
|
1619
|
+
// never touches the filesystem — the generation route wires this onto
|
|
1620
|
+
// AdapterContext.readSourceAsset. Contained to <designRoot>/assets/ (an lstat
|
|
1621
|
+
// isFile guard defeats a planted symlink, mirroring listAssets); returns null
|
|
1622
|
+
// for an unknown / traversing / oversized path. Capped at ASSET_MAX_BYTES so a
|
|
1623
|
+
// huge source can't be buffered into RAM before an outbound provider POST.
|
|
1624
|
+
async function readAssetBytes(rel: unknown): Promise<{ bytes: Uint8Array; mime: string } | null> {
|
|
1625
|
+
if (typeof rel !== 'string' || !rel.startsWith('assets/')) return null;
|
|
1626
|
+
const name = rel.slice('assets/'.length);
|
|
1627
|
+
if (
|
|
1628
|
+
!name ||
|
|
1629
|
+
name.includes('/') ||
|
|
1630
|
+
name.includes('\\') ||
|
|
1631
|
+
name.includes('..') ||
|
|
1632
|
+
name.startsWith('_')
|
|
1633
|
+
)
|
|
1634
|
+
return null;
|
|
1635
|
+
const assetsDir = path.join(paths.designRoot, 'assets');
|
|
1636
|
+
const fileAbs = path.join(assetsDir, name);
|
|
1637
|
+
// Containment backstop — the name is validated above; assert the join anyway.
|
|
1638
|
+
if (path.resolve(fileAbs) !== path.join(path.resolve(assetsDir), name)) return null;
|
|
1639
|
+
try {
|
|
1640
|
+
const st = await lstat(fileAbs);
|
|
1641
|
+
if (!st.isFile() || st.size > ASSET_MAX_BYTES) return null;
|
|
1642
|
+
} catch {
|
|
1643
|
+
return null;
|
|
1644
|
+
}
|
|
1645
|
+
const bytes = new Uint8Array(await readFile(fileAbs));
|
|
1646
|
+
const info = sniffAssetType(bytes);
|
|
1647
|
+
if (!info) return null; // not a recognised raster/video/audio — reject
|
|
1648
|
+
const mime =
|
|
1649
|
+
info.ext === 'jpg'
|
|
1650
|
+
? 'image/jpeg'
|
|
1651
|
+
: info.category === 'image'
|
|
1652
|
+
? `image/${info.ext}`
|
|
1653
|
+
: info.category === 'video'
|
|
1654
|
+
? `video/${info.ext}`
|
|
1655
|
+
: `audio/${info.ext}`;
|
|
1656
|
+
return { bytes, mime };
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
// feature-ai-media-generation (Task 2.6, DDR-164) — write a caption SIDECAR
|
|
1660
|
+
// (SRT/VTT text) next to a content-addressed source asset, so a CLOUD STT
|
|
1661
|
+
// result (ElevenLabs Scribe / Groq) lands in the SAME place the local whisper
|
|
1662
|
+
// path writes it: `assets/<sha8>.srt` beside `assets/<sha8>.<ext>`. Local +
|
|
1663
|
+
// cloud subtitles therefore live at one predictable path (the EDL caption
|
|
1664
|
+
// track / a video-comp `<track>` reads `assets/<sha8>.srt`). The caption text
|
|
1665
|
+
// is NOT run through the media magic-byte store (it isn't media) — it is a
|
|
1666
|
+
// versioned text sidecar, mirroring `.meta.json` / `.annotations.svg`.
|
|
1667
|
+
//
|
|
1668
|
+
// Contained to <designRoot>/assets/ exactly like readAssetBytes: the source
|
|
1669
|
+
// must be a validated `assets/<sha8>.<ext>` name, the format is allowlisted,
|
|
1670
|
+
// and the text is byte-capped. Returns the sidecar rel path or null on a bad
|
|
1671
|
+
// source / format.
|
|
1672
|
+
async function writeCaptionSidecar(
|
|
1673
|
+
sourceRel: unknown,
|
|
1674
|
+
format: unknown,
|
|
1675
|
+
text: unknown
|
|
1676
|
+
): Promise<{ ok: boolean; path?: string; error?: string }> {
|
|
1677
|
+
if (typeof sourceRel !== 'string' || !sourceRel.startsWith('assets/'))
|
|
1678
|
+
return { ok: false, error: 'source must be an assets/<sha8>.<ext> path' };
|
|
1679
|
+
const srcName = sourceRel.slice('assets/'.length);
|
|
1680
|
+
if (
|
|
1681
|
+
!srcName ||
|
|
1682
|
+
srcName.includes('/') ||
|
|
1683
|
+
srcName.includes('\\') ||
|
|
1684
|
+
srcName.includes('..') ||
|
|
1685
|
+
srcName.startsWith('_')
|
|
1686
|
+
)
|
|
1687
|
+
return { ok: false, error: 'invalid source asset name' };
|
|
1688
|
+
const fmt = format === 'vtt' ? 'vtt' : format === 'srt' ? 'srt' : null;
|
|
1689
|
+
if (!fmt) return { ok: false, error: 'format must be srt or vtt' };
|
|
1690
|
+
if (typeof text !== 'string' || !text.trim()) return { ok: false, error: 'empty caption text' };
|
|
1691
|
+
if (text.length > 4 * 1024 * 1024) return { ok: false, error: 'caption text too large' };
|
|
1692
|
+
|
|
1693
|
+
// Strip the source extension → `<sha8>` (or the whole name if none), append
|
|
1694
|
+
// the caption format. A dotless source name keeps its whole name as the base.
|
|
1695
|
+
const dot = srcName.lastIndexOf('.');
|
|
1696
|
+
const base = dot > 0 ? srcName.slice(0, dot) : srcName;
|
|
1697
|
+
const outName = `${base}.${fmt}`;
|
|
1698
|
+
const assetsDir = path.join(paths.designRoot, 'assets');
|
|
1699
|
+
const fileAbs = path.join(assetsDir, outName);
|
|
1700
|
+
if (path.resolve(fileAbs) !== path.join(path.resolve(assetsDir), outName))
|
|
1701
|
+
return { ok: false, error: 'resolved sidecar path escapes assets dir' };
|
|
1702
|
+
try {
|
|
1703
|
+
await mkdir(assetsDir, { recursive: true });
|
|
1704
|
+
await Bun.write(fileAbs, text);
|
|
1705
|
+
return { ok: true, path: `assets/${outName}` };
|
|
1706
|
+
} catch (err) {
|
|
1707
|
+
return { ok: false, error: err instanceof Error ? err.message : 'sidecar write failed' };
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
// feature-ai-media-generation (Task 2.5, DDR-164) — write the AUDIO INTENT
|
|
1712
|
+
// sidecar (`assets/<sha8>.audio.json`) next to a generated audio asset: the
|
|
1713
|
+
// durable, semantic "what was this audio FOR" index that reuse-before-you-pay
|
|
1714
|
+
// searches. Byte-content-addressing already dedups identical outputs; this adds
|
|
1715
|
+
// the "do we already have a warm lo-fi loop?" lookup. Contained to assets/ like
|
|
1716
|
+
// the caption sidecar; the intent is inert non-secret metadata.
|
|
1717
|
+
async function writeAudioIntent(
|
|
1718
|
+
assetRel: unknown,
|
|
1719
|
+
meta: { kind?: string; prompt?: string; provider?: string; model?: string; at?: string }
|
|
1720
|
+
): Promise<{ ok: boolean; path?: string; error?: string }> {
|
|
1721
|
+
if (typeof assetRel !== 'string' || !assetRel.startsWith('assets/'))
|
|
1722
|
+
return { ok: false, error: 'asset must be an assets/<sha8>.<ext> path' };
|
|
1723
|
+
const name = assetRel.slice('assets/'.length);
|
|
1724
|
+
if (
|
|
1725
|
+
!name ||
|
|
1726
|
+
name.includes('/') ||
|
|
1727
|
+
name.includes('\\') ||
|
|
1728
|
+
name.includes('..') ||
|
|
1729
|
+
name.startsWith('_')
|
|
1730
|
+
)
|
|
1731
|
+
return { ok: false, error: 'invalid asset name' };
|
|
1732
|
+
const dot = name.lastIndexOf('.');
|
|
1733
|
+
const base = dot > 0 ? name.slice(0, dot) : name;
|
|
1734
|
+
const outName = `${base}.audio.json`;
|
|
1735
|
+
const assetsDir = path.join(paths.designRoot, 'assets');
|
|
1736
|
+
const fileAbs = path.join(assetsDir, outName);
|
|
1737
|
+
if (path.resolve(fileAbs) !== path.join(path.resolve(assetsDir), outName))
|
|
1738
|
+
return { ok: false, error: 'resolved sidecar path escapes assets dir' };
|
|
1739
|
+
const record = {
|
|
1740
|
+
asset: assetRel,
|
|
1741
|
+
kind: meta.kind,
|
|
1742
|
+
prompt: typeof meta.prompt === 'string' ? meta.prompt.slice(0, 8000) : undefined,
|
|
1743
|
+
provider: meta.provider,
|
|
1744
|
+
model: meta.model,
|
|
1745
|
+
at: meta.at ?? new Date().toISOString(),
|
|
1746
|
+
};
|
|
1747
|
+
try {
|
|
1748
|
+
await mkdir(assetsDir, { recursive: true });
|
|
1749
|
+
await Bun.write(fileAbs, `${JSON.stringify(record, null, 2)}\n`);
|
|
1750
|
+
return { ok: true, path: `assets/${outName}` };
|
|
1751
|
+
} catch (err) {
|
|
1752
|
+
return { ok: false, error: err instanceof Error ? err.message : 'intent write failed' };
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
// feature-ai-media-generation (Task 2.5) — search the project's OWN generated
|
|
1757
|
+
// audio by intent (keyword overlap against the recorded prompt/kind), so the
|
|
1758
|
+
// reuse-first path can offer an existing track before paying for a new
|
|
1759
|
+
// generation. Reads the `assets/<sha8>.audio.json` sidecars; ranks with the
|
|
1760
|
+
// shared pure scorer (generation/audio-library.ts) so local + provider-history
|
|
1761
|
+
// results are comparable. Best-effort — a malformed sidecar is skipped.
|
|
1762
|
+
async function searchAudioLibrary(query: unknown, limit = 10): Promise<AudioMatch[]> {
|
|
1763
|
+
if (typeof query !== 'string' || !query.trim()) return [];
|
|
1764
|
+
const assetsDir = path.join(paths.designRoot, 'assets');
|
|
1765
|
+
let entries: string[];
|
|
1766
|
+
try {
|
|
1767
|
+
entries = await readdir(assetsDir);
|
|
1768
|
+
} catch {
|
|
1769
|
+
return [];
|
|
1770
|
+
}
|
|
1771
|
+
// F2 (ethical-hacker) — an intent sidecar is a peer-synced, hence UNTRUSTED
|
|
1772
|
+
// (DDR-054), file: a hostile branch peer could commit a giant `.audio.json`
|
|
1773
|
+
// or thousands of them to OOM the reader (the F1 RAM-DoS class, on disk). A
|
|
1774
|
+
// real intent record is tiny → cap the per-file read AND the number scanned.
|
|
1775
|
+
const MAX_SIDECAR_BYTES = 256 * 1024;
|
|
1776
|
+
const MAX_SIDECARS = 4000;
|
|
1777
|
+
const candidates: Candidate[] = [];
|
|
1778
|
+
let scanned = 0;
|
|
1779
|
+
for (const name of entries) {
|
|
1780
|
+
if (!name.endsWith('.audio.json')) continue;
|
|
1781
|
+
if (++scanned > MAX_SIDECARS) break;
|
|
1782
|
+
try {
|
|
1783
|
+
const fileAbs = path.join(assetsDir, name);
|
|
1784
|
+
const st = await lstat(fileAbs);
|
|
1785
|
+
if (!st.isFile() || st.size > MAX_SIDECAR_BYTES) continue;
|
|
1786
|
+
const raw = await readFile(fileAbs, 'utf8');
|
|
1787
|
+
const intent = JSON.parse(raw) as Record<string, unknown>;
|
|
1788
|
+
const asset =
|
|
1789
|
+
typeof intent.asset === 'string'
|
|
1790
|
+
? intent.asset
|
|
1791
|
+
: `assets/${name.replace(/\.audio\.json$/, '')}`;
|
|
1792
|
+
candidates.push({
|
|
1793
|
+
source: 'local',
|
|
1794
|
+
ref: asset,
|
|
1795
|
+
// Sanitize the peer-synced (untrusted) prompt before it can be echoed
|
|
1796
|
+
// into an agent context (F3).
|
|
1797
|
+
text: sanitizeReuseText(intent.prompt),
|
|
1798
|
+
kind: typeof intent.kind === 'string' ? intent.kind : undefined,
|
|
1799
|
+
provider: typeof intent.provider === 'string' ? intent.provider : undefined,
|
|
1800
|
+
at: typeof intent.at === 'string' ? intent.at : undefined,
|
|
1801
|
+
});
|
|
1802
|
+
} catch {
|
|
1803
|
+
/* skip a malformed sidecar */
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
return rankMatches(query, candidates, { limit });
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
// Phase 4 (feature-whiteboard-annotation-improvements) — the bundled sticker
|
|
1810
|
+
// catalogue for the StickerPicker. Reads from MAUDE's OWN `STICKERS_DIR`
|
|
1811
|
+
// (paths.ts, DDR-045) — never the served project's designRoot, unlike
|
|
1812
|
+
// listAssets above. A pack directory missing or with an invalid
|
|
1813
|
+
// manifest.json is skipped, not fatal — one bad pack shouldn't blank the
|
|
1814
|
+
// whole picker. MAIN-ORIGIN ONLY at the route layer (the picker is shell UI,
|
|
1815
|
+
// same posture as listAssets/AssetPicker).
|
|
1816
|
+
async function listStickers(): Promise<{ ok: true; packs: StickerPack[] }> {
|
|
1817
|
+
let packDirs: string[] = [];
|
|
1818
|
+
try {
|
|
1819
|
+
const entries = await readdir(STICKERS_DIR, { withFileTypes: true });
|
|
1820
|
+
packDirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
1821
|
+
} catch {
|
|
1822
|
+
return { ok: true, packs: [] }; // no bundled stickers in this layout
|
|
1823
|
+
}
|
|
1824
|
+
const packs: StickerPack[] = [];
|
|
1825
|
+
for (const slug of packDirs) {
|
|
1826
|
+
try {
|
|
1827
|
+
const manifestPath = path.join(STICKERS_DIR, slug, 'manifest.json');
|
|
1828
|
+
const raw = await readFile(manifestPath, 'utf8');
|
|
1829
|
+
const manifest = JSON.parse(raw) as {
|
|
1830
|
+
name?: string;
|
|
1831
|
+
author?: string;
|
|
1832
|
+
attributionUrl?: string;
|
|
1833
|
+
license?: string;
|
|
1834
|
+
stickers?: Array<{ file?: string; keywords?: string[] }>;
|
|
1835
|
+
};
|
|
1836
|
+
const stickers: StickerItem[] = (manifest.stickers ?? [])
|
|
1837
|
+
.filter(
|
|
1838
|
+
(s): s is { file: string; keywords?: string[] } =>
|
|
1839
|
+
typeof s.file === 'string' && !!s.file
|
|
1840
|
+
)
|
|
1841
|
+
.map((s) => ({
|
|
1842
|
+
file: s.file,
|
|
1843
|
+
keywords: Array.isArray(s.keywords)
|
|
1844
|
+
? s.keywords.filter((k) => typeof k === 'string')
|
|
1845
|
+
: [],
|
|
1846
|
+
url: `/_stickers/${slug}/${encodeURIComponent(s.file)}`,
|
|
1847
|
+
}));
|
|
1848
|
+
packs.push({
|
|
1849
|
+
slug,
|
|
1850
|
+
name: manifest.name ?? slug,
|
|
1851
|
+
author: manifest.author ?? '',
|
|
1852
|
+
attributionUrl: manifest.attributionUrl ?? '',
|
|
1853
|
+
license: manifest.license ?? '',
|
|
1854
|
+
stickers,
|
|
1855
|
+
});
|
|
1856
|
+
} catch {}
|
|
1857
|
+
}
|
|
1858
|
+
packs.sort((a, b) => a.name.localeCompare(b.name));
|
|
1859
|
+
return { ok: true, packs };
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1515
1862
|
// Phase 31 follow-up — persist an image pasted straight from the clipboard into
|
|
1516
1863
|
// the ACP composer (a screenshot has no path yet), so the chip can point Claude
|
|
1517
1864
|
// at a real file to Read. Sibling of saveAsset, but:
|
|
@@ -1519,13 +1866,17 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
1519
1866
|
// the versioned `assets/`; pasted screenshots are ephemeral, not canvas media.
|
|
1520
1867
|
// • returns an ABSOLUTE path — Claude runs with its own cwd, so a project-
|
|
1521
1868
|
// relative string could miss; an absolute path always resolves.
|
|
1522
|
-
// Same load-bearing caps as saveAsset (magic-byte sniff → no SVG/script,
|
|
1523
|
-
// content-addressed name, shared session write budget).
|
|
1524
|
-
// route layer (the untrusted canvas can't reach it).
|
|
1869
|
+
// Same load-bearing caps as saveAsset (magic-byte sniff → no SVG/script,
|
|
1870
|
+
// ASSET_MAX_BYTES, content-addressed name, shared session write budget).
|
|
1871
|
+
// MAIN-ORIGIN ONLY at the route layer (the untrusted canvas can't reach it).
|
|
1525
1872
|
async function saveChatAttachment(bytes: Uint8Array): Promise<SaveAssetResult> {
|
|
1526
1873
|
if (!bytes || bytes.length === 0) return { ok: false, status: 400, error: 'empty body' };
|
|
1527
1874
|
if (bytes.length > ASSET_MAX_BYTES) {
|
|
1528
|
-
return {
|
|
1875
|
+
return {
|
|
1876
|
+
ok: false,
|
|
1877
|
+
status: 413,
|
|
1878
|
+
error: `attachment exceeds the ${Math.round(ASSET_MAX_BYTES / (1024 * 1024))} MB cap`,
|
|
1879
|
+
};
|
|
1529
1880
|
}
|
|
1530
1881
|
const kind = sniffImageType(bytes);
|
|
1531
1882
|
if (!kind) {
|
|
@@ -2009,6 +2360,8 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
2009
2360
|
canvas?: unknown;
|
|
2010
2361
|
id?: unknown;
|
|
2011
2362
|
text?: unknown;
|
|
2363
|
+
occurrence?: unknown;
|
|
2364
|
+
before?: unknown;
|
|
2012
2365
|
}): Promise<EditOpResult> {
|
|
2013
2366
|
const r = resolveCanvasAbs(input.canvas);
|
|
2014
2367
|
if (!r.ok) return r;
|
|
@@ -2017,7 +2370,23 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
2017
2370
|
if (typeof input.text !== 'string') return { ok: false, status: 400, error: 'text required' };
|
|
2018
2371
|
if (input.text.length > 5000) return { ok: false, status: 413, error: 'text too long' };
|
|
2019
2372
|
const text = input.text;
|
|
2020
|
-
|
|
2373
|
+
// Optional context for editing `{variable}` text (unified-text-editing
|
|
2374
|
+
// follow-up): which rendered instance, and its pre-edit text — both used
|
|
2375
|
+
// only to target the right source string; a bad/absent value just makes an
|
|
2376
|
+
// unresolvable expression edit fail loud (route to /design:edit).
|
|
2377
|
+
const occurrence =
|
|
2378
|
+
typeof input.occurrence === 'number' &&
|
|
2379
|
+
Number.isInteger(input.occurrence) &&
|
|
2380
|
+
input.occurrence >= 0
|
|
2381
|
+
? input.occurrence
|
|
2382
|
+
: undefined;
|
|
2383
|
+
const before =
|
|
2384
|
+
typeof input.before === 'string' && input.before.length <= 5000 ? input.before : undefined;
|
|
2385
|
+
return suppressedEdit(
|
|
2386
|
+
r.abs,
|
|
2387
|
+
() => runEditText(r.abs, id, text, { occurrence, before }),
|
|
2388
|
+
'edit failed'
|
|
2389
|
+
);
|
|
2021
2390
|
}
|
|
2022
2391
|
|
|
2023
2392
|
async function editAttr(input: {
|
|
@@ -2347,10 +2716,12 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
2347
2716
|
}
|
|
2348
2717
|
}
|
|
2349
2718
|
|
|
2350
|
-
/** Insert a synthesized element (div/text/image) relative to `refId
|
|
2719
|
+
/** Insert a synthesized element (div/text/image) relative to `refId`, or —
|
|
2720
|
+
* empty-artboard fallback — as a direct child of `artboardId`. */
|
|
2351
2721
|
async function insertElementOp(input: {
|
|
2352
2722
|
canvas?: unknown;
|
|
2353
2723
|
refId?: unknown;
|
|
2724
|
+
artboardId?: unknown;
|
|
2354
2725
|
position?: unknown;
|
|
2355
2726
|
kind?: unknown;
|
|
2356
2727
|
src?: unknown;
|
|
@@ -2361,14 +2732,30 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
2361
2732
|
const r = resolveCanvasAbs(input.canvas);
|
|
2362
2733
|
if (!r.ok) return r;
|
|
2363
2734
|
if (!takeStructuralToken()) return RATE_LIMITED;
|
|
2364
|
-
const
|
|
2365
|
-
|
|
2735
|
+
const hasRefId = typeof input.refId === 'string' && input.refId.trim() !== '';
|
|
2736
|
+
const hasArtboardId = typeof input.artboardId === 'string' && input.artboardId.trim() !== '';
|
|
2737
|
+
if (hasRefId === hasArtboardId) {
|
|
2738
|
+
return { ok: false, status: 400, error: 'provide exactly one of refId / artboardId' };
|
|
2739
|
+
}
|
|
2740
|
+
const refId = hasRefId ? (input.refId as string).trim() : '';
|
|
2741
|
+
if (hasRefId && !CD_ID_RE.test(refId)) {
|
|
2366
2742
|
return { ok: false, status: 400, error: 'invalid reference data-cd-id' };
|
|
2367
2743
|
}
|
|
2744
|
+
const artboardId = hasArtboardId ? (input.artboardId as string).trim() : '';
|
|
2745
|
+
if (hasArtboardId && !/^[A-Za-z][\w-]{0,63}$/.test(artboardId)) {
|
|
2746
|
+
return { ok: false, status: 400, error: 'invalid artboard id' };
|
|
2747
|
+
}
|
|
2368
2748
|
const position = input.position;
|
|
2369
2749
|
if (typeof position !== 'string' || !MOVE_POSITIONS.has(position)) {
|
|
2370
2750
|
return { ok: false, status: 400, error: 'invalid position' };
|
|
2371
2751
|
}
|
|
2752
|
+
if (hasArtboardId && position !== 'inside-start' && position !== 'inside-end') {
|
|
2753
|
+
return {
|
|
2754
|
+
ok: false,
|
|
2755
|
+
status: 400,
|
|
2756
|
+
error: 'artboardId insert requires inside-start/inside-end',
|
|
2757
|
+
};
|
|
2758
|
+
}
|
|
2372
2759
|
const kind = input.kind;
|
|
2373
2760
|
if (kind !== 'div' && kind !== 'text' && kind !== 'image') {
|
|
2374
2761
|
return { ok: false, status: 400, error: 'invalid kind (div|text|image)' };
|
|
@@ -2383,10 +2770,18 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
2383
2770
|
ctx.bus.emit('activity:unsuppress', rel);
|
|
2384
2771
|
return { ok: false, status: 413, error: 'canvas source too large to grow' };
|
|
2385
2772
|
}
|
|
2386
|
-
const res =
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2773
|
+
const res = hasArtboardId
|
|
2774
|
+
? await insertElementIntoArtboard(
|
|
2775
|
+
r.abs,
|
|
2776
|
+
artboardId,
|
|
2777
|
+
position as 'inside-start' | 'inside-end',
|
|
2778
|
+
kind as InsertKind,
|
|
2779
|
+
{ src }
|
|
2780
|
+
)
|
|
2781
|
+
: await insertElement(r.abs, refId, position as MovePosition, kind as InsertKind, {
|
|
2782
|
+
src,
|
|
2783
|
+
occurrence: refIndex,
|
|
2784
|
+
});
|
|
2390
2785
|
const after = await Bun.file(r.abs).text();
|
|
2391
2786
|
if (after === before) {
|
|
2392
2787
|
ctx.bus.emit('activity:unsuppress', rel);
|
|
@@ -2510,6 +2905,137 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
2510
2905
|
}
|
|
2511
2906
|
}
|
|
2512
2907
|
|
|
2908
|
+
/** Toggle an artboard's Hug/Fixed height sizing mode (CSS-panel control). */
|
|
2909
|
+
async function setArtboardHugOp(input: {
|
|
2910
|
+
canvas?: unknown;
|
|
2911
|
+
artboardId?: unknown;
|
|
2912
|
+
fixed?: unknown;
|
|
2913
|
+
freezeHeight?: unknown;
|
|
2914
|
+
}): Promise<{ ok: true; seq?: number } | { ok: false; status: number; error: string }> {
|
|
2915
|
+
const r = resolveCanvasAbs(input.canvas);
|
|
2916
|
+
if (!r.ok) return r;
|
|
2917
|
+
if (!takeStructuralToken()) return RATE_LIMITED;
|
|
2918
|
+
const artboardId = typeof input.artboardId === 'string' ? input.artboardId.trim() : '';
|
|
2919
|
+
if (!/^[A-Za-z][\w-]{0,63}$/.test(artboardId)) {
|
|
2920
|
+
return { ok: false, status: 400, error: 'invalid artboard id' };
|
|
2921
|
+
}
|
|
2922
|
+
if (typeof input.fixed !== 'boolean') {
|
|
2923
|
+
return { ok: false, status: 400, error: 'fixed (boolean) required' };
|
|
2924
|
+
}
|
|
2925
|
+
const fixed = input.fixed;
|
|
2926
|
+
const freezeHeight = Number.isFinite(Number(input.freezeHeight))
|
|
2927
|
+
? Math.max(64, Math.min(8192, Math.round(Number(input.freezeHeight))))
|
|
2928
|
+
: undefined;
|
|
2929
|
+
const rel = path.relative(paths.designRoot, r.abs);
|
|
2930
|
+
ctx.bus.emit('activity:suppress', rel);
|
|
2931
|
+
try {
|
|
2932
|
+
const before = await Bun.file(r.abs).text();
|
|
2933
|
+
await setArtboardHug(r.abs, artboardId, fixed, freezeHeight);
|
|
2934
|
+
const after = await Bun.file(r.abs).text();
|
|
2935
|
+
if (after === before) {
|
|
2936
|
+
ctx.bus.emit('activity:unsuppress', rel);
|
|
2937
|
+
return { ok: true };
|
|
2938
|
+
}
|
|
2939
|
+
try {
|
|
2940
|
+
await history.writeSnapshot(rel, before, 'pre-set-artboard-hug');
|
|
2941
|
+
} catch {
|
|
2942
|
+
/* snapshot best-effort */
|
|
2943
|
+
}
|
|
2944
|
+
return { ok: true, seq: logUndo(r.abs, before, after) };
|
|
2945
|
+
} catch (err) {
|
|
2946
|
+
ctx.bus.emit('activity:unsuppress', rel);
|
|
2947
|
+
return {
|
|
2948
|
+
ok: false,
|
|
2949
|
+
status: err instanceof CanvasEditError ? 422 : 500,
|
|
2950
|
+
error: err instanceof Error ? err.message : 'set-artboard-hug failed',
|
|
2951
|
+
};
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
|
|
2955
|
+
const ARTBOARD_LAYOUT_VALUES = new Set(['block', 'flex-col', 'flex-row', 'grid']);
|
|
2956
|
+
|
|
2957
|
+
/** Set artboard "more settings" — background / padding / layout / gap. */
|
|
2958
|
+
async function setArtboardStyleOp(input: {
|
|
2959
|
+
canvas?: unknown;
|
|
2960
|
+
artboardId?: unknown;
|
|
2961
|
+
background?: unknown;
|
|
2962
|
+
padding?: unknown;
|
|
2963
|
+
layout?: unknown;
|
|
2964
|
+
gap?: unknown;
|
|
2965
|
+
}): Promise<{ ok: true; seq?: number } | { ok: false; status: number; error: string }> {
|
|
2966
|
+
const r = resolveCanvasAbs(input.canvas);
|
|
2967
|
+
if (!r.ok) return r;
|
|
2968
|
+
if (!takeStructuralToken()) return RATE_LIMITED;
|
|
2969
|
+
const artboardId = typeof input.artboardId === 'string' ? input.artboardId.trim() : '';
|
|
2970
|
+
if (!/^[A-Za-z][\w-]{0,63}$/.test(artboardId)) {
|
|
2971
|
+
return { ok: false, status: 400, error: 'invalid artboard id' };
|
|
2972
|
+
}
|
|
2973
|
+
const patch: {
|
|
2974
|
+
background?: string | null;
|
|
2975
|
+
padding?: number | null;
|
|
2976
|
+
layout?: string | null;
|
|
2977
|
+
gap?: number | null;
|
|
2978
|
+
} = {};
|
|
2979
|
+
if ('background' in input) {
|
|
2980
|
+
if (input.background === null) patch.background = null;
|
|
2981
|
+
else if (typeof input.background === 'string' && input.background.length <= 256) {
|
|
2982
|
+
patch.background = input.background;
|
|
2983
|
+
} else {
|
|
2984
|
+
return { ok: false, status: 400, error: 'invalid background' };
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2987
|
+
if ('layout' in input) {
|
|
2988
|
+
if (input.layout === null) patch.layout = null;
|
|
2989
|
+
else if (typeof input.layout === 'string' && ARTBOARD_LAYOUT_VALUES.has(input.layout)) {
|
|
2990
|
+
patch.layout = input.layout;
|
|
2991
|
+
} else {
|
|
2992
|
+
return { ok: false, status: 400, error: 'invalid layout' };
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
const clampBoxDim = (v: unknown): number | null | undefined => {
|
|
2996
|
+
if (v === null) return null;
|
|
2997
|
+
if (v === undefined) return undefined;
|
|
2998
|
+
return Number.isFinite(Number(v)) ? Math.max(0, Math.min(512, Math.round(Number(v)))) : NaN;
|
|
2999
|
+
};
|
|
3000
|
+
if ('padding' in input) {
|
|
3001
|
+
const p = clampBoxDim(input.padding);
|
|
3002
|
+
if (Number.isNaN(p)) return { ok: false, status: 400, error: 'invalid padding' };
|
|
3003
|
+
patch.padding = p;
|
|
3004
|
+
}
|
|
3005
|
+
if ('gap' in input) {
|
|
3006
|
+
const g = clampBoxDim(input.gap);
|
|
3007
|
+
if (Number.isNaN(g)) return { ok: false, status: 400, error: 'invalid gap' };
|
|
3008
|
+
patch.gap = g;
|
|
3009
|
+
}
|
|
3010
|
+
if (Object.keys(patch).length === 0) {
|
|
3011
|
+
return { ok: false, status: 400, error: 'no style props given' };
|
|
3012
|
+
}
|
|
3013
|
+
const rel = path.relative(paths.designRoot, r.abs);
|
|
3014
|
+
ctx.bus.emit('activity:suppress', rel);
|
|
3015
|
+
try {
|
|
3016
|
+
const before = await Bun.file(r.abs).text();
|
|
3017
|
+
await setArtboardStyle(r.abs, artboardId, patch);
|
|
3018
|
+
const after = await Bun.file(r.abs).text();
|
|
3019
|
+
if (after === before) {
|
|
3020
|
+
ctx.bus.emit('activity:unsuppress', rel);
|
|
3021
|
+
return { ok: true };
|
|
3022
|
+
}
|
|
3023
|
+
try {
|
|
3024
|
+
await history.writeSnapshot(rel, before, 'pre-set-artboard-style');
|
|
3025
|
+
} catch {
|
|
3026
|
+
/* snapshot best-effort */
|
|
3027
|
+
}
|
|
3028
|
+
return { ok: true, seq: logUndo(r.abs, before, after) };
|
|
3029
|
+
} catch (err) {
|
|
3030
|
+
ctx.bus.emit('activity:unsuppress', rel);
|
|
3031
|
+
return {
|
|
3032
|
+
ok: false,
|
|
3033
|
+
status: err instanceof CanvasEditError ? 422 : 500,
|
|
3034
|
+
error: err instanceof Error ? err.message : 'set-artboard-style failed',
|
|
3035
|
+
};
|
|
3036
|
+
}
|
|
3037
|
+
}
|
|
3038
|
+
|
|
2513
3039
|
/** Duplicate an element (Cmd+D) — insert a copy as the next sibling. */
|
|
2514
3040
|
async function duplicateElementOp(input: {
|
|
2515
3041
|
canvas?: unknown;
|
|
@@ -2853,6 +3379,14 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
2853
3379
|
// instead of unconditionally designSystems[0]. Populated from the canvas
|
|
2854
3380
|
// groups below; returned on the payload and folded into the client cfg.
|
|
2855
3381
|
const canvasDesignSystems: Record<string, string> = {};
|
|
3382
|
+
// DDR-174 (T15) — surface a canvas's `.meta.json` `kind` in the file tree
|
|
3383
|
+
// when it's a value the user should notice at a glance (today: only
|
|
3384
|
+
// `reconstructed-experimental`, so this map stays empty on every project
|
|
3385
|
+
// that hasn't run `/design:import --reconstruct`). Piggybacks on the same
|
|
3386
|
+
// `loadCanvasMeta` call the DDR-093 DS-map loop below already makes for
|
|
3387
|
+
// non-path-owned (`ui/`) canvases — no extra I/O for the common case.
|
|
3388
|
+
const canvasKinds: Record<string, string> = {};
|
|
3389
|
+
const NOTABLE_KINDS = new Set(['reconstructed-experimental']);
|
|
2856
3390
|
const defaultDs = cfg.defaultDesignSystem || cfg.designSystems?.[0]?.name || 'project';
|
|
2857
3391
|
// A file under `system/<folder>/` belongs to the DS that owns that folder —
|
|
2858
3392
|
// path-authoritative, because specimens/ui_kits rarely carry a sidecar
|
|
@@ -2926,6 +3460,8 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
2926
3460
|
const meta = await loadCanvasMeta(fp);
|
|
2927
3461
|
const declared = meta?.designSystem;
|
|
2928
3462
|
dsName = typeof declared === 'string' && declared.trim() ? declared : defaultDs;
|
|
3463
|
+
const kind = meta?.kind;
|
|
3464
|
+
if (typeof kind === 'string' && NOTABLE_KINDS.has(kind)) canvasKinds[fp] = kind;
|
|
2929
3465
|
}
|
|
2930
3466
|
canvasDesignSystems[fp] = dsName;
|
|
2931
3467
|
}
|
|
@@ -2990,6 +3526,7 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
2990
3526
|
designRoot: paths.designRel,
|
|
2991
3527
|
groups,
|
|
2992
3528
|
canvasDesignSystems,
|
|
3529
|
+
canvasKinds,
|
|
2993
3530
|
};
|
|
2994
3531
|
}
|
|
2995
3532
|
|
|
@@ -3208,6 +3745,11 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
3208
3745
|
saveAnnotations,
|
|
3209
3746
|
saveAsset,
|
|
3210
3747
|
listAssets,
|
|
3748
|
+
readAssetBytes,
|
|
3749
|
+
writeCaptionSidecar,
|
|
3750
|
+
writeAudioIntent,
|
|
3751
|
+
searchAudioLibrary,
|
|
3752
|
+
listStickers,
|
|
3211
3753
|
saveAssetFromStream,
|
|
3212
3754
|
saveChatAttachment,
|
|
3213
3755
|
resolveChatAttachment,
|
|
@@ -3228,6 +3770,8 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
3228
3770
|
insertElementOp,
|
|
3229
3771
|
insertArtboardOp,
|
|
3230
3772
|
resizeArtboardOp,
|
|
3773
|
+
setArtboardHugOp,
|
|
3774
|
+
setArtboardStyleOp,
|
|
3231
3775
|
deleteArtboardOp,
|
|
3232
3776
|
duplicateElementOp,
|
|
3233
3777
|
editScopeOp,
|