From 9586589453283dd7fb2585f1dc9dec05453aeddd Mon Sep 17 00:00:00 2001 From: Test User Date: Mon, 22 Dec 2025 12:10:54 -0500 Subject: [PATCH] fixing auto verify for kanban issues --- .github/workflows/security-audit.yml | 30 ++ apps/server/src/index.ts | 2 + apps/server/src/routes/github/index.ts | 18 + .../github/routes/check-github-remote.ts | 71 +++ .../server/src/routes/github/routes/common.ts | 35 ++ .../src/routes/github/routes/list-issues.ts | 89 ++++ .../src/routes/github/routes/list-prs.ts | 93 ++++ .../suggestions/generate-suggestions.ts | 12 +- apps/server/src/services/auto-mode-service.ts | 76 +++- .../sidebar/components/sidebar-navigation.tsx | 4 +- .../layout/sidebar/hooks/use-navigation.ts | 64 ++- .../sidebar/hooks/use-project-creation.ts | 22 +- .../ui/src/components/views/analysis-view.tsx | 82 ---- apps/ui/src/components/views/board-view.tsx | 4 +- .../kanban-card/card-content-sections.tsx | 33 +- .../components/kanban-card/kanban-card.tsx | 6 +- .../board-view/dialogs/add-feature-dialog.tsx | 6 - .../dialogs/edit-feature-dialog.tsx | 4 - .../dialogs/feature-suggestions-dialog.tsx | 21 +- .../board-view/hooks/use-board-actions.ts | 2 - .../board-view/shared/testing-tab-content.tsx | 50 +-- .../components/views/github-issues-view.tsx | 334 ++++++++++++++ .../src/components/views/github-prs-view.tsx | 421 ++++++++++++++++++ .../src/components/views/interview-view.tsx | 5 - apps/ui/src/lib/electron.ts | 174 +++++--- apps/ui/src/lib/http-api-client.ts | 10 + apps/ui/src/routes/github-issues.tsx | 6 + apps/ui/src/routes/github-prs.tsx | 6 + apps/ui/src/store/app-store.ts | 1 - libs/types/src/feature.ts | 1 - 30 files changed, 1376 insertions(+), 306 deletions(-) create mode 100644 .github/workflows/security-audit.yml create mode 100644 apps/server/src/routes/github/index.ts create mode 100644 apps/server/src/routes/github/routes/check-github-remote.ts create mode 100644 apps/server/src/routes/github/routes/common.ts create mode 100644 apps/server/src/routes/github/routes/list-issues.ts create mode 100644 apps/server/src/routes/github/routes/list-prs.ts create mode 100644 apps/ui/src/components/views/github-issues-view.tsx create mode 100644 apps/ui/src/components/views/github-prs-view.tsx create mode 100644 apps/ui/src/routes/github-issues.tsx create mode 100644 apps/ui/src/routes/github-prs.tsx diff --git a/.github/workflows/security-audit.yml b/.github/workflows/security-audit.yml new file mode 100644 index 00000000..1a867179 --- /dev/null +++ b/.github/workflows/security-audit.yml @@ -0,0 +1,30 @@ +name: Security Audit + +on: + pull_request: + branches: + - '*' + push: + branches: + - main + - master + schedule: + # Run weekly on Mondays at 9 AM UTC + - cron: '0 9 * * 1' + +jobs: + audit: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup project + uses: ./.github/actions/setup-project + with: + check-lockfile: 'true' + + - name: Run npm audit + run: npm audit --audit-level=moderate + continue-on-error: false diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index 4a19c4c7..b2e9f115 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -46,6 +46,7 @@ import { SettingsService } from './services/settings-service.js'; import { createSpecRegenerationRoutes } from './routes/app-spec/index.js'; import { createClaudeRoutes } from './routes/claude/index.js'; import { ClaudeUsageService } from './services/claude-usage-service.js'; +import { createGitHubRoutes } from './routes/github/index.js'; // Load environment variables dotenv.config(); @@ -145,6 +146,7 @@ app.use('/api/templates', createTemplatesRoutes()); app.use('/api/terminal', createTerminalRoutes()); app.use('/api/settings', createSettingsRoutes(settingsService)); app.use('/api/claude', createClaudeRoutes(claudeUsageService)); +app.use('/api/github', createGitHubRoutes()); // Create HTTP server const server = createServer(app); diff --git a/apps/server/src/routes/github/index.ts b/apps/server/src/routes/github/index.ts new file mode 100644 index 00000000..bda4d217 --- /dev/null +++ b/apps/server/src/routes/github/index.ts @@ -0,0 +1,18 @@ +/** + * GitHub routes - HTTP API for GitHub integration + */ + +import { Router } from 'express'; +import { createCheckGitHubRemoteHandler } from './routes/check-github-remote.js'; +import { createListIssuesHandler } from './routes/list-issues.js'; +import { createListPRsHandler } from './routes/list-prs.js'; + +export function createGitHubRoutes(): Router { + const router = Router(); + + router.post('/check-remote', createCheckGitHubRemoteHandler()); + router.post('/issues', createListIssuesHandler()); + router.post('/prs', createListPRsHandler()); + + return router; +} diff --git a/apps/server/src/routes/github/routes/check-github-remote.ts b/apps/server/src/routes/github/routes/check-github-remote.ts new file mode 100644 index 00000000..34a07198 --- /dev/null +++ b/apps/server/src/routes/github/routes/check-github-remote.ts @@ -0,0 +1,71 @@ +/** + * GET /check-github-remote endpoint - Check if project has a GitHub remote + */ + +import type { Request, Response } from 'express'; +import { execAsync, execEnv, getErrorMessage, logError } from './common.js'; + +export interface GitHubRemoteStatus { + hasGitHubRemote: boolean; + remoteUrl: string | null; + owner: string | null; + repo: string | null; +} + +export async function checkGitHubRemote(projectPath: string): Promise { + const status: GitHubRemoteStatus = { + hasGitHubRemote: false, + remoteUrl: null, + owner: null, + repo: null, + }; + + try { + // Get the remote URL (origin by default) + const { stdout } = await execAsync('git remote get-url origin', { + cwd: projectPath, + env: execEnv, + }); + + const remoteUrl = stdout.trim(); + status.remoteUrl = remoteUrl; + + // Check if it's a GitHub URL + // Formats: https://github.com/owner/repo.git, git@github.com:owner/repo.git + const httpsMatch = remoteUrl.match(/https:\/\/github\.com\/([^/]+)\/([^/.]+)/); + const sshMatch = remoteUrl.match(/git@github\.com:([^/]+)\/([^/.]+)/); + + const match = httpsMatch || sshMatch; + if (match) { + status.hasGitHubRemote = true; + status.owner = match[1]; + status.repo = match[2].replace(/\.git$/, ''); + } + } catch { + // No remote or not a git repo - that's okay + } + + return status; +} + +export function createCheckGitHubRemoteHandler() { + return async (req: Request, res: Response): Promise => { + try { + const { projectPath } = req.body; + + if (!projectPath) { + res.status(400).json({ success: false, error: 'projectPath is required' }); + return; + } + + const status = await checkGitHubRemote(projectPath); + res.json({ + success: true, + ...status, + }); + } catch (error) { + logError(error, 'Check GitHub remote failed'); + res.status(500).json({ success: false, error: getErrorMessage(error) }); + } + }; +} diff --git a/apps/server/src/routes/github/routes/common.ts b/apps/server/src/routes/github/routes/common.ts new file mode 100644 index 00000000..790f92c3 --- /dev/null +++ b/apps/server/src/routes/github/routes/common.ts @@ -0,0 +1,35 @@ +/** + * Common utilities for GitHub routes + */ + +import { exec } from 'child_process'; +import { promisify } from 'util'; + +export const execAsync = promisify(exec); + +// Extended PATH to include common tool installation locations +export const extendedPath = [ + process.env.PATH, + '/opt/homebrew/bin', + '/usr/local/bin', + '/home/linuxbrew/.linuxbrew/bin', + `${process.env.HOME}/.local/bin`, +] + .filter(Boolean) + .join(':'); + +export const execEnv = { + ...process.env, + PATH: extendedPath, +}; + +export function getErrorMessage(error: unknown): string { + if (error instanceof Error) { + return error.message; + } + return String(error); +} + +export function logError(error: unknown, context: string): void { + console.error(`[GitHub] ${context}:`, error); +} diff --git a/apps/server/src/routes/github/routes/list-issues.ts b/apps/server/src/routes/github/routes/list-issues.ts new file mode 100644 index 00000000..49850242 --- /dev/null +++ b/apps/server/src/routes/github/routes/list-issues.ts @@ -0,0 +1,89 @@ +/** + * POST /list-issues endpoint - List GitHub issues for a project + */ + +import type { Request, Response } from 'express'; +import { execAsync, execEnv, getErrorMessage, logError } from './common.js'; +import { checkGitHubRemote } from './check-github-remote.js'; + +export interface GitHubLabel { + name: string; + color: string; +} + +export interface GitHubAuthor { + login: string; +} + +export interface GitHubIssue { + number: number; + title: string; + state: string; + author: GitHubAuthor; + createdAt: string; + labels: GitHubLabel[]; + url: string; + body: string; +} + +export interface ListIssuesResult { + success: boolean; + issues?: GitHubIssue[]; + openIssues?: GitHubIssue[]; + closedIssues?: GitHubIssue[]; + error?: string; +} + +export function createListIssuesHandler() { + return async (req: Request, res: Response): Promise => { + try { + const { projectPath } = req.body; + + if (!projectPath) { + res.status(400).json({ success: false, error: 'projectPath is required' }); + return; + } + + // First check if this is a GitHub repo + const remoteStatus = await checkGitHubRemote(projectPath); + if (!remoteStatus.hasGitHubRemote) { + res.status(400).json({ + success: false, + error: 'Project does not have a GitHub remote', + }); + return; + } + + // Fetch open issues + const { stdout: openStdout } = await execAsync( + 'gh issue list --state open --json number,title,state,author,createdAt,labels,url,body --limit 100', + { + cwd: projectPath, + env: execEnv, + } + ); + + // Fetch closed issues + const { stdout: closedStdout } = await execAsync( + 'gh issue list --state closed --json number,title,state,author,createdAt,labels,url,body --limit 50', + { + cwd: projectPath, + env: execEnv, + } + ); + + const openIssues: GitHubIssue[] = JSON.parse(openStdout || '[]'); + const closedIssues: GitHubIssue[] = JSON.parse(closedStdout || '[]'); + + res.json({ + success: true, + openIssues, + closedIssues, + issues: [...openIssues, ...closedIssues], + }); + } catch (error) { + logError(error, 'List GitHub issues failed'); + res.status(500).json({ success: false, error: getErrorMessage(error) }); + } + }; +} diff --git a/apps/server/src/routes/github/routes/list-prs.ts b/apps/server/src/routes/github/routes/list-prs.ts new file mode 100644 index 00000000..2dd0b2c4 --- /dev/null +++ b/apps/server/src/routes/github/routes/list-prs.ts @@ -0,0 +1,93 @@ +/** + * POST /list-prs endpoint - List GitHub pull requests for a project + */ + +import type { Request, Response } from 'express'; +import { execAsync, execEnv, getErrorMessage, logError } from './common.js'; +import { checkGitHubRemote } from './check-github-remote.js'; + +export interface GitHubLabel { + name: string; + color: string; +} + +export interface GitHubAuthor { + login: string; +} + +export interface GitHubPR { + number: number; + title: string; + state: string; + author: GitHubAuthor; + createdAt: string; + labels: GitHubLabel[]; + url: string; + isDraft: boolean; + headRefName: string; + reviewDecision: string | null; + mergeable: string; + body: string; +} + +export interface ListPRsResult { + success: boolean; + prs?: GitHubPR[]; + openPRs?: GitHubPR[]; + mergedPRs?: GitHubPR[]; + error?: string; +} + +export function createListPRsHandler() { + return async (req: Request, res: Response): Promise => { + try { + const { projectPath } = req.body; + + if (!projectPath) { + res.status(400).json({ success: false, error: 'projectPath is required' }); + return; + } + + // First check if this is a GitHub repo + const remoteStatus = await checkGitHubRemote(projectPath); + if (!remoteStatus.hasGitHubRemote) { + res.status(400).json({ + success: false, + error: 'Project does not have a GitHub remote', + }); + return; + } + + // Fetch open PRs + const { stdout: openStdout } = await execAsync( + 'gh pr list --state open --json number,title,state,author,createdAt,labels,url,isDraft,headRefName,reviewDecision,mergeable,body --limit 100', + { + cwd: projectPath, + env: execEnv, + } + ); + + // Fetch merged PRs + const { stdout: mergedStdout } = await execAsync( + 'gh pr list --state merged --json number,title,state,author,createdAt,labels,url,isDraft,headRefName,reviewDecision,mergeable,body --limit 50', + { + cwd: projectPath, + env: execEnv, + } + ); + + const openPRs: GitHubPR[] = JSON.parse(openStdout || '[]'); + const mergedPRs: GitHubPR[] = JSON.parse(mergedStdout || '[]'); + + res.json({ + success: true, + openPRs, + mergedPRs, + prs: [...openPRs, ...mergedPRs], + }); + } catch (error) { + logError(error, 'List GitHub PRs failed'); + res.status(500).json({ success: false, error: getErrorMessage(error) }); + } + }; +} diff --git a/apps/server/src/routes/suggestions/generate-suggestions.ts b/apps/server/src/routes/suggestions/generate-suggestions.ts index 42514a0a..c8000ce5 100644 --- a/apps/server/src/routes/suggestions/generate-suggestions.ts +++ b/apps/server/src/routes/suggestions/generate-suggestions.ts @@ -23,10 +23,6 @@ const suggestionsSchema = { id: { type: 'string' }, category: { type: 'string' }, description: { type: 'string' }, - steps: { - type: 'array', - items: { type: 'string' }, - }, priority: { type: 'number', minimum: 1, @@ -34,7 +30,7 @@ const suggestionsSchema = { }, reasoning: { type: 'string' }, }, - required: ['category', 'description', 'steps', 'priority', 'reasoning'], + required: ['category', 'description', 'priority', 'reasoning'], }, }, }, @@ -62,9 +58,8 @@ Look at the codebase and provide 3-5 concrete suggestions. For each suggestion, provide: 1. A category (e.g., "User Experience", "Security", "Performance") 2. A clear description of what to implement -3. Concrete steps to implement it -4. Priority (1=high, 2=medium, 3=low) -5. Brief reasoning for why this would help +3. Priority (1=high, 2=medium, 3=low) +4. Brief reasoning for why this would help The response will be automatically formatted as structured JSON.`; @@ -164,7 +159,6 @@ The response will be automatically formatted as structured JSON.`; id: `suggestion-${Date.now()}-0`, category: 'Analysis', description: 'Review the AI analysis output for insights', - steps: ['Review the generated analysis'], priority: 1, reasoning: 'The AI provided analysis but suggestions need manual review', }, diff --git a/apps/server/src/services/auto-mode-service.ts b/apps/server/src/services/auto-mode-service.ts index da48308e..6621ff8a 100644 --- a/apps/server/src/services/auto-mode-service.ts +++ b/apps/server/src/services/auto-mode-service.ts @@ -599,15 +599,18 @@ export class AutoModeService { } ); - // Mark as waiting_approval for user review - await this.updateFeatureStatus(projectPath, featureId, 'waiting_approval'); + // Determine final status based on testing mode: + // - skipTests=false (automated testing): go directly to 'verified' (no manual verify needed) + // - skipTests=true (manual verification): go to 'waiting_approval' for manual review + const finalStatus = feature.skipTests ? 'waiting_approval' : 'verified'; + await this.updateFeatureStatus(projectPath, featureId, finalStatus); this.emitAutoModeEvent('auto_mode_feature_complete', { featureId, passes: true, message: `Feature completed in ${Math.round( (Date.now() - tempRunningFeature.startTime) / 1000 - )}s`, + )}s${finalStatus === 'verified' ? ' - auto-verified' : ''}`, projectPath, }); } catch (error) { @@ -868,13 +871,16 @@ Address the follow-up instructions above. Review the previous work and make the } ); - // Mark as waiting_approval for user review - await this.updateFeatureStatus(projectPath, featureId, 'waiting_approval'); + // Determine final status based on testing mode: + // - skipTests=false (automated testing): go directly to 'verified' (no manual verify needed) + // - skipTests=true (manual verification): go to 'waiting_approval' for manual review + const finalStatus = feature?.skipTests ? 'waiting_approval' : 'verified'; + await this.updateFeatureStatus(projectPath, featureId, finalStatus); this.emitAutoModeEvent('auto_mode_feature_complete', { featureId, passes: true, - message: 'Follow-up completed successfully', + message: `Follow-up completed successfully${finalStatus === 'verified' ? ' - auto-verified' : ''}`, projectPath, }); } catch (error) { @@ -1652,15 +1658,17 @@ You can use the Read tool to view these images at any time during implementation `; } - prompt += ` + // Add verification instructions based on testing mode + if (feature.skipTests) { + // Manual verification - just implement the feature + prompt += ` ## Instructions Implement this feature by: 1. First, explore the codebase to understand the existing structure 2. Plan your implementation approach 3. Write the necessary code changes -4. Add or update tests as needed -5. Ensure the code follows existing patterns and conventions +4. Ensure the code follows existing patterns and conventions When done, wrap your final summary in tags like this: @@ -1678,6 +1686,56 @@ When done, wrap your final summary in tags like this: This helps parse your summary correctly in the output logs.`; + } else { + // Automated testing - implement and verify with Playwright + prompt += ` +## Instructions + +Implement this feature by: +1. First, explore the codebase to understand the existing structure +2. Plan your implementation approach +3. Write the necessary code changes +4. Ensure the code follows existing patterns and conventions + +## Verification with Playwright (REQUIRED) + +After implementing the feature, you MUST verify it works correctly using Playwright: + +1. **Create a temporary Playwright test** to verify the feature works as expected +2. **Run the test** to confirm the feature is working +3. **Delete the test file** after verification - this is a temporary verification test, not a permanent test suite addition + +Example verification workflow: +\`\`\`bash +# Create a simple verification test +npx playwright test my-verification-test.spec.ts + +# After successful verification, delete the test +rm my-verification-test.spec.ts +\`\`\` + +The test should verify the core functionality of the feature. If the test fails, fix the implementation and re-test. + +When done, wrap your final summary in tags like this: + + +## Summary: [Feature Title] + +### Changes Implemented +- [List of changes made] + +### Files Modified +- [List of files] + +### Verification Status +- [Describe how the feature was verified with Playwright] + +### Notes for Developer +- [Any important notes] + + +This helps parse your summary correctly in the output logs.`; + } return prompt; } diff --git a/apps/ui/src/components/layout/sidebar/components/sidebar-navigation.tsx b/apps/ui/src/components/layout/sidebar/components/sidebar-navigation.tsx index 5178affa..b22dd8c1 100644 --- a/apps/ui/src/components/layout/sidebar/components/sidebar-navigation.tsx +++ b/apps/ui/src/components/layout/sidebar/components/sidebar-navigation.tsx @@ -51,7 +51,9 @@ export function SidebarNavigation({ return ( - - )} ); } diff --git a/apps/ui/src/components/views/github-issues-view.tsx b/apps/ui/src/components/views/github-issues-view.tsx new file mode 100644 index 00000000..3d254cb2 --- /dev/null +++ b/apps/ui/src/components/views/github-issues-view.tsx @@ -0,0 +1,334 @@ +import { useState, useEffect, useCallback } from 'react'; +import { CircleDot, Loader2, RefreshCw, ExternalLink, CheckCircle2, Circle, X } from 'lucide-react'; +import { getElectronAPI, GitHubIssue } from '@/lib/electron'; +import { useAppStore } from '@/store/app-store'; +import { Button } from '@/components/ui/button'; +import { cn } from '@/lib/utils'; + +export function GitHubIssuesView() { + const [openIssues, setOpenIssues] = useState([]); + const [closedIssues, setClosedIssues] = useState([]); + const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + const [error, setError] = useState(null); + const [selectedIssue, setSelectedIssue] = useState(null); + const { currentProject } = useAppStore(); + + const fetchIssues = useCallback(async () => { + if (!currentProject?.path) { + setError('No project selected'); + setLoading(false); + return; + } + + try { + setError(null); + const api = getElectronAPI(); + if (api.github) { + const result = await api.github.listIssues(currentProject.path); + if (result.success) { + setOpenIssues(result.openIssues || []); + setClosedIssues(result.closedIssues || []); + } else { + setError(result.error || 'Failed to fetch issues'); + } + } + } catch (err) { + console.error('[GitHubIssuesView] Error fetching issues:', err); + setError(err instanceof Error ? err.message : 'Failed to fetch issues'); + } finally { + setLoading(false); + setRefreshing(false); + } + }, [currentProject?.path]); + + useEffect(() => { + fetchIssues(); + }, [fetchIssues]); + + const handleRefresh = useCallback(() => { + setRefreshing(true); + fetchIssues(); + }, [fetchIssues]); + + const handleOpenInGitHub = useCallback((url: string) => { + const api = getElectronAPI(); + api.openExternalLink(url); + }, []); + + const formatDate = (dateString: string) => { + const date = new Date(dateString); + return date.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + }); + }; + + if (loading) { + return ( +
+ +
+ ); + } + + if (error) { + return ( +
+
+ +
+

Failed to Load Issues

+

{error}

+ +
+ ); + } + + const totalIssues = openIssues.length + closedIssues.length; + + return ( +
+ {/* Issues List */} +
+ {/* Header */} +
+
+
+ +
+
+

Issues

+

+ {totalIssues === 0 + ? 'No issues found' + : `${openIssues.length} open, ${closedIssues.length} closed`} +

+
+
+ +
+ + {/* Issues List */} +
+ {totalIssues === 0 ? ( +
+
+ +
+

No Issues

+

This repository has no issues yet.

+
+ ) : ( +
+ {/* Open Issues */} + {openIssues.map((issue) => ( + setSelectedIssue(issue)} + onOpenExternal={() => handleOpenInGitHub(issue.url)} + formatDate={formatDate} + /> + ))} + + {/* Closed Issues Section */} + {closedIssues.length > 0 && ( + <> +
+ Closed Issues ({closedIssues.length}) +
+ {closedIssues.map((issue) => ( + setSelectedIssue(issue)} + onOpenExternal={() => handleOpenInGitHub(issue.url)} + formatDate={formatDate} + /> + ))} + + )} +
+ )} +
+
+ + {/* Issue Detail Panel */} + {selectedIssue && ( +
+ {/* Detail Header */} +
+
+ {selectedIssue.state === 'OPEN' ? ( + + ) : ( + + )} + + #{selectedIssue.number} {selectedIssue.title} + +
+
+ + +
+
+ + {/* Issue Detail Content */} +
+ {/* Title */} +

{selectedIssue.title}

+ + {/* Meta info */} +
+ + {selectedIssue.state === 'OPEN' ? 'Open' : 'Closed'} + + + #{selectedIssue.number} opened {formatDate(selectedIssue.createdAt)} by{' '} + {selectedIssue.author.login} + +
+ + {/* Labels */} + {selectedIssue.labels.length > 0 && ( +
+ {selectedIssue.labels.map((label) => ( + + {label.name} + + ))} +
+ )} + + {/* Body */} + {selectedIssue.body ? ( +
+
{selectedIssue.body}
+
+ ) : ( +

No description provided.

+ )} + + {/* Open in GitHub CTA */} +
+

+ View comments, add reactions, and more on GitHub. +

+ +
+
+
+ )} +
+ ); +} + +interface IssueRowProps { + issue: GitHubIssue; + isSelected: boolean; + onClick: () => void; + onOpenExternal: () => void; + formatDate: (date: string) => string; +} + +function IssueRow({ issue, isSelected, onClick, onOpenExternal, formatDate }: IssueRowProps) { + return ( +
+ {issue.state === 'OPEN' ? ( + + ) : ( + + )} + +
+
+ {issue.title} +
+ +
+ + #{issue.number} opened {formatDate(issue.createdAt)} by {issue.author.login} + +
+ + {issue.labels.length > 0 && ( +
+ {issue.labels.map((label) => ( + + {label.name} + + ))} +
+ )} +
+ + +
+ ); +} diff --git a/apps/ui/src/components/views/github-prs-view.tsx b/apps/ui/src/components/views/github-prs-view.tsx new file mode 100644 index 00000000..4c613653 --- /dev/null +++ b/apps/ui/src/components/views/github-prs-view.tsx @@ -0,0 +1,421 @@ +import { useState, useEffect, useCallback } from 'react'; +import { + GitPullRequest, + Loader2, + RefreshCw, + ExternalLink, + GitMerge, + Circle, + X, + AlertCircle, +} from 'lucide-react'; +import { getElectronAPI, GitHubPR } from '@/lib/electron'; +import { useAppStore } from '@/store/app-store'; +import { Button } from '@/components/ui/button'; +import { cn } from '@/lib/utils'; + +export function GitHubPRsView() { + const [openPRs, setOpenPRs] = useState([]); + const [mergedPRs, setMergedPRs] = useState([]); + const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + const [error, setError] = useState(null); + const [selectedPR, setSelectedPR] = useState(null); + const { currentProject } = useAppStore(); + + const fetchPRs = useCallback(async () => { + if (!currentProject?.path) { + setError('No project selected'); + setLoading(false); + return; + } + + try { + setError(null); + const api = getElectronAPI(); + if (api.github) { + const result = await api.github.listPRs(currentProject.path); + if (result.success) { + setOpenPRs(result.openPRs || []); + setMergedPRs(result.mergedPRs || []); + } else { + setError(result.error || 'Failed to fetch pull requests'); + } + } + } catch (err) { + console.error('[GitHubPRsView] Error fetching PRs:', err); + setError(err instanceof Error ? err.message : 'Failed to fetch pull requests'); + } finally { + setLoading(false); + setRefreshing(false); + } + }, [currentProject?.path]); + + useEffect(() => { + fetchPRs(); + }, [fetchPRs]); + + const handleRefresh = useCallback(() => { + setRefreshing(true); + fetchPRs(); + }, [fetchPRs]); + + const handleOpenInGitHub = useCallback((url: string) => { + const api = getElectronAPI(); + api.openExternalLink(url); + }, []); + + const formatDate = (dateString: string) => { + const date = new Date(dateString); + return date.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + }); + }; + + const getReviewStatus = (pr: GitHubPR) => { + if (pr.isDraft) return { label: 'Draft', color: 'text-muted-foreground', bg: 'bg-muted' }; + switch (pr.reviewDecision) { + case 'APPROVED': + return { label: 'Approved', color: 'text-green-500', bg: 'bg-green-500/10' }; + case 'CHANGES_REQUESTED': + return { label: 'Changes requested', color: 'text-orange-500', bg: 'bg-orange-500/10' }; + case 'REVIEW_REQUIRED': + return { label: 'Review required', color: 'text-yellow-500', bg: 'bg-yellow-500/10' }; + default: + return null; + } + }; + + if (loading) { + return ( +
+ +
+ ); + } + + if (error) { + return ( +
+
+ +
+

Failed to Load Pull Requests

+

{error}

+ +
+ ); + } + + const totalPRs = openPRs.length + mergedPRs.length; + + return ( +
+ {/* PR List */} +
+ {/* Header */} +
+
+
+ +
+
+

Pull Requests

+

+ {totalPRs === 0 + ? 'No pull requests found' + : `${openPRs.length} open, ${mergedPRs.length} merged`} +

+
+
+ +
+ + {/* PR List */} +
+ {totalPRs === 0 ? ( +
+
+ +
+

No Pull Requests

+

+ This repository has no pull requests yet. +

+
+ ) : ( +
+ {/* Open PRs */} + {openPRs.map((pr) => ( + setSelectedPR(pr)} + onOpenExternal={() => handleOpenInGitHub(pr.url)} + formatDate={formatDate} + getReviewStatus={getReviewStatus} + /> + ))} + + {/* Merged PRs Section */} + {mergedPRs.length > 0 && ( + <> +
+ Merged ({mergedPRs.length}) +
+ {mergedPRs.map((pr) => ( + setSelectedPR(pr)} + onOpenExternal={() => handleOpenInGitHub(pr.url)} + formatDate={formatDate} + getReviewStatus={getReviewStatus} + /> + ))} + + )} +
+ )} +
+
+ + {/* PR Detail Panel */} + {selectedPR && ( +
+ {/* Detail Header */} +
+
+ {selectedPR.state === 'MERGED' ? ( + + ) : ( + + )} + + #{selectedPR.number} {selectedPR.title} + + {selectedPR.isDraft && ( + + Draft + + )} +
+
+ + +
+
+ + {/* PR Detail Content */} +
+ {/* Title */} +

{selectedPR.title}

+ + {/* Meta info */} +
+ + {selectedPR.state === 'MERGED' ? 'Merged' : selectedPR.isDraft ? 'Draft' : 'Open'} + + {getReviewStatus(selectedPR) && ( + + {getReviewStatus(selectedPR)!.label} + + )} + + #{selectedPR.number} opened {formatDate(selectedPR.createdAt)} by{' '} + {selectedPR.author.login} + +
+ + {/* Branch info */} + {selectedPR.headRefName && ( +
+ Branch: + + {selectedPR.headRefName} + +
+ )} + + {/* Labels */} + {selectedPR.labels.length > 0 && ( +
+ {selectedPR.labels.map((label) => ( + + {label.name} + + ))} +
+ )} + + {/* Body */} + {selectedPR.body ? ( +
+
{selectedPR.body}
+
+ ) : ( +

No description provided.

+ )} + + {/* Open in GitHub CTA */} +
+

+ View code changes, comments, and reviews on GitHub. +

+ +
+
+
+ )} +
+ ); +} + +interface PRRowProps { + pr: GitHubPR; + isSelected: boolean; + onClick: () => void; + onOpenExternal: () => void; + formatDate: (date: string) => string; + getReviewStatus: (pr: GitHubPR) => { label: string; color: string; bg: string } | null; +} + +function PRRow({ + pr, + isSelected, + onClick, + onOpenExternal, + formatDate, + getReviewStatus, +}: PRRowProps) { + const reviewStatus = getReviewStatus(pr); + + return ( +
+ {pr.state === 'MERGED' ? ( + + ) : ( + + )} + +
+
+ {pr.title} + {pr.isDraft && ( + + Draft + + )} +
+ +
+ + #{pr.number} opened {formatDate(pr.createdAt)} by {pr.author.login} + + {pr.headRefName && ( + + {pr.headRefName} + + )} +
+ +
+ {/* Review Status */} + {reviewStatus && ( + + {reviewStatus.label} + + )} + + {/* Labels */} + {pr.labels.map((label) => ( + + {label.name} + + ))} +
+
+ + +
+ ); +} diff --git a/apps/ui/src/components/views/interview-view.tsx b/apps/ui/src/components/views/interview-view.tsx index 47127e10..71d0fa4d 100644 --- a/apps/ui/src/components/views/interview-view.tsx +++ b/apps/ui/src/components/views/interview-view.tsx @@ -345,11 +345,6 @@ export function InterviewView() { category: 'Core', description: 'Initial project setup', status: 'backlog' as const, - steps: [ - 'Step 1: Review app_spec.txt', - 'Step 2: Set up development environment', - 'Step 3: Start implementing features', - ], skipTests: true, }; diff --git a/apps/ui/src/lib/electron.ts b/apps/ui/src/lib/electron.ts index 0cb4c4db..87bb61cd 100644 --- a/apps/ui/src/lib/electron.ts +++ b/apps/ui/src/lib/electron.ts @@ -92,12 +92,77 @@ export interface RunningAgentsAPI { getAll: () => Promise; } +// GitHub types +export interface GitHubLabel { + name: string; + color: string; +} + +export interface GitHubAuthor { + login: string; +} + +export interface GitHubIssue { + number: number; + title: string; + state: string; + author: GitHubAuthor; + createdAt: string; + labels: GitHubLabel[]; + url: string; + body: string; +} + +export interface GitHubPR { + number: number; + title: string; + state: string; + author: GitHubAuthor; + createdAt: string; + labels: GitHubLabel[]; + url: string; + isDraft: boolean; + headRefName: string; + reviewDecision: string | null; + mergeable: string; + body: string; +} + +export interface GitHubRemoteStatus { + hasGitHubRemote: boolean; + remoteUrl: string | null; + owner: string | null; + repo: string | null; +} + +export interface GitHubAPI { + checkRemote: (projectPath: string) => Promise<{ + success: boolean; + hasGitHubRemote?: boolean; + remoteUrl?: string | null; + owner?: string | null; + repo?: string | null; + error?: string; + }>; + listIssues: (projectPath: string) => Promise<{ + success: boolean; + openIssues?: GitHubIssue[]; + closedIssues?: GitHubIssue[]; + error?: string; + }>; + listPRs: (projectPath: string) => Promise<{ + success: boolean; + openPRs?: GitHubPR[]; + mergedPRs?: GitHubPR[]; + error?: string; + }>; +} + // Feature Suggestions types export interface FeatureSuggestion { id: string; category: string; description: string; - steps: string[]; priority: number; reasoning: string; } @@ -326,6 +391,7 @@ export interface ElectronAPI { autoMode?: AutoModeAPI; features?: FeaturesAPI; runningAgents?: RunningAgentsAPI; + github?: GitHubAPI; enhancePrompt?: { enhance: ( originalText: string, @@ -861,6 +927,9 @@ const getMockElectronAPI = (): ElectronAPI => { // Mock Running Agents API runningAgents: createMockRunningAgentsAPI(), + // Mock GitHub API + github: createMockGitHubAPI(), + // Mock Claude API claude: { getUsage: async () => { @@ -1963,12 +2032,6 @@ async function simulateSuggestionsGeneration(suggestionType: SuggestionType = 'f id: `suggestion-${Date.now()}-0`, category: 'Code Smell', description: 'Extract duplicate validation logic into reusable utility', - steps: [ - 'Identify all files with similar validation patterns', - 'Create a validation utilities module', - 'Replace duplicate code with utility calls', - 'Add unit tests for the new utilities', - ], priority: 1, reasoning: 'Reduces code duplication and improves maintainability', }, @@ -1976,12 +2039,6 @@ async function simulateSuggestionsGeneration(suggestionType: SuggestionType = 'f id: `suggestion-${Date.now()}-1`, category: 'Complexity', description: 'Break down large handleSubmit function into smaller functions', - steps: [ - 'Identify the handleSubmit function in form components', - 'Extract validation logic into separate function', - 'Extract API call logic into separate function', - 'Extract success/error handling into separate functions', - ], priority: 2, reasoning: 'Function is too long and handles multiple responsibilities', }, @@ -1989,12 +2046,6 @@ async function simulateSuggestionsGeneration(suggestionType: SuggestionType = 'f id: `suggestion-${Date.now()}-2`, category: 'Architecture', description: 'Move business logic out of React components into hooks', - steps: [ - 'Identify business logic in component files', - 'Create custom hooks for reusable logic', - 'Update components to use the new hooks', - 'Add tests for the extracted hooks', - ], priority: 3, reasoning: 'Improves separation of concerns and testability', }, @@ -2007,12 +2058,6 @@ async function simulateSuggestionsGeneration(suggestionType: SuggestionType = 'f id: `suggestion-${Date.now()}-0`, category: 'High', description: 'Sanitize user input before rendering to prevent XSS', - steps: [ - 'Audit all places where user input is rendered', - 'Implement input sanitization using DOMPurify', - 'Add Content-Security-Policy headers', - 'Test with common XSS payloads', - ], priority: 1, reasoning: 'User input is rendered without proper sanitization', }, @@ -2020,12 +2065,6 @@ async function simulateSuggestionsGeneration(suggestionType: SuggestionType = 'f id: `suggestion-${Date.now()}-1`, category: 'Medium', description: 'Add rate limiting to authentication endpoints', - steps: [ - 'Implement rate limiting middleware', - 'Configure limits for login attempts', - 'Add account lockout after failed attempts', - 'Log suspicious activity', - ], priority: 2, reasoning: 'Prevents brute force attacks on authentication', }, @@ -2033,12 +2072,6 @@ async function simulateSuggestionsGeneration(suggestionType: SuggestionType = 'f id: `suggestion-${Date.now()}-2`, category: 'Low', description: 'Remove sensitive information from error messages', - steps: [ - 'Audit error handling in API routes', - 'Create generic error messages for production', - 'Log detailed errors server-side only', - 'Implement proper error boundaries', - ], priority: 3, reasoning: 'Error messages may leak implementation details', }, @@ -2051,12 +2084,6 @@ async function simulateSuggestionsGeneration(suggestionType: SuggestionType = 'f id: `suggestion-${Date.now()}-0`, category: 'Rendering', description: 'Add React.memo to prevent unnecessary re-renders', - steps: [ - 'Profile component renders with React DevTools', - 'Identify components that re-render unnecessarily', - 'Wrap pure components with React.memo', - 'Use useCallback for event handlers passed as props', - ], priority: 1, reasoning: "Components re-render even when props haven't changed", }, @@ -2064,12 +2091,6 @@ async function simulateSuggestionsGeneration(suggestionType: SuggestionType = 'f id: `suggestion-${Date.now()}-1`, category: 'Bundle Size', description: 'Implement code splitting for route components', - steps: [ - 'Use React.lazy for route components', - 'Add Suspense boundaries with loading states', - 'Analyze bundle with webpack-bundle-analyzer', - 'Consider dynamic imports for heavy libraries', - ], priority: 2, reasoning: 'Initial bundle is larger than necessary', }, @@ -2077,12 +2098,6 @@ async function simulateSuggestionsGeneration(suggestionType: SuggestionType = 'f id: `suggestion-${Date.now()}-2`, category: 'Caching', description: 'Add memoization for expensive computations', - steps: [ - 'Identify expensive calculations in render', - 'Use useMemo for derived data', - 'Consider using react-query for server state', - 'Add caching headers for static assets', - ], priority: 3, reasoning: 'Expensive computations run on every render', }, @@ -2095,12 +2110,6 @@ async function simulateSuggestionsGeneration(suggestionType: SuggestionType = 'f id: `suggestion-${Date.now()}-0`, category: 'User Experience', description: 'Add dark mode toggle with system preference detection', - steps: [ - 'Create a ThemeProvider context to manage theme state', - 'Add a toggle component in the settings or header', - 'Implement CSS variables for theme colors', - 'Add localStorage persistence for user preference', - ], priority: 1, reasoning: 'Dark mode is a standard feature that improves accessibility and user comfort', }, @@ -2108,11 +2117,6 @@ async function simulateSuggestionsGeneration(suggestionType: SuggestionType = 'f id: `suggestion-${Date.now()}-1`, category: 'Performance', description: 'Implement lazy loading for heavy components', - steps: [ - 'Identify components that are heavy or rarely used', - 'Use React.lazy() and Suspense for code splitting', - 'Add loading states for lazy-loaded components', - ], priority: 2, reasoning: 'Improves initial load time and reduces bundle size', }, @@ -2120,12 +2124,6 @@ async function simulateSuggestionsGeneration(suggestionType: SuggestionType = 'f id: `suggestion-${Date.now()}-2`, category: 'Accessibility', description: 'Add keyboard navigation support throughout the app', - steps: [ - 'Implement focus management for modals and dialogs', - 'Add keyboard shortcuts for common actions', - 'Ensure all interactive elements are focusable', - 'Add ARIA labels and roles where needed', - ], priority: 3, reasoning: 'Improves accessibility for users who rely on keyboard navigation', }, @@ -2592,6 +2590,38 @@ function createMockRunningAgentsAPI(): RunningAgentsAPI { }; } +// Mock GitHub API implementation +function createMockGitHubAPI(): GitHubAPI { + return { + checkRemote: async (projectPath: string) => { + console.log('[Mock] Checking GitHub remote for:', projectPath); + return { + success: true, + hasGitHubRemote: false, + remoteUrl: null, + owner: null, + repo: null, + }; + }, + listIssues: async (projectPath: string) => { + console.log('[Mock] Listing GitHub issues for:', projectPath); + return { + success: true, + openIssues: [], + closedIssues: [], + }; + }, + listPRs: async (projectPath: string) => { + console.log('[Mock] Listing GitHub PRs for:', projectPath); + return { + success: true, + openPRs: [], + mergedPRs: [], + }; + }, + }; +} + // Utility functions for project management export interface Project { diff --git a/apps/ui/src/lib/http-api-client.ts b/apps/ui/src/lib/http-api-client.ts index cdabf0e3..18e77fe4 100644 --- a/apps/ui/src/lib/http-api-client.ts +++ b/apps/ui/src/lib/http-api-client.ts @@ -21,6 +21,9 @@ import type { SuggestionsEvent, SpecRegenerationEvent, SuggestionType, + GitHubAPI, + GitHubIssue, + GitHubPR, } from './electron'; import type { Message, SessionListItem } from '@/types/electron'; import type { Feature, ClaudeUsageResponse } from '@/store/app-store'; @@ -743,6 +746,13 @@ export class HttpApiClient implements ElectronAPI { }> => this.get('/api/running-agents'), }; + // GitHub API + github: GitHubAPI = { + checkRemote: (projectPath: string) => this.post('/api/github/check-remote', { projectPath }), + listIssues: (projectPath: string) => this.post('/api/github/issues', { projectPath }), + listPRs: (projectPath: string) => this.post('/api/github/prs', { projectPath }), + }; + // Workspace API workspace = { getConfig: (): Promise<{ diff --git a/apps/ui/src/routes/github-issues.tsx b/apps/ui/src/routes/github-issues.tsx new file mode 100644 index 00000000..16506e47 --- /dev/null +++ b/apps/ui/src/routes/github-issues.tsx @@ -0,0 +1,6 @@ +import { createFileRoute } from '@tanstack/react-router'; +import { GitHubIssuesView } from '@/components/views/github-issues-view'; + +export const Route = createFileRoute('/github-issues')({ + component: GitHubIssuesView, +}); diff --git a/apps/ui/src/routes/github-prs.tsx b/apps/ui/src/routes/github-prs.tsx new file mode 100644 index 00000000..78e321a6 --- /dev/null +++ b/apps/ui/src/routes/github-prs.tsx @@ -0,0 +1,6 @@ +import { createFileRoute } from '@tanstack/react-router'; +import { GitHubPRsView } from '@/components/views/github-prs-view'; + +export const Route = createFileRoute('/github-prs')({ + component: GitHubPRsView, +}); diff --git a/apps/ui/src/store/app-store.ts b/apps/ui/src/store/app-store.ts index 8927617e..394160b7 100644 --- a/apps/ui/src/store/app-store.ts +++ b/apps/ui/src/store/app-store.ts @@ -266,7 +266,6 @@ export interface Feature { titleGenerating?: boolean; category: string; description: string; - steps: string[]; status: 'backlog' | 'in_progress' | 'waiting_approval' | 'verified' | 'completed'; images?: FeatureImage[]; imagePaths?: FeatureImagePath[]; // Paths to temp files for agent context diff --git a/libs/types/src/feature.ts b/libs/types/src/feature.ts index a4946336..6aa2473b 100644 --- a/libs/types/src/feature.ts +++ b/libs/types/src/feature.ts @@ -18,7 +18,6 @@ export interface Feature { titleGenerating?: boolean; category: string; description: string; - steps?: string[]; passes?: boolean; priority?: number; status?: string;