ccmanager 3.4.0 → 3.5.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.
Files changed (65) hide show
  1. package/README.md +11 -5
  2. package/dist/components/App.js +17 -3
  3. package/dist/components/App.test.js +5 -5
  4. package/dist/components/Configuration.d.ts +2 -0
  5. package/dist/components/Configuration.js +6 -2
  6. package/dist/components/ConfigureCommand.js +34 -11
  7. package/dist/components/ConfigureOther.js +18 -4
  8. package/dist/components/ConfigureOther.test.js +48 -12
  9. package/dist/components/ConfigureShortcuts.js +27 -85
  10. package/dist/components/ConfigureStatusHooks.js +19 -4
  11. package/dist/components/ConfigureStatusHooks.test.js +46 -12
  12. package/dist/components/ConfigureWorktree.js +18 -4
  13. package/dist/components/ConfigureWorktreeHooks.js +19 -4
  14. package/dist/components/ConfigureWorktreeHooks.test.js +49 -14
  15. package/dist/components/Menu.js +72 -14
  16. package/dist/components/NewWorktree.js +2 -2
  17. package/dist/components/NewWorktree.test.js +6 -6
  18. package/dist/components/PresetSelector.js +2 -2
  19. package/dist/contexts/ConfigEditorContext.d.ts +21 -0
  20. package/dist/contexts/ConfigEditorContext.js +25 -0
  21. package/dist/services/autoApprovalVerifier.js +3 -3
  22. package/dist/services/autoApprovalVerifier.test.js +2 -2
  23. package/dist/services/config/configEditor.d.ts +46 -0
  24. package/dist/services/{configurationManager.effect.test.js → config/configEditor.effect.test.js} +46 -49
  25. package/dist/services/config/configEditor.js +101 -0
  26. package/dist/services/{configurationManager.selectPresetOnStart.test.js → config/configEditor.selectPresetOnStart.test.js} +27 -19
  27. package/dist/services/{configurationManager.test.js → config/configEditor.test.js} +60 -132
  28. package/dist/services/config/configReader.d.ts +28 -0
  29. package/dist/services/config/configReader.js +95 -0
  30. package/dist/services/config/configReader.multiProject.test.d.ts +1 -0
  31. package/dist/services/config/configReader.multiProject.test.js +136 -0
  32. package/dist/services/config/globalConfigManager.d.ts +30 -0
  33. package/dist/services/config/globalConfigManager.js +216 -0
  34. package/dist/services/config/index.d.ts +13 -0
  35. package/dist/services/config/index.js +13 -0
  36. package/dist/services/config/projectConfigManager.d.ts +41 -0
  37. package/dist/services/config/projectConfigManager.js +181 -0
  38. package/dist/services/config/projectConfigManager.test.d.ts +1 -0
  39. package/dist/services/config/projectConfigManager.test.js +105 -0
  40. package/dist/services/config/testUtils.d.ts +81 -0
  41. package/dist/services/config/testUtils.js +351 -0
  42. package/dist/services/sessionManager.autoApproval.test.js +5 -5
  43. package/dist/services/sessionManager.effect.test.js +27 -18
  44. package/dist/services/sessionManager.js +17 -34
  45. package/dist/services/sessionManager.statePersistence.test.js +5 -4
  46. package/dist/services/sessionManager.test.js +52 -47
  47. package/dist/services/shortcutManager.d.ts +0 -1
  48. package/dist/services/shortcutManager.js +5 -16
  49. package/dist/services/shortcutManager.test.js +2 -2
  50. package/dist/services/worktreeService.d.ts +12 -0
  51. package/dist/services/worktreeService.js +24 -4
  52. package/dist/services/worktreeService.sort.test.js +105 -109
  53. package/dist/services/worktreeService.test.js +5 -5
  54. package/dist/types/index.d.ts +41 -7
  55. package/dist/utils/gitUtils.d.ts +8 -0
  56. package/dist/utils/gitUtils.js +32 -0
  57. package/dist/utils/hookExecutor.js +2 -2
  58. package/dist/utils/hookExecutor.test.js +8 -12
  59. package/dist/utils/worktreeUtils.test.js +0 -1
  60. package/package.json +7 -7
  61. package/dist/services/configurationManager.d.ts +0 -121
  62. package/dist/services/configurationManager.js +0 -597
  63. /package/dist/services/{configurationManager.effect.test.d.ts → config/configEditor.effect.test.d.ts} +0 -0
  64. /package/dist/services/{configurationManager.selectPresetOnStart.test.d.ts → config/configEditor.selectPresetOnStart.test.d.ts} +0 -0
  65. /package/dist/services/{configurationManager.test.d.ts → config/configEditor.test.d.ts} +0 -0
@@ -2,7 +2,7 @@ import React from 'react';
2
2
  import { render } from 'ink-testing-library';
