Skip to content

Comments

Improve pull request flow, add branch selection for worktree creation, fix auto-mode concurrency count#784

Closed
gsxdsm wants to merge 5 commits intoAutoMaker-Org:v0.15.0rcfrom
gsxdsm:fix/fetch-before-pull-fetch
Closed

Improve pull request flow, add branch selection for worktree creation, fix auto-mode concurrency count#784
gsxdsm wants to merge 5 commits intoAutoMaker-Org:v0.15.0rcfrom
gsxdsm:fix/fetch-before-pull-fetch

Conversation

@gsxdsm
Copy link
Collaborator

@gsxdsm gsxdsm commented Feb 19, 2026

Summary

This PR introduces several UX improvements to the worktree workflow, focusing on making pull request creation more flexible and adding base branch selection when creating worktrees.

🔀 PR Dialog: Target Remote Support

  • Added a separate "Create PR Against" remote selector for repositories with multiple remotes (e.g., fork workflows with both origin and upstream)
  • The push remote and target remote are now independent — you can push to origin while creating the PR against upstream
  • Branch list in the PR dialog now filters to only show branches from the selected target remote when multiple remotes exist
  • Base branch selection auto-resets to the best match (main/master) when the target remote changes
  • Push remote selector is now only shown when there are actually commits to push (new branch, unpushed commits, or uncommitted changes)
  • Fixed PR dialog description to read "Create a pull request" vs "Push changes and create a pull request" depending on whether there are local changes

🌿 Create Worktree Dialog: Base Branch Selection

  • Added a collapsible "Base Branch" section to the Create Worktree dialog
  • Users can now select any local or remote branch as the starting point for a new worktree (defaults to HEAD)
  • Branch list loads lazily when the section is expanded, with a refresh button
  • Remote branches are indicated with a globe icon and a note that the latest will be fetched before creating the worktree
  • Selected base branch is shown inline when the section is collapsed

⬆️ Commit Dialog: Push After Commit

  • Added a "Push to remote after commit" checkbox to the Commit dialog
  • When enabled, a remote selector appears so the user can choose which remote to push to
  • The commit button changes to "Commit & Push" and handles the push in sequence after the commit
  • The Cmd/Ctrl+Enter shortcut is also gated on the push remote being selected when push is enabled

🛠️ Backend: Remote Branch Fetching & Target Remote

  • Worktree creation now auto-fetches from the appropriate remote when a remote branch is specified as the base (e.g., origin/main)
  • PR creation endpoint accepts a new targetRemote parameter to explicitly control which remote the PR is created against
  • Both remote and targetRemote values are now validated against injection attacks independently

🐛 Fix: Auto-mode Concurrency Count

  • Fixed a bug where the auto-mode running task count could be inflated due to duplicate auto_mode_feature_start events being fired by both the execution-service and pipeline-orchestrator for the same task ID
  • Guard added in app-store.ts to reject duplicate task entries before they are appended

Test Plan

  • Create a worktree using a remote branch (e.g., origin/main) as the base branch — verify the latest is fetched and the worktree starts from that ref
  • Create a worktree with no base branch selected — verify it defaults to HEAD behavior
  • Open the PR dialog in a fork repo (with origin and upstream remotes) — verify independent push remote and target remote selection
  • Verify base branch list filters correctly when switching target remote
  • Commit changes with "Push after commit" enabled — verify push happens after commit and correct remote is used
  • Trigger auto-mode on multiple features simultaneously — verify the running task count does not inflate

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Specify a target remote when creating pull requests
    • "Push to remote after commit" with remote picker and auto-push
    • Terminal quick-scripts: configure, run, and access from the terminal header
  • Improvements

    • Per-remote push/pull actions when multiple remotes exist
    • Automatic fetch-from-remote before create-worktree, rebase, integrate, and PR flows
    • Expose detected tracking remote for branches and improved branch-empty messaging
  • UI/UX

    • "Merge Branch" relabeled to "Integrate Branch"
    • Mobile/PWA safe-area and dialog fullscreen styling enhancements

@coderabbitai
Copy link

coderabbitai bot commented Feb 19, 2026

📝 Walkthrough

Walkthrough

Adds cross-remote PR targeting and resolution, remote-aware branch detection/fetch for worktree create/rebase/merge, "push after commit" UI, propagation of remotes/trackingRemote through worktree UI, utilities for remote name validation, and multiple UI/layout and PWA safe-area improvements.

Changes

Cohort / File(s) Summary
PR resolution & create-pr
apps/server/src/services/pr-service.ts, apps/server/src/routes/worktree/routes/create-pr.ts, apps/ui/src/lib/electron.ts, apps/ui/src/types/electron.d.ts, apps/ui/src/components/views/board-view/dialogs/create-pr-dialog.tsx
New resolvePrTarget service and targetRemote support: validate remote names, parse remotes, determine upstream/origin/push owner, adjust gh CLI args and PR URL encoding; UI/types expose and use targetRemote.
Worktree create — remote base branch
apps/server/src/routes/worktree/routes/create.ts, apps/ui/src/components/views/board-view/dialogs/create-worktree-dialog.tsx, apps/ui/src/components/ui/branch-autocomplete.tsx
Detect remote-prefixed base branches, fetch remote branch before creating worktree, add Base Branch UI with branch fetching, allowCreate/emptyMessage props for branch autocomplete.
Merge/Rebase remote option
apps/server/src/routes/worktree/routes/merge.ts, apps/server/src/routes/worktree/routes/rebase.ts, apps/server/src/services/merge-service.ts, apps/server/src/services/rebase-service.ts
Handlers and services accept optional remote, validate it, perform pre-operation fetch from that remote (non-fatal on failure), and propagate option into merge/rebase flows.
Commit dialog: push after commit
apps/ui/src/components/views/board-view/dialogs/commit-worktree-dialog.tsx
Add "Push after commit" checkbox, load/select remotes, trigger push after successful commit, disable commit while pushing and surface toasts.
Remote-aware worktree UI & actions
apps/ui/src/components/views/board-view/.../worktree-panel.tsx, .../worktree-actions-dropdown.tsx, .../worktree-dropdown.tsx, .../worktree-tab.tsx, .../hooks/use-branches.ts
Expose remotes, remotesCache, and trackingRemote; add handlers to pull/push with a specific remote and split-button submenus when multiple remotes exist.
Branch listing & trackingRemote
apps/server/src/routes/worktree/routes/list-branches.ts, apps/ui/src/hooks/queries/use-worktrees.ts, apps/ui/src/types/electron.d.ts
Server returns trackingRemote when upstream tracking exists; client hooks and types propagate it into UI consumers.
Validation & utils exports
libs/utils/src/git-validation.ts, libs/utils/src/index.ts, apps/server/src/routes/worktree/common.ts
Add isValidRemoteName validator, export it from utils and re-export in server common for remote-name validation.
Store & feature start flow
apps/ui/src/store/app-store.ts, apps/ui/src/components/views/board-view/hooks/use-board-actions.ts, apps/ui/src/components/views/graph-view-page.tsx, apps/ui/src/components/views/board-view/hooks/use-board-column-features.ts
Prevent duplicate running task IDs; add initialStatus to handleAddFeature to allow immediate in_progress creation and propagate runningAutoTasksAllWorktrees to avoid UI backlog flashes.
Service worker & PWA safe-area
apps/ui/public/sw.js, apps/ui/index.html, apps/ui/src/lib/mobile-detect.ts, apps/ui/src/renderer.tsx, apps/ui/src/styles/global.css
Guarded SW activation (SKIP_WAITING signaling), stale-while-revalidate strategies, early PWA standalone detection applying pwa-standalone class, and dialog/mobile safe-area CSS utilities.
Dialogs, layout & misc UI
many apps/ui/src/components/... (dialog repositioning, git-diff header refactor, dialog-fullscreen-mobile class additions, Merge→Integrate rename in dialog props)
Multiple responsive/layout and accessibility changes, text/prop rename (onMerged → onIntegrated), and mobile/dialog styling adjustments.

Sequence Diagram(s)

sequenceDiagram
  participant UI as Client(UI)
  participant EA as Electron API
  participant Server as Server Route
  participant Git as Local Git
  participant GH as Remote Host

  UI->>EA: createPR({ head, base, targetRemote? })
  EA->>Server: POST /worktree/create-pr { ..., targetRemote? }
  Server->>Git: git remote -v -> parse remotes (parsedRemotes)
  alt targetRemote provided
    Server->>Server: validate isValidRemoteName(targetRemote)
    Server->>Git: ensure targetRemote ∈ parsedRemotes
    Server->>Server: derive targetRepo, pushOwner, upstreamRepo, originOwner
  else infer from pushRemote
    Server->>Server: infer upstream/origin from parsedRemotes
  end
  Server->>GH: create PR (gh CLI args use --repo/upstream and --head originOwner:branch if fork)
  Server-->>EA: { prUrl, crossRemoteInfo? }
  EA-->>UI: open PR URL / show result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

Enhancement, Bug, type: feature, scope: ui, Refactor

Poem

🐰 I hopped through remotes, sniffed each name and branch,
Pushed after committing with a tiny dance,
Chose upstream rivers, encoded heads and base,
Cross-remote PRs gleam in a tidy, safe place.
Little paws, big changes—let the branches advance!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 32.61% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main objectives: improving PR flow with target remote selection, adding branch selection for worktree creation, and fixing auto-mode concurrency counting.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @gsxdsm, 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 significantly enhances the user experience for Git workflows by introducing more flexible and intuitive options for pull request creation, worktree management, and commit operations. It allows for independent control over push and target remotes, provides granular control over base branch selection for new worktrees, and streamlines the commit-and-push process. These changes aim to improve efficiency and clarity, particularly in complex repository setups like fork workflows, while also addressing a concurrency bug in auto-mode task tracking.

Highlights

  • Enhanced Pull Request Workflow: Introduced independent selection for push and target remotes in the PR dialog, allowing users to push to one remote (e.g., 'origin') while creating a PR against another (e.g., 'upstream'). The base branch list now dynamically filters to show only branches from the selected target remote, and the base branch selection auto-resets for better usability.
  • Flexible Worktree Creation: Added a new collapsible 'Base Branch' section to the Create Worktree dialog, enabling users to select any local or remote branch as the starting point for a new worktree. Remote branches are now automatically fetched before worktree creation to ensure the latest refs are used.
  • Integrated Commit & Push: Implemented a 'Push to remote after commit' checkbox in the Commit dialog. When enabled, users can select a remote, and the commit button changes to 'Commit & Push', executing the push operation immediately after a successful commit.
  • Improved Git Operations Backend: The backend now supports a targetRemote parameter for PR creation, allowing explicit control over the PR's destination. Remote names are also validated against injection attacks. Additionally, merge and rebase operations now include an optional remote parameter to fetch the latest changes before execution.
  • Auto-mode Concurrency Fix: Resolved a bug where the auto-mode running task count could be inflated due to duplicate auto_mode_feature_start events. A guard was added to prevent multiple entries for the same task ID.
  • UI Terminology Update: Updated 'Merge Branch' terminology in the UI to 'Integrate Branch' for clarity and consistency across various dialogs and actions.

🧠 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.

Changelog
  • apps/server/src/routes/worktree/routes/create-pr.ts
    • Added targetRemote to the request body for PR creation.
    • Implemented validation for the new targetRemote parameter to prevent command injection.
    • Modified PR URL construction logic to correctly handle targetRemote for cross-remote pull requests.
    • Updated gh pr list command to use owner:branch format for forks/cross-remote PRs.
  • apps/server/src/routes/worktree/routes/create.ts
    • Introduced detectRemoteBranch utility to identify remote branches.
    • Updated worktree creation handler to fetch from the specified remote if a remote base branch is used.
  • apps/server/src/routes/worktree/routes/merge.ts
    • Added an optional remote parameter to the merge request body for fetching before merge.
  • apps/server/src/routes/worktree/routes/rebase.ts
    • Added an optional remote parameter to the rebase request body for fetching before rebase.
  • apps/server/src/services/merge-service.ts
    • Extended MergeOptions interface to include an optional remote property.
    • Implemented a pre-merge fetch operation from the specified remote to ensure up-to-date refs.
  • apps/server/src/services/rebase-service.ts
    • Defined RebaseOptions interface with an optional remote property.
    • Modified runRebase function to accept RebaseOptions and perform a pre-rebase fetch from the specified remote.
  • apps/ui/src/components/ui/branch-autocomplete.tsx
    • Added allowCreate and emptyMessage props to control branch creation and empty state messages.
    • Updated search placeholder text based on allowCreate prop.
  • apps/ui/src/components/views/board-view/dialogs/commit-worktree-dialog.tsx
    • Added 'Push to remote after commit' checkbox and associated remote selection UI.
    • Implemented logic to fetch available remotes when the push option is enabled.
    • Updated commit handling to optionally perform a push after a successful commit.
    • Modified the commit button text and icon to reflect the 'Commit & Push' action.
    • Adjusted keyboard shortcut logic to account for the push-after-commit state and remote selection.
  • apps/ui/src/components/views/board-view/dialogs/create-pr-dialog.tsx
    • Introduced state and UI for selecting a 'target remote' for PR creation.
    • Enhanced remote fetching to include branch information for each remote.
    • Implemented filtering of base branches based on the selected target remote.
    • Added logic to reset the base branch selection when the target remote changes.
    • Updated the dialog description to dynamically reflect whether changes need to be pushed.
    • Adjusted visibility of the push remote selector to only appear when there are commits to push.
  • apps/ui/src/components/views/board-view/dialogs/create-worktree-dialog.tsx
    • Added a collapsible 'Base Branch' section with a branch autocomplete component.
    • Implemented fetching of local and remote branches for base branch selection.
    • Updated worktree creation to use the selected base branch.
    • Added UI indicators for remote base branches and their auto-fetch behavior.
    • Adjusted dialog width and loading messages for better user feedback.
  • apps/ui/src/components/views/board-view/dialogs/merge-worktree-dialog.tsx
    • Updated UI text from 'Merge Branch' to 'Integrate Branch' in dialog title and toast messages.
  • apps/ui/src/components/views/board-view/worktree-panel/components/worktree-actions-dropdown.tsx
    • Changed 'Merge Branch' menu item to 'Integrate Branch'.
    • Modified logic to always show the 'Create PR' option if no PR exists, regardless of uncommitted changes.
  • apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx
    • Updated internal state and comments from 'Merge Branch Dialog' to 'Integrate Branch Dialog'.
  • apps/ui/src/lib/electron.ts
    • Added targetRemote property to the CreatePROptions interface.
  • apps/ui/src/store/app-store.ts
    • Implemented a guard to prevent duplicate auto_mode_feature_start events from inflating the running task count.
  • apps/ui/src/types/electron.d.ts
    • Added targetRemote property to the CreatePROptions interface.
  • package-lock.json
    • Updated dependencies to include @automaker/platform.
