fix(web): bound project page wrapper so dashboard body scrolls (closes #1923) (#1929)

* 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) <noreply@anthropic.com>

* fix(web): make <main> a flex column so body's overflow-y actually scrolls

The body-level scroll has been broken since af2af115 (#927). The
JSX is <main className="dashboard-main flex-1 ...">, but neither
.dashboard-main nor any Tailwind class on <main> 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 <main>'s overflow:
hidden clipped it.

Pre-regression markup was a <div> 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 <main> in both Dashboard.tsx and the
projects/[projectId]/loading.tsx skeleton. Body's flex:1 now
correctly fills <main>'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) <noreply@anthropic.com>

* 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 (<div className="flex-1 min-h-screen ...">) 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) <noreply@anthropic.com>

* 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) <noreply@anthropic.com>

* fix(web): wrap loading skeleton in same bounded flex container

Addresses greptile P1 review feedback on PR #1929. The project
page (page.tsx) wraps <Dashboard> in <div className="flex min-h-0
min-w-0 flex-1 flex-col bg-..."> 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) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Harshit Singh Bhandari 2026-05-19 14:38:24 +05:30 committed by GitHub
parent ee3fb5d33a
commit 11c07de258
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 74 additions and 52 deletions

View File

@ -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`.

View File

@ -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) ──────────────────────────────────── */

View File

@ -1,40 +1,46 @@
export default function ProjectRouteLoading() {
return (
<div className="dashboard-main--desktop">
<header className="dashboard-app-header" aria-hidden="true">
<button type="button" className="dashboard-app-sidebar-toggle" aria-label="Toggle sidebar">
<svg
width="14"
height="14"
fill="none"
stroke="currentColor"
strokeWidth="1.75"
viewBox="0 0 24 24"
aria-hidden="true"
<div className="flex min-h-0 min-w-0 flex-1 flex-col bg-[var(--color-bg-canvas)]">
<div className="dashboard-main--desktop">
<header className="dashboard-app-header" aria-hidden="true">
<button
type="button"
className="dashboard-app-sidebar-toggle"
aria-label="Toggle sidebar"
>
<rect x="3" y="3" width="18" height="18" rx="2" />
<path d="M9 3v18" />
</svg>
</button>
<div className="dashboard-app-header__brand dashboard-app-header__brand--hide-mobile">
<span>Agent Orchestrator</span>
</div>
<span className="dashboard-app-header__sep topbar-desktop-only" aria-hidden="true" />
<span className="dashboard-app-header__project">Loading project</span>
<div className="dashboard-app-header__spacer" />
</header>
<main className="dashboard-main flex-1 min-h-0 overflow-hidden">
<div className="board-wrapper" aria-hidden="true">
<div className="kanban-ghost">
{["Working", "Pending", "Review", "Respond", "Merge"].map((label) => (
<div key={label} className="kanban-ghost__col">
<div className="kanban-ghost__head">{label}</div>
</div>
))}
<svg
width="14"
height="14"
fill="none"
stroke="currentColor"
strokeWidth="1.75"
viewBox="0 0 24 24"
aria-hidden="true"
>
<rect x="3" y="3" width="18" height="18" rx="2" />
<path d="M9 3v18" />
</svg>
</button>
<div className="dashboard-app-header__brand dashboard-app-header__brand--hide-mobile">
<span>Agent Orchestrator</span>
</div>
</div>
</main>
<span className="dashboard-app-header__sep topbar-desktop-only" aria-hidden="true" />
<span className="dashboard-app-header__project">Loading project</span>
<div className="dashboard-app-header__spacer" />
</header>
<main className="dashboard-main flex flex-col flex-1 min-h-0 overflow-hidden">
<div className="board-wrapper" aria-hidden="true">
<div className="kanban-ghost">
{["Working", "Pending", "Review", "Respond", "Merge"].map((label) => (
<div key={label} className="kanban-ghost__col">
<div className="kanban-ghost__head">{label}</div>
</div>
))}
</div>
</div>
</main>
</div>
</div>
);
}

View File

@ -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",

View File

@ -29,7 +29,7 @@ export default async function ProjectPage(props: {
const pageData = await getDashboardPageData(projectId);
return (
<div className="flex-1 min-h-screen bg-[var(--color-bg-canvas)]">
<div className="flex min-h-0 min-w-0 flex-1 flex-col bg-[var(--color-bg-canvas)]">
<Dashboard
initialSessions={pageData.sessions}
projectId={pageData.selectedProjectId}

View File

@ -716,7 +716,7 @@ function DashboardInner({
</div>
</header>
<main className="dashboard-main flex-1 min-h-0 overflow-hidden">
<main className="dashboard-main flex flex-col flex-1 min-h-0 overflow-hidden">
<DynamicFavicon attentionLevels={attentionLevels} projectName={projectName} />
<div className="dashboard-main__subhead">
<h1 className="dashboard-main__title">Dashboard</h1>