Skip to content

feat(studio): multi-select transitions, group editing, take history - #731

Open
scottdraves wants to merge 6 commits into
stagefrom
feat/728-transition-selection-history
Open

scottdraves wants to merge 6 commits into
stagefrom
feat/728-transition-selection-history

Conversation

@scottdraves

Copy link
Copy Markdown
Contributor

Closes #728.

Editing a flow's transitions gave no way to tell which one you were changing, only let you change one at a time, and threw the old render away on every regenerate. Three parts, matching the issue.

1. Selection

selectedTransitionIndex becomes selectedTransitionIndices, held in click order — the last click is the "primary" that the panel names and the preview plays.

  • Click selects, shift/cmd/ctrl-click toggles.
  • Selected gaps get a blue slab, from new selected/selectedDim/selectedGlow tokens. Deliberately not the existing processing blue, so "selected" and "rendering" stay tellable apart.
  • Select all / Clear all beside Reset, with a live selection count.
  • Clicking a transition plays it in the preview — every time, including when it is already selected.

That last point is why playback is an explicit request carrying a sequence number rather than a derived effect. Re-clicking the selected transition changes no state, so there is nothing to react to; a bare UUID would be an unchanged value. CrossfadeVideo takes a replayToken to restart the segment already on screen.

2. Group editing

Edits apply to the whole selection. When one would flatten a field the selection disagrees about, ForceSettingsDialog asks first — "force them all to the same value?".

Gating is per interaction, not per field. Choosing a preset writes prompt, negative prompt, LoRA and duration; gating each separately would raise four dialogs in a row. Only the field you touched is gated, and the knock-on clamps follow.

3. History

Every run is recorded on the transition and restorable.

The snapshot stores the resolved settings as a full override set, not the overrides. Globals drift while you keep working, so overrides alone would replay a take under today's defaults rather than the ones that produced it.

Shown as a right-aligned strip in the settings header, using the filmstrip frame nearest the middle of the clip — the first frame is the start reference frame, identical across every take at a position. The header reserves its height via HISTORY_ROW_HEIGHT, so the panel does not resize as takes complete and shove the preview up and down the page.

Fixes that fell out

  • clearTransitionOverride rebuilt the transition field by field and would have dropped history on "Reset to defaults".
  • Regenerating left uprezDreamUuid pointing at the replaced dream's upscale.
  • useFlowGeneration's tests were passing while silently exercising the failure path — the mock store had no recordTransitionRun, so generation threw and the catch swallowed it. Mock fixed, plus an assertion that nothing rolls back to failed.

Behaviour change

Clicking a failed transition now selects it rather than retrying immediately; the Generate button reads "Retry". Uniform click semantics were the point of part 1, but it is one less one-click path than before.

Testing

tsc --noEmit, eslint, prettier, and production build all clean. 243 tests pass — 24 new, covering selection/toggle/prune, history record-restore-cap, replay sequencing, field mismatch detection, and filmstrip/time formatting.

Also driven by hand in the browser against staging: selection and the blue highlight, preview following clicks (including the re-click-while-selected case), history restoring a real take across three generations, and the panel holding its height as takes complete.

🤖 Generated with Claude Code

Closes #728. Editing a flow's transitions gave no way to tell which one
you were changing, only let you change one at a time, and threw the old
render away on every regenerate.

Selection
- selectedTransitionIndex becomes selectedTransitionIndices, in click
  order; the last click is the "primary" the panel names and the preview
  plays. Click selects, shift/cmd/ctrl-click toggles.
- Selected gaps get a blue slab, from new selected/selectedDim/
  selectedGlow tokens. Deliberately not the existing `processing` blue,
  so "selected" and "rendering" stay tellable apart.
- Select all / Clear all sit beside Reset, with a live selection count.
- Clicking a transition plays it, every time. This is an explicit
  request carrying a sequence number, not a derived effect: re-clicking
  the selected transition changes no state, so there would be nothing to
  react to. CrossfadeVideo takes a replayToken to restart the segment
  already on screen.

Group editing
- Edits apply to the whole selection. When one would flatten a field the
  selection disagrees about, ForceSettingsDialog asks first.
- Gating is per interaction, not per field: choosing a preset writes
  prompt, negative prompt, LoRA and duration, and gating each separately
  would raise four dialogs in a row.

History
- Each run is recorded on the transition and restorable. Snapshots the
  resolved settings as a full override set, not the overrides: globals
  drift while you work, so overrides alone would replay a take under
  today's defaults.
- Shown as a right-aligned strip in the settings header, using the
  filmstrip frame nearest the middle of the clip -- the first frame is
  the start reference, identical across takes. The header reserves its
  height so the panel does not resize as takes complete and shove the
  preview around.

Two fixes fell out of this: clearTransitionOverride rebuilt the
transition field by field and would have dropped history on "Reset to
defaults", and regenerating left uprezDreamUuid pointing at the replaced
dream's upscale.

