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");
+ });
+});