feat(issues): add toast notifications, activity log, and task deletion revert

Co-Authored-By: Claude Opus 4.6 <[email protected]>
This commit is contained in:
Sondre Engebråten
2026-02-13 19:42:47 +01:00
co-authored by Claude Opus 4.6
parent 054d6b197d
commit 9ec2919023
8 changed files with 325 additions and 19 deletions
@@ -31,6 +31,16 @@ import {
} from "./github-issues/components";
import { GitHubSetupModal } from "./GitHubSetupModal";
import { ResizablePanels } from "./ui/resizable-panels";
import {
AlertDialog,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
} from "./ui/alert-dialog";
import { useMutationStore } from "../stores/github/mutation-store";
import type { GitHubIssue, InvestigationState, InvestigationDismissReason, SuggestedLabel } from "../../shared/types";
import type { GitHubIssuesProps } from "./github-issues/types";
@@ -108,6 +118,10 @@ export function GitHubIssues({ onOpenSettings, onNavigateToTask }: GitHubIssuesP
const [investigationStateFilter, setInvestigationStateFilter] = useState<InvestigationState[]>([]);
const [showDismissed, setShowDismissed] = useState(false);
// Label consent dialog state
const [showLabelConsent, setShowLabelConsent] = useState(false);
const pendingInvestigationRef = useRef<{ type: 'single'; issue: GitHubIssue } | { type: 'bulk' } | null>(null);
// Apply investigation state filter to issues
const investigationFilteredIssues = useMemo(() => {
if (investigationStateFilter.length === 0 && showDismissed) return filteredIssues;
@@ -146,7 +160,7 @@ export function GitHubIssues({ onOpenSettings, onNavigateToTask }: GitHubIssuesP
// Build investigation states map for IssueList
const investigationStatesMap = useMemo(() => {
const map: Record<string, { state: InvestigationState; progress?: number; linkedTaskId?: string }> = {};
const map: Record<string, { state: InvestigationState; progress?: number; linkedTaskId?: string; isStale?: boolean }> = {};
if (!selectedProject?.id) return map;
for (const issue of investigationFilteredIssues) {
const state = investigationStore.getDerivedState(selectedProject.id, issue.number);
@@ -156,6 +170,7 @@ export function GitHubIssues({ onOpenSettings, onNavigateToTask }: GitHubIssuesP
state,
progress: entry?.progress?.progress,
linkedTaskId: entry?.specId ?? undefined,
isStale: entry?.isStale,
};
}
return map;
@@ -270,6 +285,13 @@ export function GitHubIssues({ onOpenSettings, onNavigateToTask }: GitHubIssuesP
loadPersistedInvestigations(selectedProject.id);
}, [selectedProject?.id]);
// Mark stale investigations: cross-reference investigations with fetched issues
useEffect(() => {
if (!selectedProject?.id || storeIssues.length === 0) return;
const activeIssueNumbers = new Set(storeIssues.map((issue) => issue.number));
investigationStore.markStaleInvestigations(selectedProject.id, activeIssueNumbers);
}, [storeIssues, selectedProject?.id, investigationStore]);
// Clear selection when filters change
// biome-ignore lint/correctness/useExhaustiveDependencies: reset on filter/search change
useEffect(() => {
@@ -298,10 +320,18 @@ export function GitHubIssues({ onOpenSettings, onNavigateToTask }: GitHubIssuesP
// Sync investigation state with linked task status changes
// When a task linked to an issue changes status, update the investigation store
// Also detect deleted tasks and revert investigations to findings_ready
useEffect(() => {
if (!selectedProject?.id) return;
const projectId = selectedProject.id;
// Build a set of specIds from current tasks for fast lookup
const taskSpecIds = new Set<string>();
for (const task of tasks) {
const specId = task.specId || task.id;
if (specId) taskSpecIds.add(specId);
}
for (const task of tasks) {
const issueNumber = task.metadata?.githubIssueNumber;
if (!issueNumber || !task.status) continue;
@@ -312,6 +342,15 @@ export function GitHubIssues({ onOpenSettings, onNavigateToTask }: GitHubIssuesP
investigationStore.syncTaskState(projectId, issueNumber, task.status);
}
// Detect deleted tasks: if an investigation has a specId but no matching task exists
const { investigations } = useInvestigationStore.getState();
for (const inv of Object.values(investigations)) {
if (inv.projectId !== projectId || !inv.specId) continue;
if (!taskSpecIds.has(inv.specId)) {
investigationStore.clearLinkedTask(projectId, inv.issueNumber);
}
}
}, [tasks, selectedProject?.id, investigationStore]);
// Auto-close GitHub issues when linked task reaches "done" and autoCloseIssues is enabled
@@ -379,20 +418,64 @@ export function GitHubIssues({ onOpenSettings, onNavigateToTask }: GitHubIssuesP
}
}, [handleRefresh, autoFixConfig?.enabled, checkForNewIssues]);
// Helper: check if label consent is needed before investigating
const needsLabelConsent = useCallback(() => {
if (!selectedProject?.id) return false;
const settings = investigationStore.getSettings(selectedProject.id);
return !settings?.labelConsentGiven;
}, [selectedProject?.id, investigationStore]);
// Helper: grant label consent and persist
const grantLabelConsent = useCallback(() => {
if (!selectedProject?.id) return;
const current = investigationStore.getSettings(selectedProject.id);
const updated = { ...(current ?? { autoCreateTasks: false, autoStartTasks: false, pipelineMode: 'full' as const, autoPostToGitHub: false, autoCloseIssues: false, maxParallelInvestigations: 3, labelIncludeFilter: [] as string[], labelExcludeFilter: [] as string[] }), labelConsentGiven: true };
investigationStore.setSettings(selectedProject.id, updated);
if (window.electronAPI?.github?.saveInvestigationSettings) {
window.electronAPI.github.saveInvestigationSettings(selectedProject.id, updated).catch(() => {});
}
}, [selectedProject?.id, investigationStore]);
// Investigation callbacks for selected issue
const handleInvestigate = useCallback((issue: GitHubIssue) => {
if (selectedProject?.id) {
startIssueInvestigation(selectedProject.id, issue.number);
if (!selectedProject?.id) return;
if (needsLabelConsent()) {
pendingInvestigationRef.current = { type: 'single', issue };
setShowLabelConsent(true);
return;
}
}, [selectedProject?.id]);
startIssueInvestigation(selectedProject.id, issue.number);
}, [selectedProject?.id, needsLabelConsent]);
// Bulk investigate: queue all selected issues for investigation
const handleBulkInvestigate = useCallback(() => {
if (!selectedProject?.id) return;
if (needsLabelConsent()) {
pendingInvestigationRef.current = { type: 'bulk' };
setShowLabelConsent(true);
return;
}
for (const issueNumber of selectedIssueNumbers) {
startIssueInvestigation(selectedProject.id, issueNumber);
}
}, [selectedProject?.id, selectedIssueNumbers]);
}, [selectedProject?.id, selectedIssueNumbers, needsLabelConsent]);
// Handle consent dialog confirm
const handleConsentConfirm = useCallback(() => {
grantLabelConsent();
setShowLabelConsent(false);
if (!selectedProject?.id) return;
const pending = pendingInvestigationRef.current;
pendingInvestigationRef.current = null;
if (!pending) return;
if (pending.type === 'single') {
startIssueInvestigation(selectedProject.id, pending.issue.number);
} else {
for (const issueNumber of selectedIssueNumbers) {
startIssueInvestigation(selectedProject.id, issueNumber);
}
}
}, [grantLabelConsent, selectedProject?.id, selectedIssueNumbers]);
const handleCancelInvestigation = useCallback(() => {
if (selectedProject?.id && selectedIssue) {
@@ -556,6 +639,7 @@ export function GitHubIssues({ onOpenSettings, onNavigateToTask }: GitHubIssuesP
onDismissIssue={handleDismissIssue}
onPostToGitHub={handlePostToGitHubWrapped}
isPostingToGitHub={isPostingToGitHub}
investigationActivityLog={selectedIssueEntry?.activityLog}
/>
) : (
<EmptyState message="Select an issue to view details" />
@@ -592,6 +676,28 @@ export function GitHubIssues({ onOpenSettings, onNavigateToTask }: GitHubIssuesP
onSkip={() => setShowGitHubSetup(false)}
/>
)}
{/* Label Creation Consent Dialog */}
<AlertDialog open={showLabelConsent} onOpenChange={setShowLabelConsent}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
{t('investigation.labelConsent.title', 'Label Creation Notice')}
</AlertDialogTitle>
<AlertDialogDescription>
{t('investigation.labelConsent.body', 'The first investigation will create up to 5 auto-claude:* labels on your GitHub repository to categorize investigation results. These labels are used for filtering and organization.')}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={() => { pendingInvestigationRef.current = null; }}>
{t('buttons.cancel')}
</AlertDialogCancel>
<AlertDialogAction onClick={handleConsentConfirm}>
{t('investigation.labelConsent.confirm', 'Continue')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}
@@ -11,7 +11,9 @@ import {
Tag,
Check,
X,
FileText
FileText,
Clock,
CheckCircle2
} from 'lucide-react';
import { Button } from '../../ui/button';
import { Badge } from '../../ui/badge';
@@ -33,6 +35,12 @@ interface InvestigationPanelProps {
onAcceptLabel?: (label: SuggestedLabel) => void;
onRejectLabel?: (label: SuggestedLabel) => void;
isPostingToGitHub?: boolean;
/** Activity log entries for the investigation lifecycle */
activityLog?: Array<{ event: string; timestamp: string }>;
/** Callback to close the issue on GitHub (used for resolved suggestion) */
onCloseIssue?: () => void;
/** Whether the close-issue action is in progress */
isClosingIssue?: boolean;
}
const SEVERITY_COLORS: Record<string, string> = {
@@ -144,11 +152,38 @@ export function InvestigationPanel({
onAcceptLabel,
onRejectLabel,
isPostingToGitHub,
activityLog,
onCloseIssue,
isClosingIssue,
}: InvestigationPanelProps) {
const { t } = useTranslation('common');
const [activityOpen, setActivityOpen] = useState(false);
return (
<div className="space-y-4">
{/* Resolved suggestion banner */}
{report.likelyResolved && onCloseIssue && state !== 'done' && (
<div className="flex items-center gap-3 p-3 rounded-lg bg-green-50 border border-green-200 dark:bg-green-900/20 dark:border-green-800">
<CheckCircle2 className="h-5 w-5 text-green-600 dark:text-green-400 shrink-0" />
<p className="text-sm text-green-800 dark:text-green-300 flex-1">
{t('investigation.panel.resolvedSuggestion', 'This issue appears to be already resolved. Close it on GitHub?')}
</p>
<Button
variant="outline"
size="sm"
onClick={onCloseIssue}
disabled={isClosingIssue}
className="border-green-300 text-green-700 hover:bg-green-100 dark:border-green-700 dark:text-green-300 dark:hover:bg-green-900/40"
>
<X className="h-3.5 w-3.5 mr-1" />
{isClosingIssue
? t('investigation.panel.closingIssue', 'Closing...')
: t('investigation.panel.closeIssue', 'Close Issue')
}
</Button>
</div>
)}
{/* Header: severity + timestamp */}
<div className="flex items-center gap-2">
<Badge className={SEVERITY_COLORS[report.severity] ?? SEVERITY_COLORS.medium}>
@@ -273,6 +308,32 @@ export function InvestigationPanel({
</Button>
</div>
)}
{/* Activity Log */}
{activityLog && activityLog.length > 0 && (
<div className="border-t pt-2">
<button
type="button"
onClick={() => setActivityOpen(!activityOpen)}
className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground hover:text-foreground transition-colors"
>
{activityOpen ? <ChevronDown className="h-3 w-3" /> : <ChevronRight className="h-3 w-3" />}
<Clock className="h-3 w-3" />
{t('investigation.activityLog.title', 'Activity')}
</button>
{activityOpen && (
<ul className="mt-1.5 space-y-1">
{activityLog.map((entry, i) => (
<li key={i} className="text-xs text-muted-foreground flex items-center gap-1.5">
<span className="h-1 w-1 rounded-full bg-muted-foreground shrink-0" />
<span>{entry.event}</span>
<span className="ml-auto text-[10px]">{new Date(entry.timestamp).toLocaleString()}</span>
</li>
))}
</ul>
)}
</div>
)}
</div>
);
}
@@ -2,7 +2,7 @@ import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { ExternalLink, User, Clock, MessageCircle, CheckCircle2, Eye, X, RotateCcw, XCircle } from 'lucide-react';
import { ExternalLink, User, Clock, MessageCircle, CheckCircle2, Eye, X, RotateCcw, XCircle, AlertTriangle } from 'lucide-react';
import { Badge } from '../../ui/badge';
import { Button } from '../../ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '../../ui/card';
@@ -71,6 +71,7 @@ export function IssueDetail({
onAcceptLabel,
onRejectLabel,
isPostingToGitHub,
investigationActivityLog,
}: IssueDetailProps) {
const { t } = useTranslation('common');
const [isClosing, setIsClosing] = useState(false);
@@ -159,6 +160,16 @@ export function IssueDetail({
)}
</div>
{/* Warning banner for closed issues with active/pending investigation */}
{issue.state === 'closed' && (derivedState === 'investigating' || derivedState === 'new' || derivedState === 'findings_ready') && (
<div className="flex items-center gap-2 p-3 rounded-lg bg-yellow-50 border border-yellow-200 dark:bg-yellow-900/20 dark:border-yellow-800">
<AlertTriangle className="h-4 w-4 text-yellow-600 dark:text-yellow-400 shrink-0" />
<p className="text-sm text-yellow-800 dark:text-yellow-300">
{t('investigation.panel.closedIssueWarning', 'This issue is closed. Investigation results may not be actionable.')}
</p>
</div>
)}
{/* Meta */}
<div className="flex flex-wrap items-center gap-4 text-sm text-muted-foreground">
<div className="flex items-center gap-1">
@@ -333,6 +344,9 @@ export function IssueDetail({
onAcceptLabel={onAcceptLabel}
onRejectLabel={onRejectLabel}
isPostingToGitHub={isPostingToGitHub}
activityLog={investigationActivityLog}
onCloseIssue={issue.state === 'open' && onClose ? handleClose : undefined}
isClosingIssue={isClosing}
/>
</CardContent>
</Card>
@@ -74,6 +74,8 @@ export interface IssueListItemProps {
linkedTaskId?: string;
/** Handler to navigate to the linked task */
onViewTask?: (taskId: string) => void;
/** Whether the issue is stale (no longer exists in GitHub) */
isStale?: boolean;
}
export interface IssueDetailProps {
@@ -151,6 +153,8 @@ export interface IssueDetailProps {
onRejectLabel?: (label: SuggestedLabel) => void;
/** Whether posting to GitHub is in progress */
isPostingToGitHub?: boolean;
/** Activity log entries for the investigation lifecycle */
investigationActivityLog?: Array<{ event: string; timestamp: string }>;
}
/** @deprecated Removed in F9. Use InvestigateButton + InvestigationPanel instead. */
@@ -212,6 +216,8 @@ export interface IssueListHeaderProps {
onToggleShowDismissed?: () => void;
/** Count of active investigations */
activeInvestigationCount?: number;
/** Cancel all active investigations for this project */
onCancelAllInvestigations?: () => void;
}
export interface IssueListProps {
@@ -230,7 +236,7 @@ export interface IssueListProps {
onToggleSelect?: (issueNumber: number) => void;
compact?: boolean;
/** Investigation states keyed by issue number */
investigationStates?: Record<string, { state: InvestigationState; progress?: number; linkedTaskId?: string }>;
investigationStates?: Record<string, { state: InvestigationState; progress?: number; linkedTaskId?: string; isStale?: boolean }>;
/** Handler to navigate to a linked task */
onViewTask?: (taskId: string) => void;
}
+12 -1
View File
@@ -193,6 +193,7 @@ const browserMockAPI: ElectronAPI = {
// Investigation operations (new system)
startInvestigation: () => {},
cancelInvestigation: () => {},
cancelAllInvestigations: () => {},
createTaskFromInvestigation: async () => ({ success: true, data: { specId: '' } }),
dismissIssue: async () => ({ success: true }),
postInvestigationToGitHub: async () => ({ success: true, data: { commentId: 0 } }),
@@ -458,7 +459,17 @@ const browserMockAPI: ElectronAPI = {
openLogsFolder: async () => ({ success: false, error: 'Not available in browser mode' }),
copyDebugInfo: async () => ({ success: false, error: 'Not available in browser mode' }),
getRecentErrors: async () => [],
listLogFiles: async () => []
listLogFiles: async () => [],
// Top-level investigation operations (legacy ElectronAPI surface)
startInvestigation: () => {},
cancelInvestigation: () => {},
cancelAllInvestigations: () => {},
createTaskFromInvestigation: async () => ({ success: true, data: { specId: '' } }),
dismissIssue: async () => ({ success: true }),
postInvestigationToGitHub: async () => ({ success: true, data: { commentId: 0 } }),
getInvestigationSettings: async () => ({ success: true, data: { autoCreateTasks: false, autoStartTasks: false, pipelineMode: 'full' as const, autoPostToGitHub: false, autoCloseIssues: false, maxParallelInvestigations: 3, labelIncludeFilter: [], labelExcludeFilter: [] } }),
saveInvestigationSettings: async () => ({ success: true }),
};
/**
@@ -10,6 +10,7 @@ import type {
InvestigationState,
PersistedInvestigationState,
} from '@shared/types';
import { toast } from '../../hooks/use-toast';
// ============================================
// Per-Issue Investigation State
@@ -43,6 +44,10 @@ export interface IssueInvestigationState {
completedAt: string | null;
/** Linked task status (synced from task store for building/done states) */
linkedTaskStatus: string | null;
/** Activity log tracking key lifecycle events */
activityLog: Array<{ event: string; timestamp: string }>;
/** True if the issue no longer exists in the GitHub response (stale/deleted) */
isStale?: boolean;
}
// ============================================
@@ -73,7 +78,10 @@ interface InvestigationStoreState {
clearIssueInvestigation: (projectId: string, issueNumber: number) => void;
setSettings: (projectId: string, settings: InvestigationSettings) => void;
syncTaskState: (projectId: string, issueNumber: number, taskStatus: string) => void;
clearLinkedTask: (projectId: string, issueNumber: number) => void;
loadPersistedInvestigations: (projectId: string, states: PersistedInvestigationState[]) => void;
cancelAllInvestigations: (projectId: string) => void;
markStaleInvestigations: (projectId: string, activeIssueNumbers: Set<number>) => void;
// ---- Selectors ----
getInvestigationState: (projectId: string, issueNumber: number) => IssueInvestigationState | null;
@@ -108,6 +116,9 @@ export const useInvestigationStore = create<InvestigationStoreState>((set, get)
startInvestigation: (projectId: string, issueNumber: number) => set((state) => {
const key = `${projectId}:${issueNumber}`;
const existing = state.investigations[key];
const now = new Date().toISOString();
const event = existing?.report ? 're-investigation started' : 'investigation started';
const log = [...(existing?.activityLog ?? []), { event, timestamp: now }];
return {
investigations: {
...state.investigations,
@@ -122,9 +133,10 @@ export const useInvestigationStore = create<InvestigationStoreState>((set, get)
specId: existing?.specId ?? null,
dismissReason: null, // clear dismiss on re-investigation
githubCommentId: existing?.githubCommentId ?? null,
startedAt: new Date().toISOString(),
startedAt: now,
completedAt: null,
linkedTaskStatus: existing?.linkedTaskStatus ?? null
linkedTaskStatus: existing?.linkedTaskStatus ?? null,
activityLog: log
}
}
};
@@ -149,7 +161,8 @@ export const useInvestigationStore = create<InvestigationStoreState>((set, get)
githubCommentId: existing?.githubCommentId ?? null,
startedAt: existing?.startedAt ?? null,
completedAt: null,
linkedTaskStatus: existing?.linkedTaskStatus ?? null
linkedTaskStatus: existing?.linkedTaskStatus ?? null,
activityLog: existing?.activityLog ?? []
}
}
};
@@ -158,6 +171,7 @@ export const useInvestigationStore = create<InvestigationStoreState>((set, get)
setResult: (projectId: string, result: InvestigationResult) => set((state) => {
const key = `${projectId}:${result.issueNumber}`;
const existing = state.investigations[key];
const log = [...(existing?.activityLog ?? []), { event: 'investigation completed', timestamp: result.completedAt }];
return {
investigations: {
...state.investigations,
@@ -174,7 +188,8 @@ export const useInvestigationStore = create<InvestigationStoreState>((set, get)
githubCommentId: result.githubCommentId ?? existing?.githubCommentId ?? null,
startedAt: existing?.startedAt ?? null,
completedAt: result.completedAt,
linkedTaskStatus: existing?.linkedTaskStatus ?? null
linkedTaskStatus: existing?.linkedTaskStatus ?? null,
activityLog: log
}
}
};
@@ -183,6 +198,7 @@ export const useInvestigationStore = create<InvestigationStoreState>((set, get)
setError: (projectId: string, issueNumber: number, error: string) => set((state) => {
const key = `${projectId}:${issueNumber}`;
const existing = state.investigations[key];
const log = [...(existing?.activityLog ?? []), { event: 'investigation failed', timestamp: new Date().toISOString() }];
return {
investigations: {
...state.investigations,
@@ -199,7 +215,8 @@ export const useInvestigationStore = create<InvestigationStoreState>((set, get)
githubCommentId: existing?.githubCommentId ?? null,
startedAt: existing?.startedAt ?? null,
completedAt: null,
linkedTaskStatus: existing?.linkedTaskStatus ?? null
linkedTaskStatus: existing?.linkedTaskStatus ?? null,
activityLog: log
}
}
};
@@ -209,12 +226,14 @@ export const useInvestigationStore = create<InvestigationStoreState>((set, get)
const key = `${projectId}:${issueNumber}`;
const existing = state.investigations[key];
if (!existing) return state;
const log = [...(existing.activityLog ?? []), { event: `dismissed: ${reason}`, timestamp: new Date().toISOString() }];
return {
investigations: {
...state.investigations,
[key]: {
...existing,
dismissReason: reason
dismissReason: reason,
activityLog: log
}
}
};
@@ -276,6 +295,24 @@ export const useInvestigationStore = create<InvestigationStoreState>((set, get)
};
}),
clearLinkedTask: (projectId: string, issueNumber: number) => set((state) => {
const key = `${projectId}:${issueNumber}`;
const existing = state.investigations[key];
if (!existing) return state;
const log = [...(existing.activityLog ?? []), { event: 'linked task deleted', timestamp: new Date().toISOString() }];
return {
investigations: {
...state.investigations,
[key]: {
...existing,
specId: null,
linkedTaskStatus: null,
activityLog: log
}
}
};
}),
loadPersistedInvestigations: (projectId: string, states: PersistedInvestigationState[]) => set((state) => {
const newInvestigations = { ...state.investigations };
@@ -304,12 +341,43 @@ export const useInvestigationStore = create<InvestigationStoreState>((set, get)
startedAt: null,
completedAt: persisted.completedAt ?? null,
linkedTaskStatus: null,
activityLog: []
};
}
return { investigations: newInvestigations };
}),
cancelAllInvestigations: (projectId: string) => set((state) => {
const updated = { ...state.investigations };
let changed = false;
for (const [key, inv] of Object.entries(updated)) {
if (inv.projectId !== projectId || !inv.isInvestigating) continue;
updated[key] = {
...inv,
isInvestigating: false,
progress: null,
error: 'Investigation cancelled',
};
changed = true;
}
return changed ? { investigations: updated } : state;
}),
markStaleInvestigations: (projectId: string, activeIssueNumbers: Set<number>) => set((state) => {
const updated = { ...state.investigations };
let changed = false;
for (const [key, inv] of Object.entries(updated)) {
if (inv.projectId !== projectId) continue;
const shouldBeStale = !activeIssueNumbers.has(inv.issueNumber);
if (inv.isStale !== shouldBeStale) {
updated[key] = { ...inv, isStale: shouldBeStale };
changed = true;
}
}
return changed ? { investigations: updated } : state;
}),
// ---- Selectors ----
getInvestigationState: (projectId: string, issueNumber: number) => {
@@ -390,6 +458,9 @@ export function initializeInvestigationListeners(): void {
const cleanupComplete = window.electronAPI.github.onInvestigationComplete(
(projectId: string, result: InvestigationResult) => {
store.setResult(projectId, result);
toast({
title: `Investigation complete for Issue #${result.issueNumber}`,
});
}
);
cleanupFunctions.push(cleanupComplete);
@@ -404,6 +475,10 @@ export function initializeInvestigationListeners(): void {
// but for now we mark all active investigations as errored.
for (const inv of active) {
store.setError(projectId, inv.issueNumber, error);
toast({
title: `Investigation failed for Issue #${inv.issueNumber}`,
variant: 'destructive',
});
}
}
);
@@ -467,6 +542,15 @@ export function cancelIssueInvestigation(
window.electronAPI.github.cancelInvestigation(projectId, issueNumber);
}
/**
* Cancel all running investigations for a project.
*/
export function cancelAllIssueInvestigations(projectId: string): void {
const store = useInvestigationStore.getState();
store.cancelAllInvestigations(projectId);
window.electronAPI.github.cancelAllInvestigations(projectId);
}
/**
* Load persisted investigation state from disk.
* Call this when the GitHub Issues view mounts with a selected project
@@ -1050,7 +1050,9 @@
"postToGitHub": "Post to GitHub",
"postedToGitHub": "Posted to GitHub",
"closeIssue": "Close Issue",
"closedIssueWarning": "This issue has been closed on GitHub.",
"closingIssue": "Closing...",
"closedIssueWarning": "This issue is closed. Investigation results may not be actionable.",
"resolvedSuggestion": "This issue appears to be already resolved. Close it on GitHub?",
"confidence": "Confidence",
"rootCause": "Root Cause",
"impact": "Impact",
@@ -1090,7 +1092,8 @@
"taskCreated": "Task Created",
"building": "Building",
"done": "Done",
"dismissed": "Dismissed"
"dismissed": "Dismissed",
"stale": "Stale"
},
"stateFilters": {
"all": "All",
@@ -1161,6 +1164,15 @@
"position": "Queue position: {{position}}",
"waiting": "Waiting in queue...",
"running": "{{current}} of {{max}} running"
},
"toast": {
"investigationComplete": "Investigation complete for Issue #{{issueNumber}}",
"investigationFailed": "Investigation failed for Issue #{{issueNumber}}"
},
"labelConsent": {
"title": "Label Creation Notice",
"body": "The first investigation will create up to 5 auto-claude:* labels on your GitHub repository to categorize investigation results. These labels are used for filtering and organization.",
"confirm": "Continue"
}
}
}
@@ -1050,7 +1050,9 @@
"postToGitHub": "Publier sur GitHub",
"postedToGitHub": "Publié sur GitHub",
"closeIssue": "Fermer l'issue",
"closedIssueWarning": "Cette issue a été fermée sur GitHub.",
"closingIssue": "Fermeture...",
"closedIssueWarning": "Cette issue est fermée. Les résultats de l'investigation peuvent ne pas être exploitables.",
"resolvedSuggestion": "Cette issue semble déjà résolue. La fermer sur GitHub ?",
"confidence": "Confiance",
"rootCause": "Cause racine",
"impact": "Impact",
@@ -1090,7 +1092,8 @@
"taskCreated": "Tâche créée",
"building": "En construction",
"done": "Terminé",
"dismissed": "Rejeté"
"dismissed": "Rejeté",
"stale": "Obsolète"
},
"stateFilters": {
"all": "Tous",
@@ -1161,6 +1164,15 @@
"position": "Position dans la file : {{position}}",
"waiting": "En attente dans la file...",
"running": "{{current}} sur {{max}} en cours"
},
"toast": {
"investigationComplete": "Investigation terminée pour l'issue #{{issueNumber}}",
"investigationFailed": "Investigation échouée pour l'issue #{{issueNumber}}"
},
"labelConsent": {
"title": "Avis de création de labels",
"body": "La première investigation créera jusqu'à 5 labels auto-claude:* sur votre dépôt GitHub pour catégoriser les résultats d'investigation. Ces labels sont utilisés pour le filtrage et l'organisation.",
"confirm": "Continuer"
}
}
}