ccmanager 1.3.1 → 1.4.1

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();
@@ -246,18 +246,12 @@ const ConfigureCommand = ({ onComplete }) => {
246
246
  // In input mode, let TextInput or SelectInput handle it
247
247
  return;
248
248
  }
249
- if (viewMode === 'list' || viewMode === 'edit') {
249
+ if (viewMode === 'list' ||
250
+ viewMode === 'edit' ||
251
+ viewMode === 'delete-confirm') {
250
252
  // SelectInput handles navigation and selection
251
253
  return;
252
254
  }
253
- else if (viewMode === 'delete-confirm') {
254
- if (key.upArrow || key.downArrow) {
255
- setSelectedIndex(prev => (prev === 0 ? 1 : 0));
256
- }
257
- else if (key.return) {
258
- handleDeleteConfirm();
259
- }
260
- }
261
255
  });
262
256
  // Render strategy selection
263
257
  if (isSelectingStrategy) {
@@ -360,6 +354,19 @@ const ConfigureCommand = ({ onComplete }) => {
360
354
  // Render delete confirmation
361
355
  if (viewMode === 'delete-confirm') {
362
356
  const preset = presets.find(p => p.id === selectedPresetId);
357
+ const confirmItems = [
358
+ { label: 'Yes, delete', value: 'yes' },
359
+ { label: 'Cancel', value: 'cancel' },
360
+ ];
361
+ const handleConfirmSelect = (item) => {
362
+ if (item.value === 'yes') {
363
+ handleDeleteConfirm();
364
+ }
365
+ else {
366
+ setViewMode('edit');
367
+ setSelectedIndex(6); // Return to delete option in edit menu
368
+ }
369
+ };
363
370
  return (React.createElement(Box, { flexDirection: "column" },
364
371
  React.createElement(Box, { marginBottom: 1 },
365
372
  React.createElement(Text, { bold: true, color: "red" }, "Confirm Delete")),
@@ -368,17 +375,15 @@ const ConfigureCommand = ({ onComplete }) => {
368
375
  "Delete preset \"",
369
376
  preset?.name,
370
377
  "\"?")),
371
- React.createElement(Box, { flexDirection: "column" },
372
- React.createElement(Box, null,
373
- React.createElement(Text, { color: selectedIndex === 0 ? 'red' : undefined },
374
- selectedIndex === 0 ? '> ' : ' ',
375
- "Yes, delete")),
376
- React.createElement(Box, null,
377
- React.createElement(Text, { color: selectedIndex === 1 ? 'cyan' : undefined },
378
- selectedIndex === 1 ? '> ' : ' ',
379
- "Cancel"))),
378
+ React.createElement(SelectInput, { items: confirmItems, onSelect: handleConfirmSelect, initialIndex: 1, indicatorComponent: ({ isSelected }) => (React.createElement(Text, { color: isSelected ? 'red' : undefined }, isSelected ? '>' : ' ')), itemComponent: ({ isSelected, label }) => (React.createElement(Text, { color: label === 'Yes, delete'
379
+ ? isSelected
380
+ ? 'red'
381
+ : undefined
382
+ : isSelected
383
+ ? 'cyan'
384
+ : undefined, inverse: isSelected }, label)) }),
380
385
  React.createElement(Box, { marginTop: 1 },
381
- React.createElement(Text, { dimColor: true }, "Press \u2191\u2193 to navigate, Enter to confirm"))));
386
+ React.createElement(Text, { dimColor: true }, "Press \u2191\u2193/j/k to navigate, Enter to confirm"))));
382
387
  }
383
388
  // Render edit preset view
