From 6a8f5c6d9cded8ece18984908b9ea11e7e960691 Mon Sep 17 00:00:00 2001 From: Cody Seibert Date: Fri, 19 Dec 2025 16:51:43 -0500 Subject: [PATCH] feat: enhance Kanban card functionality with Verify button - Added logic to display a Verify button for features in the "waiting_approval" status with a PR URL, replacing the Commit button. - Updated WorktreePanel and WorktreeTab components to include properties for tracking uncommitted changes and file counts. - Implemented tooltips to indicate the number of uncommitted files in the WorktreeTab. - Added integration tests to verify the correct display of the Verify and Commit buttons based on feature status and PR URL presence. --- .../board-view/components/kanban-card.tsx | 20 ++- .../components/worktree-tab.tsx | 70 +++++++++- .../worktree-panel/worktree-panel.tsx | 4 +- apps/app/tests/worktree-integration.spec.ts | 120 ++++++++++++++++++ 4 files changed, 209 insertions(+), 5 deletions(-) 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 0cb7a366..8602b375 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 @@ -1102,7 +1102,23 @@ export const KanbanCard = memo(function KanbanCard({ Refine )} - {onCommit && ( + {/* Show Verify button if PR was created (changes are committed), otherwise show Commit button */} + {feature.prUrl && onManualVerify ? ( + + ) : onCommit ? ( - )} + ) : null} )} {!isCurrentAutoTask && feature.status === "backlog" && ( diff --git a/apps/app/src/components/views/board-view/worktree-panel/components/worktree-tab.tsx b/apps/app/src/components/views/board-view/worktree-panel/components/worktree-tab.tsx index 4f130db7..0c7adbbe 100644 --- a/apps/app/src/components/views/board-view/worktree-panel/components/worktree-tab.tsx +++ b/apps/app/src/components/views/board-view/worktree-panel/components/worktree-tab.tsx @@ -1,8 +1,14 @@ "use client"; import { Button } from "@/components/ui/button"; -import { RefreshCw, Globe, Loader2 } from "lucide-react"; +import { RefreshCw, Globe, Loader2, CircleDot } from "lucide-react"; import { cn } from "@/lib/utils"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; import type { WorktreeInfo, BranchInfo, DevServerInfo } from "../types"; import { BranchSwitchDropdown } from "./branch-switch-dropdown"; import { WorktreeActionsDropdown } from "./worktree-actions-dropdown"; @@ -10,6 +16,8 @@ import { WorktreeActionsDropdown } from "./worktree-actions-dropdown"; interface WorktreeTabProps { worktree: WorktreeInfo; cardCount?: number; // Number of unarchived cards for this branch + hasChanges?: boolean; // Whether the worktree has uncommitted changes + changedFilesCount?: number; // Number of files with uncommitted changes isSelected: boolean; isRunning: boolean; isActivating: boolean; @@ -46,6 +54,8 @@ interface WorktreeTabProps { export function WorktreeTab({ worktree, cardCount, + hasChanges, + changedFilesCount, isSelected, isRunning, isActivating, @@ -78,8 +88,24 @@ export function WorktreeTab({ onStopDevServer, onOpenDevServerUrl, }: WorktreeTabProps) { + // Determine border color based on state: + // - Running features: cyan border (high visibility, indicates active work) + // - Uncommitted changes: amber border (warning state, needs attention) + // - Both: cyan takes priority (running is more important to see) + const getBorderClasses = () => { + if (isRunning) { + return "ring-2 ring-cyan-500 ring-offset-1 ring-offset-background"; + } + if (hasChanges) { + return "ring-2 ring-amber-500 ring-offset-1 ring-offset-background"; + } + return ""; + }; + + const borderClasses = getBorderClasses(); + return ( -
+
{worktree.isMain ? ( <> )} + {hasChanges && ( + + + + + + {changedFilesCount ?? "!"} + + + +

{changedFilesCount ?? "Some"} uncommitted file{changedFilesCount !== 1 ? "s" : ""}

+
+
+
+ )} )} diff --git a/apps/app/src/components/views/board-view/worktree-panel/worktree-panel.tsx b/apps/app/src/components/views/board-view/worktree-panel/worktree-panel.tsx index 910b8793..e81f996e 100644 --- a/apps/app/src/components/views/board-view/worktree-panel/worktree-panel.tsx +++ b/apps/app/src/components/views/board-view/worktree-panel/worktree-panel.tsx @@ -110,7 +110,7 @@ export function WorktreePanel({ Branch: -
+
{worktrees.map((worktree) => { const cardCount = branchCardCounts?.[worktree.branch]; return ( @@ -118,6 +118,8 @@ export function WorktreePanel({ key={worktree.path} worktree={worktree} cardCount={cardCount} + hasChanges={worktree.hasChanges} + changedFilesCount={worktree.changedFilesCount} isSelected={isWorktreeSelected(worktree)} isRunning={hasRunningFeatures(worktree)} isActivating={isActivating} diff --git a/apps/app/tests/worktree-integration.spec.ts b/apps/app/tests/worktree-integration.spec.ts index 89fda282..03a6d67d 100644 --- a/apps/app/tests/worktree-integration.spec.ts +++ b/apps/app/tests/worktree-integration.spec.ts @@ -2733,4 +2733,124 @@ test.describe("Worktree Integration Tests", () => { "Feature with PR URL persistence - updated" ); }); + + test("feature in waiting_approval with prUrl should show Verify button instead of Commit", 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 for verify test", { + 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 for verify test"; + } + return false; + }); + expect(featureDir).toBeDefined(); + + // Update the feature to waiting_approval status with a prUrl + const featureFilePath = path.join(featuresDir, featureDir!, "feature.json"); + let featureData = JSON.parse(fs.readFileSync(featureFilePath, "utf-8")); + featureData.status = "waiting_approval"; + featureData.prUrl = "https://github.com/test/repo/pull/789"; + fs.writeFileSync(featureFilePath, JSON.stringify(featureData, null, 2)); + + // Reload the page to pick up the changes + await page.reload(); + await waitForNetworkIdle(page); + await waitForBoardView(page); + await page.waitForTimeout(1000); + + // Verify the feature card is in the waiting_approval column + const waitingApprovalColumn = page.locator( + '[data-testid="kanban-column-waiting_approval"]' + ); + const featureCard = waitingApprovalColumn.locator( + `[data-testid="kanban-card-${featureData.id}"]` + ); + await expect(featureCard).toBeVisible({ timeout: 5000 }); + + // Verify the Verify button is visible (not Commit button) + const verifyButton = page.locator(`[data-testid="verify-${featureData.id}"]`); + await expect(verifyButton).toBeVisible({ timeout: 5000 }); + + // Verify the Commit button is NOT visible + const commitButton = page.locator(`[data-testid="commit-${featureData.id}"]`); + await expect(commitButton).not.toBeVisible({ timeout: 2000 }); + }); + + test("feature in waiting_approval without prUrl should show Commit button", 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 without PR for commit test", { + 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 without PR for commit test"; + } + return false; + }); + expect(featureDir).toBeDefined(); + + // Update the feature to waiting_approval status WITHOUT prUrl + const featureFilePath = path.join(featuresDir, featureDir!, "feature.json"); + let featureData = JSON.parse(fs.readFileSync(featureFilePath, "utf-8")); + featureData.status = "waiting_approval"; + // Explicitly do NOT set prUrl + fs.writeFileSync(featureFilePath, JSON.stringify(featureData, null, 2)); + + // Reload the page to pick up the changes + await page.reload(); + await waitForNetworkIdle(page); + await waitForBoardView(page); + await page.waitForTimeout(1000); + + // Verify the feature card is in the waiting_approval column + const waitingApprovalColumn = page.locator( + '[data-testid="kanban-column-waiting_approval"]' + ); + const featureCard = waitingApprovalColumn.locator( + `[data-testid="kanban-card-${featureData.id}"]` + ); + await expect(featureCard).toBeVisible({ timeout: 5000 }); + + // Verify the Commit button is visible + const commitButton = page.locator(`[data-testid="commit-${featureData.id}"]`); + await expect(commitButton).toBeVisible({ timeout: 5000 }); + + // Verify the Verify button is NOT visible + const verifyButton = page.locator(`[data-testid="verify-${featureData.id}"]`); + await expect(verifyButton).not.toBeVisible({ timeout: 2000 }); + }); });