R217 — Multi-Select Keyboard Shortcuts (Cmd+A + Esc) — AI Summary

One-liner: Added Cmd/Ctrl+A (select all) and Escape (clear selection) to the VSIL Editor, both pushing undoable snapshots. Also fixed a latent r214 bug where onSelectAll/onLayerUp/onLayerDown read from layersRef.current (never updated) — replaced with selection.layers (singleton, kept in sync by LayersPanel).

By the numbers:

Why it matters: After r215 + r216, the editor has rich multi-select + multi-transform — but only mouse-driven. The user wants keyboard parity. Cmd+A and Esc are the universal "select all" / "deselect" gestures, expected in every modern editor.

Key implementation choices:

  1. Hook-driven shortcuts: Extended the useShortcuts interface with onSelectAll + onClearSelection (optional). VSILEditor wires them in. No new event listeners — the existing keydown listener on the editor div handles them.
  2. Singleton source of truth: selectAll(allLayerIds) reads from selection.layers (singleton, written by LayersPanel on every render via setCurrentLayers). This replaced the dead layersRef.current that r214 left behind. This is the same pattern that powers undo/redo/zoom/tool — r217 just applies it to the multi-select API.
  3. Input-skip guard: Both shortcuts bail if the event target is INPUT/TEXTAREA/SELECT/contentEditable. This means typing a name in the Inspector's name field doesn't trigger select-all or clear-selection. Same pattern as the existing undo/redo guard.
  4. No-op on same state: selectAll early-returns if the new set matches the current set. clearSelection early-returns if already empty. This prevents spam-clicking Cmd+A from polluting the undo stack.
  5. All 6 surfaces in snap: Both new pushes include activeAssetId, activeRegionId, selectedRegionIds, layers, zoom, tool — same as every other snap in r214+. This means undo correctly rolls back the entire editor state, not just the selection.

The bug it fixed:

// r214 (broken):
const layersRef = useRef<LayerEntry[]>([]); // ← never assigned
onSelectAll: () => {
  const arr = layersRef.current; // ← always []
  selectionRef.current.selectAll(arr.map(l => l.id)); // ← no-op
}

// r217 (fixed):
onSelectAll: () => {
  const arr = selection.layers; // ← from singleton, kept in sync
  selectionRef.current.selectAll(arr.map(l => l.id));
}

This same bug also affected r214's onLayerUp / onLayerDown (the [ and ] shortcuts) — those remain broken in this commit (out of scope for r217). The fix is the same: read from selection.layers instead. R218 or r217.1 candidate.

What r217 does NOT do:

Status: SHIPPED LOCALLY on feat/id9-r217-keyboard from main @ cc5a51d. Ready to push + merge.

Production deploy: BLOCKED — VPS SSH not available from sandbox. Build is ready, operator needs to deploy from their side or provide SSH key. Bulletin b1866 covers this.

Next:

  1. Commit impl report + AI summary + screenshots
  2. Push to feat/id9-r217-keyboard
  3. Bulletin b1867 (r217 SHIPPED)
  4. Memory lock (r217 pattern, 10-point reflex)
  5. Awaiting operator: SSH key, merge approval, or feedback