Skip to content

feat(studio): the desktop-app button says Install the desktop app and never dead-ends - #5854

Merged
miguel-heygen merged 6 commits into
mainfrom
scrubfix/desktopcta
Oct 18, 2026
Merged

miguel-heygen merged 6 commits into
mainfrom
scrubfix/desktopcta

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Oct 18, 2026 •

Copy link
Copy Markdown
Collaborator

Do not merge yet. The maintainer reviews this rendered result first.

What

Studio's header button that leads to the HyperFrames desktop app:

  • reads "Install the desktop app" beside Framey, drawn as the app draws it, with the app's moods: it breathes and blinks, its eyes circle while it waits, it hops when the app turns up, flies while a project hands over, and shakes when that fails. Its eyes follow the pointer.
  • App ready to take the project: the button reads "Open in desktop app" and a press opens this same project in the app. A folder the preview serves from a bare index.html gets its hyperframes.json first (created only when missing), so the app no longer refuses it with "No HyperFrames project at …".
  • No app yet: a press opens a card under the button where Framey walks three steps:
    1. Download HyperFrames for Mac/Linux: opens hyperframes.dev's download in a new tab. The step is done only when the tab really opened; a blocked tab names the download address.
    2. Install it: done only when the preview server finds the app. While the card is open and the app is not there, the card asks GET /api/open-in-desktop every 2 s; closing the card stops it. After a minute without the app it asks whether the download finished. Meanwhile the button reads "Finish installing" with "2 of 3".
    3. Open the project: the existing hand-off. If it fails, the step shows the project folder with a copy control and Try again. If the app is gone, the card goes back to step 1.
  • Linux with the app installed: the app does not take a handed-over folder there yet, so step 3 shows the folder to drop into the app.
  • No app for this platform (Windows, "coming soon" on hyperframes.dev): no button, so no one is sent to an app they cannot install.
  • The card is a popover anchored to the button. Opening it, every state change and closing it never move the preview, inspector or timeline (measured below).

Why

Users of the local preview did not find the desktop app: "Edit with Framey" did not say what it does, and a press could end in the app's "No HyperFrames project" dialog for a project the preview had just opened. The app opens only folders that pass core's isHyperframesProject (index.html plus hyperframes.json, meta.json or project.json), while preview serves any folder with an index.html. Sending people to a download and leaving them there also lost them between the download and the app: the card keeps the next step in view until the project is open.

How

  • openInDesktop (every hand-off goes through it: this button and hyperframes open) adds hyperframes.json with the existing no-overwrite createProjectConfig when the folder has index.html but fails core's project rule.
  • GET /api/open-in-desktop says handoff: true only where the app is installed and can take a project, and adds os and installed. The card's install step reads the same route; nothing new on the server.
  • OpenInDesktopButton.tsx owns the route, telemetry, the install check and the hand-off; DesktopGuideCard.tsx draws the card from one Progress value (label, Framey's mood and each step's state derive from it). Both use Studio's Popover, Button and tokens.
  • FrameyGlyph takes an optional mood; the moods' motion is in components.css, with a reduced-motion path.
  • Telemetry goes through the existing trackStudioEvent only.

Telemetry contract

Every event also carries the session properties trackStudioEvent already adds, including studio_version and tab_id (a random id per page load).

