fixing auto verify for kanban issues

This commit is contained in:
Test User
2025-12-22 12:10:54 -05:00
parent 9702f142c4
commit 9586589453
30 changed files with 1376 additions and 306 deletions

30
.github/workflows/security-audit.yml vendored Normal file
View File

@@ -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

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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<GitHubRemoteStatus> {
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<void> => {
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) });
}
};
}

View File

@@ -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);
}

View File

@@ -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<void> => {
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) });
}
};
}

View File

@@ -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<void> => {
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) });
}
};
}

View File

@@ -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',
},

View File

@@ -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,6 +1658,9 @@ You can use the Read tool to view these images at any time during implementation
`;
}
// Add verification instructions based on testing mode
if (feature.skipTests) {
// Manual verification - just implement the feature
prompt += `
## Instructions
@@ -1659,8 +1668,7 @@ 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 <summary> tags like this:
@@ -1678,6 +1686,56 @@ When done, wrap your final summary in <summary> tags like this:
</summary>
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 <summary> tags like this:
<summary>
## 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]
</summary>
This helps parse your summary correctly in the output logs.`;
}
return prompt;
}

View File

@@ -51,7 +51,9 @@ export function SidebarNavigation({
return (
<button
key={item.id}
onClick={() => navigate({ to: `/${item.id}` as const })}
onClick={() => {
navigate({ to: `/${item.id}` as const });
}}
className={cn(
'group flex items-center w-full px-3 py-2.5 rounded-xl relative overflow-hidden titlebar-no-drag',
'transition-all duration-200 ease-out',

View File

@@ -1,9 +1,20 @@
import { useMemo } from 'react';
import { useMemo, useState, useEffect } from 'react';
import type { NavigateOptions } from '@tanstack/react-router';
import { FileText, LayoutGrid, Bot, BookOpen, UserCircle, Terminal } from 'lucide-react';
import {
FileText,
LayoutGrid,
Bot,
BookOpen,
UserCircle,
Terminal,
CircleDot,
GitPullRequest,
Zap,
} from 'lucide-react';
import type { NavSection, NavItem } from '../types';
import type { KeyboardShortcut } from '@/hooks/use-keyboard-shortcuts';
import type { Project } from '@/lib/electron';
import { getElectronAPI } from '@/lib/electron';
interface UseNavigationProps {
shortcuts: {
@@ -51,6 +62,30 @@ export function useNavigation({
cyclePrevProject,
cycleNextProject,
}: UseNavigationProps) {
// Track if current project has a GitHub remote
const [hasGitHubRemote, setHasGitHubRemote] = useState(false);
useEffect(() => {
async function checkGitHubRemote() {
if (!currentProject?.path) {
setHasGitHubRemote(false);
return;
}
try {
const api = getElectronAPI();
if (api.github) {
const result = await api.github.checkRemote(currentProject.path);
setHasGitHubRemote(result.success && result.hasGitHubRemote === true);
}
} catch {
setHasGitHubRemote(false);
}
}
checkGitHubRemote();
}, [currentProject?.path]);
// Build navigation sections
const navSections: NavSection[] = useMemo(() => {
const allToolsItems: NavItem[] = [
@@ -114,7 +149,7 @@ export function useNavigation({
});
}
return [
const sections: NavSection[] = [
{
label: 'Project',
items: projectItems,
@@ -124,7 +159,28 @@ export function useNavigation({
items: visibleToolsItems,
},
];
}, [shortcuts, hideSpecEditor, hideContext, hideTerminal, hideAiProfiles]);
// Add GitHub section if project has a GitHub remote
if (hasGitHubRemote) {
sections.push({
label: 'GitHub',
items: [
{
id: 'github-issues',
label: 'Issues',
icon: CircleDot,
},
{
id: 'github-prs',
label: 'Pull Requests',
icon: GitPullRequest,
},
],
});
}
return sections;
}, [shortcuts, hideSpecEditor, hideContext, hideTerminal, hideAiProfiles, hasGitHubRemote]);
// Build keyboard shortcuts for navigation
const navigationShortcuts: KeyboardShortcut[] = useMemo(() => {

View File

@@ -40,7 +40,7 @@ export function useProjectCreation({
// Write initial app_spec.txt with proper XML structure
// Note: Must follow XML format as defined in apps/server/src/lib/app-spec-format.ts
const api = getElectronAPI();
await api.fs.writeFile(
await api.writeFile(
`${projectPath}/.automaker/app_spec.txt`,
`<project_specification>
<project_name>${projectName}</project_name>
@@ -103,7 +103,7 @@ export function useProjectCreation({
const projectPath = `${parentDir}/${projectName}`;
// Create project directory
await api.fs.createFolder(projectPath);
await api.mkdir(projectPath);
// Finalize project setup
await finalizeProjectCreation(projectPath, projectName);
@@ -127,16 +127,19 @@ export function useProjectCreation({
setIsCreatingProject(true);
try {
const api = getElectronAPI();
const projectPath = `${parentDir}/${projectName}`;
// Clone template repository
await api.git.clone(template.githubUrl, projectPath);
const cloneResult = await api.templates.clone(template.repoUrl, projectName, parentDir);
if (!cloneResult.success) {
throw new Error(cloneResult.error || 'Failed to clone template');
}
const projectPath = cloneResult.projectPath!;
// Initialize .automaker directory structure
await initializeProject(projectPath);
// Write app_spec.txt with template-specific info
await api.fs.writeFile(
await api.writeFile(
`${projectPath}/.automaker/app_spec.txt`,
`<project_specification>
<project_name>${projectName}</project_name>
@@ -196,16 +199,19 @@ export function useProjectCreation({
setIsCreatingProject(true);
try {
const api = getElectronAPI();
const projectPath = `${parentDir}/${projectName}`;
// Clone custom repository
await api.git.clone(repoUrl, projectPath);
const cloneResult = await api.templates.clone(repoUrl, projectName, parentDir);
if (!cloneResult.success) {
throw new Error(cloneResult.error || 'Failed to clone repository');
}
const projectPath = cloneResult.projectPath!;
// Initialize .automaker directory structure
await initializeProject(projectPath);
// Write app_spec.txt with custom URL info
await api.fs.writeFile(
await api.writeFile(
`${projectPath}/.automaker/app_spec.txt`,
`<project_specification>
<project_name>${projectName}</project_name>

View File

@@ -426,7 +426,6 @@ ${Object.entries(projectAnalysis.filesByExtension)
interface DetectedFeature {
category: string;
description: string;
steps: string[];
passes: boolean;
}
@@ -453,11 +452,6 @@ ${Object.entries(projectAnalysis.filesByExtension)
detectedFeatures.push({
category: 'Testing',
description: 'Automated test suite',
steps: [
'Step 1: Tests directory exists',
'Step 2: Test files are present',
'Step 3: Run test suite',
],
passes: true,
});
}
@@ -471,11 +465,6 @@ ${Object.entries(projectAnalysis.filesByExtension)
detectedFeatures.push({
category: 'UI/Design',
description: 'Component-based UI architecture',
steps: [
'Step 1: Components directory exists',
'Step 2: UI components are defined',
'Step 3: Components are reusable',
],
passes: true,
});
}
@@ -485,11 +474,6 @@ ${Object.entries(projectAnalysis.filesByExtension)
detectedFeatures.push({
category: 'Project Structure',
description: 'Organized source code structure',
steps: [
'Step 1: Source directory exists',
'Step 2: Code is properly organized',
'Step 3: Follows best practices',
],
passes: true,
});
}
@@ -504,11 +488,6 @@ ${Object.entries(projectAnalysis.filesByExtension)
detectedFeatures.push({
category: 'Frontend',
description: 'React-based user interface',
steps: [
'Step 1: React is installed',
'Step 2: Components render correctly',
'Step 3: State management works',
],
passes: true,
});
}
@@ -517,11 +496,6 @@ ${Object.entries(projectAnalysis.filesByExtension)
detectedFeatures.push({
category: 'Framework',
description: 'Next.js framework integration',
steps: [
'Step 1: Next.js is configured',
'Step 2: Pages/routes are defined',
'Step 3: Server-side rendering works',
],
passes: true,
});
}
@@ -536,11 +510,6 @@ ${Object.entries(projectAnalysis.filesByExtension)
detectedFeatures.push({
category: 'Developer Experience',
description: 'TypeScript type safety',
steps: [
'Step 1: TypeScript is configured',
'Step 2: Type definitions exist',
'Step 3: Code compiles without errors',
],
passes: true,
});
}
@@ -550,11 +519,6 @@ ${Object.entries(projectAnalysis.filesByExtension)
detectedFeatures.push({
category: 'UI/Design',
description: 'Tailwind CSS styling',
steps: [
'Step 1: Tailwind is configured',
'Step 2: Styles are applied',
'Step 3: Responsive design works',
],
passes: true,
});
}
@@ -564,11 +528,6 @@ ${Object.entries(projectAnalysis.filesByExtension)
detectedFeatures.push({
category: 'Developer Experience',
description: 'Code quality tools',
steps: [
'Step 1: Linter is configured',
'Step 2: Code passes lint checks',
'Step 3: Formatting is consistent',
],
passes: true,
});
}
@@ -578,11 +537,6 @@ ${Object.entries(projectAnalysis.filesByExtension)
detectedFeatures.push({
category: 'Platform',
description: 'Electron desktop application',
steps: [
'Step 1: Electron is configured',
'Step 2: Main process runs',
'Step 3: Renderer process loads',
],
passes: true,
});
}
@@ -592,11 +546,6 @@ ${Object.entries(projectAnalysis.filesByExtension)
detectedFeatures.push({
category: 'Testing',
description: 'Playwright end-to-end testing',
steps: [
'Step 1: Playwright is configured',
'Step 2: E2E tests are defined',
'Step 3: Tests pass successfully',
],
passes: true,
});
}
@@ -610,11 +559,6 @@ ${Object.entries(projectAnalysis.filesByExtension)
detectedFeatures.push({
category: 'Documentation',
description: 'Project documentation',
steps: [
'Step 1: README exists',
'Step 2: Documentation is comprehensive',
'Step 3: Setup instructions are clear',
],
passes: true,
});
}
@@ -629,11 +573,6 @@ ${Object.entries(projectAnalysis.filesByExtension)
detectedFeatures.push({
category: 'DevOps',
description: 'CI/CD pipeline configuration',
steps: [
'Step 1: CI config exists',
'Step 2: Pipeline runs on push',
'Step 3: Automated checks pass',
],
passes: true,
});
}
@@ -647,11 +586,6 @@ ${Object.entries(projectAnalysis.filesByExtension)
detectedFeatures.push({
category: 'Backend',
description: 'API endpoints',
steps: [
'Step 1: API routes are defined',
'Step 2: Endpoints respond correctly',
'Step 3: Error handling is implemented',
],
passes: true,
});
}
@@ -669,11 +603,6 @@ ${Object.entries(projectAnalysis.filesByExtension)
detectedFeatures.push({
category: 'Architecture',
description: 'State management system',
steps: [
'Step 1: Store is configured',
'Step 2: State updates correctly',
'Step 3: Components access state',
],
passes: true,
});
}
@@ -683,11 +612,6 @@ ${Object.entries(projectAnalysis.filesByExtension)
detectedFeatures.push({
category: 'Configuration',
description: 'Project configuration files',
steps: [
'Step 1: Config files exist',
'Step 2: Configuration is valid',
'Step 3: Build process works',
],
passes: true,
});
}
@@ -700,11 +624,6 @@ ${Object.entries(projectAnalysis.filesByExtension)
detectedFeatures.push({
category: 'Core',
description: 'Basic project structure',
steps: [
'Step 1: Project directory exists',
'Step 2: Files are present',
'Step 3: Project can be loaded',
],
passes: true,
});
}
@@ -719,7 +638,6 @@ ${Object.entries(projectAnalysis.filesByExtension)
id: crypto.randomUUID(),
category: detectedFeature.category,
description: detectedFeature.description,
steps: detectedFeature.steps,
status: 'backlog',
});
}

View File

@@ -433,9 +433,9 @@ export function BoardView() {
// Create the feature
const featureData = {
title: `Address PR #${prNumber} Review Comments`,
category: 'PR Review',
description,
steps: [],
images: [],
imagePaths: [],
skipTests: defaultSkipTests,
@@ -475,9 +475,9 @@ export function BoardView() {
// Create the feature
const featureData = {
title: `Resolve Merge Conflicts`,
category: 'Maintenance',
description,
steps: [],
images: [],
imagePaths: [],
skipTests: defaultSkipTests,

View File

@@ -1,17 +1,12 @@
import { Feature } from '@/store/app-store';
import { GitBranch, GitPullRequest, ExternalLink, CheckCircle2, Circle } from 'lucide-react';
import { GitBranch, GitPullRequest, ExternalLink } from 'lucide-react';
interface CardContentSectionsProps {
feature: Feature;
useWorktrees: boolean;
showSteps: boolean;
}
export function CardContentSections({
feature,
useWorktrees,
showSteps,
}: CardContentSectionsProps) {
export function CardContentSections({ feature, useWorktrees }: CardContentSectionsProps) {
return (
<>
{/* Target Branch Display */}
@@ -50,30 +45,6 @@ export function CardContentSections({
</div>
);
})()}
{/* Steps Preview */}
{showSteps && feature.steps && feature.steps.length > 0 && (
<div className="mb-3 space-y-1.5">
{feature.steps.slice(0, 3).map((step, index) => (
<div
key={index}
className="flex items-start gap-2 text-[11px] text-muted-foreground/80"
>
{feature.status === 'verified' ? (
<CheckCircle2 className="w-3 h-3 mt-0.5 text-[var(--status-success)] shrink-0" />
) : (
<Circle className="w-3 h-3 mt-0.5 shrink-0 text-muted-foreground/50" />
)}
<span className="break-words hyphens-auto line-clamp-2 leading-relaxed">{step}</span>
</div>
))}
{feature.steps.length > 3 && (
<p className="text-[10px] text-muted-foreground/60 pl-5">
+{feature.steps.length - 3} more
</p>
)}
</div>
)}
</>
);
}

View File

@@ -61,9 +61,7 @@ export const KanbanCard = memo(function KanbanCard({
cardBorderEnabled = true,
cardBorderOpacity = 100,
}: KanbanCardProps) {
const { kanbanCardDetailLevel, useWorktrees } = useAppStore();
const showSteps = kanbanCardDetailLevel === 'standard' || kanbanCardDetailLevel === 'detailed';
const { useWorktrees } = useAppStore();
const isDraggable =
feature.status === 'backlog' ||
@@ -152,7 +150,7 @@ export const KanbanCard = memo(function KanbanCard({
<CardContent className="px-3 pt-0 pb-0">
{/* Content Sections */}
<CardContentSections feature={feature} useWorktrees={useWorktrees} showSteps={showSteps} />
<CardContentSections feature={feature} useWorktrees={useWorktrees} />
{/* Agent Info Panel */}
<AgentInfoPanel

View File

@@ -61,7 +61,6 @@ interface AddFeatureDialogProps {
title: string;
category: string;
description: string;
steps: string[];
images: FeatureImage[];
imagePaths: DescriptionImagePath[];
skipTests: boolean;
@@ -103,7 +102,6 @@ export function AddFeatureDialog({
title: '',
category: '',
description: '',
steps: [''],
images: [] as FeatureImage[],
imagePaths: [] as DescriptionImagePath[],
skipTests: false,
@@ -190,7 +188,6 @@ export function AddFeatureDialog({
title: newFeature.title,
category,
description: newFeature.description,
steps: newFeature.steps.filter((s) => s.trim()),
images: newFeature.images,
imagePaths: newFeature.imagePaths,
skipTests: newFeature.skipTests,
@@ -207,7 +204,6 @@ export function AddFeatureDialog({
title: '',
category: '',
description: '',
steps: [''],
images: [],
imagePaths: [],
skipTests: defaultSkipTests,
@@ -493,8 +489,6 @@ export function AddFeatureDialog({
<TestingTabContent
skipTests={newFeature.skipTests}
onSkipTestsChange={(skipTests) => setNewFeature({ ...newFeature, skipTests })}
steps={newFeature.steps}
onStepsChange={(steps) => setNewFeature({ ...newFeature, steps })}
/>
</TabsContent>
</Tabs>

View File

@@ -63,7 +63,6 @@ interface EditFeatureDialogProps {
title: string;
category: string;
description: string;
steps: string[];
skipTests: boolean;
model: AgentModel;
thinkingLevel: ThinkingLevel;
@@ -163,7 +162,6 @@ export function EditFeatureDialog({
title: editingFeature.title ?? '',
category: editingFeature.category,
description: editingFeature.description,
steps: editingFeature.steps,
skipTests: editingFeature.skipTests ?? false,
model: selectedModel,
thinkingLevel: normalizedThinking,
@@ -481,8 +479,6 @@ export function EditFeatureDialog({
<TestingTabContent
skipTests={editingFeature.skipTests ?? false}
onSkipTestsChange={(skipTests) => setEditingFeature({ ...editingFeature, skipTests })}
steps={editingFeature.steps}
onStepsChange={(steps) => setEditingFeature({ ...editingFeature, steps })}
testIdPrefix="edit"
/>
</TabsContent>

View File

@@ -245,7 +245,6 @@ export function FeatureSuggestionsDialog({
id: `feature-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
category: s.category,
description: s.description,
steps: s.steps,
status: 'backlog' as const,
skipTests: true, // As specified, testing mode true
priority: s.priority, // Preserve priority from suggestion
@@ -453,23 +452,9 @@ export function FeatureSuggestionsDialog({
{suggestion.description}
</Label>
{isExpanded && (
<div className="mt-3 space-y-2 text-sm">
{suggestion.reasoning && (
{isExpanded && suggestion.reasoning && (
<div className="mt-3 text-sm">
<p className="text-muted-foreground italic">{suggestion.reasoning}</p>
)}
{suggestion.steps.length > 0 && (
<div>
<p className="text-xs font-medium text-muted-foreground mb-1">
Implementation Steps:
</p>
<ul className="list-disc list-inside text-xs text-muted-foreground space-y-0.5">
{suggestion.steps.map((step, i) => (
<li key={i}>{step}</li>
))}
</ul>
</div>
)}
</div>
)}
</div>

View File

@@ -89,7 +89,6 @@ export function useBoardActions({
title: string;
category: string;
description: string;
steps: string[];
images: FeatureImage[];
imagePaths: DescriptionImagePath[];
skipTests: boolean;
@@ -208,7 +207,6 @@ export function useBoardActions({
title: string;
category: string;
description: string;
steps: string[];
skipTests: boolean;
model: AgentModel;
thinkingLevel: ThinkingLevel;

View File

@@ -1,36 +1,20 @@
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Checkbox } from '@/components/ui/checkbox';
import { FlaskConical, Plus } from 'lucide-react';
import { FlaskConical } from 'lucide-react';
interface TestingTabContentProps {
skipTests: boolean;
onSkipTestsChange: (skipTests: boolean) => void;
steps: string[];
onStepsChange: (steps: string[]) => void;
testIdPrefix?: string;
}
export function TestingTabContent({
skipTests,
onSkipTestsChange,
steps,
onStepsChange,
testIdPrefix = '',
}: TestingTabContentProps) {
const checkboxId = testIdPrefix ? `${testIdPrefix}-skip-tests` : 'skip-tests';
const handleStepChange = (index: number, value: string) => {
const newSteps = [...steps];
newSteps[index] = value;
onStepsChange(newSteps);
};
const handleAddStep = () => {
onStepsChange([...steps, '']);
};
return (
<div className="space-y-4">
<div className="flex items-center space-x-2">
@@ -48,37 +32,9 @@ export function TestingTabContent({
</div>
</div>
<p className="text-xs text-muted-foreground">
When enabled, this feature will use automated TDD. When disabled, it will require manual
verification.
When enabled, the agent will use Playwright to verify the feature works correctly before
marking it as verified. When disabled, manual verification will be required.
</p>
{/* Verification Steps - Only shown when skipTests is enabled */}
{skipTests && (
<div className="space-y-2 pt-2 border-t border-border">
<Label>Verification Steps</Label>
<p className="text-xs text-muted-foreground mb-2">
Add manual steps to verify this feature works correctly.
</p>
{steps.map((step, index) => (
<Input
key={index}
value={step}
placeholder={`Verification step ${index + 1}`}
onChange={(e) => handleStepChange(index, e.target.value)}
data-testid={`${testIdPrefix ? testIdPrefix + '-' : ''}feature-step-${index}${testIdPrefix ? '' : '-input'}`}
/>
))}
<Button
variant="outline"
size="sm"
onClick={handleAddStep}
data-testid={`${testIdPrefix ? testIdPrefix + '-' : ''}add-step-button`}
>
<Plus className="w-4 h-4 mr-2" />
Add Verification Step
</Button>
</div>
)}
</div>
);
}

View File

@@ -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<GitHubIssue[]>([]);
const [closedIssues, setClosedIssues] = useState<GitHubIssue[]>([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [error, setError] = useState<string | null>(null);
const [selectedIssue, setSelectedIssue] = useState<GitHubIssue | null>(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 (
<div className="flex-1 flex items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
);
}
if (error) {
return (
<div className="flex-1 flex flex-col items-center justify-center text-center p-6">
<div className="p-4 rounded-full bg-destructive/10 mb-4">
<CircleDot className="h-12 w-12 text-destructive" />
</div>
<h2 className="text-lg font-medium mb-2">Failed to Load Issues</h2>
<p className="text-muted-foreground max-w-md mb-4">{error}</p>
<Button variant="outline" onClick={handleRefresh}>
<RefreshCw className="h-4 w-4 mr-2" />
Try Again
</Button>
</div>
);
}
const totalIssues = openIssues.length + closedIssues.length;
return (
<div className="flex-1 flex overflow-hidden">
{/* Issues List */}
<div
className={cn(
'flex flex-col overflow-hidden border-r border-border',
selectedIssue ? 'w-80' : 'flex-1'
)}
>
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-border">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-green-500/10">
<CircleDot className="h-5 w-5 text-green-500" />
</div>
<div>
<h1 className="text-lg font-bold">Issues</h1>
<p className="text-xs text-muted-foreground">
{totalIssues === 0
? 'No issues found'
: `${openIssues.length} open, ${closedIssues.length} closed`}
</p>
</div>
</div>
<Button variant="outline" size="sm" onClick={handleRefresh} disabled={refreshing}>
<RefreshCw className={cn('h-4 w-4', refreshing && 'animate-spin')} />
</Button>
</div>
{/* Issues List */}
<div className="flex-1 overflow-auto">
{totalIssues === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-center p-6">
<div className="p-4 rounded-full bg-muted/50 mb-4">
<CircleDot className="h-8 w-8 text-muted-foreground" />
</div>
<h2 className="text-base font-medium mb-2">No Issues</h2>
<p className="text-sm text-muted-foreground">This repository has no issues yet.</p>
</div>
) : (
<div className="divide-y divide-border">
{/* Open Issues */}
{openIssues.map((issue) => (
<IssueRow
key={issue.number}
issue={issue}
isSelected={selectedIssue?.number === issue.number}
onClick={() => setSelectedIssue(issue)}
onOpenExternal={() => handleOpenInGitHub(issue.url)}
formatDate={formatDate}
/>
))}
{/* Closed Issues Section */}
{closedIssues.length > 0 && (
<>
<div className="px-4 py-2 bg-muted/30 text-xs font-medium text-muted-foreground">
Closed Issues ({closedIssues.length})
</div>
{closedIssues.map((issue) => (
<IssueRow
key={issue.number}
issue={issue}
isSelected={selectedIssue?.number === issue.number}
onClick={() => setSelectedIssue(issue)}
onOpenExternal={() => handleOpenInGitHub(issue.url)}
formatDate={formatDate}
/>
))}
</>
)}
</div>
)}
</div>
</div>
{/* Issue Detail Panel */}
{selectedIssue && (
<div className="flex-1 flex flex-col overflow-hidden">
{/* Detail Header */}
<div className="flex items-center justify-between p-3 border-b border-border bg-muted/30">
<div className="flex items-center gap-2 min-w-0">
{selectedIssue.state === 'OPEN' ? (
<Circle className="h-4 w-4 text-green-500 flex-shrink-0" />
) : (
<CheckCircle2 className="h-4 w-4 text-purple-500 flex-shrink-0" />
)}
<span className="text-sm font-medium truncate">
#{selectedIssue.number} {selectedIssue.title}
</span>
</div>
<div className="flex items-center gap-2 flex-shrink-0">
<Button
variant="outline"
size="sm"
onClick={() => handleOpenInGitHub(selectedIssue.url)}
>
<ExternalLink className="h-4 w-4 mr-1" />
Open in GitHub
</Button>
<Button variant="ghost" size="sm" onClick={() => setSelectedIssue(null)}>
<X className="h-4 w-4" />
</Button>
</div>
</div>
{/* Issue Detail Content */}
<div className="flex-1 overflow-auto p-6">
{/* Title */}
<h1 className="text-xl font-bold mb-2">{selectedIssue.title}</h1>
{/* Meta info */}
<div className="flex items-center gap-3 text-sm text-muted-foreground mb-4">
<span
className={cn(
'px-2 py-0.5 rounded-full text-xs font-medium',
selectedIssue.state === 'OPEN'
? 'bg-green-500/10 text-green-500'
: 'bg-purple-500/10 text-purple-500'
)}
>
{selectedIssue.state === 'OPEN' ? 'Open' : 'Closed'}
</span>
<span>
#{selectedIssue.number} opened {formatDate(selectedIssue.createdAt)} by{' '}
<span className="font-medium text-foreground">{selectedIssue.author.login}</span>
</span>
</div>
{/* Labels */}
{selectedIssue.labels.length > 0 && (
<div className="flex items-center gap-2 mb-6 flex-wrap">
{selectedIssue.labels.map((label) => (
<span
key={label.name}
className="px-2 py-0.5 text-xs font-medium rounded-full"
style={{
backgroundColor: `#${label.color}20`,
color: `#${label.color}`,
border: `1px solid #${label.color}40`,
}}
>
{label.name}
</span>
))}
</div>
)}
{/* Body */}
{selectedIssue.body ? (
<div className="prose prose-sm dark:prose-invert max-w-none">
<div className="whitespace-pre-wrap text-sm">{selectedIssue.body}</div>
</div>
) : (
<p className="text-sm text-muted-foreground italic">No description provided.</p>
)}
{/* Open in GitHub CTA */}
<div className="mt-8 p-4 rounded-lg bg-muted/50 border border-border">
<p className="text-sm text-muted-foreground mb-3">
View comments, add reactions, and more on GitHub.
</p>
<Button onClick={() => handleOpenInGitHub(selectedIssue.url)}>
<ExternalLink className="h-4 w-4 mr-2" />
View Full Issue on GitHub
</Button>
</div>
</div>
</div>
)}
</div>
);
}
interface IssueRowProps {
issue: GitHubIssue;
isSelected: boolean;
onClick: () => void;
onOpenExternal: () => void;
formatDate: (date: string) => string;
}
function IssueRow({ issue, isSelected, onClick, onOpenExternal, formatDate }: IssueRowProps) {
return (
<div
className={cn(
'flex items-start gap-3 p-3 cursor-pointer hover:bg-accent/50 transition-colors',
isSelected && 'bg-accent'
)}
onClick={onClick}
>
{issue.state === 'OPEN' ? (
<Circle className="h-4 w-4 text-green-500 mt-0.5 flex-shrink-0" />
) : (
<CheckCircle2 className="h-4 w-4 text-purple-500 mt-0.5 flex-shrink-0" />
)}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-sm font-medium truncate">{issue.title}</span>
</div>
<div className="flex items-center gap-2 mt-1 flex-wrap">
<span className="text-xs text-muted-foreground">
#{issue.number} opened {formatDate(issue.createdAt)} by {issue.author.login}
</span>
</div>
{issue.labels.length > 0 && (
<div className="flex items-center gap-1 mt-2 flex-wrap">
{issue.labels.map((label) => (
<span
key={label.name}
className="px-1.5 py-0.5 text-[10px] font-medium rounded-full"
style={{
backgroundColor: `#${label.color}20`,
color: `#${label.color}`,
border: `1px solid #${label.color}40`,
}}
>
{label.name}
</span>
))}
</div>
)}
</div>
<Button
variant="ghost"
size="sm"
className="flex-shrink-0 opacity-0 group-hover:opacity-100"
onClick={(e) => {
e.stopPropagation();
onOpenExternal();
}}
>
<ExternalLink className="h-3.5 w-3.5" />
</Button>
</div>
);
}

View File

@@ -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<GitHubPR[]>([]);
const [mergedPRs, setMergedPRs] = useState<GitHubPR[]>([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [error, setError] = useState<string | null>(null);
const [selectedPR, setSelectedPR] = useState<GitHubPR | null>(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 (
<div className="flex-1 flex items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
);
}
if (error) {
return (
<div className="flex-1 flex flex-col items-center justify-center text-center p-6">
<div className="p-4 rounded-full bg-destructive/10 mb-4">
<GitPullRequest className="h-12 w-12 text-destructive" />
</div>
<h2 className="text-lg font-medium mb-2">Failed to Load Pull Requests</h2>
<p className="text-muted-foreground max-w-md mb-4">{error}</p>
<Button variant="outline" onClick={handleRefresh}>
<RefreshCw className="h-4 w-4 mr-2" />
Try Again
</Button>
</div>
);
}
const totalPRs = openPRs.length + mergedPRs.length;
return (
<div className="flex-1 flex overflow-hidden">
{/* PR List */}
<div
className={cn(
'flex flex-col overflow-hidden border-r border-border',
selectedPR ? 'w-80' : 'flex-1'
)}
>
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-border">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-blue-500/10">
<GitPullRequest className="h-5 w-5 text-blue-500" />
</div>
<div>
<h1 className="text-lg font-bold">Pull Requests</h1>
<p className="text-xs text-muted-foreground">
{totalPRs === 0
? 'No pull requests found'
: `${openPRs.length} open, ${mergedPRs.length} merged`}
</p>
</div>
</div>
<Button variant="outline" size="sm" onClick={handleRefresh} disabled={refreshing}>
<RefreshCw className={cn('h-4 w-4', refreshing && 'animate-spin')} />
</Button>
</div>
{/* PR List */}
<div className="flex-1 overflow-auto">
{totalPRs === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-center p-6">
<div className="p-4 rounded-full bg-muted/50 mb-4">
<GitPullRequest className="h-8 w-8 text-muted-foreground" />
</div>
<h2 className="text-base font-medium mb-2">No Pull Requests</h2>
<p className="text-sm text-muted-foreground">
This repository has no pull requests yet.
</p>
</div>
) : (
<div className="divide-y divide-border">
{/* Open PRs */}
{openPRs.map((pr) => (
<PRRow
key={pr.number}
pr={pr}
isSelected={selectedPR?.number === pr.number}
onClick={() => setSelectedPR(pr)}
onOpenExternal={() => handleOpenInGitHub(pr.url)}
formatDate={formatDate}
getReviewStatus={getReviewStatus}
/>
))}
{/* Merged PRs Section */}
{mergedPRs.length > 0 && (
<>
<div className="px-4 py-2 bg-muted/30 text-xs font-medium text-muted-foreground">
Merged ({mergedPRs.length})
</div>
{mergedPRs.map((pr) => (
<PRRow
key={pr.number}
pr={pr}
isSelected={selectedPR?.number === pr.number}
onClick={() => setSelectedPR(pr)}
onOpenExternal={() => handleOpenInGitHub(pr.url)}
formatDate={formatDate}
getReviewStatus={getReviewStatus}
/>
))}
</>
)}
</div>
)}
</div>
</div>
{/* PR Detail Panel */}
{selectedPR && (
<div className="flex-1 flex flex-col overflow-hidden">
{/* Detail Header */}
<div className="flex items-center justify-between p-3 border-b border-border bg-muted/30">
<div className="flex items-center gap-2 min-w-0">
{selectedPR.state === 'MERGED' ? (
<GitMerge className="h-4 w-4 text-purple-500 flex-shrink-0" />
) : (
<GitPullRequest className="h-4 w-4 text-green-500 flex-shrink-0" />
)}
<span className="text-sm font-medium truncate">
#{selectedPR.number} {selectedPR.title}
</span>
{selectedPR.isDraft && (
<span className="px-1.5 py-0.5 text-[10px] font-medium rounded bg-muted text-muted-foreground">
Draft
</span>
)}
</div>
<div className="flex items-center gap-2 flex-shrink-0">
<Button
variant="outline"
size="sm"
onClick={() => handleOpenInGitHub(selectedPR.url)}
>
<ExternalLink className="h-4 w-4 mr-1" />
Open in GitHub
</Button>
<Button variant="ghost" size="sm" onClick={() => setSelectedPR(null)}>
<X className="h-4 w-4" />
</Button>
</div>
</div>
{/* PR Detail Content */}
<div className="flex-1 overflow-auto p-6">
{/* Title */}
<h1 className="text-xl font-bold mb-2">{selectedPR.title}</h1>
{/* Meta info */}
<div className="flex items-center gap-3 text-sm text-muted-foreground mb-4 flex-wrap">
<span
className={cn(
'px-2 py-0.5 rounded-full text-xs font-medium',
selectedPR.state === 'MERGED'
? 'bg-purple-500/10 text-purple-500'
: selectedPR.isDraft
? 'bg-muted text-muted-foreground'
: 'bg-green-500/10 text-green-500'
)}
>
{selectedPR.state === 'MERGED' ? 'Merged' : selectedPR.isDraft ? 'Draft' : 'Open'}
</span>
{getReviewStatus(selectedPR) && (
<span
className={cn(
'px-2 py-0.5 rounded-full text-xs font-medium',
getReviewStatus(selectedPR)!.bg,
getReviewStatus(selectedPR)!.color
)}
>
{getReviewStatus(selectedPR)!.label}
</span>
)}
<span>
#{selectedPR.number} opened {formatDate(selectedPR.createdAt)} by{' '}
<span className="font-medium text-foreground">{selectedPR.author.login}</span>
</span>
</div>
{/* Branch info */}
{selectedPR.headRefName && (
<div className="flex items-center gap-2 mb-4">
<span className="text-xs text-muted-foreground">Branch:</span>
<span className="text-xs font-mono bg-muted px-2 py-0.5 rounded">
{selectedPR.headRefName}
</span>
</div>
)}
{/* Labels */}
{selectedPR.labels.length > 0 && (
<div className="flex items-center gap-2 mb-6 flex-wrap">
{selectedPR.labels.map((label) => (
<span
key={label.name}
className="px-2 py-0.5 text-xs font-medium rounded-full"
style={{
backgroundColor: `#${label.color}20`,
color: `#${label.color}`,
border: `1px solid #${label.color}40`,
}}
>
{label.name}
</span>
))}
</div>
)}
{/* Body */}
{selectedPR.body ? (
<div className="prose prose-sm dark:prose-invert max-w-none">
<div className="whitespace-pre-wrap text-sm">{selectedPR.body}</div>
</div>
) : (
<p className="text-sm text-muted-foreground italic">No description provided.</p>
)}
{/* Open in GitHub CTA */}
<div className="mt-8 p-4 rounded-lg bg-muted/50 border border-border">
<p className="text-sm text-muted-foreground mb-3">
View code changes, comments, and reviews on GitHub.
</p>
<Button onClick={() => handleOpenInGitHub(selectedPR.url)}>
<ExternalLink className="h-4 w-4 mr-2" />
View Full PR on GitHub
</Button>
</div>
</div>
</div>
)}
</div>
);
}
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 (
<div
className={cn(
'flex items-start gap-3 p-3 cursor-pointer hover:bg-accent/50 transition-colors',
isSelected && 'bg-accent'
)}
onClick={onClick}
>
{pr.state === 'MERGED' ? (
<GitMerge className="h-4 w-4 text-purple-500 mt-0.5 flex-shrink-0" />
) : (
<GitPullRequest className="h-4 w-4 text-green-500 mt-0.5 flex-shrink-0" />
)}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-sm font-medium truncate">{pr.title}</span>
{pr.isDraft && (
<span className="px-1.5 py-0.5 text-[10px] font-medium rounded bg-muted text-muted-foreground flex-shrink-0">
Draft
</span>
)}
</div>
<div className="flex items-center gap-2 mt-1 flex-wrap">
<span className="text-xs text-muted-foreground">
#{pr.number} opened {formatDate(pr.createdAt)} by {pr.author.login}
</span>
{pr.headRefName && (
<span className="text-xs text-muted-foreground font-mono bg-muted px-1 rounded">
{pr.headRefName}
</span>
)}
</div>
<div className="flex items-center gap-2 mt-2 flex-wrap">
{/* Review Status */}
{reviewStatus && (
<span
className={cn(
'px-1.5 py-0.5 text-[10px] font-medium rounded',
reviewStatus.bg,
reviewStatus.color
)}
>
{reviewStatus.label}
</span>
)}
{/* Labels */}
{pr.labels.map((label) => (
<span
key={label.name}
className="px-1.5 py-0.5 text-[10px] font-medium rounded-full"
style={{
backgroundColor: `#${label.color}20`,
color: `#${label.color}`,
border: `1px solid #${label.color}40`,
}}
>
{label.name}
</span>
))}
</div>
</div>
<Button
variant="ghost"
size="sm"
className="flex-shrink-0 opacity-0 group-hover:opacity-100"
onClick={(e) => {
e.stopPropagation();
onOpenExternal();
}}
>
<ExternalLink className="h-3.5 w-3.5" />
</Button>
</div>
);
}

View File

@@ -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,
};

View File

@@ -92,12 +92,77 @@ export interface RunningAgentsAPI {
getAll: () => Promise<RunningAgentsResult>;
}
// 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 {

View File

@@ -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<{

View File

@@ -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,
});

View File

@@ -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,
});

View File

@@ -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

View File

@@ -18,7 +18,6 @@ export interface Feature {
titleGenerating?: boolean;
category: string;
description: string;
steps?: string[];
passes?: boolean;
priority?: number;
status?: string;