Commit Graph
5 Commits
Author SHA1 Message Date
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 2f8b7cb306 feat(images): select many images: ranges, keys, touch and all N matching (#392) (#393)
The workspace Images page selects like a file manager in Grid, List and
Masonry, across every loaded batch: Shift+click ranges from the anchor,
Ctrl/Cmd+click toggles a card without opening it, Ctrl/Cmd+A selects the
loaded images, Shift+arrows extend, Escape clears, and a touch long-press
starts a selection mode in which taps toggle. A plain click still opens or
inspects.

Once every loaded image is checked and more match, the bar offers
"Select all N matching". Add tags, Auto-tag selected and Set category then
send the listing filter instead of N identities: the image-tags job takes
`filter` in place of `targets` and /bulk/category takes `images_filter`,
both resolved by the listing's own admission code (up to 10,000 images, a
Source bound by its root_scope). Export metadata waits for loaded images.

Recorded as deviation D135; the concept is unchanged.
2026-10-27 22:20:27 +00:00
Vito a8aff3a39d feat(annotations): bulk Add tags and Auto-tag selected as Activity jobs (#283)
* feat(activity): image tagging jobs for bulk Add tags and Auto-tag

POST /prompt_manager/jobs/image-tags starts one record-changing
"image-tags" job over 1..500 listed images (202, or the same 409 busy
envelope as /jobs/scan). Add tags hashes each current file and unions a
fixed set into that byte record; Auto-tag captures each still image, asks
one legacy engine for its top N suggestions on the capture (count from
the options or autotag.defaults.suggestion_limit), re-hashes the file and
unions only onto the same bytes. Outcomes are grouped as tagged,
unchanged, skipped (video, too large, file changed, file not found, not a
still image) and errors; cancel stops at a file boundary.

Auto-tag holds the shared model gate for the whole job (409 model-busy at
start when it is held), loads the model once and never downloads one: an
unavailable model fails the job before the first file. The registry gains
blocker() so a start answers busy before taking the gate.

* feat(annotations): Add tags and Auto-tag selected in the selection bar

The Images selection bar gains the concept's Add tags and, beside it,
Auto-tag selected (D85). Add tags starts the image-tags Activity job with
the dialog's comma-separated names; Auto-tag selected reuses the Tag image
model panel (server defaults, Suggestions count) without Generate, refuses
a model that is not downloaded, and either starts the job (Apply
automatically) or walks the selection one Tag image dialog at a time
(Review each image, D90). Busy, model-busy and refused starts toast why;
the selection stays; a finished job's files are read again.

* feat(activity): show image tagging jobs with their own outcomes

Image tagging jobs get their own Activity view (D89): the pill reads
"Tagging", the item and heading line name the images and end with
"N tagged · M errors", and the job view shows Tagged, Unchanged, Skipped
and Errors tiles and outcomes, opens Tagged and Unchanged rows in the
viewer, and reads "Tagging log" and "Cancel tagging". The per-kind table
lives in activity-store.mjs; the scan's view is unchanged.

The selection bar's copy without a concept line is recorded as D117.

* fix(annotations): final review round for bulk image tagging

- Rename the D117 copy deviation to D118 (D117 belongs to another PR).
- The tagging job's progress line gives the scan's "about X min left"
  estimate, with each finished file as one batch.
- The start body cap fits 500 targets of the longest legal path, even
  escaped; one byte over answers 413.
- A capture store at capacity is retried with a short backoff, then the
  file is skipped with a rerun sentence instead of a Tag-dialog error.
- Output roots and their labels are resolved once per job in the
  executor, not twice per file on the event loop.
- Review each image holds Stop reviewing, Escape and x while a save is in
  flight, and its primary reads "Save and next" when only the manual
  field would be saved.
- An Add tags field that parses to no names keeps the dialog and its
  text and reports the message on the field.
- Image tagging finish toasts carry the counts ("... finished · N tagged
  · M errors").
- Specs: the bulk spec waits for the answered Start before checking that
  the invalid press sent nothing; the Activity spec uses a real error row.
2026-10-22 18:55:24 -07:00
Vito 195a1d33ac feat(annotations): Tag image dialog, card Tag button and inspector tag chips (#281)
* feat(annotations): image tagging controller with staged suggestions

Add the UI-agnostic Tag image dialog controller (#249 C1): observe the
file into a capture, prefill the saved tags and the server's tagging
defaults, stage the top suggestions as removable chips, and save manual
tags plus the remaining chips with case-insensitive dedupe (manual
spelling first). Every read is serial-guarded; saves are sent once and a
lost answer is resent identically only by Retry. Conflict, storage,
file and capture outcomes keep the draft and gate Save on a reopen.

ApiError now carries a validated copy of a tag-conflict body's current
tag set so the dialog can show it.

* feat(annotations): Tag image dialog beside Info on every image card

Every Images and Library-embedded gallery card shows the concept's Tag
after Info. Tag opens the concept's Tag image dialog (image-tagging.mjs
over the C1 controller) in the shared modal shell with the sticky
tagging-modal footer: capture preview, manual tags, the four legacy
engines with real defaults, explicit Generate, removable suggestion
chips (D83), legacy model labels (D84), a Suggestions count (D91) and
Retry where the controller allows it. The capture is released on close,
route change and pagehide.

The Tags page gains the concept's Auto-tag images for the image selected
in Images (aria-disabled without one, D115) and real Images counts that
follow every save. Retires D9, #245 D2 and #245 D3. The toast now uses
the mock's copy verbatim ("tags" even for one), C1's D60/D61/D68
markers are renumbered to D83/D84/D91, and at extreme zoom a card's info
row stacks (and wraps Info and Tag) when its row is narrower than the
narrowest bounded card, list rows included (D31).

* feat(annotations): show image tags in the inspector and viewer

The inspector parses the B3 envelope's image_tags facet (strictly: exact
keys, current/none/unavailable, at most 100 vocabulary names) and draws one
chip row after the prompt blocks: the file's current image tags first, then
D21's linked tags not already shown (case-insensitive). In the Images panel
image tags are Tags filter buttons and linked tags keep D21's rule; in the
viewer and wherever no Tags filter exists every chip is plain. none and
unavailable add no chips (D88, plan D65; D21 amended).

The tagging fixture reset now drops leftover captures, so a long run of
tagging tests in one worker stays below the capture store's limit.

* fix(annotations): re-read a tagged file in place after a save

After a tag save the selected file's inspection is re-read quietly: the
panel and the viewer keep the current details until the new answer lands,
with no "Loading file details…" in between. While a Tags filter or a
search is active, Images re-reads its loaded range in place (same query,
from the first page up to the loaded count, published once), keeping the
cards, the scroll position and the selection while the selected file still
matches; when it no longer matches, the panel follows the collection.

* fix(annotations): give suggestion chips the concept chip corner

The D83 chips took the later `.tag-suggestion` row rule's 5px corner; the
concept `.chip` has 4px. The concept comparison of the Tag dialog found it.

* test(annotations): compare the Tag dialog with the approved concept

The Tag image dialog's inventory (order, text, field attributes, tokens,
preview box, chips, status, sticky actions) must equal the concept's at
1600x1100 and 390x844, dark and light, opened, generated with WD14 and
with a language model, for an empty result and in the review scope, and
the card's Info and Tag group must equal the concept's. Production runs on
the concept fixture with the concept image's tags saved and the fake model
answering the concept's own sample; only D29, D83, D84 and D91 are mapped,
and a mutation check shows the comparison notices a change.

The D3 footer check again asserts a strictly shorter footer, with short
seedless subtitles that stay on one line beside Info and Tag.

* fix(annotations): final review round for the Tag image dialog

- Option fields show the server defaults that Generate sends: the
  controller bumps an options revision on open and on a model change,
  and the panel re-renders only then (I1).
- A tag conflict shows the current set at once ("Saved now: ...") and
  puts it ahead of the draft in the manual field, so the next Save
  sends what is shown (I2).
- The opening sentence is the concept's on-screen copy; "Remove
  anything you do not want to add." leads the chips only while chips
  exist; the comparison's opened mapping is gone (I3).
- ApiClient gains an additive keepalive option; the capture release
  and the Save PUT use it, and pagehide closes the dialog (I4).
- Closing during a save keeps the capture until the PUT settles, then
  releases it and still reports the outcome (toast, page refresh) (I5).
- A refresh asked during a page load is pending and runs once it
  settles; re-reads use pages up to 200 files; a failed re-read
  toasts; Load more waits for a refresh (I6, M2, M3).
- The selection bar drops checked files a refreshed result no longer
  lists (I7).
- D116 records Retry; after a failed Generate it takes Generate's own
  path, and after a changed storage, file or expired capture it
  observes again keeping the draft (I9, M4, M5).
- A save made from Tags marks a filtered Images visit stale (M6).
- images.mjs drops under 800 lines: the tagging wiring moves to
  images-tagging.mjs and the favorites star to images-favorites.mjs
  (unchanged), and reset() reuses dropSelection() (M1).
- Restore the activity-scan.mjs coverage floors; drop the duplicate
  paired captures test (I8, M9).

* fix(annotations): coverage floors for the Tag dialog round

Floors for the new images-favorites.mjs and images-tagging.mjs, and the
measured raises of image-tagging-state.mjs and image-tagging.mjs.
activity-scan.mjs keeps main's floors.
2026-10-22 15:21:07 -07:00
Vito 2b8126b7be feat(images): card selection checkbox and selection bar (Set category, Export metadata) (#259)
* feat(images): card selection checkbox and the concept selection bar

Every Images card's overlay starts with the concept's "Select <filename>"
checkbox before the star. Checked image keys form the page's selection,
kept across Load more, infinite scroll and bounded re-mounting and cleared
whenever the collection is replaced. The #bulkbar between the filter row
and the gallery shows "N selected" with Set category, Export metadata and
Clear (the first two are wired by the next tasks); Clear empties it.

The interim D8 star rule is removed, and the concept comparison now
measures the overlay with its checkbox and the bar with one card checked.

* feat(images): export the selected images' metadata as one JSON bundle

The selection bar's Export metadata reads each checked image's inspection,
at most four at a time, and downloads prompt-manager-images-metadata.json:
format, version 1, exported_at and, in listing order, the same envelope the
inspector's "Download metadata JSON" saves (one shared builder,
metadataEnvelope). The button is aria-disabled and ignores a second press
while it runs, the selection is kept, and the toast reports the exported
count, the images that could not be read, or that none could (no download).

Also: the bar and its count are built only on the Images page, the
Library-embedded gallery is asserted to have no checkbox or bar, a sort
change is asserted to clear the selection, the checkbox label test asserts
the toggle and an unchanged selection and inspector, and TESTING.md records
that a return to Images keeps the selection while a reload starts empty.

* feat(images): set the category of the selected images' linked prompts

The selection bar's Set category opens the concept's simpleInput dialog
("Categorize selected images", one Category field, Cancel / Apply) and
posts the selection's distinct linked positive ids to
/prompt_manager/bulk/category. Images have no category of their own; the
listing's category filter matches the linked positive's, so that is what
changes. With no linked image checked nothing is posted and a toast says
so; on success the toast counts the prompts and any skipped images, then
the categories and the listing reload (clearing the selection); a failure
keeps the selection. The button ignores a second press while it runs.

The bulk category route now rejects anything but a non-empty list of at
most 1000 positive integer ids (deduplicated in order) and validates the
category with validate_category; bad JSON or a non-object body is a 400.
The response shape and the legacy admin request are unchanged.

* test(images): zoomed card keyboard order follows the concept's checkbox before the star

The card's Tab order is image, Select checkbox, star, Inspect (concept app.js:398), both ways; List hides the overlays as the concept does, so there the image is followed by Inspect.

* fix(api): answer 400 for bulk category ids beyond SQLite's INTEGER range

An id of 2**63 or more passed the parser and made sqlite3 raise OverflowError, which the route turned into a 500.

* refactor(images): move the selection bar into images-selection.mjs and harden its actions

- images.mjs is back under 800 lines; the bar, its checked set, Export metadata and Set category live in images-selection.mjs.
- Busy flags release in finally; the dead guards are gone and the category targets are taken when the modal dialog opens.
- The category write no longer carries the page signal; leaving Images mid-write drops the saved visit so the return reloads.
- The categories and the listing reload together after Set category.
- The Category field stops at the server's 100 characters; the server's updated_count is a tested pure helper.
- New browser tests: leaving mid-export and mid-write, Blob URL revoke, updated_count 0 and a 400 rejection.
- Additive coverage baseline entries for images-category, images-export and images-selection.
- TESTING.md D8 records the dialog deviations and these rulings.
2026-10-20 16:04:15 -07:00