diff --git a/DEVLOG.md b/DEVLOG.md index 36b60228..5a045e8b 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -2,6 +2,20 @@ > Append-only session log. Read at session start. Update at session end. +## 2026-07-20 — AXI-139 balanced dashboard and attention flow + +Recomposed the wide dashboard as two independently flowing rails. Workspace +flow now begins directly beneath Focus instead of waiting for the taller Live +operations stack, while narrow layouts preserve the Focus → Operations → +Workspace reading order. The flow board uses two useful-width tracks inside +the primary rail and retains the existing customizable masonry behavior. + +Standardized Command Center attention groups on a compact two-item preview +with an explicit, keyboard-accessible Show more / Show fewer affordance. Asks +no longer expands without limit, and Run recovery no longer hides additional +cards in a clipped nested scroll area. Responsive Playwright coverage now +guards both the dashboard rail fill and the attention overflow contract. + ## 2026-07-20 — AXI-137 delivery provenance and lifecycle truth Separated Agent Profile configuration from output-producing run provenance in diff --git a/src/app/(app)/w/[slug]/command-center/page.tsx b/src/app/(app)/w/[slug]/command-center/page.tsx index 1e2f7df6..08f9cb88 100644 --- a/src/app/(app)/w/[slug]/command-center/page.tsx +++ b/src/app/(app)/w/[slug]/command-center/page.tsx @@ -1,5 +1,5 @@ "use client"; -import { useRef, useState } from "react"; +import { Children, useRef, useState } from "react"; import Link from "next/link"; import { toast } from "sonner"; import { @@ -49,6 +49,7 @@ import { cn } from "@/lib/utils"; const COMMAND_MODULE_LIMIT = 4; const COMMAND_ARTIFACT_LIMIT = 6; +const ATTENTION_GROUP_PREVIEW_LIMIT = 2; /** * Command Center — the operator's **decisions + live agent operations** @@ -235,7 +236,6 @@ export default function CommandCenterPage() { title="Asks" count={data.counts.actionRequests} empty="No agent asks." - unbounded > {data.actionRequests.map((row) => completionIntent(row.payload) && row.issue ? ( @@ -1480,16 +1480,19 @@ function AttentionGroup({ count, empty, action, - unbounded = false, children, }: { title: string; count: number; empty: string; action?: React.ReactNode; - unbounded?: boolean; children: React.ReactNode; }) { + const [expanded, setExpanded] = useState(false); + const items = Children.toArray(children); + const hiddenCount = Math.max(0, items.length - ATTENTION_GROUP_PREVIEW_LIMIT); + const visibleItems = expanded ? items : items.slice(0, ATTENTION_GROUP_PREVIEW_LIMIT); + return (
{action ? {action} : null} -
+
{count === 0 ? (
{empty}
) : ( - children + visibleItems )} + {hiddenCount > 0 ? ( + + ) : null}
); diff --git a/src/app/(app)/w/[slug]/dashboard/page.tsx b/src/app/(app)/w/[slug]/dashboard/page.tsx index 41b31021..bd7b605d 100644 --- a/src/app/(app)/w/[slug]/dashboard/page.tsx +++ b/src/app/(app)/w/[slug]/dashboard/page.tsx @@ -370,16 +370,18 @@ function TeamDashboard() { />
- {/* Stage 1 — a bounded priority cockpit. The split waits for `xl` - so Focus cards keep useful widths at tablet sizes. Crucially, - this is no longer the page's permanent macro layout: once the - priority band ends, every secondary module returns to a shared - full-width board below. */} + {/* The desktop cockpit uses two independently flowing rails. Workspace + flow starts directly beneath personal work instead of waiting for + the taller operations rail to finish. Mobile keeps the intentional + Focus → Operations → Workspace reading order. */}
-
+
{/* Focus (assigned, priority-first) + Pick-up (recent). Both render rich auto-height cards. If there's no personal work at all, Suggestions takes the slot as the primary handoff. */} @@ -421,7 +423,10 @@ function TeamDashboard() { )}
-
+
-
- {/* Stage 2 — one responsive board for everything that may fold or - move. Two columns begin at tablet width; wide screens gain a - third track. Measured row spans pack unequal content without - changing the DOM or keyboard reading order. */} -
- - -
+
+ + +
+
diff --git a/tests/e2e/command-center-attention-actions.spec.ts b/tests/e2e/command-center-attention-actions.spec.ts index 02f187c9..c426fdbf 100644 --- a/tests/e2e/command-center-attention-actions.spec.ts +++ b/tests/e2e/command-center-attention-actions.spec.ts @@ -270,6 +270,14 @@ test.describe("Command Center attention actions", () => { await expect(queuedRunEvidence).toHaveCount(2); await expect(queuedRunEvidence.first()).toBeVisible(); + const showMore = page.getByRole("button", { name: /Show \d+ more/ }); + await expect(showMore).toHaveAttribute("aria-expanded", "false"); + await showMore.click(); + await expect(page.getByRole("button", { name: "Show fewer" })).toHaveAttribute( + "aria-expanded", + "true", + ); + const fallbackCard = page .getByRole("link", { name: FALLBACK_TITLE }) .locator("xpath=ancestor::div[contains(@class, 'bg-card/40')][1]"); diff --git a/tests/e2e/dashboard-layout.spec.ts b/tests/e2e/dashboard-layout.spec.ts index f0b0fec5..e07e7b1c 100644 --- a/tests/e2e/dashboard-layout.spec.ts +++ b/tests/e2e/dashboard-layout.spec.ts @@ -5,7 +5,7 @@ const STATES = [ name: "desktop", width: 1600, height: 2600, - columns: 3, + columns: 2, priorityColumns: 2, firstWidgets: ["pipeline", "whats-new", "suggestions", "quick-notes"], }, @@ -54,14 +54,7 @@ test("dashboard keeps priority work bounded and reflows secondary modules", asyn .locator(":scope > [data-widget-id]") .evaluateAll((nodes) => nodes.map((node) => node.getAttribute("data-widget-id"))), ) - .toEqual([ - "agent-attention", - "delivery-work", - "agent-activity", - "standup", - "pulse", - "today", - ]); + .toEqual(["agent-attention", "delivery-work", "agent-activity", "standup", "pulse", "today"]); const priorityColumns = await priorityGrid.evaluate((node) => { const tracks = getComputedStyle(node).gridTemplateColumns; return tracks === "none" ? 1 : tracks.split(" ").length; @@ -75,13 +68,15 @@ test("dashboard keeps priority work bounded and reflows secondary modules", asyn await expect(flow.locator('[data-widget-id="pulse"]')).toHaveCount(0); await expect(flow.locator('[data-widget-id="today"]')).toHaveCount(0); await expect(flow.getByTestId("dashboard-whats-new")).toBeVisible(); - await expect(cockpit.locator('[data-widget-id="whats-new"]')).toHaveCount(0); + await expect( + page.getByTestId("dashboard-operations-column").locator('[data-widget-id="whats-new"]'), + ).toHaveCount(0); expect(await cockpit.locator("[data-schedule-due-item]").count()).toBeLessThanOrEqual(3); expect(await flow.locator("[data-whats-new-item]").count()).toBeLessThanOrEqual(4); expect(await flow.locator("[data-whats-new-history]").count()).toBeLessThanOrEqual(3); - const grid = flow.locator(`[data-dashboard-columns="3"]`); + const grid = flow.locator(`[data-dashboard-columns="2"]`); await expect(grid).toBeVisible(); await expect .poll(() => @@ -101,15 +96,20 @@ test("dashboard keeps priority work bounded and reflows secondary modules", asyn expect(renderedColumns).toBe(state.columns); if (state.name === "desktop") { - const [boardBox, suggestionsBox] = await Promise.all([ + const [boardBox, suggestionsBox, flowBox, operationsBox] = await Promise.all([ grid.boundingBox(), grid.locator('[data-widget-id="suggestions"]').boundingBox(), + flow.boundingBox(), + page.getByTestId("dashboard-operations-column").boundingBox(), ]); expect(boardBox).not.toBeNull(); expect(suggestionsBox).not.toBeNull(); + expect(flowBox).not.toBeNull(); + expect(operationsBox).not.toBeNull(); expect(Math.abs((boardBox?.width ?? 0) - (suggestionsBox?.width ?? 0))).toBeLessThanOrEqual( 1, ); + expect(flowBox!.y).toBeLessThan(operationsBox!.y + operationsBox!.height); } const overflow = await page.evaluate(() => {