diff --git a/apps/app/src/components/views/board-view.tsx b/apps/app/src/components/views/board-view.tsx index c83c7b8d..fcfa699f 100644 --- a/apps/app/src/components/views/board-view.tsx +++ b/apps/app/src/components/views/board-view.tsx @@ -1154,7 +1154,17 @@ export function BoardView() { open={showCreatePRDialog} onOpenChange={setShowCreatePRDialog} worktree={selectedWorktreeForAction} - onCreated={() => { + 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) { + const branchName = selectedWorktreeForAction.branch; + hookFeatures + .filter((f) => f.branchName === branchName) + .forEach((feature) => { + updateFeature(feature.id, { prUrl }); + persistFeatureUpdate(feature.id, { prUrl }); + }); + } setWorktreeRefreshKey((k) => k + 1); setSelectedWorktreeForAction(null); }} diff --git a/apps/app/src/components/views/board-view/components/kanban-card.tsx b/apps/app/src/components/views/board-view/components/kanban-card.tsx index 564be40b..0cb7a366 100644 --- a/apps/app/src/components/views/board-view/components/kanban-card.tsx +++ b/apps/app/src/components/views/board-view/components/kanban-card.tsx @@ -52,6 +52,8 @@ import { MoreVertical, AlertCircle, GitBranch, + GitPullRequest, + ExternalLink, ChevronDown, ChevronUp, Brain, @@ -697,6 +699,26 @@ export const KanbanCard = memo(function KanbanCard({ )} + {/* PR URL Display */} + {feature.prUrl && ( +
+ e.stopPropagation()} + onPointerDown={(e) => e.stopPropagation()} + className="inline-flex items-center gap-1.5 text-[11px] text-purple-500 hover:text-purple-400 transition-colors" + title={feature.prUrl} + data-testid={`pr-url-${feature.id}`} + > + + Pull Request + + +
+ )} + {/* Steps Preview */} {showSteps && feature.steps && feature.steps.length > 0 && (
diff --git a/apps/app/src/components/views/board-view/dialogs/create-pr-dialog.tsx b/apps/app/src/components/views/board-view/dialogs/create-pr-dialog.tsx index dd5dd344..37f5aaba 100644 --- a/apps/app/src/components/views/board-view/dialogs/create-pr-dialog.tsx +++ b/apps/app/src/components/views/board-view/dialogs/create-pr-dialog.tsx @@ -30,7 +30,7 @@ interface CreatePRDialogProps { open: boolean; onOpenChange: (open: boolean) => void; worktree: WorktreeInfo | null; - onCreated: () => void; + onCreated: (prUrl?: string) => void; } export function CreatePRDialog({ @@ -201,7 +201,8 @@ export function CreatePRDialog({ // Only call onCreated() if an actual operation completed // This prevents unnecessary refreshes when user cancels if (operationCompletedRef.current) { - onCreated(); + // Pass the PR URL if one was created + onCreated(prUrl || undefined); } onOpenChange(false); // State reset is handled by useEffect when open becomes false diff --git a/apps/app/src/store/app-store.ts b/apps/app/src/store/app-store.ts index bec00c75..e7598cb5 100644 --- a/apps/app/src/store/app-store.ts +++ b/apps/app/src/store/app-store.ts @@ -305,6 +305,7 @@ export interface Feature { planningMode?: PlanningMode; // Planning mode for this feature planSpec?: PlanSpec; // Generated spec/plan data requirePlanApproval?: boolean; // Whether to pause and require manual approval before implementation + prUrl?: string; // Pull request URL when a PR has been created for this feature } // Parsed task from spec (for spec and full planning modes) diff --git a/apps/app/tests/worktree-integration.spec.ts b/apps/app/tests/worktree-integration.spec.ts index a8a77c40..89fda282 100644 --- a/apps/app/tests/worktree-integration.spec.ts +++ b/apps/app/tests/worktree-integration.spec.ts @@ -2610,4 +2610,127 @@ test.describe("Worktree Integration Tests", () => { // worktreePath should not exist in the feature data (worktrees are created at execution time) expect(featureData.worktreePath).toBeUndefined(); }); + + // ========================================================================== + // PR URL Tracking Tests + // ========================================================================== + + test("feature should support prUrl field for tracking pull request URLs", async ({ + page, + }) => { + await setupProjectWithPath(page, testRepo.path); + await page.goto("/"); + await waitForNetworkIdle(page); + await waitForBoardView(page); + + // Create a feature + await clickAddFeature(page); + await fillAddFeatureDialog(page, "Feature for PR URL test", { + category: "Testing", + }); + await confirmAddFeature(page); + await page.waitForTimeout(1000); + + // Verify feature was created + const featuresDir = path.join(testRepo.path, ".automaker", "features"); + const featureDirs = fs.readdirSync(featuresDir); + const featureDir = featureDirs.find((dir) => { + const featureFilePath = path.join(featuresDir, dir, "feature.json"); + if (fs.existsSync(featureFilePath)) { + const data = JSON.parse(fs.readFileSync(featureFilePath, "utf-8")); + return data.description === "Feature for PR URL test"; + } + return false; + }); + expect(featureDir).toBeDefined(); + + // Manually update the feature.json file to add prUrl (simulating what happens after PR creation) + const featureFilePath = path.join(featuresDir, featureDir!, "feature.json"); + const featureData = JSON.parse(fs.readFileSync(featureFilePath, "utf-8")); + featureData.prUrl = "https://github.com/test/repo/pull/123"; + fs.writeFileSync(featureFilePath, JSON.stringify(featureData, null, 2)); + + // Reload the page to pick up the change + await page.reload(); + await waitForNetworkIdle(page); + await waitForBoardView(page); + await page.waitForTimeout(1000); + + // Verify the PR URL link is displayed on the card + const prUrlLink = page.locator(`[data-testid="pr-url-${featureData.id}"]`); + await expect(prUrlLink).toBeVisible({ timeout: 5000 }); + await expect(prUrlLink).toHaveText(/Pull Request/); + await expect(prUrlLink).toHaveAttribute( + "href", + "https://github.com/test/repo/pull/123" + ); + }); + + test("prUrl should persist when updating feature", async ({ page }) => { + await setupProjectWithPath(page, testRepo.path); + await page.goto("/"); + await waitForNetworkIdle(page); + await waitForBoardView(page); + + // Create a feature + await clickAddFeature(page); + await fillAddFeatureDialog(page, "Feature with PR URL persistence", { + category: "Testing", + }); + await confirmAddFeature(page); + await page.waitForTimeout(1000); + + // Find the feature file + const featuresDir = path.join(testRepo.path, ".automaker", "features"); + const featureDirs = fs.readdirSync(featuresDir); + const featureDir = featureDirs.find((dir) => { + const featureFilePath = path.join(featuresDir, dir, "feature.json"); + if (fs.existsSync(featureFilePath)) { + const data = JSON.parse(fs.readFileSync(featureFilePath, "utf-8")); + return data.description === "Feature with PR URL persistence"; + } + return false; + }); + expect(featureDir).toBeDefined(); + + // Add prUrl to the feature + const featureFilePath = path.join(featuresDir, featureDir!, "feature.json"); + let featureData = JSON.parse(fs.readFileSync(featureFilePath, "utf-8")); + const originalPrUrl = "https://github.com/test/repo/pull/456"; + featureData.prUrl = originalPrUrl; + fs.writeFileSync(featureFilePath, JSON.stringify(featureData, null, 2)); + + // Reload the page + await page.reload(); + await waitForNetworkIdle(page); + await waitForBoardView(page); + await page.waitForTimeout(1000); + + // Open edit dialog by double-clicking the feature card + const featureCard = page.getByText("Feature with PR URL persistence"); + await featureCard.dblclick(); + await page.waitForTimeout(500); + + // Wait for edit dialog to open + const editDialog = page.locator('[data-testid="edit-feature-dialog"]'); + await expect(editDialog).toBeVisible({ timeout: 5000 }); + + // Update the description + const descInput = page.locator( + '[data-testid="edit-feature-description"] textarea' + ); + await descInput.fill("Feature with PR URL persistence - updated"); + + // Save the feature + const saveButton = page.locator('[data-testid="confirm-edit-feature"]'); + await saveButton.click(); + await page.waitForTimeout(1000); + + // Verify prUrl was preserved + featureData = JSON.parse(fs.readFileSync(featureFilePath, "utf-8")); + expect(featureData.prUrl).toBe(originalPrUrl); + expect(featureData.description).toBe( + "Feature with PR URL persistence - updated" + ); + }); });