From f1115355a6fd13da0f9c8f25685a4175fc779445 Mon Sep 17 00:00:00 2001 From: Rod Boev Date: Sun, 16 Aug 2026 19:45:51 -0400 Subject: [PATCH] fix(#6906): keep kanban actions reachable in short windows (#7065) * fix(#6906): tighten Kanban modal viewport regression * fix(#6906): preserve modal centering fallbacks * docs(changelog): note the #6906 Kanban modal height-cap fix --------- Co-authored-by: nesquena-hermes Co-authored-by: n --- CHANGELOG.md | 2 + static/style.css | 5 +- .../test_issue6906_kanban_modal_height_cap.py | 122 ++++++++++++++++++ 3 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 tests/test_issue6906_kanban_modal_height_cap.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 6102c8ec..d896e215 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ ### Fixed +- **Kanban task and board dialogs stay usable in short windows.** On a short window (short-wide landscape, small laptop, or a phone in landscape) the Kanban create/edit-task and create-board dialogs were taller than the viewport with no height cap and no internal scroll, so the dialog overflowed both the top and bottom edges and its action buttons (Create / Save / Cancel) were unreachable. The dialogs now cap their height to the viewport (`calc(100dvh - 48px)`, with a `100vh` fallback) and scroll their content internally, and the overlay uses safe centering so the top stays reachable when the dialog is taller than the window. Normal-height dialogs are unchanged. Thanks @rodboev. (#6906) + - **A stale approval card no longer dead-ends with "Approval response not accepted."** On the local backend, clicking a dangerous-command approval card whose approval had already been resolved or cleared could leave the card stuck showing an error, because WebUI failed to match the resolved approval back to its producer (the agent core delivers the approval as a copy that carries a `request_id` but no `approval_id`, so the existing identity/`approval_id` matches both missed and a tokenless mirror was orphaned). WebUI now also matches on the core's per-approval `request_id`, so a resolved/stale card clears gracefully. (#4948) - **Docker single-container: an explicitly configured UID/GID is no longer lost, fixing a restart loop.** The entrypoint now probes the configured state-dir bind mount before the image-owned `/workspace` when auto-detecting the container UID/GID, and persists an `explicit` marker so a deliberately supplied `WANTED_UID`/`WANTED_GID` (e.g. `1024`) survives the root→user re-entry instead of being re-detected and overwritten. This fixes the documented single-container restart loop; the default stays `1024`, the two- and three-container topologies are unaffected, and a missing legacy marker falls back safely. Thanks @jorgejiro. (#7027) diff --git a/static/style.css b/static/style.css index 4ec0268a..28adc635 100644 --- a/static/style.css +++ b/static/style.css @@ -6848,7 +6848,7 @@ main.main.showing-insights > #mainInsights{display:flex;overflow-y:auto;} feels native to the WebUI rather than a one-off bridge UI. */ .kanban-modal-overlay{ position:fixed;inset:0;background:rgba(7,12,19,.62);backdrop-filter:blur(6px); - display:flex;align-items:center;justify-content:center; + display:flex;align-items:center;align-items:safe center;justify-content:center;overflow-y:auto; z-index:1100;padding:24px; } .kanban-modal-overlay[hidden]{display:none;} @@ -6861,6 +6861,9 @@ main.main.showing-insights > #mainInsights{display:flex;overflow-y:auto;} padding:18px 18px 16px; color:var(--text); box-sizing:border-box; + max-height:calc(100vh - 48px); + max-height:calc(100dvh - 48px); + overflow-y:auto; } :root:not(.dark) .kanban-modal{ background:linear-gradient(180deg,#fff,#f5f0e8); diff --git a/tests/test_issue6906_kanban_modal_height_cap.py b/tests/test_issue6906_kanban_modal_height_cap.py new file mode 100644 index 00000000..28a3c5c8 --- /dev/null +++ b/tests/test_issue6906_kanban_modal_height_cap.py @@ -0,0 +1,122 @@ +"""Regression coverage for reachable Kanban modal actions in short windows.""" + +import pytest + +from tests._layout_helpers import assert_layout_sane, assert_no_raw_i18n_keys +from tests._pytest_port import BASE + + +_BROWSER_ARGS = ["--no-sandbox", "--disable-dev-shm-usage"] +_LOCALES = ["en", "ru", "de"] + + +def _open_modal(page, modal_id, locale): + page.goto(BASE + "/", wait_until="domcontentloaded") + page.wait_for_function("() => typeof S !== 'undefined' && S._bootReady === true", timeout=10000) + page.wait_for_function( + "() => typeof setLocale === 'function' && typeof applyLocaleToDOM === 'function'", + timeout=10000, + ) + page.evaluate( + """([lang, id]) => { + setLocale(lang); + if (id === 'kanbanTaskModal') openKanbanCreate(); + else openKanbanCreateBoard(); + applyLocaleToDOM(); + const modal = document.getElementById(id); + if (!modal || modal.hidden) throw new Error(`${id} did not open`); + }""", + [locale, modal_id], + ) + + +def _assert_reachable(page, modal_id, *, should_scroll, max_height_inset): + result = page.evaluate( + """(id) => { + const overlay = document.getElementById(id); + const modal = overlay?.querySelector('.kanban-modal'); + const actions = modal?.querySelector('.kanban-modal-actions'); + if (!overlay || !modal || !actions) throw new Error('modal geometry is incomplete'); + if (modal.scrollHeight > modal.clientHeight) modal.scrollTop = modal.scrollHeight; + const viewport = {width: innerWidth, height: innerHeight}; + const overlayBox = overlay.getBoundingClientRect(); + const modalBox = modal.getBoundingClientRect(); + const actionsBox = actions.getBoundingClientRect(); + return { + viewport, + overlay: overlayBox.toJSON(), + modal: modalBox.toJSON(), + actions: actionsBox.toJSON(), + clientHeight: modal.clientHeight, + scrollHeight: modal.scrollHeight, + maxHeight: getComputedStyle(modal).maxHeight, + maxHeightPx: parseFloat(getComputedStyle(modal).maxHeight), + }; + }""", + modal_id, + ) + assert result["overlay"]["x"] == pytest.approx(0) + assert result["overlay"]["y"] == pytest.approx(0) + assert result["modal"]["bottom"] <= result["viewport"]["height"] + 1 + assert result["modal"]["top"] >= -1 + assert result["actions"]["bottom"] <= result["modal"]["bottom"] + 1 + assert result["actions"]["top"] >= result["modal"]["top"] - 1 + assert result["maxHeightPx"] == pytest.approx(result["viewport"]["height"] - max_height_inset, abs=1) + if should_scroll: + assert result["scrollHeight"] > result["clientHeight"] + assert result["maxHeight"] != "none" + else: + assert result["scrollHeight"] == pytest.approx(result["clientHeight"], abs=1) + assert_no_raw_i18n_keys(page, f"#{modal_id}") + assert_layout_sane(page, f"#{modal_id}") + + +@pytest.mark.parametrize("locale", _LOCALES) +@pytest.mark.parametrize( + "width,height,should_scroll,max_height_inset", + [ + (1280, 720, True, 48), + (1440, 800, True, 48), + (800, 450, True, 48), + (1920, 1080, False, 48), + (400, 800, True, 24), + (640, 480, True, 24), + ], +) +def test_task_modal_actions_reachable_across_viewports( + locale, width, height, should_scroll, max_height_inset +): + pw = pytest.importorskip("playwright.sync_api") + with pw.sync_playwright() as playwright: + browser = playwright.chromium.launch(headless=True, args=_BROWSER_ARGS) + page = browser.new_page(viewport={"width": width, "height": height}) + try: + _open_modal(page, "kanbanTaskModal", locale) + _assert_reachable( + page, + "kanbanTaskModal", + should_scroll=should_scroll, + max_height_inset=max_height_inset, + ) + finally: + page.close() + browser.close() + + +@pytest.mark.parametrize("locale", _LOCALES) +def test_board_modal_inherits_reachable_modal_geometry(locale): + pw = pytest.importorskip("playwright.sync_api") + with pw.sync_playwright() as playwright: + browser = playwright.chromium.launch(headless=True, args=_BROWSER_ARGS) + page = browser.new_page(viewport={"width": 800, "height": 450}) + try: + _open_modal(page, "kanbanBoardModal", locale) + _assert_reachable( + page, + "kanbanBoardModal", + should_scroll=True, + max_height_inset=48, + ) + finally: + page.close() + browser.close()