diff --git a/apps/frontend/src/renderer/App.tsx b/apps/frontend/src/renderer/App.tsx
index 7ef070661..7e47904f7 100644
--- a/apps/frontend/src/renderer/App.tsx
+++ b/apps/frontend/src/renderer/App.tsx
@@ -76,6 +76,7 @@ import { GitHubIssuesPilotView } from './components/GitHubIssuesPilotView';
import { PatternsPilotView } from './components/PatternsPilotView';
import { AnalyticsPilotView } from './components/AnalyticsPilotView';
import { ProductivityPilotView } from './components/ProductivityPilotView';
+import { MergeAnalyticsPilotView } from './components/MergeAnalyticsPilotView';
import { useSettingsStore, loadSettings, loadProfiles, saveSettings } from './stores/settings-store';
import { useClaudeProfileStore } from './stores/claude-profile-store';
import { useTerminalStore, restoreTerminalSessions } from './stores/terminal-store';
@@ -1153,6 +1154,9 @@ export function App() {
{activeView === 'merge-analytics' && (activeProjectId || selectedProjectId) && (
)}
+ {activeView === 'merge-analytics-next' && (activeProjectId || selectedProjectId) && (
+
+ )}
{activeView === 'feedback' && (activeProjectId || selectedProjectId) && (
)}
diff --git a/apps/frontend/src/renderer/__tests__/merge-analytics-ui.test.ts b/apps/frontend/src/renderer/__tests__/merge-analytics-ui.test.ts
new file mode 100644
index 000000000..b4aa3adb5
--- /dev/null
+++ b/apps/frontend/src/renderer/__tests__/merge-analytics-ui.test.ts
@@ -0,0 +1,125 @@
+/**
+ * Tests for the MergeAnalytics / ConflictPattern → AnalyticsDashboard mappers.
+ */
+
+import { describe, expect, it } from 'vitest';
+import {
+ buildMergeBarLists,
+ buildMergeKpis,
+ buildMergeSections,
+ formatDuration,
+ shortenPath,
+} from '../lib/merge-analytics-ui';
+import type { ConflictPattern, MergeAnalytics } from '../../shared/types';
+
+function makeAnalytics(overrides: Partial = {}): MergeAnalytics {
+ return {
+ total_operations: 84,
+ total_files_merged: 312,
+ total_conflicts: 12,
+ successful_operations: 80,
+ failed_operations: 4,
+ total_ai_calls: 47,
+ total_tokens_used: 1_240_000,
+ average_duration_seconds: 125,
+ success_rate: 0.95,
+ auto_merge_rate: 0.72,
+ conflict_patterns: [],
+ ...overrides,
+ };
+}
+
+function makePattern(overrides: Partial = {}): ConflictPattern {
+ return {
+ file_path: 'apps/frontend/src/renderer/App.tsx',
+ location: 'L120',
+ occurrence_count: 6,
+ severity: 'high',
+ tasks_involved: ['001', '002'],
+ last_seen: '2026-05-22T00:00:00Z',
+ ...overrides,
+ };
+}
+
+describe('shortenPath', () => {
+ it('keeps short paths and truncates deep ones to the trailing segments', () => {
+ expect(shortenPath('App.tsx')).toBe('App.tsx');
+ expect(shortenPath('a/b')).toBe('a/b');
+ expect(shortenPath('apps/frontend/src/App.tsx')).toBe('…/src/App.tsx');
+ });
+});
+
+describe('formatDuration', () => {
+ it('renders seconds and minute+second forms', () => {
+ expect(formatDuration(45)).toBe('45s');
+ expect(formatDuration(125)).toBe('2m 05s');
+ expect(formatDuration(-1)).toBe('0s');
+ });
+});
+
+describe('buildMergeKpis', () => {
+ const labels = {
+ operations: 'ops',
+ operationsSub: 'all',
+ successRate: 'success',
+ autoMerge: 'auto',
+ conflicts: 'conflicts',
+ conflictsSub: 'total',
+ };
+
+ it('maps the analytics onto four tiles, converting 0-1 rates to percents', () => {
+ const kpis = buildMergeKpis(makeAnalytics(), labels);
+ expect(kpis.map((k) => k.value)).toEqual(['84', '95%', '72%', '12']);
+ expect(kpis[1].tone).toBe('good');
+ expect(kpis[3].tone).toBe('warn');
+ });
+
+ it('warns a low success rate and drops the conflict tone at zero', () => {
+ const kpis = buildMergeKpis(
+ makeAnalytics({ success_rate: 0.6, total_conflicts: 0 }),
+ labels,
+ );
+ expect(kpis[1].tone).toBe('warn');
+ expect(kpis[3].tone).toBeUndefined();
+ });
+});
+
+describe('buildMergeBarLists', () => {
+ it('maps conflict patterns to a distribution with severity-toned bars', () => {
+ const lists = buildMergeBarLists(
+ [
+ makePattern({ occurrence_count: 6, severity: 'high' }),
+ makePattern({ file_path: 'x/y/z/util.ts', occurrence_count: 3, severity: 'medium' }),
+ ],
+ 'Top conflicts',
+ 'aria',
+ );
+ expect(lists).toHaveLength(1);
+ expect(lists[0].items).toEqual([
+ { label: '…/renderer/App.tsx', value: 6, tone: 'bad' },
+ { label: '…/z/util.ts', value: 3, tone: 'warn' },
+ ]);
+ });
+
+ it('returns no card when there are no patterns', () => {
+ expect(buildMergeBarLists([], 't', 'a')).toEqual([]);
+ });
+});
+
+describe('buildMergeSections', () => {
+ it('builds outcome + efficiency cards', () => {
+ const sections = buildMergeSections(makeAnalytics(), {
+ outcomesTitle: 'Outcomes',
+ successful: 'Successful',
+ failed: 'Failed',
+ filesMerged: 'Files merged',
+ efficiencyTitle: 'Efficiency',
+ avgDuration: 'Avg duration',
+ aiCalls: 'AI calls',
+ tokens: 'Tokens',
+ });
+ expect(sections[0].rows[0]).toEqual({ label: 'Successful', value: '80' });
+ expect(sections[1].rows[0]).toEqual({ label: 'Avg duration', value: '2m 05s' });
+ expect(sections[1].rows[2]).toEqual({ label: 'Tokens', value: '1.2M' });
+ });
+});
diff --git a/apps/frontend/src/renderer/components/MergeAnalyticsPilotView.tsx b/apps/frontend/src/renderer/components/MergeAnalyticsPilotView.tsx
new file mode 100644
index 000000000..0cdfee553
--- /dev/null
+++ b/apps/frontend/src/renderer/components/MergeAnalyticsPilotView.tsx
@@ -0,0 +1,148 @@
+/**
+ * Merge Analytics pilot on the shared design system (U5 B2).
+ *
+ * Renders `libs/ui`'s AnalyticsDashboard from the existing `getMergeSummary`
+ * + `getConflictPatterns` IPC, mapped with the pure `merge-analytics-ui`
+ * helpers. The conflict patterns render as a BarList distribution. Read-only
+ * — the legacy Merge Analytics view keeps its export flow. Reachable via the
+ * "Merge Analytics (new UI)" sidebar item.
+ */
+
+import { useCallback, useEffect, useMemo, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { AnalyticsDashboard } from '@auto-code/ui';
+import type { UiBarListCard, UiKpi, UiMetaSection } from '@auto-code/ui';
+import type { ConflictPattern, MergeAnalytics } from '../../shared/types';
+import {
+ buildMergeBarLists,
+ buildMergeKpis,
+ buildMergeSections,
+} from '../lib/merge-analytics-ui';
+
+interface MergeAnalyticsPilotViewProps {
+ projectId: string;
+}
+
+interface PilotState {
+ analytics: MergeAnalytics | null;
+ patterns: ConflictPattern[];
+ loading: boolean;
+ error: Error | null;
+}
+
+export function MergeAnalyticsPilotView({
+ projectId,
+}: Readonly) {
+ const { t } = useTranslation(['analytics']);
+ const [state, setState] = useState({
+ analytics: null,
+ patterns: [],
+ loading: true,
+ error: null,
+ });
+ const [reloadKey, setReloadKey] = useState(0);
+
+ useEffect(() => {
+ let active = true;
+ setState({ analytics: null, patterns: [], loading: true, error: null });
+ Promise.all([
+ window.electronAPI.getMergeSummary(projectId),
+ window.electronAPI.getConflictPatterns(projectId, 10),
+ ])
+ .then(([summaryResult, patternsResult]) => {
+ if (!active) return;
+ if (!summaryResult.success || summaryResult.data == null) {
+ console.error(
+ '[MergeAnalyticsPilotView] Failed to load summary:',
+ summaryResult.success ? 'no data' : summaryResult.error,
+ );
+ setState({
+ analytics: null,
+ patterns: [],
+ loading: false,
+ error: new Error(t('analytics:mergePilot.error')),
+ });
+ return;
+ }
+ setState({
+ analytics: summaryResult.data,
+ patterns: patternsResult.success ? (patternsResult.data ?? []) : [],
+ loading: false,
+ error: null,
+ });
+ })
+ .catch((err: unknown) => {
+ if (!active) return;
+ console.error('[MergeAnalyticsPilotView] Failed to load merge analytics:', err);
+ setState({
+ analytics: null,
+ patterns: [],
+ loading: false,
+ error: new Error(t('analytics:mergePilot.error')),
+ });
+ });
+ return () => {
+ active = false;
+ };
+ }, [projectId, reloadKey, t]);
+
+ const reload = useCallback(() => setReloadKey((key) => key + 1), []);
+
+ const kpis = useMemo(() => {
+ if (state.analytics == null) return null;
+ return buildMergeKpis(state.analytics, {
+ operations: t('analytics:mergePilot.kpis.operations'),
+ operationsSub: t('analytics:mergePilot.kpis.operationsSub'),
+ successRate: t('analytics:mergePilot.kpis.successRate'),
+ autoMerge: t('analytics:mergePilot.kpis.autoMerge'),
+ conflicts: t('analytics:mergePilot.kpis.conflicts'),
+ conflictsSub: t('analytics:mergePilot.kpis.conflictsSub'),
+ });
+ }, [state.analytics, t]);
+
+ const barLists = useMemo(() => {
+ if (state.analytics == null) return undefined;
+ return buildMergeBarLists(
+ state.patterns,
+ t('analytics:mergePilot.conflicts.title'),
+ t('analytics:mergePilot.conflicts.aria'),
+ );
+ }, [state.analytics, state.patterns, t]);
+
+ const sections = useMemo(() => {
+ if (state.analytics == null) return undefined;
+ return buildMergeSections(state.analytics, {
+ outcomesTitle: t('analytics:mergePilot.sections.outcomesTitle'),
+ successful: t('analytics:mergePilot.sections.successful'),
+ failed: t('analytics:mergePilot.sections.failed'),
+ filesMerged: t('analytics:mergePilot.sections.filesMerged'),
+ efficiencyTitle: t('analytics:mergePilot.sections.efficiencyTitle'),
+ avgDuration: t('analytics:mergePilot.sections.avgDuration'),
+ aiCalls: t('analytics:mergePilot.sections.aiCalls'),
+ tokens: t('analytics:mergePilot.sections.tokens'),
+ });
+ }, [state.analytics, t]);
+
+ const stateLabels = useMemo(
+ () => ({
+ loading: t('analytics:mergePilot.states.loading'),
+ retry: t('analytics:mergePilot.states.retry'),
+ empty: t('analytics:mergePilot.states.empty'),
+ }),
+ [t],
+ );
+
+ return (
+
+ );
+}
diff --git a/apps/frontend/src/renderer/components/Sidebar.tsx b/apps/frontend/src/renderer/components/Sidebar.tsx
index 771156d71..ac230e314 100644
--- a/apps/frontend/src/renderer/components/Sidebar.tsx
+++ b/apps/frontend/src/renderer/components/Sidebar.tsx
@@ -71,7 +71,7 @@ import { SessionContextIndicator } from './SessionContextIndicator';
import { NavIndicator } from './NavIndicator';
import type { Project, AutoBuildVersionInfo, GitStatus, ProjectEnvConfig } from '../../shared/types';
-export type SidebarView = 'kanban' | 'kanban-next' | 'terminals' | 'roadmap' | 'context' | 'ideation' | 'webhooks' | 'github-issues' | 'github-issues-next' | 'gitlab-issues' | 'github-prs' | 'github-prs-next' | 'gitlab-merge-requests' | 'changelog' | 'changelog-next' | 'insights' | 'worktrees' | 'agent-tools' | 'plugins' | 'analytics' | 'analytics-next' | 'productivity' | 'productivity-next' | 'merge-analytics' | 'sessions' | 'scheduler' | 'feedback' | 'patterns' | 'patterns-next' | 'model-usage' | 'agent-inspector';
+export type SidebarView = 'kanban' | 'kanban-next' | 'terminals' | 'roadmap' | 'context' | 'ideation' | 'webhooks' | 'github-issues' | 'github-issues-next' | 'gitlab-issues' | 'github-prs' | 'github-prs-next' | 'gitlab-merge-requests' | 'changelog' | 'changelog-next' | 'insights' | 'worktrees' | 'agent-tools' | 'plugins' | 'analytics' | 'analytics-next' | 'productivity' | 'productivity-next' | 'merge-analytics' | 'merge-analytics-next' | 'sessions' | 'scheduler' | 'feedback' | 'patterns' | 'patterns-next' | 'model-usage' | 'agent-inspector';
interface SidebarProps {
onSettingsClick: () => void;
@@ -110,6 +110,7 @@ const baseNavItems: NavItem[] = [
{ id: 'plugins', labelKey: 'navigation:items.plugins', icon: Puzzle, shortcut: 'U' },
{ id: 'worktrees', labelKey: 'navigation:items.worktrees', icon: GitBranch, shortcut: 'W' },
{ id: 'merge-analytics', labelKey: 'navigation:items.mergeAnalytics', icon: Activity, shortcut: 'T' },
+ { id: 'merge-analytics-next', labelKey: 'navigation:items.mergeAnalyticsNext', icon: GitMerge },
{ id: 'sessions', labelKey: 'navigation:items.sessions', icon: Play },
{ id: 'agent-inspector', labelKey: 'navigation:items.agentInspector', icon: Brain },
{ id: 'feedback', labelKey: 'navigation:items.feedback', icon: MessageSquare, shortcut: 'F' },
diff --git a/apps/frontend/src/renderer/lib/merge-analytics-ui.ts b/apps/frontend/src/renderer/lib/merge-analytics-ui.ts
new file mode 100644
index 000000000..6ea0569aa
--- /dev/null
+++ b/apps/frontend/src/renderer/lib/merge-analytics-ui.ts
@@ -0,0 +1,138 @@
+/**
+ * Pure mapping helpers between the MergeAnalytics / ConflictPattern IPC shapes
+ * and the shared AnalyticsDashboard. There is no time series in the merge
+ * report, so the KPI tiles carry no sparklines; the conflict patterns map to a
+ * BarList distribution and the rest to summary cards.
+ */
+
+import type { UiBarListCard, UiKpi, UiMetaSection } from '@auto-code/ui';
+import type { ChartTone } from '@auto-code/ui';
+import type {
+ ConflictPattern,
+ MergeAnalytics,
+ MergeConflictSeverity,
+} from '../../shared/types';
+import { formatCount, formatPercent } from './analytics-ui';
+
+const SEVERITY_TONES: Record = {
+ none: 'neutral',
+ low: 'info',
+ medium: 'warn',
+ high: 'bad',
+ critical: 'bad',
+};
+
+/** Trailing path segment for a compact bar label; keeps a leading dir hint. */
+export function shortenPath(path: string, maxSegments = 2): string {
+ const parts = path.split('/').filter(Boolean);
+ if (parts.length <= maxSegments) return path;
+ return `…/${parts.slice(-maxSegments).join('/')}`;
+}
+
+export interface MergeKpiLabels {
+ operations: string;
+ operationsSub: string;
+ successRate: string;
+ autoMerge: string;
+ conflicts: string;
+ conflictsSub: string;
+}
+
+export function buildMergeKpis(
+ analytics: MergeAnalytics,
+ labels: MergeKpiLabels,
+): UiKpi[] {
+ // Backend rates are 0.0–1.0.
+ const successPct = analytics.success_rate * 100;
+ const autoPct = analytics.auto_merge_rate * 100;
+ const successTone = successPct >= 80 ? 'good' : 'warn';
+ return [
+ {
+ value: String(analytics.total_operations),
+ label: labels.operations,
+ sub: labels.operationsSub,
+ },
+ {
+ value: formatPercent(successPct),
+ label: labels.successRate,
+ tone: successTone,
+ },
+ {
+ value: formatPercent(autoPct),
+ label: labels.autoMerge,
+ },
+ {
+ value: String(analytics.total_conflicts),
+ label: labels.conflicts,
+ sub: labels.conflictsSub,
+ tone: analytics.total_conflicts > 0 ? 'warn' : undefined,
+ },
+ ];
+}
+
+export function buildMergeBarLists(
+ patterns: readonly ConflictPattern[],
+ title: string,
+ ariaLabel: string,
+): UiBarListCard[] {
+ if (patterns.length === 0) return [];
+ return [
+ {
+ title,
+ ariaLabel,
+ items: patterns.map((pattern) => ({
+ label: shortenPath(pattern.file_path),
+ value: pattern.occurrence_count,
+ tone: SEVERITY_TONES[pattern.severity] ?? 'info',
+ })),
+ },
+ ];
+}
+
+export interface MergeSectionLabels {
+ outcomesTitle: string;
+ successful: string;
+ failed: string;
+ filesMerged: string;
+ efficiencyTitle: string;
+ avgDuration: string;
+ aiCalls: string;
+ tokens: string;
+}
+
+/** Seconds → "45s" or "2m 05s". */
+export function formatDuration(seconds: number): string {
+ if (!Number.isFinite(seconds) || seconds < 0) return '0s';
+ const whole = Math.round(seconds);
+ if (whole < 60) return `${whole}s`;
+ const minutes = Math.floor(whole / 60);
+ const rest = whole % 60;
+ return `${minutes}m ${String(rest).padStart(2, '0')}s`;
+}
+
+export function buildMergeSections(
+ analytics: MergeAnalytics,
+ labels: MergeSectionLabels,
+): UiMetaSection[] {
+ return [
+ {
+ title: labels.outcomesTitle,
+ rows: [
+ { label: labels.successful, value: String(analytics.successful_operations) },
+ { label: labels.failed, value: String(analytics.failed_operations) },
+ { label: labels.filesMerged, value: String(analytics.total_files_merged) },
+ ],
+ },
+ {
+ title: labels.efficiencyTitle,
+ rows: [
+ {
+ label: labels.avgDuration,
+ value: formatDuration(analytics.average_duration_seconds),
+ },
+ { label: labels.aiCalls, value: String(analytics.total_ai_calls) },
+ { label: labels.tokens, value: formatCount(analytics.total_tokens_used) },
+ ],
+ },
+ ];
+}
diff --git a/apps/frontend/src/shared/i18n/locales/en/analytics.json b/apps/frontend/src/shared/i18n/locales/en/analytics.json
index ae50298d0..d3e95fbf1 100644
--- a/apps/frontend/src/shared/i18n/locales/en/analytics.json
+++ b/apps/frontend/src/shared/i18n/locales/en/analytics.json
@@ -140,5 +140,35 @@
"qaIterations": "QA iterations",
"firstAttempt": "First-attempt rate"
}
+ },
+ "mergePilot": {
+ "error": "Failed to load merge analytics.",
+ "states": {
+ "loading": "Loading merge analytics…",
+ "retry": "Retry",
+ "empty": "No merge analytics yet."
+ },
+ "kpis": {
+ "operations": "merge operations",
+ "operationsSub": "all time",
+ "successRate": "success rate",
+ "autoMerge": "auto-merged",
+ "conflicts": "conflicts",
+ "conflictsSub": "total resolved"
+ },
+ "conflicts": {
+ "title": "Top conflict files",
+ "aria": "Files by conflict occurrence count"
+ },
+ "sections": {
+ "outcomesTitle": "Outcomes",
+ "successful": "Successful",
+ "failed": "Failed",
+ "filesMerged": "Files merged",
+ "efficiencyTitle": "Efficiency",
+ "avgDuration": "Avg duration",
+ "aiCalls": "AI calls",
+ "tokens": "Tokens used"
+ }
}
}
diff --git a/apps/frontend/src/shared/i18n/locales/en/navigation.json b/apps/frontend/src/shared/i18n/locales/en/navigation.json
index 00f28413d..07b3b66ac 100644
--- a/apps/frontend/src/shared/i18n/locales/en/navigation.json
+++ b/apps/frontend/src/shared/i18n/locales/en/navigation.json
@@ -35,7 +35,8 @@
"githubIssuesNext": "GitHub Issues (New UI)",
"patternsNext": "Patterns (New UI)",
"analyticsNext": "Analytics (New UI)",
- "productivityNext": "Productivity (New UI)"
+ "productivityNext": "Productivity (New UI)",
+ "mergeAnalyticsNext": "Merge Analytics (New UI)"
},
"actions": {
"settings": "Settings",
diff --git a/apps/frontend/src/shared/i18n/locales/fr/analytics.json b/apps/frontend/src/shared/i18n/locales/fr/analytics.json
index a10d9190f..5da4b9bbe 100644
--- a/apps/frontend/src/shared/i18n/locales/fr/analytics.json
+++ b/apps/frontend/src/shared/i18n/locales/fr/analytics.json
@@ -140,5 +140,35 @@
"qaIterations": "Itérations QA",
"firstAttempt": "Taux au 1er essai"
}
+ },
+ "mergePilot": {
+ "error": "Échec du chargement de l'analytique de fusion.",
+ "states": {
+ "loading": "Chargement de l'analytique de fusion…",
+ "retry": "Réessayer",
+ "empty": "Aucune analytique de fusion pour le moment."
+ },
+ "kpis": {
+ "operations": "fusions",
+ "operationsSub": "depuis le début",
+ "successRate": "taux de réussite",
+ "autoMerge": "fusion auto",
+ "conflicts": "conflits",
+ "conflictsSub": "total résolus"
+ },
+ "conflicts": {
+ "title": "Fichiers les plus conflictuels",
+ "aria": "Fichiers par nombre d'occurrences de conflit"
+ },
+ "sections": {
+ "outcomesTitle": "Résultats",
+ "successful": "Réussies",
+ "failed": "Échouées",
+ "filesMerged": "Fichiers fusionnés",
+ "efficiencyTitle": "Efficacité",
+ "avgDuration": "Durée moyenne",
+ "aiCalls": "Appels IA",
+ "tokens": "Tokens utilisés"
+ }
}
}
diff --git a/apps/frontend/src/shared/i18n/locales/fr/navigation.json b/apps/frontend/src/shared/i18n/locales/fr/navigation.json
index ea7a36f05..113970652 100644
--- a/apps/frontend/src/shared/i18n/locales/fr/navigation.json
+++ b/apps/frontend/src/shared/i18n/locales/fr/navigation.json
@@ -35,7 +35,8 @@
"githubIssuesNext": "Issues GitHub (nouvelle interface)",
"patternsNext": "Patterns (nouvelle interface)",
"analyticsNext": "Analytique (nouvelle interface)",
- "productivityNext": "Productivité (nouvelle interface)"
+ "productivityNext": "Productivité (nouvelle interface)",
+ "mergeAnalyticsNext": "Analytique de fusion (nouvelle interface)"
},
"actions": {
"settings": "Paramètres",
diff --git a/libs/ui/src/client/types.ts b/libs/ui/src/client/types.ts
index 1105b163e..243bf3a08 100644
--- a/libs/ui/src/client/types.ts
+++ b/libs/ui/src/client/types.ts
@@ -279,3 +279,16 @@ export interface UiChartCard {
/** Accessible description of the chart. */
ariaLabel: string;
}
+
+/** One titled bar-list card (a labeled distribution) in a dashboard. */
+export interface UiBarListCard {
+ title: string;
+ items: readonly {
+ label: string;
+ value: number;
+ valueLabel?: string;
+ tone?: 'info' | 'good' | 'warn' | 'bad' | 'neutral';
+ }[];
+ /** Accessible description of the distribution. */
+ ariaLabel: string;
+}
diff --git a/libs/ui/src/index.ts b/libs/ui/src/index.ts
index 5287c681c..673caad82 100644
--- a/libs/ui/src/index.ts
+++ b/libs/ui/src/index.ts
@@ -72,6 +72,7 @@ export type {
UiPrStat,
UiKpi,
UiChartCard,
+ UiBarListCard,
CreateTaskInput,
TaskStatus,
BadgeTone,
diff --git a/libs/ui/src/screens/AnalyticsDashboard.stories.tsx b/libs/ui/src/screens/AnalyticsDashboard.stories.tsx
index ee0c50347..9122247c1 100644
--- a/libs/ui/src/screens/AnalyticsDashboard.stories.tsx
+++ b/libs/ui/src/screens/AnalyticsDashboard.stories.tsx
@@ -67,6 +67,24 @@ type Story = StoryObj;
export const Dashboard: Story = {};
+export const WithBarList: Story = {
+ args: {
+ charts: undefined,
+ barLists: [
+ {
+ title: 'Top conflict files',
+ ariaLabel: 'Files by conflict occurrence count',
+ items: [
+ { label: '…/renderer/App.tsx', value: 6, tone: 'bad' },
+ { label: '…/shell/Sidebar.tsx', value: 4, tone: 'warn' },
+ { label: '…/i18n/index.ts', value: 3, tone: 'info' },
+ { label: '…/lib/analytics-ui.ts', value: 1, tone: 'neutral' },
+ ],
+ },
+ ],
+ },
+};
+
export const NoRail: Story = {
args: { sections: undefined },
};
diff --git a/libs/ui/src/screens/AnalyticsDashboard.tsx b/libs/ui/src/screens/AnalyticsDashboard.tsx
index a0b2ac8d4..df9c061ca 100644
--- a/libs/ui/src/screens/AnalyticsDashboard.tsx
+++ b/libs/ui/src/screens/AnalyticsDashboard.tsx
@@ -1,7 +1,9 @@
+import { BarList } from '../primitives/BarList';
import { LineChart } from '../primitives/LineChart';
import { Sparkline } from '../primitives/Sparkline';
import { StatTile } from '../primitives/StatTile';
import type {
+ UiBarListCard,
UiChartCard,
UiKpi,
UiMetaSection,
@@ -19,6 +21,8 @@ export interface AnalyticsDashboardProps {
kpis: UiKpi[] | null;
/** Titled chart cards (line charts) in the main column. */
charts?: UiChartCard[];
+ /** Titled bar-list cards (distributions) in the main column. */
+ barLists?: UiBarListCard[];
/** Summary meta cards (outcomes, QA, agents …) in the right rail. */
sections?: UiMetaSection[];
loading?: boolean;
@@ -38,6 +42,7 @@ export interface AnalyticsDashboardProps {
export function AnalyticsDashboard({
kpis,
charts,
+ barLists,
sections,
loading = false,
error = null,
@@ -84,6 +89,7 @@ export function AnalyticsDashboard({
}
const chartCards = charts ?? [];
+ const barListCards = barLists ?? [];
const metaCards = sections ?? [];
return (
@@ -126,6 +132,12 @@ export function AnalyticsDashboard({
/>
))}
+ {barListCards.map((barList) => (
+
+
{barList.title}
+
+
+ ))}
{metaCards.length > 0 && (