Skip to content

Conversation

@Shironex
Copy link
Collaborator

@Shironex Shironex commented Jan 15, 2026

Summary

Community contribution from Discord member eclipxe with various improvements:

  • Mobile-responsive UI across all views (context, memory, GitHub issues, PRs)
  • "Open in Terminal" action for worktrees with cross-platform support (macOS, Windows, Linux)
  • "Discard Changes" action to reset worktree to clean state
  • "View Changes" dialog to inspect worktree modifications
  • Responsive toolbar with overflow menu for narrow screens
  • Target branch selection for merge operations (not just main)
  • Remote tracking detection for push button state
  • FeatureId sanitization fix - consistent handling across all worktree routes
  • Branch association fix for features created in "current" work mode
  • Improved sidebar behavior on mobile devices

Changes

  • 50 files changed, ~1900 additions, ~640 deletions
  • 4 new files (discard-changes, open-in-terminal, view-changes-dialog, header-overflow-menu)

Test plan

  • Test mobile responsiveness on various screen sizes
  • Test "Open in Terminal" on macOS/Windows/Linux
  • Test "Discard Changes" functionality
  • Test "View Changes" dialog
  • Test merge to non-main branches
  • Verify existing functionality not broken

Credits

Thanks to eclipxe from Discord for this contribution!

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Open worktrees directly in terminal.
    • Discard uncommitted changes in worktrees with confirmation.
    • View detailed changes per worktree before discarding.
    • Merge worktrees to any target branch, not just main.
    • Remote branch tracking indicator for better branch context.
  • UI/UX Improvements

    • Responsive mobile layouts across all views.
    • Enhanced sidebar and header controls for smaller screens.
    • Improved terminal input sizing and focus handling.

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

…nd various UX improvements

- Add mobile-responsive UI across all views (context, memory, issues, PRs)
- Add "Open in Terminal" action for worktrees with cross-platform support
- Add "Discard Changes" action to reset worktree to clean state
- Add "View Changes" dialog to inspect worktree modifications
- Add responsive toolbar with overflow menu for narrow screens
- Add target branch selection for merge operations (not just main)
- Add remote tracking branch detection for push button state
- Fix featureId sanitization consistency across all worktree routes
- Fix branch association for features created in "current" work mode
- Improve sidebar behavior on mobile devices

Co-Authored-By: eclipxe <discord>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@coderabbitai
Copy link

coderabbitai bot commented Jan 15, 2026

📝 Walkthrough

Walkthrough

This PR introduces comprehensive worktree management enhancements including cross-platform terminal integration, change-discard functionality, and path sanitization. It adds backend endpoints for terminal launching and change discarding, extends branch metadata with remote-tracking flags, enables configurable merge targets, and improves mobile responsiveness across multiple UI views while threading worktree branch context through the component hierarchy.

Changes

Cohort / File(s) Summary
Backend Worktree Routes - Path Sanitization
apps/server/src/routes/worktree/routes/diffs.ts, file-diff.ts, info.ts, status.ts
Applied featureId sanitization (replacing non-alphanumeric/hyphen/underscore chars with hyphens) across four route handlers to ensure consistent worktree path resolution and avoid special character issues.
Backend Worktree Routes - New Endpoints
apps/server/src/routes/worktree/index.ts, open-in-terminal.ts, discard-changes.ts
Added two new POST endpoints with handlers: /open-in-terminal (validates path, invokes platform terminal opening) and /discard-changes (performs git reset/checkout/clean sequence, tracks file counts). Route integration wires validation and middleware.
Backend Worktree Routes - Enhanced Branching
list-branches.ts, merge.ts
Added hasRemoteTracking flag to branch responses; extended merge endpoint to accept optional targetBranch parameter (defaults to 'main') with validation that target branch exists.
Backend Service Layer
apps/server/src/services/auto-mode-service.ts
Applied featureId sanitization to worktree path resolution; exposed branchName as optional field in running agents public data structure.
Frontend Dialog Components - Worktree Changes
apps/ui/src/components/views/board-view/dialogs/view-worktree-changes-dialog.tsx, agent-output-modal.tsx, merge-worktree-dialog.tsx, index.ts
New ViewWorktreeChangesDialog for displaying per-worktree diffs; extended MergeWorktreeDialog with dynamic targetBranch prop; added optional branchName prop to AgentOutputModal for per-branch diff viewing.
Frontend Board View - Main Worktree Targeting
apps/ui/src/components/views/board-view.tsx
Extracted mainWorktreeBranch from main branch selector to drive merge/conflict-resolution/PR defaults; extended handleResolveConflicts with ConflictResolutionSource parameter to differentiate worktree vs. main-branch resolution paths; threaded targetBranch through WorktreePanel and related components.
Frontend Board View - Header & Menu
board-header.tsx, header-overflow-menu.tsx, header-mobile-menu.tsx
Introduced responsive toolbar collapse logic with shouldCollapse* flags; added Agents concurrency slider in popover; created HeaderOverflowMenu for collapsed controls; increased mobile menu button size (h-8 w-8 → h-11 w-11).
Frontend Board View - Worktree Actions
worktree-panel/components/worktree-actions-dropdown.tsx, worktree-tab.tsx, worktree-panel.tsx
Extended dropdown with new actions (Open in Terminal, View Changes, Discard Changes); added hasRemoteTracking and targetBranch props; implemented ViewWorktreeChangesDialog and discard confirmation flow; updated Resolve Conflicts signature to include source parameter.
Frontend Board View - Hooks & Types
worktree-panel/hooks/use-branches.ts, use-worktree-actions.ts, types.ts, index.ts
Added hasRemoteTracking state exposure in useBranches; added handleOpenInTerminal action in useWorktreeActions; exported new ConflictResolutionSource type; updated WorktreePanelProps with targetBranch and new handlers.
Frontend Board View - Search & Utils
board-search-bar.tsx, use-board-actions.ts
Narrowed search width (max-w-md → max-w-xs), added search input height and ref; updated feature creation/update to use currentWorktreeBranch for 'current' mode.
Frontend Mobile Responsiveness - Multi-View
apps/ui/src/components/layout/project-switcher/project-switcher.tsx, sidebar.tsx
Added sidebarOpen state integration; switched positioning/visibility logic to responsive (fixed mobile overlay vs. relative desktop based on sidebarOpen and breakpoints).
Frontend Mobile Responsiveness - Content Views
context-view.tsx, github-issues-view.tsx, memory-view.tsx, github-issues-view/components/issue-detail-panel.tsx, github-prs-view.tsx
Implemented mobile-aware layouts hiding left panels on mobile when detail selected; added back buttons and conditional text labels; adjusted responsive widths and scroll handling across all views.
Frontend Mobile Responsiveness - Panels & Commands
worktree-panel/components/dev-server-logs-panel.tsx, board-view.tsx (responsive), components/ui/command.tsx
Updated DialogContent to full-width on mobile with constrained desktop sizes; increased input text size (text-sm → text-base); added responsive button sizing.
Frontend Additional Views
graph-view-page.tsx, running-agents-view.tsx
Added branchName prop to AgentOutputModal invocations.
Frontend API & Store
apps/ui/src/lib/electron.ts, http-api-client.ts, types/electron.d.ts
Extended RunningAgent with optional branchName; updated mergeFeature signature to accept targetBranch; added openInTerminal and discardChanges API methods with mocked and HTTP implementations; extended listBranches result with hasRemoteTracking.
Frontend Store & Hooks
apps/ui/src/store/app-store.ts, hooks/use-media-query.ts
Added pendingTerminalCwd state to TerminalState with setter; made sidebarOpen initialization responsive; added four new media-query hooks for toolbar narrowness detection.
Frontend Terminal View & Root
components/views/terminal-view.tsx, routes/__root.tsx
Implemented pending terminal CWD handling with automatic terminal creation and cwd propagation; removed mobile menu toggle, made streamer panel desktop-only, adjusted responsive margin calculation.
Platform Library - Terminal Integration
libs/platform/src/editor.ts, index.ts
Implemented cross-platform openInTerminal() with platform-specific handling (AppleScript for macOS, wt/cmd for Windows, terminal-emulator loop for Linux); exported from barrel.

Sequence Diagrams

sequenceDiagram
    participant User as UI User
    participant Handler as Route Handler
    participant Platform as `@automaker/platform`
    participant Terminal as OS Terminal
    
    User->>Handler: POST /open-in-terminal {worktreePath}
    Handler->>Handler: Validate worktreePath exists
    Handler->>Handler: Validate absolute path
    Handler->>Platform: openInTerminal(worktreePath)
    Platform->>Terminal: Platform-specific launch<br/>(AppleScript/wt/x-terminal-emulator)
    Terminal-->>Platform: Terminal opened
    Platform-->>Handler: {terminalName}
    Handler-->>User: 200 {success, message, terminalName}
Loading
sequenceDiagram
    participant User as UI User
    participant Handler as Route Handler
    participant Git as Git Operations
    participant Status as Status Check
    
    User->>Handler: POST /discard-changes {worktreePath}
    Handler->>Handler: Validate worktreePath
    Handler->>Status: git status (before)
    Status-->>Handler: {uncommitted files}
    Handler->>Git: git reset HEAD
    Handler->>Git: git checkout .
    Handler->>Git: git clean -fd
    Handler->>Status: git status (after)
    Status-->>Handler: {remaining changes}
    Handler-->>User: 200 {discarded, filesDiscarded,<br/>filesRemaining, branch}
Loading
sequenceDiagram
    participant User as UI User
    participant BoardView as Board View
    participant ConflictDialog as Conflict Resolution
    participant Git as Git Remote
    
    User->>BoardView: Click worktree (with Pull & Resolve)
    BoardView->>ConflictDialog: Show source selection<br/>(Worktree vs Main Branch)
    User->>ConflictDialog: Select source (worktree/selected)
    ConflictDialog->>Git: Pull origin/{sourceBranch}
    Git-->>ConflictDialog: Remote changes
    ConflictDialog->>Git: Merge with resolution strategy
    Git-->>ConflictDialog: Conflicts resolved
    ConflictDialog-->>BoardView: Update worktree state
    BoardView-->>User: Refresh UI
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested labels

enhancement, feature, ui-responsive, mobile-support


🐰 A Worktree's Tale

Fresh terminals spring to life,
Changes discarded without strife,
Branches dance on mobile screens,
Sanitized paths keep code pristine,
UI flows like springtime grace! 🌱✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% 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 comprehensively covers all major changes: mobile responsiveness, discard changes, open in terminal features, and UX improvements align with the 50-file changeset.

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

