Skip to content

Deep links open on their subject (+ exhibit/tour/clip links) - #831

Draft
rulkens wants to merge 26 commits into
mainfrom
worktree-deep-links-open-on-subject
Draft

rulkens wants to merge 26 commits into
mainfrom
worktree-deep-links-open-on-subject

Conversation

@rulkens

@rulkens rulkens commented Oct 1, 2026

Copy link
Copy Markdown
Owner

Spec: docs/superpowers/specs/2026-10-01-deep-link-arrival-design.md

Every deep link opens with the camera already on its subject (no fly-in), behind a veil until the subject resolves; adds read-only #exhibit=, #tour=, #clip= links; ready now means "arrived", so capture tools drop their timeouts and pose re-application.

Draft — spec under review; plan + implementation to follow on this branch (prep commits first).

🤖 Generated with Claude Code

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
skymap 789da09 Commit Preview URL

Branch Preview URL
Oct 04 2026, 08:57 AM

rulkens and others added 25 commits October 1, 2026 14:53
The focus tween, the follow seed, the home pose and the lon/lat fly-to now
all take their body distance from focusFraming, so a followed body honours
focusDistanceRadii instead of re-deriving the screen-fill distance.
Each row's read returns its share of one LinkIntent; linkIntentFrom merges
them and owns precedence (a takeover beats focus and pose, focus plus pose
is one focus view), so table order fixes only the write layout. The
applyLinkIntent shim emits exactly the actions the rows used to.
requestFocus({ id, transition }) hands its transition to
updateSelectionFocus as meta; the focus tween runs only on 'fly'. A 'cut'
still spends a parked #pose=, since it is the arrival focus. Every existing
caller passes 'fly'.
bodySelectionRow, bodyHomePose and the lon/lat fly-to build a body's
SelectionRow through bodyRowAt instead of three literals; the fly-to frames
only off the focused body. Trims tests that could not fail and tightens
three doc lines.
One path from a LinkIntent to the screen: t and orientation first, then the
subject, then one camera commit. A cut focus frames its row with framingPose
in the same tick the row lands; a fly focus tweens as before. The write half
canonicalizes on the arrival settling instead of hashArrivalApplied, and the
applyLinkIntent shim goes.
The boot read hands its LinkIntent to arrivalSaga, which cuts to the subject
once the camera exists, waits two frames and puts arrived; an id still
unresolved once loads go quiet, or the 30 s backstop, lands home as failed.
wireInput commits only the neutral home base. The urlPose park and
selectHasSelectionIntent go with it.
The arrival puts goLive before any framing when the link has no t, so a
plain boot frames Earth against the wall-clock sun; startLoop's guarded
goLive goes. A request retired while deferring is dropped on resolve, and
a failed arrival clears the selection before landing home. The backstop
starts once the camera exists, an engine error fails the arrival, and the
unknown-id check re-reads load progress after its hop. The perf hook's
setPose waits on afterTwoFrames.
Three read-only hash rows name a takeover subject; navigateSaga validates
the id against its registry before dispatching anything. An exhibit cut
commits its fitted pose in place of the fly, and its copy shows at once
(openExhibit carries entry: Transition). A tour or clip arrives once its
first clip has started, so an opening snap lands behind the veil.
An opaque veil with the splash's load progress, shown while the arrival is
pending on a boot that carried a deep link; it fades once the subject is on
screen. A plain boot keeps the splash. Mounted outside cinema mode only.
A cut tour or clip that the arrival gives up on is stopped (exitTakeover /
stopClip) by the saga that started it, so it cannot begin over home later.
A cut clip clears the seeded home selection. Tests: unknown ids for every
takeover kind, a player that starts inside the dispatch, the copy delay.
whenStablyReady now folds arrival.status into the shared "measure-ready"
predicate: it holds for arrived, rejects on failed with the reason, and
otherwise debounces as before. captureScene drops the boot waitSettled,
the POST_ESC_WAIT_MS re-settle, and the pose re-apply-and-verify via
readLiveCameraState — arrival already owns that wait, so a capture.pose
override lands once and stays. Exhibit cards boot on #exhibit=<id>
instead of hand-merging the registry's settings and pose. waitSettled,
readLiveCameraState and the now-unread poseMismatch are deleted outright.

