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
14 changes: 14 additions & 0 deletions DEVLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
37 changes: 29 additions & 8 deletions src/app/(app)/w/[slug]/command-center/page.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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**
Expand Down Expand Up @@ -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 ? (
Expand Down Expand Up @@ -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 (
<section
className="min-h-0 rounded-md border border-border bg-background/35"
Expand All @@ -1504,16 +1507,34 @@ function AttentionGroup({
</span>
{action ? <span className="ml-auto flex shrink-0 items-center gap-1">{action}</span> : null}
</header>
<div
className={cn("min-h-0 space-y-2 p-2", unbounded ? "" : "max-h-[22rem] overflow-y-auto")}
>
<div className="min-h-0 space-y-2 p-2" data-attention-group-items>
{count === 0 ? (
<div className="text-meta rounded-md border border-dashed border-border bg-card/20 p-3 text-muted-foreground">
{empty}
</div>
) : (
children
visibleItems
)}
{hiddenCount > 0 ? (
<button
type="button"
className="focus-ring text-meta flex w-full items-center justify-center gap-1 rounded-md border border-dashed border-border px-2 py-1.5 font-medium text-muted-foreground transition-colors hover:border-ember/40 hover:bg-card/40 hover:text-foreground"
aria-expanded={expanded}
onClick={() => setExpanded((value) => !value)}
>
{expanded ? (
<>
<ChevronUp className="h-3 w-3" aria-hidden />
Show fewer
</>
) : (
<>
<ChevronDown className="h-3 w-3" aria-hidden />
Show {hiddenCount} more
</>
)}
</button>
) : null}
</div>
</section>
);
Expand Down
48 changes: 26 additions & 22 deletions src/app/(app)/w/[slug]/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -370,16 +370,18 @@ function TeamDashboard() {
/>

<div className="space-y-6" data-testid="dashboard-layout">
{/* 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. */}
<div
className="grid grid-cols-1 gap-5 xl:grid-cols-12 xl:items-start"
data-testid="dashboard-priority-cockpit"
>
<div className="space-y-5 xl:col-span-8 2xl:col-span-7">
<div
className="space-y-5 xl:col-span-7 xl:col-start-1 xl:row-start-1"
data-testid="dashboard-focus-column"
>
{/* 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. */}
Expand Down Expand Up @@ -421,7 +423,10 @@ function TeamDashboard() {
)}
</div>

<div className="min-w-0 space-y-5 xl:col-span-4 2xl:col-span-5">
<div
className="min-w-0 space-y-5 xl:col-span-5 xl:col-start-8 xl:row-span-2 xl:row-start-1"
data-testid="dashboard-operations-column"
>
<ZoneDivider label="Live operations" />
<DashboardStack
widgets={priorityWidgets}
Expand All @@ -431,22 +436,21 @@ function TeamDashboard() {
columns={priorityColumnCount}
/>
</div>
</div>

{/* 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. */}
<section className="min-w-0 space-y-3" data-testid="dashboard-flow-board">
<ZoneDivider label="Workspace flow" />
<DashboardStack
widgets={flowWidgets}
layout={effLayout}
editing={editing}
onChange={persistLayout}
columns={3}
/>
</section>
<section
className="min-w-0 space-y-3 xl:col-span-7 xl:col-start-1 xl:row-start-2"
data-testid="dashboard-flow-board"
>
<ZoneDivider label="Workspace flow" />
<DashboardStack
widgets={flowWidgets}
layout={effLayout}
editing={editing}
onChange={persistLayout}
columns={2}
/>
</section>
</div>
</div>
</div>
</div>
Expand Down
8 changes: 8 additions & 0 deletions tests/e2e/command-center-attention-actions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]");
Expand Down
24 changes: 12 additions & 12 deletions tests/e2e/dashboard-layout.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
},
Expand Down Expand Up @@ -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;
Expand All @@ -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(() =>
Expand All @@ -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(() => {
Expand Down
Loading