import type { Meta, StoryObj } from '@storybook/react' import { useState } from 'react' import { MessageList } from './message-list' import type { GroupedMessage } from '../types/run' import type { SessionPart } from '../types/parts' const meta: Meta = { title: 'Chat/MessageList', component: MessageList, parameters: { layout: 'fullscreen', backgrounds: { default: 'dark' }, }, decorators: [ (Story) => (
), ], } export default meta type Story = StoryObj // --------------------------------------------------------------------------- // Mock data — a realistic agent coding session // --------------------------------------------------------------------------- const NOW = Date.now() const t = (offsetSeconds: number) => NOW - offsetSeconds * 1000 // User messages const userMsg1 = { id: 'user-1', role: 'user' as const, time: { created: t(120) }, } const userMsg2 = { id: 'user-2', role: 'user' as const, time: { created: t(60) }, } // Assistant messages (each run has one or more) const assistantMsg1 = { id: 'asst-1', role: 'assistant' as const, time: { created: t(115), completed: t(100) }, } const assistantMsg2 = { id: 'asst-2', role: 'assistant' as const, time: { created: t(55), completed: t(30) }, } const groups: GroupedMessage[] = [ { type: 'user', message: userMsg1 }, { type: 'run', run: { id: 'run-1', messages: [assistantMsg1], isComplete: true, isStreaming: false, summaryText: 'Read 3 files, identified the staleTime misconfiguration', finalTextPart: { messageId: 'asst-1', partIndex: 3, text: 'The issue is your default `staleTime` is 0.', }, stats: { toolCount: 3, messageCount: 1, thinkingDurationMs: 4200, textPartCount: 1, toolCategories: new Set(['read', 'search'] as const), }, }, }, { type: 'user', message: userMsg2 }, { type: 'run', run: { id: 'run-2', messages: [assistantMsg2], isComplete: true, isStreaming: false, summaryText: 'Patched QueryClient config and updated the staleTime', finalTextPart: { messageId: 'asst-2', partIndex: 5, text: 'Done — staleTime set to 60s, cache flicker resolved.', }, stats: { toolCount: 2, messageCount: 1, thinkingDurationMs: 2100, textPartCount: 2, toolCategories: new Set(['read', 'edit'] as const), }, }, }, ] // Part map — parts keyed by message ID const partMap: Record = { 'user-1': [ { type: 'text', text: 'Can you help me debug why my React Query cache is not working? Every component mount triggers a refetch even for data that was just fetched.', }, ], 'user-2': [ { type: 'text', text: 'Perfect, please fix it — set staleTime to 60 seconds globally.', }, ], 'asst-1': [ { type: 'reasoning', text: 'The user is experiencing stale data refetches on mount. Default staleTime in TanStack Query v5 is 0, which causes this. I should read their QueryClient setup to confirm.', time: { start: t(115), end: t(112) }, }, { type: 'tool', id: 'tool-1', tool: 'read', callID: 'call-1', state: { status: 'completed', input: { path: 'src/lib/query-client.ts' }, output: 'export const queryClient = new QueryClient()', time: { start: t(112), end: t(111) }, }, }, { type: 'tool', id: 'tool-2', tool: 'grep', callID: 'call-2', state: { status: 'completed', input: { pattern: 'staleTime', path: 'src' }, output: 'No matches found.', time: { start: t(111), end: t(110) }, }, }, { type: 'tool', id: 'tool-3', tool: 'glob', callID: 'call-3', state: { status: 'completed', input: { pattern: 'src/**/*.ts' }, output: 'src/lib/query-client.ts\nsrc/hooks/useData.ts\nsrc/app/layout.tsx', time: { start: t(110), end: t(109) }, }, }, { type: 'text', text: `The issue is your default \`staleTime\` is \`0\`, which is the TanStack Query v5 default. This means React Query considers all cached data immediately stale, triggering a background refetch on every component mount. Your current setup: \`\`\`typescript export const queryClient = new QueryClient() // No defaultOptions — staleTime defaults to 0ms \`\`\` No \`staleTime\` configuration was found anywhere in the codebase. Set it globally to fix the flicker.`, }, ], 'asst-2': [ { type: 'tool', id: 'tool-4', tool: 'read', callID: 'call-4', state: { status: 'completed', input: { path: 'src/lib/query-client.ts' }, output: 'export const queryClient = new QueryClient()', time: { start: t(55), end: t(54) }, }, }, { type: 'tool', id: 'tool-5', tool: 'edit', callID: 'call-5', state: { status: 'completed', input: { path: 'src/lib/query-client.ts', old_string: 'new QueryClient()', new_string: 'new QueryClient({ defaultOptions: { queries: { staleTime: 60_000 } } })', }, output: 'File updated.', time: { start: t(54), end: t(53) }, }, }, { type: 'text', text: `Done. Updated \`src/lib/query-client.ts\`: \`\`\`typescript export const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 60_000, // 60 seconds }, }, }) \`\`\` Cached data is now considered fresh for 60 seconds. Components that mount within that window will read from cache without triggering a refetch.`, }, ], } // --------------------------------------------------------------------------- // Interactive wrapper — handles collapse state // --------------------------------------------------------------------------- function InteractiveMessageList({ groups: g, partMap: pm, }: { groups: GroupedMessage[] partMap: Record }) { const [collapsed, setCollapsed] = useState>({}) return ( collapsed[runId] ?? false} onToggleCollapse={(runId) => setCollapsed((prev) => ({ ...prev, [runId]: !prev[runId] })) } /> ) } export const FullConversation: Story = { render: () => , } export const WithCollapsedRuns: Story = { render: () => { // Render with runs collapsed by default const [collapsed, setCollapsed] = useState>({ 'run-1': true, 'run-2': true, }) return ( collapsed[runId] ?? false} onToggleCollapse={(runId) => setCollapsed((prev) => ({ ...prev, [runId]: !prev[runId] })) } /> ) }, } export const StreamingRun: Story = { render: () => { const streamingGroups: GroupedMessage[] = [ { type: 'user', message: userMsg1 }, { type: 'run', run: { id: 'run-streaming', messages: [assistantMsg1], isComplete: false, isStreaming: true, summaryText: null, finalTextPart: null, stats: { toolCount: 1, messageCount: 1, thinkingDurationMs: 0, textPartCount: 0, toolCategories: new Set(['read'] as const), }, }, }, ] const streamingPartMap: Record = { 'user-1': partMap['user-1'], 'asst-1': [ { type: 'tool', id: 'tool-1', tool: 'read', callID: 'call-1', state: { status: 'running', input: { path: 'src/lib/query-client.ts' }, time: { start: t(2) }, }, }, ], } return ( false} onToggleCollapse={() => {}} /> ) }, } export const OnlyUserMessages: Story = { render: () => ( false} onToggleCollapse={() => {}} /> ), }