From 96196f906f944036881bb24155e862221ec8da50 Mon Sep 17 00:00:00 2001 From: Kacper Date: Sun, 28 Dec 2025 22:11:02 +0100 Subject: [PATCH 1/4] feat: add GitHub issue comments display and AI validation integration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add comments section to issue detail panel with lazy loading - Fetch comments via GraphQL API with pagination (50 at a time) - Include comments in AI validation analysis when checkbox enabled - Pass linked PRs info to AI validation for context - Add "Work in Progress" badge in validation dialog for open PRs - Add debug logging for validation requests 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- apps/server/src/routes/github/index.ts | 2 + .../src/routes/github/routes/list-comments.ts | 183 ++++++++++++++++++ .../routes/github/routes/validate-issue.ts | 84 +++++++- .../routes/github/routes/validation-schema.ts | 54 +++++- .../components/views/github-issues-view.tsx | 23 ++- .../components/comment-item.tsx | 40 ++++ .../github-issues-view/components/index.ts | 1 + .../components/issue-detail-panel.tsx | 120 +++++++++++- .../dialogs/validation-dialog.tsx | 18 ++ .../views/github-issues-view/hooks/index.ts | 1 + .../hooks/use-issue-comments.ts | 133 +++++++++++++ .../hooks/use-issue-validation.ts | 42 +++- .../views/github-issues-view/types.ts | 21 +- apps/ui/src/lib/electron.ts | 26 +++ apps/ui/src/lib/http-api-client.ts | 2 + libs/types/src/index.ts | 4 + libs/types/src/issue-validation.ts | 51 +++++ 17 files changed, 777 insertions(+), 28 deletions(-) create mode 100644 apps/server/src/routes/github/routes/list-comments.ts create mode 100644 apps/ui/src/components/views/github-issues-view/components/comment-item.tsx create mode 100644 apps/ui/src/components/views/github-issues-view/hooks/use-issue-comments.ts diff --git a/apps/server/src/routes/github/index.ts b/apps/server/src/routes/github/index.ts index 1a2f12ae..dddae96e 100644 --- a/apps/server/src/routes/github/index.ts +++ b/apps/server/src/routes/github/index.ts @@ -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'), diff --git a/apps/server/src/routes/github/routes/list-comments.ts b/apps/server/src/routes/github/routes/list-comments.ts new file mode 100644 index 00000000..1813923e --- /dev/null +++ b/apps/server/src/routes/github/routes/list-comments.ts @@ -0,0 +1,183 @@ +/** + * 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 }>; +} + +/** + * Fetch comments for a specific issue using GitHub GraphQL API + */ +async function fetchIssueComments( + projectPath: string, + owner: string, + repo: string, + issueNumber: number, + cursor?: string +): Promise { + const cursorParam = cursor ? `, after: "${cursor}"` : ''; + + const query = `{ + repository(owner: "${owner}", name: "${repo}") { + issue(number: ${issueNumber}) { + comments(first: 50${cursorParam}) { + totalCount + pageInfo { + hasNextPage + endCursor + } + nodes { + id + author { + login + avatarUrl + } + body + createdAt + updatedAt + } + } + } + } + }`; + + const requestBody = JSON.stringify({ query }); + + const response = await new Promise((resolve, reject) => { + const gh = spawn('gh', ['api', 'graphql', '--input', '-'], { + cwd: projectPath, + env: execEnv, + }); + + 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) => { + 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 => { + 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) }); + } + }; +} diff --git a/apps/server/src/routes/github/routes/validate-issue.ts b/apps/server/src/routes/github/routes/validate-issue.ts index c987453a..d7c9ffc0 100644 --- a/apps/server/src/routes/github/routes/validate-issue.ts +++ b/apps/server/src/routes/github/routes/validate-issue.ts @@ -8,7 +8,13 @@ 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 { @@ -29,6 +35,24 @@ import { getAutoLoadClaudeMdSetting } from '../../../lib/settings-helpers.js'; /** Valid model values for validation */ const VALID_MODELS: readonly AgentModel[] = ['opus', 'sonnet', 'haiku'] as const; +/** + * Comment structure for validation prompt + */ +interface ValidationComment { + author: string; + createdAt: string; + body: string; +} + +/** + * Linked PR structure for validation prompt + */ +interface ValidationLinkedPR { + number: number; + title: string; + state: string; +} + /** * Request body for issue validation */ @@ -40,6 +64,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 +85,9 @@ async function runValidation( model: AgentModel, events: EventEmitter, abortController: AbortController, - settingsService?: SettingsService + settingsService?: SettingsService, + comments?: ValidationComment[], + linkedPRs?: ValidationLinkedPR[] ): Promise { // Emit start event const startEvent: IssueValidationEvent = { @@ -76,8 +106,28 @@ 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) + logger.info( + `Building validation prompt for issue #${issueNumber}` + + (comments?.length ? ` with ${comments.length} comments` : ' without comments') + + (linkedPRs?.length ? ` and ${linkedPRs.length} linked PRs` : '') + ); + if (comments?.length) { + logger.debug(`Comments included: ${comments.map((c) => c.author).join(', ')}`); + } + if (linkedPRs?.length) { + logger.debug( + `Linked PRs: ${linkedPRs.map((pr) => `#${pr.number} (${pr.state})`).join(', ')}` + ); + } + const prompt = buildValidationPrompt( + issueNumber, + issueTitle, + issueBody, + issueLabels, + comments, + linkedPRs + ); // Load autoLoadClaudeMd setting const autoLoadClaudeMd = await getAutoLoadClaudeMdSetting( @@ -214,8 +264,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,7 +343,9 @@ export function createValidateIssueHandler( model, events, abortController, - settingsService + settingsService, + validationComments, + validationLinkedPRs ) .catch((error) => { // Error is already handled inside runValidation (event emitted) diff --git a/apps/server/src/routes/github/routes/validation-schema.ts b/apps/server/src/routes/github/routes/validation-schema.ts index 50812082..6716905b 100644 --- a/apps/server/src/routes/github/routes/validation-schema.ts +++ b/apps/server/src/routes/github/routes/validation-schema.ts @@ -103,6 +103,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 + */ +interface ValidationComment { + author: string; + createdAt: string; + body: string; +} + +/** + * Linked PR data structure for validation prompt + */ +interface ValidationLinkedPR { + number: number; + title: string; + state: string; +} + /** * Build the user prompt for issue validation. * @@ -113,26 +131,58 @@ 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).toLocaleDateString()}):\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.' : ''}`; } diff --git a/apps/ui/src/components/views/github-issues-view.tsx b/apps/ui/src/components/views/github-issues-view.tsx index 10876e38..fa41972e 100644 --- a/apps/ui/src/components/views/github-issues-view.tsx +++ b/apps/ui/src/components/views/github-issues-view.tsx @@ -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(null); const [validationResult, setValidationResult] = useState(null); const [showValidationDialog, setShowValidationDialog] = useState(false); const [showRevalidateConfirm, setShowRevalidateConfirm] = useState(false); + const [pendingRevalidateOptions, setPendingRevalidateOptions] = + useState(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 */} { + 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); } }} /> diff --git a/apps/ui/src/components/views/github-issues-view/components/comment-item.tsx b/apps/ui/src/components/views/github-issues-view/components/comment-item.tsx new file mode 100644 index 00000000..c4a60cda --- /dev/null +++ b/apps/ui/src/components/views/github-issues-view/components/comment-item.tsx @@ -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 ( +
+ {/* Comment Header */} +
+ {comment.author.avatarUrl ? ( + {comment.author.login} + ) : ( +
+ +
+ )} + {comment.author.login} + + commented {formatDate(comment.createdAt)} + +
+ + {/* Comment Body */} + {comment.body ? ( + {comment.body} + ) : ( +

No content

+ )} +
+ ); +} diff --git a/apps/ui/src/components/views/github-issues-view/components/index.ts b/apps/ui/src/components/views/github-issues-view/components/index.ts index b3af9f84..6ed7e4d0 100644 --- a/apps/ui/src/components/views/github-issues-view/components/index.ts +++ b/apps/ui/src/components/views/github-issues-view/components/index.ts @@ -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'; diff --git a/apps/ui/src/components/views/github-issues-view/components/issue-detail-panel.tsx b/apps/ui/src/components/views/github-issues-view/components/issue-detail-panel.tsx index 7969da38..831a6ce2 100644 --- a/apps/ui/src/components/views/github-issues-view/components/issue-detail-panel.tsx +++ b/apps/ui/src/components/views/github-issues-view/components/issue-detail-panel.tsx @@ -10,12 +10,18 @@ import { GitPullRequest, User, RefreshCw, + MessageSquare, + ChevronDown, + ChevronUp, } from 'lucide-react'; +import { useState } from 'react'; import { Button } from '@/components/ui/button'; 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 +38,40 @@ 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) => { + const options = { + forceRevalidate, + comments: includeCommentsInAnalysis && comments.length > 0 ? comments : undefined, + linkedPRs: issue.linkedPRs?.map((pr) => ({ + number: pr.number, + title: pr.title, + state: pr.state, + })), + }; + console.log('[IssueDetailPanel] getValidationOptions:', { + includeCommentsInAnalysis, + commentsCount: comments.length, + linkedPRsCount: issue.linkedPRs?.length ?? 0, + willIncludeComments: !!options.comments, + willIncludeLinkedPRs: !!options.linkedPRs, + }); + return options; + }; + return (
{/* Detail Header */} @@ -67,7 +107,7 @@ export function IssueDetailPanel({ @@ -226,6 +270,76 @@ export function IssueDetailPanel({

No description provided.

)} + {/* Comments Section */} +
+
+ + {comments.length > 0 && ( + + )} +
+ + {commentsExpanded && ( +
+ {commentsError ? ( +

{commentsError}

+ ) : comments.length === 0 && !commentsLoading ? ( +

No comments yet.

+ ) : ( +
+ {comments.map((comment) => ( + + ))} + + {/* Load More Button */} + {hasNextPage && ( + + )} +
+ )} +
+ )} +
+ {/* Open in GitHub CTA */}

diff --git a/apps/ui/src/components/views/github-issues-view/dialogs/validation-dialog.tsx b/apps/ui/src/components/views/github-issues-view/dialogs/validation-dialog.tsx index fba1a9ea..7ac04ea3 100644 --- a/apps/ui/src/components/views/github-issues-view/dialogs/validation-dialog.tsx +++ b/apps/ui/src/components/views/github-issues-view/dialogs/validation-dialog.tsx @@ -16,6 +16,7 @@ import { Lightbulb, AlertTriangle, Plus, + GitPullRequest, } from 'lucide-react'; import { cn } from '@/lib/utils'; import type { @@ -149,6 +150,23 @@ export function ValidationDialog({

)} + {/* Work in Progress Badge - Show when there's an open PR linked */} + {issue.linkedPRs?.some((pr) => pr.state === 'open' || pr.state === 'OPEN') && ( +
+ +
+ Work in Progress +

+ {issue.linkedPRs + .filter((pr) => pr.state === 'open' || pr.state === 'OPEN') + .map((pr) => `PR #${pr.number}`) + .join(', ')}{' '} + is open for this issue +

