Skip to content

feat: UX refinement pass: undo-first actions, Invoke card routing, queue cache popover, canvas without the generation lock, layered project settings - #10074

Merged
joshistoast merged 140 commits into
mainfrom
feat/ui-tweaks-05
Nov 1, 2026
Merged

joshistoast merged 140 commits into
mainfrom
feat/ui-tweaks-05

Conversation

@joshistoast

@joshistoast joshistoast commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

A branch-wide UX refinement pass on the workbench, one shared metadata lifecycle for images and videos, and layered widget settings: a per-project pin over a user default over the built-in. Each area below is a few short rules; the behavior is otherwise unchanged.

Undo instead of confirm

  • Moves, removals, archives, stars, preset deletes, preset applies, resets, layout changes and the settings dot actions apply at once and offer Undo in the toast and, for the rest of the session, in the Notifications list. An Undo runs once, restores only what still holds what the action wrote, and says so when the state moved on.
  • Confirmation stays only for permanent or bulk actions (image and board delete, queue clears), which name their scope and counts.

Queue

  • Cheap actions never confirm. Failures stay as notices with Show in queue and Retry.
  • Explicit offline state, rows titled from prompts, stats as filter tabs.
  • A Cache popover in the header (invocation and model cache usage, hit rate, Clear and Enable/Disable per row) replaces the two cache footers and the header's Cancel current button. It is shown to users who can manage a cache; members see no Cache button.

Gallery

  • Selection scoped to what is visible. Delete dialogs list thumbnails. Board delete keeps media by default.
  • An explicit "Saving to" destination; follow-the-selected-board is an opt-in. A locate icon on the destination board. In a rail narrower than 16rem the destination collapses to its icon and the title gives way to the board name.
  • Grouped Move to menus and a readable actions button on the selected row.
  • An item's Gallery view (Images or Assets) is decided in one place; the Preview header's mini gallery opens at the previewed item's board and view.

Gallery metadata and recall

  • One gallery query per image or video holds its metadata, shared by the menu, Preview Details and recall: one read per item, a missing record is final, a failed read is retried on the next open.
  • Preview Details reads metadata and the embedded workflow independently; a failed read gets its own Retry row. Rows are parsed for videos too, with localized labels, legacy model names and guidance-only records.
  • Recall availability and execution read the same parsed video record, so a sparse record recall can apply is never disabled in the menu.
  • One recorded-model resolver with explicit policies serves images and videos.
  • Both recall executors finish every read before reading the panel and commit without yielding, so an edit made while metadata loaded survives.
  • Video recall resolves each named medium once, a few at a time; a deleted medium is dropped, any other failure is reported.
  • Recall notices are localized.

Generate

  • Switching the base model keeps concepts badged instead of deleting them.
  • Missing models stay selected with a fix path and rematch by hash on reinstall.
  • Validation issues sit next to their fields with one-click fixes.
  • Native undo survives programmatic prompt edits. A Dynamic prompts switch. Proper template and wildcard error states.

Invoke

  • One card holds Source, Destination, the routing lock and every blocker with its fix; the separate routing button is gone.
  • The card opens on hover, ArrowDown or right-click, never steals focus. Holding Shift shows the front-of-queue icon. The button describes its route and lock to assistive tech, lists ArrowDown among its shortcuts, and announces a popup only while it is blocked.

Canvas

  • The generation lock is lifted: painting, transforms and layer edits stay available while results generate or wait in review. Results are pinned to the rect they were submitted with.
  • Tool options survive engine disposal. Refused edits explain themselves. Restore points. One visibility model per layer. Fit to view inside the zoom menu.
  • New shortcuts: marquee M, shape U, color picker I, Alt+[ ] to cycle staged results.

Shell and Launchpad

  • Layout undo (Mod+Alt+Z). A Widgets dialog built on the list primitive. A danger area for Clear saved data.
  • Session expiry warns ahead and returns you to where you were. An escape from the startup redirect. Error states that never look empty.

