Skip to content

feat(canvas): add Auto-sort to the empty-canvas Tools submenu - #163

Open
BIackFIame wants to merge 7 commits into
howdeploy:mainfrom
BIackFIame:pr/canvas-auto-sort-v2
Open

BIackFIame wants to merge 7 commits into
howdeploy:mainfrom
BIackFIame:pr/canvas-auto-sort-v2

Conversation

@BIackFIame

Copy link
Copy Markdown
Contributor

This replaces #136 (reworked on top of current main after #158 and alongside #159; the old PR is closed with a link here).

Branch pr/canvas-auto-sort-v2 (head 65706b0d, 5 commits), based on main 0fa6895d.

Merge order: independent. It merges without conflicts with main, #159, the reworked orchestration / isolation / subagent-worktree stack and the coming plugin locale/scale PR, before or after any of them.

What changed since #136

Why

Your review of #136 kept only the auto-sort idea and listed what it should be:

  • it lives in the existing empty-canvas right-click menu, as a small Tools submenu with a single Auto-sort;
  • the criteria (task, status, project, grid) are a compact choice in that same menu;
  • the cards it moved are easy to see, and the move can be undone simply;
  • no snapshots, presets, broadcast input, layouts list or tool framework;
  • it follows the UI contract and is checked on Linux.

Everything else from #136 is gone: snapshots, presets, broadcast input, the Tools panel, output search, the drop preview and the earlier layers.

What changes for users

  • Tools › in the empty-canvas menu holds Auto-sort with four radio rows: By task (each subagent goes to the right of its parent), By status, By project and Grid.
  • What moves: the current selection, if it has two or more windows. Otherwise, every terminal, plugin and Browser window outside regions moves. Notes, materials and Files cards never move. Mascots move only when selected.
  • Layout: deterministic, so sorting twice changes nothing. It keeps the snapping grid and gap, and keeps clear of HOME, regions and the windows it does not move.
  • Feedback: moved windows get the existing outline for a few seconds (this does not select them) and a short glide (none with reduced motion).
  • Undo auto-sort appears in the same submenu while the moved windows are untouched. There is no global Ctrl/Cmd+Z, so terminals keep their keys. Nothing is persisted.
  • Placement: context menus and their submenus (Tools, Launch agent) no longer open over the minimap, the hints panel or the Home/zoom controls. The chevrons point where the submenu opens.
  • Command palette and context menu: both are now separate chunks. They are preloaded once the canvas is idle after its first paint, and rendered directly, without lazy()/Suspense (whose reveal throttle would hold the first open for about 300 ms).
    • The first right-click and the first Cmd/Ctrl+K open as fast as on main.
    • This frees about 16 KB of the startup bundle, far more than auto-sort adds.
    • The menu's placement code lives in its own module, which only the menu chunk imports. The Tools submenu body loads while the empty-canvas menu is open.
  • First open (review fixes): if the idle preload finishes between an open's render and its effect, the first context menu or command palette no longer stays blank. A menu first rendered while the Tools body was still loading now shows the Tools submenu once the body arrives.

Commits

  1. cc2fa064 perf(canvas): keep the command palette and the context menu out of the startup bundle
  2. 71f33dd4 feat(canvas): add Auto-sort to the empty-canvas Tools submenu
  3. 720abc10 fix(canvas): keep the Tools menu readable near corner panels and zoomed out
  4. f22aac54 fix(canvas): show a preloaded menu that finished loading before its effect ran
  5. 65706b0d fix(canvas): copy an already-loaded Tools submenu into a new context menu

Commit 1 is independent and can be taken alone; commit 4 fixes it. Commit 3 also changes the placement of the existing Launch agent submenu.

