Agent Chat is disabled. Enable it in Experimental settings.
;
+ const error = agents.error ?? session.error;
+ return
diff --git a/ui/storybook/.storybook/preview-head.html b/ui/storybook/.storybook/preview-head.html
new file mode 100644
index 00000000000..404cd6c309a
--- /dev/null
+++ b/ui/storybook/.storybook/preview-head.html
@@ -0,0 +1,7 @@
+
diff --git a/ui/storybook/.storybook/preview.tsx b/ui/storybook/.storybook/preview.tsx
index 6a236fdf386..0dded8f745b 100644
--- a/ui/storybook/.storybook/preview.tsx
+++ b/ui/storybook/.storybook/preview.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useState, type ReactNode } from "react";
+import { useLayoutEffect, useState, type ReactNode } from "react";
import type { Preview } from "@storybook/react-vite";
import { MINIMAL_VIEWPORTS } from "storybook/viewport";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
@@ -734,9 +734,11 @@ function applyStorybookTheme(theme: "light" | "dark") {
function StorybookProviders({
children,
theme,
+ initialViewportWidth,
}: {
children: ReactNode;
theme: "light" | "dark";
+ initialViewportWidth?: number;
}) {
const [queryClient] = useState(
() =>
@@ -754,10 +756,33 @@ function StorybookProviders({
installStorybookApiFixtures();
}
- useEffect(() => {
+ const [viewportReady, setViewportReady] = useState(() => !initialViewportWidth || window.parent === window || window.innerWidth === initialViewportWidth);
+ useLayoutEffect(() => {
+ if (viewportReady) return;
+ const checkViewport = () => {
+ if (window.innerWidth === initialViewportWidth) setViewportReady(true);
+ };
+ checkViewport();
+ window.addEventListener("resize", checkViewport);
+ // A custom viewport, browser zoom, or unavailable manager must never leave
+ // a story blank indefinitely. Mount at the actual size after a bounded wait.
+ const fallback = window.setTimeout(() => setViewportReady(true), 500);
+ return () => { window.removeEventListener("resize", checkViewport); window.clearTimeout(fallback); };
+ }, [initialViewportWidth, viewportReady]);
+
+ const [themeReady, setThemeReady] = useState(false);
+ useLayoutEffect(() => {
applyStorybookTheme(theme);
+ setThemeReady(true);
}, [theme]);
+ // ThemeProvider reads the document on mount. Prepare it before mounting any
+ // app providers so they cannot paint the previous story's color mode.
+ // Fixed-viewport page stories should mount their responsive providers after
+ // the manager sizes the iframe, rather than briefly showing desktop navigation.
+ // Standalone iframe URLs use their actual browser width immediately.
+ if (!themeReady || !viewportReady) return null;
+
return (
@@ -787,8 +812,13 @@ const preview: Preview = {
decorators: [
(Story, context) => {
const theme = context.globals.theme === "light" ? "light" : "dark";
+ const viewport = context.globals.viewport;
+ const styles = context.parameters.viewport?.options?.[viewport?.value]?.styles;
+ const width = styles?.[viewport?.isRotated ? "height" : "width"];
+ const initialViewportWidth = context.parameters.waitForViewport && typeof width === "string" && /^\d+px$/.test(width)
+ ? Number.parseInt(width, 10) : undefined;
return (
-
+
);
diff --git a/ui/storybook/prototypes/agent-chat-sidebar/AgentChatSidebarReview.tsx b/ui/storybook/prototypes/agent-chat-sidebar/AgentChatSidebarReview.tsx
new file mode 100644
index 00000000000..19f9ca0084d
--- /dev/null
+++ b/ui/storybook/prototypes/agent-chat-sidebar/AgentChatSidebarReview.tsx
@@ -0,0 +1,8 @@
+import { Layout } from "@/components/Layout";
+export { AgentChats as AgentChatSidebarLanding } from "@/pages/AgentChats";
+import type { SidebarScenario } from "./fixtures";
+
+/** Story fixtures feed the production navigation and chat pages. */
+export function AgentChatSidebarReviewLayout(_props: { scenario: SidebarScenario }) {
+ return >} />;
+}
diff --git a/ui/storybook/prototypes/agent-chat-sidebar/AgentConversationSidebar.tsx b/ui/storybook/prototypes/agent-chat-sidebar/AgentConversationSidebar.tsx
new file mode 100644
index 00000000000..930c03afddd
--- /dev/null
+++ b/ui/storybook/prototypes/agent-chat-sidebar/AgentConversationSidebar.tsx
@@ -0,0 +1 @@
+export { AgentConversationSidebar, type AgentConversationSidebarProps } from "@/components/AgentConversationSidebar";
diff --git a/ui/storybook/prototypes/agent-chat-sidebar/fixtures.ts b/ui/storybook/prototypes/agent-chat-sidebar/fixtures.ts
new file mode 100644
index 00000000000..0e6fa7d388a
--- /dev/null
+++ b/ui/storybook/prototypes/agent-chat-sidebar/fixtures.ts
@@ -0,0 +1,34 @@
+import { appearanceForPalette, type Agent } from "@paperclipai/shared";
+import { storybookAgents } from "../../fixtures/paperclipData";
+
+export type SidebarScenario = "conversation" | "landing" | "large-team";
+
+// Keep the first identity compatible with the existing chat fixture controller.
+export const sidebarAgents: Agent[] = [
+ { id: "agent-codex", urlKey: "researcher", name: "Researcher", title: "Customer & market research", palette: "arctic-blue" },
+ { id: "chat-analyst", urlKey: "analyst", name: "Analyst", title: "Feedback analysis", palette: "orchid-peach" },
+ { id: "chat-writer", urlKey: "writer", name: "Writer", title: "Client updates & drafts", palette: "solar-flare" },
+ { id: "chat-strategist", urlKey: "strategist", name: "Strategist", title: "Planning & priorities", palette: "lime-lagoon" },
+].map(({ palette, ...agent }) => ({
+ ...storybookAgents[0],
+ ...agent,
+ role: "general",
+ status: "idle",
+ reportsTo: null,
+ appearance: appearanceForPalette(palette as Parameters[0]),
+}));
+
+export function sidebarRoster(scenario: SidebarScenario = "conversation"): Agent[] {
+ const roles = ["Product Designer", "Support Specialist", "Growth Marketer", "Security Engineer", "Data Scientist", "Finance Analyst", "Customer Success", "Content Editor", "QA Engineer", "Operations Lead", "Community Manager", "Product Engineer", "Technical Writer", "Recruiter", "Sales Researcher", "Release Engineer"];
+ return [...sidebarAgents, ...roles.slice(0, scenario === "large-team" ? roles.length : 4).map((title, index) => ({
+ ...sidebarAgents[index % sidebarAgents.length],
+ id: `sidebar-agent-${index}`, urlKey: `teammate-${index}`, name: title, title,
+ }))];
+}
+
+export const sidebarPreviews: Record = {
+ "agent-codex": "Clarifying your request",
+ "chat-analyst": "Feedback analysis",
+ "chat-writer": "Client update draft",
+ "chat-strategist": "Weekly priorities",
+};
diff --git a/ui/storybook/prototypes/agent-chat/AgentChatPrototype.tsx b/ui/storybook/prototypes/agent-chat/AgentChatPrototype.tsx
index d1b5d2cd221..f5babccb6dd 100644
--- a/ui/storybook/prototypes/agent-chat/AgentChatPrototype.tsx
+++ b/ui/storybook/prototypes/agent-chat/AgentChatPrototype.tsx
@@ -1,7 +1,9 @@
-import { useEffect, useLayoutEffect, useState, useRef } from "react";
+import { useLayoutEffect, useState, useRef } from "react";
import { useQueryClient } from "@tanstack/react-query";
+import { queryKeys } from "@/lib/queryKeys";
import { agentRouteRef } from "@/lib/utils";
import { recordAgentChatVisit } from "@/lib/recent-agent-chats";
+import { getRecentTasksStorageKey } from "@/lib/recent-tasks";
import { AgentDetail } from "@/pages/AgentDetail";
import { AgentChat } from "@/pages/AgentChat";
import { IssueDetail } from "@/pages/IssueDetail";
@@ -9,7 +11,7 @@ import { Agents, AGENT_FILTER_TABS } from "@/pages/Agents";
import { Layout } from "@/components/Layout";
import { usePanel } from "@/context/PanelContext";
import { PluginLauncherProvider } from "@/plugins/launchers";
-import { Routes, Route, useNavigate } from "@/lib/router";
+import { Routes, Route, useNavigate, useLocation } from "@/lib/router";
import type {
IssueChatComment,
IssueChatLinkedRun,
@@ -23,10 +25,14 @@ import {
} from "@/lib/task-side-panel-state";
import {
storybookAgents,
+ storybookAuthSession,
+ storybookCompanies,
storybookIssues,
storybookIssueDocuments,
} from "../../fixtures/paperclipData";
import { chatAgents, chatIdentifier } from "./AgentChatSidebar";
+import { AgentChatSidebarReviewLayout, AgentChatSidebarLanding } from "../agent-chat-sidebar/AgentChatSidebarReview";
+import { sidebarAgents, sidebarRoster, type SidebarScenario } from "../agent-chat-sidebar/fixtures";
import { ChatEntryReviewProvider, ChatEntrySidebar, ChatEntryLanding, reviewRoster, type EntryScenario } from "../chat-entry/ChatEntryReview";
const agent = storybookAgents.find((agent) => agent.id === "agent-codex")!;
@@ -48,8 +54,8 @@ const issue = {
labelIds: [],
currentExecutionWorkspace: null,
};
-function chatIssueId(agentId: string) {
- const index = reviewRoster("large-team").findIndex((item) => item.id === agentId);
+function chatIssueId(agentId: string, roster = reviewRoster("large-team")) {
+ const index = roster.findIndex((item) => item.id === agentId);
return agentId === agent.id ? issue.id : `00000000-0000-4000-8000-${String(index + 2).padStart(12, "0")}`;
}
const child = {
@@ -208,6 +214,7 @@ export interface AgentChatPrototypeProps {
contextInitiallyOpen?: boolean;
taskComparison?: boolean;
entryScenario?: EntryScenario;
+ sidebarScenario?: SidebarScenario;
}
/** Production pages with an in-memory API. No alternate chat controller. */
@@ -216,19 +223,25 @@ export function AgentChatPrototype({
contextInitiallyOpen = true,
taskComparison = false,
entryScenario,
+ sidebarScenario,
}: AgentChatPrototypeProps) {
const [ready, setReady] = useState(false);
const navigate = useNavigate();
+ const { pathname } = useLocation();
const initialRouteSet = useRef(false);
const queryClient = useQueryClient();
const { setPanelVisible } = usePanel();
- useEffect(() => {
+ useLayoutEffect(() => {
setPanelVisible(contextInitiallyOpen);
}, [contextInitiallyOpen, setPanelVisible]);
useLayoutEffect(() => {
const originalFetch = window.fetch;
const recentKey = `paperclip.recentAgentChats:${issue.companyId}:user-board`;
const previousRecents = localStorage.getItem(recentKey);
+ const recentTasksKey = getRecentTasksStorageKey(issue.companyId, "user-board");
+ const previousRecentTasks = localStorage.getItem(recentTasksKey);
+ if (sidebarScenario) localStorage.setItem(recentTasksKey, "[]");
+ const hasPlan = scenario !== "empty" && (!sidebarScenario || contextInitiallyOpen);
if (entryScenario) localStorage.setItem(recentKey, "[]");
const chats = new Map<
string,
@@ -252,7 +265,7 @@ export function AgentChatPrototype({
};
let failSend = scenario === "error";
let active = scenario === "working";
- const fixtureAgents = (entryScenario ? reviewRoster(entryScenario) : chatAgents).map((a) => ({
+ const fixtureAgents = (sidebarScenario ? sidebarRoster(sidebarScenario) : entryScenario ? reviewRoster(entryScenario) : chatAgents).map((a) => ({
...a,
status:
scenario === "paused" && a.id === agent.id
@@ -262,21 +275,30 @@ export function AgentChatPrototype({
for (const a of fixtureAgents) {
const task = {
...issue,
- id: chatIssueId(a.id),
- identifier: entryScenario ? `PAP-${400 + fixtureAgents.findIndex((item) => item.id === a.id)}` : chatIdentifier(a.id),
+ id: chatIssueId(a.id, fixtureAgents),
+ identifier: sidebarScenario ? `PAP-${500 + fixtureAgents.findIndex((item) => item.id === a.id)}` : entryScenario ? `PAP-${400 + fixtureAgents.findIndex((item) => item.id === a.id)}` : chatIdentifier(a.id),
assigneeAgentId: a.id,
title: `Chat with ${a.name}`,
conversationAgentId: taskComparison ? null : a.id,
conversationUserId: taskComparison ? null : "user-board",
conversationState: "waiting" as const,
};
- if (entryScenario ? entryScenario !== "first-use" && a.id === agent.id : scenario !== "empty" || a.id !== agent.id) chats.set(a.id, task);
+ const hasExistingChat = sidebarScenario
+ ? (sidebarScenario === "large-team" || sidebarAgents.some(initial => initial.id === a.id)) && (scenario !== "empty" || a.id !== agent.id)
+ : entryScenario ? entryScenario !== "first-use" && a.id === agent.id : scenario !== "empty" || a.id !== agent.id;
+ if (hasExistingChat) chats.set(a.id, task);
let history =
a.id === agent.id && scenario !== "empty"
? scenario === "working"
? comments.slice(0, 1)
: [...comments]
: [];
+ if (sidebarScenario && a.id === agent.id && scenario !== "empty") {
+ history = [
+ { ...comment("briefing-request", "Can you prepare a customer briefing for my Acme meeting on Thursday?"), issueId: task.id },
+ { ...comment("briefing-response", "Absolutely. What should I focus on—renewal risks, expansion opportunities, or a general account overview?\n\nI can have a one-page briefing ready for you to review on Wednesday.", true), issueId: task.id, runId: null },
+ ];
+ }
if (scenario === "long" && a.id === agent.id)
history = [
...Array.from({ length: 24 }, (_, i) => ({
@@ -310,12 +332,12 @@ export function AgentChatPrototype({
state: {
tabs: taskComparison
? [taskPanelPropertiesTab()]
- : [
- taskPanelDocumentTab("plan", "Launch plan"),
+ : sidebarScenario && !hasPlan ? [taskPanelSubtasksTab()] : [
+ taskPanelDocumentTab("plan", sidebarScenario ? "Customer briefing" : "Launch plan"),
taskPanelArtifactsTab(),
taskPanelSubtasksTab(),
],
- activeTabId: taskComparison ? "properties" : "document:plan",
+ activeTabId: taskComparison ? "properties" : sidebarScenario && !hasPlan ? "subtasks" : "document:plan",
},
launcherOpen: false,
userInteracted: true,
@@ -343,6 +365,7 @@ export function AgentChatPrototype({
init?.body && typeof init.body === "string"
? JSON.parse(init.body)
: {};
+ if (/\/companies\/[^/]+\/chats$/.test(path)) return Response.json([...chats.values()]);
const chatRef = path.match(/\/chats\/([^/]+)$/)?.[1];
if (chatRef) {
const a = fixtureAgents.find(
@@ -356,8 +379,8 @@ export function AgentChatPrototype({
conversationAgentId: a.id,
conversationUserId: "user-board",
conversationState: "waiting",
- id: chatIssueId(a.id),
- identifier: entryScenario ? `PAP-${400 + fixtureAgents.findIndex((item) => item.id === a.id)}` : chatIdentifier(a.id),
+ id: chatIssueId(a.id, fixtureAgents),
+ identifier: sidebarScenario ? `PAP-${500 + fixtureAgents.findIndex((item) => item.id === a.id)}` : entryScenario ? `PAP-${400 + fixtureAgents.findIndex((item) => item.id === a.id)}` : chatIdentifier(a.id),
assigneeAgentId: a.id,
title: `Chat with ${a.name}`,
});
@@ -450,7 +473,7 @@ export function AgentChatPrototype({
return Response.json({ activePauseHold: null, activeHolds: [] });
if (path.endsWith("/runs"))
return Response.json(
- task.id === issue.id && scenario !== "empty"
+ task.id === issue.id && scenario !== "empty" && !sidebarScenario
? [
{
...run,
@@ -495,14 +518,15 @@ export function AgentChatPrototype({
],
},
}]);
+ const currentPlan = sidebarScenario ? { ...plan, title: "Customer briefing", body: "# Acme customer briefing\n\nPrepare a one-page briefing for Thursday’s meeting.\n\n## Confirm the focus\nClarify whether to prioritize renewal risks, expansion opportunities, or a general account overview.\n\n## Gather context\nReview customer notes, product usage, and recent feedback. Link the sources.\n\n## Draft for review\nHave the briefing ready on Wednesday, with clear next steps for the meeting." } : plan;
if (path.endsWith("/documents/plan"))
- return task.id === issue.id && scenario !== "empty"
- ? Response.json(plan)
+ return task.id === issue.id && hasPlan
+ ? Response.json(currentPlan)
: Response.json({ error: "No plan" }, { status: 404 });
if (path.endsWith("/documents/notes")) return Response.json(notes);
if (path.endsWith("/documents"))
return Response.json(
- task.id === issue.id && scenario !== "empty" ? [plan, notes] : [],
+ task.id === issue.id && hasPlan ? (sidebarScenario ? [currentPlan] : [plan, notes]) : [],
);
if (/\/issues\/[^/]+$/.test(path))
return Response.json(
@@ -512,7 +536,7 @@ export function AgentChatPrototype({
/\/companies\/[^/]+\/issues$/.test(path) &&
(url.searchParams.has("parentId") || url.searchParams.has("descendantOf"))
)
- return Response.json(scenario === "empty" ? [] : [child]);
+ return Response.json(scenario === "empty" || sidebarScenario ? [] : [child]);
if (/\/companies\/[^/]+\/agents$/.test(path))
return Response.json(fixtureAgents);
if (/\/agents\/[^/]+$/.test(path))
@@ -537,9 +561,33 @@ export function AgentChatPrototype({
return Response.json([]);
};
queryClient.clear();
+ if (sidebarScenario) {
+ // These stories review a settled page, not a chain of loading screens.
+ // Seed the same query keys and shapes consumed by the production chat.
+ queryClient.setQueryData(queryKeys.agentChats.list(issue.companyId, "user-board"), [...chats.values()]);
+ queryClient.setQueryData(queryKeys.auth.session, storybookAuthSession);
+ queryClient.setQueryData(queryKeys.companies.list("user-board"), { companies: storybookCompanies, unauthorized: false });
+ queryClient.setQueryData(queryKeys.agents.list(issue.companyId), fixtureAgents);
+ queryClient.setQueryData(queryKeys.instance.experimentalSettings, {
+ enableAgentChat: scenario !== "disabled", enableStreamlinedUi: true,
+ enableClassicTaskInterface: false, enableExperimentalFileViewer: true,
+ });
+ for (const a of fixtureAgents) {
+ const task = chats.get(a.id) ?? null;
+ queryClient.setQueryData(queryKeys.agentChats.detail(issue.companyId, "user-board", a.id), task);
+ if (task) {
+ queryClient.setQueryData(queryKeys.issues.detail(task.id), task);
+ queryClient.setQueryData(queryKeys.issues.comments(task.id), { pages: [[...(messages.get(task.id) ?? [])].reverse()], pageParams: [null] });
+ }
+ }
+ }
setReady(true);
return () => {
window.fetch = originalFetch;
+ if (sidebarScenario) {
+ if (previousRecentTasks === null) localStorage.removeItem(recentTasksKey);
+ else localStorage.setItem(recentTasksKey, previousRecentTasks);
+ }
if (entryScenario) {
if (previousRecents === null) localStorage.removeItem(recentKey);
else localStorage.setItem(recentKey, previousRecents);
@@ -547,21 +595,21 @@ export function AgentChatPrototype({
}
queryClient.clear();
};
- }, [scenario, taskComparison, entryScenario, queryClient]);
- useEffect(() => {
+ }, [scenario, taskComparison, entryScenario, sidebarScenario, contextInitiallyOpen, queryClient]);
+ useLayoutEffect(() => {
if (ready && !initialRouteSet.current) {
initialRouteSet.current = true;
navigate(
- `/PAP/${entryScenario === "first-use" ? "chats" : entryScenario === "paused" ? "chats/operations" : taskComparison ? `issues/${issue.id}` : "chats/agent-codex"}`,
+ `/PAP/${sidebarScenario === "landing" || entryScenario === "first-use" ? "chats" : entryScenario === "paused" ? "chats/operations" : taskComparison ? `issues/${issue.id}` : "chats/agent-codex"}`,
{ replace: true },
);
}
- }, [ready, navigate, taskComparison, entryScenario]);
- if (!ready) return null;
+ }, [ready, navigate, taskComparison, entryScenario, sidebarScenario]);
+ if (!ready || pathname === "/PAP/storybook") return null;
const routes = (
- : undefined} />}>
- } />
+ : : undefined} />}>
+ : } />
} />
} />
} />
diff --git a/ui/storybook/stories/agent-chat-sidebar.stories.tsx b/ui/storybook/stories/agent-chat-sidebar.stories.tsx
new file mode 100644
index 00000000000..ec3e14b8f7b
--- /dev/null
+++ b/ui/storybook/stories/agent-chat-sidebar.stories.tsx
@@ -0,0 +1,31 @@
+import { userEvent, within } from "storybook/test";
+import type { Meta, StoryObj } from "@storybook/react-vite";
+import { AgentChatPrototype } from "../prototypes/agent-chat/AgentChatPrototype";
+
+const meta = {
+ title: "Design explorations/Agent chat sidebar/Pages",
+ component: AgentChatPrototype,
+ parameters: { layout: "fullscreen", docs: { description: { component:
+ "The production chat sidebar and pages inside the real Paperclip Layout, including the existing transcript, composer, and context panel. Click Chat to see the landing page, search by name or role, or use + to start or reopen an agent conversation. Each agent has one conversation; sends stay in this Storybook's in-memory fixtures."
+ } } },
+ args: { sidebarScenario: "conversation", scenario: "returning", contextInitiallyOpen: false },
+ render: args => ,
+} satisfies Meta;
+export default meta;
+type Story = StoryObj;
+
+export const Conversation: Story = { name: "01 · Conversation · Start here" };
+export const ChatLanding: Story = { name: "02 · Click Chat · Choose an agent", args: { sidebarScenario: "landing", scenario: "empty" } };
+export const FirstMessage: Story = { name: "03 · First conversation", args: { scenario: "empty" } };
+export const LargerTeam: Story = { name: "04 · Larger team · Scroll and search", args: { sidebarScenario: "large-team" } };
+export const ContextPanel: Story = { name: "05 · Existing plan panel", args: { contextInitiallyOpen: true } };
+export const Light: Story = { name: "06 · Light theme", globals: { theme: "light" } };
+export const Mobile: Story = { name: "07 · Mobile · Agents in navigation drawer", parameters: { waitForViewport: true }, globals: { viewport: { value: "mobile", isRotated: false } } };
+export const MobileLanding: Story = { name: "08 · Mobile · Choose an agent", args: { sidebarScenario: "landing", scenario: "empty" }, parameters: { waitForViewport: true }, globals: { viewport: { value: "mobile", isRotated: false } } };
+
+export const AddChat: Story = {
+ name: "09 · Add chat · One per agent",
+ play: async ({ canvasElement }) => {
+ await userEvent.click(await within(canvasElement).findByRole("button", { name: "Add chat" }));
+ },
+};
diff --git a/ui/storybook/stories/agent-conversation-sidebar.stories.tsx b/ui/storybook/stories/agent-conversation-sidebar.stories.tsx
new file mode 100644
index 00000000000..f0917a40002
--- /dev/null
+++ b/ui/storybook/stories/agent-conversation-sidebar.stories.tsx
@@ -0,0 +1,58 @@
+import { userEvent, within, expect } from "storybook/test";
+import { useState } from "react";
+import type { Meta, StoryObj } from "@storybook/react-vite";
+import { AgentConversationSidebar } from "../prototypes/agent-chat-sidebar/AgentConversationSidebar";
+import { sidebarAgents, sidebarPreviews, sidebarRoster } from "../prototypes/agent-chat-sidebar/fixtures";
+
+function InteractiveSidebar(args: React.ComponentProps) {
+ const [activeId, setActiveId] = useState(args.activeId);
+ const [agents, setAgents] = useState(args.agents);
+ const selectAgent: NonNullable = agent => {
+ setActiveId(agent.id);
+ setAgents(current => current.some(item => item.id === agent.id) ? current : [...current, agent]);
+ };
+ return ;
+}
+const meta = {
+ title: "Design explorations/Agent chat sidebar/Components",
+ component: AgentConversationSidebar,
+ parameters: { layout: "fullscreen", docs: { description: { component:
+ "Secondary agent navigation at the same width as the app's other contextual sidebars. Uses production AgentAvatar, Input, and Button. The entire row is a link; selection and search are interactive. Search matches agent name, title, and role. Escape clears the query. Browse all agents links to the existing agent directory in the page stories."
+ } } },
+ args: { agents: sidebarAgents, availableAgents: sidebarRoster(), activeId: "agent-codex", previews: sidebarPreviews },
+ render: args => ,
+} satisfies Meta;
+export default meta;
+type Story = StoryObj;
+export const Default: Story = { name: "01 · Agent list · Interactive" };
+export const Search: Story = { name: "02 · Search by role", args: { initialSearch: "research" } };
+export const NoResults: Story = { name: "03 · No matches", args: { initialSearch: "accountant" } };
+export const Loading: Story = { name: "04 · Loading", args: { agents: [], availableAgents: [], loading: true } };
+export const NoAgents: Story = { name: "05 · No agents", args: { agents: [], availableAgents: [] } };
+export const LargeTeam: Story = { name: "06 · Scrollable team", args: { agents: sidebarRoster("large-team"), availableAgents: sidebarRoster("large-team") } };
+export const LongNames: Story = { name: "07 · Long names and paused agent", args: { agents: [
+ { ...sidebarAgents[0], name: "Customer & Competitive Research", title: "Research across customers, markets, and competitors" },
+ { ...sidebarAgents[1], status: "paused" },
+ ...sidebarAgents.slice(2),
+], previews: {} } };
+export const Light: Story = { name: "08 · Light theme", globals: { theme: "light" } };
+export const HistoryUnavailable: Story = {
+ name: "10 · Partial history failure",
+ args: { historyError: new Error("History unavailable") },
+};
+
+export const AddAndReopenChat: Story = {
+ name: "09 · Add and reopen · One per agent",
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ const page = within(canvasElement.ownerDocument.body);
+ await userEvent.click(canvas.getByRole("button", { name: "Add chat" }));
+ await userEvent.type(await page.findByPlaceholderText("Search by name or role…"), "Product Designer");
+ await userEvent.click(page.getByRole("option", { name: /Product Designer.*New chat/ }));
+ await expect(await canvas.findByRole("link", { name: /Product Designer/ })).toHaveAttribute("aria-current", "page");
+ await userEvent.click(canvas.getByRole("button", { name: "Add chat" }));
+ await userEvent.type(await page.findByPlaceholderText("Search by name or role…"), "Product Designer");
+ await userEvent.click(page.getByRole("option", { name: /Product Designer.*Open chat/ }));
+ await expect(await canvas.findAllByRole("link", { name: /Product Designer/ })).toHaveLength(1);
+ },
+};