Settings

  • Every widget setting and the three generation flags (CPU noise, NSFW checker, watermark) resolve as project pin, then the user's default from Launchpad › Preferences, then the built-in. Viewer auto-switch stays project-only.
  • Launchpad › Preferences edits defaults for Gallery, Canvas, Preview, Image Map and Generation. Inside a project the settings dialog and each widget's cog edit pins. The Project sidebar group and every "open a project to change this" state are gone.
  • The changed-setting dot is now a 24 px button. In a project it offers Use as my default and Follow my default; in Launchpad, Reset to Invoke default. It has a tooltip, an accessible name that includes the setting, and works by keyboard, inside the modal dialog and in the cog popover.
  • Pins live on the project's instance of the widget type, so a cog on a second Gallery view (center or bottom) edits the same pins the Gallery reads.
  • The generation flags join the Generate widget's cog and quick popover; they pin on the project even without a Generate widget, since Canvas and Upscale read them.
  • Antialias progress images is a preference, no longer per project.

Consistency

  • One progress badge for gallery tiles, filmstrip slots and staging thumbnails. Segment groups styled like the tab strip. Hint and topbar cards share one header.
  • The Diagnostics widget is right-rail only. The Project widget is retired; Copy project ID moved to the project switcher.

QA Instructions

From invokeai/frontend/webv2 under Node 22 and pnpm 10:

pnpm check:release

Result on the current head: all gates pass.

Gate Result
Lint (format, oxlint, tsc, architecture) pass
Unit tests 694 files pass
Browser tests 426 files pass
Mock fixtures, architecture inventory pass
Build and browser performance gates pass; branch-only cost against main below
Project-file journeys pass
Accessibility journeys (21) pass

Branch-only cost against main (committed baselines, same main point):

Route Metric main branch Change
launchpad initial script, brotli 808 KB 817 KB +9 KB (+1.1%)
editor initial script, brotli 1,217 KB 1,254 KB +37 KB (+3.1%)
editor-canvas script, raw 4,896 KB 5,121 KB +226 KB (+4.6%)
all routes locale + document, raw 251 KB 300 KB +49 KB (+19.7%)
launchpad startup requests 29 29 0
editor routes startup requests 87 to 96 84 to 92 3 to 4 fewer

The locale grows by 731 keys (+46.7 KB raw, +9 KB brotli) for the new notices, issues, dialogs and settings copy. The script growth is the metadata lifecycle, the Invoke card, layered settings and the notifications registry; about 45 new editor sources and 11 Launchpad sources. Fonts, CSS and images are unchanged.

Manual checks, mock backend only, dark theme, Chromium:

  • Invoke card and its keyboard paths, Cache popover, Widgets dialog, board row marker and contrast, segment controls, scrubber fill, hint cards, canvas zoom menu.
  • Preview Details failed-read row: with the metadata read forced to fail, the Metadata tab disables, Workflow opens, Retry recovers and selects the filled tab.
  • The mock backend's representative videos now record Wan metadata, so video Details rows and recall verbs can be exercised against it.
  • Layered settings: the dot menu in Launchpad › Preferences, in the project settings dialog and in the Gallery and Generate cogs, opened repeatedly with the pointer leaving the menu; Use as my default followed by Follow my default; the Generate flags pinned from the cog and read by Canvas.

Not observed here: real generation, model loading, reconnects, multi-user flows, Firefox.

Docs: the project switcher screenshot is retaken; the Invoke card has no screenshot yet.

Review

All material findings were resolved:

  • A selection-bar Clear that did nothing; a board menu leaking behind its delete dialog.
  • Prompt text inserted into whichever field had focus; a drag stuck after a drop on a full reference zone.
  • The Widgets dialog preloading every widget; a chunk-load failure looping forever on Home.
  • The Invoke card's hover preview pulling focus to the button; ArrowDown on Invoke leaking to global hotkeys.
  • Layout normalization made idempotent: saved layouts are normalized after retired panels are removed, so loading a project twice gives the same layout. Trade-off: an Edit rail that differed from an older default only by the retired Project panel (Layers, Preview, Gallery, Queue, Project) is treated as that default and becomes Layers and Preview; Gallery and Queue stay in the bottom bar.
  • A partial external video recall failing on a transient read of media it would have dropped anyway.
  • A has-workflow read inheriting a retry the metadata read does not; the Details panel opening on a disabled pane when both reads fail.
  • Component slots accepting a model of the wrong type; missing writer-to-reader coverage, now added for Wan, MiniMax H3 and LTX-2 records built through the graph builders.
  • The dot menu closing on its first open inside the modal dialog once the pointer left it; a click on the menu re-toggling the switch through the row label.
  • An extra editor boot request after Launchpad Preferences stopped importing the workbench context, resolved by grouping the runtime modules it had shared rather than by raising the budget.
  • A Gallery cog on a second Gallery view writing pins nothing read; Undo expiring with its toast; the Invoke button's route and lock invisible to assistive tech; "Saving to" covering the board name in narrow rails; a canvas Clear Undo able to replace later painting; a future settings-layering marker coerced as unmarked.

