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();
+ });
+});