✨ Finishing touches
  • 📝 Generate docstrings

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 @Shironex, 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 application's user experience by introducing comprehensive mobile responsiveness across various views and adding several crucial Git-related functionalities. It empowers users with more control over their worktrees through new actions like opening in terminal, discarding changes, and viewing modifications, while also streamlining merge workflows with target branch selection. The changes aim to provide a more robust and adaptable development environment, particularly for users on diverse screen sizes and those requiring finer-grained Git operations.

Highlights

  • Mobile Responsiveness: The user interface has been made fully mobile-responsive across all views, including context, memory, GitHub issues, and pull requests, enhancing usability on smaller screens.
  • New 'Open in Terminal' Action: A new action allows users to open a terminal directly within a worktree's directory, with cross-platform support for macOS, Windows, and Linux.
  • New 'Discard Changes' Action: Users can now discard all uncommitted changes in a worktree, resetting it to a clean state. This includes resetting staged changes, discarding modified tracked files, and removing untracked files.
  • New 'View Changes' Dialog: A dedicated dialog has been introduced to inspect worktree modifications, providing a clear overview of changes.
  • Responsive Toolbar with Overflow Menu: The main board header now features a responsive toolbar that collapses less critical controls (Agents, Plan, Auto Mode) into an overflow menu on narrower screens.
  • Target Branch Selection for Merges: Merge operations now support selecting a target branch other than 'main', offering greater flexibility in development workflows.
  • Remote Tracking Detection for Push Button: The push button state now accurately reflects whether a branch has a remote tracking branch, improving clarity for users.
  • Consistent FeatureId Sanitization: Worktree paths now consistently sanitize featureId across all relevant routes and services, preventing potential issues with invalid characters.
  • Improved Sidebar Behavior on Mobile: The sidebar's behavior on mobile devices has been refined, including a toggle button in the header when closed and proper overlay positioning when open.

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

Using Gemini Code Assist

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

Invoking Gemini

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

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

Customization

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

Limitations & Feedback

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

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

Footnotes

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

@Shironex Shironex added Enhancement Improvements to existing functionality or UI. Do Not Merge Use this label if something should not be merged. type: feature A new capability or functionality that does not exist yet. Testers-Requested Request for others to test an enhancement or bug fix/etc. labels Jan 15, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Caution

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

⚠️ Outside diff range comments (5)
apps/ui/src/components/views/github-prs-view.tsx (1)

367-371: Bug: group-hover won't work without group class on parent.

The external link button uses group-hover:opacity-100 (line 435), but the parent div (line 368) is missing the group class. This means the button will always remain invisible (opacity-0) since the hover trigger class is absent.

🐛 Proposed fix
     <div
       className={cn(
-        'flex items-start gap-3 p-3 cursor-pointer hover:bg-accent/50 transition-colors',
+        'group flex items-start gap-3 p-3 cursor-pointer hover:bg-accent/50 transition-colors',
         isSelected && 'bg-accent'
       )}
       onClick={onClick}
     >

Also applies to: 432-442

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

60-71: Commit message is vulnerable to shell injection.

The options?.message value is interpolated directly into shell commands without escaping. A message containing " or backticks would break the command or allow arbitrary code execution.

🔒 Suggested fix — escape or use environment variables

One safe approach is to pass the message via environment variable:

-      const mergeCmd = options?.squash
-        ? `git merge --squash ${branchName}`
-        : `git merge ${branchName} -m "${options?.message || `Merge ${branchName}`}"`;
-
-      await execAsync(mergeCmd, { cwd: projectPath });
+      const mergeMessage = options?.message || `Merge ${branchName}`;
+      const mergeCmd = options?.squash
+        ? `git merge --squash -- "${branchName}"`
+        : `git merge -- "${branchName}" -m "$MERGE_MSG"`;
+
+      await execAsync(mergeCmd, { 
+        cwd: projectPath,
+        env: { ...process.env, MERGE_MSG: mergeMessage }
+      });

       // If squash merge, need to commit
       if (options?.squash) {
-        await execAsync(`git commit -m "${options?.message || `Merge ${branchName} (squash)`}"`, {
+        const squashMessage = options?.message || `Merge ${branchName} (squash)`;
+        await execAsync(`git commit -m "$COMMIT_MSG"`, {
           cwd: projectPath,
+          env: { ...process.env, COMMIT_MSG: squashMessage }
         });
       }

73-78: worktreePath also requires sanitization.

Similar to branch names, worktreePath is user input and should be validated before shell interpolation. A path like "; rm -rf /" would execute arbitrary commands.

Consider validating that worktreePath is a real path under an expected directory, or use path resolution and validation before use.

apps/ui/src/hooks/use-media-query.ts (1)

44-50: Documentation doesn't match implementation.

The docstring states the breakpoint is 768px, but the actual implementation uses 900px. This inconsistency could confuse developers maintaining this code.

📝 Suggested fix
 /**
- * Hook to detect if the device is mobile (screen width <= 768px)
+ * Hook to detect if the device is mobile (screen width <= 900px)
  * `@returns` boolean indicating if the device is mobile
  */
 export function useIsMobile(): boolean {
   return useMediaQuery('(max-width: 900px)');
 }
apps/server/src/routes/worktree/routes/file-diff.ts (1)

49-65: Fix command injection vulnerability in file-diff handlers.

The filePath parameter is user-provided and passed directly to execAsync with only double-quote wrapping. Double quotes do not protect against shell metacharacters—attackers can use $(...) or backticks to execute arbitrary commands.

This affects:

  • apps/server/src/routes/worktree/routes/file-diff.ts:49,61
  • apps/server/src/routes/git/routes/file-diff.ts:28,40

Use execFile or spawn instead of exec to avoid shell interpretation, or validate filePath to ensure it's a safe relative path without special characters.

🤖 Fix all issues with AI agents
In `@apps/server/src/routes/worktree/routes/open-in-terminal.ts`:
- Around line 12-49: createOpenInTerminalHandler currently performs the
open-in-terminal action but doesn't emit a server event; update the handler to
obtain/create the event emitter (via createEventEmitter()) and emit a
descriptive event (e.g., "worktree.openInTerminal" or similar) on success with
payload including worktreePath and result.terminalName, and emit a failure event
on catch including worktreePath and error details (use getErrorMessage(error))
before sending the HTTP response; reference the createOpenInTerminalHandler
function, the openInTerminal result, and the catch block to add the emitter.emit
calls.

In `@apps/ui/src/components/views/board-view/header-overflow-menu.tsx`:
- Around line 133-138: The DropdownMenuSeparator is rendered unconditionally and
can produce adjacent separators when only certain sections are collapsed; update
the render logic in header-overflow-menu.tsx to conditionally render the
DropdownMenuSeparator only when at least one of shouldCollapseAgents or
shouldCollapseAutoMode (or any other adjacent collapsed section) is true so you
don’t produce consecutive separators between the "Controls" label and the Plan
button; locate the separator instance near the Plan button render and wrap it in
a conditional that checks shouldCollapseAgents || shouldCollapseAutoMode (or
equivalent collapse flags) before rendering.

In `@apps/ui/src/components/views/terminal-view.tsx`:
- Around line 569-607: In createTerminalWithCwd, the toast success uses
pendingCwd.split('/') which breaks on Windows paths; change the basename
extraction to split on both separators (e.g., pendingCwd?.split(/[/\\]/).pop()
|| pendingCwd) or compute a safe basename variable before calling toast.success,
and use that variable in toast.success and any related logs; ensure pendingCwd
is null-checked (pendingCwd?) to avoid exceptions when clearing the cwd.
- Around line 530-622: The pending-cwd effect can race with the existing
createTerminal flow and produce duplicate terminals; modify createTerminal (the
shared function used at Lines ~931-934) to consume and clear
terminalState.pendingTerminalCwd atomically: check
terminalState.pendingTerminalCwd at start, use that value when creating the
session (instead of letting the effect also do it), call
setPendingTerminalCwd(null) immediately after successfully enqueuing/creating
the session (and also on error), and remove or guard the effect’s creation so it
only runs when createTerminal hasn't already consumed the pending value (e.g.,
have the effect return early if pendingTerminalCwdRef.current === pendingCwd or
if a consumed flag is set). Ensure you still set
pendingTerminalCreatedRef.current / call addTerminalToLayout and
setNewSessionIds in the same success path so behavior remains unchanged.

In `@apps/ui/src/routes/__root.tsx`:
- Around line 826-836: The breakpoint inconsistency causes the streamer panel to
overlap at 1024px because useIsTablet() uses max-width:1024px while the JSX/CSS
uses the Tailwind lg breakpoint (min-width:1024px) for lg:block; to fix, make
the threshold consistent by updating useIsTablet (or its underlying
use-media-query) to use max-width:1023px (so tablet is <1024) or change the CSS
logic to use min-width:1025px equivalent, and ensure the JSX logic around
streamerPanelOpen and the inline marginRight calculation (where isTablet is
referenced) aligns with the same numeric breakpoint so the panel visibility
(lg:block) and isTablet boolean never both apply at 1024px.

In `@libs/platform/src/editor.ts`:
- Around line 384-395: openInTerminal currently interpolates targetPath directly
into an AppleScript shell command (script variable) causing possible shell
injection; instead avoid embedding raw shell content and use AppleScript's safe
quoting: build an AppleScript that assigns the path to a variable and uses
quoted form of POSIX path (or quoted form of POSIX file) when composing the do
script command, and stop directly concatenating targetPath into the shell
string; update the script generation in openInTerminal (and the execFileAsync
call) to use this AppleScript quoting approach so all special characters in
targetPath are safely handled.
- Around line 433-463: The Linux branch currently awaits execFileAsync for each
terminal (see terminals array and the usage of execFileAsync(terminal.command,
terminal.args)), which blocks until the terminal exits; modify this to use
child_process.spawn in a non-blocking way: call spawn(terminal.command,
terminal.args, { detached: true, stdio: 'ignore' }), do not await it, and call
child.unref() after spawn; keep the commandExists check and return {
terminalName: terminal.name } immediately after spawning, and throw the same
Error('No terminal emulator found') if none are available.
🧹 Nitpick comments (22)
apps/ui/src/components/ui/command.tsx (1)

67-72: Redundant font-size declaration for iOS zoom prevention.

Both text-base (16px) and style={{ fontSize: '16px' }} set the same size. The inline style is likely a defensive measure to prevent iOS Safari auto-zoom on input focus (which triggers when font-size < 16px), but it's redundant with the Tailwind class and reduces flexibility for className overrides.

Consider either:

  1. Remove the inline style and rely on text-base (cleaner, more flexible)
  2. Keep the inline style but add a comment explaining the iOS zoom workaround for future maintainers
Option 1: Remove inline style
        className={cn(
          'placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-base outline-hidden disabled:cursor-not-allowed disabled:opacity-50',
          className
        )}
-        style={{ fontSize: '16px' }}
        {...props}
Option 2: Add explanatory comment
        className={cn(
          'placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-base outline-hidden disabled:cursor-not-allowed disabled:opacity-50',
          className
        )}
+        // Explicit 16px prevents iOS Safari auto-zoom on input focus
        style={{ fontSize: '16px' }}
        {...props}
apps/ui/src/components/views/github-prs-view.tsx (2)

73-80: Consider extracting to a shared utility.

This formatDate function is identical to the one in apps/ui/src/components/views/github-issues-view/utils.ts. Consider moving it to a shared location (e.g., @/lib/utils or a dedicated date utilities file) and importing from there to avoid duplication.

♻️ Suggested refactor
-  const formatDate = (dateString: string) => {
-    const date = new Date(dateString);
-    return date.toLocaleDateString('en-US', {
-      month: 'short',
-      day: 'numeric',
-      year: 'numeric',
-    });
-  };
+  // Import from shared utils instead:
+  // import { formatDate } from '@/lib/date-utils';

Or import from the existing location if appropriate:

import { formatDate } from '@/components/views/github-issues-view/utils';

277-286: Avoid redundant function calls.

getReviewStatus(selectedPR) is called 4 times. Store the result in a variable at the start of the detail section to improve readability and avoid repeated computation.

♻️ Suggested improvement
+          {(() => {
+            const reviewStatus = getReviewStatus(selectedPR);
+            return reviewStatus && (
              <span
                className={cn(
                  'px-2 py-0.5 rounded-full text-xs font-medium',
-                  getReviewStatus(selectedPR)!.bg,
-                  getReviewStatus(selectedPR)!.color
+                  reviewStatus.bg,
+                  reviewStatus.color
                )}
              >
-                {getReviewStatus(selectedPR)!.label}
+                {reviewStatus.label}
              </span>
+            );
+          })()}
-          )}

