Fix drag-and-drop files landing behind the panes in Compare (#818)

In Compare each pane renders into a sandboxed <iframe>. A file dropped on
a pane was handled by the iframe (browser default), so the browser loaded
the file *inside* the pane — appearing 'behind' the app — instead of
attaching it. The existing #chat-container drop handler never sees the
event because drag events don't bubble out of an iframe.

While a file drag is active in Compare, raise a single full-window drop
shield above the panes/iframes so the drop lands on the parent document,
then route the files into the shared composer (the same pending-files
pipeline the file picker and paste already use). Scoped to Compare via the
.compare-active class, so normal chat and the tool dropzones (gallery, RAG,
document editor, …) are unaffected.

Verified with a headless-Chromium integration test: synthetic file
dragover raises the shield, drop attaches the file to the composer, and
non-Compare mode is unaffected. Also ran node --check static/app.js.
This commit is contained in:
James Arslan
2026-06-02 04:14:59 +02:00
committed by GitHub
parent fd04ad353d
commit b3599d84f7

View File

@@ -3856,7 +3856,75 @@ function startOdysseusApp() {
e.preventDefault();
attachStrip.style.backgroundColor = '';
});
// ── Compare-mode file drop shield ──────────────────────────────────────────
// Compare reuses #chat-container, but each pane renders into a sandboxed
// <iframe>. Iframes swallow drag-and-drop events: a file dropped on a pane is
// handled by the iframe, not the parent, so the browser loads the file *inside
// the pane* ("behind" the app) instead of attaching it. The chatContainer drop
// handler above never sees it because the event doesn't bubble out of the frame.
//
// Fix: while a file drag is active in Compare, raise a single full-window shield
// that sits above every pane/iframe and becomes the drop target. The drop then
// lands on the parent document and we route the files into the shared composer
// (the same pending-files pipeline the picker and paste use). Scoped to Compare
// via the .compare-active class, so normal chat and the tool dropzones (gallery,
// RAG, document editor, …) are unaffected.
let _cmpDropShield = null;
const _isFileDrag = (e) => {
const types = e.dataTransfer && e.dataTransfer.types;
return !!types && Array.prototype.indexOf.call(types, 'Files') !== -1;
};
const _compareActive = () => {
const c = el('chat-container');
return !!c && c.classList.contains('compare-active');
};
const _showCmpShield = () => {
if (!_cmpDropShield) {
_cmpDropShield = document.createElement('div');
_cmpDropShield.id = 'compare-drop-shield';
_cmpDropShield.setAttribute('aria-hidden', 'true');
_cmpDropShield.style.cssText = 'position:fixed;inset:0;z-index:2147483646;' +
'display:none;align-items:center;justify-content:center;' +
'background:color-mix(in srgb, var(--accent, #0af) 16%, rgba(0,0,0,0.5));' +
'backdrop-filter:blur(2px);';
const _box = document.createElement('div');
_box.style.cssText = 'pointer-events:none;border:2px dashed rgba(255,255,255,0.9);' +
'border-radius:14px;padding:20px 28px;background:rgba(0,0,0,0.4);' +
'font:600 16px/1.4 system-ui,sans-serif;color:#fff;';
_box.textContent = 'Drop files to attach';
_cmpDropShield.appendChild(_box);
document.body.appendChild(_cmpDropShield);
}
_cmpDropShield.style.display = 'flex';
};
const _hideCmpShield = () => { if (_cmpDropShield) _cmpDropShield.style.display = 'none'; };
// Capture phase so we raise the shield before the pointer reaches an iframe.
window.addEventListener('dragenter', (e) => {
if (_isFileDrag(e) && _compareActive()) _showCmpShield();
}, true);
window.addEventListener('dragover', (e) => {
if (!_isFileDrag(e) || !_compareActive()) return;
e.preventDefault(); // mark as a valid drop target
if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy';
_showCmpShield();
}, true);
window.addEventListener('dragleave', (e) => {
// Hide only when the drag actually leaves the window (no relatedTarget).
if (_compareActive() && !e.relatedTarget) _hideCmpShield();
}, true);
window.addEventListener('dragend', _hideCmpShield, true);
window.addEventListener('drop', (e) => {
if (!_isFileDrag(e) || !_compareActive()) return;
e.preventDefault();
_hideCmpShield();
const files = Array.from(e.dataTransfer.files || []);
if (!files.length) return;
fileHandlerModule.addFiles(files);
fileHandlerModule.renderAttachStrip();
uiModule.showToast(`Added ${files.length} file${files.length > 1 ? 's' : ''} to attach`);
}, true);
// Load initial data
presetsModule.loadPresets(uiModule.showError);