Commit Graph
206 Commits
Author SHA1 Message Date
Vito Sansevero cfc963bb55 fix(system): show the Images page count in System Overview
System > Overview's Images metric read the overview's indexed
generated_images rows, while the workspace Overview card and the Images
sidebar badge show the Images page's own total, which also lists files
no row links (110 against 122 on the demo library). The concept's
System metric and sidebar badge are both `images.length`, so the metric
now reads the shell's Images count: one source for all three, a dash
until it is read or when its read failed, re-read on each visit.

The Statistics dialog's "Library totals" were the legacy /stats raw row
counts under the same nouns; they now read "Database records" with
"Prompt records", "Tags on prompts" and "Image records". /stats and the
legacy admin page are unchanged.

Closes #555
2026-10-31 10:34:45 -07:00
Vito d88cf9e7c6 fix(quality): sweep the Code Quality standard findings (#558)
* fix(quality): preserve cursor iteration and option validation

Keep prefetched rows on ManagedCursor so partial loops and explicit fetches share one position, and stop existing iterators after close or retirement. Preserve chunked reads and native SQLite result lifetime on reexecution and errors.

Catch OptionError before ValueError so legacy auto-tag requests receive the actual validation error. Remove the overwritten ViewerJS title option.

Regression tests failed before the fixes for lost rows, reads after close/retirement, and misleading option errors. Focused Python checks pass; cursor checks also pass on Python 3.13 and 3.14. Refs #321.

* fix(quality): make the test doubles legal raises and hashes

The fake queue no longer raises an attribute that starts as None. The
oversized kind records hashes instead of raising from __hash__, which
also catches a hash that production code swallows. Hostile.__bool__
raises the TypeError a truth test may raise.

* fix(quality): drop the extra saveCoverage argument

* refactor(quality): remove assignments that are overwritten before use

* refactor(quality): remove the remaining overwritten test assignments

* refactor(quality): drop imports repeated inside functions

* refactor(quality): give duplicate identity keys one shape

* refactor(quality): name the unused job event loop variable

* refactor(quality): release model gates in finally, not except BaseException

* refactor(quality): end the job runners with an explicit return

* refactor(quality): use the precise unittest asserts

* refactor(quality): import unittest.mock one way in the tests

* fix(quality): say why each empty except is safe, log the broad ones

* refactor(quality): stop binding values the tests never read

* refactor(quality): remove the unused preview aliases and tag page query

* refactor(quality): remove re-exports nothing imports

* refactor(quality): parenthesize deliberate string joins in lists

* refactor(quality): remove unused JavaScript variables and imports

* refactor(quality): use the precise assert in the model status test
2026-10-31 05:43:48 +00:00
Vito Sansevero 7e1847955b fix(viewer): destroy a pending viewer whose element left the stage
rebuild() empties the stage while a view is pending, and the deferred destroy then failed on the viewer's null parent: a page error, and the image kept its src. The destroy now finishes and the src is released.
2026-10-31 02:01:06 +00:00
Vito Sansevero f924d7195d fix(workspace): Overview cards show the sidebar's counts (#549)
Closes #547
2026-10-31 02:01:06 +00:00
Vito Sansevero 395dc74cbe fix(trash): trash on an output folder that is a mount point (#543) (#544)
Closes #543
2026-10-31 02:01:06 +00:00
Vito Sansevero f80e67c812 feat(workspace): selection bars stay in view, Back to top, side panels stay with you (#531) (#541)
Closes #531
2026-10-31 02:01:06 +00:00
Vito Sansevero ab6fee3a82 fix(viewer): arrow keys navigate from anywhere; a shortcuts overlay (#532) (#534)
Closes #532
2026-10-31 02:01:06 +00:00
Vito Sansevero f53a43152d feat(review): link an image to a library prompt from Needs review (#536) (#540)
Closes #536
2026-10-31 02:01:06 +00:00
Vito Sansevero fe63f01214 fix(send): name ComfyUI windows by their tab and explain why a send is blocked (#539)
Closes #535
2026-10-31 02:01:06 +00:00
Vito Sansevero aab0fedc10 fix(workspace): level two-column cards, window the Reusable tags list, full-width System (#530) (#538)
Closes #530
2026-10-31 02:01:06 +00:00
Vito Sansevero 085e45e475 fix(images): show every Show in folder outcome above the viewer and report opener failures (#537)
Closes #533
2026-10-31 02:01:06 +00:00
Vito ae3e191990 feat(workspace): show the one-time older-images notice in the shell (#529)
* feat(workspace): show the one-time older-images notice in the shell

After the switch everyone lands on Library, so someone who has not dismissed
the notice might never be prompted. It now shows above every workspace page
until dismissed, sharing pm_update_notice_dismissed with System > Sources and
the admin modal. Browser specs start with it dismissed.

Closes #527

* test(workspace): turn the older-images notice on where specs expect it

Two System specs read the Sources notice from empty storage, and the new
spec read a job list earlier specs had filled.
2026-10-30 22:06:17 +00:00
Vito cd284432f5 feat(cutover): every entry point opens the workspace; legacy pages stay reachable (#528)
* feat(cutover): every entry point opens the workspace; legacy pages stay reachable

The top-bar PM button, the nodes' Web UI buttons and /prompt_manager/web now
open the workspace Library. Overview's cards open their native pages. The legacy
pages stay at their URLs, with no route changed.

Classic Web UI no longer navigates the canvas away when a popup is blocked; it
shows a notification, as the focused node does.

Closes #525

* feat(cutover): link the legacy pages from System and when the workspace fails to load

System's Overview section ends with one small Legacy pages line (D165). If a
workspace module fails to load or throws while starting, the page shows a static
message with the same links instead of an empty shell.

Part of #525

* test(cutover): prove the Pair node's Web UI on the real host

Part of #525

* test(cutover): count the new real-host cases in the stock matrix pin

The PM button test and three renderer-and-reload tests, in 2 projects, take the
stock matrix from 110 to 118.

Part of #525
2026-10-30 22:06:12 +00:00
Vito a6d6129ec6 fix(library): keep the prompt page heading on one row; no history subtitle on a deleted prompt's page (#524) (#526)
* fix(library): keep the prompt page heading on one row; no history subtitle on a deleted prompt's page (#524)

From 951px the heading actions keep one row and a long title clamps to two lines beside them (D164). A deleted prompt's page hides the Prompt history description.

* fix(library): drop the CSS file name from a comment the consumer check reads (#524)
2026-10-30 12:16:55 +00:00
Vito e8d96a0f55 fix(library): delete review and prompt page polish from the live test, and a More menu on the prompt page (#512) (#522)
* feat(library): set the category on many selected prompts (#437)

The Prompt library shelf gets the concept's Images selection checkbox on each
card and its .bulkbar (N selected, Set category, Clear), recorded as D148.
Set category opens the shared one-value dialog and posts
{prompt_ids, category, expected_epoch} to POST /prompt_manager/bulk/category:
the server checks the storage epoch the shelf was read under, then sets the
category on the Library parents in one BEGIN IMMEDIATE transaction, writing
only rows that change. A restored storage answers 409 storage-changed and
writes nothing. The legacy admin and images_filter bodies are unchanged.

This replaces the legacy admin page's bulk Set Category, a cutover
prerequisite (#334).

Closes #437

* feat(library): add tags to many selected prompts and select every loaded prompt (#442)

The Library selection bar (D148) gets Add tags and select-all of the loaded
shelf, the legacy admin page's bulk Add Tags and select-all checkbox.

Add tags posts {prompt_ids, tags, expected_epoch} to
POST /prompt_manager/bulk/tags: in one BEGIN IMMEDIATE transaction the server
checks the storage epoch, normalizes the names as image tags are, gives each
Library parent only the names it lacks (casefold), finds or inserts tag rows
by exact name only when some prompt gets them, and skips prompts that would
pass 20 tags. Prompt tags only. Both bulk/tags forms refuse a non-object body
with 400.

Ctrl/Cmd+A in the grid and "Select all N loaded" in the bar select every
loaded prompt; the offer follows each loaded batch.

Closes #442

* feat(vocabulary): tag search, tag sort and an Untagged prompts entry (#444)

Tags & categories gains a server-side tag search and sort (keyset pages,
cursor scoped to the query) and an "Untagged prompts" row whose Browse opens
the Library with tag_mode=untagged. Refine this view gets Tag match, saved
Library views keep it, and the routes echo the applied query so a stale
server is an error, never an unfiltered list. Recorded as D149.

Refs #444

* feat(library): filter prompts by output folder and browse tags with Any (#444)

Refine this view on the Prompt library gains the concept's Folder (folders
below an output root, read when the dialog opens) and a Tag match select
(All / Any). The listing takes folder and tag_mode, matches folders on the
normalized file_path under every spelling of each output root, echoes both
so a stale server is an error, and saved Library views keep them. Recorded
as D150.

Refs #444

* docs(library): record Add tags and select-all as D151 (#442)

* feat(library): archive paired and delete unpaired selected prompts on the server (#443)

A prompt_archive table marks Library parents hidden from the shelf; older
builds ignore it. New routes /prompt_manager/library/delete/preview,
/library/delete and /library/restore classify a selection (delete,
archive, protected, already archived, not found), execute exactly what was
reviewed under a records hold, take a verified safety backup (disk checked)
before any delete, and restore archived prompts. The shelf, search and the
shelf-facing counts honor the archive (?shelf=active|archived); archiving
repairs a stale prompt hash so scans find the archived row.

* feat(library): Delete… on the selection bar, Show archived prompts and Restore (#443)

The Library bar's Delete… previews the selection and opens the shared
review: what is deleted (in no saved pair), archived (paired: hidden,
restorable) or kept (tagged __protected__), the safety backup and what
cannot be undone. Show archived prompts (N) lists archived prompts with
Restore on their bar; an archived prompt's page shows an Archived notice
with Restore. TESTING.md records D152; the route snapshot gains the three
workspace routes.

* test(library): cover the Delete… review failure and close-only paths; record coverage floors (#443)

* fix(library): let saved-view defaults fill the missing #444 filters

libraryViewState passed `?? 'all'` and `?? ''` fallbacks that
normalizeViewState already applies to an undefined value. The redundant
branches were never taken in the page, which dropped saved-views.mjs below
its 95% branch floor. A unit now covers a shelf without the new filters.

Refs #444

* fix(library): lay out a loaded page of prompts without waiting for an animation frame (#443)

A starved renderer (headless WebKit under CPU load, a background tab) can
hold requestAnimationFrame for seconds. The shelf laid out each loaded page
only in its next frame, so the grid stayed empty after the first load or a
delete's reload while the caption already showed the new count. A loaded
batch is now laid out at once; measurements still flush per frame.

The regression test holds every animation frame and fails before the fix.

* test(saved-views): give the D153 Library view the tag_mode key (#444)

The Library saved-view state now holds tag_mode, and the API requires
every key, so main's new "Highest rated" spec posts it too.

Refs #444

* test(saved-views): give the D153 Library view the folder key (#444)

Refs #444

* fix(library): name the saved pair, not negative variants, as the archive reason (#443)

A prompt in a saved pair with no negative variants was described in the
Delete… review as "1 prompt with negative variants: hidden, restorable".
Paired prompts are archived because of the pair, so the Archive row now
reads "N prompts in a saved pair: hidden, restorable" and the first
sentence explains the pair. Found in the live test of #470.

* fix(library): nest Refine folders by path segment; backslash separates only on Windows (#444)

Live test B1: the Folder list indented a server list sorted as full-path
text, so "-" and "_" sorted before "/" and children landed under the wrong
parent (kiko/char under kiko-v6), with case variants interleaved. The
options are now a tree built by segment: each folder directly under its own
parent, case variants distinct siblings.

On POSIX a folder named "Metadata\Klein..." was listed but filtered to no
prompts, because normalize_folder read "\" as a separator. It now does so
only on Windows (ntpath), as the listing already did.

Refs #444

* fix(library): say why an archived prompt is archived, and name variants only when it has any (#443)

The archived prompt page's notice always said "This prompt and its
negative variants are hidden", also for a saved-pair prompt with none.
It now says the prompt is archived because it is in a saved pair, and
counts its negative variants only when there are any. From the live
test of #470.

* feat(library): delete a prompt in a saved pair permanently, with its own pairs (#482, server)

The Delete review's preview now marks which archive ids may be deleted
permanently (in a saved pair, and nobody else's negative), with a review
digest of the prompt and its pairs and the counts the review shows. The
write accepts an optional delete_paired_ids with their digests. Each is
deleted with its own saved pairs (image-pair links, then pairs, then the
prompt as #443 deletes it) only while its digest is unchanged. Any drift
archives it and counts it changed. Paired deletes are backed up and
disk-checked like deletes.

* feat(library): ask Archive or Delete permanently for prompts in a saved pair (#482, review)

The Delete… review asks, for prompts in a saved pair, Archive (checked
on every open, D152's review) or Delete permanently, with their saved
pairs, in a D90 radio row. The choice rewrites the grid, sentences, title
and primary in place (openReviewDialog setRows/setPrimary), and the body
carries the chosen ids with their review digests. On the archived shelf,
Show archived prompts now reads the concept's way back, "← Prompt
library", as a plain button. TESTING.md records D154.

* docs(testing): number #482's deviation D160 (D154-D159 are taken)

* Revert "docs(testing): number #482's deviation D160 (D154-D159 are taken)"

This reverts commit 96c2a293a4.

* docs(testing): number #482's deviation D160 (D154-D159 are taken)

* feat(library): Delete prompt… on a prompt's page (#481)

The recipe editor's actions read Create text revision, Archive variant…
(the selected negative variant's archive, renamed from Archive… so the two
danger buttons cannot be confused) and Delete prompt…, which closes the
editor and runs the Library's Delete… review for that prompt: the same
preview, saved-pair choice (Archive stays the default), backup and write.
A delete opens the Prompt library; an archive reads the page again.

Recorded as D161 in TESTING.md and in the editor concept comparison.

* fix(library): hold Edit metadata while a prompt's page delete runs (#481)

While the page's Delete… review or its write runs, Edit metadata is
aria-disabled and its click ignored, so no editor save can race the delete.
Adds tests for a failed review, a write answered after the page was left
(no toast, no jump, the shelf reads again) and heading focus after an
archive.

* feat(library): delete archived prompts from the archived shelf (#492)

The archived shelf's bar offers Delete… after Restore. Its review is the
permanent delete only (no Archive choice: the prompts are archived
already), with #482's grid, safety backup, review digests, records hold
and epoch fence. A prompt used as another prompt's negative stays
archived; one restored since the shelf was read is kept and counted.

The preview and delete routes take an optional shelf ('active' or
'archived'). The held write classifies the same shelf, so the permanent
delete is backed up; on this shelf the write archives and rewrites
nothing and counts every reviewed id it did not delete as changed.

* fix(library): say only 'Nothing was deleted.' on the archived shelf (#492)

Review LOWs: the archived shelf archives nothing, so its empty outcome no
longer says 'Nothing was deleted or archived.'; a mixed write (deleted,
drifted, protected, gone and restored ids) counts each once.

* fix(library): open the archived shelf's delete review on Cancel (#492)

Its only action is a permanent delete, so focus starts on Cancel
(openReviewDialog's focusCancel); the spec also pins that the copy says
the delete cannot be undone and removes the prompt's saved pairs.

* fix(library): delete review and prompt page polish from the live test (#512)

- A prompt page whose prompt no longer exists says so, with Open the
  Prompt library and no Retry.
- The Delete review leaves out rows with nothing in them.
- A review from a prompt's page is titled Delete this prompt or Archive
  this prompt.
- An archived prompt used as another prompt's negative is named so.
- D162: a More menu button after Send to ComfyUI holds Delete prompt...,
  an accessible menu that fits 390px; Edit metadata keeps its entry.
- Item 5 already holds after #505; a regression test pins it.

* fix(library): act on a loaded prompt only, keep focus on a gone page, close the menu on focus loss (#512)

Review fixes: Edit metadata and the More menu are disabled while the
prompt loads or failed to load; on a gone prompt's page a focused control
hands focus to Open the Prompt library; the menu closes when focus leaves
it. A unit pins that a paired prompt with a missing digest is not called
another prompt's negative.

* docs(testing): number #512's deviation D163 (D162 is #514's)

* test(library): drive every More menu key and drop its unused close export (#512)

CI's coverage gate found more-menu.mjs functions at 71.42% (floor 80): the
ArrowUp-on-button, ArrowUp/Home/End-in-menu and Tab paths were untested and
an unused close() export counted as uncovered. The delete spec now drives
them; measured locally on that spec: 100% lines and functions.
2026-10-30 10:51:40 +00:00
Vito d0134b2d13 feat(library): delete archived prompts from the archived shelf (#492) (#509)
* feat(library): set the category on many selected prompts (#437)

The Prompt library shelf gets the concept's Images selection checkbox on each
card and its .bulkbar (N selected, Set category, Clear), recorded as D148.
Set category opens the shared one-value dialog and posts
{prompt_ids, category, expected_epoch} to POST /prompt_manager/bulk/category:
the server checks the storage epoch the shelf was read under, then sets the
category on the Library parents in one BEGIN IMMEDIATE transaction, writing
only rows that change. A restored storage answers 409 storage-changed and
writes nothing. The legacy admin and images_filter bodies are unchanged.

This replaces the legacy admin page's bulk Set Category, a cutover
prerequisite (#334).

Closes #437

* feat(library): add tags to many selected prompts and select every loaded prompt (#442)

The Library selection bar (D148) gets Add tags and select-all of the loaded
shelf, the legacy admin page's bulk Add Tags and select-all checkbox.

Add tags posts {prompt_ids, tags, expected_epoch} to
POST /prompt_manager/bulk/tags: in one BEGIN IMMEDIATE transaction the server
checks the storage epoch, normalizes the names as image tags are, gives each
Library parent only the names it lacks (casefold), finds or inserts tag rows
by exact name only when some prompt gets them, and skips prompts that would
pass 20 tags. Prompt tags only. Both bulk/tags forms refuse a non-object body
with 400.

Ctrl/Cmd+A in the grid and "Select all N loaded" in the bar select every
loaded prompt; the offer follows each loaded batch.

Closes #442

* feat(vocabulary): tag search, tag sort and an Untagged prompts entry (#444)

Tags & categories gains a server-side tag search and sort (keyset pages,
cursor scoped to the query) and an "Untagged prompts" row whose Browse opens
the Library with tag_mode=untagged. Refine this view gets Tag match, saved
Library views keep it, and the routes echo the applied query so a stale
server is an error, never an unfiltered list. Recorded as D149.

Refs #444

* feat(library): filter prompts by output folder and browse tags with Any (#444)

Refine this view on the Prompt library gains the concept's Folder (folders
below an output root, read when the dialog opens) and a Tag match select
(All / Any). The listing takes folder and tag_mode, matches folders on the
normalized file_path under every spelling of each output root, echoes both
so a stale server is an error, and saved Library views keep them. Recorded
as D150.

Refs #444

* docs(library): record Add tags and select-all as D151 (#442)

* feat(library): archive paired and delete unpaired selected prompts on the server (#443)

A prompt_archive table marks Library parents hidden from the shelf; older
builds ignore it. New routes /prompt_manager/library/delete/preview,
/library/delete and /library/restore classify a selection (delete,
archive, protected, already archived, not found), execute exactly what was
reviewed under a records hold, take a verified safety backup (disk checked)
before any delete, and restore archived prompts. The shelf, search and the
shelf-facing counts honor the archive (?shelf=active|archived); archiving
repairs a stale prompt hash so scans find the archived row.

* feat(library): Delete… on the selection bar, Show archived prompts and Restore (#443)

The Library bar's Delete… previews the selection and opens the shared
review: what is deleted (in no saved pair), archived (paired: hidden,
restorable) or kept (tagged __protected__), the safety backup and what
cannot be undone. Show archived prompts (N) lists archived prompts with
Restore on their bar; an archived prompt's page shows an Archived notice
with Restore. TESTING.md records D152; the route snapshot gains the three
workspace routes.

* test(library): cover the Delete… review failure and close-only paths; record coverage floors (#443)

* fix(library): let saved-view defaults fill the missing #444 filters

libraryViewState passed `?? 'all'` and `?? ''` fallbacks that
normalizeViewState already applies to an undefined value. The redundant
branches were never taken in the page, which dropped saved-views.mjs below
its 95% branch floor. A unit now covers a shelf without the new filters.

Refs #444

* fix(library): lay out a loaded page of prompts without waiting for an animation frame (#443)

A starved renderer (headless WebKit under CPU load, a background tab) can
hold requestAnimationFrame for seconds. The shelf laid out each loaded page
only in its next frame, so the grid stayed empty after the first load or a
delete's reload while the caption already showed the new count. A loaded
batch is now laid out at once; measurements still flush per frame.

The regression test holds every animation frame and fails before the fix.

* test(saved-views): give the D153 Library view the tag_mode key (#444)

The Library saved-view state now holds tag_mode, and the API requires
every key, so main's new "Highest rated" spec posts it too.

Refs #444

* test(saved-views): give the D153 Library view the folder key (#444)

Refs #444

* fix(library): name the saved pair, not negative variants, as the archive reason (#443)

A prompt in a saved pair with no negative variants was described in the
Delete… review as "1 prompt with negative variants: hidden, restorable".
Paired prompts are archived because of the pair, so the Archive row now
reads "N prompts in a saved pair: hidden, restorable" and the first
sentence explains the pair. Found in the live test of #470.

* fix(library): nest Refine folders by path segment; backslash separates only on Windows (#444)

Live test B1: the Folder list indented a server list sorted as full-path
text, so "-" and "_" sorted before "/" and children landed under the wrong
parent (kiko/char under kiko-v6), with case variants interleaved. The
options are now a tree built by segment: each folder directly under its own
parent, case variants distinct siblings.

On POSIX a folder named "Metadata\Klein..." was listed but filtered to no
prompts, because normalize_folder read "\" as a separator. It now does so
only on Windows (ntpath), as the listing already did.

Refs #444

* fix(library): say why an archived prompt is archived, and name variants only when it has any (#443)

The archived prompt page's notice always said "This prompt and its
negative variants are hidden", also for a saved-pair prompt with none.
It now says the prompt is archived because it is in a saved pair, and
counts its negative variants only when there are any. From the live
test of #470.

* feat(library): delete a prompt in a saved pair permanently, with its own pairs (#482, server)

The Delete review's preview now marks which archive ids may be deleted
permanently (in a saved pair, and nobody else's negative), with a review
digest of the prompt and its pairs and the counts the review shows. The
write accepts an optional delete_paired_ids with their digests. Each is
deleted with its own saved pairs (image-pair links, then pairs, then the
prompt as #443 deletes it) only while its digest is unchanged. Any drift
archives it and counts it changed. Paired deletes are backed up and
disk-checked like deletes.

* feat(library): ask Archive or Delete permanently for prompts in a saved pair (#482, review)

The Delete… review asks, for prompts in a saved pair, Archive (checked
on every open, D152's review) or Delete permanently, with their saved
pairs, in a D90 radio row. The choice rewrites the grid, sentences, title
and primary in place (openReviewDialog setRows/setPrimary), and the body
carries the chosen ids with their review digests. On the archived shelf,
Show archived prompts now reads the concept's way back, "← Prompt
library", as a plain button. TESTING.md records D154.

* docs(testing): number #482's deviation D160 (D154-D159 are taken)

* Revert "docs(testing): number #482's deviation D160 (D154-D159 are taken)"

This reverts commit 96c2a293a4.

* docs(testing): number #482's deviation D160 (D154-D159 are taken)

* feat(library): Delete prompt… on a prompt's page (#481)

The recipe editor's actions read Create text revision, Archive variant…
(the selected negative variant's archive, renamed from Archive… so the two
danger buttons cannot be confused) and Delete prompt…, which closes the
editor and runs the Library's Delete… review for that prompt: the same
preview, saved-pair choice (Archive stays the default), backup and write.
A delete opens the Prompt library; an archive reads the page again.

Recorded as D161 in TESTING.md and in the editor concept comparison.

* fix(library): hold Edit metadata while a prompt's page delete runs (#481)

While the page's Delete… review or its write runs, Edit metadata is
aria-disabled and its click ignored, so no editor save can race the delete.
Adds tests for a failed review, a write answered after the page was left
(no toast, no jump, the shelf reads again) and heading focus after an
archive.

* feat(library): delete archived prompts from the archived shelf (#492)

The archived shelf's bar offers Delete… after Restore. Its review is the
permanent delete only (no Archive choice: the prompts are archived
already), with #482's grid, safety backup, review digests, records hold
and epoch fence. A prompt used as another prompt's negative stays
archived; one restored since the shelf was read is kept and counted.

The preview and delete routes take an optional shelf ('active' or
'archived'). The held write classifies the same shelf, so the permanent
delete is backed up; on this shelf the write archives and rewrites
nothing and counts every reviewed id it did not delete as changed.

* fix(library): say only 'Nothing was deleted.' on the archived shelf (#492)

Review LOWs: the archived shelf archives nothing, so its empty outcome no
longer says 'Nothing was deleted or archived.'; a mixed write (deleted,
drifted, protected, gone and restored ids) counts each once.

* fix(library): open the archived shelf's delete review on Cancel (#492)

Its only action is a permanent delete, so focus starts on Cancel
(openReviewDialog's focusCancel); the spec also pins that the copy says
the delete cannot be undone and removes the prompt's saved pairs.
2026-10-30 04:18:01 +00:00
Vito 9af3271dbb feat(images): Show removed images and Return to library (#498) (#514)
* fix(images): background jobs skip files hidden by the removal mark; walkers skip trash folders inside output roots (#501)

- Thumbnail jobs and the legacy scan_output_dir listing leave
  library-removed files out (one mark check, mark_hides).
- Missing-file links also forgets removal marks that hide nothing any
  more (file gone from a folder that exists, or changed), only under a
  readable, non-empty source, re-checked at apply.
- The sidebar Images, Favorites and Needs review counts and the
  collection counts are read again when an image_delete job ends.
- utils/media_folders.py: one skip for thumbnails and trash folders
  (.Trash, .Trash-<uid>, .Trashes, $RECYCLE.BIN) used by every walker,
  the image monitor and the delete review.

* feat(images): Show removed images and Return to library (#498)

- GET /images/output?mode=workspace&removed=1 lists only files a removal
  mark hides; every workspace page carries removed_total.
- POST /images/removed/return {items | filter} starts a scan job,
  "Return images to the library": the marks that still hide their file
  go in one transaction, then exactly those files are scanned, so their
  prompt links come back from their metadata. Tags, categories,
  collections and favorites do not come back.
- The marks generation moves again after the commit, so a reader that
  cached the marks mid-transaction reloads them.
- Images: Show removed images (N) heading toggle (D162), Removed chips,
  a bar with only Return to library and Clear, Return to library in the
  side panel; the view is never saved or restored.

* test(images): record the return route in the legacy contract snapshot; removed_total in the workspace keys (#498)

The scan runner passes returned only when set, so a stand-in events
function without that keyword still runs.

* test(browser): wait for the answers two specs raced in the merge queue (#501)

- System diagnostics "level filter": wait for each level's own log
  answer, and poll the shown levels; the CRITICAL line is in the
  previous level's answer too, so it proved nothing (firefox).
- Library editor "restored storage after a confirmed save": wait for
  the PATCH answer, which waits for a whole restore in the test's
  route, instead of the 5 s expect default (webkit).

* fix(monitor): skip trash folders only below the watched folder (#501 review)

A source named .Trash-1000 (or another trash name) was ignored by the
image monitor while every walker still listed it; the monitor now tests
the folders below the longest watched folder holding the new file.

* fix(images): no default removedView function in the selection bar (#498)

The default arrow was never called, so images-selection.mjs fell under
its function coverage floor (97.29 < 98).

* fix(images): a category or tags chosen in Show removed images ends the view (#498 review)

The toolbar's Category and Refine's Tags stayed usable in the removed
view, and the listing then refused the combination with a page error.
Choosing one now leaves the view, as a removed file has neither.
2026-10-30 03:44:03 +00:00
Vito 98ddded73c feat(library): Delete prompt… on a prompt's page (#481) (#507)
* feat(library): set the category on many selected prompts (#437)

The Prompt library shelf gets the concept's Images selection checkbox on each
card and its .bulkbar (N selected, Set category, Clear), recorded as D148.
Set category opens the shared one-value dialog and posts
{prompt_ids, category, expected_epoch} to POST /prompt_manager/bulk/category:
the server checks the storage epoch the shelf was read under, then sets the
category on the Library parents in one BEGIN IMMEDIATE transaction, writing
only rows that change. A restored storage answers 409 storage-changed and
writes nothing. The legacy admin and images_filter bodies are unchanged.

This replaces the legacy admin page's bulk Set Category, a cutover
prerequisite (#334).

Closes #437

* feat(library): add tags to many selected prompts and select every loaded prompt (#442)

The Library selection bar (D148) gets Add tags and select-all of the loaded
shelf, the legacy admin page's bulk Add Tags and select-all checkbox.

Add tags posts {prompt_ids, tags, expected_epoch} to
POST /prompt_manager/bulk/tags: in one BEGIN IMMEDIATE transaction the server
checks the storage epoch, normalizes the names as image tags are, gives each
Library parent only the names it lacks (casefold), finds or inserts tag rows
by exact name only when some prompt gets them, and skips prompts that would
pass 20 tags. Prompt tags only. Both bulk/tags forms refuse a non-object body
with 400.

Ctrl/Cmd+A in the grid and "Select all N loaded" in the bar select every
loaded prompt; the offer follows each loaded batch.

Closes #442

* feat(vocabulary): tag search, tag sort and an Untagged prompts entry (#444)

Tags & categories gains a server-side tag search and sort (keyset pages,
cursor scoped to the query) and an "Untagged prompts" row whose Browse opens
the Library with tag_mode=untagged. Refine this view gets Tag match, saved
Library views keep it, and the routes echo the applied query so a stale
server is an error, never an unfiltered list. Recorded as D149.

Refs #444

* feat(library): filter prompts by output folder and browse tags with Any (#444)

Refine this view on the Prompt library gains the concept's Folder (folders
below an output root, read when the dialog opens) and a Tag match select
(All / Any). The listing takes folder and tag_mode, matches folders on the
normalized file_path under every spelling of each output root, echoes both
so a stale server is an error, and saved Library views keep them. Recorded
as D150.

Refs #444

* docs(library): record Add tags and select-all as D151 (#442)

* feat(library): archive paired and delete unpaired selected prompts on the server (#443)

A prompt_archive table marks Library parents hidden from the shelf; older
builds ignore it. New routes /prompt_manager/library/delete/preview,
/library/delete and /library/restore classify a selection (delete,
archive, protected, already archived, not found), execute exactly what was
reviewed under a records hold, take a verified safety backup (disk checked)
before any delete, and restore archived prompts. The shelf, search and the
shelf-facing counts honor the archive (?shelf=active|archived); archiving
repairs a stale prompt hash so scans find the archived row.

* feat(library): Delete… on the selection bar, Show archived prompts and Restore (#443)

The Library bar's Delete… previews the selection and opens the shared
review: what is deleted (in no saved pair), archived (paired: hidden,
restorable) or kept (tagged __protected__), the safety backup and what
cannot be undone. Show archived prompts (N) lists archived prompts with
Restore on their bar; an archived prompt's page shows an Archived notice
with Restore. TESTING.md records D152; the route snapshot gains the three
workspace routes.

* test(library): cover the Delete… review failure and close-only paths; record coverage floors (#443)

* fix(library): let saved-view defaults fill the missing #444 filters

libraryViewState passed `?? 'all'` and `?? ''` fallbacks that
normalizeViewState already applies to an undefined value. The redundant
branches were never taken in the page, which dropped saved-views.mjs below
its 95% branch floor. A unit now covers a shelf without the new filters.

Refs #444

* fix(library): lay out a loaded page of prompts without waiting for an animation frame (#443)

A starved renderer (headless WebKit under CPU load, a background tab) can
hold requestAnimationFrame for seconds. The shelf laid out each loaded page
only in its next frame, so the grid stayed empty after the first load or a
delete's reload while the caption already showed the new count. A loaded
batch is now laid out at once; measurements still flush per frame.

The regression test holds every animation frame and fails before the fix.

* test(saved-views): give the D153 Library view the tag_mode key (#444)

The Library saved-view state now holds tag_mode, and the API requires
every key, so main's new "Highest rated" spec posts it too.

Refs #444

* test(saved-views): give the D153 Library view the folder key (#444)

Refs #444

* fix(library): name the saved pair, not negative variants, as the archive reason (#443)

A prompt in a saved pair with no negative variants was described in the
Delete… review as "1 prompt with negative variants: hidden, restorable".
Paired prompts are archived because of the pair, so the Archive row now
reads "N prompts in a saved pair: hidden, restorable" and the first
sentence explains the pair. Found in the live test of #470.

* fix(library): nest Refine folders by path segment; backslash separates only on Windows (#444)

Live test B1: the Folder list indented a server list sorted as full-path
text, so "-" and "_" sorted before "/" and children landed under the wrong
parent (kiko/char under kiko-v6), with case variants interleaved. The
options are now a tree built by segment: each folder directly under its own
parent, case variants distinct siblings.

On POSIX a folder named "Metadata\Klein..." was listed but filtered to no
prompts, because normalize_folder read "\" as a separator. It now does so
only on Windows (ntpath), as the listing already did.

Refs #444

* fix(library): say why an archived prompt is archived, and name variants only when it has any (#443)

The archived prompt page's notice always said "This prompt and its
negative variants are hidden", also for a saved-pair prompt with none.
It now says the prompt is archived because it is in a saved pair, and
counts its negative variants only when there are any. From the live
test of #470.

* feat(library): delete a prompt in a saved pair permanently, with its own pairs (#482, server)

The Delete review's preview now marks which archive ids may be deleted
permanently (in a saved pair, and nobody else's negative), with a review
digest of the prompt and its pairs and the counts the review shows. The
write accepts an optional delete_paired_ids with their digests. Each is
deleted with its own saved pairs (image-pair links, then pairs, then the
prompt as #443 deletes it) only while its digest is unchanged. Any drift
archives it and counts it changed. Paired deletes are backed up and
disk-checked like deletes.

* feat(library): ask Archive or Delete permanently for prompts in a saved pair (#482, review)

The Delete… review asks, for prompts in a saved pair, Archive (checked
on every open, D152's review) or Delete permanently, with their saved
pairs, in a D90 radio row. The choice rewrites the grid, sentences, title
and primary in place (openReviewDialog setRows/setPrimary), and the body
carries the chosen ids with their review digests. On the archived shelf,
Show archived prompts now reads the concept's way back, "← Prompt
library", as a plain button. TESTING.md records D154.

* docs(testing): number #482's deviation D160 (D154-D159 are taken)

* Revert "docs(testing): number #482's deviation D160 (D154-D159 are taken)"

This reverts commit 96c2a293a4.

* docs(testing): number #482's deviation D160 (D154-D159 are taken)

* feat(library): Delete prompt… on a prompt's page (#481)

The recipe editor's actions read Create text revision, Archive variant…
(the selected negative variant's archive, renamed from Archive… so the two
danger buttons cannot be confused) and Delete prompt…, which closes the
editor and runs the Library's Delete… review for that prompt: the same
preview, saved-pair choice (Archive stays the default), backup and write.
A delete opens the Prompt library; an archive reads the page again.

Recorded as D161 in TESTING.md and in the editor concept comparison.

* fix(library): hold Edit metadata while a prompt's page delete runs (#481)

While the page's Delete… review or its write runs, Edit metadata is
aria-disabled and its click ignored, so no editor save can race the delete.
Adds tests for a failed review, a write answered after the page was left
(no toast, no jump, the shelf reads again) and heading focus after an
archive.
2026-10-30 03:28:19 +00:00
Vito b12a9a14ba feat(library): delete a prompt in a saved pair permanently, with its own saved pairs (#482) (#505)
* feat(library): set the category on many selected prompts (#437)

The Prompt library shelf gets the concept's Images selection checkbox on each
card and its .bulkbar (N selected, Set category, Clear), recorded as D148.
Set category opens the shared one-value dialog and posts
{prompt_ids, category, expected_epoch} to POST /prompt_manager/bulk/category:
the server checks the storage epoch the shelf was read under, then sets the
category on the Library parents in one BEGIN IMMEDIATE transaction, writing
only rows that change. A restored storage answers 409 storage-changed and
writes nothing. The legacy admin and images_filter bodies are unchanged.

This replaces the legacy admin page's bulk Set Category, a cutover
prerequisite (#334).

Closes #437

* feat(library): add tags to many selected prompts and select every loaded prompt (#442)

The Library selection bar (D148) gets Add tags and select-all of the loaded
shelf, the legacy admin page's bulk Add Tags and select-all checkbox.

Add tags posts {prompt_ids, tags, expected_epoch} to
POST /prompt_manager/bulk/tags: in one BEGIN IMMEDIATE transaction the server
checks the storage epoch, normalizes the names as image tags are, gives each
Library parent only the names it lacks (casefold), finds or inserts tag rows
by exact name only when some prompt gets them, and skips prompts that would
pass 20 tags. Prompt tags only. Both bulk/tags forms refuse a non-object body
with 400.

Ctrl/Cmd+A in the grid and "Select all N loaded" in the bar select every
loaded prompt; the offer follows each loaded batch.

Closes #442

* feat(vocabulary): tag search, tag sort and an Untagged prompts entry (#444)

Tags & categories gains a server-side tag search and sort (keyset pages,
cursor scoped to the query) and an "Untagged prompts" row whose Browse opens
the Library with tag_mode=untagged. Refine this view gets Tag match, saved
Library views keep it, and the routes echo the applied query so a stale
server is an error, never an unfiltered list. Recorded as D149.

Refs #444

* feat(library): filter prompts by output folder and browse tags with Any (#444)

Refine this view on the Prompt library gains the concept's Folder (folders
below an output root, read when the dialog opens) and a Tag match select
(All / Any). The listing takes folder and tag_mode, matches folders on the
normalized file_path under every spelling of each output root, echoes both
so a stale server is an error, and saved Library views keep them. Recorded
as D150.

Refs #444

* docs(library): record Add tags and select-all as D151 (#442)

* feat(library): archive paired and delete unpaired selected prompts on the server (#443)

A prompt_archive table marks Library parents hidden from the shelf; older
builds ignore it. New routes /prompt_manager/library/delete/preview,
/library/delete and /library/restore classify a selection (delete,
archive, protected, already archived, not found), execute exactly what was
reviewed under a records hold, take a verified safety backup (disk checked)
before any delete, and restore archived prompts. The shelf, search and the
shelf-facing counts honor the archive (?shelf=active|archived); archiving
repairs a stale prompt hash so scans find the archived row.

* feat(library): Delete… on the selection bar, Show archived prompts and Restore (#443)

The Library bar's Delete… previews the selection and opens the shared
review: what is deleted (in no saved pair), archived (paired: hidden,
restorable) or kept (tagged __protected__), the safety backup and what
cannot be undone. Show archived prompts (N) lists archived prompts with
Restore on their bar; an archived prompt's page shows an Archived notice
with Restore. TESTING.md records D152; the route snapshot gains the three
workspace routes.

* test(library): cover the Delete… review failure and close-only paths; record coverage floors (#443)

* fix(library): let saved-view defaults fill the missing #444 filters

libraryViewState passed `?? 'all'` and `?? ''` fallbacks that
normalizeViewState already applies to an undefined value. The redundant
branches were never taken in the page, which dropped saved-views.mjs below
its 95% branch floor. A unit now covers a shelf without the new filters.

Refs #444

* fix(library): lay out a loaded page of prompts without waiting for an animation frame (#443)

A starved renderer (headless WebKit under CPU load, a background tab) can
hold requestAnimationFrame for seconds. The shelf laid out each loaded page
only in its next frame, so the grid stayed empty after the first load or a
delete's reload while the caption already showed the new count. A loaded
batch is now laid out at once; measurements still flush per frame.

The regression test holds every animation frame and fails before the fix.

* test(saved-views): give the D153 Library view the tag_mode key (#444)

The Library saved-view state now holds tag_mode, and the API requires
every key, so main's new "Highest rated" spec posts it too.

Refs #444

* test(saved-views): give the D153 Library view the folder key (#444)

Refs #444

* fix(library): name the saved pair, not negative variants, as the archive reason (#443)

A prompt in a saved pair with no negative variants was described in the
Delete… review as "1 prompt with negative variants: hidden, restorable".
Paired prompts are archived because of the pair, so the Archive row now
reads "N prompts in a saved pair: hidden, restorable" and the first
sentence explains the pair. Found in the live test of #470.

* fix(library): nest Refine folders by path segment; backslash separates only on Windows (#444)

Live test B1: the Folder list indented a server list sorted as full-path
text, so "-" and "_" sorted before "/" and children landed under the wrong
parent (kiko/char under kiko-v6), with case variants interleaved. The
options are now a tree built by segment: each folder directly under its own
parent, case variants distinct siblings.

On POSIX a folder named "Metadata\Klein..." was listed but filtered to no
prompts, because normalize_folder read "\" as a separator. It now does so
only on Windows (ntpath), as the listing already did.

Refs #444

* fix(library): say why an archived prompt is archived, and name variants only when it has any (#443)

The archived prompt page's notice always said "This prompt and its
negative variants are hidden", also for a saved-pair prompt with none.
It now says the prompt is archived because it is in a saved pair, and
counts its negative variants only when there are any. From the live
test of #470.

* feat(library): delete a prompt in a saved pair permanently, with its own pairs (#482, server)

The Delete review's preview now marks which archive ids may be deleted
permanently (in a saved pair, and nobody else's negative), with a review
digest of the prompt and its pairs and the counts the review shows. The
write accepts an optional delete_paired_ids with their digests. Each is
deleted with its own saved pairs (image-pair links, then pairs, then the
prompt as #443 deletes it) only while its digest is unchanged. Any drift
archives it and counts it changed. Paired deletes are backed up and
disk-checked like deletes.

* feat(library): ask Archive or Delete permanently for prompts in a saved pair (#482, review)

The Delete… review asks, for prompts in a saved pair, Archive (checked
on every open, D152's review) or Delete permanently, with their saved
pairs, in a D90 radio row. The choice rewrites the grid, sentences, title
and primary in place (openReviewDialog setRows/setPrimary), and the body
carries the chosen ids with their review digests. On the archived shelf,
Show archived prompts now reads the concept's way back, "← Prompt
library", as a plain button. TESTING.md records D154.

* docs(testing): number #482's deviation D160 (D154-D159 are taken)

* Revert "docs(testing): number #482's deviation D160 (D154-D159 are taken)"

This reverts commit 96c2a293a4.

* docs(testing): number #482's deviation D160 (D154-D159 are taken)
2026-10-30 02:35:52 +00:00
Vito 77f08782f4 fix(tagging): keep tags typed before the image observation answers (#515) (#517)
The Tag image field is editable while the dialog observes the file, but the
observation's answer overwrote it with the saved set, so typing then Save
wrote the old tags ("0 tags saved"). The controller now reads the draft when
the answer arrives: text typed during a first observation (or before Retry
of a failed one) follows the saved tags, and a kept draft takes the text
typed while the reopen observes.
2026-10-30 00:35:48 +00:00
Vito a1ec65442d fix(images): say plainly why Windows offers no Recycle Bin (#500) (#508)
Maintainer ruling (a): Windows never uses the Recycle Bin, because it deletes
files larger than the bin's capacity permanently and IFileOperation's
PreDeleteItem guard cannot detect it (native probe runs 40951037575,
40953347904). Each file in the Delete review now reads "Moving files to the
Recycle Bin isn't available on Windows: files larger than the bin's capacity
would be deleted permanently." (code windows-recycle) instead of "This drive has
no trash.", and the summary line drops its "yet": "Moving to the trash is not
available for these files; each one lists why."
2026-10-29 22:39:53 +00:00
Vito e52cc39cd5 fix(images): background jobs skip files hidden by the removal mark; walkers skip trash folders inside output roots (#501) (#502)
* fix(images): background jobs skip files hidden by the removal mark; walkers skip trash folders inside output roots (#501)

- Thumbnail jobs and the legacy scan_output_dir listing leave
  library-removed files out (one mark check, mark_hides).
- Missing-file links also forgets removal marks that hide nothing any
  more (file gone from a folder that exists, or changed), only under a
  readable, non-empty source, re-checked at apply.
- The sidebar Images, Favorites and Needs review counts and the
  collection counts are read again when an image_delete job ends.
- utils/media_folders.py: one skip for thumbnails and trash folders
  (.Trash, .Trash-<uid>, .Trashes, $RECYCLE.BIN) used by every walker,
  the image monitor and the delete review.

* test(browser): wait for the answers two specs raced in the merge queue (#501)

- System diagnostics "level filter": wait for each level's own log
  answer, and poll the shown levels; the CRITICAL line is in the
  previous level's answer too, so it proved nothing (firefox).
- Library editor "restored storage after a confirmed save": wait for
  the PATCH answer, which waits for a whole restore in the test's
  route, instead of the 5 s expect default (webkit).
2026-10-29 22:30:32 +00:00
Vito d02a911748 feat(library): delete or archive selected prompts, Show archived prompts and Restore (#443) (#470)
* feat(library): set the category on many selected prompts (#437)

The Prompt library shelf gets the concept's Images selection checkbox on each
card and its .bulkbar (N selected, Set category, Clear), recorded as D148.
Set category opens the shared one-value dialog and posts
{prompt_ids, category, expected_epoch} to POST /prompt_manager/bulk/category:
the server checks the storage epoch the shelf was read under, then sets the
category on the Library parents in one BEGIN IMMEDIATE transaction, writing
only rows that change. A restored storage answers 409 storage-changed and
writes nothing. The legacy admin and images_filter bodies are unchanged.

This replaces the legacy admin page's bulk Set Category, a cutover
prerequisite (#334).

Closes #437

* feat(library): add tags to many selected prompts and select every loaded prompt (#442)

The Library selection bar (D148) gets Add tags and select-all of the loaded
shelf, the legacy admin page's bulk Add Tags and select-all checkbox.

Add tags posts {prompt_ids, tags, expected_epoch} to
POST /prompt_manager/bulk/tags: in one BEGIN IMMEDIATE transaction the server
checks the storage epoch, normalizes the names as image tags are, gives each
Library parent only the names it lacks (casefold), finds or inserts tag rows
by exact name only when some prompt gets them, and skips prompts that would
pass 20 tags. Prompt tags only. Both bulk/tags forms refuse a non-object body
with 400.

Ctrl/Cmd+A in the grid and "Select all N loaded" in the bar select every
loaded prompt; the offer follows each loaded batch.

Closes #442

* feat(vocabulary): tag search, tag sort and an Untagged prompts entry (#444)

Tags & categories gains a server-side tag search and sort (keyset pages,
cursor scoped to the query) and an "Untagged prompts" row whose Browse opens
the Library with tag_mode=untagged. Refine this view gets Tag match, saved
Library views keep it, and the routes echo the applied query so a stale
server is an error, never an unfiltered list. Recorded as D149.

Refs #444

* feat(library): filter prompts by output folder and browse tags with Any (#444)

Refine this view on the Prompt library gains the concept's Folder (folders
below an output root, read when the dialog opens) and a Tag match select
(All / Any). The listing takes folder and tag_mode, matches folders on the
normalized file_path under every spelling of each output root, echoes both
so a stale server is an error, and saved Library views keep them. Recorded
as D150.

Refs #444

* docs(library): record Add tags and select-all as D151 (#442)

* feat(library): archive paired and delete unpaired selected prompts on the server (#443)

A prompt_archive table marks Library parents hidden from the shelf; older
builds ignore it. New routes /prompt_manager/library/delete/preview,
/library/delete and /library/restore classify a selection (delete,
archive, protected, already archived, not found), execute exactly what was
reviewed under a records hold, take a verified safety backup (disk checked)
before any delete, and restore archived prompts. The shelf, search and the
shelf-facing counts honor the archive (?shelf=active|archived); archiving
repairs a stale prompt hash so scans find the archived row.

* feat(library): Delete… on the selection bar, Show archived prompts and Restore (#443)

The Library bar's Delete… previews the selection and opens the shared
review: what is deleted (in no saved pair), archived (paired: hidden,
restorable) or kept (tagged __protected__), the safety backup and what
cannot be undone. Show archived prompts (N) lists archived prompts with
Restore on their bar; an archived prompt's page shows an Archived notice
with Restore. TESTING.md records D152; the route snapshot gains the three
workspace routes.

* test(library): cover the Delete… review failure and close-only paths; record coverage floors (#443)

* fix(library): let saved-view defaults fill the missing #444 filters

libraryViewState passed `?? 'all'` and `?? ''` fallbacks that
normalizeViewState already applies to an undefined value. The redundant
branches were never taken in the page, which dropped saved-views.mjs below
its 95% branch floor. A unit now covers a shelf without the new filters.

Refs #444

* fix(library): lay out a loaded page of prompts without waiting for an animation frame (#443)

A starved renderer (headless WebKit under CPU load, a background tab) can
hold requestAnimationFrame for seconds. The shelf laid out each loaded page
only in its next frame, so the grid stayed empty after the first load or a
delete's reload while the caption already showed the new count. A loaded
batch is now laid out at once; measurements still flush per frame.

The regression test holds every animation frame and fails before the fix.

* test(saved-views): give the D153 Library view the tag_mode key (#444)

The Library saved-view state now holds tag_mode, and the API requires
every key, so main's new "Highest rated" spec posts it too.

Refs #444

* test(saved-views): give the D153 Library view the folder key (#444)

Refs #444

* fix(library): name the saved pair, not negative variants, as the archive reason (#443)

A prompt in a saved pair with no negative variants was described in the
Delete… review as "1 prompt with negative variants: hidden, restorable".
Paired prompts are archived because of the pair, so the Archive row now
reads "N prompts in a saved pair: hidden, restorable" and the first
sentence explains the pair. Found in the live test of #470.

* fix(library): nest Refine folders by path segment; backslash separates only on Windows (#444)

Live test B1: the Folder list indented a server list sorted as full-path
text, so "-" and "_" sorted before "/" and children landed under the wrong
parent (kiko/char under kiko-v6), with case variants interleaved. The
options are now a tree built by segment: each folder directly under its own
parent, case variants distinct siblings.

On POSIX a folder named "Metadata\Klein..." was listed but filtered to no
prompts, because normalize_folder read "\" as a separator. It now does so
only on Windows (ntpath), as the listing already did.

Refs #444

* fix(library): say why an archived prompt is archived, and name variants only when it has any (#443)

The archived prompt page's notice always said "This prompt and its
negative variants are hidden", also for a saved-pair prompt with none.
It now says the prompt is archived because it is in a saved pair, and
counts its negative variants only when there are any. From the live
test of #470.
2026-10-29 21:09:22 +00:00
Vito e0b86712a8 fix(workspace): start no sidebar count read while the page is leaving (#494) (#496)
The Favorites count is re-read 300 ms after a star changes. A reload
inside that window started the read on the leaving page, which WebKit
fails ("access control checks") and reports as a page error. After
beforeunload, the workspace clears that timer, aborts count reads in
flight and defers new ones, as well as a finished restore's reload. A
page that stays reads them on its next input or when it becomes visible,
as the legacy admin page does.
2026-10-29 20:15:35 +00:00
Vito 66ca988d2a feat(images): delete selected images, from the library or to the trash (#480) (#488)
* feat(trash): a freedesktop trash backend that renames without replacing or refuses (#480)

* feat(images): remove a file's records and hide it with a removal mark (#480)

Deletes every record of one file under every recorded spelling (image links,
tags, auto-tag marks, pending review items, category, collection memberships,
favorite) in one transaction. A Needs review item that is kept keeps its
annotation rows (ON DELETE RESTRICT). The image_removals table hides an exact
file (locator, size, mtime_ns) until it changes; linking skips a marked file,
and an explicit tag, category, favorite or collection write shows it again.

* feat(images): review and apply Delete images as a reviewed plan job (#480)

POST /prompt_manager/images/delete/preview reviews both modes; the
maintenance apply takes {token, mode}. Library mode removes the records and
writes the removal mark; trash mode moves the file first and removes its
records only after a successful move. The workspace listing, the output scan,
the duplicate find and Tag output images skip marked files.

* feat(images): Delete… in the Images selection bar, inspector and viewer (#480)

The concept's Remove links… place, D159: one review per selection that asks
each time (nothing preselected) whether to remove the files from the library
only or also move them to the trash. The browser test server uses a fake
trash.

* test(images): portable delete tests, the native no-trash proof, D159 in TESTING.md (#480)

* perf(images): cache the removal marks so a listing page reads no extra table (#480)

The marks reload only when this process writes or clears one, or a restore
replaces the database. The route snapshot gains the preview route.

* test(images): the full inspector specs expect D159's Delete… in the panel and the viewer (#480)

* fix(images): the delete review starts nothing without a chosen mode, and dims its primary until then (#480)

An aria-disabled primary ignores every activation, a chosen one starts once,
and the apply helper refuses a missing mode. Tests cover a forced click, a
double click, Enter and Space on the primary, Enter in the dialog and a
submit event, and the server refusing an apply without an exact mode.
2026-10-29 19:40:26 +00:00
Vito 599dd828e6 fix(inspect): keep a viewer whose video is still starting until it settles (#483) (#489)
A new selection closed the shared viewer at once, resetting and removing
its video even while that video was still starting. That is the WebKit
(GStreamer) deadlock D62 guards the inline preview against, and it hung
merge-queue run 40903238387. The viewer now stays open, untouched, until
its video reports metadata or an error (or 15 s, or the user closes it),
and the URL is revoked after that. An open asked for meanwhile runs once,
latest wins, only while its selection is still current.
2026-10-29 17:50:51 +00:00
Vito 0126705045 feat(library): filter prompts by output folder and browse tags with Any (#444, PR 2) (#456)
* feat(vocabulary): tag search, tag sort and an Untagged prompts entry (#444)

Tags & categories gains a server-side tag search and sort (keyset pages,
cursor scoped to the query) and an "Untagged prompts" row whose Browse opens
the Library with tag_mode=untagged. Refine this view gets Tag match, saved
Library views keep it, and the routes echo the applied query so a stale
server is an error, never an unfiltered list. Recorded as D149.

Refs #444

* feat(library): filter prompts by output folder and browse tags with Any (#444)

Refine this view on the Prompt library gains the concept's Folder (folders
below an output root, read when the dialog opens) and a Tag match select
(All / Any). The listing takes folder and tag_mode, matches folders on the
normalized file_path under every spelling of each output root, echoes both
so a stale server is an error, and saved Library views keep them. Recorded
as D150.

Refs #444

* fix(library): let saved-view defaults fill the missing #444 filters

libraryViewState passed `?? 'all'` and `?? ''` fallbacks that
normalizeViewState already applies to an undefined value. The redundant
branches were never taken in the page, which dropped saved-views.mjs below
its 95% branch floor. A unit now covers a shelf without the new filters.

Refs #444

* test(saved-views): give the D153 Library view the tag_mode key (#444)

The Library saved-view state now holds tag_mode, and the API requires
every key, so main's new "Highest rated" spec posts it too.

Refs #444

* test(saved-views): give the D153 Library view the folder key (#444)

Refs #444

* fix(library): nest Refine folders by path segment; backslash separates only on Windows (#444)

Live test B1: the Folder list indented a server list sorted as full-path
text, so "-" and "_" sorted before "/" and children landed under the wrong
parent (kiko/char under kiko-v6), with case variants interleaved. The
options are now a tree built by segment: each folder directly under its own
parent, case variants distinct siblings.

On POSIX a folder named "Metadata\Klein..." was listed but filtered to no
prompts, because normalize_folder read "\" as a separator. It now does so
only on Windows (ntpath), as the listing already did.

Refs #444
2026-10-29 14:10:29 +00:00
Vito 8870496ea3 feat(monitor): store certain prompts from workflows without a PromptManager node, with a System switch (#247, PR 2) (#467)
* feat(inspector): use recorded run history when the graph alone can't name the prompt (#386)

When a file's own graph leaves the prompt open, both inspect routes ask
recorded run history about the file (one run reported it and it was written
during that run). The save node that run reported it from anchors the walk
like a file-name match, so dataset branches that share one saver prefix
show their own prompt, labelled "Recorded run". Still open at one sampler
whose PromptManager text was recorded at execution (a language model's
output), the recorded text shows as "Execution recorded". Results the file
resolves alone never change; a failed lookup judges the file alone.

* fix(inspector): recorded texts follow history's saver; conflicts anchor nothing (#386)

Review follow-ups: the texts PromptManager recorded are looked up at the
sampler history's saver leads to, never at a sampler the unanchored walk
named; a file name naming a different saver than history anchors nothing;
outputs and texts are read in one transaction; "Recorded run" ranks below
every workflow-inferred source; unexpected lookup errors log at error level.

* fix(history): look up recorded runs by history's own spelling of the file (#386)

The history route inspects a case-normalized target on Windows, while
history matches file names exactly, so the occurrence's reference names
the file.

* refactor(inspector): share the inspector's resolve as inspection_resolution

inspect_prompt_evidence now calls inspection_resolution, which returns the
resolution to use, the anchored result and whether the recorded saver chose
it. Ingestion can call the same helper for files the default profile cannot
decide (#247).

* feat(ingestion): one per-file prompt decision for the scan and the monitor

utils/file_prompt.py holds decide_file_prompt, the ladder the output scan
runs today plus the inspector's rule (inspection profile, then the recorded
saver) before the legacy parser, with a SCAN and a MONITOR policy. Junk
text (blank, numbers, LoRA tags only, model file names) is never stored.
graph_prompt_text and parse_comfyui_prompt move here; the parser accepts
parsed metadata (#247).

* feat(ingestion): store a file's prompt in one transaction, one prompt per file

database/scanned_prompts.py links a file to the prompt of one text, creating
it when new, inside one BEGIN IMMEDIATE transaction: a file linked to
another prompt is a conflict, a failed link leaves no prompt behind and two
writers store one prompt. pair_ingestion gains file_link_rows,
link_conflict and Marks/mark_created; ingest_resolved_pair takes optional
marks for the components it creates. recorded_runs and named_prompt in
run_attribution become public (#247).

* feat(scan): decide each scanned file with the shared ladder (#247)

_ingest_scanned_file calls decide_file_prompt (scan policy) and stores the
result: pairs through ingest_resolved_pair and positives through
store_scanned_prompt, both with the scan marks. Files the default profile
leaves undecided now get the inspector's resolve, with the saver recorded
history names, before the legacy parser, so each branch of a shared-prefix
dataset run links its own prompt. Junk text is never stored, a file linked
to another prompt is a conflict instead of a second link, and a failed link
leaves no prompt behind. _BatchHistory reads the stored prompt and the
recorded saver from one recorded_runs pass. _parse_comfyui_prompt calls the
moved parse_comfyui_prompt. TESTING.md documents the decision.

* fix(ingestion): keep the file-name saver anchor out of the ladder (#390)

The #390 ruling queues a file that only its saver's file name resolves for
Needs review, never linking it. inspection_resolution gains name_anchor;
the ladder passes False, so such a file falls to the legacy floor in the
scan (and to Needs review) and is never stored by the monitor, while the
recorded saver still decides. The inspector keeps the name anchor.

* feat(config): gallery.monitoring.save_foreign_runs, default on (#247)

* fix(ingestion): no joined PromptManager text from the legacy floor; junk keeps its step's status

The legacy parser returns prepend + text + append, so its answer is refused
when a PromptManager-family node has a prepend or append part (#343). Junk
found at the undecided-graph step keeps that step's unlinked status and
review, so an ambiguous file is still queued for Needs review. The monitor
policy's ambiguous file no longer asks for the recorded run, and the file
size may be read lazily.

* fix(ingestion): keep the legacy answer that is a PromptManager node's own text

The joined-text refusal also refused the #170 parity files whose
PromptManager prepend is a link the browser parser does not follow: its
answer is then the node's literal text, which is what the node stores.
Refuse only an answer that is not such a node's own text.

* feat(monitor): store the certain prompt of a foreign workflow's output (#247)

A foreign output (no positive PromptManager node) is decided by the shared
ladder with the MONITOR policy. Certain files are stored at settle with
RUN_MARKS through survive_switch; files only recorded history can decide
wait in their own bounded set and are re-checked once after the run's
outputs are sealed, with the sealed reference. The setting
GalleryConfig.SAVE_FOREIGN_RUNS is read for every file.

* fix(scan): one prompt per file for recorded-run links; writes survive a library move

The recorded run's stored prompt now links in one transaction with the file's
link rows, so a file linked to another prompt is a conflict instead of a
second link. The scan's stores run through survive_library_switch. A single
file asks history once (a one-file _BatchHistory), the scan passes
in_output=False and reads the file size only for files with prompt
metadata. A conflict row reads "Kept: already linked to a different
prompt". TESTING.md records the rules.

* feat(settings): save_foreign_runs through POST /settings, applied live (#247)

GET /settings reports settings.save_foreign_runs; a save takes a strict
bool, writes gallery.monitoring.save_foreign_runs and publishes it to
GalleryConfig in the commit, with no restart field. The diagnostics
report lists it. The legacy contract snapshot gains the added key (an
addition, no legacy key moves).

* feat(system): Save prompts from other workflows in Sources & scans (#247, D147)

A host setting row below Watch for new outputs drafts save_foreign_runs
and saves with the one Save row; the server applies it at once, so no
restart is named. While monitoring is drafted off the row is disabled and
says it applies while Watch for new outputs is on. The concept comparison
maps the inserted row and its mutation check covers it.

* test(comfyui): a workflow of ComfyUI's own nodes creates and links its prompt (#247)

The real-host attribution spec gains the foreign case (CLIPTextEncode,
KSampler, SaveImage): its prompt is created and linked at settle and a
second run links to the same prompt. The fixture route lists the waiting
foreign files. TESTING.md records the monitor's foreign path and D147.

* test(vendor): the execution matrix counts the #247 real-host case

* fix(monitor): read a foreign file's size only when the ladder needs it (#247)

Uses PR 1's lazy file size and pins that a foreign file linked to another
prompt keeps its one link on the pair and the positive store paths.

* fix(ingestion): match a file's link rows under its resolved spelling too

The scan stores realpath spellings and the monitor the watcher's raw path,
so a file reached through a symlinked or junction output folder could get a
second generated_images row. file_link_rows now queries the path and its
realpath (each normalized and raw) and dedupes by row id. TESTING.md notes
the reverse order is matched only when the raw spelling is the realpath.

* fix(monitor): wait for the run unless history answered; monitoring off stops foreign writes (#247)

Review fixes: at settle a certain answer ends the wait only when the
lookup found a record or the answer is the inspector's or the recorded
saver's; the legacy floor waits for the re-check. A record without a
certain prompt ends the wait at once. Foreign writes need both
save_foreign_runs and monitoring_enabled, read again just before the
store. Dropped foreign files are logged outside the lock (evictions as
evictions, due ones too), and a drained re-check never drops a newer
reference.

* fix(system): the foreign row can be set while the monitor still watches; its note describes it (#247, D147)

Review fixes: while monitoring is saved off but a source still reads
watching until restart (D128), the row stays enabled with its note; the
note is the switch's aria-describedby. The browser spec restores
save_foreign_runs on the shared server. TESTING.md moves D147 into order
and records the monitor's review rules.

* test(comfyui): each branch of a shared-prefix foreign run links to its own prompt (#247 CUJ 1)

* fix(ingestion): a switched-off PromptManager node behind a pass-through stores nothing (#90)

The #90 graph walk follows only prompt-like inputs, so a PromptManagerText
with Save to library off reaching the encoder through another input name
(easy showAnything.anything, a custom node's value) was invisible to it,
and the inspector's resolve stored its text. The ladder now checks the
positive every step names: a resolved or inspected positive whose path
passes a node that is off, or a legacy answer that is such a node's own
text, makes the file not_saved.

* fix(ingestion): any switched-off node upstream of a positive stores nothing (#90)

The ladder walks every input upstream of each sampler or guider positive
(not negative, model, clip, latent or image inputs) for PromptManager nodes
whose Save to library is off or wired from an unreadable node, so the
sampler-less step, an off node with a prepend part and a PromptManagerPair
behind a pass-through no longer store its text. The legacy-floor guard reads
a node's resolved text. Every guard reports not_saved for a switch that is
off and not_saved_wired for one it cannot read.

* fix(ingestion): the switch walk skips negative inputs by #90's own rule

The upstream walk treated only names containing "negative" as negative, so
an off node on a neg, neg_text or text_neg input blocked an enabled
positive. prompt_graph gains is_negative_input (the _NEGATIVE_KEY rule, also
used by follows_input) and the walk uses it. The guards accept a node with
null inputs and a side whose provenance is not an object.

* fix(ingestion): the switch walk also skips any input named with "negative"

#90's rule matches only whole underscore-separated words, so negative2,
negativeprompt and NegativePrompt counted as positive inputs and an off
node wired only to one of them blocked the file. The walk skips those too;
follows_input is unchanged.

* fix(monitor): a foreign not_saved decision logs its skip and never waits (#247, #90)

The foreign path stored nothing for a ladder not_saved decision, but logged
it as "No certain prompt" and, when only the legacy answer was a
switched-off node's text, kept the file waiting for a seal that could only
store nothing. Such a decision now ends the wait at settle, and both settle
and the re-check log the opted-out path's own line (a wired switch says so).

Tests cover an only-PromptManager node switched off (opted out, not
foreign), a switched-off PromptManagerText behind a pass-through into a
core CLIPTextEncode, a wired switch, a negative-only switched-off node,
recorded history holding the off text, and foreign saving off.

* fix(monitor): an undecided not_saved waits for its run's record (#247, #90)

A not_saved from the ladder's undecided step is not final before a record
exists: the recorded saver may still resolve another branch. The settle
lookup now keeps such a file waiting; with the record it stores nothing and
logs the opted-out line. An early not_saved (a switched-off node upstream of
any sampler's positive) never reaches the lookup and stays final.

Tests: the undecided case (literal and wired switch) waits, then skips; a
switched-off node feeding one branch of a shared-prefix run never waits and
stores nothing, not even the branch the recorded saver names.

* docs(system): renumber the foreign-runs switch deviation to D158 (#247)

#430 took D147 on main; the Save prompts from other workflows row is D158.

* test(monitor): a switched-off node in the recorded saver's branch is never foreign (#247, #90)

PR 3's repair skips a file whose recorded saver's branch reads a
switched-off PromptManager node. On the monitor the same shape never reaches
the foreign path: a literal switch is the opted-out path's and a wired one
the node's own run's. Nothing is stored at settle or after the run, even
from a recorded run holding the off text.
2026-10-29 12:00:32 +00:00
Vito 83199518df fix(history): join outer-whitespace raw text to the history of the prompt it saves (#438) (#465)
The Library stores text.strip() while a run keeps its exact raw text. The
history window, run detail scope id, newest pair run and prompt hints now
compare the raw text's stored form (one rule, utils/hashing.py, mirrored in
SQLite with the same code points). Hints also keep matching the exact raw
text, so older rows that kept outer whitespace stay hinted and protected.
2026-10-29 11:32:53 +00:00
Vito 5e00ee479f feat(vocabulary): tag search, tag sort and an Untagged prompts entry on Tags & categories (#444, PR 1) (#452)
* feat(vocabulary): tag search, tag sort and an Untagged prompts entry (#444)

Tags & categories gains a server-side tag search and sort (keyset pages,
cursor scoped to the query) and an "Untagged prompts" row whose Browse opens
the Library with tag_mode=untagged. Refine this view gets Tag match, saved
Library views keep it, and the routes echo the applied query so a stale
server is an error, never an unfiltered list. Recorded as D149.

Refs #444

* test(saved-views): give the D153 Library view the tag_mode key (#444)

The Library saved-view state now holds tag_mode, and the API requires
every key, so main's new "Highest rated" spec posts it too.

Refs #444
2026-10-29 11:32:48 +00:00
Vito 2c54daf2d8 feat(viewer): full-screen toggle beside the close button, and a System default (#450) (#469)
* fix(inspector): give image tags their own labelled section (#449)

The inspector drew a file's tags as a bare chip row right under the negative
prompt, so they read as part of it. The side panel, the viewer details and
History's current file now draw them in labelled sections in the prompt
sections' frame: "Image tags · N" for the file's own tags, then "Prompt tags
· N" or "Recipe tags · N" for the linked tags. Each count is true: repeats are
removed within a source only. Recorded as D154.

* feat(inspect): choosing a file opens the viewer with its details (#448)

On Inspect a file, choosing or dropping a file now opens the shared viewer
on it at once, details showing, instead of previewing it and waiting for
"Open file metadata". Details show on this page even when "Collapse viewer
details by default" is on. An unreadable file or an AVI/WMV note stays on
the page; the heading action reopens any selection. A new selection that
starts while the viewer shows the previous file closes it first, so its
blob URL is never revoked under an open element. Recorded as D155.

* feat(viewer): full-screen toggle beside the close button, and a System default (#450)

Every viewer gets a button before the close button that switches between
Centered (today's framed dialog) and Full screen (the whole browser window,
edge to edge); it names the view it switches to. In full screen the details
stay toggleable. System > Appearance gains a Viewer surface with "Open viewer
in: Centered / Full screen" and the moved "Collapse viewer details by
default"; the choice is a browser display preference (viewerOpenMode). The
button changes only the current view and hides at 700px and below, where the
viewer is already full-bleed. Recorded as D156.

* fix(inspect): open a video's viewer only once its preview settles (#448)

Replacing a video while both the inline preview and the auto-opened viewer's
video were still starting deadlocked WebKit (D62). The viewer now opens for a
video once its inline preview reports metadata or an error (or after 15 s).

* test(viewer): pin the D97 view-switch spec to the desktop width (#450)

* test(inspect): cover the AVI note and an open dialog holding the auto-open back (#448)

Frontend coverage for inspect.mjs fell below its floor (branches 91.04 < 92,
functions 92.85 < 93): no spec reached the AVI toast or the open-dialog guard,
and the video wait had a page-end callback no spec reached. The wait now ends
through its own callback at page end, and two specs cover the AVI note and a
selection landing under another dialog.
2026-10-29 06:30:15 +00:00
Vito 1ce2577733 fix(library): show a loaded page of prompts without waiting for an animation frame (#464) (#466)
The shelf laid out each loaded page only in its next animation frame. A
browser that holds frames (a background tab, headless WebKit under CPU
load, seen holding them for more than 5 s) left the grid empty while the
caption already showed the count. A loaded batch is now laid out at once
when the grid is in the document; measurements still flush per frame.

The regression test holds every animation frame and fails before the fix
on all four projects.
2026-10-29 05:38:39 +00:00
Vito 2941a9b748 feat(inspect): choosing a file opens the viewer with its details (#448) (#458)
* fix(inspector): give image tags their own labelled section (#449)

The inspector drew a file's tags as a bare chip row right under the negative
prompt, so they read as part of it. The side panel, the viewer details and
History's current file now draw them in labelled sections in the prompt
sections' frame: "Image tags · N" for the file's own tags, then "Prompt tags
· N" or "Recipe tags · N" for the linked tags. Each count is true: repeats are
removed within a source only. Recorded as D154.

* feat(inspect): choosing a file opens the viewer with its details (#448)

On Inspect a file, choosing or dropping a file now opens the shared viewer
on it at once, details showing, instead of previewing it and waiting for
"Open file metadata". Details show on this page even when "Collapse viewer
details by default" is on. An unreadable file or an AVI/WMV note stays on
the page; the heading action reopens any selection. A new selection that
starts while the viewer shows the previous file closes it first, so its
blob URL is never revoked under an open element. Recorded as D155.

* fix(inspect): open a video's viewer only once its preview settles (#448)

Replacing a video while both the inline preview and the auto-opened viewer's
video were still starting deadlocked WebKit (D62). The viewer now opens for a
video once its inline preview reports metadata or an error (or after 15 s).

* test(inspect): cover the AVI note and an open dialog holding the auto-open back (#448)

Frontend coverage for inspect.mjs fell below its floor (branches 91.04 < 92,
functions 92.85 < 93): no spec reached the AVI toast or the open-dialog guard,
and the video wait had a page-end callback no spec reached. The wait now ends
through its own callback at page end, and two specs cover the AVI note and a
selection landing under another dialog.

* test(comfyui): the Inspect sender spec takes the viewer choosing a file opens (#448)

The real-host spec chose a file and then pressed "Open file metadata"; with
D155 the viewer is already open, so the press waited behind the modal until
the test timed out in the merge queue (classic-start and nodes2-start).
2026-10-29 05:19:04 +00:00
Vito 66e567cc95 fix(workspace): remove the 1-5 prompt rating from the workspace (#447) (#461)
The star is only for favorites (maintainer ruling 2026-10-28). The workspace
no longer shows or edits a prompt rating: no Rating field in the Library
editor, no stars on Library cards, no "Highest rated" sort on Images or the
Library, and no rating column or rated-images total in System > Statistics.

The editor never sends `rating`, so a workspace save keeps a stored rating
as it is. A saved view or Library visit with the retired rating sort opens on
Recently added. Data and API are unchanged; legacy pages keep their ratings.

Recorded as deviation D153 (amends D14, D23, D96); concept comparisons map
the concept through it.
2026-10-29 04:04:52 +00:00
Vito dff08bc161 fix(images): open Masonry by default, keep a saved Default layout, crop portraits from the top (#451) (#455)
- A Default layout or Thumbnail size saved in System now reaches the next Images
  visit in an open tab (this tab or another): the last visit records the
  defaults it started from, and a value its toolbar never changed follows newer
  defaults. A toolbar choice stays the current view's own.
- Images opens in Masonry when the browser has no stored Default layout
  (gallery-settings.js DEFAULT_SETTINGS.workspaceLayout); stored choices are kept.
- Grid/List card art, Library card art, the inspector cover and the Tag image
  preview crop with object-position 50% 20%, so portrait images keep their top.
- D157 in TESTING.md; concept comparisons start from the concept's stored Grid.
2026-10-29 02:55:55 +00:00
Vito aad3a37bcf fix(inspector): give image tags their own labelled section (#449) (#454)
The inspector drew a file's tags as a bare chip row right under the negative
prompt, so they read as part of it. The side panel, the viewer details and
History's current file now draw them in labelled sections in the prompt
sections' frame: "Image tags · N" for the file's own tags, then "Prompt tags
· N" or "Recipe tags · N" for the linked tags. Each count is true: repeats are
removed within a source only. Recorded as D154.
2026-10-29 02:44:42 +00:00
Vito 02190d03a8 feat(library): add tags to many selected prompts and select every loaded prompt (#442) (#453)
* feat(library): set the category on many selected prompts (#437)

The Prompt library shelf gets the concept's Images selection checkbox on each
card and its .bulkbar (N selected, Set category, Clear), recorded as D148.
Set category opens the shared one-value dialog and posts
{prompt_ids, category, expected_epoch} to POST /prompt_manager/bulk/category:
the server checks the storage epoch the shelf was read under, then sets the
category on the Library parents in one BEGIN IMMEDIATE transaction, writing
only rows that change. A restored storage answers 409 storage-changed and
writes nothing. The legacy admin and images_filter bodies are unchanged.

This replaces the legacy admin page's bulk Set Category, a cutover
prerequisite (#334).

Closes #437

* feat(library): add tags to many selected prompts and select every loaded prompt (#442)

The Library selection bar (D148) gets Add tags and select-all of the loaded
shelf, the legacy admin page's bulk Add Tags and select-all checkbox.

Add tags posts {prompt_ids, tags, expected_epoch} to
POST /prompt_manager/bulk/tags: in one BEGIN IMMEDIATE transaction the server
checks the storage epoch, normalizes the names as image tags are, gives each
Library parent only the names it lacks (casefold), finds or inserts tag rows
by exact name only when some prompt gets them, and skips prompts that would
pass 20 tags. Prompt tags only. Both bulk/tags forms refuse a non-object body
with 400.

Ctrl/Cmd+A in the grid and "Select all N loaded" in the bar select every
loaded prompt; the offer follows each loaded batch.

Closes #442

* docs(library): record Add tags and select-all as D151 (#442)
2026-10-29 01:30:58 +00:00
Vito 40dc0e5417 feat(collections): add to and remove from collections in the selection bar and the inspector (#331, PR 3) (#435)
* feat(saved-views): keep a collection in a saved view

Images saved views gain a collection key (#331 D140): empty or a collection
id of 1 to 9 digits. Writes refuse favorites together with a collection;
older rows read an empty collection, and a stored row with both opens the
collection, as the listing does for a URL with both.

Part of #331

* feat(collections): store collections in the library database

* feat(collections): add, remove and rename collections and keep members through duplicate cleanup

* feat(collections): add collection routes and the Images collection filter

* feat(images): store images' own categories in the library database (#332)

One new table, image_categories, and its category index, created empty
by a forward-only migration that reads and writes only its own objects.
Startup checks the structure; restore also checks the values. The store
sets, clears and reads own categories by file key (the deepest output
source holding the file and the path below it), and copies a removed
duplicate's category to a kept copy that has none.

* feat(images): duplicate cleanup keeps images' own categories (#332)

The kept copy's own category wins; a kept copy without one receives the
removed copy's, keyed by the deepest output source holding each file.
The removed copy's row is never deleted, so a give-back or a failed
delete loses nothing. The job log reports how many were kept.

* feat(images): set, remove and list images' own categories (#332)

PUT /prompt_manager/images/category sets the own category of up to 500
listed files, DELETE removes it (listed or not), and
GET /prompt_manager/images/categories lists the values whose file is
listed. Targets are the Images listing identity of an output source,
checked by media_target (shared with Collections). The routes never
touch prompts, recipes or links; POST /prompt_manager/bulk/category is
unchanged.

* fix(images): key symlinked output folders and keep key work off the event loop (#332)

Review fixes: a source matches by its configured spelling too, so a
detected output folder that is a symlink keys its files (routes and
duplicate cleanup). File keys are computed in the executor. A path no
source holds is a plain 403, a lone surrogate a 400. The install repairs
a table that lacks its category index instead of refusing to open.

* fix(collections): address review findings

* feat(images): set or remove a category on all N matching images (#332)

The category routes also accept the Images listing filter of "Select
all N matching" (#392) instead of explicit items, resolved now and
capped at 500 images like an explicit selection.

* feat(collections): browse a collection from the sidebar

The sidebar gains the concept's COLLECTIONS group (D136): one row per
collection with its folder icon, name and listed-member count, without the
PROPOSED badge; the label shows once a collection exists. A row opens
#/collection/<id>, the Images page filtered to that collection (D137): the
collection's name as h1, Collection as breadcrumb and title, Edit to rename
or delete it through the saved views' Edit dialog with collection nouns, and
a note counting members the sources do not list now. An unknown or deleted
collection shows a page that says so with a link to Images. A saved view on
a deleted collection lists nothing and says so (MC6, D140). The list is read
again after a restore and the counts 300 ms after a change.

Part of #331

* test(collections): compare the COLLECTIONS group with the concept

The concept hides COLLECTIONS at 950 px and below, which would leave a phone
no way to open a collection; production shows the rows there in the wrapped
layout of every other sidebar nav (D136). The shell comparison now compares
the group instead of filtering it out: the concept fixture seeds Landscapes
(12) and Experiments (8), the rows compare by name order, and at phone width
the nav compares against the concept's own sidebar-nav layout. The
collection rows are checked at 1600x1100 and 390x844 in both themes.

Part of #331

* test(vendor): count the #332 host case in the execution matrix

image-attribution.spec.mjs gains the image category route case, so the
execution config lists 92 tests (3 attribution cases per project).

* test(browser): install the slideshow clock before wrapping setTimeout

The slideshow fixture wrapped window.setTimeout before page.clock.install(),
so the viewer's timer ran on real time and clock.runFor() did not drive it;
a late WebKit timer failed 'slideshow exposes exact state labels' on CI.

* feat(collections): add and remove from the selection bar

Add to collection follows Set category in the Images selection bar and
opens the concept's simpleInput ("Add selected images to collection",
"Collection name", Add) with the existing names as a datalist. Remove
from collection follows it while a collection filter is active and
reloads the listing in place, keeping the checked files still listed.
Every outcome uses the design's strings (section 7.4). The routes take
at most 500 explicit files, so an "all N matching" selection is sent
only when every match is loaded, checked and at most 500; otherwise the
action toasts "Select at most 500 images." (D138, #331).

* fix(collections): address review findings

- A list read superseded by a newer one (the 300 ms count read, a restore)
  no longer shows "could not be loaded": the collection and saved-view
  pages wait for the list to settle.
- A listing that finds the open collection gone shows the deleted page once
  the list no longer holds it; when the list read fails or still holds it,
  the page keeps the listing's error and Retry instead of staying blank, and
  each failed read is reported again.
- The missing-members note follows every list read and goes away at zero.
- A rename that settles after its Edit dialog closed leaves any dialog
  opened since alone.
- The deleted-collection, list-failed and saved-view failure pages focus
  their title after a route change, as every page does.

Part of #331

* feat(collections): add and remove from the inspector and the viewer

The shared inspector's footer ends with Collections… (folder icon) after
Prompt history for a listed file, in the Images side panel and the
viewer. Its dialog reads the file's membership, lists every collection
as the concept's checkbox rows, takes an optional new name, and Save
sends one request per change (new name, added, removed). A failure keeps
the dialog with the membership read again. On a collection page a change
to the open collection reloads the listing at once from the side panel
and only when the viewer closes from the viewer. The inspector concept
comparison draws the button into the concept after Prompt history (D139,
#331).

* test(collections): compare the collection page and dialog with the concept

images-concept compares Add to collection in place in the selection bar
(only Remove links… stays omitted) and the collection page's heading,
toolbar and filter row against the concept's #collection-landscapes,
with D137's Edit drawn into the concept. collections-concept compares
the Add to collection dialog with the concept's simpleInput (D138:
"Add", an empty field, the datalist of names), in both themes at 1600
and 390 px, with a mutation check.

TESTING.md gains the Collections contract (database, keys, missing
members, duplicate move, restore, routes, rules, limits, the PR 1
rulings and the all-matching ruling), D136-D140, and amends D28 and D8;
docs/application-redesign.md gets the delivery-status line. The coverage
baseline adds the two new modules; optional collection callbacks are
null rather than no-op defaults so they do not count as uncovered
functions.

* test(coverage): keep main's floors for files this PR does not touch

coverage:update raised nine floors of untouched files from one run;
metadata-autocomplete.js branches then measured 84.84 against 85.

* fix(collections): address review findings

- M1: after a failed Save the Collections dialog shows the membership
  loading and keeps Save disabled until the re-read lands, so a stale
  plan cannot be sent.
- M2: the changes a Save made reach the page only when the dialog
  closes, so a collection page's reload cannot remove the dialog
  before its failure is read.
- L4: a New collection name equal to a listed collection ignoring case
  is that collection's checkbox, checked; never an add then a remove.
- L5: a name that cannot be a collection name is refused on the field;
  nothing is sent, the checkbox edits stay, no re-read.
- L6: focus starts on the field and moves to the first checkbox once
  listed; closing after a re-render replaced the trigger focuses the
  inspector's current Collections… button or its heading
  (openContentDialog takes an optional focusTarget).
- L3, L7: TESTING.md D138/D139 describe the stale-source and deleted
  collection outcomes and the above; the viewer spec asserts no listing
  request while it is open, and a browser test covers M2.

* test(legacy): record the image category routes in the legacy contract snapshot (#332)

#332 adds PUT/DELETE /prompt_manager/images/category and
GET /prompt_manager/images/categories; the legacy pages' routes are
unchanged.

* test(duplicates): a removed copy with a collection and a category moves both (#331, #332)

* test(collections): map the Add to collection suggestions to the shared dialog's datalist (#331)

After the merge with #332 PR 3 the input dialog places its datalist after the
field with the id workspace-input-suggestions; the concept mapping follows.
2026-10-29 01:14:20 +00:00
Vito ac378f07d3 feat(library): set the category on many selected prompts (#437) (#446)
The Prompt library shelf gets the concept's Images selection checkbox on each
card and its .bulkbar (N selected, Set category, Clear), recorded as D148.
Set category opens the shared one-value dialog and posts
{prompt_ids, category, expected_epoch} to POST /prompt_manager/bulk/category:
the server checks the storage epoch the shelf was read under, then sets the
category on the Library parents in one BEGIN IMMEDIATE transaction, writing
only rows that change. A restored storage answers 409 storage-changed and
writes nothing. The legacy admin and images_filter bodies are unchanged.

This replaces the legacy admin page's bulk Set Category, a cutover
prerequisite (#334).

Closes #437
2026-10-29 00:31:40 +00:00
Vito 827de84f40 fix(workspace): keep an open dialog when a restore it saw finishes (#415) (#441)
When the page's Activity store saw a restore run (its 15 s job-list poll
landed inside it), the S9 reload on finish re-rendered the open page and
closed any open dialog, dropping what was typed without notice. Whether
that happened depended on the poll's timing.

Now the S9 reload waits while any dialog is open and runs once the last
one closes (createRestoreReload, a MutationObserver on dialog[open]), so
the dialog keeps its entries and its next write meets the existing
restored-storage refusal with Reload, as when the page never saw the
restore. An open record-changing review is the exception: its staged plan
no longer describes storage, so it closes as dismissed, as before
(closeReviewDialog).

Tests: followRestore makes the page see a restore run and finish without
timers; browser cases for Create tag, New prompt and the restore review;
unit tests for the deferral and closeReviewDialog.
2026-10-28 23:31:53 +00:00
Vito 9eb5e9453d docs(redesign): record Copy settings summary and Local Inspect PR L3 as shipped (#436) (#440)
* docs(redesign): record Copy settings summary and Local Inspect PR L3 as shipped (#436)

* docs(redesign): cite D117 and fix the stale Task 9 tense in local-resolver.mjs (#436)

* docs(inspect): local-resolver.mjs header says Task 9 shipped (#436)
2026-10-28 23:19:54 +00:00
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 56d0a4904c feat(collections): COLLECTIONS sidebar, collection page and saved views (#331, PR 2) (#410)
* feat(saved-views): keep a collection in a saved view

Images saved views gain a collection key (#331 D140): empty or a collection
id of 1 to 9 digits. Writes refuse favorites together with a collection;
older rows read an empty collection, and a stored row with both opens the
collection, as the listing does for a URL with both.

Part of #331

* feat(collections): store collections in the library database

* feat(collections): add, remove and rename collections and keep members through duplicate cleanup

* feat(collections): add collection routes and the Images collection filter

* feat(images): store images' own categories in the library database (#332)

One new table, image_categories, and its category index, created empty
by a forward-only migration that reads and writes only its own objects.
Startup checks the structure; restore also checks the values. The store
sets, clears and reads own categories by file key (the deepest output
source holding the file and the path below it), and copies a removed
duplicate's category to a kept copy that has none.

* feat(images): duplicate cleanup keeps images' own categories (#332)

The kept copy's own category wins; a kept copy without one receives the
removed copy's, keyed by the deepest output source holding each file.
The removed copy's row is never deleted, so a give-back or a failed
delete loses nothing. The job log reports how many were kept.

* feat(images): set, remove and list images' own categories (#332)

PUT /prompt_manager/images/category sets the own category of up to 500
listed files, DELETE removes it (listed or not), and
GET /prompt_manager/images/categories lists the values whose file is
listed. Targets are the Images listing identity of an output source,
checked by media_target (shared with Collections). The routes never
touch prompts, recipes or links; POST /prompt_manager/bulk/category is
unchanged.

* fix(images): key symlinked output folders and keep key work off the event loop (#332)

Review fixes: a source matches by its configured spelling too, so a
detected output folder that is a symlink keys its files (routes and
duplicate cleanup). File keys are computed in the executor. A path no
source holds is a plain 403, a lone surrogate a 400. The install repairs
a table that lacks its category index instead of refusing to open.

* fix(collections): address review findings

* feat(images): set or remove a category on all N matching images (#332)

The category routes also accept the Images listing filter of "Select
all N matching" (#392) instead of explicit items, resolved now and
capped at 500 images like an explicit selection.

* feat(collections): browse a collection from the sidebar

The sidebar gains the concept's COLLECTIONS group (D136): one row per
collection with its folder icon, name and listed-member count, without the
PROPOSED badge; the label shows once a collection exists. A row opens
#/collection/<id>, the Images page filtered to that collection (D137): the
collection's name as h1, Collection as breadcrumb and title, Edit to rename
or delete it through the saved views' Edit dialog with collection nouns, and
a note counting members the sources do not list now. An unknown or deleted
collection shows a page that says so with a link to Images. A saved view on
a deleted collection lists nothing and says so (MC6, D140). The list is read
again after a restore and the counts 300 ms after a change.

Part of #331

* test(collections): compare the COLLECTIONS group with the concept

The concept hides COLLECTIONS at 950 px and below, which would leave a phone
no way to open a collection; production shows the rows there in the wrapped
layout of every other sidebar nav (D136). The shell comparison now compares
the group instead of filtering it out: the concept fixture seeds Landscapes
(12) and Experiments (8), the rows compare by name order, and at phone width
the nav compares against the concept's own sidebar-nav layout. The
collection rows are checked at 1600x1100 and 390x844 in both themes.

Part of #331

* test(vendor): count the #332 host case in the execution matrix

image-attribution.spec.mjs gains the image category route case, so the
execution config lists 92 tests (3 attribution cases per project).

* test(browser): install the slideshow clock before wrapping setTimeout

The slideshow fixture wrapped window.setTimeout before page.clock.install(),
so the viewer's timer ran on real time and clock.runFor() did not drive it;
a late WebKit timer failed 'slideshow exposes exact state labels' on CI.

* fix(collections): address review findings

- A list read superseded by a newer one (the 300 ms count read, a restore)
  no longer shows "could not be loaded": the collection and saved-view
  pages wait for the list to settle.
- A listing that finds the open collection gone shows the deleted page once
  the list no longer holds it; when the list read fails or still holds it,
  the page keeps the listing's error and Retry instead of staying blank, and
  each failed read is reported again.
- The missing-members note follows every list read and goes away at zero.
- A rename that settles after its Edit dialog closed leaves any dialog
  opened since alone.
- The deleted-collection, list-failed and saved-view failure pages focus
  their title after a route change, as every page does.

Part of #331

* test(coverage): keep main's floors for files this PR does not touch

coverage:update raised nine floors of untouched files from one run;
metadata-autocomplete.js branches then measured 84.84 against 85.

* test(legacy): record the image category routes in the legacy contract snapshot (#332)

#332 adds PUT/DELETE /prompt_manager/images/category and
GET /prompt_manager/images/categories; the legacy pages' routes are
unchanged.

* test(duplicates): a removed copy with a collection and a category moves both (#331, #332)
2026-10-28 21:45:09 +00:00
Vito 41027aa1b1 fix(autotag): report a missing backend package instead of offering the model as ready (#426)
Model status now says, per backend, which Python packages cannot be
imported (importlib.util.find_spec, nothing imported): llama-cpp-python
for GGUF, torch/transformers/accelerate/bitsandbytes for HuggingFace,
onnxruntime for WD14, with the pip install command and a note (the CUDA
CMAKE_ARGS for llama-cpp-python, onnxruntime-gpu for WD14). Nothing is
installed.

load_model refuses such a model before the loader with
MissingBackendPackages (one WARNING line, no traceback); a
ModuleNotFoundError from a loader becomes the same refusal. The suggest
route, the legacy single and batch routes and both tagging jobs answer
409 model-unavailable with that sentence.

System > Auto-tagging reads "Downloaded. Needs llama-cpp-python: pip
install llama-cpp-python. ..." instead of "Downloaded. Not loaded.",
Review explains instead of reviewing, and the bulk tagging dialog keeps
Start disabled with the sentence. "downloaded" keeps its files-only
meaning, so the legacy pages are unchanged.

Closes #424
2026-10-28 19:58:05 +00:00
Vito c7375573ed feat(images): set an image's own category, show its origin and count image categories (#332, PR 3) (#428)
* feat(images): store images' own categories in the library database (#332)

One new table, image_categories, and its category index, created empty
by a forward-only migration that reads and writes only its own objects.
Startup checks the structure; restore also checks the values. The store
sets, clears and reads own categories by file key (the deepest output
source holding the file and the path below it), and copies a removed
duplicate's category to a kept copy that has none.

* feat(images): duplicate cleanup keeps images' own categories (#332)

The kept copy's own category wins; a kept copy without one receives the
removed copy's, keyed by the deepest output source holding each file.
The removed copy's row is never deleted, so a give-back or a failed
delete loses nothing. The job log reports how many were kept.

* feat(images): set, remove and list images' own categories (#332)

PUT /prompt_manager/images/category sets the own category of up to 500
listed files, DELETE removes it (listed or not), and
GET /prompt_manager/images/categories lists the values whose file is
listed. Targets are the Images listing identity of an output source,
checked by media_target (shared with Collections). The routes never
touch prompts, recipes or links; POST /prompt_manager/bulk/category is
unchanged.

* feat(vocabulary): count and rename image categories (#332)

Tags & categories lists every category value with image_count, the
stored image_categories rows holding it. Values only images hold follow
every prompt-backed value (first_pid at SQLite's INTEGER maximum), so
keyset cursors cross the boundary. /library/options keeps its own query
and never lists image-only values.

Manage category counts image rows (impact.images), treats an image-held
target as existing, renames image rows in the same transaction, allows
renaming an image-only value, and hashes the sorted image rowids into
the impact token so a change between preview and apply is refused with
vocabulary-conflict. Every image-category statement reads
idx_image_categories_category.

* feat(images): the category filter matches each image's effective category (#332)

An image's own category wins; without one it inherits its link's: the
recipe's category when set, else the positive prompt's (MC4, one rule
with the inspector chip). Ambiguous and unlinked images inherit nothing,
so an unlinked image with its own category is now listed. The link read
no longer filters by category in SQL; the page filter, select all N
matching and saved views compare the effective value. A failed
own-category read answers the existing listing-failed 500.

* fix(images): key symlinked output folders and keep key work off the event loop (#332)

Review fixes: a source matches by its configured spelling too, so a
detected output folder that is a symlink keys its files (routes and
duplicate cleanup). File keys are computed in the executor. A path no
source holds is a plain 403, a lone surrogate a 400. The install repairs
a table that lacks its category index instead of refusing to open.

* feat(inspector): show an image's own category in the inspector and History (#332)

The inspector's library facts gain category_scope "image": an image's
own category overrides the inherited one for any link status (linked,
ambiguous, none). Images reads it for output sources only; History reads
it once per inspection inside its snapshot, keyed by the same file key
from the symlink-resolved output folder. A failed read hides the chip
rather than show a possibly wrong value. The chip's hidden prefix names
the origin ("Image category: "); the visible origin text comes in PR 3.

* perf(images): keep the category filter's link read narrow; record D141, D142, D145 (#332)

Under a category the link read keeps only links whose raw recipe or
positive category is the value (Python still decides the inherited
one), the links of images whose own category is the value are read by
path, and plain reads skip the inherited-category work. The own-category
map is keyed by normalized path. TESTING.md records D141, D142 and D145
and amends D13, D36 and D39; the filter budget seeds 2,000 own
categories.

* feat(images): set or remove a category on all N matching images (#332)

The category routes also accept the Images listing filter of "Select
all N matching" (#392) instead of explicit items, resolved now and
capped at 500 images like an explicit selection.

* fix(images): Windows path keys for own categories; plan check for the category read (#332)

Review fixes: on Windows the own-category map keys each file with
normalize_image_path (abspath drops trailing dots and spaces), and the
plan test also covers the category's narrowing clause.

* feat(images): Set category sets each image's own category; the chip names its origin (#332)

The selection bar's Set category writes the selected images' own
category (linked or not, up to 500, or all N matching up to 500), with
the toolbar categories as suggestions; Use prompt category removes it
after a confirmation that states the count. It never writes a prompt
(#259's prompt action is retired from Images; the Library editor and
the legacy admin Set Category still change prompt categories). The
toolbar lists the listed images' own categories beside the prompt and
recipe ones. The inspector chip shows its origin: "· this image",
"· prompt" or "· recipe". Tags & categories counts images and opens
Images for a category only images use.

* test(images): compare the category chip's visible origin against the concept (#332)

D146: the inspector comparison measures each chip's span.chip-source and
lays the concept's own chip out with the recorded rule and origin added at
runtime (withChipSource), in the side panel and the viewer's details. The
D39 recipe case asserts the visible " · prompt"; the mutation check
removes the origin.

* test(images): compare Set category and the category options against the concept (#332)

With one image's own category set through the real route, the toolbar's
#category-filter options are compared with D141 mapped (the image category
sorted in with the Library's, D36), and the Set category dialog against the
concept's simpleInput laid out as recorded at runtime: D140 (empty field,
datalist#workspace-input-suggestions of the toolbar's categories, Use prompt
category between Cancel and Apply) and D8 (Apply). Unmapped, the dialog
differs only in those parts.

* test(history): compare the History media inspector's category chip against the concept (#332)

D145: with an own category set on the baseline's newest output, the
Recorded run viewer's chip is compared with the concept viewer's category
chip laid out with the D146 origin and the own category at runtime; D21
maps its hidden "Image category: " prefix. Unmapped, the two differ only
in the text, the prefix, the width and span.chip-source.

* test(images): Set category sets each image's own category in page workflows (#332)

Replace the #259 prompt-category tests with CUJ 1, 2, 3, 8 and 10, the
suggestions datalist, the 500-image limit, the source-changed path, D142
and coded refusals; Images never posts /prompt_manager/bulk/category.

* test(inspector): the category chip names its origin in the panel, viewer and History (#332)

CUJ 4 and 5: "· this image", "· prompt" and "· recipe" chips, an
unlinked file's own category, and History's current-file inspector.

* test(vocabulary): category rename covers image categories; image-only rows browse Images (#332)

CUJ 6 and 7 with own categories set through the API on two output files.

* test(vocabulary): compare image-category rows and the Manage category preview against the concept (#332)

The vocabulary fixture seeds own image categories on request
(image_categories: Landscapes on two images, Portraits on one image only).
mapVocabularyConcept maps D143 ("N positive prompts · M images", image-only
rows before Manage category), a model checked against the concept page with
those rows added at runtime, and mapVocabularyDialog takes the preview
sentence, so the Manage category preview is compared with D9 and D144's
images. The default fixture and mapping are unchanged.

* test(vocabulary): park the pointer before measuring the category rows (#332)

Firefox kept the pointer where the concept's Review was pressed, over the
production Portraits row's Browse, so that button was measured hovered.

* docs(testing): record D140, D143, D144 and D146 and the image category UI (#332)

D140 (Set category writes images' own categories), D146 (the chip's visible
origin) beside D141, D142 and D145; D143 and D144 in the vocabulary
section. D8, D21 (the D20/D21 side panel) and D39 amended; the D8, the
"all N matching" and the Native vocabulary UI paragraphs name the Prompt
library editor and the legacy admin "Set Category" for prompt
categories; the #332 section lists the interface and its comparisons.

* test(vendor): count the #332 host case in the execution matrix

image-attribution.spec.mjs gains the image category route case, so the
execution config lists 92 tests (3 attribution cases per project).

* fix(images): focus after a changed source; Browse is a fresh Images visit (#332)

Review fixes: a 409 source-changed reload moves focus to the heading as a
success does; a selection the listing no longer holds says none could be
found instead of sending nothing; a Tags & categories Browse opens a
plain Images visit filtered by the category (remembered and with the
Appearance defaults) rather than a saved-view visit.

* test(legacy): record the image category routes in the legacy contract snapshot (#332)

#332 adds PUT/DELETE /prompt_manager/images/category and
GET /prompt_manager/images/categories; the legacy pages' routes are
unchanged.

* refactor(images): take the selection bar's categories from its one caller (#332)

The getCategories default was never used (images.mjs always passes the
toolbar's categories) and left images-selection.mjs function coverage at 97.05
(floor 98).
2026-10-28 19:23:41 +00:00
Vito d9f168c317 feat(nodes): per-node Save to library switch (#90) (#427)
* feat(nodes): per-node Save to library switch (#90)

Every PromptManager node (Classic, Text, Focused, Split, Pair) gets an
optional save_to_library BOOLEAN, default on and declared last. Off, the
node still returns its normal outputs but PromptManager ignores the run:

- the node saves, counts, tracks and records nothing;
- the queue hook reads the switch from the queued graph (literal, or a
  linked PrimitiveBoolean), so a cached node is not counted either; a
  switch wired from an unreadable node is left to the node itself;
- an opted-out node is no run prompt, so no tracker entry, run key,
  embedded-metadata match or history attribution links its images, and
  a run with any opted-out positive node links none of its files;
- the scan reports such files as not_saved (never stored or queued for
  Needs review), and a file whose only opted-out node is its negative is
  stored positive-only, without the pair;
- execution history records no prompt text for an opted-out role.

PromptSearchList only reads the library and gets no switch.

The switch takes the widget index older workflows used for the editor's
own DOM slot (''), so a configure step turns any non-boolean value on:
workflows saved before the switch load with it on and no earlier widget
value moves. The Focused, Split and Pair editors draw it as a "Save to
library" checkbox in their toolbar; Classic and Text show the native
toggle.

* fix(nodes): Save to library review fixes (#90)

- A switch wired from a node the graph cannot read and off at run time
  marks the node's run key, so an earlier run's tracker entry never
  links this run's images.
- The scan reports such files as not_saved_wired ("only the run itself
  can link it") instead of not_saved.
- Workflow-only metadata ignores muted and bypassed PromptManager nodes
  and reads nodes inside subgraph definitions.
- The Focused, Split and Pair footer says when the switch comes from a
  link.

* fix(usage): count cached re-runs of runtime-linked prompt text (#347)

A PromptManager-family node whose text comes from a runtime link
(PreviewAny, an LLM, a list node) counted only the job that executed it;
later jobs served from ComfyUI's output cache counted nothing.

Each execution now records a receipt (digests of its STRING outputs and
hashes of the text it stores) per run key and queue item. On
execution_cached the receipt whose digests equal the cache entry's text is
replayed once for the job, with the node's role and what the queue hook
already saw recomputed from the job's own graph. The executing node and the
replay count through one shared own_counts. Unmatched cache entries count
nothing, as before; a node that executes after its replay in the same job
skips its own count. No schema change, no backfill.

* fix(usage): never replay a node twice in one job; dedupe receipts once complete (#347)

A repeated execution_cached id or message for the same job now counts the
node once. Receipts replace equal older ones only when their job finishes
(or the next job starts), so a list still recording [A] of [A, B] no longer
drops an older [A] receipt. Node-level tests check the receipt each
execution records.

* test(coverage): baseline for the Save to library module (#90)

* test(usage): disabled node then cached jobs counts 0 (#90, #347)

A PromptManager node with Save to library off records no cached-run
receipt and is no run prompt in the replayed job's graph, so ComfyUI
serving it from its cache on later jobs counts nothing. The prompt is
saved first by an enabled run, so a replayed count would show. Covers
PromptManager, Focused, Split, Text and the Pair.

* fix(nodes): hide the editor's native widgets through one helper (#90)

The Save to library switch copied the text fallbacks' zero-height computeSize
arrow, and neither copy was ever called, so focused-node.js function coverage
fell to 87.5 (floor 90). One hideNativeWidget helper now hides both, and the
spec checks that the hidden switch and a text fallback take no node height.
2026-10-28 18:09:16 +00:00
Vito cdb771d722 feat(images): filter, inspect and count by each image's effective category (#332, PR 2) (#406)
* feat(images): store images' own categories in the library database (#332)

One new table, image_categories, and its category index, created empty
by a forward-only migration that reads and writes only its own objects.
Startup checks the structure; restore also checks the values. The store
sets, clears and reads own categories by file key (the deepest output
source holding the file and the path below it), and copies a removed
duplicate's category to a kept copy that has none.

* feat(images): duplicate cleanup keeps images' own categories (#332)

The kept copy's own category wins; a kept copy without one receives the
removed copy's, keyed by the deepest output source holding each file.
The removed copy's row is never deleted, so a give-back or a failed
delete loses nothing. The job log reports how many were kept.

* feat(images): set, remove and list images' own categories (#332)

PUT /prompt_manager/images/category sets the own category of up to 500
listed files, DELETE removes it (listed or not), and
GET /prompt_manager/images/categories lists the values whose file is
listed. Targets are the Images listing identity of an output source,
checked by media_target (shared with Collections). The routes never
touch prompts, recipes or links; POST /prompt_manager/bulk/category is
unchanged.

* feat(vocabulary): count and rename image categories (#332)

Tags & categories lists every category value with image_count, the
stored image_categories rows holding it. Values only images hold follow
every prompt-backed value (first_pid at SQLite's INTEGER maximum), so
keyset cursors cross the boundary. /library/options keeps its own query
and never lists image-only values.

Manage category counts image rows (impact.images), treats an image-held
target as existing, renames image rows in the same transaction, allows
renaming an image-only value, and hashes the sorted image rowids into
the impact token so a change between preview and apply is refused with
vocabulary-conflict. Every image-category statement reads
idx_image_categories_category.

* feat(images): the category filter matches each image's effective category (#332)

An image's own category wins; without one it inherits its link's: the
recipe's category when set, else the positive prompt's (MC4, one rule
with the inspector chip). Ambiguous and unlinked images inherit nothing,
so an unlinked image with its own category is now listed. The link read
no longer filters by category in SQL; the page filter, select all N
matching and saved views compare the effective value. A failed
own-category read answers the existing listing-failed 500.

* fix(images): key symlinked output folders and keep key work off the event loop (#332)

Review fixes: a source matches by its configured spelling too, so a
detected output folder that is a symlink keys its files (routes and
duplicate cleanup). File keys are computed in the executor. A path no
source holds is a plain 403, a lone surrogate a 400. The install repairs
a table that lacks its category index instead of refusing to open.

* feat(inspector): show an image's own category in the inspector and History (#332)

The inspector's library facts gain category_scope "image": an image's
own category overrides the inherited one for any link status (linked,
ambiguous, none). Images reads it for output sources only; History reads
it once per inspection inside its snapshot, keyed by the same file key
from the symlink-resolved output folder. A failed read hides the chip
rather than show a possibly wrong value. The chip's hidden prefix names
the origin ("Image category: "); the visible origin text comes in PR 3.

* perf(images): keep the category filter's link read narrow; record D141, D142, D145 (#332)

Under a category the link read keeps only links whose raw recipe or
positive category is the value (Python still decides the inherited
one), the links of images whose own category is the value are read by
path, and plain reads skip the inherited-category work. The own-category
map is keyed by normalized path. TESTING.md records D141, D142 and D145
and amends D13, D36 and D39; the filter budget seeds 2,000 own
categories.

* feat(images): set or remove a category on all N matching images (#332)

The category routes also accept the Images listing filter of "Select
all N matching" (#392) instead of explicit items, resolved now and
capped at 500 images like an explicit selection.

* fix(images): Windows path keys for own categories; plan check for the category read (#332)

Review fixes: on Windows the own-category map keys each file with
normalize_image_path (abspath drops trailing dots and spaces), and the
plan test also covers the category's narrowing clause.

* test(vendor): count the #332 host case in the execution matrix

image-attribution.spec.mjs gains the image category route case, so the
execution config lists 92 tests (3 attribution cases per project).

* test(legacy): record the image category routes in the legacy contract snapshot (#332)

#332 adds PUT/DELETE /prompt_manager/images/category and
GET /prompt_manager/images/categories; the legacy pages' routes are
unchanged.
2026-10-28 17:19:11 +00:00
Vito 95586e6395 refactor(gallery): open System for gallery settings, maintenance and tagging models (#330, PR 3) (#408)
* test(legacy): snapshot the routes and the settings response

Pins every registered route and the key paths of GET /prompt_manager/settings
while System S13 (#330) moves the legacy pages' tools to native System. Recorded
on the unchanged main; cutover (#334) removes it.

* test(legacy): prove each removed legacy control has native coverage

TESTING.md gains the "Legacy System UI removed by S13" table (no rows yet).
legacy-coverage.test.mjs fails when a removal cites a capability that is not
covered, copies another native test, lists an id or method twice, or leaves
the id or method on its page. legacy-system-links.spec.mjs holds the System
link table and guards the kept admin flows.

* refactor(admin): open System for Diagnostics and logs

The admin page's Diagnostics and View Logs buttons become links that open
System › Diagnostics & logs in a new tab. The legacy Diagnostics and System Logs
dialogs and their methods go; TESTING.md lists them against their covered
capabilities (rows B1–B7).

* refactor(admin): open System for settings and keep page options

Settings opens System › Overview in a new tab. The settings dialog becomes Page
options (opened from the list header) with only the Infinite Scrolling default,
and its Save posts {"infinite_scroll": …} alone, so a stale legacy form can no
longer overwrite scan folders or other values edited in System (#330 S13-9).
Scan folders, monitoring status, auto-hide, display mode, worker threads,
LoraManager and the LoRA import leave the page (rows B8–B14).

* refactor(admin): open System for maintenance

Maintenance opens System › Maintenance in a new tab, where each operation runs
as a reviewed Activity job. The one-click legacy Database Maintenance dialog
and its methods go (rows B15–B18); formatFileSize stays for the image modal.

* refactor(admin): open System for backup and restore

Backup DB and Restore DB open System › Backup & export in a new tab, where a
restore is staged, reviewed and run as an Activity job. The one-click Restore
dialog and its methods go (rows B19–B20); the scan dialog's quick backup stays.
The removed browser tests' data checks live on at the route: a new round trip
test restores every prompt with its tags and image links.

* test(legacy): find a removed modal id passed to showModal and settle the links spec

The removal check also finds an id passed as a string to a ...Modal() call and no
longer reads data-id= as id=. The no-request test waits for the page's own
start-up reads to finish, the sort check compares as the server sorts, and the
kept-flow test checks the added prompt shows in the list.

* test(admin): open Page options after the saved default is read

Drops the leftover LoraManager section comment above the Page options save.

* refactor(gallery): open System for settings and maintenance

The gallery's Settings opens System › Appearance in a new tab. Its settings
dialog becomes Page options (a new header button) with only the page's own
settings: thumbnail startup check, lazy loading, items per page, grid columns,
infinite scrolling, debug mode and API timeout. Save and Reset keep every other
stored key, so the Appearance values System saves survive (S13-13). Generate
Thumbnails, Clear cache, Rescan and Find duplicates leave the page; Page options
links System › Maintenance and Sources & scans (rows B21–B28).

* refactor(autotag): manage tagging models in System

In both auto-tag dialogs a model that is not downloaded shows a Manage in
System link to System › Auto-tagging (a new tab) instead of a Download button,
and the gallery's Unload Model becomes the same link. The legacy download
dialog and the download, cancel and unload methods go (rows B29–B31); the batch
and review stay. A test hook marks a fake model loaded.

* docs(system): name System as the home for settings and maintenance

README and TESTING.md send settings, maintenance, backups, diagnostics, the
gallery's tools and auto-tag model downloads to their System section and name
each legacy page's Page options.

* fix(gallery): keep the default view in Page options and save viewer choices alone

System › Appearance's Default layout is the workspace's own key, so the legacy
gallery's Default View Mode stays a page option instead of losing its only
control. The viewer's mute and loop buttons remember just that choice instead of
saving the Page options dialog's unfilled inputs over the stored page options.
The pending-thumbnails toast names System › Sources & scans.

* test(legacy): check each System link sends no request in its own test

The no-request check runs inside each link's test, so a slow page no longer
times out the whole table, and it ignores lazy thumbnail loads the layout
starts on its own.

* test(legacy): a lazy thumbnail load is not a System link's request

* fix(gallery): let the header buttons wrap on a narrow screen

With Page options beside Settings the header no longer fits a phone's width;
it wrapped off the page and covered the Page options dialog in the touch runs.

* feat(gallery): remove Default View Mode from Page options (#330)

System > Appearance's Default layout is the one default-layout control, as
the S13 design's G5 says. The legacy gallery's Page options no longer show
or save a default view and Reset no longer resets it. A stored legacy
defaultViewMode still picks the view the gallery opens in (grid otherwise),
and Save and Reset keep that key for the workspace-preferences migration.
2026-10-28 13:31:23 +00:00
Vito 5583159497 feat(inspector): use recorded run history when the graph alone can't name the prompt (#386) (#396)
* feat(inspector): use recorded run history when the graph alone can't name the prompt (#386)

When a file's own graph leaves the prompt open, both inspect routes ask
recorded run history about the file (one run reported it and it was written
during that run). The save node that run reported it from anchors the walk
like a file-name match, so dataset branches that share one saver prefix
show their own prompt, labelled "Recorded run". Still open at one sampler
whose PromptManager text was recorded at execution (a language model's
output), the recorded text shows as "Execution recorded". Results the file
resolves alone never change; a failed lookup judges the file alone.

* fix(inspector): recorded texts follow history's saver; conflicts anchor nothing (#386)

Review follow-ups: the texts PromptManager recorded are looked up at the
sampler history's saver leads to, never at a sampler the unanchored walk
named; a file name naming a different saver than history anchors nothing;
outputs and texts are read in one transaction; "Recorded run" ranks below
every workflow-inferred source; unexpected lookup errors log at error level.

* fix(history): look up recorded runs by history's own spelling of the file (#386)

The history route inspects a case-normalized target on Windows, while
history matches file names exactly, so the occurrence's reference names
the file.
2026-10-28 13:29:29 +00:00
Vito e769845417 refactor(admin): open System for settings, maintenance, backup and restore (#330, PR 2) (#407)
* test(legacy): snapshot the routes and the settings response

Pins every registered route and the key paths of GET /prompt_manager/settings
while System S13 (#330) moves the legacy pages' tools to native System. Recorded
on the unchanged main; cutover (#334) removes it.

* test(legacy): prove each removed legacy control has native coverage

TESTING.md gains the "Legacy System UI removed by S13" table (no rows yet).
legacy-coverage.test.mjs fails when a removal cites a capability that is not
covered, copies another native test, lists an id or method twice, or leaves
the id or method on its page. legacy-system-links.spec.mjs holds the System
link table and guards the kept admin flows.

* refactor(admin): open System for Diagnostics and logs

The admin page's Diagnostics and View Logs buttons become links that open
System › Diagnostics & logs in a new tab. The legacy Diagnostics and System Logs
dialogs and their methods go; TESTING.md lists them against their covered
capabilities (rows B1–B7).

* refactor(admin): open System for settings and keep page options

Settings opens System › Overview in a new tab. The settings dialog becomes Page
options (opened from the list header) with only the Infinite Scrolling default,
and its Save posts {"infinite_scroll": …} alone, so a stale legacy form can no
longer overwrite scan folders or other values edited in System (#330 S13-9).
Scan folders, monitoring status, auto-hide, display mode, worker threads,
LoraManager and the LoRA import leave the page (rows B8–B14).

* refactor(admin): open System for maintenance

Maintenance opens System › Maintenance in a new tab, where each operation runs
as a reviewed Activity job. The one-click legacy Database Maintenance dialog
and its methods go (rows B15–B18); formatFileSize stays for the image modal.

* refactor(admin): open System for backup and restore

Backup DB and Restore DB open System › Backup & export in a new tab, where a
restore is staged, reviewed and run as an Activity job. The one-click Restore
dialog and its methods go (rows B19–B20); the scan dialog's quick backup stays.
The removed browser tests' data checks live on at the route: a new round trip
test restores every prompt with its tags and image links.

* test(legacy): find a removed modal id passed to showModal and settle the links spec

The removal check also finds an id passed as a string to a ...Modal() call and no
longer reads data-id= as id=. The no-request test waits for the page's own
start-up reads to finish, the sort check compares as the server sorts, and the
kept-flow test checks the added prompt shows in the list.

* test(admin): open Page options after the saved default is read

Drops the leftover LoraManager section comment above the Page options save.

* test(legacy): a lazy thumbnail load is not a System link's request
2026-10-28 11:25:20 +00:00