Skip to content

feat(ui): workbench polish: desktop file drops, compare waiting state, reference image rows, Upscale prompt tools, compact dropdowns - #10136

Open
joshistoast wants to merge 34 commits into
mainfrom
feat/ui-tweaks-06
Open

joshistoast wants to merge 34 commits into
mainfrom
feat/ui-tweaks-06

Conversation

@joshistoast

@joshistoast joshistoast commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

A broad UI pass over the workbench: forms, menus, top bar, status bar, Canvas, Preview, Gallery, Generate, Upscale, Layers, Workflows, Queue and Projects.

New

  • Files from the desktop: images dragged in from a file manager can be dropped on the Canvas drop zones or on Preview. They upload to the current board first. A stray drop no longer navigates the page away.
  • Select for compare waits visibly: it shows a hint on the stage, a pressed toggle and a gallery badge until a second image is picked, and can be cancelled from each of those, from "Cancel compare" in the menu, or with Esc. Results arriving meanwhile don't complete the comparison.
  • Compare view: the compare toolbar sits in its own dock bar like the filmstrip, and differently sized images now zoom together.
  • Upscale prompt tools: Upscale runs the prompt it shares with Generate through the selected prompt template and dynamic prompts, as the legacy UI did. Its sections remember whether they were open.
  • Reference images as list rows: rows use the shared List with hover and dividers, rename by double-click, an add row, Undo after removal, and issues in an icon popover by the toggle. Concept rows show the model type under the title with triggers beside it.
  • Canvas size readout: the frame size and the size it renders at show at the top right of the Canvas.
  • Staging shows only raster layers: while a progress image or staged result is shown, control layers hide along with masks and regions, as in the legacy canvas. Painting, moving or transforming a hidden layer shows them for the edit. The Original/Map/Overlay switch is not offered meanwhile.
  • Layers opacity: a typed field with a slider behind a chevron replaces the scrubber, with lucide Opacity and Blend icons.
  • Header undo/redo: Workflows and Canvas share one pair of header history buttons with shortcut tooltips. A narrow Workflows header moves them into its overflow menu.
  • Compact dropdowns: Field gains labelPlacement="inside". Select, Combobox and ModelSelect show the label at the start of the box and the value right-aligned. Converted across Generate, Canvas and widget quick settings.
  • Quick settings: the Queue, Workflow and Image Map cogs open the mini-settings popover instead of the full dialog.
  • Open project dialog: uses the shared virtualized List; incompatible projects show dimmed with their reason.

Improved

  • Menus and popovers:
    • Every menu or popover opened from a trigger points at it with an arrow; context menus, submenus and field dropdowns get none.
    • The arrow joins the border with no seam.
    • Menu item icons are dim by default; checks and icon-only items stay bright.
    • The "Reset size" menu has shorter labels and no double indent.
  • Top bar:
    • Cancel is always shown, disabled while nothing is generating.
    • The project switcher has a folder icon that opens with its menu.
    • Layout presets have tooltips with name, shortcut and purpose; Edit uses a pencil-ruler icon.
    • An Invoke issue's Show button sits at the end of its row.
    • The batch size field's borders continue under the Invoke button.
  • Status bar:
    • Chips align their labels with their icons and take segment-style rounded fills with a small gap.
    • Notifications are rebuilt on the shared List with a row menu.
  • Widget menus: the rail menus name what they fill ("Left panel widgets", "Right panel widgets", "Status bar widgets"), and the ⋯ trigger takes the same hover fill as its neighbours.
  • Canvas:
    • The frame outline is cased dark and light around the accent line, so it reads on any background.
    • The gradient tool icon fills its box.
    • "Add regenerate region" has a shorter description, so the context menu is narrower.
  • Layers: the Overview viewport box is outlined to read on any image. Layer rows sit closer to the panel edge.
  • Gallery:
    • In Fit thumbnail mode the border, selection ring and focus ring wrap the image's own rect.
    • In-progress tiles use the job's target ratio.
    • The bottom gallery's boards divider reaches the header.
  • Queue: the filter tabs scroll horizontally on narrow widths. The cache button uses the DatabaseZap icon.
  • Settings: "Invoke follows what you edit" is now "Auto-switch Invoke to what you're editing".
  • Hints: "Don't show me these" is an ordinary outline button.