Or compute it once before the JSX block and reference the variable.

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

9-13: Duplicate execAsync definition — import from common.ts instead.

execAsync is already exported from ../common.ts (as shown in the relevant code snippets). Remove the local definition and import it alongside the existing imports.

♻️ Suggested fix
 import type { Request, Response } from 'express';
-import { exec } from 'child_process';
-import { promisify } from 'util';
-import { getErrorMessage, logError } from '../common.js';
-
-const execAsync = promisify(exec);
+import { execAsync, getErrorMessage, logError } from '../common.js';

83-83: Consider including targetBranch in the response.

The response only includes mergedBranch but not the target branch it was merged into. This information may be useful for the caller, especially now that the target is configurable.

-      res.json({ success: true, mergedBranch: branchName });
+      res.json({ success: true, mergedBranch: branchName, targetBranch: mergeTarget });
apps/ui/src/components/views/board-view/board-header.tsx (1)

131-140: Comment slightly inaccurate - button shows on tablet as well.

The comment says "only shows when sidebar is closed on mobile" but lg:hidden means it's visible on both mobile and tablet screens (below 1024px). Not a functional issue, just a minor documentation inaccuracy.

📝 Suggested comment fix
-        {/* Sidebar toggle button - only shows when sidebar is closed on mobile */}
+        {/* Sidebar toggle button - shows when sidebar is closed on screens below lg breakpoint */}
apps/ui/src/components/views/context-view.tsx (1)

756-762: Consider extracting the layout logic for improved readability.

The IIFE pattern works correctly, but extracting showList and showDetail as component-level computed values would improve readability and allow reuse elsewhere in the component.

📝 Alternative: Extract to component level
// Near other derived state (after isMobile declaration)
const showList = !isMobile || !selectedFile;
const showDetail = selectedFile !== null;

// Then in JSX, replace the IIFE with direct usage:
{showList && (
  <div className={cn(...)}>
    {/* file list */}
  </div>
)}
{showDetail && selectedFile && (
  <div className="flex-1 flex flex-col overflow-hidden">
    {/* detail view */}
  </div>
)}
{!selectedFile && !isMobile && (
  <div className="flex-1 flex items-center justify-center">
    {/* empty state */}
  </div>
)}
apps/ui/src/components/views/memory-view.tsx (2)

339-343: Consider extracting the IIFE to named variables for clarity.

The inline IIFE pattern works but is unconventional in React JSX. Since showList and showDetail are derived from isMobile and selectedFile, consider computing them before the return statement for better readability.

♻️ Suggested refactor

Move the computation before the return:

