mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
- Auto Run docs now support repo paths *and* GitHub attachment links 📎
- Introduced `DocumentReference` objects with name, path, and external flag 🧩 - Smarter issue parsing extracts markdown `.md` links into downloadable docs 🔍 - Dedupes documents by filename, preferring external attachments when present 🧠 - Added 1MB issue-body parsing cap to prevent performance blowups 🚧 - Path traversal checks now apply only to repo-relative document references 🛡️ - Auto Run Docs setup can download external files directly via `fetch` 🌐 - Switched runner file ops to Node `fs`, replacing shell `cp/rm` usage 🧰 - Runner now configures git user.name/email to ensure commits always work 🪪 - Failure paths now clean up local repos automatically to reduce clutter 🧹 - History virtualization now measures elements directly for more accurate sizing 📏 - Marketplace and Symphony modals widened for a roomier workflow view 🖥️
This commit is contained in:
@@ -43,6 +43,7 @@ import type {
|
|||||||
StartContributionResponse,
|
StartContributionResponse,
|
||||||
CompleteContributionResponse,
|
CompleteContributionResponse,
|
||||||
IssueStatus,
|
IssueStatus,
|
||||||
|
DocumentReference,
|
||||||
} from '../../../shared/symphony-types';
|
} from '../../../shared/symphony-types';
|
||||||
import { SymphonyError } from '../../../shared/symphony-types';
|
import { SymphonyError } from '../../../shared/symphony-types';
|
||||||
|
|
||||||
@@ -126,7 +127,7 @@ function validateContributionParams(params: {
|
|||||||
repoUrl: string;
|
repoUrl: string;
|
||||||
repoName: string;
|
repoName: string;
|
||||||
issueNumber: number;
|
issueNumber: number;
|
||||||
documentPaths: string[];
|
documentPaths: DocumentReference[];
|
||||||
}): { valid: boolean; error?: string } {
|
}): { valid: boolean; error?: string } {
|
||||||
// Validate repo slug
|
// Validate repo slug
|
||||||
const slugValidation = validateRepoSlug(params.repoSlug);
|
const slugValidation = validateRepoSlug(params.repoSlug);
|
||||||
@@ -150,10 +151,12 @@ function validateContributionParams(params: {
|
|||||||
return { valid: false, error: 'Invalid issue number' };
|
return { valid: false, error: 'Invalid issue number' };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate document paths (check for path traversal)
|
// Validate document paths (check for path traversal in repo-relative paths)
|
||||||
for (const docPath of params.documentPaths) {
|
for (const doc of params.documentPaths) {
|
||||||
if (docPath.includes('..') || docPath.startsWith('/')) {
|
// Skip validation for external URLs (they're downloaded, not file paths)
|
||||||
return { valid: false, error: `Invalid document path: ${docPath}` };
|
if (doc.isExternal) continue;
|
||||||
|
if (doc.path.includes('..') || doc.path.startsWith('/')) {
|
||||||
|
return { valid: false, error: `Invalid document path: ${doc.path}` };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,25 +283,69 @@ function generateBranchName(issueNumber: number): string {
|
|||||||
.replace('{timestamp}', timestamp);
|
.replace('{timestamp}', timestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Maximum body size to parse (1MB) to prevent performance issues */
|
||||||
* Parse document paths from issue body.
|
const MAX_BODY_SIZE = 1024 * 1024;
|
||||||
*/
|
|
||||||
function parseDocumentPaths(body: string): string[] {
|
|
||||||
const paths: Set<string> = new Set();
|
|
||||||
|
|
||||||
for (const pattern of DOCUMENT_PATH_PATTERNS) {
|
/**
|
||||||
// Reset lastIndex for global regex
|
* Parse document references from issue body.
|
||||||
pattern.lastIndex = 0;
|
* Supports both repository-relative paths and GitHub attachment links.
|
||||||
let match;
|
*/
|
||||||
while ((match = pattern.exec(body)) !== null) {
|
function parseDocumentPaths(body: string): DocumentReference[] {
|
||||||
const docPath = match[1];
|
// Guard against extremely large bodies that could cause performance issues
|
||||||
if (docPath && !docPath.startsWith('http')) {
|
if (body.length > MAX_BODY_SIZE) {
|
||||||
paths.add(docPath);
|
logger.warn('Issue body too large, truncating for document parsing', LOG_CONTEXT, {
|
||||||
|
bodyLength: body.length,
|
||||||
|
maxSize: MAX_BODY_SIZE,
|
||||||
|
});
|
||||||
|
body = body.substring(0, MAX_BODY_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
const docs: Map<string, DocumentReference> = new Map();
|
||||||
|
|
||||||
|
// Pattern for markdown links: [filename.md](url)
|
||||||
|
// Captures: [1] = filename (link text), [2] = URL
|
||||||
|
const markdownLinkPattern = /\[([^\]]+\.md)\]\(([^)]+)\)/gi;
|
||||||
|
|
||||||
|
// First, check for markdown links (GitHub attachments)
|
||||||
|
let match;
|
||||||
|
while ((match = markdownLinkPattern.exec(body)) !== null) {
|
||||||
|
const filename = match[1];
|
||||||
|
const url = match[2];
|
||||||
|
// Only add if it's a GitHub attachment URL or similar external URL
|
||||||
|
if (url.startsWith('http')) {
|
||||||
|
const key = filename.toLowerCase(); // Dedupe by filename
|
||||||
|
if (!docs.has(key)) {
|
||||||
|
docs.set(key, {
|
||||||
|
name: filename,
|
||||||
|
path: url,
|
||||||
|
isExternal: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Array.from(paths);
|
// Then check for repo-relative paths using existing patterns
|
||||||
|
for (const pattern of DOCUMENT_PATH_PATTERNS) {
|
||||||
|
// Reset lastIndex for global regex
|
||||||
|
pattern.lastIndex = 0;
|
||||||
|
while ((match = pattern.exec(body)) !== null) {
|
||||||
|
const docPath = match[1];
|
||||||
|
if (docPath && !docPath.startsWith('http')) {
|
||||||
|
const filename = docPath.split('/').pop() || docPath;
|
||||||
|
const key = filename.toLowerCase();
|
||||||
|
// Don't overwrite external links with same filename
|
||||||
|
if (!docs.has(key)) {
|
||||||
|
docs.set(key, {
|
||||||
|
name: filename,
|
||||||
|
path: docPath,
|
||||||
|
isExternal: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(docs.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -751,7 +798,7 @@ export function registerSymphonyHandlers({ app, getMainWindow }: SymphonyHandler
|
|||||||
repoName: string;
|
repoName: string;
|
||||||
issueNumber: number;
|
issueNumber: number;
|
||||||
issueTitle: string;
|
issueTitle: string;
|
||||||
documentPaths: string[];
|
documentPaths: DocumentReference[];
|
||||||
agentType: string;
|
agentType: string;
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
baseBranch?: string;
|
baseBranch?: string;
|
||||||
@@ -1151,7 +1198,7 @@ This PR will be updated automatically when the Auto Run completes.`;
|
|||||||
issueNumber: number;
|
issueNumber: number;
|
||||||
issueTitle: string;
|
issueTitle: string;
|
||||||
localPath: string;
|
localPath: string;
|
||||||
documentPaths: string[];
|
documentPaths: DocumentReference[];
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
branchName?: string;
|
branchName?: string;
|
||||||
@@ -1172,10 +1219,10 @@ This PR will be updated automatically when the Auto Run completes.`;
|
|||||||
return { success: false, error: 'Invalid issue number' };
|
return { success: false, error: 'Invalid issue number' };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate document paths for path traversal
|
// Validate document paths for path traversal (only repo-relative paths)
|
||||||
for (const docPath of documentPaths) {
|
for (const doc of documentPaths) {
|
||||||
if (docPath.includes('..') || docPath.startsWith('/')) {
|
if (!doc.isExternal && (doc.path.includes('..') || doc.path.startsWith('/'))) {
|
||||||
return { success: false, error: `Invalid document path: ${docPath}` };
|
return { success: false, error: `Invalid document path: ${doc.path}` };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1235,19 +1282,37 @@ This PR will be updated automatically when the Auto Run completes.`;
|
|||||||
const autoRunDir = path.join(localPath, 'Auto Run Docs');
|
const autoRunDir = path.join(localPath, 'Auto Run Docs');
|
||||||
await fs.mkdir(autoRunDir, { recursive: true });
|
await fs.mkdir(autoRunDir, { recursive: true });
|
||||||
|
|
||||||
for (const docPath of documentPaths) {
|
for (const doc of documentPaths) {
|
||||||
// Ensure the path doesn't escape the localPath
|
const destPath = path.join(autoRunDir, doc.name);
|
||||||
const resolvedSource = path.resolve(localPath, docPath);
|
|
||||||
if (!resolvedSource.startsWith(localPath)) {
|
if (doc.isExternal) {
|
||||||
logger.error('Attempted path traversal in document copy', LOG_CONTEXT, { docPath });
|
// Download external file (GitHub attachment)
|
||||||
continue;
|
try {
|
||||||
}
|
logger.info('Downloading external document', LOG_CONTEXT, { name: doc.name, url: doc.path });
|
||||||
const destPath = path.join(autoRunDir, path.basename(docPath));
|
const response = await fetch(doc.path);
|
||||||
try {
|
if (!response.ok) {
|
||||||
await fs.copyFile(resolvedSource, destPath);
|
logger.warn('Failed to download document', LOG_CONTEXT, { name: doc.name, status: response.status });
|
||||||
logger.info('Copied document', LOG_CONTEXT, { from: resolvedSource, to: destPath });
|
continue;
|
||||||
} catch (e) {
|
}
|
||||||
logger.warn('Failed to copy document', LOG_CONTEXT, { docPath, error: e instanceof Error ? e.message : String(e) });
|
const buffer = await response.arrayBuffer();
|
||||||
|
await fs.writeFile(destPath, Buffer.from(buffer));
|
||||||
|
logger.info('Downloaded document', LOG_CONTEXT, { name: doc.name, to: destPath });
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn('Failed to download document', LOG_CONTEXT, { name: doc.name, error: e instanceof Error ? e.message : String(e) });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Copy from repo - ensure the path doesn't escape the localPath
|
||||||
|
const resolvedSource = path.resolve(localPath, doc.path);
|
||||||
|
if (!resolvedSource.startsWith(localPath)) {
|
||||||
|
logger.error('Attempted path traversal in document copy', LOG_CONTEXT, { docPath: doc.path });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await fs.copyFile(resolvedSource, destPath);
|
||||||
|
logger.info('Copied document', LOG_CONTEXT, { from: resolvedSource, to: destPath });
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn('Failed to copy document', LOG_CONTEXT, { docPath: doc.path, error: e instanceof Error ? e.message : String(e) });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,20 +5,32 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import fs from 'fs/promises';
|
||||||
import { logger } from '../utils/logger';
|
import { logger } from '../utils/logger';
|
||||||
import { execFileNoThrow } from '../utils/execFile';
|
import { execFileNoThrow } from '../utils/execFile';
|
||||||
// Types imported for documentation and future use
|
import type { DocumentReference } from '../../shared/symphony-types';
|
||||||
// import type { ActiveContribution, SymphonyIssue } from '../../shared/symphony-types';
|
|
||||||
|
|
||||||
const LOG_CONTEXT = '[SymphonyRunner]';
|
const LOG_CONTEXT = '[SymphonyRunner]';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up local repository directory on failure.
|
||||||
|
*/
|
||||||
|
async function cleanupLocalRepo(localPath: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
await fs.rm(localPath, { recursive: true, force: true });
|
||||||
|
logger.info('Cleaned up local repository', LOG_CONTEXT, { localPath });
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('Failed to cleanup local repository', LOG_CONTEXT, { localPath, error });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export interface SymphonyRunnerOptions {
|
export interface SymphonyRunnerOptions {
|
||||||
contributionId: string;
|
contributionId: string;
|
||||||
repoSlug: string;
|
repoSlug: string;
|
||||||
repoUrl: string;
|
repoUrl: string;
|
||||||
issueNumber: number;
|
issueNumber: number;
|
||||||
issueTitle: string;
|
issueTitle: string;
|
||||||
documentPaths: string[];
|
documentPaths: DocumentReference[];
|
||||||
localPath: string;
|
localPath: string;
|
||||||
branchName: string;
|
branchName: string;
|
||||||
onProgress?: (progress: { completedDocuments: number; totalDocuments: number }) => void;
|
onProgress?: (progress: { completedDocuments: number; totalDocuments: number }) => void;
|
||||||
@@ -42,6 +54,23 @@ async function createBranch(localPath: string, branchName: string): Promise<bool
|
|||||||
return result.exitCode === 0;
|
return result.exitCode === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure git user for commits (required for users without global git config).
|
||||||
|
*/
|
||||||
|
async function configureGitUser(localPath: string): Promise<boolean> {
|
||||||
|
const nameResult = await execFileNoThrow('git', ['config', 'user.name', 'Maestro Symphony'], localPath);
|
||||||
|
if (nameResult.exitCode !== 0) {
|
||||||
|
logger.warn('Failed to set git user.name', LOG_CONTEXT, { error: nameResult.stderr });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const emailResult = await execFileNoThrow('git', ['config', 'user.email', 'symphony@runmaestro.ai'], localPath);
|
||||||
|
if (emailResult.exitCode !== 0) {
|
||||||
|
logger.warn('Failed to set git user.email', LOG_CONTEXT, { error: emailResult.stderr });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an empty commit to enable pushing without changes.
|
* Create an empty commit to enable pushing without changes.
|
||||||
*/
|
*/
|
||||||
@@ -98,19 +127,58 @@ Closes #${issueNumber}
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copy Auto Run documents from repo to local Auto Run Docs folder.
|
* Download a file from a URL.
|
||||||
|
*/
|
||||||
|
async function downloadFile(url: string, destPath: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) {
|
||||||
|
logger.error('Failed to download file', LOG_CONTEXT, { url, status: response.status });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const buffer = await response.arrayBuffer();
|
||||||
|
await fs.writeFile(destPath, Buffer.from(buffer));
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error downloading file', LOG_CONTEXT, { url, error });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy or download Auto Run documents to local Auto Run Docs folder.
|
||||||
|
* Handles both repo-relative paths and external URLs (GitHub attachments).
|
||||||
*/
|
*/
|
||||||
async function setupAutoRunDocs(
|
async function setupAutoRunDocs(
|
||||||
localPath: string,
|
localPath: string,
|
||||||
documentPaths: string[]
|
documentPaths: DocumentReference[]
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const autoRunPath = path.join(localPath, 'Auto Run Docs');
|
const autoRunPath = path.join(localPath, 'Auto Run Docs');
|
||||||
await execFileNoThrow('mkdir', ['-p', autoRunPath]);
|
await fs.mkdir(autoRunPath, { recursive: true });
|
||||||
|
|
||||||
for (const docPath of documentPaths) {
|
for (const doc of documentPaths) {
|
||||||
const sourcePath = path.join(localPath, docPath);
|
const destPath = path.join(autoRunPath, doc.name);
|
||||||
const destPath = path.join(autoRunPath, path.basename(docPath));
|
|
||||||
await execFileNoThrow('cp', [sourcePath, destPath]);
|
if (doc.isExternal) {
|
||||||
|
// Download external file (GitHub attachment)
|
||||||
|
logger.info('Downloading external document', LOG_CONTEXT, { name: doc.name, url: doc.path });
|
||||||
|
const success = await downloadFile(doc.path, destPath);
|
||||||
|
if (!success) {
|
||||||
|
logger.warn('Failed to download document, skipping', LOG_CONTEXT, { name: doc.name });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Copy from repo using Node.js fs API
|
||||||
|
const sourcePath = path.join(localPath, doc.path);
|
||||||
|
try {
|
||||||
|
await fs.copyFile(sourcePath, destPath);
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('Failed to copy document', LOG_CONTEXT, {
|
||||||
|
name: doc.name,
|
||||||
|
source: sourcePath,
|
||||||
|
error: error instanceof Error ? error.message : String(error)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return autoRunPath;
|
return autoRunPath;
|
||||||
@@ -154,23 +222,30 @@ export async function startContribution(options: SymphonyRunnerOptions): Promise
|
|||||||
// 2. Create branch
|
// 2. Create branch
|
||||||
onStatusChange?.('setting_up');
|
onStatusChange?.('setting_up');
|
||||||
if (!await createBranch(localPath, branchName)) {
|
if (!await createBranch(localPath, branchName)) {
|
||||||
|
await cleanupLocalRepo(localPath);
|
||||||
return { success: false, error: 'Branch creation failed' };
|
return { success: false, error: 'Branch creation failed' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2.5. Configure git user for commits
|
||||||
|
await configureGitUser(localPath);
|
||||||
|
|
||||||
// 3. Empty commit
|
// 3. Empty commit
|
||||||
const commitMessage = `[Symphony] Start contribution for #${issueNumber}`;
|
const commitMessage = `[Symphony] Start contribution for #${issueNumber}`;
|
||||||
if (!await createEmptyCommit(localPath, commitMessage)) {
|
if (!await createEmptyCommit(localPath, commitMessage)) {
|
||||||
|
await cleanupLocalRepo(localPath);
|
||||||
return { success: false, error: 'Empty commit failed' };
|
return { success: false, error: 'Empty commit failed' };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Push branch
|
// 4. Push branch
|
||||||
if (!await pushBranch(localPath, branchName)) {
|
if (!await pushBranch(localPath, branchName)) {
|
||||||
|
await cleanupLocalRepo(localPath);
|
||||||
return { success: false, error: 'Push failed' };
|
return { success: false, error: 'Push failed' };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Create draft PR
|
// 5. Create draft PR
|
||||||
const prResult = await createDraftPR(localPath, issueNumber, issueTitle);
|
const prResult = await createDraftPR(localPath, issueNumber, issueTitle);
|
||||||
if (!prResult.success) {
|
if (!prResult.success) {
|
||||||
|
await cleanupLocalRepo(localPath);
|
||||||
return { success: false, error: prResult.error };
|
return { success: false, error: prResult.error };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,6 +262,7 @@ export async function startContribution(options: SymphonyRunnerOptions): Promise
|
|||||||
autoRunPath,
|
autoRunPath,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
await cleanupLocalRepo(localPath);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: error instanceof Error ? error.message : 'Unknown error',
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
@@ -203,6 +279,9 @@ export async function finalizeContribution(
|
|||||||
issueNumber: number,
|
issueNumber: number,
|
||||||
issueTitle: string
|
issueTitle: string
|
||||||
): Promise<{ success: boolean; prUrl?: string; error?: string }> {
|
): Promise<{ success: boolean; prUrl?: string; error?: string }> {
|
||||||
|
// Configure git user for commits (in case not already configured)
|
||||||
|
await configureGitUser(localPath);
|
||||||
|
|
||||||
// Commit all changes
|
// Commit all changes
|
||||||
await execFileNoThrow('git', ['add', '-A'], localPath);
|
await execFileNoThrow('git', ['add', '-A'], localPath);
|
||||||
|
|
||||||
@@ -281,9 +360,9 @@ export async function cancelContribution(
|
|||||||
logger.warn('Failed to close PR', LOG_CONTEXT, { prNumber, error: closeResult.stderr });
|
logger.warn('Failed to close PR', LOG_CONTEXT, { prNumber, error: closeResult.stderr });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up local directory
|
// Clean up local directory using Node.js fs API
|
||||||
if (cleanup) {
|
if (cleanup) {
|
||||||
await execFileNoThrow('rm', ['-rf', localPath]);
|
await cleanupLocalRepo(localPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
|
|||||||
@@ -290,8 +290,8 @@ function IssueCard({
|
|||||||
|
|
||||||
{issue.documentPaths.length > 0 && (
|
{issue.documentPaths.length > 0 && (
|
||||||
<div className="mt-2 text-xs" style={{ color: theme.colors.textDim }}>
|
<div className="mt-2 text-xs" style={{ color: theme.colors.textDim }}>
|
||||||
{issue.documentPaths.slice(0, 2).map((path, i) => (
|
{issue.documentPaths.slice(0, 2).map((doc, i) => (
|
||||||
<div key={i} className="truncate">• {path}</div>
|
<div key={i} className="truncate">• {doc.name}</div>
|
||||||
))}
|
))}
|
||||||
{issue.documentPaths.length > 2 && (
|
{issue.documentPaths.length > 2 && (
|
||||||
<div>...and {issue.documentPaths.length - 2} more</div>
|
<div>...and {issue.documentPaths.length - 2} more</div>
|
||||||
@@ -491,17 +491,17 @@ function RepositoryDetailView({
|
|||||||
|
|
||||||
{/* Document tabs */}
|
{/* Document tabs */}
|
||||||
<div className="flex items-center gap-1 px-4 py-2 border-b overflow-x-auto" style={{ borderColor: theme.colors.border }}>
|
<div className="flex items-center gap-1 px-4 py-2 border-b overflow-x-auto" style={{ borderColor: theme.colors.border }}>
|
||||||
{selectedIssue.documentPaths.map((path) => (
|
{selectedIssue.documentPaths.map((doc) => (
|
||||||
<button
|
<button
|
||||||
key={path}
|
key={doc.name}
|
||||||
onClick={() => handleSelectDoc(path)}
|
onClick={() => handleSelectDoc(doc.path)}
|
||||||
className="px-2 py-1 rounded text-xs whitespace-nowrap transition-colors"
|
className="px-2 py-1 rounded text-xs whitespace-nowrap transition-colors"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: selectedDocPath === path ? theme.colors.accent + '20' : 'transparent',
|
backgroundColor: selectedDocPath === doc.path ? theme.colors.accent + '20' : 'transparent',
|
||||||
color: selectedDocPath === path ? theme.colors.accent : theme.colors.textDim,
|
color: selectedDocPath === doc.path ? theme.colors.accent : theme.colors.textDim,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{path.split('/').pop()}
|
{doc.name}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -1079,7 +1079,7 @@ export function SymphonyModal({
|
|||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-labelledby="symphony-modal-title"
|
aria-labelledby="symphony-modal-title"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
className="w-[1000px] max-w-[95vw] rounded-xl shadow-2xl border overflow-hidden flex flex-col max-h-[85vh] outline-none"
|
className="w-[1200px] max-w-[95vw] rounded-xl shadow-2xl border overflow-hidden flex flex-col max-h-[85vh] outline-none"
|
||||||
style={{ backgroundColor: theme.colors.bgActivity, borderColor: theme.colors.border }}
|
style={{ backgroundColor: theme.colors.bgActivity, borderColor: theme.colors.border }}
|
||||||
>
|
>
|
||||||
{/* Detail view for projects */}
|
{/* Detail view for projects */}
|
||||||
|
|||||||
@@ -69,6 +69,22 @@ export type SymphonyCategory =
|
|||||||
// GitHub Issue Types (Fetched via GitHub API)
|
// GitHub Issue Types (Fetched via GitHub API)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference to an Auto Run document.
|
||||||
|
* Supports both repository-relative paths and external URLs (e.g., GitHub attachments).
|
||||||
|
*/
|
||||||
|
export interface DocumentReference {
|
||||||
|
/** Display name (filename without path) */
|
||||||
|
name: string;
|
||||||
|
/**
|
||||||
|
* For repo-relative paths: the path within the repository (e.g., "docs/task.md")
|
||||||
|
* For external files: the download URL
|
||||||
|
*/
|
||||||
|
path: string;
|
||||||
|
/** Whether this is an external URL that needs to be downloaded */
|
||||||
|
isExternal: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A GitHub issue with the `runmaestro.ai` label.
|
* A GitHub issue with the `runmaestro.ai` label.
|
||||||
* Represents a contribution opportunity.
|
* Represents a contribution opportunity.
|
||||||
@@ -90,8 +106,8 @@ export interface SymphonyIssue {
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
/** When issue was last updated */
|
/** When issue was last updated */
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
/** Parsed Auto Run document paths from issue body */
|
/** Parsed Auto Run document references from issue body */
|
||||||
documentPaths: string[];
|
documentPaths: DocumentReference[];
|
||||||
/** Availability status */
|
/** Availability status */
|
||||||
status: IssueStatus;
|
status: IssueStatus;
|
||||||
/** If in progress, the PR working on it */
|
/** If in progress, the PR working on it */
|
||||||
|
|||||||
Reference in New Issue
Block a user