mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
fixing auto verify for kanban issues
This commit is contained in:
30
.github/workflows/security-audit.yml
vendored
Normal file
30
.github/workflows/security-audit.yml
vendored
Normal 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
|
||||||
@@ -46,6 +46,7 @@ import { SettingsService } from './services/settings-service.js';
|
|||||||
import { createSpecRegenerationRoutes } from './routes/app-spec/index.js';
|
import { createSpecRegenerationRoutes } from './routes/app-spec/index.js';
|
||||||
import { createClaudeRoutes } from './routes/claude/index.js';
|
import { createClaudeRoutes } from './routes/claude/index.js';
|
||||||
import { ClaudeUsageService } from './services/claude-usage-service.js';
|
import { ClaudeUsageService } from './services/claude-usage-service.js';
|
||||||
|
import { createGitHubRoutes } from './routes/github/index.js';
|
||||||
|
|
||||||
// Load environment variables
|
// Load environment variables
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
@@ -145,6 +146,7 @@ app.use('/api/templates', createTemplatesRoutes());
|
|||||||
app.use('/api/terminal', createTerminalRoutes());
|
app.use('/api/terminal', createTerminalRoutes());
|
||||||
app.use('/api/settings', createSettingsRoutes(settingsService));
|
app.use('/api/settings', createSettingsRoutes(settingsService));
|
||||||
app.use('/api/claude', createClaudeRoutes(claudeUsageService));
|
app.use('/api/claude', createClaudeRoutes(claudeUsageService));
|
||||||
|
app.use('/api/github', createGitHubRoutes());
|
||||||
|
|
||||||
// Create HTTP server
|
// Create HTTP server
|
||||||
const server = createServer(app);
|
const server = createServer(app);
|
||||||
|
|||||||
18
apps/server/src/routes/github/index.ts
Normal file
18
apps/server/src/routes/github/index.ts
Normal 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;
|
||||||
|
}
|
||||||
71
apps/server/src/routes/github/routes/check-github-remote.ts
Normal file
71
apps/server/src/routes/github/routes/check-github-remote.ts
Normal 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) });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
35
apps/server/src/routes/github/routes/common.ts
Normal file
35
apps/server/src/routes/github/routes/common.ts
Normal 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);
|
||||||
|
}
|
||||||
89
apps/server/src/routes/github/routes/list-issues.ts
Normal file
89
apps/server/src/routes/github/routes/list-issues.ts
Normal 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) });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
93
apps/server/src/routes/github/routes/list-prs.ts
Normal file
93
apps/server/src/routes/github/routes/list-prs.ts
Normal 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) });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -23,10 +23,6 @@ const suggestionsSchema = {
|
|||||||
id: { type: 'string' },
|
id: { type: 'string' },
|
||||||
category: { type: 'string' },
|
category: { type: 'string' },
|
||||||
description: { type: 'string' },
|
description: { type: 'string' },
|
||||||
steps: {
|
|
||||||
type: 'array',
|
|
||||||
items: { type: 'string' },
|
|
||||||
},
|
|
||||||
priority: {
|
priority: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
minimum: 1,
|
minimum: 1,
|
||||||
@@ -34,7 +30,7 @@ const suggestionsSchema = {
|
|||||||
},
|
},
|
||||||
reasoning: { type: 'string' },
|
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:
|
For each suggestion, provide:
|
||||||
1. A category (e.g., "User Experience", "Security", "Performance")
|
1. A category (e.g., "User Experience", "Security", "Performance")
|
||||||
2. A clear description of what to implement
|
2. A clear description of what to implement
|
||||||
3. Concrete steps to implement it
|
3. Priority (1=high, 2=medium, 3=low)
|
||||||
4. Priority (1=high, 2=medium, 3=low)
|
4. Brief reasoning for why this would help
|
||||||
5. Brief reasoning for why this would help
|
|
||||||
|
|
||||||
The response will be automatically formatted as structured JSON.`;
|
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`,
|
id: `suggestion-${Date.now()}-0`,
|
||||||
category: 'Analysis',
|
category: 'Analysis',
|
||||||
description: 'Review the AI analysis output for insights',
|
description: 'Review the AI analysis output for insights',
|
||||||
steps: ['Review the generated analysis'],
|
|
||||||
priority: 1,
|
priority: 1,
|
||||||
reasoning: 'The AI provided analysis but suggestions need manual review',
|
reasoning: 'The AI provided analysis but suggestions need manual review',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -599,15 +599,18 @@ export class AutoModeService {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Mark as waiting_approval for user review
|
// Determine final status based on testing mode:
|
||||||
await this.updateFeatureStatus(projectPath, featureId, 'waiting_approval');
|
// - 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', {
|
this.emitAutoModeEvent('auto_mode_feature_complete', {
|
||||||
featureId,
|
featureId,
|
||||||
passes: true,
|
passes: true,
|
||||||
message: `Feature completed in ${Math.round(
|
message: `Feature completed in ${Math.round(
|
||||||
(Date.now() - tempRunningFeature.startTime) / 1000
|
(Date.now() - tempRunningFeature.startTime) / 1000
|
||||||
)}s`,
|
)}s${finalStatus === 'verified' ? ' - auto-verified' : ''}`,
|
||||||
projectPath,
|
projectPath,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} 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
|
// Determine final status based on testing mode:
|
||||||
await this.updateFeatureStatus(projectPath, featureId, 'waiting_approval');
|
// - 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', {
|
this.emitAutoModeEvent('auto_mode_feature_complete', {
|
||||||
featureId,
|
featureId,
|
||||||
passes: true,
|
passes: true,
|
||||||
message: 'Follow-up completed successfully',
|
message: `Follow-up completed successfully${finalStatus === 'verified' ? ' - auto-verified' : ''}`,
|
||||||
projectPath,
|
projectPath,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} 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
|
## Instructions
|
||||||
|
|
||||||
Implement this feature by:
|
Implement this feature by:
|
||||||
1. First, explore the codebase to understand the existing structure
|
1. First, explore the codebase to understand the existing structure
|
||||||
2. Plan your implementation approach
|
2. Plan your implementation approach
|
||||||
3. Write the necessary code changes
|
3. Write the necessary code changes
|
||||||
4. Add or update tests as needed
|
4. Ensure the code follows existing patterns and conventions
|
||||||
5. Ensure the code follows existing patterns and conventions
|
|
||||||
|
|
||||||
When done, wrap your final summary in <summary> tags like this:
|
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>
|
</summary>
|
||||||
|
|
||||||
This helps parse your summary correctly in the output logs.`;
|
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;
|
return prompt;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,9 @@ export function SidebarNavigation({
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={item.id}
|
key={item.id}
|
||||||
onClick={() => navigate({ to: `/${item.id}` as const })}
|
onClick={() => {
|
||||||
|
navigate({ to: `/${item.id}` as const });
|
||||||
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
'group flex items-center w-full px-3 py-2.5 rounded-xl relative overflow-hidden titlebar-no-drag',
|
'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',
|
'transition-all duration-200 ease-out',
|
||||||
|
|||||||
@@ -1,9 +1,20 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo, useState, useEffect } from 'react';
|
||||||
import type { NavigateOptions } from '@tanstack/react-router';
|
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 { NavSection, NavItem } from '../types';
|
||||||
import type { KeyboardShortcut } from '@/hooks/use-keyboard-shortcuts';
|
import type { KeyboardShortcut } from '@/hooks/use-keyboard-shortcuts';
|
||||||
import type { Project } from '@/lib/electron';
|
import type { Project } from '@/lib/electron';
|
||||||
|
import { getElectronAPI } from '@/lib/electron';
|
||||||
|
|
||||||
interface UseNavigationProps {
|
interface UseNavigationProps {
|
||||||
shortcuts: {
|
shortcuts: {
|
||||||
@@ -51,6 +62,30 @@ export function useNavigation({
|
|||||||
cyclePrevProject,
|
cyclePrevProject,
|
||||||
cycleNextProject,
|
cycleNextProject,
|
||||||
}: UseNavigationProps) {
|
}: 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
|
// Build navigation sections
|
||||||
const navSections: NavSection[] = useMemo(() => {
|
const navSections: NavSection[] = useMemo(() => {
|
||||||
const allToolsItems: NavItem[] = [
|
const allToolsItems: NavItem[] = [
|
||||||
@@ -114,7 +149,7 @@ export function useNavigation({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
const sections: NavSection[] = [
|
||||||
{
|
{
|
||||||
label: 'Project',
|
label: 'Project',
|
||||||
items: projectItems,
|
items: projectItems,
|
||||||
@@ -124,7 +159,28 @@ export function useNavigation({
|
|||||||
items: visibleToolsItems,
|
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
|
// Build keyboard shortcuts for navigation
|
||||||
const navigationShortcuts: KeyboardShortcut[] = useMemo(() => {
|
const navigationShortcuts: KeyboardShortcut[] = useMemo(() => {
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export function useProjectCreation({
|
|||||||
// Write initial app_spec.txt with proper XML structure
|
// 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
|
// Note: Must follow XML format as defined in apps/server/src/lib/app-spec-format.ts
|
||||||
const api = getElectronAPI();
|
const api = getElectronAPI();
|
||||||
await api.fs.writeFile(
|
await api.writeFile(
|
||||||
`${projectPath}/.automaker/app_spec.txt`,
|
`${projectPath}/.automaker/app_spec.txt`,
|
||||||
`<project_specification>
|
`<project_specification>
|
||||||
<project_name>${projectName}</project_name>
|
<project_name>${projectName}</project_name>
|
||||||
@@ -103,7 +103,7 @@ export function useProjectCreation({
|
|||||||
const projectPath = `${parentDir}/${projectName}`;
|
const projectPath = `${parentDir}/${projectName}`;
|
||||||
|
|
||||||
// Create project directory
|
// Create project directory
|
||||||
await api.fs.createFolder(projectPath);
|
await api.mkdir(projectPath);
|
||||||
|
|
||||||
// Finalize project setup
|
// Finalize project setup
|
||||||
await finalizeProjectCreation(projectPath, projectName);
|
await finalizeProjectCreation(projectPath, projectName);
|
||||||
@@ -127,16 +127,19 @@ export function useProjectCreation({
|
|||||||
setIsCreatingProject(true);
|
setIsCreatingProject(true);
|
||||||
try {
|
try {
|
||||||
const api = getElectronAPI();
|
const api = getElectronAPI();
|
||||||
const projectPath = `${parentDir}/${projectName}`;
|
|
||||||
|
|
||||||
// Clone template repository
|
// 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
|
// Initialize .automaker directory structure
|
||||||
await initializeProject(projectPath);
|
await initializeProject(projectPath);
|
||||||
|
|
||||||
// Write app_spec.txt with template-specific info
|
// Write app_spec.txt with template-specific info
|
||||||
await api.fs.writeFile(
|
await api.writeFile(
|
||||||
`${projectPath}/.automaker/app_spec.txt`,
|
`${projectPath}/.automaker/app_spec.txt`,
|
||||||
`<project_specification>
|
`<project_specification>
|
||||||
<project_name>${projectName}</project_name>
|
<project_name>${projectName}</project_name>
|
||||||
@@ -196,16 +199,19 @@ export function useProjectCreation({
|
|||||||
setIsCreatingProject(true);
|
setIsCreatingProject(true);
|
||||||
try {
|
try {
|
||||||
const api = getElectronAPI();
|
const api = getElectronAPI();
|
||||||
const projectPath = `${parentDir}/${projectName}`;
|
|
||||||
|
|
||||||
// Clone custom repository
|
// 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
|
// Initialize .automaker directory structure
|
||||||
await initializeProject(projectPath);
|
await initializeProject(projectPath);
|
||||||
|
|
||||||
// Write app_spec.txt with custom URL info
|
// Write app_spec.txt with custom URL info
|
||||||
await api.fs.writeFile(
|
await api.writeFile(
|
||||||
`${projectPath}/.automaker/app_spec.txt`,
|
`${projectPath}/.automaker/app_spec.txt`,
|
||||||
`<project_specification>
|
`<project_specification>
|
||||||
<project_name>${projectName}</project_name>
|
<project_name>${projectName}</project_name>
|
||||||
|
|||||||
@@ -426,7 +426,6 @@ ${Object.entries(projectAnalysis.filesByExtension)
|
|||||||
interface DetectedFeature {
|
interface DetectedFeature {
|
||||||
category: string;
|
category: string;
|
||||||
description: string;
|
description: string;
|
||||||
steps: string[];
|
|
||||||
passes: boolean;
|
passes: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -453,11 +452,6 @@ ${Object.entries(projectAnalysis.filesByExtension)
|
|||||||
detectedFeatures.push({
|
detectedFeatures.push({
|
||||||
category: 'Testing',
|
category: 'Testing',
|
||||||
description: 'Automated test suite',
|
description: 'Automated test suite',
|
||||||
steps: [
|
|
||||||
'Step 1: Tests directory exists',
|
|
||||||
'Step 2: Test files are present',
|
|
||||||
'Step 3: Run test suite',
|
|
||||||
],
|
|
||||||
passes: true,
|
passes: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -471,11 +465,6 @@ ${Object.entries(projectAnalysis.filesByExtension)
|
|||||||
detectedFeatures.push({
|
detectedFeatures.push({
|
||||||
category: 'UI/Design',
|
category: 'UI/Design',
|
||||||
description: 'Component-based UI architecture',
|
description: 'Component-based UI architecture',
|
||||||
steps: [
|
|
||||||
'Step 1: Components directory exists',
|
|
||||||
'Step 2: UI components are defined',
|
|
||||||
'Step 3: Components are reusable',
|
|
||||||
],
|
|
||||||
passes: true,
|
passes: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -485,11 +474,6 @@ ${Object.entries(projectAnalysis.filesByExtension)
|
|||||||
detectedFeatures.push({
|
detectedFeatures.push({
|
||||||
category: 'Project Structure',
|
category: 'Project Structure',
|
||||||
description: 'Organized source code 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,
|
passes: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -504,11 +488,6 @@ ${Object.entries(projectAnalysis.filesByExtension)
|
|||||||
detectedFeatures.push({
|
detectedFeatures.push({
|
||||||
category: 'Frontend',
|
category: 'Frontend',
|
||||||
description: 'React-based user interface',
|
description: 'React-based user interface',
|
||||||
steps: [
|
|
||||||
'Step 1: React is installed',
|
|
||||||
'Step 2: Components render correctly',
|
|
||||||
'Step 3: State management works',
|
|
||||||
],
|
|
||||||
passes: true,
|
passes: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -517,11 +496,6 @@ ${Object.entries(projectAnalysis.filesByExtension)
|
|||||||
detectedFeatures.push({
|
detectedFeatures.push({
|
||||||
category: 'Framework',
|
category: 'Framework',
|
||||||
description: 'Next.js framework integration',
|
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,
|
passes: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -536,11 +510,6 @@ ${Object.entries(projectAnalysis.filesByExtension)
|
|||||||
detectedFeatures.push({
|
detectedFeatures.push({
|
||||||
category: 'Developer Experience',
|
category: 'Developer Experience',
|
||||||
description: 'TypeScript type safety',
|
description: 'TypeScript type safety',
|
||||||
steps: [
|
|
||||||
'Step 1: TypeScript is configured',
|
|
||||||
'Step 2: Type definitions exist',
|
|
||||||
'Step 3: Code compiles without errors',
|
|
||||||
],
|
|
||||||
passes: true,
|
passes: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -550,11 +519,6 @@ ${Object.entries(projectAnalysis.filesByExtension)
|
|||||||
detectedFeatures.push({
|
detectedFeatures.push({
|
||||||
category: 'UI/Design',
|
category: 'UI/Design',
|
||||||
description: 'Tailwind CSS styling',
|
description: 'Tailwind CSS styling',
|
||||||
steps: [
|
|
||||||
'Step 1: Tailwind is configured',
|
|
||||||
'Step 2: Styles are applied',
|
|
||||||
'Step 3: Responsive design works',
|
|
||||||
],
|
|
||||||
passes: true,
|
passes: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -564,11 +528,6 @@ ${Object.entries(projectAnalysis.filesByExtension)
|
|||||||
detectedFeatures.push({
|
detectedFeatures.push({
|
||||||
category: 'Developer Experience',
|
category: 'Developer Experience',
|
||||||
description: 'Code quality tools',
|
description: 'Code quality tools',
|
||||||
steps: [
|
|
||||||
'Step 1: Linter is configured',
|
|
||||||
'Step 2: Code passes lint checks',
|
|
||||||
'Step 3: Formatting is consistent',
|
|
||||||
],
|
|
||||||
passes: true,
|
passes: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -578,11 +537,6 @@ ${Object.entries(projectAnalysis.filesByExtension)
|
|||||||
detectedFeatures.push({
|
detectedFeatures.push({
|
||||||
category: 'Platform',
|
category: 'Platform',
|
||||||
description: 'Electron desktop application',
|
description: 'Electron desktop application',
|
||||||
steps: [
|
|
||||||
'Step 1: Electron is configured',
|
|
||||||
'Step 2: Main process runs',
|
|
||||||
'Step 3: Renderer process loads',
|
|
||||||
],
|
|
||||||
passes: true,
|
passes: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -592,11 +546,6 @@ ${Object.entries(projectAnalysis.filesByExtension)
|
|||||||
detectedFeatures.push({
|
detectedFeatures.push({
|
||||||
category: 'Testing',
|
category: 'Testing',
|
||||||
description: 'Playwright end-to-end 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,
|
passes: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -610,11 +559,6 @@ ${Object.entries(projectAnalysis.filesByExtension)
|
|||||||
detectedFeatures.push({
|
detectedFeatures.push({
|
||||||
category: 'Documentation',
|
category: 'Documentation',
|
||||||
description: 'Project documentation',
|
description: 'Project documentation',
|
||||||
steps: [
|
|
||||||
'Step 1: README exists',
|
|
||||||
'Step 2: Documentation is comprehensive',
|
|
||||||
'Step 3: Setup instructions are clear',
|
|
||||||
],
|
|
||||||
passes: true,
|
passes: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -629,11 +573,6 @@ ${Object.entries(projectAnalysis.filesByExtension)
|
|||||||
detectedFeatures.push({
|
detectedFeatures.push({
|
||||||
category: 'DevOps',
|
category: 'DevOps',
|
||||||
description: 'CI/CD pipeline configuration',
|
description: 'CI/CD pipeline configuration',
|
||||||
steps: [
|
|
||||||
'Step 1: CI config exists',
|
|
||||||
'Step 2: Pipeline runs on push',
|
|
||||||
'Step 3: Automated checks pass',
|
|
||||||
],
|
|
||||||
passes: true,
|
passes: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -647,11 +586,6 @@ ${Object.entries(projectAnalysis.filesByExtension)
|
|||||||
detectedFeatures.push({
|
detectedFeatures.push({
|
||||||
category: 'Backend',
|
category: 'Backend',
|
||||||
description: 'API endpoints',
|
description: 'API endpoints',
|
||||||
steps: [
|
|
||||||
'Step 1: API routes are defined',
|
|
||||||
'Step 2: Endpoints respond correctly',
|
|
||||||
'Step 3: Error handling is implemented',
|
|
||||||
],
|
|
||||||
passes: true,
|
passes: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -669,11 +603,6 @@ ${Object.entries(projectAnalysis.filesByExtension)
|
|||||||
detectedFeatures.push({
|
detectedFeatures.push({
|
||||||
category: 'Architecture',
|
category: 'Architecture',
|
||||||
description: 'State management system',
|
description: 'State management system',
|
||||||
steps: [
|
|
||||||
'Step 1: Store is configured',
|
|
||||||
'Step 2: State updates correctly',
|
|
||||||
'Step 3: Components access state',
|
|
||||||
],
|
|
||||||
passes: true,
|
passes: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -683,11 +612,6 @@ ${Object.entries(projectAnalysis.filesByExtension)
|
|||||||
detectedFeatures.push({
|
detectedFeatures.push({
|
||||||
category: 'Configuration',
|
category: 'Configuration',
|
||||||
description: 'Project configuration files',
|
description: 'Project configuration files',
|
||||||
steps: [
|
|
||||||
'Step 1: Config files exist',
|
|
||||||
'Step 2: Configuration is valid',
|
|
||||||
'Step 3: Build process works',
|
|
||||||
],
|
|
||||||
passes: true,
|
passes: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -700,11 +624,6 @@ ${Object.entries(projectAnalysis.filesByExtension)
|
|||||||
detectedFeatures.push({
|
detectedFeatures.push({
|
||||||
category: 'Core',
|
category: 'Core',
|
||||||
description: 'Basic project structure',
|
description: 'Basic project structure',
|
||||||
steps: [
|
|
||||||
'Step 1: Project directory exists',
|
|
||||||
'Step 2: Files are present',
|
|
||||||
'Step 3: Project can be loaded',
|
|
||||||
],
|
|
||||||
passes: true,
|
passes: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -719,7 +638,6 @@ ${Object.entries(projectAnalysis.filesByExtension)
|
|||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
category: detectedFeature.category,
|
category: detectedFeature.category,
|
||||||
description: detectedFeature.description,
|
description: detectedFeature.description,
|
||||||
steps: detectedFeature.steps,
|
|
||||||
status: 'backlog',
|
status: 'backlog',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -433,9 +433,9 @@ export function BoardView() {
|
|||||||
|
|
||||||
// Create the feature
|
// Create the feature
|
||||||
const featureData = {
|
const featureData = {
|
||||||
|
title: `Address PR #${prNumber} Review Comments`,
|
||||||
category: 'PR Review',
|
category: 'PR Review',
|
||||||
description,
|
description,
|
||||||
steps: [],
|
|
||||||
images: [],
|
images: [],
|
||||||
imagePaths: [],
|
imagePaths: [],
|
||||||
skipTests: defaultSkipTests,
|
skipTests: defaultSkipTests,
|
||||||
@@ -475,9 +475,9 @@ export function BoardView() {
|
|||||||
|
|
||||||
// Create the feature
|
// Create the feature
|
||||||
const featureData = {
|
const featureData = {
|
||||||
|
title: `Resolve Merge Conflicts`,
|
||||||
category: 'Maintenance',
|
category: 'Maintenance',
|
||||||
description,
|
description,
|
||||||
steps: [],
|
|
||||||
images: [],
|
images: [],
|
||||||
imagePaths: [],
|
imagePaths: [],
|
||||||
skipTests: defaultSkipTests,
|
skipTests: defaultSkipTests,
|
||||||
|
|||||||
@@ -1,17 +1,12 @@
|
|||||||
import { Feature } from '@/store/app-store';
|
import { Feature } from '@/store/app-store';
|
||||||
import { GitBranch, GitPullRequest, ExternalLink, CheckCircle2, Circle } from 'lucide-react';
|
import { GitBranch, GitPullRequest, ExternalLink } from 'lucide-react';
|
||||||
|
|
||||||
interface CardContentSectionsProps {
|
interface CardContentSectionsProps {
|
||||||
feature: Feature;
|
feature: Feature;
|
||||||
useWorktrees: boolean;
|
useWorktrees: boolean;
|
||||||
showSteps: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CardContentSections({
|
export function CardContentSections({ feature, useWorktrees }: CardContentSectionsProps) {
|
||||||
feature,
|
|
||||||
useWorktrees,
|
|
||||||
showSteps,
|
|
||||||
}: CardContentSectionsProps) {
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Target Branch Display */}
|
{/* Target Branch Display */}
|
||||||
@@ -50,30 +45,6 @@ export function CardContentSections({
|
|||||||
</div>
|
</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>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,9 +61,7 @@ export const KanbanCard = memo(function KanbanCard({
|
|||||||
cardBorderEnabled = true,
|
cardBorderEnabled = true,
|
||||||
cardBorderOpacity = 100,
|
cardBorderOpacity = 100,
|
||||||
}: KanbanCardProps) {
|
}: KanbanCardProps) {
|
||||||
const { kanbanCardDetailLevel, useWorktrees } = useAppStore();
|
const { useWorktrees } = useAppStore();
|
||||||
|
|
||||||
const showSteps = kanbanCardDetailLevel === 'standard' || kanbanCardDetailLevel === 'detailed';
|
|
||||||
|
|
||||||
const isDraggable =
|
const isDraggable =
|
||||||
feature.status === 'backlog' ||
|
feature.status === 'backlog' ||
|
||||||
@@ -152,7 +150,7 @@ export const KanbanCard = memo(function KanbanCard({
|
|||||||
|
|
||||||
<CardContent className="px-3 pt-0 pb-0">
|
<CardContent className="px-3 pt-0 pb-0">
|
||||||
{/* Content Sections */}
|
{/* Content Sections */}
|
||||||
<CardContentSections feature={feature} useWorktrees={useWorktrees} showSteps={showSteps} />
|
<CardContentSections feature={feature} useWorktrees={useWorktrees} />
|
||||||
|
|
||||||
{/* Agent Info Panel */}
|
{/* Agent Info Panel */}
|
||||||
<AgentInfoPanel
|
<AgentInfoPanel
|
||||||
|
|||||||
@@ -61,7 +61,6 @@ interface AddFeatureDialogProps {
|
|||||||
title: string;
|
title: string;
|
||||||
category: string;
|
category: string;
|
||||||
description: string;
|
description: string;
|
||||||
steps: string[];
|
|
||||||
images: FeatureImage[];
|
images: FeatureImage[];
|
||||||
imagePaths: DescriptionImagePath[];
|
imagePaths: DescriptionImagePath[];
|
||||||
skipTests: boolean;
|
skipTests: boolean;
|
||||||
@@ -103,7 +102,6 @@ export function AddFeatureDialog({
|
|||||||
title: '',
|
title: '',
|
||||||
category: '',
|
category: '',
|
||||||
description: '',
|
description: '',
|
||||||
steps: [''],
|
|
||||||
images: [] as FeatureImage[],
|
images: [] as FeatureImage[],
|
||||||
imagePaths: [] as DescriptionImagePath[],
|
imagePaths: [] as DescriptionImagePath[],
|
||||||
skipTests: false,
|
skipTests: false,
|
||||||
@@ -190,7 +188,6 @@ export function AddFeatureDialog({
|
|||||||
title: newFeature.title,
|
title: newFeature.title,
|
||||||
category,
|
category,
|
||||||
description: newFeature.description,
|
description: newFeature.description,
|
||||||
steps: newFeature.steps.filter((s) => s.trim()),
|
|
||||||
images: newFeature.images,
|
images: newFeature.images,
|
||||||
imagePaths: newFeature.imagePaths,
|
imagePaths: newFeature.imagePaths,
|
||||||
skipTests: newFeature.skipTests,
|
skipTests: newFeature.skipTests,
|
||||||
@@ -207,7 +204,6 @@ export function AddFeatureDialog({
|
|||||||
title: '',
|
title: '',
|
||||||
category: '',
|
category: '',
|
||||||
description: '',
|
description: '',
|
||||||
steps: [''],
|
|
||||||
images: [],
|
images: [],
|
||||||
imagePaths: [],
|
imagePaths: [],
|
||||||
skipTests: defaultSkipTests,
|
skipTests: defaultSkipTests,
|
||||||
@@ -493,8 +489,6 @@ export function AddFeatureDialog({
|
|||||||
<TestingTabContent
|
<TestingTabContent
|
||||||
skipTests={newFeature.skipTests}
|
skipTests={newFeature.skipTests}
|
||||||
onSkipTestsChange={(skipTests) => setNewFeature({ ...newFeature, skipTests })}
|
onSkipTestsChange={(skipTests) => setNewFeature({ ...newFeature, skipTests })}
|
||||||
steps={newFeature.steps}
|
|
||||||
onStepsChange={(steps) => setNewFeature({ ...newFeature, steps })}
|
|
||||||
/>
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|||||||
@@ -63,7 +63,6 @@ interface EditFeatureDialogProps {
|
|||||||
title: string;
|
title: string;
|
||||||
category: string;
|
category: string;
|
||||||
description: string;
|
description: string;
|
||||||
steps: string[];
|
|
||||||
skipTests: boolean;
|
skipTests: boolean;
|
||||||
model: AgentModel;
|
model: AgentModel;
|
||||||
thinkingLevel: ThinkingLevel;
|
thinkingLevel: ThinkingLevel;
|
||||||
@@ -163,7 +162,6 @@ export function EditFeatureDialog({
|
|||||||
title: editingFeature.title ?? '',
|
title: editingFeature.title ?? '',
|
||||||
category: editingFeature.category,
|
category: editingFeature.category,
|
||||||
description: editingFeature.description,
|
description: editingFeature.description,
|
||||||
steps: editingFeature.steps,
|
|
||||||
skipTests: editingFeature.skipTests ?? false,
|
skipTests: editingFeature.skipTests ?? false,
|
||||||
model: selectedModel,
|
model: selectedModel,
|
||||||
thinkingLevel: normalizedThinking,
|
thinkingLevel: normalizedThinking,
|
||||||
@@ -481,8 +479,6 @@ export function EditFeatureDialog({
|
|||||||
<TestingTabContent
|
<TestingTabContent
|
||||||
skipTests={editingFeature.skipTests ?? false}
|
skipTests={editingFeature.skipTests ?? false}
|
||||||
onSkipTestsChange={(skipTests) => setEditingFeature({ ...editingFeature, skipTests })}
|
onSkipTestsChange={(skipTests) => setEditingFeature({ ...editingFeature, skipTests })}
|
||||||
steps={editingFeature.steps}
|
|
||||||
onStepsChange={(steps) => setEditingFeature({ ...editingFeature, steps })}
|
|
||||||
testIdPrefix="edit"
|
testIdPrefix="edit"
|
||||||
/>
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|||||||
@@ -245,7 +245,6 @@ export function FeatureSuggestionsDialog({
|
|||||||
id: `feature-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
id: `feature-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||||
category: s.category,
|
category: s.category,
|
||||||
description: s.description,
|
description: s.description,
|
||||||
steps: s.steps,
|
|
||||||
status: 'backlog' as const,
|
status: 'backlog' as const,
|
||||||
skipTests: true, // As specified, testing mode true
|
skipTests: true, // As specified, testing mode true
|
||||||
priority: s.priority, // Preserve priority from suggestion
|
priority: s.priority, // Preserve priority from suggestion
|
||||||
@@ -453,23 +452,9 @@ export function FeatureSuggestionsDialog({
|
|||||||
{suggestion.description}
|
{suggestion.description}
|
||||||
</Label>
|
</Label>
|
||||||
|
|
||||||
{isExpanded && (
|
{isExpanded && suggestion.reasoning && (
|
||||||
<div className="mt-3 space-y-2 text-sm">
|
<div className="mt-3 text-sm">
|
||||||
{suggestion.reasoning && (
|
<p className="text-muted-foreground italic">{suggestion.reasoning}</p>
|
||||||
<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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -89,7 +89,6 @@ export function useBoardActions({
|
|||||||
title: string;
|
title: string;
|
||||||
category: string;
|
category: string;
|
||||||
description: string;
|
description: string;
|
||||||
steps: string[];
|
|
||||||
images: FeatureImage[];
|
images: FeatureImage[];
|
||||||
imagePaths: DescriptionImagePath[];
|
imagePaths: DescriptionImagePath[];
|
||||||
skipTests: boolean;
|
skipTests: boolean;
|
||||||
@@ -208,7 +207,6 @@ export function useBoardActions({
|
|||||||
title: string;
|
title: string;
|
||||||
category: string;
|
category: string;
|
||||||
description: string;
|
description: string;
|
||||||
steps: string[];
|
|
||||||
skipTests: boolean;
|
skipTests: boolean;
|
||||||
model: AgentModel;
|
model: AgentModel;
|
||||||
thinkingLevel: ThinkingLevel;
|
thinkingLevel: ThinkingLevel;
|
||||||
|
|||||||
@@ -1,36 +1,20 @@
|
|||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { Input } from '@/components/ui/input';
|
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import { FlaskConical, Plus } from 'lucide-react';
|
import { FlaskConical } from 'lucide-react';
|
||||||
|
|
||||||
interface TestingTabContentProps {
|
interface TestingTabContentProps {
|
||||||
skipTests: boolean;
|
skipTests: boolean;
|
||||||
onSkipTestsChange: (skipTests: boolean) => void;
|
onSkipTestsChange: (skipTests: boolean) => void;
|
||||||
steps: string[];
|
|
||||||
onStepsChange: (steps: string[]) => void;
|
|
||||||
testIdPrefix?: string;
|
testIdPrefix?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TestingTabContent({
|
export function TestingTabContent({
|
||||||
skipTests,
|
skipTests,
|
||||||
onSkipTestsChange,
|
onSkipTestsChange,
|
||||||
steps,
|
|
||||||
onStepsChange,
|
|
||||||
testIdPrefix = '',
|
testIdPrefix = '',
|
||||||
}: TestingTabContentProps) {
|
}: TestingTabContentProps) {
|
||||||
const checkboxId = testIdPrefix ? `${testIdPrefix}-skip-tests` : 'skip-tests';
|
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 (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
@@ -48,37 +32,9 @@ export function TestingTabContent({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
When enabled, this feature will use automated TDD. When disabled, it will require manual
|
When enabled, the agent will use Playwright to verify the feature works correctly before
|
||||||
verification.
|
marking it as verified. When disabled, manual verification will be required.
|
||||||
</p>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
334
apps/ui/src/components/views/github-issues-view.tsx
Normal file
334
apps/ui/src/components/views/github-issues-view.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
421
apps/ui/src/components/views/github-prs-view.tsx
Normal file
421
apps/ui/src/components/views/github-prs-view.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -345,11 +345,6 @@ export function InterviewView() {
|
|||||||
category: 'Core',
|
category: 'Core',
|
||||||
description: 'Initial project setup',
|
description: 'Initial project setup',
|
||||||
status: 'backlog' as const,
|
status: 'backlog' as const,
|
||||||
steps: [
|
|
||||||
'Step 1: Review app_spec.txt',
|
|
||||||
'Step 2: Set up development environment',
|
|
||||||
'Step 3: Start implementing features',
|
|
||||||
],
|
|
||||||
skipTests: true,
|
skipTests: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -92,12 +92,77 @@ export interface RunningAgentsAPI {
|
|||||||
getAll: () => Promise<RunningAgentsResult>;
|
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
|
// Feature Suggestions types
|
||||||
export interface FeatureSuggestion {
|
export interface FeatureSuggestion {
|
||||||
id: string;
|
id: string;
|
||||||
category: string;
|
category: string;
|
||||||
description: string;
|
description: string;
|
||||||
steps: string[];
|
|
||||||
priority: number;
|
priority: number;
|
||||||
reasoning: string;
|
reasoning: string;
|
||||||
}
|
}
|
||||||
@@ -326,6 +391,7 @@ export interface ElectronAPI {
|
|||||||
autoMode?: AutoModeAPI;
|
autoMode?: AutoModeAPI;
|
||||||
features?: FeaturesAPI;
|
features?: FeaturesAPI;
|
||||||
runningAgents?: RunningAgentsAPI;
|
runningAgents?: RunningAgentsAPI;
|
||||||
|
github?: GitHubAPI;
|
||||||
enhancePrompt?: {
|
enhancePrompt?: {
|
||||||
enhance: (
|
enhance: (
|
||||||
originalText: string,
|
originalText: string,
|
||||||
@@ -861,6 +927,9 @@ const getMockElectronAPI = (): ElectronAPI => {
|
|||||||
// Mock Running Agents API
|
// Mock Running Agents API
|
||||||
runningAgents: createMockRunningAgentsAPI(),
|
runningAgents: createMockRunningAgentsAPI(),
|
||||||
|
|
||||||
|
// Mock GitHub API
|
||||||
|
github: createMockGitHubAPI(),
|
||||||
|
|
||||||
// Mock Claude API
|
// Mock Claude API
|
||||||
claude: {
|
claude: {
|
||||||
getUsage: async () => {
|
getUsage: async () => {
|
||||||
@@ -1963,12 +2032,6 @@ async function simulateSuggestionsGeneration(suggestionType: SuggestionType = 'f
|
|||||||
id: `suggestion-${Date.now()}-0`,
|
id: `suggestion-${Date.now()}-0`,
|
||||||
category: 'Code Smell',
|
category: 'Code Smell',
|
||||||
description: 'Extract duplicate validation logic into reusable utility',
|
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,
|
priority: 1,
|
||||||
reasoning: 'Reduces code duplication and improves maintainability',
|
reasoning: 'Reduces code duplication and improves maintainability',
|
||||||
},
|
},
|
||||||
@@ -1976,12 +2039,6 @@ async function simulateSuggestionsGeneration(suggestionType: SuggestionType = 'f
|
|||||||
id: `suggestion-${Date.now()}-1`,
|
id: `suggestion-${Date.now()}-1`,
|
||||||
category: 'Complexity',
|
category: 'Complexity',
|
||||||
description: 'Break down large handleSubmit function into smaller functions',
|
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,
|
priority: 2,
|
||||||
reasoning: 'Function is too long and handles multiple responsibilities',
|
reasoning: 'Function is too long and handles multiple responsibilities',
|
||||||
},
|
},
|
||||||
@@ -1989,12 +2046,6 @@ async function simulateSuggestionsGeneration(suggestionType: SuggestionType = 'f
|
|||||||
id: `suggestion-${Date.now()}-2`,
|
id: `suggestion-${Date.now()}-2`,
|
||||||
category: 'Architecture',
|
category: 'Architecture',
|
||||||
description: 'Move business logic out of React components into hooks',
|
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,
|
priority: 3,
|
||||||
reasoning: 'Improves separation of concerns and testability',
|
reasoning: 'Improves separation of concerns and testability',
|
||||||
},
|
},
|
||||||
@@ -2007,12 +2058,6 @@ async function simulateSuggestionsGeneration(suggestionType: SuggestionType = 'f
|
|||||||
id: `suggestion-${Date.now()}-0`,
|
id: `suggestion-${Date.now()}-0`,
|
||||||
category: 'High',
|
category: 'High',
|
||||||
description: 'Sanitize user input before rendering to prevent XSS',
|
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,
|
priority: 1,
|
||||||
reasoning: 'User input is rendered without proper sanitization',
|
reasoning: 'User input is rendered without proper sanitization',
|
||||||
},
|
},
|
||||||
@@ -2020,12 +2065,6 @@ async function simulateSuggestionsGeneration(suggestionType: SuggestionType = 'f
|
|||||||
id: `suggestion-${Date.now()}-1`,
|
id: `suggestion-${Date.now()}-1`,
|
||||||
category: 'Medium',
|
category: 'Medium',
|
||||||
description: 'Add rate limiting to authentication endpoints',
|
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,
|
priority: 2,
|
||||||
reasoning: 'Prevents brute force attacks on authentication',
|
reasoning: 'Prevents brute force attacks on authentication',
|
||||||
},
|
},
|
||||||
@@ -2033,12 +2072,6 @@ async function simulateSuggestionsGeneration(suggestionType: SuggestionType = 'f
|
|||||||
id: `suggestion-${Date.now()}-2`,
|
id: `suggestion-${Date.now()}-2`,
|
||||||
category: 'Low',
|
category: 'Low',
|
||||||
description: 'Remove sensitive information from error messages',
|
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,
|
priority: 3,
|
||||||
reasoning: 'Error messages may leak implementation details',
|
reasoning: 'Error messages may leak implementation details',
|
||||||
},
|
},
|
||||||
@@ -2051,12 +2084,6 @@ async function simulateSuggestionsGeneration(suggestionType: SuggestionType = 'f
|
|||||||
id: `suggestion-${Date.now()}-0`,
|
id: `suggestion-${Date.now()}-0`,
|
||||||
category: 'Rendering',
|
category: 'Rendering',
|
||||||
description: 'Add React.memo to prevent unnecessary re-renders',
|
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,
|
priority: 1,
|
||||||
reasoning: "Components re-render even when props haven't changed",
|
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`,
|
id: `suggestion-${Date.now()}-1`,
|
||||||
category: 'Bundle Size',
|
category: 'Bundle Size',
|
||||||
description: 'Implement code splitting for route components',
|
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,
|
priority: 2,
|
||||||
reasoning: 'Initial bundle is larger than necessary',
|
reasoning: 'Initial bundle is larger than necessary',
|
||||||
},
|
},
|
||||||
@@ -2077,12 +2098,6 @@ async function simulateSuggestionsGeneration(suggestionType: SuggestionType = 'f
|
|||||||
id: `suggestion-${Date.now()}-2`,
|
id: `suggestion-${Date.now()}-2`,
|
||||||
category: 'Caching',
|
category: 'Caching',
|
||||||
description: 'Add memoization for expensive computations',
|
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,
|
priority: 3,
|
||||||
reasoning: 'Expensive computations run on every render',
|
reasoning: 'Expensive computations run on every render',
|
||||||
},
|
},
|
||||||
@@ -2095,12 +2110,6 @@ async function simulateSuggestionsGeneration(suggestionType: SuggestionType = 'f
|
|||||||
id: `suggestion-${Date.now()}-0`,
|
id: `suggestion-${Date.now()}-0`,
|
||||||
category: 'User Experience',
|
category: 'User Experience',
|
||||||
description: 'Add dark mode toggle with system preference detection',
|
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,
|
priority: 1,
|
||||||
reasoning: 'Dark mode is a standard feature that improves accessibility and user comfort',
|
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`,
|
id: `suggestion-${Date.now()}-1`,
|
||||||
category: 'Performance',
|
category: 'Performance',
|
||||||
description: 'Implement lazy loading for heavy components',
|
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,
|
priority: 2,
|
||||||
reasoning: 'Improves initial load time and reduces bundle size',
|
reasoning: 'Improves initial load time and reduces bundle size',
|
||||||
},
|
},
|
||||||
@@ -2120,12 +2124,6 @@ async function simulateSuggestionsGeneration(suggestionType: SuggestionType = 'f
|
|||||||
id: `suggestion-${Date.now()}-2`,
|
id: `suggestion-${Date.now()}-2`,
|
||||||
category: 'Accessibility',
|
category: 'Accessibility',
|
||||||
description: 'Add keyboard navigation support throughout the app',
|
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,
|
priority: 3,
|
||||||
reasoning: 'Improves accessibility for users who rely on keyboard navigation',
|
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
|
// Utility functions for project management
|
||||||
|
|
||||||
export interface Project {
|
export interface Project {
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ import type {
|
|||||||
SuggestionsEvent,
|
SuggestionsEvent,
|
||||||
SpecRegenerationEvent,
|
SpecRegenerationEvent,
|
||||||
SuggestionType,
|
SuggestionType,
|
||||||
|
GitHubAPI,
|
||||||
|
GitHubIssue,
|
||||||
|
GitHubPR,
|
||||||
} from './electron';
|
} from './electron';
|
||||||
import type { Message, SessionListItem } from '@/types/electron';
|
import type { Message, SessionListItem } from '@/types/electron';
|
||||||
import type { Feature, ClaudeUsageResponse } from '@/store/app-store';
|
import type { Feature, ClaudeUsageResponse } from '@/store/app-store';
|
||||||
@@ -743,6 +746,13 @@ export class HttpApiClient implements ElectronAPI {
|
|||||||
}> => this.get('/api/running-agents'),
|
}> => 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 API
|
||||||
workspace = {
|
workspace = {
|
||||||
getConfig: (): Promise<{
|
getConfig: (): Promise<{
|
||||||
|
|||||||
6
apps/ui/src/routes/github-issues.tsx
Normal file
6
apps/ui/src/routes/github-issues.tsx
Normal 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,
|
||||||
|
});
|
||||||
6
apps/ui/src/routes/github-prs.tsx
Normal file
6
apps/ui/src/routes/github-prs.tsx
Normal 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,
|
||||||
|
});
|
||||||
@@ -266,7 +266,6 @@ export interface Feature {
|
|||||||
titleGenerating?: boolean;
|
titleGenerating?: boolean;
|
||||||
category: string;
|
category: string;
|
||||||
description: string;
|
description: string;
|
||||||
steps: string[];
|
|
||||||
status: 'backlog' | 'in_progress' | 'waiting_approval' | 'verified' | 'completed';
|
status: 'backlog' | 'in_progress' | 'waiting_approval' | 'verified' | 'completed';
|
||||||
images?: FeatureImage[];
|
images?: FeatureImage[];
|
||||||
imagePaths?: FeatureImagePath[]; // Paths to temp files for agent context
|
imagePaths?: FeatureImagePath[]; // Paths to temp files for agent context
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ export interface Feature {
|
|||||||
titleGenerating?: boolean;
|
titleGenerating?: boolean;
|
||||||
category: string;
|
category: string;
|
||||||
description: string;
|
description: string;
|
||||||
steps?: string[];
|
|
||||||
passes?: boolean;
|
passes?: boolean;
|
||||||
priority?: number;
|
priority?: number;
|
||||||
status?: string;
|
status?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user