(null);
@@ -275,18 +277,45 @@ export function AboutModal({ theme, sessions, autoRunStats, onClose }: AboutModa
)}
- {/* Project Link */}
-
+ {/* Action Links */}
+
+ {/* Project Link */}
+
+
+ {/* Leaderboard Registration */}
+ {onOpenLeaderboardRegistration && (
+
+ )}
+
{/* Made in Austin */}
diff --git a/src/renderer/components/FilePreview.tsx b/src/renderer/components/FilePreview.tsx
index e3592616..7e7ad7fc 100644
--- a/src/renderer/components/FilePreview.tsx
+++ b/src/renderer/components/FilePreview.tsx
@@ -97,6 +97,17 @@ const formatTokenCount = (count: number): string => {
return count.toLocaleString();
};
+// Count markdown tasks (checkboxes)
+const countMarkdownTasks = (content: string): { open: number; closed: number } => {
+ // Match markdown checkboxes: - [ ] or - [x] (also * [ ] and * [x])
+ const openMatches = content.match(/^[\s]*[-*]\s*\[\s*\]/gm);
+ const closedMatches = content.match(/^[\s]*[-*]\s*\[[xX]\]/gm);
+ return {
+ open: openMatches?.length || 0,
+ closed: closedMatches?.length || 0
+ };
+};
+
// Lazy-loaded tokenizer encoder (cl100k_base is used by Claude/GPT-4)
let encoderPromise: Promise> | null = null;
const getEncoder = () => {
@@ -322,6 +333,15 @@ export function FilePreview({ file, onClose, theme, markdownRawMode, setMarkdown
const isMarkdown = language === 'markdown';
const isImage = isImageFile(file.name);
+ // Calculate task counts for markdown files
+ const taskCounts = useMemo(() => {
+ if (!isMarkdown || !file?.content) return null;
+ const counts = countMarkdownTasks(file.content);
+ // Only return if there are any tasks
+ if (counts.open === 0 && counts.closed === 0) return null;
+ return counts;
+ }, [isMarkdown, file?.content]);
+
// Extract directory path without filename
const directoryPath = file.path.substring(0, file.path.lastIndexOf('/'));
@@ -796,7 +816,7 @@ export function FilePreview({ file, onClose, theme, markdownRawMode, setMarkdown
{/* File Stats subbar - hidden on scroll */}
- {(fileStats || tokenCount !== null) && showStatsBar && (
+ {(fileStats || tokenCount !== null || taskCounts) && showStatsBar && (
>
)}
+ {taskCounts && (
+
+ Tasks:{' '}
+ {taskCounts.closed}
+ /
+ {taskCounts.open + taskCounts.closed}
+
+ )}
)}
diff --git a/src/renderer/components/FirstRunCelebration.tsx b/src/renderer/components/FirstRunCelebration.tsx
index 012102fc..b2aab412 100644
--- a/src/renderer/components/FirstRunCelebration.tsx
+++ b/src/renderer/components/FirstRunCelebration.tsx
@@ -28,6 +28,10 @@ interface FirstRunCelebrationProps {
totalTasks: number;
/** Callback when modal is dismissed */
onClose: () => void;
+ /** Callback to open leaderboard registration */
+ onOpenLeaderboardRegistration?: () => void;
+ /** Whether the user is already registered for the leaderboard */
+ isLeaderboardRegistered?: boolean;
}
/**
@@ -66,6 +70,8 @@ export function FirstRunCelebration({
completedTasks,
totalTasks,
onClose,
+ onOpenLeaderboardRegistration,
+ isLeaderboardRegistered,
}: FirstRunCelebrationProps): JSX.Element {
const containerRef = useRef(null);
const { registerLayer, unregisterLayer, updateLayerHandler } = useLayerStack();
@@ -408,7 +414,7 @@ export function FirstRunCelebration({
{/* Button */}
-
+
)}
+
+ {/* Leaderboard Registration */}
+ {onOpenLeaderboardRegistration && !isLeaderboardRegistered && (
+ {
+ onClose();
+ onOpenLeaderboardRegistration();
+ }}
+ className="w-full py-2.5 rounded-lg font-medium transition-all flex items-center justify-center gap-2 hover:opacity-90"
+ style={{
+ backgroundColor: `${goldColor}20`,
+ color: goldColor,
+ border: `1px solid ${goldColor}60`,
+ }}
+ >
+
+ Join Global Leaderboard
+
+ )}
diff --git a/src/renderer/constants/modalPriorities.ts b/src/renderer/constants/modalPriorities.ts
index 3e7ae9cb..b9c6a660 100644
--- a/src/renderer/constants/modalPriorities.ts
+++ b/src/renderer/constants/modalPriorities.ts
@@ -86,6 +86,9 @@ export const MODAL_PRIORITIES = {
/** Keyboard shortcuts help modal */
SHORTCUTS_HELP: 650,
+ /** Leaderboard registration modal */
+ LEADERBOARD_REGISTRATION: 620,
+
/** About/info modal */
ABOUT: 600,
diff --git a/src/renderer/hooks/useSettings.ts b/src/renderer/hooks/useSettings.ts
index ea5a6a13..dbefb1c8 100644
--- a/src/renderer/hooks/useSettings.ts
+++ b/src/renderer/hooks/useSettings.ts
@@ -1,5 +1,5 @@
import { useState, useEffect, useCallback, useMemo } from 'react';
-import type { LLMProvider, ThemeId, Shortcut, CustomAICommand, GlobalStats, AutoRunStats, OnboardingStats } from '../types';
+import type { LLMProvider, ThemeId, Shortcut, CustomAICommand, GlobalStats, AutoRunStats, OnboardingStats, LeaderboardRegistration } from '../types';
import { DEFAULT_SHORTCUTS } from '../constants/shortcuts';
// Default global stats
@@ -208,6 +208,11 @@ export interface UseSettingsReturn {
averageConversationExchanges: number;
averagePhasesPerWizard: number;
};
+
+ // Leaderboard Registration (persistent)
+ leaderboardRegistration: LeaderboardRegistration | null;
+ setLeaderboardRegistration: (value: LeaderboardRegistration | null) => void;
+ isLeaderboardRegistered: boolean;
}
export function useSettings(): UseSettingsReturn {
@@ -287,6 +292,9 @@ export function useSettings(): UseSettingsReturn {
// Onboarding Stats (persistent, local-only analytics)
const [onboardingStats, setOnboardingStatsState] = useState(DEFAULT_ONBOARDING_STATS);
+ // Leaderboard Registration (persistent)
+ const [leaderboardRegistration, setLeaderboardRegistrationState] = useState(null);
+
// Wrapper functions that persist to electron-store
// PERF: All wrapped in useCallback to prevent re-renders
const setLlmProvider = useCallback((value: LLMProvider) => {
@@ -611,8 +619,11 @@ export function useSettings(): UseSettingsReturn {
// UI collapse state setters
const setUngroupedCollapsed = useCallback((value: boolean) => {
+ console.log('[useSettings] setUngroupedCollapsed called with:', value);
setUngroupedCollapsedState(value);
- window.maestro.settings.set('ungroupedCollapsed', value);
+ window.maestro.settings.set('ungroupedCollapsed', value)
+ .then(() => console.log('[useSettings] ungroupedCollapsed persisted successfully'))
+ .catch((err: unknown) => console.error('[useSettings] Failed to persist ungroupedCollapsed:', err));
}, []);
// Onboarding setters
@@ -792,6 +803,17 @@ export function useSettings(): UseSettingsReturn {
onboardingStats.averagePhasesPerWizard,
]);
+ // Leaderboard Registration setter
+ const setLeaderboardRegistration = useCallback((value: LeaderboardRegistration | null) => {
+ setLeaderboardRegistrationState(value);
+ window.maestro.settings.set('leaderboardRegistration', value);
+ }, []);
+
+ // Computed property for checking if registered
+ const isLeaderboardRegistered = useMemo(() => {
+ return leaderboardRegistration !== null && leaderboardRegistration.emailConfirmed;
+ }, [leaderboardRegistration]);
+
// Load settings from electron-store on mount
useEffect(() => {
const loadSettings = async () => {
@@ -831,6 +853,7 @@ export function useSettings(): UseSettingsReturn {
const savedTourCompleted = await window.maestro.settings.get('tourCompleted');
const savedFirstAutoRunCompleted = await window.maestro.settings.get('firstAutoRunCompleted');
const savedOnboardingStats = await window.maestro.settings.get('onboardingStats');
+ const savedLeaderboardRegistration = await window.maestro.settings.get('leaderboardRegistration');
if (savedEnterToSendAI !== undefined) setEnterToSendAIState(savedEnterToSendAI);
if (savedEnterToSendTerminal !== undefined) setEnterToSendTerminalState(savedEnterToSendTerminal);
@@ -950,6 +973,11 @@ export function useSettings(): UseSettingsReturn {
setOnboardingStatsState({ ...DEFAULT_ONBOARDING_STATS, ...savedOnboardingStats });
}
+ // Load leaderboard registration
+ if (savedLeaderboardRegistration !== undefined) {
+ setLeaderboardRegistrationState(savedLeaderboardRegistration as LeaderboardRegistration | null);
+ }
+
// Mark settings as loaded
setSettingsLoaded(true);
};
@@ -1050,6 +1078,9 @@ export function useSettings(): UseSettingsReturn {
recordTourComplete,
recordTourSkip,
getOnboardingAnalytics,
+ leaderboardRegistration,
+ setLeaderboardRegistration,
+ isLeaderboardRegistered,
}), [
// State values
settingsLoaded,
@@ -1137,5 +1168,8 @@ export function useSettings(): UseSettingsReturn {
recordTourComplete,
recordTourSkip,
getOnboardingAnalytics,
+ leaderboardRegistration,
+ setLeaderboardRegistration,
+ isLeaderboardRegistered,
]);
}
diff --git a/src/renderer/types/index.ts b/src/renderer/types/index.ts
index 79b98f6e..66f86d14 100644
--- a/src/renderer/types/index.ts
+++ b/src/renderer/types/index.ts
@@ -451,3 +451,27 @@ export interface CustomAICommand {
isBuiltIn?: boolean; // If true, cannot be deleted (only edited)
}
+// Leaderboard registration data for runmaestro.ai integration
+export interface LeaderboardRegistration {
+ // Required fields
+ email: string; // User's email (will be confirmed)
+ displayName: string; // Display name on leaderboard
+ // Optional social handles (without @)
+ twitterHandle?: string; // X/Twitter handle
+ githubUsername?: string; // GitHub username
+ linkedinHandle?: string; // LinkedIn handle
+ // Registration state
+ registeredAt: number; // Timestamp when registered
+ emailConfirmed: boolean; // Whether email has been confirmed
+ lastSubmissionAt?: number; // Last successful submission timestamp
+}
+
+// Response from leaderboard submission API
+export interface LeaderboardSubmitResponse {
+ success: boolean;
+ message: string;
+ requiresConfirmation?: boolean;
+ confirmationUrl?: string;
+ error?: string;
+}
+