Status: ✅ All green · 18/18 r214.1 unit tests · 21/21 r214.1 Playwright smoke · 0 regressions in r207-r214
Date: 2026-08-30
Branch: feat/id9-r214-editor-polish @ bc3c338 (pushed to origin, on top of cc216ec)
Base: feat/id9-r213-vsil-editor @ 3d51f99
Repo: avidtech6/vibecoder-standalone
r214.1 is a stability + correctness pass on top of r214. It fixes 7 issues discovered after r214 was shipped:
1. Synchronous history-applied event (was queueMicrotask, caused flaky smoke)
2. Snapshot includes zoom + tool (r214 only had selection + layers)
3. Initial snapshot includes zoom + tool
4. setCurrentZoom/setCurrentTool notify subscribers (without this, zoom/tool updates weren't reflected in the UI)
5. Layer box label has pointer-events: none (was intercepting clicks)
6. VSILEditor refocuses on mouseDown (ensures keydown bubbles up to shortcuts listener)
7. Removed dummy editPreset state counter (no longer needed)
Before (r214): applySnapshot called fireHistoryApplied(snap) via queueMicrotask. The microtask delay caused flaky smoke assertions: the test ran Cmd+Z then waitForTimeout(200) and checked the DOM, but the React commit hadn't happened yet.
After (r214.1): fireHistoryApplied(snap) is called synchronously inside applySnapshot. The event fires during the React commit, so subscribers (LayersPanel) update their local state immediately. Tests can now use waitForTimeout(50) instead of 800.
function fireHistoryApplied(snap: SelectionSnapshot): void {
if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent('r214:history-applied', {
detail: { layers: snap.layers ?? null, hasLayers: Array.isArray(snap.layers), ... },
}));
}
}
The canUndo/canRedo flags still use queueMicrotask(notifySubscribers) to avoid "Cannot update a component while rendering" warnings.
Before (r214): SelectionSnapshot had only { activeAssetId, activeRegionId, layers }. Undo couldn't restore zoom or tool.
After (r214.1): Added zoom?: number and tool?: string fields. Every push includes them, so undo can roll back the full editor state.
const newSnap: SelectionSnapshot = {
activeAssetId: ctx.state.activeAssetId,
activeRegionId: id,
layers: singleton.currentLayers ? singleton.currentLayers.map((l) => ({ ...l })) : undefined,
zoom: singleton.currentZoom,
tool: singleton.currentTool,
};
Before (r214): initSingleton pushed the initial snap with only { activeAssetId, activeRegionId }. The sig function defaulted to 100/move, but the actual snap.zoom was undefined, so applySnapshot skipped the zoom update on undo.
After (r214.1): initSingleton now includes zoom: singleton.currentZoom and tool: singleton.currentTool in the initial snap, so undo from a fresh state restores the default zoom (100) and tool (move).
Before (r214): setCurrentZoom and setCurrentTool mutated singleton.currentZoom/currentTool but didn't notify subscribers. The hook's zoom and tool fields were stale because the React tree didn't re-render.
After (r214.1): Both call notifySubscribers() after the mutation. The hook re-renders and returns the new value.
const setCurrentZoom = useCallback((zoom: number) => {
singleton.currentZoom = zoom;
notifySubscribers();
}, []);
Before (r214): The label inside each layer box intercepted clicks. The smoke test had to use force: true to bypass the overlap.
After (r214.1): Label has pointerEvents: 'none'. Clicks pass through to the parent box. The smoke test now uses a regular page.click() (well, evaluate(() => el.click()) for the layer-bg → layer-ui overlap case, but the label is no longer the problem).
{l.name}
Before (r214): After clicking a child (tool button, layer box), focus moved to the child. The keydown listener on the editor div was still attached, but document.activeElement was a child. Some browsers / tests behave differently.
After (r214.1): The editor div has an onMouseDown handler that refocuses itself (via queueMicrotask) on any non-input child click.
const onEditorMouseDown = useCallback((e: React.MouseEvent) => {
const t = e.target as HTMLElement;
if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable)) {
return;
}
queueMicrotask(() => {
shortcuts.ref.current?.focus();
});
}, [shortcuts.ref]);
Also: useEffect on mount calls shortcuts.ref.current?.focus() so keyboard shortcuts work without an extra click.
Before (r214): The shell used a local useState counter that bumped on every tool/zoom change, just to force a re-render. Hacky.
After (r214.1): Removed entirely. The zoom and tool come from the singleton, and the subscriber notification triggers re-render. The "preset 1/4" counter in the edit bar is replaced with "zoom: 100%" (which is now a real value, not a dummy counter).
| Suite | Result |
|------|--------|
| r207 unit tests | 24/24 ✅ |
| r208 unit tests | 27/27 ✅ |
| r210 unit tests | 27/27 ✅ |
| r212 unit tests | 27/27 ✅ |
| r213 unit tests | 36/36 ✅ |
| r214 unit tests | 24/24 ✅ |
| r214.1 unit tests (NEW) | 18/18 ✅ |
| r212 Playwright smoke | 44/44 ✅ |
| r214 Playwright smoke | 23/23 ✅ |
| r214.1 Playwright smoke (NEW) | 21/21 ✅ |
| Total | 271/271 ✅ |
| Invariant | Status |
|-----------|--------|
| No iframe | ✅ (0 in r214.1 source, 0 in r214.1 mount tree, verified by Playwright) |
| No postMessage | ✅ (0 in r214.1 source) |
| Window-level event bus | ✅ (region:selected, vsil:asset-bound still fire) |
| a788 READ-ONLY invariant | ✅ (Inspector.readOnly bound to VSILEditor.readOnly) |
| R207 mounts functional | ✅ (PreviewHostVSIL/InspectorHost/CompositionGraphHost preserved) |
| R208 EditBar functional | ✅ (still mounts for non-timeline tiles) |
| All r206-r213 invariants | ✅ (no regressions in any unit test) |
| All r214 invariants | ✅ (24/24 still pass) |
| History-applied fires synchronously | ✅ (r214.1.06: event fires between before and after) |
| Snapshot includes zoom + tool | ✅ (r214.1.04, 05) |
| Layer click hitbox | ✅ (r214.1.08: label has pointer-events: none) |
| Focus holds after child click | ✅ (r214.1.14: editor/child has focus) |
src/host/editor/selection.ts | +60 (M, sync event + zoom/tool)
src/host/editor/LayersPanel.tsx | +8 (M, sync history-applied listener)
src/host/editor/Canvas.tsx | +6 (M, controlled zoom + label hitbox)
src/host/editor/VSILEditor.tsx | +50 (M, focus mgmt + singleton zoom/tool)
src/host/editor/__tests__/editor-r214-1.test.tsx | +390 (NEW, 18 assertions)
scripts/smoke/r214-1-smoke.spec.cjs | +400 (NEW, 21 assertions)
scripts/smoke/screenshots-r214-1/ | 3 PNGs (NEW)
Net: +914 lines. 3 new files, 4 modified files.
cd /workspace/vibecoder-standalone-r214
# Unit tests
npx tsx src/host/editor/__tests__/editor-r214-1.test.tsx # 18/18
npx tsx src/host/editor/__tests__/editor-r214.test.tsx # 24/24 (r214 regression)
npx tsx src/host/editor/__tests__/editor.test.tsx # 36/36 (r213 regression)
npx tsx src/host/chrome/__tests__/SideChrome.test.tsx # 27/27 (r212 regression)
# Playwright smoke
npx vite --port 5301 --host 0.0.0.0 &
R2141_BASE=http://localhost:5301 node scripts/smoke/r214-1-smoke.spec.cjs # 21/21
R214_BASE=http://localhost:5301 node scripts/smoke/r214-smoke.spec.cjs # 23/23
# Source check
grep -l "
1. Operator review of the r214.1 stability in the dev server (port 5301)
2. Operator decision on merge (r213 + r214 + r214.1 = the full ID9 chrome + Editor + polish + stability)
3. Layer drag-to-reorder in Layers panel (currently uses up/down buttons)
4. Multi-select layers (Shift+click to add to selection)
5. Context menu on layer boxes (right-click for delete/duplicate/etc)
6. Phase 4.2 dev-server, Phase 4.4.4 Step 3, Phase 4.5 VibeScope sync, Phase 4.6 vault code — operator picks still pending
7. a788 InspectorCard cross-repo migration (still pending)
1. Merge r214.1 (bc3c338) to main now, or wait for r215 (multi-select, drag-to-reorder)?
2. Production deploy — npm run build + scp to 185.249.73.178?
3. Inspector → AI integration (Phase 4.3 Step 2)?
4. Layer drag-to-reorder — high priority, or wait for r215?