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(); }); });