import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { DirectorySidebar } from "./DirectorySidebar";
import { SessionTabs } from "./SessionTabs";
import { SessionPicker } from "./SessionPicker";
import { ConversationWorkspace } from "./ConversationWorkspace";
import { ExtensionDialog } from "./ExtensionDialog";
import {
initialWorkspaceState,
reconcileWorkspace,
runtimeView,
} from "../workspace/reducer";
import type { DirectorySession, Workspace } from "../workspace/types";
const summary = (id: string, state = "idle") => ({
runtimeId: id,
worktreePath: "/repo",
state,
label: id,
attention: id === "b",
queueCount: id === "b" ? 2 : 0,
lastActivity: id,
openedAt: id,
});
const workspace: Workspace = {
bridgeInstanceId: "e",
latestSeq: 0,
directories: [
{
worktreePath: "/repo",
isHome: false,
openCount: 2,
workingCount: 1,
attentionCount: 1,
recoveringCount: 0,
errorCount: 1,
runtimes: [summary("a"), summary("b", "streaming")],
},
],
};
describe("multi-session navigation", () => {
it("renders directory aggregates and accessible keyboard session tabs", async () => {
const state = reconcileWorkspace(initialWorkspaceState, workspace);
const onSelect = vi.fn();
const onNew = vi.fn();
const onRenew = vi.fn();
const { rerender } = render(
<>
>,
);
expect(screen.getByLabelText(/1 attention, 1 errors/)).toBeInTheDocument();
const tabs = screen.getAllByRole("tab");
expect(tabs[0]).toHaveAttribute("aria-selected", "true");
const closeFirst = screen.getByRole("button", { name: "Close a" });
const renewFirst = screen.getByRole("button", { name: "Renew a" });
expect(tabs[0].parentElement?.lastElementChild).toBe(closeFirst);
expect(closeFirst.previousElementSibling).toBe(renewFirst);
const tablist = screen.getByRole("tablist", { name: "Open sessions" });
const newSession = screen.getByRole("button", { name: "New session" });
expect(tablist.nextElementSibling).toBe(newSession);
await userEvent.click(newSession);
expect(onNew).toHaveBeenCalledOnce();
await userEvent.click(tabs[0]);
await userEvent.keyboard("{ArrowRight}");
expect(onSelect).toHaveBeenCalledWith("b");
rerender(
,
);
expect(screen.getByRole("tab")).toHaveAccessibleName(/b/);
});
it("announces close progress only on the closing session", () => {
render(
,
);
const closing = screen.getByRole("button", { name: "Closing b" });
expect(closing).toBeDisabled();
expect(closing).toHaveAttribute("aria-busy", "true");
expect(screen.getByRole("button", { name: "Close a" })).toBeEnabled();
});
it("restores focus to the selected adjacent tab after close", () => {
const onFocusRestored = vi.fn();
render(
,
);
expect(screen.getByRole("tab", { name: /b/ })).toHaveFocus();
expect(onFocusRestored).toHaveBeenCalled();
});
it("shows two-line tool results and previews longer collapsed results", async () => {
const user = userEvent.setup();
const runtime = runtimeView(summary("tool-result"));
runtime.messages = [
{ role: "toolResult", toolName: "read", content: "first\nsecond\n" },
];
const props = {
onDraft: vi.fn(),
onSubmit: vi.fn(),
onAbort: vi.fn(),
onScroll: vi.fn(),
onModel: vi.fn().mockResolvedValue(true),
onThinking: vi.fn().mockResolvedValue(true),
onCommand: vi.fn(),
};
const { container, rerender } = render(
,
);
expect(container.querySelector("details")).toBeNull();
expect(container.querySelector(".tool-result-visible pre")).toHaveTextContent(
"first second",
);
const longRuntime = {
...runtime,
messages: [
{
role: "toolResult",
toolName: "read",
content: "first\nsecond\nthird\n",
},
],
};
rerender();
const details = container.querySelector("details");
expect(details).not.toHaveAttribute("open");
expect(container.querySelector(".tool-result-preview")?.textContent).toBe(
"first\nsecond",
);
expect(container.querySelector(".tool-result-full")?.textContent).toBe(
"first\nsecond\nthird\n",
);
expect(
screen.getByText("1 more line hidden · Expand to view"),
).toBeInTheDocument();
await user.click(screen.getByText("Tool result · read"));
expect(details).toHaveAttribute("open");
});
it("shows a jump control only while scrolled up and moves to the latest message", async () => {
const user = userEvent.setup();
const onScroll = vi.fn();
const atBottom = runtimeView(summary("scroll"));
const props = {
onDraft: vi.fn(),
onSubmit: vi.fn(),
onAbort: vi.fn(),
onScroll,
onModel: vi.fn().mockResolvedValue(true),
onThinking: vi.fn().mockResolvedValue(true),
onCommand: vi.fn(),
};
const { rerender } = render(
,
);
expect(
screen.queryByRole("button", { name: "Scroll to latest message" }),
).toBeNull();
const scrolledUp = {
...atBottom,
scroll: { atBottom: false, scrollTop: 125 },
};
rerender();
const transcript = screen.getByRole("region", { name: "Conversation" });
Object.defineProperty(transcript, "scrollHeight", {
configurable: true,
value: 600,
});
transcript.scrollTop = 125;
await user.click(
screen.getByRole("button", { name: "Scroll to latest message" }),
);
expect(transcript.scrollTop).toBe(600);
expect(onScroll).toHaveBeenCalledWith(true, 600);
expect(transcript).toHaveFocus();
});
it("shows an aborting control and labels an accepted follow-up", () => {
const runtime = runtimeView(summary("working", "streaming"));
runtime.progress = {
phase: "working",
detail: "Thinking and preparing a response",
toolCount: 0,
};
runtime.pendingSubmissions = [
{
id: 1,
text: "Continue with the plan",
userMessageCount: 0,
phase: "sent",
delivery: "follow_up",
},
];
render(
,
);
expect(screen.getByRole("button", { name: "Aborting…" })).toBeDisabled();
expect(
screen.getByText("Follow-up queued · waiting for Pi"),
).toBeInTheDocument();
});
it("searches saved sessions and displays useful metadata", async () => {
const user = userEvent.setup();
const onOpen = vi.fn();
const sessions: DirectorySession[] = [
{
path: "/planning",
id: "plan-id",
cwd: "/repo",
name: "Planning",
firstMessage: "Sketch the release plan",
modified: "2026-08-15T10:00:00.000Z",
messageCount: 1,
runtimeId: "already-open",
},
{
path: "/incident",
id: "incident-42",
cwd: "/repo",
firstMessage: "Investigate search crash",
modified: "2026-08-14T10:00:00.000Z",
messageCount: 2,
},
];
render(
,
);
const search = screen.getByRole("searchbox", {
name: "Search saved sessions",
});
expect(screen.getByRole("status")).toHaveTextContent("2 saved sessions");
expect(screen.getByText("Sketch the release plan")).toBeInTheDocument();
expect(screen.getByText("1 message")).toBeInTheDocument();
expect(screen.getByText("Open")).toBeInTheDocument();
await user.type(search, "release");
expect(screen.getByRole("status")).toHaveTextContent("1 saved session");
expect(screen.getByRole("button", { name: /Planning/ })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: /Investigate/ })).toBeNull();
await user.clear(search);
await user.type(search, "INCIDENT-42");
await user.click(screen.getByRole("button", { name: /Investigate/ }));
expect(onOpen).toHaveBeenCalledWith(sessions[1]);
await user.clear(search);
await user.type(search, "missing");
expect(screen.getByText(/No saved sessions match/)).toBeInTheDocument();
});
it("focuses search, traps focus, and restores the opener", async () => {
const user = userEvent.setup();
const opener = document.createElement("button");
document.body.append(opener);
opener.focus();
const onClose = vi.fn();
const { unmount } = render(
,
);
const search = screen.getByRole("searchbox", {
name: "Search saved sessions",
});
const close = screen.getByRole("button", { name: "Close" });
expect(search).toHaveFocus();
await user.keyboard("{Shift>}{Tab}{/Shift}");
expect(close).toHaveFocus();
await user.keyboard("{Shift>}{Tab}{/Shift}");
expect(search).toHaveFocus();
await user.keyboard("{Escape}");
expect(onClose).toHaveBeenCalled();
unmount();
expect(opener).toHaveFocus();
document.body.removeChild(opener);
});
it("labels permission requests with the action and exact details", async () => {
const user = userEvent.setup();
const onRespond = vi.fn();
render(
,
);
expect(screen.getByText("Pi agent request")).toBeInTheDocument();
expect(screen.getByText("Requested action")).toBeInTheDocument();
expect(screen.getByText("Run a shell command")).toBeInTheDocument();
expect(screen.getByText("Request details")).toBeInTheDocument();
expect(screen.getByText(/rm -rf .\/build/)).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Allow request" }));
expect(onRespond).toHaveBeenCalledWith({ confirmed: true });
});
it("uses Pi editor prefill content", () => {
render(
,
);
expect(screen.getByLabelText("Extension value")).toHaveValue(
"Existing request",
);
});
it("routes new, saved-open, renew, and close controls through callbacks", async () => {
const onNew = vi.fn(),
onResume = vi.fn(),
onRenew = vi.fn(),
onClose = vi.fn();
render(
,
);
await userEvent.click(screen.getByRole("button", { name: "New session" }));
await userEvent.click(screen.getByRole("button", { name: "Load" }));
await userEvent.click(screen.getByLabelText("Renew a"));
await userEvent.click(screen.getByLabelText("Close a"));
expect(onNew).toHaveBeenCalled();
expect(onResume).toHaveBeenCalled();
expect(onRenew).toHaveBeenCalledWith(
expect.objectContaining({
summary: expect.objectContaining({ runtimeId: "a" }),
}),
);
expect(onClose).toHaveBeenCalled();
});
});