Activity
  • No human activity (comments, reviews) was observed on this pull request yet.
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 significant enhancements to Git operations within the application, primarily focusing on improved flexibility for creating pull requests, worktrees, and handling merge/rebase operations. Key changes include adding a targetRemote option for PR creation, allowing users to specify a different remote for the PR target than for pushing, and updating the UI to support this selection. The create worktree functionality now supports specifying a base branch, including remote branches, with the system automatically fetching from the remote if needed. Merge and rebase operations have been enhanced to include an optional remote parameter, ensuring up-to-date refs by fetching from the specified remote before execution. The UI for committing changes now includes an option to 'Push to remote after commit' with a remote selector. Additionally, several UI texts have been updated to use 'Integrate Branch' instead of 'Merge Branch' for consistency, and the 'Create PR' option is now always visible if no PR exists. The BranchAutocomplete component was also updated to allow more control over branch creation and empty messages.

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: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/ui/src/components/views/board-view/dialogs/merge-worktree-dialog.tsx (1)

110-110: ⚠️ Potential issue | 🟡 Minor

Incomplete rename — several user-facing strings still say "merge/merged/merging".

The title (line 277) and error toasts (lines 135, 160) were updated to "Integrate Branch" / "integrate branch", but the following strings were left unchanged, producing an inconsistent UX:

Line Still reads
110 toast.success('Branch merged to ${targetBranch}')
108–109 success description: "has been merged into"
282 dialog body: Merge <code>…</code> into:
330 checkbox label: "Delete worktree and branch after merging"
356 spinner text: "Merging…"
361 button label: "Merge"
🔤 Proposed fix to align all user-facing strings
- toast.success(`Branch merged to ${targetBranch}`, { description });
+ toast.success(`Branch integrated into ${targetBranch}`, { description });
- ? `Branch "${worktree.branch}" has been merged into "${targetBranch}" and the worktree and branch were deleted`
- : `Branch "${worktree.branch}" has been merged into "${targetBranch}"`;
+ ? `Branch "${worktree.branch}" has been integrated into "${targetBranch}" and the worktree and branch were deleted`
+ : `Branch "${worktree.branch}" has been integrated into "${targetBranch}"`;
- <span className="block">
-   Merge <code className="font-mono bg-muted px-1 rounded">{worktree.branch}</code>{' '}
-   into:
- </span>
+ <span className="block">
+   Integrate <code className="font-mono bg-muted px-1 rounded">{worktree.branch}</code>{' '}
+   into:
+ </span>
- Delete worktree and branch after merging
+ Delete worktree and branch after integrating
- <Spinner size="sm" variant="foreground" className="mr-2" />
- Merging...
+ <Spinner size="sm" variant="foreground" className="mr-2" />
+ Integrating...
- <GitMerge className="w-4 h-4 mr-2" />
- Merge
+ <GitMerge className="w-4 h-4 mr-2" />
+ Integrate

Also applies to: 135-135, 160-160, 277-277, 282-282, 330-330, 356-356, 361-361

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/src/components/views/board-view/dialogs/merge-worktree-dialog.tsx` at
line 110, Update all remaining user-facing "merge" wording to the new
"integrate" terminology inside the MergeWorktreeDialog component: change the
toast.success call (toast.success(`Branch merged to ${targetBranch}`, {
description })) to use "integrated" (e.g., `Branch integrated into
${targetBranch}` and description "has been integrated into"), update the dialog
body text that currently reads "Merge <code>…</code> into:" to "Integrate
<code>…</code> into:", change the checkbox label "Delete worktree and branch
after merging" to "Delete worktree and branch after integrating", change the
spinner text "Merging…" to "Integrating…", and change the action button label
"Merge" to "Integrate" so all user-facing strings in MergeWorktreeDialog
consistently use "integrate/integrated".
🧹 Nitpick comments (4)
apps/ui/src/store/app-store.ts (1)

945-951: LGTM — duplicate-task guard is correct and consistent.

The Array.includes() check before appending cleanly prevents inflated runningTasks counts when auto_mode_feature_start fires from multiple services. Returning the same state reference is the right Zustand no-op idiom (already used at lines 968 and 984 in removeRunningTask/clearRunningTasks), so no spurious re-renders are triggered.

One optional structural alternative worth considering in a future refactor: storing runningTasks as a Set<string> instead of string[] would make uniqueness a structural invariant rather than a guarded invariant, and would reduce the check to O(1). This would require a corresponding type change in @automaker/types, so it's out of scope for this fix.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/src/store/app-store.ts` around lines 945 - 951, The duplicate-task
guard using current.runningTasks.includes(taskId) in the auto_mode_feature_start
handler is correct — no code change required; keep returning the same state
reference to avoid re-renders. If you want a future refactor, consider changing
runningTasks from string[] to Set<string> (update the runningTasks type in
`@automaker/types` and adjust usages in removeRunningTask and clearRunningTasks
accordingly) to make uniqueness O(1).
apps/server/src/routes/worktree/routes/create.ts (1)

209-229: Good: targeted fetch of only the specific branch ref.

Unlike the broader fetch <remote> in merge/rebase services, this fetches only remoteBranchInfo.branch from the remote, which is more efficient for large repos. The non-fatal pattern is consistent with the rest of the PR.

One minor note: line 26 imports execGitCommand from the local ../../../lib/git.js while line 18 imports isGitRepo from @automaker/git-utils. As per coding guidelines, git operations should use @automaker/git-utils. This is a pre-existing pattern in the file, but worth consolidating when convenient.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/routes/worktree/routes/create.ts` around lines 209 - 229,
This code uses a local execGitCommand (imported from ../../../lib/git.js) while
other git helpers like isGitRepo come from `@automaker/git-utils`; replace the
local execGitCommand usage/import with the corresponding git utility from
`@automaker/git-utils` (update the import and call site where execGitCommand is
invoked inside the block that also uses detectRemoteBranch) so all git
operations (e.g., the fetch of remoteBranchInfo.remote/remoteBranchInfo.branch)
consistently use the shared `@automaker/git-utils` implementation and remove the
duplicate local import.
apps/ui/src/components/views/board-view/dialogs/create-pr-dialog.tsx (1)

140-146: Effect resets baseBranch correctly but includes it in its own dependency array — verify no excessive re-renders.

The effect reads and conditionally writes baseBranch, and lists it as a dependency. This is functionally correct (a second trigger will no-op because the new value IS in branches), but it causes one extra render cycle each time the reset fires. Consider using a functional check that doesn't require baseBranch in deps, e.g. using setBaseBranch(prev => ...).

Optional: remove baseBranch from deps
-  useEffect(() => {
-    if (branches.length > 0 && baseBranch && !branches.includes(baseBranch)) {
-      // Current base branch is not in the filtered list — pick the best match
-      const mainBranch = branches.find((b) => b === 'main' || b === 'master');
-      setBaseBranch(mainBranch || branches[0]);
-    }
-  }, [branches, baseBranch]);
+  useEffect(() => {
+    if (branches.length > 0) {
+      setBaseBranch((prev) => {
+        if (prev && branches.includes(prev)) return prev;
+        return branches.find((b) => b === 'main' || b === 'master') || branches[0];
+      });
+    }
+  }, [branches]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/src/components/views/board-view/dialogs/create-pr-dialog.tsx` around
lines 140 - 146, The effect currently depends on both branches and baseBranch,
causing an extra render when it reads then writes baseBranch; change the effect
to depend only on branches and use the functional updater form of setBaseBranch
to check previous baseBranch (setBaseBranch(prev => { if (prev &&
branches.includes(prev)) return prev; const mainBranch = branches.find(b => b
=== 'main' || b === 'master'); return mainBranch || branches[0]; })) so you can
remove baseBranch from the dependency array; update the useEffect that
references branches, baseBranch and setBaseBranch accordingly to avoid the extra
render.
apps/ui/src/components/views/board-view/dialogs/commit-worktree-dialog.tsx (1)

42-45: RemoteInfo is duplicated across dialog files.

The same { name: string; url: string } shape exists in create-pr-dialog.tsx (with an extra optional branches field). Consider extracting a shared RemoteInfo type to reduce duplication. Low priority since the type is small.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/src/components/views/board-view/dialogs/commit-worktree-dialog.tsx`
around lines 42 - 45, The RemoteInfo interface in commit-worktree-dialog.tsx
duplicates the same shape used in create-pr-dialog.tsx (the other file adds an
optional branches field); extract a single shared type (e.g., export type
RemoteInfo = { name: string; url: string; branches?: string[] }) into a common
module (a shared types file or a sibling index) and import it into both
CommitWorktreeDialog and CreatePrDialog components, updating references to
RemoteInfo in both files to use the centralized exported type.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/server/src/routes/worktree/routes/create-pr.ts`:
- Around line 236-257: The handler currently ignores a client-supplied
targetRemote when parsedRemotes.get(targetRemote) is undefined; add an explicit
validation step before the override block: if targetRemote is provided (and
passes isValidRemoteName earlier) but parsedRemotes.has(targetRemote) is false,
immediately return a 400 error response indicating the requested targetRemote is
not found. Implement this check right before the if (targetRemote &&
parsedRemotes.size > 0) block (referencing targetRemote, parsedRemotes, and
parsedRemotes.get) so the endpoint fails fast instead of silently falling back
to auto-detection.

In `@apps/server/src/services/merge-service.ts`:
- Around line 93-104: Validate the remote parameter before calling
execGitCommand(['fetch', remote], projectPath) to prevent git option injection:
import and use the existing isValidRemoteName helper (from
apps/server/src/routes/worktree/common.ts) in merge-service.ts,
rebase-service.ts, and pull-service.ts, check the remote value (or default) with
isValidRemoteName and if it fails return/throw the same kind of error you use
for invalid branch names (or log and abort the fetch), ensuring you validate
before invoking execGitCommand so only safe remote names are passed.

In `@apps/ui/src/components/views/board-view/dialogs/commit-worktree-dialog.tsx`:
- Around line 369-394: The dialog can be dismissed mid-push; prevent that by
short-circuiting any external open-change requests while a push is in progress:
when using the CommitWorktreeDialog's local state (setIsPushing / isPushing)
wrap or replace the onOpenChange handler so it no-ops while isPushing is true
(i.e., ignore calls to onOpenChange(false) from overlay clicks or Escape during
a push), and ensure the UI still clears isPushing in the push finally block and
then calls onOpenChange(false) only after push completion (hook into the
existing push logic that uses api.worktree.push, pushAfterCommit,
selectedRemote, onCommitted).

In `@apps/ui/src/components/views/board-view/dialogs/create-worktree-dialog.tsx`:
- Around line 131-132: The fetch error for api.worktree.listBranches in
CreateWorktreeDialog is currently swallowed, leaving availableBranches empty
and, because BranchAutocomplete is rendered with allowCreate={false}, the user
cannot enter a custom branch; update the catch block in the effect that calls
api.worktree.listBranches to (1) set a visible inline error state (e.g.,
branchFetchError) or a short message to show to the user, and (2) populate
availableBranches with a safe fallback (e.g., ['main'] or previously cached
branches) and/or set a local flag to enable allowCreate as a fallback; then pass
that flag or error state into the BranchAutocomplete render (replace
allowCreate={false} with allowCreate={enableCreateFallback}) so the user can
type a custom branch when the fetch fails and display the error inline near the
autocomplete.

