feat(studio,cli): add an Edit with Framey button and hyperframes open for HyperFrames Studio - #4951
Conversation
…n` for HyperFrames Studio Point people from the CLI and the Studio preview to HyperFrames Studio, the desktop app. - Studio header: an "Edit with Framey" button with Framey, the app's cursor mascot (the eye follows the pointer, a wiggle on hover, honours reduced motion). Until the app opens handed-over projects (HANDOFF_READY), a press shows a "Meet Framey" card with the download. The button shows only where the preview server answers GET /api/open-in-desktop on macOS, so the desktop app's own embedded Studio never shows it. - `hyperframes open [dir]`: hands the project folder to the app with `open -b` (released app, then Canary) and points to the download when neither is installed. When run by Claude Code or Codex it leaves .hyperframes/agent-handoff.json (CLAUDE_CODE_SESSION_ID / CODEX_THREAD_ID) so the app's first chat can pick up that conversation. - render and preview print one line about the app: the download until HANDOFF_READY, then `hyperframes open` when the app is installed. Nothing for a draft render, a batch row, or a run inside the app (HYPERFRAMES_DESKTOP_PROJECT).
Edit accuracy: accurate 1556 (base branch 1556), smooth 1430 of thoseThe gate passes. Quarantined, measured but not gated (0) |
jerrai-bot-heygen
left a comment
There was a problem hiding this comment.
Changes requested at 58934a3. The code reads well, but four checks are red because of this PR, and the new POST route needs the same guard its neighbours have.
Blockers
- Tests on windows-latest fail in
desktopApp.test.ts:openCommandForreturnsfilms\awhere the test expectsfilms/a;- the
desktopInstalledstub only knows the/Applications/...paths.
- Studio: load smoke fails with
unmocked API request: GET /api/open-in-desktop. The button probes the route on mount, and the smoke harness doesn't mock it. - Comments:
studioServer.tsand the newdesktopApp.tsraise the comment share above the gate. - Studio and player captures: the body has no Before/After for the new header button and the Meet Framey card.
POST /api/open-in-desktop(studioServer.ts:973) has no Host/Origin check. A bodiless cross-originno-corsPOST is a simple request, so any page that finds the localhost port can launch the app on the project and writeagent-handoff.json. The impact is small, because the folder and the content are both fixed, but/api/telemetry-identityin the same file is already Host-guarded against DNS rebinding (identityAllowed). Gate this route the same way. It is also live today even thoughHANDOFF_READY = false.
On the stated gate: the hints and the Studio button are gated. hyperframes open is not: open.ts:25 calls openInDesktop directly, so on a Mac with the released app it runs open -b and writes the handoff file. That is fine if it's deliberate, but the description says the gate covers both surfaces.
Non-blocking
mkdirSync/writeFileSyncfor the handoff file run unwrapped after the app has opened. A read-only project throws in the CLI and returns a 500 from the server.- Any non-zero exit from
open -bis reported as "not installed". HANDOFF_READYis defined in two files. A shared constant would keep the gate from going half-flipped.- The tests branch on
HANDOFF_READY, so CI never exercises the live path, and nothing tests the server route.
Checked and fine:
open -bandmdfindare spawned with argument arrays and no shell;- the hint is skipped for draft quality (
render.ts:893/1147), for batch rows, and inside the app (HYPERFRAMES_DESKTOP_PROJECT).
— Jerrai
jrusso1020
left a comment
There was a problem hiding this comment.
Requesting changes. This adds to the existing review at this head, and I agree with its CI findings:
- the Windows path failures in
desktopApp.test.ts, which are a required check; - the missing Before/After for the captures check, also required;
- the load-smoke mock for
GET /api/open-in-desktop; - the missing Host/Origin guard on the POST.
1. hyperframes open isn't gated, and with today's app it says something that isn't true.
open.ts:25callsopenInDesktopdirectly, so on a Mac with the released app it runsopen -band writesagent-handoff.json.- It then prints "Opening X in HyperFrames Studio" and "Its chat picks up this Claude Code conversation." (
open.ts:31-34). - The released app doesn't handle a folder handed to it yet. That's the unreleased desktop change this gate waits for. So the app comes to the front without the project, and nothing reads the hand-off file.
- The command is also listed in root help (
help.ts:30), so people and agents will find it before the gate flips.
Either gate open behind HANDOFF_READY the same way the hints are (print the download line and write nothing), or leave it out of help and change the wording until the gate flips.
2. A hand-off written now can be picked up much later. While gated, both hyperframes open and the POST route write .hyperframes/agent-handoff.json. The app release that reads it takes it once, on a project's next fresh chat. A file written today could then bring in a conversation from weeks earlier. The file isn't gitignored in user projects either, because init creates no .gitignore. Gating item 1 also fixes this for the CLI. The POST route needs the same treatment.
On the POST guard: I reproduced the missing guard with a throwaway test. A POST with Host evil.example:3002, Origin https://evil.example and Sec-Fetch-Site: cross-site returned 200 and called openInDesktop. A {dir:"/etc"} body was ignored, so the folder stays this server's project. The other review's guard (identityAllowed) fixes it.
Nits
openexits 0 when the app isn't installed or the platform isn't supported, even with--json(open.ts:38-43). An agent can't tell from the exit code that nothing opened. I reproduced it on Linux:opened:false, exit 0.open --jsonon a bad directory prints the human error box, not JSON (open.ts:24usesresolveProject).previewusesresolveProjectOrThrowunder--json.- No test covers the draft skip (
render.ts:893,:1147) or the batch skip (render/execute.ts:368). Removing either one leaves all 144 render and desktop-hint tests green. The in-app skip is covered: removing it turns a test red.
What holds
- Both
HANDOFF_READYconstants gate what they cover. While gated, the button sends no POST. Removing its gate branch turnsposts==0red. - The CLI hint never prints
hyperframes open. - The route is registered only in the CLI preview server, so the button stays hidden anywhere else.
- The hint goes to stdout, and only in human modes.
render --quiet, batch--jsonand everypreview --jsonbranch skip it.
What I ran at 58934a34
desktopApp.test.ts: 14/14.cli.commands.test.ts: 12/12. It has no assertion foropen.OpenInDesktopButton+StudioHeaderdom tests: 12 pass, 2 skipped byHANDOFF_READY.- I didn't run anything on macOS. The "app ignores the folder" point comes from reading the desktop app's current main branch, which has no
open-filehandler. I didn't run it.
— Rames
…op POST, keep one gate Review of #4951 at 58934a3. - One gate: HANDOFF_READY lives only in the CLI. `GET /api/open-in-desktop` now says `handoff`, and Studio's button reads it, so the gate cannot be half-flipped. - While gated nothing opens and nothing is written: `openInDesktop` returns `handoff-unavailable` (so `hyperframes open` prints the download line and the POST route answers with it), and no `.hyperframes/agent-handoff.json` can sit waiting for a later release. - The POST answers only Studio's own same-origin request (loopback Host, matching Origin, Sec-Fetch-Site same-origin); a cross-site or rebound-host POST gets 403. Routes moved to server/desktopRoutes.ts with their own tests. - `hyperframes open`: exits 1 when nothing opened, answers in JSON under --json for a bad directory, and tells a missing app from one macOS could not open (`open-failed`). A project that cannot be written still opens, with nothing handed over. - Tests drive the live path through `ready`, are path-portable for Windows, and cover the draft and batch-row hint skips (`wantsDesktopHint`, `batchRowRenderOptions`). - The load smoke mocks the route.
jerrai-bot-heygen
left a comment
There was a problem hiding this comment.
Re-reviewed the delta 58934a3...e1016df. Every item from my changes-requested review is fixed:
- POST guard.
sameOriginPost(desktopRoutes.ts:6) reusesidentityAllowedfor Host. When present,Sec-Fetch-Sitemust besame-originand Origin must behttp://<host>, so a cross-site or rebound-host POST gets 403 andopenis never called (tested). A headerless loopback request such as curl is still accepted. That matches whatidentityAllowedis for (stopping a web page that reaches localhost by rebinding a domain name), so it is fine. - One gate.
HANDOFF_READYnow exists only in desktopApp.ts.openInDesktopreturnshandoff-unavailablebefore it spawns anything or callsleaveHandoff, which is the only writer ofagent-handoff.json. The route checksreadybeforeopen, GET reportshandoff, and the button never POSTs while gated (asserted). - Nits. Fixed: the exit code, JSON for a bad directory,
not-installedvsopen-failed, the read-only project case and the draft/batch hint tests. Windows paths, the smoke mock and the comment ratio are fixed too.
Still open, all small:
- The Studio and player captures check is red until the Before/After images are in the body.
- Windows
engine-cli/studio-2were still running at review time. open.tsitself has no test. The exit code and the--jsonbad-directory path are untested.expect(HANDOFF_READY).toBe(false)means the PR that flips the gate must edit that test.
— Jerrai
There was a problem hiding this comment.
Approve. This replaces my changes-requested review. Both of my points are fixed at e1016df4.
hyperframes openis gated.openInDesktopreturnshandoff-unavailablebefore it spawnsopenor writesagent-handoff.json, and the command prints the download line. So no hand-off file can be written now and picked up by a later release. The POST route answers the same way while gated, and the button readshandofffrom the GET, so there is one gate.- The POST guard holds.
sameOriginPostneeds a loopback (or bound) Host,Sec-Fetch-Sitesame-origin when it is sent, and an Origin equal tohttp://<host>. A cross-site page, another localhost port, and a rebound host name all get 403. - Nits fixed:
openexits 1 when nothing opened,--jsonanswers in JSON for a bad directory, and the draft and batch hint skips have a test.
Mutations, each one restored:
readycheck removed fromopenInDesktop: red, 2 fails.- Origin check removed: red.
- Host check removed: red.
- Route gate removed: red.
- Button gate removed: red.
Tests at this head: desktopRoutes + desktopApp + render/execute 23/23. OpenInDesktopButton + StudioHeader dom tests 14/14.
CI: every required check is green except Studio and player captures, which waits on the Before/After in the body. A body edit isn't a push, so this approval still counts afterwards.
Nit, not blocking: three new comments point at a private repository's PR number and package paths (desktopApp.ts:12, FrameyGlyph.tsx:3, theme.css:439). A reader of this public repo can't follow them. Describing the behavior instead would read better.
I didn't run anything on macOS.
— Rames
jrusso1020
left a comment
There was a problem hiding this comment.
Re-approving at e09b7de2. My last approval was at e1016df4, and only two commits have landed since:
13cb249b, merge of main (32d2bc6f). No conflicts were resolved by hand.git merge-tree --write-tree e1016df4 32d2bc6fgives tree5548c5de, which is exactly the merge commit's tree.e09b7de2.OpenInDesktopButton.tsxnow usesstudioApiFetchfor both the GET and the POST to/api/open-in-desktop. This is what main's new oxlint rule requires.studioApiFetchonly addscredentials(omiton loopback). The POST guard (sameOriginPost) checksHost,OriginandSec-Fetch-Site, not cookies, so leaving cookies off doesn't change whether the guard accepts the request. Test mocks ofglobalThis.fetchstill catch the call.
CI at this head: 66 pass, 0 failing. That includes regression, preview-regression, the timeline viewport gate, CodeQL and Windows render verification. The 20 edit-accuracy shards were still queued when I posted. Branch protection still gates the merge on them, and this delta doesn't touch editing.
— Rames
…and Linux (heygen-com#4999) * feat(studio,cli): add an Edit with Framey button and `hyperframes open` for HyperFrames Studio Point people from the CLI and the Studio preview to HyperFrames Studio, the desktop app. - Studio header: an "Edit with Framey" button with Framey, the app's cursor mascot (the eye follows the pointer, a wiggle on hover, honours reduced motion). Until the app opens handed-over projects (HANDOFF_READY), a press shows a "Meet Framey" card with the download. The button shows only where the preview server answers GET /api/open-in-desktop on macOS, so the desktop app's own embedded Studio never shows it. - `hyperframes open [dir]`: hands the project folder to the app with `open -b` (released app, then Canary) and points to the download when neither is installed. When run by Claude Code or Codex it leaves .hyperframes/agent-handoff.json (CLAUDE_CODE_SESSION_ID / CODEX_THREAD_ID) so the app's first chat can pick up that conversation. - render and preview print one line about the app: the download until HANDOFF_READY, then `hyperframes open` when the app is installed. Nothing for a draft render, a batch row, or a run inside the app (HYPERFRAMES_DESKTOP_PROJECT). * fix(studio,cli): gate `hyperframes open` too, guard the open-in-desktop POST, keep one gate Review of heygen-com#4951 at 58934a3. - One gate: HANDOFF_READY lives only in the CLI. `GET /api/open-in-desktop` now says `handoff`, and Studio's button reads it, so the gate cannot be half-flipped. - While gated nothing opens and nothing is written: `openInDesktop` returns `handoff-unavailable` (so `hyperframes open` prints the download line and the POST route answers with it), and no `.hyperframes/agent-handoff.json` can sit waiting for a later release. - The POST answers only Studio's own same-origin request (loopback Host, matching Origin, Sec-Fetch-Site same-origin); a cross-site or rebound-host POST gets 403. Routes moved to server/desktopRoutes.ts with their own tests. - `hyperframes open`: exits 1 when nothing opened, answers in JSON under --json for a bad directory, and tells a missing app from one macOS could not open (`open-failed`). A project that cannot be written still opens, with nothing handed over. - Tests drive the live path through `ready`, are path-portable for Windows, and cover the draft and batch-row hint skips (`wantsDesktopHint`, `batchRowRenderOptions`). - The load smoke mocks the route. * feat(cli,studio): hand off on macos now, and open the app on windows and linux The released Mac app (b254) takes a handed-over folder and its conversation, so HANDOFF_READY turns on for macOS. Windows and Linux stay on the download until the desktop change that reads a folder from the command line ships. - `hyperframes open` finds the app off macOS: the per-user install on Windows, and on Linux the AppImage the app records in launcher.json; it starts it with the folder - download links follow the machine: the DMG on macOS, the AppImage on Linux, and nothing on Windows, which has no public download yet - the Studio button shows wherever there is a download or an installed app; the card no longer says "for macOS" - every render prints the app line except batch rows: music-to-video delivers a draft - the success line names the app that took the folder (Canary included) * fix(studio): the edit-with-framey button asks its route through studioApiFetch * fix(cli,studio): name the app the hyperframes desktop app, apart from the studio preview * fix(cli,studio): fewer comment lines, so no file's comment share rises
…sktop app as the chat way to edit (heygen-com#4952) * feat(skills): tell the user about HyperFrames Studio at the final preview and after delivery /hyperframes gains § 6: mention the preview header's Edit with Framey button when the final Studio preview opens, and after the delivery render pass on the render's own desktop-app line in plain words — offer to run `hyperframes open` when the app is installed, else give the download link. Say nothing when the render prints no such line (a draft, a batch row, a run inside the app). Builds on the CLI lines added in heygen-com#4951. * feat(skills): open studio before every final render, pitch the app as the chat way to edit Users who only ask for a video never saw Studio: autonomous runs asked "preview first, or render?" and a "you decide" answer rendered. The final look now opens the Studio preview in every mode, then asks "render now, or what changes?" — opening it asks nothing, so it also holds when the user said not to ask. Section 6 says what is true: the Studio preview already edits and saves; the desktop app adds editing by chat with Framey. The agent names it "the HyperFrames Studio desktop app" so it is not mistaken for the preview. general-video, the review loop and the brief contract carry the same final look. * fix(skills): call the app the hyperframes desktop app, never studio * fix(skills): pitch edit with framey only when preview shows it, keep direct renders direct * fix(skills): mention the button only after the line that ships with it
What
Point people from the CLI and the Studio preview to HyperFrames Studio, the desktop app (b249 is downloadable today at hyperframes.dev/studio/download).
Studio header — "Edit with Framey"
GET /api/open-in-desktopwithavailable: true(macOS). The desktop app's embedded Studio has no such route, so the button never appears inside the app.theme.css(--color-framey,--color-framey-ink).CLI
hyperframes open [dir]hands the project folder to the app withopen -b(released app, then Canary). Run by Claude Code or Codex it leaves.hyperframes/agent-handoff.json(CLAUDE_CODE_SESSION_ID/CODEX_THREAD_ID) so the app's next chat can pick up that conversation. Exits 1 whenever nothing opened;--jsonanswers in JSON for a bad directory too.renderandpreviewprint one line about the app. Nothing for a--quality draftrender, a batch row, or a run inside the app (HYPERFRAMES_DESKTOP_PROJECT).One gate until the app opens handed-over folders
The app learns to open a handed-over folder in hyperframes-internal#2601 (approved, not released). Until then
HANDOFF_READY = falseinpackages/cli/src/utils/desktopApp.ts— the only copy:openInDesktopreturnshandoff-unavailable: nothing opens and nothing is written, sohyperframes openprints the download line and exits 1, and no hand-off file can wait for a later release.GET /api/open-in-desktopreportshandoff: false; Studio's button reads it and only shows the Meet Framey card (no POST).Flipping it to
trueafter that release makes the button open the project in the app and printsEdit it with Framey: hyperframes open .for people who have the app. Tests drive that live path through areadyoption, so CI covers it today.POST guard:
POST /api/open-in-desktopanswers only Studio's own same-origin request (loopback Host viaidentityAllowed,Originequal tohttp://<host>,Sec-Fetch-Site: same-originwhen present). A cross-site POST and a rebound-host POST both get 403 (server/desktopRoutes.test.ts).Before
Header today (0.8.111), light and dark:
After
Header with Edit with Framey, and the Meet Framey card a press opens, light and dark:
Testing
desktopApp.test.ts(16): gated path writes nothing; live path opens, falls back to Canary, tellsnot-installedfromopen-failed, skips non-macOS, still opens a read-only project with nothing handed over; hints for gated / missing / installed / in-app. Path-portable for Windows.desktopRoutes.test.ts(5): GET flags; cross-site and rebound-host POST → 403 and nothing opened; gated POST answers with the download; live POST opens this server's project.render/execute.test.ts: the hint is skipped for a draft and for a batch row (wantsDesktopHint,batchRowRenderOptions).OpenInDesktopButton.dom.test.tsx(5, no skips): hidden without the route or off macOS; gated press shows the card and sends no POST; live press opens and toasts; live press with the app missing shows the card.fileWatcher.atomicfails the same way onmain.