import type { Meta, StoryObj } from "@storybook/react-vite"; import type { Agent, FeedbackVote, IssueComment } from "@paperclipai/shared"; import type { TranscriptEntry } from "@/adapters"; import type { LiveRunForIssue } from "@/api/heartbeats"; import { CommentThread } from "@/components/CommentThread"; import { IssueChatThread } from "@/components/IssueChatThread"; import { RunChatSurface } from "@/components/RunChatSurface"; import type { InlineEntityOption } from "@/components/InlineEntitySelector"; import type { MentionOption } from "@/components/MarkdownEditor"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import type { IssueChatComment, IssueChatLinkedRun, IssueChatTranscriptEntry, } from "@/lib/issue-chat-messages"; import type { IssueTimelineEvent } from "@/lib/issue-timeline-events"; import { storybookAgentMap, storybookAgents } from "../fixtures/paperclipData"; const companyId = "company-storybook"; const projectId = "project-board-ui"; const issueId = "issue-chat-comments"; const currentUserId = "user-board"; type StoryComment = IssueComment & { runId?: string | null; runAgentId?: string | null; clientId?: string; clientStatus?: "pending" | "queued"; queueState?: "queued"; queueTargetRunId?: string | null; }; const codexAgent = storybookAgents.find((agent) => agent.id === "agent-codex") ?? storybookAgents[0]!; const qaAgent = storybookAgents.find((agent) => agent.id === "agent-qa") ?? storybookAgents[1]!; const ctoAgent = storybookAgents.find((agent) => agent.id === "agent-cto") ?? storybookAgents[2]!; const boardUserLabels = new Map([ ["user-board", "Riley Board"], ["user-product", "Mara Product"], ]); function Section({ eyebrow, title, children, }: { eyebrow: string; title: string; children: React.ReactNode; }) { return (
{eyebrow}

{title}

{children}
); } function ScenarioCard({ title, description, children, }: { title: string; description: string; children: React.ReactNode; }) { return ( {title} {description} {children} ); } function createComment(overrides: Partial): StoryComment { const createdAt = overrides.createdAt ?? new Date("2026-04-20T14:00:00.000Z"); const authorAgentId = overrides.authorAgentId ?? null; return { id: "comment-default", companyId, issueId, authorAgentId: null, authorUserId: currentUserId, body: "", authorType: authorAgentId ? "agent" : "user", presentation: null, metadata: null, createdAt, updatedAt: overrides.updatedAt ?? createdAt, ...overrides, }; } function createSystemEvent(overrides: Partial): IssueTimelineEvent { return { id: "event-default", createdAt: new Date("2026-04-20T14:00:00.000Z"), actorType: "system", actorId: "paperclip", statusChange: { from: "todo", to: "in_progress", }, ...overrides, }; } const mentionOptions: MentionOption[] = [ { id: `agent:${codexAgent.id}`, name: codexAgent.name, kind: "agent", agentId: codexAgent.id, agentIcon: codexAgent.icon, }, { id: `agent:${qaAgent.id}`, name: qaAgent.name, kind: "agent", agentId: qaAgent.id, agentIcon: qaAgent.icon, }, { id: `project:${projectId}`, name: "Board UI", kind: "project", projectId, projectColor: "#0f766e", }, ]; const reassignOptions: InlineEntityOption[] = [ { id: `agent:${codexAgent.id}`, label: codexAgent.name, searchText: `${codexAgent.name} engineer codex`, }, { id: `agent:${qaAgent.id}`, label: qaAgent.name, searchText: `${qaAgent.name} qa browser review`, }, { id: `agent:${ctoAgent.id}`, label: ctoAgent.name, searchText: `${ctoAgent.name} architecture review`, }, { id: `user:${currentUserId}`, label: "Riley Board", searchText: "board operator", }, ]; const singleComment = [ createComment({ id: "comment-single-board", body: "Please make the issue chat states reviewable in Storybook before the next UI pass.", createdAt: new Date("2026-04-20T13:12:00.000Z"), }), ]; const longThreadComments = [ createComment({ id: "comment-long-board", body: "The chat surface should show the operator request first, then agent progress, then review follow-up. Keep the density close to the issue page.", createdAt: new Date("2026-04-20T13:02:00.000Z"), }), createComment({ id: "comment-long-agent", authorAgentId: codexAgent.id, authorUserId: null, body: "I found the existing `IssueChatThread` and `RunChatSurface` components and am building the stories around those props.", createdAt: new Date("2026-04-20T13:08:00.000Z"), runId: "run-comment-thread-01", runAgentId: codexAgent.id, }), createComment({ id: "comment-long-product", authorUserId: "user-product", body: "Also include the old comment timeline so we can compare it with the assistant-style issue chat.", createdAt: new Date("2026-04-20T13:16:00.000Z"), }), createComment({ id: "comment-long-qa", authorAgentId: qaAgent.id, authorUserId: null, body: "QA note: the thread should stay readable with long markdown and when a queued operator reply is visible.", createdAt: new Date("2026-04-20T13:24:00.000Z"), runId: "run-comment-thread-02", runAgentId: qaAgent.id, }), ]; const markdownComments = [ createComment({ id: "comment-markdown-board", body: [ "Acceptance criteria:", "", "- Cover empty, single, and long comment states", "- Show a code block in a comment", "- Include a link to [the issue guide](/issues/PAP-1676)", "", "```ts", "const success = stories.some((story) => story.includes(\"IssueChatThread\"));", "```", ].join("\n"), createdAt: new Date("2026-04-20T13:28:00.000Z"), }), createComment({ id: "comment-mentions-agent", authorAgentId: codexAgent.id, authorUserId: null, body: "@QAChecker I added the fixture coverage. Please focus browser review on links, code blocks, and the queued comment treatment.", createdAt: new Date("2026-04-20T13:35:00.000Z"), runId: "run-markdown-01", runAgentId: codexAgent.id, }), ]; const queuedComment = createComment({ id: "comment-queued-board", body: "@CodexCoder after this run finishes, add a compact embedded variant too.", createdAt: new Date("2026-04-20T13:39:00.000Z"), clientId: "client-queued-storybook", clientStatus: "queued", queueState: "queued", queueTargetRunId: "run-live-chat-01", }); const commentTimelineEvents: IssueTimelineEvent[] = [ createSystemEvent({ id: "event-system-checkout", createdAt: new Date("2026-04-20T13:04:00.000Z"), actorType: "system", actorId: "paperclip", statusChange: { from: "todo", to: "in_progress", }, }), createSystemEvent({ id: "event-board-reassign", createdAt: new Date("2026-04-20T13:18:00.000Z"), actorType: "user", actorId: currentUserId, assigneeChange: { from: { agentId: codexAgent.id, userId: null }, to: { agentId: qaAgent.id, userId: null }, }, statusChange: undefined, }), ]; const commentLinkedRuns = [ { runId: "run-comment-thread-01", status: "succeeded", agentId: codexAgent.id, createdAt: new Date("2026-04-20T13:07:00.000Z"), startedAt: new Date("2026-04-20T13:07:00.000Z"), finishedAt: new Date("2026-04-20T13:11:00.000Z"), }, { runId: "run-comment-thread-02", status: "running", agentId: qaAgent.id, createdAt: new Date("2026-04-20T13:22:00.000Z"), startedAt: new Date("2026-04-20T13:22:00.000Z"), finishedAt: null, }, ]; const feedbackVotes: FeedbackVote[] = [ { id: "feedback-chat-comment-01", companyId, issueId, targetType: "issue_comment", targetId: "comment-issue-agent", authorUserId: currentUserId, vote: "up", reason: null, sharedWithLabs: false, sharedAt: null, consentVersion: null, redactionSummary: null, createdAt: new Date("2026-04-20T13:52:00.000Z"), updatedAt: new Date("2026-04-20T13:52:00.000Z"), }, ]; const liveRun: LiveRunForIssue = { id: "run-live-chat-01", status: "running", invocationSource: "manual", triggerDetail: "comment", createdAt: "2026-04-20T13:40:00.000Z", startedAt: "2026-04-20T13:40:02.000Z", finishedAt: null, agentId: codexAgent.id, agentName: codexAgent.name, adapterType: "codex_local", issueId, }; const liveRunTranscript: TranscriptEntry[] = [ { kind: "assistant", ts: "2026-04-20T13:40:08.000Z", text: "I am wiring the chat and comments Storybook coverage now.", }, { kind: "thinking", ts: "2026-04-20T13:40:12.000Z", text: "Need fixtures that exercise MarkdownBody, assistant-ui messages, and the embedded run transcript path without reaching the API.", }, { kind: "tool_call", ts: "2026-04-20T13:40:18.000Z", name: "rg", toolUseId: "tool-live-rg", input: { query: "IssueChatThread", cwd: "ui/src", }, }, { kind: "tool_result", ts: "2026-04-20T13:40:20.000Z", toolUseId: "tool-live-rg", content: "ui/src/components/IssueChatThread.tsx\nui/src/components/RunChatSurface.tsx", isError: false, }, { kind: "assistant", ts: "2026-04-20T13:40:31.000Z", text: [ "The live run should render code blocks as part of the assistant response:", "", "```tsx", "", "```", ].join("\n"), }, { kind: "tool_call", ts: "2026-04-20T13:40:44.000Z", name: "apply_patch", toolUseId: "tool-live-patch", input: { file: "ui/storybook/stories/chat-comments.stories.tsx", action: "add fixtures", }, }, { kind: "tool_result", ts: "2026-04-20T13:40:49.000Z", toolUseId: "tool-live-patch", content: "Added Storybook scenarios for comment thread, run chat, and issue chat.", isError: false, }, ]; const issueChatComments: IssueChatComment[] = [ createComment({ id: "comment-issue-board", body: "Please turn the comment thread into a reviewable chat surface. I need to see operator messages, agent output, system events, and live run progress together.", createdAt: new Date("2026-04-20T13:44:00.000Z"), }), createComment({ id: "comment-issue-agent", authorAgentId: codexAgent.id, authorUserId: null, body: "I kept the existing component contracts and added fixtures with realistic Paperclip work: checkout, comments, linked runs, and review feedback.", createdAt: new Date("2026-04-20T13:50:00.000Z"), runId: "run-issue-chat-01", runAgentId: codexAgent.id, }), createComment({ id: "comment-issue-system-warning", authorType: "system", authorAgentId: null, authorUserId: null, runId: "run-issue-chat-01", runAgentId: codexAgent.id, body: "Paperclip needs a disposition before this issue can continue.", presentation: { kind: "system_notice", tone: "warning", title: "Missing issue disposition", detailsDefaultOpen: false, }, metadata: { version: 1, sourceRunId: "run-issue-chat-01", sections: [ { title: "Required action", rows: [ { type: "issue_link", label: "Source issue", issueId: issueId, identifier: "PAP-3440", title: "Successful run handoff" }, { type: "agent_link", label: "Assignee", agentId: codexAgent.id, name: codexAgent.name }, { type: "key_value", label: "Status before", value: "in_progress" }, ], }, { title: "Run evidence", rows: [ { type: "run_link", label: "Successful run", runId: "run-issue-chat-01", title: "succeeded" }, { type: "key_value", label: "Normalized cause", value: "Run completed without disposition" }, ], }, ], }, createdAt: new Date("2026-04-20T13:54:00.000Z"), }), createComment({ id: "comment-issue-queued", body: "@QAChecker please do a quick visual pass after the Storybook build is green.", createdAt: new Date("2026-04-20T13:56:00.000Z"), clientId: "client-issue-queued", clientStatus: "queued", queueState: "queued", queueTargetRunId: liveRun.id, }), ]; const issueTimelineEvents: IssueTimelineEvent[] = [ createSystemEvent({ id: "event-issue-checkout", createdAt: new Date("2026-04-20T13:42:00.000Z"), actorType: "system", actorId: "paperclip", statusChange: { from: "todo", to: "in_progress", }, }), createSystemEvent({ id: "event-issue-assignee", createdAt: new Date("2026-04-20T13:43:00.000Z"), actorType: "user", actorId: currentUserId, statusChange: undefined, assigneeChange: { from: { agentId: null, userId: null }, to: { agentId: codexAgent.id, userId: null }, }, }), ]; const issueThreadNoticeReviewComments: IssueChatComment[] = [ createComment({ id: "comment-notice-board", body: "The issue thread needs to show workspace routing changes and make old missing-disposition warnings feel resolved.", createdAt: new Date("2026-04-20T13:44:00.000Z"), }), createComment({ id: "comment-notice-system-warning", authorType: "system", authorAgentId: null, authorUserId: null, runId: "run-notice-source", runAgentId: codexAgent.id, body: "Paperclip needs a disposition before this issue can continue.", presentation: { kind: "system_notice", tone: "warning", title: "Missing issue disposition", detailsDefaultOpen: false, }, metadata: { version: 1, sourceRunId: "run-notice-source", sections: [ { title: "Required action", rows: [ { type: "issue_link", label: "Source issue", issueId, identifier: "PAP-3660", title: "Show issue-thread notices" }, { type: "agent_link", label: "Assignee", agentId: codexAgent.id, name: codexAgent.name }, { type: "key_value", label: "Missing disposition", value: "clear_next_step" }, ], }, { title: "Run evidence", rows: [ { type: "run_link", label: "Completed run", runId: "run-notice-source", title: "succeeded" }, { type: "key_value", label: "Normalized cause", value: "successful_run_missing_state" }, ], }, ], }, createdAt: new Date("2026-04-20T13:48:00.000Z"), }), ]; const issueThreadNoticeReviewTimelineEvents: IssueTimelineEvent[] = [ createSystemEvent({ id: "event-notice-workspace-change", createdAt: new Date("2026-04-20T13:46:00.000Z"), statusChange: undefined, workspaceChange: { from: { label: "Project primary workspace", projectWorkspaceId: "workspace-primary", executionWorkspaceId: null, mode: "shared_workspace", }, to: { label: "PAP-3660 issue-thread-notices", projectWorkspaceId: null, executionWorkspaceId: "execution-workspace-notices", mode: "isolated_workspace", }, }, }), ]; const issueLinkedRuns: IssueChatLinkedRun[] = [ { runId: "run-issue-chat-01", status: "succeeded", agentId: codexAgent.id, agentName: codexAgent.name, adapterType: "codex_local", createdAt: new Date("2026-04-20T13:46:00.000Z"), startedAt: new Date("2026-04-20T13:46:00.000Z"), finishedAt: new Date("2026-04-20T13:51:00.000Z"), hasStoredOutput: true, }, ]; const issueTranscriptsByRunId = new Map([ [ "run-issue-chat-01", [ { kind: "thinking", ts: "2026-04-20T13:46:10.000Z", text: "Checking the existing Storybook organization before adding a new product group.", }, { kind: "tool_call", ts: "2026-04-20T13:46:16.000Z", name: "read_file", toolUseId: "tool-issue-read", input: { path: "ui/storybook/stories/overview.stories.tsx", }, }, { kind: "tool_result", ts: "2026-04-20T13:46:19.000Z", toolUseId: "tool-issue-read", content: "The coverage map already lists Chat & comments as a planned section.", isError: false, }, { kind: "assistant", ts: "2026-04-20T13:49:00.000Z", text: "Added the story file and kept every fixture local to the story so product data fixtures stay stable.", }, { kind: "diff", ts: "2026-04-20T13:49:04.000Z", changeType: "file_header", text: "diff --git a/ui/storybook/stories/chat-comments.stories.tsx b/ui/storybook/stories/chat-comments.stories.tsx", }, { kind: "diff", ts: "2026-04-20T13:49:05.000Z", changeType: "add", text: "+export const FullSurfaceMatrix: Story = {};", }, ], ], [liveRun.id, liveRunTranscript], ]); function ThreadProps({ comments, queuedComments = [], timelineEvents = [], }: { comments: StoryComment[]; queuedComments?: StoryComment[]; timelineEvents?: IssueTimelineEvent[]; }) { return ( {}} enableReassign reassignOptions={reassignOptions} currentAssigneeValue={`agent:${codexAgent.id}`} suggestedAssigneeValue={`agent:${codexAgent.id}`} mentions={mentionOptions} onInterruptQueued={async () => {}} /> ); } function CommentThreadMatrix() { return (
); } function RunChatMatrix() { return (
Run fixture shape Streaming transcript entries mixed into the same chat renderer used by issue chat.
Status running
Tool calls rg, apply_patch
Transcript entries {liveRunTranscript.length}
); } function IssueChatMatrix() { return (
{}} onVote={async () => {}} onStopRun={async () => {}} enableReassign reassignOptions={reassignOptions} currentAssigneeValue={`agent:${codexAgent.id}`} suggestedAssigneeValue={`agent:${codexAgent.id}`} mentions={mentionOptions} enableLiveTranscriptPolling={false} transcriptsByRunId={issueTranscriptsByRunId} hasOutputForRun={(runId) => issueTranscriptsByRunId.has(runId)} includeSucceededRunsWithoutOutput onInterruptQueued={async () => {}} onCancelQueued={() => undefined} />
{}} enableLiveTranscriptPolling={false} emptyMessage="No chat yet. The first operator note will start the issue conversation." /> {}} showJumpToLatest={false} enableLiveTranscriptPolling={false} composerDisabledReason="This issue is in review. Request changes or approve it from the review controls." /> undefined} onAdd={async () => {}} enableLiveTranscriptPolling={false} emptyMessage="Planning mode reply box example." />
); } function IssueThreadNoticeReview() { return (
{}} enableLiveTranscriptPolling={false} showJumpToLatest={false} />
); } function ChatCommentsStories() { return (
Chat & Comments

Threaded work conversations

Fixture-backed coverage for classic issue comments, embedded run chat, and the assistant-style issue chat surface. The scenarios use Paperclip operational content with mixed authors, system timeline events, markdown, code blocks, @mentions, links, queued comments, tool calls, and streaming run output.

); } const meta = { title: "Product/Chat & Comments", component: ChatCommentsStories, parameters: { docs: { description: { component: "Chat and comments stories exercise CommentThread, RunChatSurface, and IssueChatThread across empty, single, long, markdown, mention, timeline, queued, linked-run, and streaming transcript states.", }, }, }, } satisfies Meta; export default meta; type Story = StoryObj; export const FullSurfaceMatrix: Story = {}; export const CommentThreads: Story = { render: () => (
), }; export const LiveRunChat: Story = { render: () => (
), }; export const IssueChatWithTimeline: Story = { render: () => (
), }; export const IssueThreadNotices: Story = { render: () => , };