Skip to content

feat(cli,studio): hand off on macOS now, and open the app on Windows and Linux - #4999

Merged
WaterrrForever merged 9 commits into
mainfrom
miao/open-in-desktop-platforms
Oct 4, 2026
Merged

WaterrrForever merged 9 commits into
mainfrom
miao/open-in-desktop-platforms

Conversation

@WaterrrForever

@WaterrrForever WaterrrForever commented Oct 4, 2026 •

Copy link
Copy Markdown
Collaborator

Builds on #4951. Review only commit 4df0660 — the rest is #4951 (approved), its one-line studioApiFetch fix, and merges of main. Once #4951 merges this shows just that commit.

What

The released Mac app (b254, hyperframes-internal#2601) takes a handed-over folder and the conversation, so the hand-off turns on for macOS. Windows and Linux keep the download until the desktop change that reads a folder from the command line ships; then HANDOFF_READY widens.

  • hyperframes open finds the app off macOS: the per-user install on Windows (%LOCALAPPDATA%\Programs\HyperFrames\HyperFrames.exe), and on Linux the AppImage the app records in launcher.json in its config folder. It starts it with the folder; the app is single-instance, so a running copy is forwarded it.
  • Download links follow the machine: the DMG on macOS, the AppImage on Linux (/studio/download?os=linux), nothing on Windows, which has no public download yet (the site says "Coming soon").
  • Studio's button shows wherever there is a download or an installed app; the card drops "for macOS".
  • Every render prints the app line except batch rows: music-to-video delivers a -q draft render, so drafts no longer skip it.
  • The success line names the app that took the folder (Canary included).
  • Every surface calls it "the HyperFrames desktop app", so it is not mistaken for the Studio preview (Wenbo's review).

Before

The card said the app is macOS-only and always linked the DMG:

Meet Framey card before, light Meet Framey card before, dark

After

The app is "the HyperFrames desktop app", never "Studio" (that is the preview); on this Linux mock the button links the AppImage:

Meet Framey card after, light Meet Framey card after, dark

Testing

  • desktopApp.test.ts, desktopRoutes.test.ts, render/execute.test.ts (33), OpenInDesktopButton.dom.test.tsx + StudioHeader.dom.test.tsx (16); tsc clean for cli and studio.
  • macOS, released b254: a real Claude Code session ran hyperframes open with the app closed; it cold-started, added the project to Home, and the first chat showed "Picked up the conversation from Claude Code." and recalled what was said in the terminal. Same through Studio's button.
  • Linux (Docker, arm64): b254's app code with the desktop folder-argv change on Electron 44.2.0 — with the app running and with it closed, hyperframes open landed the project in Home. Not run: the x86 AppImage itself (its runtime can't start under emulation on a Mac).
  • Windows: unit tests only.

…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).
…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.
…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)

@jrusso1020 jrusso1020 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving at ec583f32. As asked, I reviewed commit 4df0660b. The commits after it are #4951's, which I approved earlier (13cb249b, e09b7de2), plus the merge ec583f32, which needed no hand resolution: git merge-tree --write-tree 4df0660b e09b7de2 gives tree 36cc3047, the same as the merge commit.

Behaviour per OS

  • HANDOFF_READY is now process.platform === "darwin" (desktopApp.ts:28).
  • macOS: open -b <bundle id> <dir>, trying the released app and then Canary. If that fails, desktopInstalled() checks /Applications, ~/Applications, then Spotlight, to choose between open-failed and not-installed. #4951's guarantee still holds: with ready=false nothing opens and no hand-off is written (:169).
  • Windows: %LOCALAPPDATA%\Programs\<name>\<name>.exe for HyperFrames and then HyperFrames Canary. That matches the desktop installer's install dir and exe name.
  • Linux: $XDG_CONFIG_HOME (default ~/.config), then /<name>/launcher.json, then { "appImage": string }. The value must be absolute and must exist.
  • Launch: spawn(executable, [dir], { detached: true, stdio: "ignore" }). There's no shell, and dir is always absolute (resolve() in project.ts:43), so it can't be read as an option.
  • launcher.json against the desktop side: the app writes JSON.stringify({ appImage: process.env.APPIMAGE }) to app.getPath("userData")/launcher.json, after setName to the channel name. Path, names and schema match what this side reads.
  • Bad launcher.json: I tried it as a folder, a symlink loop, malformed JSON, appImage: 5, a relative path, and a path to a symlink loop. Each one returns not-installed without throwing.
  • Draft renders: they now print the line. Batch rows still pass desktopHint: false, and --json only exists with --batch, which is quiet. So JSON output is unchanged.

Something the description could make clearer: on Windows and Linux, hyperframes open doesn't launch anything at this head yet. HANDOFF_READY is true only on darwin, so open --json on Linux returns {"opened":false,"reason":"handoff-unavailable","downloadUrl":"…?os=linux"} and exits 1. The commit message says this ("stay on the download until the desktop change ships"), but the PR body says open "works" on both. The new lookups are tested, but nothing uses them until the gate widens.

Tests: CLI tests pass 33/33 (desktopApp, desktopRoutes, render/execute), Studio tests pass 16/16 (OpenInDesktopButton, StudioHeader), and tsc --noEmit is clean for both packages. I tried six mutations and each turned tests red:

  • dropping the isAbsolute check;
  • ignoring XDG_CONFIG_HOME;
  • dropping Programs from the Windows path;
  • giving Linux the DMG URL;
  • setting HANDOFF_READY = true everywhere;
  • ignoring a failed launch.

CI at this head: 61 pass, 0 failing. The regression and edit-accuracy shards were still pending.