Remaining limits:

  • Canvas Fit to view is always enabled (the engine has no "already fitted" state).
  • The Shift-held state is re-synced from pointer events because one release was observed to go missing without a page blur.
  • Translated docs on the docs branch still describe the retired widgets.
  • Changing a default does not reset a project whose widget already paged on the old value until it reloads. Enter on the dot menu focuses the menu container before its first item.
  • Image delete has no Undo on purpose: it is permanent and still confirmed. A board archive's Undo is withdrawn when the project closes although archives are account-wide.
  • A toast's Undo cannot be reached by keyboard while the modal settings dialog is open; the Notifications list offers it after the dialog closes.
  • The session-expiry notice has no "Stay signed in" action: the server renews the token only on a write, and a renewal endpoint would be a separate change.

Compatibility / Rollout

Persisted layouts. Saved projects, session state, custom presets and built-in preset overrides that still dock the Project widget anywhere, or Diagnostics in the status bar, load with that placement dropped (the instance is kept if still placed elsewhere), alignEndInstanceIds cleaned, the region's active widget falling back to the first remaining one, and the preset not reading as modified. Retired canvas editor placements get the same treatment on every region. No schema version bump; the stored copy updates on the next save.

New per-project settings. Routing, auto-add destination and dynamic prompts have defaults, so older projects open unchanged.

Layered settings.

  • Persisted shapes are unchanged: widget instance values hold pins only, and the layered project settings keys are optional. Documents without the settingsLayering marker are coerced once at load (stored values equal to the built-in are dropped, the retired per-project antialias setting is dropped; the account preference starts off) and then carry the marker in the workspace half. A marker at or above the current version is never coerced again, so a newer build's documents keep their pins. Older builds keep loading the documents; a build predating the marker drops it on save, and the next load coerces again, which drops pins equal to the built-in so that project follows the user default for those settings.
  • User defaults live in preferences under scopedDefaults. An older build saving preferences drops that key, so defaults set here are lost after a downgrade; projects are unaffected.

Video recall, on purpose.

  • A transient failure while resolving a conditioning clip, source video or reference video now reports an error and applies nothing. A deleted medium is still dropped and the rest applies.
  • Video model references resolve by name and base when no type was recorded, and read the legacy identifier names.
  • galleryVideos.metadata is removed in favor of the shared query. The video_recall_requested event path (applyVideoRecallMetadata) keeps its contract.

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)

…d aspect placeholders

Unknown progress uses the shared spinner in gallery tiles and the Canvas staging placeholder.
Queued tiles keep their aspect in aspect mode; following a tile reveals Preview where it is docked
through one Workbench placement command that recall shares.
Required empty slots show a select prompt, an invalid state and the core missing message instead of "Model default".
…ction on Escape

Media gets a Filter & sort menu sharing the boards menu frame; the starred toggle stays separate.
Escape no longer clears the selection; Clear Selection has no default key.
…nd icons for canvas destinations

Zoom gains a magnifier and a fixed width; save becomes one menu for canvas or frame with a busy state.
The image menu, Preview Edit menu and paste dialog share one destination list with Layers panel icons.
…itle, align header controls

Details stays open while working elsewhere, follows the selection and uses segment tabs with disabled states.
An x after the image name clears the selection; auto-switch and zoom match the header size with mode icons.
…with Scrollable

