ccmanager 2.0.0 → 2.2.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
@@ -205,6 +205,24 @@ Status hooks allow you to:
205
205
 
206
206
  For detailed setup instructions, see [docs/state-hooks.md](docs/status-hooks.md).
207
207
 
208
+ ## Worktree Hooks
209
+
210
+ Worktree hooks execute custom commands when worktrees are created, enabling automation of development environment setup.
211
+
212
+ ### Features
213
+ - **Post-creation hook**: Run commands after a worktree is created
214
+ - **Environment variables**: Access worktree path, branch name, and git root
215
+ - **Non-blocking execution**: Hooks run asynchronously without delaying operations
216
+ - **Error resilience**: Hook failures don't prevent worktree creation
217
+
218
+ ### Use Cases
219
+ - Set up development dependencies (`npm install`, `bundle install`)
220
+ - Configure IDE settings per branch
221
+ - Send notifications when worktrees are created
222
+ - Initialize branch-specific configurations
223
+
224
+ For configuration and examples, see [docs/worktree-hooks.md](docs/worktree-hooks.md).
225
+
208
226
  ## Automatic Worktree Directory Generation
209
227
 
210
228
  CCManager can automatically generate worktree directory paths based on branch names, streamlining the worktree creation process.
@@ -183,7 +183,7 @@ const App = ({ devcontainerConfig, multiProject }) => {
183
183
  setView('creating-worktree');
184
184
  setError(null);
185
185
  // Create the worktree
186
- const result = worktreeService.createWorktree(path, branch, baseBranch, copySessionData, copyClaudeDirectory);
186
+ const result = await worktreeService.createWorktree(path, branch, baseBranch, copySessionData, copyClaudeDirectory);
187
187
  if (result.success) {
188
188
  // Success - return to menu
189
189
  handleReturnToMenu();
@@ -2,7 +2,8 @@ import React, { useState } from 'react';
2
2
  import { Box, Text, useInput } from 'ink';
3
3
  import SelectInput from 'ink-select-input';
4
4
  import ConfigureShortcuts from './ConfigureShortcuts.js';
5
- import ConfigureHooks from './ConfigureHooks.js';
5
+ import ConfigureStatusHooks from './ConfigureStatusHooks.js';
6
+ import ConfigureWorktreeHooks from './ConfigureWorktreeHooks.js';
6
7
  import ConfigureWorktree from './ConfigureWorktree.js';
7
8
  import ConfigureCommand from './ConfigureCommand.js';
8
9
  import { shortcutManager } from '../services/shortcutManager.js';
@@ -15,7 +16,11 @@ const Configuration = ({ onComplete }) => {
15
16
  },
16
17
  {
17
18
  label: 'H 🔧 Configure Status Hooks',
18
- value: 'hooks',
19
+ value: 'statusHooks',
20
+ },
21
+ {
22
+ label: 'T 🔨 Configure Worktree Hooks',
23
+ value: 'worktreeHooks',
19
24
  },
20
25
  {
21
26
  label: 'W 📁 Configure Worktree Settings',
@@ -37,8 +42,11 @@ const Configuration = ({ onComplete }) => {
37
42
  else if (item.value === 'shortcuts') {
38
43
  setView('shortcuts');
39
44
  }
40
- else if (item.value === 'hooks') {
41
- setView('hooks');
45
+ else if (item.value === 'statusHooks') {
46
+ setView('statusHooks');
47
+ }
48
+ else if (item.value === 'worktreeHooks') {
49
+ setView('worktreeHooks');
42
50
  }
43
51
  else if (item.value === 'worktree') {
44
52
  setView('worktree');
@@ -60,7 +68,10 @@ const Configuration = ({ onComplete }) => {
60
68
  setView('shortcuts');
61
69
  break;
62
70
  case 'h':
63
- setView('hooks');
71
+ setView('statusHooks');
72
+ break;
73
+ case 't':
74
+ setView('worktreeHooks');
64
75
  break;
65
76
  case 'w':
66
77
  setView('worktree');
@@ -80,8 +91,11 @@ const Configuration = ({ onComplete }) => {
80
91
  if (view === 'shortcuts') {
81
92
  return React.createElement(ConfigureShortcuts, { onComplete: handleSubMenuComplete });
82
93
  }
83
- if (view === 'hooks') {
84
- return React.createElement(ConfigureHooks, { onComplete: handleSubMenuComplete });
94
+ if (view === 'statusHooks') {
95
+ return React.createElement(ConfigureStatusHooks, { onComplete: handleSubMenuComplete });
96
+ }
97
+ if (view === 'worktreeHooks') {
98
+ return React.createElement(ConfigureWorktreeHooks, { onComplete: handleSubMenuComplete });
85
99
  }
86
100
  if (view === 'worktree') {
87
101
  return React.createElement(ConfigureWorktree, { onComplete: handleSubMenuComplete });
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ interface ConfigureStatusHooksProps {
3
+ onComplete: () => void;
4
+ }
5
+ declare const ConfigureStatusHooks: React.FC<ConfigureStatusHooksProps>;
6
+ export default ConfigureStatusHooks;
@@ -1,4 +1,4 @@
1
- import React, { useState, useEffect } from 'react';
1
+ import React, { useState } from 'react';
2
2
  import { Box, Text, useInput } from 'ink';
3
3
  import TextInputWrapper from './TextInputWrapper.js';
4
4
  import SelectInput from 'ink-select-input';
@@ -8,16 +8,13 @@ const STATUS_LABELS = {
8
8
  busy: 'Busy',
9
9
  waiting_input: 'Waiting for Input',
10
10
  };
11
- const ConfigureHooks = ({ onComplete }) => {
11
+ const ConfigureStatusHooks = ({ onComplete, }) => {
12
12
  const [view, setView] = useState('menu');
13
13
  const [selectedStatus, setSelectedStatus] = useState('idle');
14
- const [hooks, setHooks] = useState({});
14
+ const [statusHooks, setStatusHooks] = useState(configurationManager.getStatusHooks());
15
15
  const [currentCommand, setCurrentCommand] = useState('');
16
16
  const [currentEnabled, setCurrentEnabled] = useState(false);
17
17
  const [showSaveMessage, setShowSaveMessage] = useState(false);
18
- useEffect(() => {
19
- setHooks(configurationManager.getStatusHooks());
20
- }, []);
21
18
  useInput((input, key) => {
22
19
  if (key.escape) {
23
20
  if (view === 'edit') {
@@ -35,16 +32,16 @@ const ConfigureHooks = ({ onComplete }) => {
35
32
  const items = [];
36
33
  // Add status hook items
37
34
  ['idle', 'busy', 'waiting_input'].forEach(status => {
38
- const hook = hooks[status];
35
+ const hook = statusHooks[status];
39
36
  const enabled = hook?.enabled ? '✓' : '✗';
40
37
  const command = hook?.command || '(not set)';
41
38
  items.push({
42
39
  label: `${STATUS_LABELS[status]}: ${enabled} ${command}`,
43
- value: status,
40
+ value: `status:${status}`,
44
41
  });
45
42
  });
46
43
  items.push({
47
- label: '─────────────',
44
+ label: '',
48
45
  value: 'separator',
49
46
  });
50
47
  items.push({
@@ -59,7 +56,7 @@ const ConfigureHooks = ({ onComplete }) => {
59
56
  };
60
57
  const handleMenuSelect = (item) => {
61
58
  if (item.value === 'save') {
62
- configurationManager.setStatusHooks(hooks);
59
+ configurationManager.setStatusHooks(statusHooks);
63
60
  setShowSaveMessage(true);
64
61
  setTimeout(() => {
65
62
  onComplete();
@@ -68,17 +65,18 @@ const ConfigureHooks = ({ onComplete }) => {
68
65
  else if (item.value === 'cancel') {
69
66
  onComplete();
70
67
  }
71
- else if (item.value !== 'separator') {
72
- const status = item.value;
68
+ else if (!item.value.includes('separator') &&
69
+ item.value.startsWith('status:')) {
70
+ const status = item.value.split(':')[1];
73
71
  setSelectedStatus(status);
74
- const hook = hooks[status];
72
+ const hook = statusHooks[status];
75
73
  setCurrentCommand(hook?.command || '');
76
- setCurrentEnabled(hook?.enabled ?? true); // Default to true if not set
74
+ setCurrentEnabled(hook?.enabled ?? true);
77
75
  setView('edit');
78
76
  }
79
77
  };
80
78
  const handleCommandSubmit = (value) => {
81
- setHooks(prev => ({
79
+ setStatusHooks(prev => ({
82
80
  ...prev,
83
81
  [selectedStatus]: {
84
82
  command: value,
@@ -123,11 +121,11 @@ const ConfigureHooks = ({ onComplete }) => {
123
121
  }
124
122
  return (React.createElement(Box, { flexDirection: "column" },
125
123
  React.createElement(Box, { marginBottom: 1 },
126
- React.createElement(Text, { bold: true, color: "green" }, "Configure Status Change Hooks")),
124
+ React.createElement(Text, { bold: true, color: "green" }, "Configure Status Hooks")),
127
125
  React.createElement(Box, { marginBottom: 1 },
128
- React.createElement(Text, { dimColor: true }, "Set commands to run when Claude Code session status changes:")),
126
+ React.createElement(Text, { dimColor: true }, "Set commands to run when session status changes:")),
129
127
  React.createElement(SelectInput, { items: getMenuItems(), onSelect: handleMenuSelect, isFocused: true, limit: 10 }),
130
128
  React.createElement(Box, { marginTop: 1 },
131
129
  React.createElement(Text, { dimColor: true }, "Press Esc to go back"))));
132
130
  };
133
- export default ConfigureHooks;
131
+ export default ConfigureStatusHooks;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,62 @@
1
+ import React from 'react';
2
+ import { render } from 'ink-testing-library';
3
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
4
+ import ConfigureStatusHooks from './ConfigureStatusHooks.js';
5
+ import { configurationManager } from '../services/configurationManager.js';
6
+ // Mock ink to avoid stdin issues
7
+ vi.mock('ink', async () => {
8
+ const actual = await vi.importActual('ink');
9
+ return {
10
+ ...actual,
11
+ useInput: vi.fn(),
12
+ };
13
+ });
14
+ // Mock SelectInput to render items as simple text
15
+ vi.mock('ink-select-input', async () => {
16
+ const React = await vi.importActual('react');
17
+ const { Text, Box } = await vi.importActual('ink');
18
+ return {
19
+ default: ({ items }) => {
20
+ return React.createElement(Box, { flexDirection: 'column' }, items.map((item, index) => React.createElement(Text, { key: index }, item.label)));
21
+ },
22
+ };
23
+ });
24
+ vi.mock('../services/configurationManager.js', () => ({
25
+ configurationManager: {
26
+ getStatusHooks: vi.fn(),
27
+ setStatusHooks: vi.fn(),
28
+ },
29
+ }));
30
+ const mockedConfigurationManager = configurationManager;
31
+ describe('ConfigureStatusHooks', () => {
32
+ beforeEach(() => {
33
+ vi.clearAllMocks();
34
+ });
35
+ it('should render status hooks configuration screen', () => {
36
+ mockedConfigurationManager.getStatusHooks.mockReturnValue({});
37
+ const onComplete = vi.fn();
38
+ const { lastFrame } = render(React.createElement(ConfigureStatusHooks, { onComplete: onComplete }));
39
+ expect(lastFrame()).toContain('Configure Status Hooks');
40
+ expect(lastFrame()).toContain('Set commands to run when session status changes');
41
+ expect(lastFrame()).toContain('Idle:');
42
+ expect(lastFrame()).toContain('Busy:');
43
+ expect(lastFrame()).toContain('Waiting for Input:');
44
+ });
45
+ it('should display configured hooks', () => {
46
+ mockedConfigurationManager.getStatusHooks.mockReturnValue({
47
+ idle: {
48
+ command: 'notify-send "Idle"',
49
+ enabled: true,
50
+ },
51
+ busy: {
52
+ command: 'echo "Busy"',
53
+ enabled: false,
54
+ },
55
+ });
56
+ const onComplete = vi.fn();
57
+ const { lastFrame } = render(React.createElement(ConfigureStatusHooks, { onComplete: onComplete }));
58
+ expect(lastFrame()).toContain('Idle: ✓ notify-send "Idle"');
59
+ expect(lastFrame()).toContain('Busy: ✗ echo "Busy"');
60
+ expect(lastFrame()).toContain('Waiting for Input: ✗ (not set)');
61
+ });
62
+ });
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ interface ConfigureWorktreeHooksProps {
3
+ onComplete: () => void;
4
+ }
5
+ declare const ConfigureWorktreeHooks: React.FC<ConfigureWorktreeHooksProps>;
6
+ export default ConfigureWorktreeHooks;
@@ -0,0 +1,114 @@
1
+ import React, { useState } from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+ import TextInputWrapper from './TextInputWrapper.js';
4
+ import SelectInput from 'ink-select-input';
5
+ import { configurationManager } from '../services/configurationManager.js';
6
+ const ConfigureWorktreeHooks = ({ onComplete, }) => {
7
+ const [view, setView] = useState('menu');
8
+ const [worktreeHooks, setWorktreeHooks] = useState(configurationManager.getWorktreeHooks());
9
+ const [currentCommand, setCurrentCommand] = useState('');
10
+ const [currentEnabled, setCurrentEnabled] = useState(false);
11
+ const [showSaveMessage, setShowSaveMessage] = useState(false);
12
+ useInput((input, key) => {
13
+ if (key.escape) {
14
+ if (view === 'edit') {
15
+ setView('menu');
16
+ }
17
+ else {
18
+ onComplete();
19
+ }
20
+ }
21
+ else if (key.tab && view === 'edit') {
22
+ toggleEnabled();
23
+ }
24
+ });
25
+ const getMenuItems = () => {
26
+ const items = [];
27
+ // Add worktree hook items
28
+ const postCreationHook = worktreeHooks.post_creation;
29
+ const postCreationEnabled = postCreationHook?.enabled ? '✓' : '✗';
30
+ const postCreationCommand = postCreationHook?.command || '(not set)';
31
+ items.push({
32
+ label: `Post Creation: ${postCreationEnabled} ${postCreationCommand}`,
33
+ value: 'worktree:post_creation',
34
+ });
35
+ items.push({
36
+ label: '',
37
+ value: 'separator',
38
+ });
39
+ items.push({
40
+ label: '💾 Save and Return',
41
+ value: 'save',
42
+ });
43
+ items.push({
44
+ label: '← Cancel',
45
+ value: 'cancel',
46
+ });
47
+ return items;
48
+ };
49
+ const handleMenuSelect = (item) => {
50
+ if (item.value === 'save') {
51
+ configurationManager.setWorktreeHooks(worktreeHooks);
52
+ setShowSaveMessage(true);
53
+ setTimeout(() => {
54
+ onComplete();
55
+ }, 1000);
56
+ }
57
+ else if (item.value === 'cancel') {
58
+ onComplete();
59
+ }
60
+ else if (!item.value.includes('separator') &&
61
+ item.value === 'worktree:post_creation') {
62
+ const hook = worktreeHooks.post_creation;
63
+ setCurrentCommand(hook?.command || '');
64
+ setCurrentEnabled(hook?.enabled ?? true);
65
+ setView('edit');
66
+ }
67
+ };
68
+ const handleCommandSubmit = (value) => {
69
+ setWorktreeHooks(prev => ({
70
+ ...prev,
71
+ post_creation: {
72
+ command: value,
73
+ enabled: currentEnabled,
74
+ },
75
+ }));
76
+ setView('menu');
77
+ };
78
+ const toggleEnabled = () => {
79
+ setCurrentEnabled(prev => !prev);
80
+ };
81
+ if (showSaveMessage) {
82
+ return (React.createElement(Box, { flexDirection: "column" },
83
+ React.createElement(Text, { color: "green" }, "\u2713 Configuration saved successfully!")));
84
+ }
85
+ if (view === 'edit') {
86
+ return (React.createElement(Box, { flexDirection: "column" },
87
+ React.createElement(Box, { marginBottom: 1 },
88
+ React.createElement(Text, { bold: true, color: "green" }, "Configure Post Worktree Creation Hook")),
89
+ React.createElement(Box, { marginBottom: 1 },
90
+ React.createElement(Text, null, "Command to execute after creating a new worktree:")),
91
+ React.createElement(Box, { marginBottom: 1 },
92
+ React.createElement(TextInputWrapper, { value: currentCommand, onChange: setCurrentCommand, onSubmit: handleCommandSubmit, placeholder: "Enter command (e.g., npm install && npm run build)" })),
93
+ React.createElement(Box, { marginBottom: 1 },
94
+ React.createElement(Text, null,
95
+ "Enabled: ",
96
+ currentEnabled ? '✓' : '✗',
97
+ " (Press Tab to toggle)")),
98
+ React.createElement(Box, { marginTop: 1 },
99
+ React.createElement(Text, { dimColor: true }, "Environment variables available: CCMANAGER_WORKTREE, CCMANAGER_WORKTREE_BRANCH,")),
100
+ React.createElement(Box, null,
101
+ React.createElement(Text, { dimColor: true }, "CCMANAGER_BASE_BRANCH, CCMANAGER_GIT_ROOT")),
102
+ React.createElement(Box, { marginTop: 1 },
103
+ React.createElement(Text, { dimColor: true }, "Press Enter to save, Tab to toggle enabled, Esc to cancel"))));
104
+ }
105
+ return (React.createElement(Box, { flexDirection: "column" },
106
+ React.createElement(Box, { marginBottom: 1 },
107
+ React.createElement(Text, { bold: true, color: "green" }, "Configure Worktree Hooks")),
108
+ React.createElement(Box, { marginBottom: 1 },
109
+ React.createElement(Text, { dimColor: true }, "Set commands to run on worktree events:")),
110
+ React.createElement(SelectInput, { items: getMenuItems(), onSelect: handleMenuSelect, isFocused: true, limit: 10 }),
111
+ React.createElement(Box, { marginTop: 1 },
112
+ React.createElement(Text, { dimColor: true }, "Press Esc to go back"))));
113
+ };
114
+ export default ConfigureWorktreeHooks;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,60 @@
1
+ import React from 'react';
2
+ import { render } from 'ink-testing-library';
3
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
4
+ import ConfigureWorktreeHooks from './ConfigureWorktreeHooks.js';
5
+ import { configurationManager } from '../services/configurationManager.js';
6
+ // Mock ink to avoid stdin issues
7
+ vi.mock('ink', async () => {
8
+ const actual = await vi.importActual('ink');
9
+ return {
10
+ ...actual,
11
+ useInput: vi.fn(),
12
+ };
13
+ });
14
+ // Mock SelectInput to render items as simple text
15
+ vi.mock('ink-select-input', async () => {
16
+ const React = await vi.importActual('react');
17
+ const { Text, Box } = await vi.importActual('ink');
18
+ return {
19
+ default: ({ items }) => {
20
+ return React.createElement(Box, { flexDirection: 'column' }, items.map((item, index) => React.createElement(Text, { key: index }, item.label)));
21
+ },
22
+ };
23
+ });
24
+ vi.mock('../services/configurationManager.js', () => ({
25
+ configurationManager: {
26
+ getWorktreeHooks: vi.fn(),
27
+ setWorktreeHooks: vi.fn(),
28
+ },
29
+ }));
30
+ const mockedConfigurationManager = configurationManager;
31
+ describe('ConfigureWorktreeHooks', () => {
32
+ beforeEach(() => {
33
+ vi.clearAllMocks();
34
+ });
35
+ it('should render worktree hooks configuration screen', () => {
36
+ mockedConfigurationManager.getWorktreeHooks.mockReturnValue({});
37
+ const onComplete = vi.fn();
38
+ const { lastFrame } = render(React.createElement(ConfigureWorktreeHooks, { onComplete: onComplete }));
39
+ expect(lastFrame()).toContain('Configure Worktree Hooks');
40
+ expect(lastFrame()).toContain('Set commands to run on worktree events');
41
+ expect(lastFrame()).toContain('Post Creation:');
42
+ });
43
+ it('should display configured hooks', () => {
44
+ mockedConfigurationManager.getWorktreeHooks.mockReturnValue({
45
+ post_creation: {
46
+ command: 'npm install',
47
+ enabled: true,
48
+ },
49
+ });
50
+ const onComplete = vi.fn();
51
+ const { lastFrame } = render(React.createElement(ConfigureWorktreeHooks, { onComplete: onComplete }));
52
+ expect(lastFrame()).toContain('Post Creation: ✓ npm install');
53
+ });
54
+ it('should display not set when no hook configured', () => {
55
+ mockedConfigurationManager.getWorktreeHooks.mockReturnValue({});
56
+ const onComplete = vi.fn();
57
+ const { lastFrame } = render(React.createElement(ConfigureWorktreeHooks, { onComplete: onComplete }));
58
+ expect(lastFrame()).toContain('Post Creation: ✗ (not set)');
59
+ });
60
+ });
@@ -24,6 +24,7 @@ const Menu = ({ sessionManager, worktreeService, onSelectWorktree, onSelectRecen
24
24
  const [sessions, setSessions] = useState([]);
25
25
  const [items, setItems] = useState([]);
26
26
  const [recentProjects, setRecentProjects] = useState([]);
27
+ const limit = 10;
27
28
  // Use the search mode hook
28
29
  const { isSearchMode, searchQuery, selectedIndex, setSearchQuery } = useSearchMode(items.length, {
29
30
  isDisabled: !!error,
@@ -374,9 +375,9 @@ const Menu = ({ sessionManager, worktreeService, onSelectWorktree, onSelectRecen
374
375
  isSearchMode && items.length === 0 ? (React.createElement(Box, null,
375
376
  React.createElement(Text, { color: "yellow" }, "No worktrees match your search"))) : isSearchMode ? (
376
377
  // In search mode, show the items as a list without SelectInput
377
- React.createElement(Box, { flexDirection: "column" }, items.map((item, index) => (React.createElement(Text, { key: item.value, color: index === selectedIndex ? 'green' : undefined },
378
+ React.createElement(Box, { flexDirection: "column" }, items.slice(0, limit).map((item, index) => (React.createElement(Text, { key: item.value, color: index === selectedIndex ? 'green' : undefined },
378
379
  index === selectedIndex ? '❯ ' : ' ',
379
- item.label))))) : (React.createElement(SelectInput, { items: items, onSelect: item => handleSelect(item), isFocused: !error, initialIndex: selectedIndex })),
380
+ item.label))))) : (React.createElement(SelectInput, { items: items, onSelect: item => handleSelect(item), isFocused: !error, initialIndex: selectedIndex, limit: limit })),
380
381
  error && (React.createElement(Box, { marginTop: 1, paddingX: 1, borderStyle: "round", borderColor: "red" },
381
382
  React.createElement(Box, { flexDirection: "column" },
382
383
  React.createElement(Text, { color: "red", bold: true },
@@ -6,9 +6,11 @@ import { shortcutManager } from '../services/shortcutManager.js';
6
6
  import { configurationManager } from '../services/configurationManager.js';
7
7
  import { generateWorktreeDirectory } from '../utils/worktreeUtils.js';
8
8
  import { WorktreeService } from '../services/worktreeService.js';
9
+ import { useSearchMode } from '../hooks/useSearchMode.js';
9
10
  const NewWorktree = ({ onComplete, onCancel }) => {
10
11
  const worktreeConfig = configurationManager.getWorktreeConfig();
11
12
  const isAutoDirectory = worktreeConfig.autoDirectory;
13
+ const limit = 10;
12
14
  // Adjust initial step based on auto directory mode
13
15
  const [step, setStep] = useState(isAutoDirectory ? 'branch' : 'path');
14
16
  const [path, setPath] = useState('');
@@ -27,16 +29,32 @@ const NewWorktree = ({ onComplete, onCancel }) => {
27
29
  };
28
30
  }, []); // Empty deps array - only initialize once
29
31
  // Create branch items with default branch first (memoized)
30
- const branchItems = useMemo(() => [
32
+ const allBranchItems = useMemo(() => [
31
33
  { label: `${defaultBranch} (default)`, value: defaultBranch },
32
34
  ...branches
33
35
  .filter(br => br !== defaultBranch)
34
36
  .map(br => ({ label: br, value: br })),
35
37
  ], [branches, defaultBranch]);
38
+ // Use search mode for base branch selection
39
+ const { isSearchMode, searchQuery, selectedIndex, setSearchQuery } = useSearchMode(allBranchItems.length, {
40
+ isDisabled: step !== 'base-branch',
41
+ });
42
+ // Filter branch items based on search query
43
+ const branchItems = useMemo(() => {
44
+ if (!searchQuery)
45
+ return allBranchItems;
46
+ return allBranchItems.filter(item => item.value.toLowerCase().includes(searchQuery.toLowerCase()));
47
+ }, [allBranchItems, searchQuery]);
36
48
  useInput((input, key) => {
37
49
  if (shortcutManager.matchesShortcut('cancel', input, key)) {
38
50
  onCancel();
39
51
  }
52
+ // Handle arrow key navigation in search mode for base branch selection
53
+ if (step === 'base-branch' && isSearchMode) {
54
+ // Don't handle any keys here - let useSearchMode handle them
55
+ // The hook will handle arrow keys for navigation and Enter to exit search mode
56
+ return;
57
+ }
40
58
  });
41
59
  const handlePathSubmit = (value) => {
42
60
  if (value.trim()) {
@@ -109,7 +127,17 @@ const NewWorktree = ({ onComplete, onCancel }) => {
109
127
  "Select base branch for ",
110
128
  React.createElement(Text, { color: "cyan" }, branch),
111
129
  ":")),
112
- React.createElement(SelectInput, { items: branchItems, onSelect: handleBaseBranchSelect, initialIndex: 0, limit: 10 }))),
130
+ isSearchMode && (React.createElement(Box, { marginBottom: 1 },
131
+ React.createElement(Text, null, "Search: "),
132
+ React.createElement(TextInputWrapper, { value: searchQuery, onChange: setSearchQuery, focus: true, placeholder: "Type to filter branches..." }))),
133
+ isSearchMode && branchItems.length === 0 ? (React.createElement(Box, null,
134
+ React.createElement(Text, { color: "yellow" }, "No branches match your search"))) : isSearchMode ? (
135
+ // In search mode, show the items as a list without SelectInput
136
+ React.createElement(Box, { flexDirection: "column" }, branchItems.slice(0, limit).map((item, index) => (React.createElement(Text, { key: item.value, color: index === selectedIndex ? 'green' : undefined },
137
+ index === selectedIndex ? '❯ ' : ' ',
138
+ item.label))))) : (React.createElement(SelectInput, { items: branchItems, onSelect: handleBaseBranchSelect, initialIndex: selectedIndex, limit: limit, isFocused: !isSearchMode })),
139
+ !isSearchMode && (React.createElement(Box, { marginTop: 1 },
140
+ React.createElement(Text, { dimColor: true }, "Press / to search"))))),
113
141
  step === 'copy-settings' && (React.createElement(Box, { flexDirection: "column" },
114
142
  React.createElement(Box, { marginBottom: 1 },
115
143
  React.createElement(Text, null,
@@ -1,4 +1,4 @@
1
- import { ConfigurationData, StatusHookConfig, ShortcutConfig, WorktreeConfig, CommandConfig, CommandPreset, CommandPresetsConfig } from '../types/index.js';
1
+ import { ConfigurationData, StatusHookConfig, WorktreeHookConfig, ShortcutConfig, WorktreeConfig, CommandConfig, CommandPreset, CommandPresetsConfig } from '../types/index.js';
2
2
  export declare class ConfigurationManager {
3
3
  private configPath;
4
4
  private legacyShortcutsPath;
@@ -12,6 +12,8 @@ export declare class ConfigurationManager {
12
12
  setShortcuts(shortcuts: ShortcutConfig): void;
13
13
  getStatusHooks(): StatusHookConfig;
14
14
  setStatusHooks(hooks: StatusHookConfig): void;
15
+ getWorktreeHooks(): WorktreeHookConfig;
16
+ setWorktreeHooks(hooks: WorktreeHookConfig): void;
15
17
  getConfiguration(): ConfigurationData;
16
18
  setConfiguration(config: ConfigurationData): void;
17
19
  getWorktreeConfig(): WorktreeConfig;
@@ -70,6 +70,9 @@ export class ConfigurationManager {
70
70
  if (!this.config.statusHooks) {
71
71
  this.config.statusHooks = {};
72
72
  }
73
+ if (!this.config.worktreeHooks) {
74
+ this.config.worktreeHooks = {};
75
+ }
73
76
  if (!this.config.worktree) {
74
77
  this.config.worktree = {
75
78
  autoDirectory: false,
@@ -127,6 +130,13 @@ export class ConfigurationManager {
127
130
  this.config.statusHooks = hooks;
128
131
  this.saveConfig();
129
132
  }
133
+ getWorktreeHooks() {
134
+ return this.config.worktreeHooks || {};
135
+ }
136
+ setWorktreeHooks(hooks) {
137
+ this.config.worktreeHooks = hooks;
138
+ this.saveConfig();
139
+ }
130
140
  getConfiguration() {
131
141
  return this.config;
132
142
  }
@@ -1,16 +1,17 @@
1
1
  import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
2
- import { ProjectManager } from './projectManager.js';
3
- import { ENV_VARS } from '../constants/env.js';
4
2
  import * as fs from 'fs';
5
3
  import * as path from 'path';
6
- import * as os from 'os';
7
- // Mock fs module
4
+ // Mock modules before any other imports that might use them
8
5
  vi.mock('fs');
9
- vi.mock('os');
6
+ vi.mock('os', () => ({
7
+ homedir: vi.fn(() => '/home/user'),
8
+ platform: vi.fn(() => 'linux'),
9
+ }));
10
+ // Now import modules that depend on the mocked modules
11
+ import { ProjectManager } from './projectManager.js';
12
+ import { ENV_VARS } from '../constants/env.js';
10
13
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
14
  const mockFs = fs;
12
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
13
- const mockOs = os;
14
15
  describe('ProjectManager', () => {
15
16
  let projectManager;
16
17
  const mockProjectsDir = '/home/user/projects';
@@ -20,8 +21,6 @@ describe('ProjectManager', () => {
20
21
  vi.clearAllMocks();
21
22
  // Reset environment variables
22
23
  delete process.env[ENV_VARS.MULTI_PROJECT_ROOT];
23
- // Mock os.homedir
24
- mockOs.homedir.mockReturnValue('/home/user');
25
24
  // Mock fs methods for config directory
26
25
  mockFs.existsSync.mockImplementation((path) => {
27
26
  if (path === mockConfigDir)
@@ -32,7 +32,6 @@ export declare class SessionManager extends EventEmitter implements ISessionMana
32
32
  setSessionActive(worktreePath: string, active: boolean): void;
33
33
  destroySession(worktreePath: string): void;
34
34
  getAllSessions(): Session[];
35
- private executeStatusHook;
36
35
  createSessionWithDevcontainer(worktreePath: string, devcontainerConfig: DevcontainerConfig, presetId?: string): Promise<Session>;
37
36
  destroy(): void;
38
37
  static getSessionCounts(sessions: Session[]): SessionCounts;