mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
## 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:
@@ -1,7 +1,7 @@
|
|||||||
# Maestro
|
# Maestro
|
||||||
|
|
||||||
[](https://github.com/pedramamini/Maestro)
|
[](https://github.com/pedramamini/Maestro)
|
||||||
[](https://discord.gg/SrBsykvG)
|
[](https://discord.gg/SVSRy593)
|
||||||
[](https://docs.runmaestro.ai/)
|
[](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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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')),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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 }}
|
||||||
|
|||||||
@@ -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);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user