diff --git a/ui/src/components/assistant/HistorySheet.test.tsx b/ui/src/components/assistant/HistorySheet.test.tsx
new file mode 100644
index 00000000..26f6d098
--- /dev/null
+++ b/ui/src/components/assistant/HistorySheet.test.tsx
@@ -0,0 +1,111 @@
+// @vitest-environment jsdom
+import { act } from "react";
+import { createRoot } from "react-dom/client";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+
+// Stub ChatConversationList to avoid pulling its dependency tree into the
+// test (react-query, ChatPanelContext, etc.). We only want to assert that
+// HistorySheet mounts it when companyId is present.
+vi.mock("../ChatConversationList", () => ({
+ ChatConversationList: ({ companyId }: { companyId: string }) => (
+
+ conversation-list
+
+ ),
+}));
+
+import { HistorySheet } from "./HistorySheet";
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
+
+describe("", () => {
+ let container: HTMLDivElement;
+ let root: ReturnType | null = null;
+
+ beforeEach(() => {
+ container = document.createElement("div");
+ document.body.appendChild(container);
+ root = null;
+ });
+
+ afterEach(() => {
+ if (root) {
+ act(() => {
+ root!.unmount();
+ });
+ root = null;
+ }
+ if (container.parentNode) container.remove();
+ });
+
+ function render(node: React.ReactNode) {
+ root = createRoot(container);
+ act(() => {
+ root!.render(node);
+ });
+ }
+
+ it("renders nothing when closed", () => {
+ render( {}} companyId="co-1" />);
+ expect(container.querySelector('[data-testid="history-sheet-panel"]')).toBeNull();
+ expect(container.querySelector('[data-testid="history-sheet-backdrop"]')).toBeNull();
+ });
+
+ it("renders the panel and a backdrop when open", () => {
+ render( {}} companyId="co-1" />);
+ expect(container.querySelector('[data-testid="history-sheet-panel"]')).not.toBeNull();
+ expect(container.querySelector('[data-testid="history-sheet-backdrop"]')).not.toBeNull();
+ });
+
+ it("mounts ChatConversationList inside the panel when companyId is present", () => {
+ render( {}} companyId="co-1" />);
+ const stub = container.querySelector('[data-testid="chat-conversation-list-stub"]');
+ expect(stub).not.toBeNull();
+ expect(stub?.getAttribute("data-company-id")).toBe("co-1");
+ });
+
+ it("shows a placeholder when companyId is null", () => {
+ render( {}} companyId={null} />);
+ expect(container.querySelector('[data-testid="chat-conversation-list-stub"]')).toBeNull();
+ expect(container.textContent ?? "").toContain("Select a workspace");
+ });
+
+ it("calls onClose when the backdrop is clicked", () => {
+ const onClose = vi.fn();
+ render();
+ const backdrop = container.querySelector(
+ '[data-testid="history-sheet-backdrop"]',
+ ) as HTMLButtonElement;
+ act(() => {
+ backdrop.click();
+ });
+ expect(onClose).toHaveBeenCalledTimes(1);
+ });
+
+ it("calls onClose when ESC is pressed", () => {
+ const onClose = vi.fn();
+ render();
+ act(() => {
+ document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" }));
+ });
+ expect(onClose).toHaveBeenCalledTimes(1);
+ });
+
+ it("does not listen for ESC when closed", () => {
+ const onClose = vi.fn();
+ render();
+ act(() => {
+ document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" }));
+ });
+ expect(onClose).not.toHaveBeenCalled();
+ });
+
+ it("uses a left-side panel positioned against the icon rail", () => {
+ render( {}} companyId="co-1" />);
+ const panel = container.querySelector('[data-testid="history-sheet-panel"]') as HTMLElement;
+ // The Tailwind class string should include left-[56px] and w-[320px].
+ expect(panel.className).toContain("left-[56px]");
+ expect(panel.className).toContain("w-[320px]");
+ });
+});
diff --git a/ui/src/components/assistant/HistorySheet.tsx b/ui/src/components/assistant/HistorySheet.tsx
new file mode 100644
index 00000000..bf61f67a
--- /dev/null
+++ b/ui/src/components/assistant/HistorySheet.tsx
@@ -0,0 +1,84 @@
+// [nexus] History slide-over sheet for the Assistant (Phase 9).
+//
+// Left-side slide-over that butts against the 56px icon rail. Wraps the
+// existing ChatConversationList so we preserve the conversation model
+// without reimplementing search, pinning, or create/rename.
+import { useEffect } from "react";
+import { X } from "lucide-react";
+import { ChatConversationList } from "../ChatConversationList";
+import { cn } from "@/lib/utils";
+
+export interface HistorySheetProps {
+ open: boolean;
+ onClose: () => void;
+ companyId: string | null;
+ className?: string;
+}
+
+export function HistorySheet({ open, onClose, companyId, className }: HistorySheetProps) {
+ // ESC closes the sheet.
+ useEffect(() => {
+ if (!open) return;
+ const onKey = (e: KeyboardEvent) => {
+ if (e.key === "Escape") {
+ e.stopPropagation();
+ onClose();
+ }
+ };
+ document.addEventListener("keydown", onKey);
+ return () => document.removeEventListener("keydown", onKey);
+ }, [open, onClose]);
+
+ if (!open) return null;
+
+ return (
+ <>
+ {/* Backdrop — click to dismiss */}
+
+ {/* Panel */}
+
+ >
+ );
+}