mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 08:53:36 +00:00
feat: Implement GitHub issue validation endpoint and UI integration
- Added a new endpoint for validating GitHub issues using the Claude SDK. - Introduced validation schema and logic to handle issue validation requests. - Updated GitHub routes to include the new validation route. - Enhanced the UI with a validation dialog and button to trigger issue validation. - Mapped issue complexity to feature priority for better task management. - Integrated validation results display in the UI, allowing users to convert validated issues into tasks.
This commit is contained in:
@@ -3,16 +3,19 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
|
import { validatePathParams } from '../../middleware/validate-paths.js';
|
||||||
import { createCheckGitHubRemoteHandler } from './routes/check-github-remote.js';
|
import { createCheckGitHubRemoteHandler } from './routes/check-github-remote.js';
|
||||||
import { createListIssuesHandler } from './routes/list-issues.js';
|
import { createListIssuesHandler } from './routes/list-issues.js';
|
||||||
import { createListPRsHandler } from './routes/list-prs.js';
|
import { createListPRsHandler } from './routes/list-prs.js';
|
||||||
|
import { createValidateIssueHandler } from './routes/validate-issue.js';
|
||||||
|
|
||||||
export function createGitHubRoutes(): Router {
|
export function createGitHubRoutes(): Router {
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.post('/check-remote', createCheckGitHubRemoteHandler());
|
router.post('/check-remote', validatePathParams('projectPath'), createCheckGitHubRemoteHandler());
|
||||||
router.post('/issues', createListIssuesHandler());
|
router.post('/issues', validatePathParams('projectPath'), createListIssuesHandler());
|
||||||
router.post('/prs', createListPRsHandler());
|
router.post('/prs', validatePathParams('projectPath'), createListPRsHandler());
|
||||||
|
router.post('/validate-issue', validatePathParams('projectPath'), createValidateIssueHandler());
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
|
|||||||
164
apps/server/src/routes/github/routes/validate-issue.ts
Normal file
164
apps/server/src/routes/github/routes/validate-issue.ts
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
/**
|
||||||
|
* POST /validate-issue endpoint - Validate a GitHub issue using Claude SDK
|
||||||
|
*
|
||||||
|
* Scans the codebase to determine if an issue is valid, invalid, or needs clarification.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Request, Response } from 'express';
|
||||||
|
import { query } from '@anthropic-ai/claude-agent-sdk';
|
||||||
|
import { createLogger } from '@automaker/utils';
|
||||||
|
import type { IssueValidationResult } from '@automaker/types';
|
||||||
|
import { createSuggestionsOptions } from '../../../lib/sdk-options.js';
|
||||||
|
import {
|
||||||
|
issueValidationSchema,
|
||||||
|
ISSUE_VALIDATION_SYSTEM_PROMPT,
|
||||||
|
buildValidationPrompt,
|
||||||
|
} from './validation-schema.js';
|
||||||
|
import { getErrorMessage, logError } from './common.js';
|
||||||
|
|
||||||
|
const logger = createLogger('IssueValidation');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request body for issue validation
|
||||||
|
*/
|
||||||
|
interface ValidateIssueRequestBody {
|
||||||
|
projectPath: string;
|
||||||
|
issueNumber: number;
|
||||||
|
issueTitle: string;
|
||||||
|
issueBody: string;
|
||||||
|
issueLabels?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the handler for validating GitHub issues against the codebase.
|
||||||
|
*
|
||||||
|
* Uses Claude SDK with:
|
||||||
|
* - Read-only tools (Read, Glob, Grep) for codebase analysis
|
||||||
|
* - JSON schema structured output for reliable parsing
|
||||||
|
* - System prompt guiding the validation process
|
||||||
|
*/
|
||||||
|
export function createValidateIssueHandler() {
|
||||||
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
|
// Declare timeoutId outside try block for proper cleanup
|
||||||
|
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { projectPath, issueNumber, issueTitle, issueBody, issueLabels } =
|
||||||
|
req.body as ValidateIssueRequestBody;
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if (!projectPath) {
|
||||||
|
res.status(400).json({ success: false, error: 'projectPath is required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!issueNumber || typeof issueNumber !== 'number') {
|
||||||
|
res
|
||||||
|
.status(400)
|
||||||
|
.json({ success: false, error: 'issueNumber is required and must be a number' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!issueTitle || typeof issueTitle !== 'string') {
|
||||||
|
res.status(400).json({ success: false, error: 'issueTitle is required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof issueBody !== 'string') {
|
||||||
|
res.status(400).json({ success: false, error: 'issueBody must be a string' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Validating issue #${issueNumber}: ${issueTitle}`);
|
||||||
|
|
||||||
|
// Build the prompt
|
||||||
|
const prompt = buildValidationPrompt(issueNumber, issueTitle, issueBody, issueLabels);
|
||||||
|
|
||||||
|
// Create abort controller with 2 minute timeout for validation
|
||||||
|
const abortController = new AbortController();
|
||||||
|
const VALIDATION_TIMEOUT_MS = 120000; // 2 minutes
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
logger.warn(`Validation timeout reached after ${VALIDATION_TIMEOUT_MS}ms`);
|
||||||
|
abortController.abort();
|
||||||
|
}, VALIDATION_TIMEOUT_MS);
|
||||||
|
|
||||||
|
// Create SDK options with structured output and abort controller
|
||||||
|
const options = createSuggestionsOptions({
|
||||||
|
cwd: projectPath,
|
||||||
|
systemPrompt: ISSUE_VALIDATION_SYSTEM_PROMPT,
|
||||||
|
abortController,
|
||||||
|
outputFormat: {
|
||||||
|
type: 'json_schema',
|
||||||
|
schema: issueValidationSchema as Record<string, unknown>,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Execute the query
|
||||||
|
const stream = query({ prompt, options });
|
||||||
|
let validationResult: IssueValidationResult | null = null;
|
||||||
|
let responseText = '';
|
||||||
|
|
||||||
|
for await (const msg of stream) {
|
||||||
|
// Collect assistant text for debugging/fallback
|
||||||
|
if (msg.type === 'assistant' && msg.message?.content) {
|
||||||
|
for (const block of msg.message.content) {
|
||||||
|
if (block.type === 'text') {
|
||||||
|
responseText += block.text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract structured output on success
|
||||||
|
if (msg.type === 'result' && msg.subtype === 'success') {
|
||||||
|
const resultMsg = msg as { structured_output?: IssueValidationResult };
|
||||||
|
if (resultMsg.structured_output) {
|
||||||
|
validationResult = resultMsg.structured_output;
|
||||||
|
logger.debug('Received structured output:', validationResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle errors
|
||||||
|
if (msg.type === 'result') {
|
||||||
|
const resultMsg = msg as { subtype?: string };
|
||||||
|
if (resultMsg.subtype === 'error_max_structured_output_retries') {
|
||||||
|
logger.error('Failed to produce valid structured output after retries');
|
||||||
|
throw new Error('Could not produce valid validation output');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Require structured output - no fragile fallback parsing
|
||||||
|
if (!validationResult) {
|
||||||
|
logger.error('No structured output received from Claude SDK');
|
||||||
|
logger.debug('Raw response text:', responseText);
|
||||||
|
throw new Error('Validation failed: no structured output received');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the timeout since we completed successfully
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
logger.info(`Issue #${issueNumber} validation complete: ${validationResult.verdict}`);
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
issueNumber,
|
||||||
|
validation: validationResult,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// Clear timeout on error as well (if it was set)
|
||||||
|
if (timeoutId) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
|
||||||
|
logError(error, `Issue validation failed`);
|
||||||
|
logger.error('Issue validation error:', error);
|
||||||
|
|
||||||
|
// Check if response already sent
|
||||||
|
if (!res.headersSent) {
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: getErrorMessage(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
138
apps/server/src/routes/github/routes/validation-schema.ts
Normal file
138
apps/server/src/routes/github/routes/validation-schema.ts
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
/**
|
||||||
|
* Issue Validation Schema and System Prompt
|
||||||
|
*
|
||||||
|
* Defines the JSON schema for Claude's structured output and
|
||||||
|
* the system prompt that guides the validation process.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON Schema for issue validation structured output.
|
||||||
|
* Used with Claude SDK's outputFormat option to ensure reliable parsing.
|
||||||
|
*/
|
||||||
|
export const issueValidationSchema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
verdict: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['valid', 'invalid', 'needs_clarification'],
|
||||||
|
description: 'The validation verdict for the issue',
|
||||||
|
},
|
||||||
|
confidence: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['high', 'medium', 'low'],
|
||||||
|
description: 'How confident the AI is in its assessment',
|
||||||
|
},
|
||||||
|
reasoning: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Detailed explanation of the verdict',
|
||||||
|
},
|
||||||
|
bugConfirmed: {
|
||||||
|
type: 'boolean',
|
||||||
|
description: 'For bug reports: whether the bug was confirmed in the codebase',
|
||||||
|
},
|
||||||
|
relatedFiles: {
|
||||||
|
type: 'array',
|
||||||
|
items: { type: 'string' },
|
||||||
|
description: 'Files related to the issue found during analysis',
|
||||||
|
},
|
||||||
|
suggestedFix: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Suggested approach to fix or implement the issue',
|
||||||
|
},
|
||||||
|
missingInfo: {
|
||||||
|
type: 'array',
|
||||||
|
items: { type: 'string' },
|
||||||
|
description: 'Information needed when verdict is needs_clarification',
|
||||||
|
},
|
||||||
|
estimatedComplexity: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['trivial', 'simple', 'moderate', 'complex', 'very_complex'],
|
||||||
|
description: 'Estimated effort to address the issue',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['verdict', 'confidence', 'reasoning'],
|
||||||
|
additionalProperties: false,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* System prompt that guides Claude in validating GitHub issues.
|
||||||
|
* Instructs the model to use read-only tools to analyze the codebase.
|
||||||
|
*/
|
||||||
|
export const ISSUE_VALIDATION_SYSTEM_PROMPT = `You are an expert code analyst validating GitHub issues against a codebase.
|
||||||
|
|
||||||
|
Your task is to analyze a GitHub issue and determine if it's valid by scanning the codebase.
|
||||||
|
|
||||||
|
## Validation Process
|
||||||
|
|
||||||
|
1. **Read the issue carefully** - Understand what is being reported or requested
|
||||||
|
2. **Search the codebase** - Use Glob to find relevant files by pattern, Grep to search for keywords
|
||||||
|
3. **Examine the code** - Use Read to look at the actual implementation in relevant files
|
||||||
|
4. **Form your verdict** - Based on your analysis, determine if the issue is valid
|
||||||
|
|
||||||
|
## Verdicts
|
||||||
|
|
||||||
|
- **valid**: The issue describes a real problem that exists in the codebase, or a clear feature request that can be implemented. The referenced files/components exist and the issue is actionable.
|
||||||
|
|
||||||
|
- **invalid**: The issue describes behavior that doesn't exist, references non-existent files or components, is based on a misunderstanding of the code, or the described "bug" is actually expected behavior.
|
||||||
|
|
||||||
|
- **needs_clarification**: The issue lacks sufficient detail to verify. Specify what additional information is needed in the missingInfo field.
|
||||||
|
|
||||||
|
## For Bug Reports, Check:
|
||||||
|
- Do the referenced files/components exist?
|
||||||
|
- Does the code match what the issue describes?
|
||||||
|
- Is the described behavior actually a bug or expected?
|
||||||
|
- Can you locate the code that would cause the reported issue?
|
||||||
|
|
||||||
|
## For Feature Requests, Check:
|
||||||
|
- Does the feature already exist?
|
||||||
|
- Is the implementation location clear?
|
||||||
|
- Is the request technically feasible given the codebase structure?
|
||||||
|
|
||||||
|
## Response Guidelines
|
||||||
|
|
||||||
|
- **Always include relatedFiles** when you find relevant code
|
||||||
|
- **Set bugConfirmed to true** only if you can definitively confirm a bug exists in the code
|
||||||
|
- **Provide a suggestedFix** when you have a clear idea of how to address the issue
|
||||||
|
- **Use missingInfo** when the verdict is needs_clarification to list what's needed
|
||||||
|
- **Set estimatedComplexity** to help prioritize:
|
||||||
|
- trivial: Simple text changes, one-line fixes
|
||||||
|
- simple: Small changes to one file
|
||||||
|
- moderate: Changes to multiple files or moderate logic changes
|
||||||
|
- complex: Significant refactoring or new feature implementation
|
||||||
|
- very_complex: Major architectural changes or cross-cutting concerns
|
||||||
|
|
||||||
|
Be thorough in your analysis but focus on files that are directly relevant to the issue.`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the user prompt for issue validation.
|
||||||
|
*
|
||||||
|
* Creates a structured prompt that includes the issue details for Claude
|
||||||
|
* to analyze against the codebase.
|
||||||
|
*
|
||||||
|
* @param issueNumber - The GitHub issue number
|
||||||
|
* @param issueTitle - The issue title
|
||||||
|
* @param issueBody - The issue body/description
|
||||||
|
* @param issueLabels - Optional array of label names
|
||||||
|
* @returns Formatted prompt string for the validation request
|
||||||
|
*/
|
||||||
|
export function buildValidationPrompt(
|
||||||
|
issueNumber: number,
|
||||||
|
issueTitle: string,
|
||||||
|
issueBody: string,
|
||||||
|
issueLabels?: string[]
|
||||||
|
): string {
|
||||||
|
const labelsSection = issueLabels?.length ? `\n\n**Labels:** ${issueLabels.join(', ')}` : '';
|
||||||
|
|
||||||
|
return `Please validate the following GitHub issue by analyzing the codebase:
|
||||||
|
|
||||||
|
## Issue #${issueNumber}: ${issueTitle}
|
||||||
|
${labelsSection}
|
||||||
|
|
||||||
|
### Description
|
||||||
|
|
||||||
|
${issueBody || '(No description provided)'}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Scan the codebase to verify this issue. Look for the files, components, or functionality mentioned. Determine if this issue is valid, invalid, or needs clarification.`;
|
||||||
|
}
|
||||||
@@ -1,10 +1,43 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { CircleDot, Loader2, RefreshCw, ExternalLink, CheckCircle2, Circle, X } from 'lucide-react';
|
import {
|
||||||
import { getElectronAPI, GitHubIssue } from '@/lib/electron';
|
CircleDot,
|
||||||
|
Loader2,
|
||||||
|
RefreshCw,
|
||||||
|
ExternalLink,
|
||||||
|
CheckCircle2,
|
||||||
|
Circle,
|
||||||
|
X,
|
||||||
|
Wand2,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import {
|
||||||
|
getElectronAPI,
|
||||||
|
GitHubIssue,
|
||||||
|
IssueValidationResult,
|
||||||
|
IssueComplexity,
|
||||||
|
} from '@/lib/electron';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map issue complexity to feature priority.
|
||||||
|
* Lower complexity issues get higher priority (1 = high, 2 = medium).
|
||||||
|
*/
|
||||||
|
function getFeaturePriority(complexity: IssueComplexity | undefined): number {
|
||||||
|
switch (complexity) {
|
||||||
|
case 'trivial':
|
||||||
|
case 'simple':
|
||||||
|
return 1; // High priority for easy wins
|
||||||
|
case 'moderate':
|
||||||
|
case 'complex':
|
||||||
|
case 'very_complex':
|
||||||
|
default:
|
||||||
|
return 2; // Medium priority for larger efforts
|
||||||
|
}
|
||||||
|
}
|
||||||
import { useAppStore } from '@/store/app-store';
|
import { useAppStore } from '@/store/app-store';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Markdown } from '@/components/ui/markdown';
|
import { Markdown } from '@/components/ui/markdown';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { ValidationDialog } from './github-issues-view/validation-dialog';
|
||||||
|
|
||||||
export function GitHubIssuesView() {
|
export function GitHubIssuesView() {
|
||||||
const [openIssues, setOpenIssues] = useState<GitHubIssue[]>([]);
|
const [openIssues, setOpenIssues] = useState<GitHubIssue[]>([]);
|
||||||
@@ -13,6 +46,9 @@ export function GitHubIssuesView() {
|
|||||||
const [refreshing, setRefreshing] = useState(false);
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [selectedIssue, setSelectedIssue] = useState<GitHubIssue | null>(null);
|
const [selectedIssue, setSelectedIssue] = useState<GitHubIssue | null>(null);
|
||||||
|
const [validating, setValidating] = useState(false);
|
||||||
|
const [validationResult, setValidationResult] = useState<IssueValidationResult | null>(null);
|
||||||
|
const [showValidationDialog, setShowValidationDialog] = useState(false);
|
||||||
const { currentProject } = useAppStore();
|
const { currentProject } = useAppStore();
|
||||||
|
|
||||||
const fetchIssues = useCallback(async () => {
|
const fetchIssues = useCallback(async () => {
|
||||||
@@ -57,6 +93,103 @@ export function GitHubIssuesView() {
|
|||||||
api.openExternalLink(url);
|
api.openExternalLink(url);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleValidateIssue = useCallback(
|
||||||
|
async (issue: GitHubIssue) => {
|
||||||
|
if (!currentProject?.path) {
|
||||||
|
toast.error('No project selected');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setValidating(true);
|
||||||
|
setValidationResult(null);
|
||||||
|
setShowValidationDialog(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const api = getElectronAPI();
|
||||||
|
if (api.github?.validateIssue) {
|
||||||
|
const result = await api.github.validateIssue(currentProject.path, {
|
||||||
|
issueNumber: issue.number,
|
||||||
|
issueTitle: issue.title,
|
||||||
|
issueBody: issue.body || '',
|
||||||
|
issueLabels: issue.labels.map((l) => l.name),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
setValidationResult(result.validation);
|
||||||
|
} else {
|
||||||
|
toast.error(result.error || 'Failed to validate issue');
|
||||||
|
setShowValidationDialog(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[GitHubIssuesView] Validation error:', err);
|
||||||
|
toast.error(err instanceof Error ? err.message : 'Failed to validate issue');
|
||||||
|
setShowValidationDialog(false);
|
||||||
|
} finally {
|
||||||
|
setValidating(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[currentProject?.path]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleConvertToTask = useCallback(
|
||||||
|
async (issue: GitHubIssue, validation: IssueValidationResult) => {
|
||||||
|
if (!currentProject?.path) {
|
||||||
|
toast.error('No project selected');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const api = getElectronAPI();
|
||||||
|
if (api.features?.create) {
|
||||||
|
// Build description from issue body + validation info
|
||||||
|
const description = [
|
||||||
|
`**From GitHub Issue #${issue.number}**`,
|
||||||
|
'',
|
||||||
|
issue.body || 'No description provided.',
|
||||||
|
'',
|
||||||
|
'---',
|
||||||
|
'',
|
||||||
|
'**AI Validation Analysis:**',
|
||||||
|
validation.reasoning,
|
||||||
|
validation.suggestedFix ? `\n**Suggested Approach:**\n${validation.suggestedFix}` : '',
|
||||||
|
validation.relatedFiles?.length
|
||||||
|
? `\n**Related Files:**\n${validation.relatedFiles.map((f) => `- \`${f}\``).join('\n')}`
|
||||||
|
: '',
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
const feature = {
|
||||||
|
id: `issue-${issue.number}-${crypto.randomUUID()}`,
|
||||||
|
title: issue.title,
|
||||||
|
description,
|
||||||
|
category: 'From GitHub',
|
||||||
|
status: 'backlog' as const,
|
||||||
|
passes: false,
|
||||||
|
priority: getFeaturePriority(validation.estimatedComplexity),
|
||||||
|
model: 'opus' as const,
|
||||||
|
thinkingLevel: 'none' as const,
|
||||||
|
branchName: '',
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await api.features.create(currentProject.path, feature);
|
||||||
|
if (result.success) {
|
||||||
|
toast.success(`Created task: ${issue.title}`);
|
||||||
|
} else {
|
||||||
|
toast.error(result.error || 'Failed to create task');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[GitHubIssuesView] Convert to task error:', err);
|
||||||
|
toast.error(err instanceof Error ? err.message : 'Failed to create task');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[currentProject?.path]
|
||||||
|
);
|
||||||
|
|
||||||
const formatDate = (dateString: string) => {
|
const formatDate = (dateString: string) => {
|
||||||
const date = new Date(dateString);
|
const date = new Date(dateString);
|
||||||
return date.toLocaleDateString('en-US', {
|
return date.toLocaleDateString('en-US', {
|
||||||
@@ -184,6 +317,19 @@ export function GitHubIssuesView() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 flex-shrink-0">
|
<div className="flex items-center gap-2 flex-shrink-0">
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleValidateIssue(selectedIssue)}
|
||||||
|
disabled={validating}
|
||||||
|
>
|
||||||
|
{validating ? (
|
||||||
|
<Loader2 className="h-4 w-4 mr-1 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Wand2 className="h-4 w-4 mr-1" />
|
||||||
|
)}
|
||||||
|
Validate with AI
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -260,6 +406,16 @@ export function GitHubIssuesView() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Validation Dialog */}
|
||||||
|
<ValidationDialog
|
||||||
|
open={showValidationDialog}
|
||||||
|
onOpenChange={setShowValidationDialog}
|
||||||
|
issue={selectedIssue}
|
||||||
|
validationResult={validationResult}
|
||||||
|
isValidating={validating}
|
||||||
|
onConvertToTask={handleConvertToTask}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,238 @@
|
|||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
CheckCircle2,
|
||||||
|
XCircle,
|
||||||
|
AlertCircle,
|
||||||
|
FileCode,
|
||||||
|
Lightbulb,
|
||||||
|
AlertTriangle,
|
||||||
|
Loader2,
|
||||||
|
Plus,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import type {
|
||||||
|
IssueValidationResult,
|
||||||
|
IssueValidationVerdict,
|
||||||
|
IssueValidationConfidence,
|
||||||
|
IssueComplexity,
|
||||||
|
GitHubIssue,
|
||||||
|
} from '@/lib/electron';
|
||||||
|
|
||||||
|
interface ValidationDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
issue: GitHubIssue | null;
|
||||||
|
validationResult: IssueValidationResult | null;
|
||||||
|
isValidating: boolean;
|
||||||
|
onConvertToTask?: (issue: GitHubIssue, validation: IssueValidationResult) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const verdictConfig: Record<
|
||||||
|
IssueValidationVerdict,
|
||||||
|
{ label: string; color: string; bgColor: string; icon: typeof CheckCircle2 }
|
||||||
|
> = {
|
||||||
|
valid: {
|
||||||
|
label: 'Valid',
|
||||||
|
color: 'text-green-500',
|
||||||
|
bgColor: 'bg-green-500/10',
|
||||||
|
icon: CheckCircle2,
|
||||||
|
},
|
||||||
|
invalid: {
|
||||||
|
label: 'Invalid',
|
||||||
|
color: 'text-red-500',
|
||||||
|
bgColor: 'bg-red-500/10',
|
||||||
|
icon: XCircle,
|
||||||
|
},
|
||||||
|
needs_clarification: {
|
||||||
|
label: 'Needs Clarification',
|
||||||
|
color: 'text-yellow-500',
|
||||||
|
bgColor: 'bg-yellow-500/10',
|
||||||
|
icon: AlertCircle,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const confidenceConfig: Record<IssueValidationConfidence, { label: string; color: string }> = {
|
||||||
|
high: { label: 'High Confidence', color: 'text-green-500' },
|
||||||
|
medium: { label: 'Medium Confidence', color: 'text-yellow-500' },
|
||||||
|
low: { label: 'Low Confidence', color: 'text-orange-500' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const complexityConfig: Record<IssueComplexity, { label: string; color: string }> = {
|
||||||
|
trivial: { label: 'Trivial', color: 'text-green-500' },
|
||||||
|
simple: { label: 'Simple', color: 'text-blue-500' },
|
||||||
|
moderate: { label: 'Moderate', color: 'text-yellow-500' },
|
||||||
|
complex: { label: 'Complex', color: 'text-orange-500' },
|
||||||
|
very_complex: { label: 'Very Complex', color: 'text-red-500' },
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ValidationDialog({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
issue,
|
||||||
|
validationResult,
|
||||||
|
isValidating,
|
||||||
|
onConvertToTask,
|
||||||
|
}: ValidationDialogProps) {
|
||||||
|
if (!issue) return null;
|
||||||
|
|
||||||
|
const handleConvertToTask = () => {
|
||||||
|
if (validationResult && onConvertToTask) {
|
||||||
|
onConvertToTask(issue, validationResult);
|
||||||
|
onOpenChange(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">Issue Validation Result</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
#{issue.number}: {issue.title}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{isValidating ? (
|
||||||
|
<div className="flex flex-col items-center justify-center py-12 space-y-4">
|
||||||
|
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||||
|
<p className="text-sm text-muted-foreground">Analyzing codebase to validate issue...</p>
|
||||||
|
</div>
|
||||||
|
) : validationResult ? (
|
||||||
|
<div className="space-y-6 py-4">
|
||||||
|
{/* Verdict Badge */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{(() => {
|
||||||
|
const config = verdictConfig[validationResult.verdict];
|
||||||
|
const Icon = config.icon;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={cn('p-2 rounded-lg', config.bgColor)}>
|
||||||
|
<Icon className={cn('h-6 w-6', config.color)} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className={cn('text-lg font-semibold', config.color)}>{config.label}</p>
|
||||||
|
<p
|
||||||
|
className={cn(
|
||||||
|
'text-sm',
|
||||||
|
confidenceConfig[validationResult.confidence].color
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{confidenceConfig[validationResult.confidence].label}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
{validationResult.estimatedComplexity && (
|
||||||
|
<div className="text-right">
|
||||||
|
<p className="text-xs text-muted-foreground">Estimated Complexity</p>
|
||||||
|
<p
|
||||||
|
className={cn(
|
||||||
|
'text-sm font-medium',
|
||||||
|
complexityConfig[validationResult.estimatedComplexity].color
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{complexityConfig[validationResult.estimatedComplexity].label}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bug Confirmed Badge */}
|
||||||
|
{validationResult.bugConfirmed && (
|
||||||
|
<div className="flex items-center gap-2 p-3 rounded-lg bg-red-500/10 border border-red-500/20">
|
||||||
|
<AlertTriangle className="h-5 w-5 text-red-500 flex-shrink-0" />
|
||||||
|
<span className="text-sm font-medium text-red-500">Bug Confirmed in Codebase</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Reasoning */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h4 className="text-sm font-medium flex items-center gap-2">
|
||||||
|
<Lightbulb className="h-4 w-4 text-muted-foreground" />
|
||||||
|
Analysis
|
||||||
|
</h4>
|
||||||
|
<p className="text-sm text-muted-foreground leading-relaxed whitespace-pre-wrap">
|
||||||
|
{validationResult.reasoning}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Related Files */}
|
||||||
|
{validationResult.relatedFiles && validationResult.relatedFiles.length > 0 && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h4 className="text-sm font-medium flex items-center gap-2">
|
||||||
|
<FileCode className="h-4 w-4 text-muted-foreground" />
|
||||||
|
Related Files
|
||||||
|
</h4>
|
||||||
|
<div className="space-y-1">
|
||||||
|
{validationResult.relatedFiles.map((file, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="text-sm font-mono bg-muted/50 px-2 py-1 rounded text-muted-foreground"
|
||||||
|
>
|
||||||
|
{file}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Suggested Fix */}
|
||||||
|
{validationResult.suggestedFix && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h4 className="text-sm font-medium">Suggested Approach</h4>
|
||||||
|
<p className="text-sm text-muted-foreground leading-relaxed whitespace-pre-wrap bg-muted/30 p-3 rounded-lg border border-border">
|
||||||
|
{validationResult.suggestedFix}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Missing Info (for needs_clarification) */}
|
||||||
|
{validationResult.missingInfo && validationResult.missingInfo.length > 0 && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h4 className="text-sm font-medium flex items-center gap-2">
|
||||||
|
<AlertCircle className="h-4 w-4 text-yellow-500" />
|
||||||
|
Missing Information
|
||||||
|
</h4>
|
||||||
|
<ul className="space-y-1 list-disc list-inside">
|
||||||
|
{validationResult.missingInfo.map((info, index) => (
|
||||||
|
<li key={index} className="text-sm text-muted-foreground">
|
||||||
|
{info}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||||
|
<AlertCircle className="h-8 w-8 text-muted-foreground mb-4" />
|
||||||
|
<p className="text-sm text-muted-foreground">No validation result available.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="ghost" onClick={() => onOpenChange(false)}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
{validationResult?.verdict === 'valid' && onConvertToTask && (
|
||||||
|
<Button onClick={handleConvertToTask}>
|
||||||
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
|
Convert to Task
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,8 +1,26 @@
|
|||||||
// Type definitions for Electron IPC API
|
// Type definitions for Electron IPC API
|
||||||
import type { SessionListItem, Message } from '@/types/electron';
|
import type { SessionListItem, Message } from '@/types/electron';
|
||||||
import type { ClaudeUsageResponse } from '@/store/app-store';
|
import type { ClaudeUsageResponse } from '@/store/app-store';
|
||||||
|
import type {
|
||||||
|
IssueValidationVerdict,
|
||||||
|
IssueValidationConfidence,
|
||||||
|
IssueComplexity,
|
||||||
|
IssueValidationInput,
|
||||||
|
IssueValidationResult,
|
||||||
|
IssueValidationResponse,
|
||||||
|
} from '@automaker/types';
|
||||||
import { getJSON, setJSON, removeItem } from './storage';
|
import { getJSON, setJSON, removeItem } from './storage';
|
||||||
|
|
||||||
|
// Re-export issue validation types for use in components
|
||||||
|
export type {
|
||||||
|
IssueValidationVerdict,
|
||||||
|
IssueValidationConfidence,
|
||||||
|
IssueComplexity,
|
||||||
|
IssueValidationInput,
|
||||||
|
IssueValidationResult,
|
||||||
|
IssueValidationResponse,
|
||||||
|
};
|
||||||
|
|
||||||
export interface FileEntry {
|
export interface FileEntry {
|
||||||
name: string;
|
name: string;
|
||||||
isDirectory: boolean;
|
isDirectory: boolean;
|
||||||
@@ -156,6 +174,10 @@ export interface GitHubAPI {
|
|||||||
mergedPRs?: GitHubPR[];
|
mergedPRs?: GitHubPR[];
|
||||||
error?: string;
|
error?: string;
|
||||||
}>;
|
}>;
|
||||||
|
validateIssue: (
|
||||||
|
projectPath: string,
|
||||||
|
issue: IssueValidationInput
|
||||||
|
) => Promise<IssueValidationResponse | { success: false; error: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Feature Suggestions types
|
// Feature Suggestions types
|
||||||
@@ -2631,6 +2653,22 @@ function createMockGitHubAPI(): GitHubAPI {
|
|||||||
mergedPRs: [],
|
mergedPRs: [],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
validateIssue: async (projectPath: string, issue: IssueValidationInput) => {
|
||||||
|
console.log('[Mock] Validating GitHub issue:', { projectPath, issue });
|
||||||
|
// Return a mock validation result
|
||||||
|
return {
|
||||||
|
success: true as const,
|
||||||
|
issueNumber: issue.issueNumber,
|
||||||
|
validation: {
|
||||||
|
verdict: 'valid' as const,
|
||||||
|
confidence: 'medium' as const,
|
||||||
|
reasoning:
|
||||||
|
'This is a mock validation. In production, Claude SDK would analyze the codebase to validate this issue.',
|
||||||
|
relatedFiles: ['src/components/example.tsx'],
|
||||||
|
estimatedComplexity: 'moderate' as const,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import type {
|
|||||||
GitHubAPI,
|
GitHubAPI,
|
||||||
GitHubIssue,
|
GitHubIssue,
|
||||||
GitHubPR,
|
GitHubPR,
|
||||||
|
IssueValidationInput,
|
||||||
} 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';
|
||||||
@@ -751,6 +752,8 @@ export class HttpApiClient implements ElectronAPI {
|
|||||||
checkRemote: (projectPath: string) => this.post('/api/github/check-remote', { projectPath }),
|
checkRemote: (projectPath: string) => this.post('/api/github/check-remote', { projectPath }),
|
||||||
listIssues: (projectPath: string) => this.post('/api/github/issues', { projectPath }),
|
listIssues: (projectPath: string) => this.post('/api/github/issues', { projectPath }),
|
||||||
listPRs: (projectPath: string) => this.post('/api/github/prs', { projectPath }),
|
listPRs: (projectPath: string) => this.post('/api/github/prs', { projectPath }),
|
||||||
|
validateIssue: (projectPath: string, issue: IssueValidationInput) =>
|
||||||
|
this.post('/api/github/validate-issue', { projectPath, ...issue }),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Workspace API
|
// Workspace API
|
||||||
|
|||||||
@@ -81,3 +81,15 @@ export {
|
|||||||
THINKING_LEVEL_LABELS,
|
THINKING_LEVEL_LABELS,
|
||||||
getModelDisplayName,
|
getModelDisplayName,
|
||||||
} from './model-display.js';
|
} from './model-display.js';
|
||||||
|
|
||||||
|
// Issue validation types
|
||||||
|
export type {
|
||||||
|
IssueValidationVerdict,
|
||||||
|
IssueValidationConfidence,
|
||||||
|
IssueComplexity,
|
||||||
|
IssueValidationInput,
|
||||||
|
IssueValidationRequest,
|
||||||
|
IssueValidationResult,
|
||||||
|
IssueValidationResponse,
|
||||||
|
IssueValidationErrorResponse,
|
||||||
|
} from './issue-validation.js';
|
||||||
|
|||||||
78
libs/types/src/issue-validation.ts
Normal file
78
libs/types/src/issue-validation.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
/**
|
||||||
|
* Issue Validation Types
|
||||||
|
*
|
||||||
|
* Types for validating GitHub issues against the codebase using Claude SDK.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verdict from issue validation
|
||||||
|
*/
|
||||||
|
export type IssueValidationVerdict = 'valid' | 'invalid' | 'needs_clarification';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Confidence level of the validation
|
||||||
|
*/
|
||||||
|
export type IssueValidationConfidence = 'high' | 'medium' | 'low';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Complexity estimation for valid issues
|
||||||
|
*/
|
||||||
|
export type IssueComplexity = 'trivial' | 'simple' | 'moderate' | 'complex' | 'very_complex';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Issue data for validation (without projectPath)
|
||||||
|
* Used by UI when calling the validation API
|
||||||
|
*/
|
||||||
|
export interface IssueValidationInput {
|
||||||
|
issueNumber: number;
|
||||||
|
issueTitle: string;
|
||||||
|
issueBody: string;
|
||||||
|
issueLabels?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full request payload for issue validation endpoint
|
||||||
|
* Includes projectPath for server-side handling
|
||||||
|
*/
|
||||||
|
export interface IssueValidationRequest extends IssueValidationInput {
|
||||||
|
projectPath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result from Claude's issue validation analysis
|
||||||
|
*/
|
||||||
|
export interface IssueValidationResult {
|
||||||
|
/** Whether the issue is valid, invalid, or needs clarification */
|
||||||
|
verdict: IssueValidationVerdict;
|
||||||
|
/** How confident the AI is in its assessment */
|
||||||
|
confidence: IssueValidationConfidence;
|
||||||
|
/** Detailed explanation of the verdict */
|
||||||
|
reasoning: string;
|
||||||
|
/** For bug reports: whether the bug was confirmed in the codebase */
|
||||||
|
bugConfirmed?: boolean;
|
||||||
|
/** Files related to the issue found during analysis */
|
||||||
|
relatedFiles?: string[];
|
||||||
|
/** Suggested approach to fix or implement */
|
||||||
|
suggestedFix?: string;
|
||||||
|
/** Information that's missing and needed for validation (when verdict = needs_clarification) */
|
||||||
|
missingInfo?: string[];
|
||||||
|
/** Estimated effort to address the issue */
|
||||||
|
estimatedComplexity?: IssueComplexity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Successful response from validate-issue endpoint
|
||||||
|
*/
|
||||||
|
export interface IssueValidationResponse {
|
||||||
|
success: true;
|
||||||
|
issueNumber: number;
|
||||||
|
validation: IssueValidationResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error response from validate-issue endpoint
|
||||||
|
*/
|
||||||
|
export interface IssueValidationErrorResponse {
|
||||||
|
success: false;
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user