diff --git a/apps/frontend/src/renderer/components/GitHubIssues.tsx b/apps/frontend/src/renderer/components/GitHubIssues.tsx index cf56952a..6a4f93a6 100644 --- a/apps/frontend/src/renderer/components/GitHubIssues.tsx +++ b/apps/frontend/src/renderer/components/GitHubIssues.tsx @@ -12,6 +12,7 @@ import { useIssueFiltering, useAutoFix, useBulkOperations, + useAITriage, } from "./github-issues/hooks"; import { useAnalyzePreview } from "./github-issues/hooks/useAnalyzePreview"; import { @@ -23,6 +24,9 @@ import { InvestigationDialog, BatchReviewWizard, BulkActionBar, + TriageProgressOverlay, + IssueSplitDialog, + EnrichmentCommentPreview, } from "./github-issues/components"; import { GitHubSetupModal } from "./GitHubSetupModal"; import type { GitHubIssue } from "../../shared/types"; @@ -135,6 +139,9 @@ export function GitHubIssues({ onOpenSettings, onNavigateToTask }: GitHubIssuesP [executeBulk, selectedIssueNumbers], ); + // AI Triage + const aiTriage = useAITriage(selectedProject?.id ?? ''); + // Clear selection when filters change useEffect(() => { setSelectedIssueNumbers(new Set()); @@ -289,6 +296,10 @@ export function GitHubIssues({ onOpenSettings, onNavigateToTask }: GitHubIssuesP autoFixQueueItem={getAutoFixQueueItem(selectedIssue.number)} enrichment={enrichments[String(selectedIssue.number)] ?? null} onTransition={handleTransition} + onAITriage={() => aiTriage.runEnrichment(selectedIssue.number)} + onImproveIssue={() => aiTriage.runEnrichment(selectedIssue.number)} + onSplitIssue={() => aiTriage.runSplitSuggestion(selectedIssue.number)} + isAIBusy={aiTriage.isTriaging} /> ) : ( @@ -321,6 +332,24 @@ export function GitHubIssues({ onOpenSettings, onNavigateToTask }: GitHubIssuesP isApproving={isApproving} /> + {/* AI Triage Progress */} + {(aiTriage.enrichmentProgress || aiTriage.triageProgress) && ( + { /* cancel handled by store */ }} + /> + )} + + {/* Split Dialog */} + {aiTriage.splitSuggestion && ( + { /* reset split state */ }} + /> + )} + {/* GitHub Setup Modal - shown when GitHub module is not configured */} {selectedProject && ( ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})); + +vi.mock('../../../../shared/constants/ai-triage', () => ({ + ENRICHMENT_COMMENT_FOOTER: '---\n_AI-generated_', +})); + +describe('AI Triage integration', () => { + it('TriageProgressOverlay renders progress bar and message', () => { + const progress = { progress: 60, message: 'Enriching issue...' }; + render(); + expect(screen.getByText('Enriching issue...')).toBeDefined(); + expect(screen.getByRole('progressbar')).toBeDefined(); + }); + + it('TriageProgressOverlay cancel button calls onCancel', () => { + const onCancel = vi.fn(); + const progress = { progress: 30, message: 'Working...' }; + render(); + fireEvent.click(screen.getByRole('button')); + expect(onCancel).toHaveBeenCalledOnce(); + }); + + it('IssueSplitDialog renders sub-issues from suggestion', () => { + const suggestion = { + issueNumber: 42, + rationale: 'This issue should be split', + subIssues: [ + { title: 'Sub-issue A', body: 'Body A', labels: ['bug'] }, + { title: 'Sub-issue B', body: 'Body B', labels: [] }, + ], + }; + render( + , + ); + expect(screen.getByDisplayValue('Sub-issue A')).toBeDefined(); + expect(screen.getByDisplayValue('Sub-issue B')).toBeDefined(); + expect(screen.getByText('This issue should be split')).toBeDefined(); + }); + + it('IssueSplitDialog onConfirm passes sub-issues', () => { + const onConfirm = vi.fn(); + const suggestion = { + issueNumber: 42, + rationale: 'Split needed', + subIssues: [{ title: 'Sub A', body: 'Body A', labels: [] }], + }; + render( + , + ); + // Click the confirm button + const confirmBtn = screen.getByRole('button', { name: 'common:issueSplit.confirm' }); + fireEvent.click(confirmBtn); + expect(onConfirm).toHaveBeenCalledWith(42, [{ title: 'Sub A', body: 'Body A', labels: [] }]); + }); + + it('EnrichmentCommentPreview renders content and post button', () => { + render( + , + ); + expect(screen.getByDisplayValue('AI analysis comment')).toBeDefined(); + }); + + it('EnrichmentCommentPreview calls onCancel on discard', () => { + const onCancel = vi.fn(); + render( + , + ); + const cancelBtn = screen.getByRole('button', { name: 'common:enrichmentComment.cancel' }); + fireEvent.click(cancelBtn); + expect(onCancel).toHaveBeenCalledOnce(); + }); +});