Verification

  • macOS: this exact tree (65706b0d), with a fake HOME. Results: build:helpers --host, typecheck, build and audit:secrets pass. The full suite gives 2277 / 2273 / 0 fail / 4 skipped (a first run had one failure in browser-engine-tabs, the known load flake that fix: fifteen bug fixes in isolation, orchestration, UI, plugins, GitHub sign-in and tests #159 fixes in that test; it passed on the rerun). The startup bundle is 1,676,591 bytes, which is 12,811 bytes smaller than main (1,689,402).

  • Tests: canvas-preloaded-surfaces renders through the real React and react-dom/server and the real dynamic imports. It checks that:

    • nothing is loaded at startup;
    • a preloaded menu and palette are in the very first render of an open;
    • the idle preload has a timeout;
    • a failed load is retried;
    • a preload that settles after the render still shows.

    In canvas-auto-sort, the Tools body is ready by the first render after Tools. renderer-bundle-splitting still checks that both are their own chunks, reachable only through import().

  • Combined: main + fix: fifteen bug fixes in isolation, orchestration, UI, plugins, GitHub sign-in and tests #159 + the reworked stack + this PR + the plugin locale/scale PR give the same tree in either merge order. The full suite on that tree gives 2627 / 2621 / 0 fail / 6 skipped.

  • Linux (Ubuntu 24.04 VM, xvfb, earlier head, same markup and styles): screenshots of every criterion, the outline, undo, the disabled hint and submenu placement, in ru/en, sage/night, at UI scale 0.85, 1.0 and 1.25.

    context menu Tools submenu
    after sort submenu near the right edge at 1.25

  • First open on Linux (VM, xvfb, CDP input, integration build, 3 launches × 2 combos, earlier head):

    • first right-click: 9.3–13.1 ms (2nd/3rd 4–7 ms; main 5.1–8.8 ms);
    • first Ctrl+K: synchronous, in the DOM before the key event's dispatch ends, as on main;
    • Tools submenu: 1.8–3.4 ms.
  • Not verified: Windows; the lilac palette.

Risks

  • On a crowded canvas, the block can move far right or down to find a clear spot. Undo puts it back.
  • Only the last sort can be undone, and the undo step is lost on restart.
  • The menu and palette chunks are loaded once the app is idle, even if they are never opened: about 16 KB of local code, parsed after startup. A right-click in the first moments after launch, before the idle preload has run, waits for the chunk (a local file, a few ms).

Depends on

Nothing.

…e startup bundle

The canvas command palette and the canvas context menu are rendered only
while they are open, but both were static imports of the canvas and so
part of the startup bundle. They are now separate chunks, which frees
about 18 KB of the startup bundle, and still open at once: the canvas
preloads both chunks once it is idle after its first paint, and renders
the loaded component directly. There is no lazy() and no Suspense
boundary on this path (React holds a suspended boundary's reveal for up
to 300 ms, which would delay the first right-click and the first
Cmd/Ctrl+K). An open before the preload has finished loads the chunk at
once and shows the menu as soon as it arrives.

renderer-bundle-splitting checks that each is its own chunk, reachable
only through a dynamic import(); canvas-preloaded-surfaces renders both
with the real React through the real imports and checks that a preloaded
menu and palette are in the very first render of an open.
Right-clicking empty canvas now shows a Tools row with one Auto-sort
action. The criteria (by task, by status, by project, grid) are radio rows
in that submenu, built with the existing canvas menu primitives. There is
no separate panel, toolbar, presets or layouts list.

- Scope: the current multi-selection when it holds two windows,
  otherwise terminal, plugin and Browser windows that are not completely
  inside a region. Companion mascots move only when selected. Sticky
  notes, materials and Files cards never move.
- "By task" uses the orchestrator-to-subagent relation that already
  exists (parentSessionId): each subagent goes to the right of its parent.
- The layout is deterministic: ties follow the current reading order, and
  sorting twice changes nothing. The block keeps the windows' top-left
  corner and keeps clear of HOME, regions, notes, materials, Files cards
  and windows it does not move (it shifts right or down, whichever is
  shorter, and never lands inside a region). With snapping on, every
  window lands on the 10px grid with the 20px snapping gap.
- Moved windows get a presentation-only outline that clears on the next
  press or after a few seconds (they are not made the marquee selection),
  and a short glide that is skipped when reduced motion is requested. The
  Browser card does not glide, because its native view is placed from the
  measured card.
- "Undo auto-sort" stays in the submenu while a sorted window is still
  where the sort put it. It restores only those windows. There is no
  global undo shortcut, so terminals and text inputs keep their keys.
- The last criterion is kept in renderer memory for the running session,
  only after a sort happened. Nothing is persisted. A failed load or sort
  shows a localized toast.
- The group-drag commit is extracted into commitLayerBounds and shared
  with auto-sort.
- The Tools submenu body, scope and layout are not in the startup
  bundle: they load while the empty-canvas menu is open (or when Tools
  is pressed first), so the submenu still opens at once.

Adds the official Lucide wrench and undo-2 icons, ru/en strings, and the
UI contract text in en/ru/zh-CN. Tests drive the menu and the submenu.
…ed out

- A context menu opened near a corner panel (minimap, shortcut hints,
  Home/zoom controls) moves up until it clears it; it was drawn over the
  bottom of the Home/zoom bar. When moving up cannot clear every panel (a
  tall menu near the right edge at UI scale 1.25, between the minimap and
  the shortcut hints), it moves sideways beside the panel it hits, at its
  own height; with no room on either side it keeps its place. Menus stay
  above the corner panels.
- A submenu (Tools, Launch agent) opens where it covers no corner panel
  either: when its usual side and direction would run over the shortcut
  hints, minimap or Home/zoom controls (in whichever corners Settings put
  them), it opens in the other direction or on the other side; at UI
  scale 1.25 near the right edge, Tools opened over the hints panel and
  left its clipped text showing beside it.
- The chevrons of Launch agent and Tools point to the side their submenu
  opens (the vendored chevron, rotated), not down.
- "Needs at least 2 windows" is a plain hint, not the uppercase group
  label. It lines up with the radio labels and is as muted as the
  disabled rows it explains, so it does not read like an active item.
- Zoomed out (summary), the outline of the windows a sort moved grows with
  the summary scale, so it stays visible at overview zoom for its 4 s.

The placement code (and the existing chooseCanvasSubmenuSide) moves from
canvasContextRouting to canvasMenuPlacement, which only the lazily loaded
menu imports, so it stays out of the startup bundle.
…ffect ran

When the idle preload settled after an open's render read the surface but
before its effect ran, the effect saw a loaded surface and returned without
re-rendering, so the first context menu or command palette could stay blank.
The effect now re-renders whenever the render it follows had nothing to show.
…menu

A menu first rendered while the Tools body was still loading started with no
body; if the load finished before its effect ran (or before it became the
empty-canvas menu), the effect and Tools both saw the module-level copy and
skipped loading, so Tools expanded with no submenu. Both now copy it in.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-22T13:42:04.598942Z 10a13b1 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 65706b0d05

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

});
if (!plan) return;
host.commit(plan.after);
host.setState({ mode, undo: { mode, ...plan } });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve the previous undo across no-op sorts

