mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
- 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:
@@ -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
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -211,6 +211,8 @@ const createSampleData = () => ({
|
||||
],
|
||||
avgSessionDuration: 144000,
|
||||
byAgentByDay: {},
|
||||
bySessionByDay: {},
|
||||
|
||||
});
|
||||
|
||||
describe('UsageDashboard Responsive Layout', () => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -146,6 +146,8 @@ const createSampleData = () => ({
|
||||
],
|
||||
avgSessionDuration: 144000,
|
||||
byAgentByDay: {},
|
||||
bySessionByDay: {},
|
||||
|
||||
});
|
||||
|
||||
describe('UsageDashboardModal', () => {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
1
src/renderer/assets.d.ts
vendored
1
src/renderer/assets.d.ts
vendored
@@ -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 {
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
1
src/renderer/global.d.ts
vendored
1
src/renderer/global.d.ts
vendored
@@ -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>;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }>>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user