Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 4 additions & 1 deletion static/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;}
Expand All @@ -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);
Expand Down
122 changes: 122 additions & 0 deletions tests/test_issue6906_kanban_modal_height_cap.py
Original file line number Diff line number Diff line change
@@ -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),
Comment thread
rodboev marked this conversation as resolved.
(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()
Loading