diff --git a/apps/frontend/src/renderer/components/github-issues/components/TriageResultCard.tsx b/apps/frontend/src/renderer/components/github-issues/components/TriageResultCard.tsx index d8b93998..2b1cd0ad 100644 --- a/apps/frontend/src/renderer/components/github-issues/components/TriageResultCard.tsx +++ b/apps/frontend/src/renderer/components/github-issues/components/TriageResultCard.tsx @@ -16,9 +16,11 @@ interface TriageResultCardProps { item: TriageReviewItem; onAccept: (issueNumber: number) => void; onReject: (issueNumber: number) => void; + onNavigateToIssue?: (issueNumber: number) => void; + onCloseAsDuplicate?: (issueNumber: number, duplicateOf: number) => void; } -export function TriageResultCard({ item, onAccept, onReject }: TriageResultCardProps) { +export function TriageResultCard({ item, onAccept, onReject, onNavigateToIssue, onCloseAsDuplicate }: TriageResultCardProps) { const { t } = useTranslation(['common']); const { result } = item; const level = getConfidenceLevel(result.confidence); @@ -58,11 +60,37 @@ export function TriageResultCard({ item, onAccept, onReject }: TriageResultCardP {/* Duplicate */} - {result.isDuplicate && result.duplicateOf && ( -
- {t('common:aiTriage.duplicateOf')} #{result.duplicateOf} -
- )} + {result.isDuplicate && result.duplicateOf && (() => { + const dupOf = result.duplicateOf; + return ( +
+ + {t('common:aiTriage.duplicateOf')}{' '} + {onNavigateToIssue ? ( + + ) : ( + #{dupOf} + )} + + {onCloseAsDuplicate && isPending && ( + + )} +
+ ); + })()} {/* Status / Actions */} {item.status === 'accepted' && ( diff --git a/apps/frontend/src/renderer/components/github-issues/components/__tests__/TriageResultCard.test.tsx b/apps/frontend/src/renderer/components/github-issues/components/__tests__/TriageResultCard.test.tsx index cdde1011..edfa52cd 100644 --- a/apps/frontend/src/renderer/components/github-issues/components/__tests__/TriageResultCard.test.tsx +++ b/apps/frontend/src/renderer/components/github-issues/components/__tests__/TriageResultCard.test.tsx @@ -94,4 +94,52 @@ describe('TriageResultCard', () => { render(); expect(screen.queryByRole('button', { name: /accept/i })).toBeNull(); }); + + it('makes duplicate issue number clickable when onNavigateToIssue provided', () => { + const onNavigate = vi.fn(); + const item = createItem(); + item.result.isDuplicate = true; + item.result.duplicateOf = 10; + render( + , + ); + const link = screen.getByRole('button', { name: '#10' }); + expect(link).toBeDefined(); + fireEvent.click(link); + expect(onNavigate).toHaveBeenCalledWith(10); + }); + + it('renders duplicate number as static text when onNavigateToIssue absent', () => { + const item = createItem(); + item.result.isDuplicate = true; + item.result.duplicateOf = 10; + render(); + // Should have #10 text but not as a button + expect(screen.getByText(/#10/)).toBeDefined(); + expect(screen.queryByRole('button', { name: '#10' })).toBeNull(); + }); + + it('shows Close as Duplicate button when onCloseAsDuplicate provided and pending', () => { + const onClose = vi.fn(); + const item = createItem(); + item.result.isDuplicate = true; + item.result.duplicateOf = 10; + render( + , + ); + const btn = screen.getByRole('button', { name: 'common:aiTriage.closeAsDuplicate' }); + expect(btn).toBeDefined(); + fireEvent.click(btn); + expect(onClose).toHaveBeenCalledWith(42, 10); + }); + + it('hides Close as Duplicate for already-accepted items', () => { + const item = createItem({ status: 'accepted' }); + item.result.isDuplicate = true; + item.result.duplicateOf = 10; + render( + , + ); + expect(screen.queryByRole('button', { name: 'common:aiTriage.closeAsDuplicate' })).toBeNull(); + }); }); diff --git a/apps/frontend/src/shared/i18n/locales/en/common.json b/apps/frontend/src/shared/i18n/locales/en/common.json index e9865888..a55b76ce 100644 --- a/apps/frontend/src/shared/i18n/locales/en/common.json +++ b/apps/frontend/src/shared/i18n/locales/en/common.json @@ -564,7 +564,8 @@ "improveButton": "Improve Issue", "splitButton": "Split Issue", "triageAllButton": "Triage All Untriaged", - "retry": "Retry" + "retry": "Retry", + "closeAsDuplicate": "Close as Duplicate" }, "batchReview": { "title": "Batch Triage Review", diff --git a/apps/frontend/src/shared/i18n/locales/fr/common.json b/apps/frontend/src/shared/i18n/locales/fr/common.json index 9a95f25b..35b2f33b 100644 --- a/apps/frontend/src/shared/i18n/locales/fr/common.json +++ b/apps/frontend/src/shared/i18n/locales/fr/common.json @@ -564,7 +564,8 @@ "improveButton": "Améliorer l'issue", "splitButton": "Diviser l'issue", "triageAllButton": "Trier tous les non triés", - "retry": "Réessayer" + "retry": "Réessayer", + "closeAsDuplicate": "Fermer comme doublon" }, "batchReview": { "title": "Revue de triage par lots", diff --git a/docs/gap-tracker.md b/docs/gap-tracker.md index df2d6e7e..a759653a 100644 --- a/docs/gap-tracker.md +++ b/docs/gap-tracker.md @@ -3,7 +3,7 @@ **Branch:** `terminal/enhancement-issues-tab` **Created:** 2026-02-12 **Total Gaps:** 41 confirmed (from triple-verified audit) -**Status:** 25 / 41 complete +**Status:** 26 / 41 complete --- @@ -405,16 +405,16 @@ Each gap has: ID, description, status, files to modify, doc reference, test stat - **Commit:** pending ### GAP-33: Duplicate detection display-only -- **Status:** `PENDING` +- **Status:** `DONE` - **Priority:** SHOULD-FIX - **Scope:** Medium - **Doc ref:** Phase 3 PRD > US-7 > AC7.2, AC7.3 -- **Files to modify:** `renderer/components/github-issues/components/TriageResultCard.tsx` -- **Fix:** Make #{duplicateOf} a clickable button with onNavigateToIssue prop. Add "Close as Duplicate" button with onCloseAsDuplicate prop. -- **Tests:** Click duplicate link → onNavigateToIssue called; Close as Duplicate → onCloseAsDuplicate called -- **Test status:** `PENDING` +- **Files modified:** `TriageResultCard.tsx`, `TriageResultCard.test.tsx`, `en/common.json`, `fr/common.json` +- **Fix:** Added `onNavigateToIssue` and `onCloseAsDuplicate` props. Duplicate number rendered as clickable button (text-primary hover:underline) when onNavigateToIssue provided, else static span. "Close as Duplicate" button shown when onCloseAsDuplicate provided and item is pending. i18n key: aiTriage.closeAsDuplicate (EN+FR). +- **Tests:** 5 new tests: clickable duplicate navigates, static when absent, close-as-duplicate fires callback, hidden when accepted. 13 total. +- **Test status:** `PASS` - **Depends on:** None -- **Commit:** — +- **Commit:** pending ### GAP-34: No batch triage confirmation dialog or cost estimate - **Status:** `PENDING`