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({
);
@@ -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; }
`}
- 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 + }; }