mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
- Added main-process GitHub document fetching to bypass pesky CORS limits 🚀
- Exposed `fetchDocumentContent` through preload + typed Maestro API bridge 🔌 - Symphony issue docs now auto-preview first attachment when selected ⚡ - Replaced document tabs with a cleaner dropdown document selector 🧭 - Added Cmd/Ctrl+Shift+[ / ] shortcuts to cycle preview documents ⌨️ - Markdown previews now use centralized prose styling + custom components 📝 - External links in markdown open safely via system browser integration 🌐 - Improved Symphony UI theming: consistent backgrounds, borders, and scroll layout 🎨 - Updated Marketplace left sidebar width to match Symphony layout guidance 📐 - Registry refreshed: Maestro now “AI agents” focused and recategorized 🏷️
This commit is contained in:
@@ -1354,5 +1354,47 @@ This PR will be updated automatically when the Auto Run completes.`;
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Handler for fetching document content (from main process to avoid CORS)
|
||||||
|
ipcMain.handle(
|
||||||
|
'symphony:fetchDocumentContent',
|
||||||
|
createIpcHandler(
|
||||||
|
handlerOpts('fetchDocumentContent'),
|
||||||
|
async (params: { url: string }): Promise<{ success: boolean; content?: string; error?: string }> => {
|
||||||
|
const { url } = params;
|
||||||
|
|
||||||
|
// Validate URL - only allow GitHub URLs
|
||||||
|
try {
|
||||||
|
const parsed = new URL(url);
|
||||||
|
if (!['github.com', 'raw.githubusercontent.com', 'objects.githubusercontent.com'].some(
|
||||||
|
host => parsed.hostname === host || parsed.hostname.endsWith('.' + host)
|
||||||
|
)) {
|
||||||
|
return { success: false, error: 'Only GitHub URLs are allowed' };
|
||||||
|
}
|
||||||
|
if (parsed.protocol !== 'https:') {
|
||||||
|
return { success: false, error: 'Only HTTPS URLs are allowed' };
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return { success: false, error: 'Invalid URL' };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.info('Fetching document content', LOG_CONTEXT, { url });
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) {
|
||||||
|
return { success: false, error: `HTTP ${response.status}: ${response.statusText}` };
|
||||||
|
}
|
||||||
|
const content = await response.text();
|
||||||
|
return { success: true, content };
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to fetch document content', LOG_CONTEXT, { url, error });
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to fetch document',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
logger.info('Symphony handlers registered', LOG_CONTEXT);
|
logger.info('Symphony handlers registered', LOG_CONTEXT);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import {
|
|||||||
GitMerge,
|
GitMerge,
|
||||||
Clock,
|
Clock,
|
||||||
Zap,
|
Zap,
|
||||||
Star,
|
|
||||||
Play,
|
Play,
|
||||||
Pause,
|
Pause,
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
@@ -36,6 +35,7 @@ import {
|
|||||||
Flame,
|
Flame,
|
||||||
FileText,
|
FileText,
|
||||||
Hash,
|
Hash,
|
||||||
|
ChevronDown,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import type { Theme } from '../types';
|
import type { Theme } from '../types';
|
||||||
import type {
|
import type {
|
||||||
@@ -53,6 +53,7 @@ import { MODAL_PRIORITIES } from '../constants/modalPriorities';
|
|||||||
import { useSymphony } from '../hooks/symphony';
|
import { useSymphony } from '../hooks/symphony';
|
||||||
import { useContributorStats, type Achievement } from '../hooks/symphony/useContributorStats';
|
import { useContributorStats, type Achievement } from '../hooks/symphony/useContributorStats';
|
||||||
import { AgentCreationDialog, type AgentCreationConfig } from './AgentCreationDialog';
|
import { AgentCreationDialog, type AgentCreationConfig } from './AgentCreationDialog';
|
||||||
|
import { generateProseStyles, createMarkdownComponents } from '../utils/markdownConfig';
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Types
|
// Types
|
||||||
@@ -207,7 +208,6 @@ function RepositoryTile({
|
|||||||
<span>{categoryInfo.emoji}</span>
|
<span>{categoryInfo.emoji}</span>
|
||||||
<span>{categoryInfo.label}</span>
|
<span>{categoryInfo.label}</span>
|
||||||
</span>
|
</span>
|
||||||
{repo.featured && <Star className="w-3 h-3" style={{ color: '#eab308' }} />}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 className="font-semibold mb-1 line-clamp-1" style={{ color: theme.colors.textMain }} title={repo.name}>
|
<h3 className="font-semibold mb-1 line-clamp-1" style={{ color: theme.colors.textMain }} title={repo.name}>
|
||||||
@@ -255,7 +255,7 @@ function IssueCard({
|
|||||||
!isAvailable ? 'opacity-60 cursor-not-allowed' : 'hover:bg-white/5'
|
!isAvailable ? 'opacity-60 cursor-not-allowed' : 'hover:bg-white/5'
|
||||||
} ${isSelected ? 'ring-2' : ''}`}
|
} ${isSelected ? 'ring-2' : ''}`}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: isSelected ? theme.colors.bgActivity : 'transparent',
|
backgroundColor: isSelected ? theme.colors.bgActivity : theme.colors.bgMain,
|
||||||
borderColor: isSelected ? theme.colors.accent : theme.colors.border,
|
borderColor: isSelected ? theme.colors.accent : theme.colors.border,
|
||||||
...(isSelected && { boxShadow: `0 0 0 2px ${theme.colors.accent}` }),
|
...(isSelected && { boxShadow: `0 0 0 2px ${theme.colors.accent}` }),
|
||||||
}}
|
}}
|
||||||
@@ -331,15 +331,92 @@ function RepositoryDetailView({
|
|||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
onSelectIssue: (issue: SymphonyIssue) => void;
|
onSelectIssue: (issue: SymphonyIssue) => void;
|
||||||
onStartContribution: () => void;
|
onStartContribution: () => void;
|
||||||
onPreviewDocument: (path: string) => void;
|
onPreviewDocument: (path: string, isExternal: boolean) => void;
|
||||||
}) {
|
}) {
|
||||||
const categoryInfo = SYMPHONY_CATEGORIES[repo.category] ?? { label: repo.category, emoji: '📦' };
|
const categoryInfo = SYMPHONY_CATEGORIES[repo.category] ?? { label: repo.category, emoji: '📦' };
|
||||||
const availableIssues = issues.filter(i => i.status === 'available');
|
const availableIssues = issues.filter(i => i.status === 'available');
|
||||||
const [selectedDocPath, setSelectedDocPath] = useState<string | null>(null);
|
const [selectedDocIndex, setSelectedDocIndex] = useState<number>(0);
|
||||||
|
const [showDocDropdown, setShowDocDropdown] = useState(false);
|
||||||
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const handleSelectDoc = (path: string) => {
|
// Generate prose styles scoped to symphony preview panel
|
||||||
setSelectedDocPath(path);
|
const proseStyles = useMemo(
|
||||||
onPreviewDocument(path);
|
() =>
|
||||||
|
generateProseStyles({
|
||||||
|
theme,
|
||||||
|
coloredHeadings: true,
|
||||||
|
compactSpacing: false,
|
||||||
|
includeCheckboxStyles: true,
|
||||||
|
scopeSelector: '.symphony-preview',
|
||||||
|
}),
|
||||||
|
[theme]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create markdown components with link handling
|
||||||
|
const markdownComponents = useMemo(
|
||||||
|
() =>
|
||||||
|
createMarkdownComponents({
|
||||||
|
theme,
|
||||||
|
onExternalLinkClick: (href) => window.maestro.shell?.openExternal?.(href),
|
||||||
|
}),
|
||||||
|
[theme]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Close dropdown when clicking outside
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (e: MouseEvent) => {
|
||||||
|
if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
|
||||||
|
setShowDocDropdown(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Auto-load first document when issue is selected
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedIssue && selectedIssue.documentPaths.length > 0) {
|
||||||
|
const firstDoc = selectedIssue.documentPaths[0];
|
||||||
|
setSelectedDocIndex(0);
|
||||||
|
onPreviewDocument(firstDoc.path, firstDoc.isExternal);
|
||||||
|
}
|
||||||
|
}, [selectedIssue, onPreviewDocument]);
|
||||||
|
|
||||||
|
// Keyboard shortcuts for document navigation: Cmd+Shift+[ and Cmd+Shift+]
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (!selectedIssue || selectedIssue.documentPaths.length === 0) return;
|
||||||
|
|
||||||
|
if ((e.metaKey || e.ctrlKey) && e.shiftKey && (e.key === '[' || e.key === ']')) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const docCount = selectedIssue.documentPaths.length;
|
||||||
|
let newIndex: number;
|
||||||
|
|
||||||
|
if (e.key === '[') {
|
||||||
|
// Go backwards, wrap around
|
||||||
|
newIndex = selectedDocIndex <= 0 ? docCount - 1 : selectedDocIndex - 1;
|
||||||
|
} else {
|
||||||
|
// Go forwards, wrap around
|
||||||
|
newIndex = selectedDocIndex >= docCount - 1 ? 0 : selectedDocIndex + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const doc = selectedIssue.documentPaths[newIndex];
|
||||||
|
setSelectedDocIndex(newIndex);
|
||||||
|
onPreviewDocument(doc.path, doc.isExternal);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||||
|
}, [selectedIssue, selectedDocIndex, onPreviewDocument]);
|
||||||
|
|
||||||
|
const handleSelectDoc = (index: number) => {
|
||||||
|
if (!selectedIssue) return;
|
||||||
|
const doc = selectedIssue.documentPaths[index];
|
||||||
|
setSelectedDocIndex(index);
|
||||||
|
setShowDocDropdown(false);
|
||||||
|
onPreviewDocument(doc.path, doc.isExternal);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOpenExternal = useCallback((url: string) => {
|
const handleOpenExternal = useCallback((url: string) => {
|
||||||
@@ -362,7 +439,6 @@ function RepositoryDetailView({
|
|||||||
<span>{categoryInfo.emoji}</span>
|
<span>{categoryInfo.emoji}</span>
|
||||||
<span>{categoryInfo.label}</span>
|
<span>{categoryInfo.label}</span>
|
||||||
</span>
|
</span>
|
||||||
{repo.featured && <Star className="w-3 h-3" style={{ color: '#eab308' }} />}
|
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-lg font-semibold truncate" style={{ color: theme.colors.textMain }}>
|
<h2 className="text-lg font-semibold truncate" style={{ color: theme.colors.textMain }}>
|
||||||
{repo.name}
|
{repo.name}
|
||||||
@@ -475,10 +551,10 @@ function RepositoryDetailView({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right: Issue preview */}
|
{/* Right: Issue preview */}
|
||||||
<div className="flex-1 flex flex-col min-w-0 overflow-hidden">
|
<div className="flex-1 flex flex-col min-w-0 min-h-0 overflow-hidden">
|
||||||
{selectedIssue ? (
|
{selectedIssue ? (
|
||||||
<>
|
<>
|
||||||
<div className="px-4 py-3 border-b" style={{ borderColor: theme.colors.border }}>
|
<div className="px-4 py-3 border-b shrink-0" style={{ borderColor: theme.colors.border }}>
|
||||||
<div className="flex items-center gap-2 mb-1">
|
<div className="flex items-center gap-2 mb-1">
|
||||||
<span className="text-sm" style={{ color: theme.colors.textDim }}>#{selectedIssue.number}</span>
|
<span className="text-sm" style={{ color: theme.colors.textDim }}>#{selectedIssue.number}</span>
|
||||||
<h3 className="font-semibold" style={{ color: theme.colors.textMain }}>{selectedIssue.title}</h3>
|
<h3 className="font-semibold" style={{ color: theme.colors.textMain }}>{selectedIssue.title}</h3>
|
||||||
@@ -489,46 +565,66 @@ function RepositoryDetailView({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Document tabs */}
|
{/* Document selector dropdown */}
|
||||||
<div className="flex items-center gap-1 px-4 py-2 border-b overflow-x-auto" style={{ borderColor: theme.colors.border }}>
|
<div className="px-4 py-3 border-b shrink-0" style={{ borderColor: theme.colors.border, backgroundColor: theme.colors.bgMain }}>
|
||||||
{selectedIssue.documentPaths.map((doc) => (
|
<div className="relative" ref={dropdownRef}>
|
||||||
<button
|
<button
|
||||||
key={doc.name}
|
onClick={() => setShowDocDropdown(!showDocDropdown)}
|
||||||
onClick={() => handleSelectDoc(doc.path)}
|
className="w-full flex items-center justify-between px-3 py-2 rounded text-sm"
|
||||||
className="px-2 py-1 rounded text-xs whitespace-nowrap transition-colors"
|
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: selectedDocPath === doc.path ? theme.colors.accent + '20' : 'transparent',
|
backgroundColor: theme.colors.bgActivity,
|
||||||
color: selectedDocPath === doc.path ? theme.colors.accent : theme.colors.textDim,
|
color: theme.colors.textMain,
|
||||||
|
border: `1px solid ${theme.colors.border}`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{doc.name}
|
<span>{selectedIssue.documentPaths[selectedDocIndex]?.name || 'Select document'}</span>
|
||||||
|
<ChevronDown
|
||||||
|
className={`w-4 h-4 transition-transform ${showDocDropdown ? 'rotate-180' : ''}`}
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
))}
|
|
||||||
|
{showDocDropdown && (
|
||||||
|
<div
|
||||||
|
className="absolute top-full left-0 right-0 mt-1 rounded shadow-lg z-10 overflow-hidden max-h-64 overflow-y-auto"
|
||||||
|
style={{
|
||||||
|
backgroundColor: theme.colors.bgSidebar,
|
||||||
|
border: `1px solid ${theme.colors.border}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{selectedIssue.documentPaths.map((doc, index) => (
|
||||||
|
<button
|
||||||
|
key={doc.name}
|
||||||
|
onClick={() => handleSelectDoc(index)}
|
||||||
|
className="w-full px-3 py-2 text-sm text-left hover:bg-white/5 transition-colors"
|
||||||
|
style={{
|
||||||
|
color: selectedDocIndex === index ? theme.colors.accent : theme.colors.textMain,
|
||||||
|
backgroundColor: selectedDocIndex === index ? theme.colors.bgActivity : 'transparent',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{doc.name}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto p-4">
|
{/* Document preview - Markdown preview scrollable container with prose styles */}
|
||||||
|
<div
|
||||||
|
className="symphony-preview flex-1 min-h-0 p-4"
|
||||||
|
style={{ backgroundColor: theme.colors.bgMain, overflowY: 'auto' }}
|
||||||
|
>
|
||||||
|
<style>{proseStyles}</style>
|
||||||
{isLoadingDocument ? (
|
{isLoadingDocument ? (
|
||||||
<div className="flex items-center justify-center h-32">
|
<div className="flex items-center justify-center h-32">
|
||||||
<Loader2 className="w-6 h-6 animate-spin" style={{ color: theme.colors.accent }} />
|
<Loader2 className="w-6 h-6 animate-spin" style={{ color: theme.colors.accent }} />
|
||||||
</div>
|
</div>
|
||||||
) : documentPreview ? (
|
) : documentPreview ? (
|
||||||
<div
|
<div className="prose prose-sm max-w-none" style={{ color: theme.colors.textMain }}>
|
||||||
className="prose prose-sm max-w-none"
|
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
|
||||||
style={{
|
{documentPreview}
|
||||||
color: theme.colors.textMain,
|
</ReactMarkdown>
|
||||||
'--tw-prose-body': theme.colors.textMain,
|
|
||||||
'--tw-prose-headings': theme.colors.textMain,
|
|
||||||
'--tw-prose-links': theme.colors.accent,
|
|
||||||
'--tw-prose-bold': theme.colors.textMain,
|
|
||||||
'--tw-prose-code': theme.colors.textMain,
|
|
||||||
} as React.CSSProperties}
|
|
||||||
>
|
|
||||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>{documentPreview}</ReactMarkdown>
|
|
||||||
</div>
|
</div>
|
||||||
) : selectedDocPath ? (
|
|
||||||
<p className="text-center" style={{ color: theme.colors.textDim }}>
|
|
||||||
Document preview unavailable
|
|
||||||
</p>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center justify-center h-full">
|
<div className="flex flex-col items-center justify-center h-full">
|
||||||
<FileText className="w-12 h-12 mb-3" style={{ color: theme.colors.textDim }} />
|
<FileText className="w-12 h-12 mb-3" style={{ color: theme.colors.textDim }} />
|
||||||
@@ -538,7 +634,10 @@ function RepositoryDetailView({
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex-1 flex items-center justify-center">
|
<div
|
||||||
|
className="flex-1 flex items-center justify-center"
|
||||||
|
style={{ backgroundColor: theme.colors.bgMain }}
|
||||||
|
>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<Music className="w-12 h-12 mx-auto mb-3" style={{ color: theme.colors.textDim }} />
|
<Music className="w-12 h-12 mx-auto mb-3" style={{ color: theme.colors.textDim }} />
|
||||||
<p style={{ color: theme.colors.textDim }}>Select an issue to see details</p>
|
<p style={{ color: theme.colors.textDim }}>Select an issue to see details</p>
|
||||||
@@ -963,14 +1062,31 @@ export function SymphonyModal({
|
|||||||
setDocumentPreview(null);
|
setDocumentPreview(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Preview document (stub - not yet implemented in IPC)
|
// Preview document - fetches content from external URLs (GitHub attachments)
|
||||||
const handlePreviewDocument = useCallback(async (path: string) => {
|
const handlePreviewDocument = useCallback(async (path: string, isExternal: boolean) => {
|
||||||
if (!selectedRepo) return;
|
if (!selectedRepo) return;
|
||||||
setIsLoadingDocument(true);
|
setIsLoadingDocument(true);
|
||||||
// TODO: Implement document preview via IPC
|
setDocumentPreview(null);
|
||||||
// const content = await window.maestro.symphony.previewDocument(selectedRepo.slug, path);
|
|
||||||
setDocumentPreview(`# Document Preview\n\nPreview for \`${path}\` is not yet available.\n\nThis document will be processed when you start the Symphony contribution.`);
|
try {
|
||||||
setIsLoadingDocument(false);
|
if (isExternal && path.startsWith('http')) {
|
||||||
|
// Fetch content from external URL via main process (to avoid CORS)
|
||||||
|
const result = await window.maestro.symphony.fetchDocumentContent(path);
|
||||||
|
if (result.success && result.content) {
|
||||||
|
setDocumentPreview(result.content);
|
||||||
|
} else {
|
||||||
|
setDocumentPreview(`*Failed to load document: ${result.error || 'Unknown error'}*`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For repo-relative paths, we can't preview until contribution starts
|
||||||
|
setDocumentPreview(`*This document is located at \`${path}\` in the repository and will be available when you start the contribution.*`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch document:', error);
|
||||||
|
setDocumentPreview(`*Failed to load document: ${error instanceof Error ? error.message : 'Unknown error'}*`);
|
||||||
|
} finally {
|
||||||
|
setIsLoadingDocument(false);
|
||||||
|
}
|
||||||
}, [selectedRepo]);
|
}, [selectedRepo]);
|
||||||
|
|
||||||
// Start contribution - opens agent creation dialog
|
// Start contribution - opens agent creation dialog
|
||||||
@@ -1154,7 +1270,10 @@ export function SymphonyModal({
|
|||||||
{activeTab === 'projects' && (
|
{activeTab === 'projects' && (
|
||||||
<>
|
<>
|
||||||
{/* Search + Category tabs */}
|
{/* Search + Category tabs */}
|
||||||
<div className="px-4 py-3 border-b" style={{ borderColor: theme.colors.border }}>
|
<div
|
||||||
|
className="px-4 py-3 border-b"
|
||||||
|
style={{ borderColor: theme.colors.border, backgroundColor: theme.colors.bgMain }}
|
||||||
|
>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="relative flex-1 max-w-xs">
|
<div className="relative flex-1 max-w-xs">
|
||||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4" style={{ color: theme.colors.textDim }} />
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4" style={{ color: theme.colors.textDim }} />
|
||||||
@@ -1164,8 +1283,12 @@ export function SymphonyModal({
|
|||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
placeholder="Search repositories..."
|
placeholder="Search repositories..."
|
||||||
className="w-full pl-9 pr-3 py-2 rounded border bg-transparent outline-none text-sm focus:ring-1"
|
className="w-full pl-9 pr-3 py-2 rounded border outline-none text-sm focus:ring-1"
|
||||||
style={{ borderColor: theme.colors.border, color: theme.colors.textMain }}
|
style={{
|
||||||
|
borderColor: theme.colors.border,
|
||||||
|
color: theme.colors.textMain,
|
||||||
|
backgroundColor: theme.colors.bgActivity,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1174,8 +1297,9 @@ export function SymphonyModal({
|
|||||||
onClick={() => setSelectedCategory('all')}
|
onClick={() => setSelectedCategory('all')}
|
||||||
className={`px-3 py-1.5 rounded text-sm transition-colors ${selectedCategory === 'all' ? 'font-semibold' : ''}`}
|
className={`px-3 py-1.5 rounded text-sm transition-colors ${selectedCategory === 'all' ? 'font-semibold' : ''}`}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: selectedCategory === 'all' ? theme.colors.accent + '20' : 'transparent',
|
backgroundColor: selectedCategory === 'all' ? theme.colors.bgActivity : 'transparent',
|
||||||
color: selectedCategory === 'all' ? theme.colors.accent : theme.colors.textDim,
|
color: selectedCategory === 'all' ? theme.colors.accent : theme.colors.textDim,
|
||||||
|
border: selectedCategory === 'all' ? `1px solid ${theme.colors.accent}` : '1px solid transparent',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
All
|
All
|
||||||
@@ -1190,8 +1314,9 @@ export function SymphonyModal({
|
|||||||
selectedCategory === cat ? 'font-semibold' : ''
|
selectedCategory === cat ? 'font-semibold' : ''
|
||||||
}`}
|
}`}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: selectedCategory === cat ? theme.colors.accent + '20' : 'transparent',
|
backgroundColor: selectedCategory === cat ? theme.colors.bgActivity : 'transparent',
|
||||||
color: selectedCategory === cat ? theme.colors.accent : theme.colors.textDim,
|
color: selectedCategory === cat ? theme.colors.accent : theme.colors.textDim,
|
||||||
|
border: selectedCategory === cat ? `1px solid ${theme.colors.accent}` : '1px solid transparent',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span>{info?.emoji}</span>
|
<span>{info?.emoji}</span>
|
||||||
@@ -1204,7 +1329,7 @@ export function SymphonyModal({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Repository grid */}
|
{/* Repository grid */}
|
||||||
<div className="flex-1 overflow-y-auto p-4">
|
<div className="flex-1 overflow-y-auto p-4" style={{ backgroundColor: theme.colors.bgMain }}>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="grid grid-cols-3 gap-4">
|
<div className="grid grid-cols-3 gap-4">
|
||||||
{[1, 2, 3, 4, 5, 6].map((i) => <RepositoryTileSkeleton key={i} theme={theme} />)}
|
{[1, 2, 3, 4, 5, 6].map((i) => <RepositoryTileSkeleton key={i} theme={theme} />)}
|
||||||
|
|||||||
@@ -5,9 +5,9 @@
|
|||||||
{
|
{
|
||||||
"slug": "pedramamini/Maestro",
|
"slug": "pedramamini/Maestro",
|
||||||
"name": "Maestro",
|
"name": "Maestro",
|
||||||
"description": "Desktop app for managing multiple AI coding assistants with a keyboard-first interface.",
|
"description": "Desktop app for managing multiple AI agents with a keyboard-first interface.",
|
||||||
"url": "https://github.com/pedramamini/Maestro",
|
"url": "https://github.com/pedramamini/Maestro",
|
||||||
"category": "developer-tools",
|
"category": "ai-ml",
|
||||||
"tags": ["electron", "ai", "productivity", "typescript"],
|
"tags": ["electron", "ai", "productivity", "typescript"],
|
||||||
"maintainer": {
|
"maintainer": {
|
||||||
"name": "Pedram Amini",
|
"name": "Pedram Amini",
|
||||||
|
|||||||
Reference in New Issue
Block a user