Replaces the scene-index-tick ruler with composer's new track.default
element: clips sized by real duration (frames/fps, not equal-width
boxes), a ruler in genuine elapsed seconds, and a playhead that follows
the selected scene but can be dragged to any exact second within a
clip via click-to-seek.
Also fixes a real crash found while live-testing this: addScene()
built a scene object missing source_image/references, which crashed
the Inspector the moment it tried to draw the new scene.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Found by the adversarial pass on the commit that moved it to the Project
tab: it had no persistence and no per-project sync at all. width/height/
length (project.video) get both -- a real dict field on Project, and
syncVideo() re-reading it into the controls on every project switch.
Negative got neither: it lived only in the pipeline-override controls'
own in-memory state, so it silently carried over from whichever project
was open last. Typed for project A, it was sent as project B's negative
prompt the moment someone switched -- with nothing on screen to say so,
and the Project tab's own copy ("Size and length live in the
Constructor") actively implying every field there was project-scoped.
Project.negative is now a real field (core/projects.py, round-tripped
through the existing whole-project PUT with no route changes needed),
and boot.js gained syncNegative(), wired everywhere syncVideo() already
is: onOpen, and after the pipeline changes underneath. Reproduced the
exact leak end-to-end before fixing it, and the new project_video.spec.js
test is that same reproduction, now asserting it doesn't happen.
Player: Save to bin (whatever the viewer shows, if it has a real file
identity) and Save frame (canvas-capture the video on screen, upload
through ComfyUI's own /upload/image, no new route needed). Both add
without navigating the viewer -- an upload is a real round trip, and
the result is added to the bin, not shown as a stale finished pick.
Save/Load Project File: a real download route with Content-Disposition,
and an import route that never trusts an id in the file -- an import
always lands as its own project, never overwrites one already here.
Generate All: every scene in order, waiting for each to finish before
starting the next -- not a montage (v5 has no render/stitch stage),
N separate generations each landing on its own scene.
Both project-switching (open/newProject/importProject) and the earlier
run-reattach path shared a bug class this session kept finding: state
captured at one moment, applied at a later async moment with no check
that nothing had moved on in between. Closed it properly this time --
a generation counter every switch bumps and checks, including start()
(the page's own handlers are live before it resolves) and undo/redo
(also "a different project is open now"), and scheduleSave() captures
the object an edit actually touched instead of re-reading a variable
that can point somewhere else by the time its debounce timer fires.
Four adversarial rounds against this range, each fixed before landing.
Two routes silently shared /api/log; a pack's "updates available" badge
outlived the update; nothing server-side stopped a restart mid-generation,
and the fixes for that had their own dead end, a lock scoped to the wrong
event loop, an unlocked status fetch, and a check that only ran once
instead of at the moment it mattered. A run's result was lost outright if
the project it belonged to wasn't the one open when it finished -- on a
project switch or a page reload. A brand-new project inherited the
previous one's width, then inherited the wrong "default" for it.
Found and confirmed via an adversarial pass over 9dcd94c..HEAD, one round
per fix.
First off the port list. Every edit in v5 was final -- v4 has had undo since
store.js, which is why it never showed up in a file-by-file inventory: there
is no undo.js to find.
A project is plain JSON and a few kilobytes, so undo is a snapshot before each
edit rather than an inverse per action. Capped at 40. The SELECTION travels
with the snapshot: undoing a removal and landing on a different scene than the
one that came back is not what anyone means by "take that back".
Two things it deliberately does not do. A new edit ends the redo line, or a
redo walks into a version that never followed from what is on screen. And a
project just opened has no past -- otherwise undo reaches back across a
project switch and replaces the open project with a version of a different one.
Edit menu: undo, redo, add and delete scene, move clip either way. Built on
each press so Undo is offered only when there is something behind it; a menu
that always offers it teaches people it does nothing. ⌘Z and ⇧⌘Z do the same,
except inside a text box, where the browser's own undo is the right one --
taking it over means one keystroke un-typing a paragraph instead of a word.
View gains Reset layout, for the person who closed both panels and cannot
remember what brings them back.
Found by the adversarial pass, reproduced twice.
Switching projects put the new project's numbers in the store and left the
OLD ones in the controls that produce what a run actually sends. The store's
own getter reported the new project correctly and the Inspector agreed, so
the only place the stale value showed was the Constructor -- a window nobody
has a reason to reopen after switching. Generate then ran at the size of the
project you left.
The attacker found it on the File-menu path. It is on three: opening from the
menu, opening programmatically, and applying a pipeline change, which rebuilds
every control from the PIPELINE's defaults and discards the project's. So the
fix is not in the menu handler -- the store now says when a DIFFERENT project
is open, which is not the same event as a field changing, and everything that
follows a project is wired to that instead of to the one route in that
happened to be reported.
Second finding, same pass: setVideo said "nothing on screen draws from this"
and did not report the change. True when written; the timeline started
computing its clip widths and its ruler from that value an hour later. A
comment is not a thing the next change has to keep true.
Also serial-mode for the project spec: two tests making and switching projects
share one real store on the dev server, and the config runs a file in
parallel. A race over a JSON directory, not a bug in the app.
The surfaces v4 has that v5 did not. Each announces itself and each can be
absent without the rest noticing, which is the point of the exercise: the
frontend should read as v4, and nothing in it should be load-bearing for
anything else.
* SCENE AND PROJECT, in the column v4 keeps them in, which now says which
scene it is about. Length on the timeline is the scene's own CROP and says
outright that a regenerate reads the project's -- the rule that mixing the
two is how a per-scene value quietly becomes everyone's.
* RATINGS on a scene, and a clip carries its rating as a colour on its edge.
Stored, not yet read: nothing in v5 learns from them yet.
* A RULER, and clips as wide as they are long, so the strip reads as time
rather than a row of equal boxes. Both ends bounded -- one long clip must
not become the whole strip, and one short one must stay hittable.
* THE PICKER WHEEL, on the middle button anywhere. It shows whatever
announced itself to actions.js, so a part of the app that grew a new action
is in the wheel without the wheel being told, and a part that failed to
load is simply not among them. v4's three pinned buttons had a fixed idea
of what could go in them.
* THE WAY IN, and it closes -- on Escape, on the backdrop, on its own button.
v4's could only be completed: a page refresh was the only way out, which
makes it a trap rather than a wizard. Dismissing is a real answer.
* File > rename, and the listing moves with it rather than going stale until
a reload.
Two things the kit refused, correctly, and now nobody has to know: a wheel of
fewer than two items throws (there is nothing to aim between), so the shell
does not offer one; and a strip cell with no ceiling took the whole strip.
setResult existed and nothing called it; the timeline already drew
scene.result. One wire short of a loop -- generate a scene, and what came
back is on the strip and still there after a reload.
The scene id is read when the run STARTS. A run takes minutes and the user
goes on clicking, so the selected scene when it finishes is not the one that
was generated.
A strip cell now falls back to its glyph when a thumbnail does not arrive,
the way the bin's cells already did -- the browser's broken-image icon reads
as a damaged file rather than a missing one.
Width, height and length belong to the PROJECT, not to a scene: a scene
cropped on the timeline and regenerated comes back at the project's size,
because the crop was a timeline decision and a regenerate is a new scene.
They sit in the Constructor beside the prompt -- the two things settled in
one sitting -- and travel to the graph on the path the prompt already uses.
No new mechanism: the pipeline declares roles at `project.video` the same
way it declares the prompt, so core still names no node and no setting. A
role whose value is a number gets a number control instead of a box; that
is the whole of the UI change. What the pipeline does not offer is not
shown, so a graph with no length input simply has no Length.
Bounds are the NODE's and stay there -- it already refuses a value outside
them with a reason, beside Generate. A project file is the one thing here
that outlives the code that wrote it, so everything read back out of one is
checked: a whole positive number, or gone.
Also, four faults the browser suite was already red with before this:
* Six tests still clicked a rail toggle that moved into the timeline head.
* At 420px the panel OVERLAYS the centre -- and the named toggles are in
the centre, so the panel covered the control that closes it. That is the
v4 trap this project forbids. They are not offered at that width now; the
View menu, which nothing can cover, is the way in and out.
* A 768px window laid out 860px wide and scrolled sideways. Root cause, and
it was never about one component: a flex item's automatic minimum is its
CONTENT, so every stack, split, panel body and frame region in the app
handed its content's width upward. A scroller that cannot shrink below
what it holds is not a scroller -- the strip of scenes made the WINDOW as
wide as the strip. Floored at the five places every layout routes through.
* A bin test counted `.cx-cell-glyph` across the whole page, so the timeline
drawing cells with the same glyph in them broke a test about the bin.