ccmanager 3.3.2 → 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/Menu.recent-projects.test.js +2 -0
- package/dist/components/Menu.test.js +2 -0
- package/dist/components/NewWorktree.js +2 -2
- package/dist/components/NewWorktree.test.js +6 -6
- package/dist/components/PresetSelector.js +2 -2
- package/dist/constants/statusIcons.d.ts +4 -1
- package/dist/constants/statusIcons.js +10 -1
- package/dist/constants/statusIcons.test.js +42 -0
- 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/config/configEditor.test.d.ts +1 -0
- 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 +9 -6
- package/dist/services/sessionManager.d.ts +2 -0
- package/dist/services/sessionManager.effect.test.js +27 -18
- package/dist/services/sessionManager.js +43 -40
- package/dist/services/sessionManager.statePersistence.test.js +5 -4
- package/dist/services/sessionManager.test.js +71 -49
- 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/stateDetector/base.d.ts +1 -0
- package/dist/services/stateDetector/claude.d.ts +1 -0
- package/dist/services/stateDetector/claude.js +8 -0
- package/dist/services/stateDetector/claude.test.js +102 -0
- package/dist/services/stateDetector/cline.d.ts +1 -0
- package/dist/services/stateDetector/cline.js +3 -0
- package/dist/services/stateDetector/codex.d.ts +1 -0
- package/dist/services/stateDetector/codex.js +3 -0
- package/dist/services/stateDetector/cursor.d.ts +1 -0
- package/dist/services/stateDetector/cursor.js +3 -0
- package/dist/services/stateDetector/gemini.d.ts +1 -0
- package/dist/services/stateDetector/gemini.js +3 -0
- package/dist/services/stateDetector/github-copilot.d.ts +1 -0
- package/dist/services/stateDetector/github-copilot.js +3 -0
- package/dist/services/stateDetector/opencode.d.ts +1 -0
- package/dist/services/stateDetector/opencode.js +3 -0
- package/dist/services/stateDetector/types.d.ts +1 -0
- 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 +47 -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 +13 -12
- package/dist/utils/mutex.d.ts +1 -0
- package/dist/utils/mutex.js +1 -0
- package/dist/utils/worktreeUtils.js +3 -2
- package/dist/utils/worktreeUtils.test.js +2 -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 → constants/statusIcons.test.d.ts} +0 -0
- /package/dist/services/{configurationManager.selectPresetOnStart.test.d.ts → config/configEditor.effect.test.d.ts} +0 -0
- /package/dist/services/{configurationManager.test.d.ts → config/configEditor.selectPresetOnStart.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;
|
|
@@ -123,6 +123,7 @@ describe('Menu - Recent Projects', () => {
|
|
|
123
123
|
waiting_input: 0,
|
|
124
124
|
pending_auto_approval: 0,
|
|
125
125
|
total: 0,
|
|
126
|
+
backgroundTasks: 0,
|
|
126
127
|
});
|
|
127
128
|
vi.spyOn(SessionManager, 'formatSessionCounts').mockReturnValue('');
|
|
128
129
|
const { lastFrame } = render(React.createElement(Menu, { sessionManager: mockSessionManager, worktreeService: mockWorktreeService, onSelectWorktree: vi.fn(), onSelectRecentProject: vi.fn(), multiProject: true }));
|
|
@@ -153,6 +154,7 @@ describe('Menu - Recent Projects', () => {
|
|
|
153
154
|
waiting_input: 0,
|
|
154
155
|
pending_auto_approval: 0,
|
|
155
156
|
total: 0,
|
|
157
|
+
backgroundTasks: 0,
|
|
156
158
|
});
|
|
157
159
|
vi.spyOn(SessionManager, 'formatSessionCounts').mockReturnValue('');
|
|
158
160
|
const { lastFrame } = render(React.createElement(Menu, { sessionManager: mockSessionManager, worktreeService: mockWorktreeService, onSelectWorktree: vi.fn(), onSelectRecentProject: vi.fn(), multiProject: true }));
|
|
@@ -276,6 +276,7 @@ describe('Menu component rendering', () => {
|
|
|
276
276
|
waiting_input: 0,
|
|
277
277
|
pending_auto_approval: 0,
|
|
278
278
|
total: 0,
|
|
279
|
+
backgroundTasks: 0,
|
|
279
280
|
});
|
|
280
281
|
vi.spyOn(SessionManager, 'formatSessionCounts').mockReturnValue('');
|
|
281
282
|
const { lastFrame } = render(React.createElement(Menu, { sessionManager: sessionManager, worktreeService: worktreeService, onSelectWorktree: onSelectWorktree, onSelectRecentProject: onSelectRecentProject, multiProject: true }));
|
|
@@ -317,6 +318,7 @@ describe('Menu component rendering', () => {
|
|
|
317
318
|
waiting_input: 0,
|
|
318
319
|
pending_auto_approval: 0,
|
|
319
320
|
total: 0,
|
|
321
|
+
backgroundTasks: 0,
|
|
320
322
|
});
|
|
321
323
|
vi.spyOn(SessionManager, 'formatSessionCounts').mockReturnValue('');
|
|
322
324
|
const { lastFrame } = render(React.createElement(Menu, { sessionManager: sessionManager, worktreeService: worktreeService, onSelectWorktree: onSelectWorktree, onSelectRecentProject: onSelectRecentProject, multiProject: true }));
|
|
@@ -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 => {
|
|
@@ -10,6 +10,9 @@ export declare const STATUS_LABELS: {
|
|
|
10
10
|
readonly PENDING_AUTO_APPROVAL: "Pending Auto Approval";
|
|
11
11
|
readonly IDLE: "Idle";
|
|
12
12
|
};
|
|
13
|
+
export declare const STATUS_TAGS: {
|
|
14
|
+
readonly BACKGROUND_TASK: "\u001B[2m[BG]\u001B[0m";
|
|
15
|
+
};
|
|
13
16
|
export declare const MENU_ICONS: {
|
|
14
17
|
readonly NEW_WORKTREE: "⊕";
|
|
15
18
|
readonly MERGE_WORKTREE: "⇄";
|
|
@@ -17,4 +20,4 @@ export declare const MENU_ICONS: {
|
|
|
17
20
|
readonly CONFIGURE_SHORTCUTS: "⌨";
|
|
18
21
|
readonly EXIT: "⏻";
|
|
19
22
|
};
|
|
20
|
-
export declare const getStatusDisplay: (status: SessionState) => string;
|
|
23
|
+
export declare const getStatusDisplay: (status: SessionState, hasBackgroundTask?: boolean) => string;
|
|
@@ -9,6 +9,9 @@ export const STATUS_LABELS = {
|
|
|
9
9
|
PENDING_AUTO_APPROVAL: 'Pending Auto Approval',
|
|
10
10
|
IDLE: 'Idle',
|
|
11
11
|
};
|
|
12
|
+
export const STATUS_TAGS = {
|
|
13
|
+
BACKGROUND_TASK: '\x1b[2m[BG]\x1b[0m',
|
|
14
|
+
};
|
|
12
15
|
export const MENU_ICONS = {
|
|
13
16
|
NEW_WORKTREE: '⊕',
|
|
14
17
|
MERGE_WORKTREE: '⇄',
|
|
@@ -16,7 +19,7 @@ export const MENU_ICONS = {
|
|
|
16
19
|
CONFIGURE_SHORTCUTS: '⌨',
|
|
17
20
|
EXIT: '⏻',
|
|
18
21
|
};
|
|
19
|
-
|
|
22
|
+
const getBaseStatusDisplay = (status) => {
|
|
20
23
|
switch (status) {
|
|
21
24
|
case 'busy':
|
|
22
25
|
return `${STATUS_ICONS.BUSY} ${STATUS_LABELS.BUSY}`;
|
|
@@ -28,3 +31,9 @@ export const getStatusDisplay = (status) => {
|
|
|
28
31
|
return `${STATUS_ICONS.IDLE} ${STATUS_LABELS.IDLE}`;
|
|
29
32
|
}
|
|
30
33
|
};
|
|
34
|
+
export const getStatusDisplay = (status, hasBackgroundTask = false) => {
|
|
35
|
+
const display = getBaseStatusDisplay(status);
|
|
36
|
+
return hasBackgroundTask
|
|
37
|
+
? `${display} ${STATUS_TAGS.BACKGROUND_TASK}`
|
|
38
|
+
: display;
|
|
39
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { getStatusDisplay, STATUS_ICONS, STATUS_LABELS, STATUS_TAGS, } from './statusIcons.js';
|
|
3
|
+
describe('getStatusDisplay', () => {
|
|
4
|
+
it('should return busy display for busy state', () => {
|
|
5
|
+
const result = getStatusDisplay('busy');
|
|
6
|
+
expect(result).toBe(`${STATUS_ICONS.BUSY} ${STATUS_LABELS.BUSY}`);
|
|
7
|
+
});
|
|
8
|
+
it('should return waiting display for waiting_input state', () => {
|
|
9
|
+
const result = getStatusDisplay('waiting_input');
|
|
10
|
+
expect(result).toBe(`${STATUS_ICONS.WAITING} ${STATUS_LABELS.WAITING}`);
|
|
11
|
+
});
|
|
12
|
+
it('should return pending auto approval display', () => {
|
|
13
|
+
const result = getStatusDisplay('pending_auto_approval');
|
|
14
|
+
expect(result).toBe(`${STATUS_ICONS.WAITING} ${STATUS_LABELS.PENDING_AUTO_APPROVAL}`);
|
|
15
|
+
});
|
|
16
|
+
it('should return idle display for idle state', () => {
|
|
17
|
+
const result = getStatusDisplay('idle');
|
|
18
|
+
expect(result).toBe(`${STATUS_ICONS.IDLE} ${STATUS_LABELS.IDLE}`);
|
|
19
|
+
});
|
|
20
|
+
describe('background task indicator', () => {
|
|
21
|
+
it('should append [BG] badge when idle and hasBackgroundTask is true', () => {
|
|
22
|
+
const result = getStatusDisplay('idle', true);
|
|
23
|
+
expect(result).toBe(`${STATUS_ICONS.IDLE} ${STATUS_LABELS.IDLE} ${STATUS_TAGS.BACKGROUND_TASK}`);
|
|
24
|
+
});
|
|
25
|
+
it('should not append [BG] badge when idle and hasBackgroundTask is false', () => {
|
|
26
|
+
const result = getStatusDisplay('idle', false);
|
|
27
|
+
expect(result).toBe(`${STATUS_ICONS.IDLE} ${STATUS_LABELS.IDLE}`);
|
|
28
|
+
});
|
|
29
|
+
it('should append [BG] badge when busy and hasBackgroundTask is true', () => {
|
|
30
|
+
const result = getStatusDisplay('busy', true);
|
|
31
|
+
expect(result).toBe(`${STATUS_ICONS.BUSY} ${STATUS_LABELS.BUSY} ${STATUS_TAGS.BACKGROUND_TASK}`);
|
|
32
|
+
});
|
|
33
|
+
it('should append [BG] badge when waiting_input and hasBackgroundTask is true', () => {
|
|
34
|
+
const result = getStatusDisplay('waiting_input', true);
|
|
35
|
+
expect(result).toBe(`${STATUS_ICONS.WAITING} ${STATUS_LABELS.WAITING} ${STATUS_TAGS.BACKGROUND_TASK}`);
|
|
36
|
+
});
|
|
37
|
+
it('should append [BG] badge when pending_auto_approval and hasBackgroundTask is true', () => {
|
|
38
|
+
const result = getStatusDisplay('pending_auto_approval', true);
|
|
39
|
+
expect(result).toBe(`${STATUS_ICONS.WAITING} ${STATUS_LABELS.PENDING_AUTO_APPROVAL} ${STATUS_TAGS.BACKGROUND_TASK}`);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -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 };
|