From a7f5ebf82451972552b09862cb65bd228e9f29c5 Mon Sep 17 00:00:00 2001 From: Pedram Amini Date: Thu, 8 Jan 2026 12:16:44 -0600 Subject: [PATCH] =?UTF-8?q?-=20Added=20main-process=20GitHub=20document=20?= =?UTF-8?q?fetching=20to=20bypass=20pesky=20CORS=20limits=20=F0=9F=9A=80?= =?UTF-8?q?=20-=20Exposed=20`fetchDocumentContent`=20through=20preload=20+?= =?UTF-8?q?=20typed=20Maestro=20API=20bridge=20=F0=9F=94=8C=20-=20Symphony?= =?UTF-8?q?=20issue=20docs=20now=20auto-preview=20first=20attachment=20whe?= =?UTF-8?q?n=20selected=20=E2=9A=A1=20-=20Replaced=20document=20tabs=20wit?= =?UTF-8?q?h=20a=20cleaner=20dropdown=20document=20selector=20=F0=9F=A7=AD?= =?UTF-8?q?=20-=20Added=20Cmd/Ctrl+Shift+[=20/=20]=20shortcuts=20to=20cycl?= =?UTF-8?q?e=20preview=20documents=20=E2=8C=A8=EF=B8=8F=20-=20Markdown=20p?= =?UTF-8?q?reviews=20now=20use=20centralized=20prose=20styling=20+=20custo?= =?UTF-8?q?m=20components=20=F0=9F=93=9D=20-=20External=20links=20in=20mar?= =?UTF-8?q?kdown=20open=20safely=20via=20system=20browser=20integration=20?= =?UTF-8?q?=F0=9F=8C=90=20-=20Improved=20Symphony=20UI=20theming:=20consis?= =?UTF-8?q?tent=20backgrounds,=20borders,=20and=20scroll=20layout=20?= =?UTF-8?q?=F0=9F=8E=A8=20-=20Updated=20Marketplace=20left=20sidebar=20wid?= =?UTF-8?q?th=20to=20match=20Symphony=20layout=20guidance=20=F0=9F=93=90?= =?UTF-8?q?=20-=20Registry=20refreshed:=20Maestro=20now=20=E2=80=9CAI=20ag?= =?UTF-8?q?ents=E2=80=9D=20focused=20and=20recategorized=20=F0=9F=8F=B7?= =?UTF-8?q?=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/ipc/handlers/symphony.ts | 42 ++++ src/renderer/components/SymphonyModal.tsx | 227 +++++++++++++++++----- symphony-registry.json | 4 +- 3 files changed, 220 insertions(+), 53 deletions(-) diff --git a/src/main/ipc/handlers/symphony.ts b/src/main/ipc/handlers/symphony.ts index 8f4d2e64..5b5ab3e9 100644 --- a/src/main/ipc/handlers/symphony.ts +++ b/src/main/ipc/handlers/symphony.ts @@ -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); } diff --git a/src/renderer/components/SymphonyModal.tsx b/src/renderer/components/SymphonyModal.tsx index 29932899..909fdf5e 100644 --- a/src/renderer/components/SymphonyModal.tsx +++ b/src/renderer/components/SymphonyModal.tsx @@ -27,7 +27,6 @@ import { GitMerge, Clock, Zap, - Star, Play, Pause, AlertCircle, @@ -36,6 +35,7 @@ import { Flame, FileText, Hash, + ChevronDown, } from 'lucide-react'; import type { Theme } from '../types'; import type { @@ -53,6 +53,7 @@ import { MODAL_PRIORITIES } from '../constants/modalPriorities'; import { useSymphony } from '../hooks/symphony'; import { useContributorStats, type Achievement } from '../hooks/symphony/useContributorStats'; import { AgentCreationDialog, type AgentCreationConfig } from './AgentCreationDialog'; +import { generateProseStyles, createMarkdownComponents } from '../utils/markdownConfig'; // ============================================================================ // Types @@ -207,7 +208,6 @@ function RepositoryTile({ {categoryInfo.emoji} {categoryInfo.label} - {repo.featured && }

@@ -255,7 +255,7 @@ function IssueCard({ !isAvailable ? 'opacity-60 cursor-not-allowed' : 'hover:bg-white/5' } ${isSelected ? 'ring-2' : ''}`} style={{ - backgroundColor: isSelected ? theme.colors.bgActivity : 'transparent', + backgroundColor: isSelected ? theme.colors.bgActivity : theme.colors.bgMain, borderColor: isSelected ? theme.colors.accent : theme.colors.border, ...(isSelected && { boxShadow: `0 0 0 2px ${theme.colors.accent}` }), }} @@ -331,15 +331,92 @@ function RepositoryDetailView({ onBack: () => void; onSelectIssue: (issue: SymphonyIssue) => void; onStartContribution: () => void; - onPreviewDocument: (path: string) => void; + onPreviewDocument: (path: string, isExternal: boolean) => void; }) { const categoryInfo = SYMPHONY_CATEGORIES[repo.category] ?? { label: repo.category, emoji: '📦' }; const availableIssues = issues.filter(i => i.status === 'available'); - const [selectedDocPath, setSelectedDocPath] = useState(null); + const [selectedDocIndex, setSelectedDocIndex] = useState(0); + const [showDocDropdown, setShowDocDropdown] = useState(false); + const dropdownRef = useRef(null); - const handleSelectDoc = (path: string) => { - setSelectedDocPath(path); - onPreviewDocument(path); + // Generate prose styles scoped to symphony preview panel + const proseStyles = useMemo( + () => + 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) => { @@ -362,7 +439,6 @@ function RepositoryDetailView({ {categoryInfo.emoji} {categoryInfo.label} - {repo.featured && }

{repo.name} @@ -475,10 +551,10 @@ function RepositoryDetailView({ {/* Right: Issue preview */} -
+
{selectedIssue ? ( <> -
+
#{selectedIssue.number}

{selectedIssue.title}

@@ -489,46 +565,66 @@ function RepositoryDetailView({
- {/* Document tabs */} -
- {selectedIssue.documentPaths.map((doc) => ( + {/* Document selector dropdown */} +
+
- ))} + + {showDocDropdown && ( +
+ {selectedIssue.documentPaths.map((doc, index) => ( + + ))} +
+ )} +
-
+ {/* Document preview - Markdown preview scrollable container with prose styles */} +
+ {isLoadingDocument ? (
) : documentPreview ? ( -
- {documentPreview} +
+ + {documentPreview} +
- ) : selectedDocPath ? ( -

- Document preview unavailable -

) : (
@@ -538,7 +634,10 @@ function RepositoryDetailView({
) : ( -
+

Select an issue to see details

@@ -963,14 +1062,31 @@ export function SymphonyModal({ setDocumentPreview(null); }, []); - // Preview document (stub - not yet implemented in IPC) - const handlePreviewDocument = useCallback(async (path: string) => { + // Preview document - fetches content from external URLs (GitHub attachments) + const handlePreviewDocument = useCallback(async (path: string, isExternal: boolean) => { if (!selectedRepo) return; setIsLoadingDocument(true); - // TODO: Implement document preview via IPC - // 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.`); - setIsLoadingDocument(false); + setDocumentPreview(null); + + try { + 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]); // Start contribution - opens agent creation dialog @@ -1154,7 +1270,10 @@ export function SymphonyModal({ {activeTab === 'projects' && ( <> {/* Search + Category tabs */} -
+
@@ -1164,8 +1283,12 @@ export function SymphonyModal({ value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} placeholder="Search repositories..." - className="w-full pl-9 pr-3 py-2 rounded border bg-transparent outline-none text-sm focus:ring-1" - style={{ borderColor: theme.colors.border, color: theme.colors.textMain }} + 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, + backgroundColor: theme.colors.bgActivity, + }} />
@@ -1174,8 +1297,9 @@ export function SymphonyModal({ onClick={() => setSelectedCategory('all')} className={`px-3 py-1.5 rounded text-sm transition-colors ${selectedCategory === 'all' ? 'font-semibold' : ''}`} 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, + border: selectedCategory === 'all' ? `1px solid ${theme.colors.accent}` : '1px solid transparent', }} > All @@ -1190,8 +1314,9 @@ export function SymphonyModal({ selectedCategory === cat ? 'font-semibold' : '' }`} 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, + border: selectedCategory === cat ? `1px solid ${theme.colors.accent}` : '1px solid transparent', }} > {info?.emoji} @@ -1204,7 +1329,7 @@ export function SymphonyModal({
{/* Repository grid */} -
+
{isLoading ? (
{[1, 2, 3, 4, 5, 6].map((i) => )} diff --git a/symphony-registry.json b/symphony-registry.json index 67258208..1ee94e7d 100644 --- a/symphony-registry.json +++ b/symphony-registry.json @@ -5,9 +5,9 @@ { "slug": "pedramamini/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", - "category": "developer-tools", + "category": "ai-ml", "tags": ["electron", "ai", "productivity", "typescript"], "maintainer": { "name": "Pedram Amini",