mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-01 08:13:37 +00:00
fixing auto verify for kanban issues
This commit is contained in:
@@ -426,7 +426,6 @@ ${Object.entries(projectAnalysis.filesByExtension)
|
||||
interface DetectedFeature {
|
||||
category: string;
|
||||
description: string;
|
||||
steps: string[];
|
||||
passes: boolean;
|
||||
}
|
||||
|
||||
@@ -453,11 +452,6 @@ ${Object.entries(projectAnalysis.filesByExtension)
|
||||
detectedFeatures.push({
|
||||
category: 'Testing',
|
||||
description: 'Automated test suite',
|
||||
steps: [
|
||||
'Step 1: Tests directory exists',
|
||||
'Step 2: Test files are present',
|
||||
'Step 3: Run test suite',
|
||||
],
|
||||
passes: true,
|
||||
});
|
||||
}
|
||||
@@ -471,11 +465,6 @@ ${Object.entries(projectAnalysis.filesByExtension)
|
||||
detectedFeatures.push({
|
||||
category: 'UI/Design',
|
||||
description: 'Component-based UI architecture',
|
||||
steps: [
|
||||
'Step 1: Components directory exists',
|
||||
'Step 2: UI components are defined',
|
||||
'Step 3: Components are reusable',
|
||||
],
|
||||
passes: true,
|
||||
});
|
||||
}
|
||||
@@ -485,11 +474,6 @@ ${Object.entries(projectAnalysis.filesByExtension)
|
||||
detectedFeatures.push({
|
||||
category: 'Project Structure',
|
||||
description: 'Organized source code structure',
|
||||
steps: [
|
||||
'Step 1: Source directory exists',
|
||||
'Step 2: Code is properly organized',
|
||||
'Step 3: Follows best practices',
|
||||
],
|
||||
passes: true,
|
||||
});
|
||||
}
|
||||
@@ -504,11 +488,6 @@ ${Object.entries(projectAnalysis.filesByExtension)
|
||||
detectedFeatures.push({
|
||||
category: 'Frontend',
|
||||
description: 'React-based user interface',
|
||||
steps: [
|
||||
'Step 1: React is installed',
|
||||
'Step 2: Components render correctly',
|
||||
'Step 3: State management works',
|
||||
],
|
||||
passes: true,
|
||||
});
|
||||
}
|
||||
@@ -517,11 +496,6 @@ ${Object.entries(projectAnalysis.filesByExtension)
|
||||
detectedFeatures.push({
|
||||
category: 'Framework',
|
||||
description: 'Next.js framework integration',
|
||||
steps: [
|
||||
'Step 1: Next.js is configured',
|
||||
'Step 2: Pages/routes are defined',
|
||||
'Step 3: Server-side rendering works',
|
||||
],
|
||||
passes: true,
|
||||
});
|
||||
}
|
||||
@@ -536,11 +510,6 @@ ${Object.entries(projectAnalysis.filesByExtension)
|
||||
detectedFeatures.push({
|
||||
category: 'Developer Experience',
|
||||
description: 'TypeScript type safety',
|
||||
steps: [
|
||||
'Step 1: TypeScript is configured',
|
||||
'Step 2: Type definitions exist',
|
||||
'Step 3: Code compiles without errors',
|
||||
],
|
||||
passes: true,
|
||||
});
|
||||
}
|
||||
@@ -550,11 +519,6 @@ ${Object.entries(projectAnalysis.filesByExtension)
|
||||
detectedFeatures.push({
|
||||
category: 'UI/Design',
|
||||
description: 'Tailwind CSS styling',
|
||||
steps: [
|
||||
'Step 1: Tailwind is configured',
|
||||
'Step 2: Styles are applied',
|
||||
'Step 3: Responsive design works',
|
||||
],
|
||||
passes: true,
|
||||
});
|
||||
}
|
||||
@@ -564,11 +528,6 @@ ${Object.entries(projectAnalysis.filesByExtension)
|
||||
detectedFeatures.push({
|
||||
category: 'Developer Experience',
|
||||
description: 'Code quality tools',
|
||||
steps: [
|
||||
'Step 1: Linter is configured',
|
||||
'Step 2: Code passes lint checks',
|
||||
'Step 3: Formatting is consistent',
|
||||
],
|
||||
passes: true,
|
||||
});
|
||||
}
|
||||
@@ -578,11 +537,6 @@ ${Object.entries(projectAnalysis.filesByExtension)
|
||||
detectedFeatures.push({
|
||||
category: 'Platform',
|
||||
description: 'Electron desktop application',
|
||||
steps: [
|
||||
'Step 1: Electron is configured',
|
||||
'Step 2: Main process runs',
|
||||
'Step 3: Renderer process loads',
|
||||
],
|
||||
passes: true,
|
||||
});
|
||||
}
|
||||
@@ -592,11 +546,6 @@ ${Object.entries(projectAnalysis.filesByExtension)
|
||||
detectedFeatures.push({
|
||||
category: 'Testing',
|
||||
description: 'Playwright end-to-end testing',
|
||||
steps: [
|
||||
'Step 1: Playwright is configured',
|
||||
'Step 2: E2E tests are defined',
|
||||
'Step 3: Tests pass successfully',
|
||||
],
|
||||
passes: true,
|
||||
});
|
||||
}
|
||||
@@ -610,11 +559,6 @@ ${Object.entries(projectAnalysis.filesByExtension)
|
||||
detectedFeatures.push({
|
||||
category: 'Documentation',
|
||||
description: 'Project documentation',
|
||||
steps: [
|
||||
'Step 1: README exists',
|
||||
'Step 2: Documentation is comprehensive',
|
||||
'Step 3: Setup instructions are clear',
|
||||
],
|
||||
passes: true,
|
||||
});
|
||||
}
|
||||
@@ -629,11 +573,6 @@ ${Object.entries(projectAnalysis.filesByExtension)
|
||||
detectedFeatures.push({
|
||||
category: 'DevOps',
|
||||
description: 'CI/CD pipeline configuration',
|
||||
steps: [
|
||||
'Step 1: CI config exists',
|
||||
'Step 2: Pipeline runs on push',
|
||||
'Step 3: Automated checks pass',
|
||||
],
|
||||
passes: true,
|
||||
});
|
||||
}
|
||||
@@ -647,11 +586,6 @@ ${Object.entries(projectAnalysis.filesByExtension)
|
||||
detectedFeatures.push({
|
||||
category: 'Backend',
|
||||
description: 'API endpoints',
|
||||
steps: [
|
||||
'Step 1: API routes are defined',
|
||||
'Step 2: Endpoints respond correctly',
|
||||
'Step 3: Error handling is implemented',
|
||||
],
|
||||
passes: true,
|
||||
});
|
||||
}
|
||||
@@ -669,11 +603,6 @@ ${Object.entries(projectAnalysis.filesByExtension)
|
||||
detectedFeatures.push({
|
||||
category: 'Architecture',
|
||||
description: 'State management system',
|
||||
steps: [
|
||||
'Step 1: Store is configured',
|
||||
'Step 2: State updates correctly',
|
||||
'Step 3: Components access state',
|
||||
],
|
||||
passes: true,
|
||||
});
|
||||
}
|
||||
@@ -683,11 +612,6 @@ ${Object.entries(projectAnalysis.filesByExtension)
|
||||
detectedFeatures.push({
|
||||
category: 'Configuration',
|
||||
description: 'Project configuration files',
|
||||
steps: [
|
||||
'Step 1: Config files exist',
|
||||
'Step 2: Configuration is valid',
|
||||
'Step 3: Build process works',
|
||||
],
|
||||
passes: true,
|
||||
});
|
||||
}
|
||||
@@ -700,11 +624,6 @@ ${Object.entries(projectAnalysis.filesByExtension)
|
||||
detectedFeatures.push({
|
||||
category: 'Core',
|
||||
description: 'Basic project structure',
|
||||
steps: [
|
||||
'Step 1: Project directory exists',
|
||||
'Step 2: Files are present',
|
||||
'Step 3: Project can be loaded',
|
||||
],
|
||||
passes: true,
|
||||
});
|
||||
}
|
||||
@@ -719,7 +638,6 @@ ${Object.entries(projectAnalysis.filesByExtension)
|
||||
id: crypto.randomUUID(),
|
||||
category: detectedFeature.category,
|
||||
description: detectedFeature.description,
|
||||
steps: detectedFeature.steps,
|
||||
status: 'backlog',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -433,9 +433,9 @@ export function BoardView() {
|
||||
|
||||
// Create the feature
|
||||
const featureData = {
|
||||
title: `Address PR #${prNumber} Review Comments`,
|
||||
category: 'PR Review',
|
||||
description,
|
||||
steps: [],
|
||||
images: [],
|
||||
imagePaths: [],
|
||||
skipTests: defaultSkipTests,
|
||||
@@ -475,9 +475,9 @@ export function BoardView() {
|
||||
|
||||
// Create the feature
|
||||
const featureData = {
|
||||
title: `Resolve Merge Conflicts`,
|
||||
category: 'Maintenance',
|
||||
description,
|
||||
steps: [],
|
||||
images: [],
|
||||
imagePaths: [],
|
||||
skipTests: defaultSkipTests,
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import { Feature } from '@/store/app-store';
|
||||
import { GitBranch, GitPullRequest, ExternalLink, CheckCircle2, Circle } from 'lucide-react';
|
||||
import { GitBranch, GitPullRequest, ExternalLink } from 'lucide-react';
|
||||
|
||||
interface CardContentSectionsProps {
|
||||
feature: Feature;
|
||||
useWorktrees: boolean;
|
||||
showSteps: boolean;
|
||||
}
|
||||
|
||||
export function CardContentSections({
|
||||
feature,
|
||||
useWorktrees,
|
||||
showSteps,
|
||||
}: CardContentSectionsProps) {
|
||||
export function CardContentSections({ feature, useWorktrees }: CardContentSectionsProps) {
|
||||
return (
|
||||
<>
|
||||
{/* Target Branch Display */}
|
||||
@@ -50,30 +45,6 @@ export function CardContentSections({
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* Steps Preview */}
|
||||
{showSteps && feature.steps && feature.steps.length > 0 && (
|
||||
<div className="mb-3 space-y-1.5">
|
||||
{feature.steps.slice(0, 3).map((step, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-start gap-2 text-[11px] text-muted-foreground/80"
|
||||
>
|
||||
{feature.status === 'verified' ? (
|
||||
<CheckCircle2 className="w-3 h-3 mt-0.5 text-[var(--status-success)] shrink-0" />
|
||||
) : (
|
||||
<Circle className="w-3 h-3 mt-0.5 shrink-0 text-muted-foreground/50" />
|
||||
)}
|
||||
<span className="break-words hyphens-auto line-clamp-2 leading-relaxed">{step}</span>
|
||||
</div>
|
||||
))}
|
||||
{feature.steps.length > 3 && (
|
||||
<p className="text-[10px] text-muted-foreground/60 pl-5">
|
||||
+{feature.steps.length - 3} more
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -61,9 +61,7 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
cardBorderEnabled = true,
|
||||
cardBorderOpacity = 100,
|
||||
}: KanbanCardProps) {
|
||||
const { kanbanCardDetailLevel, useWorktrees } = useAppStore();
|
||||
|
||||
const showSteps = kanbanCardDetailLevel === 'standard' || kanbanCardDetailLevel === 'detailed';
|
||||
const { useWorktrees } = useAppStore();
|
||||
|
||||
const isDraggable =
|
||||
feature.status === 'backlog' ||
|
||||
@@ -152,7 +150,7 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
|
||||
<CardContent className="px-3 pt-0 pb-0">
|
||||
{/* Content Sections */}
|
||||
<CardContentSections feature={feature} useWorktrees={useWorktrees} showSteps={showSteps} />
|
||||
<CardContentSections feature={feature} useWorktrees={useWorktrees} />
|
||||
|
||||
{/* Agent Info Panel */}
|
||||
<AgentInfoPanel
|
||||
|
||||
@@ -61,7 +61,6 @@ interface AddFeatureDialogProps {
|
||||
title: string;
|
||||
category: string;
|
||||
description: string;
|
||||
steps: string[];
|
||||
images: FeatureImage[];
|
||||
imagePaths: DescriptionImagePath[];
|
||||
skipTests: boolean;
|
||||
@@ -103,7 +102,6 @@ export function AddFeatureDialog({
|
||||
title: '',
|
||||
category: '',
|
||||
description: '',
|
||||
steps: [''],
|
||||
images: [] as FeatureImage[],
|
||||
imagePaths: [] as DescriptionImagePath[],
|
||||
skipTests: false,
|
||||
@@ -190,7 +188,6 @@ export function AddFeatureDialog({
|
||||
title: newFeature.title,
|
||||
category,
|
||||
description: newFeature.description,
|
||||
steps: newFeature.steps.filter((s) => s.trim()),
|
||||
images: newFeature.images,
|
||||
imagePaths: newFeature.imagePaths,
|
||||
skipTests: newFeature.skipTests,
|
||||
@@ -207,7 +204,6 @@ export function AddFeatureDialog({
|
||||
title: '',
|
||||
category: '',
|
||||
description: '',
|
||||
steps: [''],
|
||||
images: [],
|
||||
imagePaths: [],
|
||||
skipTests: defaultSkipTests,
|
||||
@@ -493,8 +489,6 @@ export function AddFeatureDialog({
|
||||
<TestingTabContent
|
||||
skipTests={newFeature.skipTests}
|
||||
onSkipTestsChange={(skipTests) => setNewFeature({ ...newFeature, skipTests })}
|
||||
steps={newFeature.steps}
|
||||
onStepsChange={(steps) => setNewFeature({ ...newFeature, steps })}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
@@ -63,7 +63,6 @@ interface EditFeatureDialogProps {
|
||||
title: string;
|
||||
category: string;
|
||||
description: string;
|
||||
steps: string[];
|
||||
skipTests: boolean;
|
||||
model: AgentModel;
|
||||
thinkingLevel: ThinkingLevel;
|
||||
@@ -163,7 +162,6 @@ export function EditFeatureDialog({
|
||||
title: editingFeature.title ?? '',
|
||||
category: editingFeature.category,
|
||||
description: editingFeature.description,
|
||||
steps: editingFeature.steps,
|
||||
skipTests: editingFeature.skipTests ?? false,
|
||||
model: selectedModel,
|
||||
thinkingLevel: normalizedThinking,
|
||||
@@ -481,8 +479,6 @@ export function EditFeatureDialog({
|
||||
<TestingTabContent
|
||||
skipTests={editingFeature.skipTests ?? false}
|
||||
onSkipTestsChange={(skipTests) => setEditingFeature({ ...editingFeature, skipTests })}
|
||||
steps={editingFeature.steps}
|
||||
onStepsChange={(steps) => setEditingFeature({ ...editingFeature, steps })}
|
||||
testIdPrefix="edit"
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
@@ -245,7 +245,6 @@ export function FeatureSuggestionsDialog({
|
||||
id: `feature-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||
category: s.category,
|
||||
description: s.description,
|
||||
steps: s.steps,
|
||||
status: 'backlog' as const,
|
||||
skipTests: true, // As specified, testing mode true
|
||||
priority: s.priority, // Preserve priority from suggestion
|
||||
@@ -453,23 +452,9 @@ export function FeatureSuggestionsDialog({
|
||||
{suggestion.description}
|
||||
</Label>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="mt-3 space-y-2 text-sm">
|
||||
{suggestion.reasoning && (
|
||||
<p className="text-muted-foreground italic">{suggestion.reasoning}</p>
|
||||
)}
|
||||
{suggestion.steps.length > 0 && (
|
||||
<div>
|
||||
<p className="text-xs font-medium text-muted-foreground mb-1">
|
||||
Implementation Steps:
|
||||
</p>
|
||||
<ul className="list-disc list-inside text-xs text-muted-foreground space-y-0.5">
|
||||
{suggestion.steps.map((step, i) => (
|
||||
<li key={i}>{step}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
{isExpanded && suggestion.reasoning && (
|
||||
<div className="mt-3 text-sm">
|
||||
<p className="text-muted-foreground italic">{suggestion.reasoning}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -89,7 +89,6 @@ export function useBoardActions({
|
||||
title: string;
|
||||
category: string;
|
||||
description: string;
|
||||
steps: string[];
|
||||
images: FeatureImage[];
|
||||
imagePaths: DescriptionImagePath[];
|
||||
skipTests: boolean;
|
||||
@@ -208,7 +207,6 @@ export function useBoardActions({
|
||||
title: string;
|
||||
category: string;
|
||||
description: string;
|
||||
steps: string[];
|
||||
skipTests: boolean;
|
||||
model: AgentModel;
|
||||
thinkingLevel: ThinkingLevel;
|
||||
|
||||
@@ -1,36 +1,20 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { FlaskConical, Plus } from 'lucide-react';
|
||||
import { FlaskConical } from 'lucide-react';
|
||||
|
||||
interface TestingTabContentProps {
|
||||
skipTests: boolean;
|
||||
onSkipTestsChange: (skipTests: boolean) => void;
|
||||
steps: string[];
|
||||
onStepsChange: (steps: string[]) => void;
|
||||
testIdPrefix?: string;
|
||||
}
|
||||
|
||||
export function TestingTabContent({
|
||||
skipTests,
|
||||
onSkipTestsChange,
|
||||
steps,
|
||||
onStepsChange,
|
||||
testIdPrefix = '',
|
||||
}: TestingTabContentProps) {
|
||||
const checkboxId = testIdPrefix ? `${testIdPrefix}-skip-tests` : 'skip-tests';
|
||||
|
||||
const handleStepChange = (index: number, value: string) => {
|
||||
const newSteps = [...steps];
|
||||
newSteps[index] = value;
|
||||
onStepsChange(newSteps);
|
||||
};
|
||||
|
||||
const handleAddStep = () => {
|
||||
onStepsChange([...steps, '']);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
@@ -48,37 +32,9 @@ export function TestingTabContent({
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
When enabled, this feature will use automated TDD. When disabled, it will require manual
|
||||
verification.
|
||||
When enabled, the agent will use Playwright to verify the feature works correctly before
|
||||
marking it as verified. When disabled, manual verification will be required.
|
||||
</p>
|
||||
|
||||
{/* Verification Steps - Only shown when skipTests is enabled */}
|
||||
{skipTests && (
|
||||
<div className="space-y-2 pt-2 border-t border-border">
|
||||
<Label>Verification Steps</Label>
|
||||
<p className="text-xs text-muted-foreground mb-2">
|
||||
Add manual steps to verify this feature works correctly.
|
||||
</p>
|
||||
{steps.map((step, index) => (
|
||||
<Input
|
||||
key={index}
|
||||
value={step}
|
||||
placeholder={`Verification step ${index + 1}`}
|
||||
onChange={(e) => handleStepChange(index, e.target.value)}
|
||||
data-testid={`${testIdPrefix ? testIdPrefix + '-' : ''}feature-step-${index}${testIdPrefix ? '' : '-input'}`}
|
||||
/>
|
||||
))}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleAddStep}
|
||||
data-testid={`${testIdPrefix ? testIdPrefix + '-' : ''}add-step-button`}
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Add Verification Step
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
334
apps/ui/src/components/views/github-issues-view.tsx
Normal file
334
apps/ui/src/components/views/github-issues-view.tsx
Normal file
@@ -0,0 +1,334 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { CircleDot, Loader2, RefreshCw, ExternalLink, CheckCircle2, Circle, X } from 'lucide-react';
|
||||
import { getElectronAPI, GitHubIssue } from '@/lib/electron';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export function GitHubIssuesView() {
|
||||
const [openIssues, setOpenIssues] = useState<GitHubIssue[]>([]);
|
||||
const [closedIssues, setClosedIssues] = useState<GitHubIssue[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [selectedIssue, setSelectedIssue] = useState<GitHubIssue | null>(null);
|
||||
const { currentProject } = useAppStore();
|
||||
|
||||
const fetchIssues = useCallback(async () => {
|
||||
if (!currentProject?.path) {
|
||||
setError('No project selected');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setError(null);
|
||||
const api = getElectronAPI();
|
||||
if (api.github) {
|
||||
const result = await api.github.listIssues(currentProject.path);
|
||||
if (result.success) {
|
||||
setOpenIssues(result.openIssues || []);
|
||||
setClosedIssues(result.closedIssues || []);
|
||||
} else {
|
||||
setError(result.error || 'Failed to fetch issues');
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[GitHubIssuesView] Error fetching issues:', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to fetch issues');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
}
|
||||
}, [currentProject?.path]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchIssues();
|
||||
}, [fetchIssues]);
|
||||
|
||||
const handleRefresh = useCallback(() => {
|
||||
setRefreshing(true);
|
||||
fetchIssues();
|
||||
}, [fetchIssues]);
|
||||
|
||||
const handleOpenInGitHub = useCallback((url: string) => {
|
||||
const api = getElectronAPI();
|
||||
api.openExternalLink(url);
|
||||
}, []);
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
});
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex-1 flex flex-col items-center justify-center text-center p-6">
|
||||
<div className="p-4 rounded-full bg-destructive/10 mb-4">
|
||||
<CircleDot className="h-12 w-12 text-destructive" />
|
||||
</div>
|
||||
<h2 className="text-lg font-medium mb-2">Failed to Load Issues</h2>
|
||||
<p className="text-muted-foreground max-w-md mb-4">{error}</p>
|
||||
<Button variant="outline" onClick={handleRefresh}>
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
Try Again
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const totalIssues = openIssues.length + closedIssues.length;
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
{/* Issues List */}
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col overflow-hidden border-r border-border',
|
||||
selectedIssue ? 'w-80' : 'flex-1'
|
||||
)}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-border">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-lg bg-green-500/10">
|
||||
<CircleDot className="h-5 w-5 text-green-500" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-lg font-bold">Issues</h1>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{totalIssues === 0
|
||||
? 'No issues found'
|
||||
: `${openIssues.length} open, ${closedIssues.length} closed`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={handleRefresh} disabled={refreshing}>
|
||||
<RefreshCw className={cn('h-4 w-4', refreshing && 'animate-spin')} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Issues List */}
|
||||
<div className="flex-1 overflow-auto">
|
||||
{totalIssues === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-center p-6">
|
||||
<div className="p-4 rounded-full bg-muted/50 mb-4">
|
||||
<CircleDot className="h-8 w-8 text-muted-foreground" />
|
||||
</div>
|
||||
<h2 className="text-base font-medium mb-2">No Issues</h2>
|
||||
<p className="text-sm text-muted-foreground">This repository has no issues yet.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-border">
|
||||
{/* Open Issues */}
|
||||
{openIssues.map((issue) => (
|
||||
<IssueRow
|
||||
key={issue.number}
|
||||
issue={issue}
|
||||
isSelected={selectedIssue?.number === issue.number}
|
||||
onClick={() => setSelectedIssue(issue)}
|
||||
onOpenExternal={() => handleOpenInGitHub(issue.url)}
|
||||
formatDate={formatDate}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Closed Issues Section */}
|
||||
{closedIssues.length > 0 && (
|
||||
<>
|
||||
<div className="px-4 py-2 bg-muted/30 text-xs font-medium text-muted-foreground">
|
||||
Closed Issues ({closedIssues.length})
|
||||
</div>
|
||||
{closedIssues.map((issue) => (
|
||||
<IssueRow
|
||||
key={issue.number}
|
||||
issue={issue}
|
||||
isSelected={selectedIssue?.number === issue.number}
|
||||
onClick={() => setSelectedIssue(issue)}
|
||||
onOpenExternal={() => handleOpenInGitHub(issue.url)}
|
||||
formatDate={formatDate}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Issue Detail Panel */}
|
||||
{selectedIssue && (
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
{/* Detail Header */}
|
||||
<div className="flex items-center justify-between p-3 border-b border-border bg-muted/30">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
{selectedIssue.state === 'OPEN' ? (
|
||||
<Circle className="h-4 w-4 text-green-500 flex-shrink-0" />
|
||||
) : (
|
||||
<CheckCircle2 className="h-4 w-4 text-purple-500 flex-shrink-0" />
|
||||
)}
|
||||
<span className="text-sm font-medium truncate">
|
||||
#{selectedIssue.number} {selectedIssue.title}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleOpenInGitHub(selectedIssue.url)}
|
||||
>
|
||||
<ExternalLink className="h-4 w-4 mr-1" />
|
||||
Open in GitHub
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={() => setSelectedIssue(null)}>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Issue Detail Content */}
|
||||
<div className="flex-1 overflow-auto p-6">
|
||||
{/* Title */}
|
||||
<h1 className="text-xl font-bold mb-2">{selectedIssue.title}</h1>
|
||||
|
||||
{/* Meta info */}
|
||||
<div className="flex items-center gap-3 text-sm text-muted-foreground mb-4">
|
||||
<span
|
||||
className={cn(
|
||||
'px-2 py-0.5 rounded-full text-xs font-medium',
|
||||
selectedIssue.state === 'OPEN'
|
||||
? 'bg-green-500/10 text-green-500'
|
||||
: 'bg-purple-500/10 text-purple-500'
|
||||
)}
|
||||
>
|
||||
{selectedIssue.state === 'OPEN' ? 'Open' : 'Closed'}
|
||||
</span>
|
||||
<span>
|
||||
#{selectedIssue.number} opened {formatDate(selectedIssue.createdAt)} by{' '}
|
||||
<span className="font-medium text-foreground">{selectedIssue.author.login}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Labels */}
|
||||
{selectedIssue.labels.length > 0 && (
|
||||
<div className="flex items-center gap-2 mb-6 flex-wrap">
|
||||
{selectedIssue.labels.map((label) => (
|
||||
<span
|
||||
key={label.name}
|
||||
className="px-2 py-0.5 text-xs font-medium rounded-full"
|
||||
style={{
|
||||
backgroundColor: `#${label.color}20`,
|
||||
color: `#${label.color}`,
|
||||
border: `1px solid #${label.color}40`,
|
||||
}}
|
||||
>
|
||||
{label.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Body */}
|
||||
{selectedIssue.body ? (
|
||||
<div className="prose prose-sm dark:prose-invert max-w-none">
|
||||
<div className="whitespace-pre-wrap text-sm">{selectedIssue.body}</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground italic">No description provided.</p>
|
||||
)}
|
||||
|
||||
{/* Open in GitHub CTA */}
|
||||
<div className="mt-8 p-4 rounded-lg bg-muted/50 border border-border">
|
||||
<p className="text-sm text-muted-foreground mb-3">
|
||||
View comments, add reactions, and more on GitHub.
|
||||
</p>
|
||||
<Button onClick={() => handleOpenInGitHub(selectedIssue.url)}>
|
||||
<ExternalLink className="h-4 w-4 mr-2" />
|
||||
View Full Issue on GitHub
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface IssueRowProps {
|
||||
issue: GitHubIssue;
|
||||
isSelected: boolean;
|
||||
onClick: () => void;
|
||||
onOpenExternal: () => void;
|
||||
formatDate: (date: string) => string;
|
||||
}
|
||||
|
||||
function IssueRow({ issue, isSelected, onClick, onOpenExternal, formatDate }: IssueRowProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-start gap-3 p-3 cursor-pointer hover:bg-accent/50 transition-colors',
|
||||
isSelected && 'bg-accent'
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
{issue.state === 'OPEN' ? (
|
||||
<Circle className="h-4 w-4 text-green-500 mt-0.5 flex-shrink-0" />
|
||||
) : (
|
||||
<CheckCircle2 className="h-4 w-4 text-purple-500 mt-0.5 flex-shrink-0" />
|
||||
)}
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium truncate">{issue.title}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 mt-1 flex-wrap">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
#{issue.number} opened {formatDate(issue.createdAt)} by {issue.author.login}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{issue.labels.length > 0 && (
|
||||
<div className="flex items-center gap-1 mt-2 flex-wrap">
|
||||
{issue.labels.map((label) => (
|
||||
<span
|
||||
key={label.name}
|
||||
className="px-1.5 py-0.5 text-[10px] font-medium rounded-full"
|
||||
style={{
|
||||
backgroundColor: `#${label.color}20`,
|
||||
color: `#${label.color}`,
|
||||
border: `1px solid #${label.color}40`,
|
||||
}}
|
||||
>
|
||||
{label.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="flex-shrink-0 opacity-0 group-hover:opacity-100"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onOpenExternal();
|
||||
}}
|
||||
>
|
||||
<ExternalLink className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
421
apps/ui/src/components/views/github-prs-view.tsx
Normal file
421
apps/ui/src/components/views/github-prs-view.tsx
Normal file
@@ -0,0 +1,421 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
GitPullRequest,
|
||||
Loader2,
|
||||
RefreshCw,
|
||||
ExternalLink,
|
||||
GitMerge,
|
||||
Circle,
|
||||
X,
|
||||
AlertCircle,
|
||||
} from 'lucide-react';
|
||||
import { getElectronAPI, GitHubPR } from '@/lib/electron';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export function GitHubPRsView() {
|
||||
const [openPRs, setOpenPRs] = useState<GitHubPR[]>([]);
|
||||
const [mergedPRs, setMergedPRs] = useState<GitHubPR[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [selectedPR, setSelectedPR] = useState<GitHubPR | null>(null);
|
||||
const { currentProject } = useAppStore();
|
||||
|
||||
const fetchPRs = useCallback(async () => {
|
||||
if (!currentProject?.path) {
|
||||
setError('No project selected');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setError(null);
|
||||
const api = getElectronAPI();
|
||||
if (api.github) {
|
||||
const result = await api.github.listPRs(currentProject.path);
|
||||
if (result.success) {
|
||||
setOpenPRs(result.openPRs || []);
|
||||
setMergedPRs(result.mergedPRs || []);
|
||||
} else {
|
||||
setError(result.error || 'Failed to fetch pull requests');
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[GitHubPRsView] Error fetching PRs:', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to fetch pull requests');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
}
|
||||
}, [currentProject?.path]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchPRs();
|
||||
}, [fetchPRs]);
|
||||
|
||||
const handleRefresh = useCallback(() => {
|
||||
setRefreshing(true);
|
||||
fetchPRs();
|
||||
}, [fetchPRs]);
|
||||
|
||||
const handleOpenInGitHub = useCallback((url: string) => {
|
||||
const api = getElectronAPI();
|
||||
api.openExternalLink(url);
|
||||
}, []);
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
});
|
||||
};
|
||||
|
||||
const getReviewStatus = (pr: GitHubPR) => {
|
||||
if (pr.isDraft) return { label: 'Draft', color: 'text-muted-foreground', bg: 'bg-muted' };
|
||||
switch (pr.reviewDecision) {
|
||||
case 'APPROVED':
|
||||
return { label: 'Approved', color: 'text-green-500', bg: 'bg-green-500/10' };
|
||||
case 'CHANGES_REQUESTED':
|
||||
return { label: 'Changes requested', color: 'text-orange-500', bg: 'bg-orange-500/10' };
|
||||
case 'REVIEW_REQUIRED':
|
||||
return { label: 'Review required', color: 'text-yellow-500', bg: 'bg-yellow-500/10' };
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex-1 flex flex-col items-center justify-center text-center p-6">
|
||||
<div className="p-4 rounded-full bg-destructive/10 mb-4">
|
||||
<GitPullRequest className="h-12 w-12 text-destructive" />
|
||||
</div>
|
||||
<h2 className="text-lg font-medium mb-2">Failed to Load Pull Requests</h2>
|
||||
<p className="text-muted-foreground max-w-md mb-4">{error}</p>
|
||||
<Button variant="outline" onClick={handleRefresh}>
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
Try Again
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const totalPRs = openPRs.length + mergedPRs.length;
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
{/* PR List */}
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col overflow-hidden border-r border-border',
|
||||
selectedPR ? 'w-80' : 'flex-1'
|
||||
)}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-border">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-lg bg-blue-500/10">
|
||||
<GitPullRequest className="h-5 w-5 text-blue-500" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-lg font-bold">Pull Requests</h1>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{totalPRs === 0
|
||||
? 'No pull requests found'
|
||||
: `${openPRs.length} open, ${mergedPRs.length} merged`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={handleRefresh} disabled={refreshing}>
|
||||
<RefreshCw className={cn('h-4 w-4', refreshing && 'animate-spin')} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* PR List */}
|
||||
<div className="flex-1 overflow-auto">
|
||||
{totalPRs === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-center p-6">
|
||||
<div className="p-4 rounded-full bg-muted/50 mb-4">
|
||||
<GitPullRequest className="h-8 w-8 text-muted-foreground" />
|
||||
</div>
|
||||
<h2 className="text-base font-medium mb-2">No Pull Requests</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
This repository has no pull requests yet.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-border">
|
||||
{/* Open PRs */}
|
||||
{openPRs.map((pr) => (
|
||||
<PRRow
|
||||
key={pr.number}
|
||||
pr={pr}
|
||||
isSelected={selectedPR?.number === pr.number}
|
||||
onClick={() => setSelectedPR(pr)}
|
||||
onOpenExternal={() => handleOpenInGitHub(pr.url)}
|
||||
formatDate={formatDate}
|
||||
getReviewStatus={getReviewStatus}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Merged PRs Section */}
|
||||
{mergedPRs.length > 0 && (
|
||||
<>
|
||||
<div className="px-4 py-2 bg-muted/30 text-xs font-medium text-muted-foreground">
|
||||
Merged ({mergedPRs.length})
|
||||
</div>
|
||||
{mergedPRs.map((pr) => (
|
||||
<PRRow
|
||||
key={pr.number}
|
||||
pr={pr}
|
||||
isSelected={selectedPR?.number === pr.number}
|
||||
onClick={() => setSelectedPR(pr)}
|
||||
onOpenExternal={() => handleOpenInGitHub(pr.url)}
|
||||
formatDate={formatDate}
|
||||
getReviewStatus={getReviewStatus}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* PR Detail Panel */}
|
||||
{selectedPR && (
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
{/* Detail Header */}
|
||||
<div className="flex items-center justify-between p-3 border-b border-border bg-muted/30">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
{selectedPR.state === 'MERGED' ? (
|
||||
<GitMerge className="h-4 w-4 text-purple-500 flex-shrink-0" />
|
||||
) : (
|
||||
<GitPullRequest className="h-4 w-4 text-green-500 flex-shrink-0" />
|
||||
)}
|
||||
<span className="text-sm font-medium truncate">
|
||||
#{selectedPR.number} {selectedPR.title}
|
||||
</span>
|
||||
{selectedPR.isDraft && (
|
||||
<span className="px-1.5 py-0.5 text-[10px] font-medium rounded bg-muted text-muted-foreground">
|
||||
Draft
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleOpenInGitHub(selectedPR.url)}
|
||||
>
|
||||
<ExternalLink className="h-4 w-4 mr-1" />
|
||||
Open in GitHub
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={() => setSelectedPR(null)}>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* PR Detail Content */}
|
||||
<div className="flex-1 overflow-auto p-6">
|
||||
{/* Title */}
|
||||
<h1 className="text-xl font-bold mb-2">{selectedPR.title}</h1>
|
||||
|
||||
{/* Meta info */}
|
||||
<div className="flex items-center gap-3 text-sm text-muted-foreground mb-4 flex-wrap">
|
||||
<span
|
||||
className={cn(
|
||||
'px-2 py-0.5 rounded-full text-xs font-medium',
|
||||
selectedPR.state === 'MERGED'
|
||||
? 'bg-purple-500/10 text-purple-500'
|
||||
: selectedPR.isDraft
|
||||
? 'bg-muted text-muted-foreground'
|
||||
: 'bg-green-500/10 text-green-500'
|
||||
)}
|
||||
>
|
||||
{selectedPR.state === 'MERGED' ? 'Merged' : selectedPR.isDraft ? 'Draft' : 'Open'}
|
||||
</span>
|
||||
{getReviewStatus(selectedPR) && (
|
||||
<span
|
||||
className={cn(
|
||||
'px-2 py-0.5 rounded-full text-xs font-medium',
|
||||
getReviewStatus(selectedPR)!.bg,
|
||||
getReviewStatus(selectedPR)!.color
|
||||
)}
|
||||
>
|
||||
{getReviewStatus(selectedPR)!.label}
|
||||
</span>
|
||||
)}
|
||||
<span>
|
||||
#{selectedPR.number} opened {formatDate(selectedPR.createdAt)} by{' '}
|
||||
<span className="font-medium text-foreground">{selectedPR.author.login}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Branch info */}
|
||||
{selectedPR.headRefName && (
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<span className="text-xs text-muted-foreground">Branch:</span>
|
||||
<span className="text-xs font-mono bg-muted px-2 py-0.5 rounded">
|
||||
{selectedPR.headRefName}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Labels */}
|
||||
{selectedPR.labels.length > 0 && (
|
||||
<div className="flex items-center gap-2 mb-6 flex-wrap">
|
||||
{selectedPR.labels.map((label) => (
|
||||
<span
|
||||
key={label.name}
|
||||
className="px-2 py-0.5 text-xs font-medium rounded-full"
|
||||
style={{
|
||||
backgroundColor: `#${label.color}20`,
|
||||
color: `#${label.color}`,
|
||||
border: `1px solid #${label.color}40`,
|
||||
}}
|
||||
>
|
||||
{label.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Body */}
|
||||
{selectedPR.body ? (
|
||||
<div className="prose prose-sm dark:prose-invert max-w-none">
|
||||
<div className="whitespace-pre-wrap text-sm">{selectedPR.body}</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground italic">No description provided.</p>
|
||||
)}
|
||||
|
||||
{/* Open in GitHub CTA */}
|
||||
<div className="mt-8 p-4 rounded-lg bg-muted/50 border border-border">
|
||||
<p className="text-sm text-muted-foreground mb-3">
|
||||
View code changes, comments, and reviews on GitHub.
|
||||
</p>
|
||||
<Button onClick={() => handleOpenInGitHub(selectedPR.url)}>
|
||||
<ExternalLink className="h-4 w-4 mr-2" />
|
||||
View Full PR on GitHub
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface PRRowProps {
|
||||
pr: GitHubPR;
|
||||
isSelected: boolean;
|
||||
onClick: () => void;
|
||||
onOpenExternal: () => void;
|
||||
formatDate: (date: string) => string;
|
||||
getReviewStatus: (pr: GitHubPR) => { label: string; color: string; bg: string } | null;
|
||||
}
|
||||
|
||||
function PRRow({
|
||||
pr,
|
||||
isSelected,
|
||||
onClick,
|
||||
onOpenExternal,
|
||||
formatDate,
|
||||
getReviewStatus,
|
||||
}: PRRowProps) {
|
||||
const reviewStatus = getReviewStatus(pr);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-start gap-3 p-3 cursor-pointer hover:bg-accent/50 transition-colors',
|
||||
isSelected && 'bg-accent'
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
{pr.state === 'MERGED' ? (
|
||||
<GitMerge className="h-4 w-4 text-purple-500 mt-0.5 flex-shrink-0" />
|
||||
) : (
|
||||
<GitPullRequest className="h-4 w-4 text-green-500 mt-0.5 flex-shrink-0" />
|
||||
)}
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium truncate">{pr.title}</span>
|
||||
{pr.isDraft && (
|
||||
<span className="px-1.5 py-0.5 text-[10px] font-medium rounded bg-muted text-muted-foreground flex-shrink-0">
|
||||
Draft
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 mt-1 flex-wrap">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
#{pr.number} opened {formatDate(pr.createdAt)} by {pr.author.login}
|
||||
</span>
|
||||
{pr.headRefName && (
|
||||
<span className="text-xs text-muted-foreground font-mono bg-muted px-1 rounded">
|
||||
{pr.headRefName}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 mt-2 flex-wrap">
|
||||
{/* Review Status */}
|
||||
{reviewStatus && (
|
||||
<span
|
||||
className={cn(
|
||||
'px-1.5 py-0.5 text-[10px] font-medium rounded',
|
||||
reviewStatus.bg,
|
||||
reviewStatus.color
|
||||
)}
|
||||
>
|
||||
{reviewStatus.label}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Labels */}
|
||||
{pr.labels.map((label) => (
|
||||
<span
|
||||
key={label.name}
|
||||
className="px-1.5 py-0.5 text-[10px] font-medium rounded-full"
|
||||
style={{
|
||||
backgroundColor: `#${label.color}20`,
|
||||
color: `#${label.color}`,
|
||||
border: `1px solid #${label.color}40`,
|
||||
}}
|
||||
>
|
||||
{label.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="flex-shrink-0 opacity-0 group-hover:opacity-100"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onOpenExternal();
|
||||
}}
|
||||
>
|
||||
<ExternalLink className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -345,11 +345,6 @@ export function InterviewView() {
|
||||
category: 'Core',
|
||||
description: 'Initial project setup',
|
||||
status: 'backlog' as const,
|
||||
steps: [
|
||||
'Step 1: Review app_spec.txt',
|
||||
'Step 2: Set up development environment',
|
||||
'Step 3: Start implementing features',
|
||||
],
|
||||
skipTests: true,
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user