From 185667954cce5ab7ad1491a44e42e97197dd0cd9 Mon Sep 17 00:00:00 2001 From: Lukas Parsons Date: Tue, 7 Jul 2026 11:16:26 -0400 Subject: [PATCH] =?UTF-8?q?fix:=20zoom-aware=20window=20constraints=20?= =?UTF-8?q?=E2=80=94=20prevent=20modals=20overflowing=20viewport=20after?= =?UTF-8?q?=20ui-scale=20change?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CSS: added comprehensive zoom compensation for ALL modal-content variants (memory, tasks, preset, doclib, gallery, notes-pane, research, compare, email-lib) and a safety net for user-resized windows carrying inline height styles. JS (windowResize.js): _uiZoom() helper reads computed zoom from :root. Restore path divides saved localStorage dimensions by zoom factor. Resize-move viewport clamps use zoom-adjusted vh/vw so dragging never pushes the window beyond the visible area. Also added docs/improvement-tasks.md as the Ulysses task board. --- docs/improvement-tasks.md | 48 +++++++++++++++++++++++++++++++++++++++ static/js/windowResize.js | 21 +++++++++++++---- static/style.css | 44 +++++++++++++++++++++++++++++++---- 3 files changed, 104 insertions(+), 9 deletions(-) create mode 100644 docs/improvement-tasks.md diff --git a/docs/improvement-tasks.md b/docs/improvement-tasks.md new file mode 100644 index 00000000..02bf96b7 --- /dev/null +++ b/docs/improvement-tasks.md @@ -0,0 +1,48 @@ +# Ulysses Improvement Tasks + +Last updated: 2026-07-07 + +## Priority: HIGH + +- [ ] **Floating windows overflow viewport after zoom/font changes — unreachable controls** + `:root.ui-scale-125 { zoom: 1.25 }` renders all content 25% larger, but `window.innerHeight` doesn't change with `zoom` (CSS rendering only, not DOM). The `windowResize.js` saved-size restore at line 224 clamps to `window.innerHeight`, which after zoom makes windows 1.25× the visible viewport — pushing headers, close buttons, and top resize handles off-screen. The user literally can't close or move them. Fix: (a) divide restored height by zoom factor in the restore path; (b) add a general safety net — any `.modal-content` on open should clamp to `calc(100dvh / var(--ui-zoom, 1) - 40px)`. Also `windowResize.js` line 134 `if (top + height > vh)` needs zoom-aware `vh`. Current zoom compensation rules (line 181-196) only cover `.modal-content`, `.cal-modal-content`, `.settings-modal-content`, `#theme-popup`, `#cookbook-modal`, and `.pdf-export-overlay` — many windows lack the divisor. + +- [ ] **Fix manage_documents tool schema — missing "read" action** + The `manage_documents` function-calling schema in `tool_schemas.py` has `"enum": ["list", "delete", "tidy"]` but the handler in `document_tools.py` supports `"read", "view", "open", "get"`. The model can't read documents through function calling. Fix: add read/view/open/get to enum, add `document_id`/`offset`/`limit` params, update description to mention reading. + +- [ ] **Add "Send to Chat" button to document editor** + Button left of the Save button in `#doc-actions-footer`. Sends a document reference (title + doc_id + snippet) to the active chat, similar to how Cursor injects file references. Must NOT paste full content — just a reference the agent can resolve. Backend needs an endpoint or the existing `open_panel: documents` UI action may be enough. Requires: `POST /api/document//reference` or use existing `ui_control` with a new action. Frontend: insert button in `document.js` `_buildPaneHTML()` at line ~4598, wire JS handler. + +## Priority: MEDIUM + +- [ ] **Bolster Library/Documents CRUD via MCP or native tools** + Current document tools are solid but disjointed from agent-mode workflow. The agent has filesystem access (`read_file`, `write_file`, `edit_file`, `glob`, `ls`, `grep`, `bash`) but these tools operate on disk files, not Ulysses library documents. Need to bridge the gap: the agent should be able to list, read, grep, and edit library documents as naturally as files. Consider: + - A unified `read_document` tool distinct from `manage_documents` (which is an admin-style multi-action tool) + - A `grep_documents` or `search_documents` tool for semantic/fulltext search + - Auto-inject active document content into agent context when relevant + +- [ ] **First-party document support in agent mode** + Documents feel "disjointed from the agent." The agent creates documents fine but can't easily discover or reference existing ones contextually. Ideas: + - "Agent workspace" awareness — automatically surface relevant documents based on chat context + - Document references in chat that are clickable and auto-expand + - The "Send to Chat" button (HIGH priority) is step 1; step 2 is making the agent proactively aware of library contents + +- [ ] **Improve `create_document` schema description** + Current description is verbose and overlaps with `edit_file`/`write_file` disambiguation. Models sometimes use `create_document` for disk files. Add a clear leading signal: "DOCUMENT PANEL (not disk)" prefix like the existing "FILESYSTEM TOOL (not memory)" pattern. + +## Priority: LOW + +- [ ] **Document library: batch operations** + Multi-select, bulk delete, bulk export. Currently only single-document operations. + +- [ ] **Document version diff view** + The version history panel shows versions but doesn't offer a side-by-side diff view between versions. + +- [ ] **Auto-language detection on document open** + The editor auto-detects language on typing but not on initial document open from the library. Opening a Python file saved as "markdown" should suggest the language switch. + +--- + +## Completed + + \ No newline at end of file diff --git a/static/js/windowResize.js b/static/js/windowResize.js index 57828920..c30fcecb 100644 --- a/static/js/windowResize.js +++ b/static/js/windowResize.js @@ -29,6 +29,14 @@ const MIN_H = 200; // within EDGE px of the window border (close buttons, sliders, inputs, links). const INTERACTIVE = 'button, input, select, textarea, a, [contenteditable=""], [contenteditable="true"]'; +// `zoom` (ui-scale-*) makes window.innerHeight misleading — CSS renders +// content larger than the viewport, so a window saved at 700px pre-zoom +// renders at 875px after 1.25x. Divide constraints by the zoom factor. +function _uiZoom() { + const z = parseFloat(getComputedStyle(document.documentElement).zoom) || 1; + return z > 0 ? z : 1; +} + export function makeWindowResizable(content, options = {}) { if (!content) return; const modal = options.modal || null; @@ -128,10 +136,13 @@ export function makeWindowResizable(content, options = {}) { if (width < minW) { if (active.l) left = startRect.left + (startRect.width - minW); width = minW; } if (height < minH) { if (active.t) top = startRect.top + (startRect.height - minH); height = minH; } // Keep the window on-screen and never larger than the viewport. + // Zoom-aware: divide by ui-zoom so clamped dimensions fit the visible area. + const z = _uiZoom(); + const vhZ = vh / z, vwZ = vw / z; if (active.l && left < 0) { width += left; left = 0; } if (active.t && top < 0) { height += top; top = 0; } - if (left + width > vw) width = Math.max(minW, vw - left); - if (top + height > vh) height = Math.max(minH, vh - top); + if (left + width > vwZ) width = Math.max(minW, vwZ - left); + if (top + height > vhZ) height = Math.max(minH, vhZ - top); content.style.left = left + 'px'; content.style.top = top + 'px'; content.style.width = width + 'px'; @@ -220,8 +231,10 @@ export function makeWindowResizable(content, options = {}) { try { const saved = JSON.parse(localStorage.getItem(storageKey) || 'null'); if (saved && saved.w && saved.h) { - const w = Math.max(minW, Math.min(saved.w, window.innerWidth)); - const h = Math.max(minH, Math.min(saved.h, window.innerHeight)); + const z = _uiZoom(); + const vwZ = window.innerWidth / z, vhZ = window.innerHeight / z; + const w = Math.max(minW, Math.min(saved.w, vwZ)); + const h = Math.max(minH, Math.min(saved.h, vhZ)); content.style.width = w + 'px'; content.style.height = h + 'px'; content.style.maxWidth = 'none'; diff --git a/static/style.css b/static/style.css index d5de9a46..b0b6d622 100644 --- a/static/style.css +++ b/static/style.css @@ -177,11 +177,43 @@ html { (a catch-22 — you can't reach the control to turn the size back down). Divide each max-height by the same factor to keep the original on-screen footprint. Desktop only — the mobile `!important` full-sheet rules win on small screens - and stay top-anchored, so their headers are already visible. */ -:root.ui-scale-125 .modal-content { max-height: calc(85dvh / 1.25); } -:root.ui-scale-125 .cal-modal-content { max-height: calc(88dvh / 1.25); } -:root.ui-scale-125 .settings-modal-content { max-height: calc(85dvh / 1.25); } -:root.ui-scale-125 #theme-popup { max-height: min(calc(85dvh / 1.25), 480px); } + and stay top-anchored, so their headers are already visible. + + Comprehensive coverage: every modal-content variant, standalone panes, + and user-resized windows that carry inline height styles. Users who + changed zoom/font settings saw headers and close buttons pushed + off-screen because only a subset of windows had the divisor. */ +:root.ui-scale-125 .modal-content, +:root.ui-scale-125 .cal-modal-content, +:root.ui-scale-125 .settings-modal-content, +:root.ui-scale-125 .memory-modal-content, +:root.ui-scale-125 .tasks-modal-content, +:root.ui-scale-125 .preset-modal-content, +:root.ui-scale-125 .doclib-modal-content, +:root.ui-scale-125 .gallery-modal-content, +:root.ui-scale-125 .notes-pane, +:root.ui-scale-125 #research-overlay .modal-content, +:root.ui-scale-125 #research-overlay, +:root.ui-scale-125 #compare-model-overlay .modal-content, +:root.ui-scale-125 #compare-model-overlay, +:root.ui-scale-125 #email-lib-modal .modal-content, +:root.ui-scale-125 #doclib-modal .modal-content { + max-height: calc(85dvh / 1.25); +} +/* Any modal-content carrying an inline height style (user-resized) gets + viewport-clamped — this catches windows whose saved localStorage size + was set before a zoom change and would otherwise render off-screen. */ +:root.ui-scale-125 .modal-content[style*="height"], +:root.ui-scale-125 .cal-modal-content[style*="height"], +:root.ui-scale-125 .settings-modal-content[style*="height"], +:root.ui-scale-125 .memory-modal-content[style*="height"], +:root.ui-scale-125 .tasks-modal-content[style*="height"], +:root.ui-scale-125 .preset-modal-content[style*="height"], +:root.ui-scale-125 .doclib-modal-content[style*="height"], +:root.ui-scale-125 .gallery-modal-content[style*="height"], +:root.ui-scale-125 .notes-pane[style*="height"] { + max-height: calc(100dvh / 1.25 - 16px) !important; +} /* Cookbook is the one modal that set its height inline (94vh), which beat the .modal-content compensation above and overflowed the viewport at 1.25x (header + close button pushed off-screen). Own its height here so the same @@ -194,6 +226,8 @@ html { 1.25x compensation applies. */ .pdf-export-overlay .modal-content { max-height: 86vh; } :root.ui-scale-125 .pdf-export-overlay .modal-content { max-height: calc(86dvh / 1.25); } +/* Theme popup: cap at a reasonable fixed height regardless of zoom */ +:root.ui-scale-125 #theme-popup { max-height: min(calc(85dvh / 1.25), 480px); } /* ── Background Patterns ── */