+
+
+ )} + {/* Reasoning */}

diff --git a/apps/ui/src/components/views/github-issues-view/hooks/index.ts b/apps/ui/src/components/views/github-issues-view/hooks/index.ts index c3417416..57b78868 100644 --- a/apps/ui/src/components/views/github-issues-view/hooks/index.ts +++ b/apps/ui/src/components/views/github-issues-view/hooks/index.ts @@ -1,2 +1,3 @@ export { useGithubIssues } from './use-github-issues'; export { useIssueValidation } from './use-issue-validation'; +export { useIssueComments } from './use-issue-comments'; diff --git a/apps/ui/src/components/views/github-issues-view/hooks/use-issue-comments.ts b/apps/ui/src/components/views/github-issues-view/hooks/use-issue-comments.ts new file mode 100644 index 00000000..e0505b36 --- /dev/null +++ b/apps/ui/src/components/views/github-issues-view/hooks/use-issue-comments.ts @@ -0,0 +1,133 @@ +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([]); + const [totalCount, setTotalCount] = useState(0); + const [loading, setLoading] = useState(false); + const [loadingMore, setLoadingMore] = useState(false); + const [hasNextPage, setHasNextPage] = useState(false); + const [endCursor, setEndCursor] = useState(undefined); + const [error, setError] = useState(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); + } + + 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, + }; +} diff --git a/apps/ui/src/components/views/github-issues-view/hooks/use-issue-validation.ts b/apps/ui/src/components/views/github-issues-view/hooks/use-issue-validation.ts index 136185c5..198beef9 100644 --- a/apps/ui/src/components/views/github-issues-view/hooks/use-issue-validation.ts +++ b/apps/ui/src/components/views/github-issues-view/hooks/use-issue-validation.ts @@ -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,23 @@ 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; + console.log('[useIssueValidation] handleValidateIssue called with:', { + issueNumber: issue.number, + forceRevalidate, + commentsProvided: !!comments, + commentsCount: comments?.length ?? 0, + linkedPRsProvided: !!linkedPRs, + linkedPRsCount: linkedPRs?.length ?? 0, + }); if (!currentProject?.path) { toast.error('No project selected'); @@ -236,14 +253,23 @@ 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 + }; + console.log('[useIssueValidation] Sending validation request:', { + hasComments: !!validationInput.comments, + commentsCount: validationInput.comments?.length ?? 0, + hasLinkedPRs: !!validationInput.linkedPRs, + linkedPRsCount: validationInput.linkedPRs?.length ?? 0, + }); 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 ); diff --git a/apps/ui/src/components/views/github-issues-view/types.ts b/apps/ui/src/components/views/github-issues-view/types.ts index 9fce6d53..5bf1fd0c 100644 --- a/apps/ui/src/components/views/github-issues-view/types.ts +++ b/apps/ui/src/components/views/github-issues-view/types.ts @@ -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; cachedValidations: Map; - onValidateIssue: ( - issue: GitHubIssue, - options?: { showDialog?: boolean; forceRevalidate?: boolean } - ) => Promise; + onValidateIssue: (issue: GitHubIssue, options?: ValidateIssueOptions) => Promise; onViewCachedValidation: (issue: GitHubIssue) => Promise; 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; } diff --git a/apps/ui/src/lib/electron.ts b/apps/ui/src/lib/electron.ts index 698f915e..bafa5cf3 100644 --- a/apps/ui/src/lib/electron.ts +++ b/apps/ui/src/lib/electron.ts @@ -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, + }; + }, }; } diff --git a/apps/ui/src/lib/http-api-client.ts b/apps/ui/src/lib/http-api-client.ts index ddbfd51a..1efcbcd1 100644 --- a/apps/ui/src/lib/http-api-client.ts +++ b/apps/ui/src/lib/http-api-client.ts @@ -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 diff --git a/libs/types/src/index.ts b/libs/types/src/index.ts index ad45206a..11d3c39d 100644 --- a/libs/types/src/index.ts +++ b/libs/types/src/index.ts @@ -87,6 +87,7 @@ export type { IssueValidationVerdict, IssueValidationConfidence, IssueComplexity, + LinkedPRInfo, IssueValidationInput, IssueValidationRequest, IssueValidationResult, @@ -94,6 +95,9 @@ export type { IssueValidationErrorResponse, IssueValidationEvent, StoredValidation, + GitHubCommentAuthor, + GitHubComment, + IssueCommentsResult, } from './issue-validation.js'; // Backlog plan types diff --git a/libs/types/src/issue-validation.ts b/libs/types/src/issue-validation.ts index 2c0d2f64..89131b6c 100644 --- a/libs/types/src/issue-validation.ts +++ b/libs/types/src/issue-validation.ts @@ -21,6 +21,15 @@ export type IssueValidationConfidence = 'high' | 'medium' | 'low'; */ export type IssueComplexity = 'trivial' | 'simple' | 'moderate' | 'complex' | 'very_complex'; +/** + * Linked PR info for validation + */ +export interface LinkedPRInfo { + number: number; + title: string; + state: string; +} + /** * Issue data for validation (without projectPath) * Used by UI when calling the validation API @@ -30,6 +39,10 @@ export interface IssueValidationInput { issueTitle: string; issueBody: string; issueLabels?: string[]; + /** Comments to include in validation analysis */ + comments?: GitHubComment[]; + /** Linked pull requests for this issue */ + linkedPRs?: LinkedPRInfo[]; } /** @@ -133,3 +146,41 @@ export interface StoredValidation { /** ISO timestamp when user viewed this validation (undefined = not yet viewed) */ 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; +} From 97ae4b63625c04c7ecb0f8e21873a91c286b260e Mon Sep 17 00:00:00 2001 From: Kacper Date: Sun, 28 Dec 2025 22:22:14 +0100 Subject: [PATCH 2/4] feat: enhance AI validation with PR analysis and UI improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace HTML checkbox with proper UI Checkbox component - Add system prompt instructions for AI to check PR changes via gh CLI - Add PRAnalysis schema field with recommendation (wait_for_merge, pr_needs_work, no_pr) - Show detailed PR analysis badge in validation dialog - Hide "Convert to Task" button when PR fix is ready (wait_for_merge) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../routes/github/routes/validation-schema.ts | 51 +++++++++- .../components/issue-detail-panel.tsx | 9 +- .../dialogs/validation-dialog.tsx | 96 +++++++++++++++---- libs/types/src/index.ts | 2 + libs/types/src/issue-validation.ts | 23 +++++ 5 files changed, 156 insertions(+), 25 deletions(-) diff --git a/apps/server/src/routes/github/routes/validation-schema.ts b/apps/server/src/routes/github/routes/validation-schema.ts index 6716905b..e33a2bda 100644 --- a/apps/server/src/routes/github/routes/validation-schema.ts +++ b/apps/server/src/routes/github/routes/validation-schema.ts @@ -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 \` 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 \`** to see what changes the PR makes +2. **Run \`gh pr view \`** 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 diff --git a/apps/ui/src/components/views/github-issues-view/components/issue-detail-panel.tsx b/apps/ui/src/components/views/github-issues-view/components/issue-detail-panel.tsx index 831a6ce2..758cdcb1 100644 --- a/apps/ui/src/components/views/github-issues-view/components/issue-detail-panel.tsx +++ b/apps/ui/src/components/views/github-issues-view/components/issue-detail-panel.tsx @@ -16,6 +16,7 @@ import { } 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'; @@ -291,12 +292,10 @@ export function IssueDetailPanel({ )} {comments.length > 0 && ( - diff --git a/apps/ui/src/components/views/github-issues-view/dialogs/validation-dialog.tsx b/apps/ui/src/components/views/github-issues-view/dialogs/validation-dialog.tsx index 7ac04ea3..214c0abe 100644 --- a/apps/ui/src/components/views/github-issues-view/dialogs/validation-dialog.tsx +++ b/apps/ui/src/components/views/github-issues-view/dialogs/validation-dialog.tsx @@ -17,6 +17,8 @@ import { AlertTriangle, Plus, GitPullRequest, + Clock, + Wrench, } from 'lucide-react'; import { cn } from '@/lib/utils'; import type { @@ -150,23 +152,77 @@ export function ValidationDialog({

)} - {/* Work in Progress Badge - Show when there's an open PR linked */} - {issue.linkedPRs?.some((pr) => pr.state === 'open' || pr.state === 'OPEN') && ( -
- -
- Work in Progress -

- {issue.linkedPRs - .filter((pr) => pr.state === 'open' || pr.state === 'OPEN') - .map((pr) => `PR #${pr.number}`) - .join(', ')}{' '} - is open for this issue -

+ {/* PR Analysis Section - Show AI's analysis of linked PRs */} + {validationResult.prAnalysis && validationResult.prAnalysis.hasOpenPR && ( +
+
+ {validationResult.prAnalysis.recommendation === 'wait_for_merge' ? ( + + ) : validationResult.prAnalysis.recommendation === 'pr_needs_work' ? ( + + ) : ( + + )} +
+ + {validationResult.prAnalysis.recommendation === 'wait_for_merge' + ? 'Fix Ready - Wait for Merge' + : validationResult.prAnalysis.recommendation === 'pr_needs_work' + ? 'PR Needs Work' + : 'Work in Progress'} + + {validationResult.prAnalysis.prNumber && ( +

+ PR #{validationResult.prAnalysis.prNumber} + {validationResult.prAnalysis.prFixesIssue && ' appears to fix this issue'} +

+ )} + {validationResult.prAnalysis.prSummary && ( +

+ {validationResult.prAnalysis.prSummary} +

+ )} +
)} + {/* 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') && ( +
+ +
+ Work in Progress +

+ {issue.linkedPRs + .filter((pr) => pr.state === 'open' || pr.state === 'OPEN') + .map((pr) => `PR #${pr.number}`) + .join(', ')}{' '} + is open for this issue +

+
+
+ )} + {/* Reasoning */}

@@ -236,12 +292,14 @@ export function ValidationDialog({ - {validationResult?.verdict === 'valid' && onConvertToTask && ( - - )} + {validationResult?.verdict === 'valid' && + onConvertToTask && + validationResult?.prAnalysis?.recommendation !== 'wait_for_merge' && ( + + )} diff --git a/libs/types/src/index.ts b/libs/types/src/index.ts index 11d3c39d..2bcb20a0 100644 --- a/libs/types/src/index.ts +++ b/libs/types/src/index.ts @@ -87,6 +87,8 @@ export type { IssueValidationVerdict, IssueValidationConfidence, IssueComplexity, + PRRecommendation, + PRAnalysis, LinkedPRInfo, IssueValidationInput, IssueValidationRequest, diff --git a/libs/types/src/issue-validation.ts b/libs/types/src/issue-validation.ts index 89131b6c..1384a3ef 100644 --- a/libs/types/src/issue-validation.ts +++ b/libs/types/src/issue-validation.ts @@ -21,6 +21,27 @@ export type IssueValidationConfidence = 'high' | 'medium' | 'low'; */ 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 */ @@ -73,6 +94,8 @@ export interface IssueValidationResult { missingInfo?: string[]; /** Estimated effort to address the issue */ estimatedComplexity?: IssueComplexity; + /** Analysis of linked pull requests (if any) */ + prAnalysis?: PRAnalysis; } /** From 6bdac230df02261ea26ffab26058b83da10c4ad0 Mon Sep 17 00:00:00 2001 From: Kacper Date: Sun, 28 Dec 2025 22:40:37 +0100 Subject: [PATCH 3/4] fix: address PR review comments for GitHub issue comments feature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use GraphQL variables instead of string interpolation for safety - Add cursor validation to prevent potential GraphQL injection - Add 30s timeout for spawned gh process to prevent hanging - Export ValidationComment and ValidationLinkedPR from validation-schema - Remove duplicate interface definitions from validate-issue.ts - Use ISO date format instead of locale-dependent toLocaleDateString() - Reset error state when issue is deselected in useIssueComments hook 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../src/routes/github/routes/list-comments.ts | 71 +++++++++++++------ .../routes/github/routes/validate-issue.ts | 20 +----- .../routes/github/routes/validation-schema.ts | 8 ++- .../hooks/use-issue-comments.ts | 1 + 4 files changed, 58 insertions(+), 42 deletions(-) diff --git a/apps/server/src/routes/github/routes/list-comments.ts b/apps/server/src/routes/github/routes/list-comments.ts index 1813923e..2c057709 100644 --- a/apps/server/src/routes/github/routes/list-comments.ts +++ b/apps/server/src/routes/github/routes/list-comments.ts @@ -43,6 +43,16 @@ interface GraphQLResponse { 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 */ @@ -53,33 +63,45 @@ async function fetchIssueComments( issueNumber: number, cursor?: string ): Promise { - const cursorParam = cursor ? `, after: "${cursor}"` : ''; + // Validate cursor format to prevent potential injection + if (cursor && !isValidCursor(cursor)) { + throw new Error('Invalid cursor format'); + } - const query = `{ - repository(owner: "${owner}", name: "${repo}") { - issue(number: ${issueNumber}) { - comments(first: 50${cursorParam}) { - totalCount - pageInfo { - hasNextPage - endCursor - } - nodes { - id - author { - login - avatarUrl + // 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 } - body - createdAt - updatedAt } } } - } - }`; + }`; - const requestBody = JSON.stringify({ query }); + const variables = { + owner, + repo, + issueNumber, + cursor: cursor || null, + }; + + const requestBody = JSON.stringify({ query, variables }); const response = await new Promise((resolve, reject) => { const gh = spawn('gh', ['api', 'graphql', '--input', '-'], { @@ -87,12 +109,19 @@ async function fetchIssueComments( 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}`)); } diff --git a/apps/server/src/routes/github/routes/validate-issue.ts b/apps/server/src/routes/github/routes/validate-issue.ts index d7c9ffc0..8d51c4b1 100644 --- a/apps/server/src/routes/github/routes/validate-issue.ts +++ b/apps/server/src/routes/github/routes/validate-issue.ts @@ -21,6 +21,8 @@ import { issueValidationSchema, ISSUE_VALIDATION_SYSTEM_PROMPT, buildValidationPrompt, + ValidationComment, + ValidationLinkedPR, } from './validation-schema.js'; import { trySetValidationRunning, @@ -35,24 +37,6 @@ import { getAutoLoadClaudeMdSetting } from '../../../lib/settings-helpers.js'; /** Valid model values for validation */ const VALID_MODELS: readonly AgentModel[] = ['opus', 'sonnet', 'haiku'] as const; -/** - * Comment structure for validation prompt - */ -interface ValidationComment { - author: string; - createdAt: string; - body: string; -} - -/** - * Linked PR structure for validation prompt - */ -interface ValidationLinkedPR { - number: number; - title: string; - state: string; -} - /** * Request body for issue validation */ diff --git a/apps/server/src/routes/github/routes/validation-schema.ts b/apps/server/src/routes/github/routes/validation-schema.ts index e33a2bda..010fcd7f 100644 --- a/apps/server/src/routes/github/routes/validation-schema.ts +++ b/apps/server/src/routes/github/routes/validation-schema.ts @@ -155,7 +155,7 @@ Be thorough in your analysis but focus on files that are directly relevant to th /** * Comment data structure for validation prompt */ -interface ValidationComment { +export interface ValidationComment { author: string; createdAt: string; body: string; @@ -164,7 +164,7 @@ interface ValidationComment { /** * Linked PR data structure for validation prompt */ -interface ValidationLinkedPR { +export interface ValidationLinkedPR { number: number; title: string; state: string; @@ -207,7 +207,9 @@ export function buildValidationPrompt( // 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).toLocaleDateString()}):\n${c.body}`) + .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}`; diff --git a/apps/ui/src/components/views/github-issues-view/hooks/use-issue-comments.ts b/apps/ui/src/components/views/github-issues-view/hooks/use-issue-comments.ts index e0505b36..b5b3534f 100644 --- a/apps/ui/src/components/views/github-issues-view/hooks/use-issue-comments.ts +++ b/apps/ui/src/components/views/github-issues-view/hooks/use-issue-comments.ts @@ -101,6 +101,7 @@ export function useIssueComments(issueNumber: number | null): UseIssueCommentsRe setHasNextPage(false); setEndCursor(undefined); setLoading(false); + setError(null); } return () => { From d028932dc85955b913d61da6fbf93036d6b7405e Mon Sep 17 00:00:00 2001 From: Kacper Date: Sun, 28 Dec 2025 22:48:32 +0100 Subject: [PATCH 4/4] chore: remove debug logs from issue validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove console.log and logger.debug calls that were added during development. Keep essential logger.info and logger.error calls. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../routes/github/routes/validate-issue.ts | 24 ++----------------- .../components/issue-detail-panel.tsx | 10 +------- .../hooks/use-issue-validation.ts | 14 ----------- 3 files changed, 3 insertions(+), 45 deletions(-) diff --git a/apps/server/src/routes/github/routes/validate-issue.ts b/apps/server/src/routes/github/routes/validate-issue.ts index 8d51c4b1..99673593 100644 --- a/apps/server/src/routes/github/routes/validate-issue.ts +++ b/apps/server/src/routes/github/routes/validate-issue.ts @@ -91,19 +91,6 @@ async function runValidation( try { // Build the prompt (include comments and linked PRs if provided) - logger.info( - `Building validation prompt for issue #${issueNumber}` + - (comments?.length ? ` with ${comments.length} comments` : ' without comments') + - (linkedPRs?.length ? ` and ${linkedPRs.length} linked PRs` : '') - ); - if (comments?.length) { - logger.debug(`Comments included: ${comments.map((c) => c.author).join(', ')}`); - } - if (linkedPRs?.length) { - logger.debug( - `Linked PRs: ${linkedPRs.map((pr) => `#${pr.number} (${pr.state})`).join(', ')}` - ); - } const prompt = buildValidationPrompt( issueNumber, issueTitle, @@ -136,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, @@ -162,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); } } @@ -182,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'); } @@ -331,9 +312,8 @@ export function createValidateIssueHandler( 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); diff --git a/apps/ui/src/components/views/github-issues-view/components/issue-detail-panel.tsx b/apps/ui/src/components/views/github-issues-view/components/issue-detail-panel.tsx index 758cdcb1..e2d7e8b4 100644 --- a/apps/ui/src/components/views/github-issues-view/components/issue-detail-panel.tsx +++ b/apps/ui/src/components/views/github-issues-view/components/issue-detail-panel.tsx @@ -54,7 +54,7 @@ export function IssueDetailPanel({ // Helper to get validation options with comments and linked PRs const getValidationOptions = (forceRevalidate = false) => { - const options = { + return { forceRevalidate, comments: includeCommentsInAnalysis && comments.length > 0 ? comments : undefined, linkedPRs: issue.linkedPRs?.map((pr) => ({ @@ -63,14 +63,6 @@ export function IssueDetailPanel({ state: pr.state, })), }; - console.log('[IssueDetailPanel] getValidationOptions:', { - includeCommentsInAnalysis, - commentsCount: comments.length, - linkedPRsCount: issue.linkedPRs?.length ?? 0, - willIncludeComments: !!options.comments, - willIncludeLinkedPRs: !!options.linkedPRs, - }); - return options; }; return ( diff --git a/apps/ui/src/components/views/github-issues-view/hooks/use-issue-validation.ts b/apps/ui/src/components/views/github-issues-view/hooks/use-issue-validation.ts index 198beef9..78a7eea4 100644 --- a/apps/ui/src/components/views/github-issues-view/hooks/use-issue-validation.ts +++ b/apps/ui/src/components/views/github-issues-view/hooks/use-issue-validation.ts @@ -216,14 +216,6 @@ export function useIssueValidation({ } = {} ) => { const { forceRevalidate = false, comments, linkedPRs } = options; - console.log('[useIssueValidation] handleValidateIssue called with:', { - issueNumber: issue.number, - forceRevalidate, - commentsProvided: !!comments, - commentsCount: comments?.length ?? 0, - linkedPRsProvided: !!linkedPRs, - linkedPRsCount: linkedPRs?.length ?? 0, - }); if (!currentProject?.path) { toast.error('No project selected'); @@ -261,12 +253,6 @@ export function useIssueValidation({ comments, // Include comments if provided linkedPRs, // Include linked PRs if provided }; - console.log('[useIssueValidation] Sending validation request:', { - hasComments: !!validationInput.comments, - commentsCount: validationInput.comments?.length ?? 0, - hasLinkedPRs: !!validationInput.linkedPRs, - linkedPRsCount: validationInput.linkedPRs?.length ?? 0, - }); const result = await api.github.validateIssue( currentProject.path, validationInput,