Context properties on every event below: os (the preview server's platform: darwin / linux / win32), app_installed (boolean), download_available (boolean).

Event When Extra properties
studio:desktop_cta_eligibility once per page, when the preview server answers eligibility: open_in_app / download / no_app_for_platform
studio:desktop_cta_impression once per page, when the button is first on screen (IntersectionObserver) eligibility
studio:desktop_cta_guide each guide step change step: opened / closed / download_opened / install_detected
studio:toolbar_action (existing) a download press or a hand-off press action: get_desktop_app / open_in_desktop
studio:desktop_cta_outcome how a press ended outcome: opened / sent_to_download / failed; reason when known: not-installed, open-failed, handoff-unavailable, download_tab_blocked, preview_server_unreachable

A download is never counted as an install: download_opened and sent_to_download mean a new tab opened on the download page, nothing more. install_detected fires only when the preview server reports the app installed, once per page, even if the app is removed and found again.

Correlation field: the page's tab_id (a random UUID, no personal data), passed on as hf_cta:

  • Download: https://hyperframes.dev/studio/download?…&hf_cta=<tab_id>.
  • Hand-off: POST /api/open-in-desktop with body { "cta": "<tab_id>" }. Only [A-Za-z0-9-]{1,64} is accepted. When the app took the folder, the CLI writes .hyperframes/desktop-cta.json = { "cta": "<tab_id>", "at": <ms> } in the project.

Unmatched today: nothing in the app reads desktop-cta.json yet, and hyperframes.dev does not yet carry hf_cta into the installer.

No layout shift

The real Studio build from this branch, headless Chrome at 1440x900, a fixture project. The preview server's answers were staged per scenario so every state could be driven without an app on the test machine. A probe recorded the boxes of the Compositions, Preview, Inspector and Timeline panels and of the preview stage on every animation frame, from the settled page through each press:

Scenario States driven Frames Distinct layouts
Mac, no app idle, card open, download opened, app found, opening, after 418 1
Failed hand-off idle, card at step 3, folder copied, Try again, card closed 182 1
Slow download card open, "Still looking" after 60 s 3727 1
Linux, app installed card at step 3 (drop the folder) 49 1
Windows no button 5 1

Every frame of every scenario has the same boxes (x,y,w×h): Compositions 3,43,364×494 · Preview 373,43,655×494 · Inspector 1034,43,403×494 · Timeline 3,543,1434×354 · preview stage 395,116,611×344.

Test plan

  • Tests that fail on main and pass now:
    • CLI, desktopApp.test.ts and desktopRoutes.test.ts:
      • A bare-index.html folder passes isHyperframesProject at the moment of hand-off, and a project's own marker is left alone.
      • The hand-off is offered only where the app is installed.
      • The cta id is forwarded only when valid and recorded only when the app took the folder.
    • Studio, OpenInDesktopButton.dom.test.tsx (16 of 20 fail on main):
      • The copy for each state, and that a press without the app opens the card at step 1 and hands nothing over.
      • Step 1 opens the tagged download in a new tab and drops the opener; a blocked tab names the download and leaves step 1 to do.
      • The install is found only from the preview server, asked only while the card is open, and the asking stops when the card closes or the app is found.
      • A failed hand-off opens step 3 with the folder, copy and Try again; an app that is gone sends the card back to step 1.
      • Linux with the app shows the folder to drop in and posts nothing.
      • Eligibility, one impression, click and outcome properties, and that no property carries a path or counts a download as an install.
      • One install_detected per page when the app is found, removed and found again (review finding; fails without the page latch).
    • Breaking each on purpose fails a test: polling with the card closed, counting the download as the install, going back to the old download redirect on failure, and ignoring the server's answer.
    • All pass 3 runs in a row, with Studio's style and token gates and the header tests.
  • Built and run for real: the branch's Studio and CLI build, served by hyperframes preview on a fixture, every state above driven in headless Chrome; captures below. The browser's analytics requests were blocked, so nothing reached analytics.
  • Not run: a press into a really installed app on macOS, and the card on a real Linux machine at this head. The hand-off path is unchanged from the earlier Linux and macOS runs of this PR; the guide on Linux was driven with the Linux route answer.
  • Documentation updated (if applicable): not applicable.
  • Comments follow CONTRIBUTING.md "Comments".

Before

main, no app on the machine: the button, then the press.

Before: Edit with Framey

Before: the press opens Meet Framey

After

The button, no app on the machine:

After: Install the desktop app

The press opens the guide at step 1:

After: the guide at step 1

The download opened; the card waits for the install, and the button says what is left:

After: waiting for the install

The app turned up; step 3 is ready:

After: app found, open the project

A failed hand-off: the folder to copy and Try again:

After: failed hand-off

After a minute without the app:

After: still looking

Linux with the app installed: the folder to drop in:

After: Linux, drop the folder in

Windows: no button.

After: Windows, no button

…p app and never dead-ends

The button now reads "Install the desktop app" beside Framey drawn as the app
draws it. Where the app is installed, a press opens this project in it; a
folder preview serves from a bare index.html gets its hyperframes.json first,
so the app no longer refuses it. Without the app, the button links straight to
hyperframes.dev's download, and a failed open lands there too. Platforms with
no build of the app show no button. Telemetry now records eligibility, one
impression, the click and the press outcome, with OS, app installed and
download available.
@github-actions

github-actions Bot commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 271 (base branch 271), smooth 189 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)

