Commit Graph
10 Commits
Author SHA1 Message Date
Vito 4040007264 fix(workspace): keep open pages valid across a restart and recover a changed source (#430) (#434)
The gallery root scope and the Images listing cursor were keyed with a
per-process random key, so every ComfyUI restart made an open tab's
scopes answer 409 source-changed (blank Tag image preview, a Generate
that did nothing) and its cursors fail Load more.

- utils/workspace_keys.py: one random secret in workspace-scope.key
  beside the database (owner-only, never config.json or the database),
  two derived keys; per-process fallback with a warning. Move library
  copies it.
- Cursor v2 binds the listing scope in its MAC instead of sending an
  unkeyed hash of the root paths; an unverifiable cursor answers 400
  cursor-stale and Images offers Refresh instead of a dead Retry.
- POST /prompt_manager/images/rescope maps a stale target to its
  current root only when the held scope was issued for it.
- Images finds a renumbered file again from the inspector, the viewer
  and the Tag image dialog (the dialog stays open, draft kept); nothing
  else is kept by an old root_index:path key.
- Tag image dialog: a state that blocks Save shows its reason above the
  actions with Refresh, disabled actions look disabled, and an empty
  preview says "Preview unavailable" (D147).
2026-10-28 23:06:36 +00:00
Vito f95f66d76a ci(browser): run page workflows with parallel workers and CI shards (#274)
* test(browser): one test server per Playwright worker

The page workflows started one global webServer and ran with one worker.
Each worker now owns its own tests/browser/server.py on an OS-chosen port,
with its own temp root and database, through a shared `test` in
tests/browser/fixtures.mjs. baseURL, and so page, request and
browser.newContext, point at that worker's server. A server that dies
fails the running test with its stderr tail and the next test gets a
fresh one. The worker count comes from PM_TEST_WORKERS, else 1 under CI,
else a quarter of the CPUs up to 8.

Chromium workflows in the pinned image: 510 tests in 606 s with one
worker on main, 511 (one new isolation spec) in 119 s with eight.

* test(browser): test servers bind an OS-chosen port themselves

tests/browser/server.py accepts --port 0: it binds 127.0.0.1:0 before
building the app, holds that socket for its whole life and prints
PM_TEST_SERVER_PORT=<n> to stdout. startBrowserServer defaults to port 0
and reads the announced port, so no port is reserved and released before
the server binds and two launchers can no longer race for one. The
"address already in use" retry and free-port.mjs are gone; --port N still
works and is checked against the announcement.

* ci(coverage): shard and merge modes for the frontend coverage run

frontend-coverage.mjs now plans its steps from the command line.
No arguments still runs everything, then reports and checks.
--shard I/N runs one slice of the chromium+touch workflows into
coverage/js/raw without wiping it or reporting; shard 1 also runs the
node units and the viewer suite. --merge-only reports and checks over raw
coverage the shards wrote, with --update-baseline as before, and refuses
an empty or missing raw dir. npm scripts: test:coverage:shard and
coverage:merge.

* ci(viewerjs): run browser suites with workers and shards

Frontend coverage runs as three shards (PM_TEST_WORKERS=3 each) that upload
their raw V8 files as coverage-raw-N; a merge job downloads them, fails unless
all three arrived with raw files and no clashing names, then runs
coverage:merge. Firefox and WebKit page workflows run as two shards each with
three workers. The required viewerjs context keeps its name and now needs the
shards, the merge and the browser shards, failing unless every one succeeded.

* ci(viewerjs): cache npm downloads and size timeouts from the first sharded run

The first sharded run lost a webkit shard to its 12-minute limit after all
267 tests passed, and npm ci stalled for 5 minutes in six of eight jobs. Each
npm ci now restores npm's download cache keyed on the vendor lockfile, and
browser job timeouts are the measured times plus at least 50%, never under 20
minutes. The workflow comment records the measurements.

* test(browser): wait for rendering frames, not timers or luck, under load

Two Library workflows failed in CI's webkit shard 2/2 (run 39714840809)
under parallel load:

- "library infinite loading ..." counted cards right after the last
  page hid "Load more". That render queues the window update for the
  next frame, so a slow frame left the 120 cards of the all-rows
  layout in place (received 120, expected < 80). It now waits for the
  next frame, which runs after every callback the page queued.
- "row measurement preserves anchor ..." settled with a 60 ms timer. A
  scroll event is delivered in a rendering frame, so on a starved
  renderer the timer ended before the scroll's frame and no card was in
  view (null anchor). settleLibrary now waits for two consecutive
  native frames with no Library frame queued, checked inside the frame
  callback.

Before/after: a scratch copy that delays every frame by 120 ms (as a
starved runner) failed the old helpers 19 of 40 runs, at the CI lines,
and passed the new ones 40 of 40 on chromium and webkit. Throttled to
1 CPU with 4 workers, the original spec failed 1 of 42 for each test.

The Tags viewer workflow closed the viewer as soon as its image was
visible, before ViewerJS rendered the title, so CI's coverage never ran
the page's title callback (tags-page.js functions 26/46 = 56.52%, one
function above the floor of 56; locally 27/46). The test now asserts the
title names the shown file, which always runs the callback.

* test(browser): stable under parallel workers

Document how to run the browser suites in parallel and sharded, with the
measured times, and what keeps specs stable beside each other: wait for
a rendering frame, a response or a visible state, never for time.

With b665aa6 the full workflows suite passed on chromium, touch, firefox
and webkit with 8 workers, each test 3 times (6132 passed, 0 failed,
24 minutes), and once at CI size (docker --cpus=4, 3 workers: 2044
passed, 25.5 minutes).

The npm download cache removed the npm ci stall: 0 to 2 s in every job
of run 39722172529, against about 300 s in most jobs before. The
workflow's timing comment now records that run; the whole workflow took
8m54s against about 33 minutes before sharding. Shard imbalance (shard 1
carries the concept-comparison specs) is documented, not tuned.

* test(browser): server stderr on failures, no orphan servers, waits on state

Final review fixes for the per-worker test servers (#274):

- A failed test gets the stderr its server wrote during that test
  attached as `server-stderr`, so an aiohttp traceback behind a 500
  reaches the CI report. The launcher keeps the newest 256 KB with a
  stream position per test (stderrMark/stderrSince), and the fixture
  attaches it only when the test did not end as expected.
- The port announcement counts only as a complete line; a chunk ending
  mid-number was read as a shorter port.
- server.py --exit-on-stdin-eof shuts down gracefully (database closed,
  temp root removed) when stdin closes. The launcher holds that pipe,
  so a SIGKILLed worker leaves no server behind, on every OS.
- settled() gives up after 600 frames with the observation stats
  instead of hanging to the test timeout.
- The Classic text-sync test waits for the page's 50 ms timer to
  install serializeValue instead of sleeping 60 ms. With that timer run
  100 ms late (a scratch copy), the old wait failed 20/20 and the poll
  passed 20/20; the spec passed 240/240 at 8 workers x10 and 240/240
  at 1 CPU with 4 workers.
- The workflow test checks each npm cache key names its job's pinned
  image tag.

Every new unit test failed before its change (missing export, the old
regex timing out on the split announcement, an unknown server flag, a
hung settle, a mismatched cache key).

* docs(testing): server lifetime, stderr attachments and the final measured CI times

TESTING.md describes the stdin-EOF shutdown and the server-stderr
attachment, states that the npm cache key names the job's image tag
(now tested), quotes run 39725176320 at cbc8a61 (8m38s) and explains the
20-minute browser job limit. The workflow timing comment quotes the same
run.

* fix(browser): server shutdown and its tests on Windows, Python < 3.13 and CI

Three CI failures on ed809f7:

- Windows: server.py exited 1 on stdin EOF because create_app works with
  the temp root as its cwd, and Windows cannot remove a process's cwd.
  main() now restores the original cwd before the root is removed.
- Python 3.10 to 3.12: the lifetime test's cleanup called communicate()
  after closing stdin, which flushes the closed pipe (ValueError); 3.13
  tolerates it. It now waits and closes the pipes itself, and the exit
  assertion shows the server's stderr. Under 3.12: errors=2 before, OK
  after.
- Tooling (CI container): the SIGKILLed-worker tests treated an exited
  server as alive, because the container's PID 1 does not reap orphans
  and kill(pid, 0) succeeds on a zombie. Reproduced locally with docker
  without --init (2 failed); a zombie now counts as exited (2 passed).
2026-10-21 23:10:57 -07:00
Vito 3476f8ede9 feat(inspector): concept comparison, Show in folder and toast placement (#264)
* test(inspector): concept comparison for the panel and viewer details

Compare the Images side panel and the viewer's full details with the
concept (1600x1100 and 390x844, both themes): structure, tokens, geometry
and controls, plus copy, the acknowledged pair send, the tag chip filter
and Prompt history. mapInspectorConcept applies only D3, D20, D21 and D22;
a mutation test shows the comparison notices a changed role label and a
removed footer.

The comparison showed the role copy and send names lacked the concept's
": <title>" subject, so the inspector now passes its title to the prompt
blocks. It also showed that a recipe-linked image shows its recipe's own
category and tags (#224), which the concept fixture's recipes lack; that
is recorded as D38, and the fixture's new positive_links option links the
compared image to its positive alone.

* docs(inspector): renumber the recipe-category decision to D39

* feat(images): local-only Show in folder route

POST /prompt_manager/images/reveal takes the listing identity
{root_index, root_scope, path} and opens this computer's file manager at
that file. It answers 403 not-local unless the client is loopback, no
proxy header is present and Host/Origin name this computer; the scope is
checked with _gallery_scope_error (409) and the path resolves only inside
that root to a regular media file. Openers run as argv lists with an
absolute executable, no shell and DEVNULL streams (D38).

* feat(inspector): Show in folder after the File & provenance path

The viewer's File & provenance offers "Show in folder" right after the
Path value for a listed file only, POSTs its listing identity and toasts
the outcome (opened, not-local, unavailable or failed). The browser test
server stubs the opener; TESTING.md records the decision as D38.

* fix(images): keep the discovery alert clear of the Rescan toast

A rescan that leaves the Images discovery alert visible could show its
fixed bottom toast over the alert on a short window. The concept's toast
keeps its placement; the production-only alert now scrolls into view only
as far as needed, with a 96px block-end scroll margin (D42).

* docs(inspector): concept panel and viewer details

* fix(images): open Windows folders through the shell API, not an Explorer command line

Explorer splits its own command line on commas, so a gallery path such as
output\a,\Windows\System32\calc.exe,b\x.png escaped the roots through
[explorer, "/select,", path] (security review H1). Windows now selects the
file with ILCreateFromPathW and SHOpenFolderAndSelectItems, and on any shell
failure falls back to os.startfile on the folder. No process is started.

* fix(images): re-check, isolate and reap the Show in folder opener

- re-check that the real path is still a regular file inside its root
  right before launching; a changed target answers 404 (review L4)
- run openers on a dedicated single worker, not ComfyUI's default
  executor, and make dbus-send wait for the FileManager1 reply
  (--print-reply --reply-timeout=2000) so a missing service falls back
  to xdg-open (L1)
- reap launched processes on a daemon thread, give them a minimal
  environment with a fixed system PATH, and use /usr/bin/open on macOS
  and dbus-send/xdg-open resolved only on that PATH (L5)

* fix(images): accept Show in folder only as a same-origin JSON request, one at a time

- require an Origin equal to the request's own scheme://host:port and,
  when sent, Sec-Fetch-Site: same-origin; refuse every forwarding-style
  header (Via, X-Forwarded-*, X-Original-*, *-Client-IP, CF-Connecting-IP
  and the rest) as remote (security review M1, L2)
- answer a non-JSON body with 415, so a cross-origin request always needs
  a CORS preflight (L2)
- single flight plus a 1 s cooldown per process: 429 code busy (L1)
- path resolution errors such as ENAMETOOLONG are a JSON 404, and neither
  that nor a start failure logs the client's path at error level (L3)

* fix(inspector): say the folder is already opening on a busy Show in folder

A 429 busy from the reveal route now toasts "The folder is already
opening." The browser workflow checks that the reveal POST carries the
page's own Origin and an application/json body, and TESTING.md D38 records
the tightened route, the toast table and the accepted residual risk of a
header-less local proxy.

* test(inspector): compare the viewer details under a coarse pointer and prove its mutations

The touch project measured production's D34 40px disclosure targets against
the concept's fine-pointer summaries. The concept's plain-chip copy now also
carries the recorded D34 coarse rule, so both documents are measured under the
same emulation. The open File & provenance is compared too (D38 Show in folder
and D20 Download original mapped), and the mutation check now removes the
viewer's pair actions and Show in folder.

* refactor(inspector): attach Show in folder to the Path value it follows

The button was placed on the second fact row by position; the Path dd is now
built by the caller and kept, so reordering the facts cannot move it.

* test(inspector): let the uncertain-file send check fail again

Inspector send buttons are named "... to ComfyUI: <title>", so the anchored
regex could never match and the no-send assertion always passed.

* fix(images): run the Windows folder fallback inside COM and keep desktop session variables

Show in folder's os.startfile fallback now runs inside the same
CoInitializeEx/CoUninitialize bracket as the shell selection, as ShellExecute
callers should. The Linux opener keeps XAUTHORITY, DESKTOP_SESSION,
XDG_SESSION_DESKTOP and XDG_CONFIG_HOME, so a file manager can reach the X11
display under GDM or SDDM and honour the user's mimeapps.list.

A Windows-only test calls the real ILCreateFromPathW and ILFree on a temp file
and pairs CoInitializeEx with CoUninitialize through the module's helper,
without SHOpenFolderAndSelectItems, so no window opens on CI.

* fix(images): scroll to the rescan's discovery alert only when the toast band covers it

D42 scrolled the alert into view after every rescan, so a user scrolled down
the grid was sent back to the top. It now scrolls only when the alert
intersects the toast's band (its scroll-margin-block-end).

* refactor(inspector): name prompt actions as PR #263 does

The title option keeps its name, default and output ("Copy positive prompt:
<title>") and now uses #263's actionName helper; the region-name arrow no
longer shadows the title option.

* test(inspector): the comparison opens Filters by its D44 active-count name

After a tag chip applies its filter, the shared toolbar's Filters button reads
"Filters · 1" (D44, #263), so the comparison opens it by that name.

* fix(images): POSIX opener paths stay absolute under Python 3.13 on Windows

Python 3.13 on Windows no longer counts "/usr/bin/x" as absolute, so the
faked-Linux reveal tests failed on windows-latest py3.13 (the Linux and macOS
openers are POSIX paths whatever host evaluates them). Use posixpath.isabs for
those lookups.
2026-10-21 00:53:04 -07:00
Vito 00bda8f442 feat(inspector): concept prompt blocks, side panel and viewer details (#260)
* feat(workspace): concept prompt blocks

Shared prompt blocks (Images panel, viewer details, Library cards and
saved texts, recorded runs) now render the approved mock's markup
(app.js promptBlock a298-302, concept.css s550-588), #249 D22:

- section.prompt-section.<role>[aria-label="<Role> prompt"] >
  .prompt-heading (span.role-label "+ Positive prompt" / "- Negative
  prompt"; .prompt-actions: copy button.icon-btn "Copy <role> prompt",
  then send button.send.icon-btn "Send <role> prompt to ComfyUI") +
  p.prompt-text.
- No per-block source line; a file's evidence moves to the panel badge
  (D2) and "File & provenance" (D3). Recorded runs keep every field's
  source in "Frozen field provenance".
- Empty text reads "No negative prompt (explicitly empty)" and still
  sends an explicit clear. Absent, ambiguous and unresolved roles render
  p.prompt-text.muted with the existing status strings and no actions;
  unknown role names render nothing and cannot be sent or copied.
- Copy shows the shared toast "Prompt copied."; without a clipboard it
  opens the concept's "Copy prompt" select-to-copy dialog.
- The data-send-role / data-copy-role contract is unchanged.

The D22 interim overrides (26px actions, prompt-text margins, copy
status, source line) are retired so the concept's rules apply; the
action buttons measure the mock's 31x31. pre.prompt-text styling stays
only for the Library editor's Archive dialog and frozen field
provenance, which keep their own markup until their page rebuilds.

Test selector mapping:
  .prompt-block / .prompt-positive  -> .prompt-section(.positive)
  .prompt-block-heading h3          -> .prompt-heading span.role-label
  region pre / pre.prompt-text      -> p.prompt-text
  "Empty negative prompt"           -> "No negative prompt (explicitly empty)"
  "Send <role> prompt"              -> "Send <role> prompt to ComfyUI"
  .prompt-source / copy status line -> removed (toast or Copy prompt dialog)
  copy button 30x26                 -> 31x31 (measured in the mock)

* feat(inspector): concept side panel

The Images side panel is the concept's aside#image-inspector.detail-panel in the
order of app.js imageDetails: plain cover, heading with the format chip, filename,
the D21 evidence badge and category chip, prompt blocks, tag chips, the six facts
and the footer (Send to ComfyUI pair, Prompt history). Provenance, raw records and
downloads stay in the viewer's full details. Positive-scope tag chips set the
Images Tags filter to that tag.

* fix(inspector): mark the side panel's overlay rules with their D2 decision

The overlay lint (tests/vendor/concept-css.test.mjs) requires a /* D<n> */
marker on every workspace.css rule that overrides a concept selector; the
three .detail-panel rules from D2 lacked one.

* feat(inspector): full details in the viewer

The viewer's #viewer-details renders the concept's imageDetails(i, true):
the side panel content without the cover in a class-less wrapper, then
"File & provenance" and "Workflow & raw metadata" (pre[tabindex=0] > code
and the two downloads with their unavailable reasons), then the pair send
("Send to ComfyUI", labelled "Send prompt pair to ComfyUI: <title>") and
Prompt history buttons. One renderer now serves both variants.

Provenance wording (maintainer, 2026-10-20): the six per-fact
"<Fact>: Configuration evidence · <source>" lines and FACT_SOURCES are
gone; the concept's "Prompt evidence: <badge>" line carries the D21 badge
in lower case and is left out without a badge. The honest facts stay
(current-file note, Source, Path, Size, Dimensions, Format, Media type,
Modified, occurrence, association, Download original). An unverified
format reads "PNG extension", no stray leading dot.

inspector.css and images.css are deleted; only rules with a live
consumer moved to the overlay under #249 D3 (inspector wrapping, the
inert aria-disabled download, the narrow Images container, the Library
and recorded-run prompt blocks, the Archive negative's prompt-block, the
prompt sender) plus a D20 wrap rule for the viewer's buttons at 200% zoom.

D1 review follow-ups: library.css no longer restyles recorded-run
p.prompt-text.muted (m2); the copy fallback has a seam and unit tests for
a missing clipboard and a refused write (m3); a stale test title is
renamed (m4); the fallback trigger falls back to the same-role button
when the clicked one was detached by a re-render (m7); TESTING.md records
the D20 and D22 deviations (m1).

Selector mapping in tests: .file-inspector-inner -> .panel-heading,
.generation-facts -> .meta-grid (first), .file-facts -> details .meta-grid,
.file-name -> #viewer-details > div > p.small, .file-inspector-footer ->
#viewer-details, viewer "Send prompt pair" -> /^Send prompt pair to ComfyUI: /.

* test(comfyui): real-host specs read the concept prompt blocks

library-sender: an explicitly empty saved negative now reads the concept's
"No negative prompt (explicitly empty)" (D22). The prompt-sender spec's
panel and viewer selectors (complementary "Selected image details", the
card image opening the viewer, .meta-grid facts, the labelled pair send)
moved with the D3 commit.

* test(comfyui): the frozen run's prompt regions read the concept prompt text

* fix(inspector): coarse viewer-detail targets and Prompt history on the shown route

- Prompt history closes the viewer and, when its target is the route
  already shown, re-renders it as the concept's go() does (navigateTo
  plus a router refresh event), instead of doing nothing.
- The viewer details' disclosure summaries and "Download original" keep
  40px targets on coarse pointers (#249 D34), as TESTING.md promises.
- Drop the dead `.prompt-positive` rules and merge the two
  `pre.prompt-text` rules.
- TESTING.md records the side panel's D20/D21 deviations.
2026-10-20 20:15:21 -07:00
Vito 86d82186ee feat(images): concept Images page — toolbar, cards, star, viewer, compared against the mock (#258)
* feat(workspace): shared concept toolbar and dialogs

Add the concept's shared page toolbar (search.toolbar with the search
field, category filter, sort and Filters) and its dialogs in one modal
shell: "Refine this view" (Source / Folder / Media / Tags, Apply filters),
"Import & inspect" with only the real operations (Open inspector, Rescan),
and the "Copy prompt" select-to-copy fallback. The sort lists the
concept's three options first (D14); there is no Save view (D16). Every
dialog returns focus to its trigger on close or Escape.

Refs #249

* fix(workspace): bind shared dialogs to the page and its route

- Drop the invented Refine note; every field is real (D14).
- Ignore a late close event once the shell was reopened or replaced, so
  an action that opens the next dialog keeps it open.
- Dialog openers require the page scope; disposing it closes and removes
  the shell unless a newer page's dialog is open. Every route change
  closes the shared dialog before the next page mounts.
- Clear filters also reports category '' so the page clears the toolbar
  category, as the concept does.
- The toolbar requires its scope and falls back to the first sort for an
  unknown value; dialog callbacks are validated up front.

Refs #249

* feat(images): concept toolbar, filter row and caption

The Images page now renders the approved mock's heading, shared toolbar,
filter row, section caption and infinite continuation (#249, Task C2).

- Heading: "Your generations, their prompts, and the next idea." with
  Import (upload icon) before New prompt (primary).
- Toolbar (shared mountToolbar): search over filenames, linked prompt
  text, titles and tags; category (the Library's categories); sort
  Recently added / Name A–Z / Highest rated, then Oldest first and
  Name Z–A (D14).
- Filter row: Grid/List/Masonry segmented control, Thumbnail size,
  the scope chip ("All output folders" or "<source> · <folder>") and
  "Group by positive prompt →".
- Caption: "N of M images · K positive prompts" from positive_count; a
  plain cursor page keeps the first page's count and an unknown count
  hides the "· K positive prompts" part. The date span shows
  "<Today|Yesterday|weekday> · <Month D>" of the newest loaded result
  under a date sort and is hidden otherwise (D17).
- Continuation: "N of M images · more load as you scroll" with Load
  more, the concept's end copy, Retry for a failed page, and the
  concept's "K more images loaded. N of M." toast.
- The listing sends category, tags and sort=rating (PR B params).

Old control -> new control:
- "Search filenames" field -> toolbar search "Search workspace"
  (placeholder "Search prompts, tags, filenames…"), also matching
  prompt text and tags
- Source select -> Filters -> "Refine this view" Source
- Folder select -> Refine Folder (all sources' folders and ancestors)
- Media select -> Refine Media
- (new) Tags -> Refine Tags; Category -> toolbar category select
- Sort select (Newest/Oldest/Filename A–Z/Z–A) -> toolbar sort
  (Recently added/Oldest first/Name A–Z/Name Z–A, plus Highest rated)
- Layout buttons -> .segmented Grid/List/Masonry in the filter row
- Thumbnail size field -> label.small + select#image-size
- "Group by positive prompt" link -> button.chip "Group by positive
  prompt →" (#/library)
- Refresh -> Import -> "Scan an output folder" -> Rescan (D15)
- "Current gallery" link -> removed; Overview's tool cards link the
  legacy gallery (D29)
- "All images" h2 + count -> .section-caption #result-count
- .image-continuation -> .infinite-continuation (Retry kept)

Legacy behavior kept: video items, keyboard grid navigation, scroll and
filter restoration, bounded DOM, continuation retry, source-change
recovery (now disabling the toolbar, Filters and Import). The Filters
and Import dialogs close on route change and browser Back.

The shared toolbar (C1) called .some() on select.options, which is an
HTMLOptionsCollection in a browser (only the unit tests' fake DOM gave
an array); setValue and setCategories now spread it first. The layout
segmented control may wrap at extreme zoom (D31) instead of scrolling
the 390px page sideways.

The retired .image-filters, .image-view-controls, .image-toolbar,
.image-layouts, .image-collection, .image-continuation and duplicated
.image-workspace grid rules are gone from images.css.

* test(workspace): model select.options as a browser collection

The toolbar units' fake DOM returned select.options as an Array, so the
toolbar's .some() call passed there while it threw in every browser
(fixed in 12228e2). options is now an indexed, iterable, frozen
HTMLOptionsCollection-like object without Array methods; against the C1
toolbar three units fail with "sort.options.some is not a function".
The units also assert that an unknown sort falls back to the first
option and that a still-listed category survives a category refresh.

* fix(images): concept Load more focus, empty state and review rulings

Review of #249 C2:
- Load more and Retry focus the first appended image, as the concept
  does (app.js loadMoreImages). Focus moves without scrolling so the
  visible cards keep their place (bounded-DOM anchor), and only while
  focus is still on the pressed control: a viewer or a field the user
  moved to keeps it. When a last page adds nothing and the button hides,
  focus goes to the last image, never to the page; a failed page focuses
  Retry.
- The concept's empty state beside the grid: .empty "No matching
  images. Try another search or category."
- D17: the caption date shows only under Newest first (", YYYY" outside
  the current year) and is hidden under Oldest first and the name and
  rating sorts.
- en-US number grouping in the embedded count too.
- Refine "Folder" defaults to the concept's "All output folders". It is
  not narrowed by Source: the concept has no Source field, and the list
  holds every source's folders, so any pair can be chosen in one dialog.
- The shared toast moves to toast.mjs; library-editor re-exports it.
- The D32 overlay (extreme zoom) wraps the layout segmented control and
  the continuation row instead of pushing them out of the 390px column.
- The legacy gallery checks moved to Overview's "Open existing image
  gallery" card (D29).

Mapping additions: grid with no results -> .empty concept copy beside
the grid; Load more press -> focus the first new image (concept).

* feat(images): concept image cards; image opens the viewer

Cards are the concept media card: .media-art > button.image-open with the
image or video, and .media-info with the D10 title (linked title only when
the link is confirmed, else the filename; title attribute = path), the D11
subtitle "W x H · EXT · Seed N" (seed shown as the listing's string) and an
Inspect info button. The byte size and the Preview button are gone.

Activating the image (click, Enter, Space) opens the viewer and focus
returns to it on Escape; Info selects the image into the panel (D19).
Arrow, Home and End navigation, bounded rendering and scroll restoration
are kept.

Columns and gap now come from the concept grid's computed style
(gridMetrics) instead of a production breakpoint table, so the concept
breakpoints apply; a 120px minimum card bounds extreme zoom (D3, D31).
images.css card rules are retired into marked overlay rules. The scope
chip reads "All output folders" when no folder is chosen (D14).

* fix(images): pair-linked cards take the positive's title; concept Info, alt and size

- Pair links title the card, inspector and viewer from the linked positive's
  Library title or confirmed first line, never the pair title (the negative
  variant's name). The card now equals the concept card, title included.
- Info drops aria-pressed; the card's .selected carries selection, as in the
  concept.
- Card images take the card title as alt and the listing's width/height.
- One shared minimum card width for layout and the narrow-card class.
- A debounced search that lands after a sort change no longer reloads page 1.
- 200% zoom test also runs at an 800px viewport and device scale 2.

Refs #249

* feat(images): concept hover zoom

* feat(images): favorite images from the card

* feat(images): concept viewer shell

* fix(images): one favorite request per file across leaving and returning

The pending set and the receiver of each answer now live at module level
beside the saved visit. A press on a returned Images page waits for the
request the earlier page sent, and its success or rollback reaches the
card on screen, or the saved visit when no Images page is mounted.

* fix(images): mark a star busy while its favorite request is pending

The star carries aria-disabled="true" until the answer arrives, also on a
card rendered after returning to Images, so the ignored second press is
announced as unavailable instead of doing nothing silently.

* perf(images): repaint one card on a favorite toggle instead of the collection

stream.updateItem tells onChange which item it patched, and the Images
page then skips the full layout pass (computed style, grid metrics and
layout over every loaded item) because no card changed size or place.

* docs(images): say the lone-star overlay rule is interim until the checkbox PR

* test(images): concept comparison for the Images page

images-concept.spec.mjs compares the production Images page with the
immutable concept on the concept-shaped fixture: heading, toolbar, filter
row, caption, grid, the first six cards, continuation and panel box in
Grid, List and Masonry, at 1600x1100 and 390x844, dark and light, normal
and reduced motion; plus hover zoom, the viewer title, focus restore,
Info selection, search and Load more with its toast. mapImagesConcept
applies only recorded deviations (D3, D8 and D16 follow-up PRs, D9, D10,
D14, D15, D17, D29, D31, D32); removed row items flow again from the
concept's own boxes. A mutation check proves the comparison fails on a
.media-info padding change and on removed Inspect buttons.

Corrections the comparison demonstrated:
- the grid and continuation carry the concept's ids (image-grid,
  gallery-continuation);
- bounded rendering reserves the card footer at its fractional height:
  offsetHeight rounded a wrapped 67.5px row up, so at 390px every card
  was a pixel taller and the grid 6px taller than the concept.

Two mismatches wait for a ruling and are pinned on both sides: the
category order (concept prompt order, production alphabetical) and the
singular count line ("1 positive prompt").

* fix(images): ignore sub-pixel noise when the measured card footer grows

Under page zoom the fractional footer height (rect / scale) can carry float
noise above the stored value, which relaid the grid and scrolled for nothing.

* test(images): map D35-D37 and widen the Images concept comparison

- D35 (singular counts) and D36 (alphabetical categories) are mapped
  deviations instead of pinned values; a search for exactly one image
  checks both singular nouns, and a one-image Load more checks the toast
  and continuation.
- Grid, cards and panel at 1200x900 (2 columns, 275px panel) and
  1800x1100 (4 columns); Large and Compact join the layout loop.
- Masonry tops and heights compare unrounded within one layout unit per
  card above in the column (D3), not as rounded integers.
- The Save view and sample-tag re-layout is checked against the concept
  page with those items removed at runtime.
- The Load more row is compared before the press in both documents (D37).
- Two seedless fixture images (seedless_images on the concept reset)
  prove every row reserves the tallest footer (D3).
- D10 paths come from the fixture manifest; the D8 star margin is mapped;
  list-row derivations cite D3 and D9.

* fix(images): search never matches a pair's own variant title

Search covers the filename, the linked positive's text and Library title,
and its tags (#249 D12). The pair title names a negative variant, so a
variant name alone no longer admits the positive's images.

* fix(viewer): keep the Media controls group and drop a stale position description

The concept viewer toolbar is again a named group (#249 D34 #8), and
Previous/Next are described by the position line only while a single
item is loaded.

* docs(history): drop the remaining-polish notes the concept viewer and cards closed

The viewer now takes the concept stage and toolbar, and Images cards open
the viewer, select into the panel and favorite. Only the select checkbox
(#249 D8 follow-up) and the tag button (#249 D9) stay outside #209.

* test(images): the singular copy when scrolling loads one image; countCopy in the shared inventory helper

* refactor(images): retire the replaced images.css rules and lint the kept page sheets

The unused .image-facts rules go. The three prompt-block overrides on
concept selectors are marked #249 D22 interim until PR D deletes the
file, and every kept page stylesheet now passes the overlay lint.

* test(images): the extreme-zoom anchor probe reads the fractional footer measurement

Since the concept comparison (#249 C7) the grid measures footers with
getBoundingClientRect, so the probe that hooked offsetHeight never fired.

* test(images): cover the masonry fallback when a finite size underflows its ratio

* docs(images): concept Images page and recorded deviations

TESTING.md documents the Images page controls, the recorded #249
deviations and how the earlier workspace filters moved, and notes that
every kept page stylesheet is linted. The frontend coverage baseline
adds the toast, toolbar and workspace dialog modules.

* fix(images): show that Import → Rescan runs, and how it ended

The old Refresh button disabled itself while /gallery/rescan ran; Import →
Rescan closed its dialog and showed nothing. Now a toast says the rescan
started ("Rescanning output folders…") and how it ended ("Rescan finished.
N files in M output folders.", from the scan's own total and roots, or
"Rescan could not finish. Try again."). A reopened Import dialog shows
Rescan as "Rescanning…" and aria-disabled, and ignores a press on it.

Failing before: the held-rescan browser test saw no toast and no busy
state; the dialog unit test found no "Rescanning…" button.

* fix(images): rewrite the live status only when its text changes

A star press republishes the stream, and the textContent setter replaced
the continuation status, the count and the caption date with the same
text, so a polite role="status" region could announce it again. Text and
the role attribute are now written only when they change.

Failing before: a MutationObserver saw 10 records on those nodes after a
favorite toggle; now none.

* fix(images): Refine's Folder list follows the chosen Source

The workspace subfolder discovery returned only the union of every
source's folders, so "Source: temp" could be paired with a folder that
exists only under output and silently match nothing. In workspace mode
each root now lists its own folders, and "Refine this view" offers only
the chosen Source's folders: changing Source keeps a folder it also has
and otherwise returns Folder to "All output folders", as the old field
row did when it reloaded a Source's folders.

Failing before: the API test found no per-root folders (KeyError), the
dialog unit test and the browser test still offered output-only folders
under the input source.

* fix(images): an unchanged Refine apply keeps the loaded page

"Apply filters" (and "Clear filters" with nothing set) reset the stream,
refetched the first page and dropped the reader's scroll position even
when no filter changed. Like the toolbar's equality guard, an unchanged
refinement now does nothing.

Failing before: the browser test counted one listing fetch after an
unchanged Apply; now none, and the loaded cards stay in place.

* fix(images): one discovery alert when sources and categories both fail

With the server unreachable on load, the sources and the categories
status paragraphs each fired role="alert" back to back. The page now
keeps one alert, written once both first loads settled, naming whatever
failed ("Sources, folders and categories could not be loaded. Rescan
from Import to retry." when both did).

Failing before: getByRole('alert') resolved to two elements.

* fix(images): sort toolbar categories alphabetically on the client

D36 promises alphabetical real categories, which held only while the
server sorted them and a restored category was among its list. The page
now sorts them case-insensitively itself, a restored category included.

Failing before: the toolbar listed a server's [zeta, Alpha, beta] in
that order.

* refactor(db): drop the unused pair title from the link summary query

_link_summary has not read the pair's own title since the card took the
linked positive's Library title (D10), so the query no longer selects
it; the row indexes and the stale comment are updated. All values stay
bound.

Failing before: the summary SQL still selected pp.title.

* test(images): end negative checks on real signals, not fixed sleeps

The "no second request" checks waited a fixed 150 ms or 1000 ms and the
hover probe 450 ms, which can pass vacuously on a slow Firefox or WebKit
run. They now read the fetches the page's code started (the API client
calls fetch in the same task as the press), wait for a timer of the
toolbar's own debounce delay set after the debounced report, and wait
for the hover transition to finish.

Checked by mutation: without the favorite pending guard or the toolbar
equality guard, the rewritten checks fail.

* docs(testing): record the Rescan feedback, Source-bound folders and category sort

The Images contract now names the Rescan toasts and busy state (D15),
the Folder list that follows Source and the no-op unchanged refinement
(D14), and the page-side category sort (D36).

* test(viewer): escape every regex metacharacter in the filename match

* fix(viewer): title a linked card at once instead of waiting for its inspection

On a slow runner the viewer showed only the filename until the inspection
answered, while the concept titles it immediately. The card's linked
title is already loaded, so the header uses it on open.

The favorite test now waits for the first request to settle before the
Space press, which the one-request-per-file guard otherwise ignores.

* fix(images): cap the search at the listing API's 500 characters

A longer paste was sent as is, the API rejected it and the collection
showed "Images could not be loaded". The legacy search capped it.
2026-10-20 11:31:53 -07:00
Vito 9d398f9862 feat(workspace): concept shell — sidebar, topbar, heading and footer on the approved mock's CSS (#256)
* build(workspace): vendor the approved concept stylesheet with a drift check

* refactor(workspace): adopt the concept stylesheet as the workspace base

* feat(workspace): concept sidebar with real navigation counts

* feat(workspace): concept topbar with Theme and ComfyUI link

* refactor(workspace): one concept heading and footer for every route

* fix(workspace): keep the canvas link on the proxy mount

A resolver failure no longer stops the shell from mounting: the ComfyUI link falls back to the root read from the page's own mount path (never another origin). Tests cover a non-standard /ComfyBackendDirect mount, including that no request escapes it.

* test(workspace): concept fixture and shell comparison

* test(workspace): concept fixture carries the concept inspector facts

* fix(workspace): match the concept brand name and compare shell hover states

* test(workspace): shell comparison mutation check and docs

Add mutate() to the shared concept inventory and a shell mutation check: a
nav-row min-height rule and a removed .breadcrumbs each make the comparison
fail with the changed path. Harden the resolver-failure guard test with an
injection count, make the overlay lint fail when workspace.css is missing,
mark the library.css .row-actions leftovers for PR E, and document the
concept CSS base, the overlay lint, the comparison method and the recorded
shell deviations in TESTING.md.

* fix(workspace): focus the page title on a route change

A sidebar or history navigation now focuses #page-title, as the concept
does on every hashchange (app.js 1168), so a screen reader announces the
page heading instead of the main region. The first render leaves focus
alone and the skip link still focuses main. The heading module's comment
now describes what the shell does.

* fix(workspace): give the Overview nav entry its own home glyph

Overview reused the concept's Activity pulse byte for byte, so once the
Activity row lands two entries would share one icon. Overview now has an
outline home glyph. The shell comparison takes it from the D28 ruling
instead of the concept's activity path, and the icon test checks that
every approved glyph is distinct.

* test(workspace): accept only #249 decision markers and state the lint scope

The overlay lint accepted any D-number, so another issue's ruling such
as "D5 (#245)" could mark a concept override. A marker now has to be a
D-number with no other issue cited; the vocabulary rule that cited #245
reads "#245 decision 5". TESTING.md now states that the lint covers
workspace.css only, lists the unmarked overrides still in images.css,
and says PRs C and D run the lint over each page sheet they keep.

* perf(workspace): re-read only Library and Tags counts after a Library write

A confirmed Library or vocabulary write cannot change the Images count,
but it re-read all three, including the Images output listing. It now
re-reads only the Library and Tags counts. Each count has its own read,
so a write never cancels an Images read that is still in flight.
2026-10-19 23:04:40 -07:00
Vito 6d2213ead5 feat(inspect): declarative node support matrix (#250)
* feat(inspect): add a declarative inspection node semantics table

* fix(inspect): bound and isolate the semantics table

Bound the semantics loader: at most 1 MiB is read, containers may nest
at most eight levels, and a RecursionError from the JSON parser becomes
a SemanticsError instead of escaping.

The table is no longer loaded at import. get_semantics() reads it on
first use and caches only a valid result, and the resolver imports it
lazily, only for a profile with semantics on. The default ingestion
profile never imports the module, so a missing or corrupt table cannot
break scan, monitor or admin import. Inspection of a prompt graph with
an unusable table is unresolved with the reason "Inspection node
semantics are unavailable" and an error log, rather than a crash.

INSPECTION_PROFILE now has semantics on; no strategy consumes the table
yet, so resolution is unchanged.

* feat(inspect): resolve guider-based samplers

Under INSPECTION_PROFILE the resolver discovers samplers from the
semantics table and follows SamplerCustomAdvanced through its guider
link (Reroutes allowed) to a listed guider:

- CFGGuider: positive and negative, so the pair resolves.
- BasicGuider: positive only. The state is "absent" with the reason
  "Guider has no negative input", the existing missing-negative path.
- Any other guider, including DualCFGGuider (D17), is unsupported-node
  and names that guider.

Guider hops draw visits, scan and depth from the existing budgets. A
SamplerCustomAdvanced upstream in latent ancestry is an earlier stage,
so the result stays multistage-ambiguous. Positive and negative
provenance record guider_id. FaceDetailer (detailer-stage) is not a
candidate yet: deciding it needs output ancestry. DEFAULT_PROFILE never
loads the table and is unchanged.

Vector changes (inspection profile only, default vectors unchanged):
- f07b: no-sampler -> resolved (CFGGuider).
- f17: no-sampler -> unsupported-node (FluxGuidance, Task 2.3).
- f01b: no-sampler -> unsupported-node (EmptyFlux2LatentImage latent,
  Task 2.5).
- f01: no-sampler -> ambiguous (24 SamplerCustomAdvanced branches).

The no-sampler tests (resolver and inspect route) now use a sampler
class the inspector does not know, because f17 has a supported one.

* feat(inspect): trace conditioning through supported pass-through and selector nodes

* feat(inspect): treat detailers as sampler stages

* fix(inspect): choose selectors that share one condition together

* fix(inspect): bound detailer stage chains by the depth limit

* feat(inspect): support common text encoders and string sources

* fix(inspect): keep pair execution evidence behind unreadable string sources

* feat(inspect): accept any non-sampler latent origin

* fix(inspect): queue each node once in breadth-first ancestry walks

The latent and detailer image walks queued every link before checking
whether its node was already seen, and popped from the list head. A dense
shared fan-in grew the queue far past the visit budget before any visit
was charged. Both walks now use a deque and enqueue each node at most
once, charging every examined edge as scanned work; outcomes are
unchanged.

* fix(inspect): treat unlisted pipe and embed samplers as possible stages

The latent and detailer ancestry walks only flagged unlisted nodes with a
linked latent_image, guider, positive or negative input, so easy-use pipe
samplers, Impact pipe detailers and embed-driven video samplers were walked
through and the final sampler's prompt resolved silently. A separate
stage-shaped input set (adding pipes, embeds and custom-sampler parts)
now makes such nodes unsupported-node, named.

* fix(inspect): walk each sampler's latent ancestry once per resolution

Every selector branch run and candidate re-walked the same latent
ancestry, so conditioning-carrying origins such as WanImageToVideo turned
a three-selector graph from ambiguous into work-limit. The walk outcome is
now cached per sampler; branch runs of one candidate pay for it once and
other candidates are charged its visit cost, as the text memo does. Walks
that ran out of work are not cached. Every link input is still walked.

* fix(inspect): treat unlisted model-driven samplers as possible stages

Unlisted samplers that link a model with conditioning or a latent under
other input names (XlabsSampler, generic video samplers) were still walked
through, so the final sampler's prompt resolved silently. An unlisted node
with a linked model plus conditioning, neg_conditioning, samples or latent
is now unsupported-node, named; model patches such as LoraLoader and
ModelSampling nodes are not.

* feat(inspect): compute facts from the resolved sampler

* fix(inspect): merge facts across sampler branches when ambiguous

* fix(inspect): merge facts over unresolved stages and share stage inputs via the table

* feat(inspect): explain why a prompt could not be resolved

* feat(inspect): port legacy parser rules the new resolver missed (PromptManager)

R1: before 3.2.3 a save patch overwrote every PromptManager node's saved
text with the last-run node's prompt. The legacy viewer recognizes the
signature (comfy-metadata.js createContext: two or more PromptManager
workflow nodes with one API text and different widget values). Inspection
now refuses those nodes as malformed instead of resolving to another
node's prompt; the inspector hands the parsed workflow to the resolver for
this check only. Lands before the new pass-throughs (R6) that would
otherwise make these graphs resolve to the wrong text.

R7: this repository's node classes up to 07a5af3 (PromptManagerV2,
Positive, Negative, Detailer, V2Text, PositiveText, NegativeText) resolve
with their own outputs and their exact strip and ", " join.

The default (ingestion) profile ignores both.

* feat(inspect): port legacy parser rules the new resolver missed (A1111)

R3: an accumulating batch saver repeats the A1111 parameters record once
per batch item, identical except "Batch index: N". The bounded PNG reader
now keeps the last copy (Pillow's rule, which #165 relied on) instead of
calling the file unreadable. Any other difference, including a missing or
second batch index, is still a conflict, and decoding the copies draws from
the duplicate role budget.

R4: parameters text that starts with "Negative prompt:" has an empty
positive (#165), instead of showing the delimiter line as the positive.

R5: the settings footer may be followed by sd-dynamic-prompts' one-line
"Template:" and "Negative Template:" records, and may be "Steps: N" alone
when there is no full footer.

R10: commas inside A1111's quoted settings values no longer split them, so
an "ADetailer prompt" with a comma keeps the other facts known.

The facts' A1111 reader mirrors R4, R5 and R10; the default (ingestion)
profile keeps the historical split. Also from the Task 2.7 review: a guard
test fails on any uncoded unresolved reason without a user line (M1), and
the encoder bounds role lines to the client's 512 code units (M4).

* feat(inspect): port legacy parser rules the new resolver missed (installed nodes)

R6 and R9 for nodes whose sources are installed beside ComfyUI, each
entry's input names and output slots read from the node's code (the task
report lists file and line):

- detailer stages UltimateSDUpscale (image), UltimateSDUpscaleNoUpscale
  (upscaled_image) and DetailerForEach (image), judged with their upstream
  samplers like FaceDetailer;
- pass-throughs ControlNetInpaintingAliMamaApply and LTXVConditioning
  (positive 0, negative 1) and FluxKontextMultiReferenceLatentMethod;
- encoder Text to Conditioning, strings TextInput and DisplayText;
- facts model pass-throughs Lora Loader (LoraManager), Power Lora Loader
  (rgthree), ModelPatchTorchSettings, CFGNorm, PerturbedAttentionGuidance.

Impact Pack encodes a detailer's non-empty wildcard instead of its positive
input, so FaceDetailer and DetailerForEach now list it as a prompt
override: a literal wildcard is decided at run time and a linked one names
its source, instead of resolving to the positive input. The overwritten
PromptManager refusal (R1) still holds behind the new pass-throughs. The
default profile does not read the table.

* feat(inspect): port legacy parser rules the new resolver missed (upstream nodes)

R6 and R9 for nodes not installed beside ComfyUI here. Their input names
and output slots come from each pack's source at the commit the saved
workflows record in the node's cnr_id and ver properties (the task report
lists repository, commit, file and line):

- samplers ClownsharKSampler_Beta (RES4LYF), KSampler (Efficient) and
  ttN KSampler_v2 (latent input "latent");
- pass-throughs SeedVarianceEnhancer and ApplyInstantIDAdvanced (positive
  1, negative 2; output 0 is the model);
- selector Switch conditioning [Crystools];
- encoder smZ CLIPTextEncode: its text when with_SDXL is false; when true
  the connected CLIP decides at run time which inputs are read, so the
  prompt is known only when text, text_g and text_l agree;
- strings ttN text, CR Text, CR Prompt Text and TextBoxMira;
- facts model pass-through CR Apply LoRA Stack.

Inputs that replace what a sampler reads are not followed: RES4LYF options
(may carry a guider, and with sigmas replace the settings), an Efficiency
script and a ttN XY plot leave the prompt unsupported and the facts
unresolved. The overwritten PromptManager refusal (R1) still holds behind
SeedVarianceEnhancer. The default profile does not read the table.

* test(inspect): cover batch copies past the role budget and orphan widgets

* fix(inspect): match the batch index only in the settings footer

Review I-1: duplicate A1111 parameters copies were compared after blanking
"Batch index: N" anywhere in the record, so copies whose prompts differ
only by that phrase were accepted. The field is now blanked only inside
the record's one "Steps: N, ..." footer line, which must hold it exactly
once; any other difference stays a conflict.

Review M-4: the inspector parses the workflow record only when the API
graph can show the overwritten-PromptManager signature (a PromptManager
class, or a node with PromptManager's search_text input).

* fix(inspect): refuse overwritten PromptManager text inside subgraphs

Controller ruling on review M-1: PromptManager nodes inside subgraphs run
as "<instance>:<inner>" API IDs, nested instances chaining their IDs. The
overwritten-text check now walks subgraph instances (bounded in depth and
nodes) and reads a text widget stored by name, so these nodes are refused
like top-level ones, anchored or not. A definition without an instance
runs nothing and is ignored.

Review M-5: the historical adapters skip an explicit null prepend or append,
as the 07a5af3 code's "if prepend_text:" did.

* test(inspect): import unittest one way in the inspection suites

Code scanning flagged the mixed 'import unittest' and 'from unittest import
mock' forms. Use 'import unittest' plus 'import unittest.mock' everywhere.

* fix(history): name the recorded run's current-file prompts apart from its frozen fields

Now that the inspector resolves more saved files, the Recorded run dialog can
hold two regions named "Positive prompt" (and two of each Copy/Send action):
the frozen run field and the inline current file's prompt. Assistive
technology could not tell them apart, and the real-host chronology test read
two <pre> elements.

Prompt blocks take an optional context label. The inline current file inside
a recorded run names its regions "Current file positive prompt" and its
actions "Copy/Send current file positive prompt"; the visible headings stay
as in the concept. The real-host test now also scopes its frozen-field reads
to the "Frozen run prompt texts" section, and a disposable-server workflow
pins the distinct names with a resolved current file.

* fix(inspect): drop preview-only stages from unanchored candidates

A stage whose outputs reach only non-file outputs (PreviewImage and the other
non_file_outputs) cannot have written a saved file, yet it stayed an
unanchored candidate, so a preview-only FaceDetailer made the saved branch
ambiguous (f01b).

The inspection profile now drops such stages before detailer absorption,
using two linear backward passes over the graph (charged like the consumer
map; a failed pass keeps every stage). A stage without consumers, or one that
also reaches any other terminal, is kept, and nothing is dropped when every
stage is preview-only. Raw plus detailed double saves both write files and
stay ambiguous. A file in the host temp folder may itself be a preview, so it
is inspected with PREVIEW_INSPECTION_PROFILE, which keeps those stages.

Workspace facts follow the same choice: an absent or unresolved result that
names a sampler merges facts across that sampler's stages only, so the
dropped detailer's settings no longer blur the saved file's facts.

f01b now resolves (absent negative, guider has none); its vector is updated
and the expected failure removed. The default profile is unchanged:
differential against 39dcc40 identical over 160,256 cases. Over 60,054
inspection cases no previously resolved or absent result changed.

* fix(history): name the current file's prompt pair button apart from the frozen run's

The recorded run dialog nests the current-file inspector, whose footer repeated the "Send prompt pair" name. Name it from the prompt context, scope the real-host test to the frozen region, and add a dialog-wide duplicate-name check.
2026-10-19 17:26:37 -07:00
Vito d9537a726c feat(workspace): complete shared inspector and collection viewer (#224)
* feat(inspector): expose bounded physical file evidence

* fix(inspector): distinguish ambiguous saved associations

* feat(inspector): derive conservative generation facts

* fix(inspector): guard fact ancestry and inspection state

* feat(inspector): share complete file details and exports

* fix(inspector): preserve fact states and touch targets

* feat(viewer): navigate the loaded media collection

Refs #216

* fix(viewer): refit after current stage geometry changes

Ref #216. Observe the current media stage after readiness and wrapped title changes without rebuilding focused controls or resetting transforms at unchanged dimensions.

* feat(viewer): add lifecycle-safe slideshow and media preferences

* fix(viewer): resume gallery continuation after close (#216)

* fix(images): guard source recovery and wrap viewer actions

Clear stale root and folder intent, cancel old pages, and wait for current source discovery before loading a refreshed collection. Preserve safe retry through failed, superseded, and disposed discovery.

Wrap narrow viewer footer actions and wait for measured gallery viewport settling in responsive checks. Refs #216; addresses PR #224 review.

* test(viewer): await gallery layout after viewport changes

Require actual inner, root client, and document widths to agree after each responsive-case viewport and CSS zoom transition. Keep the existing exact frame, overflow, focus, and coarse target assertions unchanged.

Observed PR #224 CI initial mobile resize at537px before pending card relayout settled to356px. Refs #216.
2026-10-18 09:41:53 -07:00
Vito e7cfcc2bfb style(workspace): align browsing with the approved concept (#219)
* style(workspace): align approved shell and visual fixtures

* test(workflows): preserve evidence across browser projects

* style(workspace): match approved image browsing geometry

* style(workspace): match natural library card layout

* fix(workspace): retain wheel motion during row correction

* fix(workspace): contain zoomed controls and image metadata (#215)
2026-10-18 02:53:26 -07:00
Vito dc345671ae feat(workspace): inspect and send positive and negative prompts (#206)
* feat(workspace): inspect and send positive and negative prompts

* docs(workspace): capture complete inspector and viewer previews

* docs(inspector): explain the subprocess cleanup race

* fix(workspace): defer observed gallery layout writes (#207)

* fix(inspector): keep path-only prompt associations unverified
2026-10-17 13:43:19 -07:00