Every manager focuses its search on "/" through ManagerLayout; Settings composes the shared search field.
The sidebar and open projects list use Scrollable, the list shrinking before the sidebar scrolls.
The models queue footer, table and rows move into platform ManagerQueue; nodes render through it with
retry, open and remove actions, and Clear finished keeps installs that are still running.
A Behavior preference chooses the Launchpad or the most recent project when the app opens at its bare URL;
deep links, reloads and later visits to Home are never redirected.
…its own group

Menu titles use MenuHeadingGroup; app, project and queue menu chevrons brighten alike on hover and open.
Donate moves after the Manage group, ahead of the account section and icon row.
Names MOSS-Image, Qwen-Image-2.1 and FLUX.3 Klein alongside the new video models.
…wners

searchShortcut and layerTypeIcon join existing shared chunks so startup request counts hold.
Owner sets gain the new modules; the build baseline is re-recorded.
Center-area header islands cap their width and show a fading edge cue with a button that jumps to the start or end.
The cue blocks clicks on items beneath it and forwards wheel input to the strip.
Board submenus share a scrolling menu that fills the available height and keeps the keyboard highlight in view.
Used by the image context menu, Gallery selection bar and board menu, and the layer save-to-board menu.
Every Generate settings toggle uses one row: the label toggles it, wraps instead of truncating, and keeps the switch at the trailing edge.
Override switches are named after their setting, with the slider below when on; paired controls and previews sit outside the label.
Adds ChromeIsland, wheelScrollsHorizontally and ScrollableMenuContent to the editor owner sets and re-records route bytes.
…ct flush

The heal dispatched its first no-op scroll synchronously inside the mount effect, so any virtualizer still
using flushSync logged "flushSync was called from inside a lifecycle method" when the Projects page or the
editor opened. The first check now runs in a microtask, still before the next paint, with a disposed guard.
Projects browser opts out of flushSync like the other list virtualizers; comments describe the remaining reason.
…anel is open

The lazily mounted panel paints one frame at the origin over its wide trigger, which fires a pointer leave and
lets the tooltip reopen on top of the panel header. Force the tooltip shut while the popover is open.
…s and color swatches

Project pin buttons and the color target chips relied on the native title attribute. They now use the platform
Tooltip, and the swatch tooltip names the target and its hex through a locale key.
… menu

