feat: enhance PR handling and UI integration for worktrees

- Added a new route for fetching PR info, allowing users to retrieve details about existing pull requests associated with worktrees.
- Updated the create PR handler to store metadata for existing PRs and handle cases where a PR already exists.
- Enhanced the UI components to display PR information, including a new button to address PR comments directly from the worktree panel.
- Improved the overall user experience by integrating PR state indicators and ensuring seamless interaction with the GitHub CLI for PR management.
This commit is contained in:
Cody Seibert
2025-12-19 19:48:14 -05:00
parent 9bfcb91774
commit d4365de4b9
14 changed files with 980 additions and 45 deletions

View File

@@ -39,6 +39,7 @@ import { CommitWorktreeDialog } from "./board-view/dialogs/commit-worktree-dialo
import { CreatePRDialog } from "./board-view/dialogs/create-pr-dialog";
import { CreateBranchDialog } from "./board-view/dialogs/create-branch-dialog";
import { WorktreePanel } from "./board-view/worktree-panel";
import type { PRInfo, WorktreeInfo } from "./board-view/worktree-panel/types";
import { COLUMNS } from "./board-view/constants";
import {
useBoardFeatures,
@@ -415,6 +416,95 @@ export function BoardView() {
currentWorktreeBranch,
});
// Handler for addressing PR comments - creates a feature and starts it automatically
const handleAddressPRComments = useCallback(
async (worktree: WorktreeInfo, prInfo: PRInfo) => {
// If comments are empty, fetch them from GitHub
let fullPRInfo = prInfo;
if (prInfo.comments.length === 0 && prInfo.reviewComments.length === 0) {
try {
const api = getElectronAPI();
if (api?.worktree?.getPRInfo) {
const result = await api.worktree.getPRInfo(
worktree.path,
worktree.branch
);
if (result.success && result.result?.hasPR && result.result.prInfo) {
fullPRInfo = result.result.prInfo;
}
}
} catch (error) {
console.error("[Board] Failed to fetch PR comments:", error);
}
}
// Format PR comments into a feature description
const allComments = [
...fullPRInfo.comments.map((c) => ({
...c,
type: "comment" as const,
})),
...fullPRInfo.reviewComments.map((c) => ({
...c,
type: "review" as const,
})),
].sort(
(a, b) =>
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
);
let description = `Address PR #${fullPRInfo.number} feedback: "${fullPRInfo.title}"\n\n`;
description += `PR URL: ${fullPRInfo.url}\n\n`;
if (allComments.length === 0) {
description += `No comments found on this PR yet. Check the PR for any new feedback.\n`;
} else {
description += `## Feedback to address:\n\n`;
for (const comment of allComments) {
if (comment.type === "review" && comment.path) {
description += `### ${comment.path}${comment.line ? `:${comment.line}` : ""}\n`;
}
description += `**@${comment.author}:**\n${comment.body}\n\n`;
}
}
// Create the feature
const featureData = {
category: "PR Review",
description: description.trim(),
steps: [],
images: [],
imagePaths: [],
skipTests: defaultSkipTests,
model: "sonnet" as const,
thinkingLevel: "medium" as const,
branchName: worktree.branch,
priority: 1, // High priority for PR feedback
planningMode: "skip" as const,
requirePlanApproval: false,
};
await handleAddFeature(featureData);
// Find the newly created feature and start it
// We need to wait a moment for the feature to be created
setTimeout(async () => {
const latestFeatures = useAppStore.getState().features;
const newFeature = latestFeatures.find(
(f) =>
f.branchName === worktree.branch &&
f.status === "backlog" &&
f.description.includes(`PR #${fullPRInfo.number}`)
);
if (newFeature) {
await handleStartImplementation(newFeature);
}
}, 500);
},
[handleAddFeature, handleStartImplementation, defaultSkipTests]
);
// Client-side auto mode: periodically check for backlog items and move them to in-progress
// Use a ref to track the latest auto mode state so async operations always check the current value
const autoModeRunningRef = useRef(autoMode.isRunning);
@@ -874,6 +964,7 @@ export function BoardView() {
setSelectedWorktreeForAction(worktree);
setShowCreateBranchDialog(true);
}}
onAddressPRComments={handleAddressPRComments}
onRemovedWorktrees={handleRemovedWorktrees}
runningFeatureIds={runningAutoTasks}
branchCardCounts={branchCardCounts}
@@ -1153,6 +1244,7 @@ export function BoardView() {
open={showCreatePRDialog}
onOpenChange={setShowCreatePRDialog}
worktree={selectedWorktreeForAction}
<<<<<<< Updated upstream
onCreated={(prUrl) => {
// If a PR was created and we have the worktree branch, update all features on that branch with the PR URL
if (prUrl && selectedWorktreeForAction?.branch) {
@@ -1164,6 +1256,10 @@ export function BoardView() {
persistFeatureUpdate(feature.id, { prUrl });
});
}
=======
projectPath={currentProject?.path || null}
onCreated={() => {
>>>>>>> Stashed changes
setWorktreeRefreshKey((k) => k + 1);
setSelectedWorktreeForAction(null);
}}

View File

@@ -29,13 +29,19 @@ interface CreatePRDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
worktree: WorktreeInfo | null;
<<<<<<< Updated upstream
onCreated: (prUrl?: string) => void;
=======
projectPath: string | null;
onCreated: () => void;
>>>>>>> Stashed changes
}
export function CreatePRDialog({
open,
onOpenChange,
worktree,
projectPath,
onCreated,
}: CreatePRDialogProps) {
const [title, setTitle] = useState("");
@@ -96,6 +102,7 @@ export function CreatePRDialog({
return;
}
const result = await api.worktree.createPR(worktree.path, {
projectPath: projectPath || undefined,
commitMessage: commitMessage || undefined,
prTitle: title || worktree.branch,
prBody: body || `Changes from branch ${worktree.branch}`,
@@ -108,13 +115,25 @@ export function CreatePRDialog({
setPrUrl(result.result.prUrl);
// Mark operation as completed for refresh on close
operationCompletedRef.current = true;
toast.success("Pull request created!", {
description: `PR created from ${result.result.branch}`,
action: {
label: "View PR",
onClick: () => window.open(result.result!.prUrl!, "_blank"),
},
});
// Show different message based on whether PR already existed
if (result.result.prAlreadyExisted) {
toast.success("Pull request found!", {
description: `PR already exists for ${result.result.branch}`,
action: {
label: "View PR",
onClick: () => window.open(result.result!.prUrl!, "_blank"),
},
});
} else {
toast.success("Pull request created!", {
description: `PR created from ${result.result.branch}`,
action: {
label: "View PR",
onClick: () => window.open(result.result!.prUrl!, "_blank"),
},
});
}
// Don't call onCreated() here - keep dialog open to show success message
// onCreated() will be called when user closes the dialog
} else {

View File

@@ -19,9 +19,10 @@ import {
Play,
Square,
Globe,
MessageSquare,
} from "lucide-react";
import { cn } from "@/lib/utils";
import type { WorktreeInfo, DevServerInfo } from "../types";
import type { WorktreeInfo, DevServerInfo, PRInfo } from "../types";
interface WorktreeActionsDropdownProps {
worktree: WorktreeInfo;
@@ -40,6 +41,7 @@ interface WorktreeActionsDropdownProps {
onOpenInEditor: (worktree: WorktreeInfo) => void;
onCommit: (worktree: WorktreeInfo) => void;
onCreatePR: (worktree: WorktreeInfo) => void;
onAddressPRComments: (worktree: WorktreeInfo, prInfo: PRInfo) => void;
onDeleteWorktree: (worktree: WorktreeInfo) => void;
onStartDevServer: (worktree: WorktreeInfo) => void;
onStopDevServer: (worktree: WorktreeInfo) => void;
@@ -63,11 +65,15 @@ export function WorktreeActionsDropdown({
onOpenInEditor,
onCommit,
onCreatePR,
onAddressPRComments,
onDeleteWorktree,
onStartDevServer,
onStopDevServer,
onOpenDevServerUrl,
}: WorktreeActionsDropdownProps) {
// Check if there's a PR associated with this worktree from stored metadata
const hasPR = !!worktree.pr;
return (
<DropdownMenu onOpenChange={onOpenChange}>
<DropdownMenuTrigger asChild>
@@ -170,12 +176,45 @@ export function WorktreeActionsDropdown({
</DropdownMenuItem>
)}
{/* Show PR option for non-primary worktrees, or primary worktree with changes */}
{(!worktree.isMain || worktree.hasChanges) && (
{(!worktree.isMain || worktree.hasChanges) && !hasPR && (
<DropdownMenuItem onClick={() => onCreatePR(worktree)} className="text-xs">
<GitPullRequest className="w-3.5 h-3.5 mr-2" />
Create Pull Request
</DropdownMenuItem>
)}
{/* Show PR info and Address Comments button if PR exists */}
{!worktree.isMain && hasPR && worktree.pr && (
<>
<DropdownMenuLabel className="text-xs flex items-center gap-2">
<GitPullRequest className="w-3 h-3" />
PR #{worktree.pr.number}
<span className="ml-auto text-[10px] bg-green-500/20 text-green-600 px-1.5 py-0.5 rounded">
{worktree.pr.state}
</span>
</DropdownMenuLabel>
<DropdownMenuItem
onClick={() => {
// Convert stored PR info to the full PRInfo format for the handler
// The handler will fetch full comments from GitHub
const prInfo: PRInfo = {
number: worktree.pr!.number,
title: worktree.pr!.title,
url: worktree.pr!.url,
state: worktree.pr!.state,
author: "", // Will be fetched
body: "", // Will be fetched
comments: [],
reviewComments: [],
};
onAddressPRComments(worktree, prInfo);
}}
className="text-xs text-blue-500 focus:text-blue-600"
>
<MessageSquare className="w-3.5 h-3.5 mr-2" />
Address PR Comments
</DropdownMenuItem>
</>
)}
{!worktree.isMain && (
<>
<DropdownMenuSeparator />

View File

@@ -1,5 +1,6 @@
import { Button } from "@/components/ui/button";
<<<<<<< Updated upstream
import { RefreshCw, Globe, Loader2, CircleDot } from "lucide-react";
import { cn } from "@/lib/utils";
import {
@@ -9,6 +10,11 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import type { WorktreeInfo, BranchInfo, DevServerInfo } from "../types";
=======
import { RefreshCw, Globe, Loader2, GitPullRequest } from "lucide-react";
import { cn } from "@/lib/utils";
import type { WorktreeInfo, BranchInfo, DevServerInfo, PRInfo } from "../types";
>>>>>>> Stashed changes
import { BranchSwitchDropdown } from "./branch-switch-dropdown";
import { WorktreeActionsDropdown } from "./worktree-actions-dropdown";
@@ -44,6 +50,7 @@ interface WorktreeTabProps {
onOpenInEditor: (worktree: WorktreeInfo) => void;
onCommit: (worktree: WorktreeInfo) => void;
onCreatePR: (worktree: WorktreeInfo) => void;
onAddressPRComments: (worktree: WorktreeInfo, prInfo: PRInfo) => void;
onDeleteWorktree: (worktree: WorktreeInfo) => void;
onStartDevServer: (worktree: WorktreeInfo) => void;
onStopDevServer: (worktree: WorktreeInfo) => void;
@@ -82,11 +89,13 @@ export function WorktreeTab({
onOpenInEditor,
onCommit,
onCreatePR,
onAddressPRComments,
onDeleteWorktree,
onStartDevServer,
onStopDevServer,
onOpenDevServerUrl,
}: WorktreeTabProps) {
<<<<<<< Updated upstream
// Determine border color based on state:
// - Running features: cyan border (high visibility, indicates active work)
// - Uncommitted changes: amber border (warning state, needs attention)
@@ -102,6 +111,93 @@ export function WorktreeTab({
};
const borderClasses = getBorderClasses();
=======
let prBadge: JSX.Element | null = null;
if (worktree.pr) {
const prState = worktree.pr.state?.toLowerCase() ?? "open";
const prStateClasses = (() => {
// When selected (active tab), use high contrast solid background (paper-like)
if (isSelected) {
return "bg-background text-foreground border-transparent shadow-sm";
}
// When not selected, use the colored variants
switch (prState) {
case "open":
case "reopened":
return "bg-emerald-500/15 dark:bg-emerald-500/20 text-emerald-600 dark:text-emerald-400 border-emerald-500/30 dark:border-emerald-500/40 hover:bg-emerald-500/25";
case "draft":
return "bg-amber-500/15 dark:bg-amber-500/20 text-amber-600 dark:text-amber-400 border-amber-500/30 dark:border-amber-500/40 hover:bg-amber-500/25";
case "merged":
return "bg-purple-500/15 dark:bg-purple-500/20 text-purple-600 dark:text-purple-400 border-purple-500/30 dark:border-purple-500/40 hover:bg-purple-500/25";
case "closed":
return "bg-rose-500/15 dark:bg-rose-500/20 text-rose-600 dark:text-rose-400 border-rose-500/30 dark:border-rose-500/40 hover:bg-rose-500/25";
default:
return "bg-muted text-muted-foreground border-border/60 hover:bg-muted/80";
}
})();
const prTitle = worktree.pr.title || `Pull Request #${worktree.pr.number}`;
const prLabel = `Pull Request #${worktree.pr.number}, ${prState}${worktree.pr.title ? `: ${worktree.pr.title}` : ""}`;
// Helper to get status icon color for the selected state
const getStatusColorClass = () => {
if (!isSelected) return "";
switch (prState) {
case "open":
case "reopened":
return "text-emerald-600 dark:text-emerald-500";
case "draft":
return "text-amber-600 dark:text-amber-500";
case "merged":
return "text-purple-600 dark:text-purple-500";
case "closed":
return "text-rose-600 dark:text-rose-500";
default:
return "text-muted-foreground";
}
};
prBadge = (
<button
type="button"
className={cn(
"ml-1.5 inline-flex items-center gap-1 rounded-full border px-1.5 py-0.5 text-[10px] font-medium transition-colors",
"focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-1 focus:ring-offset-background",
"appearance-none cursor-pointer", // Reset button appearance but keep cursor
prStateClasses
)}
style={{
// Override any inherited button styles
backgroundImage: "none",
boxShadow: "none",
}}
title={prLabel}
aria-label={prLabel}
onClick={(e) => {
e.stopPropagation(); // Prevent triggering worktree selection
window.open(worktree.pr.url, "_blank", "noopener,noreferrer");
}}
onKeyDown={(e) => {
// Prevent event from bubbling to parent button
e.stopPropagation();
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
window.open(worktree.pr.url, "_blank", "noopener,noreferrer");
}
}}
>
<GitPullRequest className={cn("w-3 h-3", getStatusColorClass())} aria-hidden="true" />
<span aria-hidden="true" className={isSelected ? "text-foreground font-semibold" : ""}>
#{worktree.pr.number}
</span>
<span className={cn("capitalize", getStatusColorClass())} aria-hidden="true">
{prState}
</span>
</button>
);
}
>>>>>>> Stashed changes
return (
<div className={cn("flex items-center rounded-md", borderClasses)}>
@@ -129,6 +225,7 @@ export function WorktreeTab({
{cardCount}
</span>
)}
<<<<<<< Updated upstream
{hasChanges && (
<TooltipProvider>
<Tooltip>
@@ -149,6 +246,9 @@ export function WorktreeTab({
</Tooltip>
</TooltipProvider>
)}
=======
{prBadge}
>>>>>>> Stashed changes
</Button>
<BranchSwitchDropdown
worktree={worktree}
@@ -192,6 +292,7 @@ export function WorktreeTab({
{cardCount}
</span>
)}
<<<<<<< Updated upstream
{hasChanges && (
<TooltipProvider>
<Tooltip>
@@ -212,6 +313,9 @@ export function WorktreeTab({
</Tooltip>
</TooltipProvider>
)}
=======
{prBadge}
>>>>>>> Stashed changes
</Button>
)}
@@ -249,6 +353,7 @@ export function WorktreeTab({
onOpenInEditor={onOpenInEditor}
onCommit={onCommit}
onCreatePR={onCreatePR}
onAddressPRComments={onAddressPRComments}
onDeleteWorktree={onDeleteWorktree}
onStartDevServer={onStartDevServer}
onStopDevServer={onStopDevServer}

View File

@@ -1,3 +1,11 @@
export interface WorktreePRInfo {
number: number;
url: string;
title: string;
state: string;
createdAt: string;
}
export interface WorktreeInfo {
path: string;
branch: string;
@@ -6,6 +14,7 @@ export interface WorktreeInfo {
hasWorktree: boolean;
hasChanges?: boolean;
changedFilesCount?: number;
pr?: WorktreePRInfo;
}
export interface BranchInfo {
@@ -25,6 +34,31 @@ export interface FeatureInfo {
branchName?: string;
}
export interface PRInfo {
number: number;
title: string;
url: string;
state: string;
author: string;
body: string;
comments: Array<{
id: number;
author: string;
body: string;
createdAt: string;
isReviewComment: boolean;
}>;
reviewComments: Array<{
id: number;
author: string;
body: string;
path?: string;
line?: number;
createdAt: string;
isReviewComment: boolean;
}>;
}
export interface WorktreePanelProps {
projectPath: string;
onCreateWorktree: () => void;
@@ -32,6 +66,7 @@ export interface WorktreePanelProps {
onCommit: (worktree: WorktreeInfo) => void;
onCreatePR: (worktree: WorktreeInfo) => void;
onCreateBranch: (worktree: WorktreeInfo) => void;
onAddressPRComments: (worktree: WorktreeInfo, prInfo: PRInfo) => void;
onRemovedWorktrees?: (removedWorktrees: Array<{ path: string; branch: string }>) => void;
runningFeatureIds?: string[];
features?: FeatureInfo[];

View File

@@ -20,6 +20,7 @@ export function WorktreePanel({
onCommit,
onCreatePR,
onCreateBranch,
onAddressPRComments,
onRemovedWorktrees,
runningFeatureIds = [],
features = [],
@@ -146,6 +147,7 @@ export function WorktreePanel({
onOpenInEditor={handleOpenInEditor}
onCommit={onCommit}
onCreatePR={onCreatePR}
onAddressPRComments={onAddressPRComments}
onDeleteWorktree={onDeleteWorktree}
onStartDevServer={handleStartDevServer}
onStopDevServer={handleStopDevServer}