From 7d0d903417c6922ca989e92afea426b96f47f1a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Sat, 9 May 2026 07:21:06 +0000 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20alpha=20preview=20e2e=20fixes=20?= =?UTF-8?q?=E2=80=94=20exports,=20init=20templates,=20EPIPE=20crash?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three bugs found via automated e2e testing of the v0.6.0-alpha preview: 1. core: add missing package.json export specifiers for studio-api/manual-edits-render-script and studio-api/studio-motion-render-script — the alpha.3 npm publish failed because the studio build could not resolve these sub-paths. 2. cli: fix init --example creating empty projects — tsup leaves empty template directories in dist/ during the build, causing existsSync(templateDir) to return true and skip the remote fetch fallback. Now checks for index.html inside the dir instead. 3. engine: fix unhandled EPIPE crash in streaming encoder — ffmpeg stdin/stdout had no error handlers, so a write after the ffmpeg process exits throws an uncaught error that crashes the process. Verified with 8 consecutive e2e iterations (424 test runs, 0 flaky). Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/cli/src/commands/init.ts | 6 ++++-- packages/core/package.json | 16 ++++++++++++++++ packages/engine/src/services/streamingEncoder.ts | 3 +++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index 443368619..196820aee 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -510,9 +510,11 @@ async function scaffoldProject( ): Promise { mkdirSync(destDir, { recursive: true }); - // Use bundled template if available, otherwise fetch from GitHub + // Use bundled template if available, otherwise fetch from GitHub. + // Check for index.html inside the dir — an empty directory left by the + // build toolchain should not prevent the remote fetch fallback. const templateDir = getStaticTemplateDir(templateId); - if (existsSync(templateDir)) { + if (existsSync(join(templateDir, "index.html"))) { cpSync(templateDir, destDir, { recursive: true }); } else { await fetchRemoteTemplate(templateId, destDir); diff --git a/packages/core/package.json b/packages/core/package.json index 5c5339647..e7b4f59c5 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -42,6 +42,14 @@ "import": "./src/studio-api/helpers/screenshotClip.ts", "types": "./src/studio-api/helpers/screenshotClip.ts" }, + "./studio-api/manual-edits-render-script": { + "import": "./src/studio-api/helpers/manualEditsRenderScript.ts", + "types": "./src/studio-api/helpers/manualEditsRenderScript.ts" + }, + "./studio-api/studio-motion-render-script": { + "import": "./src/studio-api/helpers/studioMotionRenderScript.ts", + "types": "./src/studio-api/helpers/studioMotionRenderScript.ts" + }, "./text": { "import": "./src/text/index.ts", "types": "./src/text/index.ts" @@ -81,6 +89,14 @@ "import": "./dist/studio-api/helpers/screenshotClip.js", "types": "./dist/studio-api/helpers/screenshotClip.d.ts" }, + "./studio-api/manual-edits-render-script": { + "import": "./dist/studio-api/helpers/manualEditsRenderScript.js", + "types": "./dist/studio-api/helpers/manualEditsRenderScript.d.ts" + }, + "./studio-api/studio-motion-render-script": { + "import": "./dist/studio-api/helpers/studioMotionRenderScript.js", + "types": "./dist/studio-api/helpers/studioMotionRenderScript.d.ts" + }, "./text": { "import": "./dist/text/index.js", "types": "./dist/text/index.d.ts" diff --git a/packages/engine/src/services/streamingEncoder.ts b/packages/engine/src/services/streamingEncoder.ts index 03c0d0735..740a8afd2 100644 --- a/packages/engine/src/services/streamingEncoder.ts +++ b/packages/engine/src/services/streamingEncoder.ts @@ -389,6 +389,9 @@ export async function spawnStreamingEncoder( exitPromiseResolve?.(); }); + ffmpeg.stdin?.on("error", () => {}); + ffmpeg.stdout?.on("error", () => {}); + // Handle abort signal const onAbort = () => { if (exitStatus === "running") { From 1e0ca35fd6c3d5d07191a5c1b121d8c512900aac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Sat, 9 May 2026 18:08:45 +0000 Subject: [PATCH 2/2] fix(studio): thumbnail crash, feature defaults, multi-select UX, fps selector Power-user audit fixes for the alpha studio: - vite.config.ts: wrap thumbnail generation in try/catch so Puppeteer TimeoutError doesn't crash the entire vite dev server as an uncaught rejection. Close the page on error to prevent browser session leaks. - manualEditingAvailability.ts: enable motion panel and manual canvas drag editing by default (were both false, undiscoverable without knowing the env vars). - PropertyPanel.tsx: show "N elements selected" feedback when multiple elements are selected instead of the generic "Select an element" empty state. - RenderQueue.tsx + App.tsx: add FPS selector (24/30/60) to the render export bar instead of hardcoding 30fps. Pass the user's choice through to startRender. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/studio/src/App.tsx | 5 +- .../src/components/editor/PropertyPanel.tsx | 31 +++- .../editor/manualEditingAvailability.ts | 4 +- .../src/components/renders/RenderQueue.tsx | 15 +- packages/studio/vite.config.ts | 148 ++++++++++-------- 5 files changed, 125 insertions(+), 78 deletions(-) diff --git a/packages/studio/src/App.tsx b/packages/studio/src/App.tsx index 5f734be5e..e11c55230 100644 --- a/packages/studio/src/App.tsx +++ b/packages/studio/src/App.tsx @@ -4106,6 +4106,7 @@ export function StudioApp() { projectId={projectId} assets={assets} element={domEditGroupSelections.length > 1 ? null : domEditSelection} + multiSelectCount={domEditGroupSelections.length} copiedAgentPrompt={copiedAgentPrompt} onClearSelection={clearDomSelection} onSetStyle={handleDomStyleCommit} @@ -4135,9 +4136,9 @@ export function StudioApp() { projectId={projectId} onDelete={renderQueue.deleteRender} onClearCompleted={renderQueue.clearCompleted} - onStartRender={async (format, quality, resolution) => { + onStartRender={async (format, quality, resolution, fps) => { await waitForPendingDomEditSaves(); - await renderQueue.startRender({ fps: 30, quality, format, resolution }); + await renderQueue.startRender({ fps, quality, format, resolution }); }} isRendering={renderQueue.isRendering} /> diff --git a/packages/studio/src/components/editor/PropertyPanel.tsx b/packages/studio/src/components/editor/PropertyPanel.tsx index d46468651..4f06bbf42 100644 --- a/packages/studio/src/components/editor/PropertyPanel.tsx +++ b/packages/studio/src/components/editor/PropertyPanel.tsx @@ -54,6 +54,7 @@ interface PropertyPanelProps { projectId: string; assets: string[]; element: DomEditSelection | null; + multiSelectCount?: number; copiedAgentPrompt: boolean; onClearSelection: () => void; onSetStyle: (prop: string, value: string) => void | Promise; @@ -2212,6 +2213,7 @@ export const PropertyPanel = memo(function PropertyPanel({ projectId, assets, element, + multiSelectCount = 0, copiedAgentPrompt, onClearSelection, onSetStyle, @@ -2258,12 +2260,29 @@ export const PropertyPanel = memo(function PropertyPanel({ if (!element) { return (
- -

Select an element in the preview.

-

- The inspector is tuned for element edits with safer geometry controls, color picking, and - cleaner grouped layer controls. -

+ {multiSelectCount > 1 ? ( + <> + +

+ {multiSelectCount} elements selected +

+

+ Select a single element to edit its properties. Click an element in the preview or use + the timeline layer panel. +

+ + ) : ( + <> + +

+ Select an element in the preview. +

+

+ The inspector is tuned for element edits with safer geometry controls, color picking, + and cleaner grouped layer controls. +

+ + )}
); } diff --git a/packages/studio/src/components/editor/manualEditingAvailability.ts b/packages/studio/src/components/editor/manualEditingAvailability.ts index 057a47a7b..2a5c7cb74 100644 --- a/packages/studio/src/components/editor/manualEditingAvailability.ts +++ b/packages/studio/src/components/editor/manualEditingAvailability.ts @@ -32,7 +32,7 @@ const env = import.meta.env as StudioFeatureFlagEnv; export const STUDIO_PREVIEW_MANUAL_EDITING_ENABLED = resolveStudioBooleanEnvFlag( env, [STUDIO_PREVIEW_MANUAL_DRAGGING_ENV, "VITE_STUDIO_PREVIEW_MANUAL_EDITING_ENABLED"], - false, + true, ); export const STUDIO_INSPECTOR_PANELS_ENABLED = resolveStudioBooleanEnvFlag( @@ -44,7 +44,7 @@ export const STUDIO_INSPECTOR_PANELS_ENABLED = resolveStudioBooleanEnvFlag( export const STUDIO_MOTION_PANEL_ENABLED = resolveStudioBooleanEnvFlag( env, [STUDIO_MOTION_PANEL_ENV, "VITE_STUDIO_MOTION_PANEL_ENABLED"], - false, + true, ); export const STUDIO_TIMELINE_LAYER_INSPECTOR_ENABLED = diff --git a/packages/studio/src/components/renders/RenderQueue.tsx b/packages/studio/src/components/renders/RenderQueue.tsx index 297a9bbfe..e0a51dc8a 100644 --- a/packages/studio/src/components/renders/RenderQueue.tsx +++ b/packages/studio/src/components/renders/RenderQueue.tsx @@ -6,6 +6,7 @@ type StartRenderHandler = ( format: "mp4" | "webm" | "mov", quality: "draft" | "standard" | "high", resolution: ResolutionPreset | "auto", + fps: 24 | 30 | 60, ) => void | Promise; interface RenderQueueProps { @@ -133,6 +134,7 @@ function FormatExportButton({ const [format, setFormat] = useState<"mp4" | "webm" | "mov">("mp4"); const [quality, setQuality] = useState<"draft" | "standard" | "high">("standard"); const [resolution, setResolution] = useState("auto"); + const [fps, setFps] = useState<24 | 30 | 60>(30); // MOV (ProRes) is a fixed-quality codec — quality selector has no effect. const showQuality = format !== "mov"; @@ -172,6 +174,17 @@ function FormatExportButton({ ))} )} +