From 6bdacd26af943db6e957b35546b1279c1bf1c48d Mon Sep 17 00:00:00 2001 From: iliya Date: Sun, 1 Mar 2026 13:47:33 +0200 Subject: [PATCH] feat: add support for Claude Code CLI worktrees - Introduced CLAUDE_CODE_DIR constant for the .claude/worktrees/ directory. - Updated GitIdentityResolver to recognize and handle worktrees from Claude Code CLI. - Enhanced WorktreeBadge and DashboardView to display appropriate labels for Claude Code worktrees. - Updated type definitions and source labels to include 'claude-code' for better integration. - Improved session handling in DateGroupedSessions and FileSectionDiff components to accommodate new worktree type. --- src/main/constants/worktreePatterns.ts | 3 + .../services/parsing/GitIdentityResolver.ts | 37 ++++++++ src/main/types/domain.ts | 1 + .../components/common/WorktreeBadge.tsx | 5 + .../components/dashboard/DashboardView.tsx | 91 +++++++++++++++---- .../sidebar/DateGroupedSessions.tsx | 1 + .../team/review/FileSectionDiff.tsx | 9 +- .../team/review/ReviewDiffContent.tsx | 13 --- 8 files changed, 125 insertions(+), 35 deletions(-) diff --git a/src/main/constants/worktreePatterns.ts b/src/main/constants/worktreePatterns.ts index f2ed2c02..26a355f4 100644 --- a/src/main/constants/worktreePatterns.ts +++ b/src/main/constants/worktreePatterns.ts @@ -42,3 +42,6 @@ export const CLAUDE_WORKTREES_DIR = '.claude-worktrees'; /** ccswitch worktrees directory */ export const CCSWITCH_DIR = '.ccswitch'; + +/** Claude Code CLI worktrees directory (.claude/worktrees/) */ +export const CLAUDE_CODE_DIR = '.claude'; diff --git a/src/main/services/parsing/GitIdentityResolver.ts b/src/main/services/parsing/GitIdentityResolver.ts index bed2c1f3..03043082 100644 --- a/src/main/services/parsing/GitIdentityResolver.ts +++ b/src/main/services/parsing/GitIdentityResolver.ts @@ -18,6 +18,7 @@ import { AUTO_CLAUDE_DIR, CCSWITCH_DIR, + CLAUDE_CODE_DIR, CLAUDE_WORKTREES_DIR, CONDUCTOR_DIR, CURSOR_DIR, @@ -242,6 +243,15 @@ class GitIdentityResolver { return parts[claudeWorktreesIdx + 1]; } + // Pattern 6b: /.claude/worktrees/{name} (Claude Code CLI) + const claudeCodeDirIdx = parts.indexOf(CLAUDE_CODE_DIR); + if (claudeCodeDirIdx >= 0 && parts[claudeCodeDirIdx + 1] === WORKTREES_DIR) { + // Repo is the directory containing .claude + if (claudeCodeDirIdx > 0) { + return parts[claudeCodeDirIdx - 1]; + } + } + // Pattern 7: /.ccswitch/worktrees/{repo}/{name} const ccswitchIdx = parts.indexOf(CCSWITCH_DIR); if (ccswitchIdx >= 0 && parts[ccswitchIdx + 1] === WORKTREES_DIR) { @@ -282,6 +292,11 @@ class GitIdentityResolver { if (parts.includes(CCSWITCH_DIR) && parts.includes(WORKTREES_DIR)) { return true; } + // Pattern: .claude/worktrees/{name} (Claude Code CLI worktrees) + const claudeCodeIdx = parts.indexOf(CLAUDE_CODE_DIR); + if (claudeCodeIdx >= 0 && parts[claudeCodeIdx + 1] === WORKTREES_DIR) { + return true; + } if (parts.includes(CONDUCTOR_DIR) && parts.includes(WORKSPACES_DIR)) { // Subpaths in conductor/workspaces are worktrees const conductorIdx = parts.indexOf(CONDUCTOR_DIR); @@ -569,6 +584,15 @@ class GitIdentityResolver { return 'ccswitch'; } + // Pattern: claude-code (Claude Code CLI) + // /Users/.../.claude/worktrees/{name} + { + const claudeCodeIdx = parts.indexOf(CLAUDE_CODE_DIR); + if (claudeCodeIdx >= 0 && parts[claudeCodeIdx + 1] === WORKTREES_DIR) { + return 'claude-code'; + } + } + // Check if it's a standard git repo (only if filesystem exists) // For deleted repos, we'll return 'git' as fallback since we can't verify if (await fileExists(path.join(projectPath, '.git'))) { @@ -668,6 +692,19 @@ class GitIdentityResolver { break; } + case 'claude-code': { + // Pattern: .claude/worktrees/{name} + // Display: {name} (e.g., "editor-feature") + const claudeCodeDirIdx = parts.indexOf(CLAUDE_CODE_DIR); + if (claudeCodeDirIdx >= 0) { + const worktreesIdx = parts.indexOf(WORKTREES_DIR, claudeCodeDirIdx); + if (worktreesIdx >= 0 && parts[worktreesIdx + 1]) { + return parts[worktreesIdx + 1]; + } + } + break; + } + case 'git': // Standard git worktree - use branch or path-based name if (isMainWorktree) { diff --git a/src/main/types/domain.ts b/src/main/types/domain.ts index 9b1abde8..6f7342b7 100644 --- a/src/main/types/domain.ts +++ b/src/main/types/domain.ts @@ -147,6 +147,7 @@ export type WorktreeSource = | 'auto-claude' // /Users/.../.auto-claude/worktrees/tasks/{task-id} | '21st' // /Users/.../.21st/worktrees/{id}/{name [bracket-id]} | 'claude-desktop' // /Users/.../.claude-worktrees/{repo}/{name} + | 'claude-code' // /Users/.../.claude/worktrees/{name} | 'ccswitch' // /Users/.../.ccswitch/worktrees/{repo}/{name} | 'git' // Standard git worktree (main repo or detached) | 'unknown'; // Non-git project or undetectable diff --git a/src/renderer/components/common/WorktreeBadge.tsx b/src/renderer/components/common/WorktreeBadge.tsx index ad6febc0..7d9686ac 100644 --- a/src/renderer/components/common/WorktreeBadge.tsx +++ b/src/renderer/components/common/WorktreeBadge.tsx @@ -52,6 +52,11 @@ const SOURCE_CONFIG: Record = { bgColor: WORKTREE_BADGE_BG, textColor: WORKTREE_BADGE_TEXT, }, + 'claude-code': { + label: 'Worktree', + bgColor: WORKTREE_BADGE_BG, + textColor: WORKTREE_BADGE_TEXT, + }, ccswitch: { label: 'ccswitch', bgColor: WORKTREE_BADGE_BG, diff --git a/src/renderer/components/dashboard/DashboardView.tsx b/src/renderer/components/dashboard/DashboardView.tsx index 6efb75bf..53a952b2 100644 --- a/src/renderer/components/dashboard/DashboardView.tsx +++ b/src/renderer/components/dashboard/DashboardView.tsx @@ -24,8 +24,9 @@ import { createLogger } from '@shared/utils/logger'; import { useShallow } from 'zustand/react/shallow'; const logger = createLogger('Component:DashboardView'); +import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip'; import { formatDistanceToNow } from 'date-fns'; -import { Command, FolderGit2, FolderOpen, GitBranch, Search, Users } from 'lucide-react'; +import { Command, FolderGit2, FolderOpen, GitBranch, GitFork, Search, Users } from 'lucide-react'; import { CliStatusBanner } from './CliStatusBanner'; @@ -138,6 +139,41 @@ const RepositoryCard = ({ const mainWorktree = repo.worktrees.find((w) => w.isMainWorktree) ?? repo.worktrees[0]; const mainBranch = mainWorktree?.gitBranch; + // Detect if this is a worktree project: + // 1. No main worktree in the group (isMainWorktree flag) + // 2. OR the shown worktree has a tool-created source + // 3. OR path-based fallback for .claude/worktrees/ directories + const WORKTREE_PATH_MARKERS = [ + '/.claude/worktrees/', + '/.claude-worktrees/', + '/.auto-claude/worktrees/', + '/.21st/worktrees/', + '/.ccswitch/worktrees/', + '/.cursor/worktrees/', + '/vibe-kanban/worktrees/', + '/conductor/workspaces/', + ]; + + const shownWorktree = repo.worktrees[0]; + const isWorktreeBySource = + shownWorktree?.source && !['git', 'unknown'].includes(shownWorktree.source); + const isWorktreeByPath = + shownWorktree && WORKTREE_PATH_MARKERS.some((m) => shownWorktree.path.includes(m)); + const isWorktreeProject = + !repo.worktrees.some((w) => w.isMainWorktree) || isWorktreeBySource || isWorktreeByPath; + + // Get the source label for worktree badge + const SOURCE_LABELS: Record = { + 'vibe-kanban': 'Vibe', + conductor: 'Conductor', + 'auto-claude': 'Auto', + '21st': '21st', + 'claude-desktop': 'Desktop', + 'claude-code': 'Worktree', + ccswitch: 'ccswitch', + }; + const worktreeSourceLabel = shownWorktree?.source && SOURCE_LABELS[shownWorktree.source]; + const color = useMemo(() => projectColor(repo.name), [repo.name]); const cardRef = useRef(null); const [isHovered, setIsHovered] = useState(false); @@ -171,30 +207,49 @@ const RepositoryCard = ({ {/* Icon + Project name */}
- + {isWorktreeProject ? ( + + ) : ( + + )}

{repo.name}

+ {isWorktreeProject && ( + + {worktreeSourceLabel ?? 'Worktree'} + + )}
{/* Project path - monospace, muted, clickable to open in file manager */} -
{ - if (e.key === 'Enter' || e.key === ' ') handleOpenPath(e as unknown as React.MouseEvent); - }} - className="flex w-full min-w-0 cursor-pointer items-center gap-1 truncate text-left font-mono text-[10px] text-text-muted transition-colors hover:text-text-secondary" - title={`Open in file manager: ${projectPath}`} - > - - {formattedPath} -
+ + +
{ + if (e.key === 'Enter' || e.key === ' ') + handleOpenPath(e as unknown as React.MouseEvent); + }} + className="flex w-full min-w-0 cursor-pointer items-center gap-1 truncate text-left font-mono text-[10px] text-text-muted transition-colors hover:text-text-secondary" + > + + {formattedPath} +
+
+ +

