mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 08:53: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 { 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 { createListCommentsHandler } from './routes/list-comments.js';
|
||||||
import { createValidateIssueHandler } from './routes/validate-issue.js';
|
import { createValidateIssueHandler } from './routes/validate-issue.js';
|
||||||
import {
|
import {
|
||||||
createValidationStatusHandler,
|
createValidationStatusHandler,
|
||||||
@@ -27,6 +28,7 @@ export function createGitHubRoutes(
|
|||||||
router.post('/check-remote', validatePathParams('projectPath'), createCheckGitHubRemoteHandler());
|
router.post('/check-remote', validatePathParams('projectPath'), createCheckGitHubRemoteHandler());
|
||||||
router.post('/issues', validatePathParams('projectPath'), createListIssuesHandler());
|
router.post('/issues', validatePathParams('projectPath'), createListIssuesHandler());
|
||||||
router.post('/prs', validatePathParams('projectPath'), createListPRsHandler());
|
router.post('/prs', validatePathParams('projectPath'), createListPRsHandler());
|
||||||
|
router.post('/issue-comments', validatePathParams('projectPath'), createListCommentsHandler());
|
||||||
router.post(
|
router.post(
|
||||||
'/validate-issue',
|
'/validate-issue',
|
||||||
validatePathParams('projectPath'),
|
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 type { Request, Response } from 'express';
|
||||||
import { query } from '@anthropic-ai/claude-agent-sdk';
|
import { query } from '@anthropic-ai/claude-agent-sdk';
|
||||||
import type { EventEmitter } from '../../../lib/events.js';
|
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 { createSuggestionsOptions } from '../../../lib/sdk-options.js';
|
||||||
import { writeValidation } from '../../../lib/validation-storage.js';
|
import { writeValidation } from '../../../lib/validation-storage.js';
|
||||||
import {
|
import {
|
||||||
issueValidationSchema,
|
issueValidationSchema,
|
||||||
ISSUE_VALIDATION_SYSTEM_PROMPT,
|
ISSUE_VALIDATION_SYSTEM_PROMPT,
|
||||||
buildValidationPrompt,
|
buildValidationPrompt,
|
||||||
|
ValidationComment,
|
||||||
|
ValidationLinkedPR,
|
||||||
} from './validation-schema.js';
|
} from './validation-schema.js';
|
||||||
import {
|
import {
|
||||||
trySetValidationRunning,
|
trySetValidationRunning,
|
||||||
@@ -40,6 +48,10 @@ interface ValidateIssueRequestBody {
|
|||||||
issueLabels?: string[];
|
issueLabels?: string[];
|
||||||
/** Model to use for validation (opus, sonnet, haiku) */
|
/** Model to use for validation (opus, sonnet, haiku) */
|
||||||
model?: AgentModel;
|
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,
|
model: AgentModel,
|
||||||
events: EventEmitter,
|
events: EventEmitter,
|
||||||
abortController: AbortController,
|
abortController: AbortController,
|
||||||
settingsService?: SettingsService
|
settingsService?: SettingsService,
|
||||||
|
comments?: ValidationComment[],
|
||||||
|
linkedPRs?: ValidationLinkedPR[]
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Emit start event
|
// Emit start event
|
||||||
const startEvent: IssueValidationEvent = {
|
const startEvent: IssueValidationEvent = {
|
||||||
@@ -76,8 +90,15 @@ async function runValidation(
|
|||||||
}, VALIDATION_TIMEOUT_MS);
|
}, VALIDATION_TIMEOUT_MS);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Build the prompt
|
// Build the prompt (include comments and linked PRs if provided)
|
||||||
const prompt = buildValidationPrompt(issueNumber, issueTitle, issueBody, issueLabels);
|
const prompt = buildValidationPrompt(
|
||||||
|
issueNumber,
|
||||||
|
issueTitle,
|
||||||
|
issueBody,
|
||||||
|
issueLabels,
|
||||||
|
comments,
|
||||||
|
linkedPRs
|
||||||
|
);
|
||||||
|
|
||||||
// Load autoLoadClaudeMd setting
|
// Load autoLoadClaudeMd setting
|
||||||
const autoLoadClaudeMd = await getAutoLoadClaudeMdSetting(
|
const autoLoadClaudeMd = await getAutoLoadClaudeMdSetting(
|
||||||
@@ -102,16 +123,12 @@ async function runValidation(
|
|||||||
// Execute the query
|
// Execute the query
|
||||||
const stream = query({ prompt, options });
|
const stream = query({ prompt, options });
|
||||||
let validationResult: IssueValidationResult | null = null;
|
let validationResult: IssueValidationResult | null = null;
|
||||||
let responseText = '';
|
|
||||||
|
|
||||||
for await (const msg of stream) {
|
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) {
|
if (msg.type === 'assistant' && msg.message?.content) {
|
||||||
for (const block of msg.message.content) {
|
for (const block of msg.message.content) {
|
||||||
if (block.type === 'text') {
|
if (block.type === 'text') {
|
||||||
responseText += block.text;
|
|
||||||
|
|
||||||
// Emit progress event
|
|
||||||
const progressEvent: IssueValidationEvent = {
|
const progressEvent: IssueValidationEvent = {
|
||||||
type: 'issue_validation_progress',
|
type: 'issue_validation_progress',
|
||||||
issueNumber,
|
issueNumber,
|
||||||
@@ -128,7 +145,6 @@ async function runValidation(
|
|||||||
const resultMsg = msg as { structured_output?: IssueValidationResult };
|
const resultMsg = msg as { structured_output?: IssueValidationResult };
|
||||||
if (resultMsg.structured_output) {
|
if (resultMsg.structured_output) {
|
||||||
validationResult = resultMsg.structured_output;
|
validationResult = resultMsg.structured_output;
|
||||||
logger.debug('Received structured output:', validationResult);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,7 +164,6 @@ async function runValidation(
|
|||||||
// Require structured output
|
// Require structured output
|
||||||
if (!validationResult) {
|
if (!validationResult) {
|
||||||
logger.error('No structured output received from Claude SDK');
|
logger.error('No structured output received from Claude SDK');
|
||||||
logger.debug('Raw response text:', responseText);
|
|
||||||
throw new Error('Validation failed: no structured output received');
|
throw new Error('Validation failed: no structured output received');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,8 +229,30 @@ export function createValidateIssueHandler(
|
|||||||
issueBody,
|
issueBody,
|
||||||
issueLabels,
|
issueLabels,
|
||||||
model = 'opus',
|
model = 'opus',
|
||||||
|
comments: rawComments,
|
||||||
|
linkedPRs: rawLinkedPRs,
|
||||||
} = req.body as ValidateIssueRequestBody;
|
} = 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
|
// Validate required fields
|
||||||
if (!projectPath) {
|
if (!projectPath) {
|
||||||
res.status(400).json({ success: false, error: 'projectPath is required' });
|
res.status(400).json({ success: false, error: 'projectPath is required' });
|
||||||
@@ -271,11 +308,12 @@ export function createValidateIssueHandler(
|
|||||||
model,
|
model,
|
||||||
events,
|
events,
|
||||||
abortController,
|
abortController,
|
||||||
settingsService
|
settingsService,
|
||||||
|
validationComments,
|
||||||
|
validationLinkedPRs
|
||||||
)
|
)
|
||||||
.catch((error) => {
|
.catch(() => {
|
||||||
// Error is already handled inside runValidation (event emitted)
|
// Error is already handled inside runValidation (event emitted)
|
||||||
logger.debug('Validation error caught in background handler:', error);
|
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
clearValidationStatus(projectPath, issueNumber);
|
clearValidationStatus(projectPath, issueNumber);
|
||||||
|
|||||||
@@ -49,6 +49,34 @@ export const issueValidationSchema = {
|
|||||||
enum: ['trivial', 'simple', 'moderate', 'complex', 'very_complex'],
|
enum: ['trivial', 'simple', 'moderate', 'complex', 'very_complex'],
|
||||||
description: 'Estimated effort to address the issue',
|
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'],
|
required: ['verdict', 'confidence', 'reasoning'],
|
||||||
additionalProperties: false,
|
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
|
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
|
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
|
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
|
## 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 implementation location clear?
|
||||||
- Is the request technically feasible given the codebase structure?
|
- 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
|
## Response Guidelines
|
||||||
|
|
||||||
- **Always include relatedFiles** when you find relevant code
|
- **Always include relatedFiles** when you find relevant code
|
||||||
- **Set bugConfirmed to true** only if you can definitively confirm a bug exists in the 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
|
- **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
|
- **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:
|
- **Set estimatedComplexity** to help prioritize:
|
||||||
- trivial: Simple text changes, one-line fixes
|
- trivial: Simple text changes, one-line fixes
|
||||||
- simple: Small changes to one file
|
- 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.`;
|
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.
|
* 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 issueTitle - The issue title
|
||||||
* @param issueBody - The issue body/description
|
* @param issueBody - The issue body/description
|
||||||
* @param issueLabels - Optional array of label names
|
* @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
|
* @returns Formatted prompt string for the validation request
|
||||||
*/
|
*/
|
||||||
export function buildValidationPrompt(
|
export function buildValidationPrompt(
|
||||||
issueNumber: number,
|
issueNumber: number,
|
||||||
issueTitle: string,
|
issueTitle: string,
|
||||||
issueBody: string,
|
issueBody: string,
|
||||||
issueLabels?: string[]
|
issueLabels?: string[],
|
||||||
|
comments?: ValidationComment[],
|
||||||
|
linkedPRs?: ValidationLinkedPR[]
|
||||||
): string {
|
): string {
|
||||||
const labelsSection = issueLabels?.length ? `\n\n**Labels:** ${issueLabels.join(', ')}` : '';
|
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:
|
return `Please validate the following GitHub issue by analyzing the codebase:
|
||||||
|
|
||||||
## Issue #${issueNumber}: ${issueTitle}
|
## Issue #${issueNumber}: ${issueTitle}
|
||||||
${labelsSection}
|
${labelsSection}
|
||||||
|
${linkedPRsSection}
|
||||||
|
|
||||||
### Description
|
### Description
|
||||||
|
|
||||||
${issueBody || '(No description provided)'}
|
${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 { IssueRow, IssueDetailPanel, IssuesListHeader } from './github-issues-view/components';
|
||||||
import { ValidationDialog } from './github-issues-view/dialogs';
|
import { ValidationDialog } from './github-issues-view/dialogs';
|
||||||
import { formatDate, getFeaturePriority } from './github-issues-view/utils';
|
import { formatDate, getFeaturePriority } from './github-issues-view/utils';
|
||||||
|
import type { ValidateIssueOptions } from './github-issues-view/types';
|
||||||
|
|
||||||
export function GitHubIssuesView() {
|
export function GitHubIssuesView() {
|
||||||
const [selectedIssue, setSelectedIssue] = useState<GitHubIssue | null>(null);
|
const [selectedIssue, setSelectedIssue] = useState<GitHubIssue | null>(null);
|
||||||
const [validationResult, setValidationResult] = useState<IssueValidationResult | null>(null);
|
const [validationResult, setValidationResult] = useState<IssueValidationResult | null>(null);
|
||||||
const [showValidationDialog, setShowValidationDialog] = useState(false);
|
const [showValidationDialog, setShowValidationDialog] = useState(false);
|
||||||
const [showRevalidateConfirm, setShowRevalidateConfirm] = useState(false);
|
const [showRevalidateConfirm, setShowRevalidateConfirm] = useState(false);
|
||||||
|
const [pendingRevalidateOptions, setPendingRevalidateOptions] =
|
||||||
|
useState<ValidateIssueOptions | null>(null);
|
||||||
|
|
||||||
const { currentProject, defaultAIProfileId, aiProfiles, getCurrentWorktree, worktreesByProject } =
|
const { currentProject, defaultAIProfileId, aiProfiles, getCurrentWorktree, worktreesByProject } =
|
||||||
useAppStore();
|
useAppStore();
|
||||||
@@ -203,7 +206,10 @@ export function GitHubIssuesView() {
|
|||||||
onViewCachedValidation={handleViewCachedValidation}
|
onViewCachedValidation={handleViewCachedValidation}
|
||||||
onOpenInGitHub={handleOpenInGitHub}
|
onOpenInGitHub={handleOpenInGitHub}
|
||||||
onClose={() => setSelectedIssue(null)}
|
onClose={() => setSelectedIssue(null)}
|
||||||
onShowRevalidateConfirm={() => setShowRevalidateConfirm(true)}
|
onShowRevalidateConfirm={(options) => {
|
||||||
|
setPendingRevalidateOptions(options);
|
||||||
|
setShowRevalidateConfirm(true);
|
||||||
|
}}
|
||||||
formatDate={formatDate}
|
formatDate={formatDate}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -220,15 +226,24 @@ export function GitHubIssuesView() {
|
|||||||
{/* Revalidate Confirmation Dialog */}
|
{/* Revalidate Confirmation Dialog */}
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
open={showRevalidateConfirm}
|
open={showRevalidateConfirm}
|
||||||
onOpenChange={setShowRevalidateConfirm}
|
onOpenChange={(open) => {
|
||||||
|
setShowRevalidateConfirm(open);
|
||||||
|
if (!open) {
|
||||||
|
setPendingRevalidateOptions(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
title="Re-validate Issue"
|
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.`}
|
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}
|
icon={RefreshCw}
|
||||||
iconClassName="text-primary"
|
iconClassName="text-primary"
|
||||||
confirmText="Re-validate"
|
confirmText="Re-validate"
|
||||||
onConfirm={() => {
|
onConfirm={() => {
|
||||||
if (selectedIssue) {
|
if (selectedIssue && pendingRevalidateOptions) {
|
||||||
handleValidateIssue(selectedIssue, { forceRevalidate: true });
|
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 { IssueRow } from './issue-row';
|
||||||
export { IssueDetailPanel } from './issue-detail-panel';
|
export { IssueDetailPanel } from './issue-detail-panel';
|
||||||
export { IssuesListHeader } from './issues-list-header';
|
export { IssuesListHeader } from './issues-list-header';
|
||||||
|
export { CommentItem } from './comment-item';
|
||||||
|
|||||||
@@ -10,12 +10,19 @@ import {
|
|||||||
GitPullRequest,
|
GitPullRequest,
|
||||||
User,
|
User,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
|
MessageSquare,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronUp,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import { useState } from 'react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import { Markdown } from '@/components/ui/markdown';
|
import { Markdown } from '@/components/ui/markdown';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import type { IssueDetailPanelProps } from '../types';
|
import type { IssueDetailPanelProps } from '../types';
|
||||||
import { isValidationStale } from '../utils';
|
import { isValidationStale } from '../utils';
|
||||||
|
import { useIssueComments } from '../hooks';
|
||||||
|
import { CommentItem } from './comment-item';
|
||||||
|
|
||||||
export function IssueDetailPanel({
|
export function IssueDetailPanel({
|
||||||
issue,
|
issue,
|
||||||
@@ -32,6 +39,32 @@ export function IssueDetailPanel({
|
|||||||
const cached = cachedValidations.get(issue.number);
|
const cached = cachedValidations.get(issue.number);
|
||||||
const isStale = cached ? isValidationStale(cached.validatedAt) : false;
|
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 (
|
return (
|
||||||
<div className="flex-1 flex flex-col overflow-hidden">
|
<div className="flex-1 flex flex-col overflow-hidden">
|
||||||
{/* Detail Header */}
|
{/* Detail Header */}
|
||||||
@@ -67,7 +100,7 @@ export function IssueDetailPanel({
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={onShowRevalidateConfirm}
|
onClick={() => onShowRevalidateConfirm(getValidationOptions(true))}
|
||||||
title="Re-validate"
|
title="Re-validate"
|
||||||
>
|
>
|
||||||
<RefreshCw className="h-4 w-4" />
|
<RefreshCw className="h-4 w-4" />
|
||||||
@@ -86,7 +119,7 @@ export function IssueDetailPanel({
|
|||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="default"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => onValidateIssue(issue, { forceRevalidate: true })}
|
onClick={() => onValidateIssue(issue, getValidationOptions(true))}
|
||||||
>
|
>
|
||||||
<Wand2 className="h-4 w-4 mr-1" />
|
<Wand2 className="h-4 w-4 mr-1" />
|
||||||
Re-validate
|
Re-validate
|
||||||
@@ -96,7 +129,11 @@ export function IssueDetailPanel({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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" />
|
<Wand2 className="h-4 w-4 mr-1" />
|
||||||
Validate with AI
|
Validate with AI
|
||||||
</Button>
|
</Button>
|
||||||
@@ -226,6 +263,74 @@ export function IssueDetailPanel({
|
|||||||
<p className="text-sm text-muted-foreground italic">No description provided.</p>
|
<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 */}
|
{/* Open in GitHub CTA */}
|
||||||
<div className="mt-8 p-4 rounded-lg bg-muted/50 border border-border">
|
<div className="mt-8 p-4 rounded-lg bg-muted/50 border border-border">
|
||||||
<p className="text-sm text-muted-foreground mb-3">
|
<p className="text-sm text-muted-foreground mb-3">
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ import {
|
|||||||
Lightbulb,
|
Lightbulb,
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
Plus,
|
Plus,
|
||||||
|
GitPullRequest,
|
||||||
|
Clock,
|
||||||
|
Wrench,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import type {
|
import type {
|
||||||
@@ -149,6 +152,77 @@ export function ValidationDialog({
|
|||||||
</div>
|
</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 */}
|
{/* Reasoning */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<h4 className="text-sm font-medium flex items-center gap-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)}>
|
<Button variant="ghost" onClick={() => onOpenChange(false)}>
|
||||||
Close
|
Close
|
||||||
</Button>
|
</Button>
|
||||||
{validationResult?.verdict === 'valid' && onConvertToTask && (
|
{validationResult?.verdict === 'valid' &&
|
||||||
<Button onClick={handleConvertToTask}>
|
onConvertToTask &&
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
validationResult?.prAnalysis?.recommendation !== 'wait_for_merge' && (
|
||||||
Convert to Task
|
<Button onClick={handleConvertToTask}>
|
||||||
</Button>
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
)}
|
Convert to Task
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
export { useGithubIssues } from './use-github-issues';
|
export { useGithubIssues } from './use-github-issues';
|
||||||
export { useIssueValidation } from './use-issue-validation';
|
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 {
|
import {
|
||||||
getElectronAPI,
|
getElectronAPI,
|
||||||
GitHubIssue,
|
GitHubIssue,
|
||||||
|
GitHubComment,
|
||||||
IssueValidationResult,
|
IssueValidationResult,
|
||||||
IssueValidationEvent,
|
IssueValidationEvent,
|
||||||
StoredValidation,
|
StoredValidation,
|
||||||
} from '@/lib/electron';
|
} from '@/lib/electron';
|
||||||
|
import type { LinkedPRInfo } from '@automaker/types';
|
||||||
import { useAppStore } from '@/store/app-store';
|
import { useAppStore } from '@/store/app-store';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { isValidationStale } from '../utils';
|
import { isValidationStale } from '../utils';
|
||||||
@@ -205,8 +207,15 @@ export function useIssueValidation({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleValidateIssue = useCallback(
|
const handleValidateIssue = useCallback(
|
||||||
async (issue: GitHubIssue, options: { forceRevalidate?: boolean } = {}) => {
|
async (
|
||||||
const { forceRevalidate = false } = options;
|
issue: GitHubIssue,
|
||||||
|
options: {
|
||||||
|
forceRevalidate?: boolean;
|
||||||
|
comments?: GitHubComment[];
|
||||||
|
linkedPRs?: LinkedPRInfo[];
|
||||||
|
} = {}
|
||||||
|
) => {
|
||||||
|
const { forceRevalidate = false, comments, linkedPRs } = options;
|
||||||
|
|
||||||
if (!currentProject?.path) {
|
if (!currentProject?.path) {
|
||||||
toast.error('No project selected');
|
toast.error('No project selected');
|
||||||
@@ -236,14 +245,17 @@ export function useIssueValidation({
|
|||||||
try {
|
try {
|
||||||
const api = getElectronAPI();
|
const api = getElectronAPI();
|
||||||
if (api.github?.validateIssue) {
|
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(
|
const result = await api.github.validateIssue(
|
||||||
currentProject.path,
|
currentProject.path,
|
||||||
{
|
validationInput,
|
||||||
issueNumber: issue.number,
|
|
||||||
issueTitle: issue.title,
|
|
||||||
issueBody: issue.body || '',
|
|
||||||
issueLabels: issue.labels.map((l) => l.name),
|
|
||||||
},
|
|
||||||
validationModel
|
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 {
|
export interface IssueRowProps {
|
||||||
issue: GitHubIssue;
|
issue: GitHubIssue;
|
||||||
@@ -12,17 +13,25 @@ export interface IssueRowProps {
|
|||||||
isValidating?: boolean;
|
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 {
|
export interface IssueDetailPanelProps {
|
||||||
issue: GitHubIssue;
|
issue: GitHubIssue;
|
||||||
validatingIssues: Set<number>;
|
validatingIssues: Set<number>;
|
||||||
cachedValidations: Map<number, StoredValidation>;
|
cachedValidations: Map<number, StoredValidation>;
|
||||||
onValidateIssue: (
|
onValidateIssue: (issue: GitHubIssue, options?: ValidateIssueOptions) => Promise<void>;
|
||||||
issue: GitHubIssue,
|
|
||||||
options?: { showDialog?: boolean; forceRevalidate?: boolean }
|
|
||||||
) => Promise<void>;
|
|
||||||
onViewCachedValidation: (issue: GitHubIssue) => Promise<void>;
|
onViewCachedValidation: (issue: GitHubIssue) => Promise<void>;
|
||||||
onOpenInGitHub: (url: string) => void;
|
onOpenInGitHub: (url: string) => void;
|
||||||
onClose: () => 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;
|
formatDate: (date: string) => string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import type {
|
|||||||
IssueValidationEvent,
|
IssueValidationEvent,
|
||||||
StoredValidation,
|
StoredValidation,
|
||||||
AgentModel,
|
AgentModel,
|
||||||
|
GitHubComment,
|
||||||
|
IssueCommentsResult,
|
||||||
} from '@automaker/types';
|
} from '@automaker/types';
|
||||||
import { getJSON, setJSON, removeItem } from './storage';
|
import { getJSON, setJSON, removeItem } from './storage';
|
||||||
|
|
||||||
@@ -24,6 +26,8 @@ export type {
|
|||||||
IssueValidationResponse,
|
IssueValidationResponse,
|
||||||
IssueValidationEvent,
|
IssueValidationEvent,
|
||||||
StoredValidation,
|
StoredValidation,
|
||||||
|
GitHubComment,
|
||||||
|
IssueCommentsResult,
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface FileEntry {
|
export interface FileEntry {
|
||||||
@@ -234,6 +238,19 @@ export interface GitHubAPI {
|
|||||||
) => Promise<{ success: boolean; error?: string }>;
|
) => Promise<{ success: boolean; error?: string }>;
|
||||||
/** Subscribe to validation events */
|
/** Subscribe to validation events */
|
||||||
onValidationEvent: (callback: (event: IssueValidationEvent) => void) => () => void;
|
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
|
// Feature Suggestions types
|
||||||
@@ -2786,6 +2803,15 @@ function createMockGitHubAPI(): GitHubAPI {
|
|||||||
mockValidationCallbacks = mockValidationCallbacks.filter((cb) => cb !== callback);
|
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 }),
|
this.post('/api/github/validation-mark-viewed', { projectPath, issueNumber }),
|
||||||
onValidationEvent: (callback: (event: IssueValidationEvent) => void) =>
|
onValidationEvent: (callback: (event: IssueValidationEvent) => void) =>
|
||||||
this.subscribeToEvent('issue-validation:event', callback as EventCallback),
|
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
|
// Workspace API
|
||||||
|
|||||||
@@ -87,6 +87,9 @@ export type {
|
|||||||
IssueValidationVerdict,
|
IssueValidationVerdict,
|
||||||
IssueValidationConfidence,
|
IssueValidationConfidence,
|
||||||
IssueComplexity,
|
IssueComplexity,
|
||||||
|
PRRecommendation,
|
||||||
|
PRAnalysis,
|
||||||
|
LinkedPRInfo,
|
||||||
IssueValidationInput,
|
IssueValidationInput,
|
||||||
IssueValidationRequest,
|
IssueValidationRequest,
|
||||||
IssueValidationResult,
|
IssueValidationResult,
|
||||||
@@ -94,6 +97,9 @@ export type {
|
|||||||
IssueValidationErrorResponse,
|
IssueValidationErrorResponse,
|
||||||
IssueValidationEvent,
|
IssueValidationEvent,
|
||||||
StoredValidation,
|
StoredValidation,
|
||||||
|
GitHubCommentAuthor,
|
||||||
|
GitHubComment,
|
||||||
|
IssueCommentsResult,
|
||||||
} from './issue-validation.js';
|
} from './issue-validation.js';
|
||||||
|
|
||||||
// Backlog plan types
|
// Backlog plan types
|
||||||
|
|||||||
@@ -21,6 +21,36 @@ export type IssueValidationConfidence = 'high' | 'medium' | 'low';
|
|||||||
*/
|
*/
|
||||||
export type IssueComplexity = 'trivial' | 'simple' | 'moderate' | 'complex' | 'very_complex';
|
export type IssueComplexity = 'trivial' | 'simple' | 'moderate' | 'complex' | 'very_complex';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recommendation for PR-related action
|
||||||
|
*/
|
||||||
|
export type PRRecommendation = 'wait_for_merge' | 'pr_needs_work' | 'no_pr';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Analysis of a linked pull request
|
||||||
|
*/
|
||||||
|
export interface PRAnalysis {
|
||||||
|
/** Whether there is an open PR linked to this issue */
|
||||||
|
hasOpenPR: boolean;
|
||||||
|
/** Whether the PR appears to fix the issue based on the diff */
|
||||||
|
prFixesIssue?: boolean;
|
||||||
|
/** The PR number that was analyzed */
|
||||||
|
prNumber?: number;
|
||||||
|
/** Brief summary of what the PR changes */
|
||||||
|
prSummary?: string;
|
||||||
|
/** Recommendation: wait for PR to merge, PR needs more work, or no relevant PR */
|
||||||
|
recommendation: PRRecommendation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Linked PR info for validation
|
||||||
|
*/
|
||||||
|
export interface LinkedPRInfo {
|
||||||
|
number: number;
|
||||||
|
title: string;
|
||||||
|
state: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Issue data for validation (without projectPath)
|
* Issue data for validation (without projectPath)
|
||||||
* Used by UI when calling the validation API
|
* Used by UI when calling the validation API
|
||||||
@@ -30,6 +60,10 @@ export interface IssueValidationInput {
|
|||||||
issueTitle: string;
|
issueTitle: string;
|
||||||
issueBody: string;
|
issueBody: string;
|
||||||
issueLabels?: string[];
|
issueLabels?: string[];
|
||||||
|
/** Comments to include in validation analysis */
|
||||||
|
comments?: GitHubComment[];
|
||||||
|
/** Linked pull requests for this issue */
|
||||||
|
linkedPRs?: LinkedPRInfo[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -60,6 +94,8 @@ export interface IssueValidationResult {
|
|||||||
missingInfo?: string[];
|
missingInfo?: string[];
|
||||||
/** Estimated effort to address the issue */
|
/** Estimated effort to address the issue */
|
||||||
estimatedComplexity?: IssueComplexity;
|
estimatedComplexity?: IssueComplexity;
|
||||||
|
/** Analysis of linked pull requests (if any) */
|
||||||
|
prAnalysis?: PRAnalysis;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -133,3 +169,41 @@ export interface StoredValidation {
|
|||||||
/** ISO timestamp when user viewed this validation (undefined = not yet viewed) */
|
/** ISO timestamp when user viewed this validation (undefined = not yet viewed) */
|
||||||
viewedAt?: string;
|
viewedAt?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Author of a GitHub comment
|
||||||
|
*/
|
||||||
|
export interface GitHubCommentAuthor {
|
||||||
|
login: string;
|
||||||
|
avatarUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A comment on a GitHub issue
|
||||||
|
*/
|
||||||
|
export interface GitHubComment {
|
||||||
|
/** Unique comment ID */
|
||||||
|
id: string;
|
||||||
|
/** Author of the comment */
|
||||||
|
author: GitHubCommentAuthor;
|
||||||
|
/** Comment body (markdown) */
|
||||||
|
body: string;
|
||||||
|
/** ISO timestamp when comment was created */
|
||||||
|
createdAt: string;
|
||||||
|
/** ISO timestamp when comment was last updated */
|
||||||
|
updatedAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result from fetching issue comments
|
||||||
|
*/
|
||||||
|
export interface IssueCommentsResult {
|
||||||
|
/** List of comments */
|
||||||
|
comments: GitHubComment[];
|
||||||
|
/** Total number of comments on the issue */
|
||||||
|
totalCount: number;
|
||||||
|
/** Whether there are more comments to fetch */
|
||||||
|
hasNextPage: boolean;
|
||||||
|
/** Cursor for pagination (pass to next request) */
|
||||||
|
endCursor?: string;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user