ccmanager 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,6 +15,7 @@ https://github.com/user-attachments/assets/15914a88-e288-4ac9-94d5-8127f2e19dbf
15
15
  - Switch between sessions seamlessly
16
16
  - Visual status indicators for session states (busy, waiting, idle)
17
17
  - Create, merge, and delete worktrees from within the app
18
+ - **Copy Claude Code session data** between worktrees to maintain conversation context
18
19
  - Configurable keyboard shortcuts
19
20
  - Command presets with automatic fallback support
20
21
  - Configurable state detection strategies for different CLI tools
@@ -154,6 +155,41 @@ CCManager supports configuring the command and arguments used to run Claude Code
154
155
  For detailed configuration options and examples, see [docs/command-config.md](docs/command-config.md).
155
156
 
156
157
 
158
+ ## Session Data Copying
159
+
160
+ CCManager can copy Claude Code session data (conversation history, context, and project state) when creating new worktrees, allowing you to maintain context across different branches.
161
+
162
+ ### Features
163
+
164
+ - **Seamless Context Transfer**: Continue conversations in new worktrees without losing context
165
+ - **Configurable Default**: Set whether to copy session data by default
166
+ - **Per-Creation Choice**: Decide on each worktree creation whether to copy data
167
+ - **Safe Operation**: Copying is non-fatal - worktree creation succeeds even if copying fails
168
+
169
+ ### How It Works
170
+
171
+ When creating a new worktree, CCManager:
172
+ 1. Asks whether to copy session data from the current worktree
173
+ 2. Copies all session files from `~/.claude/projects/[source-path]` to `~/.claude/projects/[target-path]`
174
+ 3. Preserves conversation history, project context, and Claude Code state
175
+ 4. Allows immediate continuation of conversations in the new worktree
176
+
177
+ ### Configuration
178
+
179
+ 1. Navigate to **Configuration** → **Configure Worktree**
180
+ 2. Toggle **Copy Session Data** to set the default behavior
181
+ 3. Save changes
182
+
183
+ The default choice (copy or start fresh) will be pre-selected when creating new worktrees.
184
+
185
+ ### Use Cases
186
+
187
+ - **Feature Development**: Copy session data when creating feature branches to maintain project context
188
+ - **Experimentation**: Start fresh when testing unrelated changes
189
+ - **Collaboration**: Share session state across team worktrees
190
+ - **Context Preservation**: Maintain long conversations across multiple development branches
191
+
192
+
157
193
  ## Status Change Hooks
158
194
 
159
195
  CCManager can execute custom commands when Claude Code session status changes. This enables powerful automation workflows like desktop notifications, logging, or integration with other tools.
@@ -150,11 +150,11 @@ const App = ({ devcontainerConfig }) => {
150
150
  }
151
151
  }, 50); // Small delay to ensure proper cleanup
152
152
  };
