diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx
index d9c0e3fb..c799bf2c 100644
--- a/src/renderer/App.tsx
+++ b/src/renderer/App.tsx
@@ -11951,24 +11951,6 @@ You are taking over this conversation. Based on the context above, provide a bri
() => setEnterToSendAI(!enterToSendAI),
[enterToSendAI]
);
- // OpenSpec command injection - sets prompt content into input field
- const handleInjectOpenSpecPrompt = useCallback(
- (prompt: string) => {
- if (activeGroupChatId) {
- // Update group chat draft
- setGroupChats(prev =>
- prev.map(c =>
- c.id === activeGroupChatId ? { ...c, draftMessage: prompt } : c
- )
- );
- } else {
- setInputValue(prompt);
- }
- // Focus the input so user can edit/send the injected prompt
- setTimeout(() => inputRef.current?.focus(), 0);
- },
- [activeGroupChatId, setInputValue]
- );
// QuickActionsModal stable callbacks
const handleQuickActionsRenameTab = useCallback(() => {
@@ -12816,7 +12798,6 @@ You are taking over this conversation. Based on the context above, provide a bri
isFilePreviewOpen={previewFile !== null}
ghCliAvailable={ghCliAvailable}
onPublishGist={() => setGistPublishModalOpen(true)}
- onInjectOpenSpecPrompt={handleInjectOpenSpecPrompt}
lastGraphFocusFile={lastGraphFocusFilePath}
onOpenLastDocumentGraph={() => {
if (lastGraphFocusFilePath) {
diff --git a/src/renderer/components/AppModals.tsx b/src/renderer/components/AppModals.tsx
index 485005d3..91d12800 100644
--- a/src/renderer/components/AppModals.tsx
+++ b/src/renderer/components/AppModals.tsx
@@ -791,8 +791,6 @@ export interface AppUtilityModalsProps {
autoRunSelectedDocument: string | null;
autoRunCompletedTaskCount: number;
onAutoRunResetTasks: () => void;
- // OpenSpec commands
- onInjectOpenSpecPrompt?: (prompt: string) => void;
// Gist publishing (for QuickActionsModal)
isFilePreviewOpen: boolean;
@@ -975,8 +973,6 @@ export function AppUtilityModals({
isFilePreviewOpen,
ghCliAvailable,
onPublishGist,
- // OpenSpec commands
- onInjectOpenSpecPrompt,
// Document Graph - quick re-open last graph
lastGraphFocusFile,
onOpenLastDocumentGraph,
@@ -1127,7 +1123,6 @@ export function AppUtilityModals({
isFilePreviewOpen={isFilePreviewOpen}
ghCliAvailable={ghCliAvailable}
onPublishGist={onPublishGist}
- onInjectOpenSpecPrompt={onInjectOpenSpecPrompt}
onOpenPlaybookExchange={onOpenMarketplace}
lastGraphFocusFile={lastGraphFocusFile}
onOpenLastDocumentGraph={onOpenLastDocumentGraph}
@@ -1839,8 +1834,6 @@ export interface AppModalsProps {
isFilePreviewOpen: boolean;
ghCliAvailable: boolean;
onPublishGist?: () => void;
- // OpenSpec commands
- onInjectOpenSpecPrompt?: (prompt: string) => void;
// Document Graph - quick re-open last graph
lastGraphFocusFile?: string;
onOpenLastDocumentGraph?: () => void;
@@ -2131,8 +2124,6 @@ export function AppModals(props: AppModalsProps) {
isFilePreviewOpen,
ghCliAvailable,
onPublishGist,
- // OpenSpec commands
- onInjectOpenSpecPrompt,
// Document Graph - quick re-open last graph
lastGraphFocusFile,
onOpenLastDocumentGraph,
@@ -2433,7 +2424,6 @@ export function AppModals(props: AppModalsProps) {
isFilePreviewOpen={isFilePreviewOpen}
ghCliAvailable={ghCliAvailable}
onPublishGist={onPublishGist}
- onInjectOpenSpecPrompt={onInjectOpenSpecPrompt}
lastGraphFocusFile={lastGraphFocusFile}
onOpenLastDocumentGraph={onOpenLastDocumentGraph}
lightboxImage={lightboxImage}
diff --git a/src/renderer/components/QuickActionsModal.tsx b/src/renderer/components/QuickActionsModal.tsx
index 0fb04045..8588c4d5 100644
--- a/src/renderer/components/QuickActionsModal.tsx
+++ b/src/renderer/components/QuickActionsModal.tsx
@@ -6,7 +6,6 @@ import { useLayerStack } from '../contexts/LayerStackContext';
import { useToast } from '../contexts/ToastContext';
import { MODAL_PRIORITIES } from '../constants/modalPriorities';
import { gitService } from '../services/git';
-import { getOpenSpecCommand } from '../services/openspec';
import { formatShortcutKeys } from '../utils/shortcutFormatter';
import type { WizardStep } from './Wizard/WizardContext';
import { useListNavigation } from '../hooks';
@@ -105,8 +104,6 @@ interface QuickActionsModalProps {
isFilePreviewOpen?: boolean;
ghCliAvailable?: boolean;
onPublishGist?: () => void;
- // OpenSpec commands
- onInjectOpenSpecPrompt?: (prompt: string) => void;
// Playbook Exchange
onOpenPlaybookExchange?: () => void;
// Document Graph - quick re-open last graph
@@ -133,7 +130,6 @@ export function QuickActionsModal(props: QuickActionsModalProps) {
autoRunSelectedDocument, autoRunCompletedTaskCount, onAutoRunResetTasks,
onCloseAllTabs, onCloseOtherTabs, onCloseTabsLeft, onCloseTabsRight,
isFilePreviewOpen, ghCliAvailable, onPublishGist,
- onInjectOpenSpecPrompt,
onOpenPlaybookExchange,
lastGraphFocusFile, onOpenLastDocumentGraph
} = props;
@@ -448,69 +444,6 @@ export function QuickActionsModal(props: QuickActionsModalProps) {
...(onNewGroupChat && sessions.filter(s => s.toolType !== 'terminal').length >= 2 ? [{ id: 'newGroupChat', label: 'New Group Chat', action: () => { onNewGroupChat(); setQuickActionOpen(false); } }] : []),
...(activeGroupChatId && onCloseGroupChat ? [{ id: 'closeGroupChat', label: 'Close Group Chat', action: () => { onCloseGroupChat(); setQuickActionOpen(false); } }] : []),
...(activeGroupChatId && onDeleteGroupChat && groupChats ? [{ id: 'deleteGroupChat', label: `Remove Group Chat: ${groupChats.find(c => c.id === activeGroupChatId)?.name || 'Group Chat'}`, shortcut: shortcuts.killInstance, action: () => { onDeleteGroupChat(activeGroupChatId); setQuickActionOpen(false); } }] : []),
- // OpenSpec commands - inject prompt when selected
- ...(onInjectOpenSpecPrompt ? [
- {
- id: 'openspec-proposal',
- label: 'OpenSpec: Create Proposal',
- subtext: 'Create a new change proposal',
- action: async () => {
- const cmd = await getOpenSpecCommand('/openspec.proposal');
- if (cmd) {
- onInjectOpenSpecPrompt(cmd.prompt);
- }
- setQuickActionOpen(false);
- }
- },
- {
- id: 'openspec-apply',
- label: 'OpenSpec: Apply Changes',
- subtext: 'Apply an approved change proposal',
- action: async () => {
- const cmd = await getOpenSpecCommand('/openspec.apply');
- if (cmd) {
- onInjectOpenSpecPrompt(cmd.prompt);
- }
- setQuickActionOpen(false);
- }
- },
- {
- id: 'openspec-archive',
- label: 'OpenSpec: Archive Change',
- subtext: 'Archive a completed change',
- action: async () => {
- const cmd = await getOpenSpecCommand('/openspec.archive');
- if (cmd) {
- onInjectOpenSpecPrompt(cmd.prompt);
- }
- setQuickActionOpen(false);
- }
- },
- {
- id: 'openspec-implement',
- label: 'OpenSpec: Generate Auto Run',
- subtext: 'Generate Auto Run document from proposal',
- action: async () => {
- const cmd = await getOpenSpecCommand('/openspec.implement');
- if (cmd) {
- onInjectOpenSpecPrompt(cmd.prompt);
- }
- setQuickActionOpen(false);
- }
- },
- {
- id: 'openspec-help',
- label: 'OpenSpec: Help',
- subtext: 'Show OpenSpec workflow help',
- action: async () => {
- const cmd = await getOpenSpecCommand('/openspec.help');
- if (cmd) {
- onInjectOpenSpecPrompt(cmd.prompt);
- }
- setQuickActionOpen(false);
- }
- }
- ] : []),
// Debug commands - only visible when user types "debug"
{ id: 'debugResetBusy', label: 'Debug: Reset Busy State', subtext: 'Clear stuck thinking/busy state for all sessions', action: () => {
// Reset all sessions and tabs to idle state
diff --git a/src/renderer/components/UsageDashboard/AgentComparisonChart.tsx b/src/renderer/components/UsageDashboard/AgentComparisonChart.tsx
index 48081475..2e809bdf 100644
--- a/src/renderer/components/UsageDashboard/AgentComparisonChart.tsx
+++ b/src/renderer/components/UsageDashboard/AgentComparisonChart.tsx
@@ -157,7 +157,7 @@ export function AgentComparisonChart({ data, theme, colorBlindMode = false }: Ag
className="p-4 rounded-lg"
style={{ backgroundColor: theme.colors.bgMain }}
role="figure"
- aria-label={`Agent comparison chart showing query counts and duration by agent type. ${agentData.length} agents displayed.`}
+ aria-label={`Provider comparison chart showing query counts and duration by provider type. ${agentData.length} providers displayed.`}
>
{/* Header */}
@@ -165,7 +165,7 @@ export function AgentComparisonChart({ data, theme, colorBlindMode = false }: Ag
className="text-sm font-medium"
style={{ color: theme.colors.textMain }}
>
- Agent Comparison
+ Provider Comparison
diff --git a/src/renderer/components/UsageDashboard/AgentUsageChart.tsx b/src/renderer/components/UsageDashboard/AgentUsageChart.tsx
new file mode 100644
index 00000000..1d3880fc
--- /dev/null
+++ b/src/renderer/components/UsageDashboard/AgentUsageChart.tsx
@@ -0,0 +1,548 @@
+/**
+ * AgentUsageChart
+ *
+ * Line chart showing agent usage over time with dual metrics.
+ * Displays both session count and total time on the same chart.
+ *
+ * Features:
+ * - Dual Y-axes: sessions (left) and time (right)
+ * - Color-coded lines for each metric
+ * - Hover tooltips with exact values
+ * - Responsive SVG rendering
+ * - Theme-aware styling
+ */
+
+import React, { useState, useMemo, useCallback } from 'react';
+import { format, parseISO } from 'date-fns';
+import type { Theme } from '../../types';
+import type { StatsTimeRange, StatsAggregation } from '../../hooks/useStats';
+import { COLORBLIND_LINE_COLORS } from '../../constants/colorblindPalettes';
+
+// Data point for the chart
+interface DataPoint {
+ date: string;
+ formattedDate: string;
+ sessions: number;
+ duration: number; // Total duration in ms
+}
+
+interface AgentUsageChartProps {
+ /** Aggregated stats data from the API */
+ data: StatsAggregation;
+ /** Current time range selection */
+ timeRange: StatsTimeRange;
+ /** Current theme for styling */
+ theme: Theme;
+ /** Enable colorblind-friendly colors */
+ colorBlindMode?: boolean;
+}
+
+/**
+ * Format duration in milliseconds to human-readable string
+ */
+function formatDuration(ms: number): string {
+ if (ms === 0) return '0s';
+
+ const totalSeconds = Math.floor(ms / 1000);
+ const hours = Math.floor(totalSeconds / 3600);
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
+ const seconds = totalSeconds % 60;
+
+ if (hours > 0) {
+ return `${hours}h ${minutes}m`;
+ }
+ if (minutes > 0) {
+ return `${minutes}m ${seconds}s`;
+ }
+ return `${seconds}s`;
+}
+
+/**
+ * Format duration for Y-axis labels (shorter format)
+ */
+function formatYAxisDuration(ms: number): string {
+ if (ms === 0) return '0';
+
+ const totalSeconds = Math.floor(ms / 1000);
+ const hours = Math.floor(totalSeconds / 3600);
+ const minutes = Math.floor(totalSeconds / 60);
+
+ if (hours > 0) {
+ return `${hours}h`;
+ }
+ if (minutes > 0) {
+ return `${minutes}m`;
+ }
+ return `${totalSeconds}s`;
+}
+
+/**
+ * Format date for X-axis based on time range
+ */
+function formatXAxisDate(dateStr: string, timeRange: StatsTimeRange): string {
+ const date = parseISO(dateStr);
+
+ switch (timeRange) {
+ case 'day':
+ return format(date, 'HH:mm');
+ case 'week':
+ return format(date, 'EEE');
+ case 'month':
+ return format(date, 'MMM d');
+ case 'year':
+ return format(date, 'MMM');
+ case 'all':
+ return format(date, 'MMM yyyy');
+ default:
+ return format(date, 'MMM d');
+ }
+}
+
+export function AgentUsageChart({ data, timeRange, theme, colorBlindMode = false }: AgentUsageChartProps) {
+ const [hoveredPoint, setHoveredPoint] = useState<{ point: DataPoint; index: number } | null>(null);
+ const [tooltipPos, setTooltipPos] = useState<{ x: number; y: number } | null>(null);
+
+ // Chart dimensions
+ const chartWidth = 600;
+ const chartHeight = 220;
+ const padding = { top: 20, right: 60, bottom: 40, left: 50 };
+ const innerWidth = chartWidth - padding.left - padding.right;
+ const innerHeight = chartHeight - padding.top - padding.bottom;
+
+ // Colors for the two metrics
+ const sessionColor = useMemo(() => {
+ return colorBlindMode ? COLORBLIND_LINE_COLORS.primary : theme.colors.accent;
+ }, [colorBlindMode, theme.colors.accent]);
+
+ const durationColor = useMemo(() => {
+ return colorBlindMode ? COLORBLIND_LINE_COLORS.secondary : '#10b981'; // emerald
+ }, [colorBlindMode]);
+
+ // Combine byDay data with sessionsByDay to get both metrics per day
+ const chartData = useMemo((): DataPoint[] => {
+ if (data.byDay.length === 0) return [];
+
+ // Create a map of sessions by date
+ const sessionsByDateMap = new Map();
+ for (const day of data.sessionsByDay || []) {
+ sessionsByDateMap.set(day.date, day.count);
+ }
+
+ return data.byDay.map((day) => ({
+ date: day.date,
+ formattedDate: format(parseISO(day.date), 'EEEE, MMM d, yyyy'),
+ sessions: sessionsByDateMap.get(day.date) || 0,
+ duration: day.duration,
+ }));
+ }, [data.byDay, data.sessionsByDay]);
+
+ // Calculate scales for both Y-axes
+ const { xScale, yScaleSessions, yScaleDuration, yTicksSessions, yTicksDuration } = useMemo(() => {
+ if (chartData.length === 0) {
+ return {
+ xScale: (_: number) => padding.left,
+ yScaleSessions: (_: number) => chartHeight - padding.bottom,
+ yScaleDuration: (_: number) => chartHeight - padding.bottom,
+ yTicksSessions: [0],
+ yTicksDuration: [0],
+ };
+ }
+
+ const maxSessions = Math.max(...chartData.map((d) => d.sessions), 1);
+ const maxDuration = Math.max(...chartData.map((d) => d.duration), 1);
+
+ // Add 10% padding to max values
+ const sessionMax = Math.ceil(maxSessions * 1.1);
+ const durationMax = maxDuration * 1.1;
+
+ // X scale - linear across data points
+ const xScaleFn = (index: number) =>
+ padding.left + (index / Math.max(chartData.length - 1, 1)) * innerWidth;
+
+ // Y scale for sessions (left axis) - inverted for SVG coordinates
+ const yScaleSessionsFn = (value: number) =>
+ chartHeight - padding.bottom - (value / sessionMax) * innerHeight;
+
+ // Y scale for duration (right axis)
+ const yScaleDurationFn = (value: number) =>
+ chartHeight - padding.bottom - (value / durationMax) * innerHeight;
+
+ // Generate nice Y-axis ticks
+ const tickCount = 5;
+ const yTicksSessionsArr = Array.from({ length: tickCount }, (_, i) =>
+ Math.round((sessionMax / (tickCount - 1)) * i)
+ );
+ const yTicksDurationArr = Array.from({ length: tickCount }, (_, i) =>
+ (durationMax / (tickCount - 1)) * i
+ );
+
+ return {
+ xScale: xScaleFn,
+ yScaleSessions: yScaleSessionsFn,
+ yScaleDuration: yScaleDurationFn,
+ yTicksSessions: yTicksSessionsArr,
+ yTicksDuration: yTicksDurationArr,
+ };
+ }, [chartData, chartHeight, innerWidth, innerHeight, padding]);
+
+ // Generate line paths for both metrics
+ const sessionsPath = useMemo(() => {
+ if (chartData.length === 0) return '';
+ return chartData
+ .map((point, idx) => {
+ const x = xScale(idx);
+ const y = yScaleSessions(point.sessions);
+ return `${idx === 0 ? 'M' : 'L'} ${x} ${y}`;
+ })
+ .join(' ');
+ }, [chartData, xScale, yScaleSessions]);
+
+ const durationPath = useMemo(() => {
+ if (chartData.length === 0) return '';
+ return chartData
+ .map((point, idx) => {
+ const x = xScale(idx);
+ const y = yScaleDuration(point.duration);
+ return `${idx === 0 ? 'M' : 'L'} ${x} ${y}`;
+ })
+ .join(' ');
+ }, [chartData, xScale, yScaleDuration]);
+
+ // Handle mouse events for tooltip
+ const handleMouseEnter = useCallback(
+ (point: DataPoint, index: number, event: React.MouseEvent) => {
+ setHoveredPoint({ point, index });
+ const rect = event.currentTarget.getBoundingClientRect();
+ setTooltipPos({
+ x: rect.left + rect.width / 2,
+ y: rect.top,
+ });
+ },
+ []
+ );
+
+ const handleMouseLeave = useCallback(() => {
+ setHoveredPoint(null);
+ setTooltipPos(null);
+ }, []);
+
+ // Generate unique IDs for gradients
+ const gradientIdSessions = useMemo(() => `sessions-gradient-${Math.random().toString(36).slice(2, 9)}`, []);
+ const gradientIdDuration = useMemo(() => `duration-gradient-${Math.random().toString(36).slice(2, 9)}`, []);
+
+ // Parse colors for gradients
+ const sessionRgb = useMemo(() => {
+ const color = sessionColor;
+ if (color.startsWith('#')) {
+ const hex = color.slice(1);
+ return {
+ r: parseInt(hex.slice(0, 2), 16),
+ g: parseInt(hex.slice(2, 4), 16),
+ b: parseInt(hex.slice(4, 6), 16),
+ };
+ }
+ return { r: 100, g: 149, b: 237 };
+ }, [sessionColor]);
+
+ const durationRgb = useMemo(() => {
+ const color = durationColor;
+ if (color.startsWith('#')) {
+ const hex = color.slice(1);
+ return {
+ r: parseInt(hex.slice(0, 2), 16),
+ g: parseInt(hex.slice(2, 4), 16),
+ b: parseInt(hex.slice(4, 6), 16),
+ };
+ }
+ return { r: 16, g: 185, b: 129 };
+ }, [durationColor]);
+
+ // Area paths for gradient fills
+ const sessionsAreaPath = useMemo(() => {
+ if (chartData.length === 0) return '';
+ const pathStart = chartData
+ .map((point, idx) => {
+ const x = xScale(idx);
+ const y = yScaleSessions(point.sessions);
+ return `${idx === 0 ? 'M' : 'L'} ${x} ${y}`;
+ })
+ .join(' ');
+ const lastX = xScale(chartData.length - 1);
+ const firstX = xScale(0);
+ const baseline = chartHeight - padding.bottom;
+ return `${pathStart} L ${lastX} ${baseline} L ${firstX} ${baseline} Z`;
+ }, [chartData, xScale, yScaleSessions, chartHeight, padding.bottom]);
+
+ return (
+
+ {/* Header */}
+
+
+ Agent Usage Over Time
+
+
+
+ {/* Chart container */}
+
+ {chartData.length === 0 ? (
+
+ No usage data available
+
+ ) : (
+
+ {/* Gradient definitions */}
+
+
+
+
+
+
+
+
+
+
+
+ {/* Grid lines (horizontal) */}
+ {yTicksSessions.map((tick, idx) => (
+
+ ))}
+
+ {/* Left Y-axis labels (Sessions) */}
+ {yTicksSessions.map((tick, idx) => (
+
+ {tick}
+
+ ))}
+
+ {/* Right Y-axis labels (Duration) */}
+ {yTicksDuration.map((tick, idx) => (
+
+ {formatYAxisDuration(tick)}
+
+ ))}
+
+ {/* X-axis labels */}
+ {chartData.map((point, idx) => {
+ const labelInterval = chartData.length > 14
+ ? Math.ceil(chartData.length / 7)
+ : chartData.length > 7 ? 2 : 1;
+
+ if (idx % labelInterval !== 0 && idx !== chartData.length - 1) {
+ return null;
+ }
+
+ return (
+
+ {formatXAxisDate(point.date, timeRange)}
+
+ );
+ })}
+
+ {/* Sessions area fill */}
+
+
+ {/* Sessions line */}
+
+
+ {/* Duration line */}
+
+
+ {/* Data points for sessions */}
+ {chartData.map((point, idx) => {
+ const x = xScale(idx);
+ const y = yScaleSessions(point.sessions);
+ const isHovered = hoveredPoint?.index === idx;
+
+ return (
+ handleMouseEnter(point, idx, e)}
+ onMouseLeave={handleMouseLeave}
+ />
+ );
+ })}
+
+ {/* Data points for duration */}
+ {chartData.map((point, idx) => {
+ const x = xScale(idx);
+ const y = yScaleDuration(point.duration);
+ const isHovered = hoveredPoint?.index === idx;
+
+ return (
+ handleMouseEnter(point, idx, e)}
+ onMouseLeave={handleMouseLeave}
+ />
+ );
+ })}
+
+ {/* Y-axis labels */}
+
+ Sessions
+
+
+ Time
+
+
+ )}
+
+ {/* Tooltip */}
+ {hoveredPoint && tooltipPos && (
+
+
{hoveredPoint.point.formattedDate}
+
+
+
+ Sessions: {hoveredPoint.point.sessions}
+
+
+
+ Time: {formatDuration(hoveredPoint.point.duration)}
+
+
+
+ )}
+
+
+ {/* Legend */}
+
+
+ );
+}
+
+export default AgentUsageChart;
diff --git a/src/renderer/components/UsageDashboard/SessionStats.tsx b/src/renderer/components/UsageDashboard/SessionStats.tsx
index db13878b..fcc3fcfa 100644
--- a/src/renderer/components/UsageDashboard/SessionStats.tsx
+++ b/src/renderer/components/UsageDashboard/SessionStats.tsx
@@ -187,7 +187,7 @@ export function SessionStats({ sessions, theme, colorBlindMode = false }: Sessio
className="text-sm font-medium mb-4"
style={{ color: theme.colors.textMain }}
>
- Session Statistics
+ Agent Statistics
- Session Statistics
+ Agent Statistics
{/* Summary Cards */}
diff --git a/src/renderer/components/UsageDashboard/SourceDistributionChart.tsx b/src/renderer/components/UsageDashboard/SourceDistributionChart.tsx
index b4ecb6f3..874cd930 100644
--- a/src/renderer/components/UsageDashboard/SourceDistributionChart.tsx
+++ b/src/renderer/components/UsageDashboard/SourceDistributionChart.tsx
@@ -242,7 +242,7 @@ export function SourceDistributionChart({
className="p-4 rounded-lg"
style={{ backgroundColor: theme.colors.bgMain }}
role="figure"
- aria-label={`Source distribution chart showing ${metricMode === 'count' ? 'query counts' : 'duration'} breakdown between Interactive and Auto Run sources.`}
+ aria-label={`Session type chart showing ${metricMode === 'count' ? 'query counts' : 'duration'} breakdown between Interactive and Auto Run sessions.`}
>
{/* Header with title and metric toggle */}
@@ -250,7 +250,7 @@ export function SourceDistributionChart({
className="text-sm font-medium"
style={{ color: theme.colors.textMain }}
>
- Source Distribution
+ Session Type
{
const labels: Record = {
'summary-cards': 'Summary Cards',
- 'session-stats': 'Session Statistics',
- 'agent-comparison': 'Agent Comparison Chart',
- 'source-distribution': 'Source Distribution Chart',
+ 'session-stats': 'Agent Statistics',
+ 'agent-comparison': 'Provider Comparison Chart',
+ 'agent-usage': 'Agent Usage Chart',
+ 'source-distribution': 'Session Type Chart',
'location-distribution': 'Location Distribution Chart',
'peak-hours': 'Peak Hours Chart',
'activity-heatmap': 'Activity Heatmap',
@@ -822,12 +824,12 @@ export function UsageDashboardModal({
{viewMode === 'agents' && (
<>
- {/* Session Statistics */}
+ {/* Agent Statistics */}
handleSectionKeyDown(e, 'session-stats')}
className="outline-none rounded-lg transition-shadow dashboard-section-enter"
style={{
@@ -836,12 +838,12 @@ export function UsageDashboardModal({
}}
data-testid="section-session-stats"
>
-
+
- {/* Agent-focused view */}
+ {/* Provider Comparison */}
handleSectionKeyDown(e, 'agent-comparison')}
className="outline-none rounded-lg transition-shadow dashboard-section-enter"
style={{
- minHeight: '400px',
+ minHeight: '180px',
boxShadow: focusedSection === 'agent-comparison' ? `0 0 0 2px ${theme.colors.accent}` : 'none',
animationDelay: '100ms',
}}
data-testid="section-agent-comparison"
>
-
+
+
+ {/* Agent Usage Over Time */}
+ handleSectionKeyDown(e, 'agent-usage')}
+ className="outline-none rounded-lg transition-shadow dashboard-section-enter"
+ style={{
+ minHeight: '280px',
+ boxShadow: focusedSection === 'agent-usage' ? `0 0 0 2px ${theme.colors.accent}` : 'none',
+ animationDelay: '200ms',
+ }}
+ data-testid="section-agent-usage"
+ >
+
+
+
+
>
)}
diff --git a/src/renderer/hooks/batch/useBatchProcessor.ts b/src/renderer/hooks/batch/useBatchProcessor.ts
index c38777e8..631c3236 100644
--- a/src/renderer/hooks/batch/useBatchProcessor.ts
+++ b/src/renderer/hooks/batch/useBatchProcessor.ts
@@ -219,8 +219,11 @@ export function useBatchProcessor({
sessionsRef.current = sessions;
// Ref to track latest batchRunStates for time tracking callback
- const batchRunStatesRef = useRef(batchRunStates);
- batchRunStatesRef.current = batchRunStates;
+ // NOTE: Do NOT auto-sync with batchRunStates on every render!
+ // The dispatch wrapper updates this ref synchronously after each action.
+ // Auto-syncing can reset the ref to stale React state when batches are pending,
+ // causing the "0 of N tasks completed" regression. See commit 01eca193.
+ const batchRunStatesRef = useRef>(batchRunStates);
// Ref to track latest updateBatchStateAndBroadcast for async callbacks (fixes HMR stale closure)
const updateBatchStateAndBroadcastRef = useRef(null);