diff --git a/routes/email_routes.py b/routes/email_routes.py index 1dc621e..4243209 100644 --- a/routes/email_routes.py +++ b/routes/email_routes.py @@ -456,7 +456,8 @@ def setup_email_routes(): _IMAP_POOL = {} # account_id → (conn, last_used_at) _IMAP_IDLE_MAX = 60.0 _WARMING_READS = set() - _WARM_READ_LIMIT = 24 + _WARM_READ_LIMIT = 3 + _WARM_MAX_BYTES = 128 * 1024 _WARM_RECENT_SECONDS = 7 * 24 * 60 * 60 _pool_lock = _threading.Lock() @@ -1322,9 +1323,16 @@ def setup_email_routes(): epoch = 0 if epoch and now - epoch > _WARM_RECENT_SECONDS: continue + try: + size = int((em or {}).get("size") or 0) + except Exception: + size = 0 + if size > _WARM_MAX_BYTES: + continue ck = _read_cache_key(account_id, folder, uid, owner=owner) if _read_cache_get(ck) is not None or ck in _WARMING_READS: continue + _WARMING_READS.add(ck) selected.append((uid, ck)) if len(selected) >= _WARM_READ_LIMIT: break @@ -1334,8 +1342,8 @@ def setup_email_routes(): async def _warm(): for uid, ck in selected: if _read_cache_get(ck) is not None: + _WARMING_READS.discard(ck) continue - _WARMING_READS.add(ck) try: result = await _asyncio.to_thread(_read_email_sync, uid, folder, account_id, owner, False) if result and not result.get("error"): diff --git a/static/js/modalManager.js b/static/js/modalManager.js index 4ff6291..c28cfba 100644 --- a/static/js/modalManager.js +++ b/static/js/modalManager.js @@ -28,7 +28,7 @@ import { previewZoneAt, clearPreview, snapModalToZone } from './tileManager.js'; import { suspendDock, resumeDock, clearRightDock, applyEdgeDock } from './modalSnap.js'; -const _state = new Map(); // id -> { restoreFn, closeFn, railBtnId, isMinimized } +const _state = new Map(); // id -> { restoreFn, closeFn, railBtnId, isMinimized, restoreMinHeight } const _rememberedDockKey = (id) => `odysseus-modal-remembered-dock-${id}`; function _rememberDock(id, side) { @@ -73,6 +73,26 @@ function _emitModalOpened(id, modal) { } catch (_) {} } +function _captureRestoreHeight(modal, state) { + if (!modal || !state) return; + const content = modal.querySelector('.modal-content'); + if (!content) return; + const rect = content.getBoundingClientRect(); + if (!rect || rect.height < 120) return; + const maxHeight = Math.max(180, window.innerHeight - 24); + state.restoreMinHeight = `${Math.round(Math.min(rect.height, maxHeight))}px`; +} + +function _applyRestoreHeight(modal, state) { + if (!modal || !state?.restoreMinHeight) return; + const content = modal.querySelector('.modal-content'); + if (!content) return; + const maxHeight = Math.max(180, window.innerHeight - 24); + const requested = parseInt(state.restoreMinHeight, 10); + const height = Number.isFinite(requested) ? Math.min(requested, maxHeight) : null; + if (height) content.style.minHeight = `${height}px`; +} + function _setBadge(btnIds, on) { if (!btnIds) return; const ids = Array.isArray(btnIds) ? btnIds : [btnIds]; @@ -1109,6 +1129,7 @@ export function register(id, { restoreFn, closeFn, railBtnId, sidebarBtnId, labe closeFn: closeFn || (() => {}), btnIds, isMinimized: false, + restoreMinHeight: '', }); // Auto-stack: whichever modal becomes visible last sits on top of any // already-open modals. The various tool open() functions (gallery, @@ -1188,6 +1209,7 @@ export function minimize(id) { // and let the chip drive restore/close via the registered functions. const modal = document.getElementById(id); if (modal) { + _captureRestoreHeight(modal, s); // If this window is edge-docked (right/left), SUSPEND the dock: release // the body push so the chat returns to full width while the window is // minimized, but keep the dock so restoring the chip snaps it back in. @@ -1218,6 +1240,7 @@ export function restore(id) { if (modal) { modal.classList.remove('hidden', 'modal-minimized'); modal.style.display = ''; + _applyRestoreHeight(modal, s); // Surface above any already-open tool window — restoring from the dock // should bring this tool to the front, not leave it stuck behind one with // a higher static z-index. diff --git a/static/js/ui.js b/static/js/ui.js index dacd804..7ce7588 100644 --- a/static/js/ui.js +++ b/static/js/ui.js @@ -5,10 +5,13 @@ */ import themeModule from './theme.js'; +import * as Modals from './modalManager.js'; let toastEl = null; let autoScrollEnabled = true; let hoveredToggleCard = null; +let hoveredToggleWindow = null; +let hoveredDockChip = null; // Smooth scroll state let _scrollRafId = null; @@ -19,25 +22,92 @@ function _isTextEditingTarget(target) { return !!(el && el.closest('input, textarea, select, [contenteditable="true"], [contenteditable=""]')); } +const SPACE_CARD_SELECTOR = [ + '#email-lib-modal .doclib-card', + '#doclib-modal .doclib-card', + '#memory-modal .doclib-card', + '#tasks-modal .task-card', + '#tasks-modal .task-log-row', + '#research-overlay [data-job-id]', + '#cookbook-modal .doclib-card', + '.email-reader-tab-modal .doclib-card', + '.email-window-modal .doclib-card', +].join(', '); + +const SPACE_BLOCKED_SELECTOR = [ + 'button', + 'a', + 'input', + 'textarea', + 'select', + '[contenteditable="true"]', + '[contenteditable=""]', + '.recipient-chip', + '.doclib-card-dropdown', + '.email-card-dropdown', + '.task-log-row-actions', + '.modal-header', +].join(', '); + +function _visibleModalForSpace(win) { + const modal = win?.closest?.('.modal[id]'); + if (!modal || modal.classList.contains('hidden') || modal.classList.contains('modal-minimized')) return null; + return modal; +} + +function _isSpaceVisible(el) { + if (!el || !document.contains(el)) return false; + if (el.closest?.('.modal.hidden, .modal.modal-minimized, [hidden]')) return false; + return true; +} + +function _spaceWindowId(win) { + if (!win || !document.contains(win)) return null; + const modal = _visibleModalForSpace(win); + if (modal && Modals.isRegistered(modal.id)) return modal.id; + if (win.closest?.('.doc-editor-pane') && Modals.isRegistered('doc-panel') && !Modals.isMinimized('doc-panel')) return 'doc-panel'; + return null; +} + function _initHoverCardSpaceToggle() { if (document._odysseusHoverCardSpaceToggle) return; document._odysseusHoverCardSpaceToggle = true; document.addEventListener('pointerover', (e) => { - const card = e.target?.closest?.('#email-lib-modal .doclib-card, #doclib-modal .doclib-card, .email-reader-tab-modal .doclib-card, .email-window-modal .doclib-card'); + const chip = e.target?.closest?.('.minimized-dock-chip[data-modal-id]'); + if (chip) hoveredDockChip = chip; + const card = e.target?.closest?.(SPACE_CARD_SELECTOR); if (card) hoveredToggleCard = card; + const win = e.target?.closest?.('.modal:not(.hidden):not(.modal-minimized) .modal-content, .doc-editor-pane'); + if (win) hoveredToggleWindow = win; }, true); document.addEventListener('pointerout', (e) => { - if (!hoveredToggleCard) return; const next = e.relatedTarget; - if (!next || !hoveredToggleCard.contains(next)) hoveredToggleCard = null; + if (hoveredDockChip && (!next || !hoveredDockChip.contains(next))) hoveredDockChip = null; + if (hoveredToggleCard && (!next || !hoveredToggleCard.contains(next))) hoveredToggleCard = null; + if (hoveredToggleWindow && (!next || !hoveredToggleWindow.contains(next))) hoveredToggleWindow = null; }, true); document.addEventListener('keydown', (e) => { - if (e.code !== 'Space' || e.repeat || !hoveredToggleCard || !document.contains(hoveredToggleCard)) return; + if (e.code !== 'Space' || e.repeat) return; if (_isTextEditingTarget(e.target)) return; - const blocked = e.target?.closest?.('button, a, input, textarea, select, [contenteditable="true"], [contenteditable=""], .recipient-chip, .doclib-card-dropdown, .email-card-dropdown'); + const blocked = e.target?.closest?.(SPACE_BLOCKED_SELECTOR); if (blocked) return; + if (hoveredToggleCard && _isSpaceVisible(hoveredToggleCard)) { + e.preventDefault(); + hoveredToggleCard.click(); + return; + } + if (hoveredDockChip && document.contains(hoveredDockChip)) { + const id = hoveredDockChip.dataset.modalId; + if (id && Modals.isRegistered(id)) { + e.preventDefault(); + Modals.restore(id); + } + return; + } + const id = _spaceWindowId(hoveredToggleWindow); + if (!id) return; e.preventDefault(); - hoveredToggleCard.click(); + Modals.minimize(id); }, true); }