153
- const handleCreateWorktree = async (path, branch, baseBranch, copyClaudeDirectory) => {
153
+ const handleCreateWorktree = async (path, branch, baseBranch, copySessionData, copyClaudeDirectory) => {
154
154
  setView('creating-worktree');
155
155
  setError(null);
156
156
  // Create the worktree
157
- const result = worktreeService.createWorktree(path, branch, baseBranch, copyClaudeDirectory);
157
+ const result = worktreeService.createWorktree(path, branch, baseBranch, copySessionData, copyClaudeDirectory);
158
158
  if (result.success) {
159
159
  // Success - return to menu
160
160
  handleReturnToMenu();
@@ -8,6 +8,7 @@ const ConfigureWorktree = ({ onComplete }) => {
8
8
  const worktreeConfig = configurationManager.getWorktreeConfig();
9
9
  const [autoDirectory, setAutoDirectory] = useState(worktreeConfig.autoDirectory);
10
10
  const [pattern, setPattern] = useState(worktreeConfig.autoDirectoryPattern || '../{branch}');
11
+ const [copySessionData, setCopySessionData] = useState(worktreeConfig.copySessionData ?? true);
11
12
  const [editMode, setEditMode] = useState('menu');
12
13
  const [tempPattern, setTempPattern] = useState(pattern);
13
14
  useInput((input, key) => {
@@ -25,6 +26,10 @@ const ConfigureWorktree = ({ onComplete }) => {
25
26
  label: `Pattern: ${pattern}`,
26
27
  value: 'pattern',
27
28
  },
29
+ {
30
+ label: `Copy Session Data: ${copySessionData ? '✅ Enabled' : '❌ Disabled'}`,
31
+ value: 'toggleCopy',
32
+ },
28
33
  {
29
34
  label: '💾 Save Changes',
30
35
  value: 'save',
@@ -43,11 +48,15 @@ const ConfigureWorktree = ({ onComplete }) => {
43
48
  setTempPattern(pattern);
44
49
  setEditMode('pattern');
45
50
  break;
51
+ case 'toggleCopy':
52
+ setCopySessionData(!copySessionData);
53
+ break;
46
54
  case 'save':
47
55
  // Save the configuration
48
56
  configurationManager.setWorktreeConfig({
49
57
  autoDirectory,
50
58
  autoDirectoryPattern: pattern,
59
+ copySessionData,
51
60
  });
52
61
  onComplete();
53
62
  break;
@@ -83,7 +92,7 @@ const ConfigureWorktree = ({ onComplete }) => {
83
92
  React.createElement(Box, { marginBottom: 1 },
84
93
  React.createElement(Text, { bold: true, color: "green" }, "Configure Worktree Settings")),
85
94
  React.createElement(Box, { marginBottom: 1 },
86
- React.createElement(Text, { dimColor: true }, "Configure automatic worktree directory generation")),
95
+ React.createElement(Text, { dimColor: true }, "Configure worktree creation settings")),
87
96
  autoDirectory && (React.createElement(Box, { marginBottom: 1 },
88
97
  React.createElement(Text, null,
89
98
  "Example: branch \"feature/my-feature\" \u2192 directory \"",
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  interface NewWorktreeProps {
3
- onComplete: (path: string, branch: string, baseBranch: string, copyClaudeDirectory: boolean) => void;
3
+ onComplete: (path: string, branch: string, baseBranch: string, copySessionData: boolean, copyClaudeDirectory: boolean) => void;
4
4
  onCancel: () => void;
5
5
  }
6
6
  declare const NewWorktree: React.FC<NewWorktreeProps>;
@@ -14,6 +14,8 @@ const NewWorktree = ({ onComplete, onCancel }) => {
14
14
  const [path, setPath] = useState('');
15
15
  const [branch, setBranch] = useState('');
16
16
  const [baseBranch, setBaseBranch] = useState('');
17
+ const [copyClaudeDirectory, setCopyClaudeDirectory] = useState(true);
18
+ const [copySessionData, setCopySessionData] = useState(worktreeConfig.copySessionData ?? true);
17
19
  // Initialize worktree service and load branches (memoized to avoid re-initialization)
18
20
  const { branches, defaultBranch } = useMemo(() => {
19
21
  const service = new WorktreeService();
@@ -50,30 +52,22 @@ const NewWorktree = ({ onComplete, onCancel }) => {
50
52
  };
51
53
  const handleBaseBranchSelect = (item) => {
52
54
  setBaseBranch(item.value);
53
- // Check if .claude directory exists in the base branch
54
- const service = new WorktreeService();
55
- if (service.hasClaudeDirectoryInBranch(item.value)) {
56
- setStep('copy-settings');
57
- }
58
- else {
59
- // Skip copy-settings step and complete with copySettings = false
60
- if (isAutoDirectory) {
61
- const autoPath = generateWorktreeDirectory(branch, worktreeConfig.autoDirectoryPattern);
62
- onComplete(autoPath, branch, item.value, false);
63
- }
64
- else {
65
- onComplete(path, branch, item.value, false);
66
- }
67
- }
55
+ setStep('copy-settings');
68
56
  };
69
57
  const handleCopySettingsSelect = (item) => {
58
+ setCopyClaudeDirectory(item.value);
59
+ setStep('copy-session');
60
+ };
61
+ const handleCopySessionSelect = (item) => {
62
+ const shouldCopy = item.value === 'yes';
63
+ setCopySessionData(shouldCopy);
70
64
  if (isAutoDirectory) {
71
65
  // Generate path from branch name
72
66
  const autoPath = generateWorktreeDirectory(branch, worktreeConfig.autoDirectoryPattern);
73
- onComplete(autoPath, branch, baseBranch, item.value);
67
+ onComplete(autoPath, branch, baseBranch, shouldCopy, copyClaudeDirectory);
74
68
  }
75
69
  else {
76
- onComplete(path, branch, baseBranch, item.value);
70
+ onComplete(path, branch, baseBranch, shouldCopy, copyClaudeDirectory);
77
71
  }
78
72
  };
79
73
  // Calculate generated path for preview (memoized to avoid expensive recalculations)
@@ -129,6 +123,15 @@ const NewWorktree = ({ onComplete, onCancel }) => {
129
123
  },
130
124
  { label: 'No - Start without .claude directory', value: false },
131
125
  ], onSelect: handleCopySettingsSelect, initialIndex: 0 }))),
126
+ step === 'copy-session' && (React.createElement(Box, { flexDirection: "column" },
127
+ React.createElement(Box, { marginBottom: 1 },
128
+ React.createElement(Text, null, "Copy Claude Code session data to the new worktree?")),
129
+ React.createElement(Box, { marginBottom: 1 },
130
+ React.createElement(Text, { dimColor: true }, "This will copy conversation history and context from the current worktree")),
131
+ React.createElement(SelectInput, { items: [
132
+ { label: '✅ Yes, copy session data', value: 'yes' },
133
+ { label: '❌ No, start fresh', value: 'no' },
134
+ ], onSelect: handleCopySessionSelect, initialIndex: copySessionData ? 0 : 1 }))),
132
135
  React.createElement(Box, { marginTop: 1 },
133
136
  React.createElement(Text, { dimColor: true },
134
137
  "Press ",
@@ -66,8 +66,12 @@ export class ConfigurationManager {
66
66
  if (!this.config.worktree) {
67
67
  this.config.worktree = {
68
68
  autoDirectory: false,
69
+ copySessionData: true,
69
70
  };
70
71
  }
72
+ if (!Object.prototype.hasOwnProperty.call(this.config.worktree, 'copySessionData')) {
73
+ this.config.worktree.copySessionData = true;
74
+ }
71
75
  if (!this.config.command) {
72
76
  this.config.command = {
73
77
  command: 'claude',
@@ -12,6 +12,13 @@ export declare class SessionManager extends EventEmitter implements ISessionMana
12
12
  private createSessionInternal;
13
13
  createSessionWithPreset(worktreePath: string, presetId?: string): Promise<Session>;
14
14
  private setupDataHandler;
15
+ /**
16
+ * Sets up exit handler for the session process.
17
+ * When the process exits with code 1 and it's the primary command,
18
+ * it will attempt to spawn a fallback process.
19
+ * If fallbackArgs are configured, they will be used.
20
+ * If no fallbackArgs are configured, the command will be retried with no arguments.
21
+ */
15
22
  private setupExitHandler;
16
23
  private setupBackgroundHandler;
17
24
  private cleanupSession;
@@ -99,31 +99,10 @@ export class SessionManager extends EventEmitter {
99
99
  args: preset.args,
100
100
  fallbackArgs: preset.fallbackArgs,
101
101
  };
102
- // Try to spawn the process
103
- let ptyProcess;
104
- let isPrimaryCommand = true;
105
- try {
106
- ptyProcess = await this.spawn(command, args, worktreePath);
107
- }
108
- catch (error) {
109
- // If primary command fails and we have fallback args, try them
110
- if (preset.fallbackArgs) {
111
- try {
112
- ptyProcess = await this.spawn(command, preset.fallbackArgs, worktreePath);
113
- isPrimaryCommand = false;
114
- }
115
- catch (_fallbackError) {
116
- // Both attempts failed, throw the original error
117
- throw error;
118
- }
119
- }
120
- else {
121
- // No fallback args, throw the error
122
- throw error;
123
- }
124
- }
102
+ // Spawn the process - fallback will be handled by setupExitHandler
103
+ const ptyProcess = await this.spawn(command, args, worktreePath);
125
104
  return this.createSessionInternal(worktreePath, ptyProcess, commandConfig, {
126
- isPrimaryCommand,
105
+ isPrimaryCommand: true,
127
106
  detectionStrategy: preset.detectionStrategy,
128
107
  });
129
108
  }
@@ -151,13 +130,40 @@ export class SessionManager extends EventEmitter {
151
130
  }
152
131
  });
153
132
  }
133
+ /**
134
+ * Sets up exit handler for the session process.
135
+ * When the process exits with code 1 and it's the primary command,
136
+ * it will attempt to spawn a fallback process.
137
+ * If fallbackArgs are configured, they will be used.
138
+ * If no fallbackArgs are configured, the command will be retried with no arguments.
139
+ */
154
140
  setupExitHandler(session) {
155
141
  session.process.onExit(async (e) => {
156
142
  // Check if we should attempt fallback
157
143
  if (e.exitCode === 1 && !e.signal && session.isPrimaryCommand) {
158
144
  try {
159
- // Spawn fallback process
160
- const fallbackProcess = await this.spawn(session.commandConfig?.command || 'claude', session.commandConfig?.fallbackArgs || [], session.worktreePath);
145
+ let fallbackProcess;
146
+ // Use fallback args if available, otherwise use empty args
147
+ const fallbackArgs = session.commandConfig?.fallbackArgs || [];
148
+ // Check if we're in a devcontainer session
149
+ if (session.devcontainerConfig) {
150
+ // Parse the exec command to extract arguments
151
+ const execParts = session.devcontainerConfig.execCommand.split(/\s+/);
152
+ const devcontainerCmd = execParts[0] || 'devcontainer';
153
+ const execArgs = execParts.slice(1);
154
+ // Build fallback command for devcontainer
155
+ const fallbackFullArgs = [
156
+ ...execArgs,
157
+ '--',
158
+ session.commandConfig?.command || 'claude',
159
+ ...fallbackArgs,
160
+ ];
161
+ fallbackProcess = await this.spawn(devcontainerCmd, fallbackFullArgs, session.worktreePath);
162
+ }
163
+ else {
164
+ // Regular fallback without devcontainer
165
+ fallbackProcess = await this.spawn(session.commandConfig?.command || 'claude', fallbackArgs, session.worktreePath);
166
+ }
161
167
  // Replace the process
162
168
  session.process = fallbackProcess;
163
169
  session.isPrimaryCommand = false;
@@ -304,7 +310,7 @@ export class SessionManager extends EventEmitter {
304
310
  preset.command,
305
311
  ...(preset.args || []),
306
312
  ];
307
- // Spawn the process within devcontainer
313
+ // Spawn the process within devcontainer - fallback will be handled by setupExitHandler
308
314
  const ptyProcess = await this.spawn(devcontainerCmd, fullArgs, worktreePath);
309
315
  const commandConfig = {
310
316
  command: preset.command,
@@ -155,7 +155,7 @@ describe('SessionManager', () => {
155
155
  expect(configurationManager.getDefaultPreset).toHaveBeenCalled();
156
156
  expect(spawn).toHaveBeenCalledWith('claude', [], expect.any(Object));
157
157
  });
158
- it('should try fallback args with preset if main command fails', async () => {
158
+ it('should throw error when spawn fails with preset', async () => {
159
159
  // Setup mock preset with fallback
160
160
  vi.mocked(configurationManager.getDefaultPreset).mockReturnValue({
161
161
  id: '1',
@@ -164,21 +164,15 @@ describe('SessionManager', () => {
164
164
  args: ['--bad-flag'],
165
165
  fallbackArgs: ['--good-flag'],
166
166
  });
167
- // Mock spawn to fail first, succeed second
168
- let callCount = 0;
167
+ // Mock spawn to fail
169
168
  vi.mocked(spawn).mockImplementation(() => {
170
- callCount++;
171
- if (callCount === 1) {
172
- throw new Error('Command failed');
173
- }
174
- return mockPty;
169
+ throw new Error('Command failed');
175
170
  });
176
- // Create session
177
- await sessionManager.createSessionWithPreset('/test/worktree');
178
- // Verify both attempts were made
179
- expect(spawn).toHaveBeenCalledTimes(2);
180
- expect(spawn).toHaveBeenNthCalledWith(1, 'claude', ['--bad-flag'], expect.any(Object));
181
- expect(spawn).toHaveBeenNthCalledWith(2, 'claude', ['--good-flag'], expect.any(Object));
171
+ // Expect createSessionWithPreset to throw
172
+ await expect(sessionManager.createSessionWithPreset('/test/worktree')).rejects.toThrow('Command failed');
173
+ // Verify only one spawn attempt was made
174
+ expect(spawn).toHaveBeenCalledTimes(1);
175
+ expect(spawn).toHaveBeenCalledWith('claude', ['--bad-flag'], expect.any(Object));
182
176
  });
183
177
  it('should return existing session if already created', async () => {
184
178
  // Setup mock preset
@@ -264,6 +258,39 @@ describe('SessionManager', () => {
264
258
  expect(spawn).toHaveBeenCalledTimes(1);
265
259
  expect(spawn).toHaveBeenCalledWith('claude', ['--resume'], expect.objectContaining({ cwd: '/test/worktree' }));
266
260
  });
261
+ it('should use empty args as fallback when no fallback args specified', async () => {
262
+ // Setup mock preset without fallback args
263
+ vi.mocked(configurationManager.getDefaultPreset).mockReturnValue({
264
+ id: '1',
265
+ name: 'Main',
266
+ command: 'claude',
267
+ args: ['--invalid-flag'],
268
+ // No fallbackArgs
269
+ });
270
+ // First spawn attempt - will exit with code 1
271
+ const firstMockPty = new MockPty();
272
+ // Second spawn attempt - succeeds
273
+ const secondMockPty = new MockPty();
274
+ vi.mocked(spawn)
275
+ .mockReturnValueOnce(firstMockPty)
276
+ .mockReturnValueOnce(secondMockPty);
277
+ // Create session
278
+ const session = await sessionManager.createSessionWithPreset('/test/worktree');
279
+ // Verify initial spawn
280
+ expect(spawn).toHaveBeenCalledTimes(1);
281
+ expect(spawn).toHaveBeenCalledWith('claude', ['--invalid-flag'], expect.objectContaining({ cwd: '/test/worktree' }));
282
+ // Simulate exit with code 1 on first attempt
283
+ firstMockPty.emit('exit', { exitCode: 1 });
284
+ // Wait for fallback to occur
285
+ await new Promise(resolve => setTimeout(resolve, 50));
286
+ // Verify fallback spawn was called with empty args
287
+ expect(spawn).toHaveBeenCalledTimes(2);
288
+ expect(spawn).toHaveBeenNthCalledWith(2, 'claude', [], // Empty args
289
+ expect.objectContaining({ cwd: '/test/worktree' }));
290
+ // Verify session process was replaced
291
+ expect(session.process).toBe(secondMockPty);
292
+ expect(session.isPrimaryCommand).toBe(false);
293
+ });
267
294
  it('should handle custom command configuration', async () => {
268
295
  // Setup mock preset with custom command
269
296
  vi.mocked(configurationManager.getDefaultPreset).mockReturnValue({
@@ -575,5 +602,96 @@ describe('SessionManager', () => {
575
602
  'claude-3-opus',
576
603
  ], expect.any(Object));
577
604
  });
605
+ it('should use empty args as fallback in devcontainer when no fallback args specified', async () => {
606
+ const mockExec = vi.mocked(exec);
607
+ mockExec.mockImplementation((cmd, options, callback) => {
608
+ if (typeof options === 'function') {
609
+ callback = options;
610
+ options = undefined;
611
+ }
612
+ if (callback && typeof callback === 'function') {
613
+ callback(null, 'Container started', '');
614
+ }
615
+ return {};
616
+ });
617
+ // Setup preset without fallback args
618
+ vi.mocked(configurationManager.getDefaultPreset).mockReturnValue({
619
+ id: '1',
620
+ name: 'Main',
621
+ command: 'claude',
622
+ args: ['--invalid-flag'],
623
+ // No fallbackArgs
624
+ });
625
+ // First spawn attempt - will exit with code 1
626
+ const firstMockPty = new MockPty();
627
+ // Second spawn attempt - succeeds
628
+ const secondMockPty = new MockPty();
629
+ vi.mocked(spawn)
630
+ .mockReturnValueOnce(firstMockPty)
631
+ .mockReturnValueOnce(secondMockPty);
632
+ const session = await sessionManager.createSessionWithDevcontainer('/test/worktree', {
633
+ upCommand: 'devcontainer up --workspace-folder .',
634
+ execCommand: 'devcontainer exec --workspace-folder .',
635
+ });
636
+ // Verify initial spawn
637
+ expect(spawn).toHaveBeenCalledTimes(1);
638
+ expect(spawn).toHaveBeenCalledWith('devcontainer', ['exec', '--workspace-folder', '.', '--', 'claude', '--invalid-flag'], expect.objectContaining({ cwd: '/test/worktree' }));
639
+ // Simulate exit with code 1 on first attempt
640
+ firstMockPty.emit('exit', { exitCode: 1 });
641
+ // Wait for fallback to occur
642
+ await new Promise(resolve => setTimeout(resolve, 50));
643
+ // Verify fallback spawn was called with empty args
644
+ expect(spawn).toHaveBeenCalledTimes(2);
645
+ expect(spawn).toHaveBeenNthCalledWith(2, 'devcontainer', ['exec', '--workspace-folder', '.', '--', 'claude'], // No args after claude
646
+ expect.objectContaining({ cwd: '/test/worktree' }));
647
+ // Verify session process was replaced
648
+ expect(session.process).toBe(secondMockPty);
649
+ expect(session.isPrimaryCommand).toBe(false);
650
+ });
651
+ it('should use fallback args in devcontainer when primary command exits with code 1', async () => {
652
+ const mockExec = vi.mocked(exec);
653
+ mockExec.mockImplementation((cmd, options, callback) => {
654
+ if (typeof options === 'function') {
655
+ callback = options;
656
+ options = undefined;
657
+ }
658
+ if (callback && typeof callback === 'function') {
659
+ callback(null, 'Container started', '');
660
+ }
661
+ return {};
662
+ });
663
+ // Setup preset with fallback
664
+ vi.mocked(configurationManager.getDefaultPreset).mockReturnValue({
665
+ id: '1',
666
+ name: 'Main',
667
+ command: 'claude',
668
+ args: ['--bad-flag'],
669
+ fallbackArgs: ['--good-flag'],
670
+ });
671
+ // First spawn attempt - will exit with code 1
672
+ const firstMockPty = new MockPty();
673
+ // Second spawn attempt - succeeds
674
+ const secondMockPty = new MockPty();
675
+ vi.mocked(spawn)
676
+ .mockReturnValueOnce(firstMockPty)
677
+ .mockReturnValueOnce(secondMockPty);
678
+ const session = await sessionManager.createSessionWithDevcontainer('/test/worktree', {
679
+ upCommand: 'devcontainer up --workspace-folder .',
680
+ execCommand: 'devcontainer exec --workspace-folder .',
681
+ });
682
+ // Verify initial spawn
683
+ expect(spawn).toHaveBeenCalledTimes(1);
684
+ expect(spawn).toHaveBeenCalledWith('devcontainer', ['exec', '--workspace-folder', '.', '--', 'claude', '--bad-flag'], expect.objectContaining({ cwd: '/test/worktree' }));
685
+ // Simulate exit with code 1 on first attempt
686
+ firstMockPty.emit('exit', { exitCode: 1 });
687
+ // Wait for fallback to occur
688
+ await new Promise(resolve => setTimeout(resolve, 50));
689
+ // Verify fallback spawn was called
690
+ expect(spawn).toHaveBeenCalledTimes(2);
691
+ expect(spawn).toHaveBeenNthCalledWith(2, 'devcontainer', ['exec', '--workspace-folder', '.', '--', 'claude', '--good-flag'], expect.objectContaining({ cwd: '/test/worktree' }));
692
+ // Verify session process was replaced
693
+ expect(session.process).toBe(secondMockPty);
694
+ expect(session.isPrimaryCommand).toBe(false);
695
+ });
578
696
  });
579
697
  });
@@ -9,7 +9,7 @@ export declare class WorktreeService {
9
9
  isGitRepository(): boolean;
10
10
  getDefaultBranch(): string;
11
11
  getAllBranches(): string[];
12
- createWorktree(worktreePath: string, branch: string, baseBranch: string, copyClaudeDirectory?: boolean): {
12
+ createWorktree(worktreePath: string, branch: string, baseBranch: string, copySessionData?: boolean, copyClaudeDirectory?: boolean): {
13
13
  success: boolean;
14
14
  error?: string;
15
15
  };
@@ -27,6 +27,7 @@ export declare class WorktreeService {
27
27
  success: boolean;
28
28
  error?: string;
29
29
  };
30
+ private copyClaudeSessionData;
30
31
  hasClaudeDirectoryInBranch(branchName: string): boolean;
31
32
  private copyClaudeDirectoryFromBaseBranch;
32
33
  }
@@ -2,6 +2,7 @@ import { execSync } from 'child_process';
2
2
  import { existsSync, statSync, cpSync } from 'fs';
3
3
  import path from 'path';
4
4
  import { setWorktreeParentBranch } from '../utils/worktreeConfig.js';
5
+ import { getClaudeProjectsDir, pathToClaudeProjectName, } from '../utils/claudeDir.js';
5
6
  const CLAUDE_DIR = '.claude';
6
7
  export class WorktreeService {
7
8
  constructor(rootPath) {
@@ -172,7 +173,7 @@ export class WorktreeService {
172
173
  return [];
173
174
  }
174
175
  }
175
- createWorktree(worktreePath, branch, baseBranch, copyClaudeDirectory = false) {
176
+ createWorktree(worktreePath, branch, baseBranch, copySessionData = false, copyClaudeDirectory = false) {
176
177
  try {
177
178
  // Resolve the worktree path relative to the git repository root
178
179
  const resolvedPath = path.isAbsolute(worktreePath)
@@ -203,6 +204,10 @@ export class WorktreeService {
203
204
  cwd: this.gitRootPath, // Execute from git root to ensure proper resolution
204
205
  encoding: 'utf8',
205
206
  });
207
+ // Copy session data if requested
208
+ if (copySessionData) {
209
+ this.copyClaudeSessionData(this.rootPath, resolvedPath);
210
+ }
206
211
  // Store the parent branch in worktree config
207
212
  try {
208
213
  setWorktreeParentBranch(resolvedPath, baseBranch);
@@ -348,6 +353,32 @@ export class WorktreeService {
348
353
  };
349
354
  }
350
355
  }
356
+ copyClaudeSessionData(sourceWorktreePath, targetWorktreePath) {
357
+ try {
358
+ const projectsDir = getClaudeProjectsDir();
359
+ if (!existsSync(projectsDir)) {
360
+ throw new Error(`Claude projects directory does not exist: ${projectsDir}`);
361
+ }
362
+ // Convert paths to Claude's naming convention
363
+ const sourceProjectName = pathToClaudeProjectName(sourceWorktreePath);
364
+ const targetProjectName = pathToClaudeProjectName(targetWorktreePath);
365
+ const sourceProjectDir = path.join(projectsDir, sourceProjectName);
366
+ const targetProjectDir = path.join(projectsDir, targetProjectName);
367
+ // Only copy if source project exists
368
+ if (existsSync(sourceProjectDir)) {
369
+ cpSync(sourceProjectDir, targetProjectDir, {
370
+ recursive: true,
371
+ force: true,
372
+ errorOnExist: false,
373
+ preserveTimestamps: true,
374
+ });
375
+ }
376
+ }
377
+ catch (error) {
378
+ console.error(`Failed to copy Claude session data: ${error}`);
379
+ throw new Error(`Failed to copy Claude session data: ${error}`);
380
+ }
381
+ }
351
382
  hasClaudeDirectoryInBranch(branchName) {
352
383
  // Find the worktree directory for the branch
353
384
  const worktrees = this.getWorktrees();
@@ -57,6 +57,7 @@ export interface StatusHookConfig {
57
57
  export interface WorktreeConfig {
58
58
  autoDirectory: boolean;
59
59
  autoDirectoryPattern?: string;
60
+ copySessionData?: boolean;
60
61
  }
61
62
  export interface CommandConfig {
62
63
  command: string;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @fileoverview Utilities for Claude Code directory and project path handling.
3
+ * Provides functions to get Claude configuration directories respecting the
4
+ * CLAUDE_CONFIG_DIR environment variable and convert worktree paths to Claude's
5
+ * project naming convention.
6
+ */
7
+ /**
8
+ * Get the Claude directory path, respecting CLAUDE_CONFIG_DIR environment variable
9
+ * @returns The Claude directory path
10
+ */
11
+ export declare function getClaudeDir(): string;
12
+ /**
13
+ * Get the Claude projects directory path
14
+ * @returns The Claude projects directory path
15
+ */
16
+ export declare function getClaudeProjectsDir(): string;
17
+ /**
18
+ * Convert a worktree path to Claude's project naming convention
19
+ * @param worktreePath The path to the worktree
20
+ * @returns The project name used by Claude
21
+ */
22
+ export declare function pathToClaudeProjectName(worktreePath: string): string;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * @fileoverview Utilities for Claude Code directory and project path handling.
3
+ * Provides functions to get Claude configuration directories respecting the
4
+ * CLAUDE_CONFIG_DIR environment variable and convert worktree paths to Claude's
5
+ * project naming convention.
6
+ */
7
+ import path from 'path';
8
+ import os from 'os';
9
+ /**
10
+ * Get the Claude directory path, respecting CLAUDE_CONFIG_DIR environment variable
11
+ * @returns The Claude directory path
12
+ */
13
+ export function getClaudeDir() {
14
+ const envConfigDir = process.env['CLAUDE_CONFIG_DIR'];
15
+ if (envConfigDir) {
16
+ return envConfigDir.trim();
17
+ }
18
+ // Default to ~/.claude for backward compatibility and when not set
19
+ return path.join(os.homedir(), '.claude');
20
+ }
21
+ /**
22
+ * Get the Claude projects directory path
23
+ * @returns The Claude projects directory path
24
+ */
25
+ export function getClaudeProjectsDir() {
26
+ return path.join(getClaudeDir(), 'projects');
27
+ }
28
+ /**
29
+ * Convert a worktree path to Claude's project naming convention
30
+ * @param worktreePath The path to the worktree
31
+ * @returns The project name used by Claude
32
+ */
33
+ export function pathToClaudeProjectName(worktreePath) {
34
+ // Convert absolute path to Claude's project naming convention
35
+ // Claude replaces all path separators and dots with dashes
36
+ const resolved = path.resolve(worktreePath);
37
+ // Handle both forward slashes (Linux/macOS) and backslashes (Windows)
38
+ return resolved.replace(/[/\\.]/g, '-');
39
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccmanager",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "TUI application for managing multiple Claude Code sessions across Git worktrees",
5
5
  "license": "MIT",
6
6
  "author": "Kodai Kabasawa",