diff --git a/apps/web/src/features/panes/ActivityPane.tsx b/apps/web/src/features/panes/ActivityPane.tsx index 7832777c..0a8b1b7e 100644 --- a/apps/web/src/features/panes/ActivityPane.tsx +++ b/apps/web/src/features/panes/ActivityPane.tsx @@ -206,7 +206,14 @@ export function ActivityPane({ api, trailing }: { readonly api: InspectorStoreAp const expandedEntry = expandedSeq === null ? undefined : entries.find((entry) => entry.seq === expandedSeq); - if (entries.length === 0) return ; + if (entries.length === 0) { + return ( +
+ + +
+ ); + } const exportText = () => exportActivity(visible); const copyAll = () => { diff --git a/apps/web/src/features/panes/AgentsPane.tsx b/apps/web/src/features/panes/AgentsPane.tsx index 4bb246be..e224601c 100644 --- a/apps/web/src/features/panes/AgentsPane.tsx +++ b/apps/web/src/features/panes/AgentsPane.tsx @@ -459,7 +459,14 @@ export function AgentsPane({ [api, rows], ); - if (rows.length === 0) return ; + if (rows.length === 0) { + return ( +
+ + +
+ ); + } const moveFocus = (nextIndex: number) => { const clamped = Math.min(Math.max(nextIndex, 0), rows.length - 1); diff --git a/apps/web/src/features/panes/FamilyEmpty.tsx b/apps/web/src/features/panes/FamilyEmpty.tsx index e4869b7f..7ebc695b 100644 --- a/apps/web/src/features/panes/FamilyEmpty.tsx +++ b/apps/web/src/features/panes/FamilyEmpty.tsx @@ -1,15 +1,21 @@ // Honest per-family empty state, reusing the shell's fixed copy so the // closed-pane promise and the open-pane reality never drift apart. -import { Empty, EmptyDescription, EmptyHeader, EmptyTitle } from "@t4-code/ui"; +import { cn, Empty, EmptyDescription, EmptyHeader, EmptyTitle } from "@t4-code/ui"; import { PANE_FAMILY_META } from "../../components/pane-families.tsx"; import type { PaneFamily } from "../../state/workspace-store.ts"; -export function FamilyEmpty({ family }: { readonly family: PaneFamily }) { +export function FamilyEmpty({ + family, + className, +}: { + readonly family: PaneFamily; + readonly className?: string | undefined; +}) { const meta = PANE_FAMILY_META.find((entry) => entry.id === family); if (meta === undefined) return null; return ( - + {meta.emptyTitle} {meta.emptyDescription} diff --git a/apps/web/src/features/panes/PaneContent.tsx b/apps/web/src/features/panes/PaneContent.tsx index 97f0024e..57c6c46b 100644 --- a/apps/web/src/features/panes/PaneContent.tsx +++ b/apps/web/src/features/panes/PaneContent.tsx @@ -3,6 +3,7 @@ // owns everything inside it. import type * as React from "react"; import { FamilyEmpty } from "./FamilyEmpty.tsx"; +import { PaneHeading } from "./PaneHeading.tsx"; import { desktopRuntime } from "../../platform/desktop-runtime.ts"; import { rendererPlatform, useWorkspace } from "../../state/store-instance.ts"; import type { PaneFamily } from "../../state/workspace-store.ts"; @@ -79,7 +80,14 @@ export interface PaneContentProps { export function PaneContent({ family, trailing }: PaneContentProps) { const sessionId = useWorkspace((state) => state.activeSessionId); const store = sessionId === null ? null : getInspectorStore(sessionId); - if (sessionId === null || store === null) return ; + if (sessionId === null || store === null) { + return ( +
+ + +
+ ); + } switch (family) { case "agents": { const controller = rendererPlatform.mode === "browser" ? null : desktopRuntime(); diff --git a/apps/web/src/features/panes/ReviewPane.tsx b/apps/web/src/features/panes/ReviewPane.tsx index 0d5ed4b6..fd15d688 100644 --- a/apps/web/src/features/panes/ReviewPane.tsx +++ b/apps/web/src/features/panes/ReviewPane.tsx @@ -396,7 +396,14 @@ export function ReviewPane({ api, trailing }: { readonly api: InspectorStoreApi; null, ); - if (files.length === 0) return ; + if (files.length === 0) { + return ( +
+ + +
+ ); + } const selected = files.find((file) => file.path === selectedPath); const additions = files.reduce((sum, file) => sum + file.additions, 0); diff --git a/apps/web/test/pane-empty-states.test.tsx b/apps/web/test/pane-empty-states.test.tsx new file mode 100644 index 00000000..0b3513a1 --- /dev/null +++ b/apps/web/test/pane-empty-states.test.tsx @@ -0,0 +1,79 @@ +import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, it } from "vite-plus/test"; +import * as React from "react"; + +import { PaneContent } from "../src/features/panes/PaneContent.tsx"; +import { AgentsPane } from "../src/features/panes/AgentsPane.tsx"; +import { ActivityPane } from "../src/features/panes/ActivityPane.tsx"; +import { ReviewPane } from "../src/features/panes/ReviewPane.tsx"; +import { createInspectorStore, type InspectorStoreApi } from "../src/features/panes/inspector-store.ts"; + +function createEmptyMockStore(): InspectorStoreApi { + return createInspectorStore({ + sampleMode: true, + controller: () => ({ + kind: "fixture", + performControl() {}, + performReview() {}, + loadDir() {}, + loadPreview() {}, + }), + seed: { + activity: [], + agentMap: { order: [], agents: {} }, + review: { + files: [], + selectedPath: null, + view: "unified", + comments: [], + wrap: false, + viewedByPath: {}, + draftAnchor: null, + }, + terminals: [], + }, + }); +} + +describe("Pane empty state headers and close controls", () => { + const mockTrailing = ; + + it("renders PaneContent no-store fallback with header and close trailing element", () => { + const html = renderToStaticMarkup( + + ); + expect(html).toContain("Agents"); + expect(html).toContain("aria-label=\"Close pane\""); + expect(html).toContain("No agents running"); + }); + + it("renders empty AgentsPane with header and close trailing element", () => { + const store = createEmptyMockStore(); + const html = renderToStaticMarkup( + + ); + expect(html).toContain("Agents"); + expect(html).toContain("aria-label=\"Close pane\""); + expect(html).toContain("No agents running"); + }); + + it("renders empty ActivityPane with header and close trailing element", () => { + const store = createEmptyMockStore(); + const html = renderToStaticMarkup( + + ); + expect(html).toContain("Activity"); + expect(html).toContain("aria-label=\"Close pane\""); + expect(html).toContain("Nothing recorded yet"); + }); + + it("renders empty ReviewPane with header and close trailing element", () => { + const store = createEmptyMockStore(); + const html = renderToStaticMarkup( + + ); + expect(html).toContain("Review"); + expect(html).toContain("aria-label=\"Close pane\""); + expect(html).toContain("Nothing to review"); + }); +});