No "clip frame-0 guess" was found in tools/record/record.ts to delete —
its clip takes already resolve frame 0 through the recorder hook's own
seenActive latch, independent of arrival; left untouched.
#exhibit=, #tour= and #clip= joined #focus= as read-only deep-link
rows; the README's feature list named only the one it had before them.
No stale urlPose/applyUrlPose/POST_ESC_WAIT_MS mentions remain outside
this plan's own history.
… seeded

wireInput seeds the runtime in the store's orientation; the arrival used to
set a linked #orientation= only after waiting for that runtime, so frame 1
re-encoded the already-linked-frame commit. The arrival now lands t and
orientation on the boot read and hands navigateSaga only the view.
A takeover link's rows write nothing back, so the settled body after
#exhibit= differed from the URL and the write pushed; Back then reopened the
exhibit and pushed again. applyNavigation now signals the burst, and the
write half replaces for it, as it does for the arrival.
A cut #exhibit= starts the held exhibit's slow spin, and an exhibit card
has no capture pose to stop it, so the thumbnail caught a drifting camera.
The last step's dispatches also went straight into the screenshot; the shot
now waits two frames first.
exhibit=, tour= and clip= now write from store state while that play
runs, however it started, and drop when it ends. A hash navigation that
leaves out the running key exits it; one that names it does not restart
it. The registry clip's id lives on camera.clipId, written by
watchClipSaga.
takeover.active is now the one record of what runs: TakeoverSource gains
{ kind: 'clip', id }, watchTakeoverSaga takes startClip as its third start
request, and exitTakeover is the one exit verb. Each kind brings its own
bracket: tours and exhibits run inside withSceneSnapshotSaga (scene
snapshot/restore + FOV pin), and a clip's body (clipBodySaga, formerly
watchClipSaga's worker) freezes and restores the clock.

Deleted: camera.clipId + clipIdChanged, selectPlayingClipId, stopClip,
watchClipSaga, selectTakeoverActive, tour.tourId (tourStarted is
payload-less; selectActiveTour reads the takeover's id). The three hash
rows write from takeover.active; applyNavigation has one keep/exit test.
The HUD still hides only for a tour or exhibit.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…a stale test comment

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Applies the safe-now bin from the branch deletion audit, plus the comment
fix it flags separately:

- S1: drop branch-history narration in hashParamSources.ts (main never had
  takeover rows, so there was nothing to narrate becoming one)
- S2/S3/S4: drop three tests already covered elsewhere — navigateSaga's
  "focus cut commits once" (arrivalSaga's mars + late-catalog cases),
  watchHashReadSaga's pose-before-focus ordering (linkIntentFrom's merge +
  navigateSaga's focus+pose outcome), linkIntentFrom's "parses each key"
  (each row's read is pinned in hashParamSources.test.ts)
- S5: drop withSceneSnapshotSaga's claim that a body merges its own lens
  after the snapshot (grep finds no fovDeg/camera: override anywhere)
- S6: drop captureScene's exhibitId+focusId guard and the SceneShot doc
  sentence it backed — the only builder (capture.ts shotFor) sets them from
  a discriminated union, so both can never be defined at once
- S7: drop clipBodySaga's fork-time warning, carried over from the deleted
  watchClipSaga — the body is only ever invoked on demand, with no fork-time
  alternative to warn against
- ArrivalVeilContainer.tsx: fix a stale comment — since a2c270b a running
  takeover's key stays in the hash for the run; only canonicalization, a
  failed play, or an ended one drops it

Skipped every needs-ruling item (R1-R6) per instruction.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
DoD audit READY: suite 14973 green, tsc clean, 22/22 boxes, smoke attested
by the user, no new TODOs, deletion audit applied (safe-now a99ad93,
R2 fold 4309e0e; R1/R3/R4 declined). Backlogged: afterTwoFrames ->
renderScheduler.nextFrame, npm run shot.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…pen-on-subject

# Conflicts:
#	docs/BACKLOG.md
#	src/services/engine/phases/startLoop.ts
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.

1 participant