import { useState } from 'react' import type { Meta, StoryObj } from '@storybook/react' import { RunGroup } from './run-group' import type { Run } from '../types/run' import type { SessionPart, ToolPart, ReasoningPart, TextPart } from '../types/parts' import type { AgentBranding } from '../types/branding' const NOW = Date.now() // -- Helpers -- function msg(id: string) { return { id, role: 'assistant' as const } } function toolPart( id: string, tool: string, status: ToolPart['state']['status'], input: unknown, output?: unknown, error?: string, durationMs?: number, ): ToolPart { const start = NOW - (durationMs ?? 0) - 1000 return { type: 'tool', id, tool, state: { status, input, output, error, time: { start, end: status === 'completed' || status === 'error' ? start + (durationMs ?? 500) : undefined, }, }, } } function reasoningPart(text: string, durationMs: number): ReasoningPart { return { type: 'reasoning', text, time: { start: NOW - durationMs - 2000, end: NOW - 2000 }, } } function textPart(text: string): TextPart { return { type: 'text', text } } // -- Scenario: complete debugging run -- const debugMsg1 = msg('msg-1') const debugMsg2 = msg('msg-2') const debugMsg3 = msg('msg-3') const debugPartMap: Record = { 'msg-1': [ reasoningPart( `The user wants me to fix the failing test. Let me read the failing test first, then look at the implementation to understand the root cause. I should avoid guessing — grep for the actual error message pattern to find where it originates, then trace backwards to the source.`, 8400, ), toolPart('t1', 'bash', 'completed', { command: 'pnpm test --run 2>&1 | head -40' }, `FAIL src/run/run-group.test.ts\n ● RunGroup › renders tool parts\n\n TypeError: Cannot read properties of undefined (reading 'map')\n at RunGroup (src/run/run-group.tsx:42:40)\n at render (/node_modules/@testing-library/react/pure.js:55:22)`, undefined, 1240), toolPart('t2', 'read', 'completed', { file_path: '/home/user/project/src/run/run-group.test.ts' }, `import { render } from '@testing-library/react'\nimport { RunGroup } from './run-group'\n\nconst mockRun = { id: 'run-1', messages: [{ id: 'msg-a', role: 'assistant' }], ... }\n\nit('renders tool parts', () => {\n render( {}} />)\n})`, undefined, 58), ], 'msg-2': [ toolPart('t3', 'grep', 'completed', { pattern: 'partMap', path: 'src/run/run-group.tsx' }, `src/run/run-group.tsx:184: const msgParts = partMap[msg.id] ?? []\nsrc/run/run-group.tsx:287: }, [run.messages, partMap])`, undefined, 44), toolPart('t4', 'edit', 'completed', { file_path: '/home/user/project/src/run/run-group.tsx', old_string: '{ run, partMap, collapsed', new_string: '{ run, partMap = {}, collapsed' }, 'Edit applied.', undefined, 30), toolPart('t5', 'bash', 'completed', { command: 'pnpm test --run src/run/run-group.test.ts' }, ` ✓ src/run/run-group.test.ts (3)\n\nTest Files 1 passed (1)\n Tests 3 passed (3)\n Duration 1.21s`, undefined, 1210), ], 'msg-3': [ textPart( 'Fixed. The test was passing `partMap={undefined}` but the component destructured it without a default. Adding `partMap = {}` as a default parameter makes the component safe when callers omit the prop. All 3 tests pass.', ), ], } const debugRun: Run = { id: 'run-debug', messages: [debugMsg1, debugMsg2, debugMsg3], isComplete: true, isStreaming: false, summaryText: 'Fixed failing RunGroup test by adding default parameter for partMap.', finalTextPart: { messageId: 'msg-3', partIndex: 0, text: 'Fixed. The test was passing...' }, stats: { toolCount: 5, messageCount: 3, thinkingDurationMs: 8400, textPartCount: 1, toolCategories: new Set(['command', 'read', 'search', 'edit']), }, } // -- Scenario: streaming run -- const streamMsg1 = msg('stream-msg-1') const streamPartMap: Record = { 'stream-msg-1': [ reasoningPart( `The user wants me to add minute-level formatting to formatDuration. I need to:\n1. Update the implementation\n2. Update the tests\n3. Run tests to verify`, 0, // still active — would normally have no endTime but for story we set a duration ), toolPart('st1', 'read', 'completed', { file_path: '/home/user/project/src/utils/format.ts' }, `export function formatDuration(ms: number): string {\n if (ms < 1000) return \`\${ms}ms\`\n return \`\${(ms / 1000).toFixed(1)}s\`\n}`, undefined, 42), toolPart('st2', 'write', 'running', { file_path: '/home/user/project/src/utils/format.ts' }, undefined, undefined, undefined), ], } const streamRun: Run = { id: 'run-stream', messages: [streamMsg1], isComplete: false, isStreaming: true, summaryText: null, finalTextPart: null, stats: { toolCount: 2, messageCount: 1, thinkingDurationMs: 6200, textPartCount: 0, toolCategories: new Set(['read', 'write']), }, } // -- Scenario: error run -- const errMsg1 = msg('err-msg-1') const errPartMap: Record = { 'err-msg-1': [ toolPart('e1', 'bash', 'completed', { command: 'pnpm build' }, undefined, `error TS2322: Type 'string | undefined' is not assignable to type 'string'.\n src/utils/format.ts:8:5\n\nerror TS2345: Argument of type 'number | undefined' is not assignable to parameter of type 'number'.\n src/run/run-item-primitives.tsx:6:48\n\nFound 2 errors.`, 3200), toolPart('e2', 'edit', 'completed', { file_path: '/home/user/project/src/utils/format.ts', old_string: 'ms: number', new_string: 'ms: number | undefined' }, 'Edit applied.', undefined, 31), toolPart('e3', 'edit', 'error', { file_path: '/home/user/project/src/run/run-item-primitives.tsx' }, undefined, 'Edit failed: old_string not found in file. The text may have been modified by a previous edit.', 22), textPart('The second edit failed because the file was already modified. Let me re-read it and apply the correct patch.'), ], } const errRun: Run = { id: 'run-error', messages: [errMsg1], isComplete: true, isStreaming: false, summaryText: 'Attempted to fix TypeScript build errors; one edit failed.', finalTextPart: null, stats: { toolCount: 3, messageCount: 1, thinkingDurationMs: 0, textPartCount: 1, toolCategories: new Set(['command', 'edit']), }, } // -- Scenario: single tool call (minimal) -- const minimalMsg = msg('min-msg-1') const minimalPartMap: Record = { 'min-msg-1': [ toolPart('m1', 'bash', 'completed', { command: 'ls -la' }, 'total 48\ndrwxr-xr-x 8 user user 4096 Mar 30 12:00 .\ndrwxr-xr-x 22 user user 4096 Mar 30 11:58 ..', undefined, 88), textPart('The directory is empty except for the standard dot entries.'), ], } const minimalRun: Run = { id: 'run-minimal', messages: [minimalMsg], isComplete: true, isStreaming: false, summaryText: 'Listed directory contents.', finalTextPart: { messageId: 'min-msg-1', partIndex: 1, text: 'The directory is empty...' }, stats: { toolCount: 1, messageCount: 1, thinkingDurationMs: 0, textPartCount: 1, toolCategories: new Set(['command']), }, } // -- Custom branding -- const customBranding: AgentBranding = { label: 'Sandbox Agent', accentClass: 'text-violet-400', bgClass: 'bg-violet-500/8', containerBgClass: 'bg-muted/60', borderClass: 'border-violet-500/30', iconClass: '', textClass: 'text-violet-300', } // -- Controlled wrapper -- function ControlledRunGroup(props: Omit, 'collapsed' | 'onToggle'>) { const [collapsed, setCollapsed] = useState(false) return ( setCollapsed((c) => !c)} /> ) } // -- const meta: Meta = { title: 'Run/RunGroup', component: RunGroup, parameters: { layout: 'fullscreen', backgrounds: { default: 'dark' }, }, } export default meta type Story = StoryObj export const CompleteDebuggingRun: Story = { name: 'Complete — debugging run', render: () => (
), } export const Streaming: Story = { name: 'Streaming — in progress', render: () => (
), } export const WithErrors: Story = { name: 'Complete — with tool errors', render: () => (
), } export const Minimal: Story = { name: 'Minimal — single tool + text', render: () => (
), } export const Collapsed: Story = { name: 'Collapsed state', render: () => (
{}} />
), } export const CustomBranding: Story = { name: 'Custom branding', render: () => (
), } export const MultipleRuns: Story = { name: 'Multiple consecutive runs', render: () => (
), }