diff --git a/README.md b/README.md index 64c2aa62..765aef62 100644 --- a/README.md +++ b/README.md @@ -36,8 +36,6 @@ Download the latest release for your platform from the [Releases](https://github - **Linux**: `.AppImage`, `.deb`, or `.rpm` - **Upgrading**: Simply replace the old binary with the new one. All your data (sessions, settings, playbooks, history) persists in your [config directory](#configuration). -NOTE: On macOS you may need to clear the quarantine label to successfully launch: `xattr -dr com.apple.quarantine Maestro.app` - ### Requirements - [Claude Code](https://docs.anthropic.com/en/docs/claude-code) installed and authenticated diff --git a/scripts/github-community-analytics.js b/scripts/github-community-analytics.js index 0b7b0553..d53ae2d0 100755 --- a/scripts/github-community-analytics.js +++ b/scripts/github-community-analytics.js @@ -586,24 +586,48 @@ async function main() { uniqueUsers.join('\n') ); - // Optionally fetch user details + // Optionally fetch user details (with caching) let userDetails = null; if (fetchDetails) { - console.log(`\nFetching details for ${uniqueUsers.length} users (this may take a while)...`); - userDetails = {}; - for (let i = 0; i < uniqueUsers.length; i++) { - const username = uniqueUsers[i]; - process.stdout.write(` [${i + 1}/${uniqueUsers.length}] ${username}...`); - userDetails[username] = fetchUserDetails(username); - console.log(' done'); - - // Rate limiting - GitHub allows 5000 requests/hour for authenticated users - if (i > 0 && i % 50 === 0) { - console.log(' Pausing for rate limiting...'); - await new Promise(r => setTimeout(r, 2000)); + // Load existing cached user details + const cacheFile = path.join(OUTPUT_DIR, 'user_details.json'); + let cachedDetails = {}; + if (fs.existsSync(cacheFile)) { + try { + cachedDetails = JSON.parse(fs.readFileSync(cacheFile, 'utf-8')); + console.log(`\nLoaded ${Object.keys(cachedDetails).length} cached user profiles`); + } catch (e) { + console.log('\nCould not load cache, starting fresh'); } } + // Determine which users need fetching + const usersToFetch = uniqueUsers.filter(u => !cachedDetails[u]); + const cachedUsers = uniqueUsers.filter(u => cachedDetails[u]); + + console.log(` ${cachedUsers.length} users already cached`); + console.log(` ${usersToFetch.length} users need fetching`); + + userDetails = { ...cachedDetails }; + + if (usersToFetch.length > 0) { + console.log(`\nFetching details for ${usersToFetch.length} new users...`); + for (let i = 0; i < usersToFetch.length; i++) { + const username = usersToFetch[i]; + process.stdout.write(` [${i + 1}/${usersToFetch.length}] ${username}...`); + userDetails[username] = fetchUserDetails(username); + console.log(' done'); + + // Rate limiting - GitHub allows 5000 requests/hour for authenticated users + if (i > 0 && i % 50 === 0) { + console.log(' Pausing for rate limiting...'); + await new Promise(r => setTimeout(r, 2000)); + } + } + } else { + console.log('\nAll user details already cached, no API calls needed'); + } + fs.writeFileSync( path.join(OUTPUT_DIR, 'user_details.json'), JSON.stringify(userDetails, null, 2) diff --git a/src/main/index.ts b/src/main/index.ts index 3cb08134..f72594f9 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -5022,6 +5022,20 @@ function setupIpcHandlers() { requiresConfirmation?: boolean; confirmationUrl?: string; error?: string; + ranking?: { + cumulative: { + rank: number; + total: number; + previousRank: number | null; + improved: boolean; + }; + longestRun: { + rank: number; + total: number; + previousRank: number | null; + improved: boolean; + } | null; + }; }> => { try { logger.info('Submitting leaderboard entry', 'Leaderboard', { @@ -5045,17 +5059,33 @@ function setupIpcHandlers() { requiresConfirmation?: boolean; confirmationUrl?: string; error?: string; + ranking?: { + cumulative: { + rank: number; + total: number; + previousRank: number | null; + improved: boolean; + }; + longestRun: { + rank: number; + total: number; + previousRank: number | null; + improved: boolean; + } | null; + }; }; if (response.ok) { logger.info('Leaderboard submission successful', 'Leaderboard', { requiresConfirmation: result.requiresConfirmation, + ranking: result.ranking, }); return { success: true, message: result.message || 'Submission received', requiresConfirmation: result.requiresConfirmation, confirmationUrl: result.confirmationUrl, + ranking: result.ranking, }; } else { logger.warn('Leaderboard submission failed', 'Leaderboard', { diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 29d7a8b5..3ff515bd 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -2743,6 +2743,39 @@ export default function MaestroConsole() { lastSubmissionAt: Date.now(), emailConfirmed: !result.requiresConfirmation, }); + + // Show ranking notification if available + if (result.ranking) { + const { cumulative, longestRun } = result.ranking; + let message = ''; + + // Build cumulative ranking message + if (cumulative.previousRank === null) { + // New entry + message = `You're ranked #${cumulative.rank} of ${cumulative.total}!`; + } else if (cumulative.improved) { + // Moved up + const spotsUp = cumulative.previousRank - cumulative.rank; + message = `You moved up ${spotsUp} spot${spotsUp > 1 ? 's' : ''}! Now #${cumulative.rank} (was #${cumulative.previousRank})`; + } else if (cumulative.rank === cumulative.previousRank) { + // Holding steady + message = `You're holding steady at #${cumulative.rank}`; + } else { + // Dropped (shouldn't happen often, but handle it) + message = `You're now #${cumulative.rank} of ${cumulative.total}`; + } + + // Add longest run info if it's a new record or improved + if (longestRun && isNewRecord) { + message += ` | New personal best! #${longestRun.rank} on longest runs!`; + } + + addToastRef.current({ + type: 'success', + title: 'Leaderboard Updated', + message, + }); + } } // Silent failure - don't bother the user if submission fails }).catch(() => { diff --git a/src/renderer/components/FilePreview.tsx b/src/renderer/components/FilePreview.tsx index 87dbc81f..446207cf 100644 --- a/src/renderer/components/FilePreview.tsx +++ b/src/renderer/components/FilePreview.tsx @@ -164,7 +164,11 @@ function MarkdownImage({ const isRemoteUrl = src?.startsWith('http://') || src?.startsWith('https://'); useEffect(() => { + // Reset state when src or showRemoteImages changes + setError(null); + if (!src) { + setDataUrl(null); setLoading(false); return; } @@ -181,12 +185,16 @@ function MarkdownImage({ if (showRemoteImages) { setDataUrl(src); } else { + // Explicitly clear the dataUrl when hiding remote images setDataUrl(null); } setLoading(false); return; } + // For local files, we need to load them + setLoading(true); + // Resolve the path relative to the markdown file const resolvedPath = resolveImagePath(src, markdownFilePath); @@ -253,7 +261,7 @@ function MarkdownImage({ {alt ); @@ -1053,6 +1061,7 @@ export function FilePreview({ file, onClose, theme, markdownEditMode, setMarkdow .prose th { background-color: ${theme.colors.bgActivity}; font-weight: bold; } .prose strong { font-weight: bold; } .prose em { font-style: italic; } + .prose img { display: block; max-width: 100%; height: auto; } `} {hoveredLink.url} diff --git a/src/renderer/components/LeaderboardRegistrationModal.tsx b/src/renderer/components/LeaderboardRegistrationModal.tsx index 044b4871..42df72ce 100644 --- a/src/renderer/components/LeaderboardRegistrationModal.tsx +++ b/src/renderer/components/LeaderboardRegistrationModal.tsx @@ -286,7 +286,7 @@ export function LeaderboardRegistrationModal({

)}