Behaviour change: clicking a failed transition selects it rather than
retrying immediately; the button reads "Retry".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 22, 2026

Copy link
Copy Markdown

Deploying frontend-alpha with  Cloudflare Pages  Cloudflare Pages

Latest commit: 07511ab
Status: ✅  Deploy successful!
Preview URL: https://69a9b91c.frontend-btm.pages.dev
Branch Preview URL: https://feat-728-transition-selectio.frontend-btm.pages.dev

View logs

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 22, 2026

Copy link
Copy Markdown

Deploying frontend-stage with  Cloudflare Pages  Cloudflare Pages

Latest commit: 07511ab
Status: ✅  Deploy successful!
Preview URL: https://208c0b9f.frontend-d7v.pages.dev
Branch Preview URL: https://feat-728-transition-selectio.frontend-d7v.pages.dev

View logs

@scottdraves

Copy link
Copy Markdown
Contributor Author

still need to test the force dialog! do not merge yet

Two ways the flow could misrepresent itself.

Selecting transitions that disagreed about a setting left the panel with
no honest value to show, so it showed the last-clicked one's and an edit
silently flattened the rest. The disagreement is now settled when the
selection is made: a shift-click (or Select all) that would mix settings
asks first. OK forces the clashing fields onto all of them, taking the
values already on screen; Cancel leaves the selection alone. Only the
clashing fields are pinned -- ones the selection already agrees about
agree because both sides resolve the same way, and pinning them would
freeze values that are free to follow the globals.

Return cancels, and both dialog buttons wear the same neutral style:
forcing settings is the destructive answer, so nothing should pull the
eye toward it.

Editing an already-rendered transition left the old video in place with
nothing to say so, and Generate skipped it outright -- resolveGeneration-
Targets read "processed" as "done". A transition is now stale when its
resolved settings differ from the snapshot recorded for the take in the
flow. Stale ones carry a dot, and Generate targets them along with the
never-rendered ones. With a selection active the selection is the scope
and runs unconditionally, so the strip header carries an "N edited"
count to keep work outside that scope visible.

The mapping from resolved settings to a recorded snapshot now lives in
one place, which generation writes through and the staleness check reads
through: two copies would let a field be recorded one way and compared
another, showing up as a transition that is permanently stale or never
stale. Prompt and LoRA resolve through the effective preset on both
sides of every comparison, matching what the panel displays -- without
it, two transitions on different presets showed different prompts while
comparing equal.

Duration labels under the gaps are gone; the dot took their place, so a
gap keeps its height whether or not it is marked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
scottdraves and others added 4 commits September 12, 2026 17:21
Conflicts resolved:
- flow-preview: adopt stage's shared SegmentPreview/useDreamSegments; keep
  the selection-follow and play-request effects, threading replayToken
  through SegmentPreview to CrossfadeVideo.
- transition-gap: use stage's DreamCardProgress for queue/processing and
  uprez progress; keep selection aria labels, mouse handling and StaleDot.
- transition-gap.styled: keep both the selection styling and the
  dream-progress overrides.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RH6gcEKeCTjKG6CS751YQm
…ache

[DREAM_QUERY_KEY, uuid] is a single React Query cache entry per dream, and
every reader stores the whole ApiResponse in it -- except transition-history,
which used getDream and stored an unwrapped Dream. The two polled each other's
shape out of the cache:

- History read data.filmstrip, undefined whenever useDreamSegments wrote last,
  so thumbnails fell back to the placeholder and reappeared on the next poll.
  Its refetchInterval never saw a filmstrip either, so it polled forever and
  kept the flicker going.
- useDreamSegments' select returned undefined whenever history wrote last, so
  dreamsToSegments dropped the segment. SegmentPreview then clamped the
  out-of-range index to 0 and the container ratio changed with it, which is the
  preview visibly jumping around.

Use getDreamResponse + select in transition-history so the stored shape matches
every other reader, and read the unselected response in refetchInterval.

FlowPreview also held a positional index into a list that legitimately grows as
renders land, so each new segment could shift what was playing. Track the clip
by dream uuid and derive the index instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ynpMfJGqavM4QnereDtUz
Take thumbnails go from 84x48 to 126x72. HISTORY_ROW_HEIGHT grows by the
same 24px (88 -> 112) so the settings header still reserves the strip's full
height and the preview below doesn't jump as takes complete.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012AB1rUaDPEQCR3Dmhye5mN
… check

Replace the gold disc with a check on processed transitions with a gold
filmstrip glyph: three frame windows between two rows of sprocket holes.
Windows and holes are cut out (evenodd) so the selection slab shows through.
The disc and its box-shadow ring are gone; the glow now follows the strip's
outline via drop-shadow.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012AB1rUaDPEQCR3Dmhye5mN
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