{projectPath}

+
+
{/* Git branch / worktree info */} {mainBranch ? ( diff --git a/src/renderer/components/sidebar/DateGroupedSessions.tsx b/src/renderer/components/sidebar/DateGroupedSessions.tsx index c72701fc..6d09260b 100644 --- a/src/renderer/components/sidebar/DateGroupedSessions.tsx +++ b/src/renderer/components/sidebar/DateGroupedSessions.tsx @@ -57,6 +57,7 @@ const SOURCE_LABELS: Record = { 'auto-claude': 'Auto Claude', '21st': '21st', 'claude-desktop': 'Claude Desktop', + 'claude-code': 'Claude Code', ccswitch: 'ccswitch', git: 'Git', unknown: 'Other', diff --git a/src/renderer/components/team/review/FileSectionDiff.tsx b/src/renderer/components/team/review/FileSectionDiff.tsx index 701c3b68..867d2347 100644 --- a/src/renderer/components/team/review/FileSectionDiff.tsx +++ b/src/renderer/components/team/review/FileSectionDiff.tsx @@ -89,8 +89,9 @@ export const FileSectionDiff = ({ return writeSnippets[writeSnippets.length - 1].newString; })(); - const hasCodeMirrorContent = - fileContent && fileContent.contentSource !== 'unavailable' && resolvedModified !== null; + // Show CodeMirror whenever we have resolved modified content (including new files + // where contentSource may be 'unavailable' but write-new snippet provides the content) + const hasCodeMirrorContent = resolvedModified !== null; if (!hasCodeMirrorContent) { return ( @@ -105,12 +106,12 @@ export const FileSectionDiff = ({
{ return (
- {/* Заголовок файла */} -
- {file.relativePath} - {file.isNewFile && ( - - NEW - - )} - - {nonErrorSnippets.length} change{nonErrorSnippets.length !== 1 ? 's' : ''} - -
- {/* Snippets */} {nonErrorSnippets.map((snippet, index) => (