3
3
  import { describe, it, expect, vi, beforeEach } from 'vitest';
4
4
  import ConfigureStatusHooks from './ConfigureStatusHooks.js';
5
- import { configurationManager } from '../services/configurationManager.js';
5
+ import { ConfigEditorProvider } from '../contexts/ConfigEditorContext.js';
6
6
  // Mock ink to avoid stdin issues
7
7
  vi.mock('ink', async () => {
8
8
  const actual = await vi.importActual('ink');
@@ -21,21 +21,54 @@ vi.mock('ink-select-input', async () => {
21
21
  },
22
22
  };
23
23
  });
24
- vi.mock('../services/configurationManager.js', () => ({
25
- configurationManager: {
26
- getStatusHooks: vi.fn(),
27
- setStatusHooks: vi.fn(),
28
- },
29
- }));
30
- const mockedConfigurationManager = configurationManager;
24
+ // Create mock functions that will be used by the mock class
25
+ const mockFns = {
26
+ getStatusHooks: vi.fn(),
27
+ setStatusHooks: vi.fn(),
28
+ hasProjectOverride: vi.fn().mockReturnValue(false),
29
+ getScope: vi.fn().mockReturnValue('global'),
30
+ };
31
+ vi.mock('../services/config/configEditor.js', () => {
32
+ return {
33
+ ConfigEditor: class {
34
+ constructor() {
35
+ Object.defineProperty(this, "getStatusHooks", {
36
+ enumerable: true,
37
+ configurable: true,
38
+ writable: true,
39
+ value: mockFns.getStatusHooks
40
+ });
41
+ Object.defineProperty(this, "setStatusHooks", {
42
+ enumerable: true,
43
+ configurable: true,
44
+ writable: true,
45
+ value: mockFns.setStatusHooks
46
+ });
47
+ Object.defineProperty(this, "hasProjectOverride", {
48
+ enumerable: true,
49
+ configurable: true,
50
+ writable: true,
51
+ value: mockFns.hasProjectOverride
52
+ });
53
+ Object.defineProperty(this, "getScope", {
54
+ enumerable: true,
55
+ configurable: true,
56
+ writable: true,
57
+ value: mockFns.getScope
58
+ });
59
+ }
60
+ },
61
+ };
62
+ });
31
63
  describe('ConfigureStatusHooks', () => {
32
64
  beforeEach(() => {
33
65
  vi.clearAllMocks();
34
66
  });
35
67
  it('should render status hooks configuration screen', () => {
36
- mockedConfigurationManager.getStatusHooks.mockReturnValue({});
68
+ mockFns.getStatusHooks.mockReturnValue({});
37
69
  const onComplete = vi.fn();
38
- const { lastFrame } = render(React.createElement(ConfigureStatusHooks, { onComplete: onComplete }));
70
+ const { lastFrame } = render(React.createElement(ConfigEditorProvider, { scope: "global" },
71
+ React.createElement(ConfigureStatusHooks, { onComplete: onComplete })));
39
72
  expect(lastFrame()).toContain('Configure Status Hooks');
40
73
  expect(lastFrame()).toContain('Set commands to run when session status changes');
41
74
  expect(lastFrame()).toContain('Idle:');
@@ -43,7 +76,7 @@ describe('ConfigureStatusHooks', () => {
43
76
  expect(lastFrame()).toContain('Waiting for Input:');
44
77
  });
45
78
  it('should display configured hooks', () => {
46
- mockedConfigurationManager.getStatusHooks.mockReturnValue({
79
+ mockFns.getStatusHooks.mockReturnValue({
47
80
  idle: {
48
81
  command: 'notify-send "Idle"',
49
82
  enabled: true,
@@ -54,7 +87,8 @@ describe('ConfigureStatusHooks', () => {
54
87
  },
55
88
  });
56
89
  const onComplete = vi.fn();
57
- const { lastFrame } = render(React.createElement(ConfigureStatusHooks, { onComplete: onComplete }));
90
+ const { lastFrame } = render(React.createElement(ConfigEditorProvider, { scope: "global" },
91
+ React.createElement(ConfigureStatusHooks, { onComplete: onComplete })));
58
92
  expect(lastFrame()).toContain('Idle: ✓ notify-send "Idle"');
59
93
  expect(lastFrame()).toContain('Busy: ✗ echo "Busy"');
60
94
  expect(lastFrame()).toContain('Waiting for Input: ✗ (not set)');
@@ -2,17 +2,22 @@ import React, { useState } from 'react';
2
2
  import { Box, Text, useInput } from 'ink';
3
3
  import SelectInput from 'ink-select-input';
4
4
  import TextInputWrapper from './TextInputWrapper.js';
5
- import { configurationManager } from '../services/configurationManager.js';
5
+ import { useConfigEditor } from '../contexts/ConfigEditorContext.js';
6
6
  import { shortcutManager } from '../services/shortcutManager.js';
7
7
  import { generateWorktreeDirectory } from '../utils/worktreeUtils.js';
8
8
  const ConfigureWorktree = ({ onComplete }) => {
9
- const worktreeConfig = configurationManager.getWorktreeConfig();
9
+ const configEditor = useConfigEditor();
10
+ const scope = configEditor.getScope();
11
+ // Get initial worktree config based on scope
12
+ const worktreeConfig = configEditor.getWorktreeConfig();
10
13
  const [autoDirectory, setAutoDirectory] = useState(worktreeConfig.autoDirectory);
11
14
  const [pattern, setPattern] = useState(worktreeConfig.autoDirectoryPattern || '../{branch}');
12
15
  const [copySessionData, setCopySessionData] = useState(worktreeConfig.copySessionData ?? true);
13
16
  const [sortByLastSession, setSortByLastSession] = useState(worktreeConfig.sortByLastSession ?? false);
14
17
  const [editMode, setEditMode] = useState('menu');
15
18
  const [tempPattern, setTempPattern] = useState(pattern);
19
+ // Show if inheriting from global (for project scope)
20
+ const isInheriting = scope === 'project' && !configEditor.hasProjectOverride('worktree');
16
21
  // Example values for preview
17
22
  const exampleProjectPath = '/home/user/src/myproject';
18
23
  const exampleBranchName = 'feature/my-feature';
@@ -65,7 +70,7 @@ const ConfigureWorktree = ({ onComplete }) => {
65
70
  break;
66
71
  case 'save':
67
72
  // Save the configuration
68
- configurationManager.setWorktreeConfig({
73
+ configEditor.setWorktreeConfig({
69
74
  autoDirectory,
70
75
  autoDirectoryPattern: pattern,
71
76
  copySessionData,
@@ -104,9 +109,18 @@ const ConfigureWorktree = ({ onComplete }) => {
104
109
  React.createElement(Box, { marginTop: 1 },
105
110
  React.createElement(Text, { dimColor: true }, "Press Enter to save or Escape to cancel"))));
106
111
  }
112
+ const scopeLabel = scope === 'project' ? 'Project' : 'Global';
107
113
  return (React.createElement(Box, { flexDirection: "column" },
108
114
  React.createElement(Box, { marginBottom: 1 },
109
- React.createElement(Text, { bold: true, color: "green" }, "Configure Worktree Settings")),
115
+ React.createElement(Text, { bold: true, color: "green" },
116
+ "Configure Worktree Settings (",
117
+ scopeLabel,
118
+ ")")),
119
+ isInheriting && (React.createElement(Box, { marginBottom: 1 },
120
+ React.createElement(Text, { backgroundColor: "cyan", color: "black" },
121
+ ' ',
122
+ "\uD83D\uDCCB Inheriting from global configuration",
123
+ ' '))),
110
124
  React.createElement(Box, { marginBottom: 1 },
111
125
  React.createElement(Text, { dimColor: true }, "Configure worktree creation settings")),
112
126
  autoDirectory && (React.createElement(Box, { marginBottom: 1 },
@@ -2,13 +2,19 @@ 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';
5
- import { configurationManager } from '../services/configurationManager.js';
5
+ import { useConfigEditor } from '../contexts/ConfigEditorContext.js';
6
6
  const ConfigureWorktreeHooks = ({ onComplete, }) => {
7
+ const configEditor = useConfigEditor();
8
+ const scope = configEditor.getScope();
7
9
  const [view, setView] = useState('menu');
8
- const [worktreeHooks, setWorktreeHooks] = useState(configurationManager.getWorktreeHooks());
10
+ // Get initial worktree hooks based on scope
11
+ const initialWorktreeHooks = configEditor.getWorktreeHooks() ?? {};
12
+ const [worktreeHooks, setWorktreeHooks] = useState(initialWorktreeHooks);
9
13
  const [currentCommand, setCurrentCommand] = useState('');
10
14
  const [currentEnabled, setCurrentEnabled] = useState(false);
11
15
  const [showSaveMessage, setShowSaveMessage] = useState(false);
16
+ // Show if inheriting from global (for project scope)
17
+ const isInheriting = scope === 'project' && !configEditor.hasProjectOverride('worktreeHooks');
12
18
  useInput((input, key) => {
13
19
  if (key.escape) {
14
20
  if (view === 'edit') {
@@ -48,7 +54,7 @@ const ConfigureWorktreeHooks = ({ onComplete, }) => {
48
54
  };
49
55
  const handleMenuSelect = (item) => {
50
56
  if (item.value === 'save') {
51
- configurationManager.setWorktreeHooks(worktreeHooks);
57
+ configEditor.setWorktreeHooks(worktreeHooks);
52
58
  setShowSaveMessage(true);
53
59
  setTimeout(() => {
54
60
  onComplete();
@@ -102,9 +108,18 @@ const ConfigureWorktreeHooks = ({ onComplete, }) => {
102
108
  React.createElement(Box, { marginTop: 1 },
103
109
  React.createElement(Text, { dimColor: true }, "Press Enter to save, Tab to toggle enabled, Esc to cancel"))));
104
110
  }
111
+ const scopeLabel = scope === 'project' ? 'Project' : 'Global';
105
112
  return (React.createElement(Box, { flexDirection: "column" },
106
113
  React.createElement(Box, { marginBottom: 1 },
107
- React.createElement(Text, { bold: true, color: "green" }, "Configure Worktree Hooks")),
114
+ React.createElement(Text, { bold: true, color: "green" },
115
+ "Configure Worktree Hooks (",
116
+ scopeLabel,
117
+ ")")),
118
+ isInheriting && (React.createElement(Box, { marginBottom: 1 },
119
+ React.createElement(Text, { backgroundColor: "cyan", color: "black" },
120
+ ' ',
121
+ "\uD83D\uDCCB Inheriting from global configuration",
122
+ ' '))),
108
123
  React.createElement(Box, { marginBottom: 1 },
109
124
  React.createElement(Text, { dimColor: true }, "Set commands to run on worktree events:")),
110
125
  React.createElement(SelectInput, { items: getMenuItems(), onSelect: handleMenuSelect, isFocused: true, limit: 10 }),
@@ -2,7 +2,7 @@ import React from 'react';
2
2
  import { render } from 'ink-testing-library';
3
3
  import { describe, it, expect, vi, beforeEach } from 'vitest';
4
4
  import ConfigureWorktreeHooks from './ConfigureWorktreeHooks.js';
5
- import { configurationManager } from '../services/configurationManager.js';
5
+ import { ConfigEditorProvider } from '../contexts/ConfigEditorContext.js';
6
6
  // Mock ink to avoid stdin issues
7
7
  vi.mock('ink', async () => {
8
8
  const actual = await vi.importActual('ink');
@@ -21,40 +21,75 @@ vi.mock('ink-select-input', async () => {
21
21
  },
22
22
  };
23
23
  });
24
- vi.mock('../services/configurationManager.js', () => ({
25
- configurationManager: {
26
- getWorktreeHooks: vi.fn(),
27
- setWorktreeHooks: vi.fn(),
28
- },
29
- }));
30
- const mockedConfigurationManager = configurationManager;
24
+ // Create mock functions that will be used by the mock class
25
+ const mockFns = {
26
+ getWorktreeHooks: vi.fn(),
27
+ setWorktreeHooks: vi.fn(),
28
+ hasProjectOverride: vi.fn().mockReturnValue(false),
29
+ getScope: vi.fn().mockReturnValue('global'),
30
+ };
31
+ vi.mock('../services/config/configEditor.js', () => {
32
+ return {
33
+ ConfigEditor: class {
34
+ constructor() {
35
+ Object.defineProperty(this, "getWorktreeHooks", {
36
+ enumerable: true,
37
+ configurable: true,
38
+ writable: true,
39
+ value: mockFns.getWorktreeHooks
40
+ });
41
+ Object.defineProperty(this, "setWorktreeHooks", {
42
+ enumerable: true,
43
+ configurable: true,
44
+ writable: true,
45
+ value: mockFns.setWorktreeHooks
46
+ });
47
+ Object.defineProperty(this, "hasProjectOverride", {
48
+ enumerable: true,
49
+ configurable: true,
50
+ writable: true,
51
+ value: mockFns.hasProjectOverride
52
+ });
53
+ Object.defineProperty(this, "getScope", {
54
+ enumerable: true,
55
+ configurable: true,
56
+ writable: true,
57
+ value: mockFns.getScope
58
+ });
59
+ }
60
+ },
61
+ };
62
+ });
31
63
  describe('ConfigureWorktreeHooks', () => {
32
64
  beforeEach(() => {
33
65
  vi.clearAllMocks();
34
66
  });
35
67
  it('should render worktree hooks configuration screen', () => {
36
- mockedConfigurationManager.getWorktreeHooks.mockReturnValue({});
68
+ mockFns.getWorktreeHooks.mockReturnValue({});
37
69
  const onComplete = vi.fn();
38
- const { lastFrame } = render(React.createElement(ConfigureWorktreeHooks, { onComplete: onComplete }));
70
+ const { lastFrame } = render(React.createElement(ConfigEditorProvider, { scope: "global" },
71
+ React.createElement(ConfigureWorktreeHooks, { onComplete: onComplete })));
39
72
  expect(lastFrame()).toContain('Configure Worktree Hooks');
40
73
  expect(lastFrame()).toContain('Set commands to run on worktree events');
41
74
  expect(lastFrame()).toContain('Post Creation:');
42
75
  });
43
76
  it('should display configured hooks', () => {
44
- mockedConfigurationManager.getWorktreeHooks.mockReturnValue({
77
+ mockFns.getWorktreeHooks.mockReturnValue({
45
78
  post_creation: {
46
79
  command: 'npm install',
47
80
  enabled: true,
48
81
  },
49
82
  });
50
83
  const onComplete = vi.fn();
51
- const { lastFrame } = render(React.createElement(ConfigureWorktreeHooks, { onComplete: onComplete }));
84
+ const { lastFrame } = render(React.createElement(ConfigEditorProvider, { scope: "global" },
85
+ React.createElement(ConfigureWorktreeHooks, { onComplete: onComplete })));
52
86
  expect(lastFrame()).toContain('Post Creation: ✓ npm install');
53
87
  });
54
88
  it('should display not set when no hook configured', () => {
55
- mockedConfigurationManager.getWorktreeHooks.mockReturnValue({});
89
+ mockFns.getWorktreeHooks.mockReturnValue({});
56
90
  const onComplete = vi.fn();
57
- const { lastFrame } = render(React.createElement(ConfigureWorktreeHooks, { onComplete: onComplete }));
91
+ const { lastFrame } = render(React.createElement(ConfigEditorProvider, { scope: "global" },
92
+ React.createElement(ConfigureWorktreeHooks, { onComplete: onComplete })));
58
93
  expect(lastFrame()).toContain('Post Creation: ✗ (not set)');
59
94
  });
60
95
  });
@@ -10,7 +10,7 @@ import { projectManager } from '../services/projectManager.js';
10
10
  import TextInputWrapper from './TextInputWrapper.js';
11
11
  import { useSearchMode } from '../hooks/useSearchMode.js';
12
12
  import { globalSessionOrchestrator } from '../services/globalSessionOrchestrator.js';
13
- import { configurationManager } from '../services/configurationManager.js';
13
+ import { configReader } from '../services/config/configReader.js';
14
14
  const createSeparatorWithText = (text, totalWidth = 35) => {
15
15
  const textWithSpaces = ` ${text} `;
16
16
  const textLength = textWithSpaces.length;
@@ -36,7 +36,7 @@ const Menu = ({ sessionManager, worktreeService, onSelectWorktree, onSelectRecen
36
36
  const [recentProjects, setRecentProjects] = useState([]);
37
37
  const limit = 10;
38
38
  // Get worktree configuration for sorting
39
- const worktreeConfig = configurationManager.getWorktreeConfig();
39
+ const worktreeConfig = configReader.getWorktreeConfig();
40
40
  // Use the search mode hook
41
41
  const { isSearchMode, searchQuery, selectedIndex, setSearchQuery } = useSearchMode(items.length, {
42
42
  isDisabled: !!error || !!loadError,
@@ -207,12 +207,29 @@ const Menu = ({ sessionManager, worktreeService, onSelectWorktree, onSelectRecen
207
207
  label: `D ${MENU_ICONS.DELETE_WORKTREE} Delete Worktree`,
208
208
  value: 'delete-worktree',
209
209
  },
210
- {
210
+ ];
211
+ // Add configuration menu items based on multiProject mode
212
+ if (multiProject) {
213
+ // In multi-project mode, only show global configuration (backward compatible)
214
+ otherMenuItems.push({
211
215
  type: 'common',
212
216
  label: `C ${MENU_ICONS.CONFIGURE_SHORTCUTS} Configuration`,
213
217
  value: 'configuration',
214
- },
215
- ];
218
+ });
219
+ }
220
+ else {
221
+ // In single-project mode, show both Project and Global configuration
222
+ otherMenuItems.push({
223
+ type: 'common',
224
+ label: `P ${MENU_ICONS.CONFIGURE_SHORTCUTS} Project Configuration`,
225
+ value: 'configuration-project',
226
+ });
227
+ otherMenuItems.push({
228
+ type: 'common',
229
+ label: `C ${MENU_ICONS.CONFIGURE_SHORTCUTS} Global Configuration`,
230
+ value: 'configuration-global',
231
+ });
232
+ }
216
233
  menuItems.push(...otherMenuItems);
217
234
  if (projectName) {
218
235
  // In multi-project mode, show 'Back to project list'
@@ -313,14 +330,37 @@ const Menu = ({ sessionManager, worktreeService, onSelectWorktree, onSelectRecen
313
330
  hasSession: false,
314
331
  });
315
332
  break;
333
+ case 'p':
334
+ // Trigger project configuration action (only in single-project mode)
335
+ if (!multiProject) {
336
+ onSelectWorktree({
337
+ path: 'CONFIGURATION_PROJECT',
338
+ branch: '',
339
+ isMainWorktree: false,
340
+ hasSession: false,
341
+ });
342
+ }
343
+ break;
316
344
  case 'c':
317
345
  // Trigger configuration action
318
- onSelectWorktree({
319
- path: 'CONFIGURATION',
320
- branch: '',
321
- isMainWorktree: false,
322
- hasSession: false,
323
- });
346
+ if (multiProject) {
347
+ // In multi-project mode, 'c' opens global configuration (backward compatible)
348
+ onSelectWorktree({
349
+ path: 'CONFIGURATION',
350
+ branch: '',
351
+ isMainWorktree: false,
352
+ hasSession: false,
353
+ });
354
+ }
355
+ else {
356
+ // In single-project mode, 'c' opens global configuration
357
+ onSelectWorktree({
358
+ path: 'CONFIGURATION_GLOBAL',
359
+ branch: '',
360
+ isMainWorktree: false,
361
+ hasSession: false,
362
+ });
363
+ }
324
364
  break;
325
365
  case 'b':
326
366
  // In multi-project mode, go back to project list
@@ -391,7 +431,7 @@ const Menu = ({ sessionManager, worktreeService, onSelectWorktree, onSelectRecen
391
431
  });
392
432
  }
393
433
  else if (item.value === 'configuration') {
394
- // Handle in parent component - use special marker
434
+ // Handle in parent component - use special marker (backward compatible for multi-project mode)
395
435
  onSelectWorktree({
396
436
  path: 'CONFIGURATION',
397
437
  branch: '',
@@ -399,6 +439,24 @@ const Menu = ({ sessionManager, worktreeService, onSelectWorktree, onSelectRecen
399
439
  hasSession: false,
400
440
  });
401
441
  }
442
+ else if (item.value === 'configuration-project') {
443
+ // Handle in parent component - use special marker for project config
444
+ onSelectWorktree({
445
+ path: 'CONFIGURATION_PROJECT',
446
+ branch: '',
447
+ isMainWorktree: false,
448
+ hasSession: false,
449
+ });
450
+ }
451
+ else if (item.value === 'configuration-global') {
452
+ // Handle in parent component - use special marker for global config
453
+ onSelectWorktree({
454
+ path: 'CONFIGURATION_GLOBAL',
455
+ branch: '',
456
+ isMainWorktree: false,
457
+ hasSession: false,
458
+ });
459
+ }
402
460
  else if (item.value === 'exit') {
403
461
  // Handle in parent component - use special marker
404
462
  onSelectWorktree({
@@ -459,7 +517,7 @@ const Menu = ({ sessionManager, worktreeService, onSelectWorktree, onSelectRecen
459
517
  React.createElement(Text, { dimColor: true }, isSearchMode
460
518
  ? 'Search Mode: Type to filter, Enter to exit search, ESC to exit search'
461
519
  : searchQuery
462
- ? `Filtered: "${searchQuery}" | ↑↓ Navigate Enter Select | /-Search ESC-Clear 0-9 Quick Select N-New M-Merge D-Delete C-Config ${projectName ? 'B-Back' : 'Q-Quit'}`
463
- : `Controls: ↑↓ Navigate Enter Select | Hotkeys: 0-9 Quick Select /-Search N-New M-Merge D-Delete C-Config ${projectName ? 'B-Back' : 'Q-Quit'}`))));
520
+ ? `Filtered: "${searchQuery}" | ↑↓ Navigate Enter Select | /-Search ESC-Clear 0-9 Quick Select N-New M-Merge D-Delete ${multiProject ? 'C-Config' : 'P-ProjConfig C-GlobalConfig'} ${projectName ? 'B-Back' : 'Q-Quit'}`
521
+ : `Controls: ↑↓ Navigate Enter Select | Hotkeys: 0-9 Quick Select /-Search N-New M-Merge D-Delete ${multiProject ? 'C-Config' : 'P-ProjConfig C-GlobalConfig'} ${projectName ? 'B-Back' : 'Q-Quit'}`))));
464
522
  };
465
523
  export default Menu;
@@ -3,13 +3,13 @@ import { Box, Text, useInput } from 'ink';
3
3
  import TextInputWrapper from './TextInputWrapper.js';
4
4
  import SelectInput from 'ink-select-input';
5
5
  import { shortcutManager } from '../services/shortcutManager.js';
6
- import { configurationManager } from '../services/configurationManager.js';
6
+ import { configReader } from '../services/config/configReader.js';
7
7
  import { generateWorktreeDirectory } from '../utils/worktreeUtils.js';
8
8
  import { WorktreeService } from '../services/worktreeService.js';
9
9
  import { useSearchMode } from '../hooks/useSearchMode.js';
10
10
  import { Effect } from 'effect';
11
11
  const NewWorktree = ({ projectPath, onComplete, onCancel, }) => {
12
- const worktreeConfig = configurationManager.getWorktreeConfig();
12
+ const worktreeConfig = configReader.getWorktreeConfig();
13
13
  const isAutoDirectory = worktreeConfig.autoDirectory;
14
14
  const limit = 10;
15
15
  // Adjust initial step based on auto directory mode
@@ -43,8 +43,8 @@ vi.mock('../services/shortcutManager.js', () => ({
43
43
  matchesShortcut: () => false,
44
44
  },
45
45
  }));
46
- vi.mock('../services/configurationManager.js', () => ({
47
- configurationManager: {
46
+ vi.mock('../services/config/configReader.js', () => ({
47
+ configReader: {
48
48
  getWorktreeConfig: () => ({
49
49
  autoDirectory: false,
50
50
  autoDirectoryPattern: '../{project}-{branch}',
@@ -175,9 +175,9 @@ describe('NewWorktree component Effect integration', () => {
175
175
  it('should handle empty branch list', async () => {
176
176
  const { Effect } = await import('effect');
177
177
  const { WorktreeService } = await import('../services/worktreeService.js');
178
- const { configurationManager } = await import('../services/configurationManager.js');
178
+ const { configReader } = await import('../services/config/configReader.js');
179
179
  // Mock autoDirectory to true so component starts at base-branch step
180
- vi.spyOn(configurationManager, 'getWorktreeConfig').mockReturnValue({
180
+ vi.spyOn(configReader, 'getWorktreeConfig').mockReturnValue({
181
181
  autoDirectory: true,
182
182
  autoDirectoryPattern: '../{project}-{branch}',
183
183
  copySessionData: true,
@@ -203,9 +203,9 @@ describe('NewWorktree component Effect integration', () => {
203
203
  it('should display branches after successful loading', async () => {
204
204
  const { Effect } = await import('effect');
205
205
  const { WorktreeService } = await import('../services/worktreeService.js');
206
- const { configurationManager } = await import('../services/configurationManager.js');
206
+ const { configReader } = await import('../services/config/configReader.js');
207
207
  // Mock autoDirectory to true so component starts at base-branch step
208
- vi.spyOn(configurationManager, 'getWorktreeConfig').mockReturnValue({
208
+ vi.spyOn(configReader, 'getWorktreeConfig').mockReturnValue({
209
209
  autoDirectory: true,
210
210
  autoDirectoryPattern: '../{project}-{branch}',
211
211
  copySessionData: true,
@@ -1,9 +1,9 @@
1
1
  import React, { useState } from 'react';
2
2
  import { Box, Text, useInput } from 'ink';
3
3
  import SelectInput from 'ink-select-input';
4
- import { configurationManager } from '../services/configurationManager.js';
4
+ import { configReader } from '../services/config/configReader.js';
5
5
  const PresetSelector = ({ onSelect, onCancel, }) => {
6
- const presetsConfig = configurationManager.getCommandPresets();
6
+ const presetsConfig = configReader.getCommandPresets();
7
7
  const [presets] = useState(presetsConfig.presets);
8
8
  const defaultPresetId = presetsConfig.defaultPresetId;
9
9
  const selectItems = presets.map(preset => {
@@ -0,0 +1,21 @@
1
+ import React from 'react';
2
+ import { ConfigEditor } from '../services/config/configEditor.js';
3
+ import { ConfigScope } from '../types/index.js';
4
+ declare const ConfigEditorContext: React.Context<ConfigEditor | null>;
5
+ interface ConfigEditorProviderProps {
6
+ scope: ConfigScope;
7
+ children: React.ReactNode;
8
+ }
9
+ /**
10
+ * Provider component for ConfigEditor context.
11
+ * Creates a ConfigEditor instance based on scope.
12
+ * Uses singleton config editors to ensure config changes are
13
+ * immediately visible to all components.
14
+ */
15
+ export declare function ConfigEditorProvider({ scope, children, }: ConfigEditorProviderProps): React.JSX.Element;
16
+ /**
17
+ * Hook to access ConfigEditor from context.
18
+ * Must be used within a ConfigEditorProvider.
19
+ */
20
+ export declare function useConfigEditor(): ConfigEditor;
21
+ export { ConfigEditorContext };
@@ -0,0 +1,25 @@
1
+ import React, { createContext, useContext, useMemo } from 'react';
2
+ import { ConfigEditor } from '../services/config/configEditor.js';
3
+ const ConfigEditorContext = createContext(null);
4
+ /**
5
+ * Provider component for ConfigEditor context.
6
+ * Creates a ConfigEditor instance based on scope.
7
+ * Uses singleton config editors to ensure config changes are
8
+ * immediately visible to all components.
9
+ */
10
+ export function ConfigEditorProvider({ scope, children, }) {
11
+ const configEditor = useMemo(() => new ConfigEditor(scope), [scope]);
12
+ return (React.createElement(ConfigEditorContext.Provider, { value: configEditor }, children));
13
+ }
14
+ /**
15
+ * Hook to access ConfigEditor from context.
16
+ * Must be used within a ConfigEditorProvider.
17
+ */
18
+ export function useConfigEditor() {
19
+ const context = useContext(ConfigEditorContext);
20
+ if (context === null) {
21
+ throw new Error('useConfigEditor must be used within a ConfigEditorProvider');
22
+ }
23
+ return context;
24
+ }
25
+ export { ConfigEditorContext };
@@ -1,11 +1,11 @@
1
1
  import { Effect } from 'effect';
2
2
  import { ProcessError } from '../types/errors.js';
3
- import { configurationManager } from './configurationManager.js';
3
+ import { configReader } from './config/configReader.js';
4
4
  import { logger } from '../utils/logger.js';
5
5
  import { execFile, spawn, } from 'child_process';
6
6
  const DEFAULT_TIMEOUT_SECONDS = 30;
7
7
  const getTimeoutMs = () => {
8
- const config = configurationManager.getAutoApprovalConfig();
8
+ const config = configReader.getAutoApprovalConfig();
9
9
  const timeoutSeconds = config.timeout ?? DEFAULT_TIMEOUT_SECONDS;
10
10
  return timeoutSeconds * 1000;
11
11
  };
@@ -222,7 +222,7 @@ export class AutoApprovalVerifier {
222
222
  verifyNeedsPermission(terminalOutput, options) {
223
223
  const attemptVerification = Effect.tryPromise({
224
224
  try: async () => {
225
- const autoApprovalConfig = configurationManager.getAutoApprovalConfig();
225
+ const autoApprovalConfig = configReader.getAutoApprovalConfig();
226
226
  const customCommand = autoApprovalConfig.customCommand?.trim();
227
227
  const prompt = buildPrompt(terminalOutput);
228
228
  const jsonSchema = JSON.stringify({
@@ -5,8 +5,8 @@ const execFileMock = vi.fn();
5
5
  vi.mock('child_process', () => ({
6
6
  execFile: (...args) => execFileMock(...args),
7
7
  }));
8
- vi.mock('./configurationManager.js', () => ({
9
- configurationManager: {
8
+ vi.mock('./config/configReader.js', () => ({
9
+ configReader: {
10
10
  getAutoApprovalConfig: vi.fn().mockReturnValue({ enabled: false }),
11
11
  },
12
12
  }));
@@ -0,0 +1,46 @@
1
+ import { ConfigScope, ProjectConfigurationData, ShortcutConfig, StatusHookConfig, WorktreeHookConfig, WorktreeConfig, CommandPresetsConfig, IConfigEditor, AutoApprovalConfig } from '../../types/index.js';
2
+ /**
3
+ * ConfigEditor provides scope-aware configuration editing.
4
+ * The scope is determined at construction time.
5
+ *
6
+ * - When scope='global', uses GlobalConfigManager singleton
7
+ * - When scope='project', uses ProjectConfigManager singleton
8
+ * (with fallback to global if project value is undefined)
9
+ *
10
+ * IMPORTANT: Uses singletons to ensure that config changes are
11
+ * immediately visible to all components (e.g., shortcutManager, configReader).
12
+ */
13
+ export declare class ConfigEditor implements IConfigEditor {
14
+ private scope;
15
+ private configEditor;
16
+ constructor(scope: ConfigScope);
17
+ getShortcuts(): ShortcutConfig | undefined;
18
+ setShortcuts(value: ShortcutConfig): void;
19
+ getStatusHooks(): StatusHookConfig | undefined;
20
+ setStatusHooks(value: StatusHookConfig): void;
21
+ getWorktreeHooks(): WorktreeHookConfig | undefined;
22
+ setWorktreeHooks(value: WorktreeHookConfig): void;
23
+ getWorktreeConfig(): WorktreeConfig | undefined;
24
+ setWorktreeConfig(value: WorktreeConfig): void;
25
+ getCommandPresets(): CommandPresetsConfig | undefined;
26
+ setCommandPresets(value: CommandPresetsConfig): void;
27
+ getAutoApprovalConfig(): AutoApprovalConfig | undefined;
28
+ setAutoApprovalConfig(value: AutoApprovalConfig): void;
29
+ reload(): void;
30
+ /**
31
+ * Get the current scope
32
+ */
33
+ getScope(): ConfigScope;
34
+ /**
35
+ * Check if project has an override for a specific field
36
+ */
37
+ hasProjectOverride(field: keyof ProjectConfigurationData): boolean;
38
+ /**
39
+ * Remove project override for a specific field
40
+ */
41
+ removeProjectOverride(field: keyof ProjectConfigurationData): void;
42
+ }
43
+ /**
44
+ * Factory function to create a ConfigEditor instance
45
+ */
46
+ export declare function createConfigEditor(scope: ConfigScope): ConfigEditor;