Skip to content

Conversation

@webdevcody
Copy link
Collaborator

@webdevcody webdevcody commented Dec 19, 2025

Changes from branch pull-request

Summary by CodeRabbit

  • New Features

    • Per-branch PR metadata persisted on the server and a new PR-info API to fetch PR details and comments.
    • Create-PR flow now returns PR info and propagates PR URLs to related features and worktrees.
  • UI Improvements

    • PR URLs shown on feature cards with external links and a Verify action.
    • Worktree tabs show PR badges, change indicators with file-count tooltips, and an "Address PR Comments" action.
  • Tests

    • Integration and unit tests added for PR metadata, PR flows, and filesystem edge cases (integration tests duplicated).

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Dec 19, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds PR discovery, creation, and persistence for worktrees; surfaces PR status and actions in the board UI; propagates PR URLs into features; adds server endpoints to fetch PR details/comments; and introduces branch validation, gh CLI detection, and filesystem-backed worktree metadata.

Changes

Cohort / File(s) Change Summary
Server — worktree metadata & routes
apps/server/src/lib/worktree-metadata.ts, apps/server/src/routes/worktree/index.ts, apps/server/src/routes/worktree/routes/pr-info.ts, apps/server/src/routes/worktree/routes/create-pr.ts, apps/server/src/routes/worktree/routes/list.ts, apps/server/src/routes/worktree/common.ts
New filesystem-backed worktree metadata store; new POST /pr-info handler to fetch PR details/comments; create-pr enhanced to detect existing PRs, handle forks, use gh CLI or browser fallback, persist PR info; list attaches stored PR metadata; common utilities added (branch validation, gh CLI detection, execEnv, helpers).
UI — board PR flow, dialog & types
apps/ui/src/components/views/board-view.tsx, apps/ui/src/components/views/board-view/dialogs/create-pr-dialog.tsx, apps/ui/src/store/app-store.ts, apps/ui/src/types/electron.d.ts
Added optional prUrl to Feature; CreatePRDialog accepts projectPath and onCreated(prUrl?: string); board wiring propagates prUrl to features (local state + async persist); Electron typings updated for new API shapes and PR result fields.
UI — worktree panel, tabs & actions
apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx, apps/ui/src/components/views/board-view/worktree-panel/components/worktree-tab.tsx, apps/ui/src/components/views/board-view/worktree-panel/components/worktree-actions-dropdown.tsx, apps/ui/src/components/views/board-view/worktree-panel/types.ts
Added WorktreePRInfo and PRInfo types; worktree entries may include pr; UI shows PR badges, change indicators, and PR actions; new props hasChanges, changedFilesCount, branchCardCounts, refreshTrigger; added onAddressPRComments(worktree, prInfo) callback and dropdown action.
UI — kanban card PR display & verify
apps/ui/src/components/views/board-view/components/kanban-card.tsx
Kanban card renders PR link (with icons) when feature.prUrl exists; shows conditional "Verify" button (and hides Commit) when prUrl present and onManualVerify provided; click propagation suppressed for PR actions.
UI — API plumbing & mocks
apps/ui/src/lib/http-api-client.ts, apps/ui/src/lib/electron.ts
Added worktree.getPRInfo(worktreePath, branchName) client API and mock; createPR accepts optional projectPath and returns extended PR-related fields (prNumber, prAlreadyExisted, etc.).
Tests
apps/ui/tests/worktree-integration.spec.ts, apps/server/tests/unit/lib/worktree-metadata.test.ts, apps/server/tests/unit/lib/fs-utils.test.ts
Added PR URL tracking integration tests for feature cards and Verify/Commit behavior (tests duplicated in file); unit tests for worktree-metadata and fs utils covering many edge cases.

Sequence Diagram(s)

sequenceDiagram
  participant UI as Browser UI (board-view)
  participant Client as HttpApiClient / Electron
  participant Server as Automaker Server
  participant FS as .automaker/worktrees (metadata)

  UI->>Client: POST /api/worktree/create-pr (worktreePath, branch, projectPath?)
  Client->>Server: forward request
  Server->>Server: inspect remotes, determine repo & fork context
  Server->>Server: gh pr list (check existing PR for branch)
  alt PR exists
    Server->>FS: updateWorktreePRInfo(branch, prInfo)
    Server-->>Client: { success, prUrl, prNumber, prAlreadyExisted }
  else create PR
    Server->>Server: gh pr create (or build browser URL)
    Server->>FS: updateWorktreePRInfo(branch, prInfo)
    Server-->>Client: { success, prUrl, prNumber, prCreated }
  end
  Client-->>UI: return pr metadata
  UI->>UI: board-view.onCreated(prUrl) — update features on branch (state + persist)
  UI->>Client: POST /api/worktree/pr-info (worktreePath, branchName)
  Client->>Server: forward request
  Server->>Server: gh queries -> fetch PR, comments, reviewComments
  Server-->>Client: { success, prInfo }
  Client-->>UI: deliver prInfo
  UI->>UI: invoke onAddressPRComments(worktree, prInfo)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Areas needing careful review:
    • apps/server/src/routes/worktree/routes/create-pr.ts — fork-aware head/repo logic, gh CLI command construction, error handling, metadata persistence.
    • apps/server/src/routes/worktree/routes/pr-info.ts — gh CLI/gh API usage, comment aggregation, and input validation.
    • apps/server/src/lib/worktree-metadata.ts — branch sanitization, filesystem IO robustness, concurrent update semantics.
    • UI wiring: apps/ui/src/components/views/board-view.tsx, create-pr-dialog.tsx, and Kanban card changes — ensure prUrl propagation and button behavior are correct.
    • Test duplication in apps/ui/tests/worktree-integration.spec.ts (likely unintentional copy).

Possibly related PRs

Suggested labels

Refactor, Tests

Poem

🐇 I dug a burrow and found a PR bright,
I hopped its link from morning to night,
Badges now shimmer where branches once lay,
Cards learned the URL and point the way,
I'll chase comments, munch bugs, then hop away. 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title 'pull-request' is vague and generic, failing to communicate the actual changes made in this substantial PR that introduces PR tracking, metadata persistence, and UI enhancements. Replace with a specific, descriptive title that summarizes the main change, such as 'Add PR tracking and metadata persistence for worktrees' or 'Implement PR URL tracking across features and worktrees'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch pull-request

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @webdevcody, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request implements a new feature that allows the application to track and display Pull Request (PR) URLs directly within the feature management interface. By extending the "Feature" data model with a "prUrl" field, updating the UI to show a clickable PR link on Kanban cards, and automating the capture and persistence of this URL during PR creation, the changes significantly improve the workflow by providing immediate access to associated PRs from the feature board.

Highlights

  • PR URL Tracking in Features: Introduced a new "prUrl" field in the "Feature" interface to store the URL of an associated pull request.
  • UI Display for PRs: Kanban cards now visually display a clickable link to the associated pull request, enhancing visibility and navigation.
  • Automated PR URL Assignment: Upon successful creation of a pull request, the system automatically updates the "prUrl" for all features linked to the worktree's branch.
  • Enhanced PR Creation Dialog: The "CreatePRDialog" now passes the newly generated PR URL back to its caller, enabling downstream updates.
  • Comprehensive Testing: New integration tests have been added to ensure the "prUrl" field is correctly supported, displayed, and persists across feature updates.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a valuable feature for tracking pull request URLs associated with features. The implementation is well-structured, touching the data store, UI components, and adding integration tests to ensure correctness. The changes are logical and cover the full lifecycle of the new prUrl property, from creation to display. I've included a few suggestions to enhance robustness and user experience. Specifically, I've pointed out an opportunity to improve asynchronous handling when updating features, a way to make the new pull request link on the Kanban card more informative, and a best practice for writing more reliable tests by avoiding fixed timeouts.

Comment on lines 1161 to 1166
hookFeatures
.filter((f) => f.branchName === branchName)
.forEach((feature) => {
updateFeature(feature.id, { prUrl });
persistFeatureUpdate(feature.id, { prUrl });
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The use of forEach with an async function (persistFeatureUpdate) can lead to unhandled promises and potential race conditions, as forEach does not wait for promises to resolve. It's better to separate the synchronous state updates from the asynchronous persistence operations. You can update the local state for all features first, and then use Promise.all to handle all persistence calls concurrently. This ensures that all persistence operations are tracked and any potential errors are caught.

            const featuresToUpdate = hookFeatures.filter((f) => f.branchName === branchName);

            // Update local state synchronously
            featuresToUpdate.forEach((feature) => {
              updateFeature(feature.id, { prUrl });
            });

            // Persist changes asynchronously and in parallel
            Promise.all(
              featuresToUpdate.map((feature) =>
                persistFeatureUpdate(feature.id, { prUrl })
              )
            ).catch(console.error);

Comment on lines 703 to 719
{feature.prUrl && (
<div className="mb-2">
<a
href={feature.prUrl}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => 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}`}
>
<GitPullRequest className="w-3 h-3 shrink-0" />
<span className="truncate max-w-[150px]">Pull Request</span>
<ExternalLink className="w-2.5 h-2.5 shrink-0" />
</a>
</div>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Displaying the pull request number would provide more context than the generic "Pull Request" text. You can extract the PR number from the prUrl and display it. Using an IIFE (Immediately Invoked Function Expression) allows you to declare a variable for the PR number cleanly within the JSX.

        {feature.prUrl && (() => {
          const prNumber = feature.prUrl.split('/').pop();
          return (
            <div className="mb-2">
              <a
                href={feature.prUrl}
                target="_blank"
                rel="noopener noreferrer"
                onClick={(e) => 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}`}
              >
                <GitPullRequest className="w-3 h-3 shrink-0" />
                <span className="truncate max-w-[150px]">
                  {prNumber ? `Pull Request #${prNumber}` : 'Pull Request'}
                </span>
                <ExternalLink className="w-2.5 h-2.5 shrink-0" />
              </a>
            </div>
          );
        })()}

category: "Testing",
});
await confirmAddFeature(page);
await page.waitForTimeout(1000);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using page.waitForTimeout() can lead to flaky tests. It's better to wait for a specific condition to be met. In this case, you could wait for the feature file to be created on the filesystem, or for the corresponding UI element to appear. Playwright's waitForFunction or expect().toPass() are great for these kinds of assertions. This comment applies to other uses of waitForTimeout in this test file as well.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
apps/app/src/components/views/board-view.tsx (1)

1153-1170: Branch-level PR URL propagation looks correct

Using onCreated(prUrl) to update and persist prUrl on all features whose branchName matches the selected worktree’s branch is consistent with the “one PR per branch” model, and the guards around prUrl and selectedWorktreeForAction?.branch avoid null/undefined issues. If this ever needs to scale to many features per branch, you might consider a small helper or batching to avoid repeated persistFeatureUpdate calls, but it’s fine as-is.