Non-blocking (should land before the gate widens):

  1. False success on Windows and Linux. launchApp returns true once spawn returns. EACCES/ENOENT arrive later on 'error', which is swallowed (desktopApp.ts:60-68). If launcher.json points at a folder or a non-executable file, openInDesktop returns {"opened":true}. An isFile + X_OK check before spawning would close most of this. You can't hit it at this head.
  2. No version check on macOS. An install from before the hand-off build still accepts open -b … dir, so the CLI reports success and writes a hand-off that app ignores. The updater makes that window small. A CFBundleVersion check would close it.
  3. mdfind on every preview and render. desktopHint no longer stops early on macOS, so every preview and every non-quiet render calls desktopInstalled(). On a Mac without the app in /Applications, that can be two synchronous mdfind spawns with no timeout (:189-192). Passing timeout to spawnSync would cap it.

Nits

  • The JSDoc at render.ts:1917 still says "never a draft or a batch row".
  • The success toast in OpenInDesktopButton names "HyperFrames Studio" even when Canary took the folder.
  • On arm64 Linux, the download link goes to the x86_64 AppImage.

— Rames

@github-actions

github-actions Bot commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 1556 (base branch 1556), smooth 1355 of those

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Quarantined, measured but not gated (0)

@jerrai-bot-heygen jerrai-bot-heygen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approving 4df0660b. The later commits are #4951's plus a merge.

Non-blocking:

  • Macs on a build before b254. HANDOFF_READY turns the hand-off on for every Mac without checking what version is installed. On an older build, open -b still starts the app, so the CLI and Studio say it's opening, but the app never imports the folder. Desktop auto-updates, so this is temporary and nothing is lost. A version or capability check would make the "Opening" message true.
  • Before Windows/Linux are enabled. launchApp returns true right after spawn. An AppImage that has lost its execute bit fails afterwards with EACCES, and the empty error listener swallows it. The CLI then reports success instead of falling back to Canary or returning open-failed. The tests inject launch, so they never hit this path.
  • Drafts. Draft renders now print the open line, but the changed test no longer covers a draft render.

— Jerrai

@jrusso1020 jrusso1020 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-approving at 4d63607b. I reviewed only the delta from ec583f32, which is one commit and copy only, as described:

  • CLI: the open description and help, the not-installed and open-failed messages, desktopHint, and the AppName value.
  • Studio: the "Edit with Framey" button's title, popover, link and toast.
  • The matching test expectations.

git grep finds no "HyperFrames Studio" left anywhere the CLI or the button prints for the desktop app. The remaining uses refer to the preview server and the WebMCP tools, which is correct. Opening <name> in ${result.app} still reads correctly for both app names.

Verified locally at this head: desktopApp.test.ts and desktopRoutes.test.ts pass (31/31), OpenInDesktopButton.dom.test.tsx passes (7/7), and tsc --noEmit is clean for cli and studio. CI is green.

Nit, not blocking: app in hyperframes open --json is now a prose phrase ("the HyperFrames desktop app", lowercase article) next to "HyperFrames Canary". The command is new, so nothing depends on it yet. If scripts are expected to read that field, a stable id with the phrase kept for display would age better.

The three should-fixes from my last review are unchanged by this delta:

  • opened: true is reported when the spawn fails.
  • There is no macOS version gate.
  • mdfind has no timeout.

— Rames

…platforms

# Conflicts:
#	packages/cli/src/commands/open.ts
#	packages/cli/src/commands/render.ts
#	packages/cli/src/commands/render/execute.test.ts
#	packages/cli/src/help.ts
#	packages/cli/src/server/desktopRoutes.test.ts
#	packages/cli/src/server/desktopRoutes.ts
#	packages/cli/src/utils/desktopApp.test.ts
#	packages/cli/src/utils/desktopApp.ts
#	packages/studio/src/components/OpenInDesktopButton.dom.test.tsx
#	packages/studio/src/components/OpenInDesktopButton.tsx
#	scripts/studio-runtime-smoke.mjs

@jrusso1020 jrusso1020 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-approving at ca8d5abe. It merges main, including the squash of the first Edit with Framey change, into the head I approved at 4d63607b.

Every conflict (open.ts, render.ts, execute.test.ts, help.ts, desktopRoutes*, desktopApp*, OpenInDesktopButton*, studio-runtime-smoke.mjs) resolves to this PR's side. I listed each line main added since the merge-base and checked whether it survives here. The only behavior main had that this head drops is wantsDesktopHint's quality !== "draft" exclusion and its test. This PR already removed that on purpose ("Drafts do too: music-to-video delivers one"), so nothing is lost by accident.

Nit: printRenderComplete's doc comment on desktopHint still says "never a draft or a batch row", which contradicts that.

Verified at this head: the desktopApp, desktopRoutes and render execute tests pass (33/33), OpenInDesktopButton.dom passes (7/7), and tsc --noEmit is clean for cli and studio. The one red check is the non-required Comments.

— Rames

@jrusso1020 jrusso1020 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-approving at 779c566.

The delta from ca8d5ab (the head I last approved, its parent) is one commit that only shortens doc comments in desktopApp.ts, desktopRoutes.ts and OpenInDesktopButton.tsx. Filtering the diff to non-comment lines leaves nothing. The rewritten comments still describe the code correctly. Comments is green now.

My earlier should-fixes still stand and don't block: opened: true when the spawn fails, no macOS version gate, and no timeout on mdfind.

— Rames

@WaterrrForever
WaterrrForever added this pull request to the merge queue Oct 4, 2026
Merged via the queue into main with commit 6a27a34 Oct 4, 2026
94 checks passed
@WaterrrForever
WaterrrForever deleted the miao/open-in-desktop-platforms branch October 4, 2026 13:06
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.

3 participants