When the already-selected criterion is invoked again before anything moves, planCanvasAutoSort is idempotent, so the new plan has value-equal before and after maps. Replacing host.state.undo with it makes canvasAutoSortUndoTargets empty and removes “Undo auto-sort”, discarding the still-valid positions from the first sort even though all of those windows remain untouched; preserve the prior undo when the new plan changes no bounds (and skip the no-op commit/mark).

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch, confirmed with a test: running the same criterion again on an already sorted layout replaced the undo step with an equal before/after pair, so Undo auto-sort disappeared. Fixed in 62e95fb: a sort that moves nothing now skips the commit and the outline and keeps the earlier undo step. If a different criterion turns out to be a no-op, it only becomes the checked row and the undo step stays.

const selected = movable.filter((candidate) => selection.has(candidate.id));
if (selected.length >= 2) return selected.map((candidate) => candidate.id);
return movable
.filter((candidate) => !candidate.decorative)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep explicitly selected mascots in the fallback scope

When a marquee contains exactly one mascot (or a mascot plus only annotations), selected.length is below two and this fallback removes every decorative candidate unconditionally. With two regular windows present, Auto-sort therefore rearranges the regular windows but leaves the explicitly selected mascot behind; with only one regular window it is disabled altogether. Exclude unselected mascots here, but retain decorative candidates whose id is in selection.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Confirmed. A selection with one mascot (or a mascot plus notes) fell back to the default scope, which dropped every mascot, including the selected one. Fixed in 10a13b1: the fallback now leaves out only mascots that are not selected. This matches the UI contract ("companion mascots move only when selected"). Tests cover the scope function and the menu case of one window plus a selected mascot.

Choosing a criterion the layout already follows produced a plan whose
before and after were equal. Storing it as the undo step dropped the
previous sort's positions and hid Undo auto-sort. Such a sort now
commits and outlines nothing and keeps the earlier undo step.
A selection with fewer than two windows falls back to the default
scope, which left out every mascot, including one the person had just
selected. A lone selected mascot was then left behind, or auto-sort was
disabled next to a single regular window. The fallback now skips only
mascots that are not selected, as the UI contract describes.
@BIackFIame

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep them coming!

Reviewed commit: 10a13b11ab

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant