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
9 changes: 8 additions & 1 deletion apps/web/src/features/panes/ActivityPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <FamilyEmpty family="activity" />;
if (entries.length === 0) {
return (
<div className="flex h-full min-h-0 flex-col">
<PaneHeading family="activity" summary="0 recorded" trailing={trailing} />
<FamilyEmpty className="min-h-0 flex-1" family="activity" />
</div>
);
}

const exportText = () => exportActivity(visible);
const copyAll = () => {
Expand Down
9 changes: 8 additions & 1 deletion apps/web/src/features/panes/AgentsPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,14 @@ export function AgentsPane({
[api, rows],
);

if (rows.length === 0) return <FamilyEmpty family="agents" />;
if (rows.length === 0) {
return (
<div className="flex h-full min-h-0 flex-col">
<PaneHeading family="agents" summary={summary} trailing={trailing} />
<FamilyEmpty className="min-h-0 flex-1" family="agents" />
</div>
);
}

const moveFocus = (nextIndex: number) => {
const clamped = Math.min(Math.max(nextIndex, 0), rows.length - 1);
Expand Down
12 changes: 9 additions & 3 deletions apps/web/src/features/panes/FamilyEmpty.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Empty className="h-full border-0">
<Empty className={cn("border-0", className ?? "h-full")}>
<EmptyHeader>
<EmptyTitle className="text-base">{meta.emptyTitle}</EmptyTitle>
<EmptyDescription>{meta.emptyDescription}</EmptyDescription>
Expand Down
10 changes: 9 additions & 1 deletion apps/web/src/features/panes/PaneContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 <FamilyEmpty family={family} />;
if (sessionId === null || store === null) {
return (
<div className="flex h-full min-h-0 flex-col">
<PaneHeading family={family} trailing={trailing} />
<FamilyEmpty className="min-h-0 flex-1" family={family} />
</div>
Comment on lines +85 to +88

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The outer container uses h-full min-h-0 to fill the panel, while FamilyEmpty is adjusted to min-h-0 flex-1 to sit within the flex structure without causing column scroll/height overflow.

);
}
switch (family) {
case "agents": {
const controller = rendererPlatform.mode === "browser" ? null : desktopRuntime();
Expand Down
9 changes: 8 additions & 1 deletion apps/web/src/features/panes/ReviewPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,14 @@ export function ReviewPane({ api, trailing }: { readonly api: InspectorStoreApi;
null,
);

if (files.length === 0) return <FamilyEmpty family="review" />;
if (files.length === 0) {
return (
<div className="flex h-full min-h-0 flex-col">
<PaneHeading family="review" summary="0 files" trailing={trailing} />
<FamilyEmpty className="min-h-0 flex-1" family="review" />
</div>
);
}

const selected = files.find((file) => file.path === selectedPath);
const additions = files.reduce((sum, file) => sum + file.additions, 0);
Expand Down
79 changes: 79 additions & 0 deletions apps/web/test/pane-empty-states.test.tsx
Original file line number Diff line number Diff line change
@@ -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 = <button aria-label="Close pane">X</button>;

it("renders PaneContent no-store fallback with header and close trailing element", () => {
const html = renderToStaticMarkup(
<PaneContent family="agents" trailing={mockTrailing} />
);
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(
<AgentsPane api={store} sessionId="test" trailing={mockTrailing} />
);
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(
<ActivityPane api={store} trailing={mockTrailing} />
);
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(
<ReviewPane api={store} trailing={mockTrailing} />
);
expect(html).toContain("Review");
expect(html).toContain("aria-label=\"Close pane\"");
expect(html).toContain("Nothing to review");
});
});
Loading