Fixed

  • Submenus that open to the left started to close as the pointer crossed into them. Fixed with a patch to @zag-js/menu that keeps the submenu's placement on pointer entry.
  • Control weight and begin/end sliders reset after each drag.
  • Masks reappeared when a staged result was toggled off to compare, covering the original.
  • The delete media and delete board dialogs opened at the top of the page instead of the centre.
  • After deleting an image from Preview's context menu, the menu reopened and focus was lost.
  • The loading-model chip showed a raw translation key.

Related Issues / Discussions

None.

QA Instructions

Commands run from invokeai/frontend/webv2 with Node 22.14 and pnpm 10, on the pushed head:

  • Passed: lint:code, architecture:check, test (13,254 unit tests), test:fixtures, architecture:inventory, test:performance:architecture, the project-file journey and the browser smoke.
  • test:browser: one failure, ConfirmDialog.browser.test.tsx "reconciles a confirmation closed while its Activity is hidden". It fails the same way locally without these changes.
  • test:accessibility:browser fails locally at the same hidden-dialog step. I have not proven on a clean checkout that this is the same local cause.
  • Performance baselines re-recorded: editor-route scripts grow about 47 KB (1.0%), launchpad about 8 KB; request counts are unchanged.
  • Every commit in the last push typechecks on its own.
  • test:browser also had one failure in referenceImageRow.browser.test.tsx ("offers Choose model…") in a full run; it passed 3 of 3 when rerun alone.
  • CI on the previous push failed the frontend job on one font-dependent width assertion in LayerContextMenu.browser.test.tsx; the assertion now allows a few pixels. The macOS Python job also failed there; its log was not available when I looked, and this PR changes no Python.

Manual checks against the mock backend (pnpm dev:mock):

  • Drag an image from a file manager onto the Canvas and onto Preview.
  • Preview → Select for compare: the hint, pressed toggle and gallery badge appear; Esc, the hint's Cancel and "Cancel compare" each end the wait; picking a second image opens the comparison.
  • Compare two differently sized images side by side and zoom.
  • Upscale: pick a prompt template and enable dynamic prompts; the Invoke count follows.
  • Generate → Reference images: add, rename by double-click, remove and Undo, reorder; an issue shows as an icon by the toggle.
  • Layers: type an opacity, open the chevron for the slider; drag a control layer's weight and begin/end sliders and release.
  • Canvas: the size readout follows the frame while it is resized; the frame outline reads over blue, white and dark images.
  • Workflows and Canvas headers: Undo/Redo with tooltips; narrow the Workflows widget below 24rem and find them in ⋯.
  • Canvas staging (needs a live backend): with a result staged, control layers, masks and regions are hidden; paint on or transform a control layer and they show for the edit.
  • Open a submenu that opens to the left and move into it.
  • Status bar chips, notifications popover, layout preset tooltips, project switcher icon, disabled Cancel.

Not checked: real generation and staging against a live backend, real OS drags outside the test harness, Firefox and WebKit, the light theme.

Screenshots aren't attached yet.

Review

Material findings resolved:

  • File drops: a capture-phase drop listener unmounted the zones before React's onDrop ran, so drops did nothing outside tests. Fixed, with tests that drive real drag events. Files dropped on text fields were also swallowed; fixed.
  • Cancel button: the disabled button could still arm cancel-all with Shift held. Fixed.
  • Submenu patch: the first version kept the placement across closes, so a reopened submenu flickered. It now skips the reset only on pointer entry.
  • Compare: waiting could coexist with Auto-switch and be completed by an arriving result or a multi-select. Each now ends the wait, and deleting the shown image keeps an open comparison when a successor follows.
  • Reference rows: Backspace and a held Delete removed rows with no undo; IME composition committed renames early; issue names were positional; switching Kontext → Redux regressed. All fixed.
  • List keyboard guard: it broke arrow navigation from checkboxes. Narrowed to targets that use the navigation keys.
  • Workflows header: hiding Save at narrow widths broke the project-file journey. Undo/Redo move to the overflow menu instead.
  • Frame outline: the first casing failed on mid and dark blues, and culling and pixel snapping were asymmetric. Fixed; drawn as device-pixel fills.
  • Startup requests: the size readout added a Canvas startup request. Fixed with a chunk group; request counts are unchanged.
  • Staging: with control layers hidden, transforms and moved selections on them happened blind, and a layer converted during a transform kept a stale reveal. Both fixed; the staged preview now draws under control layers so a reveal shows over the result.
  • From the earlier round: gallery mouse drags, focus after clicks beside an image, arrow hit boxes, queue tab focus ring, focus after Preview delete, and Open project focus handling.

