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.
- package/README.md +11 -5
- package/dist/components/App.js +17 -3
- package/dist/components/App.test.js +5 -5
- package/dist/components/Configuration.d.ts +2 -0
- package/dist/components/Configuration.js +6 -2
- package/dist/components/ConfigureCommand.js +34 -11
- package/dist/components/ConfigureOther.js +18 -4
- package/dist/components/ConfigureOther.test.js +48 -12
- package/dist/components/ConfigureShortcuts.js +27 -85
- package/dist/components/ConfigureStatusHooks.js +19 -4
- package/dist/components/ConfigureStatusHooks.test.js +46 -12
- package/dist/components/ConfigureWorktree.js +18 -4
- package/dist/components/ConfigureWorktreeHooks.js +19 -4
- package/dist/components/ConfigureWorktreeHooks.test.js +49 -14
- package/dist/components/Menu.js +72 -14
- package/dist/components/NewWorktree.js +2 -2
- package/dist/components/NewWorktree.test.js +6 -6
- package/dist/components/PresetSelector.js +2 -2
- package/dist/contexts/ConfigEditorContext.d.ts +21 -0
- package/dist/contexts/ConfigEditorContext.js +25 -0
- package/dist/services/autoApprovalVerifier.js +3 -3
- package/dist/services/autoApprovalVerifier.test.js +2 -2
- package/dist/services/config/configEditor.d.ts +46 -0
- package/dist/services/{configurationManager.effect.test.js → config/configEditor.effect.test.js} +46 -49
- package/dist/services/config/configEditor.js +101 -0
- package/dist/services/{configurationManager.selectPresetOnStart.test.js → config/configEditor.selectPresetOnStart.test.js} +27 -19
- package/dist/services/{configurationManager.test.js → config/configEditor.test.js} +60 -132
- package/dist/services/config/configReader.d.ts +28 -0
- package/dist/services/config/configReader.js +95 -0
- package/dist/services/config/configReader.multiProject.test.d.ts +1 -0
- package/dist/services/config/configReader.multiProject.test.js +136 -0
- package/dist/services/config/globalConfigManager.d.ts +30 -0
- package/dist/services/config/globalConfigManager.js +216 -0
- package/dist/services/config/index.d.ts +13 -0
- package/dist/services/config/index.js +13 -0
- package/dist/services/config/projectConfigManager.d.ts +41 -0
- package/dist/services/config/projectConfigManager.js +181 -0
- package/dist/services/config/projectConfigManager.test.d.ts +1 -0
- package/dist/services/config/projectConfigManager.test.js +105 -0
- package/dist/services/config/testUtils.d.ts +81 -0
- package/dist/services/config/testUtils.js +351 -0
- package/dist/services/sessionManager.autoApproval.test.js +5 -5
- package/dist/services/sessionManager.effect.test.js +27 -18
- package/dist/services/sessionManager.js +17 -34
- package/dist/services/sessionManager.statePersistence.test.js +5 -4
- package/dist/services/sessionManager.test.js +52 -47
- package/dist/services/shortcutManager.d.ts +0 -1
- package/dist/services/shortcutManager.js +5 -16
- package/dist/services/shortcutManager.test.js +2 -2
- package/dist/services/worktreeService.d.ts +12 -0
- package/dist/services/worktreeService.js +24 -4
- package/dist/services/worktreeService.sort.test.js +105 -109
- package/dist/services/worktreeService.test.js +5 -5
- package/dist/types/index.d.ts +41 -7
- package/dist/utils/gitUtils.d.ts +8 -0
- package/dist/utils/gitUtils.js +32 -0
- package/dist/utils/hookExecutor.js +2 -2
- package/dist/utils/hookExecutor.test.js +8 -12
- package/dist/utils/worktreeUtils.test.js +0 -1
- package/package.json +7 -7
- package/dist/services/configurationManager.d.ts +0 -121
- package/dist/services/configurationManager.js +0 -597
- /package/dist/services/{configurationManager.effect.test.d.ts → config/configEditor.effect.test.d.ts} +0 -0
- /package/dist/services/{configurationManager.selectPresetOnStart.test.d.ts → config/configEditor.selectPresetOnStart.test.d.ts} +0 -0
- /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 {
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
68
|
+
mockFns.getStatusHooks.mockReturnValue({});
|
|
37
69
|
const onComplete = vi.fn();
|
|
38
|
-
const { lastFrame } = render(React.createElement(
|
|
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
|
-
|
|
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(
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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" },
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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" },
|
|
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 {
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
68
|
+
mockFns.getWorktreeHooks.mockReturnValue({});
|
|
37
69
|
const onComplete = vi.fn();
|
|
38
|
-
const { lastFrame } = render(React.createElement(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
89
|
+
mockFns.getWorktreeHooks.mockReturnValue({});
|
|
56
90
|
const onComplete = vi.fn();
|
|
57
|
-
const { lastFrame } = render(React.createElement(
|
|
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
|
});
|
package/dist/components/Menu.js
CHANGED
|
@@ -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 {
|
|
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 =
|
|
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
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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 {
|
|
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 =
|
|
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/
|
|
47
|
-
|
|
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 {
|
|
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(
|
|
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 {
|
|
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(
|
|
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 {
|
|
4
|
+
import { configReader } from '../services/config/configReader.js';
|
|
5
5
|
const PresetSelector = ({ onSelect, onCancel, }) => {
|
|
6
|
-
const presetsConfig =
|
|
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 {
|
|
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 =
|
|
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 =
|
|
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('./
|
|
9
|
-
|
|
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;
|