apps/app/tests/worktree-integration.spec.ts (1)

2613-2735: PR URL integration tests are well targeted and consistent with existing patterns

Both tests correctly exercise the new prUrl field end-to-end (JSON storage → UI render → persistence across edits) using the same filesystem + UI flow as the surrounding worktree tests. The additional waitForTimeout calls mirror existing tests and should be acceptable here; if these ever get flaky, you could tighten them with file-based polling or more specific UI waits.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 46c3dd2 and b8afb6c.

📒 Files selected for processing (5)
  • apps/app/src/components/views/board-view.tsx (1 hunks)
  • apps/app/src/components/views/board-view/components/kanban-card.tsx (2 hunks)
  • apps/app/src/components/views/board-view/dialogs/create-pr-dialog.tsx (2 hunks)
  • apps/app/src/store/app-store.ts (1 hunks)
  • apps/app/tests/worktree-integration.spec.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/app/tests/worktree-integration.spec.ts (3)
apps/app/tests/utils/git/worktree.ts (2)
  • setupProjectWithPath (309-353)
  • waitForBoardView (462-464)
apps/app/tests/utils/core/waiting.ts (1)
  • waitForNetworkIdle (7-9)
apps/app/tests/utils/views/board.ts (3)
  • clickAddFeature (121-126)
  • fillAddFeatureDialog (131-180)
  • confirmAddFeature (185-192)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: e2e
  • GitHub Check: build
🔇 Additional comments (2)
apps/app/src/store/app-store.ts (1)

281-309: Feature model extension with prUrl is safe and consistent

Adding prUrl?: string to Feature is an additive, backward-compatible change; it fits existing updateFeature/persistFeatureUpdate flows and will be naturally persisted via the existing features slice without requiring a migration bump.

apps/app/src/components/views/board-view/dialogs/create-pr-dialog.tsx (1)

29-41: onCreated(prUrl?) wiring correctly encodes PR vs non-PR outcomes

The updated onCreated: (prUrl?: string) => void contract plus the handleClose logic cleanly distinguish cases:

  • Successful PR → onCreated(prUrl) fired on close.
  • Branch push with browser fallback or gh error → onCreated(undefined) on close, so callers can refresh without assigning a PR URL.
  • Plain push/no PR & no browser URL → onCreated() from handleCreate, dialog closes immediately and handleClose doesn’t fire a second time.

This matches the BoardView usage and avoids double-notification.

Also applies to: 200-209

- 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.
- 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.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

♻️ Duplicate comments (1)
apps/ui/src/components/views/board-view.tsx (1)

1254-1267: Duplicate: Unhandled promises in feature update loop.

This issue has already been identified in the past review comments. The forEach with async persistFeatureUpdate calls can lead to unhandled promise rejections and race conditions.

As noted in the previous review, use Promise.all to properly handle the async persistence operations.

🧹 Nitpick comments (4)
apps/ui/src/components/views/board-view/worktree-panel/types.ts (1)

1-7: Consider unifying common PR fields and removing redundant isReviewComment flag.

  1. WorktreePRInfo and PRInfo share number, url, title, state fields. Consider extracting a base type or having PRInfo extend WorktreePRInfo.

  2. The isReviewComment field in both comments and reviewComments arrays is redundant since the array membership already distinguishes them.

🔎 Optional refactor to reduce duplication
+interface BasePRComment {
+  id: number;
+  author: string;
+  body: string;
+  createdAt: string;
+}
+
+interface PRIssueComment extends BasePRComment {}
+
+interface PRReviewComment extends BasePRComment {
+  path?: string;
+  line?: number;
+}
+
 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;
-  }>;
+  comments: PRIssueComment[];
+  reviewComments: PRReviewComment[];
 }

Also applies to: 37-60

apps/server/src/routes/worktree/routes/pr-info.ts (1)

12-43: Extract PATH extension logic to a shared utility.

This PATH extension pattern for locating gh and git binaries is duplicated in both pr-info.ts and create-pr.ts. Moving this ~30-line block to a shared utility module would improve maintainability and reduce duplication.

apps/ui/src/components/views/board-view/worktree-panel/components/worktree-actions-dropdown.tsx (2)

185-217: PR state badge color is hardcoded to green regardless of actual state.

The dropdown label at line 191 always uses bg-green-500/20 text-green-600 for the PR state badge, but PRs can be in different states (open, closed, merged, draft). Consider varying the color based on the state for visual consistency with the PR badge in worktree-tab.tsx.

🔎 Proposed enhancement for state-based coloring
+            const stateClasses = (() => {
+              const state = worktree.pr.state?.toLowerCase();
+              switch (state) {
+                case "open":
+                case "reopened":
+                  return "bg-green-500/20 text-green-600";
+                case "draft":
+                  return "bg-amber-500/20 text-amber-600";
+                case "merged":
+                  return "bg-purple-500/20 text-purple-600";
+                case "closed":
+                  return "bg-rose-500/20 text-rose-600";
+                default:
+                  return "bg-muted text-muted-foreground";
+              }
+            })();
             <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">
+              <span className={cn("ml-auto text-[10px] px-1.5 py-0.5 rounded", stateClasses)}>
                 {worktree.pr.state}
               </span>
             </DropdownMenuLabel>

196-209: Consider documenting the partial PRInfo contract more explicitly.

The code correctly constructs a partial PRInfo object with empty fields that will be fetched later. The inline comment explains this, but since this creates a contract between this component and the handler, consider adding a brief JSDoc or ensuring the handler gracefully handles these empty fields.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9bfcb91 and 6c25680.

📒 Files selected for processing (14)
  • apps/server/src/lib/worktree-metadata.ts (1 hunks)
  • apps/server/src/routes/worktree/index.ts (2 hunks)
  • apps/server/src/routes/worktree/routes/create-pr.ts (9 hunks)
  • apps/server/src/routes/worktree/routes/list.ts (4 hunks)
  • apps/server/src/routes/worktree/routes/pr-info.ts (1 hunks)
  • apps/ui/src/components/views/board-view.tsx (6 hunks)
  • apps/ui/src/components/views/board-view/dialogs/create-pr-dialog.tsx (4 hunks)
  • apps/ui/src/components/views/board-view/worktree-panel/components/worktree-actions-dropdown.tsx (4 hunks)
  • apps/ui/src/components/views/board-view/worktree-panel/components/worktree-tab.tsx (7 hunks)
  • apps/ui/src/components/views/board-view/worktree-panel/types.ts (3 hunks)
  • apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx (3 hunks)
  • apps/ui/src/lib/electron.ts (1 hunks)
  • apps/ui/src/lib/http-api-client.ts (1 hunks)
  • apps/ui/src/types/electron.d.ts (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx
🧰 Additional context used
🧬 Code graph analysis (7)
apps/server/src/routes/worktree/index.ts (1)
apps/server/src/routes/worktree/routes/pr-info.ts (1)
  • createPRInfoHandler (66-296)
apps/server/src/routes/worktree/routes/pr-info.ts (2)
apps/ui/src/components/views/board-view/worktree-panel/types.ts (1)
  • PRInfo (37-60)
apps/server/src/routes/common.ts (1)
  • getErrorMessage (370-372)
apps/server/src/routes/worktree/routes/list.ts (2)
apps/server/src/lib/worktree-metadata.ts (2)
  • WorktreePRInfo (9-15)
  • readAllWorktreeMetadata (113-139)
apps/ui/src/components/views/board-view/worktree-panel/types.ts (1)
  • WorktreePRInfo (1-7)
apps/server/src/routes/worktree/routes/create-pr.ts (1)
apps/server/src/lib/worktree-metadata.ts (1)
  • updateWorktreePRInfo (77-97)
apps/ui/src/components/views/board-view/worktree-panel/components/worktree-actions-dropdown.tsx (2)
apps/ui/src/components/views/board-view/worktree-panel/types.ts (2)
  • WorktreeInfo (9-18)
  • PRInfo (37-60)
apps/ui/src/types/electron.d.ts (1)
  • WorktreeInfo (578-583)
apps/ui/src/components/views/board-view.tsx (4)
apps/ui/src/components/views/board-view/worktree-panel/types.ts (2)
  • WorktreeInfo (9-18)
  • PRInfo (37-60)
apps/ui/src/lib/electron.ts (1)
  • WorktreeInfo (52-52)
apps/ui/src/types/electron.d.ts (1)
  • WorktreeInfo (578-583)
apps/server/src/routes/worktree/routes/pr-info.ts (1)
  • PRInfo (55-64)
apps/server/src/lib/worktree-metadata.ts (1)
apps/ui/src/components/views/board-view/worktree-panel/types.ts (1)
  • WorktreePRInfo (1-7)
🪛 Biome (2.1.2)
apps/ui/src/components/views/board-view/dialogs/create-pr-dialog.tsx

[error] 33-35: Expected a statement but instead found '=======
projectPath: string | null'.

Expected a statement here.

(parse)


[error] 36-38: Expected a statement but instead found '>>>>>>> Stashed changes
}'.

Expected a statement here.

(parse)

apps/ui/src/components/views/board-view/worktree-panel/components/worktree-tab.tsx

[error] 12-13: Expected a statement but instead found '======='.

Expected a statement here.

(parse)


[error] 16-17: Expected a statement but instead found '>>>>>>> Stashed changes'.

Expected a statement here.

(parse)


[error] 14-14: Shouldn't redeclare 'RefreshCw'. Consider to delete it or rename it.

'RefreshCw' is defined here:

(lint/suspicious/noRedeclare)


[error] 14-14: Shouldn't redeclare 'Globe'. Consider to delete it or rename it.

'Globe' is defined here:

(lint/suspicious/noRedeclare)


[error] 14-14: Shouldn't redeclare 'Loader2'. Consider to delete it or rename it.

'Loader2' is defined here:

(lint/suspicious/noRedeclare)


[error] 15-15: Shouldn't redeclare 'cn'. Consider to delete it or rename it.

'cn' is defined here:

(lint/suspicious/noRedeclare)


[error] 16-16: Shouldn't redeclare 'WorktreeInfo'. Consider to delete it or rename it.

'WorktreeInfo' is defined here:

(lint/suspicious/noRedeclare)


[error] 16-16: Shouldn't redeclare 'BranchInfo'. Consider to delete it or rename it.

'BranchInfo' is defined here:

(lint/suspicious/noRedeclare)


[error] 16-16: Shouldn't redeclare 'DevServerInfo'. Consider to delete it or rename it.

'DevServerInfo' is defined here:

(lint/suspicious/noRedeclare)


[error] 97-98: Expected a statement but instead found '<<<<<<< Updated upstream'.

Expected a statement here.

(parse)


[error] 113-114: Expected a statement but instead found '======='.

