mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +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 { validatePathParams } from '../../middleware/validate-paths.js';
|
||||
import { createCheckGitHubRemoteHandler } from './routes/check-github-remote.js';
|
||||
import { createListIssuesHandler } from './routes/list-issues.js';
|
||||
import { createListPRsHandler } from './routes/list-prs.js';
|
||||
import { createValidateIssueHandler } from './routes/validate-issue.js';
|
||||
|
||||
export function createGitHubRoutes(): Router {
|
||||
const router = Router();
|
||||
|
||||
router.post('/check-remote', createCheckGitHubRemoteHandler());
|
||||
router.post('/issues', createListIssuesHandler());
|
||||
router.post('/prs', createListPRsHandler());
|
||||
router.post('/check-remote', validatePathParams('projectPath'), createCheckGitHubRemoteHandler());
|
||||
router.post('/issues', validatePathParams('projectPath'), createListIssuesHandler());
|
||||
router.post('/prs', validatePathParams('projectPath'), createListPRsHandler());
|
||||
router.post('/validate-issue', validatePathParams('projectPath'), createValidateIssueHandler());
|
||||
|
||||
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 { CircleDot, Loader2, RefreshCw, ExternalLink, CheckCircle2, Circle, X } from 'lucide-react';
|
||||
import { getElectronAPI, GitHubIssue } from '@/lib/electron';
|
||||
import {
|
||||
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 { Button } from '@/components/ui/button';
|
||||
import { Markdown } from '@/components/ui/markdown';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { toast } from 'sonner';
|
||||
import { ValidationDialog } from './github-issues-view/validation-dialog';
|
||||
|
||||
export function GitHubIssuesView() {
|
||||
const [openIssues, setOpenIssues] = useState<GitHubIssue[]>([]);
|
||||
@@ -13,6 +46,9 @@ export function GitHubIssuesView() {
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [error, setError] = useState<string | 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 fetchIssues = useCallback(async () => {
|
||||
@@ -57,6 +93,103 @@ export function GitHubIssuesView() {
|
||||
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 date = new Date(dateString);
|
||||
return date.toLocaleDateString('en-US', {
|
||||
@@ -184,6 +317,19 @@ export function GitHubIssuesView() {
|
||||
</span>
|
||||
</div>
|
||||
<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
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@@ -260,6 +406,16 @@ export function GitHubIssuesView() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Validation Dialog */}
|
||||
<ValidationDialog
|
||||
open={showValidationDialog}
|
||||
onOpenChange={setShowValidationDialog}
|
||||
issue={selectedIssue}
|
||||
validationResult={validationResult}
|
||||
isValidating={validating}
|
||||
onConvertToTask={handleConvertToTask}
|
||||
/>
|
||||
</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
|
||||
import type { SessionListItem, Message } from '@/types/electron';
|
||||
import type { ClaudeUsageResponse } from '@/store/app-store';
|
||||
import type {
|
||||
IssueValidationVerdict,
|
||||
IssueValidationConfidence,
|
||||
IssueComplexity,
|
||||
IssueValidationInput,
|
||||
IssueValidationResult,
|
||||
IssueValidationResponse,
|
||||
} from '@automaker/types';
|
||||
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 {
|
||||
name: string;
|
||||
isDirectory: boolean;
|
||||
@@ -156,6 +174,10 @@ export interface GitHubAPI {
|
||||
mergedPRs?: GitHubPR[];
|
||||
error?: string;
|
||||
}>;
|
||||
validateIssue: (
|
||||
projectPath: string,
|
||||
issue: IssueValidationInput
|
||||
) => Promise<IssueValidationResponse | { success: false; error: string }>;
|
||||
}
|
||||
|
||||
// Feature Suggestions types
|
||||
@@ -2631,6 +2653,22 @@ function createMockGitHubAPI(): GitHubAPI {
|
||||
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,
|
||||
GitHubIssue,
|
||||
GitHubPR,
|
||||
IssueValidationInput,
|
||||
} from './electron';
|
||||
import type { Message, SessionListItem } from '@/types/electron';
|
||||
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 }),
|
||||
listIssues: (projectPath: string) => this.post('/api/github/issues', { 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
|
||||
|
||||
@@ -81,3 +81,15 @@ export {
|
||||
THINKING_LEVEL_LABELS,
|
||||
getModelDisplayName,
|
||||
} 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