* feat(history): concept heading and summary
The Prompt history page takes the approved mock's heading and summary
surface (docs/concepts/workspace/app.js renderHistory a591-598, #249 F1):
- Heading description is "Prompt history · one positive, one shared
gallery"; the actions stay "← Prompt library", "Edit metadata" and
button.send "Send to ComfyUI" (aria-label "Send prompt pair to ComfyUI:
<title>"), on one row at 1600. Send is disabled only while the selected
pair's roles are loading (D24).
- The summary is article.surface > span.chip category, the positive and
selected negative prompt blocks, then .card-meta "N tracked runs · N
images · N negative variants". Tracked runs come from the recorded
history window's recorded_run_count (hidden until a read completes,
kept while a refresh is in flight); images and variants from the
saved positive's image_count and variant_count.
- The summary's copy/send actions are named after the prompt, as in the
concept ("Copy positive prompt: <title>"); mountPromptBlocks gains
setTitle so an in-place metadata edit renames them without a remount.
- mountRecordedHistory gains an optional onRecordedCount callback.
Selector mapping:
- .library-surface (history summary) -> .library-history article.surface
- .library-surface .library-chip -> article.surface > span.chip
- "Copy|Send <role> prompt[ to ComfyUI]" in "Saved prompt texts"
-> "...: <title>"
- heading p.muted "One positive prompt. Reusable negative variants. One
shared gallery." -> "Prompt history · one positive, one shared gallery"
- Unchanged: region "Saved prompt texts", .library-variants,
.library-shared-gallery, .recorded-history (still .library-surface).
Tests: workspace-history "history heading actions stay on one row at 1600
and are enabled after load" and "summary meta uses recorded runs" (both
failed before), a prompt-blocks setTitle unit, and the mapped selectors in
workspace-visual, workspace-library-editor and the canonical comparison.
* feat(history): concept negative variants panel
The history's Negative variants panel is the concept's (app.js a598,
style.css 1059-1154): section.surface.timeline-panel with the panel
heading (h2, p.small "Choose a red dot to change the negative and filter
this gallery."), row actions Compare negatives (aria-pressed; its section
is Task F3), + Add negative and the approved Show archived; a .timeline of
.positive-lane and .variant-lane rows (--lane-offset 38 + 47i) with the
"−" .variant-dot, "<name> · N runs" and a .run-dot when the pair's
run_count > 1; and .timeline-lower with the legend and All images for
this positive.
D26: All images for this positive opens the concept dialog with the
embedded, bounded (Load more) media gallery instead of filtering the
shared gallery. The run dot opens Recorded run for the pair's newest
recorded execution, else a Repeat run dialog with the pair's newest
linked image. Backing data:
- library variants carry prompt_pairs.run_count;
- GET /prompt_manager/library/{positive_id}/history/pairs/{pair_id}/newest
reads the newest run in the positive's exact raw-text scope whose
current-epoch saved-pair hint still resolves to that pair (the
get_history_run hint rule), inside the fenced history snapshot.
- workspace-dialogs gains openContentDialog (onClose releases the body).
- mountRecordedHistory returns openRun.
D45 (new deviations):
- a dot is named "Select negative <title>" when no other loaded lane has
that title, else "Select negative variant N", so names stay unique;
- the bounded "N of M saved negative variants" count stays a visually
hidden status; "Saved combinations, not an execution timeline." is gone;
- archived lanes (#235) are concept lanes marked .library-variant-archived
at .6 emphasis with their Restore button; coarse dots keep a 47px
target; the heading, legend row and All images button wrap at 390px
and 200% zoom.
Retired CSS: .library-branches, .library-positive-lane,
.library-variant-lane, .library-variant-dot, .library-variants,
.library-panel-heading, .library-variant-footer, .library-variant-summary.
Selector mapping:
- .library-variants -> .library-history .timeline-panel (region "Saved
negative variants" unchanged)
- .library-branches -> .timeline; .library-positive-lane -> .positive-lane
- .library-variant-lane -> .variant-lane; .library-variant-dot -> .variant-dot
- .library-variant-archived and .library-variant-restore unchanged
- .library-variant-footer / button:last-child -> .timeline-lower / button
- .library-variants > p.muted -> .timeline-panel .panel-heading .small
- "Select negative variant N" -> "Select negative <title>" for titled
pairs (history, native library, inspector, visual and real-host
fixtures); untitled pairs unchanged
- lane text "· N linked image(s)" -> "· N run(s)"
- All images for this positive: aria-pressed filter -> dialog "All images
for this positive"; tests that used it for the whole gallery select the
lane linking the image, or read the dialog
- variant panel row actions gain Compare negatives first
Tests: new workspace-history "variants panel matches the concept", "run
dot appears only for pairs with more than one run and opens its run",
"all images dialog lists every image for the positive"; new
workspace-library-editor "show archived and restore still work" (all four
failed before the change). Python: variants carry run_count, newest pair
run (DB and route). Unit: content dialog. The history fixture counts two
canonical pairs and links one image to Clean geometry.
* feat(history): compare negative variants
Compare negatives now opens the concept's comparison (app.js a599-616,
style.css 1155-1179) as a section.surface right after the timeline panel:
h2 "Compare negative variants", p "Baseline compared with <name>. Choose
another red dot to change the comparison.", a .compare-grid of two
.compare-item negative blocks (baseline, selected), p.small "Added terms",
then span.diff-added "+ term" per added term or p.small "No added terms.".
Choosing another red dot updates the same section in place; pressing
Compare negatives again removes it.
D25: the baseline is the oldest active variant; with no selection the
selected side falls back to it, as the concept's v does. The pure
addedTerms(base, selected) in history-state.mjs is the concept's rule:
comma split, trim, drop empty terms, exact case-sensitive difference that
keeps the selected order and repeats.
D46 (new deviations):
- lanes are newest first, so the baseline is the lowest-id active variant,
not the first lane; while the variant page has more, the oldest is not
loaded and the section says "Load more negative variants to reach the
baseline." instead of guessing;
- the two blocks are named "Baseline" and "Selected" (regions and copy/send
actions), so no name repeats on the page;
- Compare negatives is disabled when there is no active variant.
Selector mapping: none changed; the section is new
(.library-history .timeline-panel + section.surface).
Tests: unit "added terms split, trim and exact-diff like the concept";
workspace-history "compare toggles, follows the selected dot and stacks at
390" and "compare waits for the page holding the oldest active negative"
(all failed before the change).
* test(history): guard the newest-pair route and record D45/D46
TESTING.md lists the Prompt history page's spec decisions (D24-D26) and
its new deviations D45 and D46 beside D23 and D40-D44, notes under D46
that production opens on the newest lane while the concept opens on its
baseline, adds GET /prompt_manager/library/{positive_id}/history/pairs/
{pair_id}/newest to the history route list with its guards, and documents
the detail variants' run_count.
Tests: the newest-pair read skips a stale reference_epoch row, stops at
16 stale-hint candidates (17 newer stale rows hide an older valid run and
report {run: null}), and rejects a negative-only or deleted positive; the
route answers 404 for an ineligible positive, 503 storage-unavailable for
storage failures without leaking details, and 409 history-changed for a
restore during the snapshot.
* fix(history): run-dot touch target, unique dot names and dialog cleanup
- D45: under pointer: coarse the 12px run dot gets a 44px ::after target,
and while a run dot shows the lane text wraps within 139px before it.
- D45: dot names compare trimmed titles and never use a title that reads
like a fallback "variant N", so names stay unique.
- All images and Repeat run share one dialog-scope slot: each open
replaces the previous scope instead of adding a cleanup per open.
- A run-dot lookup that settles after the view went stale opens nothing.
- Retire the unused .library-main .library-chip rule and the stale
"Saved combinations" comparison note.
Tests (workspace-history, Chromium and touch): the coarse run-dot target
and lane width; a failed newest lookup shows the toast and Repeat run;
Recorded run that cannot open (recorded history failed) falls back to
Repeat run; an archived pair's Repeat run reads archive=archived; Compare
negatives is disabled with no active variant; dot names stay unique.
* fix(workspace): one inert handle for aborted history and prompt-block mounts
An aborted parent no longer gets a separate never-called stub per method:
mountRecordedHistory returns its hoisted openRun (it answers false before
reading later state), and mountPromptBlocks returns one shared no-op for
update, setState, setTitle and dispose. This keeps the history.mjs and
prompt-blocks.mjs function-coverage floors (98) without lowering them.
Tests: test:coverage (Chromium and touch, 922 workflows) passes for all
61 files; it failed before on history.mjs 94.44 and prompt-blocks.mjs
93.75 function coverage.