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`