Unstable (3)

  • seqplainkeys-none-px-r0-root-z100: tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true
  • crop-tween-px-r0-nested-z100: tracking 0.04, pressJump 0, drop 0.09, reload 0.09, render 0.26, renderKey -, undo true, teleport true / tracking 0.04, pressJump 0, drop 0.09, reload 0.09, render 0.26, renderKey -, undo true, teleport true / tracking 0.04, pressJump 0, drop 0.09, reload 0.09, render 0.26, renderKey -, undo true, teleport true
  • seqplainkeys-none-pct-r0-root-z100: tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo false, teleport true / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true

…d, install and open

Without the app, the header button opens a card where Framey walks three steps: the
download opens in a new tab, the install is found by asking the preview server while the
card is open, and the project opens through the existing hand-off. A failed hand-off shows
the folder to copy and Try again. With the app ready the press opens the project directly.
Each step change is recorded through trackStudioEvent; a download is never counted as an
install.

@terencecho terencecho 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.

Reviewed immutable HEAD 2f73a0a52b82ab111178800888de9c9de1136f7d against effective base a971c2bb257cf3f11feddab36c2b888b0e08a1cc (API base b46ebedeae959d3060ca4ece244ea6861d0f2352). No blocking introduced defect found in the inspected source.

What holds up

  • The GET response separates installation from hand-off capability, while POST retains its same-origin checks and bounds the optional correlation ID (packages/cli/src/server/desktopRoutes.ts:36–61).
  • Bare-index marker preparation uses the existing no-overwrite publication path: desktopApp.ts:138–147 → projectConfig.ts:191–197 → writeNewFile.ts:5–17; an existing destination is not replaced by that operation.
  • The card derives its three steps from one progress value, keeps an actionable download retry, and disables automatic opening when hand-off is unavailable (packages/studio/src/components/DesktopGuideCard.tsx:164–265). The production provider/header path supplies the project folder.

Nonblocking follow-up

packages/studio/src/components/OpenInDesktopButton.tsx:180–189: install_detected is not guaranteed once per page as the PR body promises. On one page: detect installation → remove the app → Open returns not-installed (lines 240–241) → reinstall → the restarted watcher detects it again. Both calls enter the telemetry queue, which has no deduplication (studioTelemetry.ts:115–141), with the same page ID. A page-lifetime detection latch and a repeated-cycle test would enforce that contract. This is a low-priority measurement issue, not a blocked hand-off; I did not query live analytics.

Scope and limits

I fully read all nine existing changed effective-base files before all ten changed HEAD files, inspected applicable standards and the pinned review rubric, and adjudicated both independent reviewers' final reports. Supporting production paths and workflow trigger excerpts were inspected selectively, not exhaustively. Exact-head check metadata includes successful build, lint, typecheck, Studio tests and CLI smoke checks, as well as skipped and historical cancelled rows; this is not a claim that I ran them or certified their checkout trees.

This is source approval only: I did not run tests, builds, a browser, native hand-off or render QA, and do not certify visual/layout behavior or end-to-end installer attribution. The maintainer's rendered-result QA before merge remains required. This review does not clear that hold or authorize a merge. The ending snapshot has no native auto-merge request, and the inspected repository workflow triggers do not run on review approval; this does not certify external automation.

— Review by tai (pr-review)

Verdict: APPROVE
Reasoning: No source-proven blocking regression survived verification. The telemetry-contract follow-up is nonblocking, and the separate maintainer render-QA-before-merge condition remains in force.

The app removed and found again on the same page recorded install_detected twice; a page
latch keeps it to one, as the telemetry contract says.
@miguel-heygen

Copy link
Copy Markdown
Collaborator Author

Review follow-up on install_detected (found, removed, found again on one page fired it twice): fixed at 65dafff. A page latch in useGuide keeps it to one event; the route update and Framey's hop still happen each time. New test records the install once per page, even when the app goes and comes back fails without the latch (2 events) and passes with it; the Studio suites pass 3 runs in a row.

@miguel-heygen
miguel-heygen merged commit 5975dd7 into main Oct 18, 2026
110 of 111 checks passed
@miguel-heygen
miguel-heygen deleted the scrubfix/desktopcta branch October 18, 2026 21:35
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