From 80441de62cd3736f862bf40d8c8aa9973cd39554 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sondre=20Engebr=C3=A5ten?= Date: Fri, 13 Feb 2026 00:31:32 +0100 Subject: [PATCH] feat(triage): GAP-19,35,40,41,42,43 Phase E polish - GAP-19: Ctrl+1/2/3 keyboard shortcuts for triage panel navigation - GAP-35: Undo batch mechanism (snapshot/restore in store + UI button) - GAP-40: Label sync debounce (2000ms via useRef timer) - GAP-41: Bulk label sync IPC handler + preload + hook method - GAP-42: Color preview swatches in LabelSyncSettings - GAP-43: Write/Preview markdown toggle in CommentForm + i18n 40/41 gaps complete. Co-Authored-By: Claude Opus 4.6 --- .../github/label-sync-handlers.ts | 57 +++++++++++++ .../src/preload/api/modules/github-api.ts | 4 + .../src/renderer/components/GitHubIssues.tsx | 10 ++- .../components/BatchTriageReview.tsx | 12 +++ .../github-issues/components/CommentForm.tsx | 69 ++++++++++++--- .../components/LabelSyncSettings.tsx | 20 +++++ .../__tests__/BatchTriageReview.test.tsx | 16 ++++ .../components/__tests__/CommentForm.test.tsx | 60 +++++++++---- .../__tests__/LabelSyncSettings.test.tsx | 12 +++ .../hooks/__tests__/useLabelSync.test.ts | 71 ++++++++++++++-- .../hooks/__tests__/useTriageMode.test.ts | 85 ++++++++++++++++++- .../github-issues/hooks/useLabelSync.ts | 46 ++++++++-- .../github-issues/hooks/useTriageMode.ts | 21 +++++ .../github/__tests__/ai-triage-store.test.ts | 45 ++++++++++ .../renderer/stores/github/ai-triage-store.ts | 17 ++++ apps/frontend/src/shared/constants/ipc.ts | 1 + .../src/shared/i18n/locales/en/common.json | 11 ++- .../src/shared/i18n/locales/fr/common.json | 11 ++- docs/gap-tracker.md | 80 +++++++++-------- 19 files changed, 562 insertions(+), 86 deletions(-) diff --git a/apps/frontend/src/main/ipc-handlers/github/label-sync-handlers.ts b/apps/frontend/src/main/ipc-handlers/github/label-sync-handlers.ts index a3a72b94..981dd4f1 100644 --- a/apps/frontend/src/main/ipc-handlers/github/label-sync-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/github/label-sync-handlers.ts @@ -184,6 +184,63 @@ export function registerLabelSyncHandlers( }, ); + // Bulk sync labels for multiple issues + ipcMain.handle( + 'github:label-sync:bulk', + async (_, projectId: string, issueNumbers: number[]) => { + return withProject(projectId, async (project) => { + const config = readConfig(project.path); + if (!config.enabled) { + return { synced: 0, errors: 0 }; + } + + const env = getAugmentedEnv(); + const data = await readEnrichmentFile(project.path); + let synced = 0; + let errors = 0; + + for (const issueNumber of issueNumbers) { + const enrichment = data.issues[String(issueNumber)]; + if (!enrichment?.triageState) continue; + + const targetLabel = getLabelForState(enrichment.triageState as WorkflowState); + + try { + // Get current labels + const labelsJson = execFileSync('gh', [ + 'issue', 'view', String(issueNumber), + '--json', 'labels', + '--jq', '.labels', + ], { env, cwd: project.path, encoding: 'utf-8' }); + + const currentLabels = JSON.parse(labelsJson) as Array<{ name: string }>; + if (currentLabels.some((l) => l.name === targetLabel)) { + synced++; + continue; + } + + const args = ['issue', 'edit', String(issueNumber)]; + for (const label of currentLabels) { + if (isAutoClaudeLabel(label.name) && label.name !== targetLabel) { + args.push('--remove-label', label.name); + } + } + args.push('--add-label', targetLabel); + + execFileSync('gh', args, { env, cwd: project.path, encoding: 'utf-8' }); + synced++; + } catch (error) { + logger.debug('Bulk sync error for issue', { issueNumber, error }); + errors++; + } + } + + logger.debug('Bulk label sync complete', { synced, errors }); + return { synced, errors }; + }); + }, + ); + // Save label sync config ipcMain.handle( 'github:label-sync:save', diff --git a/apps/frontend/src/preload/api/modules/github-api.ts b/apps/frontend/src/preload/api/modules/github-api.ts index 828919ab..47589a73 100644 --- a/apps/frontend/src/preload/api/modules/github-api.ts +++ b/apps/frontend/src/preload/api/modules/github-api.ts @@ -400,6 +400,7 @@ export interface GitHubAPI { syncIssueLabel: (projectId: string, issueNumber: number, newState: string, oldState: string | null) => Promise<{ synced?: boolean; skipped?: boolean; error?: string }>; getLabelSyncStatus: (projectId: string) => Promise; saveLabelSyncConfig: (projectId: string, config: LabelSyncConfig) => Promise<{ success: boolean }>; + bulkLabelSync: (projectId: string, issueNumbers: number[]) => Promise<{ synced: number; errors: number }>; // Dependencies (Phase 4) fetchDependencies: (projectId: string, issueNumber: number) => Promise; @@ -1007,6 +1008,9 @@ export const createGitHubAPI = (): GitHubAPI => ({ saveLabelSyncConfig: (projectId: string, config: LabelSyncConfig): Promise<{ success: boolean }> => invokeIpc(IPC_CHANNELS.GITHUB_LABEL_SYNC_SAVE, projectId, config), + bulkLabelSync: (projectId: string, issueNumbers: number[]): Promise<{ synced: number; errors: number }> => + invokeIpc(IPC_CHANNELS.GITHUB_LABEL_SYNC_BULK, projectId, issueNumbers), + // Dependencies (Phase 4) fetchDependencies: (projectId: string, issueNumber: number) => invokeIpc(IPC_CHANNELS.GITHUB_DEPS_FETCH, projectId, issueNumber), diff --git a/apps/frontend/src/renderer/components/GitHubIssues.tsx b/apps/frontend/src/renderer/components/GitHubIssues.tsx index 3786dec3..9a42c6ca 100644 --- a/apps/frontend/src/renderer/components/GitHubIssues.tsx +++ b/apps/frontend/src/renderer/components/GitHubIssues.tsx @@ -181,6 +181,7 @@ export function GitHubIssues({ onOpenSettings, onNavigateToTask }: GitHubIssuesP // AI Triage const aiTriage = useAITriage(selectedProject?.id ?? ''); + const lastBatchSnapshot = useAITriageStore((s) => s.lastBatchSnapshot); // Triage mode (3-panel layout) const { isEnabled: triageModeEnabled, isAvailable: triageModeAvailable, toggle: toggleTriageMode } = useTriageMode(); @@ -393,7 +394,7 @@ export function GitHubIssues({ onOpenSettings, onNavigateToTask }: GitHubIssuesP {/* Content */}
{/* Issue List */} -
+
{/* Issue Detail */} -
+
{selectedIssue ? ( +
{ useAITriageStore.getState().acceptAllRemaining(); }} onDismiss={() => { useAITriageStore.getState().dismissReview(); }} - onApply={aiTriage.applyTriageResults} + onApply={() => { useAITriageStore.getState().snapshotBeforeApply(); aiTriage.applyTriageResults(); }} + onUndo={lastBatchSnapshot ? () => { useAITriageStore.getState().undoLastBatch(); } : undefined} /> )} diff --git a/apps/frontend/src/renderer/components/github-issues/components/BatchTriageReview.tsx b/apps/frontend/src/renderer/components/github-issues/components/BatchTriageReview.tsx index cbb780fe..e2f97d3b 100644 --- a/apps/frontend/src/renderer/components/github-issues/components/BatchTriageReview.tsx +++ b/apps/frontend/src/renderer/components/github-issues/components/BatchTriageReview.tsx @@ -13,6 +13,7 @@ interface BatchTriageReviewProps { onAcceptAll: () => void; onDismiss: () => void; onApply: () => void; + onUndo?: () => void; } export function BatchTriageReview({ @@ -22,6 +23,7 @@ export function BatchTriageReview({ onAcceptAll, onDismiss, onApply, + onUndo, }: BatchTriageReviewProps) { const { t } = useTranslation(['common']); const reviewed = items.filter((i) => i.status !== 'pending').length; @@ -67,6 +69,16 @@ export function BatchTriageReview({ {t('common:batchReview.apply')} )} + {onUndo && ( + + )}