Remaining limits:

  • Arrow-key nudges of a control layer during staging happen with it hidden.
  • Staging always hides non-raster layers; the legacy "Isolated Staging Preview" switch is not ported.
  • The empty-control-layer hint still covers the staged result (unchanged from before).
  • The staging change was checked by engine and browser tests, not against a live backend.
  • Reference rows remove with Delete only; Backspace is not wired.
  • The size readout has no show/hide setting and no click action.
  • A truncated inside label has no tooltip.
  • The Gallery grid still drops focus to the page after a delete (unchanged from before).
  • There is no typed i18n, so template-literal keys can still miss without a compile error.

Compatibility / Rollout

  • @zag-js/menu@1.43.3 is patched through pnpm patchedDependencies. A bump of that package needs the patch re-applied or dropped.
  • lucide-react moves from 1.61 to 1.69.
  • No persisted-data migration or API change.

Checklist

  • The PR has a short but descriptive title, suitable for a changelog
  • Meaningful regression coverage added / updated where needed; obsolete tests/code removed
  • Persisted-state and API changes include required migrations / compatibility validation
  • Relevant performance/efficiency opportunities considered; material claims have evidence
  • Material review findings resolved and relevant checks rerun
  • Documentation added / updated (if applicable)
  • Updated What's New copy (if doing a release after this PR)

Autofix with [code]smith
Open in [code]smith · Need help on this PR? Tag @codesmith-bot with what you need.

Field gains labelPlacement="inside"; Select, Combobox and ModelSelect render the label at the start of their box.
Generate and canvas panels use inside labels for their dropdowns.
Quick settings show selects as a labelled row with a value-width select.
Queue offers its job scope, Workflow its snap, minimap and connection style, and Image Map its cluster options.
The board list marks the destination board with the Saving to icon; the docs describe it.
The boards toggle icon follows the gallery body's measured layout instead of its region.
Picker board labels keep the list's text size, and the search field gets space below it.
SegmentTabs gains isScrollable: wheel scrolling, edge fades and the selected tab kept in view.
Scroll-edge measurement moves from the shell's overflow edges to a shared platform module.
The Queue widget's filter strip and lists share the gallery rail's inset.
Menus, popovers and hover cards draw an arrow kept clear of the content's rounded corners.
Context menus anchored to the pointer and submenus show none; the layers keyboard menu anchors to a point.
The menu renders outside the item branches, so its leftover target no longer reopens it on the next item.
Focus returns to the opener when it survives the deletion, otherwise to Preview, via a new returnFocus prop.
The dialog lists projects with the virtualized List, dims incompatible ones and recovers from load errors.
List gains defaultFocusKey and ListItem isDisabled; the project switcher lazy-loads the dialog.
The chip resolves one literal translation key per state instead of a missing interpolated key.
Adds the loadingModel and loadingModelUnnamed strings, the latter for loads without a model name.
In Fit mode the tile border, selection and focus ring wrap the image's own rect inside the square cell; hover tints the cell and badges stay on its corners.
Generate, canvas, upscale and video progress tiles use the job's target ratio; ratios clamp to 1:3–3:1.
Clicks beside the image select and focus the tile, and mouse drags still start from anywhere in the cell.
@github-actions github-actions Bot added frontend PRs that change frontend files docs PRs that change docs labels Nov 3, 2026

@lstein lstein left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at 7a09ad9. The gallery, performance baselines, Open project dialog, quick-settings cogs, queue-status strings and the converted fields all held up. I found four low-severity issues, three of them confirmed in the browser, plus one focus issue I could not reproduce.

