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 <nesquena+hermes@gmail.com>
Co-authored-by: n <a@n>
This commit is contained in:
authored and GitHub committed 2026-08-16 23:45:51 +00:00
1 parent 53d96fa7d7
commit f1115355a6
3 files changed
+128 -1

No files matched your search

+2
View File
@@ -5,6 +5,8 @@
### Fixed ### 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) - **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) - **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)
+4 -1
View File
@@ -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. */ feels native to the WebUI rather than a one-off bridge UI. */
.kanban-modal-overlay{ .kanban-modal-overlay{
position:fixed;inset:0;background:rgba(7,12,19,.62);backdrop-filter:blur(6px); 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; z-index:1100;padding:24px;
} }
.kanban-modal-overlay[hidden]{display:none;} .kanban-modal-overlay[hidden]{display:none;}
@@ -6861,6 +6861,9 @@ main.main.showing-insights > #mainInsights{display:flex;overflow-y:auto;}
padding:18px 18px 16px; padding:18px 18px 16px;
color:var(--text); color:var(--text);
box-sizing:border-box; box-sizing:border-box;
max-height:calc(100vh - 48px);
max-height:calc(100dvh - 48px);
overflow-y:auto;
} }
:root:not(.dark) .kanban-modal{ :root:not(.dark) .kanban-modal{
background:linear-gradient(180deg,#fff,#f5f0e8); background:linear-gradient(180deg,#fff,#f5f0e8);
@@ -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()