- You'll receive a confirmation email to verify your registration + Your email is kept private and will not be displayed on the leaderboard

diff --git a/src/renderer/components/QuickActionsModal.tsx b/src/renderer/components/QuickActionsModal.tsx index cf149567..48e18de4 100644 --- a/src/renderer/components/QuickActionsModal.tsx +++ b/src/renderer/components/QuickActionsModal.tsx @@ -257,6 +257,7 @@ export function QuickActionsModal(props: QuickActionsModalProps) { } }] : []), { id: 'devtools', label: 'Toggle JavaScript Console', action: () => { window.maestro.devtools.toggle(); setQuickActionOpen(false); } }, { id: 'about', label: 'About Maestro', action: () => { setAboutModalOpen(true); setQuickActionOpen(false); } }, + { id: 'website', label: 'Maestro Website', subtext: 'Open the Maestro website', action: () => { window.maestro.shell.openExternal('https://runmaestro.ai/'); setQuickActionOpen(false); } }, { id: 'discord', label: 'Join Discord', subtext: 'Join the Maestro community', action: () => { window.maestro.shell.openExternal('https://discord.gg/86crXbGb'); setQuickActionOpen(false); } }, ...(setUpdateCheckModalOpen ? [{ id: 'updateCheck', label: 'Check for Updates', action: () => { setUpdateCheckModalOpen(true); setQuickActionOpen(false); } }] : []), { id: 'goToFiles', label: 'Go to Files Tab', action: () => { setRightPanelOpen(true); setActiveRightTab('files'); setQuickActionOpen(false); } }, diff --git a/src/renderer/types/index.ts b/src/renderer/types/index.ts index 66f86d14..3ffb179e 100644 --- a/src/renderer/types/index.ts +++ b/src/renderer/types/index.ts @@ -466,6 +466,14 @@ export interface LeaderboardRegistration { lastSubmissionAt?: number; // Last successful submission timestamp } +// Ranking info for a single leaderboard category +export interface LeaderboardRankingInfo { + rank: number; // User's position (1 = first place) + total: number; // Total entries on leaderboard + previousRank: number | null; // Previous position (null if new entry) + improved: boolean; // Did they move up? +} + // Response from leaderboard submission API export interface LeaderboardSubmitResponse { success: boolean; @@ -473,5 +481,9 @@ export interface LeaderboardSubmitResponse { requiresConfirmation?: boolean; confirmationUrl?: string; error?: string; + ranking?: { + cumulative: LeaderboardRankingInfo; + longestRun: LeaderboardRankingInfo | null; // null if no longestRunMs submitted + }; }