Skip to content

Tabs creation and management#78

Merged
Kitenite merged 7 commits intomainfrom
top-tabs
Nov 14, 2025
Merged

Tabs creation and management#78
Kitenite merged 7 commits intomainfrom
top-tabs

Conversation

@Kitenite
Copy link
Copy Markdown
Collaborator

@Kitenite Kitenite commented Nov 14, 2025

Summary by CodeRabbit

  • New Features
    • Delete worktrees with a confirmation dialog and uncommitted-changes warning; respects workspace scope and updates selection automatically.
    • Close controls on worktree tabs and responsive tab sizing for better layout.
    • Enhanced "Add Task" modal: choose creating vs. listing mode, select/clone branches or worktrees, show live creation progress and setup output, and prevent closing while a worktree is being created.
  • UX
    • Improved modal lifecycle and clearer setup/status feedback during task/worktree creation.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Nov 14, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

Adds worktree deletion (UI close control, confirmation, IPC removal, workspace/worktrees refresh, selection-reset) and expands add-task/worktree creation flows (modal modes, branch fetching, live setup progress, creation state/output). Hooks and tab/modal components updated to surface new handlers and state.

Changes

Cohort / File(s) Summary
Hook: worktrees
apps/desktop/src/renderer/screens/main/hooks/useWorktrees.ts
Added selectedWorktreeId option and exported handleDeleteWorktree(worktreeId: string): Promise<void>; deletion calls IPC, refreshes workspace list, and resets selection when needed.
Hook: tasks / creation state
apps/desktop/src/renderer/screens/main/hooks/useTasks.ts
Added modal initial mode, branches, isCreatingWorktree, setupStatus, setupOutput; handleOpenAddTaskModal accepts mode and fetches branches; handleCreateTask listens for worktree-setup-progress and manages live creation/cleanup.
Main wiring / Layout
apps/desktop/src/renderer/screens/main/MainScreen.tsx, apps/desktop/src/renderer/screens/main/components/Layout/NewLayoutMain.tsx
Threaded handleDeleteWorktree and workspaceId into TaskTabs; expanded add-task wiring to pass addTaskModalInitialMode, branches, worktrees, isCreating, setupStatus, setupOutput; implemented deletion and creation lifecycle handling (IPC, refresh, selection).
Task tabs surface
apps/desktop/src/renderer/screens/main/components/Layout/TaskTabs/TaskTabs.tsx, .../TaskTabs/types.ts
Added onDeleteWorktree?: (worktreeId: string)=>Promise<void> and workspaceId?: string props; added remove-confirmation dialog with uncommitted-changes warning and confirm flow; added ResizeObserver-driven tab width calculation.
Tab components / UI
apps/desktop/src/renderer/screens/main/components/Layout/TaskTabs/WorktreeTab.tsx, .../WorktreeTabButton.tsx, .../AddTaskButton.tsx
Exposed optional onClose?: (e)=>void and width?: number; WorktreeTab/WorktreeTabButton render optional close button and honor width for layout; AddTaskButton onClick now accepts mode (`"list"
Add Task Modal
apps/desktop/src/renderer/screens/main/components/Layout/AddTaskModal.tsx
Expanded props to accept initialMode, branches, worktrees, isCreating, setupStatus, setupOutput; added sourceBranch & cloneTabsFromWorktreeId options, collision-avoidant branch naming, UI for setup status/output, and guarded close behavior during creation.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI as TaskTabs UI
    participant Dialog as Confirm Dialog
    participant Hook as useWorktrees.handleDeleteWorktree
    participant IPC as IPC ("worktree-remove")
    participant State as Workspace State

    User->>UI: Click tab close (onClose)
    UI->>Dialog: Open confirmation (may show uncommitted-changes warning)
    User->>Dialog: Confirm removal
    Dialog->>Hook: Call onDeleteWorktree(worktreeId)
    Hook->>IPC: send "worktree-remove" request
    IPC-->>Hook: success / error
    alt success
        Hook->>State: refresh current workspace
        Hook->>State: reload all workspaces
        alt deleted was selected
            Hook->>State: select first remaining worktree & its first tab
        else no remaining worktrees
            Hook->>State: clear selection
        end
        Hook-->>UI: deletion complete
    else error
        Hook->>UI: surface error (console/alert)
    end
    UI->>User: update tabs view
Loading
sequenceDiagram
    participant User
    participant UI as AddTaskModal
    participant Hook as useTasks.handleCreateTask
    participant IPC as IPC ("worktree-create", "worktree-setup-progress")
    participant State as Workspace State

    User->>UI: Open AddTaskModal (mode="new")
    UI->>Hook: handleOpenAddTaskModal(mode="new")
    Hook->>IPC: fetch branches
    Hook-->>UI: branches populated
    User->>UI: Submit create (branch, options)
    UI->>Hook: handleCreateTask(payload)
    Hook->>IPC: send "worktree-create"
    IPC-->>Hook: emits "worktree-setup-progress" events
    loop progress updates
        IPC-->>Hook: progress chunk
        Hook-->>UI: update setupStatus / setupOutput
    end
    IPC-->>Hook: creation success / failure
    alt success
        Hook->>State: refresh workspace, select new worktree/tab
        Hook-->>UI: optionally show setup results, close modal after delay
    else failure
        Hook-->>UI: show failure status/output, keep modal open
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Areas to inspect carefully:
    • Deletion side-effects and selection-reset logic in useWorktrees.ts
    • Live creation progress subscription/unsubscription and error paths in useTasks.ts
    • Modal close-guarding and state reset in AddTaskModal.tsx / NewLayoutMain
    • IPC call signatures and consistent error handling (alerts/logging) in NewLayoutMain
    • ResizeObserver usage and tab width constraints in TaskTabs.tsx
    • Prop threading: ensure onDeleteWorktree, workspaceId, branches, and creation state are passed consistently

Possibly related PRs

  • workspace #71 — modifies renderer hooks/components (useWorktrees, TaskTabs, MainScreen); likely intersects with deletion and wiring changes.
  • made pending #48 — touches task/worktree creation UI and TaskTabs; related to switching from optimistic to live setup flows.
  • update tabs ui #76 — updates TaskTabs / WorktreeTabButton rendering and behavior; overlaps with tab close/width UI changes.

Poem

🐰 I nudged a tab and gave a tap,

A tiny dialog set the trap,
IPC hummed and off it flew,
New branches sprouted, tabs anew,
I hopped away with carrots—cheer! 🥕

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings, 1 inconclusive)
Check name Status Explanation Resolution
Description check ⚠️ Warning No pull request description was provided; required template sections including Description, Related Issues, Type of Change, and Testing are entirely missing. Add a comprehensive PR description following the template with clear explanation of changes, related issues, change type, and testing details.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title 'management' is vague and generic, failing to convey the specific change of adding worktree deletion capability. Use a more descriptive title that summarizes the main change, such as 'Add worktree deletion capability' or 'Implement worktree removal workflow'.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0bc19fd and bdbfe90.

📒 Files selected for processing (7)
  • apps/desktop/src/renderer/screens/main/MainScreen.tsx (3 hunks)
  • apps/desktop/src/renderer/screens/main/components/Layout/AddTaskModal.tsx (8 hunks)
  • apps/desktop/src/renderer/screens/main/components/Layout/NewLayoutMain.tsx (6 hunks)
  • apps/desktop/src/renderer/screens/main/components/Layout/TaskTabs/AddTaskButton.tsx (1 hunks)
  • apps/desktop/src/renderer/screens/main/components/Layout/TaskTabs/TaskTabs.tsx (2 hunks)
  • apps/desktop/src/renderer/screens/main/components/Layout/TaskTabs/WorktreeTabButton.tsx (2 hunks)
  • apps/desktop/src/renderer/screens/main/hooks/useTasks.ts (3 hunks)

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
apps/desktop/src/renderer/screens/main/components/Layout/TaskTabs/WorktreeTab.tsx (1)

13-41: Prop plumb-through looks correct; consider tightening event type (optional).

The new onClose / width props are cleanly added and correctly forwarded to WorktreeTabButton; wiring looks good. If you want stricter typing, you could narrow onClose to React.MouseEvent<HTMLButtonElement> here and in WorktreeTabButton, but it’s not required.

apps/desktop/src/renderer/screens/main/MainScreen.tsx (1)

75-90: Hook wiring for deletion looks good; verify active-selection sync behavior.

The integration of useWorktrees (including handleDeleteWorktree) with MainScreen and the new TaskTabs props (onDeleteWorktree, workspaceId) is consistent and type-safe.

One thing to double-check in the broader flow: handleTabSelect explicitly updates the main process via workspace-set-active-selection, but handleDeleteWorktree in useWorktrees only updates selectedWorktreeId / selectedTabId when auto-selecting a new worktree after deletion. If you rely on the main process’s “active selection” for later operations, you may want to ensure that code paths triggered by delete also invoke the same IPC update (either inside the hook or via a shared helper) so state stays fully in sync.

Also applies to: 136-168

apps/desktop/src/renderer/screens/main/hooks/useWorktrees.ts (1)

144-188: Consider simplifying the selection reset logic.

The nested conditionals for resetting selection (lines 165-178) work correctly but are complex. Consider extracting this into a helper function for clarity.

Additionally, the check refreshedWorkspace.worktrees && refreshedWorkspace.worktrees.length > 0 on line 166 should be sufficient, but ensure that worktrees is always defined as an array in the Workspace type to avoid potential issues.

Example refactor:

+const resetSelectionAfterDelete = (
+  workspace: Workspace,
+  deletedId: string,
+  currentSelectedId: string | null
+) => {
+  if (currentSelectedId !== deletedId) return;
+  
+  const worktrees = workspace.worktrees || [];
+  if (worktrees.length === 0) {
+    setSelectedWorktreeId(null);
+    setSelectedTabId(null);
+    return;
+  }
+  
+  const firstWorktree = worktrees[0];
+  setSelectedWorktreeId(firstWorktree.id);
+  const firstTab = firstWorktree.tabs?.[0];
+  setSelectedTabId(firstTab?.id || null);
+};

 const handleDeleteWorktree = async (worktreeId: string) => {
   // ... existing code ...
   if (refreshedWorkspace) {
     setCurrentWorkspace(refreshedWorkspace);
     await loadAllWorkspaces();
-    
-    // If we deleted the selected worktree, select the first available one
-    if (selectedWorktreeId === worktreeId) {
-      if (refreshedWorkspace.worktrees && refreshedWorkspace.worktrees.length > 0) {
-        const firstWorktree = refreshedWorkspace.worktrees[0];
-        setSelectedWorktreeId(firstWorktree.id);
-        if (firstWorktree.tabs && firstWorktree.tabs.length > 0) {
-          setSelectedTabId(firstWorktree.tabs[0].id);
-        } else {
-          setSelectedTabId(null);
-        }
-      } else {
-        setSelectedWorktreeId(null);
-        setSelectedTabId(null);
-      }
-    }
+    resetSelectionAfterDelete(refreshedWorkspace, worktreeId, selectedWorktreeId);
   }
 };
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a157754 and a8862aa.

📒 Files selected for processing (7)
  • apps/desktop/src/renderer/screens/main/MainScreen.tsx (2 hunks)
  • apps/desktop/src/renderer/screens/main/components/Layout/NewLayoutMain.tsx (2 hunks)
  • apps/desktop/src/renderer/screens/main/components/Layout/TaskTabs/TaskTabs.tsx (2 hunks)
  • apps/desktop/src/renderer/screens/main/components/Layout/TaskTabs/WorktreeTab.tsx (2 hunks)
  • apps/desktop/src/renderer/screens/main/components/Layout/TaskTabs/WorktreeTabButton.tsx (3 hunks)
  • apps/desktop/src/renderer/screens/main/components/Layout/TaskTabs/types.ts (1 hunks)
  • apps/desktop/src/renderer/screens/main/hooks/useWorktrees.ts (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
apps/desktop/src/renderer/screens/main/components/Layout/TaskTabs/WorktreeTabButton.tsx (1)
apps/desktop/src/renderer/screens/main/components/Layout/StatusIndicator.tsx (1)
  • StatusIndicator (55-115)
apps/desktop/src/renderer/screens/main/components/Layout/TaskTabs/TaskTabs.tsx (5)
apps/desktop/src/renderer/screens/main/components/Layout/TaskTabs/SidebarToggle.tsx (1)
  • SidebarToggle (12-42)
apps/desktop/src/renderer/screens/main/components/Layout/TaskTabs/ModeToggle.tsx (1)
  • ModeToggle (8-33)
apps/desktop/src/renderer/screens/main/components/Layout/TaskTabs/WorktreeTab.tsx (1)
  • WorktreeTab (21-54)
apps/desktop/src/renderer/screens/main/components/Layout/TaskTabs/AddTaskButton.tsx (1)
  • AddTaskButton (10-21)
apps/desktop/src/renderer/screens/main/components/Layout/TaskTabs/PRActions.tsx (1)
  • PRActions (14-69)
apps/desktop/src/renderer/screens/main/MainScreen.tsx (2)
apps/desktop/src/renderer/screens/main/hooks/useWorktrees.ts (1)
  • useWorktrees (14-197)
apps/desktop/src/renderer/screens/main/hooks/index.ts (1)
  • useWorktrees (6-6)
🔇 Additional comments (10)
apps/desktop/src/renderer/screens/main/components/Layout/TaskTabs/types.ts (1)

20-34: TaskTabsProps extension is consistent with usage.

The additions of onDeleteWorktree and workspaceId align with how TaskTabs is now consumed (IPC-backed delete + workspace scoping). Types look correct.

apps/desktop/src/renderer/screens/main/hooks/useWorktrees.ts (2)

9-9: LGTM!

The addition of selectedWorktreeId to the hook props is necessary for handling selection reset after deletion.

Also applies to: 19-19


195-195: LGTM!

The new handler is correctly exposed in the hook's API.

apps/desktop/src/renderer/screens/main/components/Layout/TaskTabs/TaskTabs.tsx (7)

1-11: LGTM!

The new Dialog component imports and React hooks are correctly added for the deletion confirmation flow.


29-30: LGTM!

The new props correctly enable workspace-scoped worktree deletion.


34-38: LGTM!

State management for the deletion dialog and tab width calculation is well-structured.


40-79: LGTM!

The tab width calculation with ResizeObserver is well-implemented:

  • Correctly accounts for gaps between tabs
  • Applies sensible min/max constraints (60-240px)
  • Properly cleans up the observer
  • Has correct dependencies

85-119: LGTM!

The close handler is well-implemented with:

  • Proper event propagation control
  • Guard clauses for pending worktrees and missing dependencies
  • Pre-deletion check for uncommitted changes
  • Graceful error handling (still shows dialog if check fails)

121-130: Verify error handling is sufficient.

The function clears state and closes the dialog before the async onDeleteWorktree completes. If onDeleteWorktree fails, the user won't see the dialog anymore. However, since handleDeleteWorktree in useWorktrees.ts shows error alerts, this might be intentional.

Consider whether you want to:

  1. Keep the current behavior (close dialog immediately, errors shown via alerts)
  2. Wait for the operation to complete before closing (better UX)

Example alternative (if desired):

 const confirmRemoveWorktree = async () => {
   if (!onDeleteWorktree || !worktreeToDelete) return;
 
-  setShowRemoveDialog(false);
-  setRemoveWarning("");
   const worktreeId = worktreeToDelete;
-  setWorktreeToDelete(null);
 
-  await onDeleteWorktree(worktreeId);
+  try {
+    await onDeleteWorktree(worktreeId);
+  } finally {
+    setShowRemoveDialog(false);
+    setRemoveWarning("");
+    setWorktreeToDelete(null);
+  }
 };

132-241: LGTM!

The JSX changes are well-implemented:

  • Tab width is properly passed to WorktreeTab components
  • Close handlers are conditionally provided
  • Confirmation dialog has clear messaging and appropriate styling for warnings
  • Dialog accessibility is maintained with proper semantic structure

Comment on lines +501 to +546
// Handle worktree deletion
const handleDeleteWorktree = async (worktreeId: string) => {
if (!currentWorkspace) return;

try {
const result = await window.ipcRenderer.invoke("worktree-remove", {
workspaceId: currentWorkspace.id,
worktreeId,
});

if (result.success) {
// Reload workspace to get updated worktree list
const refreshedWorkspace = await window.ipcRenderer.invoke(
"workspace-get",
currentWorkspace.id,
);

if (refreshedWorkspace) {
setCurrentWorkspace(refreshedWorkspace);
await loadAllWorkspaces();

// If we deleted the selected worktree, select the first available one
if (selectedWorktreeId === worktreeId) {
if (refreshedWorkspace.worktrees && refreshedWorkspace.worktrees.length > 0) {
const firstWorktree = refreshedWorkspace.worktrees[0];
setSelectedWorktreeId(firstWorktree.id);
if (firstWorktree.tabs && firstWorktree.tabs.length > 0) {
handleTabSelect(firstWorktree.id, firstWorktree.tabs[0].id);
} else {
setSelectedTabId(null);
}
} else {
setSelectedWorktreeId(null);
setSelectedTabId(null);
}
}
}
} else {
console.error("Failed to remove worktree:", result.error);
alert(`Failed to remove worktree: ${result.error || "Unknown error"}`);
}
} catch (error) {
console.error("Error removing worktree:", error);
alert(`Error: ${error instanceof Error ? error.message : String(error)}`);
}
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Worktree deletion can revert to stale workspace state due to double setCurrentWorkspace.

In handleDeleteWorktree, after a successful delete you:

  • call setCurrentWorkspace(refreshedWorkspace);
  • then, when the deleted worktree was selected, you eventually call handleTabSelect(firstWorktree.id, firstWorktree.tabs[0].id);

But handleTabSelect does:

if (currentWorkspace) {
  window.ipcRenderer.invoke("workspace-set-active-selection", { ... });
  setCurrentWorkspace({
    ...currentWorkspace,
    activeWorktreeId: worktreeId,
    activeTabId: tabId,
  });
}

Because both setCurrentWorkspace calls are plain object updates in the same event, the one inside handleTabSelect will win and is based on the stale currentWorkspace snapshot (pre‑deletion). That can reintroduce the deleted worktree into the in‑memory workspace state.

A minimal, safer fix here is to keep the fresh workspace and only adjust selection IDs, similar to the useWorktrees hook:

-          if (selectedWorktreeId === worktreeId) {
-            if (refreshedWorkspace.worktrees && refreshedWorkspace.worktrees.length > 0) {
-              const firstWorktree = refreshedWorkspace.worktrees[0];
-              setSelectedWorktreeId(firstWorktree.id);
-              if (firstWorktree.tabs && firstWorktree.tabs.length > 0) {
-                handleTabSelect(firstWorktree.id, firstWorktree.tabs[0].id);
-              } else {
-                setSelectedTabId(null);
-              }
-            } else {
-              setSelectedWorktreeId(null);
-              setSelectedTabId(null);
-            }
-          }
+          if (selectedWorktreeId === worktreeId) {
+            if (refreshedWorkspace.worktrees && refreshedWorkspace.worktrees.length > 0) {
+              const firstWorktree = refreshedWorkspace.worktrees[0];
+              setSelectedWorktreeId(firstWorktree.id);
+              if (firstWorktree.tabs && firstWorktree.tabs.length > 0) {
+                setSelectedTabId(firstWorktree.tabs[0].id);
+              } else {
+                setSelectedTabId(null);
+              }
+            } else {
+              setSelectedWorktreeId(null);
+              setSelectedTabId(null);
+            }
+          }

(Optionally, if you want the main process’s active selection kept in sync, you can inline the workspace-set-active-selection IPC call here using refreshedWorkspace.id.)

The wiring of onDeleteWorktree={handleDeleteWorktree} and workspaceId={currentWorkspace?.id} into TaskTabs looks correct and matches the new props.

Also applies to: 913-945









🏁 Script executed:

#!/bin/bash
# Locate the file and examine the handleDeleteWorktree function
find . -name "NewLayoutMain.tsx" -type f | head -1

Length of output: 76


🏁 Script executed:

#!/bin/bash
# View the file to get the full context around the mentioned lines
if [ -f "apps/desktop/src/renderer/screens/main/components/Layout/NewLayoutMain.tsx" ]; then
  sed -n '495,555p' "apps/desktop/src/renderer/screens/main/components/Layout/NewLayoutMain.tsx"
else
  echo "File not found at expected path"
fi

Length of output: 1809


🏁 Script executed:

#!/bin/bash
# Find handleTabSelect definition to understand what it does
if [ -f "apps/desktop/src/renderer/screens/main/components/Layout/NewLayoutMain.tsx" ]; then
  rg -n "handleTabSelect\s*=" "apps/desktop/src/renderer/screens/main/components/Layout/NewLayoutMain.tsx" -A 20
fi

Length of output: 584


🏁 Script executed:

#!/bin/bash
# Check lines 913-945 mentioned in the review
if [ -f "apps/desktop/src/renderer/screens/main/components/Layout/NewLayoutMain.tsx" ]; then
  sed -n '910,950p' "apps/desktop/src/renderer/screens/main/components/Layout/NewLayoutMain.tsx"
fi

Length of output: 1374


Replace handleTabSelect call with direct setSelectedTabId to prevent stale workspace state from overriding deletion.

The issue is confirmed. After successful worktree deletion:

  1. setCurrentWorkspace(refreshedWorkspace) is called at line 525
  2. handleTabSelect() is called at line 533, which internally calls setCurrentWorkspace({ ...currentWorkspace, ... }) using a stale closure value
  3. Both state updates batch together in the same event handler, and the stale one wins, reintroducing deleted data

Apply the suggested minimal fix by replacing the handleTabSelect call with direct selection setters:

-              if (firstWorktree.tabs && firstWorktree.tabs.length > 0) {
-                handleTabSelect(firstWorktree.id, firstWorktree.tabs[0].id);
-              } else {
-                setSelectedTabId(null);
-              }
+              if (firstWorktree.tabs && firstWorktree.tabs.length > 0) {
+                setSelectedTabId(firstWorktree.tabs[0].id);
+              } else {
+                setSelectedTabId(null);
+              }

(Optionally inline the workspace-set-active-selection IPC call here using refreshedWorkspace.id if needed to keep the main process in sync.)

📝 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
// Handle worktree deletion
const handleDeleteWorktree = async (worktreeId: string) => {
if (!currentWorkspace) return;
try {
const result = await window.ipcRenderer.invoke("worktree-remove", {
workspaceId: currentWorkspace.id,
worktreeId,
});
if (result.success) {
// Reload workspace to get updated worktree list
const refreshedWorkspace = await window.ipcRenderer.invoke(
"workspace-get",
currentWorkspace.id,
);
if (refreshedWorkspace) {
setCurrentWorkspace(refreshedWorkspace);
await loadAllWorkspaces();
// If we deleted the selected worktree, select the first available one
if (selectedWorktreeId === worktreeId) {
if (refreshedWorkspace.worktrees && refreshedWorkspace.worktrees.length > 0) {
const firstWorktree = refreshedWorkspace.worktrees[0];
setSelectedWorktreeId(firstWorktree.id);
if (firstWorktree.tabs && firstWorktree.tabs.length > 0) {
handleTabSelect(firstWorktree.id, firstWorktree.tabs[0].id);
} else {
setSelectedTabId(null);
}
} else {
setSelectedWorktreeId(null);
setSelectedTabId(null);
}
}
}
} else {
console.error("Failed to remove worktree:", result.error);
alert(`Failed to remove worktree: ${result.error || "Unknown error"}`);
}
} catch (error) {
console.error("Error removing worktree:", error);
alert(`Error: ${error instanceof Error ? error.message : String(error)}`);
}
};
// Handle worktree deletion
const handleDeleteWorktree = async (worktreeId: string) => {
if (!currentWorkspace) return;
try {
const result = await window.ipcRenderer.invoke("worktree-remove", {
workspaceId: currentWorkspace.id,
worktreeId,
});
if (result.success) {
// Reload workspace to get updated worktree list
const refreshedWorkspace = await window.ipcRenderer.invoke(
"workspace-get",
currentWorkspace.id,
);
if (refreshedWorkspace) {
setCurrentWorkspace(refreshedWorkspace);
await loadAllWorkspaces();
// If we deleted the selected worktree, select the first available one
if (selectedWorktreeId === worktreeId) {
if (refreshedWorkspace.worktrees && refreshedWorkspace.worktrees.length > 0) {
const firstWorktree = refreshedWorkspace.worktrees[0];
setSelectedWorktreeId(firstWorktree.id);
if (firstWorktree.tabs && firstWorktree.tabs.length > 0) {
setSelectedTabId(firstWorktree.tabs[0].id);
} else {
setSelectedTabId(null);
}
} else {
setSelectedWorktreeId(null);
setSelectedTabId(null);
}
}
}
} else {
console.error("Failed to remove worktree:", result.error);
alert(`Failed to remove worktree: ${result.error || "Unknown error"}`);
}
} catch (error) {
console.error("Error removing worktree:", error);
alert(`Error: ${error instanceof Error ? error.message : String(error)}`);
}
};
🤖 Prompt for AI Agents
In apps/desktop/src/renderer/screens/main/components/Layout/NewLayoutMain.tsx
around lines 501 to 546, after setting setCurrentWorkspace(refreshedWorkspace)
the code calls handleTabSelect which uses a stale closure of currentWorkspace
and can reintroduce deleted data; replace the handleTabSelect(...) call with
direct state setters (setSelectedWorktreeId(firstWorktree.id) and
setSelectedTabId(firstWorktree.tabs[0].id)) and, if required, invoke the
workspace-set-active-selection IPC directly with refreshedWorkspace.id to keep
main-process selection in sync instead of calling handleTabSelect.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
apps/desktop/src/renderer/screens/main/components/Layout/TaskTabs/WorktreeTabButton.tsx (1)

62-83: Add focus visibility to the close button for keyboard users.

The close button remains invisible when focused via keyboard because it only shows on group-hover:opacity-100. Keyboard users navigating with Tab cannot see the button even though they can interact with it.

Apply the suggested fix to reveal the button on keyboard focus:

 			<button
 				type="button"
 				onClick={(e) => {
 					e.stopPropagation();
 					onClose(e);
 				}}
 				className={`
 					absolute right-1 top-1/2 -translate-y-1/2
 					flex items-center justify-center
 					w-4 h-4 rounded
-					opacity-0 group-hover:opacity-100
+					opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 focus:opacity-100
 					transition-opacity
 					hover:bg-neutral-700
+					focus:outline-none focus:ring-2 focus:ring-blue-500
 					text-neutral-400 hover:text-neutral-200
 					${isSelected ? "text-neutral-300" : ""}
 				`}
 				aria-label="Close tab"
 			>
 				<X size={12} />
 			</button>
🧹 Nitpick comments (2)
apps/desktop/src/renderer/screens/main/components/Layout/TaskTabs/TaskTabs.tsx (1)

85-119: Consider using finally block for consistent state cleanup.

The state cleanup logic is duplicated in both the success path (lines 109-111) and the error catch block (lines 115-117). This could lead to inconsistencies if the logic diverges in the future.

Apply this diff to consolidate state management:

 const handleCloseClick = async (e: React.MouseEvent, worktreeId: string) => {
 	e.stopPropagation();
 
 	if (!onDeleteWorktree || !workspaceId) return;
 
 	const worktree = worktrees.find((wt) => wt.id === worktreeId);
 	if (!worktree || worktree.isPending) return;
 
+	let warning = "";
+
 	// Check if the worktree has uncommitted changes
 	try {
 		const canRemoveResult = await window.ipcRenderer.invoke(
 			"worktree-can-remove",
 			{
 				workspaceId,
 				worktreeId,
 			},
 		);
 
 		// Build warning message if there are uncommitted changes
-		let warning = "";
 		if (canRemoveResult.hasUncommittedChanges) {
 			warning = `Warning: This worktree (${worktree.branch}) has uncommitted changes. Removing it will delete these changes permanently.`;
 		}
-
-		setRemoveWarning(warning);
-		setWorktreeToDelete(worktreeId);
-		setShowRemoveDialog(true);
 	} catch (error) {
 		console.error("Error checking if worktree can be removed:", error);
-		// Still show dialog even if check fails
-		setRemoveWarning("");
-		setWorktreeToDelete(worktreeId);
-		setShowRemoveDialog(true);
+	} finally {
+		setRemoveWarning(warning);
+		setWorktreeToDelete(worktreeId);
+		setShowRemoveDialog(true);
 	}
 };
apps/desktop/src/renderer/screens/main/components/Layout/TaskTabs/WorktreeTabButton.tsx (1)

26-29: Consider handling overflow behavior for the wrapper.

The wrapper div uses shrink-0 which prevents it from shrinking, but when combined with a fixed width, this could cause layout issues if the container is narrower than expected. Consider whether overflow-hidden or similar constraints are needed.

If overflow issues occur, apply this diff:

 	<div 
-		className="group relative flex items-end shrink-0" 
+		className="group relative flex items-end shrink-0 overflow-hidden" 
 		style={{ width: width ? `${width}px` : undefined }}
 	>
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a8862aa and 0bc19fd.

📒 Files selected for processing (2)
  • apps/desktop/src/renderer/screens/main/components/Layout/TaskTabs/TaskTabs.tsx (2 hunks)
  • apps/desktop/src/renderer/screens/main/components/Layout/TaskTabs/WorktreeTabButton.tsx (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
apps/desktop/src/renderer/screens/main/components/Layout/TaskTabs/TaskTabs.tsx (5)
apps/desktop/src/renderer/screens/main/components/Layout/TaskTabs/SidebarToggle.tsx (1)
  • SidebarToggle (12-42)
apps/desktop/src/renderer/screens/main/components/Layout/TaskTabs/ModeToggle.tsx (1)
  • ModeToggle (8-33)
apps/desktop/src/renderer/screens/main/components/Layout/TaskTabs/WorktreeTab.tsx (1)
  • WorktreeTab (21-54)
apps/desktop/src/renderer/screens/main/components/Layout/TaskTabs/AddTaskButton.tsx (1)
  • AddTaskButton (10-21)
apps/desktop/src/renderer/screens/main/components/Layout/TaskTabs/PRActions.tsx (1)
  • PRActions (14-69)
apps/desktop/src/renderer/screens/main/components/Layout/TaskTabs/WorktreeTabButton.tsx (1)
apps/desktop/src/renderer/screens/main/components/Layout/StatusIndicator.tsx (1)
  • StatusIndicator (55-115)
🔇 Additional comments (3)
apps/desktop/src/renderer/screens/main/components/Layout/TaskTabs/TaskTabs.tsx (2)

198-238: LGTM! Dialog implementation follows accessibility best practices.

The confirmation dialog uses semantic DialogHeader, DialogTitle, and DialogDescription components, providing proper context for screen readers. The warning message is clearly differentiated with visual styling and the destructive action is appropriately styled with the "destructive" variant.


41-79: ****

The calculation is correct. The offsetWidth property returns the viewable width of an element including padding and border, so any padding on the container is already accounted for. The tabs container's className has no padding classes, and flex-1 allows a flex item to grow and shrink as needed—combined with min-w-0, this ensures the tabs container only receives the space available after siblings (like AddTaskButton) are sized. The flex layout automatically handles this distribution, making the offsetWidth measurement accurate for tab width calculations. The ResizeObserver setup and cleanup are properly implemented.

Likely an incorrect or invalid review comment.

apps/desktop/src/renderer/screens/main/components/Layout/TaskTabs/WorktreeTabButton.tsx (1)

30-61: LGTM! Dynamic padding adjustment handles the close button spacing.

The conditional padding (onClose && !isPending ? "pl-3 pr-6" : "px-3") correctly reserves space for the absolutely positioned close button, preventing text overlap. The width constraints and truncation ensure the tab label degrades gracefully when space is limited.

Comment on lines +121 to +130
const confirmRemoveWorktree = async () => {
if (!onDeleteWorktree || !worktreeToDelete) return;

setShowRemoveDialog(false);
setRemoveWarning("");
const worktreeId = worktreeToDelete;
setWorktreeToDelete(null);

await onDeleteWorktree(worktreeId);
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Handle errors from the deletion operation.

confirmRemoveWorktree calls onDeleteWorktree but doesn't catch potential errors. If the deletion fails, the user won't be notified and the UI state will be inconsistent.

Apply this diff to add error handling:

 const confirmRemoveWorktree = async () => {
 	if (!onDeleteWorktree || !worktreeToDelete) return;
 
 	setShowRemoveDialog(false);
-	setRemoveWarning("");
 	const worktreeId = worktreeToDelete;
-	setWorktreeToDelete(null);
 
-	await onDeleteWorktree(worktreeId);
+	try {
+		await onDeleteWorktree(worktreeId);
+	} catch (error) {
+		console.error("Error deleting worktree:", error);
+		// TODO: Show error notification to user
+	} finally {
+		setRemoveWarning("");
+		setWorktreeToDelete(null);
+	}
 };
📝 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
const confirmRemoveWorktree = async () => {
if (!onDeleteWorktree || !worktreeToDelete) return;
setShowRemoveDialog(false);
setRemoveWarning("");
const worktreeId = worktreeToDelete;
setWorktreeToDelete(null);
await onDeleteWorktree(worktreeId);
};
const confirmRemoveWorktree = async () => {
if (!onDeleteWorktree || !worktreeToDelete) return;
setShowRemoveDialog(false);
const worktreeId = worktreeToDelete;
try {
await onDeleteWorktree(worktreeId);
} catch (error) {
console.error("Error deleting worktree:", error);
// TODO: Show error notification to user
} finally {
setRemoveWarning("");
setWorktreeToDelete(null);
}
};
🤖 Prompt for AI Agents
In
apps/desktop/src/renderer/screens/main/components/Layout/TaskTabs/TaskTabs.tsx
around lines 121-130, confirmRemoveWorktree awaits onDeleteWorktree without
handling errors; wrap the await in a try/catch, call onDeleteWorktree inside
try, and on success clear dialog state as now; in catch set an informative
user-visible error (e.g. setRemoveWarning(error.message || "Failed to delete
worktree")), restore or keep worktreeToDelete and re-open the remove dialog
(setShowRemoveDialog(true)) so UI remains consistent, and log the error for
debugging.

@Kitenite Kitenite changed the title management Tabs creation and management Nov 14, 2025
@Kitenite Kitenite merged commit b6157d4 into main Nov 14, 2025
1 of 5 checks passed
@Kitenite Kitenite deleted the top-tabs branch November 14, 2025 19:50
@Kitenite Kitenite restored the top-tabs branch November 14, 2025 19:50
@Kitenite Kitenite deleted the top-tabs branch November 19, 2025 00:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant