* feat(cli): catch up on what the desktop app did since a project was handed over
The desktop app records each chat turn in .hyperframes/app-history.jsonl.
`hyperframes catch-up [dir]` prints the turns since the hand-off or the last
catch-up (what the person asked, what Framey did, which files changed) plus
the project files changed since, by the app or by hand, then marks them seen.
`hyperframes open` and Studio's Edit with Framey mark the hand-off moment.
Any command run on a project with unseen turns ends with a line naming
catch-up, so an agent back in the terminal hears about it even when it
forgets. /hyperframes and /hyperframes-cli tell the agent to catch up as
soon as the person writes again.
* feat(cli): hand a Grok terminal session over to the desktop app
Grok gives every tool command its session as GROK_SESSION_ID (grok 1.0.4),
so `hyperframes open` and Edit with Framey now name a Grok session in
.hyperframes/agent-handoff.json, after Claude Code and Codex. The desktop
app reads its turns from Grok's own session files.
* feat(cli): tell a new project's agent to catch up after the desktop app
Scaffolded projects' CLAUDE.md and AGENTS.md now say to run
`npx hyperframes catch-up` before the next change once the project was
opened in the desktop app. Claude Code, Codex and Grok read these files on
their own, so a resumed session catches up even when it never loaded a
HyperFrames skill: a resumed Claude Code session that had not, asked to
change the title, ran catch-up first.
catch-up also prints each reply on one line.
* fix(cli): read and write .hyperframes records without following links, mark seen after showing
Review (Jerrai, Rames):
- One helper (projectRecords.ts) now reads and writes every record in
.hyperframes/. A linked folder is refused. A read opens once with
O_NOFOLLOW and checks the open file is a plain file, which clears the
CodeQL race. A write goes to a temp file renamed over the record, so a
link is replaced, never written through. agent-handoff.json, the seen
record and the history all go through it.
- catch-up marks a turn seen only after showing it, and only up to the
newest turn shown, so a turn written just after the read shows next
time. Changed files have their own "checked" time.
- Turns show only once the project was handed over from this machine, so
a history file shipped in a cloned repo is never shown as the person's
words. The end-of-command check skips reading when the history is
older than the seen record.
- The skills mention catch-up only when the CLI printed a line naming it,
so an older CLI never meets an unknown command.
- The notice test uses basename, which fixes Windows.
* feat(cli): add the catch-up line to an older project's instructions on hand-off
A project scaffolded before catch-up existed has no line telling its agent
to catch up, and a resumed session that never loaded a HyperFrames skill
then edits without knowing what happened in the app. When `hyperframes open`
or Edit with Framey hands such a project over, its scaffolded CLAUDE.md and
AGENTS.md (they start "# HyperFrames Composition Project") gain the
template's line, in the template's place. A file of the person's own, a
link, a missing file, or one that has the line is left alone. The file is
rewritten through a temp file with its mode kept. A test keeps the line the
same as the template's.
* fix(cli): keep catch-up's cursor in the person's home, never reset it, read without links on Windows
Review (Rames, Jerrai):
- The seen record moved from the project to ~/.hyperframes/catch-up/,
keyed by the project's real path. A seen file a cloned repo commits
can no longer make its history read as the person's words.
- A second hand-off (`open` again, or another Edit with Framey click)
keeps the cursor, so turns the app recorded meanwhile still show.
- Reads lstat the path, open it once, and refuse a file whose dev/ino
differs from what lstat saw. That holds where O_NOFOLLOW doesn't
exist, fixing the Windows test. The note writer reads the same way,
which clears the new CodeQL alert.
- Writes use core's replaceFileAtomically, which retries a rename that
Windows briefly refuses.
- catch-up names every changed file, and the cursor is the newest `at`
shown, not the last line's.
- `open --json` carries `catchUp` after a hand-off.
- The bin scripts are back to mode 644.
* fix(cli): open before checking for CodeQL, never show a turn dated ahead
Review (Rames, Jerrai):
- readPlainFile opens the file first, then requires lstat to find the same
plain file (dev/ino) at the path, and reads only through the fd. That is
just as safe, and it clears CodeQL's check-then-use alert.
- catch-up skips turns dated after the moment it reads. So a cloned history
can't show a far-future turn as the person's words or park the cursor
past real ones.
- The seen key uses core's realpath (realpathSync.native), so letter case
can't split a project in two. ~/.hyperframes/catch-up is made 0700, and
a hand-off that can't note itself says so.
- Tests stub HOME with vi.stubEnv and restore it.
* docs(cli): map the pinned Sherpa runtime loading contract
* fix(cli): use a healthy pinned Sherpa copy beside the CLI
* chore(cli): keep runtime loader comments within the ratchet
* docs(cli): wrap the runtime contract palette declarations
* chore(cli): keep the Sherpa contract note out of the repo
* fix(studio): undo takes back the first edit of a film whose media is still being copied
* fix(studio): the first undo of a film repaints the preview in place
* fix(studio): background media copies keep the media ledger and survive a history prune
* fix(studio): a busy project history never makes writes wait for its lock again
* refactor(studio): name the restore stamping instead of commenting it
* perf(studio): undo stamps only a restore side that has no stamps
* fix(studio): closing a project history finishes its media copy first
* fix(studio): media copies stay resumable until recorded and skip a replaced folder
* fix(studio): the history lock wait comes back after a successful open
* feat(core): the renderer's font pipeline is importable from @hyperframes/core/fonts/embed
* style(core): format the font pipeline and state the bundled-subset invariant
* test(producer): repin the test lane map after the font tests moved to core
* fix(core): images build fonts once, CLI bundles one pipeline, font tests pass on Windows
* test(core): font retry tests get a budget no slow machine can exhaust
* test(core): the Retry-After budget test fails when the budget is ignored
* fix(cli): a project .env cannot set the feedback email in another letter case
* fix(cli): capture loads a project .env through the same rules as the CLI
* 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 #4951 at 58934a34.
- 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
* 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 #4951 at 58934a34.
- 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.
* fix(studio): the edit-with-framey button asks its route through studioApiFetch
* fix(core): composition scripts start after web fonts are ready
A scene script that measures text (offsetHeight, getBoundingClientRect) ran while
the page parsed, often before its web font loaded, so it laid the scene out with
fallback metrics. Renders of the same film differed run to run, and preview could
differ from export.
The compilers now give every body script a type the browser does not run
(text/hf-after-fonts, or +module), through one helper in scriptRuns.ts called
at the end of the bundler, the producer compile and the sub-composition preview.
The Studio motion script is emitted the same way so it still runs after them.
The runtime entry owns the wait: on DOMContentLoaded it forces a layout, waits for
document.fonts.ready or FONT_WAIT_TIMEOUT_MS (5 s), then re-creates each script
in its own place in parser order (classic in order, awaiting src loads; then
module and defer scripts in insertion order), boots as before, and calls the
DOMContentLoaded and load listeners the scripts added. On timeout the scripts
run anyway and runtime_font_wait_timeout names the families still loading.
Scene swaps and the composition loader use the same wait.
* fix(core): a page whose runtime predates the font gate still runs its scripts
A bundle can be served with a runtime loaded from HYPERFRAME_RUNTIME_URL. If that
runtime is older than the font gate, nothing would run the deferred composition
scripts and the film would be blank.
Whenever the marking helper defers scripts, it also puts one small inline script
first in the head. At DOMContentLoaded it runs the deferred scripts in parser
order itself, without waiting for fonts and with a console warning, unless a
runtime has claimed them. The gate's runtime claims them when it loads, before
DOMContentLoaded, so the scripts run exactly once either way.
* fix(studio-server): motion paths animate in Studio preview when gsap loads in the body
The MotionPathPlugin tag Studio inserts after a body gsap script now carries
that script's type, so the runtime runs it right after gsap once fonts are
ready, instead of before gsap exists.
* fix(cli): layout, motion-shot and validate wait for the composition scripts to run
The three commands waited on the timeline registry, which exists as soon as the
runtime file loads, or on a flat sleep. They now wait for the runtime's
render-ready flag through one shared helper, keeping each command's timeout and
proceeding as before when it passes.
* fix(core): a script that wraps addEventListener keeps its wrapper after its scripts run
While composition scripts run, the runtime patches addEventListener to hold
load events that already passed. Putting the original back now happens only if
the patch is still in place, so a library wrapper installed meanwhile survives.
* docs: comments on script timing match scripts that wait for web fonts
The early stub's flush and the player's iframe-load handler described page
scripts as done by DOMContentLoaded and the runtime as ready before load.
Composition scripts now wait for web fonts, so both can come later. Comment-only.
* test(core): pin script order after an external script and the scene swap's font wait
An inline composition script that follows an external one sees its global, and
a scene swap runs the new scene's script only once web fonts are ready.
* docs(producer): the early stub comment names what drains late font-gated work
* fix(studio): motion paths add without a flash when gsap loads after web fonts
Studio preloads MotionPathPlugin on iframe load. A body gsap now runs after web fonts, often after that load, so the preload retries once the runtime reports ready.
* test(cli): pin the runtime wait before each command first samples the page
* fix(studio): the motion path preload stops waiting once the preview runtime has booted
A composition with no global gsap posts ready before the preload runs; the retry listener then never detached and kept each retired preview document alive. It now waits only while the runtime has not booted, and the tests pin the cleanup and the ready filter.
* fix(studio): a preview frame that never boots is not kept alive by the motion path preload
Only the latest preview waits for the ready message of its runtime; a newer load drops the older wait, so a crashed or missing composition frame can be collected.
* style: comments added by the font gate move into names
* fix(media-use): a slow download no longer leaves an empty asset that undo brings back
* docs(media-use): tell the person an OAuth sign-in makes voice and avatar video free
* docs(media-use): the OAuth free usage line names the monthly allowance
* docs(media-use): the OAuth line says a free allowance
* feat(cli): feedback says whether a person or an agent wrote it, with the email an app attaches
* refactor(cli): split the .env loader so each part stays simple
* fix(cli): a broken .env loader fails loudly; only a missing .env is ignored
* fix(cli): publish with an expired login stops and asks you to log in again
The server now answers 401 to a rejected credential instead of publishing
anonymously, so the CLI says so and drops the warning for the old fallback.
* fix(cli): publish refreshes an expired login first, and names a rejected API key
An expired access token with a refresh token is refreshed (the cloud commands
already do) instead of being sent and refused. Only the requests that carry the
credential map a 401; an environment API key is named, since logging in would
not replace it.
* fix(cli): an expired login without a refresh token asks you to log in again
A stored login past expiry with no refresh token now resolves as
LOGIN_EXPIRED instead of "not signed in", so publish stops with the
re-login message before sending anything. A signed-in publish that the
server answers with an unclaimed project fails the same way and links nothing.
* fix(cli): keep the resolver comment share at its base
* fix(cli): an API-key publish still succeeds, and only an owned publish is linked
* fix(cli): --update and --space refuse an API key instead of being ignored
* fix(cli): publish uploads with the credential it checked, so --update and --space are never dropped
* fix(cli): a login that expires during the bake is refreshed, not reported as expired
* fix(cli): an expired login that cannot refresh stops publish before anything is sent
* fix(cli): a token refresh never writes over a login that replaced it
* fix(cli): a refresh that cannot read the saved login reports that, not a changed login
* fix(studio): load MotionPathPlugin at the composition's own gsap version
* fix(core): list the gsap-cdn subpath where core's exports are generated from
* fix(core): every default gsap URL comes from the one gsap-cdn owner
* fix(core): the gsap a new composition or first tween gets comes from the gsap-cdn owner
* fix(cli): close every browser on SIGTERM and SIGINT through one launch owner
* fix(cli): own SIGTERM before the GPU probe browser launches
* fix(cli): keep tests on the managed GPU mode and drop the engine signal flag
* test(cli): give the browser fallback fake a process handle
* test(cli): signal the GPU probe case only once the signal owner is installed
* test(cli): mark the launching case only after the signal owner is installed
* test(cli): time history turns on one injected clock instead of real waits
* fix(studio-server): end an idle history window by its own clock, not a bare timer
* fix(studio-server): cap a history window's idle timer at Node's timer range
* fix(studio): show each timeline row its own layer instead of a grey bar
A row got a frame only if it spanned under 92% of the film and its id did not
look like a backdrop; every other row drew a plain grey bar. Every element row
now gets its frame, and the capture fades the row element's siblings first, so
the frame shows that layer and not the whole film. Studio's dev server and the
CLI share one capture helper; Studio's own copy is gone.
* fix(studio): read timeline rows from any realm so each keeps its selector
* refactor(studio-server): always isolate the element in a thumbnail clip
* fix(cli): undo a thumbnail's isolation before its page takes the next capture
* chore(core): generate the dom-realm export and trim thumbnail clip comments
* fix(studio-server): restore sibling opacity after a clip, hash selectors in cache keys
* fix(studio-server): keep faded siblings in a page-owned list so restore is exact
* feat(studio): draw a text layer row as its own live text instead of a captured picture
* refactor(studio): move timeline text reading into its own module
* fix(studio): keep text rows readable and current, and leave painted layers as pictures
* fix(studio): refresh a canvas-edited text row by its own identity and source file
* fix(studio): refresh a canvas-edited text row known only by its data-hf-id
* fix(cli): select containing source frames in snapshots
* fix(cli): find the containing frame from packet timestamps, not a full decode
Probing decoded frames and selecting by index decoded the source from its start, so a sample 290 s into a 1080p clip took about 35 s and hit the 30 s extraction bound. Packet timestamps need only a demux, and an accurate -ss to the midpoint before the containing frame decodes from the nearest keyframe.
* fix(cli): end snapshot's ffprobe options with -- before the input
* fix(cli): seek snapshot frames from their keyframe and skip unreadable videos
Fragmented MP4 seeks by dts and MPEG-TS seeks imprecisely, so an input seek to
the frame midpoint returned the next frame before each keyframe (fMP4) or a
corrupt or missing frame (TS). Seek to the keyframe that starts the frame's
decode instead and pick the frame by decoded time with a select filter.
A failed or unparsable ffprobe now returns null, so one unreadable video is
blanked with a warning instead of aborting the whole snapshot.
* fix(cli): keep open-GOP leading frames and extract without ffprobe
The keyframe walk went by decode order, so an open-GOP leading frame seeked to
the keyframe that follows it, the decoder dropped it and the next frame was
returned. Only keyframes at or before the frame in presentation order count.
Without ffprobe, extraction falls back to a plain seek instead of failing the
snapshot, as it worked before frame selection needed ffprobe.
* fix(cli): measure snapshot frame times from the exact video start
ffprobe prints format.start_time rounded to microseconds. When it rounds down,
t=0 found no frame and every exact frame time picked the previous frame (24 fps
fragmented MP4 with B-frames, many MPEG-TS files). Subtract the stream's
integer start_pts in time-base ticks when the video sets the format start.
* fix(cli): probe snapshot packets to an absolute end past edit-list preroll
A stream-copy trim keeps preroll packets before the first visible frame. The packet
probe read a window relative to the first packet, so it stopped before the requested
time and picked an earlier frame. Probe the stream start first, then up to start + time + 1 s.
* fix(cli): keep snapshot ffprobe argv a literal ending in -- before the input
The producer ffprobe argv contract reads each argv literal and requires -- right before the input;
the probe helper spliced flags in from its callers, so the contract saw argv without it.
* fix(cli): select snapshot frames on the video stream clock when audio leads
When the video starts after the container (TS with audio, late video), FFmpeg puts zero at
the first video frame after a seek to 0, so the threshold picked the next frame, and times
before the first frame gave none. Select on raw stream time (-copyts) and hold the first frame.
* fix(cli): measure snapshot frame selection from the first decoded frame
-copyts kept raw time in the filter but changed which frames survive the seek trim, so a
file whose timestamps start below zero froze on a late frame. Seek to the keyframe display
time and select relative to start_t; the no-ffprobe path keeps the plain seek.
* fix(cli): anchor snapshot frame selection on the keyframe a seek lands on
MPEG-TS seeks can land a whole keyframe group late, and an open-GOP keyframe is not a clean
start, so a predicted start_t picked later frames. Read the landing keyframe with a stream
copy, seek exactly to it so its leading frames are trimmed, and select relative to it.
---------
Co-authored-by: Miguel Angel Simon Sierra <miguel.sierra@heygen.com>
* fix(cli): a background preview launch keeps only the server its own child started
* fix(cli): the detached preview child always starts its own server
* fix(cli): reject extra positional arguments on every command
* fix(cli): read the command name without assuming a resolved meta
* fix(cli): join free-text words for catalog and tts, reject a bad repo root
* test(cli): load the listed commands through static imports
* fix(cli): keep a tts file path out of the word join and read the parsed json flag
* fix(cli): apply the file exception only to a tts .txt path
* fix(cli): keep usageLine private and test the json fallback for undeclared json
* fix(cli): keep extra arguments and the repo root value out of telemetry
* test(cli): type the skills update lookup
* fix(cli): read json mode from the parsed flag, reject tts words with --text-file, restore add's hint
* fix(cli): check tts words with --text-file before --list and test --json=false
* fix: honor explicit --port when reusing a background preview server
startBackgroundPreview never compared a reuse candidate's actual bound
port against the caller's explicitly requested --port before returning
it, so a second `preview --port <N>` call could silently return an
existing server on a different port instead of honoring the request.
Reuses the existing PreviewServerPortMismatchError (already used by
findPreviewServerForProject for the same discipline) instead of adding
a new error type: an explicit --port that doesn't match the reuse
candidate now throws a clear conflict rather than substituting the
wrong port silently. A bare launch with no --port keeps reusing any
project-matching server, unchanged.
Deliberately out of scope: the interactive/embedded launch path
(runEmbeddedMode -> findPortAndServe) has the same silent-substitution
shape when a same-project server is found mid-scan at a port other
than the one requested. That path's output isn't the JSON lifecycle
schema this bug was reported against, and fixing it would mean
touching the interactive dev-server bind/scan loop rather than a
simple pre-return check, so it's left as a follow-up candidate.
Co-Authored-By: Miguel Angel <miguel.sierra@heygen.com>
* fix: honor explicit --port on every background preview exit path
`startBackgroundPreview` already refused to reuse a same-project server on
the wrong port when --port was explicit, but the fresh-launch path still
trusted whatever port the detached child bound. The child scans upward from
--port and takes the first free port, so `--background --port N` with N busy
reported `{ type: "started", port: N+1 }` and recorded ownership of it.
- After the child comes up, compare its port with the explicit --port. On a
mismatch, reap the wrapper, wait until the substitute stops answering, and
throw `PreviewPortUnavailableError` (JSON code `preview-port-unavailable`).
If the substitute never stops, fail loudly instead of reporting it.
- Share one `unmetPreferredPort` predicate between the reuse and launch
paths, and one `awaitServerGone` wait between the reap path and
`stopBackgroundPreview`.
- When several same-project servers are running, prefer the one on the
explicit --port as the reuse candidate instead of the lowest port.
- Map launch failures to JSON codes in one place
(`backgroundStartFailureCode`).
Tests: preferred port bound → reported as-is; preferred port taken → reaped
and rejected with both ports, no session record; substitute that keeps
serving → distinct error; bare launch keeps the next free port; explicit
port picks the matching sibling. Deleting the launch-path check, the
candidate ordering, or the post-kill wait each turns its test red.
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
* fix(cli): reuse the same-project server on the explicit --port
`startBackgroundPreview` picked its reuse candidate before honouring
`--port`, so with an owned server on 3002 and an unmanaged same-project
server on 3003, `--port 3003` rejected with "No Studio preview server …
on port 3003. Matching server port: 3002" even though 3003 was running.
- A policy-matching same-project server already on the explicit port is
now the reuse candidate, owned or not; the owned server stays
authoritative otherwise, and an explicit GPU-policy change on the
requested port still replaces it.
- `PreviewServerPortMismatchError` receives every policy-matching
same-project server, so the ports it reports are the ones running.
- `policyMatchingServers` is the single owner of the same-project +
GPU-policy predicate; `matchingServer` derives from it.
- `BackgroundPreviewResult` names the launch result union once;
`PREVIEW_PORT_MISMATCH_CODE` replaces three literal error codes.
Tests: unmanaged sibling on the explicit port is reused without touching
the owned server; mismatch error lists every same-project port and no
foreign-project port; owned server on the explicit port is replaced on a
GPU-policy change. Each new check fails when its guard is removed.
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
* refactor(cli): keep the --port honour comments short
* fix(cli): drop the scan binding stopBackgroundPreview no longer reads
* refactor(cli): keep comment share at base in the preview lifecycle files
* fix(cli): reuse a same-project server on an explicit --port beyond the scan window
* test(cli): pin the JSON codes for a background start that misses --port
* refactor(cli): probe an out-of-window --port in its own helper
---------
Co-authored-by: Miguel Angel <miguel.sierra@heygen.com>
* fix(cli): hide console windows on Windows for background/detached spawns
Add windowsHide: true to 8 spawn/execFile call sites that were missing
it: ffprobe/ffmpeg probes in the producer and engine packages, the
telemetry exit-flush and studio dev-server spawns, and the browser-open
and background-preview detached launches in the CLI.
previewLifecycle.ts's spawnDetachedPreview redirects stdout/stderr to a
raw log-file descriptor; libuv only adds Windows' CREATE_NO_WINDOW flag
when no stdio slot inherits a raw handle, so windowsHide alone does not
fully suppress a console flash there (documented inline) — it's still
added for consistency and because it correctly sets the process's
show-window state regardless.
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
* fix(cli): correct windowsHide flash-risk claims and add missing dev-server spawn tests
- The previewLifecycle.ts comment (and its mirrored test comment)
overstated the residual console-flash risk on the detached background
preview spawn: DETACHED_PROCESS already leaves that child without a
console regardless of whether CREATE_NO_WINDOW gets applied.
- Add the two missing regression tests for preview.ts's runDevMode and
runLocalStudioMode spawns, exporting both for test access.
- Drop an unused export in audioPadTrim.ts flagged by the repo's dead-code
gate; the constant is only ever used within its own file.
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
* fix(engine): hide console window for the drawElement self-verify PSNR comparison
psnr.ts's execFile call runs in-process inside the Studio server's render
path (parallelCoordinator.ts's drawElement disk self-verify), so a render
started from Studio's UI under a detached preview hits this call from a
console-less parent — the same flash case the other windowsHide sites in
this PR already cover. psnrFilterAvailability.ts's probe is on the same
in-process render path and already had the flag.
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
* fix(cli): keep only the spawn sites main still misses and leave browser windows visible
windowsHide maps to libuv UV_PROCESS_WINDOWS_HIDE, which also starts GUI apps with SW_HIDE,
so the detached browser launch must not set it. The background preview child already has
no console (DETACHED_PROCESS with inherited log fds), so its flag was a no-op. The
telemetry, psnr and audioPadTrim sites shipped on main via #3839 and #4604.
* fix(cli): stop the studio dev server when the terminal closes
With windowsHide the dev server runs in its own hidden console on Windows, so closing the terminal
only signals the wrapper (SIGHUP). Handle it like SIGINT and SIGTERM and kill the process tree.
* fix(studio-server): hide the console window for loudness measurement
The background preview has no console, so its ffmpeg loudness run opened a visible window on Windows.
* refactor(cli): drop the comment on the studio shutdown signals
---------
Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
* fix(cli): stop browser --force from deleting chrome under running renders
* style(cli): format browser manager test
* fix(cli): sweep leftovers of killed browser installs and on browser clear
* docs(cli): state what browser clear returns without history wording