Confirmed in the browser

1. Hint cards on inside-label dropdowns open the dropdown.
FieldInsideLabel (src/platform/ui/Field.tsx:105) renders the FeatureHint inside the Select/ModelSelect trigger <button>. The hint card is portalled, but React synthetic events still bubble through the portal to the trigger. Clicking "Don't show me these" or "Learn more" in the card also opens the dropdown. Pressing Enter on them is caught by the trigger's keydown handler, so the dropdown opens and the link or button does nothing. This affects VAE, VAE precision, PID mode, encoder quantization, DyPE preset and Scheduler. With the outside label the bubble stopped at a plain <label>, so this is new.
Suggested fix: stop click/keydown/pointerdown from the hint card's content reaching the trigger, and add a test that interacts with the card.

2. Clicking a popover or menu arrow tip dismisses it.
arrowClearOfCorners (src/platform/ui/theme/recipes.ts:51) sets pointerEvents: 'none' on the arrow box. The hover-card arrowTip restores auto (:133); the popover (:159) and menu (:648) arrowTip recipes don't, so the tip inherits none. A click on the visible tip falls through, Zag treats it as an outside interaction, and the popover closes (or toggles, if it lands on the trigger). Before this PR the tip was part of the content's hit area.
Suggested fix: pointerEvents: 'auto' on the popover and menu arrowTip; the clip-path already limits the hit area to the visible half.
Separately, the tip is very hard to see in the dark theme: its fill matches the popover background and only a 1px outline shows.

3. Clicking the Scheduler's inside label while its list is open closes and reopens it, and the typed filter is lost.
The inside <label> (src/platform/ui/Combobox.tsx:231) sits inside Combobox.Control, but Zag's dismiss layer only excludes the input, trigger and clear trigger. Pointerdown on the label counts as outside: handleOpenChange(false) clears the query, then the label activates the input and openOnClick reopens an unfiltered list. To reproduce, open Scheduler, type dpm, click the word "Scheduler". The existing test only clicks the label while the list is closed.
Suggested fix: preventDefault the label's pointerdown while open, or add the label to the dismiss layer's exclusions.

From code reading, not verified

4. Hovering an invalid Combobox with an inside label tints the input part twice.
INSIDE_INPUT_CSS sets bg: 'transparent' on the input, but that doesn't cancel the recipe's nested _hover._invalid background from formControlInteraction (combobox.input in recipes.ts). The Control also gets bg.error/60 on hover, so the input region shows two stacked tints and the label region one, with a seam between them. I couldn't produce an error state on Scheduler to check it on screen.

Could not reproduce, may depend on environment

Preview: deleting without confirmation from a filmstrip thumbnail or the header ⋮ may drop focus to <body>.
closeContextMenu (src/workbench/widgets/preview/PreviewWidgetView.tsx:500) resolves the return target through getContextMenuReturnFocus (:485) synchronously when the menu closes. With confirmation off, deleteItems applies the optimistic removal in the same click, but React hasn't committed yet, so opener.isConnected is still true and focus goes to the thumb or ⋮. The commit then removes that element. I tested both paths on Linux (Cinnamon, click-to-focus) and focus stayed in Preview, so something in the real ordering saves it there. Because the code relies on timing rather than resolving the target after the commit, it may still fail in other browsers or under different load. The tests only cover the thumb and ⋮ paths with confirmation on; a {asks: false} variant of those would settle it.

…card

Hint cards stop pressed, clicked and keyed events from reaching the field that holds their label.
An open Combobox no longer dismisses, and loses its filter, when its inside label is pressed.
The visible tip takes the pointer; a menu ignores outside presses on its own arrow and keeps focus.
…tion

The menu's return target is resolved after the removal commits, and a removed filmstrip thumb hands focus to Preview.
@joshistoast
joshistoast requested a review from lstein November 5, 2026 00:20

@Pfannkuchensack Pfannkuchensack left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at 31bafcd, with lstein's review at 7a09ad9 as the starting point. No blockers. Findings come from reading the code and tests; nothing was run, so "fixed" below rests on the code and the new tests as written.

