From 3e9794a23a8a5683f7d1eaa0c531c91ac47131a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Thu, 21 May 2026 18:46:59 -0400 Subject: [PATCH 01/19] feat(studio): add dual-action buttons to catalog cards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each catalog card now shows two hover buttons: - "Add" — inserts the block/component into the composition at the current playhead position (existing behavior, now with + icon) - "Agent prompt" — copies a contextual prompt to clipboard tailored to the block's category (captions, transitions, VFX, etc.) so the user can paste it into their AI agent for guided customization --- .../src/components/sidebar/BlocksTab.tsx | 100 ++++++++++++++++-- 1 file changed, 92 insertions(+), 8 deletions(-) diff --git a/packages/studio/src/components/sidebar/BlocksTab.tsx b/packages/studio/src/components/sidebar/BlocksTab.tsx index 51e4c7622..14cce1eff 100644 --- a/packages/studio/src/components/sidebar/BlocksTab.tsx +++ b/packages/studio/src/components/sidebar/BlocksTab.tsx @@ -113,6 +113,8 @@ export const BlocksTab = memo(function BlocksTab({ onAddBlock, onPreviewBlock }: key={block.name} name={block.name} title={block.title} + description={block.description} + blockType={block.type} duration={dur} category={block.category} tags={block.tags} @@ -160,9 +162,47 @@ function CategoryPill({ ); } +function buildAgentPrompt( + title: string, + name: string, + description: string, + category: BlockCategory, + blockType: string, +): string { + const isComponent = blockType === "hyperframes:component"; + const kind = isComponent ? "component" : "block"; + + const categoryHints: Record = { + captions: + "This is a caption style. Add it to the composition, then customize the transcript text, fonts, colors, and timing to match the voiceover or audio.", + vfx: "This is a VFX effect. Add it as an overlay and adjust shader parameters, colors, and intensity to match the scene.", + transitions: + "This is a transition. Place it between two scenes on the timeline and adjust the duration and direction.", + effects: + "This is a visual effect overlay. Layer it on top of existing content and tweak colors, opacity, and animation timing.", + social: + "This is a social media template. Customize the text, handle, avatar, and metrics to match the content.", + data: "This is a data visualization. Update the data values, labels, colors, and animation stagger to tell the right story.", + scenes: + "This is a full scene. Customize the text, images, layout, and animation timing to fit the narrative.", + }; + + const hint = categoryHints[category] ?? `Customize this ${kind} to fit the composition.`; + + return [ + `Add the "${title}" ${kind} (registry: ${name}) to my composition using /hyperframes.`, + "", + `${description}`, + "", + hint, + ].join("\n"); +} + function BlockCard({ name, title, + description, + blockType, duration, category, tags, @@ -174,6 +214,8 @@ function BlockCard({ }: { name: string; title: string; + description: string; + blockType: string; duration?: number; category: BlockCategory; tags?: string[]; @@ -185,6 +227,7 @@ function BlockCard({ }) { const [hovered, setHovered] = useState(false); const [adding, setAdding] = useState(false); + const [copied, setCopied] = useState(false); const hoverTimer = useRef | null>(null); const colors = getCategoryColors(category); const needsWebGL = tags?.includes("html-in-canvas") || tags?.includes("webgl"); @@ -222,6 +265,17 @@ function BlockCard({ [onAdd, adding], ); + const handleCopyPrompt = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + const prompt = buildAgentPrompt(title, name, description, category, blockType); + navigator.clipboard.writeText(prompt); + setCopied(true); + setTimeout(() => setCopied(false), 1500); + }, + [title, name, description, category, blockType], + ); + const handleDragStart = useCallback( (e: React.DragEvent) => { e.dataTransfer.effectAllowed = "copy"; @@ -267,14 +321,44 @@ function BlockCard({ )} - {/* Add button overlay */} - + {/* Action buttons overlay */} +
+ + +
{/* Badges */}
From e316aa440977630461df8bb9db12895e3a8a3701 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Thu, 21 May 2026 18:52:11 -0400 Subject: [PATCH 02/19] feat(studio): replace Add with Ask agent in catalog cards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the Add button and drag-and-drop from catalog cards — blocks and components need agent-guided customization, not blind insertion. Each card now shows a single "Ask agent" button that copies a rich, category-specific prompt to clipboard with context about what the block does and how to customize it (captions: transcribe + style, transitions: place at cut point, data: replace values, etc.). --- .../src/components/sidebar/BlocksTab.tsx | 161 +++++++----------- .../src/components/sidebar/LeftSidebar.tsx | 5 +- 2 files changed, 68 insertions(+), 98 deletions(-) diff --git a/packages/studio/src/components/sidebar/BlocksTab.tsx b/packages/studio/src/components/sidebar/BlocksTab.tsx index 14cce1eff..f4474b3e7 100644 --- a/packages/studio/src/components/sidebar/BlocksTab.tsx +++ b/packages/studio/src/components/sidebar/BlocksTab.tsx @@ -5,8 +5,6 @@ import { getCategoryColors, type BlockCategory, } from "../../utils/blockCategories"; -import { TIMELINE_BLOCK_MIME } from "../../utils/timelineAssetDrop"; - export interface BlockPreviewInfo { videoUrl?: string; posterUrl?: string; @@ -14,12 +12,11 @@ export interface BlockPreviewInfo { } interface BlocksTabProps { - onAddBlock: (blockName: string) => void; onPreviewBlock?: (preview: BlockPreviewInfo | null) => void; } // fallow-ignore-next-line complexity -export const BlocksTab = memo(function BlocksTab({ onAddBlock, onPreviewBlock }: BlocksTabProps) { +export const BlocksTab = memo(function BlocksTab({ onPreviewBlock }: BlocksTabProps) { const { loading, error, search, setSearch, category, setCategory, filteredBlocks } = useBlockCatalog(); @@ -104,10 +101,6 @@ export const BlocksTab = memo(function BlocksTab({ onAddBlock, onPreviewBlock }: > {filteredBlocks.map((block) => { const dur = "duration" in block ? (block.duration as number) : undefined; - const dims = - "dimensions" in block - ? (block.dimensions as { width: number; height: number }) - : undefined; return ( onAddBlock(block.name)} onPreview={onPreviewBlock} /> ); @@ -172,30 +163,52 @@ function buildAgentPrompt( const isComponent = blockType === "hyperframes:component"; const kind = isComponent ? "component" : "block"; - const categoryHints: Record = { - captions: - "This is a caption style. Add it to the composition, then customize the transcript text, fonts, colors, and timing to match the voiceover or audio.", - vfx: "This is a VFX effect. Add it as an overlay and adjust shader parameters, colors, and intensity to match the scene.", - transitions: - "This is a transition. Place it between two scenes on the timeline and adjust the duration and direction.", - effects: - "This is a visual effect overlay. Layer it on top of existing content and tweak colors, opacity, and animation timing.", - social: - "This is a social media template. Customize the text, handle, avatar, and metrics to match the content.", - data: "This is a data visualization. Update the data values, labels, colors, and animation stagger to tell the right story.", - scenes: - "This is a full scene. Customize the text, images, layout, and animation timing to fit the narrative.", + const categoryPrompts: Record = { + captions: [ + `Using /hyperframes, add the "${title}" caption style (registry: ${name}) to my composition.`, + `${description}`, + `Transcribe the audio with /hyperframes-media, then wire the transcript into this caption component. Match the font colors and animation timing to my composition's design tokens. Place it as an overlay above the main content with the highest z-index.`, + ].join("\n\n"), + vfx: [ + `Using /hyperframes, add the "${title}" VFX (registry: ${name}) as a full-screen overlay on my composition.`, + `${description}`, + `This is a WebGL effect that requires chrome://flags/#html-in-canvas. Layer it on top of all content, adjust the shader uniforms and color palette to complement my scene, and set the duration to match the composition length.`, + ].join("\n\n"), + transitions: [ + `Using /hyperframes, add the "${title}" transition (registry: ${name}) between my scenes.`, + `${description}`, + `Place this transition at the cut point between the current scene and the next. Set the duration to 0.5–1s, position it at the scene boundary on the timeline, and make sure the z-index is above both scenes. Adjust colors to match my palette.`, + ].join("\n\n"), + effects: [ + `Using /hyperframes, add the "${title}" effect (registry: ${name}) as an overlay on my composition.`, + `${description}`, + `Layer this on top of the current content. Adjust the opacity, colors, and animation timing to enhance the scene without overwhelming the main content.`, + ].join("\n\n"), + social: [ + `Using /hyperframes, add the "${title}" template (registry: ${name}) to my composition.`, + `${description}`, + `Replace the placeholder text, handle, and avatar with my actual content. Match the typography and colors to my brand. Adjust timing so the elements animate in sync with the voiceover.`, + ].join("\n\n"), + data: [ + `Using /hyperframes, add the "${title}" visualization (registry: ${name}) to my composition.`, + `${description}`, + `Replace the placeholder data with my actual values and labels. Adjust the color scale, animation stagger timing, and typography to match my composition's design system. Size it to fit the current viewport.`, + ].join("\n\n"), + scenes: [ + `Using /hyperframes, add the "${title}" scene (registry: ${name}) to my composition.`, + `${description}`, + `Replace all placeholder text, images, and content with my actual material. Match fonts, colors, and layout to my existing design tokens. Set the timeline position and duration to fit the narrative flow.`, + ].join("\n\n"), }; - const hint = categoryHints[category] ?? `Customize this ${kind} to fit the composition.`; - - return [ - `Add the "${title}" ${kind} (registry: ${name}) to my composition using /hyperframes.`, - "", - `${description}`, - "", - hint, - ].join("\n"); + return ( + categoryPrompts[category] ?? + [ + `Using /hyperframes, add the "${title}" ${kind} (registry: ${name}) to my composition.`, + `${description}`, + `Customize it to match my composition's design and timeline.`, + ].join("\n\n") + ); } function BlockCard({ @@ -208,8 +221,6 @@ function BlockCard({ tags, posterUrl, videoUrl, - dimensions, - onAdd, onPreview, }: { name: string; @@ -221,12 +232,9 @@ function BlockCard({ tags?: string[]; posterUrl?: string; videoUrl?: string; - dimensions?: { width: number; height: number }; - onAdd: () => void; onPreview?: (preview: BlockPreviewInfo | null) => void; }) { const [hovered, setHovered] = useState(false); - const [adding, setAdding] = useState(false); const [copied, setCopied] = useState(false); const hoverTimer = useRef | null>(null); const colors = getCategoryColors(category); @@ -254,17 +262,6 @@ function BlockCard({ }; }, []); - const handleAdd = useCallback( - (e: React.MouseEvent) => { - e.stopPropagation(); - if (adding) return; - setAdding(true); - onAdd(); - setTimeout(() => setAdding(false), 1000); - }, - [onAdd, adding], - ); - const handleCopyPrompt = useCallback( (e: React.MouseEvent) => { e.stopPropagation(); @@ -276,21 +273,11 @@ function BlockCard({ [title, name, description, category, blockType], ); - const handleDragStart = useCallback( - (e: React.DragEvent) => { - e.dataTransfer.effectAllowed = "copy"; - e.dataTransfer.setData(TIMELINE_BLOCK_MIME, JSON.stringify({ name, duration, dimensions })); - }, - [name, duration, dimensions], - ); - return (
{/* Thumbnail */}
@@ -321,44 +308,28 @@ function BlockCard({
)} - {/* Action buttons overlay */} -
- - -
+ + + + + {copied ? "Copied!" : "Ask agent"} + + {/* Badges */}
diff --git a/packages/studio/src/components/sidebar/LeftSidebar.tsx b/packages/studio/src/components/sidebar/LeftSidebar.tsx index 0bbb73a8f..634fe6e1f 100644 --- a/packages/studio/src/components/sidebar/LeftSidebar.tsx +++ b/packages/studio/src/components/sidebar/LeftSidebar.tsx @@ -84,7 +84,6 @@ export const LeftSidebar = memo( onLint, linting, onToggleCollapse, - onAddBlock, onPreviewBlock, takeoverContent, }, @@ -246,8 +245,8 @@ export const LeftSidebar = memo(
)} - {STUDIO_BLOCKS_PANEL_ENABLED && tab === "blocks" && onAddBlock && ( - + {STUDIO_BLOCKS_PANEL_ENABLED && tab === "blocks" && ( + )} {/* Lint button pinned at the bottom */} From 5c79cd0a8ed2f30c699320df1b4b4a2192c60018 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Thu, 21 May 2026 18:54:36 -0400 Subject: [PATCH 03/19] fix(studio): rewrite block dimensions to match host project on install Registry blocks are authored at 1920x1080 but projects may use different dimensions (e.g. 1280x720). After installing a block, the server now reads the host project's data-width/data-height from index.html and rewrites the block's viewport meta and CSS dimensions to match, preventing overflow. --- packages/cli/src/server/studioServer.ts | 30 +++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/packages/cli/src/server/studioServer.ts b/packages/cli/src/server/studioServer.ts index d7165e2c7..690db51ac 100644 --- a/packages/cli/src/server/studioServer.ts +++ b/packages/cli/src/server/studioServer.ts @@ -398,8 +398,38 @@ export function createStudioServer(options: StudioServerOptions): StudioServer { async installRegistryBlock(opts) { const { resolveItem } = await import("../registry/resolver.js"); const { installItem } = await import("../registry/installer.js"); + const { readFileSync, writeFileSync, existsSync } = await import("node:fs"); + const { join } = await import("node:path"); const item = await resolveItem(opts.blockName); const { written } = await installItem(item, { destDir: opts.project.dir }); + + const indexPath = join(opts.project.dir, "index.html"); + if (existsSync(indexPath)) { + const indexHtml = readFileSync(indexPath, "utf-8"); + const hostW = indexHtml.match(/data-width="(\d+)"/)?.[1]; + const hostH = indexHtml.match(/data-height="(\d+)"/)?.[1]; + if (hostW && hostH) { + for (const absPath of written) { + if (!absPath.endsWith(".html")) continue; + let content = readFileSync(absPath, "utf-8"); + content = content.replace( + /( { + if (match.includes("1920") || match.includes("1080")) { + return `${pre}${hostW}${mid}${hostH}${post}`; + } + return match; + }, + ); + writeFileSync(absPath, content, "utf-8"); + } + } + } + const relativePaths = written.map((abs) => { const rel = abs.startsWith(opts.project.dir) ? abs.slice(opts.project.dir.length + 1) : abs; return rel; From 2614071618abd8101565482cc5851d49e062d9c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Thu, 21 May 2026 19:01:20 -0400 Subject: [PATCH 04/19] feat(studio): show main composition behind component sub-composition previews When previewing a component (compositions/components/*), render the main composition player as a backdrop behind the transparent component overlay. This lets users see captions, vignettes, and other overlays in context instead of against a black void. --- packages/studio/src/components/nle/NLEPreview.tsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/studio/src/components/nle/NLEPreview.tsx b/packages/studio/src/components/nle/NLEPreview.tsx index 7f2d7e543..8ceb1b729 100644 --- a/packages/studio/src/components/nle/NLEPreview.tsx +++ b/packages/studio/src/components/nle/NLEPreview.tsx @@ -391,6 +391,16 @@ export const NLEPreview = memo(function NLEPreview({ }} data-testid="preview-zoom-stage" > + {directUrl?.includes("/components/") && ( + {}} + portrait={portrait} + suppressLoadingOverlay + style={{ position: "absolute", inset: 0, zIndex: 0 }} + /> + )}
From fb48b8d9b4a993e10cfce0b09ee0c9da87a97056 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Thu, 21 May 2026 19:23:08 -0400 Subject: [PATCH 05/19] feat(studio): include composition context in catalog agent prompts The "Ask agent" button now copies the current composition state along with the category-specific prompt: playback time, active composition path, dimensions, and all elements visible at the current time with their track, timing, and source paths. Gives the agent full context to place and customize the block correctly. --- .../src/components/sidebar/BlocksTab.tsx | 74 +++++++++++++++++-- 1 file changed, 69 insertions(+), 5 deletions(-) diff --git a/packages/studio/src/components/sidebar/BlocksTab.tsx b/packages/studio/src/components/sidebar/BlocksTab.tsx index f4474b3e7..d85d465cf 100644 --- a/packages/studio/src/components/sidebar/BlocksTab.tsx +++ b/packages/studio/src/components/sidebar/BlocksTab.tsx @@ -5,6 +5,9 @@ import { getCategoryColors, type BlockCategory, } from "../../utils/blockCategories"; +import { usePlayerStore } from "../../player"; +import { formatTime } from "../../player/lib/time"; +import { useStudioContext } from "../../contexts/StudioContext"; export interface BlockPreviewInfo { videoUrl?: string; posterUrl?: string; @@ -153,15 +156,59 @@ function CategoryPill({ ); } +interface CompositionContext { + currentTime: number; + activeCompPath: string | null; + elements: Array<{ + id: string; + start: number; + duration: number; + track: number; + label?: string; + compositionSrc?: string; + }>; + compositionDimensions?: { width: number; height: number }; +} + +function formatCompositionContext(ctx: CompositionContext): string { + const lines: string[] = [ + `Playback time: ${formatTime(ctx.currentTime)}`, + `Active composition: ${ctx.activeCompPath || "index.html"}`, + ]; + if (ctx.compositionDimensions) { + lines.push( + `Dimensions: ${ctx.compositionDimensions.width}x${ctx.compositionDimensions.height}`, + ); + } + const visibleNow = ctx.elements.filter( + (el) => ctx.currentTime >= el.start && ctx.currentTime < el.start + el.duration, + ); + if (visibleNow.length > 0) { + lines.push( + "", + `Elements visible at ${formatTime(ctx.currentTime)}:`, + ...visibleNow.map( + (el) => + `- ${el.label || el.id} (track ${el.track}, ${formatTime(el.start)}–${formatTime(el.start + el.duration)}${el.compositionSrc ? `, src: ${el.compositionSrc}` : ""})`, + ), + ); + } + const maxZ = ctx.elements.length > 0 ? Math.max(...ctx.elements.map((_, i) => i + 1)) : 0; + lines.push("", `Highest track index: ${maxZ}`); + return lines.join("\n"); +} + function buildAgentPrompt( title: string, name: string, description: string, category: BlockCategory, blockType: string, + context: CompositionContext, ): string { const isComponent = blockType === "hyperframes:component"; const kind = isComponent ? "component" : "block"; + const compositionInfo = formatCompositionContext(context); const categoryPrompts: Record = { captions: [ @@ -201,14 +248,15 @@ function buildAgentPrompt( ].join("\n\n"), }; - return ( + const instruction = categoryPrompts[category] ?? [ `Using /hyperframes, add the "${title}" ${kind} (registry: ${name}) to my composition.`, `${description}`, `Customize it to match my composition's design and timeline.`, - ].join("\n\n") - ); + ].join("\n\n"); + + return [instruction, "", "## Current composition state", "", compositionInfo].join("\n"); } function BlockCard({ @@ -262,15 +310,31 @@ function BlockCard({ }; }, []); + const { activeCompPath, compositionDimensions } = useStudioContext(); + const handleCopyPrompt = useCallback( (e: React.MouseEvent) => { e.stopPropagation(); - const prompt = buildAgentPrompt(title, name, description, category, blockType); + const state = usePlayerStore.getState(); + const context: CompositionContext = { + currentTime: state.currentTime, + activeCompPath, + elements: state.elements.map((el) => ({ + id: el.id, + start: el.start, + duration: el.duration, + track: el.track, + label: el.label, + compositionSrc: el.compositionSrc, + })), + compositionDimensions: compositionDimensions ?? undefined, + }; + const prompt = buildAgentPrompt(title, name, description, category, blockType, context); navigator.clipboard.writeText(prompt); setCopied(true); setTimeout(() => setCopied(false), 1500); }, - [title, name, description, category, blockType], + [title, name, description, category, blockType, activeCompPath, compositionDimensions], ); return ( From 8c698d7f2ccea62be397b594cce47c1dd99ac67c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Thu, 21 May 2026 19:31:14 -0400 Subject: [PATCH 06/19] feat(studio): show Add button for components, Ask agent for all Components (hyperframes:component) get both "Add" and "Ask agent" buttons since they work as drop-in overlays. Blocks (scenes, data, VFX, transitions) show only "Ask agent" since they need agent-guided customization. --- .../src/components/sidebar/BlocksTab.tsx | 79 ++++++++++++++----- .../src/components/sidebar/LeftSidebar.tsx | 3 +- 2 files changed, 60 insertions(+), 22 deletions(-) diff --git a/packages/studio/src/components/sidebar/BlocksTab.tsx b/packages/studio/src/components/sidebar/BlocksTab.tsx index d85d465cf..e16766305 100644 --- a/packages/studio/src/components/sidebar/BlocksTab.tsx +++ b/packages/studio/src/components/sidebar/BlocksTab.tsx @@ -15,11 +15,12 @@ export interface BlockPreviewInfo { } interface BlocksTabProps { + onAddBlock?: (blockName: string) => void; onPreviewBlock?: (preview: BlockPreviewInfo | null) => void; } // fallow-ignore-next-line complexity -export const BlocksTab = memo(function BlocksTab({ onPreviewBlock }: BlocksTabProps) { +export const BlocksTab = memo(function BlocksTab({ onAddBlock, onPreviewBlock }: BlocksTabProps) { const { loading, error, search, setSearch, category, setCategory, filteredBlocks } = useBlockCatalog(); @@ -117,6 +118,11 @@ export const BlocksTab = memo(function BlocksTab({ onPreviewBlock }: BlocksTabPr posterUrl={block.preview?.poster} videoUrl={block.preview?.video} onPreview={onPreviewBlock} + onAdd={ + block.type === "hyperframes:component" + ? () => onAddBlock?.(block.name) + : undefined + } /> ); })} @@ -280,9 +286,11 @@ function BlockCard({ tags?: string[]; posterUrl?: string; videoUrl?: string; + onAdd?: () => void; onPreview?: (preview: BlockPreviewInfo | null) => void; }) { const [hovered, setHovered] = useState(false); + const [adding, setAdding] = useState(false); const [copied, setCopied] = useState(false); const hoverTimer = useRef | null>(null); const colors = getCategoryColors(category); @@ -310,6 +318,17 @@ function BlockCard({ }; }, []); + const handleAdd = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + if (adding || !onAdd) return; + setAdding(true); + onAdd(); + setTimeout(() => setAdding(false), 1000); + }, + [adding], + ); + const { activeCompPath, compositionDimensions } = useStudioContext(); const handleCopyPrompt = useCallback( @@ -372,28 +391,46 @@ function BlockCard({ )} - {/* Ask agent overlay */} - + + {/* Badges */}
diff --git a/packages/studio/src/components/sidebar/LeftSidebar.tsx b/packages/studio/src/components/sidebar/LeftSidebar.tsx index 634fe6e1f..dcadab0b0 100644 --- a/packages/studio/src/components/sidebar/LeftSidebar.tsx +++ b/packages/studio/src/components/sidebar/LeftSidebar.tsx @@ -84,6 +84,7 @@ export const LeftSidebar = memo( onLint, linting, onToggleCollapse, + onAddBlock, onPreviewBlock, takeoverContent, }, @@ -246,7 +247,7 @@ export const LeftSidebar = memo( )} {STUDIO_BLOCKS_PANEL_ENABLED && tab === "blocks" && ( - + )} {/* Lint button pinned at the bottom */} From d5518b26f1ba04e54b475d9233d8b931c4ae4674 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Thu, 21 May 2026 19:50:36 -0400 Subject: [PATCH 07/19] fix(studio): destructure onAdd prop in BlockCard --- packages/studio/src/components/sidebar/BlocksTab.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/studio/src/components/sidebar/BlocksTab.tsx b/packages/studio/src/components/sidebar/BlocksTab.tsx index e16766305..f61eecf2e 100644 --- a/packages/studio/src/components/sidebar/BlocksTab.tsx +++ b/packages/studio/src/components/sidebar/BlocksTab.tsx @@ -275,6 +275,7 @@ function BlockCard({ tags, posterUrl, videoUrl, + onAdd, onPreview, }: { name: string; @@ -326,7 +327,7 @@ function BlockCard({ onAdd(); setTimeout(() => setAdding(false), 1000); }, - [adding], + [onAdd, adding], ); const { activeCompPath, compositionDimensions } = useStudioContext(); From 76d2c6af5dfbad5def07e199b480f0d228b95e28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Thu, 21 May 2026 19:54:35 -0400 Subject: [PATCH 08/19] fix(studio): fix catalog Add button per category, search by tags Add button rules: VFX, Social, Scenes get Add + Ask agent. Captions, Transitions, Effects, Data get Ask agent only. Search now matches category names and tags, so searching "captions" shows all caption blocks. --- packages/studio/src/components/sidebar/BlocksTab.tsx | 4 +++- packages/studio/src/hooks/useBlockCatalog.ts | 6 +++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/studio/src/components/sidebar/BlocksTab.tsx b/packages/studio/src/components/sidebar/BlocksTab.tsx index f61eecf2e..46a4a734c 100644 --- a/packages/studio/src/components/sidebar/BlocksTab.tsx +++ b/packages/studio/src/components/sidebar/BlocksTab.tsx @@ -119,7 +119,9 @@ export const BlocksTab = memo(function BlocksTab({ onAddBlock, onPreviewBlock }: videoUrl={block.preview?.video} onPreview={onPreviewBlock} onAdd={ - block.type === "hyperframes:component" + block.category === "vfx" || + block.category === "social" || + block.category === "scenes" ? () => onAddBlock?.(block.name) : undefined } diff --git a/packages/studio/src/hooks/useBlockCatalog.ts b/packages/studio/src/hooks/useBlockCatalog.ts index f11454c0f..83ea930ed 100644 --- a/packages/studio/src/hooks/useBlockCatalog.ts +++ b/packages/studio/src/hooks/useBlockCatalog.ts @@ -56,7 +56,11 @@ export function useBlockCatalog() { if (search.trim()) { const q = search.toLowerCase(); result = result.filter( - (b) => b.title.toLowerCase().includes(q) || b.description.toLowerCase().includes(q), + (b) => + b.title.toLowerCase().includes(q) || + b.description.toLowerCase().includes(q) || + b.category.toLowerCase().includes(q) || + b.tags?.some((t) => t.toLowerCase().includes(q)), ); } return result; From 0b0021a3100e4a14363b8c7ca0cbf13e65ee2dba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Thu, 21 May 2026 19:57:22 -0400 Subject: [PATCH 09/19] feat(studio): improve Ask agent UX, add tooltips to tabs and buttons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Ask agent button turns green with checkmark on copy - Add button shows tooltip "Add to composition at current time" - Ask agent shows tooltip "Copy a prompt to paste into your AI agent" - Tab tooltips: Code, Comps, Assets, Catalog each explain their purpose - Search placeholder updated to "Search by name, category, or tag…" --- .../src/components/sidebar/BlocksTab.tsx | 56 ++++++++++++++----- .../src/components/sidebar/LeftSidebar.tsx | 4 ++ 2 files changed, 45 insertions(+), 15 deletions(-) diff --git a/packages/studio/src/components/sidebar/BlocksTab.tsx b/packages/studio/src/components/sidebar/BlocksTab.tsx index 46a4a734c..e12b78a0b 100644 --- a/packages/studio/src/components/sidebar/BlocksTab.tsx +++ b/packages/studio/src/components/sidebar/BlocksTab.tsx @@ -63,7 +63,7 @@ export const BlocksTab = memo(function BlocksTab({ onAddBlock, onPreviewBlock }: type="text" value={search} onChange={(e) => setSearch(e.target.value)} - placeholder="Search blocks…" + placeholder="Search by name, category, or tag…" className="w-full bg-neutral-900 border border-neutral-800 rounded-md pl-7 pr-2 py-1.5 text-[11px] text-neutral-200 placeholder:text-neutral-600 focus:outline-none focus:border-neutral-700 transition-colors" />
@@ -400,6 +400,7 @@ function BlockCard({ )} diff --git a/packages/studio/src/components/sidebar/LeftSidebar.tsx b/packages/studio/src/components/sidebar/LeftSidebar.tsx index dcadab0b0..a7b9fe167 100644 --- a/packages/studio/src/components/sidebar/LeftSidebar.tsx +++ b/packages/studio/src/components/sidebar/LeftSidebar.tsx @@ -127,6 +127,7 @@ export const LeftSidebar = memo( @@ -491,3 +481,78 @@ function BlockCard({ ); } + +function PromptPreviewModal({ + title, + prompt, + onClose, +}: { + title: string; + prompt: string; + onClose: () => void; +}) { + const [copied, setCopied] = useState(false); + + const handleCopy = useCallback(() => { + navigator.clipboard.writeText(prompt); + setCopied(true); + setTimeout(() => setCopied(false), 1500); + }, [prompt]); + + return ( +
+
e.stopPropagation()} + > +
+
+

Ask agent

+

{title}

+
+ +
+
+

+ Copy this prompt and paste it into your AI agent (Claude Code, Cursor, etc.) +

+
+            {prompt}
+          
+
+
+ Paste into your agent after copying + +
+
+
+ ); +} From 300ef395ea1d08ab117411eab52ce38249331ab9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Thu, 21 May 2026 20:29:04 -0400 Subject: [PATCH 11/19] fix(studio): destructure onShowPrompt prop in BlockCard --- packages/studio/src/components/sidebar/BlocksTab.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/studio/src/components/sidebar/BlocksTab.tsx b/packages/studio/src/components/sidebar/BlocksTab.tsx index a68ed3e2a..4208d1731 100644 --- a/packages/studio/src/components/sidebar/BlocksTab.tsx +++ b/packages/studio/src/components/sidebar/BlocksTab.tsx @@ -290,6 +290,7 @@ function BlockCard({ posterUrl, videoUrl, onAdd, + onShowPrompt, onPreview, }: { name: string; @@ -366,7 +367,7 @@ function BlockCard({ const prompt = buildAgentPrompt(title, name, description, category, blockType, context); onShowPrompt?.({ title, prompt }); }, - [title, name, description, category, blockType, activeCompPath, compositionDimensions], + [title, name, description, category, blockType, activeCompPath, compositionDimensions, onShowPrompt], ); return ( From cf940326e93c409623722a67aaf2218ba967a5c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Thu, 21 May 2026 20:31:00 -0400 Subject: [PATCH 12/19] feat(studio): make prompt modal editable with textarea --- .../src/components/sidebar/BlocksTab.tsx | 41 +++++++++++-------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/packages/studio/src/components/sidebar/BlocksTab.tsx b/packages/studio/src/components/sidebar/BlocksTab.tsx index 4208d1731..b3938479a 100644 --- a/packages/studio/src/components/sidebar/BlocksTab.tsx +++ b/packages/studio/src/components/sidebar/BlocksTab.tsx @@ -492,13 +492,19 @@ function PromptPreviewModal({ prompt: string; onClose: () => void; }) { + const [value, setValue] = useState(prompt); const [copied, setCopied] = useState(false); + const textareaRef = useRef(null); + + useEffect(() => { + requestAnimationFrame(() => textareaRef.current?.focus()); + }, []); const handleCopy = useCallback(() => { - navigator.clipboard.writeText(prompt); + navigator.clipboard.writeText(value); setCopied(true); setTimeout(() => setCopied(false), 1500); - }, [prompt]); + }, [value]); return (
e.stopPropagation()} >
@@ -518,15 +524,7 @@ function PromptPreviewModal({ className="p-1 rounded-md text-neutral-500 hover:text-neutral-300 hover:bg-neutral-800/50" onClick={onClose} > - + @@ -534,14 +532,23 @@ function PromptPreviewModal({

- Copy this prompt and paste it into your AI agent (Claude Code, Cursor, etc.) + Edit the prompt below, then copy and paste into your AI agent

-
-            {prompt}
-          
+