384
389
  if (viewMode === 'edit') {
@@ -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,81 +1,61 @@
1
1
  import React, { useState } from 'react';
2
2
  import { Box, Text, useInput } from 'ink';
3
+ import SelectInput from 'ink-select-input';
3
4
  import { shortcutManager } from '../services/shortcutManager.js';
4
5
  const DeleteConfirmation = ({ worktrees, onConfirm, onCancel, }) => {
5
6
  // Check if any worktrees have branches
6
7
  const hasAnyBranches = worktrees.some(wt => wt.branch);
7
8
  const [deleteBranch, setDeleteBranch] = useState(true);
8
- const [focusedOption, setFocusedOption] = useState(hasAnyBranches ? 'deleteBranch' : 'confirm');
9
- // Helper functions for navigation
10
- const isRadioOption = (option) => option === 'deleteBranch' || option === 'keepBranch';
11
- const isActionButton = (option) => option === 'confirm' || option === 'cancel';
12
- const handleUpArrow = () => {
13
- if (!hasAnyBranches) {
14
- if (focusedOption === 'cancel')
15
- setFocusedOption('confirm');
16
- return;
17
- }
18
- const navigationMap = {
19
- keepBranch: 'deleteBranch',
20
- confirm: 'keepBranch',
21
- cancel: 'keepBranch',
22
- };
23
- const next = navigationMap[focusedOption];
24
- if (next)
25
- setFocusedOption(next);
26
- };
27
- const handleDownArrow = () => {
28
- if (!hasAnyBranches) {
29
- if (focusedOption === 'confirm')
30
- setFocusedOption('cancel');
31
- return;
32
- }
33
- const navigationMap = {
34
- deleteBranch: 'keepBranch',
35
- keepBranch: 'confirm',
36
- confirm: 'cancel',
37
- };
38
- const next = navigationMap[focusedOption];
39
- if (next)
40
- setFocusedOption(next);
41
- };
42
- const handleHorizontalArrow = (direction) => {
43
- if (isActionButton(focusedOption)) {
44
- setFocusedOption(direction === 'left' ? 'confirm' : 'cancel');
45
- }
9
+ const [view, setView] = useState(hasAnyBranches ? 'options' : 'confirm');
10
+ const [focusedOption, setFocusedOption] = useState(deleteBranch ? 'deleteBranch' : 'keepBranch');
11
+ // Menu items for branch options
12
+ const branchOptions = [
13
+ {
14
+ label: `${deleteBranch ? '()' : '( )'} Delete the branches too`,
15
+ value: 'deleteBranch',
16
+ },
17
+ {
18
+ label: `${!deleteBranch ? '(•)' : '( )'} Keep the branches`,
19
+ value: 'keepBranch',
20
+ },
21
+ ];
22
+ // Menu items for actions
23
+ const actionOptions = [
24
+ { label: 'Confirm', value: 'confirm' },
25
+ { label: 'Cancel', value: 'cancel' },
26
+ ];
27
+ const handleBranchSelect = (item) => {
28
+ // Don't toggle on Enter - only update focused option
29
+ setFocusedOption(item.value);
46
30
  };
47
- const handleSelect = () => {
48
- if (isRadioOption(focusedOption)) {
49
- setDeleteBranch(focusedOption === 'deleteBranch');
50
- }
51
- else if (focusedOption === 'confirm') {
31
+ const handleActionSelect = (item) => {
32
+ if (item.value === 'confirm') {
52
33
  onConfirm(deleteBranch);
53
34
  }
54
- else if (focusedOption === 'cancel') {
35
+ else {
55
36
  onCancel();
56
37
  }
57
38
  };
58
39
  useInput((input, key) => {
59
- if (key.upArrow) {
60
- handleUpArrow();
61
- }
62
- else if (key.downArrow) {
63
- handleDownArrow();
64
- }
65
- else if (key.leftArrow) {
66
- handleHorizontalArrow('left');
67
- }
68
- else if (key.rightArrow) {
69
- handleHorizontalArrow('right');
40
+ if (shortcutManager.matchesShortcut('cancel', input, key)) {
41
+ onCancel();
70
42
  }
71
- else if (input === ' ' && isRadioOption(focusedOption)) {
72
- setDeleteBranch(focusedOption === 'deleteBranch');
43
+ else if (hasAnyBranches && view === 'options' && key.return) {
44
+ // Move to confirm view when Enter is pressed in options
45
+ setView('confirm');
73
46
  }
74
- else if (key.return) {
75
- handleSelect();
47
+ else if (hasAnyBranches && view === 'confirm' && key.escape) {
48
+ // Go back to options when Escape is pressed in confirm
49
+ setView('options');
76
50
  }
77
- else if (shortcutManager.matchesShortcut('cancel', input, key)) {
78
- onCancel();
51
+ else if (hasAnyBranches && view === 'options' && input === ' ') {
52
+ // Toggle selection on space for radio buttons
53
+ if (focusedOption === 'deleteBranch') {
54
+ setDeleteBranch(true);
55
+ }
56
+ else {
57
+ setDeleteBranch(false);
58
+ }
79
59
  }
80
60
  });
81
61
  return (React.createElement(Box, { flexDirection: "column" },
@@ -101,33 +81,38 @@ const DeleteConfirmation = ({ worktrees, onConfirm, onCancel, }) => {
101
81
  "... and ",
102
82
  worktrees.length - 8,
103
83
  " more worktrees")))),
104
- hasAnyBranches && (React.createElement(Box, { marginBottom: 1, flexDirection: "column" },
84
+ hasAnyBranches && view === 'options' && (React.createElement(Box, { marginBottom: 1, flexDirection: "column" },
105
85
  React.createElement(Text, { bold: true }, "What do you want to do with the associated branches?"),
106
- React.createElement(Box, { marginTop: 1, flexDirection: "column" },
107
- React.createElement(Box, null,
108
- React.createElement(Text, { color: focusedOption === 'deleteBranch' ? 'red' : undefined, inverse: focusedOption === 'deleteBranch' },
109
- deleteBranch ? '()' : '( )',
110
- " Delete the branches too")),
111
- React.createElement(Box, null,
112
- React.createElement(Text, { color: focusedOption === 'keepBranch' ? 'green' : undefined, inverse: focusedOption === 'keepBranch' },
113
- !deleteBranch ? '(•)' : '( )',
114
- " Keep the branches"))))),
115
- React.createElement(Box, { marginTop: 1 },
116
- React.createElement(Box, { marginRight: 2 },
117
- React.createElement(Text, { color: focusedOption === 'confirm' ? 'green' : 'white', inverse: focusedOption === 'confirm' },
118
- ' ',
119
- "Confirm",
120
- ' ')),
121
- React.createElement(Box, null,
122
- React.createElement(Text, { color: focusedOption === 'cancel' ? 'red' : 'white', inverse: focusedOption === 'cancel' },
123
- ' ',
124
- "Cancel",
125
- ' '))),
86
+ React.createElement(Box, { marginTop: 1 },
87
+ React.createElement(SelectInput, { items: branchOptions, onSelect: handleBranchSelect, onHighlight: (item) => {
88
+ setFocusedOption(item.value);
89
+ }, initialIndex: deleteBranch ? 0 : 1, indicatorComponent: ({ isSelected }) => (React.createElement(Text, { color: isSelected ? 'red' : undefined }, isSelected ? '>' : ' ')), itemComponent: ({ isSelected, label }) => (React.createElement(Text, { color: isSelected ? 'red' : undefined, inverse: isSelected }, label)) })))),
90
+ hasAnyBranches && view === 'confirm' && (React.createElement(Box, { marginBottom: 1, flexDirection: "column" },
91
+ React.createElement(Text, { bold: true }, "Branch option selected:"),
92
+ React.createElement(Text, { color: "yellow" }, deleteBranch ? '✓ Delete the branches too' : '✓ Keep the branches'))),
93
+ (view === 'confirm' || !hasAnyBranches) && (React.createElement(Box, { marginTop: 1 },
94
+ React.createElement(SelectInput, { items: actionOptions, onSelect: handleActionSelect, initialIndex: 1, indicatorComponent: ({ isSelected }) => (React.createElement(Text, null, isSelected ? '>' : ' ')), itemComponent: ({ isSelected, label }) => {
95
+ const color = label === 'Confirm' ? 'green' : 'red';
96
+ return (React.createElement(Text, { color: isSelected ? color : 'white', inverse: isSelected },
97
+ ' ',
98
+ label,
99
+ ' '));
100
+ } }))),
126
101
  React.createElement(Box, { marginTop: 1 },
127
- React.createElement(Text, { dimColor: true },
128
- "Use \u2191\u2193 to navigate options, Space/Enter to select,",
102
+ React.createElement(Text, { dimColor: true }, hasAnyBranches && view === 'options' ? (React.createElement(React.Fragment, null,
103
+ "Use \u2191\u2193/j/k to navigate, Space to toggle, Enter to continue,",
104
+ ' ',
105
+ shortcutManager.getShortcutDisplay('cancel'),
106
+ " to cancel")) : view === 'confirm' ? (React.createElement(React.Fragment, null,
107
+ "Use \u2191\u2193/j/k to navigate, Enter to select",
108
+ hasAnyBranches ? ', Esc to go back' : '',
109
+ ",",
110
+ ' ',
111
+ shortcutManager.getShortcutDisplay('cancel'),
112
+ " to cancel")) : (React.createElement(React.Fragment, null,
113
+ "Use \u2191\u2193/j/k to navigate, Enter to select,",
129
114
  ' ',
130
115
  shortcutManager.getShortcutDisplay('cancel'),
131
- " to cancel"))));
116
+ " to cancel"))))));
132
117
  };
133
118
  export default DeleteConfirmation;
@@ -1,14 +1,14 @@
1
1
  import React, { useState, useEffect } from 'react';
2
2
  import { Box, Text, useInput } from 'ink';
3
+ import SelectInput from 'ink-select-input';
3
4
  import { WorktreeService } from '../services/worktreeService.js';
4
5
  import DeleteConfirmation from './DeleteConfirmation.js';
5
6
  import { shortcutManager } from '../services/shortcutManager.js';
6
7
  const DeleteWorktree = ({ onComplete, onCancel, }) => {
7
8
  const [worktrees, setWorktrees] = useState([]);
8
9
  const [selectedIndices, setSelectedIndices] = useState(new Set());
9
- const [focusedIndex, setFocusedIndex] = useState(0);
10
10
  const [confirmMode, setConfirmMode] = useState(false);
11
- const VIEWPORT_SIZE = 10; // Maximum number of items to display at once
11
+ const [focusedIndex, setFocusedIndex] = useState(0);
12
12
  useEffect(() => {
13
13
  const worktreeService = new WorktreeService();
14
14
  const allWorktrees = worktreeService.getWorktrees();
@@ -16,23 +16,30 @@ const DeleteWorktree = ({ onComplete, onCancel, }) => {
16
16
  const deletableWorktrees = allWorktrees.filter(wt => !wt.isMainWorktree);
17
17
  setWorktrees(deletableWorktrees);
18
18
  }, []);
19
+ // Create menu items from worktrees
20
+ const menuItems = worktrees.map((worktree, index) => {
21
+ const branchName = worktree.branch
22
+ ? worktree.branch.replace('refs/heads/', '')
23
+ : 'detached';
24
+ const isSelected = selectedIndices.has(index);
25
+ return {
26
+ label: `${isSelected ? '[✓]' : '[ ]'} ${branchName} (${worktree.path})`,
27
+ value: index.toString(),
28
+ };
29
+ });
30
+ const handleSelect = (item) => {
31
+ // Don't toggle on Enter - this will be used to confirm
32
+ // We'll handle Space key separately for toggling
33
+ const index = parseInt(item.value, 10);
34
+ setFocusedIndex(index);
35
+ };
19
36
  useInput((input, key) => {
20
- if (key.ctrl && input === 'c') {
21
- onCancel();
22
- return;
23
- }
24
37
  if (confirmMode) {
25
38
  // Confirmation component handles input
26
39
  return;
27
40
  }
28
- if (key.upArrow) {
29
- setFocusedIndex(prev => Math.max(0, prev - 1));
30
- }
31
- else if (key.downArrow) {
32
- setFocusedIndex(prev => Math.min(worktrees.length - 1, prev + 1));
33
- }
34
- else if (input === ' ') {
35
- // Toggle selection
41
+ if (input === ' ') {
42
+ // Toggle selection on space
36
43
  setSelectedIndices(prev => {
37
44
  const newSet = new Set(prev);
38
45
  if (newSet.has(focusedIndex)) {
@@ -44,10 +51,8 @@ const DeleteWorktree = ({ onComplete, onCancel, }) => {
44
51
  return newSet;
45
52
  });
46
53
  }
47
- else if (key.return) {
48
- if (selectedIndices.size > 0) {
49
- setConfirmMode(true);
50
- }
54
+ else if (key.return && selectedIndices.size > 0) {
55
+ setConfirmMode(true);
51
56
  }
52
57
  else if (shortcutManager.matchesShortcut('cancel', input, key)) {
53
58
  onCancel();
@@ -77,40 +82,17 @@ const DeleteWorktree = ({ onComplete, onCancel, }) => {
77
82
  React.createElement(Text, { bold: true, color: "red" }, "Delete Worktrees")),
78
83
  React.createElement(Box, { marginBottom: 1 },
79
84
  React.createElement(Text, { dimColor: true }, "Select worktrees to delete (Space to select, Enter to confirm):")),
80
- (() => {
81
- // Calculate viewport window
82
- const viewportStart = Math.max(0, Math.min(focusedIndex - Math.floor(VIEWPORT_SIZE / 2), worktrees.length - VIEWPORT_SIZE));
83
- const viewportEnd = Math.min(viewportStart + VIEWPORT_SIZE, worktrees.length);
84
- const visibleWorktrees = worktrees.slice(viewportStart, viewportEnd);
85
- return (React.createElement(React.Fragment, null,
86
- viewportStart > 0 && (React.createElement(Text, { dimColor: true },
87
- "\u2191 ",
88
- viewportStart,
89
- " more...")),
90
- visibleWorktrees.map((worktree, relativeIndex) => {
91
- const actualIndex = viewportStart + relativeIndex;
92
- const isSelected = selectedIndices.has(actualIndex);
93
- const isFocused = actualIndex === focusedIndex;
94
- const branchName = worktree.branch
95
- ? worktree.branch.replace('refs/heads/', '')
96
- : 'detached';
97
- return (React.createElement(Box, { key: worktree.path },
98
- React.createElement(Text, { color: isFocused ? 'green' : undefined, inverse: isFocused, dimColor: !isFocused && !isSelected },
99
- isSelected ? '[✓]' : '[ ]',
100
- " ",
101
- branchName,
102
- " (",
103
- worktree.path,
104
- ")")));
105
- }),
106
- viewportEnd < worktrees.length && (React.createElement(Text, { dimColor: true },
107
- "\u2193 ",
108
- worktrees.length - viewportEnd,
109
- " more..."))));
110
- })(),
85
+ React.createElement(SelectInput, { items: menuItems, onSelect: handleSelect, onHighlight: (item) => {
86
+ const index = parseInt(item.value, 10);
87
+ setFocusedIndex(index);
88
+ }, limit: 10, indicatorComponent: ({ isSelected }) => (React.createElement(Text, { color: isSelected ? 'green' : undefined }, isSelected ? '>' : ' ')), itemComponent: ({ isSelected, label }) => {
89
+ // Check if this item is actually selected (checkbox checked)
90
+ const hasCheckmark = label.includes('[✓]');
91
+ return (React.createElement(Text, { color: isSelected ? 'green' : undefined, inverse: isSelected, dimColor: !isSelected && !hasCheckmark }, label));
92
+ } }),
111
93
  React.createElement(Box, { marginTop: 1, flexDirection: "column" },
112
94
  React.createElement(Text, { dimColor: true },
113
- "Controls: \u2191\u2193 Navigate, Space Select, Enter Confirm,",
95
+ "Controls: \u2191\u2193/j/k Navigate, Space Select, Enter Confirm,",
114
96
  ' ',
115
97
  shortcutManager.getShortcutDisplay('cancel'),
116
98
  " Cancel"),
@@ -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',
@@ -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.1",
3
+ "version": "1.4.1",
4
4
  "description": "TUI application for managing multiple Claude Code sessions across Git worktrees",
5
5
  "license": "MIT",
6
6
  "author": "Kodai Kabasawa",