The settings popover repeated its group headings as field labels ("Thumbnails / Thumbnails", "Pagination /
Pagination"); fields now read "Thumbnail shape" and "Page loading" under a "Grid" group. Boards without videos
offer a plain "Download board", and the board menu's download, archive and delete labels use sentence case.
… scope

Queue actions, queue header items and the graph menu mixed Title Case with the sentence-case labels around
them. The fonts upload scope read "My font" next to "My fonts" elsewhere.
…nimap, data-list selector

The splash heading wrapped the version mid-token; it is now a mono badge under the app name and the unused
combined locale key is gone. Fonts' empty state used the node-pack icon. An empty workflow rendered a bare
minimap panel. The data-list divider selector no longer triggers emotion's first-child warning.
… notices

Notification actions are data (open widget, undo, reveal or retry a queue item) with typed handlers,
undoable toasts carry project scope and supersede each other, and confirm dialogs take a cancel label.
…state

Cheap actions never confirm and bulk actions always name their scope, failures stay as notices with
Show in queue and Retry, stats become filter tabs, and rows take their titles from prompts.
…for moves

Selection follows what is visible, delete dialogs list thumbnails, board delete keeps media by default,
and "Saving to" names the destination with follow-the-selected-board as an opt-in.
…ues inline

Switching the base model keeps LoRAs badged instead of deleting them, missing models stay selected
with a fix path and rematch by hash, presets and reset offer Undo, and validation issues sit next to
their fields.
…ad errors

Programmatic prompt edits keep native undo and never target the wrong field, dynamic prompts get a
switch and a comment warning, and templates and wildcards show proper error states.
The Invoke button opens a card naming each blocker with a jump to its fix, layout changes undo with
Mod+Alt+Z, the Widgets dialog lists every widget and placement, and the router error screen offers
Reload, Home and Copy details.
…artup escape

Clearing saved data lives in a danger area, session expiry warns ahead and returns to where you
were, load errors never look empty, and the startup redirect can be left for the Launchpad.
Placement checks no longer load widget implementations on open, and no-op layout steps stay out
of the undo stack.
…bility model

Tool options survive engine disposal, refused edits explain themselves, restore points cap per kind,
layers have a single visibility, and the marquee, shape and color picker shortcuts move to M, U and I.
@joshistoast
joshistoast enabled auto-merge (squash) October 31, 2026 20:26
# Conflicts:
#	invokeai/frontend/webv2/src/features/generation/core/baseGenerationPolicies.test.ts
#	invokeai/frontend/webv2/src/features/generation/ui/promptFields/ExpandPromptSuggestion.browser.test.tsx
#	invokeai/frontend/webv2/src/features/generation/ui/promptFields/GeneratePromptFields.tsx
#	invokeai/frontend/webv2/src/features/generation/ui/promptFields/PositivePromptField.browser.test.tsx
#	invokeai/frontend/webv2/src/workbench/invocation.ts
@Pfannkuchensack

Copy link
Copy Markdown
Member

Review notes, round 2 (head 42ff009306)

This round covers the layered settings (0145cf15b6 to 65feef8f48), the fixes for both earlier rounds, and the persisted-data, performance-budget and accessibility changes. I read the code, ran focused unit tests (all pass) and checked the main flows in Chromium against the mock backend.

Both earlier rounds are resolved, except lstein's narrow-header report (item 2 below). The fixes hold up: the restore-point lock after the paint barrier, unplaced instances on layout undo, Cancel all except current, the board-delete choices, the required recall getter, 404 vs. 500, video 403, Retry for every failed item, Archive undo and the unload prompt on dispose. I also agree that my earlier #3 and #4 are non-issues.

The layered settings work in the browser. A default set in Launchpad › Preferences is inherited by the project, a pin from the cog shows the dot, and Follow my default removes it again. The dot menu opens with Enter and returns focus on Escape. Undo after deleting a preset restores it.

Should fix

1. A Gallery cog on gallery:center or gallery:bottom writes pins the Gallery never reads

const getTargetInstance = (project: Project, ownerId: string, target: SettingsTarget) => {
if (target.projectId !== project.id) {
return undefined;
}
const instance = target.instanceId
? project.widgetInstances[target.instanceId]
: getProjectWidgetInstance(project, ownerId);
return instance?.typeId === ownerId ? instance : undefined;
};

getTargetInstance resolves the cog's own instanceId, but GalleryUiAdapter always reads the gallery instance. On main the Gallery settings wrote through gallery.updateSettings, which always targets gallery, so this is a regression. settingsBindings.browser.test.tsx only uses targets without an instance id.

Reproduced: in Compose, switch the center view to Gallery, open its cog and turn off Always show dimensions. The cog shows the pin, but both grids keep showing dimensions, and the right-rail cog still shows On with no pin.

2. "Saving to" covers the board breadcrumb in narrow rails

const DESTINATION_MAX_WIDTH = 'min(18rem, max(6.5rem, 50cqi - 3rem))';

0cc3f872ca improves this, but the destination keeps a 6.5rem minimum.

Reproduced: at an 820 px viewport (iPad portrait) the right rail is 205 px wide. The destination button then sits fully on top of the "Toggle boards" button: a hit test at its left edge, center and right edge all lands on the destination, and the board name is not visible. Below some width one of the two needs to give way, for example by collapsing the destination to its icon.

3. The Invoke card's route and lock are invisible to assistive tech

aria-expanded={mode === 'pinned'}
aria-haspopup="dialog"
aria-keyshortcuts={ariaKeyShortcuts}

The removed RoutingControl carried source, destination and lock state in its name. Now:

  • The Invoke button's name is only "Invoke · 2 issues", and there is no aria-describedby.
  • The lock marker is aria-hidden.
  • aria-keyshortcuts lists Ctrl+Enter but not ArrowDown, the only keyboard way into the card.
  • aria-haspopup="dialog" announces a popup, but Enter queues a generation.

Scenario: a screen reader user can't hear the route, can't discover how to change it, and can queue a run by accident. Suggest putting the route and lock in a description, adding ArrowDown to aria-keyshortcuts, and reconsidering aria-haspopup on a button whose activation submits.

4. Undo only lives as long as its toast

actions: [{ label: undoLabel ?? i18next.t('common.undo'), onClick: () => runNotificationUndo(undoId) }],
description: message,
duration: ACTION_TOAST_DURATION_MS,
onStatusChange: ({ status }) => {
if (status === 'unmounted') {
releaseNotificationUndo(undoId);
}
},

The undo closure is released when the toast unmounts (8 s, paused on hover or focus), and the Notifications list stops offering Undo after that.

Scenario: deleting a preset or a rebalance curve no longer asks for confirmation, so 8 s later a deleted preset is gone for good. That is a tight window for keyboard and screen reader users, who have to reach the toast with Alt+T first. Keeping the undo on the notification record for the session would fit the registry's existing cap of 20 idle entries.

5. Weakened accessibility journey assertion

assert.ok(itemCount >= 1, 'The bottom widget menu must list at least one widget.');

"At least 8 bottom widgets, all enabled" became "at least 1". Only Diagnostics left the status bar, so >= 7 keeps the guard. As written, a regression that empties the menu down to one widget passes.

6. Raised performance budgets without the branch's own share

performance/browser-baseline.json, performance/architecture-baseline.json

The description says the bytes "follow main plus this branch", and 23704ac2fc credits the raise mostly to main. Most of it belongs to this branch:

  • Locale: the minified translation asset grows from 253,637 bytes at the merge-base to 300,365 at head, so +46.7 KB of the +49.4 KB re-recorded.
  • Script: editor brotli +3.1 %, editor-canvas script +4.6 %.

The scope plausibly justifies the growth, and request counts went down, but performance/README.md asks for the measured cost in review. Please state the branch-only before and after.

Minor

  • Older builds can delete pins.
    • Where: projectDocument.ts#L41-L44, #L199-L212.
    • An older build (a stale tab or a downgrade) that saves the project drops settingsLayering but keeps the values. The next load coerces again and drops pins that equal the built-in, so the project starts following the user default. That includes the NSFW checker and watermark flags.
    • Example: the user default is thumbnailFit: 'aspect' and the project pins 'square'. After an old build saves it, the project shows Aspect.
    • The comment calls this a harmless repeat. At least the comment and the Compatibility section should say so.
  • Future markers are coerced again. projectDocument.ts#L200 checks === SETTINGS_LAYERING_VERSION, so a marker 2 from a newer build counts as unmarked, gets the v1 rules and is re-stamped 1. A check for >= 1 would avoid that.
  • A customized Edit rail is replaced.
    • Where: workbenchState.ts#L2054 together with #L1808-L1823.
    • ['layers','preview','gallery','queue','project'] is stripped to a legacy default and becomes ['layers','preview'], which the new test asserts.
    • Making it idempotent is right, but the Review section lists this case as resolved. Please reword it or state the trade-off.
  • The dot actions have no Undo. In useLayeredSetting.ts#L166-L197, Use as my default replaces the account-wide default, generation flags included, with no toast or Undo. Follow my default and Reset to Invoke default behave the same way.
  • The antialias choice is dropped silently. projectDocument.ts#L189 does not carry the per-project value into the new preference, which defaults to off.
  • The session-expiry notice offers no action. SessionExpiryNotice.tsx#L14 has no "Stay signed in", and "unless you make a change" is vague.
  • The short name reads pins only. getProjectWidgetValues returns pins only; the resolved read is getResolvedProjectWidgetValues. Today's readers only read keys that aren't layered, but the short name invites the wrong call.
  • App duplicates a Gallery-private value. GalleryUiAdapter.tsx#L38 copies Gallery's private 'follow' sentinel and reads the raw autoAddBoardId at #L139. If Gallery ever normalizes the legacy value away, the one-time notice silently stops.
  • Dead code:
  • Backend test gaps. In test_images.py#L886-L916, the 500 case is pinned only through pytest.raises and never asserted as a 500 response. The workflow route's not-found → 404 branch is untested.

Description

  • The Review section narrates the process ("reviewed three ways"), which the template asks to leave out.
  • There are no screenshots of the UI changes. Two docs screenshots are still stale, and the project switcher's alt text already describes the new menu.
  • The Cache popover bullet says "Enable/Disable for admins", which is out of date: since 339e76dc9f non-admins get no Cache button at all.
  • lstein's report that deleting an image offers no Undo matches the stated policy (deletion is permanent and still confirmed). An answer on the thread would close it.

Checks

  • CI is green on 42ff009306.
  • Focused unit tests pass for settings, project documents, workbench state, notifications and recall, and so does architecture:check.
  • Not run: browser tests, performance gates, accessibility journeys, a real backend, multi-user flows (the account settings fix was only read), Firefox.

…r the dot actions

A cog on a second Gallery view wrote pins onto that alias instance, which nothing reads; pins now live on the
project's instance of the owner type and an instance target only fences. Use as my default, Follow my default and
Reset to Invoke default report with an Undo that restores by project id while the state still holds what they wrote.
The undo closure was released when its toast unmounted, so the Notifications list lost Undo after eight seconds
and untoasted notices never offered it. The listed record now owns the closure and toasts only hold it; entries go
when run, superseded, delisted, their project closes or the account changes. Notices restored by Clear's Undo keep
their actions and do not toast again.
…edited

With Undo living for the session, a late Undo could replace painting done since. The restore checks for edits
before and again after the paint barrier and leaves the point in History when it refuses.
…ed again

Preset restore and the archive Undo were checked for a late run; the archive now supersedes by board.
Below 20rem the rail label drops the Gallery title and separator; below 15rem the destination shows only its
icon and chevron, keeping its accessible name and tooltip. The destination used to cover the board button on
iPad-width rails.
The button carries a hidden description of source, destination and lock, lists ArrowDown among its shortcuts,
announces a popup only while blocked, and its card is named by its title. The accessibility journey asserts the
bottom widget menu against the named built-ins instead of a floor of one.
A marker at or above the current version counts as layered instead of being coerced and re-stamped. The
compatibility notes state that a build predating the marker drops it on save, so the next load drops pins equal to
the built-in, and that the retired per-project antialias flag is dropped.
… and drop dead selectors

The gallery's legacy follow sentinel is read through one exported predicate, the unused scoped-defaults selector
is gone, and the video recall test mocks the metadata read it still uses instead of a removed export.
The first ResizeObserver report rendered after the first paint, so a Preview narrower than full painted one frame
with full-density padding and chrome before shifting under the pointer. The arrow-button browser test lost its
hover in that frame on slow runners.
@Pfannkuchensack

Copy link
Copy Markdown
Member

Follow-up on round 2 (head a318bca84d)

Thanks, this round of fixes is thorough. I reviewed the branch delta 42ff009306..12295ad79f. The merge only brings in 4206a02b37 from main plus the matching scopedDefaults: {} line in its test.

All six should-fix items are resolved. I checked the UI fixes in Chromium against the mock backend:

  1. Gallery cog on a second Gallery view: the center cog and the right-rail cog now share one pin, and toggling Always show dimensions from either one changes both grids.
  2. "Saving to" in narrow rails: at an 820 px viewport the destination collapses to its icon, and the board toggle can be clicked again.
  3. Invoke card and assistive tech: the button keeps the name "Invoke · 2 issues", describes the route ("Workflow to Gallery. Down Arrow opens the route."), and lists ArrowDown in aria-keyshortcuts. ArrowDown opens the card with its first action focused.
  4. Undo lifetime: after the toast closed, Undo in the Notifications list restored a deleted preset 17 s later.
  5. Accessibility journey: it now asserts the exact set of bottom widgets, which is stricter than before.
  6. Performance: the branch-only table matches what I measured (locale 253,637 → 300,365 bytes minified; the brotli and request rows match the baselines).

Minor items: the marker check, the Undo for the dot actions, the backend 500/404 assertions and the dead code are fixed. The pin loss after a pre-marker build saves, the Edit rail trade-off, antialias and session expiry are now documented, which works for me.

Two new points:

Should fix: two Invoke card tests no longer prove the card is pinned

it('pins a hover preview on a press inside it, so leaving no longer closes it', async () => {
const { button } = await renderInvokeButton();
await act(() => userEvent.hover(button));
await expect.element(destinationGroup()).toBeVisible();
expect(button.getAttribute('aria-controls')).toBe(document.querySelector('[role="dialog"]')?.id);
await act(() => destinationGroup().getByText('Canvas').click());
await act(() => userEvent.unhover(destinationGroup()));
await act(() => userEvent.hover(document.body));
await expect.element(destinationGroup()).toBeVisible();
expect(harness.destinations).toEqual(['canvas']);
});

aria-expanded used to distinguish a hover preview (false) from a pinned card (true). Its replacement aria-controls is set in both states, and the pin check after the press inside the card was removed.

Scenario: if a press inside the preview stopped pinning it, this test would still pass. The final toBeVisible() succeeds on its first poll, inside the 200 ms close delay. The right-click test has the same gap: it never moves the pointer away, so a right-click that opened only a preview would pass too.

Fix: after the pointer leaves, wait past the close delay before asserting the card is still visible, or expose a pinned/preview state the tests can read.

Nit: the board name renders as a fragment in the narrowest rail

At an 820 px viewport the right rail's board toggle shows "Uncategorized" as "egoriz", with no ellipsis. MiddleTruncate keeps an 8-grapheme tail that never shrinks. The new layout gives the name about 33 px, so the head collapses to 0 px and the 50 px tail is clipped. This behavior predates the PR; only the new width exposes it. A plain end ellipsis below some width would read better.

Checks

  • CI is green on a318bca84d.
  • Focused unit tests for notifications, settings, project documents, canvas restore points and workbench state pass, and so does architecture:check.
  • Not run: browser tests, performance gates, accessibility journeys, a real backend.

The fixed eight-grapheme tail was clipped to a fragment once the head collapsed, as in the narrow gallery rail
header. The label measures on attach and resize and falls back to a plain end ellipsis when the tail and a few
head graphemes do not fit, keeping its full accessible name.
The card exposes its mode for tests; the right-click and press-inside-preview tests move the pointer away and
wait past the close delay, and a hover preview is shown to close.
The paragraph was read while the dialog's scale-in animation ran, so its bounding box depended on the frame.

@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.

Approving at 017a44126e. Both points from the follow-up are resolved, and I found nothing material in the new commits.

  • Invoke card tests (2c4ca38ec9): the right-click and press-inside tests now read data-invoke-card-mode after waiting past the close delay. The new control test shows that the same pointer moves do close a preview. I ran the file locally three times (34/34 each) and the three card tests five more times, all green.
  • MiddleTruncate (2c78886399): the end-ellipsis fallback fixes the fragment in narrow rails. The measuring is cheap (only labels with a tail, one shared ResizeObserver), and the two forms can't flip back and forth because both are measured from the same text ranges.
  • Preview density (277d2f2401) and the ConfirmDialog test (49cdea2e06): the density is now measured in the mounting commit, and its new test fails without the fix. The dialog test reads the layout height after the fonts load, which makes it more deterministic without weakening it.
  • The merge matches main exactly.

Optional nit: expect(cardMode()).toBe('closed') in the new control test relies on the card still being mid-exit when it is read. not.toBe('preview'), or just the not.toBeInTheDocument() poll that follows, would not depend on the exit animation's length.

CI is green apart from windows-arm64, which still reports no result.

@joshistoast
joshistoast merged commit 6b43aca into main Nov 1, 2026
25 checks passed
@joshistoast
joshistoast deleted the feat/ui-tweaks-05 branch November 1, 2026 06:44
@joshistoast joshistoast changed the title UX refinement pass: undo-first actions, Invoke card routing, queue cache popover, canvas without the generation lock, layered project settings feat: UX refinement pass: undo-first actions, Invoke card routing, queue cache popover, canvas without the generation lock, layered project settings Nov 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

7.0.0 api docs PRs that change docs frontend PRs that change frontend files python PRs that change python files python-tests PRs that change python tests Root services PRs that change app services

Projects

Status: 7.0.0: UX overhaul

Development

Successfully merging this pull request may close these issues.

4 participants