Expected a statement here.

(parse)


[error] 199-200: Expected a statement but instead found '>>>>>>> Stashed changes'.

Expected a statement here.

(parse)


[error] 229-229: expected ... but instead found hasChanges

Remove hasChanges

(parse)


[error] 248-249: Expected a JSX attribute but instead found '======='.

Expected a JSX attribute here.

(parse)


[error] 250-250: expected ... but instead found prBadge

Remove prBadge

(parse)


[error] 228-251: Expected corresponding JSX closing tag for 'Updated'.

Opening tag

closing tag

(parse)


[error] 251-251: Unexpected token. Did you mean {'>'} or &gt;?

(parse)


[error] 251-251: Unexpected token. Did you mean {'>'} or &gt;?

(parse)


[error] 251-251: Unexpected token. Did you mean {'>'} or &gt;?

(parse)


[error] 251-251: Unexpected token. Did you mean {'>'} or &gt;?

(parse)


[error] 251-251: Unexpected token. Did you mean {'>'} or &gt;?

(parse)


[error] 251-251: Unexpected token. Did you mean {'>'} or &gt;?

(parse)


[error] 296-296: expected ... but instead found hasChanges

Remove hasChanges

(parse)


[error] 315-316: Expected a JSX attribute but instead found '======='.

Expected a JSX attribute here.

(parse)


[error] 317-317: expected ... but instead found prBadge

Remove prBadge

(parse)


[error] 295-318: Expected corresponding JSX closing tag for 'Updated'.

Opening tag

closing tag

(parse)


[error] 318-318: Unexpected token. Did you mean {'>'} or &gt;?

(parse)


[error] 318-318: Unexpected token. Did you mean {'>'} or &gt;?

(parse)


[error] 318-318: Unexpected token. Did you mean {'>'} or &gt;?

(parse)


[error] 318-318: Unexpected token. Did you mean {'>'} or &gt;?

(parse)


[error] 318-318: Unexpected token. Did you mean {'>'} or &gt;?

(parse)


[error] 318-318: Unexpected token. Did you mean {'>'} or &gt;?

(parse)

🪛 GitHub Actions: PR Build Check
apps/ui/src/components/views/board-view/dialogs/create-pr-dialog.tsx

[error] 32-32: Build failed during Vite/esbuild transform. Unexpected '<<<<<< Updated upstream' merge conflict marker found in the source file.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: e2e
🔇 Additional comments (20)
apps/ui/src/components/views/board-view/worktree-panel/types.ts (1)

69-69: LGTM!

The onAddressPRComments callback is well-typed with appropriate parameters for handling PR comment addressing workflow.

apps/server/src/routes/worktree/routes/pr-info.ts (1)

66-104: LGTM!

The handler structure is well-organized with proper input validation, gh CLI availability check, and graceful fallback when the CLI is unavailable.

apps/server/src/routes/worktree/index.ts (1)

15-15: LGTM!

The new PR info route is properly imported and registered following the established patterns in this router.

Also applies to: 44-44

apps/ui/src/lib/http-api-client.ts (1)

675-676: LGTM!

The getPRInfo method is correctly implemented following the established pattern for worktree API methods.

apps/ui/src/lib/electron.ts (1)

1356-1366: LGTM!

The mock implementation correctly simulates the unavailable gh CLI scenario and follows the established pattern for mock worktree API methods.

apps/server/src/routes/worktree/routes/list.ts (2)

13-13: LGTM!

The import and type extension are well-aligned with the WorktreePRInfo interface defined in worktree-metadata.ts. The optional pr field correctly extends the existing WorktreeInfo interface.

Also applies to: 25-25


111-141: LGTM!

The implementation correctly:

  1. Reads all worktree metadata once after the worktree list is built
  2. Attaches PR info to each worktree by matching on branch name
  3. Handles the case where metadata or PR info doesn't exist gracefully via optional chaining

The metadata reading is appropriately placed after pruning removed worktrees, ensuring we only attach PR info to valid worktrees.

apps/ui/src/components/views/board-view/dialogs/create-pr-dialog.tsx (2)

118-136: Good enhancement for differentiating PR creation vs. discovery.

The toast messaging logic correctly distinguishes between finding an existing PR (prAlreadyExisted) and creating a new one, providing appropriate user feedback in each case.


218-227: LGTM!

The handleClose function correctly passes the PR URL to onCreated when an operation completed, enabling the parent component to update feature records with the PR URL as described in the PR objectives.

apps/ui/src/components/views/board-view/worktree-panel/components/worktree-actions-dropdown.tsx (1)

22-25: LGTM!

The new imports, prop additions, and hasPR helper are well-structured. The prop type correctly uses the PRInfo interface from the types file.

Also applies to: 44-44, 68-68, 74-76

apps/server/src/routes/worktree/routes/create-pr.ts (5)

9-9: LGTM!

The import addition and request body extension are well-structured. The effectiveProjectPath logic correctly handles the case where projectPath may not be provided, falling back to worktreePath.

Also applies to: 52-54, 70-72


219-228: LGTM!

The cross-platform gh CLI availability check correctly uses where on Windows and command -v on Unix-like systems.


247-289: Good implementation for PR discovery before creation.

The logic correctly:

  1. Constructs fork-aware head references for the gh pr list command
  2. Parses the JSON output to detect existing PRs
  3. Stores metadata for discovered PRs
  4. Gracefully handles failures without blocking PR creation

The logging statements will help with debugging fork-related issues.


274-282: LGTM! Consistent metadata storage across all PR paths.

The PR metadata is correctly stored in all three scenarios:

  1. When an existing PR is discovered via gh pr list
  2. When a new PR is created successfully
  3. When PR creation fails with "already exists" error and we fetch via gh pr view

This ensures PR info is always persisted for UI consumption.

Also applies to: 324-330, 357-363


388-390: LGTM!

The response payload correctly includes the new prNumber and prAlreadyExisted fields, enabling the UI to differentiate between newly created and discovered PRs.

apps/ui/src/components/views/board-view.tsx (2)

41-41: LGTM! Type imports properly support PR addressing workflow.

The imported types enable the new PR addressing feature and are correctly sourced from the worktree-panel types module.


973-973: LGTM! PR addressing callback properly wired.

The onAddressPRComments handler is correctly passed to the WorktreePanel component.

apps/ui/src/types/electron.d.ts (1)

670-676: LGTM! Type definitions properly extend the Electron API for PR support.

The type additions are well-structured and consistent with existing API patterns:

  • PR metadata in listAll entries provides UI visibility
  • getPRInfo method signature aligns with the nested result pattern used throughout the API
  • Extended createPR options and result support the full PR workflow

Also applies to: 747-747, 762-764, 908-944

apps/server/src/lib/worktree-metadata.ts (2)

9-21: LGTM! Well-structured type definitions for worktree metadata.

The interfaces clearly separate PR-specific info from general worktree metadata, with appropriate optional fields.


42-154: LGTM! Robust error handling for metadata operations.

The implementation correctly:

  • Returns null for missing or unreadable metadata (appropriate for optional data)
  • Ensures directories exist before writing
  • Silently skips invalid entries when reading all metadata (good for robustness)
  • Uses force: true for safe deletion

The silent error handling pattern is appropriate for metadata operations that should degrade gracefully.

