diff --git a/ccw/frontend/src/components/issue/discovery/DiscoveryCard.tsx b/ccw/frontend/src/components/issue/discovery/DiscoveryCard.tsx
index 89196c55..128b1f0c 100644
--- a/ccw/frontend/src/components/issue/discovery/DiscoveryCard.tsx
+++ b/ccw/frontend/src/components/issue/discovery/DiscoveryCard.tsx
@@ -21,17 +21,17 @@ const statusConfig = {
running: {
icon: Clock,
variant: 'warning' as const,
- label: 'issues.discovery.status.running',
+ label: 'issues.discovery.session.status.running',
},
completed: {
icon: CheckCircle,
variant: 'success' as const,
- label: 'issues.discovery.status.completed',
+ label: 'issues.discovery.session.status.completed',
},
failed: {
icon: XCircle,
variant: 'destructive' as const,
- label: 'issues.discovery.status.failed',
+ label: 'issues.discovery.session.status.failed',
},
};
@@ -79,7 +79,7 @@ export function DiscoveryCard({ session, isActive, onClick }: DiscoveryCardProps
- {formatMessage({ id: 'issues.discovery.findings' })}:
+ {formatMessage({ id: 'issues.discovery.session.findings' }, { count: session.findings_count })}:
{session.findings_count}
diff --git a/ccw/frontend/src/components/issue/discovery/DiscoveryDetail.tsx b/ccw/frontend/src/components/issue/discovery/DiscoveryDetail.tsx
index fd0ea1fe..2ccf0110 100644
--- a/ccw/frontend/src/components/issue/discovery/DiscoveryDetail.tsx
+++ b/ccw/frontend/src/components/issue/discovery/DiscoveryDetail.tsx
@@ -12,6 +12,7 @@ import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/Tabs';
import { Badge } from '@/components/ui/Badge';
import { Progress } from '@/components/ui/Progress';
import { IssueDrawer } from '@/components/issue/hub/IssueDrawer';
+import { FindingDrawer } from './FindingDrawer';
import type { DiscoverySession, Finding } from '@/lib/api';
import type { Issue } from '@/lib/api';
import type { FindingFilters } from '@/hooks/useIssues';
@@ -43,6 +44,7 @@ export function DiscoveryDetail({
const { formatMessage } = useIntl();
const [activeTab, setActiveTab] = useState('findings');
const [selectedIssue, setSelectedIssue] = useState
(null);
+ const [selectedFinding, setSelectedFinding] = useState(null);
const [selectedIds, setSelectedIds] = useState([]);
const handleFindingClick = (finding: Finding) => {
@@ -51,14 +53,21 @@ export function DiscoveryDetail({
const relatedIssue = issues.find(i => i.id === finding.issue_id);
if (relatedIssue) {
setSelectedIssue(relatedIssue);
+ return;
}
}
+ // Otherwise, show the finding details in FindingDrawer
+ setSelectedFinding(finding);
};
- const handleCloseDrawer = () => {
+ const handleCloseIssueDrawer = () => {
setSelectedIssue(null);
};
+ const handleCloseFindingDrawer = () => {
+ setSelectedFinding(null);
+ };
+
const handleExportSelected = async () => {
if (onExportSelected && selectedIds.length > 0) {
await onExportSelected(selectedIds);
@@ -130,7 +139,7 @@ export function DiscoveryDetail({
- {formatMessage({ id: `issues.discovery.status.${session.status}` })}
+ {formatMessage({ id: `issues.discovery.session.status.${session.status}` })}
{formatMessage({ id: 'issues.discovery.createdAt' })}: {formatDate(session.created_at)}
@@ -192,7 +201,7 @@ export function DiscoveryDetail({
- {formatMessage({ id: `issues.discovery.severity.${severity}` })}
+ {formatMessage({ id: `issues.discovery.findings.severity.${severity}` })}
{count}
@@ -240,7 +249,7 @@ export function DiscoveryDetail({
- {formatMessage({ id: `issues.discovery.status.${session.status}` })}
+ {formatMessage({ id: `issues.discovery.session.status.${session.status}` })}
@@ -277,7 +286,14 @@ export function DiscoveryDetail({
+
+ {/* Finding Detail Drawer */}
+
);
diff --git a/ccw/frontend/src/components/issue/discovery/FindingDrawer.tsx b/ccw/frontend/src/components/issue/discovery/FindingDrawer.tsx
new file mode 100644
index 00000000..3ac626e4
--- /dev/null
+++ b/ccw/frontend/src/components/issue/discovery/FindingDrawer.tsx
@@ -0,0 +1,213 @@
+// ========================================
+// FindingDrawer Component
+// ========================================
+// Right-side finding detail drawer for displaying discovery finding details
+
+import { useEffect } from 'react';
+import { useIntl } from 'react-intl';
+import { X, FileText, AlertTriangle, ExternalLink, MapPin, Code, Lightbulb, Target } from 'lucide-react';
+import { Badge } from '@/components/ui/Badge';
+import { Button } from '@/components/ui/Button';
+import { cn } from '@/lib/utils';
+import type { Finding } from '@/lib/api';
+
+// ========== Types ==========
+export interface FindingDrawerProps {
+ finding: Finding | null;
+ isOpen: boolean;
+ onClose: () => void;
+}
+
+// ========== Severity Configuration ==========
+const severityConfig: Record = {
+ critical: { label: 'issues.discovery.findings.severity.critical', variant: 'destructive' },
+ high: { label: 'issues.discovery.findings.severity.high', variant: 'destructive' },
+ medium: { label: 'issues.discovery.findings.severity.medium', variant: 'warning' },
+ low: { label: 'issues.discovery.findings.severity.low', variant: 'secondary' },
+};
+
+function getSeverityConfig(severity: string) {
+ return severityConfig[severity] || { label: 'issues.discovery.findings.severity.unknown', variant: 'outline' };
+}
+
+// ========== Component ==========
+
+export function FindingDrawer({ finding, isOpen, onClose }: FindingDrawerProps) {
+ const { formatMessage } = useIntl();
+
+ // ESC key to close
+ useEffect(() => {
+ if (!isOpen) return;
+ const handleEsc = (e: KeyboardEvent) => {
+ if (e.key === 'Escape') onClose();
+ };
+ window.addEventListener('keydown', handleEsc);
+ return () => window.removeEventListener('keydown', handleEsc);
+ }, [isOpen, onClose]);
+
+ if (!finding || !isOpen) {
+ return null;
+ }
+
+ const severity = getSeverityConfig(finding.severity);
+
+ return (
+ <>
+ {/* Overlay */}
+
+
+ {/* Drawer */}
+
+ {/* Header */}
+
+
+
+ {finding.id}
+
+ {formatMessage({ id: severity.label })}
+
+ {finding.type && (
+ {finding.type}
+ )}
+ {finding.category && (
+ {finding.category}
+ )}
+
+
+ {finding.title}
+
+
+
+
+
+ {/* Content */}
+
+ {/* Description */}
+
+
+
+ {formatMessage({ id: 'issues.discovery.findings.description' })}
+
+
+ {finding.description}
+
+
+
+ {/* File Location */}
+ {finding.file && (
+
+
+
+ {formatMessage({ id: 'issues.discovery.findings.location' })}
+
+
+
+
+ {finding.file}
+ {finding.line && `:${finding.line}`}
+
+
+
+ )}
+
+ {/* Code Snippet */}
+ {finding.code_snippet && (
+
+
+
+ {formatMessage({ id: 'issues.discovery.findings.codeSnippet' })}
+
+
+ {finding.code_snippet}
+
+
+ )}
+
+ {/* Suggested Fix */}
+ {finding.suggested_issue && (
+
+
+
+ {formatMessage({ id: 'issues.discovery.findings.suggestedFix' })}
+
+
+ {finding.suggested_issue}
+
+
+ )}
+
+ {/* Confidence */}
+ {finding.confidence !== undefined && (
+
+
+
+ {formatMessage({ id: 'issues.discovery.findings.confidence' })}
+
+
+
+
= 0.9 ? "bg-green-500" :
+ finding.confidence >= 0.7 ? "bg-yellow-500" : "bg-red-500"
+ )}
+ style={{ width: `${finding.confidence * 100}%` }}
+ />
+
+
+ {Math.round(finding.confidence * 100)}%
+
+
+
+ )}
+
+ {/* Reference */}
+ {finding.reference && (
+
+ )}
+
+ {/* Perspective */}
+ {finding.perspective && (
+
+
+ {formatMessage({ id: 'issues.discovery.findings.perspective' })}: {finding.perspective}
+
+
+ )}
+
+
+ >
+ );
+}
+
+export default FindingDrawer;
diff --git a/ccw/frontend/src/components/issue/discovery/FindingList.tsx b/ccw/frontend/src/components/issue/discovery/FindingList.tsx
index 9393d183..e522424d 100644
--- a/ccw/frontend/src/components/issue/discovery/FindingList.tsx
+++ b/ccw/frontend/src/components/issue/discovery/FindingList.tsx
@@ -24,14 +24,14 @@ interface FindingListProps {
}
const severityConfig: Record
= {
- critical: { variant: 'destructive', label: 'issues.discovery.severity.critical' },
- high: { variant: 'destructive', label: 'issues.discovery.severity.high' },
- medium: { variant: 'warning', label: 'issues.discovery.severity.medium' },
- low: { variant: 'secondary', label: 'issues.discovery.severity.low' },
+ critical: { variant: 'destructive', label: 'issues.discovery.findings.severity.critical' },
+ high: { variant: 'destructive', label: 'issues.discovery.findings.severity.high' },
+ medium: { variant: 'warning', label: 'issues.discovery.findings.severity.medium' },
+ low: { variant: 'secondary', label: 'issues.discovery.findings.severity.low' },
};
function getSeverityConfig(severity: string) {
- return severityConfig[severity] || { variant: 'outline', label: 'issues.discovery.severity.unknown' };
+ return severityConfig[severity] || { variant: 'outline', label: 'issues.discovery.findings.severity.unknown' };
}
export function FindingList({
@@ -116,10 +116,10 @@ export function FindingList({
{formatMessage({ id: 'issues.discovery.findings.severity.all' })}
- {formatMessage({ id: 'issues.discovery.severity.critical' })}
- {formatMessage({ id: 'issues.discovery.severity.high' })}
- {formatMessage({ id: 'issues.discovery.severity.medium' })}
- {formatMessage({ id: 'issues.discovery.severity.low' })}
+ {formatMessage({ id: 'issues.discovery.findings.severity.critical' })}
+ {formatMessage({ id: 'issues.discovery.findings.severity.high' })}
+ {formatMessage({ id: 'issues.discovery.findings.severity.medium' })}
+ {formatMessage({ id: 'issues.discovery.findings.severity.low' })}
{uniqueTypes.length > 0 && (
diff --git a/ccw/frontend/src/lib/api.ts b/ccw/frontend/src/lib/api.ts
index 4a08579a..39f40d8b 100644
--- a/ccw/frontend/src/lib/api.ts
+++ b/ccw/frontend/src/lib/api.ts
@@ -1077,6 +1077,12 @@ export interface Finding {
created_at: string;
issue_id?: string; // Associated issue ID if exported
exported?: boolean; // Whether this finding has been exported as an issue
+ // Additional fields from discovery backend
+ category?: string;
+ suggested_issue?: string;
+ confidence?: number;
+ reference?: string;
+ perspective?: string;
}
export async function fetchDiscoveries(projectPath?: string): Promise {
@@ -1131,7 +1137,11 @@ export async function fetchDiscoveryFindings(
? `/api/discoveries/${encodeURIComponent(sessionId)}/findings?path=${encodeURIComponent(projectPath)}`
: `/api/discoveries/${encodeURIComponent(sessionId)}/findings`;
const data = await fetchApi<{ findings?: Finding[] }>(url);
- return data.findings ?? [];
+ // Map backend 'priority' to frontend 'severity' for compatibility
+ return (data.findings ?? []).map(f => ({
+ ...f,
+ severity: f.severity || (f as any).priority || 'medium'
+ }));
}
/**
diff --git a/ccw/frontend/src/locales/en/issues.json b/ccw/frontend/src/locales/en/issues.json
index 50971a42..7353cacc 100644
--- a/ccw/frontend/src/locales/en/issues.json
+++ b/ccw/frontend/src/locales/en/issues.json
@@ -353,7 +353,14 @@
"hasIssue": "Linked",
"export": "Export",
"selectAll": "Select All",
- "deselectAll": "Deselect All"
+ "deselectAll": "Deselect All",
+ "description": "Description",
+ "location": "Location",
+ "codeSnippet": "Code Snippet",
+ "suggestedFix": "Suggested Fix",
+ "confidence": "Confidence",
+ "reference": "Reference",
+ "perspective": "Perspective"
},
"tabs": {
"findings": "Findings",
diff --git a/ccw/frontend/src/locales/zh/issues.json b/ccw/frontend/src/locales/zh/issues.json
index 98111fd4..d403759a 100644
--- a/ccw/frontend/src/locales/zh/issues.json
+++ b/ccw/frontend/src/locales/zh/issues.json
@@ -353,7 +353,14 @@
"hasIssue": "已关联",
"export": "导出",
"selectAll": "全选",
- "deselectAll": "取消全选"
+ "deselectAll": "取消全选",
+ "description": "问题描述",
+ "location": "文件位置",
+ "codeSnippet": "代码片段",
+ "suggestedFix": "建议修复",
+ "confidence": "置信度",
+ "reference": "参考链接",
+ "perspective": "视角"
},
"tabs": {
"findings": "发现",
diff --git a/ccw/frontend/src/pages/DiscoveryPage.tsx b/ccw/frontend/src/pages/DiscoveryPage.tsx
index 6bcd063c..690608b6 100644
--- a/ccw/frontend/src/pages/DiscoveryPage.tsx
+++ b/ccw/frontend/src/pages/DiscoveryPage.tsx
@@ -7,7 +7,7 @@ import { useIntl } from 'react-intl';
import { Radar, AlertCircle, Loader2 } from 'lucide-react';
import { Card } from '@/components/ui/Card';
import { Badge } from '@/components/ui/Badge';
-import { useIssueDiscovery } from '@/hooks/useIssues';
+import { useIssueDiscovery, useIssues } from '@/hooks/useIssues';
import { DiscoveryCard } from '@/components/issue/discovery/DiscoveryCard';
import { DiscoveryDetail } from '@/components/issue/discovery/DiscoveryDetail';
@@ -29,6 +29,12 @@ export function DiscoveryPage() {
isExporting,
} = useIssueDiscovery({ refetchInterval: 3000 });
+ // Fetch issues to find related ones when clicking findings
+ const { issues } = useIssues({
+ // Don't apply filters to get all issues for matching
+ filter: undefined
+ });
+
if (error) {
return (
@@ -167,6 +173,7 @@ export function DiscoveryPage() {
onExport={exportFindings}
onExportSelected={exportSelectedFindings}
isExporting={isExporting}
+ issues={issues}
/>
)}
diff --git a/ccw/src/core/routes/discovery-routes.ts b/ccw/src/core/routes/discovery-routes.ts
index 8fb24e7b..5595da88 100644
--- a/ccw/src/core/routes/discovery-routes.ts
+++ b/ccw/src/core/routes/discovery-routes.ts
@@ -262,7 +262,13 @@ function flattenFindings(perspectiveResults: any[]): any[] {
const allFindings: any[] = [];
for (const result of perspectiveResults) {
if (result.findings) {
- allFindings.push(...result.findings);
+ // Map backend 'priority' to frontend 'severity' for compatibility
+ const mappedFindings = result.findings.map((f: any) => ({
+ ...f,
+ severity: f.severity || f.priority || 'medium',
+ sessionId: f.discovery_id || result.discovery_id
+ }));
+ allFindings.push(...mappedFindings);
}
}
return allFindings;
diff --git a/ccw/src/tools/write-file.ts b/ccw/src/tools/write-file.ts
index 20590666..ababcc29 100644
--- a/ccw/src/tools/write-file.ts
+++ b/ccw/src/tools/write-file.ts
@@ -105,8 +105,8 @@ export const schema: ToolSchema = {
name: 'write_file',
description: `Write content to file. Auto-creates parent directories.
-Usage: write_file(path="file.js", content="code here")
-Options: backup=true (backup before overwrite), createDirectories=false (disable auto-creation), encoding="utf8"`,
+Required: path (string), content (string)
+Options: backup=true, createDirectories=false, encoding="utf8"`,
inputSchema: {
type: 'object',
properties: {