Skip to content

feat(studio): render queue, layout restructure, home page, hover preview#95

Merged
miguel-heygen merged 8 commits intomainfrom
studio/7-render-queue-layout
Mar 28, 2026
Merged

feat(studio): render queue, layout restructure, home page, hover preview#95
miguel-heygen merged 8 commits intomainfrom
studio/7-render-queue-layout

Conversation

@miguel-heygen
Copy link
Copy Markdown
Collaborator

@miguel-heygen miguel-heygen commented Mar 27, 2026

Summary

  • Add render queue panel with progress tracking, download, and delete actions
  • Restructure App layout: home page with project picker, session-based routing
  • Add ExpandOnHover component for preview-on-hover interactions (uses motion/react)
  • CompositionsTab now supports hover preview with expanded iframe view
  • Vite config: guard setInterval cleanup to dev-only (fixes CI build timeout)
  • Add favicon and update studio package deps

Test plan

  • Render queue shows progress, completes, and allows download
  • Home page lists projects and navigates to session view
  • ExpandOnHover shows expanded preview on mouse hover with spring animation
  • vite build exits cleanly (no hanging process from setInterval)

🤖 Generated with Claude Code

@miguel-heygen miguel-heygen force-pushed the studio/7-render-queue-layout branch from 2e4f5e5 to dcf047b Compare March 27, 2026 18:54
@miguel-heygen miguel-heygen marked this pull request as ready for review March 27, 2026 19:22
@miguel-heygen miguel-heygen changed the base branch from studio/6-left-sidebar to graphite-base/95 March 27, 2026 19:36
@miguel-heygen miguel-heygen force-pushed the studio/7-render-queue-layout branch from dcf047b to efa7f60 Compare March 27, 2026 19:42
@miguel-heygen miguel-heygen changed the base branch from graphite-base/95 to studio/6-left-sidebar March 27, 2026 19:42
@miguel-heygen miguel-heygen force-pushed the studio/7-render-queue-layout branch 2 times, most recently from b774491 to fcea476 Compare March 27, 2026 19:52
@miguel-heygen miguel-heygen force-pushed the studio/6-left-sidebar branch from 6bc905e to 27a5251 Compare March 27, 2026 19:52
@miguel-heygen miguel-heygen force-pushed the studio/7-render-queue-layout branch from fcea476 to 441646a Compare March 27, 2026 20:00
Copy link
Copy Markdown
Collaborator

@vanceingalls vanceingalls left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall: Big PR — layout restructure, render queue, home page, hover preview. The render queue hook is well-architected. The ExpandOnHover interaction is a nice touch. A few concerns:


useRenderQueue.tsisRendering is stale

return {
  jobs,
  startRender,
  deleteRender,
  clearCompleted,
  isRendering: activeJobRef.current !== null,
};

activeJobRef.current is a ref, not state. When the render completes and activeJobRef.current is set to null, this return value won't trigger a re-render. Components reading isRendering will show the wrong state until something else causes a re-render. Use state instead of a ref for this value, or derive it from jobs: isRendering: jobs.some(j => j.status === "rendering").


useRenderQueue.ts — EventSource not cleaned up on unmount during active render

The cleanup effect:

useEffect(() => {
  return () => {
    eventSourceRef.current?.close();
  };
}, []);

This only runs on unmount. If the component re-mounts (e.g., navigating away and back), stale EventSource connections could accumulate. Also, if projectId changes mid-render, the old EventSource keeps running. Consider closing on projectId change too.


useRenderQueue.tsdeleteRender fires DELETE then removes from state regardless

const deleteRender = useCallback(async (jobId: string) => {
  try {
    await fetch(`/api/render/${jobId}`, { method: "DELETE" });
  } catch {
    // ignore
  }
  setJobs((prev) => prev.filter((j) => j.id !== jobId));
}, []);

If the DELETE fails (network error, 404), the job disappears from the UI but still exists on the server. Next time loadRenders runs, it'll reappear. Either keep it in the UI on failure (show an error state), or don't call loadRenders after a failed delete.


ExpandedPreviewIframe — offset calculation uses stale refs

const offsetX = containerRef.current
  ? (containerRef.current.clientWidth - dims.w * scale) / 2
  : 0;

This reads from containerRef.current during render, which works but won't update reactively. The scale state updates from the ResizeObserver, but offsetX/offsetY are recalculated on every render, not just when the container resizes. Consider storing offset in state alongside scale inside the ResizeObserver callback.


ExpandedPreviewIframe — polling for __player with setInterval

const interval = setInterval(() => {
  // ... 25 attempts at 200ms = 5 seconds max
}, 200);

This is a lot of polling. Consider using a MutationObserver on the iframe or listening for a custom event from the runtime instead. If the composition takes >5s to load GSAP, the preview silently fails.


CompositionsTab.tsx — hover preview spawns iframe per card

When hovering a composition, a full iframe loads with the preview URL. If the user mouses across several cards quickly, multiple iframes spawn and load simultaneously. Consider debouncing the hover or reusing a single preview iframe that repositions.


App.tsx — PR adds motion dependency for ExpandOnHover

The motion package (motion/react, formerly framer-motion) is a significant dependency addition for a single hover animation. The expand-on-hover effect could likely be done with CSS transitions + a portal, avoiding the extra ~40KB. If motion is planned for more features, fine — but if it's just for this one interaction, it's heavy.


App.tsx — inline SVG logo repeated 3 times

The HyperFrames logo SVG appears inline in the favicon, the header, and the empty state. Extract to a shared component.


RenderQueueItem.tsxformatTimeAgo doesn't update

The "just now" / "5m ago" text is calculated once on render and never updates. If the user leaves the renders tab open, timestamps go stale. Consider a small interval to refresh, or use relative timestamps that update on visibility change.

