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:
co-authored by
Claude Opus 4.6
parent
054d6b197d
commit
9ec2919023
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
+62
-1
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user