-
-
Chat ({messages.length})
-
+
+
Jump to latest
-
+
-
-
+
+
This issue conversation is empty. Start with a message below.
+
-
+ {showComposer ? (
+
+ ) : null}
);
diff --git a/ui/src/fixtures/issueChatUxFixtures.ts b/ui/src/fixtures/issueChatUxFixtures.ts
new file mode 100644
index 00000000..17925c00
--- /dev/null
+++ b/ui/src/fixtures/issueChatUxFixtures.ts
@@ -0,0 +1,308 @@
+import type { Agent, FeedbackVote } from "@paperclipai/shared";
+import type { LiveRunForIssue } from "../api/heartbeats";
+import type { InlineEntityOption } from "../components/InlineEntitySelector";
+import type { MentionOption } from "../components/MarkdownEditor";
+import type {
+ IssueChatComment,
+ IssueChatLinkedRun,
+ IssueChatTranscriptEntry,
+} from "../lib/issue-chat-messages";
+import type { IssueTimelineEvent } from "../lib/issue-timeline-events";
+
+function createAgent(
+ id: string,
+ name: string,
+ icon: string,
+ urlKey: string,
+): Agent {
+ const now = new Date("2026-04-06T12:00:00.000Z");
+ return {
+ id,
+ companyId: "company-ux",
+ name,
+ urlKey,
+ role: "engineer",
+ title: null,
+ icon,
+ status: "active",
+ reportsTo: null,
+ capabilities: null,
+ adapterType: "codex_local",
+ adapterConfig: {},
+ runtimeConfig: {},
+ budgetMonthlyCents: 0,
+ spentMonthlyCents: 0,
+ lastHeartbeatAt: null,
+ metadata: null,
+ createdAt: now,
+ updatedAt: now,
+ pauseReason: null,
+ pausedAt: null,
+ permissions: { canCreateAgents: false },
+ };
+}
+
+function createComment(overrides: Partial
): IssueChatComment {
+ return {
+ id: "comment-default",
+ companyId: "company-ux",
+ issueId: "issue-ux",
+ authorAgentId: null,
+ authorUserId: "user-1",
+ body: "",
+ createdAt: new Date("2026-04-06T12:00:00.000Z"),
+ updatedAt: new Date("2026-04-06T12:00:00.000Z"),
+ ...overrides,
+ };
+}
+
+const primaryAgent = createAgent("agent-1", "CodexCoder", "code", "codexcoder");
+const reviewAgent = createAgent("agent-2", "ClaudeFixer", "sparkles", "claudefixer");
+
+export const issueChatUxAgentMap = new Map([
+ [primaryAgent.id, primaryAgent],
+ [reviewAgent.id, reviewAgent],
+]);
+
+export const issueChatUxMentions: MentionOption[] = [
+ {
+ id: "mention-agent-1",
+ name: primaryAgent.name,
+ kind: "agent",
+ agentId: primaryAgent.id,
+ agentIcon: primaryAgent.icon,
+ },
+ {
+ id: "mention-agent-2",
+ name: reviewAgent.name,
+ kind: "agent",
+ agentId: reviewAgent.id,
+ agentIcon: reviewAgent.icon,
+ },
+ {
+ id: "mention-project-1",
+ name: "Paperclip Board UI",
+ kind: "project",
+ projectId: "project-1",
+ projectColor: "#0f766e",
+ },
+];
+
+export const issueChatUxReassignOptions: InlineEntityOption[] = [
+ {
+ id: `agent:${primaryAgent.id}`,
+ label: primaryAgent.name,
+ searchText: `${primaryAgent.name} codex engineer`,
+ },
+ {
+ id: `agent:${reviewAgent.id}`,
+ label: reviewAgent.name,
+ searchText: `${reviewAgent.name} claude reviewer`,
+ },
+ {
+ id: "user:user-1",
+ label: "Board",
+ searchText: "board user",
+ },
+];
+
+export const issueChatUxLiveComments: IssueChatComment[] = [
+ createComment({
+ id: "comment-live-user",
+ body: "Ship the issue page as a real chat. Keep the activity feed, but make the assistant flow feel conversational.",
+ createdAt: new Date("2026-04-06T11:55:00.000Z"),
+ updatedAt: new Date("2026-04-06T11:55:00.000Z"),
+ }),
+ createComment({
+ id: "comment-live-agent",
+ authorAgentId: primaryAgent.id,
+ authorUserId: null,
+ body: "I swapped the old comment stack for the new assistant-ui thread and kept the existing issue mutations intact.",
+ createdAt: new Date("2026-04-06T12:01:00.000Z"),
+ updatedAt: new Date("2026-04-06T12:01:00.000Z"),
+ runId: "run-history-1",
+ runAgentId: primaryAgent.id,
+ }),
+ createComment({
+ id: "comment-live-queued",
+ body: "Can you also make a dedicated review page that shows every chat state side by side?",
+ createdAt: new Date("2026-04-06T12:05:30.000Z"),
+ updatedAt: new Date("2026-04-06T12:05:30.000Z"),
+ clientId: "client-queued-1",
+ clientStatus: "queued",
+ queueState: "queued",
+ queueTargetRunId: "run-live-1",
+ }),
+];
+
+export const issueChatUxLiveEvents: IssueTimelineEvent[] = [
+ {
+ id: "event-live-1",
+ createdAt: new Date("2026-04-06T11:54:00.000Z"),
+ actorType: "user",
+ actorId: "user-1",
+ statusChange: {
+ from: "done",
+ to: "todo",
+ },
+ },
+ {
+ id: "event-live-2",
+ createdAt: new Date("2026-04-06T11:54:30.000Z"),
+ actorType: "user",
+ actorId: "user-1",
+ assigneeChange: {
+ from: { agentId: null, userId: null },
+ to: { agentId: primaryAgent.id, userId: null },
+ },
+ },
+];
+
+export const issueChatUxLiveRuns: LiveRunForIssue[] = [
+ {
+ id: "run-live-1",
+ status: "running",
+ invocationSource: "manual",
+ triggerDetail: null,
+ startedAt: "2026-04-06T12:04:00.000Z",
+ finishedAt: null,
+ createdAt: "2026-04-06T12:04:00.000Z",
+ agentId: primaryAgent.id,
+ agentName: primaryAgent.name,
+ adapterType: "codex_local",
+ issueId: "issue-ux",
+ },
+];
+
+export const issueChatUxLinkedRuns: IssueChatLinkedRun[] = [
+ {
+ runId: "run-history-1",
+ status: "succeeded",
+ agentId: primaryAgent.id,
+ createdAt: new Date("2026-04-06T11:58:00.000Z"),
+ startedAt: new Date("2026-04-06T11:58:00.000Z"),
+ finishedAt: new Date("2026-04-06T12:00:00.000Z"),
+ },
+ {
+ runId: "run-review-1",
+ status: "failed",
+ agentId: reviewAgent.id,
+ createdAt: new Date("2026-04-06T12:31:00.000Z"),
+ startedAt: new Date("2026-04-06T12:31:00.000Z"),
+ finishedAt: new Date("2026-04-06T12:33:00.000Z"),
+ },
+];
+
+export const issueChatUxTranscriptsByRunId = new Map([
+ [
+ "run-live-1",
+ [
+ {
+ kind: "assistant",
+ ts: "2026-04-06T12:04:02.000Z",
+ text: "I am reshaping the issue page so the thread reads like a conversation instead of a run log.",
+ },
+ {
+ kind: "thinking",
+ ts: "2026-04-06T12:04:05.000Z",
+ text: "Need to remove the internal scrollbox first, otherwise the page still feels like a nested console.",
+ },
+ {
+ kind: "tool_call",
+ ts: "2026-04-06T12:04:08.000Z",
+ name: "read_file",
+ toolUseId: "tool-read-1",
+ input: { path: "ui/src/components/IssueChatThread.tsx" },
+ },
+ {
+ kind: "tool_result",
+ ts: "2026-04-06T12:04:11.000Z",
+ toolUseId: "tool-read-1",
+ content: "Loaded the current chat surface and found the max-h viewport constraint.",
+ isError: false,
+ },
+ {
+ kind: "tool_call",
+ ts: "2026-04-06T12:04:14.000Z",
+ name: "apply_patch",
+ toolUseId: "tool-edit-1",
+ input: { file: "ui/src/components/IssueChatThread.tsx", action: "remove scroll pane" },
+ },
+ {
+ kind: "tool_result",
+ ts: "2026-04-06T12:04:22.000Z",
+ toolUseId: "tool-edit-1",
+ content: "Updated layout classes and swapped Jump to latest to page-level scrolling.",
+ isError: false,
+ },
+ {
+ kind: "stderr",
+ ts: "2026-04-06T12:04:24.000Z",
+ text: "vite warm-up: rebuilding route chunks",
+ },
+ ],
+ ],
+]);
+
+export const issueChatUxReviewComments: IssueChatComment[] = [
+ createComment({
+ id: "comment-review-user",
+ body: "This looks close. Tighten the spacing and keep the composer grounded to the chat surface.",
+ createdAt: new Date("2026-04-06T12:28:00.000Z"),
+ updatedAt: new Date("2026-04-06T12:28:00.000Z"),
+ }),
+ createComment({
+ id: "comment-review-agent",
+ authorAgentId: reviewAgent.id,
+ authorUserId: null,
+ body: [
+ "Adjusted the treatment to feel more like a product conversation.",
+ "",
+ "- Removed the count from the heading",
+ "- Let the page own scrolling",
+ "- Added a dedicated `/tests/ux/chat` review page",
+ ].join("\n"),
+ createdAt: new Date("2026-04-06T12:34:00.000Z"),
+ updatedAt: new Date("2026-04-06T12:34:00.000Z"),
+ runId: "run-review-1",
+ runAgentId: reviewAgent.id,
+ }),
+ createComment({
+ id: "comment-review-user-followup",
+ body: "Perfect. I also want to see an empty state and a blocked composer state before we merge.",
+ createdAt: new Date("2026-04-06T12:36:00.000Z"),
+ updatedAt: new Date("2026-04-06T12:36:00.000Z"),
+ }),
+];
+
+export const issueChatUxReviewEvents: IssueTimelineEvent[] = [
+ {
+ id: "event-review-1",
+ createdAt: new Date("2026-04-06T12:27:00.000Z"),
+ actorType: "user",
+ actorId: "user-1",
+ assigneeChange: {
+ from: { agentId: primaryAgent.id, userId: null },
+ to: { agentId: reviewAgent.id, userId: null },
+ },
+ },
+];
+
+export const issueChatUxFeedbackVotes: FeedbackVote[] = [
+ {
+ id: "feedback-1",
+ companyId: "company-ux",
+ issueId: "issue-ux",
+ targetType: "issue_comment",
+ targetId: "comment-review-agent",
+ authorUserId: "user-1",
+ vote: "up",
+ reason: null,
+ sharedWithLabs: false,
+ sharedAt: null,
+ consentVersion: null,
+ redactionSummary: null,
+ createdAt: new Date("2026-04-06T12:35:00.000Z"),
+ updatedAt: new Date("2026-04-06T12:35:00.000Z"),
+ },
+];
diff --git a/ui/src/pages/IssueChatUxLab.tsx b/ui/src/pages/IssueChatUxLab.tsx
new file mode 100644
index 00000000..357943fe
--- /dev/null
+++ b/ui/src/pages/IssueChatUxLab.tsx
@@ -0,0 +1,240 @@
+import { useState, type ReactNode } from "react";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { IssueChatThread } from "../components/IssueChatThread";
+import {
+ issueChatUxAgentMap,
+ issueChatUxFeedbackVotes,
+ issueChatUxLinkedRuns,
+ issueChatUxLiveComments,
+ issueChatUxLiveEvents,
+ issueChatUxLiveRuns,
+ issueChatUxMentions,
+ issueChatUxReassignOptions,
+ issueChatUxReviewComments,
+ issueChatUxReviewEvents,
+ issueChatUxTranscriptsByRunId,
+} from "../fixtures/issueChatUxFixtures";
+import { cn } from "../lib/utils";
+import { Bot, FlaskConical, MessagesSquare, Route, Sparkles, WandSparkles } from "lucide-react";
+
+const noop = async () => {};
+
+const highlights = [
+ "Running assistant replies with streamed text, reasoning, tool cards, and noisy notices",
+ "Historical issue events and linked runs rendered inline with the chat timeline",
+ "Queued user messages, settled assistant comments, and feedback controls",
+ "Empty and disabled-composer states without relying on live backend data",
+];
+
+function LabSection({
+ id,
+ eyebrow,
+ title,
+ description,
+ accentClassName,
+ children,
+}: {
+ id?: string;
+ eyebrow: string;
+ title: string;
+ description: string;
+ accentClassName?: string;
+ children: ReactNode;
+}) {
+ return (
+
+
+
+
+ {eyebrow}
+
+
{title}
+
{description}
+
+
+ {children}
+
+ );
+}
+
+export function IssueChatUxLab() {
+ const [showComposer, setShowComposer] = useState(true);
+
+ return (
+
+
+
+
+
+
+ Chat UX Lab
+
+
Issue chat review surface
+
+ This page exercises the real assistant-ui issue chat with fixture-backed messages. Use it to review
+ spacing, chronology, running states, tool rendering, activity rows, queueing, and composer behavior
+ without needing a live issue in progress.
+
+
+
+
+ /tests/ux/chat
+
+
+ assistant-ui thread
+
+
+ fixture-backed live run
+
+
+
+
+
+
+
+
+
+
+
+ issueChatUxTranscriptsByRunId.has(runId)}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Review checklist
+
+ What to evaluate on this page
+
+ This route should be the fastest way to inspect the chat system before or after tweaks.
+
+
+
+
+
+
+ Message hierarchy
+
+ Check that user, assistant, and system rows scan differently without feeling like separate products.
+
+
+
+
+ Stream polish
+
+ Watch the live preview for reasoning density, tool expansion behavior, and queued follow-up readability.
+
+
+
+
+
+
+ );
+}