+ // On mobile, hide list when a file is selected
+ const showList = !isMobile || !selectedFile;
+ const showDetail = selectedFile !== null;
+
  return (
    <div className="flex-1 flex flex-col overflow-hidden content-bg" data-testid="memory-view">
      {/* Header */}
      ...
      {/* Main content area with file list and editor */}
-     {/* On mobile, hide list when a file is selected */}
-     {(() => {
-       const showList = !isMobile || !selectedFile;
-       const showDetail = selectedFile !== null;
-
-       return (
-         <div className="flex-1 flex overflow-hidden">
+     <div className="flex-1 flex overflow-hidden">
          ...
-         </div>
-       );
-     })()}
+     </div>

434-443: Add aria-label for accessibility on the icon-only back button.

The back button only contains an icon without text. Screen reader users would benefit from an explicit label.

♿ Suggested fix
                     <Button
                       variant="ghost"
                       size="sm"
                       onClick={() => setSelectedFile(null)}
                       className="mr-1 -ml-1"
+                      aria-label="Back to file list"
                     >
                       <ArrowLeft className="w-4 h-4" />
                     </Button>
apps/ui/src/components/views/github-issues-view/components/issue-detail-panel.tsx (1)

78-83: Add aria-label for accessibility on the icon-only back button.

Similar to the memory-view, this back button only contains an icon. Adding an aria-label improves accessibility for screen reader users.

♿ Suggested fix
           {isMobile && (
-            <Button variant="ghost" size="sm" onClick={onClose} className="mr-1 -ml-1">
+            <Button variant="ghost" size="sm" onClick={onClose} className="mr-1 -ml-1" aria-label="Back to issues list">
               <ArrowLeft className="h-4 w-4" />
             </Button>
           )}
apps/server/src/routes/worktree/routes/info.ts (1)

31-34: Good defensive sanitization for consistent path resolution.

This sanitization ensures the worktree lookup uses the same path format as when worktrees are created. The inline comment referencing create.ts is helpful for maintainability.

Consider extracting this sanitization logic into a shared utility function (e.g., sanitizeFeatureId in common.ts) to ensure consistency and avoid duplication across all worktree routes.

♻️ Optional: Extract to shared utility

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

export function sanitizeFeatureId(featureId: string): string {
  return featureId.replace(/[^a-zA-Z0-9_-]/g, '-');
}

Then in each route:

-const sanitizedFeatureId = featureId.replace(/[^a-zA-Z0-9_-]/g, '-');
+const sanitizedFeatureId = sanitizeFeatureId(featureId);
libs/platform/src/editor.ts (1)

345-371: Remove or use getTerminalInfo() (currently dead code).
It’s unused, and openInTerminal() has its own platform branching anyway. Keeping this increases maintenance surface.

apps/ui/src/store/app-store.ts (1)

434-435: pendingTerminalCwd should ideally be scoped (or at least guarded) by project.
Because it’s global, a project switch before it’s consumed could open a terminal in a path unrelated to the active project. Consider storing { cwd, projectPath } (and ignore if project mismatches) or clearing it on project change if that’s acceptable UX.

Also applies to: 1092-1093, 2645-2648, 2739-2745, 1297-1298

apps/ui/src/components/layout/project-switcher/project-switcher.tsx (1)

270-280: Consider adding a backdrop overlay for mobile consistency.

The responsive logic is well-structured: fixed overlay on mobile when open, hidden when closed, and always relative on desktop. However, the Sidebar component (sidebar.tsx, lines 257-263) includes a backdrop overlay (bg-black/50) when open on mobile to allow clicking outside to close the sidebar. The ProjectSwitcher lacks this, which may create an inconsistent UX where users can't tap outside to close it on mobile.

If the intent is for both panels to share a single backdrop controlled elsewhere, this is fine. Otherwise, consider adding a similar backdrop for parity.

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

11-17: Consider importing WorktreeInfo from the existing types module.

This local WorktreeInfo interface duplicates a subset of the interface already defined in worktree-panel/types.ts. Importing from the shared types ensures consistency and reduces maintenance burden if the type evolves.

♻️ Proposed refactor
-import { GitDiffPanel } from '@/components/ui/git-diff-panel';
+import { GitDiffPanel } from '@/components/ui/git-diff-panel';
+import type { WorktreeInfo } from '@/components/views/board-view/worktree-panel';

-interface WorktreeInfo {
-  path: string;
-  branch: string;
-  isMain: boolean;
-  hasChanges?: boolean;
-  changedFilesCount?: number;
-}
-

Then update the props interface to use Pick if you only need a subset:

interface ViewWorktreeChangesDialogProps {
  open: boolean;
  onOpenChange: (open: boolean) => void;
  worktree: Pick<WorktreeInfo, 'path' | 'branch' | 'isMain' | 'hasChanges' | 'changedFilesCount'> | null;
  projectPath: string;
}

55-55: Minor: Extra whitespace in className.

There's a double space between sm:max-h-[60vh] and overflow-y-auto.

✨ Suggested fix
-        <div className="flex-1 sm:min-h-[600px] sm:max-h-[60vh]  overflow-y-auto scrollbar-visible -mx-6 -mb-6">
+        <div className="flex-1 sm:min-h-[600px] sm:max-h-[60vh] overflow-y-auto scrollbar-visible -mx-6 -mb-6">
apps/ui/src/components/views/board-view/dialogs/agent-output-modal.tsx (1)

394-394: Consider renaming the featureId prop in GitDiffPanel for semantic clarity.

The branchName value is passed to GitDiffPanel's featureId prop via fallback (branchName || featureId). While this works as intended, the prop name doesn't reflect that it accepts a branch name when available. The prop could be renamed to branchOrFeatureId, worktreeId, or similar to make the intent clearer and align with the worktree API it ultimately feeds to.

apps/server/src/routes/worktree/routes/discard-changes.ts (2)

61-75: Verify: The three-step discard approach is sound but silently swallows errors.

The discard sequence (git reset HEAD, git checkout ., git clean -fd) is correct and follows Git best practices. Silently catching errors is acceptable since these commands may legitimately fail (e.g., nothing staged for reset).

However, consider logging these errors at debug level for troubleshooting, even if they're expected to fail sometimes.

Optional: Add debug logging for suppressed errors
       // 1. Reset any staged changes
-      await execAsync('git reset HEAD', { cwd: worktreePath }).catch(() => {
-        // Ignore errors - might fail if there's nothing staged
-      });
+      await execAsync('git reset HEAD', { cwd: worktreePath }).catch((err) => {
+        // Expected to fail if nothing staged - log for debugging
+        console.debug('git reset HEAD (expected):', err.message);
+      });

20-111: Consider emitting events for frontend streaming.

Per coding guidelines, server operations should use createEventEmitter() from lib/events.ts to emit events that stream to the frontend via WebSocket. This would allow the UI to show real-time progress during the discard operation.

Currently, this handler only returns a final response. For a destructive operation like discarding changes, progress feedback could improve UX.

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

1865-1878: Minor: discardChanges mock always returns filesDiscarded: 0.

The mock returns filesDiscarded: 0 even when discarded: true. While this works, a more realistic mock might return a non-zero count to better simulate the actual behavior.

Optional: Return a realistic file count in the mock
     discardChanges: async (worktreePath: string) => {
       console.log('[Mock] Discarding changes:', { worktreePath });
       return {
         success: true,
         result: {
           discarded: true,
-          filesDiscarded: 0,
+          filesDiscarded: 3,
           filesRemaining: 0,
           branch: 'main',
-          message: 'Mock: Changes discarded successfully',
+          message: 'Discarded 3 files',
         },
       };
     },
apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx (2)

346-375: Consider: Dialog components are duplicated between mobile and desktop views.

The ViewWorktreeChangesDialog, ConfirmDialog, and DevServerLogsPanel are rendered in both the mobile view (lines 346-375) and desktop view (lines 532-560). Since they share the same state and handlers, they could be extracted to render once after the conditional.

This is a minor refactor that would reduce code duplication and potential maintenance overhead.

Optional: Extract dialogs to render once
   // Mobile/Desktop view code above...

+  // Shared dialogs rendered once
+  const dialogs = (
+    <>
+      <ViewWorktreeChangesDialog
+        open={viewChangesDialogOpen}
+        onOpenChange={setViewChangesDialogOpen}
+        worktree={viewChangesWorktree}
+        projectPath={projectPath}
+      />
+      <ConfirmDialog
+        open={discardChangesDialogOpen}
+        onOpenChange={setDiscardChangesDialogOpen}
+        onConfirm={handleConfirmDiscardChanges}
+        title="Discard Changes"
+        description={`Are you sure you want to discard all changes in "${discardChangesWorktree?.branch}"? This will reset staged changes, discard modifications to tracked files, and remove untracked files. This action cannot be undone.`}
+        icon={Undo2}
+        iconClassName="text-destructive"
+        confirmText="Discard Changes"
+        confirmVariant="destructive"
+      />
+      <DevServerLogsPanel
+        open={logPanelOpen}
+        onClose={handleCloseLogPanel}
+        worktree={logPanelWorktree}
+        onStopDevServer={handleStopDevServer}
+        onOpenDevServerUrl={handleOpenDevServerUrl}
+      />
+    </>
+  );

   if (isMobile) {
     return (
       <div className="...">
         {/* Mobile content without dialogs */}
-        <ViewWorktreeChangesDialog ... />
-        <ConfirmDialog ... />
-        <DevServerLogsPanel ... />
+        {dialogs}
       </div>
     );
   }

   return (
     <div className="...">
       {/* Desktop content without dialogs */}
-      <ViewWorktreeChangesDialog ... />
-      <ConfirmDialog ... />
-      <DevServerLogsPanel ... />
+      {dialogs}
     </div>
   );

Also applies to: 532-560


205-228: Consider adding a loading state during discard operation.

The handleConfirmDiscardChanges function is async but there's no visual feedback while the discard operation is in progress. Users might click the confirm button multiple times or think the UI is frozen.

Optional: Add loading state for discard operation
+  const [isDiscarding, setIsDiscarding] = useState(false);

   const handleConfirmDiscardChanges = useCallback(async () => {
     if (!discardChangesWorktree) return;

+    setIsDiscarding(true);
     try {
       const api = getHttpApiClient();
       const result = await api.worktree.discardChanges(discardChangesWorktree.path);
       // ... rest of handler
     } catch (error) {
       // ... error handling
+    } finally {
+      setIsDiscarding(false);
     }
   }, [discardChangesWorktree, fetchWorktrees]);

Then pass isDiscarding to ConfirmDialog as a loading prop if it supports one.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 78e5ddb and fdae0b6.

📒 Files selected for processing (50)
  • apps/server/src/routes/worktree/index.ts
  • apps/server/src/routes/worktree/routes/diffs.ts
  • apps/server/src/routes/worktree/routes/discard-changes.ts
  • apps/server/src/routes/worktree/routes/file-diff.ts
  • apps/server/src/routes/worktree/routes/info.ts
  • apps/server/src/routes/worktree/routes/list-branches.ts
  • apps/server/src/routes/worktree/routes/merge.ts
  • apps/server/src/routes/worktree/routes/open-in-terminal.ts
  • apps/server/src/routes/worktree/routes/status.ts
  • apps/server/src/services/auto-mode-service.ts
  • apps/ui/src/components/layout/project-switcher/project-switcher.tsx
  • apps/ui/src/components/layout/sidebar.tsx
  • apps/ui/src/components/ui/command.tsx
  • apps/ui/src/components/views/board-view.tsx
  • apps/ui/src/components/views/board-view/board-header.tsx
  • apps/ui/src/components/views/board-view/board-search-bar.tsx
  • apps/ui/src/components/views/board-view/dialogs/add-feature-dialog.tsx
  • apps/ui/src/components/views/board-view/dialogs/agent-output-modal.tsx
  • apps/ui/src/components/views/board-view/dialogs/index.ts
  • apps/ui/src/components/views/board-view/dialogs/merge-worktree-dialog.tsx
  • apps/ui/src/components/views/board-view/dialogs/view-worktree-changes-dialog.tsx
  • apps/ui/src/components/views/board-view/header-mobile-menu.tsx
  • apps/ui/src/components/views/board-view/header-overflow-menu.tsx
  • apps/ui/src/components/views/board-view/hooks/use-board-actions.ts
  • apps/ui/src/components/views/board-view/worktree-panel/components/branch-switch-dropdown.tsx
  • apps/ui/src/components/views/board-view/worktree-panel/components/dev-server-logs-panel.tsx
  • apps/ui/src/components/views/board-view/worktree-panel/components/worktree-actions-dropdown.tsx
  • apps/ui/src/components/views/board-view/worktree-panel/components/worktree-tab.tsx
  • apps/ui/src/components/views/board-view/worktree-panel/hooks/use-branches.ts
  • apps/ui/src/components/views/board-view/worktree-panel/hooks/use-worktree-actions.ts
  • apps/ui/src/components/views/board-view/worktree-panel/index.ts
  • apps/ui/src/components/views/board-view/worktree-panel/types.ts
  • apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx
  • apps/ui/src/components/views/context-view.tsx
  • apps/ui/src/components/views/github-issues-view.tsx
  • apps/ui/src/components/views/github-issues-view/components/issue-detail-panel.tsx
  • apps/ui/src/components/views/github-issues-view/types.ts
  • apps/ui/src/components/views/github-prs-view.tsx
  • apps/ui/src/components/views/graph-view-page.tsx
  • apps/ui/src/components/views/memory-view.tsx
  • apps/ui/src/components/views/running-agents-view.tsx
  • apps/ui/src/components/views/terminal-view.tsx
  • apps/ui/src/hooks/use-media-query.ts
  • apps/ui/src/lib/electron.ts
  • apps/ui/src/lib/http-api-client.ts
  • apps/ui/src/routes/__root.tsx
  • apps/ui/src/store/app-store.ts
  • apps/ui/src/types/electron.d.ts
  • libs/platform/src/editor.ts
  • libs/platform/src/index.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Always import from shared packages (@automaker/*), never from old relative paths

Files:

  • apps/ui/src/components/views/board-view/worktree-panel/index.ts
  • apps/server/src/routes/worktree/routes/info.ts
  • apps/ui/src/components/views/github-issues-view/types.ts
  • apps/ui/src/components/views/board-view/dialogs/view-worktree-changes-dialog.tsx
  • apps/server/src/routes/worktree/routes/diffs.ts
  • apps/server/src/routes/worktree/routes/open-in-terminal.ts
  • apps/server/src/routes/worktree/routes/status.ts
  • apps/server/src/routes/worktree/routes/merge.ts
  • apps/ui/src/components/views/board-view/worktree-panel/components/dev-server-logs-panel.tsx
  • apps/ui/src/components/views/github-prs-view.tsx
  • apps/server/src/routes/worktree/routes/file-diff.ts
  • apps/ui/src/components/views/board-view/dialogs/add-feature-dialog.tsx
  • apps/ui/src/components/views/board-view/dialogs/index.ts
  • apps/ui/src/components/views/board-view/worktree-panel/types.ts
  • apps/server/src/routes/worktree/routes/discard-changes.ts
  • apps/ui/src/components/views/board-view/header-overflow-menu.tsx
  • libs/platform/src/index.ts
  • apps/ui/src/components/views/board-view/header-mobile-menu.tsx
  • libs/platform/src/editor.ts
  • apps/ui/src/components/views/board-view/worktree-panel/hooks/use-branches.ts
  • apps/ui/src/components/views/board-view/worktree-panel/hooks/use-worktree-actions.ts
  • apps/ui/src/components/layout/sidebar.tsx
  • apps/ui/src/components/views/board-view/hooks/use-board-actions.ts
  • apps/server/src/routes/worktree/index.ts
  • apps/ui/src/components/views/board-view/dialogs/agent-output-modal.tsx
  • apps/ui/src/store/app-store.ts
  • apps/ui/src/components/views/github-issues-view.tsx
  • apps/ui/src/components/views/board-view/dialogs/merge-worktree-dialog.tsx
  • apps/ui/src/routes/__root.tsx
  • apps/ui/src/components/views/board-view/board-header.tsx
  • apps/ui/src/components/ui/command.tsx
  • apps/ui/src/components/views/context-view.tsx
  • apps/ui/src/components/layout/project-switcher/project-switcher.tsx
  • apps/ui/src/components/views/running-agents-view.tsx
  • apps/ui/src/components/views/board-view/worktree-panel/components/branch-switch-dropdown.tsx
  • apps/ui/src/components/views/memory-view.tsx
  • apps/ui/src/components/views/graph-view-page.tsx
  • apps/ui/src/components/views/board-view/board-search-bar.tsx
  • apps/ui/src/components/views/github-issues-view/components/issue-detail-panel.tsx
  • apps/ui/src/hooks/use-media-query.ts
  • apps/ui/src/components/views/board-view/worktree-panel/components/worktree-actions-dropdown.tsx
  • apps/ui/src/lib/http-api-client.ts
  • apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx
  • apps/ui/src/lib/electron.ts
  • apps/ui/src/types/electron.d.ts
  • apps/ui/src/components/views/board-view/worktree-panel/components/worktree-tab.tsx
  • apps/server/src/routes/worktree/routes/list-branches.ts
  • apps/ui/src/components/views/board-view.tsx
  • apps/ui/src/components/views/terminal-view.tsx
  • apps/server/src/services/auto-mode-service.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use resolveModelString() from @automaker/model-resolver to convert model aliases (haiku, sonnet, opus) to full model names

Files:

  • apps/ui/src/components/views/board-view/worktree-panel/index.ts
  • apps/server/src/routes/worktree/routes/info.ts
  • apps/ui/src/components/views/github-issues-view/types.ts
  • apps/ui/src/components/views/board-view/dialogs/view-worktree-changes-dialog.tsx
  • apps/server/src/routes/worktree/routes/diffs.ts
  • apps/server/src/routes/worktree/routes/open-in-terminal.ts
  • apps/server/src/routes/worktree/routes/status.ts
  • apps/server/src/routes/worktree/routes/merge.ts
  • apps/ui/src/components/views/board-view/worktree-panel/components/dev-server-logs-panel.tsx
  • apps/ui/src/components/views/github-prs-view.tsx
  • apps/server/src/routes/worktree/routes/file-diff.ts
  • apps/ui/src/components/views/board-view/dialogs/add-feature-dialog.tsx
  • apps/ui/src/components/views/board-view/dialogs/index.ts
  • apps/ui/src/components/views/board-view/worktree-panel/types.ts
  • apps/server/src/routes/worktree/routes/discard-changes.ts
  • apps/ui/src/components/views/board-view/header-overflow-menu.tsx
  • libs/platform/src/index.ts
  • apps/ui/src/components/views/board-view/header-mobile-menu.tsx
  • libs/platform/src/editor.ts
  • apps/ui/src/components/views/board-view/worktree-panel/hooks/use-branches.ts
  • apps/ui/src/components/views/board-view/worktree-panel/hooks/use-worktree-actions.ts
  • apps/ui/src/components/layout/sidebar.tsx
  • apps/ui/src/components/views/board-view/hooks/use-board-actions.ts
  • apps/server/src/routes/worktree/index.ts
  • apps/ui/src/components/views/board-view/dialogs/agent-output-modal.tsx
  • apps/ui/src/store/app-store.ts
  • apps/ui/src/components/views/github-issues-view.tsx
  • apps/ui/src/components/views/board-view/dialogs/merge-worktree-dialog.tsx
  • apps/ui/src/routes/__root.tsx
  • apps/ui/src/components/views/board-view/board-header.tsx
  • apps/ui/src/components/ui/command.tsx
  • apps/ui/src/components/views/context-view.tsx
  • apps/ui/src/components/layout/project-switcher/project-switcher.tsx
  • apps/ui/src/components/views/running-agents-view.tsx
  • apps/ui/src/components/views/board-view/worktree-panel/components/branch-switch-dropdown.tsx
  • apps/ui/src/components/views/memory-view.tsx
  • apps/ui/src/components/views/graph-view-page.tsx
  • apps/ui/src/components/views/board-view/board-search-bar.tsx
  • apps/ui/src/components/views/github-issues-view/components/issue-detail-panel.tsx
  • apps/ui/src/hooks/use-media-query.ts
  • apps/ui/src/components/views/board-view/worktree-panel/components/worktree-actions-dropdown.tsx
  • apps/ui/src/lib/http-api-client.ts
  • apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx
  • apps/ui/src/lib/electron.ts
  • apps/ui/src/types/electron.d.ts
  • apps/ui/src/components/views/board-view/worktree-panel/components/worktree-tab.tsx
  • apps/server/src/routes/worktree/routes/list-branches.ts
  • apps/ui/src/components/views/board-view.tsx
  • apps/ui/src/components/views/terminal-view.tsx
  • apps/server/src/services/auto-mode-service.ts
apps/server/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use createEventEmitter() from lib/events.ts for all server operations to emit events that stream to frontend via WebSocket

Files:

  • apps/server/src/routes/worktree/routes/info.ts
  • apps/server/src/routes/worktree/routes/diffs.ts
  • apps/server/src/routes/worktree/routes/open-in-terminal.ts
  • apps/server/src/routes/worktree/routes/status.ts
  • apps/server/src/routes/worktree/routes/merge.ts
  • apps/server/src/routes/worktree/routes/file-diff.ts
  • apps/server/src/routes/worktree/routes/discard-changes.ts
  • apps/server/src/routes/worktree/index.ts
  • apps/server/src/routes/worktree/routes/list-branches.ts
  • apps/server/src/services/auto-mode-service.ts
🧠 Learnings (3)
📚 Learning: 2025-12-28T05:07:48.147Z
Learnt from: CR
Repo: AutoMaker-Org/automaker PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T05:07:48.147Z
Learning: Each feature executes in an isolated git worktree created via automaker/git-utils to protect the main branch during AI agent execution

Applied to files:

  • apps/server/src/routes/worktree/routes/info.ts
  • apps/server/src/routes/worktree/routes/diffs.ts
  • apps/server/src/routes/worktree/routes/status.ts
  • apps/server/src/routes/worktree/routes/file-diff.ts
  • apps/ui/src/components/views/board-view/hooks/use-board-actions.ts
  • apps/ui/src/components/views/board-view.tsx
  • apps/server/src/services/auto-mode-service.ts
📚 Learning: 2025-12-30T01:02:07.114Z
Learnt from: illia1f
Repo: AutoMaker-Org/automaker PR: 324
File: apps/ui/src/components/views/board-view/components/kanban-card/kanban-card.tsx:122-131
Timestamp: 2025-12-30T01:02:07.114Z
Learning: Tailwind CSS v4 uses postfix syntax for the important modifier: append ! at the end of the utility class (e.g., backdrop-blur-[0px]! or hover:bg-red-500!). The older v3 style used a prefix (!) at the start (e.g., !backdrop-blur-[0px]); prefer the postfix form for consistency across TSX files.

Applied to files:

  • apps/ui/src/components/views/board-view/dialogs/view-worktree-changes-dialog.tsx
  • apps/ui/src/components/views/board-view/worktree-panel/components/dev-server-logs-panel.tsx
  • apps/ui/src/components/views/github-prs-view.tsx
  • apps/ui/src/components/views/board-view/dialogs/add-feature-dialog.tsx
  • apps/ui/src/components/views/board-view/header-overflow-menu.tsx
  • apps/ui/src/components/views/board-view/header-mobile-menu.tsx
  • apps/ui/src/components/layout/sidebar.tsx
  • apps/ui/src/components/views/board-view/dialogs/agent-output-modal.tsx
  • apps/ui/src/components/views/github-issues-view.tsx
  • apps/ui/src/components/views/board-view/dialogs/merge-worktree-dialog.tsx
  • apps/ui/src/routes/__root.tsx
  • apps/ui/src/components/views/board-view/board-header.tsx
  • apps/ui/src/components/ui/command.tsx
  • apps/ui/src/components/views/context-view.tsx
  • apps/ui/src/components/layout/project-switcher/project-switcher.tsx
  • apps/ui/src/components/views/running-agents-view.tsx
  • apps/ui/src/components/views/board-view/worktree-panel/components/branch-switch-dropdown.tsx
  • apps/ui/src/components/views/memory-view.tsx
  • apps/ui/src/components/views/graph-view-page.tsx
  • apps/ui/src/components/views/board-view/board-search-bar.tsx
  • apps/ui/src/components/views/github-issues-view/components/issue-detail-panel.tsx
  • apps/ui/src/components/views/board-view/worktree-panel/components/worktree-actions-dropdown.tsx
  • apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx
  • apps/ui/src/components/views/board-view/worktree-panel/components/worktree-tab.tsx
  • apps/ui/src/components/views/board-view.tsx
  • apps/ui/src/components/views/terminal-view.tsx
📚 Learning: 2025-12-28T05:07:48.147Z
Learnt from: CR
Repo: AutoMaker-Org/automaker PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T05:07:48.147Z
Learning: Frontend UI must use TanStack Router for file-based routing, organize components in components/views/, implement stores with Zustand, and use custom hooks in hooks/ directory

Applied to files:

  • apps/ui/src/components/views/board-view/worktree-panel/hooks/use-worktree-actions.ts
🧬 Code graph analysis (13)
apps/ui/src/components/views/board-view/dialogs/view-worktree-changes-dialog.tsx (5)
apps/ui/src/components/views/board-view/worktree-panel/index.ts (1)
  • WorktreeInfo (3-3)
apps/ui/src/components/views/board-view/worktree-panel/types.ts (1)
  • WorktreeInfo (9-18)
apps/ui/src/lib/electron.ts (1)
  • WorktreeInfo (193-193)
apps/ui/src/types/electron.d.ts (1)
  • WorktreeInfo (625-630)
apps/ui/src/components/views/board-view/dialogs/index.ts (1)
  • ViewWorktreeChangesDialog (11-11)
apps/server/src/routes/worktree/routes/open-in-terminal.ts (2)
libs/platform/src/editor.ts (1)
  • openInTerminal (384-464)
libs/platform/src/index.ts (1)
  • openInTerminal (172-172)
apps/server/src/routes/worktree/routes/merge.ts (1)
apps/server/src/routes/worktree/common.ts (1)
  • execAsync (12-12)
apps/ui/src/components/views/github-prs-view.tsx (3)
apps/ui/src/hooks/use-media-query.ts (1)
  • useIsMobile (48-50)
apps/ui/src/lib/utils.ts (1)
  • cn (6-8)
apps/ui/src/components/views/github-issues-view/utils.ts (1)
  • formatDate (21-28)
apps/ui/src/components/views/board-view/worktree-panel/types.ts (2)
apps/ui/src/components/views/board-view/worktree-panel/index.ts (2)
  • ConflictResolutionSource (8-8)
  • WorktreeInfo (3-3)
apps/ui/src/types/electron.d.ts (1)
  • WorktreeInfo (625-630)
apps/server/src/routes/worktree/index.ts (4)
apps/server/src/middleware/validate-paths.ts (1)
  • validatePathParams (38-87)
apps/server/src/routes/worktree/routes/open-in-terminal.ts (1)
  • createOpenInTerminalHandler (12-50)
apps/server/src/routes/worktree/middleware.ts (1)
  • requireGitRepoOnly (72-75)
apps/server/src/routes/worktree/routes/discard-changes.ts (1)
  • createDiscardChangesHandler (20-112)
apps/ui/src/components/views/github-issues-view.tsx (5)
apps/ui/src/hooks/use-media-query.ts (1)
  • useIsMobile (48-50)
apps/ui/src/lib/utils.ts (1)
  • cn (6-8)
apps/ui/src/components/views/github-issues-view/components/issues-list-header.tsx (1)
  • IssuesListHeader (12-38)
apps/ui/src/components/views/github-issues-view/components/issue-row.tsx (1)
  • IssueRow (16-136)
apps/ui/src/components/views/github-issues-view/utils.ts (1)
  • formatDate (21-28)
apps/ui/src/routes/__root.tsx (1)
apps/ui/src/hooks/use-media-query.ts (1)
  • useIsTablet (56-58)
apps/ui/src/components/views/board-view/board-header.tsx (2)
apps/ui/src/hooks/use-media-query.ts (3)
  • useIsNarrowToolbarForAgents (92-94)
  • useIsNarrowToolbarForPlan (74-76)
  • useIsNarrowToolbarForAutoMode (83-85)
apps/ui/src/components/views/board-view/header-overflow-menu.tsx (1)
  • HeaderOverflowMenu (38-157)
apps/ui/src/components/views/memory-view.tsx (3)
apps/ui/src/hooks/use-media-query.ts (1)
  • useIsMobile (48-50)
apps/ui/src/lib/utils.ts (1)
  • cn (6-8)
apps/ui/src/components/ui/markdown.tsx (1)
  • Markdown (16-51)
apps/ui/src/components/views/github-issues-view/components/issue-detail-panel.tsx (3)
apps/ui/src/components/ui/button.tsx (1)
  • Button (108-108)
apps/ui/src/lib/utils.ts (1)
  • cn (6-8)
apps/ui/src/components/shared/model-override-trigger.tsx (1)
  • ModelOverrideTrigger (37-126)
apps/ui/src/components/views/board-view/worktree-panel/components/worktree-actions-dropdown.tsx (4)
apps/ui/src/components/views/board-view/worktree-panel/types.ts (3)
  • WorktreeInfo (9-18)
  • PRInfo (42-65)
  • ConflictResolutionSource (68-68)
libs/types/src/settings.ts (1)
  • WorktreeInfo (576-587)
apps/ui/src/components/views/board-view/worktree-panel/components/tooltip-wrapper.tsx (1)
  • TooltipWrapper (21-44)
apps/ui/src/lib/utils.ts (1)
  • cn (6-8)
apps/ui/src/components/views/board-view.tsx (5)
apps/ui/src/components/views/board-view/worktree-panel/index.ts (2)
  • WorktreeInfo (3-3)
  • ConflictResolutionSource (8-8)
apps/ui/src/components/views/board-view/worktree-panel/types.ts (2)
  • WorktreeInfo (9-18)
  • ConflictResolutionSource (68-68)
apps/ui/src/types/electron.d.ts (1)
  • WorktreeInfo (625-630)
libs/types/src/settings.ts (1)
  • WorktreeInfo (576-587)
libs/types/src/index.ts (1)
  • WorktreeInfo (130-130)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: e2e

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.


// Validate source branch exists
try {
await execAsync(`git rev-parse --verify ${branchName}`, { cwd: projectPath });
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Command injection vulnerability — sanitize branch names before shell interpolation.

branchName and mergeTarget are user-supplied inputs interpolated directly into shell commands. A malicious branch name like "; rm -rf /" or $(malicious_command) could execute arbitrary code.

Use -- to separate options from arguments and validate/escape branch names, or use a library like execa with array arguments to avoid shell interpretation.

🔒 Suggested safer approach
-      await execAsync(`git rev-parse --verify ${branchName}`, { cwd: projectPath });
+      // Validate branch name format (alphanumeric, hyphens, underscores, slashes)
+      const validBranchRegex = /^[a-zA-Z0-9_\-\/]+$/;
+      if (!validBranchRegex.test(branchName)) {
+        res.status(400).json({
+          success: false,
+          error: 'Invalid branch name format',
+        });
+        return;
+      }
+      await execAsync(`git rev-parse --verify -- "${branchName}"`, { cwd: projectPath });

Apply similar validation and quoting for mergeTarget and all other git commands in this file (lines 50, 61-62, 68, 75, 78).

📝 Committable suggestion

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

Suggested change
await execAsync(`git rev-parse --verify ${branchName}`, { cwd: projectPath });
// Validate branch name format (alphanumeric, hyphens, underscores, slashes)
const validBranchRegex = /^[a-zA-Z0-9_\-\/]+$/;
if (!validBranchRegex.test(branchName)) {
res.status(400).json({
success: false,
error: 'Invalid branch name format',
});
return;
}
await execAsync(`git rev-parse --verify -- "${branchName}"`, { cwd: projectPath });

Comment on lines +12 to +49
export function createOpenInTerminalHandler() {
return async (req: Request, res: Response): Promise<void> => {
try {
const { worktreePath } = req.body as {
worktreePath: string;
};

if (!worktreePath) {
res.status(400).json({
success: false,
error: 'worktreePath required',
});
return;
}

// Security: Validate that worktreePath is an absolute path
if (!isAbsolute(worktreePath)) {
res.status(400).json({
success: false,
error: 'worktreePath must be an absolute path',
});
return;
}

// Use the platform utility to open in terminal
const result = await openInTerminal(worktreePath);
res.json({
success: true,
result: {
message: `Opened terminal in ${worktreePath}`,
terminalName: result.terminalName,
},
});
} catch (error) {
logError(error, 'Open in terminal failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Consider emitting an event via createEventEmitter() for this server operation.
The repo guideline for apps/server/src/**/* says server operations should emit events that stream to the frontend. This endpoint currently doesn’t.

🤖 Prompt for AI Agents
In `@apps/server/src/routes/worktree/routes/open-in-terminal.ts` around lines 12 -
49, createOpenInTerminalHandler currently performs the open-in-terminal action
but doesn't emit a server event; update the handler to obtain/create the event
emitter (via createEventEmitter()) and emit a descriptive event (e.g.,
"worktree.openInTerminal" or similar) on success with payload including
worktreePath and result.terminalName, and emit a failure event on catch
including worktreePath and error details (use getErrorMessage(error)) before
sending the HTTP response; reference the createOpenInTerminalHandler function,
the openInTerminal result, and the catch block to add the emitter.emit calls.

⚠️ Potential issue | 🟠 Major

Tighten authorization/validation: absolute path isn’t enough.
This endpoint can open a terminal at any absolute path on the machine. Please constrain worktreePath to a known worktree for the current project/user (or accept featureId and resolve the path server-side), and at least verify it exists + is a directory (ideally using realpath before allowlisting).

Possible hardening sketch
 import type { Request, Response } from 'express';
 import { isAbsolute } from 'path';
+import { realpath, stat } from 'fs/promises';
 import { openInTerminal } from '@automaker/platform';
 import { getErrorMessage, logError } from '../common.js';

 export function createOpenInTerminalHandler() {
   return async (req: Request, res: Response): Promise<void> => {
     try {
       const { worktreePath } = req.body as {
         worktreePath: string;
       };

       if (!worktreePath) {
         res.status(400).json({
           success: false,
           error: 'worktreePath required',
         });
         return;
       }

       // Security: Validate that worktreePath is an absolute path
       if (!isAbsolute(worktreePath)) {
         res.status(400).json({
           success: false,
           error: 'worktreePath must be an absolute path',
         });
         return;
       }

+      const rp = await realpath(worktreePath);
+      const st = await stat(rp);
+      if (!st.isDirectory()) {
+        res.status(400).json({ success: false, error: 'worktreePath must be a directory' });
+        return;
+      }
+      // TODO: enforce rp is within an allowlisted project/worktree root
+
       // Use the platform utility to open in terminal
       const result = await openInTerminal(worktreePath);
📝 Committable suggestion

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

Suggested change
export function createOpenInTerminalHandler() {
return async (req: Request, res: Response): Promise<void> => {
try {
const { worktreePath } = req.body as {
worktreePath: string;
};
if (!worktreePath) {
res.status(400).json({
success: false,
error: 'worktreePath required',
});
return;
}
// Security: Validate that worktreePath is an absolute path
if (!isAbsolute(worktreePath)) {
res.status(400).json({
success: false,
error: 'worktreePath must be an absolute path',
});
return;
}
// Use the platform utility to open in terminal
const result = await openInTerminal(worktreePath);
res.json({
success: true,
result: {
message: `Opened terminal in ${worktreePath}`,
terminalName: result.terminalName,
},
});
} catch (error) {
logError(error, 'Open in terminal failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
import type { Request, Response } from 'express';
import { isAbsolute } from 'path';
import { realpath, stat } from 'fs/promises';
import { openInTerminal } from '@automaker/platform';
import { getErrorMessage, logError } from '../common.js';
export function createOpenInTerminalHandler() {
return async (req: Request, res: Response): Promise<void> => {
try {
const { worktreePath } = req.body as {
worktreePath: string;
};
if (!worktreePath) {
res.status(400).json({
success: false,
error: 'worktreePath required',
});
return;
}
// Security: Validate that worktreePath is an absolute path
if (!isAbsolute(worktreePath)) {
res.status(400).json({
success: false,
error: 'worktreePath must be an absolute path',
});
return;
}
const rp = await realpath(worktreePath);
const st = await stat(rp);
if (!st.isDirectory()) {
res.status(400).json({ success: false, error: 'worktreePath must be a directory' });
return;
}
// TODO: enforce rp is within an allowlisted project/worktree root
// Use the platform utility to open in terminal
const result = await openInTerminal(worktreePath);
res.json({
success: true,
result: {
message: `Opened terminal in ${worktreePath}`,
terminalName: result.terminalName,
},
});
} catch (error) {
logError(error, 'Open in terminal failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

Comment on lines +133 to +138
)}

<DropdownMenuSeparator />

{/* Plan Button with Settings */}
<div
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

Unconditional separator can cause visual inconsistency.

When shouldCollapseAgents and shouldCollapseAutoMode are both false (e.g., only Plan collapsed into overflow), this results in consecutive separators between the "Controls" label and the Plan button.

📝 Suggested fix: Conditionally render the separator
-        )}
+        )}

-        <DropdownMenuSeparator />
+        {/* Only show separator if at least one item above it was rendered */}
+        {(shouldCollapseAgents || shouldCollapseAutoMode) && <DropdownMenuSeparator />}

         {/* Plan Button with Settings */}
🤖 Prompt for AI Agents
In `@apps/ui/src/components/views/board-view/header-overflow-menu.tsx` around
lines 133 - 138, The DropdownMenuSeparator is rendered unconditionally and can
produce adjacent separators when only certain sections are collapsed; update the
render logic in header-overflow-menu.tsx to conditionally render the
DropdownMenuSeparator only when at least one of shouldCollapseAgents or
shouldCollapseAutoMode (or any other adjacent collapsed section) is true so you
don’t produce consecutive separators between the "Controls" label and the Plan
button; locate the separator instance near the Plan button render and wrap it in
a conditional that checks shouldCollapseAgents || shouldCollapseAutoMode (or
equivalent collapse flags) before rendering.

Comment on lines +530 to +622
// Handle pending terminal cwd (from "open in terminal" action on worktree menu)
// When pendingTerminalCwd is set and we're ready, create a terminal with that cwd
const pendingTerminalCwdRef = useRef<string | null>(null);
const pendingTerminalCreatedRef = useRef<boolean>(false);
useEffect(() => {
const pendingCwd = terminalState.pendingTerminalCwd;

// Skip if no pending cwd
if (!pendingCwd) {
// Reset the created ref when there's no pending cwd
pendingTerminalCreatedRef.current = false;
pendingTerminalCwdRef.current = null;
return;
}

// Skip if we already created a terminal for this exact cwd
if (pendingCwd === pendingTerminalCwdRef.current && pendingTerminalCreatedRef.current) {
return;
}

// Skip if still loading or terminal not enabled
if (loading || !status?.enabled) {
logger.debug('Waiting for terminal to be ready before creating terminal for pending cwd');
return;
}

// Skip if password is required but not unlocked yet
if (status.passwordRequired && !terminalState.isUnlocked) {
logger.debug('Waiting for terminal unlock before creating terminal for pending cwd');
return;
}

// Track that we're processing this cwd
pendingTerminalCwdRef.current = pendingCwd;

// Create a terminal with the pending cwd
logger.info('Creating terminal from pending cwd:', pendingCwd);

// Create terminal with the specified cwd
const createTerminalWithCwd = async () => {
try {
const headers: Record<string, string> = {};
const authToken = useAppStore.getState().terminalState.authToken;
if (authToken) {
headers['X-Terminal-Token'] = authToken;
}

const response = await apiFetch('/api/terminal/sessions', 'POST', {
headers,
body: { cwd: pendingCwd, cols: 80, rows: 24 },
});
const data = await response.json();

if (data.success) {
// Mark as successfully created
pendingTerminalCreatedRef.current = true;
addTerminalToLayout(data.data.id);
// Mark this session as new for running initial command
if (defaultRunScript) {
setNewSessionIds((prev) => new Set(prev).add(data.data.id));
}
fetchServerSettings();
toast.success(`Opened terminal in ${pendingCwd.split('/').pop() || pendingCwd}`);
// Clear the pending cwd after successful creation
setPendingTerminalCwd(null);
} else {
logger.error('Failed to create session from pending cwd:', data.error);
toast.error('Failed to open terminal', {
description: data.error || 'Unknown error',
});
// Clear pending cwd on failure to prevent infinite retries
setPendingTerminalCwd(null);
}
} catch (err) {
logger.error('Create session error from pending cwd:', err);
toast.error('Failed to open terminal');
// Clear pending cwd on error to prevent infinite retries
setPendingTerminalCwd(null);
}
};

createTerminalWithCwd();
}, [
terminalState.pendingTerminalCwd,
terminalState.isUnlocked,
loading,
status?.enabled,
status?.passwordRequired,
setPendingTerminalCwd,
addTerminalToLayout,
defaultRunScript,
fetchServerSettings,
]);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix potential duplicate terminal creation when pendingTerminalCwd is also consumed elsewhere.
Right now, pendingTerminalCwd can be used by createTerminal() (Line 931-934) and independently by the pending-cwd useEffect (Line 534+), and only the effect clears the pending value. If the user triggers a manual create while pendingTerminalCwd is set, the effect can still create a second terminal.

One way to de-duplicate (reuse createTerminal + clear pending once consumed)
-    // Create terminal with the specified cwd
-    const createTerminalWithCwd = async () => {
+    const createTerminalWithCwd = async () => {
       try {
-        const headers: Record<string, string> = {};
-        const authToken = useAppStore.getState().terminalState.authToken;
-        if (authToken) {
-          headers['X-Terminal-Token'] = authToken;
-        }
-
-        const response = await apiFetch('/api/terminal/sessions', 'POST', {
-          headers,
-          body: { cwd: pendingCwd, cols: 80, rows: 24 },
-        });
-        const data = await response.json();
-
-        if (data.success) {
-          // Mark as successfully created
-          pendingTerminalCreatedRef.current = true;
-          addTerminalToLayout(data.data.id);
-          // Mark this session as new for running initial command
-          if (defaultRunScript) {
-            setNewSessionIds((prev) => new Set(prev).add(data.data.id));
-          }
-          fetchServerSettings();
-          toast.success(`Opened terminal in ${pendingCwd.split('/').pop() || pendingCwd}`);
-          // Clear the pending cwd after successful creation
-          setPendingTerminalCwd(null);
-        } else {
-          logger.error('Failed to create session from pending cwd:', data.error);
-          toast.error('Failed to open terminal', {
-            description: data.error || 'Unknown error',
-          });
-          // Clear pending cwd on failure to prevent infinite retries
-          setPendingTerminalCwd(null);
-        }
+        await createTerminal(undefined, undefined, pendingCwd);
+        pendingTerminalCreatedRef.current = true;
+        setPendingTerminalCwd(null); // clear once consumed
       } catch (err) {
         logger.error('Create session error from pending cwd:', err);
         toast.error('Failed to open terminal');
         // Clear pending cwd on error to prevent infinite retries
         setPendingTerminalCwd(null);
       }
     };
-  const createTerminal = async (
+  const createTerminal = async (
     direction?: 'horizontal' | 'vertical',
     targetSessionId?: string,
     cwd?: string
   ) => {
@@
-      const terminalCwd =
-        cwd || terminalState.pendingTerminalCwd || currentProject?.path || undefined;
+      const usedPending = !cwd && !!terminalState.pendingTerminalCwd;
+      const terminalCwd = cwd || terminalState.pendingTerminalCwd || currentProject?.path || undefined;
@@
       if (data.success) {
         addTerminalToLayout(data.data.id, direction, targetSessionId);
+        if (usedPending) setPendingTerminalCwd(null);

Also applies to: 913-972

🤖 Prompt for AI Agents
In `@apps/ui/src/components/views/terminal-view.tsx` around lines 530 - 622, The
pending-cwd effect can race with the existing createTerminal flow and produce
duplicate terminals; modify createTerminal (the shared function used at Lines
~931-934) to consume and clear terminalState.pendingTerminalCwd atomically:
check terminalState.pendingTerminalCwd at start, use that value when creating
the session (instead of letting the effect also do it), call
setPendingTerminalCwd(null) immediately after successfully enqueuing/creating
the session (and also on error), and remove or guard the effect’s creation so it
only runs when createTerminal hasn't already consumed the pending value (e.g.,
have the effect return early if pendingTerminalCwdRef.current === pendingCwd or
if a consumed flag is set). Ensure you still set
pendingTerminalCreatedRef.current / call addTerminalToLayout and
setNewSessionIds in the same success path so behavior remains unchanged.

Comment on lines +569 to +607
const createTerminalWithCwd = async () => {
try {
const headers: Record<string, string> = {};
const authToken = useAppStore.getState().terminalState.authToken;
if (authToken) {
headers['X-Terminal-Token'] = authToken;
}

const response = await apiFetch('/api/terminal/sessions', 'POST', {
headers,
body: { cwd: pendingCwd, cols: 80, rows: 24 },
});
const data = await response.json();

if (data.success) {
// Mark as successfully created
pendingTerminalCreatedRef.current = true;
addTerminalToLayout(data.data.id);
// Mark this session as new for running initial command
if (defaultRunScript) {
setNewSessionIds((prev) => new Set(prev).add(data.data.id));
}
fetchServerSettings();
toast.success(`Opened terminal in ${pendingCwd.split('/').pop() || pendingCwd}`);
// Clear the pending cwd after successful creation
setPendingTerminalCwd(null);
} else {
logger.error('Failed to create session from pending cwd:', data.error);
toast.error('Failed to open terminal', {
description: data.error || 'Unknown error',
});
// Clear pending cwd on failure to prevent infinite retries
setPendingTerminalCwd(null);
}
} catch (err) {
logger.error('Create session error from pending cwd:', err);
toast.error('Failed to open terminal');
// Clear pending cwd on error to prevent infinite retries
setPendingTerminalCwd(null);
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

Toast basename extraction isn’t Windows-safe.
pendingCwd.split('/') won’t handle C:\path\to\dir. Consider splitting on both separators.

🤖 Prompt for AI Agents
In `@apps/ui/src/components/views/terminal-view.tsx` around lines 569 - 607, In
createTerminalWithCwd, the toast success uses pendingCwd.split('/') which breaks
on Windows paths; change the basename extraction to split on both separators
(e.g., pendingCwd?.split(/[/\\]/).pop() || pendingCwd) or compute a safe
basename variable before calling toast.success, and use that variable in
toast.success and any related logs; ensure pendingCwd is null-checked
(pendingCwd?) to avoid exceptions when clearing the cwd.

Comment on lines +826 to 836
style={{ marginRight: !isTablet && streamerPanelOpen ? '250px' : '0' }}
>
<Outlet />
</div>

{/* Hidden streamer panel - opens with "\" key, pushes content */}
{/* Hidden streamer panel - opens with "\" key, pushes content - desktop only */}
<div
className={`fixed top-0 right-0 h-full w-[250px] bg-background border-l border-border transition-transform duration-300 ${
className={`fixed top-0 right-0 h-full w-[250px] bg-background border-l border-border transition-transform duration-300 hidden lg:block ${
streamerPanelOpen ? 'translate-x-0' : 'translate-x-full'
}`}
/>
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

Breakpoint mismatch at 1024px boundary.

The useIsTablet() hook uses max-width: 1024px while the CSS uses lg:block (min-width: 1024px). At exactly 1024px viewport width, both conditions are satisfied: isTablet is true (no margin applied) but lg:block makes the panel visible, causing the panel to overlap content when open.

Consider aligning the breakpoints:

Suggested fix in use-media-query.ts
 export function useIsTablet(): boolean {
-  return useMediaQuery('(max-width: 1024px)');
+  return useMediaQuery('(max-width: 1023px)');
 }

Alternatively, use a CSS variable or consistent threshold across both JS and CSS logic.

🤖 Prompt for AI Agents
In `@apps/ui/src/routes/__root.tsx` around lines 826 - 836, The breakpoint
inconsistency causes the streamer panel to overlap at 1024px because
useIsTablet() uses max-width:1024px while the JSX/CSS uses the Tailwind lg
breakpoint (min-width:1024px) for lg:block; to fix, make the threshold
consistent by updating useIsTablet (or its underlying use-media-query) to use
max-width:1023px (so tablet is <1024) or change the CSS logic to use
min-width:1025px equivalent, and ensure the JSX logic around streamerPanelOpen
and the inline marginRight calculation (where isTablet is referenced) aligns
with the same numeric breakpoint so the panel visibility (lg:block) and isTablet
boolean never both apply at 1024px.

Comment on lines +384 to +395
export async function openInTerminal(targetPath: string): Promise<{ terminalName: string }> {
if (isMac) {
// Use AppleScript to open Terminal.app in the specified directory
const script = `
tell application "Terminal"
do script "cd ${targetPath.replace(/"/g, '\\"').replace(/\$/g, '\\$')}"
activate
end tell
`;
await execFileAsync('osascript', ['-e', script]);
return { terminalName: 'Terminal' };
} else if (isWindows) {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Block shell injection in the macOS AppleScript cd command.
targetPath is interpolated into a shell command executed by Terminal; escaping only " and $ is not sufficient (e.g., backticks, newlines). Please use AppleScript’s quoted form of (and avoid embedding raw shell).

Proposed fix
 export async function openInTerminal(targetPath: string): Promise<{ terminalName: string }> {
   if (isMac) {
     // Use AppleScript to open Terminal.app in the specified directory
-    const script = `
-      tell application "Terminal"
-        do script "cd ${targetPath.replace(/"/g, '\\"').replace(/\$/g, '\\$')}"
-        activate
-      end tell
-    `;
+    const script = `
+      tell application "Terminal"
+        do script "cd " & quoted form of ${JSON.stringify(targetPath)}
+        activate
+      end tell
+    `;
     await execFileAsync('osascript', ['-e', script]);
     return { terminalName: 'Terminal' };
   } else if (isWindows) {
🤖 Prompt for AI Agents
In `@libs/platform/src/editor.ts` around lines 384 - 395, openInTerminal currently
interpolates targetPath directly into an AppleScript shell command (script
variable) causing possible shell injection; instead avoid embedding raw shell
content and use AppleScript's safe quoting: build an AppleScript that assigns
the path to a variable and uses quoted form of POSIX path (or quoted form of
POSIX file) when composing the do script command, and stop directly
concatenating targetPath into the shell string; update the script generation in
openInTerminal (and the execFileAsync call) to use this AppleScript quoting
approach so all special characters in targetPath are safely handled.

Comment on lines +433 to +463
} else {
// Linux: Try common terminal emulators in order
const terminals = [
{
name: 'GNOME Terminal',
command: 'gnome-terminal',
args: ['--working-directory', targetPath],
},
{ name: 'Konsole', command: 'konsole', args: ['--workdir', targetPath] },
{
name: 'xfce4-terminal',
command: 'xfce4-terminal',
args: ['--working-directory', targetPath],
},
{ name: 'xterm', command: 'xterm', args: ['-e', `cd "${targetPath}" && $SHELL`] },
{
name: 'x-terminal-emulator',
command: 'x-terminal-emulator',
args: ['--working-directory', targetPath],
},
];

for (const terminal of terminals) {
if (await commandExists(terminal.command)) {
await execFileAsync(terminal.command, terminal.args);
return { terminalName: terminal.name };
}
}

throw new Error('No terminal emulator found');
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

wc -l libs/platform/src/editor.ts

Repository: AutoMaker-Org/automaker

Length of output: 98


🏁 Script executed:

head -100 libs/platform/src/editor.ts | cat -n

Repository: AutoMaker-Org/automaker

Length of output: 3452


🏁 Script executed:

sed -n '350,480p' libs/platform/src/editor.ts | cat -n

Repository: AutoMaker-Org/automaker

Length of output: 4347


Linux terminal launch blocks the caller; use non-blocking spawn pattern like Windows.

The Linux implementation uses await execFileAsync(), which waits for the child process to exit. For terminal emulators like xterm (especially with -e), this blocks the server/CLI until the terminal closes. Use the same non-blocking pattern as Windows: spawn() with detached: true, stdio: 'ignore', and child.unref().

Proposed fix
    for (const terminal of terminals) {
      if (await commandExists(terminal.command)) {
-       await execFileAsync(terminal.command, terminal.args);
+       const child: ChildProcess = spawn(terminal.command, terminal.args, {
+         stdio: 'ignore',
+         detached: true,
+       });
+       child.unref();
        return { terminalName: terminal.name };
      }
    }
🤖 Prompt for AI Agents
In `@libs/platform/src/editor.ts` around lines 433 - 463, The Linux branch
currently awaits execFileAsync for each terminal (see terminals array and the
usage of execFileAsync(terminal.command, terminal.args)), which blocks until the
terminal exits; modify this to use child_process.spawn in a non-blocking way:
call spawn(terminal.command, terminal.args, { detached: true, stdio: 'ignore'
}), do not await it, and call child.unref() after spawn; keep the commandExists
check and return { terminalName: terminal.name } immediately after spawning, and
throw the same Error('No terminal emulator found') if none are available.

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 new features and significant UI/UX improvements, particularly for worktree management and mobile responsiveness. Key backend changes include new API endpoints for opening a terminal in a worktree and discarding changes, along with updates to existing endpoints to sanitize feature IDs and allow merging into a specified target branch. The discard-changes endpoint, however, has a review comment suggesting more robust error logging for git commands to prevent silent failures.

On the frontend, a new ViewWorktreeChangesDialog and ConfirmDialog for discarding changes have been added. The WorktreePanel now includes actions to open a terminal, view changes, and discard changes, with the 'Pull & Resolve Conflicts' action offering options to pull from the worktree's own remote branch or the main worktree's branch. A review comment highlights a discrepancy where the UI's "Open in Terminal" action currently navigates to the in-app terminal, while a new backend endpoint was created for opening a native OS terminal, suggesting the UI should be updated to use the native terminal API.

UI/UX enhancements focus on responsive design across various views: the ProjectSwitcher, Sidebar, ContextView, GitHubIssuesView, GitHubPRsView, and MemoryView have been updated to improve their layout and functionality on mobile devices. This includes conditional rendering of elements, dynamic sizing, and the introduction of a HeaderOverflowMenu in BoardHeader to manage controls (Agents, Auto Mode, Plan) that collapse based on screen width. Additionally, the use-media-query hook has been expanded with new breakpoints for responsive toolbar elements, and the AgentOutputModal now passes the branch name to the GitDiffPanel for more accurate diff viewing. A minor cleanup suggestion was made to remove an unused getTerminalInfo function in the platform library.

Comment on lines +63 to +75
await execAsync('git reset HEAD', { cwd: worktreePath }).catch(() => {
// Ignore errors - might fail if there's nothing staged
});

// 2. Discard changes in tracked files
await execAsync('git checkout .', { cwd: worktreePath }).catch(() => {
// Ignore errors - might fail if there are no tracked changes
});

// 3. Remove untracked files and directories
await execAsync('git clean -fd', { cwd: worktreePath }).catch(() => {
// Ignore errors - might fail if there are no untracked files
});
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

Silently catching and ignoring all errors from these execAsync calls is risky. While the comments mention expected failures (e.g., nothing to clean), this will also suppress unexpected errors like file system permissions issues, which could leave the worktree in an inconsistent state without any indication to the user or in the logs. It would be more robust to log these errors, so you can monitor for unexpected failures while still not blocking the discard operation.

      await execAsync('git reset HEAD', { cwd: worktreePath }).catch((error) => {
        // Ignore errors - might fail if there's nothing staged
        logError(error, 'Ignoring error from `git reset HEAD`');
      });

      // 2. Discard changes in tracked files
      await execAsync('git checkout .', { cwd: worktreePath }).catch((error) => {
        // Ignore errors - might fail if there are no tracked changes
        logError(error, 'Ignoring error from `git checkout .`');
      });

      // 3. Remove untracked files and directories
      await execAsync('git clean -fd', { cwd: worktreePath }).catch((error) => {
        // Ignore errors - might fail if there are no untracked files
        logError(error, 'Ignoring error from `git clean -fd`');
      });

Comment on lines +150 to +159
const handleOpenInTerminal = useCallback(
(worktree: WorktreeInfo) => {
// Set the pending terminal cwd to the worktree path
setPendingTerminalCwd(worktree.path);
// Navigate to the terminal page
navigate({ to: '/terminal' });
logger.info('Opening terminal for worktree:', worktree.path);
},
[navigate, setPendingTerminalCwd]
);
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

There's a discrepancy between the UI implementation and the new backend capability for the "Open in Terminal" feature. This implementation navigates to the in-app terminal view, but a new backend endpoint (/api/worktree/open-in-terminal) was added to open a native OS terminal, which seems to be the intended feature based on the PR description. To provide the cross-platform native terminal experience, this should call the backend API instead.

You'll need to import getHttpApiClient and toast to use the suggested code.

  const handleOpenInTerminal = useCallback(async (worktree: WorktreeInfo) => {
    const api = getHttpApiClient();
    try {
      const result = await api.worktree.openInTerminal(worktree.path);
      if (result.success) {
        toast.success(result.result?.message || `Opened terminal for ${worktree.branch}`);
      } else {
        toast.error('Failed to open terminal', { description: result.error });
      }
    } catch (error) {
      logger.error('Failed to open terminal', error);
      toast.error('Failed to open terminal', { description: error instanceof Error ? error.message : 'Unknown error' });
    }
  }, []);

Comment on lines +348 to +371
function getTerminalInfo(): { name: string; command: string; args: string[] } {
if (isMac) {
// On macOS, use Terminal.app with AppleScript to open in a specific directory
return {
name: 'Terminal',
command: 'open',
args: ['-a', 'Terminal'],
};
} else if (isWindows) {
// On Windows, use Windows Terminal if available, otherwise cmd
return {
name: 'Windows Terminal',
command: 'wt',
args: ['-d'],
};
} else {
// On Linux, try common terminal emulators in order of preference
return {
name: 'Terminal',
command: 'x-terminal-emulator',
args: ['--working-directory'],
};
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

This getTerminalInfo function appears to be unused. The openInTerminal function implements its own platform-specific logic directly. To improve code maintainability, this dead code should be removed.

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

Labels

Do Not Merge Use this label if something should not be merged. Enhancement Improvements to existing functionality or UI. Testers-Requested Request for others to test an enhancement or bug fix/etc. type: feature A new capability or functionality that does not exist yet.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants