- Added per-session daily stats aggregation for richer usage analytics 📊

- Agent Usage chart now tracks Maestro sessions, not providers 🎯
- Map session IDs to friendly names in charts and legends 🏷️
- Limit Agent Usage chart to top 10 sessions by queries 🔟
- Toggle Agent Usage chart between query counts and time metrics ⏱️
- Auto Run queries now record stats with accurate `source: auto` 🤖
- Interactive queries detect Auto Run state to tag stats correctly 🧠
- Smarter cumulative token normalization avoids inflated first-event context % 🛡️
- About modal shows dev commit hash alongside app version 🔍
- Group chat history summaries now strip markdown for cleaner reading 🧹
This commit is contained in:
Pedram Amini
2026-01-22 10:15:01 -06:00
parent 8a6faf7836
commit 30868daa0d
20 changed files with 254 additions and 2441 deletions

View File

@@ -71,6 +71,7 @@ describe('stats IPC handlers', () => {
sessionsByDay: [],
avgSessionDuration: 0,
byAgentByDay: {},
bySessionByDay: {},
}),
exportToCsv: vi.fn().mockReturnValue('id,sessionId,...'),
clearOldData: vi.fn().mockReturnValue({ success: true, deletedCount: 0 }),

File diff suppressed because it is too large Load Diff

View File

@@ -107,8 +107,9 @@ vi.mock('../../../renderer/components/AchievementCard', () => ({
),
}));
// Add __APP_VERSION__ global
// Add __APP_VERSION__ and __COMMIT_HASH__ globals
(globalThis as unknown as { __APP_VERSION__: string }).__APP_VERSION__ = '1.0.0';
(globalThis as unknown as { __COMMIT_HASH__: string }).__COMMIT_HASH__ = '';
// Create test theme
const createTheme = (): Theme => ({
@@ -1110,4 +1111,5 @@ describe('AboutModal', () => {
expect(screen.getByText('$12,345,678.90')).toBeInTheDocument();
});
});
});

View File

@@ -41,6 +41,8 @@ const mockData: StatsAggregation = {
sessionsByDay: [],
avgSessionDuration: 288000,
byAgentByDay: {},
bySessionByDay: {},
};
// Empty data for edge case testing
@@ -58,6 +60,8 @@ const emptyData: StatsAggregation = {
sessionsByDay: [],
avgSessionDuration: 0,
byAgentByDay: {},
bySessionByDay: {},
};
// Data with large numbers
@@ -78,6 +82,8 @@ const largeNumbersData: StatsAggregation = {
sessionsByDay: [],
avgSessionDuration: 7200000,
byAgentByDay: {},
bySessionByDay: {},
};
// Single agent data
@@ -97,6 +103,8 @@ const singleAgentData: StatsAggregation = {
sessionsByDay: [],
avgSessionDuration: 360000,
byAgentByDay: {},
bySessionByDay: {},
};
// Only auto queries
@@ -116,6 +124,8 @@ const onlyAutoData: StatsAggregation = {
sessionsByDay: [],
avgSessionDuration: 360000,
byAgentByDay: {},
bySessionByDay: {},
};
describe('SummaryCards', () => {

View File

@@ -69,6 +69,8 @@ const mockStatsData: StatsAggregation = {
],
avgSessionDuration: 288000,
byAgentByDay: {},
bySessionByDay: {},
};
describe('Chart Accessibility - AgentComparisonChart', () => {
@@ -409,8 +411,10 @@ describe('Chart Accessibility - General ARIA Patterns', () => {
sessionsByDay: [],
avgSessionDuration: 0,
byAgentByDay: {},
bySessionByDay: {},
};
render(<AgentComparisonChart data={emptyData} theme={mockTheme} />);
expect(screen.getByText(/no agent data available/i)).toBeInTheDocument();
});

View File

@@ -211,6 +211,8 @@ const createSampleData = () => ({
],
avgSessionDuration: 144000,
byAgentByDay: {},
bySessionByDay: {},
});
describe('UsageDashboard Responsive Layout', () => {

View File

@@ -159,8 +159,10 @@ beforeEach(() => {
],
avgSessionDuration: 180000,
byAgentByDay: {},
bySessionByDay: {},
});
mockStats.getDatabaseSize.mockResolvedValue(1024 * 1024); // 1 MB
});
afterEach(() => {
@@ -280,6 +282,7 @@ describe('Usage Dashboard State Transition Animations', () => {
sessionsByDay: [],
avgSessionDuration: 240000,
byAgentByDay: {},
bySessionByDay: {},
};
it('applies dashboard-card-enter class to metric cards', () => {
@@ -586,4 +589,5 @@ describe('Usage Dashboard State Transition Animations', () => {
expect(totalMaxDuration).toBeLessThan(1000);
});
});
});

View File

@@ -146,6 +146,8 @@ const createSampleData = () => ({
],
avgSessionDuration: 144000,
byAgentByDay: {},
bySessionByDay: {},
});
describe('UsageDashboardModal', () => {

View File

@@ -1565,6 +1565,40 @@ export class StatsDB {
sessionCount: sessionTotals.count,
});
// By session by day (for agent usage chart - shows each Maestro session's usage over time)
const bySessionByDayStart = perfMetrics.start();
const bySessionByDayStmt = this.db.prepare(`
SELECT session_id,
date(start_time / 1000, 'unixepoch', 'localtime') as date,
COUNT(*) as count,
SUM(duration) as duration
FROM query_events
WHERE start_time >= ?
GROUP BY session_id, date(start_time / 1000, 'unixepoch', 'localtime')
ORDER BY session_id, date ASC
`);
const bySessionByDayRows = bySessionByDayStmt.all(startTime) as Array<{
session_id: string;
date: string;
count: number;
duration: number;
}>;
const bySessionByDay: Record<
string,
Array<{ date: string; count: number; duration: number }>
> = {};
for (const row of bySessionByDayRows) {
if (!bySessionByDay[row.session_id]) {
bySessionByDay[row.session_id] = [];
}
bySessionByDay[row.session_id].push({
date: row.date,
count: row.count,
duration: row.duration,
});
}
perfMetrics.end(bySessionByDayStart, 'getAggregatedStats:bySessionByDay', { range });
const totalDuration = perfMetrics.end(perfStart, 'getAggregatedStats:total', {
range,
totalQueries: totals.count,
@@ -1593,6 +1627,7 @@ export class StatsDB {
sessionsByDay: sessionsByDayRows,
avgSessionDuration: Math.round(avgSessionDurationResult.avg_duration),
byAgentByDay,
bySessionByDay,
};
}

View File

@@ -2314,11 +2314,17 @@ function MaestroConsoleInner() {
// Fire side effects AFTER state update (outside the updater function)
// Record stats for any completed query (even if we have queued items to process next)
if (toastData?.startTime && toastData?.agentType) {
// Determine if this query was part of an Auto Run session
const sessionIdForStats = toastData.sessionId || actualSessionId;
const isAutoRunQuery = getBatchStateRef.current
? getBatchStateRef.current(sessionIdForStats).isRunning
: false;
window.maestro.stats
.recordQuery({
sessionId: toastData.sessionId || actualSessionId,
sessionId: sessionIdForStats,
agentType: toastData.agentType,
source: 'user', // Interactive queries are always user-initiated
source: isAutoRunQuery ? 'auto' : 'user',
startTime: toastData.startTime,
duration: toastData.duration,
projectPath: toastData.projectPath,

View File

@@ -30,6 +30,7 @@ declare module '*.webp' {
// Vite-injected build-time constants
declare const __APP_VERSION__: string;
declare const __COMMIT_HASH__: string;
// Splash screen global functions (defined in index.html)
interface Window {

View File

@@ -188,6 +188,7 @@ export function AboutModal({
</h1>
<span className="text-xs font-mono" style={{ color: theme.colors.textDim }}>
v{__APP_VERSION__}
{__COMMIT_HASH__ && ` (${__COMMIT_HASH__})`}
</span>
</div>
<p className="text-xs opacity-70" style={{ color: theme.colors.textDim }}>

View File

@@ -10,6 +10,7 @@ import React, { useState, useMemo, useRef, useEffect } from 'react';
import { Check } from 'lucide-react';
import type { Theme } from '../types';
import type { GroupChatHistoryEntry } from '../../shared/group-chat-types';
import { stripMarkdown } from '../utils/textProcessing';
// Lookback period options for the activity graph
type LookbackPeriod = {
@@ -529,9 +530,9 @@ export function GroupChatHistoryPanel({
</span>
</div>
{/* Summary - full text */}
{/* Summary - strip markdown for clean display */}
<p className="text-xs leading-relaxed" style={{ color: theme.colors.textMain }}>
{entry.summary}
{stripMarkdown(entry.summary)}
</p>
{/* Footer with cost */}

View File

@@ -1,47 +1,52 @@
/**
* AgentUsageChart
*
* Line chart showing provider usage over time with one line per provider.
* Displays query counts and duration for each provider (claude-code, codex, opencode).
* Line chart showing Maestro agent (session) usage over time with one line per agent.
* Displays query counts and duration for each agent that was used during the time period.
*
* Features:
* - One line per provider
* - Dual Y-axes: queries (left) and time (right)
* - Provider-specific colors
* - One line per Maestro agent (named session from left panel)
* - Toggle between query count and time metrics
* - Session ID to name mapping when names are available
* - Hover tooltips with exact values
* - Responsive SVG rendering
* - Theme-aware styling
* - Limits display to top 10 agents by query count
*/
import React, { useState, useMemo, useCallback } from 'react';
import { format, parseISO } from 'date-fns';
import type { Theme } from '../../types';
import type { Theme, Session } from '../../types';
import type { StatsTimeRange, StatsAggregation } from '../../hooks/useStats';
import {
COLORBLIND_AGENT_PALETTE,
COLORBLIND_LINE_COLORS,
} from '../../constants/colorblindPalettes';
import { COLORBLIND_AGENT_PALETTE } from '../../constants/colorblindPalettes';
// Provider colors (matching AgentComparisonChart)
const PROVIDER_COLORS: Record<string, string> = {
'claude-code': '#a78bfa', // violet
codex: '#34d399', // emerald
opencode: '#60a5fa', // blue
};
// 10 distinct colors for agents
const AGENT_COLORS = [
'#a78bfa', // violet
'#34d399', // emerald
'#60a5fa', // blue
'#f472b6', // pink
'#fbbf24', // amber
'#fb923c', // orange
'#4ade80', // green
'#38bdf8', // sky
'#c084fc', // purple
'#f87171', // red
];
// Data point for a single provider on a single day
interface ProviderDayData {
// Data point for a single agent on a single day
interface AgentDayData {
date: string;
formattedDate: string;
count: number;
duration: number;
}
// All providers' data for a single day
// All agents' data for a single day
interface DayData {
date: string;
formattedDate: string;
providers: Record<string, { count: number; duration: number }>;
agents: Record<string, { count: number; duration: number }>;
}
interface AgentUsageChartProps {
@@ -53,6 +58,8 @@ interface AgentUsageChartProps {
theme: Theme;
/** Enable colorblind-friendly colors */
colorBlindMode?: boolean;
/** Current sessions for mapping IDs to names */
sessions?: Session[];
}
/**
@@ -117,13 +124,40 @@ function formatXAxisDate(dateStr: string, timeRange: StatsTimeRange): string {
}
/**
* Get provider color, with colorblind mode support
* Get agent color based on index, with colorblind mode support
*/
function getProviderColor(provider: string, index: number, colorBlindMode: boolean): string {
function getAgentColor(index: number, colorBlindMode: boolean): string {
if (colorBlindMode) {
return COLORBLIND_AGENT_PALETTE[index % COLORBLIND_AGENT_PALETTE.length];
}
return PROVIDER_COLORS[provider] || COLORBLIND_LINE_COLORS.primary;
return AGENT_COLORS[index % AGENT_COLORS.length];
}
/**
* Extract a display name from a session ID
* Session IDs are in format: "sessionId-ai-tabId" or similar
* Returns the first 8 chars of the session UUID or the name if found
*/
function getSessionDisplayName(sessionId: string, sessions?: Session[]): string {
// Try to find the session by ID to get its name
if (sessions) {
// Session IDs in stats may include tab suffixes like "-ai-tabId"
// Try to match the base session ID
const session = sessions.find((s) => sessionId.startsWith(s.id));
if (session?.name) {
return session.name;
}
}
// Fallback: extract the UUID part and show first 8 chars
// Format is typically "uuid-ai-tabId" or just "uuid"
const parts = sessionId.split('-');
if (parts.length >= 5) {
// UUID format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
// Take first segment
return parts[0].substring(0, 8).toUpperCase();
}
return sessionId.substring(0, 8).toUpperCase();
}
export function AgentUsageChart({
@@ -131,10 +165,9 @@ export function AgentUsageChart({
timeRange,
theme,
colorBlindMode = false,
sessions,
}: AgentUsageChartProps) {
const [hoveredDay, setHoveredDay] = useState<{ dayIndex: number; provider?: string } | null>(
null
);
const [hoveredDay, setHoveredDay] = useState<{ dayIndex: number; agent?: string } | null>(null);
const [tooltipPos, setTooltipPos] = useState<{ x: number; y: number } | null>(null);
const [metricMode, setMetricMode] = useState<'count' | 'duration'>('count');
@@ -145,29 +178,46 @@ export function AgentUsageChart({
const innerWidth = chartWidth - padding.left - padding.right;
const innerHeight = chartHeight - padding.top - padding.bottom;
// Get list of providers and their data
const { providers, chartData, allDates } = useMemo(() => {
const byAgentByDay = data.byAgentByDay || {};
const providerList = Object.keys(byAgentByDay).sort();
// Get list of agents and their data (limited to top 10 by total queries)
const { agents, chartData, allDates, agentDisplayNames } = useMemo(() => {
const bySessionByDay = data.bySessionByDay || {};
// Collect all unique dates
// Calculate total queries per session to rank them
const sessionTotals: Array<{ sessionId: string; totalQueries: number }> = [];
for (const sessionId of Object.keys(bySessionByDay)) {
const totalQueries = bySessionByDay[sessionId].reduce((sum, day) => sum + day.count, 0);
sessionTotals.push({ sessionId, totalQueries });
}
// Sort by total queries descending and take top 10
sessionTotals.sort((a, b) => b.totalQueries - a.totalQueries);
const topSessions = sessionTotals.slice(0, 10);
const agentList = topSessions.map((s) => s.sessionId);
// Build display name map
const displayNames: Record<string, string> = {};
for (const sessionId of agentList) {
displayNames[sessionId] = getSessionDisplayName(sessionId, sessions);
}
// Collect all unique dates from selected agents
const dateSet = new Set<string>();
for (const provider of providerList) {
for (const day of byAgentByDay[provider]) {
for (const sessionId of agentList) {
for (const day of bySessionByDay[sessionId]) {
dateSet.add(day.date);
}
}
const sortedDates = Array.from(dateSet).sort();
// Build per-provider arrays aligned to all dates
const providerData: Record<string, ProviderDayData[]> = {};
for (const provider of providerList) {
// Build per-agent arrays aligned to all dates
const agentData: Record<string, AgentDayData[]> = {};
for (const sessionId of agentList) {
const dayMap = new Map<string, { count: number; duration: number }>();
for (const day of byAgentByDay[provider]) {
for (const day of bySessionByDay[sessionId]) {
dayMap.set(day.date, { count: day.count, duration: day.duration });
}
providerData[provider] = sortedDates.map((date) => ({
agentData[sessionId] = sortedDates.map((date) => ({
date,
formattedDate: format(parseISO(date), 'EEEE, MMM d, yyyy'),
count: dayMap.get(date)?.count || 0,
@@ -177,26 +227,27 @@ export function AgentUsageChart({
// Build combined day data for tooltips
const combinedData: DayData[] = sortedDates.map((date) => {
const providers: Record<string, { count: number; duration: number }> = {};
for (const provider of providerList) {
const dayData = providerData[provider].find((d) => d.date === date);
const agents: Record<string, { count: number; duration: number }> = {};
for (const sessionId of agentList) {
const dayData = agentData[sessionId].find((d) => d.date === date);
if (dayData) {
providers[provider] = { count: dayData.count, duration: dayData.duration };
agents[sessionId] = { count: dayData.count, duration: dayData.duration };
}
}
return {
date,
formattedDate: format(parseISO(date), 'EEEE, MMM d, yyyy'),
providers,
agents,
};
});
return {
providers: providerList,
chartData: providerData,
agents: agentList,
chartData: agentData,
allDates: combinedData,
agentDisplayNames: displayNames,
};
}, [data.byAgentByDay]);
}, [data.bySessionByDay, sessions]);
// Calculate scales
const { xScale, yScale, yTicks } = useMemo(() => {
@@ -208,13 +259,13 @@ export function AgentUsageChart({
};
}
// Find max value across all providers
// Find max value across all agents
let maxValue = 1;
for (const provider of providers) {
const providerMax = Math.max(
...chartData[provider].map((d) => (metricMode === 'count' ? d.count : d.duration))
for (const agent of agents) {
const agentMax = Math.max(
...chartData[agent].map((d) => (metricMode === 'count' ? d.count : d.duration))
);
maxValue = Math.max(maxValue, providerMax);
maxValue = Math.max(maxValue, agentMax);
}
// Add 10% padding
@@ -235,16 +286,16 @@ export function AgentUsageChart({
: Array.from({ length: tickCount }, (_, i) => (yMax / (tickCount - 1)) * i);
return { xScale: xScaleFn, yScale: yScaleFn, yTicks: yTicksArr };
}, [allDates, providers, chartData, metricMode, chartHeight, innerWidth, innerHeight, padding]);
}, [allDates, agents, chartData, metricMode, chartHeight, innerWidth, innerHeight, padding]);
// Generate line paths for each provider
// Generate line paths for each agent
const linePaths = useMemo(() => {
const paths: Record<string, string> = {};
for (const provider of providers) {
const providerDays = chartData[provider];
if (providerDays.length === 0) continue;
for (const agent of agents) {
const agentDays = chartData[agent];
if (agentDays.length === 0) continue;
paths[provider] = providerDays
paths[agent] = agentDays
.map((day, idx) => {
const x = xScale(idx);
const y = yScale(metricMode === 'count' ? day.count : day.duration);
@@ -253,12 +304,12 @@ export function AgentUsageChart({
.join(' ');
}
return paths;
}, [providers, chartData, xScale, yScale, metricMode]);
}, [agents, chartData, xScale, yScale, metricMode]);
// Handle mouse events
const handleMouseEnter = useCallback(
(dayIndex: number, provider: string, event: React.MouseEvent<SVGCircleElement>) => {
setHoveredDay({ dayIndex, provider });
(dayIndex: number, agent: string, event: React.MouseEvent<SVGCircleElement>) => {
setHoveredDay({ dayIndex, agent });
const rect = event.currentTarget.getBoundingClientRect();
setTooltipPos({
x: rect.left + rect.width / 2,
@@ -278,7 +329,7 @@ export function AgentUsageChart({
className="p-4 rounded-lg"
style={{ backgroundColor: theme.colors.bgMain }}
role="figure"
aria-label={`Provider usage chart showing ${metricMode === 'count' ? 'query counts' : 'duration'} over time. ${providers.length} providers displayed.`}
aria-label={`Agent usage chart showing ${metricMode === 'count' ? 'query counts' : 'duration'} over time. ${agents.length} agents displayed.`}
>
{/* Header with title and metric toggle */}
<div className="flex items-center justify-between mb-4">
@@ -319,7 +370,7 @@ export function AgentUsageChart({
{/* Chart container */}
<div className="relative">
{allDates.length === 0 || providers.length === 0 ? (
{allDates.length === 0 || agents.length === 0 ? (
<div
className="flex items-center justify-center"
style={{ height: chartHeight, color: theme.colors.textDim }}
@@ -332,7 +383,7 @@ export function AgentUsageChart({
viewBox={`0 0 ${chartWidth} ${chartHeight}`}
preserveAspectRatio="xMidYMid meet"
role="img"
aria-label={`Line chart showing ${metricMode === 'count' ? 'query counts' : 'duration'} per provider over time`}
aria-label={`Line chart showing ${metricMode === 'count' ? 'query counts' : 'duration'} per agent over time`}
>
{/* Grid lines */}
{yTicks.map((tick, idx) => (
@@ -386,13 +437,13 @@ export function AgentUsageChart({
);
})}
{/* Lines for each provider */}
{providers.map((provider, providerIdx) => {
const color = getProviderColor(provider, providerIdx, colorBlindMode);
{/* Lines for each agent */}
{agents.map((agent, agentIdx) => {
const color = getAgentColor(agentIdx, colorBlindMode);
return (
<path
key={`line-${provider}`}
d={linePaths[provider]}
key={`line-${agent}`}
d={linePaths[agent]}
fill="none"
stroke={color}
strokeWidth={2}
@@ -403,18 +454,17 @@ export function AgentUsageChart({
);
})}
{/* Data points for each provider */}
{providers.map((provider, providerIdx) => {
const color = getProviderColor(provider, providerIdx, colorBlindMode);
return chartData[provider].map((day, dayIdx) => {
{/* Data points for each agent */}
{agents.map((agent, agentIdx) => {
const color = getAgentColor(agentIdx, colorBlindMode);
return chartData[agent].map((day, dayIdx) => {
const x = xScale(dayIdx);
const y = yScale(metricMode === 'count' ? day.count : day.duration);
const isHovered =
hoveredDay?.dayIndex === dayIdx && hoveredDay?.provider === provider;
const isHovered = hoveredDay?.dayIndex === dayIdx && hoveredDay?.agent === agent;
return (
<circle
key={`point-${provider}-${dayIdx}`}
key={`point-${agent}-${dayIdx}`}
cx={x}
cy={y}
r={isHovered ? 6 : 4}
@@ -425,7 +475,7 @@ export function AgentUsageChart({
cursor: 'pointer',
transition: 'r 0.15s ease',
}}
onMouseEnter={(e) => handleMouseEnter(dayIdx, provider, e)}
onMouseEnter={(e) => handleMouseEnter(dayIdx, agent, e)}
onMouseLeave={handleMouseLeave}
/>
);
@@ -462,14 +512,14 @@ export function AgentUsageChart({
>
<div className="font-medium mb-1">{allDates[hoveredDay.dayIndex].formattedDate}</div>
<div style={{ color: theme.colors.textDim }}>
{providers.map((provider, idx) => {
const dayData = allDates[hoveredDay.dayIndex].providers[provider];
{agents.map((agent, idx) => {
const dayData = allDates[hoveredDay.dayIndex].agents[agent];
if (!dayData || (dayData.count === 0 && dayData.duration === 0)) return null;
const color = getProviderColor(provider, idx, colorBlindMode);
const color = getAgentColor(idx, colorBlindMode);
return (
<div key={provider} className="flex items-center gap-2">
<div key={agent} className="flex items-center gap-2">
<span className="w-2 h-2 rounded-full" style={{ backgroundColor: color }} />
<span>{provider}:</span>
<span>{agentDisplayNames[agent]}:</span>
<span style={{ color: theme.colors.textMain }}>
{metricMode === 'count'
? `${dayData.count} ${dayData.count === 1 ? 'query' : 'queries'}`
@@ -488,13 +538,13 @@ export function AgentUsageChart({
className="flex items-center justify-center gap-4 mt-3 pt-3 border-t flex-wrap"
style={{ borderColor: theme.colors.border }}
>
{providers.map((provider, idx) => {
const color = getProviderColor(provider, idx, colorBlindMode);
{agents.map((agent, idx) => {
const color = getAgentColor(idx, colorBlindMode);
return (
<div key={provider} className="flex items-center gap-1.5">
<div key={agent} className="flex items-center gap-1.5">
<div className="w-3 h-0.5 rounded" style={{ backgroundColor: color }} />
<span className="text-xs" style={{ color: theme.colors.textDim }}>
{provider}
{agentDisplayNames[agent]}
</span>
</div>
);

View File

@@ -75,8 +75,10 @@ interface StatsAggregation {
sessionsByAgent: Record<string, number>;
sessionsByDay: Array<{ date: string; count: number }>;
avgSessionDuration: number;
// Per-agent per-day breakdown for provider usage chart
// Per-provider per-day breakdown for provider comparison
byAgentByDay: Record<string, Array<{ date: string; count: number; duration: number }>>;
// Per-session per-day breakdown for agent usage chart
bySessionByDay: Record<string, Array<{ date: string; count: number; duration: number }>>;
}
// View mode options for the dashboard
@@ -971,6 +973,7 @@ export function UsageDashboardModal({
timeRange={timeRange}
theme={theme}
colorBlindMode={colorBlindMode}
sessions={sessions}
/>
</ChartErrorBoundary>
</div>

View File

@@ -2162,6 +2162,7 @@ interface MaestroAPI {
sessionsByDay: Array<{ date: string; count: number }>;
avgSessionDuration: number;
byAgentByDay: Record<string, Array<{ date: string; count: number; duration: number }>>;
bySessionByDay: Record<string, Array<{ date: string; count: number; duration: number }>>;
}>;
// Export query events to CSV
exportCsv: (range: 'day' | 'week' | 'month' | 'year' | 'all') => Promise<string>;

View File

@@ -190,6 +190,7 @@ export function useAgentExecution(deps: UseAgentExecutionDeps): UseAgentExecutio
let agentSessionId: string | undefined;
let responseText = '';
let taskUsageStats: UsageStats | undefined;
const queryStartTime = Date.now(); // Track start time for stats
// Array to collect cleanup functions as listeners are registered
const cleanupFns: (() => void)[] = [];
@@ -231,6 +232,25 @@ export function useAgentExecution(deps: UseAgentExecutionDeps): UseAgentExecutio
// Clean up listeners
cleanup();
// Record query stats for Auto Run queries
const queryDuration = Date.now() - queryStartTime;
const activeTab = getActiveTab(session);
window.maestro.stats
.recordQuery({
sessionId: sessionId, // Use the original session ID, not the batch ID
agentType: session.toolType,
source: 'auto', // Auto Run queries are always 'auto'
startTime: queryStartTime,
duration: queryDuration,
projectPath: effectiveCwd,
tabId: activeTab?.id,
isRemote: session.sessionSshRemoteConfig?.enabled ?? false,
})
.catch((err) => {
// Don't fail the batch flow if stats recording fails
console.warn('[spawnAgentForSession] Failed to record query stats:', err);
});
// Check for queued items BEFORE updating state (using sessionsRef for latest state)
const currentSession = sessionsRef.current.find((s) => s.id === sessionId);
let queuedItemToProcess: { sessionId: string; item: QueuedItem } | null = null;

View File

@@ -34,8 +34,10 @@ export interface StatsAggregation {
sessionsByAgent: Record<string, number>;
sessionsByDay: Array<{ date: string; count: number }>;
avgSessionDuration: number;
// Per-agent per-day breakdown for provider usage chart
// Per-provider per-day breakdown for provider comparison
byAgentByDay: Record<string, Array<{ date: string; count: number; duration: number }>>;
// Per-session per-day breakdown for agent usage chart
bySessionByDay: Record<string, Array<{ date: string; count: number; duration: number }>>;
}
// Return type for the useStats hook

View File

@@ -93,8 +93,10 @@ export interface StatsAggregation {
sessionsByDay: Array<{ date: string; count: number }>;
/** Average session duration in ms (for closed sessions) */
avgSessionDuration: number;
/** Queries and duration by agent per day (for provider usage chart) */
/** Queries and duration by provider per day (for provider comparison) */
byAgentByDay: Record<string, Array<{ date: string; count: number; duration: number }>>;
/** Queries and duration by Maestro session per day (for agent usage chart) */
bySessionByDay: Record<string, Array<{ date: string; count: number; duration: number }>>;
}
/**

View File

@@ -2,12 +2,23 @@ import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
import { readFileSync } from 'fs';
import { execSync } from 'child_process';
// Read version from package.json as fallback
const packageJson = JSON.parse(readFileSync(path.join(__dirname, 'package.json'), 'utf-8'));
// Use VITE_APP_VERSION env var if set (during CI builds), otherwise use package.json
const appVersion = process.env.VITE_APP_VERSION || packageJson.version;
// Get the first 8 chars of git commit hash for dev mode
function getCommitHash(): string {
try {
// Note: execSync is safe here - no user input, static git command
return execSync('git rev-parse HEAD', { encoding: 'utf-8' }).trim().slice(0, 8);
} catch {
return '';
}
}
const disableHmr = process.env.DISABLE_HMR === '1';
export default defineConfig(({ mode }) => ({
@@ -16,6 +27,8 @@ export default defineConfig(({ mode }) => ({
base: './',
define: {
__APP_VERSION__: JSON.stringify(appVersion),
// Show commit hash only in development mode
__COMMIT_HASH__: JSON.stringify(mode === 'development' ? getCommitHash() : ''),
// Explicitly define NODE_ENV for React and related packages
'process.env.NODE_ENV': JSON.stringify(mode),
},