From b172be49cab701442e780c9caaa0f2c600583d4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sondre=20Engebr=C3=A5ten?= Date: Thu, 12 Feb 2026 22:10:03 +0100 Subject: [PATCH] feat(triage): GAP-34 batch triage confirmation dialog with cost estimate Triage All button now shows inline confirmation (role=alert) with issue count and estimated cost via estimateBatchCost() before executing. Confirm fires onTriageAll, cancel reverts to button. i18n key added for both EN and FR. 2 new tests, 16 total. Co-Authored-By: Claude Opus 4.6 --- .../components/BulkActionBar.tsx | 48 ++++++++++++++----- .../__tests__/BulkActionBar.test.tsx | 46 ++++++++++++++++++ .../src/shared/i18n/locales/en/common.json | 3 +- .../src/shared/i18n/locales/fr/common.json | 3 +- docs/gap-tracker.md | 14 +++--- 5 files changed, 93 insertions(+), 21 deletions(-) diff --git a/apps/frontend/src/renderer/components/github-issues/components/BulkActionBar.tsx b/apps/frontend/src/renderer/components/github-issues/components/BulkActionBar.tsx index 10c7e909..5fe8dc33 100644 --- a/apps/frontend/src/renderer/components/github-issues/components/BulkActionBar.tsx +++ b/apps/frontend/src/renderer/components/github-issues/components/BulkActionBar.tsx @@ -1,6 +1,7 @@ import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import type { BulkActionType, BulkOperationProgress } from '../../../../shared/types/mutations'; +import { estimateBatchCost } from '../../../../shared/constants/ai-triage'; interface BulkActionBarProps { selectedCount: number; @@ -35,6 +36,7 @@ export function BulkActionBar({ }: BulkActionBarProps) { const { t } = useTranslation('common'); const [pendingAction, setPendingAction] = useState(null); + const [pendingTriageAll, setPendingTriageAll] = useState(false); if (selectedCount === 0) { return null; @@ -119,18 +121,40 @@ export function BulkActionBar({ )} {onTriageAll && untriagedCount != null && untriagedCount > 0 && ( - + pendingTriageAll ? ( +
+ + {t('aiTriage.confirmTriage', { count: untriagedCount, cost: estimateBatchCost(untriagedCount, 'sonnet') })} + + + +
+ ) : ( + + ) )} {isOperating && progress && ( diff --git a/apps/frontend/src/renderer/components/github-issues/components/__tests__/BulkActionBar.test.tsx b/apps/frontend/src/renderer/components/github-issues/components/__tests__/BulkActionBar.test.tsx index 666b1364..1913c9f8 100644 --- a/apps/frontend/src/renderer/components/github-issues/components/__tests__/BulkActionBar.test.tsx +++ b/apps/frontend/src/renderer/components/github-issues/components/__tests__/BulkActionBar.test.tsx @@ -11,6 +11,10 @@ vi.mock('react-i18next', () => ({ }), })); +vi.mock('../../../../../shared/constants/ai-triage', () => ({ + estimateBatchCost: (count: number) => `~$${(count * 0.0035).toFixed(2)}`, +})); + describe('BulkActionBar', () => { it('not rendered when selectedCount is 0', () => { const { container } = render( @@ -215,4 +219,46 @@ describe('BulkActionBar', () => { const triageBtn = screen.getByRole('button', { name: 'aiTriage.triageAllButton' }); expect(triageBtn).toBeDefined(); }); + + it('Triage All shows confirmation dialog with cost estimate before executing', () => { + const onTriageAll = vi.fn(); + render( + , + ); + // Click Triage All — should NOT fire immediately + fireEvent.click(screen.getByRole('button', { name: 'aiTriage.triageAllButton' })); + expect(onTriageAll).not.toHaveBeenCalled(); + // Confirmation dialog should appear with cost estimate text + expect(screen.getByRole('alert')).toBeDefined(); + expect(screen.getByText('aiTriage.confirmTriage')).toBeDefined(); + // Confirm fires onTriageAll + fireEvent.click(screen.getByText('bulk.confirm')); + expect(onTriageAll).toHaveBeenCalledTimes(1); + }); + + it('Triage All confirmation can be cancelled', () => { + const onTriageAll = vi.fn(); + render( + , + ); + fireEvent.click(screen.getByRole('button', { name: 'aiTriage.triageAllButton' })); + expect(screen.getByRole('alert')).toBeDefined(); + // Cancel + fireEvent.click(screen.getByText('bulk.cancel')); + expect(onTriageAll).not.toHaveBeenCalled(); + // Button should be back + expect(screen.getByRole('button', { name: 'aiTriage.triageAllButton' })).toBeDefined(); + }); }); diff --git a/apps/frontend/src/shared/i18n/locales/en/common.json b/apps/frontend/src/shared/i18n/locales/en/common.json index a55b76ce..3fa47ead 100644 --- a/apps/frontend/src/shared/i18n/locales/en/common.json +++ b/apps/frontend/src/shared/i18n/locales/en/common.json @@ -565,7 +565,8 @@ "splitButton": "Split Issue", "triageAllButton": "Triage All Untriaged", "retry": "Retry", - "closeAsDuplicate": "Close as Duplicate" + "closeAsDuplicate": "Close as Duplicate", + "confirmTriage": "Triage {{count}} issues? Estimated cost: {{cost}}" }, "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 35b2f33b..e34f08a0 100644 --- a/apps/frontend/src/shared/i18n/locales/fr/common.json +++ b/apps/frontend/src/shared/i18n/locales/fr/common.json @@ -565,7 +565,8 @@ "splitButton": "Diviser l'issue", "triageAllButton": "Trier tous les non triés", "retry": "Réessayer", - "closeAsDuplicate": "Fermer comme doublon" + "closeAsDuplicate": "Fermer comme doublon", + "confirmTriage": "Trier {{count}} issues ? Coût estimé : {{cost}}" }, "batchReview": { "title": "Revue de triage par lots", diff --git a/docs/gap-tracker.md b/docs/gap-tracker.md index a759653a..626774ff 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:** 26 / 41 complete +**Status:** 27 / 41 complete --- @@ -417,16 +417,16 @@ Each gap has: ID, description, status, files to modify, doc reference, test stat - **Commit:** pending ### GAP-34: No batch triage confirmation dialog or cost estimate -- **Status:** `PENDING` +- **Status:** `DONE` - **Priority:** SHOULD-FIX - **Scope:** Medium - **Doc ref:** Phase 3 PRD > US-2 > AC2.2; NFR 3.2 -- **Files to modify:** `renderer/components/github-issues/components/BulkActionBar.tsx` -- **Fix:** Add confirmation state. Import estimateBatchCost. Show dialog with count + estimated cost before triaging. Only call onTriageAll after confirm. -- **Tests:** Click Triage All → confirm dialog with cost; confirm → action; cancel → no action -- **Test status:** `PENDING` +- **Files modified:** `BulkActionBar.tsx`, `BulkActionBar.test.tsx`, `en/common.json`, `fr/common.json` +- **Fix:** Added `pendingTriageAll` state. Clicking Triage All shows inline confirmation (role=alert) with issue count + estimated cost via `estimateBatchCost()`. Confirm fires `onTriageAll`, cancel reverts to button. i18n key: aiTriage.confirmTriage (EN+FR). +- **Tests:** 2 new tests: confirm fires action with cost shown, cancel reverts. 16 total. +- **Test status:** `PASS` - **Depends on:** None -- **Commit:** — +- **Commit:** pending ### GAP-35: No undo batch mechanism - **Status:** `PENDING`