## Implementation Plan: Fix Devii Window Shrinkage on Double-Click ### 1. Disable CSS Transition During Programmatic Resizes **File:** `FloatingWindow.js` **Rationale:** The `transition: width 0.18s ease, height 0.18s ease` on `.devii-window` causes the ResizeObserver to fire during intermediate animation frames. Even with the `Math.max` floor, this triggers redundant localStorage writes and could still lead to state corruption if any future change removes the floor. **Changes:** - In the `_applyGeometry` method (around line 424-425), before setting `width` and `height` on the element, add a style `transition: none` to the window element to suppress animation during programmatic resizes. After a `requestAnimationFrame`, restore the original transition property. - In the `_observeResize` handler (around line 407-421), add the same suppression when writing the new geometry back (or better, call a helper that defers the write until after animations settle). - Alternatively, add a class `.resizing` that sets `transition: none !important` and toggle it around geometry updates. This is simpler and avoids micro-tasks. **Implementation sketch:** ```javascript // FloatingWindow.js, inside _applyGeometry this.element.classList.add('resizing'); this.element.style.width = g.width + 'px'; this.element.style.height = g.height + 'px'; // Remove the class after a paint frame requestAnimationFrame(() => { this.element.classList.remove('resizing'); }); ``` Add to `devii.css`: ```css .devii-window.resizing { transition: none !important; } ``` The same pattern should be used in `_observeResize` when it sets `this.geometry.width/height` (around lines 411-412). Because `_observeResize` is called from the ResizeObserver callback, which fires during the animation, adding the `resizing` class here will suppress the transition for the next frame. However, a safer approach is to **debounce** or **coalesce** ResizeObserver events during animations. For simplicity, we can skip the suppression in `_observeResize` and rely on the fact that the floor prevents shrinkage; but to eliminate wasteful writes, we should add a flag `_isAnimating` that the ResizeObserver handler can check and skip persistence if true. Set `_isAnimating = true` before any geometry change that might trigger animation, and clear it after a short timeout (e.g., 250ms) or after the next animation frame. **Preferred approach:** Add a `_suppressResizePersistence` flag toggled by `_applyGeometry` and state transitions. The `_observeResize` callback will skip writing to localStorage when this flag is true. After a requestAnimationFrame, clear the flag and allow the next resize to persist. ### 2. Fix `_onViewportChange` Bypass **File:** `devii-terminal.js` (around line 454-456) **Problem:** `_onViewportChange` calls `this._setState("open")` unconditionally when viewport crosses the 640px mobile breakpoint, even if the terminal is already open. On Firefox+Windows, a double-click may cause a viewport change event (e.g., virtual keyboard or layout shift), which then re-enters state machine and triggers the open animation → triggers ResizeObserver → persists intermediate geometry. **Fix:** Add a guard to check if the window is already in an open state before calling `_setState("open")`. Use the existing `_canOpenOnGesture` pattern or simply check `this.element.state !== 'closed'`: ```javascript _onViewportChange() { if (window.innerWidth >= 640 && this.element && this.element.state === 'closed') { this._setState("open"); } // ... rest of existing logic } ``` Additionally, ensure `_onViewportChange` is not called from within a dblclick handler that might be re-entrant. If needed, wrap the handler with a debounce or a simple `if (this._processingViewportChange) return;` flag. ### 3. Add Unit Tests **File:** `tests/test_devii_terminal.py` (or create new test file) **Coverage needed:** - Test that `_canOpenOnGesture` returns `false` when window state is not `'closed'`. - Test that double-click outside (simulated via `_onDoubleClick` or a custom event) does not change geometry below 320x220. - Test that `_observeResize` clamps geometry to minimum dimensions. - Test that after a state transition that triggers animation, the persisted geometry is not an intermediate value (i.e., final size is saved, not transient). - Test that `_onViewportChange` does not call `_setState("open")` when the window is already open. Because the codebase uses DOM and browser APIs, consider using a mock `window`, `ResizeObserver`, and `localStorage` in the test environment. If the test framework supports it, create a minimal DOM simulation. If the existing test suite is Python-based (e.g., pytest with Selenium or Playwright), write integration tests that open Devii, double-click outside, and verify the window size remains stable after multiple close/reopen cycles. Otherwise, write unit-level tests for the JavaScript functions by mocking the required globals. **Implementation:** - Create `tests/test_floating_window.js` and `tests/test_devii_terminal.js` (or extend existing `tests/` files if any). - Use Node.js with `jsdom` and `@sinonjs/fake-timers` if JavaScript tests are expected. - Or, if the project's test suite is Python-based, use `pytest` with `selenium` or `playwright` to drive Firefox. Given the environment is Windows+Firefox, the integration test should be run in the CI pipeline. For this plan, assume we will add at least three test cases covering the core fix. ### 4. Verification Run the project's own verification commands: ```bash pip install -e '.[dev]' -q make test-unit pip install -q ruff ruff check . ``` Additionally, manually verify: - Open Devii, double-click outside the terminal window – window size remains unchanged. - Close and reopen Devii 10 times – window size stays at its previous (or default) size. - Check that no intermediate sizes are logged to localStorage during animations (optional, can be verified with developer tools). --- ## Definition of Done - [ ] All code changes pass `ruff check .` with zero errors and zero warnings. - [ ] All existing unit tests pass (`make test-unit`) – no regressions. - [ ] New unit tests added (at least 3) covering: - `_canOpenOnGesture` returns `false` for non-closed states. - Geometry clamping in `_observeResize` and `_clampGeometry` floors width and height at 320 and 220 respectively. - `_onViewportChange` does not call `_setState("open")` when window state is already open. - [ ] The CSS transition animation does not cause persistent geometry changes: verified by running a sequence of 10 close/reopen cycles programmatically and asserting the persisted geometry never drops below 320x220. - [ ] Double-clicking outside the Devii window on Firefox/Windows does not shrink the window (tested manually or via integration test). - [ ] No unnecessary localStorage writes occur during resize animations (optional but recommended – can be verified by spying on `localStorage.setItem` in tests).