## CHANGES

- Finds npm-installed CLIs inside nvm/fnm/volta/mise/asdf paths automatically 🔍
- Adds macOS Apple Events entitlement for better automation support 🔐
- Updates Discord invite link across app, docs, and README 🗣️
- Makes FilePreview scroll listener passive for smoother scrolling performance 🏎️
- Refines Activity Heatmap hour labels and spacing for cleaner readability 📊
- Repositions heatmap tooltip slightly above cells for nicer hover UX 🎯
This commit is contained in:
Pedram Amini
2025-12-30 14:45:30 -06:00
parent 6e56a5ef1c
commit eea260475b
9 changed files with 125 additions and 14 deletions

View File

@@ -1,7 +1,7 @@
# Maestro # Maestro
[![Made with Maestro](docs/assets/made-with-maestro.svg)](https://github.com/pedramamini/Maestro) [![Made with Maestro](docs/assets/made-with-maestro.svg)](https://github.com/pedramamini/Maestro)
[![Discord](https://img.shields.io/badge/Discord-Join%20Us-5865F2?logo=discord&logoColor=white)](https://discord.gg/SrBsykvG) [![Discord](https://img.shields.io/badge/Discord-Join%20Us-5865F2?logo=discord&logoColor=white)](https://discord.gg/SVSRy593)
[![User Docs](https://img.shields.io/badge/Docs-Usage%20%26%20Documentation-blue?logo=readthedocs&logoColor=white)](https://docs.runmaestro.ai/) [![User Docs](https://img.shields.io/badge/Docs-Usage%20%26%20Documentation-blue?logo=readthedocs&logoColor=white)](https://docs.runmaestro.ai/)
> Maestro hones fractured attention into focused intent. > Maestro hones fractured attention into focused intent.
@@ -163,7 +163,7 @@ Full documentation and usage guide available at **[docs.runmaestro.ai](https://d
## Community ## Community
- **Discord**: [Join Us](https://discord.gg/SrBsykvG) - **Discord**: [Join Us](https://discord.gg/SVSRy593)
- **GitHub Issues**: [Report bugs & request features](https://github.com/pedramamini/Maestro/issues) - **GitHub Issues**: [Report bugs & request features](https://github.com/pedramamini/Maestro/issues)
## Contributing ## Contributing

View File

@@ -10,5 +10,7 @@
<true/> <true/>
<key>com.apple.security.device.audio-input</key> <key>com.apple.security.device.audio-input</key>
<false/> <false/>
<key>com.apple.security.automation.apple-events</key>
<true/>
</dict> </dict>
</plist> </plist>

View File

@@ -101,7 +101,7 @@
"links": [ "links": [
{ {
"label": "Discord", "label": "Discord",
"href": "https://discord.gg/SrBsykvG" "href": "https://discord.gg/SVSRy593"
}, },
{ {
"label": "GitHub", "label": "GitHub",
@@ -120,7 +120,7 @@
}, },
"footer": { "footer": {
"socials": { "socials": {
"discord": "https://discord.gg/SrBsykvG", "discord": "https://discord.gg/SVSRy593",
"github": "https://github.com/pedramamini/Maestro" "github": "https://github.com/pedramamini/Maestro"
} }
} }

View File

@@ -138,5 +138,5 @@ For new projects, always clone to the Linux filesystem from the start.
## Getting Help ## Getting Help
- **GitHub Issues**: [Report bugs or request features](https://github.com/pedramamini/Maestro/issues) - **GitHub Issues**: [Report bugs or request features](https://github.com/pedramamini/Maestro/issues)
- **Discord**: [Join the community](https://discord.gg/SrBsykvG) - **Discord**: [Join the community](https://discord.gg/SVSRy593)
- **Documentation**: [Docs site](https://docs.runmaestro.ai), [CONTRIBUTING.md](https://github.com/pedramamini/Maestro/blob/main/CONTRIBUTING.md), and [ARCHITECTURE.md](https://github.com/pedramamini/Maestro/blob/main/ARCHITECTURE.md) - **Documentation**: [Docs site](https://docs.runmaestro.ai), [CONTRIBUTING.md](https://github.com/pedramamini/Maestro/blob/main/CONTRIBUTING.md), and [ARCHITECTURE.md](https://github.com/pedramamini/Maestro/blob/main/ARCHITECTURE.md)

View File

@@ -522,6 +522,99 @@ export class AgentDetector {
return null; return null;
} }
/**
* Detect Node version manager paths on Unix systems (macOS/Linux).
* Returns an array of bin paths from nvm, fnm, volta, mise, asdf, and n.
* These paths are needed to find npm-installed CLIs like codex, claude, gemini.
*/
private detectNodeVersionManagerBinPaths(): string[] {
const home = os.homedir();
const detectedPaths: string[] = [];
// nvm: Check for ~/.nvm and find installed node versions
const nvmDir = process.env.NVM_DIR || path.join(home, '.nvm');
if (fs.existsSync(nvmDir)) {
const versionsDir = path.join(nvmDir, 'versions', 'node');
if (fs.existsSync(versionsDir)) {
try {
const versions = fs.readdirSync(versionsDir).filter(v => v.startsWith('v'));
// Sort versions descending to check newest first
versions.sort((a, b) => {
const parseVersion = (v: string) => v.replace('v', '').split('.').map(n => parseInt(n, 10));
const av = parseVersion(a);
const bv = parseVersion(b);
for (let i = 0; i < 3; i++) {
if ((bv[i] || 0) !== (av[i] || 0)) return (bv[i] || 0) - (av[i] || 0);
}
return 0;
});
for (const version of versions) {
const versionBin = path.join(versionsDir, version, 'bin');
if (fs.existsSync(versionBin)) {
detectedPaths.push(versionBin);
}
}
} catch {
// Ignore errors reading versions directory
}
}
// Also check nvm/current symlink
const nvmCurrentBin = path.join(nvmDir, 'current', 'bin');
if (fs.existsSync(nvmCurrentBin) && !detectedPaths.includes(nvmCurrentBin)) {
detectedPaths.unshift(nvmCurrentBin);
}
}
// fnm: Fast Node Manager
const fnmPaths = [
path.join(home, 'Library', 'Application Support', 'fnm'), // macOS default
path.join(home, '.local', 'share', 'fnm'), // Linux default
path.join(home, '.fnm'), // Legacy/custom location
];
for (const fnmDir of fnmPaths) {
if (fs.existsSync(fnmDir)) {
const fnmCurrentBin = path.join(fnmDir, 'aliases', 'default', 'bin');
if (fs.existsSync(fnmCurrentBin)) {
detectedPaths.push(fnmCurrentBin);
}
const fnmNodeVersions = path.join(fnmDir, 'node-versions');
if (fs.existsSync(fnmNodeVersions)) {
try {
const versions = fs.readdirSync(fnmNodeVersions).filter(v => v.startsWith('v'));
for (const version of versions) {
const versionBin = path.join(fnmNodeVersions, version, 'installation', 'bin');
if (fs.existsSync(versionBin)) {
detectedPaths.push(versionBin);
}
}
} catch {
// Ignore errors
}
}
}
}
// volta: Uses ~/.volta/bin for shims
const voltaBin = path.join(home, '.volta', 'bin');
if (fs.existsSync(voltaBin)) {
detectedPaths.push(voltaBin);
}
// mise (formerly rtx): Uses ~/.local/share/mise/shims
const miseShims = path.join(home, '.local', 'share', 'mise', 'shims');
if (fs.existsSync(miseShims)) {
detectedPaths.push(miseShims);
}
// asdf: Uses ~/.asdf/shims
const asdfShims = path.join(home, '.asdf', 'shims');
if (fs.existsSync(asdfShims)) {
detectedPaths.push(asdfShims);
}
return detectedPaths;
}
/** /**
* On macOS/Linux, directly probe known installation paths for a binary. * On macOS/Linux, directly probe known installation paths for a binary.
* This is necessary because packaged Electron apps don't inherit shell aliases, * This is necessary because packaged Electron apps don't inherit shell aliases,
@@ -531,6 +624,9 @@ export class AgentDetector {
private async probeUnixPaths(binaryName: string): Promise<string | null> { private async probeUnixPaths(binaryName: string): Promise<string | null> {
const home = os.homedir(); const home = os.homedir();
// Get dynamic paths from Node version managers (nvm, fnm, volta, etc.)
const versionManagerPaths = this.detectNodeVersionManagerBinPaths();
// Define known installation paths for each binary, in priority order // Define known installation paths for each binary, in priority order
const knownPaths: Record<string, string[]> = { const knownPaths: Record<string, string[]> = {
claude: [ claude: [
@@ -546,6 +642,8 @@ export class AgentDetector {
path.join(home, '.npm-global', 'bin', 'claude'), path.join(home, '.npm-global', 'bin', 'claude'),
// User bin directory // User bin directory
path.join(home, 'bin', 'claude'), path.join(home, 'bin', 'claude'),
// Add paths from Node version managers (nvm, fnm, volta, etc.)
...versionManagerPaths.map(p => path.join(p, 'claude')),
], ],
codex: [ codex: [
// User local bin // User local bin
@@ -555,6 +653,8 @@ export class AgentDetector {
'/usr/local/bin/codex', '/usr/local/bin/codex',
// npm global // npm global
path.join(home, '.npm-global', 'bin', 'codex'), path.join(home, '.npm-global', 'bin', 'codex'),
// Add paths from Node version managers (nvm, fnm, volta, etc.)
...versionManagerPaths.map(p => path.join(p, 'codex')),
], ],
opencode: [ opencode: [
// OpenCode installer default location // OpenCode installer default location
@@ -566,12 +666,16 @@ export class AgentDetector {
// Homebrew paths // Homebrew paths
'/opt/homebrew/bin/opencode', '/opt/homebrew/bin/opencode',
'/usr/local/bin/opencode', '/usr/local/bin/opencode',
// Add paths from Node version managers (nvm, fnm, volta, etc.)
...versionManagerPaths.map(p => path.join(p, 'opencode')),
], ],
gemini: [ gemini: [
// npm global paths // npm global paths
path.join(home, '.npm-global', 'bin', 'gemini'), path.join(home, '.npm-global', 'bin', 'gemini'),
'/opt/homebrew/bin/gemini', '/opt/homebrew/bin/gemini',
'/usr/local/bin/gemini', '/usr/local/bin/gemini',
// Add paths from Node version managers (nvm, fnm, volta, etc.)
...versionManagerPaths.map(p => path.join(p, 'gemini')),
], ],
aider: [ aider: [
// pip installation // pip installation
@@ -579,6 +683,8 @@ export class AgentDetector {
// Homebrew paths // Homebrew paths
'/opt/homebrew/bin/aider', '/opt/homebrew/bin/aider',
'/usr/local/bin/aider', '/usr/local/bin/aider',
// Add paths from Node version managers (in case installed via npm)
...versionManagerPaths.map(p => path.join(p, 'aider')),
], ],
}; };

View File

@@ -130,7 +130,7 @@ export function AboutModal({ theme, autoRunStats, usageStats, handsOnTimeMs, onC
<Globe className="w-4 h-4" /> <Globe className="w-4 h-4" />
</button> </button>
<button <button
onClick={() => window.maestro.shell.openExternal('https://discord.gg/SrBsykvG')} onClick={() => window.maestro.shell.openExternal('https://discord.gg/SVSRy593')}
className="p-1 rounded hover:bg-white/10 transition-colors" className="p-1 rounded hover:bg-white/10 transition-colors"
title="Join our Discord" title="Join our Discord"
style={{ color: theme.colors.accent }} style={{ color: theme.colors.accent }}

View File

@@ -577,7 +577,7 @@ export function FilePreview({ file, onClose, theme, markdownEditMode, setMarkdow
setShowStatsBar(contentEl.scrollTop <= 10); setShowStatsBar(contentEl.scrollTop <= 10);
}; };
contentEl.addEventListener('scroll', handleScroll); contentEl.addEventListener('scroll', handleScroll, { passive: true });
return () => contentEl.removeEventListener('scroll', handleScroll); return () => contentEl.removeEventListener('scroll', handleScroll);
}, []); }, []);

View File

@@ -376,7 +376,7 @@ export function QuickActionsModal(props: QuickActionsModalProps) {
{ id: 'about', label: 'About Maestro', action: () => { setAboutModalOpen(true); 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: 'website', label: 'Maestro Website', subtext: 'Open the Maestro website', action: () => { window.maestro.shell.openExternal('https://runmaestro.ai/'); setQuickActionOpen(false); } },
{ id: 'docs', label: 'Documentation and User Guide', subtext: 'Open the Maestro documentation', action: () => { window.maestro.shell.openExternal('https://docs.runmaestro.ai/'); setQuickActionOpen(false); } }, { id: 'docs', label: 'Documentation and User Guide', subtext: 'Open the Maestro documentation', action: () => { window.maestro.shell.openExternal('https://docs.runmaestro.ai/'); setQuickActionOpen(false); } },
{ id: 'discord', label: 'Join Discord', subtext: 'Join the Maestro community', action: () => { window.maestro.shell.openExternal('https://discord.gg/SrBsykvG'); setQuickActionOpen(false); } }, { id: 'discord', label: 'Join Discord', subtext: 'Join the Maestro community', action: () => { window.maestro.shell.openExternal('https://discord.gg/SVSRy593'); setQuickActionOpen(false); } },
...(setUpdateCheckModalOpen ? [{ id: 'updateCheck', label: 'Check for Updates', action: () => { setUpdateCheckModalOpen(true); setQuickActionOpen(false); } }] : []), ...(setUpdateCheckModalOpen ? [{ id: 'updateCheck', label: 'Check for Updates', action: () => { setUpdateCheckModalOpen(true); setQuickActionOpen(false); } }] : []),
{ id: 'createDebugPackage', label: 'Create Debug Package', subtext: 'Generate a support bundle for bug reporting', action: () => { { id: 'createDebugPackage', label: 'Create Debug Package', subtext: 'Generate a support bundle for bug reporting', action: () => {
setQuickActionOpen(false); setQuickActionOpen(false);

View File

@@ -192,9 +192,10 @@ export function ActivityHeatmap({ data, timeRange, theme, colorBlindMode = false
// Determine hour rows based on mode // Determine hour rows based on mode
const hours = useAmPm ? [0, 12] : Array.from({ length: 24 }, (_, i) => i); const hours = useAmPm ? [0, 12] : Array.from({ length: 24 }, (_, i) => i);
// Labels for Y-axis: show every 2 hours for readability
const labels = useAmPm const labels = useAmPm
? ['AM', 'PM'] ? ['AM', 'PM']
: ['12a', '', '2a', '', '4a', '', '6a', '', '8a', '', '10a', '', '12p', '', '2p', '', '4p', '', '6p', '', '8p', '', '10p', '']; : ['12a', '1a', '2a', '3a', '4a', '5a', '6a', '7a', '8a', '9a', '10a', '11a', '12p', '1p', '2p', '3p', '4p', '5p', '6p', '7p', '8p', '9p', '10p', '11p'];
// Track max values for intensity calculation // Track max values for intensity calculation
let maxCount = 0; let maxCount = 0;
@@ -272,9 +273,10 @@ export function ActivityHeatmap({ data, timeRange, theme, colorBlindMode = false
(cell: HourData, event: React.MouseEvent<HTMLDivElement>) => { (cell: HourData, event: React.MouseEvent<HTMLDivElement>) => {
setHoveredCell(cell); setHoveredCell(cell);
const rect = event.currentTarget.getBoundingClientRect(); const rect = event.currentTarget.getBoundingClientRect();
// Position tooltip above and centered on the cell
setTooltipPos({ setTooltipPos({
x: rect.left + rect.width / 2, x: rect.left + rect.width / 2,
y: rect.top, y: rect.top - 4,
}); });
}, },
[] []
@@ -354,7 +356,7 @@ export function ActivityHeatmap({ data, timeRange, theme, colorBlindMode = false
{/* Heatmap grid */} {/* Heatmap grid */}
<div className="flex gap-2"> <div className="flex gap-2">
{/* Hour labels (Y-axis) */} {/* Hour labels (Y-axis) - only show every 2 hours for readability */}
<div className="flex flex-col flex-shrink-0" style={{ width: 28, paddingTop: 20 }}> <div className="flex flex-col flex-shrink-0" style={{ width: 28, paddingTop: 20 }}>
{hourLabels.map((label, idx) => ( {hourLabels.map((label, idx) => (
<div <div
@@ -362,10 +364,11 @@ export function ActivityHeatmap({ data, timeRange, theme, colorBlindMode = false
className="text-xs text-right flex items-center justify-end" className="text-xs text-right flex items-center justify-end"
style={{ style={{
color: theme.colors.textDim, color: theme.colors.textDim,
height: useAmPm ? 32 : 12, height: useAmPm ? 34 : 14,
}} }}
> >
{label} {/* Only show labels for even hours (0, 2, 4, etc.) */}
{useAmPm || idx % 2 === 0 ? label : ''}
</div> </div>
))} ))}
</div> </div>
@@ -393,7 +396,7 @@ export function ActivityHeatmap({ data, timeRange, theme, colorBlindMode = false
key={hourData.hourKey} key={hourData.hourKey}
className="rounded-sm cursor-default" className="rounded-sm cursor-default"
style={{ style={{
height: useAmPm ? 32 : 12, height: useAmPm ? 34 : 14,
backgroundColor: getIntensityColor( backgroundColor: getIntensityColor(
hourData.intensity, hourData.intensity,
theme, theme,