From f1b2a2ab7549ab4cfa80a889b2be3bfa3c71454a Mon Sep 17 00:00:00 2001 From: Codeman maintainer Date: Fri, 13 Nov 2026 14:44:22 +0100 Subject: [PATCH] refactor(web): one detach-and-drag routine for the monitor and subagents panels toggleMonitorDetach / toggleSubagentsDetach and their two drag setups were the same 97 lines twice, differing only in three element ids. They now call _togglePanelDetach(panelId, headerId, detachBtnId) and _setupPanelDrag(panelId, headerId); the two public names stay, since index.html calls them. Checked in a browser: each panel detaches, drags by its header inside the viewport, docks back, and after a second detach one drag moves it exactly once (the header's previous listeners are still replaced). Co-Authored-By: Claude Fable 5.2 --- src/web/public/panels-ui.js | 122 +++++------------------------------- 1 file changed, 17 insertions(+), 105 deletions(-) diff --git a/src/web/public/panels-ui.js b/src/web/public/panels-ui.js index c50317aa..ca41c1f4 100644 --- a/src/web/public/panels-ui.js +++ b/src/web/public/panels-ui.js @@ -1388,12 +1388,21 @@ Object.assign(CodemanApp.prototype, { }, // ═══════════════════════════════════════════════════════════════ - // Monitor Panel Detach & Drag + // Monitor and Subagents Panel Detach & Drag // ═══════════════════════════════════════════════════════════════ toggleMonitorDetach() { - const panel = document.getElementById('monitorPanel'); - const detachBtn = document.getElementById('monitorDetachBtn'); + this._togglePanelDetach('monitorPanel', 'monitorPanelHeader', 'monitorDetachBtn'); + }, + + toggleSubagentsDetach() { + this._togglePanelDetach('subagentsPanel', 'subagentsPanelHeader', 'subagentsDetachBtn'); + }, + + /** Float a bottom panel as a draggable window, or dock it back. */ + _togglePanelDetach(panelId, headerId, detachBtnId) { + const panel = document.getElementById(panelId); + const detachBtn = document.getElementById(detachBtnId); if (panel.classList.contains('detached')) { // Re-attach to bottom @@ -1414,111 +1423,14 @@ Object.assign(CodemanApp.prototype, { detachBtn.innerHTML = '⊞'; // Attach icon (squared plus - dock back) detachBtn.title = 'Attach panel'; } - // Setup drag functionality - this.setupMonitorDrag(); + this._setupPanelDrag(panelId, headerId); } }, - setupMonitorDrag() { - const panel = document.getElementById('monitorPanel'); - const header = document.getElementById('monitorPanelHeader'); - - if (!panel || !header) return; - - let isDragging = false; - let startX, startY, startLeft, startTop; - - const onStart = (e) => { - // Only drag from header, not from buttons - if (e.target.closest('button')) return; - if (!panel.classList.contains('detached')) return; - - isDragging = true; - const coords = getEventCoords(e); - startX = coords.clientX; - startY = coords.clientY; - const rect = panel.getBoundingClientRect(); - startLeft = rect.left; - startTop = rect.top; - - document.addEventListener('mousemove', onMove); - document.addEventListener('mouseup', onEnd); - document.addEventListener('touchmove', onMove, { passive: false }); - document.addEventListener('touchend', onEnd); - e.preventDefault(); - }; - - const onMove = (e) => { - if (!isDragging) return; - - const coords = getEventCoords(e); - const dx = coords.clientX - startX; - const dy = coords.clientY - startY; - - let newLeft = startLeft + dx; - let newTop = startTop + dy; - - // Keep within viewport bounds - const rect = panel.getBoundingClientRect(); - newLeft = Math.max(0, Math.min(window.innerWidth - rect.width, newLeft)); - newTop = Math.max(0, Math.min(window.innerHeight - rect.height, newTop)); - - panel.style.left = newLeft + 'px'; - panel.style.top = newTop + 'px'; - }; - - const onEnd = () => { - isDragging = false; - document.removeEventListener('mousemove', onMove); - document.removeEventListener('mouseup', onEnd); - document.removeEventListener('touchmove', onMove); - document.removeEventListener('touchend', onEnd); - }; - - // Remove existing listeners before adding new ones - header.removeEventListener('mousedown', header._dragHandler); - header.removeEventListener('touchstart', header._touchDragHandler); - header._dragHandler = onStart; - header._touchDragHandler = onStart; - header.addEventListener('mousedown', onStart); - header.addEventListener('touchstart', onStart, { passive: false }); - }, - - // ═══════════════════════════════════════════════════════════════ - // Subagents Panel Detach & Drag - // ═══════════════════════════════════════════════════════════════ - - toggleSubagentsDetach() { - const panel = document.getElementById('subagentsPanel'); - const detachBtn = document.getElementById('subagentsDetachBtn'); - - if (panel.classList.contains('detached')) { - // Re-attach to bottom - panel.classList.remove('detached'); - panel.style.top = ''; - panel.style.left = ''; - panel.style.width = ''; - panel.style.height = ''; - if (detachBtn) { - detachBtn.innerHTML = '⧉'; // Detach icon - detachBtn.title = 'Detach panel'; - } - } else { - // Detach as floating window - panel.classList.add('detached'); - panel.classList.add('open'); // Ensure it's visible - if (detachBtn) { - detachBtn.innerHTML = '⊞'; // Attach icon - detachBtn.title = 'Attach panel'; - } - // Setup drag functionality - this.setupSubagentsDrag(); - } - }, - - setupSubagentsDrag() { - const panel = document.getElementById('subagentsPanel'); - const header = document.getElementById('subagentsPanelHeader'); + /** Let a detached panel be dragged by its header, kept inside the viewport. */ + _setupPanelDrag(panelId, headerId) { + const panel = document.getElementById(panelId); + const header = document.getElementById(headerId); if (!panel || !header) return;