In
`@apps/ui/src/components/views/board-view/worktree-panel/components/worktree-actions-dropdown.tsx`:
- Around line 220-223: Update the stale JSX comment that still describes the old
behavior so it matches the new logic where showCreatePR is computed as "!hasPR"
(i.e., Create PR is shown for any worktree with no existing PR); locate the
comment adjacent to the worktree actions JSX that references PR visibility (the
block related to showCreatePR, showPRInfo, hasChangesSectionContent) and replace
the outdated text ("Show PR option for non-primary worktrees, or primary
worktree with changes") with a concise note like "Show Create PR when no
existing PR is linked" to reflect the new behavior of showCreatePR, leaving the
surrounding logic (showCreatePR, showPRInfo, hasChangesSectionContent)
unchanged.

---

Outside diff comments:
In `@apps/ui/src/components/views/board-view/dialogs/merge-worktree-dialog.tsx`:
- Line 110: Update all remaining user-facing "merge" wording to the new
"integrate" terminology inside the MergeWorktreeDialog component: change the
toast.success call (toast.success(`Branch merged to ${targetBranch}`, {
description })) to use "integrated" (e.g., `Branch integrated into
${targetBranch}` and description "has been integrated into"), update the dialog
body text that currently reads "Merge <code>…</code> into:" to "Integrate
<code>…</code> into:", change the checkbox label "Delete worktree and branch
after merging" to "Delete worktree and branch after integrating", change the
spinner text "Merging…" to "Integrating…", and change the action button label
"Merge" to "Integrate" so all user-facing strings in MergeWorktreeDialog
consistently use "integrate/integrated".

---

Duplicate comments:
In `@apps/server/src/services/rebase-service.ts`:
- Around line 72-83: The code passes options?.remote directly into
execGitCommand(['fetch', remote]) which allows a value starting with '-' to be
interpreted as a flag; update the rebase logic to validate/sanitize the remote
variable (the remote assignment and the fetch call using execGitCommand) by
checking if options?.remote exists and does not startWith('-'); if it does start
with '-', log a warning via logger.warn and fall back to the safe default
'origin' (or reject the value), then call execGitCommand(['fetch', remoteSafe],
worktreePath); ensure the same guarded behavior mirrors the fix used in
merge-service.ts.

---

Nitpick comments:
In `@apps/server/src/routes/worktree/routes/create.ts`:
- Around line 209-229: This code uses a local execGitCommand (imported from
../../../lib/git.js) while other git helpers like isGitRepo come from
`@automaker/git-utils`; replace the local execGitCommand usage/import with the
corresponding git utility from `@automaker/git-utils` (update the import and call
site where execGitCommand is invoked inside the block that also uses
detectRemoteBranch) so all git operations (e.g., the fetch of
remoteBranchInfo.remote/remoteBranchInfo.branch) consistently use the shared
`@automaker/git-utils` implementation and remove the duplicate local import.

In `@apps/ui/src/components/views/board-view/dialogs/commit-worktree-dialog.tsx`:
- Around line 42-45: The RemoteInfo interface in commit-worktree-dialog.tsx
duplicates the same shape used in create-pr-dialog.tsx (the other file adds an
optional branches field); extract a single shared type (e.g., export type
RemoteInfo = { name: string; url: string; branches?: string[] }) into a common
module (a shared types file or a sibling index) and import it into both
CommitWorktreeDialog and CreatePrDialog components, updating references to
RemoteInfo in both files to use the centralized exported type.

In `@apps/ui/src/components/views/board-view/dialogs/create-pr-dialog.tsx`:
- Around line 140-146: The effect currently depends on both branches and
baseBranch, causing an extra render when it reads then writes baseBranch; change
the effect to depend only on branches and use the functional updater form of
setBaseBranch to check previous baseBranch (setBaseBranch(prev => { if (prev &&
branches.includes(prev)) return prev; const mainBranch = branches.find(b => b
=== 'main' || b === 'master'); return mainBranch || branches[0]; })) so you can
remove baseBranch from the dependency array; update the useEffect that
references branches, baseBranch and setBaseBranch accordingly to avoid the extra
render.

In `@apps/ui/src/store/app-store.ts`:
- Around line 945-951: The duplicate-task guard using
current.runningTasks.includes(taskId) in the auto_mode_feature_start handler is
correct — no code change required; keep returning the same state reference to
avoid re-renders. If you want a future refactor, consider changing runningTasks
from string[] to Set<string> (update the runningTasks type in `@automaker/types`
and adjust usages in removeRunningTask and clearRunningTasks accordingly) to
make uniqueness O(1).

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: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/ui/src/components/views/board-view/dialogs/merge-worktree-dialog.tsx (1)

311-311: ⚠️ Potential issue | 🟡 Minor

Missed text update: "before merging" should be "before integrating".

This warning text was not updated along with the rest of the dialog, leaving a user-visible inconsistency.

📝 Proposed fix
-                    commit or discard them before merging.
+                    commit or discard them before integrating.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/src/components/views/board-view/dialogs/merge-worktree-dialog.tsx` at
line 311, The warning string "commit or discard them before merging." in the
MergeWorktreeDialog component should be updated to "commit or discard them
before integrating." Locate the message text in
apps/ui/src/components/views/board-view/dialogs/merge-worktree-dialog.tsx
(inside the MergeWorktreeDialog render/JSX) and replace the old phrase with the
new one so the dialog matches the updated wording elsewhere.
apps/server/src/routes/worktree/routes/create-pr.ts (1)

296-307: ⚠️ Potential issue | 🟡 Minor

Add validation for baseBranch parameter or percent-encode it in the browser URL.

baseBranch is an optional request parameter (line 32) that flows directly into the URL at line 303/306 without validation or encoding. While branchName is validated with isValidBranchName() (which rejects special characters like #, %, spaces), baseBranch lacks this validation. If a user passes baseBranch containing characters like # or %, the GitHub compare URL would be malformed.

The fix is either to:

  1. Apply isValidBranchName() validation to baseBranch before use, or
  2. Percent-encode base when constructing the URL (consistent with title/body encoding at lines 298–299)

Note: upstreamRepo and originOwner are derived from git remote config (parsed from git URLs), not directly from user input, so they are less at risk despite targetRemote being user-supplied.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/routes/worktree/routes/create-pr.ts` around lines 296 - 307,
baseBranch (used as variable base) is currently inserted into the GitHub compare
URL without validation or encoding; either run it through the same
isValidBranchName() check used for branchName or percent-encode it with
encodeURIComponent before building browserUrl. Update the code that constructs
browserUrl (the branches comparison string that uses base and branchName in both
the upstreamRepo/originOwner and regular-repo branches) to use the
validated/encoded base value so special chars like # or % can’t produce a
malformed URL; keep encodedTitle/encodedBody usage as-is.
🧹 Nitpick comments (3)
apps/ui/src/components/views/board-view/dialogs/merge-worktree-dialog.tsx (2)

131-133: Conflict resolution dialog/toasts still use "Merge" terminology — consider aligning with "Integrate".

The success path and most button labels now say "integrate", but the conflict toast titles ("Merge conflicts detected"), the conflict dialog title ("Merge Conflicts Detected"), and the description text ("There are conflicts when merging") were left untouched. Whether this is intentional (preserving the technical git term) or an oversight is worth confirming. If the intent is a full UX rebrand to "Integrate", these should also be updated.

Also applies to: 156-158, 192-199

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/src/components/views/board-view/dialogs/merge-worktree-dialog.tsx`
around lines 131 - 133, Update the remaining UI strings that use "Merge" to
instead use "Integrate" within the MergeWorktreeDialog component: replace the
toast.error title 'Merge conflicts detected', the dialog title 'Merge Conflicts
Detected', and any description text like 'There are conflicts when merging'
(also the occurrences around the blocks noted at 156-158 and 192-199) so they
read with "Integrate" (e.g., 'Integrate conflicts detected', 'Integrate
Conflicts Detected', 'There are conflicts when integrating'); ensure you update
the toast.error call and the dialog title/description props in
MergeWorktreeDialog so wording is consistent with the rest of the success/button
labels.

27-28: Align prop naming and JSDoc with "Integrate" terminology used in UI.

The component displays "Branch integrated into..." in toast messages and the button says "Integrate Branch", but the prop callback is onMerged with JSDoc saying "merge is successful". This terminology mismatch is visible to callers. Rename the prop to onIntegrated, the parameter to integratedWorktree, and update the JSDoc to reference "integration" for consistency.

Call sites using onMerged appear in worktree-panel.tsx and will need updates.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/src/components/views/board-view/dialogs/merge-worktree-dialog.tsx`
around lines 27 - 28, Rename the prop and JSDoc to use "Integrate" terminology:
change the prop signature from onMerged: (mergedWorktree: WorktreeInfo,
deletedBranch: boolean) => void to onIntegrated: (integratedWorktree:
WorktreeInfo, deletedBranch: boolean) => void and update the JSDoc comment to
reference "integration" (e.g., "Called when integration is successful.
integratedWorktree indicates the integrated worktree and deletedBranch indicates
if the branch was also deleted."). Update all call sites that use onMerged
(e.g., where worktree-panel.tsx invokes the callback) to call onIntegrated and
rename the first argument from mergedWorktree to integratedWorktree to match the
new signature.
apps/server/src/services/merge-service.ts (1)

40-42: JSDoc @param options is stale — missing remote.

📝 Proposed update
- * `@param` options - Merge options (squash, message, deleteWorktreeAndBranch)
+ * `@param` options - Merge options (squash, message, deleteWorktreeAndBranch, remote)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/services/merge-service.ts` around lines 40 - 42, The JSDoc
for the merge function in merge-service.ts is out of date: the `@param` options
docblock does not document the options.remote property; update the JSDoc for the
function that accepts targetBranch and options (the merge service method in this
file) to include a line documenting `@param` options.remote (describe its purpose,
type, and default behavior) alongside the existing `@param` options entries so the
docs reflect the actual parameter shape.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/server/src/routes/worktree/routes/create-pr.ts`:
- Around line 244-269: When targetRemote !== pushRemote and parsedRemotes
contains targetRemote but parsedRemotes.get(pushRemote) returns undefined
(pushInfo undefined), the code ends up setting originOwner to null and later
builds an incorrect browser URL; update the cross-remote branch in the create-pr
handler to detect missing pushInfo and fail fast: check pushInfo after const
pushInfo = parsedRemotes.get(pushRemote) and if undefined either throw / return
an error response (or at minimum processLogger.warn and return an error) instead
of continuing, so you don't set originOwner/upstreamRepo/repoUrl incorrectly;
reference targetRemote, pushRemote, parsedRemotes, targetInfo, pushInfo,
pushOwner, originOwner, upstreamRepo, and repoUrl when adding the defensive
check and error handling.

In `@apps/server/src/services/merge-service.ts`:
- Line 10: The service imports isValidRemoteName from a route module which
violates the shared-package rule; extract isValidRemoteName (and any related
validation) into a canonical shared location (e.g., `@automaker/utils` or
apps/server/src/services/common.ts), update merge-service.ts to import it from
that shared module instead of '../routes/worktree/common.js', and update any
route handlers to also import from the new shared location; additionally, update
the JSDoc on the merge function (the comment around lines ~40–41 in
merge-service.ts) to list all options including the newly added remote parameter
(e.g., "squash, message, deleteWorktreeAndBranch, remote").
- Around line 94-105: The current logic in merge-service that assigns const
rawRemote = options?.remote || 'origin' and then falls back to remote = 'origin'
when isValidRemoteName(rawRemote) is false should instead reject invalid remotes
like branch validation does: remove the silent fallback and return an error when
isValidRemoteName(rawRemote) fails (include the invalid value in the error/log),
so the caller knows their remote was rejected; update the code paths that use
remote (the pre-merge fetch) to only proceed when a validated remote is present,
and apply the same change to the identical logic in rebase-service (the
rawRemote/remote + isValidRemoteName check) so both services consistently
hard-fail on invalid remote names.

In `@apps/ui/src/components/views/board-view/dialogs/commit-worktree-dialog.tsx`:
- Around line 728-730: The span showing the remote URL is currently inline so
Tailwind's truncate and max-w-[200px] don't apply; update the element used in
CommitWorktreeDialog (the <span> containing {remote.url}) to be an inline-block
(e.g., add the "inline-block" utility or convert to a <div> with inline-block
behavior) so truncate and max-w-[200px] take effect and long URLs are
ellipsized.
- Around line 213-250: The effect that fetches remotes (the useEffect guarded by
pushAfterCommit, worktree, remotes.length) can loop forever when listRemotes
returns an empty array; add a new boolean state remotesFetched (with setter
setRemotesFetched) and change the effect guard to run only when !remotesFetched
instead of remotes.length === 0; inside fetchRemotes setRemotesFetched(true)
after any successful call (even if result.remotes is empty) and still call
setRemotes(result.remotes) as before; also reset remotesFetched to false
wherever you currently reset remotes when the dialog opens (the dialog-open
effect) so fetching will re-run on new dialogs.

In `@apps/ui/src/components/views/board-view/dialogs/create-worktree-dialog.tsx`:
- Around line 117-147: fetchBranches currently never clears branchFetchError on
a successful call and also silently ignores API responses where
branchResult.success === false; update fetchBranches to (1) call
setBranchFetchError(null) when branchResult.success and branchResult.result are
present and you successfully call setAvailableBranches, and (2) treat
branchResult.success === false as an error path: setBranchFetchError to a
user-facing message and setAvailableBranches to the safe fallback [{ name:
'main', isRemote: false }], so the UI shows the fallback and allowCreate
behavior consistently; use the existing identifiers fetchBranches, branchResult,
setBranchFetchError, setAvailableBranches and setIsLoadingBranches to locate and
implement these changes.

In `@apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx`:
- Around line 476-486: The fire-and-forget call to api.worktree.listRemotes
(obtained via getHttpApiClient()) lacks a .catch, causing unhandled promise
rejections; wrap the promise returned by api.worktree.listRemotes(worktree.path)
with a .catch that swallows or logs the error (using console.warn or the app
logger) and does not rethrow, while keeping the existing success handling that
calls setRemotesCache for worktree.path — ensure the catch is attached to the
same promise chain so any network/server rejection is suppressed.

---

Outside diff comments:
In `@apps/server/src/routes/worktree/routes/create-pr.ts`:
- Around line 296-307: baseBranch (used as variable base) is currently inserted
into the GitHub compare URL without validation or encoding; either run it
through the same isValidBranchName() check used for branchName or percent-encode
it with encodeURIComponent before building browserUrl. Update the code that
constructs browserUrl (the branches comparison string that uses base and
branchName in both the upstreamRepo/originOwner and regular-repo branches) to
use the validated/encoded base value so special chars like # or % can’t produce
a malformed URL; keep encodedTitle/encodedBody usage as-is.

In `@apps/ui/src/components/views/board-view/dialogs/merge-worktree-dialog.tsx`:
- Line 311: The warning string "commit or discard them before merging." in the
MergeWorktreeDialog component should be updated to "commit or discard them
before integrating." Locate the message text in
apps/ui/src/components/views/board-view/dialogs/merge-worktree-dialog.tsx
(inside the MergeWorktreeDialog render/JSX) and replace the old phrase with the
new one so the dialog matches the updated wording elsewhere.

---

Duplicate comments:
In
`@apps/ui/src/components/views/board-view/worktree-panel/components/worktree-actions-dropdown.tsx`:
- Around line 912-913: The JSX comment above the conditional rendering
referencing showCreatePR is stale and describes the old logic; update or remove
it so it matches the current condition where showCreatePR is simply !hasPR.
Locate the JSX comment near the conditional that renders when showCreatePR is
true in worktree-actions-dropdown.tsx and either rewrite it to reflect the new
meaning (e.g., "Show PR option when there is no existing PR (showCreatePR ===
!hasPR)") or remove the comment altogether to avoid confusion.

---

Nitpick comments:
In `@apps/server/src/services/merge-service.ts`:
- Around line 40-42: The JSDoc for the merge function in merge-service.ts is out
of date: the `@param` options docblock does not document the options.remote
property; update the JSDoc for the function that accepts targetBranch and
options (the merge service method in this file) to include a line documenting
`@param` options.remote (describe its purpose, type, and default behavior)
alongside the existing `@param` options entries so the docs reflect the actual
parameter shape.

In `@apps/ui/src/components/views/board-view/dialogs/merge-worktree-dialog.tsx`:
- Around line 131-133: Update the remaining UI strings that use "Merge" to
instead use "Integrate" within the MergeWorktreeDialog component: replace the
toast.error title 'Merge conflicts detected', the dialog title 'Merge Conflicts
Detected', and any description text like 'There are conflicts when merging'
(also the occurrences around the blocks noted at 156-158 and 192-199) so they
read with "Integrate" (e.g., 'Integrate conflicts detected', 'Integrate
Conflicts Detected', 'There are conflicts when integrating'); ensure you update
the toast.error call and the dialog title/description props in
MergeWorktreeDialog so wording is consistent with the rest of the success/button
labels.
- Around line 27-28: Rename the prop and JSDoc to use "Integrate" terminology:
change the prop signature from onMerged: (mergedWorktree: WorktreeInfo,
deletedBranch: boolean) => void to onIntegrated: (integratedWorktree:
WorktreeInfo, deletedBranch: boolean) => void and update the JSDoc comment to
reference "integration" (e.g., "Called when integration is successful.
integratedWorktree indicates the integrated worktree and deletedBranch indicates
if the branch was also deleted."). Update all call sites that use onMerged
(e.g., where worktree-panel.tsx invokes the callback) to call onIntegrated and
rename the first argument from mergedWorktree to integratedWorktree to match the
new signature.

Comment on lines 94 to 105
// Validate and sanitize the remote name to prevent git option injection.
// If the caller supplied an invalid remote name, fall back to 'origin' and
// log a warning rather than passing a potentially dangerous value to git.
const rawRemote = options?.remote || 'origin';
let remote = rawRemote;
if (!isValidRemoteName(rawRemote)) {
logger.warn('Invalid remote name supplied to merge-service; falling back to "origin"', {
remote: rawRemote,
projectPath,
});
remote = 'origin';
}
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, search for isValidRemoteName implementation to verify it rejects injection patterns
rg -n "isValidRemoteName" --type=ts -A 10 -B 2

Repository: AutoMaker-Org/automaker

Length of output: 10711


🏁 Script executed:

#!/bin/bash
# Search for branch name validation logic in merge-service.ts to understand the "above" reference
fd merge-service.ts --type=ts -x cat -n {} | head -120

Repository: AutoMaker-Org/automaker

Length of output: 236


🏁 Script executed:

#!/bin/bash
# Check rebase-service for similar patterns with remote/branch validation
rg -n "rebase-service" --type=ts -l

Repository: AutoMaker-Org/automaker

Length of output: 251


🏁 Script executed:

#!/bin/bash
# Read the full merge-service.ts file to see branch validation and return types
cat -n apps/server/src/services/merge-service.ts | head -150

Repository: AutoMaker-Org/automaker

Length of output: 6374


🏁 Script executed:

#!/bin/bash
# Also check the MergeServiceResult type definition and how errors are returned
rg -n "MergeServiceResult\|interface.*Result\|type.*Result" apps/server/src/services/merge-service.ts -A 5

Repository: AutoMaker-Org/automaker

Length of output: 49


Silent remote fallback to 'origin' masks configuration intent and can fetch from wrong ref namespace in multi-remote scenarios.

The code silently substitutes 'origin' when options.remote fails validation, without returning an error. This inconsistency contrasts sharply with the hard-reject pattern used for branch name validation above (lines 61–72), where invalid branchName or targetBranch return an error immediately.

In fork-PR workflows, silently using 'origin' instead of a caller-specified remote (e.g., 'upstream') means the pre-merge fetch pulls from the wrong remote namespace, causing the merge to proceed against stale remote-tracking branches without the caller knowing their remote choice was ignored.

Align with the existing branch-validation pattern by returning a hard error:

Suggested fix
-  const rawRemote = options?.remote || 'origin';
-  let remote = rawRemote;
-  if (!isValidRemoteName(rawRemote)) {
-    logger.warn('Invalid remote name supplied to merge-service; falling back to "origin"', {
-      remote: rawRemote,
-      projectPath,
-    });
-    remote = 'origin';
-  }
+  const remote = options?.remote || 'origin';
+  if (!isValidRemoteName(remote)) {
+    return {
+      success: false,
+      error: `Invalid remote name: "${remote}"`,
+    };
+  }

Note: Apply the same fix to rebase-service.ts (lines 76–84), which uses an identical fallback pattern.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/services/merge-service.ts` around lines 94 - 105, The current
logic in merge-service that assigns const rawRemote = options?.remote ||
'origin' and then falls back to remote = 'origin' when
isValidRemoteName(rawRemote) is false should instead reject invalid remotes like
branch validation does: remove the silent fallback and return an error when
isValidRemoteName(rawRemote) fails (include the invalid value in the error/log),
so the caller knows their remote was rejected; update the code paths that use
remote (the pre-merge fetch) to only proceed when a validated remote is present,
and apply the same change to the identical logic in rebase-service (the
rawRemote/remote + isValidRemoteName check) so both services consistently
hard-fail on invalid remote names.

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: 5

🧹 Nitpick comments (10)
apps/ui/public/sw.js (2)

320-371: Cache-first navigation strategy is well-structured for SPA use.

The approach is sound for an SPA where the shell rarely changes meaningfully between deploys. The preload response is correctly consumed in only one branch (cached path vs. first-visit path), and the / fallback on line 334 correctly handles client-side routing.

One minor note: on line 362, cache.put is fire-and-forget (not awaited, not wrapped in waitUntil). If the SW is terminated very quickly after the first visit, the shell might not be cached until the next navigation. In practice this is unlikely to matter since the SW stays alive while streaming the response, but wrapping it in event.waitUntil would be more robust — consistent with how you handle it on line 353.

-            cache.put(event.request, response.clone());
+            event.waitUntil(cache.put(event.request, response.clone()));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/public/sw.js` around lines 320 - 371, The first-visit branch in the
navigation handler uses cache.put(event.request, response.clone()) as
fire-and-forget inside the try block of the event.respondWith handler; wrap that
cache update in event.waitUntil (or otherwise ensure the promise is observed) so
the Service Worker isn't terminated before the shell is cached—mirror the
pattern used by the updateCache() helper (and the cached-path where you call
event.waitUntil(updateCache())), targeting the cache.put call in the anonymous
async passed to event.respondWith.

253-264: Background fetch may be terminated before cache update completes on mobile.

When the 2-second timeout wins the race (line 261) and the stale cachedResponse is returned, fetchPromise continues in the background without event.waitUntil(). On mobile — the exact environment this code targets — the SW can be killed aggressively, losing the cache update.

Consider extending the event's lifetime so the background fetch completes:

Suggested fix
          if (cachedResponse) {
-           // Give network a brief window (2s) to respond, otherwise use stale cache
-           const networkResult = await Promise.race([
+           const fetchWithCacheUpdate = fetchPromise.catch(() => null);
+           // Keep SW alive until the background fetch completes
+           event.waitUntil(fetchWithCacheUpdate);
+           // Give network a brief window (2s) to respond, otherwise use stale cache
+           const networkResult = await Promise.race([
              fetchPromise,
              new Promise((resolve) => setTimeout(() => resolve(null), 2000)),
            ]);
            return networkResult || cachedResponse;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/public/sw.js` around lines 253 - 264, When the 2s timeout wins and
cachedResponse is returned, the ongoing fetchPromise (from startNetworkFetch)
must be kept alive via the service worker event's lifetime so the cache update
can complete; update the branch that handles cachedResponse to call
event.waitUntil(fetchPromise.then(...).catch(...)) (or .finally(...)) so the
background fetch continues to completion and updates the cache even after
returning cachedResponse, referencing fetchPromise and startNetworkFetch to
locate the code and using event.waitUntil to extend the worker lifetime.
apps/ui/src/components/ui/git-diff-panel.tsx (1)

927-976: Floating Promises in onClick handlers + duplicated staging controls block

Lines 956 and 967 call async functions without void:

onClick={() => handleUnstageFile(file.path)}   // line 956
onClick={() => handleStageFile(file.path)}      // line 967

This is inconsistent with onClick={() => void loadDiffs()} used at lines 768 and 872 in the same file. While internal try/catch prevents crashes, the floating Promise is a lint hazard and breaks the codebase's established pattern.

Additionally, the staging controls block here (lines 948–974) is nearly identical to FileDiffSection's staging block (lines 391–425). Even as a "safety net," diverging copies raise maintenance risk when the staging UI evolves.

Proposed fixes

Fix the unhandled Promises:

- onClick={() => handleUnstageFile(file.path)}
+ onClick={() => void handleUnstageFile(file.path)}
...
- onClick={() => handleStageFile(file.path)}
+ onClick={() => void handleStageFile(file.path)}

Long-term, consider extracting a shared FileRow component that owns the two-zone layout + staging controls, then composing it in both FileDiffSection and this fallback:

// Shared component
function FileRow({ fileIcon, filePath, stagingState, enableStaging, isStagingFile, onStage, onUnstage, children }: FileRowProps) {
  return (
    <div className="w-full px-3 py-2 flex flex-col gap-1 text-left bg-card sm:flex-row sm:items-center sm:gap-2">
      {children /* left zone (clickable or static) */}
      <div className="flex items-center gap-2 flex-shrink-0 pl-6 sm:pl-0">
        {/* shared indicators + staging controls */}
      </div>
    </div>
  );
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/src/components/ui/git-diff-panel.tsx` around lines 927 - 976, The
onClick handlers create floating Promises: change onClick={() =>
handleUnstageFile(file.path)} and onClick={() => handleStageFile(file.path)} to
the established pattern (e.g. onClick={() => void handleUnstageFile(file.path)})
so the async calls are explicitly ignored and satisfy the lint rule; then remove
the duplicated staging controls by extracting and reusing a shared component
(e.g. FileRow or FileStagingControls) used by both this fallback block and
FileDiffSection so the staging UI (stagingState, stagingInProgress,
handleStageFile, handleUnstageFile, enableStaging) is implemented once and
composed in both places.
apps/ui/src/components/views/board-view/dialogs/create-worktree-dialog.tsx (1)

195-198: isRemoteBaseBranch is always false for manually typed remote branch names.

When allowCreate={!!branchFetchError} is true (fetch failed, availableBranches is [{ name: 'main', isRemote: false }]), a user who types origin/feature manually won't see the "Remote branch — will fetch latest" hint (line 352) and the button label stays "Creating…" rather than "Fetching & Creating…". The baseBranch value is still forwarded to the API correctly, so this is a UX labeling gap only.

A simple heuristic to cover the typed case:

♻️ Suggested improvement
  const isRemoteBaseBranch = useMemo(() => {
    if (!baseBranch) return false;
-   return availableBranches.some((b) => b.name === baseBranch && b.isRemote);
+   // Prefer the authoritative list; fall back to name-based heuristic when
+   // allowCreate is active (fetch failed) and the branch wasn't in the list.
+   const fromList = availableBranches.some((b) => b.name === baseBranch && b.isRemote);
+   if (fromList) return true;
+   // Treat "remote/branch" patterns as remote when the list is unavailable.
+   const inList = availableBranches.some((b) => b.name === baseBranch);
+   return !inList && baseBranch.includes('/');
  }, [baseBranch, availableBranches]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/src/components/views/board-view/dialogs/create-worktree-dialog.tsx`
around lines 195 - 198, isRemoteBaseBranch currently only checks
availableBranches and thus returns false when the user types a remote branch
like "origin/feature"; update the isRemoteBaseBranch computation (used by
baseBranch, availableBranches, allowCreate, branchFetchError) to also detect
manually entered remote-style names by checking the baseBranch string pattern
(e.g., contains '/' or matches /^remoteName\/.+$/ or startsWith('origin/')) and
treat those as remote when availableBranches doesn't contain a match so the UI
shows the "Remote branch — will fetch latest" hint and switches the button label
to "Fetching & Creating…".
apps/server/src/services/merge-service.ts (1)

97-123: Past remote-validation issues fully addressed — one cosmetic nit.

All three concerns from prior reviews have been resolved: import now sourced from @automaker/utils, invalid remote names return a hard error (not a silent fallback), and validation precedes any git operation.

The only minor nit: const remote = rawRemote; on line 111 is a no-op alias — after the validation block, rawRemote is already the validated value. You can drop the intermediate let rawRemote / const remote split and just use a single const remote.

🔧 Suggested simplification
-  const rawRemote = options?.remote || 'origin';
-  if (!isValidRemoteName(rawRemote)) {
+  const remote = options?.remote || 'origin';
+  if (!isValidRemoteName(remote)) {
     logger.warn('Invalid remote name supplied to merge-service', {
-      remote: rawRemote,
+      remote,
       projectPath,
     });
     return {
       success: false,
-      error: `Invalid remote name: "${rawRemote}"`,
+      error: `Invalid remote name: "${remote}"`,
     };
   }
-  const remote = rawRemote;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/services/merge-service.ts` around lines 97 - 123, The const
remote = rawRemote alias is unnecessary; simplify by using a single validated
variable: replace rawRemote + remote usage with one const (e.g., const remote =
options?.remote || 'origin'), keep the isValidRemoteName(remote) check and the
logger/execGitCommand(projectPath) calls unchanged, and remove the redundant
rawRemote declaration so all subsequent references use remote directly (ensuring
logger.warn and execGitCommand still reference the same symbol).
libs/utils/src/git-validation.ts (1)

66-73: isValidRemoteName implementation — solid overall; one redundant check.

The guard !name || name.length === 0 is redundant: !name already short-circuits on an empty string (and is the only falsy value reachable from a string parameter). The name.length === 0 branch is dead code.

🔧 Suggested simplification
 export function isValidRemoteName(name: string): boolean {
-  if (!name || name.length === 0 || name.length >= MAX_BRANCH_NAME_LENGTH) return false;
+  if (!name || name.length >= MAX_BRANCH_NAME_LENGTH) return false;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/utils/src/git-validation.ts` around lines 66 - 73, The initial guard in
isValidRemoteName redundantly checks name.length === 0 after !name; remove the
dead branch so the first line becomes a single falsy/length and max-length check
(e.g. if (!name || name.length >= MAX_BRANCH_NAME_LENGTH) return false;),
leaving the other guards (startsWith '-', startsWith '.', '..', '/', '\0' and
the final regex /^[a-zA-Z0-9._-]+$/) and the MAX_BRANCH_NAME_LENGTH reference
unchanged to preserve behavior.
apps/server/src/services/rebase-service.ts (1)

72-98: Consistent with merge-service.ts; same redundant alias nit applies.

The rawRemoteconst remote = rawRemote pattern is a no-op alias identical to merge-service.ts. Collapsing it keeps the diff minimal.

🔧 Suggested simplification (mirrors merge-service fix)
-  const rawRemote = options?.remote || 'origin';
-  if (!isValidRemoteName(rawRemote)) {
+  const remote = options?.remote || 'origin';
+  if (!isValidRemoteName(remote)) {
     logger.warn('Invalid remote name supplied to rebase-service', {
-      remote: rawRemote,
+      remote,
       worktreePath,
     });
     return {
       success: false,
-      error: `Invalid remote name: "${rawRemote}"`,
+      error: `Invalid remote name: "${remote}"`,
     };
   }
-  const remote = rawRemote;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/services/rebase-service.ts` around lines 72 - 98, The code
creates a redundant alias by assigning const remote = rawRemote after validating
rawRemote; remove the no-op alias and use rawRemote directly (or rename
rawRemote to remote) to collapse the variables in rebase-service.ts—update
references to use the single name (e.g., use rawRemote in the isValidRemoteName
check and in the execGitCommand(['fetch', remote], worktreePath) call or rename
rawRemote to remote) so only one variable is used around isValidRemoteName,
rawRemote/remote, and the fetch error handling (fetchError/getErrorMessage).
apps/server/src/routes/worktree/routes/create-pr.ts (1)

23-285: Consider extracting the PR creation logic into a service.
This route is accruing more business logic; moving the new target-remote handling into a service would better align with the server layering rules.

As per coding guidelines: “Server business logic should be organized into services in the services/ directory, with Express route handlers in routes/ that delegate to services.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/routes/worktree/routes/create-pr.ts` around lines 23 - 285,
The createCreatePRHandler route contains substantial business logic around
parsing remotes and computing targetRepo/pushOwner/pushRemote/upstreamRepo (see
parsedRemotes map population and the targetRemote handling block); extract this
into a new service function (e.g., services/prService.ts export an async
function like resolvePrTarget({worktreePath, pushRemote, targetRemote})) that
accepts the repo path and remote inputs and returns structured data ({repoUrl,
targetRepo, pushOwner, upstreamRepo, originOwner, parsedRemotes}) or throws
validation errors; update createCreatePRHandler to call resolvePrTarget and
handle its returned values or errors, and move all parsing, validation
(including the targetRemote existence check) and related logging from
createCreatePRHandler into that service.
apps/ui/src/components/ui/dialog.tsx (1)

88-91: hasCustomMaxWidth string detection is fragile — consider an explicit prop.

className.includes('max-w-') works for every current consumer but is an implicit convention: any class containing the substring max-w- (e.g., a future utility like clamp-max-w-sm or a composed class) would inadvertently suppress the sm:max-w-2xl default. An explicit opt-out prop (noDefaultMaxWidth?: boolean or similar) expresses intent unambiguously.

♻️ Suggested refactor
 export type DialogContentProps = Omit<
   React.ComponentProps<typeof DialogPrimitive.Content>,
   'ref'
 > & {
   showCloseButton?: boolean;
   compact?: boolean;
+  noDefaultMaxWidth?: boolean;
 };

 const DialogContent = React.forwardRef<HTMLDivElement, DialogContentProps>(
-  ({ className, children, showCloseButton = true, compact = false, ...props }, ref) => {
-    const hasCustomMaxWidth = typeof className === 'string' && className.includes('max-w-');
+  ({ className, children, showCloseButton = true, compact = false, noDefaultMaxWidth = false, ...props }, ref) => {
     return (
       ...
-      compact ? 'max-w-4xl p-4' : !hasCustomMaxWidth ? 'sm:max-w-2xl p-6' : 'p-6',
+      compact ? 'max-w-4xl p-4' : !noDefaultMaxWidth ? 'sm:max-w-2xl p-6' : 'p-6',
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/src/components/ui/dialog.tsx` around lines 88 - 91, The className
substring check in DialogContent (hasCustomMaxWidth) is fragile; add an explicit
boolean prop (e.g., noDefaultMaxWidth?: boolean) to DialogContentProps and use
it instead of or in conjunction with hasCustomMaxWidth to decide whether to
apply the default max-width; update the forwardRef signature and the
destructuring ({ ..., noDefaultMaxWidth = false, ... }) in the DialogContent
function, replace/augment the hasCustomMaxWidth usage with a condition that
respects noDefaultMaxWidth, and update any consumers/tests to pass the new prop
where they intentionally want to suppress the default max-width.
apps/ui/src/renderer.tsx (1)

6-11: Redundant pwa-standalone class application — index.html already covers this.

apps/ui/index.html (lines 96–101) applies pwa-standalone synchronously in an inline <script> block that runs before any CSS or modules load — which is exactly the stated goal of "before CSS renders." By the time this module executes, the class is already on document.documentElement.

classList.add is idempotent so there's no behavioral issue, but if the intent is purely defensive (guard against the inline script failing), a brief comment saying so would make the duplication intentional rather than accidental.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/src/renderer.tsx` around lines 6 - 11, The duplicate application of
the 'pwa-standalone' class via the isPwaStandalone check is redundant because
index.html's inline script already sets it; update the block around
isPwaStandalone and the document.documentElement.classList.add('pwa-standalone')
call to either remove it or keep it with a clear comment stating it's a
defensive fallback if the inline script fails, e.g., collapse the current if
(isPwaStandalone) { document.documentElement.classList.add('pwa-standalone'); }
into a single-line defensive comment or remove it entirely depending on desired
behavior so reviewers know the duplication is intentional.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/server/src/services/execution-service.ts`:
- Around line 214-218: The internal-call branch unconditionally calls
updateFeatureStatusFn when options?._calledInternally, which can overwrite a
concurrent terminal status; modify the internal branch to mirror the external
guard by checking the loaded feature's status (the same condition used
externally: feature.status === 'backlog' || feature.status === 'ready') before
calling updateFeatureStatusFn (use the feature returned from loadFeatureFn), so
updateFeatureStatusFn is only invoked when the feature is in backlog or ready.

In `@apps/ui/index.html`:
- Around line 94-101: The PWA standalone detection using
window.matchMedia('(display-mode: standalone)') and navigator.standalone is
placed outside the existing try/catch and may throw uncaught exceptions; move
that block into the same try/catch inside the IIFE so any exceptions from
window.matchMedia or navigator.standalone are caught, and then call
document.documentElement.classList.add('pwa-standalone') within that guarded
block; ensure you don’t change the detection logic, only its location so it runs
under the existing error handling.

In `@apps/ui/src/components/views/agent-view.tsx`:
- Around line 133-138: handleCreateSessionFromEmptyState can be a no-op on
mobile because quickCreateSessionRef is cleared when SessionManager is
unmounted; change the handler to ensure the SessionManager is visible before
calling the ref: if quickCreateSessionRef.current is null, call the state setter
that shows SessionManager (showSessionManager -> setShowSessionManager(true)),
await a microtask/next render (e.g., await new Promise(r =>
requestAnimationFrame(r)) or next tick) so the component mounts and re-populates
quickCreateSessionRef, then invoke quickCreateSessionRef.current(); otherwise
call it immediately. Reference: handleCreateSessionFromEmptyState,
quickCreateSessionRef, SessionManager, and the showSessionManager state setter.

In `@apps/ui/src/components/views/board-view/dialogs/create-worktree-dialog.tsx`:
- Around line 164-173: The dialog reset effect currently clears branchName,
baseBranch, showBaseBranch, error, availableBranches, and branchFetchError but
not isLoadingBranches, so add setIsLoadingBranches(false) to that useEffect;
additionally, update the fetchBranches implementation to accept and use an
AbortController (or return a cancel token) so in-flight fetches can be aborted
on dialog close to prevent stale writes to availableBranches and
branchFetchError—ensure the cleanup/close path calls controller.abort() (or the
cancel function) and that fetchBranches checks for abort before setting state
(affecting isLoadingBranches, availableBranches, branchFetchError and the
BranchAutocomplete/Refresh button state).

In `@apps/ui/src/styles/global.css`:
- Around line 564-577: The .dialog-fullscreen-mobile rules are unintentionally
overriding the p-6 shorthand from DialogContent; update
.dialog-fullscreen-mobile in global.css to preserve the base p-6 padding by
using calc() so safe-area insets are added to the existing 1.5rem padding (p-6).
Concretely, change the padding-top and padding-bottom rules to compute
env(safe-area-inset-*, 0px) + 1.5rem to avoid collapsing p-6 on non-notched
devices, and in the `@media` (min-width: 640px) block restore the plain 1.5rem
top/bottom padding (or equivalent calc(0px + 1.5rem)) so desktop centered
dialogs keep the original p-6 spacing.

---

Nitpick comments:
In `@apps/server/src/routes/worktree/routes/create-pr.ts`:
- Around line 23-285: The createCreatePRHandler route contains substantial
business logic around parsing remotes and computing
targetRepo/pushOwner/pushRemote/upstreamRepo (see parsedRemotes map population
and the targetRemote handling block); extract this into a new service function
(e.g., services/prService.ts export an async function like
resolvePrTarget({worktreePath, pushRemote, targetRemote})) that accepts the repo
path and remote inputs and returns structured data ({repoUrl, targetRepo,
pushOwner, upstreamRepo, originOwner, parsedRemotes}) or throws validation
errors; update createCreatePRHandler to call resolvePrTarget and handle its
returned values or errors, and move all parsing, validation (including the
targetRemote existence check) and related logging from createCreatePRHandler
into that service.

In `@apps/server/src/services/merge-service.ts`:
- Around line 97-123: The const remote = rawRemote alias is unnecessary;
simplify by using a single validated variable: replace rawRemote + remote usage
with one const (e.g., const remote = options?.remote || 'origin'), keep the
isValidRemoteName(remote) check and the logger/execGitCommand(projectPath) calls
unchanged, and remove the redundant rawRemote declaration so all subsequent
references use remote directly (ensuring logger.warn and execGitCommand still
reference the same symbol).

In `@apps/server/src/services/rebase-service.ts`:
- Around line 72-98: The code creates a redundant alias by assigning const
remote = rawRemote after validating rawRemote; remove the no-op alias and use
rawRemote directly (or rename rawRemote to remote) to collapse the variables in
rebase-service.ts—update references to use the single name (e.g., use rawRemote
in the isValidRemoteName check and in the execGitCommand(['fetch', remote],
worktreePath) call or rename rawRemote to remote) so only one variable is used
around isValidRemoteName, rawRemote/remote, and the fetch error handling
(fetchError/getErrorMessage).

In `@apps/ui/public/sw.js`:
- Around line 320-371: The first-visit branch in the navigation handler uses
cache.put(event.request, response.clone()) as fire-and-forget inside the try
block of the event.respondWith handler; wrap that cache update in
event.waitUntil (or otherwise ensure the promise is observed) so the Service
Worker isn't terminated before the shell is cached—mirror the pattern used by
the updateCache() helper (and the cached-path where you call
event.waitUntil(updateCache())), targeting the cache.put call in the anonymous
async passed to event.respondWith.
- Around line 253-264: When the 2s timeout wins and cachedResponse is returned,
the ongoing fetchPromise (from startNetworkFetch) must be kept alive via the
service worker event's lifetime so the cache update can complete; update the
branch that handles cachedResponse to call
event.waitUntil(fetchPromise.then(...).catch(...)) (or .finally(...)) so the
background fetch continues to completion and updates the cache even after
returning cachedResponse, referencing fetchPromise and startNetworkFetch to
locate the code and using event.waitUntil to extend the worker lifetime.

In `@apps/ui/src/components/ui/dialog.tsx`:
- Around line 88-91: The className substring check in DialogContent
(hasCustomMaxWidth) is fragile; add an explicit boolean prop (e.g.,
noDefaultMaxWidth?: boolean) to DialogContentProps and use it instead of or in
conjunction with hasCustomMaxWidth to decide whether to apply the default
max-width; update the forwardRef signature and the destructuring ({ ...,
noDefaultMaxWidth = false, ... }) in the DialogContent function, replace/augment
the hasCustomMaxWidth usage with a condition that respects noDefaultMaxWidth,
and update any consumers/tests to pass the new prop where they intentionally
want to suppress the default max-width.

In `@apps/ui/src/components/ui/git-diff-panel.tsx`:
- Around line 927-976: The onClick handlers create floating Promises: change
onClick={() => handleUnstageFile(file.path)} and onClick={() =>
handleStageFile(file.path)} to the established pattern (e.g. onClick={() => void
handleUnstageFile(file.path)}) so the async calls are explicitly ignored and
satisfy the lint rule; then remove the duplicated staging controls by extracting
and reusing a shared component (e.g. FileRow or FileStagingControls) used by
both this fallback block and FileDiffSection so the staging UI (stagingState,
stagingInProgress, handleStageFile, handleUnstageFile, enableStaging) is
implemented once and composed in both places.

In `@apps/ui/src/components/views/board-view/dialogs/create-worktree-dialog.tsx`:
- Around line 195-198: isRemoteBaseBranch currently only checks
availableBranches and thus returns false when the user types a remote branch
like "origin/feature"; update the isRemoteBaseBranch computation (used by
baseBranch, availableBranches, allowCreate, branchFetchError) to also detect
manually entered remote-style names by checking the baseBranch string pattern
(e.g., contains '/' or matches /^remoteName\/.+$/ or startsWith('origin/')) and
treat those as remote when availableBranches doesn't contain a match so the UI
shows the "Remote branch — will fetch latest" hint and switches the button label
to "Fetching & Creating…".

In `@apps/ui/src/renderer.tsx`:
- Around line 6-11: The duplicate application of the 'pwa-standalone' class via
the isPwaStandalone check is redundant because index.html's inline script
already sets it; update the block around isPwaStandalone and the
document.documentElement.classList.add('pwa-standalone') call to either remove
it or keep it with a clear comment stating it's a defensive fallback if the
inline script fails, e.g., collapse the current if (isPwaStandalone) {
document.documentElement.classList.add('pwa-standalone'); } into a single-line
defensive comment or remove it entirely depending on desired behavior so
reviewers know the duplication is intentional.

In `@libs/utils/src/git-validation.ts`:
- Around line 66-73: The initial guard in isValidRemoteName redundantly checks
name.length === 0 after !name; remove the dead branch so the first line becomes
a single falsy/length and max-length check (e.g. if (!name || name.length >=
MAX_BRANCH_NAME_LENGTH) return false;), leaving the other guards (startsWith
'-', startsWith '.', '..', '/', '\0' and the final regex /^[a-zA-Z0-9._-]+$/)
and the MAX_BRANCH_NAME_LENGTH reference unchanged to preserve behavior.

refactor: Extract PR target resolution logic into dedicated service
```
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: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/ui/src/components/ui/dialog.tsx (1)

115-126: ⚠️ Potential issue | 🟡 Minor

compact mode loses the viewport-margin constraint via tailwind-merge conflict

Line 115 adds max-w-[calc(100%-2rem)] to enforce a 1rem gutter on each side of the dialog. However, when compact=true, line 126 injects max-w-4xl (no breakpoint prefix) into the same cn() call.

Since cn() uses twMerge(clsx(inputs)), it resolves conflicts between classes in the same utility group. Both max-w-[calc(100%-2rem)] and max-w-4xl are unqualified max-width utilities; tailwind-merge keeps only the last one, dropping the earlier max-w-[calc(100%-2rem)]. This means compact dialogs will render at max-w-4xl (448px) without the 1rem viewport margin — there's no edge-to-edge protection on narrow screens.

This doesn't affect the non-compact path because sm:max-w-2xl (breakpoint-prefixed) and max-w-[calc(100%-2rem)] (unprefixed) are independent modifiers and both are preserved.

🐛 Proposed fix — encode both constraints into a single `max-w-` token

Replace the two-part approach with a single class that uses CSS min() to enforce both constraints at once:

-           'flex flex-col w-full max-w-[calc(100%-2rem)]',
+           'flex flex-col w-full',
-           compact ? 'max-w-4xl p-4' : !hasCustomMaxWidth ? 'sm:max-w-2xl p-6' : 'p-6',
+           compact
+             ? 'max-w-[min(56rem,calc(100%-2rem))] p-4'
+             : !hasCustomMaxWidth
+             ? 'max-w-[min(42rem,calc(100%-2rem))] sm:max-w-2xl p-6'
+             : 'p-6',

Alternatively, keep max-w-[calc(100%-2rem)] and add a wrapping check so compact mode also references a capped arbitrary value rather than a named token.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/src/components/ui/dialog.tsx` around lines 115 - 126, The compact
branch in the cn() class list (the ternary using compact and hasCustomMaxWidth)
overrides the earlier unprefixed max-w-[calc(100%-2rem)] because tailwind-merge
drops earlier max-w tokens; update the compact branch in
apps/ui/src/components/ui/dialog.tsx so it emits a single max-w token that
encodes both the 1rem gutter and the intended cap (e.g., use CSS min() or an
equivalent arbitrary value combining calc(100%-2rem) and the 4xl cap) instead of
plain "max-w-4xl", keeping the other padding classes the same; this ensures
compact dialogs keep the viewport-margin constraint while still being capped.
🧹 Nitpick comments (13)
apps/ui/src/components/views/agent-view.tsx (1)

133-149: Single requestAnimationFrame may not guarantee the SessionManager has committed in React 19 concurrent mode

The past review's fix (showing the SessionManager and waiting a frame) is a real improvement. However, because the await occurs inside an async function — not a synchronous browser event handler — React 19's concurrent scheduler is not required to have committed the setState before the first RAF fires. If the commit races past the RAF, quickCreateSessionRef.current is still null after the wait and the call is silently dropped (same failure mode as before the fix, just now with the SessionManager visible).

Two nested requestAnimationFrame calls, or a setTimeout(0), give the scheduler a full JS turn to commit:

🛡️ Proposed fix — more reliable mount wait
-  await new Promise<void>((r) => requestAnimationFrame(() => r()));
+  await new Promise<void>((r) => requestAnimationFrame(() => requestAnimationFrame(() => r())));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/src/components/views/agent-view.tsx` around lines 133 - 149, The
current handleCreateSessionFromEmptyState uses a single requestAnimationFrame
after setShowSessionManager but in React 19 concurrent mode that may not
guarantee SessionManager has committed; update the wait logic (inside
handleCreateSessionFromEmptyState) to wait one more tick (e.g., use two nested
requestAnimationFrame calls or a setTimeout(() => ..., 0)) before re-checking
quickCreateSessionRef.current so the component has time to mount and populate
the ref, then call createFn if present.
apps/ui/public/sw.js (1)

444-461: Precache fetches may be terminated early — no event.waitUntil.

The PRECACHE_ASSETS handler fires independent fetch calls without wrapping them in event.waitUntil(). The browser may terminate the SW before all fetches complete, especially on mobile where SW lifetime is aggressively managed. Since precaching is best-effort this is acceptable, but wrapping in waitUntil would improve reliability.

Optional: keep SW alive during precache
   if (event.data?.type === 'PRECACHE_ASSETS' && Array.isArray(event.data.urls)) {
-    caches.open(IMMUTABLE_CACHE).then((cache) => {
-      event.data.urls.forEach((url) => {
-        cache.match(url).then((existing) => {
-          if (!existing) {
-            fetch(url, { priority: 'low' })
-              .then((response) => {
-                if (response.ok) {
-                  cache.put(url, response);
-                }
-              })
-              .catch(() => {
-                // Silently ignore precache failures
-              });
-          }
-        });
-      });
-    });
+    event.waitUntil(
+      caches.open(IMMUTABLE_CACHE).then((cache) => {
+        return Promise.all(
+          event.data.urls.map((url) =>
+            cache.match(url).then((existing) => {
+              if (existing) return;
+              return fetch(url, { priority: 'low' })
+                .then((response) => {
+                  if (response.ok) {
+                    return cache.put(url, response);
+                  }
+                })
+                .catch(() => {
+                  // Silently ignore precache failures
+                });
+            })
+          )
+        );
+      })
+    );
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/public/sw.js` around lines 444 - 461, The PRECACHE_ASSETS handler
starts independent fetches that can be cut off because they aren't awaited;
update the block handling event.data?.type === 'PRECACHE_ASSETS' (and
IMMUATABLE_CACHE / the cache.open logic) to build a single Promise that waits
for all cache match/fetch/put operations (e.g., using Promise.all over
event.data.urls.map(...)) and pass that Promise into event.waitUntil(...) so the
service worker stays alive until precache work completes.
apps/server/src/routes/worktree/routes/create-pr.ts (2)

236-243: gh pr list command built via string interpolation — inconsistent with gh pr create.

The gh pr list command (line 238) is assembled as a shell string with execAsync, while gh pr create (line 296) uses spawnProcess with an args array. repoArg and headRef contain values derived from remote-parsed output (owner/repo), which could theoretically contain shell metacharacters. Using spawnProcess with an args array would eliminate this risk and be consistent with the gh pr create pattern below.

Suggested refactor
-        const listCmd = `gh pr list${repoArg} --head "${headRef}" --json number,title,url,state --limit 1`;
-        logger.debug(`Running: ${listCmd}`);
-        const { stdout: existingPrOutput } = await execAsync(listCmd, {
-          cwd: worktreePath,
-          env: execEnv,
-        });
+        const listArgs = ['pr', 'list', '--head', headRef, '--json', 'number,title,url,state', '--limit', '1'];
+        if (upstreamRepo) {
+          listArgs.push('--repo', upstreamRepo);
+        }
+        logger.debug(`Checking for existing PR: gh ${listArgs.join(' ')}`);
+        const listResult = await spawnProcess({
+          command: 'gh',
+          args: listArgs,
+          cwd: worktreePath,
+          env: execEnv,
+        });
+        const existingPrOutput = listResult.stdout;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/routes/worktree/routes/create-pr.ts` around lines 236 - 243,
The gh pr list invocation builds a shell string via listCmd and execAsync (using
repoArg and headRef), which is inconsistent with the gh pr create usage and
risks shell injection; replace the execAsync call with the spawnProcess pattern
used for gh pr create: call spawnProcess("gh", ["pr","list", repoArg, "--head",
headRef, "--json","number,title,url,state","--limit","1"], { cwd: worktreePath,
env: execEnv }) (or equivalent) and consume its stdout to populate
existingPrOutput, ensuring repoArg and headRef are passed as separate args
rather than interpolated into a shell string.

146-153: Shell string interpolation in git push — prefer arg-array form.

Lines 146 and 153 pass user-influenced values (pushRemote, branchName) via string interpolation into execAsync (shell execution). While both are validated, the same file uses the safer execGitCommand (array args, no shell) for git commit at line 103 and spawnProcess for gh pr create at line 296. Switching to execGitCommand here would eliminate the shell-parsing layer entirely and make the security posture consistent.

Suggested refactor
-        await execAsync(`git push ${pushRemote} ${branchName}`, {
-          cwd: worktreePath,
-          env: execEnv,
-        });
+        await execGitCommand(['push', pushRemote, branchName], worktreePath);
       } catch {
         // If push fails, try with --set-upstream
         try {
-          await execAsync(`git push --set-upstream ${pushRemote} ${branchName}`, {
-            cwd: worktreePath,
-            env: execEnv,
-          });
+          await execGitCommand(['push', '--set-upstream', pushRemote, branchName], worktreePath);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/routes/worktree/routes/create-pr.ts` around lines 146 - 153,
The git push calls use shell-interpolated strings with user-influenced values
(pushRemote, branchName) passed to execAsync; replace both attempts with the
existing safe helper execGitCommand (which accepts an args array) to avoid shell
parsing. Concretely, in the try/catch around the first git push and the inner
retry that adds --set-upstream, call execGitCommand with cwd set to
worktreePath, env execEnv, and args like ["push", pushRemote, branchName] (and
["push", "--set-upstream", pushRemote, branchName] for the retry) so you remove
string interpolation while preserving the same behavior and error handling.
Ensure you keep the surrounding try/catch logic and any logging unchanged.
apps/server/src/services/pr-service.ts (2)

60-63: git remote -v uses shell string via execAsync instead of execGitCommand.

The service uses execAsync('git remote -v', ...) (shell-based) instead of execGitCommand(['remote', '-v'], ...) (array-based, no shell). While the command here has no user-supplied arguments so injection isn't a risk, using execGitCommand would be consistent with the rest of the codebase and the coding guidelines around using @automaker/git-utils.

Similarly applies to line 150: execAsync('git config --get remote.origin.url', ...).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/services/pr-service.ts` around lines 60 - 63, Replace the
shell-based execAsync invocations with the array-based helper execGitCommand to
match project conventions: swap execAsync('git remote -v', {cwd: worktreePath,
env: execEnv}) (which populates remotes) for execGitCommand(['remote','-v'],
{cwd: worktreePath, env: execEnv}) and likewise replace execAsync('git config
--get remote.origin.url', {cwd: worktreePath, env: execEnv}) with
execGitCommand(['config','--get','remote.origin.url'], {cwd: worktreePath, env:
execEnv}); keep using the same variables (remotes, execEnv, worktreePath) and
ensure the returned stdout handling remains unchanged.

47-48: Redundant pushRemote !== undefined check.

pushRemote is typed as a required string parameter (line 43), so it's always defined. The !== undefined guard is a no-op.

Suggested simplification
-  if (pushRemote !== undefined && !isValidRemoteName(pushRemote)) {
+  if (!isValidRemoteName(pushRemote)) {
     throw new Error(`Invalid push remote name: "${pushRemote}"`);
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/services/pr-service.ts` around lines 47 - 48, The conditional
guarding `isValidRemoteName(pushRemote)` is using a redundant `pushRemote !==
undefined` check because `pushRemote` is a required string parameter; remove the
`!== undefined` part and simplify the condition to directly validate the
parameter (i.e., replace the `if (pushRemote !== undefined &&
!isValidRemoteName(pushRemote))` check with a direct `if
(!isValidRemoteName(pushRemote))` branch), keeping the existing error throw for
`"Invalid push remote name: \"${pushRemote}\""`.
apps/ui/src/components/ui/dialog.tsx (1)

102-104: hasCustomMaxWidth name is misleading when noDefaultMaxWidth=true

When noDefaultMaxWidth is true, the variable is set to true regardless of whether className actually contains a max-w- class. The name implies "a custom max-width exists in className," but the noDefaultMaxWidth branch means "suppress the default even without a custom one." Consider renaming to skipDefaultMaxWidth to match the actual semantics.

♻️ Suggested rename
-   // Check if className contains a custom max-width (fallback heuristic)
-   const hasCustomMaxWidth =
-     noDefaultMaxWidth || (typeof className === 'string' && className.includes('max-w-'));
+   // Skip default max-width when explicitly requested or when caller already provides one
+   const skipDefaultMaxWidth =
+     noDefaultMaxWidth || (typeof className === 'string' && className.includes('max-w-'));
-   compact ? 'max-w-4xl p-4' : !hasCustomMaxWidth ? 'sm:max-w-2xl p-6' : 'p-6',
+   compact ? 'max-w-4xl p-4' : !skipDefaultMaxWidth ? 'sm:max-w-2xl p-6' : 'p-6',
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/src/components/ui/dialog.tsx` around lines 102 - 104, The variable
hasCustomMaxWidth is misnamed because it becomes true when noDefaultMaxWidth is
true even if className lacks a max-w- token; rename hasCustomMaxWidth to
skipDefaultMaxWidth (or similar) and update all references (including the
declaration and any conditional logic that uses hasCustomMaxWidth) so the name
reflects that the default max-width should be suppressed when noDefaultMaxWidth
is true, while retaining the original fallback heuristic that also sets it when
className contains 'max-w-'.
apps/ui/src/components/views/terminal-view/terminal-scripts-dropdown.tsx (2)

52-52: as unknown as Array<...> double-cast is a code smell from as const.

Using as const on DEFAULT_SCRIPTS widens the type to deeply readonly literals, forcing the cast when the consumer expects a mutable type. This will be eliminated if the shared constant (see refactor suggestion on terminal-scripts-section.tsx) uses an explicit type annotation instead of as const.

♻️ Alternative — explicit type annotation avoids the cast entirely

In the shared constants file:

export const DEFAULT_TERMINAL_SCRIPTS: Array<{ id: string; name: string; command: string }> = [
  ...
];

No as const, no cast needed at the call site.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/src/components/views/terminal-view/terminal-scripts-dropdown.tsx` at
line 52, The double-cast on DEFAULT_SCRIPTS in terminal-scripts-dropdown.tsx is
compensating for an upstream `as const` on the shared constant; update the
shared constant (referenced in terminal-scripts-section.tsx) to declare an
explicit type like Array<{id: string; name: string; command: string}> (e.g.
export const DEFAULT_TERMINAL_SCRIPTS: Array<...> = [...]) instead of using `as
const`, then remove the `as unknown as Array<...>` cast and return the constant
directly (use the symbol DEFAULT_SCRIPTS / DEFAULT_TERMINAL_SCRIPTS to locate
and adjust the declaration and consumer).

94-94: cn() is unnecessary here — only a single static class string is passed.

♻️ Proposed change
-<Play className={cn('h-3.5 w-3.5 shrink-0 text-brand-500')} />
+<Play className="h-3.5 w-3.5 shrink-0 text-brand-500" />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/src/components/views/terminal-view/terminal-scripts-dropdown.tsx` at
line 94, The JSX uses the cn utility unnecessarily for a single static class
string; in the Play component invocation inside terminal-scripts-dropdown.tsx
replace the call to cn('h-3.5 w-3.5 shrink-0 text-brand-500') with the plain
className string 'h-3.5 w-3.5 shrink-0 text-brand-500' (i.e., remove cn) to
simplify the code and avoid an extra function call.
apps/ui/src/components/views/project-settings-view/terminal-scripts-section.tsx (1)

76-76: Prefer structuredClone() over JSON.parse(JSON.stringify()) for deep cloning.

structuredClone is available in all modern browsers and Electron (Chromium 98+), is faster than a JSON round-trip, and handles more data types correctly.

♻️ Proposed change
-setOriginalScripts(JSON.parse(JSON.stringify(scriptList)));
+setOriginalScripts(structuredClone(scriptList));

Apply the same change at lines 99 and 107.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/ui/src/components/views/project-settings-view/terminal-scripts-section.tsx`
at line 76, Replace the JSON round-trip deep clones with structuredClone: where
the code calls setOriginalScripts(JSON.parse(JSON.stringify(scriptList))) and
the similar two occurrences later in the file, use
setOriginalScripts(structuredClone(scriptList)) (and the corresponding
setX(structuredClone(scriptList))) instead so cloning uses
structuredClone(scriptList) for correctness and performance.
apps/ui/src/components/views/board-view/worktree-panel/components/worktree-actions-dropdown.tsx (2)

334-352: Consider adding a separator after the Auto Mode toggle block.

The Auto Mode item immediately precedes the Dev Server section with no visual divider. Since the Dev Server section appends its own separator at the bottom, Auto Mode and Dev Server render as a single visual group. Adding a <DropdownMenuSeparator /> after the closing </> of the Auto Mode block would give it its own grouping.

✏️ Proposed fix
     )}
+    {onToggleAutoMode && <DropdownMenuSeparator />}
     {isDevServerRunning ? (
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/ui/src/components/views/board-view/worktree-panel/components/worktree-actions-dropdown.tsx`
around lines 334 - 352, Add a visual separator after the Auto Mode toggle group
so it’s visually distinct from the Dev Server section: inside
worktree-actions-dropdown.tsx, after the conditional block that renders the Auto
Mode items (the block that checks onToggleAutoMode and uses isAutoModeRunning
and calls onToggleAutoMode(worktree)), insert a <DropdownMenuSeparator />
immediately following the closing fragment so the Auto Mode items are separated
from subsequent menu items.

582-649: Pull and Push inner item markup is copy-pasted across split/simple branches.

Both the Pull (Lines 584-602 vs 634-649) and Push (Lines 658-701 vs 733-771) sections duplicate their entire DropdownMenuItem body across the split-button and simple-item branches. Badge rendering (behindCount, aheadCount, CloudOff, trackingRemote) and the disabled/opacity logic are maintained in two places each.

Extracting small helper components (e.g., PullMenuItemContent / PushMenuItemContent) and wrapping them conditionally in DropdownMenuSub would eliminate the duplication and make future badge/logic changes a single-site edit.

Also applies to: 656-771

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/ui/src/components/views/board-view/worktree-panel/components/worktree-actions-dropdown.tsx`
around lines 582 - 649, Extract the duplicated inner markup for Pull and Push
into small pure components (e.g., PullMenuItemContent and PushMenuItemContent)
that accept props: worktree, isGitOpsAvailable, isPulling/isPushing,
behindCount/aheadCount, trackingRemote, and icons like CloudOff; render badges
and disabled/opacity logic inside those components and return only the common
DropdownMenuItem children (icon + label + badges/alerts). Then in the current
branches replace the duplicated bodies under DropdownMenuSub and the simple
DropdownMenuItem with a call to the new component (for split remotes wrap it in
DropdownMenuSubContent and map remotes to call
onPullWithRemote/onPushWithRemote, for single/no-remote use onPull/onPush),
keeping the existing event handlers (onPull, onPullWithRemote, onPush,
onPushWithRemote), props (worktree, remotes) and className/disabled wiring
intact so behavior is unchanged.
apps/ui/src/components/views/board-view/dialogs/create-worktree-dialog.tsx (1)

226-238: Heuristic can produce a misleading "Remote branch — will fetch latest" hint in the branch-fetch-error path.

When branchFetchError is set, availableBranches is the [{ name: 'main', isRemote: false }] fallback and allowCreate becomes true. A user who types a new local branch like feature/my-branch will trigger the baseBranch.includes('/') path, find it is not a known local branch (it isn't in the fallback list), and isRemoteBaseBranch returns true — showing the "Remote branch — will fetch latest" hint even though the user intends a new local branch.

The comment documents this as an intentional heuristic, and the worktree creation itself is unaffected. A simple mitigation is to suppress the hint when the branch list couldn't be loaded (i.e., when branchFetchError is set):

- const isRemoteBaseBranch = useMemo(() => {
+ const isRemoteBaseBranch = useMemo(() => {
    if (!baseBranch) return false;
+   // Suppress heuristic when branch list is unavailable; we can't distinguish remote from local
+   if (branchFetchError && availableBranches.length === 1) return false;
    ...
- }, [baseBranch, availableBranches]);
+ }, [baseBranch, availableBranches, branchFetchError]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/src/components/views/board-view/dialogs/create-worktree-dialog.tsx`
around lines 226 - 238, The heuristic in the isRemoteBaseBranch useMemo can
misclassify a user-entered local branch as remote when availableBranches is the
fallback due to a failed fetch; update the useMemo (isRemoteBaseBranch) to first
check branchFetchError and return false if it's set so the "Remote branch — will
fetch latest" hint is suppressed on fetch failures; keep the rest of the logic
(checks against availableBranches, the '/' heuristic) unchanged and ensure the
dependency array still includes baseBranch, availableBranches, and
branchFetchError so the memo updates correctly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/server/src/services/pr-service.ts`:
- Around line 96-106: The catch block swallowing failures from execAsync('git
remote -v', ...) leads to parsedRemotes remaining empty and an explicit
targetRemote being silently ignored; update the logic so that if targetRemote is
provided but parsedRemotes.size === 0 you throw a clear error (e.g., referencing
parsedRemotes and targetRemote) instead of proceeding to silent fallback—apply
this check after the catch and before any auto-detection/override logic so
callers are notified when remote parsing failed and their explicit target cannot
be validated.
- Line 8: pr-service.ts currently imports route-level helpers execAsync,
execEnv, and isValidRemoteName from routes/worktree/common.js which inverts
dependency direction; replace the import of isValidRemoteName to import directly
from `@automaker/utils` (use the named export isValidRemoteName) and remove the
import of execAsync and execEnv from that route module, then wire
execAsync/execEnv to a shared utility once created (add a TODO referencing
lib/exec.ts or `@automaker/utils` and temporarily implement a thin wrapper or mock
within the service boundary if necessary) so that services no longer depend on
route internals.

In `@apps/ui/public/sw.js`:
- Around line 160-164: Update the misleading comment above self.clients.claim():
explain that self.clients.claim() is called unconditionally on activation, while
the activate event itself only fires at safe times (first install, after
skipWaiting() is called, or after all old clients are closed), and mention
skipWaiting() and the activate event to clarify why unconditional claim() is
safe; keep the comment concise and reference self.clients.claim() and
skipWaiting().
- Around line 123-141: The conditional guard using the skipWaitingRequested flag
inside the install event handler is dead code because skipWaitingRequested is
only set by the SKIP_WAITING message handler after install has completed; remove
the unreachable if (skipWaitingRequested) { self.skipWaiting(); } from the
install listener (or alternatively simplify the install handler comments) and
ensure the real activation path is documented to point to the SKIP_WAITING
message handler where self.skipWaiting() is actually invoked (reference
skipWaitingRequested, the install event listener, the SKIP_WAITING message
handler, and the self.skipWaiting() call).

In `@apps/ui/src/components/views/board-view/dialogs/create-worktree-dialog.tsx`:
- Around line 267-268: The code currently forwards effectiveBaseBranch (derived
from baseBranch) to api.worktree.create without the same validation applied to
branchName; update the dialog to validate/sanitize baseBranch using the same
/^[a-zA-Z0-9._/-]+$/ check used for branchName (or reuse the same validation
helper if one exists) before assigning effectiveBaseBranch, and only pass the
validated value (or undefined when empty) to api.worktree.create to prevent
shell-special characters or invalid names from reaching the API.
- Line 129: The call to api.worktree.listBranches(projectPath, true) doesn't
accept an AbortSignal so the underlying HTTP request keeps running after
controller.abort(); update the API and HTTP client to accept and forward an
optional AbortSignal: add an optional signal parameter to worktree.listBranches
and propagate it into the http client's post(...) call, then modify post in
http-api-client.ts to accept a signal and pass it into fetch({ signal }) so
controller.abort() cancels the in-flight request; ensure create-worktree-dialog
uses controller.signal when calling listBranches and keep the existing
post-await abort check.

In
`@apps/ui/src/components/views/board-view/worktree-panel/components/worktree-actions-dropdown.tsx`:
- Around line 892-912: The dropdown renders a visible, clickable "Stashes" item
that is a silent no-op when worktree.hasChanges is false and onViewStashes is
undefined (even if onStashChanges exists); update the rendering guard so the
simple DropdownMenuItem only renders when at least one action is meaningful:
i.e. render the block only if (worktree.hasChanges && onStashChanges) ||
onViewStashes, keep disabled tied to isGitOpsAvailable, and preserve the
existing labels/icons (references: DropdownMenuItem, worktree.hasChanges,
onStashChanges, onViewStashes, isGitOpsAvailable).

In
`@apps/ui/src/components/views/project-settings-view/terminal-scripts-section.tsx`:
- Around line 152-172: handleDragEnd currently always applies the reorder if
draggedIndex and dragOverIndex are set; change handleDragEnd to accept the drag
event (e: React.DragEvent) and only perform the splice-reorder when
e.dataTransfer.dropEffect !== 'none' (meaning the drop target accepted the
drop). Keep the existing checks for draggedIndex/dragOverIndex before touching
setScripts, and always reset setDraggedIndex(null) and setDragOverIndex(null) at
the end. Update any onDragEnd usages to pass the event into handleDragEnd;
reference functions/vars: handleDragEnd, handleDragOver, draggedIndex,
dragOverIndex, setScripts, setDraggedIndex, setDragOverIndex.
- Around line 12-17: DEFAULT_SCRIPTS is duplicated between
terminal-scripts-section.tsx and terminal-scripts-dropdown.tsx; extract the
shared array into a single exported constant and import it where needed. Create
a new module (e.g., terminal-scripts-constants.ts) that exports
DEFAULT_TERMINAL_SCRIPTS (same shape: id, name, command) and update
terminal-scripts-section.tsx to replace DEFAULT_SCRIPTS with that import; also
update terminal-scripts-dropdown.tsx to import the same constant (removing any
casts like as unknown as Array<...>). Ensure both files reference the exported
symbol name (DEFAULT_TERMINAL_SCRIPTS) consistently.

In `@apps/ui/src/components/views/terminal-view/terminal-panel.tsx`:
- Around line 381-385: sendCommand always appends '\n' which breaks Windows
shells; make isWindowsShell a component-level ref so sendCommand can use it.
Create a useRef<boolean> isWindowsShellRef, set isWindowsShellRef.current inside
the existing ws.onmessage detection logic (where isWindowsShell is computed),
and update sendCommand to read isWindowsShellRef.current and append '\r\n' when
true or '\n' otherwise (use wsRef.current.send as before). Ensure you reference
sendCommand, runCommandOnConnect (for consistency), ws.onmessage, wsRef and
isWindowsShellRef in the change.

In `@apps/ui/src/styles/global.css`:
- Around line 419-424: The CSS rule setting padding-bottom: 0 on
:root.pwa-standalone `#app` removes safe-area inset for iOS PWAs and can cause
content to be obscured; revert to using the safe-area inset (e.g., leave
calc(env(safe-area-inset-bottom, 0px) * 0.5)) or apply the zero padding only
when running in a desktop/Electron context by gating the rule with an
Electron-specific root class or via a media query for non-touch/hover-capable
devices; use the existing isPwaStandalone/mobile-detect flag (from
mobile-detect.ts) or an Electron runtime marker to ensure the zero-padding rule
does not apply to iOS standalone PWAs.

---

Outside diff comments:
In `@apps/ui/src/components/ui/dialog.tsx`:
- Around line 115-126: The compact branch in the cn() class list (the ternary
using compact and hasCustomMaxWidth) overrides the earlier unprefixed
max-w-[calc(100%-2rem)] because tailwind-merge drops earlier max-w tokens;
update the compact branch in apps/ui/src/components/ui/dialog.tsx so it emits a
single max-w token that encodes both the 1rem gutter and the intended cap (e.g.,
use CSS min() or an equivalent arbitrary value combining calc(100%-2rem) and the
4xl cap) instead of plain "max-w-4xl", keeping the other padding classes the
same; this ensures compact dialogs keep the viewport-margin constraint while
still being capped.

---

Duplicate comments:
In `@apps/ui/src/components/views/terminal-view/terminal-scripts-dropdown.tsx`:
- Around line 17-22: DEFAULT_SCRIPTS is duplicated; extract the constant into a
single shared module (e.g., export DEFAULT_SCRIPTS from a new/shared file) and
replace the local definitions with imports. Update the files that currently
declare DEFAULT_SCRIPTS (referenced here by the symbol DEFAULT_SCRIPTS in
terminal-scripts-dropdown and the duplicate in terminal-scripts-section) to
import the exported constant instead of defining it inline, and ensure the
export uses the same readonly/const typing so callers continue to work without
type changes.

In `@apps/ui/src/styles/global.css`:
- Around line 564-580: No code change needed for the padding bug — keep the
.dialog-fullscreen-mobile selector using padding-top/bottom:
calc(env(safe-area-inset-*, 0px) + 1.5rem) and the `@media` (min-width: 640px)
block restoring padding-top/bottom: 1.5rem; ensure both rules remain inside the
same `@layer` utilities scope so cascade/order is preserved and remove the
duplicate review comment markers.

---

Nitpick comments:
In `@apps/server/src/routes/worktree/routes/create-pr.ts`:
- Around line 236-243: The gh pr list invocation builds a shell string via
listCmd and execAsync (using repoArg and headRef), which is inconsistent with
the gh pr create usage and risks shell injection; replace the execAsync call
with the spawnProcess pattern used for gh pr create: call spawnProcess("gh",
["pr","list", repoArg, "--head", headRef,
"--json","number,title,url,state","--limit","1"], { cwd: worktreePath, env:
execEnv }) (or equivalent) and consume its stdout to populate existingPrOutput,
ensuring repoArg and headRef are passed as separate args rather than
interpolated into a shell string.
- Around line 146-153: The git push calls use shell-interpolated strings with
user-influenced values (pushRemote, branchName) passed to execAsync; replace
both attempts with the existing safe helper execGitCommand (which accepts an
args array) to avoid shell parsing. Concretely, in the try/catch around the
first git push and the inner retry that adds --set-upstream, call execGitCommand
with cwd set to worktreePath, env execEnv, and args like ["push", pushRemote,
branchName] (and ["push", "--set-upstream", pushRemote, branchName] for the
retry) so you remove string interpolation while preserving the same behavior and
error handling. Ensure you keep the surrounding try/catch logic and any logging
unchanged.

In `@apps/server/src/services/pr-service.ts`:
- Around line 60-63: Replace the shell-based execAsync invocations with the
array-based helper execGitCommand to match project conventions: swap
execAsync('git remote -v', {cwd: worktreePath, env: execEnv}) (which populates
remotes) for execGitCommand(['remote','-v'], {cwd: worktreePath, env: execEnv})
and likewise replace execAsync('git config --get remote.origin.url', {cwd:
worktreePath, env: execEnv}) with
execGitCommand(['config','--get','remote.origin.url'], {cwd: worktreePath, env:
execEnv}); keep using the same variables (remotes, execEnv, worktreePath) and
ensure the returned stdout handling remains unchanged.
- Around line 47-48: The conditional guarding `isValidRemoteName(pushRemote)` is
using a redundant `pushRemote !== undefined` check because `pushRemote` is a
required string parameter; remove the `!== undefined` part and simplify the
condition to directly validate the parameter (i.e., replace the `if (pushRemote
!== undefined && !isValidRemoteName(pushRemote))` check with a direct `if
(!isValidRemoteName(pushRemote))` branch), keeping the existing error throw for
`"Invalid push remote name: \"${pushRemote}\""`.

In `@apps/ui/public/sw.js`:
- Around line 444-461: The PRECACHE_ASSETS handler starts independent fetches
that can be cut off because they aren't awaited; update the block handling
event.data?.type === 'PRECACHE_ASSETS' (and IMMUATABLE_CACHE / the cache.open
logic) to build a single Promise that waits for all cache match/fetch/put
operations (e.g., using Promise.all over event.data.urls.map(...)) and pass that
Promise into event.waitUntil(...) so the service worker stays alive until
precache work completes.

In `@apps/ui/src/components/ui/dialog.tsx`:
- Around line 102-104: The variable hasCustomMaxWidth is misnamed because it
becomes true when noDefaultMaxWidth is true even if className lacks a max-w-
token; rename hasCustomMaxWidth to skipDefaultMaxWidth (or similar) and update
all references (including the declaration and any conditional logic that uses
hasCustomMaxWidth) so the name reflects that the default max-width should be
suppressed when noDefaultMaxWidth is true, while retaining the original fallback
heuristic that also sets it when className contains 'max-w-'.

In `@apps/ui/src/components/views/agent-view.tsx`:
- Around line 133-149: The current handleCreateSessionFromEmptyState uses a
single requestAnimationFrame after setShowSessionManager but in React 19
concurrent mode that may not guarantee SessionManager has committed; update the
wait logic (inside handleCreateSessionFromEmptyState) to wait one more tick
(e.g., use two nested requestAnimationFrame calls or a setTimeout(() => ..., 0))
before re-checking quickCreateSessionRef.current so the component has time to
mount and populate the ref, then call createFn if present.

In `@apps/ui/src/components/views/board-view/dialogs/create-worktree-dialog.tsx`:
- Around line 226-238: The heuristic in the isRemoteBaseBranch useMemo can
misclassify a user-entered local branch as remote when availableBranches is the
fallback due to a failed fetch; update the useMemo (isRemoteBaseBranch) to first
check branchFetchError and return false if it's set so the "Remote branch — will
fetch latest" hint is suppressed on fetch failures; keep the rest of the logic
(checks against availableBranches, the '/' heuristic) unchanged and ensure the
dependency array still includes baseBranch, availableBranches, and
branchFetchError so the memo updates correctly.

In
`@apps/ui/src/components/views/board-view/worktree-panel/components/worktree-actions-dropdown.tsx`:
- Around line 334-352: Add a visual separator after the Auto Mode toggle group
so it’s visually distinct from the Dev Server section: inside
worktree-actions-dropdown.tsx, after the conditional block that renders the Auto
Mode items (the block that checks onToggleAutoMode and uses isAutoModeRunning
and calls onToggleAutoMode(worktree)), insert a <DropdownMenuSeparator />
immediately following the closing fragment so the Auto Mode items are separated
from subsequent menu items.
- Around line 582-649: Extract the duplicated inner markup for Pull and Push
into small pure components (e.g., PullMenuItemContent and PushMenuItemContent)
that accept props: worktree, isGitOpsAvailable, isPulling/isPushing,
behindCount/aheadCount, trackingRemote, and icons like CloudOff; render badges
and disabled/opacity logic inside those components and return only the common
DropdownMenuItem children (icon + label + badges/alerts). Then in the current
branches replace the duplicated bodies under DropdownMenuSub and the simple
DropdownMenuItem with a call to the new component (for split remotes wrap it in
DropdownMenuSubContent and map remotes to call
onPullWithRemote/onPushWithRemote, for single/no-remote use onPull/onPush),
keeping the existing event handlers (onPull, onPullWithRemote, onPush,
onPushWithRemote), props (worktree, remotes) and className/disabled wiring
intact so behavior is unchanged.

In
`@apps/ui/src/components/views/project-settings-view/terminal-scripts-section.tsx`:
- Line 76: Replace the JSON round-trip deep clones with structuredClone: where
the code calls setOriginalScripts(JSON.parse(JSON.stringify(scriptList))) and
the similar two occurrences later in the file, use
setOriginalScripts(structuredClone(scriptList)) (and the corresponding
setX(structuredClone(scriptList))) instead so cloning uses
structuredClone(scriptList) for correctness and performance.

In `@apps/ui/src/components/views/terminal-view/terminal-scripts-dropdown.tsx`:
- Line 52: The double-cast on DEFAULT_SCRIPTS in terminal-scripts-dropdown.tsx
is compensating for an upstream `as const` on the shared constant; update the
shared constant (referenced in terminal-scripts-section.tsx) to declare an
explicit type like Array<{id: string; name: string; command: string}> (e.g.
export const DEFAULT_TERMINAL_SCRIPTS: Array<...> = [...]) instead of using `as
const`, then remove the `as unknown as Array<...>` cast and return the constant
directly (use the symbol DEFAULT_SCRIPTS / DEFAULT_TERMINAL_SCRIPTS to locate
and adjust the declaration and consumer).
- Line 94: The JSX uses the cn utility unnecessarily for a single static class
string; in the Play component invocation inside terminal-scripts-dropdown.tsx
replace the call to cn('h-3.5 w-3.5 shrink-0 text-brand-500') with the plain
className string 'h-3.5 w-3.5 shrink-0 text-brand-500' (i.e., remove cn) to
simplify the code and avoid an extra function call.

Comment on lines +160 to 164
// Claim clients so the SW controls the page — but only on first install
// (when skipWaiting was requested) or when the page loads fresh.
// This is safe because without skipWaiting, the activate event only fires
// after all old-SW-controlled pages are closed and reopened.
self.clients.claim();
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

Comment on clients.claim() is misleading — it executes unconditionally.

Lines 160–163 say claim happens "only on first install (when skipWaiting was requested) or when the page loads fresh," but the code calls self.clients.claim() unconditionally on every activation. The comment should reflect the actual behavior: claim() is always called, but activate itself only fires at safe times (first install, after skipWaiting(), or after all old tabs close).

Suggested comment fix
-  // Claim clients so the SW controls the page — but only on first install
-  // (when skipWaiting was requested) or when the page loads fresh.
-  // This is safe because without skipWaiting, the activate event only fires
-  // after all old-SW-controlled pages are closed and reopened.
+  // Claim clients so the SW immediately controls all open pages.
+  // This is safe because the activate event only fires at safe times:
+  // on first install, after an explicit skipWaiting() call, or after all
+  // tabs using the old SW have been closed.
   self.clients.claim();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/public/sw.js` around lines 160 - 164, Update the misleading comment
above self.clients.claim(): explain that self.clients.claim() is called
unconditionally on activation, while the activate event itself only fires at
safe times (first install, after skipWaiting() is called, or after all old
clients are closed), and mention skipWaiting() and the activate event to clarify
why unconditional claim() is safe; keep the comment concise and reference
self.clients.claim() and skipWaiting().

@gsxdsm gsxdsm closed this Feb 19, 2026
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.

1 participant