From 11c07de2582393e2bdddeaf3ecb6e5e7aecd65a5 Mon Sep 17 00:00:00 2001 From: Harshit Singh Bhandari Date: Tue, 19 May 2026 14:38:24 +0530 Subject: [PATCH] fix(web): bound project page wrapper so dashboard body scrolls (closes #1923) (#1929) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(web): make dashboard body scroll as one unit (closes #1923) PR #1925 added internal scroll on .done-bar__cards, but the desired UX is page-level scroll — kanban above, Done/Terminated below, scrolling as one unit (the pre-#927 behavior). Internal scroll on the cards container produced a separate scrollable region inside an otherwise locked layout. Root cause: af2af115 (#927, Warm Terminal design refresh) added `flex: 1; min-height: 0` to .kanban-board-wrap. That made the wrap greedily consume all remaining body height, so body content always equalled body height exactly — `.dashboard-main__body { overflow-y: auto }` never triggered. Drop .kanban-board-wrap from the combined flex:1 rule. It now takes its content size (= the inner .kanban-board's fixed `calc(100vh - 240px)`). When done-bar expands below, body content exceeds body height and the existing overflow-y: auto produces natural page scroll. .board-wrapper (empty state + loading skeleton) keeps flex:1 — it relies on filling space to vertically center its CTA. Also revert the now-unneeded .done-bar__cards max-height + overflow-y + scrollbar styling from #1925. Co-Authored-By: Claude Opus 4.7 (1M context) * fix(web): make
a flex column so body's overflow-y actually scrolls The body-level scroll has been broken since af2af115 (#927). The JSX is
, but neither .dashboard-main nor any Tailwind class on
sets display: flex. So .dashboard-main__body's `flex: 1` and `overflow-y: auto` never functioned — body simply took content height, never had overflow to scroll, and
's overflow: hidden clipped it. Pre-regression markup was a
with `overflow-y-auto` directly on it (the dashboard container itself scrolled). The af2af115 refactor moved overflow to a nested body and assumed main was a flex column — but never declared it. Add `flex flex-col` to
in both Dashboard.tsx and the projects/[projectId]/loading.tsx skeleton. Body's flex:1 now correctly fills
's height, overflow-y: auto activates when body content exceeds, and the kanban-board-wrap fix from f7f843cd lets that overflow actually happen. Co-Authored-By: Claude Opus 4.7 (1M context) * fix(web): bound project page wrapper + lock kanban/done from flex-shrink The real clipper was .dashboard-shell--desktop (height 900px, overflow: hidden), not anything inside Dashboard. The project page wrapper (
) was a non-flex block with min-height: 100vh, so .dashboard-main-- desktop grew to its content height (~1411px) instead of being bounded by the shell's 900px. As a result .dashboard-main__body had overflow-y: auto but also grew to content height (~1308px), so it never overflowed and no scrollbar appeared anywhere. Verified in devtools: with this fix .dashboard-main__body clientHeight is 797 and scrollHeight is 1308 — body becomes the single vertical scroll container, kanban above and Done section below, scrolling as one unit. Changes: 1. /projects/[projectId]/page.tsx wrapper: 'flex-1 min-h-screen' -> 'flex min-h-0 min-w-0 flex-1'. Makes the wrapper a bounded flex item so .dashboard-main--desktop inherits a constrained height from the shell row instead of growing past it. 2. globals.css: flex-shrink: 0 on .kanban-board-wrap and .done-bar. Prevents body's flex column from crushing the kanban to make room for the expanded done section. Both children now assert their content size; body content exceeds body height; overflow-y: auto activates. Root cause credit to harshitsinghbhandari — found via devtools after my three prior attempts (#1925 internal scroll on cards, plus the two prior commits on this branch) all addressed the wrong layer of the layout tree. Co-Authored-By: Claude Opus 4.7 (1M context) * fix(web): add flex-col to project page wrapper + regression test Follow-up to 110a4225 — the wrapper was 'flex min-h-0 min-w-0 flex-1 ...' but missing flex-col. Because Dashboard renders multiple siblings when inside ProjectLayoutClient (UpdateBanner, ConnectionBar, mainPanel, bottomSheet), any visible UpdateBanner becomes a horizontal flex sibling next to the dashboard shell instead of stacking above it. ConnectionBar is fixed-positioned, bottomSheet is overlay — but UpdateBanner is in normal flow and exposes the row-vs-column bug whenever it's visible. Add flex-col so siblings stack vertically. Also add a regression test in page.test.tsx that the Dashboard parent has the bounded-flex classes (flex, min-h-0, min-w-0, flex-1) and no longer has min-h-screen, so future refactors can't silently re-break the dashboard's scroll container. Credit to harshitsinghbhandari for catching the missing flex-col and writing the test. Co-Authored-By: Claude Opus 4.7 (1M context) * fix(web): wrap loading skeleton in same bounded flex container Addresses greptile P1 review feedback on PR #1929. The project page (page.tsx) wraps in
so .dashboard-main--desktop inherits the project shell's 900px constraint instead of growing past it. The loading skeleton (loading.tsx) renders .dashboard-main--desktop directly as the root, landing in the same layout slot WITHOUT that constraint — so during a slow load the skeleton would still overflow the shell and reproduce the original clip behavior. Wrap the skeleton in the same outer bounded flex container as page.tsx for layout parity. Co-Authored-By: Claude Opus 4.7 (1M context) --------- Co-authored-by: Claude Opus 4.7 (1M context) --- .changeset/fix-done-bar-scroll.md | 2 +- packages/web/src/app/globals.css | 21 ++---- .../src/app/projects/[projectId]/loading.tsx | 72 ++++++++++--------- .../app/projects/[projectId]/page.test.tsx | 27 +++++++ .../web/src/app/projects/[projectId]/page.tsx | 2 +- packages/web/src/components/Dashboard.tsx | 2 +- 6 files changed, 74 insertions(+), 52 deletions(-) diff --git a/.changeset/fix-done-bar-scroll.md b/.changeset/fix-done-bar-scroll.md index edac04f39..f049da4a8 100644 --- a/.changeset/fix-done-bar-scroll.md +++ b/.changeset/fix-done-bar-scroll.md @@ -2,4 +2,4 @@ "@aoagents/ao-web": patch --- -Fix Done/Terminated section on the dashboard not scrolling. The expanded cards grid now has an internal scroll container (mirroring the kanban column body pattern), so older terminated sessions are reachable when many accumulate. +Fix Done/Terminated section on the dashboard not scrolling. The dashboard body now scrolls as a single page — kanban above, Done/Terminated below — so older terminated sessions are reachable when many accumulate. Restores the pre-Warm-Terminal-refresh behavior by dropping the `flex: 1` that was making `.kanban-board-wrap` greedily consume all remaining body height and defeat the body's `overflow-y: auto`. diff --git a/packages/web/src/app/globals.css b/packages/web/src/app/globals.css index d85a5c6c0..f7e239966 100644 --- a/packages/web/src/app/globals.css +++ b/packages/web/src/app/globals.css @@ -1545,7 +1545,6 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { padding: 0 16px 16px; } -.dashboard-main__body .kanban-board-wrap, .dashboard-main__body .board-wrapper { flex: 1; min-height: 0; @@ -5185,6 +5184,7 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { position: relative; margin-top: 14px; overflow-x: auto; + flex-shrink: 0; } .board-section-head { @@ -5803,6 +5803,10 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { /* ── Done / Terminated collapsible bar ────────────────────────────────── */ +.done-bar { + flex-shrink: 0; +} + .done-bar__toggle { display: flex; align-items: center; @@ -5858,21 +5862,6 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 8px; margin-top: 12px; - /* 320px ≈ dashboard header + subhead + body padding + done-bar toggle/margins. - Mirrors the .kanban-board viewport-anchored height pattern (line 5107). */ - max-height: calc(100vh - 320px); - overflow-y: auto; -} - -.done-bar__cards::-webkit-scrollbar { - width: 4px; -} -.done-bar__cards::-webkit-scrollbar-track { - background: transparent; -} -.done-bar__cards::-webkit-scrollbar-thumb { - background: var(--color-scrollbar); - border-radius: 0; } /* ── Done card (compact grid card) ──────────────────────────────────── */ diff --git a/packages/web/src/app/projects/[projectId]/loading.tsx b/packages/web/src/app/projects/[projectId]/loading.tsx index ffeee105c..7df4da536 100644 --- a/packages/web/src/app/projects/[projectId]/loading.tsx +++ b/packages/web/src/app/projects/[projectId]/loading.tsx @@ -1,40 +1,46 @@ export default function ProjectRouteLoading() { return ( -
- - -
-
+
); } diff --git a/packages/web/src/app/projects/[projectId]/page.test.tsx b/packages/web/src/app/projects/[projectId]/page.test.tsx index bb1ab40f6..8ba4b4d21 100644 --- a/packages/web/src/app/projects/[projectId]/page.test.tsx +++ b/packages/web/src/app/projects/[projectId]/page.test.tsx @@ -36,6 +36,33 @@ vi.mock("@/components/Dashboard", () => ({ import ProjectPage from "./page"; describe("ProjectPage", () => { + it("renders the dashboard inside a bounded flex item owned by the project shell", async () => { + hoisted.getProjectRouteDataMock.mockResolvedValue({ + projectId: "project-1", + project: { id: "project-1" }, + projects: [{ id: "project-1", name: "Project 1" }], + degradedProject: null, + }); + hoisted.getDashboardPageDataMock.mockResolvedValue({ + sessions: [], + selectedProjectId: "project-1", + projectName: "Project 1", + projects: [{ id: "project-1", name: "Project 1" }], + orchestrators: [], + attentionZones: "simple", + }); + + render(await ProjectPage({ params: Promise.resolve({ projectId: "project-1" }) })); + + expect(screen.getByTestId("dashboard").parentElement).toHaveClass( + "flex", + "min-h-0", + "min-w-0", + "flex-1", + ); + expect(screen.getByTestId("dashboard").parentElement).not.toHaveClass("min-h-screen"); + }); + it("renders degraded project state when the project is degraded", async () => { hoisted.getProjectRouteDataMock.mockResolvedValue({ projectId: "broken", diff --git a/packages/web/src/app/projects/[projectId]/page.tsx b/packages/web/src/app/projects/[projectId]/page.tsx index 39d317391..92582e82a 100644 --- a/packages/web/src/app/projects/[projectId]/page.tsx +++ b/packages/web/src/app/projects/[projectId]/page.tsx @@ -29,7 +29,7 @@ export default async function ProjectPage(props: { const pageData = await getDashboardPageData(projectId); return ( -
+
-
+

Dashboard