diff --git a/CHANGELOG.md b/CHANGELOG.md index 6102c8ec5d7..d896e2157a4 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 4ec0268ab1e..28adc635aa7 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 00000000000..28a3c5c85eb --- /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()