diff --git a/frontend/src/components/ArtifactsPanel.tsx b/frontend/src/components/ArtifactsPanel.tsx
new file mode 100644
index 0000000..29cf1c4
--- /dev/null
+++ b/frontend/src/components/ArtifactsPanel.tsx
@@ -0,0 +1,125 @@
+import { useEffect } from 'react'
+import ReactMarkdown from 'react-markdown'
+import { IconFileText, IconRefresh } from '@tabler/icons-react'
+import { useArtifacts } from '../hooks/useArtifacts'
+
+export interface ArtifactsPanelProps {
+ sessionId: string | null
+}
+
+export function ArtifactsPanel({ sessionId }: ArtifactsPanelProps) {
+ const { artifacts, activeArtifact, refresh, loadArtifact } = useArtifacts(sessionId)
+
+ useEffect(() => {
+ if (sessionId) {
+ refresh()
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [sessionId])
+
+ if (!sessionId) {
+ return (
+
+ {/* Header bar */}
+
+ {countLabel}
+
+
+
+ {/* Tab strip — only when there are artifacts */}
+ {artifacts.length > 0 && (
+
+ {artifacts.map((artifact) => {
+ const isActive = activeArtifact?.name === artifact.name
+ return (
+
+ )
+ })}
+
+ )}
+
+ {/* Content area */}
+
+ {activeArtifact ? (
+
+ {activeArtifact.content}
+
+ ) : artifacts.length > 0 ? (
+
+ Click an artifact tab to view it
+
+ ) : (
+
+ No artifacts yet. The researcher will create them during research.
+
+ )}
+
+
+ )
+}
diff --git a/frontend/src/test/ArtifactsPanel.test.tsx b/frontend/src/test/ArtifactsPanel.test.tsx
new file mode 100644
index 0000000..384a8b5
--- /dev/null
+++ b/frontend/src/test/ArtifactsPanel.test.tsx
@@ -0,0 +1,283 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest'
+import { render, screen, act } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import type { ArtifactContent, Artifact } from '../lib/types'
+
+// Must be hoisted before imports
+vi.mock('react-markdown', () => ({
+ default: ({ children }: { children: string }) =>