Copy link
Copy Markdown
Collaborator

The isRendering stale ref is the only one that will cause a visible bug — the Export MP4 button will stay disabled (or enabled) at the wrong time because the ref change doesn't trigger a re-render.

@miguel-heygen miguel-heygen force-pushed the studio/7-render-queue-layout branch from 441646a to d89c8f7 Compare March 27, 2026 20:38
@miguel-heygen miguel-heygen force-pushed the studio/6-left-sidebar branch from 27a5251 to 036fe70 Compare March 27, 2026 20:38
@miguel-heygen miguel-heygen force-pushed the studio/7-render-queue-layout branch 6 times, most recently from 08fcb1f to fa1ae02 Compare March 28, 2026 04:23
@miguel-heygen miguel-heygen force-pushed the studio/7-render-queue-layout branch from f36ff01 to d7543d0 Compare March 28, 2026 18:27
@miguel-heygen miguel-heygen force-pushed the studio/6-left-sidebar branch from 0840b63 to 4252f7a Compare March 28, 2026 18:27
@miguel-heygen miguel-heygen force-pushed the studio/7-render-queue-layout branch 8 times, most recently from 3221ba1 to a0433ac Compare March 28, 2026 19:37
@miguel-heygen miguel-heygen force-pushed the studio/6-left-sidebar branch from 4252f7a to 717d292 Compare March 28, 2026 19:41
@miguel-heygen miguel-heygen force-pushed the studio/7-render-queue-layout branch from a0433ac to b549e32 Compare March 28, 2026 19:41
@miguel-heygen miguel-heygen force-pushed the studio/6-left-sidebar branch from 717d292 to f553966 Compare March 28, 2026 19:45
@miguel-heygen miguel-heygen force-pushed the studio/7-render-queue-layout branch from b549e32 to 4ce235c Compare March 28, 2026 19:46
@miguel-heygen miguel-heygen force-pushed the studio/6-left-sidebar branch 2 times, most recently from 4567dad to 3ab9c2b Compare March 28, 2026 19:51
@miguel-heygen miguel-heygen force-pushed the studio/7-render-queue-layout branch from 4ce235c to dcf46dd Compare March 28, 2026 19:51
@miguel-heygen miguel-heygen changed the base branch from studio/6-left-sidebar to graphite-base/95 March 28, 2026 19:55
@miguel-heygen miguel-heygen force-pushed the studio/7-render-queue-layout branch from dcf46dd to a6b1de5 Compare March 28, 2026 19:55
@miguel-heygen miguel-heygen force-pushed the studio/7-render-queue-layout branch from a6b1de5 to 7708567 Compare March 28, 2026 19:56
@graphite-app graphite-app bot changed the base branch from graphite-base/95 to main March 28, 2026 19:57
miguel-heygen and others added 8 commits March 28, 2026 16:08
…d tests

- Add zoom state (zoomMode, pixelsPerSecond) to player store
- Add updateElementDuration, updateElementTrack, updateElement actions
- Remove agent activity tracking (activeEdits, agentId, agentColor)
- Add comprehensive store tests (265 lines)
- Add time utility tests
- TimelineClip: extracted clip rendering with drag, resize, and selection
- CompositionThumbnail: renders composition preview as timeline thumbnail
- VideoThumbnail: extracts and displays video frame thumbnails via canvas
…ayer

- Timeline: refactored track rendering, zoom support, drag/resize
- PlayerControls: updated layout, added playback rate selector
- useTimelinePlayer: improved seeking, element discovery, composition support
- Added Timeline tests (149 lines)
- NLELayout: add toolbar slot, composition breadcrumb, improved resizing
- App: update toolbar wiring, remove split/delete, add edit modal
- Remove AgentActivityTrack (unused)
- Update vite config with improved dev server and build settings
- Update package.json dependencies
- Simplify htmlEditor.ts (keep parseStyleString, mergeStyleIntoTag, findElementBlock)
- Update exports in index.ts
- NLELayout: add toolbar slot, composition breadcrumb, improved resizing
- App: update toolbar wiring, remove split/delete, add edit modal
- Remove AgentActivityTrack (unused)
- Update vite config with improved dev server and build settings
- Update package.json dependencies
- Simplify htmlEditor.ts (keep parseStyleString, mergeStyleIntoTag, findElementBlock)
- Update exports in index.ts
- Add TimelineToolbar with Edit button (replaces Split/Delete)
- Add EditModal with time range selection, element list, prompt textarea
- "Copy to Agent" copies structured context to clipboard
- Wire toolbar and modal into App.tsx
Code quality improvements (desloppify):
- Fix composition drill-down: patch compositionSrc from raw HTML map,
  handle -host suffix mismatch between DOM IDs and composition IDs
- Reset duration/timeline state on drill-down navigation
- Replace inline linter (~90 LOC) with server-side /api/lint endpoint
  using @hyperframes/core's canonical lintHyperframeHtml
- Convert 13 useEffect(fn,[]) to useMountEffect(fn) — ban direct useEffect
- Remove wrapPlayer pass-through (identical PlayerAPI/PlaybackAdapter)
- Deduplicate useMountEffect to single canonical location
- Replace formatTick duplicate with formatTime re-export
- Move EditModal.tsx from components/timeline/ to player/components/
- Fix App.tsx barrel bypass — use player barrel imports
- Fix fetch() during render → proper useEffect with race condition guard
- Fix missing blur event listener cleanup (memory leak)
- Fix WebM content-type in download, renders listing, delete endpoints
- Fix render endpoint to read fps/quality/format from body
- Convert FileTree from flat list to collapsible tree with sidebar layout

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants