All notable user-facing changes to Hovercraft. Engineering scaffolding (Day 1–3 plumbing, repo seeding, build pipeline) is summarised rather than enumerated.
[v1.0.0] — 2026-05-02
Leaving beta
Hovercraft is 1.0. No code change from v0.9.62 — promoting the build that landed the V-hide backdrop fix, the lock button, the Welcome deck rebuild, the slide-strip active state, and the FF appearance restore. Landing page drops the "Beta" eyebrow, pricing renames the entry tier to "Hovercraft Lite", and the gated download endpoint now serves Hovercraft-1.0.0.dmg.
[v0.9.62] — 2026-05-02
V-hide no longer leaves a soft-focus rectangle behind
Caught while shooting the demo video: with a slide in F (full-frame, soft-focus background), triggering V to hide the window left a visible rectangle of blurred camera floating over an already-sharp world for the duration of the fade, then snapped off. Cause: the frosted-glass plane that lives behind the slide rounded-rect was drawing at full strength as long as the slide was nominally on-screen, while the slide above it faded smoothly and the global background blur racked back to sharp on its own (front-loaded visibility²) curve. Three layers, three different envelopes. Coupled the backdrop fill's alpha to the slide's opacity so the frosted-glass plane fades on the same envelope as the slide it sits under — the rectangle now dissolves cleanly instead of hovering.
[v0.9.61] — 2026-05-02
Lock / unlock now has a button
Two new entry points for the input lock that previously only responded to the L hotkey and the L hand pose:
- A lock button in the chrome's source toolbar, sitting next to the eye (hide-slide) button. The icon flips between
lock.openandlock.fillto mirror state; tooltip swaps too. Same toggle path the L key uses, so the three entry points share a single state machine. - The "Locked" pill that floats in the top-trailing corner of the hero preview is now click-to-unlock. A presenter who's mid-call and wants pinch back doesn't have to hunt for the chrome — they click the affordance that's already in their face. Hit-testing is gated on visibility so the invisible (unlocked) pill doesn't catch stray clicks in the corner.
Capture window — TCC reset hint for multi-build users
If a user has more than one Hovercraft build installed (a developer build alongside the release, an old App Store install alongside a hand-notarized one, etc.), macOS coalesces them into one "Hovercraft" row in System Settings → Screen Recording but keeps a separate TCC grant per code signature. Toggling the visible row only flips one of them, so the other build stays denied no matter how many times the user retries. The Capture-window permission empty state now has a collapsible "Already enabled but still denied?" disclosure that explains the cause and offers a one-click copy of the tccutil reset ScreenCapture vision.sandwich.Hovercraft command. Collapsed by default so the primary "open Settings → enable" path stays the obvious one for normal users.
Help → Load Hovercraft Guide + ? button
The Welcome deck now has a second discovery surface beyond the Help menu — a small circular ? button anchored bottom-right of the chrome (visual register matches the camera-picker glyph). Clicking it opens a confirmation dialog ("Load Hovercraft Guide? — This will replace what you currently have loaded") so a stray click during a live share doesn't blow away the user's current deck or window capture. The Help menu item, renamed from "Show Welcome Deck" to "Load Hovercraft Guide" to match the dialog's title, loads the deck directly without confirming — picking a menu item is already a deliberate two-step gesture.
Welcome deck — Manual page, hotkey legibility, sign-off
Three changes to the bundled walkthrough:
- New page 6 ("Manual") inserted between Hotkeys and the sign-off, mirroring the Pinch page's layout. Tells users that mouse and trackpad still work for everything; the gesture story doesn't replace the manual one. Deck is now 7 pages.
- Page 5 (Hotkeys) drops the rounded-rect "keycap pill" container that earlier revisions used. Bare bold monospaced letters in a right-aligned column read unmistakably as keys without competing with the surrounding type, and the two-letter "FF" cap no longer renders at a different shape than the single-letter caps.
- Page 3 (Push) the z-axis arrow is rebuilt as a single outline glyph: open V at each tip, horizontal base wings extending out from the shaft, no fill. The earlier three-tone monochrome fill read as a literal solid object that competed with the slide content; an outline reads as a diagram and sits more politely next to the hand-drawn illustrations on surrounding pages.
- Page 7 (sign-off) drops the "Tell all your friends. / sndw.ch/hover" footer in favour of larger affordance cards (580×320, up from 440×180) and a smaller "Then show all your friends." line below them. The URL was unevenly spaced at preview scale and there's no good way to copy a URL out of a deck loaded into a Zoom share anyway.
Hotkey hint strip — wider keys, more breathing room
The persistent hotkey hint strip in the chrome (V / F / FF / C / L) had two cramped patches: the keycap pills were tight-fitting squares that ballooned into a different shape for the two-letter "FF" cap, and the spacing between cap+label clusters was 14pt — close enough that the strip read as one dense line rather than five discrete entries. Dropped the pill containers entirely (consistent with the deck's page 5 treatment) and bumped the inter-entry spacing to 22pt.
[v0.9.60] — 2026-05-01
FF appearance restores when pulled out manually
When the user pinched out of FF (full-frame) without pressing F, the slide stayed sharp-cornered and fully opaque indefinitely until they pressed F. Root cause: the FF entry path snapshots the user's tuned opacity and cornerRadius and forces opacity to 1.0 / cornerRadius to 0 for the FF hold, but the only path that animated those back was performFillRestore — invoked exclusively from F. A manual pinch-out left the appearance overrides parked. Fixed by hooking pinchActivity.isPinching and, on transition false, checking whether we still hold FF appearance snapshots AND the transform has left every fill state. If both, we fire the same animateRestoreAppearance the F-press uses, so a manual pull-out feels identical to an F-press pull-out from the appearance side.
Slide-strip active state
The slide-strip overlay along the bottom of the floating viewer now actually shows which slide is active. Prior builds shipped the visual treatment for the active thumbnail (full opacity, soft halo, 1.05× scale bump) and the "n / N" counter pill, but neither updated as the user navigated, because SlideDeck.currentIndex was a computed property that delegated to the underlying SlideSource. The @Observable macro only instruments stored properties, so mutating the source's currentIndex directly never woke up the SwiftUI views that subscribe to the deck. Fixed by mirroring the index into a stored @Observable property on SlideDeck that every navigation method (next, previous, jump(to:), source-swap loaders) writes alongside the source-of-truth source. UI reads the mirror; the texture renderer keeps reading the source.
Onboarding deck — new authoring model
The Welcome deck (Hovercraft/Welcome to Hovercraft.pdf, surfaced on first launch and via Help → Show Welcome Deck) is rebuilt around a new authoring model: the Swift script is a typesetter (layout, typography, copy, theming, slide-strip safe-area math), and every illustration on the deck lives as a PNG asset under scripts/welcome-deck-assets/ that the script loads by name and drops into a named slot on the relevant page.
The v0.9.59 deck procedurally drew the V and L hand poses from CoreGraphics primitives. The result read as schematic at best and didn't share a visual register with Apple's hand.pinch SF Symbol it was paired with on the Pinch page. v0.9.60 replaces all three hand drawings (Pinch, V, L) with high-quality line-art illustrations dropped in by the user. Swap workflow: regenerate the PNG with the same filename, re-run swift scripts/generate_onboarding_pdf.swift. No code change required.
Slide-strip safe area in the deck
Every page now reserves a 200pt floor (~22% of canvas height) that the in-app slide-strip overlay occludes when the deck is loaded into the floating viewer. Bottom-edge content (keystroke hint, friends URL, hotkey rows) shifted up so nothing important hides behind the strip. The page background still paints to the bottom edge so the strip's translucent glass has surface to sit on.
Light theme — cream
Light pages (Pinch, Hide & Lock, Hotkeys) shifted from #F4F5FA (pale lavender) to #FAF8EC (warm cream) to match the canvas color of the new hand illustrations. Image edges sit invisibly on the page, no card-edge mismatch.
Help → Show Welcome Deck
Carryover from v0.9.59. Surfacing it again because the menu item is the way users will get to the rebuilt deck after first launch.
[v0.9.59] — 2026-04-30
Hand-pose gestures: V (peace sign), L
Two hand gestures that mirror existing keyboard hotkeys, so a presenter who's already gesturing on camera can drive the most common controls without reaching for the keyboard.
V (peace sign) — index and middle extended, ring and pinky curled (thumb don't-care, since both "thumb tucked" and "thumb out" peace signs are common) — fires the V key path, toggling the slide between visible and hidden in the camera feed.
L — index extended, thumb extended, middle / ring / pinky all curled — on either hand fires the L key path, toggling the input lock that gates pinch and cursor inputs.
Both gestures run through a new HandGestureRecognizer that lives next to PinchRecognizer over the same HandPoseFeed stream — separate machines so changes to the static-pose vocabulary can never regress the continuous pinch tracking we've been tuning across the v0.9.5x cycle. Each pose requires a 200ms continuous hold before firing to reject transient classifications.
Latching — the critical "fire once per held pose" guarantee. After a gesture fires, it enters a latched state: no further fires of that gesture until the arming pose has been continuously released for 100ms. Single-frame Vision flickers (confidence dips, brief occlusion) don't count as a release, so a held V-sign produces exactly one toggle, not a cycle. The 100ms threshold is two frames at 30fps plus a frame of margin for noisy lighting.
Per-finger "extended" classification uses the ratio dist(tip, wrist) / dist(PIP, wrist) against a 1.15 threshold — robust at any hand orientation, doesn't drift with hand size or distance from camera, and the gap between an extended-finger ratio (>1.3) and a curled-finger ratio (<1.0) is wide enough that natural in-between poses don't ping-pong across the boundary. Joint confidence floors match PinchRecognizer's 0.4 standard, so noisy frames are treated as "no clear classification" rather than as reset events.
The recognizer is actively suppressed while a pinch session is live, so a pinch-then-release with the hand still in frame can't false-fire as the hand reshapes. Suppression also clears any in-flight hold timer so the post-pinch reshape always starts from a clean slate.
An open-palm swipe gesture for slide nav was prototyped and cut. Extensive testing showed the open-palm pose was geometrically too close to a transitional hand shape between idle and pinch — wave motions tripped false-positive pinch begins, which then suppressed the gesture recognizer for the duration of the spurious pinch, which made waves feel like they "wore out" after the first few. Bidirectional gating (refuse pinch while open-palm is classified) untangled the interlock cleanly but the underlying classifier confusion was a fundamental product issue, not a tuning issue. Slide nav stays on the keyboard arrow keys, which is the better experience anyway. Lesson banked for any future motion gesture: prove the arming pose can't be confused with the pinch grip's transitional shape before tuning the trajectory layer.
F-toggle — FF pulls opacity and corners to a "presentation surface", plus deferred restore from margin
A double-tap on F (FF) escalates the slide to full-bleed fill, and now also pulls two appearance properties to their "screen" values: opacity goes to 1.0 (fully opaque) so the slide reads as a presentation surface rather than the slightly-translucent glass plate the user's tuned scale → opacity curve normally lands at near-fill scales, and corner radius animates to 0 so the slide hugs the canvas edge-to-edge instead of carrying its glass-pane rounded corners into the bleed.
Both transitions are animated, not snapped. The opacity curve and corner radius interpolate over the same 0.65s smootherStep envelope as the FF transform animation, driven by a per-frame display-link animator running in lockstep — so the slide grows, opens up, and squares its corners as one fluid composed move. The matching restore (single F from full-bleed) reverses both animations in sync, so as scale eases off the fill target the corner radius rounds back and opacity flows back along the user's tuned curve. The user's tuned values are snapshotted on entry-to-fill and held verbatim until the restore animation completes, so a dev-panel slider edit during a long FF hold doesn't pop the corner back to rounded mid-presentation.
The single-F → margin path is unchanged — opacity stays on the user's curve at the margin scale (the Day-5 PRD §4.3 "you sense the camera through it" feel) and corner radius stays at the tuned glass radius. Only the FF escalation forces both.
The other F-toggle fix in this build: pressing F at the margin-fill state now waits the same 350ms doubleTapWindow before restoring to the pre-fill pose, so a follow-up F (FF) can catch the pending restore and escalate to full-bleed instead. Without this, the sequence "F → margin → F-F to full-bleed" briefly retreated toward the original pose between the two F's of the FF before reversing course, which read as a stutter. From the at-full state, restore is still instant — there's no further escalation to disambiguate, so no waiting needed. Any new F press, deck swap, or other action cancels a pending deferred restore.
Edge case handled: a deck swap while parked at full-bleed cancels any in-flight appearance animation and instantly restores the user's opacity curve and corner radius so the new deck doesn't inherit the forced overrides.
Onboarding deck — V/L gestures, FF and C hotkeys, dot strip retired
The bundled "Welcome to Hovercraft" PDF that auto-loads on first launch grew to six pages to absorb everything that landed across the v0.9.5x cycle. The structural shape stayed the same — Welcome → gestures → keyboard → make-it-yours — but every part of the gesture and keyboard arc got fuller coverage.
Page 4 is new — Hide & Lock — a single light-themed sheet pairing the V (peace sign, hides the slide) and L (index + thumb, locks input) hand gestures, with a verb headline and a custom hand glyph for each. The glyphs are drawn from primitives (palm + per-finger stroked capsules + thumb) rather than SF Symbols — Apple's San Francisco set ships hand.pinch (used on the Pinch page) but no peace-sign or L-sign hand glyph as of macOS 26.2 SDK, so V and L are bespoke. They're proportioned and stroke-weighted to read as a family with the pinch hand: same line register, same scale-relative stroke width, same hollow-outline silhouette.
The Hotkeys page extends from three rows to five — F (fill the frame), FF (full-bleed fill), V (hide the slide), L (lock input), C (recenter the window) — matching the live in-app hotkey hint strip exactly. Row pitch tightens from 100 to 86pt and body type drops from 48 to 44pt to fit the two new entries on the page without crowding the headline above or the bottom margin.
The bottom progress dots are gone. The live slide-strip in the floating window already shows page count and current position; rasterized dots inside each slide were redundant and stole vertical real estate from the body composition. Each page got a small rebalance pass to take advantage of the freed bottom margin (Welcome's keystroke hint sits 8% lower; Pinch's hand glyph drops 4%; Push's body copy drops 4%).
A new "Help → Show Welcome Deck" menu item loads the bundled PDF on demand, so users who skipped past the deck on first launch — or who want to reference it again after the FF/C/V/L additions — can pull it back up without hunting through Finder for a sample file. Wired through NotificationCenter because the menu commands closure can't reach the rootView's slideDeck state directly.
App icon — H corners pulled toward the macOS 26 squircle radius
The H letterform's outer corner radius bumps from 0.039 → 0.12 (in the icon's normalized unit space — the H bars are 0.40 wide, so this is roughly 30% of bar-width). The change pulls the H's outer silhouette closer to the corner-rounding character of the macOS 26 squircle the icon sits inside, so the letter and its container read as the same family rather than as a square letter dropped onto a rounded backdrop. Inner corners (the four pairs that bracket the crossbar's negative space) stay sharp at 0, preserving the H's letterform legibility — only the outer silhouette warmed up. Re-exported through the IconLab pipeline; both Hovercraft/AppIcon.icon (Tahoe Liquid Glass bundle) and Hovercraft/Assets.xcassets/AppIcon.appiconset (legacy raster) regenerated in lockstep so platform variants stay visually identical.
Hotkey hint shows FF and C
The compact hotkey strip under the welcome chrome now lists FF (full bleed) and C (center) alongside V, F, and L. The double-tap-on-F shortcut and the new C composition tool were easy to miss without the hint — now they're right there with their siblings.
Blur leads the slide's exit on V-hide
A subtle visual artefact: when V faded the slide out, the global camera blur (which is scale-coupled and rides the slide's interpolated visibility) faded along with it on the same linear envelope. The eye read the two channels — slide fading and world resolving — as separate perceptual events sharing a clock. Adam's complaint: "the blurred background lingers as blurred for a split second before it goes sharp." A first attempt that gated blur off the target visibility (snap-to-sharp the moment V fires) made it worse: the area outside the slide rect popped sharp instantly while the slide's own pixels were still cross-fading over a backdrop that had already snapped, producing a disjoint discontinuity at the slide boundary.
Fix (per Lisagor): keep blur and visibility on the same shared clock — one global formula, no inside/outside split — but square the visibility coupling so the blur front-loads its decay. blur = visibility² * scaleToBlur(scale) instead of blur = visibility * scaleToBlur(scale). At visibility = 0.7 (early in the fade), blur is 0.49 — already half gone. At visibility = 0.5, blur is 0.25 — three-quarters gone. The world is most-of-the-way sharp by the time the slide is half faded, but the two channels never decouple, so there's no boundary discontinuity inside vs outside the slide rect. Reads as "the slide goes and the world is here" rather than "the slide goes and then the world resolves." On the way back in (V to show) the curve trails — blur ramps up slowly at first and catches up as the slide lands, which reads as the slide pulling the world out of focus around it as it arrives. Symmetric in math, asymmetric in feel, both in the right direction.
Implemented as a one-line change in AppearanceDriver._cook: transform.visibility * transform.visibility in place of transform.visibility for the blur factor only. Opacity coupling is unchanged.
WindowCapture stack-overflow guard
A crash report from Noah on v0.9.57 (macOS 26.5 beta, Mac Studio M2 Max with three displays) showed the SCStream sample-delivery thread killed by SIGBUS at the stack-guard page after 6,656 levels of mutual recursion through two functions in HovercraftShared. The recursion sat between the SCStreamOutput callback and the per-frame onFrame?(mtl) invocation that hands the texture to SlideDeck. The path is supposed to be flat — one frame in, one callback out — and the in-house pipeline reproduces flat with no growth. Most likely a re-entrant SCStream delivery on that specific OS/hardware combo, or a Swift runtime quirk in Sendable thunk dispatch under Apple Silicon LPDDR5; without a reliable repro we can't symbolicate the exact frames.
The defensive fix doesn't depend on knowing the cause: a non-recursive NSLock around the per-frame callback dispatch acts as a same-thread reentrancy detector. First entry succeeds and runs the callback as before; any synchronous re-entry on the same thread fails the tryLock, drops the new sample, and logs once. A dropped frame at 30fps is invisible. The crash was a force-quit during a live demo. Trade is obvious.
Pinch enter / exit defaults retuned
The pinch recognizer's pinchEnterRatio and pinchExitRatio defaults moved from (0.40, 0.45) to (0.30, 0.35) after Adam's bake-off in dev. Pinches now require a firmer-feeling close (enter at 30% of hand size instead of 40%) and release more responsively the moment fingers separate (exit at 35% instead of 45%), with the same 0.05 hysteresis preserved for jitter rejection. The earlier looser values were a v0.9.53 / v0.9.54 fix for close-range stickiness that the ratio-scaling formula has since absorbed; with that path now stable, the tighter pair feels closer to the physical "I'm pinching → I let go" intent.
[v0.9.58] — 2026-04-30
Two new hotkeys: C (center) and FF (full-bleed fill)
C animates the slide to the canvas center without changing scale. Idempotent — pressing C while already centered is a no-op (no kicked animation, no tilt impulse), so a tap-tap doesn't fire spurious motion. Tolerance is tight enough (~0.005 in canvas-fraction units, well below the soft-snap bias well's radius) that any deliberate user reposition counts as "off-center" and earns a recenter on the next press. Composition tool first; doesn't touch the F-toggle's pre-fill snapshot, so an F-then-C-then-F sequence still restores cleanly to the original pre-fill pose.
FF is a double-tap on F: the first F lands at the existing fill-with-margin (~3% inset per side, the "almost edge-to-edge" feel), and a second F arriving within 350ms escalates to full-bleed fill — no margin, the slide hugs the canvas exactly. After FF, a single F restores all the way back to the original pre-fill pose, just like a single F-toggle would. The 350ms window is just past the system double-click threshold, which feels right for a deliberate keyboard double-tap. Outside the window a second F is treated as a fresh single press, which from at-fill means "restore" (the existing behaviour). Optimistic-then-escalate beats delaying single-F to disambiguate: every user pays 0ms instead of 350ms for the common case, and the FF stage transition reads as an intentional two-stage "almost…there" rather than a stutter.
Both hotkeys reuse the F-toggle's animation feel — quintic smootherstep over 0.65s with damped tilt drive — so all three keyboard composition gestures (F, FF, C) move with a single visual vocabulary.
The bundled welcome deck still lists only F / V / L on its Hotkeys page; C and FF are advanced enough that surfacing them in the first-run onboarding would crowd the page without earning their keep. They live in the changelog and (eventually) the docs.
[v0.9.57] — 2026-04-29
Welcome deck survives the camera-extension restart
Adam tested v0.9.56 on his Intel Mac mini — clean install, camera extension installed correctly, app restarted as part of the install flow — and discovered a bug we'd shipped without seeing: the welcome deck disappeared on the post-restart launch. The root cause was the v0.9.55 design treating "the welcome deck rendered" as "the user has been onboarded". Stamping hovercraft.welcomeShown = true the moment the deck loaded meant any process that triggered an early relaunch (the camera-extension install being the canonical case) burned the one-shot before the user had actually seen, let alone engaged with, the deck.
v0.9.57 redefines the gate. The flag now means "the user has engaged with the app", and is written by the actions that prove engagement: replacing the welcome deck with their own PDF / image / window share (any of loadPDF / loadImage / loadWindow), or completing a first pinch (firstPinchCompletedKey, which the recognizer already writes for the first-run tooltip dismiss). Either signal retires the welcome deck for good. Otherwise — including across system-extension-install restarts — the welcome deck replays on the next launch where no other deck is bookmarked. A user who opens and closes the app without engaging gets it again on launch two; a user who pinched even once doesn't.
The drop zone also grows a small "Show welcome deck" link sized down to read as tertiary text — there for the curious, for the user who dismissed and wants it back, and as a manual recovery path if the gate ever misclassifies a real first run as engagement. It bypasses the gate entirely so summoning the deck is always one click away from the empty state.
Existing v0.9.55 / v0.9.56 users who already have welcomeShown = true on disk are unaffected; they're already past the onboarding question. The fix matters for net-new installs and for the post-extension-install relaunch path that brought it to light.
[v0.9.56] — 2026-04-29
Image-file drop, opt-out quit warning, Intel-Mac crash fix
Three small landings stacked into one bump.
Drop a still image as a slide. The drop zone (and the active deck surface) now accepts the standard image formats — PNG, JPG/JPEG, HEIC/HEIF, GIF, TIFF, BMP, WebP — anything ImageIO knows how to decode. The dropped image becomes a single-page deck via the new ImageSlideSource, sibling to PDFSlideSource and WindowCaptureSlideSource. Mixed drops resolve PDF-first, then image-first; unrecognised file types are silently ignored so a stray document in a multi-file drop doesn't block the rest. Single-image decks share the same security-scoped bookmark machinery as PDFs, so the last-loaded image reopens automatically on the next launch. The drop-zone hint text changes from "Drop a PDF here" to "Drop a PDF or image here" to match.
"Warn before quitting" is now an opt-out preference. When an app is reading from Hovercraft's virtual camera and the user tries to quit, the existing warning dialog (introduced in an earlier build) now carries a checked-by-default checkbox labelled "Warn before quitting". Unticking it persists vision.sandwich.Hovercraft.warnBeforeQuitting = false and silences the prompt for future quits while broadcasting; ticking it (or leaving it ticked) preserves the warning. The setting writes regardless of whether the user clicks Quit or Cancel — the act of unticking is itself the commitment to the new preference. The default is true (warn), enforced via register(defaults:) so a clean install lands on the safety net automatically. Users who want to re-enable the warning today need a defaults delete; a Settings-panel exposure can come later if anyone asks.
Intel-Mac DAL camera crash, fixed. John Siracusa's Mac Pro 7,1 crashed Hovercraft v0.9.3 the first time a USB camera tried to come online. The crash log pointed at -[AVCaptureDALDevice setActiveVideoMinFrameDuration:] throwing an uncatchable Objective-C exception from inside applyFormat. AVCaptureDALDevice is the legacy CoreMediaIO Device Abstraction Layer subclass — the path used by external USB cameras, virtual cameras, and Continuity Camera on Intel Macs. Some DAL devices (and DAL plugins) reject setActiveVideoMinFrameDuration: even when the format's videoSupportedFrameRateRanges advertises the requested rate, and Swift can't catch ObjC exceptions, so the assignment kills the app. Fix: detect DAL devices by class-name (AVCaptureDALDevice / *DALDevice) and skip frame-rate pinning on them entirely. The device keeps whatever rate the chosen format defaults to, which is fine — rate pinning was a CPU optimisation against Continuity Camera dropping us into 60 fps, not a correctness requirement. Modern Apple Silicon Macs talk to their built-in cameras through AVCaptureHALDevice and never hit this code. Siracusa needs to update from v0.9.3 to pick up the fix; subsequent users on Intel + USB cameras are pre-immunised.
[v0.9.55] — 2026-04-29
First-run welcome deck
Hovercraft now ships with a 5-page Welcome to Hovercraft.pdf in the app bundle. On first launch — or any launch where no prior deck has been picked yet — the app loads the welcome deck automatically, so the first thing the user sees in the floating window is content rather than an empty preview.
The deck is the gesture model in five spreads: a hero page that introduces the app and tells the user to hit the right arrow to advance, a Pinch page that explains reaching up and grabbing the slide window out of thin air, a Push page that explains moving the window forward and back along the depth axis, a Hotkeys page covering the F / V / L shortcuts, and a "now make it yours" page inviting the user to drop in their own PDF or share an open window. The look matches sandwich.vision/hovercraft exactly: same near-black ground, same blue-violet bloom, same SF Pro light weight, same letter-spacing on the eyebrow type. Apple's hand.pinch SF Symbol — the glyph their iOS / visionOS user guides use for this gesture — anchors the Pinch page; a custom forced-perspective z-axis arrow anchors the Push page. The final page surfaces the short link sndw.ch/hover with "Tell all your friends." as a soft call-to-action.
The deck is gated by a hovercraft.welcomeShown flag in UserDefaults, so it only auto-loads once. Existing users updating from v0.9.53 will not see it on update — their last-loaded deck restores normally — which is the right call: the welcome content is for net-new users discovering the app, not for users who already know how it works. (If we ever want to surface it again to existing users, it's a one-line defaults delete away or a future "re-introduce me" menu item.)
The PDF generator lives at scripts/generate_onboarding_pdf.swift so the deck can be regenerated whenever copy or visuals need to evolve, and the script is heavily commented with the reasoning behind type sizes (calibrated for the floating window's typical 3-4× preview downsample), color choices (pre-blended bloom stops to dodge PDF gradient-alpha quantization), and layout decisions.
Bundles v0.9.54's pinch-exit re-tune and the DEBUG-gate on dev controls (the v0.9.54 build was committed locally on 2026-04-29 but never published; v0.9.55 is the first build to ship those changes to users).
[v0.9.54] — 2026-04-29
Production builds drop dev controls; pinch-exit retuned to 0.45
Two small landings before public release. Adam dialed pinchExitRatio live on the Mac Studio after v0.9.53 shipped and settled on 0.45 as the right seed (down from v0.9.53's 0.48, which itself was down from v0.9.52's 0.65). The 0.05 hysteresis above pinchEnterRatio is still enough to suppress jitter at the boundary, and close-range exit lands at ~0.09 — squarely back in the desk-distance neighborhood where the original absolute threshold lived.
Second landing is the DEBUG-gate on dev controls, the last item from the pre-1.0 list that's been sitting in the todos through the v0.9.5x line. The slider-icon flip button in the chrome header, the ⌘D keyboard shortcut, and the entire reverse-side dev panel (sliders for the appearance, bias, tilt, opacity, tracking, and pinch tuning, plus the save/reset/export controls) all compile out of release builds via #if DEBUG. Public-facing builds now expose only the pickable camera, status pill, and source picker — the controls a regular user would ever touch. Debug builds are unchanged so internal tuning work continues against live sliders.
Selective ROI rescue (Commit K from the v0.9.52 plan) remains deferred — Lisagor's read still holds that K is a narrower job after the v0.9.52 + v0.9.53 + v0.9.54 soaks, and the right next soak window is 1.0 itself.
[v0.9.53] — 2026-04-29
Pinch-release responsiveness fix
Adam tested v0.9.52 on the Mac Studio and reported the build felt decent overall but with one glaring regression: unpinch detection was bad — the slide kept moving after the pinch was released. Tracing it back to v0.9.52's hand-size-relative pinch threshold (commit L): the back-derived 0.65 exit ratio was correct on average but catastrophic at close range, where pinchExitRatio × handSize ballooned to 0.13 (fingers had to separate 13% of frame diagonal to count as released). At desk distance the threshold matched the prior absolute behaviour exactly; at close range — exactly where Adam was test-driving the most — release felt broken.
- Default
pinchExitRatiolowered from 0.65 to 0.48. Hysteresis abovepinchEnterRatioshrinks from 0.25 to 0.08, still plenty for jitter rejection. Close-range exit is now 0.10 instead of 0.13 — same neighborhood as desk-distance feel rather than a different gesture entirely. - Pinch ratios surfaced as dev-panel sliders. v0.9.52 added the unit-change but not the sliders, which is why the miscalibration needed a rebuild instead of a slider drag. From v0.9.53 onward, "Pinch enter" and "Pinch exit" are dialed live alongside the confidence floors.
Selective ROI rescue (Commit K from the v0.9.52 plan) defers further to v0.9.54 — Lisagor's read holds: K's job is narrower after I+J+L soak, and there's no reason to bundle the hotfix with the experiment.
[v0.9.52] — 2026-04-29
Hand-recognition robustness pass
Adam tested v0.9.5.1 on the Mac Studio and read it correctly: hand recognition is the single biggest perceived-quality lift available. The two failure modes were close-to-camera dropouts (the hand "stops being recognized" once it gets close enough to fill more than half the frame) and asymmetric latitude (pinches that start close-to-camera survive better than pinches that start far and pull close — the natural arc real users take). Both came back to a single bug dressed up as a tuning problem: the recognizer was holding wrist and middleMCP — postural anchors whose only job is to scale the pinch threshold — to the same confidence floor as thumbTip and indexTip, which carry the actual gesture geometry. Lisagor reframed the fix as an invariant: a locked pinch can only be released by a deliberate open, not by landmark dropout. v0.9.52 lands three commits toward that invariant.
- Per-landmark confidence floors. Replaces the single
minLandmarkConfidencefloor (0.30 across all four tracked landmarks) with separate floors per role. Pinch landmarks (thumbTip, indexTip) gate strictly at 0.40; postural landmarks (wrist, middleMCP) gate permissively at 0.15. Stops punishing the wrong landmarks for being at the wrong end of the arm. - Named lock-release invariant. The pre-v0.9.52 design folded "no pose at all" and "pose returned but soft" into a single 0.8s grace clock, which is what made close-range pinches feel fragile — every soft-focus frame chipped away at the release timer even though the user wasn't doing anything. Now there are three explicit triggers —
confirmedOpen,posturalAmbiguity,noPose— and onlynoPoseticks any release clock. Postural-ambiguity frames freeze the lock indefinitely and lean on spatial-continuity to filter false positives. The no-pose hard timeout was bumped from 0.8s to 1.5s, generous enough that brief out-of-frame moments don't kill the gesture, short enough that a user who genuinely walked away gets a clean release inside two seconds. The invariant is named in code so the next tuning pass doesn't accidentally erode it. - Engaged-vs-armed threshold split. Once a pinch is locked, the spatial-continuity machinery (chirality continuity, position-proximity fallback, the lock-release invariant above) is doing most of the false-positive filtering, so per-frame confidence can be relaxed to keep the gesture alive through frames that would have been rejected during arming. A single
engagedFloorRelaxationknob (default 0.10) subtracts uniformly from every floor while locked, preserving the relative strictness between pinch and postural landmarks. - Hand-size-relative pinch thresholds.
pinchEnter/pinchExitwere absolute distances in normalized image space, which read as too tight at close range (the same physical pinch produces a much larger absolute thumb-to-index distance when the hand fills the frame). Renamed topinchEnterRatio/pinchExitRatioand now scaled by the wrist→middleMCP distance the recognizer uses as a depth proxy. A "barely pinched at close range" now reads the same as "barely pinched at far range." Defaults (0.40 / 0.65) are feel-equivalent to the prior absolute thresholds at typical desk distance and expand gracefully as the hand approaches.
Sliders for all five new knobs (four per-landmark floors + lock relaxation) are surfaced in the dev panel for live tuning.
Selective ROI rescue (the constrained re-take of the reverted v0.9.5 H commit, for the partial-out-of-frame case Adam separately flagged) is deferred to v0.9.53 as its own soak build per Lisagor's read that it'll have a much narrower job once these three commits land.
[v0.9.5.1] — 2026-04-29
Hand-tracking smoothness pass
The v0.9.5 line is one large pass on perceived smoothness of the pinch gesture. Tracking is the same Vision pipeline — what changed is what we do with each pose between Vision and the slide.
- Landmark-level 1€ smoothing. Per-landmark adaptive low-pass filter on every joint Vision returns. Cuts visible jitter at rest without adding noticeable latency under motion (the cutoff lifts as the hand moves, so brisk pinch-and-slide stays crisp).
- Predictive position extrapolation. The composited slide position is projected ~15ms forward from velocity during sustained motion, gated off when the hand stops. Compensates for the camera→Vision→render pipeline without the ringing you get from a fixed lead.
- Critical-damped spring on slide response. The slide's translation now follows pinch midpoint through a spring instead of a hard tether. Dialled to ride right at the critical-damping line so it never overshoots, never wobbles, just settles.
- Chirality continuity tracking. When Vision flips its left/right call mid-gesture (it happens, especially with a hand turning palm-up), the recognizer now keeps the gesture going by matching position rather than chirality, so a confirmed pinch survives a momentary mis-classification.
- Confirmation pulse on
.began. A 200ms scale pulse on the slide the instant the pinch confirms. Tiny, not toy-like, but enough that "I just grabbed" stops being a guess. - Release easing on
.ended. Letting go now coasts the slide to a stop over a tunable window (default 0.1s, capped by the spring's actual velocity) instead of stopping dead. Reads as a real object you set down rather than a dragged element snapped to a frame.
Tracking transparency
- "Tracking warming up…" status pill. Vision's first-frame JIT prewarm can occasionally stretch from ~200ms (Apple Silicon, Neural Engine) to several seconds when the system has routed ML traffic onto its slow path. A small ultra-thin-material capsule with a spinner now appears in the top-leading corner of the preview during that window, so the multi-second wait reads as "tracking is loading" instead of "the pinch is broken." Disappears the instant the prewarm completes.
Dev-loop quality of life
- DEBUG builds skip the
/Applicationsrelocation gate and the license gate, so re-running from Xcode never bounces through the install/activate flow when the binary is plainly running out ofDerivedData. Release builds are unchanged. - License- and relocation-gate windows now have no traffic lights and a flush-with-content title bar when they do appear, so the framing matches the rest of the gate UI rather than reading as a mis-styled standard window.
- Tunable parameters surfaced in the dev panel. Spring stiffness/damping, pulse magnitude/decay, release ease, prediction lead, and the 1€ filter cutoffs are all wired to live sliders so we can dial without rebuilds. Persists across launches.
[v0.9.4] — 2026-04-28
Onboarding, end-to-end
The first wave of beta feedback all converged on one cliff: getting the camera extension installed. v0.9.4 flattens four distinct failure modes Jalkut hit in a single session, three of which were unrecoverable without quit-and-relaunch. Every one of them now self-heals with a one-click affordance.
- Pre-flight
/Applicationsgate. Hovercraft now refuses to do anything until it's running from/Applications(or~/Applications). Launched off a mounted DMG or a Downloads folder, the entire UI collapses to a calm "Move to Applications and Relaunch" panel that copies the bundle into place, launches the relocated copy, and quits. macOS rejects camera-extension activation from anywhere else, and v0.9.3 surfaced the rejection as an opaque "Failed" pill with no path forward. - Mounted-DMG sibling detection. Even after relocating, a still-mounted DMG holding a sibling
Hovercraft.appcauses sysextd to treat both bundles as competing registrations, which makes the toggle in System Settings → General → Login Items & Extensions bounce back off the moment the user flips it. Hovercraft now watches/Volumesfor siblings on every mount/unmount notification and surfaces a per-volume "Eject Hovercraft.dmg" button ahead of every other piece of install guidance. - "Almost there" relaunch CTA. When macOS reports the extension is enabled but the host's CMIO DAL hasn't picked it up for the current process (the documented after-respawn path), the activation guidance pivots from "toggle it on" to "macOS says the extension is on, but Hovercraft can't see it yet" with a single-click Restart Hovercraft button. The installer polls
OSSystemExtensionRequest.propertiesRequestevery 3 seconds while in the awaiting-activation state to know authoritatively whether the toggle is on, with a 5-second grace window so the healthy bind path never sees the relaunch CTA flash on. - Click-to-reveal failure detail. The status pill becomes a button when the install request errors. Clicking it pops a panel with the OS's
localizedDescriptionverbatim plus, for the failure modes we already know about, a one-line recommended action — drag-into-Applications, retry-the-password-sheet, re-download-the-DMG, try-again-on-superseded. A "Try Again" default-action button retries the request directly. v0.9.3 swallowed the underlying message; the popover was Jalkut's explicit ask.
License gate polish
- Single-line HVCT entry. The HVCT-XXXX-XXXX-XXXX format introduced with the v2 short-key Keygen policies is short enough that the multi-line input from v0.9.3 (a hedge against legacy ED25519 keys) is no longer pulling its weight. Activation now uses a single 22pt centered monospaced field sized exactly to hold a filled key.
- Traffic-light suppression on the gate. Closing or zooming the activation window can't make meaningful progress, so the traffic-light cluster is hidden for the duration of the gate and restored the instant a valid license drops the user into the main view. Cmd+Q remains the universal Mac escape hatch.
[v0.9.3] — 2026-04-28
Activation gate (direct channel)
- Hard license gate at first launch. Hovercraft now refuses to bring up the camera UI until a valid Keygen license is on file. Single-pane activation screen: paste the key from the purchase email, press Activate, into the app. Keys are stored in the macOS Keychain (
vision.sandwich.Hovercraft.license) so a reinstall doesn't re-prompt. - Cache + grace policy so paying customers on a plane don't get locked out: keychain validation < 7 days old is trusted instantly, 7–30 days requires an online check but tolerates network failure, > 30 days is hard-checked. A server-confirmed "rejected" answer (refund, revoke) clears the cache regardless of age.
- Sandboxed network access added (
com.apple.security.network.client) so the validator can reachapi.keygen.sh. Stays scoped to the main app target — the camera extension does not need it. - Manual-issue path for friends and comp licenses: mint a Beta key in the Keygen dashboard, send it alongside the DMG. No Stripe involvement required. Documented in
docs/DISTRIBUTION.md§ "Manual-issue workflow".
Broadcast feed parity
- Pinch indicator bar now appears in the virtual camera output, not just the local preview. The visionOS-style glass capsule that fades in below the floating slide on a confirmed pinch is now composited into the broadcast frame by
FrameCompositor, so audience-side viewers on Zoom/Meet/Teams see the same gesture acknowledgement the presenter does. Opacity is smoothed on the FramePump cadence (~400ms ease) and the indicator render pass is short-circuited at near-zero opacity so the off-state has zero GPU cost. The previous SwiftUI overlay is gone — single source of truth for what audiences see.
[v0.9.1] — 2026-04-27
Post-demo hardening pass. The 0.9.0 build hung during a live customer demo because macOS Reactions preloaded its RealityKit VFX bundles into our process on first capture; that's now neutralised at the format layer rather than the gesture-trigger layer. The camera-extension onboarding cliff that the demo also surfaced — the unmarked manual toggle in Login Items & Extensions — now has explicit UI.
Performance and stability
- macOS Reactions fully suppressed on Hovercraft's own capture.
CameraFeednow picks anAVCaptureDevice.FormatwhosereactionEffectsSupportedflag isfalseinstead of lettingsessionPresetresolve to whatever the OS prefers. With no Reactions-supporting format active, the OS skips the RealityKit confetti/balloon/firework/heart/laser bundle preload entirely — the workload that caused the demo-day hang. Falls back to the prior preset path on cameras (rare Continuity formats) where every variant supports Reactions. - Frame rate pinned to 30 fps on the per-format path so we don't accidentally land on a 60-fps Continuity mode that just costs CPU upstream of the compositor.
Onboarding
.awaitingActivationinstall state. The previous flow flipped the installer to "Ready" the momentOSSystemExtensionRequestfinished, which is before the user has toggled the extension on in System Settings → General → Login Items & Extensions. The new state honestly represents "OS approved the kext, but it isn't running yet" and only transitions to "Ready" onceFramePumpconfirms a live sink binding. UI grows a yellow guidance panel in this state with the exact path to the toggle and a one-click deep link to that pane.
[v0.9.0] — 2026-04-27
First notarized beta build. Universal (Apple Silicon + Intel), macOS 15 Sequoia minimum. Stapled .dmg clears Gatekeeper offline.
The hero
- Pinch-to-pose gesture. A real visionOS-style magic trick on macOS: pinch in the air to grab the floating slide window and move, scale, or hide it with a hand gesture. Hand tracking via Vision's
VNDetectHumanHandPoseRequestwith a 1€ filter on the joint stream and aPinchRecognizerthat handles lost-track grace, latency telemetry, and a clean session lifecycle. - Spring-damped tilt physics drive a subtle wobble on the slide window in response to motion velocity (gesture-driven and programmatic). Tuned to feel like an object with mass, not an animated rect.
- visionOS-inspired glass overlay with a separately-tunable backdrop blur behind the slide. Global blur and backdrop blur are chained so they don't visually fight each other.
- Soft-snap-to-center and soft-snap-to-fill-frame with a C¹-continuous cubic falloff. Anchored centering uses a smooth-breakaway resistance curve (
r − B·t(1−t)²) so pulling out of the dock feels graceful, not abrupt. - Scale-coupled appearance: opacity and blur cook off scale so the window dissolves into the camera feed as it recedes and crisps up as it approaches the lens.
Inputs and sources
- PDF deck source with arrow-key navigation and a live filmstrip (in-preview, only shown for multi-page decks).
- Live window share via
ScreenCaptureKit. Pick any open application window and project it into the overlay. Double-click to confirm in the picker. Texture origin is propagated through the compositor so window captures render right-side-up (CoreVideo top-left vs. Metal bottom-left). - Window-share strips app chrome. A per-bundle-ID heuristic crops title bar + tab strip + URL bar before the frame reaches the compositor, so the audience sees content rather than browser furniture. Safari, Chrome, Brave, Edge, Firefox, Opera, Vivaldi, and Arc each get a tuned inset; everything else gets the standard 28pt
NSWindowtitle bar trim. - Source swap preserves pose. Replacing one source with another (PDF → PDF, PDF → window, window → window) keeps the user's current position and scale intact. Only the very first source of the session lands on the upper-right factory placement; subsequent swaps respect whatever the presenter just dialed in by hand.
- Camera switcher with Continuity Camera support, hot-plug detection via the modern
wasConnectedNotificationAPI, distinct active-camera indicator, and CMIO virtual cameras (Hovercraft itself, OBS, etc.) filtered out of the picker. - Camera stall resilience: holds the last good frame for ~5s on a stalled feed instead of going black.
Hotkeys
←/→— page through deck.V— toggle slide visibility (works while input is locked).F— toggle "fill the frame" with a margin. Animates with a quinticsmootherStepease and a per-animation tilt-drive override (0.0025) so the move glides without wobble. Robust toggle: re-checks the current target each press, so manual moves between F presses don't strand you on a stale restore pose.L— lock input. Disables hand-gesture and cursor input; programmatic changes still apply.Vcontinues to function in lock mode.- A compact
V / F / Lhotkey tip strip lives directly under the source selector for first-run discoverability.
Mouse and trackpad
- Drag the slide in the live preview to translate it.
- Pinch-magnify on a trackpad scales the slide.
- Two-finger scroll is a gentler scale fallback for mice without pinch. Polarity matches the natural "push away / pull closer" mental model: swipe up to shrink, swipe down to enlarge.
Dev panel
- Live tuning sliders for tilt (stiffness, damping, drive, max-angle, accel-clamp), opacity min/max, backdrop blur sigma, and bias config (center radius/strength, fill radius/strength).
- Two-column
LazyVGridlayout to keep the panel compact. - Manual numeric entry on every slider, plus Save as Default to persist tuning to
UserDefaultsviaCodable. - Pinch indicator bar overlays the live preview during a pinch session, scaled proportionally to the slide.
Build, signing, distribution
MARKETING_VERSIONaligned to0.9.0across the app, theHovercraftSharedframework, and the camera system extension.- Universal
Hovercraft.appandHovercraft.dmg, both signed withDeveloper ID Application: Sandwich Vision Inc. (VMAY6SU9SH)and stapled with separate Apple notary tickets. - Self-healing CMIO virtual-camera installation across reinstalls (Day 3.5).
- Sink-to-source latency probe instrumentation in the extension pipeline.
scripts/notarize.shproduces a Gatekeeper-clean.dmgend-to-end in ~75s once the Apple notary service responds.
Known limitations
- License entry screen not yet built; this build is open (no Keygen activation gate). See
docs/DISTRIBUTION.mdpre-launch checklist. - macOS 15 Sequoia minimum.
- First launch requires admin password (camera system extension install) and Camera + Screen Recording TCC grants.