Status of lstein's points

  1. Hint card reaching the trigger: fixed. CARD_EVENT_BOUNDARY in FeatureHint.tsx, with pointer and Enter tests for both dropdown types.
  2. Arrow tip: the click is fixed (pointerEvents: 'auto' in recipes.ts:73, plus the Menu.tsx guard). The dark-theme visibility is unchanged: recipes.ts has no colour or contrast change since 7a09ad9.
  3. Combobox inside label while open: fixed in Combobox.tsx handlePointerDownOutside. The test asserts the list, filter, aria-expanded and focus survive.
  4. Double tint on invalid hover: fixed through INSIDE_INPUT_CSS, with a computed-style test.
  5. Preview delete without confirmation: fixed for the tested paths, with {asks: true/false} variants for stage, thumb and header ⋮. Those tests delete synchronously. Removal is optimistic in useImageActions, so the real path should match, but that is not confirmed.

Low

  • PreviewWidgetView.tsx:486-534: a later removal of the header ⋮ has no focus handoff of its own, while thumbs have releaseFocusOnRemoval. Only a removal landing in the same click is covered.
  • PreviewWidgetView.tsx, closeContextMenu (about :521): if onClose is ever reached from an effect, flushSync logs and does not flush. The possible route is the useRegisterWidgetOverlay mount effect calling setOpen(false). Not verified; checking whether closeWidgetOverlays can fire while this menu is open would settle it.
  • CARD_EVENT_BOUNDARY stops onKeyDown, onFocus and onBlur for all React ancestors of every hint card. It is documented and Zag's capture listeners are unaffected, so this is acceptable. It is worth keeping in mind for any Escape or focus handler above a hint label.

Sound: the lazy Open project dialog, the baseline edits (they match the lazy split), the Select and ModelSelect inside-label wiring, the new en.json keys, and the accessibility script locator. Gallery tiles, SegmentTabs and the queue changes are unchanged since lstein's review, and I did not review them in depth.

Thumbs, the header ⋮ and the video return focus to Preview when removed while focused, so closing the context menu no longer flushes React.
…us regions

Moves releaseFocusOnRemoval into the eagerly loaded focusRegions module instead of a new startup source.

@Pfannkuchensack Pfannkuchensack left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up at 175dee8, covering the changes since 31bafcd. Both Low focus points from my last review are fixed: the header ⋮ and the video now hand focus to Preview when removed, and flushSync is gone from closeContextMenu. There is one thing to do before this can be judged. Findings come from reading the code and CI logs; nothing was run locally.

This head has no real CI, and the next one will go red without a test fix.

  • The PR now conflicts with main in PreviewActionStrip.tsx, on the import block that #10114 changed. GitHub does not start pull_request workflows on a conflicted PR, so 175dee8810 has only CodeQL and the labeler. The closeContextMenu change is therefore untested at this head.
  • frontend tests failed on f10b93eb57, which still merged cleanly. PreviewNavigation.browser.test.tsx failed at import with does not provide an export named 'VideoUseGroups'. The test now renders the real PreviewActionStrip, which imports VideoUseGroups after the merge with main, and the vi.mock('@workbench/image-actions', …) factory at :467 does not list it. 175dee8810 does not touch that file, so the same failure should return once the import conflict is resolved. Adding VideoUseGroups: () => null to the mock should fix it.

Please rebase on main and add that, then the whole Preview focus suite gets its first run against these changes, including the {asks: true/false} delete variants that the flushSync removal affects.

The dark-theme arrow visibility from lstein's point 2 is still unchanged, as recipes.ts only moved pointerEvents since 7a09ad9.

Sound, from reading: releaseFocusOnRemoval in focusRegions.tsx resolves its target before the node goes and only acts if focus really fell to the body. Moving the closest lookup to cleanup time is fine, because refs detach before the node is removed. The video ref cleanup keeps the old behaviour of clearing videoRef and publishing false.

# Conflicts:
#	invokeai/frontend/webv2/src/workbench/widgets/preview/PreviewActionStrip.tsx