Comment on lines 229 to 237
try {
const reviewsEndpoint = targetRepo
? `repos/${targetRepo}/pulls/${prNumber}/comments`
: `repos/{owner}/{repo}/pulls/${prNumber}/comments`;
const reviewsCmd = `gh api ${reviewsEndpoint}`;
const { stdout: reviewsOutput } = await execAsync(
reviewsCmd,
{ cwd: worktreePath, env: execEnv }
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Placeholder {owner}/{repo} will not be interpolated by gh api.

When targetRepo is null, the endpoint becomes repos/{owner}/{repo}/pulls/${prNumber}/comments with literal {owner} and {repo} strings. The gh api command doesn't automatically replace these placeholders in this context - it only does so for certain documented patterns.

🔎 Proposed fix: Handle the case when targetRepo is unavailable
         // Get review comments (inline code comments)
         let reviewComments: PRComment[] = [];
         try {
-          const reviewsEndpoint = targetRepo
-            ? `repos/${targetRepo}/pulls/${prNumber}/comments`
-            : `repos/{owner}/{repo}/pulls/${prNumber}/comments`;
+          if (!targetRepo) {
+            console.warn("[PRInfo] Cannot fetch review comments: repository info not available");
+          } else {
+            const reviewsEndpoint = `repos/${targetRepo}/pulls/${prNumber}/comments`;
-          const reviewsCmd = `gh api ${reviewsEndpoint}`;
-          const { stdout: reviewsOutput } = await execAsync(
-            reviewsCmd,
-            { cwd: worktreePath, env: execEnv }
-          );
+            const reviewsCmd = `gh api ${reviewsEndpoint}`;
+            const { stdout: reviewsOutput } = await execAsync(
+              reviewsCmd,
+              { cwd: worktreePath, env: execEnv }
+            );
-          const reviewsData = JSON.parse(reviewsOutput);
+            const reviewsData = JSON.parse(reviewsOutput);
-          reviewComments = reviewsData.map((c: {
+            reviewComments = reviewsData.map((c: {
             // ... rest of mapping
-          }));
+            }));
+          }
         } catch (error) {
           console.warn("[PRInfo] Failed to fetch review comments:", error);
         }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/server/src/routes/worktree/routes/pr-info.ts around lines 229-237, the
code currently builds reviewsEndpoint using a literal placeholder when
targetRepo is null which results in `repos/{owner}/{repo}/...` being passed to
`gh api` unchanged; instead detect the missing targetRepo and resolve the actual
owner/repo before building the endpoint (for example, read the repo from the
worktree git remote or call `gh repo view` to get owner and name), then
construct `repos/${owner}/${repo}/pulls/${prNumber}/comments` with those values;
ensure you handle errors when resolving the repo and only call `gh api` with a
fully-qualified endpoint.

Comment on lines 421 to 512
// 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]
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Race condition and error handling issues in PR comment addressing flow.

The implementation has several reliability concerns:

  1. Race condition (Lines 497-509): The setTimeout with a fixed 500ms delay assumes the feature will be created within that timeframe. If handleAddFeature or state updates take longer, the newly created feature won't be found and won't auto-start.

  2. Direct store access (Line 498): Using useAppStore.getState().features bypasses the React hook and could reference stale data instead of using hookFeatures.

  3. Fragile feature matching (Lines 499-504): The search criteria (branch name + backlog status + PR number in description) could match the wrong feature if multiple similar features exist.

  4. No error handling: The entire setTimeout block lacks try-catch, and the async handleStartImplementation call is fire-and-forget.

🔎 Suggested refactor using a more reliable approach

Consider refactoring to use a callback or Promise-based approach instead of setTimeout:

- 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);
+ // Add feature and get the result
+ const newFeature = await handleAddFeature(featureData);
+ 
+ // Start implementation immediately with the newly created feature
+ if (newFeature) {
+   try {
+     await handleStartImplementation(newFeature);
+   } catch (error) {
+     console.error("[Board] Failed to start PR addressing feature:", error);
+   }
+ }

This requires updating handleAddFeature to return the created feature, which would eliminate the race condition entirely.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/ui/src/components/views/board-view.tsx around lines 421-512, the
post-create logic uses a fragile setTimeout + direct store access and weak
matching to find the new feature, leading to race conditions and no error
handling. Modify handleAddFeature to return the created feature (or its id),
then await that Promise here instead of using setTimeout; use the returned
feature object (or id) to uniquely identify the created feature (avoid searching
by description/branch); call handleStartImplementation(awaitedFeature) and wrap
the start flow in try/catch to handle errors; remove direct use of
useAppStore.getState().features in this handler and update any callers of
handleAddFeature to handle the new return value.

Comment on lines 3 to 17
<<<<<<< Updated upstream
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 { RefreshCw, Globe, Loader2, GitPullRequest } from "lucide-react";
import { cn } from "@/lib/utils";
import type { WorktreeInfo, BranchInfo, DevServerInfo, PRInfo } from "../types";
>>>>>>> Stashed changes
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Unresolved merge conflict in imports section.

This file contains multiple unresolved git merge conflict markers that prevent compilation. The conflicts span imports, component logic, and JSX rendering.

🔎 Proposed fix for imports section
-<<<<<<< Updated upstream
-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 { RefreshCw, Globe, Loader2, GitPullRequest } from "lucide-react";
-import { cn } from "@/lib/utils";
-import type { WorktreeInfo, BranchInfo, DevServerInfo, PRInfo } from "../types";
->>>>>>> Stashed changes
+import { RefreshCw, Globe, Loader2, CircleDot, GitPullRequest } from "lucide-react";
+import { cn } from "@/lib/utils";
+import {
+  Tooltip,
+  TooltipContent,
+  TooltipProvider,
+  TooltipTrigger,
+} from "@/components/ui/tooltip";
+import type { WorktreeInfo, BranchInfo, DevServerInfo, PRInfo } from "../types";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<<<<<<< Updated upstream
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 { RefreshCw, Globe, Loader2, GitPullRequest } from "lucide-react";
import { cn } from "@/lib/utils";
import type { WorktreeInfo, BranchInfo, DevServerInfo, PRInfo } from "../types";
>>>>>>> Stashed changes
import { RefreshCw, Globe, Loader2, CircleDot, GitPullRequest } from "lucide-react";
import { cn } from "@/lib/utils";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import type { WorktreeInfo, BranchInfo, DevServerInfo, PRInfo } from "../types";
🧰 Tools
🪛 Biome (2.1.2)

[error] 12-13: Expected a statement but instead found '======='.

Expected a statement here.

(parse)


[error] 16-17: Expected a statement but instead found '>>>>>>> Stashed changes'.

Expected a statement here.

(parse)


[error] 14-14: Shouldn't redeclare 'RefreshCw'. Consider to delete it or rename it.

'RefreshCw' is defined here:

(lint/suspicious/noRedeclare)


[error] 14-14: Shouldn't redeclare 'Globe'. Consider to delete it or rename it.

'Globe' is defined here:

(lint/suspicious/noRedeclare)


[error] 14-14: Shouldn't redeclare 'Loader2'. Consider to delete it or rename it.

'Loader2' is defined here:

(lint/suspicious/noRedeclare)


[error] 15-15: Shouldn't redeclare 'cn'. Consider to delete it or rename it.

'cn' is defined here:

(lint/suspicious/noRedeclare)


[error] 16-16: Shouldn't redeclare 'WorktreeInfo'. Consider to delete it or rename it.

'WorktreeInfo' is defined here:

(lint/suspicious/noRedeclare)


[error] 16-16: Shouldn't redeclare 'BranchInfo'. Consider to delete it or rename it.

'BranchInfo' is defined here:

(lint/suspicious/noRedeclare)


[error] 16-16: Shouldn't redeclare 'DevServerInfo'. Consider to delete it or rename it.

'DevServerInfo' is defined here:

(lint/suspicious/noRedeclare)

🤖 Prompt for AI Agents
In
apps/ui/src/components/views/board-view/worktree-panel/components/worktree-tab.tsx
around lines 3 to 17, there are unresolved git conflict markers (<<<<<<<,
=======, >>>>>>>) in the imports; remove those markers and reconcile the two
sides by merging needed imports into a single import block: include RefreshCw,
Globe, Loader2 and any icon actually used (CircleDot and/or GitPullRequest) from
"lucide-react", preserve cn from "@/lib/utils", restore
Tooltip/TooltipContent/TooltipProvider/TooltipTrigger imports if the component
uses them, and merge types so WorktreeInfo, BranchInfo, DevServerInfo and PRInfo
(if referenced) are imported from "../types"; ensure no duplicate or unused
imports remain and run TypeScript to verify compilation.

Comment on lines 228 to 251
<<<<<<< Updated upstream
{hasChanges && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span className={cn(
"inline-flex items-center justify-center h-4 min-w-[1rem] px-1 text-[10px] font-medium rounded border",
isSelected
? "bg-amber-500 text-amber-950 border-amber-400"
: "bg-amber-500/20 text-amber-600 dark:text-amber-400 border-amber-500/30"
)}>
<CircleDot className="w-2.5 h-2.5 mr-0.5" />
{changedFilesCount ?? "!"}
</span>
</TooltipTrigger>
<TooltipContent>
<p>{changedFilesCount ?? "Some"} uncommitted file{changedFilesCount !== 1 ? "s" : ""}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
=======
{prBadge}
>>>>>>> Stashed changes
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Unresolved merge conflicts in JSX rendering blocks.

The JSX sections have conflicts between rendering the hasChanges badge with tooltips vs. rendering the prBadge. The resolution should include both badges, as they serve different purposes (uncommitted changes indicator vs. PR status).

Also applies to: 295-318

🧰 Tools
🪛 Biome (2.1.2)

[error] 229-229: expected ... but instead found hasChanges

Remove hasChanges

(parse)


[error] 248-249: Expected a JSX attribute but instead found '======='.

Expected a JSX attribute here.

(parse)


[error] 250-250: expected ... but instead found prBadge

Remove prBadge

(parse)


[error] 228-251: Expected corresponding JSX closing tag for 'Updated'.

Opening tag

closing tag

(parse)


[error] 251-251: Unexpected token. Did you mean {'>'} or &gt;?

(parse)


[error] 251-251: Unexpected token. Did you mean {'>'} or &gt;?

(parse)


[error] 251-251: Unexpected token. Did you mean {'>'} or &gt;?

(parse)


[error] 251-251: Unexpected token. Did you mean {'>'} or &gt;?

(parse)


[error] 251-251: Unexpected token. Did you mean {'>'} or &gt;?

(parse)


[error] 251-251: Unexpected token. Did you mean {'>'} or &gt;?

(parse)

🤖 Prompt for AI Agents
In
apps/ui/src/components/views/board-view/worktree-panel/components/worktree-tab.tsx
around lines 228-251 (and similarly 295-318), there is an unresolved merge
conflict that currently chooses either the uncommitted-changes badge block
(hasChanges Tooltip) or the prBadge; both must be rendered. Remove the conflict
markers and combine the UI so both elements are shown (e.g., render the
hasChanges Tooltip block and also render prBadge adjacent to it), preserving
existing props/conditional rendering (only show hasChanges when true and prBadge
when present) and keeping styles/TooltipProvider/TooltipContent intact.

webdevcody and others added 2 commits December 19, 2025 20:39
Security fixes:
- Enhanced branch name sanitization for cross-platform filesystem safety
  (handles Windows-invalid chars, reserved names, path length limits)
- Added branch name validation in pr-info.ts to prevent command injection
- Sanitized prUrl in kanban-card to only allow http/https URLs

Code quality improvements:
- Fixed placeholder issue where {owner}/{repo} was passed literally to gh api
- Replaced async forEach with Promise.all for proper async handling
- Display PR number extracted from URL in kanban cards

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Updated WorktreeActionsDropdown to use DropdownMenuItem for better interaction with PR links.
- Enhanced WorktreeTab to include hover and active states for buttons, and improved accessibility with updated titles and aria-labels.
- Ensured PR URLs are safely opened only if they exist, enhancing user experience and preventing errors.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
apps/ui/src/components/views/board-view.tsx (1)

421-464: Race condition persists in PR comment addressing flow.

This issue was flagged in a previous review and remains unaddressed. The setTimeout with a fixed 500ms delay is fragile:

  1. Race condition: If handleAddFeature takes longer than 500ms, the feature won't be found.
  2. Direct store access: useAppStore.getState().features bypasses React's state management.
  3. Fragile matching: Searching by branchName + status + description.includes() could match the wrong feature.

Consider refactoring handleAddFeature to return the created feature, eliminating the need for the timeout entirely.

apps/server/src/routes/worktree/routes/create-pr.ts (1)

11-20: shellEscape function defined but not used.

The shellEscape function is defined but never used in this file. The PR title, body, and commit message are still using simple .replace(/"/g, '\\"') escaping at lines 124 and 333, which doesn't protect against command substitution via $() or backticks.

This is the same command injection vulnerability flagged in a previous review. The shellEscape function should be applied to user-provided inputs:

🔎 Proposed fix
-        await execAsync(`git commit -m "${message.replace(/"/g, '\\"')}"`, {
+        await execAsync(`git commit -m ${shellEscape(message)}`, {
           cwd: worktreePath,
           env: execEnv,
         });

And for PR creation:

-            prCmd += ` --title "${title.replace(/"/g, '\\"')}" --body "${body.replace(/"/g, '\\"')}" ${draftFlag}`;
+            prCmd += ` --title ${shellEscape(title)} --body ${shellEscape(body)} ${draftFlag}`;
🧹 Nitpick comments (9)
apps/ui/src/components/views/board-view/worktree-panel/components/worktree-actions-dropdown.tsx (1)

200-214: Consider documenting the partial PRInfo contract.

The PRInfo object is constructed with placeholder values (author: "", body: "", empty arrays). While this works because handleAddressPRComments only uses prInfo.number to build the feature description, this implicit contract could be fragile if the handler's implementation changes.

A brief inline comment explaining that only number is used would improve maintainability.

🔎 Suggested documentation
             onClick={() => {
-                // Convert stored PR info to the full PRInfo format for the handler
-                // The handler will fetch full comments from GitHub
+                // Only prInfo.number is used by the handler to reference the PR.
+                // Other fields are placeholders - the agent fetches full PR details directly.
                 const prInfo: PRInfo = {
                   number: worktree.pr!.number,
apps/ui/src/components/views/board-view/worktree-panel/components/worktree-tab.tsx (1)

224-244: Consider extracting duplicate change indicator badge.

The uncommitted changes badge (Tooltip with CircleDot icon) is duplicated between main and non-main tab branches. Extracting this into a small helper component or variable would reduce duplication.

🔎 Example extraction
// Before the return statement, create a reusable badge:
const changesBadge = hasChanges && (
  <TooltipProvider>
    <Tooltip>
      <TooltipTrigger asChild>
        <span className={cn(
          "inline-flex items-center justify-center h-4 min-w-[1rem] px-1 text-[10px] font-medium rounded border",
          isSelected
            ? "bg-amber-500 text-amber-950 border-amber-400"
            : "bg-amber-500/20 text-amber-600 dark:text-amber-400 border-amber-500/30"
        )}>
          <CircleDot className="w-2.5 h-2.5 mr-0.5" />
          {changedFilesCount ?? "!"}
        </span>
      </TooltipTrigger>
      <TooltipContent>
        <p>{changedFilesCount ?? "Some"} uncommitted file{changedFilesCount !== 1 ? "s" : ""}</p>
      </TooltipContent>
    </Tooltip>
  </TooltipProvider>
);

// Then use {changesBadge} in both branches

Also applies to: 288-308

apps/server/src/routes/worktree/routes/pr-info.ts (3)

78-98: Missing validation for worktreePath parameter.

While branchName is validated against command injection, worktreePath is passed directly to execAsync as the cwd option. Although cwd itself doesn't execute shell commands, a malicious path could still be used for path traversal attacks or to access unintended directories.

Consider adding path validation to ensure worktreePath is within expected bounds:

🔎 Suggested validation
 if (!worktreePath || !branchName) {
   res.status(400).json({
     success: false,
     error: "worktreePath and branchName required",
   });
   return;
 }

+// Basic path validation - ensure it's an absolute path and doesn't contain suspicious patterns
+const normalizedPath = path.resolve(worktreePath);
+if (normalizedPath !== worktreePath || worktreePath.includes('..')) {
+  res.status(400).json({
+    success: false,
+    error: "Invalid worktreePath",
+  });
+  return;
+}

 // Validate branch name to prevent command injection

12-43: Duplicate code: PATH extension logic.

This PATH extension logic (lines 12-43) is duplicated across both pr-info.ts and create-pr.ts. Consider extracting this into a shared utility in common.ts to improve maintainability.


54-73: Share type definitions between server and UI to prevent drift.

The PRComment and PRInfo interfaces are duplicated across the codebase. The server exports them from apps/server/src/routes/worktree/routes/pr-info.ts, while the UI redefines them inline in apps/ui/src/components/views/board-view/worktree-panel/types.ts without importing from the server. Both currently have identical structures, but this duplication increases the risk of unintended divergence if one side changes without updating the other.

Consider extracting these shared types to a dedicated package or ensuring a single source of truth to maintain consistency across the full stack.

apps/server/src/routes/worktree/routes/create-pr.ts (1)

140-150: Branch name used in shell command without escaping.

Although branchName is validated with isValidBranchName, the validation allows / characters which could have unintended behavior in certain contexts. The validated pattern [a-zA-Z0-9._\-/] is safe for git branch names, but consider using shellEscape for consistency.

apps/server/src/lib/worktree-metadata.ts (3)

31-36: Minor: Sanitization order could leave edge cases.

The current order removes trailing dots (line 34) before collapsing dashes (line 35). If a branch like foo..-- is processed:

  1. After line 32: foo.---
  2. After line 33: foo.---
  3. After line 34: foo.--- (trailing dots only, not mid-string)
  4. After line 35: foo.-
  5. After line 36: foo.

This could still leave a trailing dot. Consider reordering or adding a final trim:

🔎 Suggested improvement
   let safeBranch = branch
     .replace(/[/\\:*?"<>|]/g, "-")  // Replace invalid chars with dash
     .replace(/\s+/g, "_")           // Replace spaces with underscores
-    .replace(/\.+$/g, "")           // Remove trailing dots (Windows issue)
     .replace(/-+/g, "-")            // Collapse multiple dashes
-    .replace(/^-|-$/g, "");         // Remove leading/trailing dashes
+    .replace(/^-|-$/g, "")          // Remove leading/trailing dashes
+    .replace(/\.+$/g, "");          // Remove trailing dots (Windows issue) - do last

68-80: Silent error swallowing may hide issues.

The readWorktreeMetadata function catches all errors and returns null. While this is appropriate for "file not found" scenarios, it also silently hides JSON parse errors from corrupted files. Consider distinguishing between these cases for better debugging:

🔎 Suggested improvement
 export async function readWorktreeMetadata(
   projectPath: string,
   branch: string
 ): Promise<WorktreeMetadata | null> {
   try {
     const metadataPath = getWorktreeMetadataPath(projectPath, branch);
     const content = await fs.readFile(metadataPath, "utf-8");
     return JSON.parse(content) as WorktreeMetadata;
-  } catch (error) {
-    // File doesn't exist or can't be read
+  } catch (error: unknown) {
+    if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
+      console.warn(`[WorktreeMetadata] Failed to read metadata for ${branch}:`, error);
+    }
     return null;
   }
 }

145-159: Sequential file reads could be parallelized.

The loop reads metadata files sequentially. For projects with many worktrees, parallel reads would be more efficient:

🔎 Suggested improvement
-    for (const dir of dirs) {
-      if (dir.isDirectory()) {
-        const metadataPath = path.join(worktreesDir, dir.name, "worktree.json");
-        try {
-          const content = await fs.readFile(metadataPath, "utf-8");
-          const metadata = JSON.parse(content) as WorktreeMetadata;
-          result.set(metadata.branch, metadata);
-        } catch {
-          // Skip if file doesn't exist or can't be read
-        }
-      }
-    }
+    const readPromises = dirs
+      .filter(dir => dir.isDirectory())
+      .map(async (dir) => {
+        const metadataPath = path.join(worktreesDir, dir.name, "worktree.json");
+        try {
+          const content = await fs.readFile(metadataPath, "utf-8");
+          return JSON.parse(content) as WorktreeMetadata;
+        } catch {
+          return null;
+        }
+      });
+    
+    const metadataResults = await Promise.all(readPromises);
+    for (const metadata of metadataResults) {
+      if (metadata) {
+        result.set(metadata.branch, metadata);
+      }
+    }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6c25680 and bb5f68c.

📒 Files selected for processing (8)
  • apps/server/src/lib/worktree-metadata.ts (1 hunks)
  • apps/server/src/routes/worktree/routes/create-pr.ts (9 hunks)
  • apps/server/src/routes/worktree/routes/pr-info.ts (1 hunks)
  • apps/ui/src/components/views/board-view.tsx (6 hunks)
  • apps/ui/src/components/views/board-view/components/kanban-card.tsx (4 hunks)
  • apps/ui/src/components/views/board-view/dialogs/create-pr-dialog.tsx (4 hunks)
  • apps/ui/src/components/views/board-view/worktree-panel/components/worktree-actions-dropdown.tsx (4 hunks)
  • apps/ui/src/components/views/board-view/worktree-panel/components/worktree-tab.tsx (7 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/ui/src/components/views/board-view/components/kanban-card.tsx
🧰 Additional context used
🧬 Code graph analysis (5)
apps/server/src/routes/worktree/routes/pr-info.ts (3)
apps/ui/src/components/views/board-view/worktree-panel/types.ts (1)
  • PRInfo (37-60)
init.mjs (3)
  • req (169-171)
  • lines (79-79)
  • match (82-82)
apps/server/src/routes/common.ts (1)
  • getErrorMessage (370-372)
apps/server/src/routes/worktree/routes/create-pr.ts (1)
apps/server/src/lib/worktree-metadata.ts (1)
  • updateWorktreePRInfo (103-123)
apps/ui/src/components/views/board-view/worktree-panel/components/worktree-tab.tsx (3)
apps/ui/src/components/views/board-view/worktree-panel/types.ts (2)
  • WorktreeInfo (9-18)
  • PRInfo (37-60)
apps/ui/src/types/electron.d.ts (1)
  • WorktreeInfo (578-583)
apps/server/src/routes/worktree/routes/pr-info.ts (1)
  • PRInfo (64-73)
apps/ui/src/components/views/board-view.tsx (3)
apps/ui/src/components/views/board-view/worktree-panel/types.ts (2)
  • WorktreeInfo (9-18)
  • PRInfo (37-60)
apps/ui/src/types/electron.d.ts (1)
  • WorktreeInfo (578-583)
apps/server/src/routes/worktree/routes/pr-info.ts (1)
  • PRInfo (64-73)
apps/server/src/lib/worktree-metadata.ts (1)
apps/ui/src/components/views/board-view/worktree-panel/types.ts (1)
  • WorktreePRInfo (1-7)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: e2e
🔇 Additional comments (26)
apps/ui/src/components/views/board-view/dialogs/create-pr-dialog.tsx (3)

32-33: LGTM! Merge conflict resolved and props properly wired.

The projectPath prop and updated onCreated signature are correctly added and destructured. The PR URL propagation flow is well-implemented.

Also applies to: 40-41


114-132: Good differentiation between existing and newly created PRs.

The toast messaging correctly distinguishes between finding an existing PR versus creating a new one, providing appropriate feedback to users.


214-223: Clean close handler with proper PR URL propagation.

The handleClose function correctly passes the PR URL when an operation completed, and the state reset is properly handled by the useEffect when open becomes false.

apps/ui/src/components/views/board-view.tsx (4)

41-41: LGTM! Type imports properly added.

The PRInfo and WorktreeInfo types are correctly imported from the centralized types module.


273-284: Clean formatting improvement for branchCardCounts.

The reduce logic is properly formatted and the type annotation is correct.


1205-1223: Good fix: Promise.all for parallel persistence.

The previous review's suggestion to separate synchronous state updates from asynchronous persistence has been properly implemented. The Promise.all pattern ensures all persistence operations are tracked together.


925-925: LGTM! Handler properly wired to WorktreePanel.

The onAddressPRComments callback is correctly passed down to enable the PR addressing workflow.

apps/ui/src/components/views/board-view/worktree-panel/components/worktree-actions-dropdown.tsx (3)

22-25: LGTM! New imports for PR-related functionality.

The MessageSquare icon and PRInfo type are correctly imported to support the new PR addressing feature.


74-76: Clean derivation of PR presence.

The hasPR variable provides a clear boolean check for conditional rendering.


179-184: LGTM! PR-related dropdown items well structured.

The conditional rendering correctly shows "Create Pull Request" when no PR exists, and shows PR info with "Address PR Comments" when a PR is present. The click handlers properly prevent event bubbling and open links in new tabs.

Also applies to: 185-222

apps/ui/src/components/views/board-view/worktree-panel/components/worktree-tab.tsx (4)

3-11: LGTM! Merge conflicts resolved.

All imports are properly consolidated with CircleDot, GitPullRequest, Tooltip components, and type imports correctly combined.


92-106: Good visual state indication logic.

The border classes correctly prioritize running state (cyan) over uncommitted changes (amber), providing clear visual hierarchy.


108-196: Well-implemented PR badge with accessibility.

The PR badge includes:

  • State-based color coding for different PR states
  • Proper accessibility attributes (aria-label, aria-hidden)
  • Keyboard navigation support (onKeyDown)
  • Event propagation prevention to avoid triggering parent handlers

The styling logic correctly handles both selected and unselected tab states.


346-346: LGTM! Handler properly forwarded.

The onAddressPRComments callback is correctly passed to the WorktreeActionsDropdown component.

apps/server/src/routes/worktree/routes/pr-info.ts (3)

45-52: Good addition of branch name validation.

The isValidBranchName function properly addresses the command injection concern from previous reviews by restricting branch names to safe characters and limiting length.


245-279: Good fix for the {owner}/{repo} placeholder issue.

The conditional check at line 248 (if (targetRepo)) now properly skips fetching review comments when repository info is unavailable, with an appropriate warning logged at line 278. This addresses the previous review concern about literal placeholders being passed to gh api.


196-215: Proper use of gh pr list with JSON output.

Good approach using gh pr list with --json flag for structured output. The --limit 1 is appropriate since you only need the first matching PR.

apps/server/src/routes/worktree/routes/create-pr.ts (5)

22-27: Good: Branch name validation function added.

The isValidBranchName function provides proper input validation matching the implementation in pr-info.ts. This addresses the branch name injection concern.


99-106: Good: Branch validation applied before use.

The branch name is now validated before being used in shell commands, preventing command injection through malicious branch names.


301-309: Good: PR metadata persistence for existing PRs.

The code properly stores metadata when discovering an existing PR, ensuring the metadata is synchronized regardless of whether the PR was just created or already existed.


349-362: Good: PR metadata storage with error handling.

Appropriate error handling around metadata storage that doesn't fail the entire PR creation if metadata persistence fails.


370-396: Good: Graceful recovery when PR already exists.

The fallback logic to fetch existing PR details when creation fails with "already exists" provides good resilience against race conditions and edge cases.

apps/server/src/lib/worktree-metadata.ts (4)

26-48: Good: Comprehensive branch name sanitization.

The sanitization function now properly addresses cross-platform filesystem safety:

  • Replaces invalid characters (including Windows-specific ones)
  • Handles trailing dots and spaces
  • Truncates to safe length
  • Handles Windows reserved names

This addresses the previous review concern about incomplete sanitization.


9-21: Type definitions align with UI types.

The WorktreePRInfo interface matches the corresponding type in apps/ui/src/components/views/board-view/worktree-panel/types.ts. Good consistency between server and client types.


103-123: Good: Read-modify-write pattern for PR info updates.

The updateWorktreePRInfo function properly reads existing metadata before updating, preserving other fields like createdAt. This is the correct pattern for partial updates.


170-180: Good: Safe deletion with force: true.

Using fs.rm with { recursive: true, force: true } is appropriate for cleanup operations where the directory may or may not exist.

Comment on lines +88 to +90
// Use projectPath if provided, otherwise derive from worktreePath
// For worktrees, projectPath is needed to store metadata in the main project's .automaker folder
const effectiveProjectPath = projectPath || worktreePath;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find the create-pr.ts file and examine its context
find . -name "create-pr.ts" -type f 2>/dev/null | head -20

Repository: AutoMaker-Org/automaker

Length of output: 120


🏁 Script executed:

#!/bin/bash
# Search for create-pr related code and projectPath/worktreePath usage
rg -l "create-pr|projectPath|worktreePath" --type ts --type js 2>/dev/null | head -20

Repository: AutoMaker-Org/automaker

Length of output: 995


🏁 Script executed:

#!/bin/bash
# Search for .automaker usage in the codebase
rg "\.automaker|automaker" --type ts --type js 2>/dev/null | head -30

Repository: AutoMaker-Org/automaker

Length of output: 3481


🏁 Script executed:

#!/bin/bash
# Look for the function that contains lines 88-90 of create-pr.ts
fd "create-pr.ts" -type f --exec cat {} \;

Repository: AutoMaker-Org/automaker

Length of output: 237


🏁 Script executed:

#!/bin/bash
# Read the create-pr.ts file to see the full context around lines 88-90
cat -n apps/server/src/routes/worktree/routes/create-pr.ts

Repository: AutoMaker-Org/automaker

Length of output: 19264


🏁 Script executed:

#!/bin/bash
# Search for callers of create-pr endpoint
rg "create-pr|createPr" --type ts --type js -B 3 -A 3 2>/dev/null | head -100

Repository: AutoMaker-Org/automaker

Length of output: 7841


🏁 Script executed:

#!/bin/bash
# Search for the route definition to understand API contract
rg -A 10 "effectiveProjectPath" apps/server/src/routes/worktree/routes/create-pr.ts 2>/dev/null

Repository: AutoMaker-Org/automaker

Length of output: 2162


🏁 Script executed:

#!/bin/bash
# Check how metadata is stored and where .automaker is used
rg "\.automaker|effectiveProjectPath" apps/server/src -type ts -B 2 -A 2 2>/dev/null | head -150

Repository: AutoMaker-Org/automaker

Length of output: 49


🏁 Script executed:

#!/bin/bash
# Check the CreatePRDialog to see if it passes projectPath
rg -A 30 "CreatePRDialog" apps/ui/src/components/views/board-view/dialogs/ -type ts 2>/dev/null | grep -A 30 "createPR"

Repository: AutoMaker-Org/automaker

Length of output: 49


🏁 Script executed:

#!/bin/bash
# Check the worktree-metadata.js to see how updateWorktreePRInfo uses effectiveProjectPath
find . -name "worktree-metadata.js" -o -name "worktree-metadata.ts" | xargs cat 2>/dev/null | head -150

Repository: AutoMaker-Org/automaker

Length of output: 4334


🏁 Script executed:

#!/bin/bash
# Search for where the createPR API is called in the UI
rg "api\.createPR\|createPR\(" apps/ui/src/components -A 5 -B 5 --type ts 2>/dev/null | head -100

Repository: AutoMaker-Org/automaker

Length of output: 49


🏁 Script executed:

#!/bin/bash
# Check how createPR is called in the board-view - specifically the commit/PR dialog
find apps/ui/src/components/views/board-view/dialogs -name "*pr*" -o -name "*commit*" | xargs cat 2>/dev/null | grep -A 20 "createPR\|api\." | head -150

Repository: AutoMaker-Org/automaker

Length of output: 1791


🏁 Script executed:

#!/bin/bash
# Check if there's any logic to derive or resolve projectPath from worktreePath
rg "getProjectPath|resolveProjectPath|projectPath" apps/server/src/lib -A 5 -B 2 --type ts 2>/dev/null | head -100

Repository: AutoMaker-Org/automaker

Length of output: 6654


🏁 Script executed:

#!/bin/bash
# Check how the createPR endpoint is registered and what the expected contract is
rg -B 10 "createCreatePRHandler" apps/server/src/routes/worktree/index.ts 2>/dev/null

Repository: AutoMaker-Org/automaker

Length of output: 1091


Require projectPath parameter or implement explicit derivation logic for metadata storage.

The fallback const effectiveProjectPath = projectPath || worktreePath breaks the intended architecture. Metadata must be stored in the main project's .automaker/worktrees/ directory, not in the linked worktree's directory. Since projectPath is optional in the API contract, callers can omit it, causing PR metadata to be stored in the wrong location and become isolated to individual worktrees. Either make projectPath a required parameter or implement explicit logic to derive the main project path from the worktree, but do not silently fall back to worktreePath.

🤖 Prompt for AI Agents
In apps/server/src/routes/worktree/routes/create-pr.ts around lines 88 to 90,
the current fallback "const effectiveProjectPath = projectPath || worktreePath"
allows metadata to be written into the worktree instead of the main project's
.automaker directory; either require projectPath or derive the main project path
explicitly. Fix by validating and returning a 4xx error when projectPath is
missing (making it effectively required for this endpoint) OR implement
deterministic derivation: resolve the worktree's link/parent to locate the main
project root (e.g., follow symlink or search upward for the main project's
marker like .git or .automaker) and set effectiveProjectPath to that root; then
always write metadata to path.join(effectiveProjectPath, '.automaker',
'worktrees', <id>). Ensure tests and API docs are updated and add a clear error
message when derivation fails.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (4)
apps/ui/src/components/views/board-view.tsx (1)

424-467: Race condition and fragile feature matching (duplicate concern).

This implementation has the same issues previously flagged in the review of lines 424-467:

  1. Race condition: The setTimeout with FEATURE_CREATION_SETTLE_DELAY_MS (500ms) assumes the feature will be created within that timeframe.
  2. Direct store access: Line 453 uses useAppStore.getState().features instead of hookFeatures, which could reference stale data.
  3. Fragile matching (Lines 454-459): Searching by branchName, status === "backlog", and description.includes(\PR #${prNumber}`)` could match the wrong feature.
  4. No error handling: The async handleStartImplementation call lacks try-catch.

The previously suggested fix remains valid: modify handleAddFeature to return the created feature (or its ID), then use that directly instead of searching.

apps/server/src/routes/worktree/routes/create-pr.ts (3)

73-76: Fix command injection vulnerability in commit message.

The code uses execAsync (which spawns a shell via child_process.exec()) with user-provided commitMessage interpolated into a double-quoted shell string. The .replace(/"/g, '\\"') escaping is insufficient because dollar signs ($), backticks (`), and backslashes (\) retain special meaning within double quotes in shell contexts.

Attack example:

commitMessage = 'Fix $(rm -rf /tmp/test)'  // Executes rm command
commitMessage = 'Update `cat /etc/passwd`'  // Executes cat command

Use execFile or pass the message via stdin to avoid shell interpolation, or properly escape all shell metacharacters (which is error-prone).

🔎 Recommended fix using execFile
+import { execFile } from "child_process";
+import { promisify } from "util";
+const execFileAsync = promisify(execFile);

 // Create commit
-await execAsync(`git commit -m "${message.replace(/"/g, '\\"')}"`, {
-  cwd: worktreePath,
-  env: execEnv,
-});
+await execFileAsync("git", ["commit", "-m", message], {
+  cwd: worktreePath,
+  env: execEnv,
+});

37-39: Require projectPath parameter or implement explicit derivation logic for metadata storage.

The fallback const effectiveProjectPath = projectPath || worktreePath breaks the intended architecture. Metadata must be stored in the main project's .automaker/worktrees/ directory, not in the linked worktree's directory. Since projectPath is optional in the API contract, callers can omit it, causing PR metadata to be stored in the wrong location and become isolated to individual worktrees. Either make projectPath a required parameter or implement explicit logic to derive the main project path from the worktree, but do not silently fall back to worktreePath.


260-304: Fix command injection vulnerability in PR title and body.

The code constructs a shell command with user-provided title and body parameters using only .replace(/"/g, '\\"') for escaping. This is insufficient when using execAsync (which spawns a shell), as dollar signs ($), backticks (`), and backslashes (\) remain interpreted within double-quoted strings.

Attack examples:

prTitle = 'Feature $(whoami)'      // Executes whoami
prBody = 'Description `ls -la`'    // Executes ls

Replace execAsync with execFile to pass arguments directly without shell interpolation, or use stdin for multi-line content.

🔎 Recommended fix using execFile
+import { execFile } from "child_process";
+import { promisify } from "util";
+const execFileAsync = promisify(execFile);

-            prCmd += ` --title "${title.replace(/"/g, '\\"')}" --body "${body.replace(/"/g, '\\"')}" ${draftFlag}`;
-            prCmd = prCmd.trim();
-
-            console.log(`[CreatePR] Creating PR with command: ${prCmd}`);
-            const { stdout: prOutput } = await execAsync(prCmd, {
-              cwd: worktreePath,
-              env: execEnv,
-            });
+            const args = ["pr", "create", "--base", base];
+            if (upstreamRepo && originOwner) {
+              args.push("--repo", upstreamRepo, "--head", `${originOwner}:${branchName}`);
+            } else {
+              args.push("--head", branchName);
+            }
+            args.push("--title", title, "--body", body);
+            if (draft) {
+              args.push("--draft");
+            }
+            
+            console.log(`[CreatePR] Creating PR with command: gh ${args.join(' ')}`);
+            const { stdout: prOutput } = await execFileAsync("gh", args, {
+              cwd: worktreePath,
+              env: execEnv,
+            });
🧹 Nitpick comments (4)
apps/ui/src/components/views/board-view/worktree-panel/components/worktree-tab.tsx (1)

223-243: Consider extracting the hasChanges badge to reduce duplication.

The uncommitted changes badge (CircleDot with tooltip) is duplicated in both the main worktree path (lines 223-243) and the non-main path (lines 287-307). Consider extracting this into a shared component or helper function.

🔎 Example: Extract to a separate component

Create a new component in the same file:

function UncommittedChangesBadge({ 
  isSelected, 
  changedFilesCount 
}: { 
  isSelected: boolean; 
  changedFilesCount?: number; 
}) {
  return (
    <TooltipProvider>
      <Tooltip>
        <TooltipTrigger asChild>
          <span className={cn(
            "inline-flex items-center justify-center h-4 min-w-[1rem] px-1 text-[10px] font-medium rounded border",
            isSelected
              ? "bg-amber-500 text-amber-950 border-amber-400"
              : "bg-amber-500/20 text-amber-600 dark:text-amber-400 border-amber-500/30"
          )}>
            <CircleDot className="w-2.5 h-2.5 mr-0.5" />
            {changedFilesCount ?? "!"}
          </span>
        </TooltipTrigger>
        <TooltipContent>
          <p>{changedFilesCount ?? "Some"} uncommitted file{changedFilesCount !== 1 ? "s" : ""}</p>
        </TooltipContent>
      </Tooltip>
    </TooltipProvider>
  );
}

Then replace both instances with:

{hasChanges && <UncommittedChangesBadge isSelected={isSelected} changedFilesCount={changedFilesCount} />}

Also applies to: 287-307

apps/server/src/routes/worktree/routes/pr-info.ts (2)

89-98: Consider consolidating duplicate regex patterns.

The three regex patterns for matching remote URLs are very similar and differ only slightly in structure. This duplication makes maintenance harder.

🔎 Proposed consolidation
-          let match =
-            line.match(
-              /^(\w+)\s+.*[:/]([^/]+)\/([^/\s]+?)(?:\.git)?\s+\(fetch\)/
-            ) ||
-            line.match(
-              /^(\w+)\s+git@[^:]+:([^/]+)\/([^\s]+?)(?:\.git)?\s+\(fetch\)/
-            ) ||
-            line.match(
-              /^(\w+)\s+https?:\/\/[^/]+\/([^/]+)\/([^\s]+?)(?:\.git)?\s+\(fetch\)/
-            );
+          // Match SSH (git@host:owner/repo), HTTPS (https://host/owner/repo), or generic (remote host:owner/repo)
+          const match = line.match(
+            /^(\w+)\s+(?:git@[^:]+:|https?:\/\/[^/]+\/|.*[:/])([^/]+)\/([^/\s]+?)(?:\.git)?\s+\(fetch\)/
+          );

36-269: Add tests to meet coverage thresholds.

This new file contributes to the pipeline coverage failure (statements 64.81% < 65%, lines 64.75% < 65%, branches 57.99% < 58%). Add unit tests covering:

  • Input validation paths (valid/invalid inputs)
  • gh CLI unavailable scenario
  • Repository detection fallbacks
  • PR found vs not found scenarios
  • Error handling paths

Would you like me to generate example test cases for this handler?

apps/server/src/lib/worktree-metadata.ts (1)

142-168: Consider logging skipped metadata files.

Lines 158-160 silently skip files that can't be read or parsed. While this provides graceful degradation, it might hide issues. Consider logging a warning when metadata files fail to parse, which could help diagnose configuration problems.

🔎 Proposed enhancement
         try {
           const content = await fs.readFile(metadataPath, "utf-8");
           const metadata = JSON.parse(content) as WorktreeMetadata;
           result.set(metadata.branch, metadata);
         } catch {
-          // Skip if file doesn't exist or can't be read
+          // Skip if file doesn't exist or can't be read
+          console.warn(`[WorktreeMetadata] Failed to read metadata from ${metadataPath}`);
         }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bb5f68c and 3842eb1.

📒 Files selected for processing (6)
  • apps/server/src/lib/worktree-metadata.ts (1 hunks)
  • apps/server/src/routes/worktree/common.ts (1 hunks)
  • apps/server/src/routes/worktree/routes/create-pr.ts (8 hunks)
  • apps/server/src/routes/worktree/routes/pr-info.ts (1 hunks)
  • apps/ui/src/components/views/board-view.tsx (7 hunks)
  • apps/ui/src/components/views/board-view/worktree-panel/components/worktree-tab.tsx (7 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
apps/ui/src/components/views/board-view.tsx (1)
apps/ui/src/components/views/board-view/worktree-panel/types.ts (2)
  • WorktreeInfo (9-18)
  • PRInfo (37-60)
apps/server/src/routes/worktree/routes/create-pr.ts (2)
apps/server/src/routes/worktree/common.ts (4)
  • execAsync (17-17)
  • execEnv (65-68)
  • isValidBranchName (79-81)
  • isGhCliAvailable (86-96)
apps/server/src/lib/worktree-metadata.ts (1)
  • updateWorktreePRInfo (106-126)
apps/server/src/lib/worktree-metadata.ts (1)
apps/ui/src/components/views/board-view/worktree-panel/types.ts (1)
  • WorktreePRInfo (1-7)
apps/server/src/routes/worktree/common.ts (1)
apps/server/src/services/feature-loader.ts (1)
  • FeatureLoader (49-463)
🪛 GitHub Actions: Test Suite
apps/server/src/routes/worktree/routes/create-pr.ts

[error] 1-1: Coverage thresholds not met: global statements 64.81% (required 65%), lines 64.75% (required 65%), branches 57.99% (required 58%). The command 'vitest run --coverage' did not meet the coverage thresholds.

apps/server/src/routes/worktree/routes/pr-info.ts

[error] 1-1: Coverage thresholds not met: global statements 64.81% (required 65%), lines 64.75% (required 65%), branches 57.99% (required 58%). The command 'vitest run --coverage' did not meet the coverage thresholds.

apps/server/src/lib/worktree-metadata.ts

[error] 1-1: Coverage thresholds not met: global statements 64.81% (required 65%), lines 64.75% (required 65%), branches 57.99% (required 58%). The command 'vitest run --coverage' did not meet the coverage thresholds.

apps/server/src/routes/worktree/common.ts

[error] 1-1: Coverage thresholds not met: global statements 64.81% (required 65%), lines 64.75% (required 65%), branches 57.99% (required 58%). The command 'vitest run --coverage' did not meet the coverage thresholds.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: e2e
🔇 Additional comments (12)
apps/ui/src/components/views/board-view.tsx (4)

41-41: LGTM!

The import of PRInfo and WorktreeInfo types is correctly placed and supports the new PR addressing functionality.


277-287: LGTM!

The refactored branchCardCounts memoization is cleaner and more efficient by mutating the accumulator directly instead of spreading.


928-928: LGTM!

The wiring of onAddressPRComments to WorktreePanel is correct.


1209-1226: Well-structured PR URL propagation.

The implementation correctly separates synchronous state updates from asynchronous persistence operations:

  • Local state is updated synchronously for all features (Lines 1216-1218)
  • Persistence calls are handled concurrently with Promise.all (Lines 1221-1225)

This addresses the concern raised in the previous review comment about using forEach with async functions.

apps/ui/src/components/views/board-view/worktree-panel/components/worktree-tab.tsx (5)

3-11: LGTM!

The imports are clean and properly organized, with all necessary icons, UI components, and types included.


18-19: LGTM!

The new props (hasChanges, changedFilesCount, onAddressPRComments) are properly typed and integrated into the component signature.

Also applies to: 47-47, 57-58, 86-86


92-106: LGTM!

The border styling logic is well-documented and appropriately prioritizes running state (cyan) over uncommitted changes (amber), providing clear visual feedback.


198-198: LGTM!

The application of borderClasses to the wrapper div correctly applies the computed border styling based on worktree state.


345-345: LGTM!

The onAddressPRComments callback is correctly passed to WorktreeActionsDropdown.

apps/server/src/routes/worktree/routes/pr-info.ts (1)

197-231: LGTM! Repository info check properly implemented.

The code now correctly checks for targetRepo availability before attempting to fetch review comments, addressing the previous concern about placeholder interpolation.

apps/server/src/routes/worktree/routes/create-pr.ts (1)

48-55: LGTM! Branch name validation added.

The new validation using isValidBranchName properly addresses command injection concerns by ensuring branch names only contain safe characters before use in shell commands.

apps/server/src/lib/worktree-metadata.ts (1)

29-51: LGTM! Comprehensive branch name sanitization implemented.

The sanitization function now properly handles all cross-platform filesystem concerns raised in the previous review:

  • Invalid characters for Windows and Unix
  • Windows reserved names (CON, PRN, etc.)
  • Trailing dots and spaces
  • Length limits
  • Empty string edge case

Comment on lines +46 to +52
// Unix/Mac paths
additionalPaths.push(
"/opt/homebrew/bin", // Homebrew on Apple Silicon
"/usr/local/bin", // Homebrew on Intel Mac, common Linux location
"/home/linuxbrew/.linuxbrew/bin", // Linuxbrew
`${process.env.HOME}/.local/bin`, // pipx, other user installs
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard against undefined process.env.HOME.

Line 51 uses ${process.env.HOME}/.local/bin in a template literal. If process.env.HOME is undefined (which can happen in some environments), this produces the string "undefined/.local/bin" which passes the .filter(Boolean) check but is an invalid path.

🔎 Proposed fix
 } else {
   // Unix/Mac paths
   additionalPaths.push(
     "/opt/homebrew/bin",        // Homebrew on Apple Silicon
     "/usr/local/bin",           // Homebrew on Intel Mac, common Linux location
     "/home/linuxbrew/.linuxbrew/bin", // Linuxbrew
-    `${process.env.HOME}/.local/bin`, // pipx, other user installs
   );
+  if (process.env.HOME) {
+    additionalPaths.push(`${process.env.HOME}/.local/bin`);
+  }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Unix/Mac paths
additionalPaths.push(
"/opt/homebrew/bin", // Homebrew on Apple Silicon
"/usr/local/bin", // Homebrew on Intel Mac, common Linux location
"/home/linuxbrew/.linuxbrew/bin", // Linuxbrew
`${process.env.HOME}/.local/bin`, // pipx, other user installs
);
// Unix/Mac paths
additionalPaths.push(
"/opt/homebrew/bin", // Homebrew on Apple Silicon
"/usr/local/bin", // Homebrew on Intel Mac, common Linux location
"/home/linuxbrew/.linuxbrew/bin", // Linuxbrew
);
if (process.env.HOME) {
additionalPaths.push(`${process.env.HOME}/.local/bin`); // pipx, other user installs
}
🤖 Prompt for AI Agents
In apps/server/src/routes/worktree/common.ts around lines 46 to 52, the template
literal `${process.env.HOME}/.local/bin` can produce "undefined/.local/bin" when
process.env.HOME is not set; change the code to guard against undefined by using
a reliable home directory (e.g. require('os').homedir()) or only push the path
when process.env.HOME is truthy — update the additionalPaths push to either use
os.homedir() + '/.local/bin' or wrap the `${process.env.HOME}/.local/bin` entry
in a conditional so it is added only if process.env.HOME is defined.

Comment on lines +108 to +195
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 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 hover:opacity-80 active:opacity-70", // Reset button appearance but keep cursor, add hover/active states
prStateClasses
)}
style={{
// Override any inherited button styles
backgroundImage: "none",
boxShadow: "none",
}}
title={`${prLabel} - Click to open`}
aria-label={`${prLabel} - Click to open pull request`}
onClick={(e) => {
e.stopPropagation(); // Prevent triggering worktree selection
if (worktree.pr?.url) {
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();
if (worktree.pr?.url) {
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" : ""}>
PR #{worktree.pr.number}
</span>
<span className={cn("capitalize", getStatusColorClass())} aria-hidden="true">
{prState}
</span>
</button>
);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Invalid HTML: button nested inside Button component.

Lines 154-194 render a <button> element that is later placed inside the parent <Button> component (lines 201-244 for main, 260-308 for non-main). This creates nested interactive elements, which is invalid HTML and causes:

  1. HTML5 validation errors: Interactive content cannot be nested inside interactive elements.
  2. Accessibility issues: Screen readers may behave unpredictably with nested buttons.
  3. Focus management problems: Tab navigation and focus states may conflict.
🔎 Refactor to use a non-button element

Replace the <button> with a <span> or <div> styled to look clickable:

-      <button
+      <span
         type="button"
+        role="button"
+        tabIndex={0}
         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 hover:opacity-80 active:opacity-70",
           prStateClasses
         )}
         // ... rest of props unchanged
-      </button>
+      </span>

Remove the type="button" attribute and add role="button" and tabIndex={0} to maintain accessibility.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
apps/ui/src/components/views/board-view/worktree-panel/components/worktree-tab.tsx
around lines 108 to 195, the code renders a <button> that will be placed inside
a parent <Button>, causing nested interactive elements; replace the inner
<button> with a non-interactive element (e.g., <div> or <span>) styled like a
button, remove the type attribute, add role="button" and tabIndex={0} to
preserve semantics, keep the existing onClick/onKeyDown handlers and
e.stopPropagation() logic, and ensure aria-label/title remain unchanged so
keyboard and screen-reader accessibility are preserved while avoiding invalid
nested buttons.

- Updated the description input locator to use a more specific selector.
- Added a visibility check for the description textarea before filling it, improving test reliability.
- Added tests to ensure mkdirSafe handles ELOOP and EEXIST errors gracefully.
- Implemented checks for existsSafe to return true for ELOOP errors and throw for other errors.
- Improved overall robustness of filesystem utility tests.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
apps/server/tests/unit/lib/fs-utils.test.ts (1)

69-80: Remove unused variable.

Line 71 declares originalLstat but never uses it. This appears to be leftover code from development.

🔎 Proposed fix
     it("should handle ELOOP error gracefully when checking path", async () => {
       // Mock lstat to throw ELOOP error
-      const originalLstat = fs.lstat;
       const mkdirSafePath = path.join(testDir, "eloop-path");
       
       vi.spyOn(fs, "lstat").mockRejectedValueOnce({ code: "ELOOP" });
apps/server/tests/unit/lib/worktree-metadata.test.ts (2)

32-69: Good coverage of branch name sanitization edge cases.

The tests effectively verify that branch names with special characters and Windows reserved names are handled correctly through the write/read cycle.

Consider adding tests for additional edge cases:

  • Empty branch names
  • Very long branch names (filesystem path length limits)
  • Branch names with only spaces or dots
  • Branch names ending with periods (Windows restriction)
Example additional test cases
it("should handle empty branch names gracefully", async () => {
  const branch = "";
  const metadata: WorktreeMetadata = {
    branch,
    createdAt: new Date().toISOString(),
  };

  await writeWorktreeMetadata(testProjectPath, branch, metadata);
  const result = await readWorktreeMetadata(testProjectPath, branch);
  expect(result).toEqual(metadata);
});

it("should handle very long branch names", async () => {
  const branch = "a".repeat(300);
  const metadata: WorktreeMetadata = {
    branch,
    createdAt: new Date().toISOString(),
  };

  await writeWorktreeMetadata(testProjectPath, branch, metadata);
  const result = await readWorktreeMetadata(testProjectPath, branch);
  expect(result).toEqual(metadata);
});

1-368: Excellent test suite with comprehensive coverage.

This test file demonstrates high-quality testing practices:

  • ✅ Complete coverage of all exported functions
  • ✅ Thorough edge case testing (malformed JSON, missing files, empty directories)
  • ✅ Proper test isolation with temporary directories
  • ✅ Clear organization with descriptive test names
  • ✅ Proper cleanup to avoid test pollution

The test suite provides strong confidence in the worktree-metadata module's reliability and robustness.

For even more comprehensive coverage, consider adding tests for:

  • Concurrent operations (if the module should support them)
  • Invalid input types (null/undefined parameters)
  • Permission errors (read-only directories)
  • Filesystem edge cases (symbolic links, very deeply nested paths)

These are optional enhancements that could further strengthen the test suite but are not essential given the already strong coverage.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ff6a5a5 and 19fd23c.

📒 Files selected for processing (2)
  • apps/server/tests/unit/lib/fs-utils.test.ts (2 hunks)
  • apps/server/tests/unit/lib/worktree-metadata.test.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
apps/server/tests/unit/lib/fs-utils.test.ts (1)
apps/server/src/lib/fs-utils.ts (2)
  • mkdirSafe (12-47)
  • existsSafe (53-67)
apps/server/tests/unit/lib/worktree-metadata.test.ts (1)
apps/server/src/lib/worktree-metadata.ts (7)
  • WorktreeMetadata (20-24)
  • writeWorktreeMetadata (88-101)
  • readWorktreeMetadata (71-83)
  • updateWorktreePRInfo (106-126)
  • getWorktreePRInfo (131-137)
  • readAllWorktreeMetadata (142-168)
  • deleteWorktreeMetadata (173-183)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: e2e
🔇 Additional comments (11)
apps/server/tests/unit/lib/fs-utils.test.ts (4)

82-94: LGTM!

This test correctly verifies the race condition handling where a directory might be created between the existence check and the mkdir call.


96-108: LGTM!

This test correctly verifies graceful handling of ELOOP errors that occur during directory creation, which can happen with symlink loops in the path.


154-162: LGTM!

This test correctly verifies that ELOOP (symlink loop) is treated as true existence, which aligns with the implementation's design decision that a looping symlink still "exists" as a filesystem entity.


164-171: LGTM!

This test correctly verifies that errors other than ENOENT and ELOOP are propagated to the caller, ensuring the function doesn't silently swallow unexpected filesystem errors.

apps/server/tests/unit/lib/worktree-metadata.test.ts (7)

19-30: Excellent test isolation and cleanup.

The setup creates isolated temporary directories for each test run, and the cleanup properly handles errors during teardown. This follows best practices for test isolation.


71-107: Comprehensive coverage of read operations.

The tests cover all expected scenarios: missing metadata, basic metadata, and metadata with PR information. The assertions correctly verify the full object structure.


109-145: Good coverage of write operations.

The tests verify both creating new metadata and overwriting existing metadata, ensuring the write operation is reliable in both scenarios.


147-210: Excellent coverage of PR info updates.

The tests thoroughly verify that updateWorktreePRInfo handles both creation and update scenarios correctly. The test at lines 187-209 is particularly valuable as it ensures existing metadata fields are preserved during PR info updates.


212-244: Complete coverage of PR info retrieval scenarios.

The tests verify all possible outcomes: missing metadata, metadata without PR info, and metadata with PR info. This ensures the function behaves correctly in all cases.


246-341: Outstanding edge case coverage for bulk read operations.

The test suite for readAllWorktreeMetadata is exceptionally thorough, covering:

  • Missing and empty directories
  • Multiple valid entries
  • Graceful handling of invalid entries (missing files, non-directories, malformed JSON)

This comprehensive coverage ensures the function is robust against various filesystem states.


343-366: Good coverage of deletion scenarios.

The tests verify both successful deletion and graceful handling of non-existent metadata. The idempotent behavior is particularly important for cleanup operations.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants