Commit Graph
8 Commits
Author SHA1 Message Date
digital-garbage 62f92ecaf8 app: a menu bar, a timeline, and the prompt in a window of its own
v4's arrangement, which is the one that reads as an editor: a zone head
holds what acts on THAT zone, so the app grows a bar of its own for what
acts on the app.

* The menu bar. Brand, View and Settings, and whether ComfyUI is still on
  the other end. Only menus with something real in them exist -- an empty
  File menu is a promise the app does not keep.
* The prompt is written in the CONSTRUCTOR, a window. v4 called this the
  Composer; that name is taken here by the element kit, and two things
  called Composer in one repo is one thing nobody can search for. It is
  the longest thing a person writes and the least often read, and a
  permanent third of the centre column is the room the timeline needs.
  The host is built at startup and outlives every opening, so a window
  that opens twice does not throw away what was typed.
* The timeline replaces it, and Generate moves into its head with what
  the run is doing at the far end of the same head. No bar across the
  bottom: one that spans the window belongs to no region.

Four faults found on the way, three of them mine and one much older:

* THE OVERLAY ROOT WAS A STACKING CONTEXT. It documents at length that it
  must never be one -- and then set `position: fixed`, which makes one on
  its own. So the whole z ladder was measured inside that single element:
  an open menu at z 500 painted UNDER a button at z 1. Every overlay in
  the app was one positive z-index away from being buried, and the only
  reason nobody had seen it is that almost nothing claims a z-index. The
  root is static and zero-sized now; every layer in it is fixed already.
* Generate's hover ran `filter: brightness()`, which lightens the LABEL
  along with the fill under it -- the one thing that had to stay readable
  lost contrast in both themes. Only the fill moves now, and which way it
  moves depends on the theme: dark's accent carries near-black text so it
  goes lighter, light's carries white so it goes darker. Measured: no
  gradient stop under 4.1:1, hovered or at rest.
* A fixed-height head could not shrink below the width of everything in
  it, and a head now holds a title, three controls and a status. That
  min-content became the centre column's, so a 768px window laid out
  878px wide and scrolled sideways. It wraps now, as v4's does.
* A CSS patch of mine silently did nothing because its anchor text had
  moved and I had left the assertion off that one replacement. Every
  patch asserts now.
2026-08-30 22:11:53 +03:00
digital-garbage 5b59043fc8 core, app: a prompt you can type, on the main window
The prompt was a slot input on a CLIPTextEncode, reachable only by opening
the pipeline window and finding the node. A slot can now say that one of
its inputs belongs on the app's surface -- a place and a label -- and the
shell offers a region with that name or does not, exactly as a module's
panel works. Core validates that a role is names and nothing else.

Values for those inputs travel addressed by slot rather than as a whole
pipeline, so the window that owns the structure and a box that owns one
value cannot disagree about the slots between them. An address that is
not there is refused: a prompt typed into a box for a removed slot has
gone nowhere, and a run that starts anyway is not the run that was asked
for.

Region stand-ins are empty states now, taken down once something mounts --
'modules appear here' left above the modules that appeared explained
nothing to nobody.
2026-08-30 20:27:44 +03:00
digital-garbage 1b0f489950 core, app: what the panels hold now reaches the run
Every module panel was decoration. The values lived in the browser and
nothing sent them, so a modifier switched on was one the run never heard
about. They now travel with the pipeline on every check, and core places
them wherever a module says they belong -- announced, not known, so the
node that carries them is not privileged.

Settings with nowhere to go are said beside Generate rather than
swallowed, and the warning survives the run it is about: the status line
is redrawn by every message, so a warning put there was gone the moment
the run said Queued.

Also: a settings row in a docked panel squeezed its label to one word per
line, because the control keeps a 140px floor and took it out of the
label. Below the width where they fit side by side, they stack.
2026-08-30 20:16:46 +03:00
digital-garbage 7337721943 app: the bin — results a session produced, in three views
A second run replaced the first on screen with no way back. Results now
accumulate in the Assets panel across runs, and the bin owns what the
viewer shows: the session subscription drove the viewer as well, so every
progress message walked a click on an older result back to the newest.

