feat(integration): WP-6 AI triage dialogs — useAITriage, progress overlay, split dialog wiring

Co-Authored-By: Claude Opus 4.6 <[email protected]>
This commit is contained in:
Sondre Engebråten
2026-02-12 19:58:44 +01:00
co-authored by Claude Opus 4.6
parent 402588448b
commit ed04daea01
2 changed files with 128 additions and 0 deletions
@@ -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}
/>
) : (
<EmptyState message="Select an issue to view details" />
@@ -321,6 +332,24 @@ export function GitHubIssues({ onOpenSettings, onNavigateToTask }: GitHubIssuesP
isApproving={isApproving}
/>
{/* AI Triage Progress */}
{(aiTriage.enrichmentProgress || aiTriage.triageProgress) && (
<TriageProgressOverlay
progress={aiTriage.enrichmentProgress ?? aiTriage.triageProgress ?? { progress: 0, message: '' }}
onCancel={() => { /* cancel handled by store */ }}
/>
)}
{/* Split Dialog */}
{aiTriage.splitSuggestion && (
<IssueSplitDialog
suggestion={aiTriage.splitSuggestion}
progress={aiTriage.splitProgress}
onConfirm={aiTriage.confirmSplit}
onCancel={() => { /* reset split state */ }}
/>
)}
{/* GitHub Setup Modal - shown when GitHub module is not configured */}
{selectedProject && (
<GitHubSetupModal
@@ -0,0 +1,99 @@
/**
* @vitest-environment jsdom
*/
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { TriageProgressOverlay } from '../components/TriageProgressOverlay';
import { IssueSplitDialog } from '../components/IssueSplitDialog';
import { EnrichmentCommentPreview } from '../components/EnrichmentCommentPreview';
vi.mock('react-i18next', () => ({
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(<TriageProgressOverlay progress={progress} onCancel={vi.fn()} />);
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(<TriageProgressOverlay progress={progress} onCancel={onCancel} />);
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(
<IssueSplitDialog
suggestion={suggestion}
progress={null}
onConfirm={vi.fn()}
onCancel={vi.fn()}
/>,
);
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(
<IssueSplitDialog
suggestion={suggestion}
progress={null}
onConfirm={onConfirm}
onCancel={vi.fn()}
/>,
);
// 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(
<EnrichmentCommentPreview
content="AI analysis comment"
onPost={vi.fn()}
onCancel={vi.fn()}
/>,
);
expect(screen.getByDisplayValue('AI analysis comment')).toBeDefined();
});
it('EnrichmentCommentPreview calls onCancel on discard', () => {
const onCancel = vi.fn();
render(
<EnrichmentCommentPreview content="test" onPost={vi.fn()} onCancel={onCancel} />,
);
const cancelBtn = screen.getByRole('button', { name: 'common:enrichmentComment.cancel' });
fireEvent.click(cancelBtn);
expect(onCancel).toHaveBeenCalledOnce();
});
});