mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-01 20:23:36 +00:00
Merge pull request #308 from AutoMaker-Org/feat/github-issue-comments
feat: add GitHub issue comments display and AI validation integration
This commit is contained in:
@@ -8,6 +8,7 @@ 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 { createListCommentsHandler } from './routes/list-comments.js';
|
||||
import { createValidateIssueHandler } from './routes/validate-issue.js';
|
||||
import {
|
||||
createValidationStatusHandler,
|
||||
@@ -27,6 +28,7 @@ export function createGitHubRoutes(
|
||||
router.post('/check-remote', validatePathParams('projectPath'), createCheckGitHubRemoteHandler());
|
||||
router.post('/issues', validatePathParams('projectPath'), createListIssuesHandler());
|
||||
router.post('/prs', validatePathParams('projectPath'), createListPRsHandler());
|
||||
router.post('/issue-comments', validatePathParams('projectPath'), createListCommentsHandler());
|
||||
router.post(
|
||||
'/validate-issue',
|
||||
validatePathParams('projectPath'),
|
||||
|
||||
212
apps/server/src/routes/github/routes/list-comments.ts
Normal file
212
apps/server/src/routes/github/routes/list-comments.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
/**
|
||||
* POST /issue-comments endpoint - Fetch comments for a GitHub issue
|
||||
*/
|
||||
|
||||
import { spawn } from 'child_process';
|
||||
import type { Request, Response } from 'express';
|
||||
import type { GitHubComment, IssueCommentsResult } from '@automaker/types';
|
||||
import { execEnv, getErrorMessage, logError } from './common.js';
|
||||
import { checkGitHubRemote } from './check-github-remote.js';
|
||||
|
||||
interface ListCommentsRequest {
|
||||
projectPath: string;
|
||||
issueNumber: number;
|
||||
cursor?: string;
|
||||
}
|
||||
|
||||
interface GraphQLComment {
|
||||
id: string;
|
||||
author: {
|
||||
login: string;
|
||||
avatarUrl?: string;
|
||||
} | null;
|
||||
body: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface GraphQLResponse {
|
||||
data?: {
|
||||
repository?: {
|
||||
issue?: {
|
||||
comments: {
|
||||
totalCount: number;
|
||||
pageInfo: {
|
||||
hasNextPage: boolean;
|
||||
endCursor: string | null;
|
||||
};
|
||||
nodes: GraphQLComment[];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
errors?: Array<{ message: string }>;
|
||||
}
|
||||
|
||||
/** Timeout for GitHub API requests in milliseconds */
|
||||
const GITHUB_API_TIMEOUT_MS = 30000;
|
||||
|
||||
/**
|
||||
* Validate cursor format (GraphQL cursors are typically base64 strings)
|
||||
*/
|
||||
function isValidCursor(cursor: string): boolean {
|
||||
return /^[A-Za-z0-9+/=]+$/.test(cursor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch comments for a specific issue using GitHub GraphQL API
|
||||
*/
|
||||
async function fetchIssueComments(
|
||||
projectPath: string,
|
||||
owner: string,
|
||||
repo: string,
|
||||
issueNumber: number,
|
||||
cursor?: string
|
||||
): Promise<IssueCommentsResult> {
|
||||
// Validate cursor format to prevent potential injection
|
||||
if (cursor && !isValidCursor(cursor)) {
|
||||
throw new Error('Invalid cursor format');
|
||||
}
|
||||
|
||||
// Use GraphQL variables instead of string interpolation for safety
|
||||
const query = `
|
||||
query GetIssueComments($owner: String!, $repo: String!, $issueNumber: Int!, $cursor: String) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
issue(number: $issueNumber) {
|
||||
comments(first: 50, after: $cursor) {
|
||||
totalCount
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
nodes {
|
||||
id
|
||||
author {
|
||||
login
|
||||
avatarUrl
|
||||
}
|
||||
body
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
const variables = {
|
||||
owner,
|
||||
repo,
|
||||
issueNumber,
|
||||
cursor: cursor || null,
|
||||
};
|
||||
|
||||
const requestBody = JSON.stringify({ query, variables });
|
||||
|
||||
const response = await new Promise<GraphQLResponse>((resolve, reject) => {
|
||||
const gh = spawn('gh', ['api', 'graphql', '--input', '-'], {
|
||||
cwd: projectPath,
|
||||
env: execEnv,
|
||||
});
|
||||
|
||||
// Add timeout to prevent hanging indefinitely
|
||||
const timeoutId = setTimeout(() => {
|
||||
gh.kill();
|
||||
reject(new Error('GitHub API request timed out'));
|
||||
}, GITHUB_API_TIMEOUT_MS);
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
gh.stdout.on('data', (data: Buffer) => (stdout += data.toString()));
|
||||
gh.stderr.on('data', (data: Buffer) => (stderr += data.toString()));
|
||||
|
||||
gh.on('close', (code) => {
|
||||
clearTimeout(timeoutId);
|
||||
if (code !== 0) {
|
||||
return reject(new Error(`gh process exited with code ${code}: ${stderr}`));
|
||||
}
|
||||
try {
|
||||
resolve(JSON.parse(stdout));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
|
||||
gh.stdin.write(requestBody);
|
||||
gh.stdin.end();
|
||||
});
|
||||
|
||||
if (response.errors && response.errors.length > 0) {
|
||||
throw new Error(response.errors[0].message);
|
||||
}
|
||||
|
||||
const commentsData = response.data?.repository?.issue?.comments;
|
||||
|
||||
if (!commentsData) {
|
||||
throw new Error('Issue not found or no comments data available');
|
||||
}
|
||||
|
||||
const comments: GitHubComment[] = commentsData.nodes.map((node) => ({
|
||||
id: node.id,
|
||||
author: {
|
||||
login: node.author?.login || 'ghost',
|
||||
avatarUrl: node.author?.avatarUrl,
|
||||
},
|
||||
body: node.body,
|
||||
createdAt: node.createdAt,
|
||||
updatedAt: node.updatedAt,
|
||||
}));
|
||||
|
||||
return {
|
||||
comments,
|
||||
totalCount: commentsData.totalCount,
|
||||
hasNextPage: commentsData.pageInfo.hasNextPage,
|
||||
endCursor: commentsData.pageInfo.endCursor || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function createListCommentsHandler() {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { projectPath, issueNumber, cursor } = req.body as ListCommentsRequest;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// First check if this is a GitHub repo and get owner/repo
|
||||
const remoteStatus = await checkGitHubRemote(projectPath);
|
||||
if (!remoteStatus.hasGitHubRemote || !remoteStatus.owner || !remoteStatus.repo) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Project does not have a GitHub remote',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await fetchIssueComments(
|
||||
projectPath,
|
||||
remoteStatus.owner,
|
||||
remoteStatus.repo,
|
||||
issueNumber,
|
||||
cursor
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
...result,
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error, `Fetch comments for issue failed`);
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -8,13 +8,21 @@
|
||||
import type { Request, Response } from 'express';
|
||||
import { query } from '@anthropic-ai/claude-agent-sdk';
|
||||
import type { EventEmitter } from '../../../lib/events.js';
|
||||
import type { IssueValidationResult, IssueValidationEvent, AgentModel } from '@automaker/types';
|
||||
import type {
|
||||
IssueValidationResult,
|
||||
IssueValidationEvent,
|
||||
AgentModel,
|
||||
GitHubComment,
|
||||
LinkedPRInfo,
|
||||
} from '@automaker/types';
|
||||
import { createSuggestionsOptions } from '../../../lib/sdk-options.js';
|
||||
import { writeValidation } from '../../../lib/validation-storage.js';
|
||||
import {
|
||||
issueValidationSchema,
|
||||
ISSUE_VALIDATION_SYSTEM_PROMPT,
|
||||
buildValidationPrompt,
|
||||
ValidationComment,
|
||||
ValidationLinkedPR,
|
||||
} from './validation-schema.js';
|
||||
import {
|
||||
trySetValidationRunning,
|
||||
@@ -40,6 +48,10 @@ interface ValidateIssueRequestBody {
|
||||
issueLabels?: string[];
|
||||
/** Model to use for validation (opus, sonnet, haiku) */
|
||||
model?: AgentModel;
|
||||
/** Comments to include in validation analysis */
|
||||
comments?: GitHubComment[];
|
||||
/** Linked pull requests for this issue */
|
||||
linkedPRs?: LinkedPRInfo[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -57,7 +69,9 @@ async function runValidation(
|
||||
model: AgentModel,
|
||||
events: EventEmitter,
|
||||
abortController: AbortController,
|
||||
settingsService?: SettingsService
|
||||
settingsService?: SettingsService,
|
||||
comments?: ValidationComment[],
|
||||
linkedPRs?: ValidationLinkedPR[]
|
||||
): Promise<void> {
|
||||
// Emit start event
|
||||
const startEvent: IssueValidationEvent = {
|
||||
@@ -76,8 +90,15 @@ async function runValidation(
|
||||
}, VALIDATION_TIMEOUT_MS);
|
||||
|
||||
try {
|
||||
// Build the prompt
|
||||
const prompt = buildValidationPrompt(issueNumber, issueTitle, issueBody, issueLabels);
|
||||
// Build the prompt (include comments and linked PRs if provided)
|
||||
const prompt = buildValidationPrompt(
|
||||
issueNumber,
|
||||
issueTitle,
|
||||
issueBody,
|
||||
issueLabels,
|
||||
comments,
|
||||
linkedPRs
|
||||
);
|
||||
|
||||
// Load autoLoadClaudeMd setting
|
||||
const autoLoadClaudeMd = await getAutoLoadClaudeMdSetting(
|
||||
@@ -102,16 +123,12 @@ async function runValidation(
|
||||
// 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 and emit progress
|
||||
// Emit progress events for assistant text
|
||||
if (msg.type === 'assistant' && msg.message?.content) {
|
||||
for (const block of msg.message.content) {
|
||||
if (block.type === 'text') {
|
||||
responseText += block.text;
|
||||
|
||||
// Emit progress event
|
||||
const progressEvent: IssueValidationEvent = {
|
||||
type: 'issue_validation_progress',
|
||||
issueNumber,
|
||||
@@ -128,7 +145,6 @@ async function runValidation(
|
||||
const resultMsg = msg as { structured_output?: IssueValidationResult };
|
||||
if (resultMsg.structured_output) {
|
||||
validationResult = resultMsg.structured_output;
|
||||
logger.debug('Received structured output:', validationResult);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,7 +164,6 @@ async function runValidation(
|
||||
// Require structured output
|
||||
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');
|
||||
}
|
||||
|
||||
@@ -214,8 +229,30 @@ export function createValidateIssueHandler(
|
||||
issueBody,
|
||||
issueLabels,
|
||||
model = 'opus',
|
||||
comments: rawComments,
|
||||
linkedPRs: rawLinkedPRs,
|
||||
} = req.body as ValidateIssueRequestBody;
|
||||
|
||||
// Transform GitHubComment[] to ValidationComment[] if provided
|
||||
const validationComments: ValidationComment[] | undefined = rawComments?.map((c) => ({
|
||||
author: c.author?.login || 'ghost',
|
||||
createdAt: c.createdAt,
|
||||
body: c.body,
|
||||
}));
|
||||
|
||||
// Transform LinkedPRInfo[] to ValidationLinkedPR[] if provided
|
||||
const validationLinkedPRs: ValidationLinkedPR[] | undefined = rawLinkedPRs?.map((pr) => ({
|
||||
number: pr.number,
|
||||
title: pr.title,
|
||||
state: pr.state,
|
||||
}));
|
||||
|
||||
logger.info(
|
||||
`[ValidateIssue] Received validation request for issue #${issueNumber}` +
|
||||
(rawComments?.length ? ` with ${rawComments.length} comments` : ' (no comments)') +
|
||||
(rawLinkedPRs?.length ? ` and ${rawLinkedPRs.length} linked PRs` : '')
|
||||
);
|
||||
|
||||
// Validate required fields
|
||||
if (!projectPath) {
|
||||
res.status(400).json({ success: false, error: 'projectPath is required' });
|
||||
@@ -271,11 +308,12 @@ export function createValidateIssueHandler(
|
||||
model,
|
||||
events,
|
||||
abortController,
|
||||
settingsService
|
||||
settingsService,
|
||||
validationComments,
|
||||
validationLinkedPRs
|
||||
)
|
||||
.catch((error) => {
|
||||
.catch(() => {
|
||||
// Error is already handled inside runValidation (event emitted)
|
||||
logger.debug('Validation error caught in background handler:', error);
|
||||
})
|
||||
.finally(() => {
|
||||
clearValidationStatus(projectPath, issueNumber);
|
||||
|
||||
@@ -49,6 +49,34 @@ export const issueValidationSchema = {
|
||||
enum: ['trivial', 'simple', 'moderate', 'complex', 'very_complex'],
|
||||
description: 'Estimated effort to address the issue',
|
||||
},
|
||||
prAnalysis: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
hasOpenPR: {
|
||||
type: 'boolean',
|
||||
description: 'Whether there is an open PR linked to this issue',
|
||||
},
|
||||
prFixesIssue: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the PR appears to fix the issue based on the diff',
|
||||
},
|
||||
prNumber: {
|
||||
type: 'number',
|
||||
description: 'The PR number that was analyzed',
|
||||
},
|
||||
prSummary: {
|
||||
type: 'string',
|
||||
description: 'Brief summary of what the PR changes',
|
||||
},
|
||||
recommendation: {
|
||||
type: 'string',
|
||||
enum: ['wait_for_merge', 'pr_needs_work', 'no_pr'],
|
||||
description:
|
||||
'Recommendation: wait for PR to merge, PR needs more work, or no relevant PR',
|
||||
},
|
||||
},
|
||||
description: 'Analysis of linked pull requests if any exist',
|
||||
},
|
||||
},
|
||||
required: ['verdict', 'confidence', 'reasoning'],
|
||||
additionalProperties: false,
|
||||
@@ -67,7 +95,8 @@ Your task is to analyze a GitHub issue and determine if it's valid by scanning t
|
||||
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
|
||||
4. **Check linked PRs** - If there are linked pull requests, use \`gh pr diff <PR_NUMBER>\` to review the changes
|
||||
5. **Form your verdict** - Based on your analysis, determine if the issue is valid
|
||||
|
||||
## Verdicts
|
||||
|
||||
@@ -88,12 +117,32 @@ Your task is to analyze a GitHub issue and determine if it's valid by scanning t
|
||||
- Is the implementation location clear?
|
||||
- Is the request technically feasible given the codebase structure?
|
||||
|
||||
## Analyzing Linked Pull Requests
|
||||
|
||||
When an issue has linked PRs (especially open ones), you MUST analyze them:
|
||||
|
||||
1. **Run \`gh pr diff <PR_NUMBER>\`** to see what changes the PR makes
|
||||
2. **Run \`gh pr view <PR_NUMBER>\`** to see PR description and status
|
||||
3. **Evaluate if the PR fixes the issue** - Does the diff address the reported problem?
|
||||
4. **Provide a recommendation**:
|
||||
- \`wait_for_merge\`: The PR appears to fix the issue correctly. No additional work needed - just wait for it to be merged.
|
||||
- \`pr_needs_work\`: The PR attempts to fix the issue but is incomplete or has problems.
|
||||
- \`no_pr\`: No relevant PR exists for this issue.
|
||||
|
||||
5. **Include prAnalysis in your response** with:
|
||||
- hasOpenPR: true/false
|
||||
- prFixesIssue: true/false (based on diff analysis)
|
||||
- prNumber: the PR number you analyzed
|
||||
- prSummary: brief description of what the PR changes
|
||||
- recommendation: one of the above values
|
||||
|
||||
## 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
|
||||
- **Include prAnalysis** when there are linked PRs - this is critical for avoiding duplicate work
|
||||
- **Set estimatedComplexity** to help prioritize:
|
||||
- trivial: Simple text changes, one-line fixes
|
||||
- simple: Small changes to one file
|
||||
@@ -103,6 +152,24 @@ Your task is to analyze a GitHub issue and determine if it's valid by scanning t
|
||||
|
||||
Be thorough in your analysis but focus on files that are directly relevant to the issue.`;
|
||||
|
||||
/**
|
||||
* Comment data structure for validation prompt
|
||||
*/
|
||||
export interface ValidationComment {
|
||||
author: string;
|
||||
createdAt: string;
|
||||
body: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Linked PR data structure for validation prompt
|
||||
*/
|
||||
export interface ValidationLinkedPR {
|
||||
number: number;
|
||||
title: string;
|
||||
state: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the user prompt for issue validation.
|
||||
*
|
||||
@@ -113,26 +180,60 @@ Be thorough in your analysis but focus on files that are directly relevant to th
|
||||
* @param issueTitle - The issue title
|
||||
* @param issueBody - The issue body/description
|
||||
* @param issueLabels - Optional array of label names
|
||||
* @param comments - Optional array of comments to include in analysis
|
||||
* @param linkedPRs - Optional array of linked pull requests
|
||||
* @returns Formatted prompt string for the validation request
|
||||
*/
|
||||
export function buildValidationPrompt(
|
||||
issueNumber: number,
|
||||
issueTitle: string,
|
||||
issueBody: string,
|
||||
issueLabels?: string[]
|
||||
issueLabels?: string[],
|
||||
comments?: ValidationComment[],
|
||||
linkedPRs?: ValidationLinkedPR[]
|
||||
): string {
|
||||
const labelsSection = issueLabels?.length ? `\n\n**Labels:** ${issueLabels.join(', ')}` : '';
|
||||
|
||||
let linkedPRsSection = '';
|
||||
if (linkedPRs && linkedPRs.length > 0) {
|
||||
const prsText = linkedPRs
|
||||
.map((pr) => `- PR #${pr.number} (${pr.state}): ${pr.title}`)
|
||||
.join('\n');
|
||||
linkedPRsSection = `\n\n### Linked Pull Requests\n\n${prsText}`;
|
||||
}
|
||||
|
||||
let commentsSection = '';
|
||||
if (comments && comments.length > 0) {
|
||||
// Limit to most recent 10 comments to control prompt size
|
||||
const recentComments = comments.slice(-10);
|
||||
const commentsText = recentComments
|
||||
.map(
|
||||
(c) => `**${c.author}** (${new Date(c.createdAt).toISOString().slice(0, 10)}):\n${c.body}`
|
||||
)
|
||||
.join('\n\n---\n\n');
|
||||
|
||||
commentsSection = `\n\n### Comments (${comments.length} total${comments.length > 10 ? ', showing last 10' : ''})\n\n${commentsText}`;
|
||||
}
|
||||
|
||||
const hasWorkInProgress =
|
||||
linkedPRs && linkedPRs.some((pr) => pr.state === 'open' || pr.state === 'OPEN');
|
||||
const workInProgressNote = hasWorkInProgress
|
||||
? '\n\n**Note:** This issue has an open pull request linked. Consider that someone may already be working on a fix.'
|
||||
: '';
|
||||
|
||||
return `Please validate the following GitHub issue by analyzing the codebase:
|
||||
|
||||
## Issue #${issueNumber}: ${issueTitle}
|
||||
${labelsSection}
|
||||
${linkedPRsSection}
|
||||
|
||||
### Description
|
||||
|
||||
${issueBody || '(No description provided)'}
|
||||
${commentsSection}
|
||||
${workInProgressNote}
|
||||
|
||||
---
|
||||
|
||||
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.`;
|
||||
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.${comments && comments.length > 0 ? ' Consider the context provided in the comments as well.' : ''}${hasWorkInProgress ? ' Also note in your analysis if there is already work in progress on this issue.' : ''}`;
|
||||
}
|
||||
|
||||
@@ -11,12 +11,15 @@ import { useGithubIssues, useIssueValidation } from './github-issues-view/hooks'
|
||||
import { IssueRow, IssueDetailPanel, IssuesListHeader } from './github-issues-view/components';
|
||||
import { ValidationDialog } from './github-issues-view/dialogs';
|
||||
import { formatDate, getFeaturePriority } from './github-issues-view/utils';
|
||||
import type { ValidateIssueOptions } from './github-issues-view/types';
|
||||
|
||||
export function GitHubIssuesView() {
|
||||
const [selectedIssue, setSelectedIssue] = useState<GitHubIssue | null>(null);
|
||||
const [validationResult, setValidationResult] = useState<IssueValidationResult | null>(null);
|
||||
const [showValidationDialog, setShowValidationDialog] = useState(false);
|
||||
const [showRevalidateConfirm, setShowRevalidateConfirm] = useState(false);
|
||||
const [pendingRevalidateOptions, setPendingRevalidateOptions] =
|
||||
useState<ValidateIssueOptions | null>(null);
|
||||
|
||||
const { currentProject, defaultAIProfileId, aiProfiles, getCurrentWorktree, worktreesByProject } =
|
||||
useAppStore();
|
||||
@@ -203,7 +206,10 @@ export function GitHubIssuesView() {
|
||||
onViewCachedValidation={handleViewCachedValidation}
|
||||
onOpenInGitHub={handleOpenInGitHub}
|
||||
onClose={() => setSelectedIssue(null)}
|
||||
onShowRevalidateConfirm={() => setShowRevalidateConfirm(true)}
|
||||
onShowRevalidateConfirm={(options) => {
|
||||
setPendingRevalidateOptions(options);
|
||||
setShowRevalidateConfirm(true);
|
||||
}}
|
||||
formatDate={formatDate}
|
||||
/>
|
||||
)}
|
||||
@@ -220,15 +226,24 @@ export function GitHubIssuesView() {
|
||||
{/* Revalidate Confirmation Dialog */}
|
||||
<ConfirmDialog
|
||||
open={showRevalidateConfirm}
|
||||
onOpenChange={setShowRevalidateConfirm}
|
||||
onOpenChange={(open) => {
|
||||
setShowRevalidateConfirm(open);
|
||||
if (!open) {
|
||||
setPendingRevalidateOptions(null);
|
||||
}
|
||||
}}
|
||||
title="Re-validate Issue"
|
||||
description={`Are you sure you want to re-validate issue #${selectedIssue?.number}? This will run a new AI analysis and replace the existing validation result.`}
|
||||
icon={RefreshCw}
|
||||
iconClassName="text-primary"
|
||||
confirmText="Re-validate"
|
||||
onConfirm={() => {
|
||||
if (selectedIssue) {
|
||||
handleValidateIssue(selectedIssue, { forceRevalidate: true });
|
||||
if (selectedIssue && pendingRevalidateOptions) {
|
||||
console.log('[GitHubIssuesView] Revalidating with options:', {
|
||||
commentsCount: pendingRevalidateOptions.comments?.length ?? 0,
|
||||
linkedPRsCount: pendingRevalidateOptions.linkedPRs?.length ?? 0,
|
||||
});
|
||||
handleValidateIssue(selectedIssue, pendingRevalidateOptions);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { User } from 'lucide-react';
|
||||
import { Markdown } from '@/components/ui/markdown';
|
||||
import type { GitHubComment } from '@/lib/electron';
|
||||
import { formatDate } from '../utils';
|
||||
|
||||
interface CommentItemProps {
|
||||
comment: GitHubComment;
|
||||
}
|
||||
|
||||
export function CommentItem({ comment }: CommentItemProps) {
|
||||
return (
|
||||
<div className="p-3 rounded-lg bg-background border border-border">
|
||||
{/* Comment Header */}
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
{comment.author.avatarUrl ? (
|
||||
<img
|
||||
src={comment.author.avatarUrl}
|
||||
alt={comment.author.login}
|
||||
className="h-6 w-6 rounded-full"
|
||||
/>
|
||||
) : (
|
||||
<div className="h-6 w-6 rounded-full bg-muted flex items-center justify-center">
|
||||
<User className="h-3 w-3 text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
<span className="text-sm font-medium">{comment.author.login}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
commented {formatDate(comment.createdAt)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Comment Body */}
|
||||
{comment.body ? (
|
||||
<Markdown className="text-sm">{comment.body}</Markdown>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground italic">No content</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export { IssueRow } from './issue-row';
|
||||
export { IssueDetailPanel } from './issue-detail-panel';
|
||||
export { IssuesListHeader } from './issues-list-header';
|
||||
export { CommentItem } from './comment-item';
|
||||
|
||||
@@ -10,12 +10,19 @@ import {
|
||||
GitPullRequest,
|
||||
User,
|
||||
RefreshCw,
|
||||
MessageSquare,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
} from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Markdown } from '@/components/ui/markdown';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { IssueDetailPanelProps } from '../types';
|
||||
import { isValidationStale } from '../utils';
|
||||
import { useIssueComments } from '../hooks';
|
||||
import { CommentItem } from './comment-item';
|
||||
|
||||
export function IssueDetailPanel({
|
||||
issue,
|
||||
@@ -32,6 +39,32 @@ export function IssueDetailPanel({
|
||||
const cached = cachedValidations.get(issue.number);
|
||||
const isStale = cached ? isValidationStale(cached.validatedAt) : false;
|
||||
|
||||
// Comments state
|
||||
const [commentsExpanded, setCommentsExpanded] = useState(true);
|
||||
const [includeCommentsInAnalysis, setIncludeCommentsInAnalysis] = useState(true);
|
||||
const {
|
||||
comments,
|
||||
totalCount,
|
||||
loading: commentsLoading,
|
||||
loadingMore,
|
||||
hasNextPage,
|
||||
error: commentsError,
|
||||
loadMore,
|
||||
} = useIssueComments(issue.number);
|
||||
|
||||
// Helper to get validation options with comments and linked PRs
|
||||
const getValidationOptions = (forceRevalidate = false) => {
|
||||
return {
|
||||
forceRevalidate,
|
||||
comments: includeCommentsInAnalysis && comments.length > 0 ? comments : undefined,
|
||||
linkedPRs: issue.linkedPRs?.map((pr) => ({
|
||||
number: pr.number,
|
||||
title: pr.title,
|
||||
state: pr.state,
|
||||
})),
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
{/* Detail Header */}
|
||||
@@ -67,7 +100,7 @@ export function IssueDetailPanel({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onShowRevalidateConfirm}
|
||||
onClick={() => onShowRevalidateConfirm(getValidationOptions(true))}
|
||||
title="Re-validate"
|
||||
>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
@@ -86,7 +119,7 @@ export function IssueDetailPanel({
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={() => onValidateIssue(issue, { forceRevalidate: true })}
|
||||
onClick={() => onValidateIssue(issue, getValidationOptions(true))}
|
||||
>
|
||||
<Wand2 className="h-4 w-4 mr-1" />
|
||||
Re-validate
|
||||
@@ -96,7 +129,11 @@ export function IssueDetailPanel({
|
||||
}
|
||||
|
||||
return (
|
||||
<Button variant="default" size="sm" onClick={() => onValidateIssue(issue)}>
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={() => onValidateIssue(issue, getValidationOptions())}
|
||||
>
|
||||
<Wand2 className="h-4 w-4 mr-1" />
|
||||
Validate with AI
|
||||
</Button>
|
||||
@@ -226,6 +263,74 @@ export function IssueDetailPanel({
|
||||
<p className="text-sm text-muted-foreground italic">No description provided.</p>
|
||||
)}
|
||||
|
||||
{/* Comments Section */}
|
||||
<div className="mt-6 p-3 rounded-lg bg-muted/30 border border-border">
|
||||
<div className="flex items-center justify-between">
|
||||
<button
|
||||
className="flex items-center gap-2 text-left"
|
||||
onClick={() => setCommentsExpanded(!commentsExpanded)}
|
||||
>
|
||||
<MessageSquare className="h-4 w-4 text-blue-500" />
|
||||
<span className="text-sm font-medium">
|
||||
Comments {totalCount > 0 && `(${totalCount})`}
|
||||
</span>
|
||||
{commentsLoading && (
|
||||
<Loader2 className="h-3 w-3 animate-spin text-muted-foreground" />
|
||||
)}
|
||||
{commentsExpanded ? (
|
||||
<ChevronUp className="h-4 w-4 text-muted-foreground" />
|
||||
) : (
|
||||
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
</button>
|
||||
{comments.length > 0 && (
|
||||
<label className="flex items-center gap-2 text-xs text-muted-foreground cursor-pointer">
|
||||
<Checkbox
|
||||
checked={includeCommentsInAnalysis}
|
||||
onCheckedChange={setIncludeCommentsInAnalysis}
|
||||
/>
|
||||
Include in AI analysis
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{commentsExpanded && (
|
||||
<div className="mt-3">
|
||||
{commentsError ? (
|
||||
<p className="text-sm text-red-500">{commentsError}</p>
|
||||
) : comments.length === 0 && !commentsLoading ? (
|
||||
<p className="text-sm text-muted-foreground italic">No comments yet.</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{comments.map((comment) => (
|
||||
<CommentItem key={comment.id} comment={comment} />
|
||||
))}
|
||||
|
||||
{/* Load More Button */}
|
||||
{hasNextPage && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full"
|
||||
onClick={loadMore}
|
||||
disabled={loadingMore}
|
||||
>
|
||||
{loadingMore ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
Loading...
|
||||
</>
|
||||
) : (
|
||||
'Load More Comments'
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 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">
|
||||
|
||||
@@ -16,6 +16,9 @@ import {
|
||||
Lightbulb,
|
||||
AlertTriangle,
|
||||
Plus,
|
||||
GitPullRequest,
|
||||
Clock,
|
||||
Wrench,
|
||||
} from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type {
|
||||
@@ -149,6 +152,77 @@ export function ValidationDialog({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* PR Analysis Section - Show AI's analysis of linked PRs */}
|
||||
{validationResult.prAnalysis && validationResult.prAnalysis.hasOpenPR && (
|
||||
<div
|
||||
className={cn(
|
||||
'p-3 rounded-lg border',
|
||||
validationResult.prAnalysis.recommendation === 'wait_for_merge'
|
||||
? 'bg-green-500/10 border-green-500/20'
|
||||
: validationResult.prAnalysis.recommendation === 'pr_needs_work'
|
||||
? 'bg-yellow-500/10 border-yellow-500/20'
|
||||
: 'bg-purple-500/10 border-purple-500/20'
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start gap-2">
|
||||
{validationResult.prAnalysis.recommendation === 'wait_for_merge' ? (
|
||||
<Clock className="h-5 w-5 text-green-500 shrink-0 mt-0.5" />
|
||||
) : validationResult.prAnalysis.recommendation === 'pr_needs_work' ? (
|
||||
<Wrench className="h-5 w-5 text-yellow-500 shrink-0 mt-0.5" />
|
||||
) : (
|
||||
<GitPullRequest className="h-5 w-5 text-purple-500 shrink-0 mt-0.5" />
|
||||
)}
|
||||
<div className="flex-1">
|
||||
<span
|
||||
className={cn(
|
||||
'text-sm font-medium',
|
||||
validationResult.prAnalysis.recommendation === 'wait_for_merge'
|
||||
? 'text-green-500'
|
||||
: validationResult.prAnalysis.recommendation === 'pr_needs_work'
|
||||
? 'text-yellow-500'
|
||||
: 'text-purple-500'
|
||||
)}
|
||||
>
|
||||
{validationResult.prAnalysis.recommendation === 'wait_for_merge'
|
||||
? 'Fix Ready - Wait for Merge'
|
||||
: validationResult.prAnalysis.recommendation === 'pr_needs_work'
|
||||
? 'PR Needs Work'
|
||||
: 'Work in Progress'}
|
||||
</span>
|
||||
{validationResult.prAnalysis.prNumber && (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
PR #{validationResult.prAnalysis.prNumber}
|
||||
{validationResult.prAnalysis.prFixesIssue && ' appears to fix this issue'}
|
||||
</p>
|
||||
)}
|
||||
{validationResult.prAnalysis.prSummary && (
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{validationResult.prAnalysis.prSummary}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Fallback Work in Progress Badge - Show when there's an open PR but no AI analysis */}
|
||||
{!validationResult.prAnalysis?.hasOpenPR &&
|
||||
issue.linkedPRs?.some((pr) => pr.state === 'open' || pr.state === 'OPEN') && (
|
||||
<div className="flex items-center gap-2 p-3 rounded-lg bg-purple-500/10 border border-purple-500/20">
|
||||
<GitPullRequest className="h-5 w-5 text-purple-500 shrink-0" />
|
||||
<div className="flex-1">
|
||||
<span className="text-sm font-medium text-purple-500">Work in Progress</span>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{issue.linkedPRs
|
||||
.filter((pr) => pr.state === 'open' || pr.state === 'OPEN')
|
||||
.map((pr) => `PR #${pr.number}`)
|
||||
.join(', ')}{' '}
|
||||
is open for this issue
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Reasoning */}
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-medium flex items-center gap-2">
|
||||
@@ -218,12 +292,14 @@ export function ValidationDialog({
|
||||
<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>
|
||||
)}
|
||||
{validationResult?.verdict === 'valid' &&
|
||||
onConvertToTask &&
|
||||
validationResult?.prAnalysis?.recommendation !== 'wait_for_merge' && (
|
||||
<Button onClick={handleConvertToTask}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Convert to Task
|
||||
</Button>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export { useGithubIssues } from './use-github-issues';
|
||||
export { useIssueValidation } from './use-issue-validation';
|
||||
export { useIssueComments } from './use-issue-comments';
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { getElectronAPI, GitHubComment } from '@/lib/electron';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
|
||||
interface UseIssueCommentsResult {
|
||||
comments: GitHubComment[];
|
||||
totalCount: number;
|
||||
loading: boolean;
|
||||
loadingMore: boolean;
|
||||
hasNextPage: boolean;
|
||||
error: string | null;
|
||||
loadMore: () => void;
|
||||
refresh: () => void;
|
||||
}
|
||||
|
||||
export function useIssueComments(issueNumber: number | null): UseIssueCommentsResult {
|
||||
const { currentProject } = useAppStore();
|
||||
const [comments, setComments] = useState<GitHubComment[]>([]);
|
||||
const [totalCount, setTotalCount] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingMore, setLoadingMore] = useState(false);
|
||||
const [hasNextPage, setHasNextPage] = useState(false);
|
||||
const [endCursor, setEndCursor] = useState<string | undefined>(undefined);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const isMountedRef = useRef(true);
|
||||
|
||||
const fetchComments = useCallback(
|
||||
async (cursor?: string) => {
|
||||
if (!currentProject?.path || !issueNumber) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isLoadingMore = !!cursor;
|
||||
|
||||
try {
|
||||
if (isMountedRef.current) {
|
||||
setError(null);
|
||||
if (isLoadingMore) {
|
||||
setLoadingMore(true);
|
||||
} else {
|
||||
setLoading(true);
|
||||
}
|
||||
}
|
||||
|
||||
const api = getElectronAPI();
|
||||
if (api.github) {
|
||||
const result = await api.github.getIssueComments(
|
||||
currentProject.path,
|
||||
issueNumber,
|
||||
cursor
|
||||
);
|
||||
|
||||
if (isMountedRef.current) {
|
||||
if (result.success) {
|
||||
if (isLoadingMore) {
|
||||
// Append new comments
|
||||
setComments((prev) => [...prev, ...(result.comments || [])]);
|
||||
} else {
|
||||
// Replace all comments
|
||||
setComments(result.comments || []);
|
||||
}
|
||||
setTotalCount(result.totalCount || 0);
|
||||
setHasNextPage(result.hasNextPage || false);
|
||||
setEndCursor(result.endCursor);
|
||||
} else {
|
||||
setError(result.error || 'Failed to fetch comments');
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (isMountedRef.current) {
|
||||
console.error('[useIssueComments] Error fetching comments:', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to fetch comments');
|
||||
}
|
||||
} finally {
|
||||
if (isMountedRef.current) {
|
||||
setLoading(false);
|
||||
setLoadingMore(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
[currentProject?.path, issueNumber]
|
||||
);
|
||||
|
||||
// Reset and fetch when issue changes
|
||||
useEffect(() => {
|
||||
isMountedRef.current = true;
|
||||
|
||||
if (issueNumber) {
|
||||
// Reset state when issue changes
|
||||
setComments([]);
|
||||
setTotalCount(0);
|
||||
setHasNextPage(false);
|
||||
setEndCursor(undefined);
|
||||
setError(null);
|
||||
fetchComments();
|
||||
} else {
|
||||
// Clear comments when no issue is selected
|
||||
setComments([]);
|
||||
setTotalCount(0);
|
||||
setHasNextPage(false);
|
||||
setEndCursor(undefined);
|
||||
setLoading(false);
|
||||
setError(null);
|
||||
}
|
||||
|
||||
return () => {
|
||||
isMountedRef.current = false;
|
||||
};
|
||||
}, [issueNumber, fetchComments]);
|
||||
|
||||
const loadMore = useCallback(() => {
|
||||
if (hasNextPage && endCursor && !loadingMore) {
|
||||
fetchComments(endCursor);
|
||||
}
|
||||
}, [hasNextPage, endCursor, loadingMore, fetchComments]);
|
||||
|
||||
const refresh = useCallback(() => {
|
||||
setComments([]);
|
||||
setEndCursor(undefined);
|
||||
fetchComments();
|
||||
}, [fetchComments]);
|
||||
|
||||
return {
|
||||
comments,
|
||||
totalCount,
|
||||
loading,
|
||||
loadingMore,
|
||||
hasNextPage,
|
||||
error,
|
||||
loadMore,
|
||||
refresh,
|
||||
};
|
||||
}
|
||||
@@ -2,10 +2,12 @@ import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import {
|
||||
getElectronAPI,
|
||||
GitHubIssue,
|
||||
GitHubComment,
|
||||
IssueValidationResult,
|
||||
IssueValidationEvent,
|
||||
StoredValidation,
|
||||
} from '@/lib/electron';
|
||||
import type { LinkedPRInfo } from '@automaker/types';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import { toast } from 'sonner';
|
||||
import { isValidationStale } from '../utils';
|
||||
@@ -205,8 +207,15 @@ export function useIssueValidation({
|
||||
}, []);
|
||||
|
||||
const handleValidateIssue = useCallback(
|
||||
async (issue: GitHubIssue, options: { forceRevalidate?: boolean } = {}) => {
|
||||
const { forceRevalidate = false } = options;
|
||||
async (
|
||||
issue: GitHubIssue,
|
||||
options: {
|
||||
forceRevalidate?: boolean;
|
||||
comments?: GitHubComment[];
|
||||
linkedPRs?: LinkedPRInfo[];
|
||||
} = {}
|
||||
) => {
|
||||
const { forceRevalidate = false, comments, linkedPRs } = options;
|
||||
|
||||
if (!currentProject?.path) {
|
||||
toast.error('No project selected');
|
||||
@@ -236,14 +245,17 @@ export function useIssueValidation({
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (api.github?.validateIssue) {
|
||||
const validationInput = {
|
||||
issueNumber: issue.number,
|
||||
issueTitle: issue.title,
|
||||
issueBody: issue.body || '',
|
||||
issueLabels: issue.labels.map((l) => l.name),
|
||||
comments, // Include comments if provided
|
||||
linkedPRs, // Include linked PRs if provided
|
||||
};
|
||||
const result = await api.github.validateIssue(
|
||||
currentProject.path,
|
||||
{
|
||||
issueNumber: issue.number,
|
||||
issueTitle: issue.title,
|
||||
issueBody: issue.body || '',
|
||||
issueLabels: issue.labels.map((l) => l.name),
|
||||
},
|
||||
validationInput,
|
||||
validationModel
|
||||
);
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { GitHubIssue, StoredValidation } from '@/lib/electron';
|
||||
import type { GitHubIssue, StoredValidation, GitHubComment } from '@/lib/electron';
|
||||
import type { LinkedPRInfo } from '@automaker/types';
|
||||
|
||||
export interface IssueRowProps {
|
||||
issue: GitHubIssue;
|
||||
@@ -12,17 +13,25 @@ export interface IssueRowProps {
|
||||
isValidating?: boolean;
|
||||
}
|
||||
|
||||
/** Options for issue validation */
|
||||
export interface ValidateIssueOptions {
|
||||
showDialog?: boolean;
|
||||
forceRevalidate?: boolean;
|
||||
/** Include comments in AI analysis */
|
||||
comments?: GitHubComment[];
|
||||
/** Linked pull requests */
|
||||
linkedPRs?: LinkedPRInfo[];
|
||||
}
|
||||
|
||||
export interface IssueDetailPanelProps {
|
||||
issue: GitHubIssue;
|
||||
validatingIssues: Set<number>;
|
||||
cachedValidations: Map<number, StoredValidation>;
|
||||
onValidateIssue: (
|
||||
issue: GitHubIssue,
|
||||
options?: { showDialog?: boolean; forceRevalidate?: boolean }
|
||||
) => Promise<void>;
|
||||
onValidateIssue: (issue: GitHubIssue, options?: ValidateIssueOptions) => Promise<void>;
|
||||
onViewCachedValidation: (issue: GitHubIssue) => Promise<void>;
|
||||
onOpenInGitHub: (url: string) => void;
|
||||
onClose: () => void;
|
||||
onShowRevalidateConfirm: () => void;
|
||||
/** Called when user wants to revalidate - receives the validation options including comments/linkedPRs */
|
||||
onShowRevalidateConfirm: (options: ValidateIssueOptions) => void;
|
||||
formatDate: (date: string) => string;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ import type {
|
||||
IssueValidationEvent,
|
||||
StoredValidation,
|
||||
AgentModel,
|
||||
GitHubComment,
|
||||
IssueCommentsResult,
|
||||
} from '@automaker/types';
|
||||
import { getJSON, setJSON, removeItem } from './storage';
|
||||
|
||||
@@ -24,6 +26,8 @@ export type {
|
||||
IssueValidationResponse,
|
||||
IssueValidationEvent,
|
||||
StoredValidation,
|
||||
GitHubComment,
|
||||
IssueCommentsResult,
|
||||
};
|
||||
|
||||
export interface FileEntry {
|
||||
@@ -234,6 +238,19 @@ export interface GitHubAPI {
|
||||
) => Promise<{ success: boolean; error?: string }>;
|
||||
/** Subscribe to validation events */
|
||||
onValidationEvent: (callback: (event: IssueValidationEvent) => void) => () => void;
|
||||
/** Fetch comments for a specific issue */
|
||||
getIssueComments: (
|
||||
projectPath: string,
|
||||
issueNumber: number,
|
||||
cursor?: string
|
||||
) => Promise<{
|
||||
success: boolean;
|
||||
comments?: GitHubComment[];
|
||||
totalCount?: number;
|
||||
hasNextPage?: boolean;
|
||||
endCursor?: string;
|
||||
error?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
// Feature Suggestions types
|
||||
@@ -2786,6 +2803,15 @@ function createMockGitHubAPI(): GitHubAPI {
|
||||
mockValidationCallbacks = mockValidationCallbacks.filter((cb) => cb !== callback);
|
||||
};
|
||||
},
|
||||
getIssueComments: async (projectPath: string, issueNumber: number, cursor?: string) => {
|
||||
console.log('[Mock] Getting issue comments:', { projectPath, issueNumber, cursor });
|
||||
return {
|
||||
success: true,
|
||||
comments: [],
|
||||
totalCount: 0,
|
||||
hasNextPage: false,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -766,6 +766,8 @@ export class HttpApiClient implements ElectronAPI {
|
||||
this.post('/api/github/validation-mark-viewed', { projectPath, issueNumber }),
|
||||
onValidationEvent: (callback: (event: IssueValidationEvent) => void) =>
|
||||
this.subscribeToEvent('issue-validation:event', callback as EventCallback),
|
||||
getIssueComments: (projectPath: string, issueNumber: number, cursor?: string) =>
|
||||
this.post('/api/github/issue-comments', { projectPath, issueNumber, cursor }),
|
||||
};
|
||||
|
||||
// Workspace API
|
||||
|
||||
Reference in New Issue
Block a user