Grid, list and icons are one collection in three shapes, sharing selection
so switching view is not a way to lose it. No <video> thumbnails, ever --
that is v4's connection-pool stall. A thumbnail that does not arrive falls
back to the glyph rather than the browser's broken-image icon.
2026-08-30 20:07:07 +03:00
digital-garbage ea30989752 app: half the queue was never read
/queue answers with queue_running AND queue_pending, and only the first was
looked at. A job sits in the second from the moment /prompt returns until the
worker picks it up -- always briefly, and for as long as it takes whenever
something is running ahead of it, which on a shared box is the ordinary case. A
reload in that window found nothing, handed Generate back, and let the same job
be queued twice: the first copy then ran unnamed, its messages failing the id
check, burning GPU time while the UI narrated the second.

Every fixture in client.test.js was cleaner than the real payload -- none of
them carried a queue_pending key at all, though ComfyUI always sends both. That
is the third time a stand-in tidier than the real thing hid what it stood in
for, so the fixtures now carry both halves, including the malformed entries a
real answer can contain.

Also renamed, because the function no longer answers the question its name
asked: queuedFor returns which half it found the job in, and a job waiting its
turn is adopted as QUEUED. Saying "working" over a run that has not started is
the same small lie as saying "cancelled" over one that has not stopped.
2026-08-30 04:39:00 +03:00
digital-garbage 28550e5742 app: a button that looked live and did nothing, one line after being disabled
subscribe() delivers the CURRENT state at once, and boot.js registered it right
after wire() had deliberately disabled Generate for the length of the reattach
lookup. That first delivery drew an idle run, and drawing an idle run hands the
button back -- so on every single page load Generate looked available while the
page still had no idea whether it already had a run. The queue stayed safe,
because the generator waits on the same lookup, but a live-looking button that
answers a click with nothing is its own fault.

Fixed twice over, because either alone would rot. The subscription moved into
wire() with the rest of the wiring, and the button being held is now a state the
transport keeps rather than something a caller does once: draw() runs on every
change, and each of those was free to undo it.

The fake run had no subscribe() at all -- which is exactly why the suite could
not see this. That is the second time a fake was tidier than the real object and
hid the fault it stood in for, so it now delivers state on subscribe like the
real one, and announces changes.

boot.js is down to assembling six things and mounting the modules.
2026-08-30 04:33:40 +03:00
digital-garbage 0a7a2a1917 app: the load ordering, where every fault on this path has been
reattach() was started and not waited on, and the Generate button is on screen
before it resolves -- so on a reload during a generation the phase still read
"idle" for the length of the queue lookup, and a click in that window queued a
SECOND job. The adopt that would have found the first one then refused, because
the phase was no longer idle, and from that moment the original run's messages
belonged to nobody: not shown, not cancellable, still burning GPU time.

Generate now waits on that lookup, and says so while it waits rather than
sitting dead with no explanation. A queue that never answers still gives the
button back.

The ordering moved into shell/session.js with the rest of it. session.js's own
docstring said the faults it was extracted to catch lived in boot.js "where
nothing could be driven by a test" -- and then the fix was wired up in boot.js,
where nothing could be driven by a test, and reopened the same class of fault.
The wiring is the thing under test, so it lives where tests can reach it.

Also: the fake run's gate now starts OPEN. Held shut by default, any regression
letting an extra start() through hung the suite instead of failing it, and a
hang says far less than an assertion.
2026-08-30 04:27:51 +03:00
digital-garbage 4bc6a1e400 app: one client id per TAB, one run per click, and a reason that survives
Two faults from the round, and a third I introduced fixing them.

ComfyUI keys its socket table by client id and pops the old entry when a new
socket arrives with the same one, so two tabs sharing an id meant the second
took the first's updates -- the first froze mid-progress while the GPU carried
on -- and the socket's `finally` pops that id again on close, so closing the
dead tab disconnected the live one. The id lives in sessionStorage now: it
survives a reload of this tab, which is what reattach needs, and does not reach
another tab. Duplicating a tab copies sessionStorage and is still a collision;
said in the comment rather than pretended away.

Generate only went dead when the RUN reached "queued", and reaching it takes a
round trip to ask what to queue. For the whole of that round trip the button was
live, so two clicks each posted their own /prompt: two jobs burning GPU time,
with the UI following one.

Both lived in boot.js, which runs on import and needs a document, so none of it
could be driven by a test. That wiring is now in shell/session.js and tested --
and the first version of those tests used a hand-rolled transport whose draw()
did nothing, which hid the fault the change introduced: the real draw() writes
the status from the run state, and a refused attempt leaves the run idle, so the
reason was overwritten with "Ready" in the same breath as it appeared. The tests
use the real transport, and a refusal now outlives the redraw that follows it.
2026-08-30 04:09:02 +03:00