diff --git a/app/drag.js b/app/drag.js
index 96d41734..688183ec 100644
--- a/app/drag.js
+++ b/app/drag.js
@@ -1,136 +1,11 @@
// app/drag.js
-// Click-and-hold window dragging utility
-
-const HOLD_DELAY = 300; // ms before drag starts
-const MOVE_THRESHOLD = 5; // px - cancel hold if mouse moves more than this
-
-let isDragging = false;
-let holdTimer = null;
-let startMouse = null;
-let startWindowPos = null;
-let windowId = null;
-
-// Elements that should not trigger drag
-const isInteractive = (el) => {
- if (!el) return false;
- const tag = el.tagName?.toLowerCase();
- if (['input', 'textarea', 'button', 'a', 'select', 'label'].includes(tag)) return true;
- if (el.isContentEditable) return true;
- if (el.hasAttribute('data-no-drag')) return true;
- if (el.closest('[data-no-drag]')) return true;
- // Check for -webkit-app-region: no-drag
- try {
- const style = getComputedStyle(el);
- if (style.webkitAppRegion === 'no-drag') return true;
- } catch (e) {
- // Ignore errors from pseudo-elements
- }
- return false;
-};
-
-const cancelHold = () => {
- if (holdTimer) {
- clearTimeout(holdTimer);
- holdTimer = null;
- }
-};
-
-const endDrag = () => {
- cancelHold();
- if (isDragging) {
- isDragging = false;
- document.body.style.cursor = '';
- document.body.classList.remove('is-dragging');
- }
- startMouse = null;
- startWindowPos = null;
- windowId = null;
-};
-
-const onMouseDown = async (e) => {
- // Only left click, not on interactive elements
- if (e.button !== 0) return;
- if (isInteractive(e.target)) return;
-
- startMouse = { x: e.screenX, y: e.screenY };
-
- holdTimer = setTimeout(async () => {
- try {
- // Check if window is draggable (API option can disable this)
- const draggableResult = await window.app.invoke('window-is-draggable');
- if (!draggableResult?.draggable) return;
-
- // Get window ID and position
- windowId = await window.app.invoke('get-window-id');
- if (!windowId) return;
-
- const pos = await window.app.window.getPosition();
- if (!pos.success) return;
-
- startWindowPos = { x: pos.x, y: pos.y };
- isDragging = true;
- document.body.style.cursor = 'grabbing';
- document.body.classList.add('is-dragging');
- } catch (err) {
- console.error('Failed to start drag:', err);
- }
- }, HOLD_DELAY);
-};
-
-const onMouseMove = (e) => {
- if (!startMouse) return;
-
- if (!isDragging) {
- // Cancel hold if mouse moves too much before delay
- const dx = Math.abs(e.screenX - startMouse.x);
- const dy = Math.abs(e.screenY - startMouse.y);
- if (dx > MOVE_THRESHOLD || dy > MOVE_THRESHOLD) {
- cancelHold();
- startMouse = null;
- }
- return;
- }
-
- // Calculate and apply new position
- const deltaX = e.screenX - startMouse.x;
- const deltaY = e.screenY - startMouse.y;
- const newX = startWindowPos.x + deltaX;
- const newY = startWindowPos.y + deltaY;
-
- window.app.window.move(windowId, newX, newY);
-};
-
-const onMouseUp = () => {
- endDrag();
-};
-
-// Also end drag if window loses focus
-const onBlur = () => {
- endDrag();
-};
+// Window dragging is now handled by the unified drag system in preload.js.
+// This module is kept as a no-op stub so existing imports (e.g., settings.html)
+// don't break. The preload drag runs automatically in every window.
/**
- * Initialize click-and-hold window dragging
- * Call this once when the page loads
+ * No-op: drag is handled by preload.js
*/
export function initWindowDrag() {
- // Check if window.app is available
- if (!window.app?.window?.move || !window.app?.invoke) {
- console.warn('Window drag: app API not available');
- return;
- }
-
- document.addEventListener('mousedown', onMouseDown);
- document.addEventListener('mousemove', onMouseMove);
- document.addEventListener('mouseup', onMouseUp);
- window.addEventListener('blur', onBlur);
-
- console.log('Window drag initialized');
-}
-
-// Auto-initialize when DOM is ready
-if (document.readyState === 'loading') {
- document.addEventListener('DOMContentLoaded', initWindowDrag);
-} else {
- initWindowDrag();
+ // Preload handles window dragging for all windows.
}
diff --git a/extensions/cmd/panel.html b/extensions/cmd/panel.html
index d1409424..12980e6a 100644
--- a/extensions/cmd/panel.html
+++ b/extensions/cmd/panel.html
@@ -63,7 +63,7 @@
font-weight: 500;
padding: 0;
box-sizing: border-box;
- -webkit-app-region: no-drag;
+
z-index: 2;
}
@@ -111,7 +111,7 @@
padding: 10px;
margin: 5px 10px;
box-sizing: border-box;
- -webkit-app-region: no-drag;
+
background-color: rgba(40, 44, 52, 0.95);
border-radius: 8px;
}
@@ -196,7 +196,7 @@
cursor: pointer;
font-size: 14px;
padding: 2px 6px;
- -webkit-app-region: no-drag;
+
}
#chain-indicator #chain-cancel:hover {
@@ -215,7 +215,7 @@
border-radius: 8px;
max-height: 200px;
overflow: auto;
- -webkit-app-region: no-drag;
+
width: calc(100% - 20px);
box-sizing: border-box;
}
@@ -379,7 +379,7 @@
cursor: pointer;
font-size: 12px;
padding: 2px 8px;
- -webkit-app-region: no-drag;
+
}
#execution-state .exec-cancel:hover {
@@ -403,7 +403,7 @@
transition: all 0.15s ease;
margin-right: 10px;
flex-shrink: 0;
- -webkit-app-region: no-drag;
+
user-select: none;
}
diff --git a/extensions/cmd/panel.js b/extensions/cmd/panel.js
index 089bdfc7..96daf96d 100644
--- a/extensions/cmd/panel.js
+++ b/extensions/cmd/panel.js
@@ -297,113 +297,10 @@ function isCommandAvailable(cmd) {
return cmd.modes.includes(currentMajorMode);
}
-// ===== Custom Drag Handler =====
-// Native -webkit-app-region: drag is unreliable on transparent frameless panel
-// windows (especially macOS type:'panel' with modal blur). Use a custom drag
-// handler that moves the window via IPC, similar to how page.js handles dragging.
-
-let dragState = null; // { windowId, startScreenX, startScreenY, startPosX, startPosY }
-
-/**
- * Check if an element is interactive (should not trigger drag)
- */
-function isDragInteractive(el) {
- if (!el) return false;
- const tag = el.tagName?.toLowerCase();
- if (['input', 'textarea', 'button', 'a', 'select', 'label'].includes(tag)) return true;
- if (el.isContentEditable) return true;
- if (el.hasAttribute('data-no-drag')) return true;
- if (el.closest('[data-no-drag]')) return true;
- return false;
-}
-
-/**
- * Initialize custom window drag from visible UI areas (center-wrapper, chain indicator, etc.)
- * Drag starts immediately on mousedown (no hold delay) for quick repositioning.
- */
-async function initDrag() {
- let windowId = null;
-
- // Get window ID once at init
- try {
- windowId = await api.window.getWindowId();
- log('cmd:panel', 'Drag: got window ID', windowId);
- } catch (err) {
- log.error('cmd:panel', 'Drag: failed to get window ID', err);
- return;
- }
-
- // Drag starts on mousedown on any non-interactive visible element
- document.addEventListener('mousedown', (e) => {
- // Only left click, skip interactive elements
- if (e.button !== 0) return;
- if (isDragInteractive(e.target)) return;
- // Skip if clicking on transparent body background (not on a visible element)
- // Only allow drag from visible UI containers
- const visibleContainer = e.target.closest('.center-wrapper, #chain-indicator, #execution-state');
- if (!visibleContainer) return;
-
- // Prevent default immediately (before async call) to avoid text selection
- e.preventDefault();
-
- // Capture screen position synchronously
- const startScreenX = e.screenX;
- const startScreenY = e.screenY;
-
- // Get window position async, then start drag
- api.window.getPosition().then(pos => {
- if (!pos.success) return;
-
- dragState = {
- windowId,
- startScreenX,
- startScreenY,
- startPosX: pos.x,
- startPosY: pos.y
- };
-
- document.body.style.cursor = 'grabbing';
- }).catch(err => {
- log.error('cmd:panel', 'Drag: failed to get position', err);
- });
- });
-
- document.addEventListener('mousemove', (e) => {
- if (!dragState) return;
-
- // Safety: cancel if no button held
- if (e.buttons === 0) {
- dragState = null;
- document.body.style.cursor = '';
- return;
- }
-
- const dx = e.screenX - dragState.startScreenX;
- const dy = e.screenY - dragState.startScreenY;
- const newX = dragState.startPosX + dx;
- const newY = dragState.startPosY + dy;
-
- api.window.move(dragState.windowId, newX, newY);
- });
-
- document.addEventListener('mouseup', () => {
- if (dragState) {
- dragState = null;
- document.body.style.cursor = '';
- }
- });
-
- // Add cursor hint on the center-wrapper for discoverability
- const centerWrapper = document.querySelector('.center-wrapper');
- if (centerWrapper) {
- centerWrapper.style.cursor = 'grab';
- }
-
- log('cmd:panel', 'Custom drag handler initialized');
-}
-
-// Initialize drag handler
-initDrag();
+// ===== Window Drag =====
+// Window dragging is handled by the preload's unified drag system.
+// Hold the mouse button on any non-interactive area for ~150ms to start dragging.
+// No custom drag handler needed here -- the preload covers all windows.
// Window sizing constants
const COLLAPSED_HEIGHT = 60; // Just the command bar
diff --git a/notes/ux-rules.md b/notes/ux-rules.md
index 2a671417..0e86e5ef 100644
--- a/notes/ux-rules.md
+++ b/notes/ux-rules.md
@@ -23,13 +23,18 @@ When multiple interactions could trigger from the same gesture, follow this prio
## Window Dragging
Frameless windows can be dragged by click-and-hold anywhere on the window body.
+Dragging is handled by a single unified system in `preload.js` that runs in every
+window. Individual windows do NOT need their own drag code.
### Activation
| Parameter | Value | Purpose |
|-----------|-------|---------|
-| Hold delay | 300ms | Time before drag activates |
+| Hold delay | 150ms | Time before drag activates |
| Movement threshold | 5px | Cancel if mouse moves before delay |
+The window position is fetched eagerly on mousedown (overlapping with the hold
+timer) so that dragging begins instantly when the timer fires.
+
### Rules
**1. Text selection wins over dragging**
@@ -42,26 +47,34 @@ These never trigger drag:
- ``, `