@Pfannkuchensack Pfannkuchensack left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up at 197ad69. The conflict is resolved, VideoUseGroups is in the mock, and CI is green here, including frontend-webv2-tests, so the Preview focus suite has now run against these changes. Nothing left from my reviews.

The merge resolution in PreviewActionStrip.tsx keeps both sides' imports, and the only other merge-side change is the mock line. The PR's own diff is otherwise unchanged. Findings come from reading the code and CI status; nothing was run locally.

Still open is the dark-theme arrow visibility from lstein's review: recipes.ts has not changed since my last look. I am leaving the decision on that, and on the requested changes, to lstein. As said on 05.11., I did not review the gallery tiles, SegmentTabs or the queue changes in depth, so this is not an approval.

# Conflicts:
#	invokeai/frontend/webv2/src/features/queue/ui/QueueWidgetView.tsx
#	invokeai/frontend/webv2/src/workbench/widgets/preview/PreviewWidgetView.tsx
# Conflicts:
#	invokeai/frontend/webv2/src/features/generation/ui/GenerateComponentsSection.tsx
#	invokeai/frontend/webv2/src/workbench/image-actions/ImageContextMenu.tsx
@joshistoast

Copy link
Copy Markdown
Collaborator Author

@Pfannkuchensack I'm adding more commits onto this branch here shortly, but am unable to replicate whatever dark arrow behavior is being reported here.

lucide-react 1.69 adds the Opacity and Blend icons. The menu patch keeps a submenu's placement when the pointer
enters it, so a submenu that opens to the left no longer starts to close on the way in.
… border

Menu item icons default to the subtle colour; checks and icon-only items stay bright. Arrow tips no longer show the
content border through them. The reset size menu gets shorter labels and loses its double indent.
Status bar chips align their labels and take segment-style fills. The top bar always shows Cancel, opens a folder
icon with the project menu, gives layout presets tooltips and puts an issue's Show at the end of its row.
Notifications use the list component, widget menus name the panels they fill, and delete dialogs are centred.
Workflows and Canvas share one pair of header history buttons with shortcut tooltips. A narrow Workflows header
moves them into its overflow menu.
Opacity is a typed field with a slider behind a chevron, replacing the scrubber. The Overview viewport box is
outlined to read on any image. Control weight and begin/end sliders keep their value after a drag.
…view

Files dragged in from outside the app upload to the current board, then land on the Canvas drop zone they were
released over or open in Preview. A stray drop no longer navigates the page away.
…ble on any background

A readout at the top right gives the frame size and the size it renders at. The frame outline is cased dark and
light around the accent line. Masks stay hidden while a staged result is toggled off.
Select for compare shows a hint, a pressed toggle and a gallery badge until a second image is picked, and can be
cancelled from each or with Esc. The compare toolbar sits in a dock bar like the filmstrip, and differently sized
images zoom together.
Upscale runs the prompt it shares with Generate through the selected template and dynamic prompts. Its sections
remember whether they were open.
Reference images are list rows with rename, an add row, Undo after removal and issues in a popover by the toggle.
Concept rows show the model type under the title with triggers beside it.
Editor route scripts grow about 1%; request counts are unchanged. Startup owner sets follow the new modules.
@github-actions github-actions Bot added the frontend-deps PRs that change frontend dependencies label Nov 11, 2026
@blacksmith-sh

This comment has been minimized.

@joshistoast joshistoast changed the title feat: UI tweaks: inside-label dropdowns, menu arrows, Fit-mode gallery tiles, queue and project polish feat(ui): workbench polish: desktop file drops, compare waiting state, reference image rows, Upscale prompt tools, compact dropdowns Nov 11, 2026
Whether the description or the widest label sets the menu width depends on the font, so the check allows a few
pixels instead of none.
Control layers hide with masks and regions under a progress image or staged result, as in the legacy canvas. Painting,
moving or transforming a hidden layer shows them for the edit. The Original/Map/Overlay switch is not offered meanwhile.
Layer rows sit 3px from the panel edge instead of 6px.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

7.0.0 7.1.0 docs PRs that change docs frontend PRs that change frontend files frontend-deps PRs that change frontend dependencies

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants