From 120071cd42f68e5994d9ee06ef7b6df03f4aa134 Mon Sep 17 00:00:00 2001 From: Andy <119136210+AndyMik90@users.noreply.github.com> Date: Tue, 13 Jan 2026 20:44:48 +0100 Subject: [PATCH] feat(terminal): add task worktrees section and remove terminal limit (#1033) * feat(terminal): add task worktrees section and remove terminal limit - Remove 12 terminal worktree limit (now unlimited) - Add "Task Worktrees" section in worktree dropdown below terminal worktrees - Task worktrees (created by kanban) now accessible for manual work - Update translations for new section labels (EN + FR) Co-Authored-By: Claude Opus 4.5 * fix(terminal): address PR review feedback - Clear taskWorktrees state when project is null or changes - Parallelize API calls with Promise.all for better performance - Use consistent path-based filtering for both worktree types - Add clarifying comment for createdAt timestamp Co-Authored-By: Claude Opus 4.5 --------- Co-authored-by: Claude Opus 4.5 Co-authored-by: Test User --- .../terminal/worktree-handlers.ts | 10 +- .../components/terminal/WorktreeSelector.tsx | 168 +++++++++++++----- .../src/renderer/stores/terminal-store.ts | 2 +- .../src/shared/i18n/locales/en/terminal.json | 3 +- .../src/shared/i18n/locales/fr/terminal.json | 3 +- 5 files changed, 128 insertions(+), 58 deletions(-) diff --git a/apps/frontend/src/main/ipc-handlers/terminal/worktree-handlers.ts b/apps/frontend/src/main/ipc-handlers/terminal/worktree-handlers.ts index ca91fd70..d9ac8cd2 100644 --- a/apps/frontend/src/main/ipc-handlers/terminal/worktree-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/terminal/worktree-handlers.ts @@ -138,7 +138,7 @@ function isValidProjectPath(projectPath: string): boolean { return projects.some(p => p.path === projectPath); } -const MAX_TERMINAL_WORKTREES = 12; +// No limit on terminal worktrees - users can create as many as needed /** * Get the default branch from project settings OR env config @@ -267,14 +267,6 @@ async function createTerminalWorktree( }; } - const existing = await listTerminalWorktrees(projectPath); - if (existing.length >= MAX_TERMINAL_WORKTREES) { - return { - success: false, - error: `Maximum of ${MAX_TERMINAL_WORKTREES} terminal worktrees reached.`, - }; - } - // Auto-fix any misconfigured bare repo before worktree operations // This prevents crashes when git worktree operations have incorrectly set bare=true if (fixMisconfiguredBareRepo(projectPath)) { diff --git a/apps/frontend/src/renderer/components/terminal/WorktreeSelector.tsx b/apps/frontend/src/renderer/components/terminal/WorktreeSelector.tsx index d9b449e3..1394133a 100644 --- a/apps/frontend/src/renderer/components/terminal/WorktreeSelector.tsx +++ b/apps/frontend/src/renderer/components/terminal/WorktreeSelector.tsx @@ -1,7 +1,7 @@ import { useState, useEffect } from 'react'; -import { FolderGit, Plus, ChevronDown, Loader2, Trash2 } from 'lucide-react'; +import { FolderGit, Plus, ChevronDown, Loader2, Trash2, ListTodo } from 'lucide-react'; import { useTranslation } from 'react-i18next'; -import type { TerminalWorktreeConfig } from '../../../shared/types'; +import type { TerminalWorktreeConfig, WorktreeListItem } from '../../../shared/types'; import { DropdownMenu, DropdownMenuContent, @@ -20,6 +20,7 @@ import { AlertDialogTitle, } from '../ui/alert-dialog'; import { cn } from '../../lib/utils'; +import { useProjectStore } from '../../stores/project-store'; interface WorktreeSelectorProps { terminalId: string; @@ -33,7 +34,7 @@ interface WorktreeSelectorProps { } export function WorktreeSelector({ - terminalId: _terminalId, + terminalId, projectPath, currentWorktree, onCreateWorktree, @@ -41,24 +42,48 @@ export function WorktreeSelector({ }: WorktreeSelectorProps) { const { t } = useTranslation(['terminal', 'common']); const [worktrees, setWorktrees] = useState([]); + const [taskWorktrees, setTaskWorktrees] = useState([]); const [isLoading, setIsLoading] = useState(false); const [isOpen, setIsOpen] = useState(false); const [deleteWorktree, setDeleteWorktree] = useState(null); const [isDeleting, setIsDeleting] = useState(false); + // Get project ID from projectPath for task worktrees API + const project = useProjectStore((state) => + state.projects.find((p) => p.path === projectPath) + ); + // Fetch worktrees when dropdown opens const fetchWorktrees = async () => { if (!projectPath) return; setIsLoading(true); try { - const result = await window.electronAPI.listTerminalWorktrees(projectPath); - if (result.success && result.data) { - // Filter out the current worktree from the list + // Fetch terminal worktrees and task worktrees in parallel + const [terminalResult, taskResult] = await Promise.all([ + window.electronAPI.listTerminalWorktrees(projectPath), + project?.id ? window.electronAPI.listWorktrees(project.id) : Promise.resolve(null), + ]); + + // Process terminal worktrees + if (terminalResult.success && terminalResult.data) { + // Filter out the current worktree from the list using path for consistency const available = currentWorktree - ? result.data.filter((wt) => wt.name !== currentWorktree.name) - : result.data; + ? terminalResult.data.filter((wt) => wt.worktreePath !== currentWorktree.worktreePath) + : terminalResult.data; setWorktrees(available); } + + // Process task worktrees + if (taskResult?.success && taskResult.data?.worktrees) { + // Filter out current worktree if it matches a task worktree + const availableTaskWorktrees = currentWorktree + ? taskResult.data.worktrees.filter((wt) => wt.path !== currentWorktree.worktreePath) + : taskResult.data.worktrees; + setTaskWorktrees(availableTaskWorktrees); + } else { + // Clear task worktrees when project is null or fetch failed + setTaskWorktrees([]); + } } catch (err) { console.error('Failed to fetch worktrees:', err); } finally { @@ -66,11 +91,26 @@ export function WorktreeSelector({ } }; + // Convert task worktree to terminal worktree config for selection + const selectTaskWorktree = (taskWt: WorktreeListItem) => { + const config: TerminalWorktreeConfig = { + name: taskWt.specName, + worktreePath: taskWt.path, + branchName: taskWt.branch, + baseBranch: taskWt.baseBranch, + hasGitBranch: true, + // Note: This represents when the worktree was attached to this terminal, not when it was originally created + createdAt: new Date().toISOString(), + terminalId, + }; + onSelectWorktree(config); + }; + useEffect(() => { if (isOpen && projectPath) { fetchWorktrees(); } - }, [isOpen, projectPath, currentWorktree]); + }, [isOpen, projectPath, currentWorktree, project?.id]); // Handle delete worktree const handleDeleteWorktree = async () => { @@ -137,46 +177,82 @@ export function WorktreeSelector({ - ) : worktrees.length > 0 ? ( + ) : ( <> - -
- {t('terminal:worktree.existing')} -
- {worktrees.map((wt) => ( - { - e.stopPropagation(); - setIsOpen(false); - onSelectWorktree(wt); - }} - className="text-xs group" - > - -
- {wt.name} - {wt.branchName && ( - - {wt.branchName} - - )} + {/* Terminal Worktrees Section */} + {worktrees.length > 0 && ( + <> + +
+ {t('terminal:worktree.existing')}
- - - ))} + {worktrees.map((wt) => ( + { + e.stopPropagation(); + setIsOpen(false); + onSelectWorktree(wt); + }} + className="text-xs group" + > + +
+ {wt.name} + {wt.branchName && ( + + {wt.branchName} + + )} +
+ +
+ ))} + + )} + + {/* Task Worktrees Section */} + {taskWorktrees.length > 0 && ( + <> + +
+ {t('terminal:worktree.taskWorktrees')} +
+ {taskWorktrees.map((wt) => ( + { + e.stopPropagation(); + setIsOpen(false); + selectTaskWorktree(wt); + }} + className="text-xs group" + > + +
+ {wt.specName} + {wt.branch && ( + + {wt.branch} + + )} +
+
+ ))} + + )} - ) : null} + )} diff --git a/apps/frontend/src/renderer/stores/terminal-store.ts b/apps/frontend/src/renderer/stores/terminal-store.ts index 4975603c..263edb82 100644 --- a/apps/frontend/src/renderer/stores/terminal-store.ts +++ b/apps/frontend/src/renderer/stores/terminal-store.ts @@ -70,7 +70,7 @@ export const useTerminalStore = create((set, get) => ({ terminals: [], layouts: [], activeTerminalId: null, - maxTerminals: 12, + maxTerminals: Infinity, // No limit on terminals hasRestoredSessions: false, addTerminal: (cwd?: string, projectPath?: string) => { diff --git a/apps/frontend/src/shared/i18n/locales/en/terminal.json b/apps/frontend/src/shared/i18n/locales/en/terminal.json index 9552256e..370d4032 100644 --- a/apps/frontend/src/shared/i18n/locales/en/terminal.json +++ b/apps/frontend/src/shared/i18n/locales/en/terminal.json @@ -10,7 +10,8 @@ "worktree": { "create": "Worktree", "createNew": "New Worktree", - "existing": "Existing Worktrees", + "existing": "Terminal Worktrees", + "taskWorktrees": "Task Worktrees", "createTitle": "Create Terminal Worktree", "createDescription": "Create an isolated workspace for this terminal. All work will happen in the worktree directory.", "name": "Worktree Name", diff --git a/apps/frontend/src/shared/i18n/locales/fr/terminal.json b/apps/frontend/src/shared/i18n/locales/fr/terminal.json index d691fefb..20abb2ea 100644 --- a/apps/frontend/src/shared/i18n/locales/fr/terminal.json +++ b/apps/frontend/src/shared/i18n/locales/fr/terminal.json @@ -10,7 +10,8 @@ "worktree": { "create": "Worktree", "createNew": "Nouveau Worktree", - "existing": "Worktrees Existants", + "existing": "Worktrees Terminal", + "taskWorktrees": "Worktrees de Taches", "createTitle": "Creer un Worktree Terminal", "createDescription": "Creer un espace de travail isole pour ce terminal. Tout le travail se fera dans le repertoire du worktree.", "name": "Nom du Worktree",