ccmanager 3.4.0 → 3.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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/constants/statusIcons.d.ts +2 -1
- package/dist/constants/statusIcons.js +13 -4
- package/dist/constants/statusIcons.test.js +41 -11
- 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.d.ts +1 -1
- package/dist/services/sessionManager.effect.test.js +27 -18
- package/dist/services/sessionManager.js +26 -44
- package/dist/services/sessionManager.statePersistence.test.js +5 -4
- package/dist/services/sessionManager.test.js +91 -50
- 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 -1
- package/dist/services/stateDetector/claude.d.ts +1 -1
- package/dist/services/stateDetector/claude.js +11 -4
- package/dist/services/stateDetector/claude.test.js +47 -24
- package/dist/services/stateDetector/cline.d.ts +1 -1
- package/dist/services/stateDetector/cline.js +1 -1
- package/dist/services/stateDetector/codex.d.ts +1 -1
- package/dist/services/stateDetector/codex.js +1 -1
- package/dist/services/stateDetector/cursor.d.ts +1 -1
- package/dist/services/stateDetector/cursor.js +1 -1
- package/dist/services/stateDetector/gemini.d.ts +1 -1
- package/dist/services/stateDetector/gemini.js +1 -1
- package/dist/services/stateDetector/github-copilot.d.ts +1 -1
- package/dist/services/stateDetector/github-copilot.js +1 -1
- package/dist/services/stateDetector/opencode.d.ts +1 -1
- package/dist/services/stateDetector/opencode.js +1 -1
- package/dist/services/stateDetector/types.d.ts +1 -1
- 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/mutex.d.ts +1 -1
- package/dist/utils/mutex.js +1 -1
- package/dist/utils/worktreeUtils.js +1 -1
- 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
package/README.md
CHANGED
|
@@ -81,8 +81,8 @@ npx ccmanager
|
|
|
81
81
|
|
|
82
82
|
You can customize keyboard shortcuts in two ways:
|
|
83
83
|
|
|
84
|
-
1. **Through the UI**: Select
|
|
85
|
-
2. **Configuration file**: Edit `~/.config/ccmanager/config.json`
|
|
84
|
+
1. **Through the UI**: Select **Global Configuration** → **Configure Shortcuts** from the main menu
|
|
85
|
+
2. **Configuration file**: Edit `~/.config/ccmanager/config.json` or `.ccmanager.json` for per-project settings (see [Per-Project Configuration](#per-project-configuration))
|
|
86
86
|
|
|
87
87
|
Example configuration:
|
|
88
88
|
```json
|
|
@@ -110,6 +110,12 @@ Note: Shortcuts from `shortcuts.json` will be automatically migrated to `config.
|
|
|
110
110
|
- Ctrl+D
|
|
111
111
|
- Ctrl+[ (equivalent to Escape)
|
|
112
112
|
|
|
113
|
+
## Per-Project Configuration
|
|
114
|
+
|
|
115
|
+
CCManager supports per-project configuration by placing a `.ccmanager.json` file in your git repository root. Project settings are merged with the global config (`~/.config/ccmanager/config.json`), with project settings always taking priority.
|
|
116
|
+
|
|
117
|
+
For detailed configuration options and examples, see [docs/project-config.md](docs/project-config.md).
|
|
118
|
+
|
|
113
119
|
## Supported AI Assistants
|
|
114
120
|
|
|
115
121
|
CCManager supports multiple AI coding assistants with tailored state detection for each:
|
|
@@ -148,7 +154,7 @@ CCManager supports configuring the command and arguments used to run Claude Code
|
|
|
148
154
|
|
|
149
155
|
### Quick Start
|
|
150
156
|
|
|
151
|
-
1. Navigate to **Configuration** → **Configure Command Presets**
|
|
157
|
+
1. Navigate to **Global Configuration** → **Configure Command Presets**
|
|
152
158
|
2. Set your desired arguments (e.g., `--resume` for resuming sessions)
|
|
153
159
|
3. Optionally set fallback arguments
|
|
154
160
|
4. Save changes
|
|
@@ -177,7 +183,7 @@ When creating a new worktree, CCManager:
|
|
|
177
183
|
|
|
178
184
|
### Configuration
|
|
179
185
|
|
|
180
|
-
1. Navigate to **Configuration** → **Configure Worktree**
|
|
186
|
+
1. Navigate to **Global Configuration** → **Configure Worktree**
|
|
181
187
|
2. Toggle **Copy Session Data** to set the default behavior
|
|
182
188
|
3. Save changes
|
|
183
189
|
|
|
@@ -246,7 +252,7 @@ CCManager can automatically approve Claude Code prompts that don't require user
|
|
|
246
252
|
|
|
247
253
|
### Quick Start
|
|
248
254
|
|
|
249
|
-
1. Navigate to **Configuration** → **Other & Experimental**
|
|
255
|
+
1. Navigate to **Global Configuration** → **Other & Experimental**
|
|
250
256
|
2. Enable **Auto Approval (experimental)**
|
|
251
257
|
3. (Optional) Configure a custom command for verification
|
|
252
258
|
|
package/dist/components/App.js
CHANGED
|
@@ -14,7 +14,7 @@ import LoadingSpinner from './LoadingSpinner.js';
|
|
|
14
14
|
import { globalSessionOrchestrator } from '../services/globalSessionOrchestrator.js';
|
|
15
15
|
import { WorktreeService } from '../services/worktreeService.js';
|
|
16
16
|
import { AmbiguousBranchError, } from '../types/index.js';
|
|
17
|
-
import {
|
|
17
|
+
import { configReader } from '../services/config/configReader.js';
|
|
18
18
|
import { ENV_VARS } from '../constants/env.js';
|
|
19
19
|
import { MULTI_PROJECT_ERRORS } from '../constants/error.js';
|
|
20
20
|
import { projectManager } from '../services/projectManager.js';
|
|
@@ -28,6 +28,7 @@ const App = ({ devcontainerConfig, multiProject }) => {
|
|
|
28
28
|
const [menuKey, setMenuKey] = useState(0); // Force menu refresh
|
|
29
29
|
const [selectedWorktree, setSelectedWorktree] = useState(null); // Store selected worktree for preset selection
|
|
30
30
|
const [selectedProject, setSelectedProject] = useState(null); // Store selected project in multi-project mode
|
|
31
|
+
const [configScope, setConfigScope] = useState('global'); // Store config scope for configuration view
|
|
31
32
|
// State for remote branch disambiguation
|
|
32
33
|
const [pendingWorktreeCreation, setPendingWorktreeCreation] = useState(null);
|
|
33
34
|
// State for loading context - track flags for message composition
|
|
@@ -177,6 +178,19 @@ const App = ({ devcontainerConfig, multiProject }) => {
|
|
|
177
178
|
}
|
|
178
179
|
// Check if this is the configuration option
|
|
179
180
|
if (worktree.path === 'CONFIGURATION') {
|
|
181
|
+
setConfigScope('global');
|
|
182
|
+
navigateWithClear('configuration');
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
// Check if this is the project configuration option
|
|
186
|
+
if (worktree.path === 'CONFIGURATION_PROJECT') {
|
|
187
|
+
setConfigScope('project');
|
|
188
|
+
navigateWithClear('configuration');
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
// Check if this is the global configuration option
|
|
192
|
+
if (worktree.path === 'CONFIGURATION_GLOBAL') {
|
|
193
|
+
setConfigScope('global');
|
|
180
194
|
navigateWithClear('configuration');
|
|
181
195
|
return;
|
|
182
196
|
}
|
|
@@ -197,7 +211,7 @@ const App = ({ devcontainerConfig, multiProject }) => {
|
|
|
197
211
|
let session = sessionManager.getSession(worktree.path);
|
|
198
212
|
if (!session) {
|
|
199
213
|
// Check if we should show preset selector
|
|
200
|
-
if (
|
|
214
|
+
if (configReader.getSelectPresetOnStart()) {
|
|
201
215
|
setSelectedWorktree(worktree);
|
|
202
216
|
navigateWithClear('preset-selector');
|
|
203
217
|
return;
|
|
@@ -424,7 +438,7 @@ const App = ({ devcontainerConfig, multiProject }) => {
|
|
|
424
438
|
React.createElement(MergeWorktree, { onComplete: handleReturnToMenu, onCancel: handleReturnToMenu })));
|
|
425
439
|
}
|
|
426
440
|
if (view === 'configuration') {
|
|
427
|
-
return React.createElement(Configuration, { onComplete: handleReturnToMenu });
|
|
441
|
+
return (React.createElement(Configuration, { scope: configScope, onComplete: handleReturnToMenu }));
|
|
428
442
|
}
|
|
429
443
|
if (view === 'preset-selector') {
|
|
430
444
|
return (React.createElement(PresetSelector, { onSelect: handlePresetSelected, onCancel: handlePresetSelectorCancel }));
|
|
@@ -59,7 +59,7 @@ const getManagerForProjectMock = vi.fn((_) => {
|
|
|
59
59
|
sessionManagers.push(manager);
|
|
60
60
|
return manager;
|
|
61
61
|
});
|
|
62
|
-
const
|
|
62
|
+
const configReaderMock = {
|
|
63
63
|
getSelectPresetOnStart: vi.fn(() => false),
|
|
64
64
|
};
|
|
65
65
|
const projectManagerMock = {
|
|
@@ -91,8 +91,8 @@ vi.mock('../services/globalSessionOrchestrator.js', () => ({
|
|
|
91
91
|
vi.mock('../services/projectManager.js', () => ({
|
|
92
92
|
projectManager: projectManagerMock,
|
|
93
93
|
}));
|
|
94
|
-
vi.mock('../services/
|
|
95
|
-
|
|
94
|
+
vi.mock('../services/config/configReader.js', () => ({
|
|
95
|
+
configReader: configReaderMock,
|
|
96
96
|
}));
|
|
97
97
|
vi.mock('../services/worktreeService.js', () => ({
|
|
98
98
|
WorktreeService: vi.fn(function () {
|
|
@@ -152,8 +152,8 @@ beforeEach(() => {
|
|
|
152
152
|
deleteWorktreeEffectMock.mockImplementation(() => Effect.succeed(undefined));
|
|
153
153
|
sessionManagers.length = 0;
|
|
154
154
|
getManagerForProjectMock.mockClear();
|
|
155
|
-
|
|
156
|
-
|
|
155
|
+
configReaderMock.getSelectPresetOnStart.mockReset();
|
|
156
|
+
configReaderMock.getSelectPresetOnStart.mockReturnValue(false);
|
|
157
157
|
projectManagerMock.addRecentProject.mockReset();
|
|
158
158
|
});
|
|
159
159
|
afterEach(() => {
|
|
@@ -8,8 +8,10 @@ import ConfigureWorktree from './ConfigureWorktree.js';
|
|
|
8
8
|
import ConfigureCommand from './ConfigureCommand.js';
|
|
9
9
|
import ConfigureOther from './ConfigureOther.js';
|
|
10
10
|
import { shortcutManager } from '../services/shortcutManager.js';
|
|
11
|
-
|
|
11
|
+
import { ConfigEditorProvider } from '../contexts/ConfigEditorContext.js';
|
|
12
|
+
const ConfigurationContent = ({ scope, onComplete }) => {
|
|
12
13
|
const [view, setView] = useState('menu');
|
|
14
|
+
const title = scope === 'project' ? 'Project Configuration' : 'Global Configuration';
|
|
13
15
|
const menuItems = [
|
|
14
16
|
{
|
|
15
17
|
label: 'S ⌨ Configure Shortcuts',
|
|
@@ -119,9 +121,11 @@ const Configuration = ({ onComplete }) => {
|
|
|
119
121
|
}
|
|
120
122
|
return (React.createElement(Box, { flexDirection: "column" },
|
|
121
123
|
React.createElement(Box, { marginBottom: 1 },
|
|
122
|
-
React.createElement(Text, { bold: true, color: "green" },
|
|
124
|
+
React.createElement(Text, { bold: true, color: "green" }, title)),
|
|
123
125
|
React.createElement(Box, { marginBottom: 1 },
|
|
124
126
|
React.createElement(Text, { dimColor: true }, "Select a configuration option:")),
|
|
125
127
|
React.createElement(SelectInput, { items: menuItems, onSelect: handleSelect, isFocused: true, limit: 10 })));
|
|
126
128
|
};
|
|
129
|
+
const Configuration = ({ scope, onComplete }) => (React.createElement(ConfigEditorProvider, { scope: scope },
|
|
130
|
+
React.createElement(ConfigurationContent, { scope: scope, onComplete: onComplete })));
|
|
127
131
|
export default Configuration;
|
|
@@ -2,7 +2,7 @@ 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
|
import { shortcutManager } from '../services/shortcutManager.js';
|
|
7
7
|
import Confirmation from './Confirmation.js';
|
|
8
8
|
// This function ensures all strategies are included at compile time
|
|
@@ -55,10 +55,23 @@ const formatDetectionStrategy = (strategy) => {
|
|
|
55
55
|
}
|
|
56
56
|
};
|
|
57
57
|
const ConfigureCommand = ({ onComplete }) => {
|
|
58
|
-
const
|
|
58
|
+
const configEditor = useConfigEditor();
|
|
59
|
+
const scope = configEditor.getScope();
|
|
60
|
+
// Get initial presets based on scope
|
|
61
|
+
const presetsConfig = configEditor.getCommandPresets();
|
|
59
62
|
const [presets, setPresets] = useState(presetsConfig.presets);
|
|
60
63
|
const [defaultPresetId, setDefaultPresetId] = useState(presetsConfig.defaultPresetId);
|
|
61
|
-
const [selectPresetOnStart, setSelectPresetOnStart] = useState(
|
|
64
|
+
const [selectPresetOnStart, setSelectPresetOnStart] = useState(presetsConfig.selectPresetOnStart ?? false);
|
|
65
|
+
// Show if inheriting from global (for project scope)
|
|
66
|
+
const isInheriting = scope === 'project' && !configEditor.hasProjectOverride('commandPresets');
|
|
67
|
+
// Helper function to save presets config
|
|
68
|
+
const savePresetsConfig = (updatedPresets, updatedDefaultId, updatedSelectOnStart) => {
|
|
69
|
+
configEditor.setCommandPresets({
|
|
70
|
+
presets: updatedPresets,
|
|
71
|
+
defaultPresetId: updatedDefaultId ?? defaultPresetId,
|
|
72
|
+
selectPresetOnStart: updatedSelectOnStart ?? selectPresetOnStart,
|
|
73
|
+
});
|
|
74
|
+
};
|
|
62
75
|
const [viewMode, setViewMode] = useState('list');
|
|
63
76
|
const [selectedPresetId, setSelectedPresetId] = useState(null);
|
|
64
77
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
@@ -101,7 +114,7 @@ const ConfigureCommand = ({ onComplete }) => {
|
|
|
101
114
|
break;
|
|
102
115
|
case 'setDefault':
|
|
103
116
|
setDefaultPresetId(preset.id);
|
|
104
|
-
|
|
117
|
+
savePresetsConfig(presets, preset.id);
|
|
105
118
|
break;
|
|
106
119
|
case 'delete':
|
|
107
120
|
if (presets.length > 1) {
|
|
@@ -145,7 +158,7 @@ const ConfigureCommand = ({ onComplete }) => {
|
|
|
145
158
|
}
|
|
146
159
|
const updatedPresets = presets.map(p => p.id === preset.id ? updatedPreset : p);
|
|
147
160
|
setPresets(updatedPresets);
|
|
148
|
-
|
|
161
|
+
savePresetsConfig(updatedPresets);
|
|
149
162
|
setEditField(null);
|
|
150
163
|
setInputValue('');
|
|
151
164
|
setErrorMessage(null);
|
|
@@ -193,7 +206,7 @@ const ConfigureCommand = ({ onComplete }) => {
|
|
|
193
206
|
};
|
|
194
207
|
const updatedPresets = [...presets, completePreset];
|
|
195
208
|
setPresets(updatedPresets);
|
|
196
|
-
|
|
209
|
+
savePresetsConfig(updatedPresets);
|
|
197
210
|
setViewMode('list');
|
|
198
211
|
setSelectedIndex(updatedPresets.length - 1);
|
|
199
212
|
setNewPreset({});
|
|
@@ -213,7 +226,7 @@ const ConfigureCommand = ({ onComplete }) => {
|
|
|
213
226
|
updatedPreset.detectionStrategy = item.value;
|
|
214
227
|
const updatedPresets = presets.map(p => p.id === preset.id ? updatedPreset : p);
|
|
215
228
|
setPresets(updatedPresets);
|
|
216
|
-
|
|
229
|
+
savePresetsConfig(updatedPresets);
|
|
217
230
|
setIsSelectingStrategy(false);
|
|
218
231
|
};
|
|
219
232
|
const handleAddStrategySelect = (item) => {
|
|
@@ -234,14 +247,15 @@ const ConfigureCommand = ({ onComplete }) => {
|
|
|
234
247
|
const newPresets = presets.filter(p => p.id !== selectedPresetId);
|
|
235
248
|
setPresets(newPresets);
|
|
236
249
|
// Update default if needed
|
|
250
|
+
let newDefaultId = defaultPresetId;
|
|
237
251
|
if (defaultPresetId === selectedPresetId && newPresets.length > 0) {
|
|
238
252
|
const firstPreset = newPresets[0];
|
|
239
253
|
if (firstPreset) {
|
|
254
|
+
newDefaultId = firstPreset.id;
|
|
240
255
|
setDefaultPresetId(firstPreset.id);
|
|
241
|
-
configurationManager.setDefaultPreset(firstPreset.id);
|
|
242
256
|
}
|
|
243
257
|
}
|
|
244
|
-
|
|
258
|
+
savePresetsConfig(newPresets, newDefaultId);
|
|
245
259
|
setViewMode('list');
|
|
246
260
|
setSelectedIndex(0);
|
|
247
261
|
}
|
|
@@ -531,7 +545,7 @@ const ConfigureCommand = ({ onComplete }) => {
|
|
|
531
545
|
// Toggle select preset on start
|
|
532
546
|
const newValue = !selectPresetOnStart;
|
|
533
547
|
setSelectPresetOnStart(newValue);
|
|
534
|
-
|
|
548
|
+
savePresetsConfig(presets, undefined, newValue);
|
|
535
549
|
}
|
|
536
550
|
else if (item.value.startsWith('separator')) {
|
|
537
551
|
// Ignore separator selections
|
|
@@ -547,9 +561,18 @@ const ConfigureCommand = ({ onComplete }) => {
|
|
|
547
561
|
}
|
|
548
562
|
}
|
|
549
563
|
};
|
|
564
|
+
const scopeLabel = scope === 'project' ? 'Project' : 'Global';
|
|
550
565
|
return (React.createElement(Box, { flexDirection: "column" },
|
|
551
566
|
React.createElement(Box, { marginBottom: 1 },
|
|
552
|
-
React.createElement(Text, { bold: true, color: "green" },
|
|
567
|
+
React.createElement(Text, { bold: true, color: "green" },
|
|
568
|
+
"Command Presets (",
|
|
569
|
+
scopeLabel,
|
|
570
|
+
")")),
|
|
571
|
+
isInheriting && (React.createElement(Box, { marginBottom: 1 },
|
|
572
|
+
React.createElement(Text, { backgroundColor: "cyan", color: "black" },
|
|
573
|
+
' ',
|
|
574
|
+
"\uD83D\uDCCB Inheriting from global configuration",
|
|
575
|
+
' '))),
|
|
553
576
|
React.createElement(Box, { marginBottom: 1 },
|
|
554
577
|
React.createElement(Text, { dimColor: true }, "Configure command presets for running code sessions")),
|
|
555
578
|
React.createElement(SelectInput, { items: selectItems, onSelect: handleSelectItem, initialIndex: selectedIndex }),
|
|
@@ -1,19 +1,24 @@
|
|
|
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 { useConfigEditor } from '../contexts/ConfigEditorContext.js';
|
|
5
5
|
import { shortcutManager } from '../services/shortcutManager.js';
|
|
6
6
|
import ConfigureCustomCommand from './ConfigureCustomCommand.js';
|
|
7
7
|
import ConfigureTimeout from './ConfigureTimeout.js';
|
|
8
8
|
import CustomCommandSummary from './CustomCommandSummary.js';
|
|
9
9
|
const ConfigureOther = ({ onComplete }) => {
|
|
10
|
-
const
|
|
10
|
+
const configEditor = useConfigEditor();
|
|
11
|
+
const scope = configEditor.getScope();
|
|
12
|
+
// Get initial auto-approval config based on scope
|
|
13
|
+
const autoApprovalConfig = configEditor.getAutoApprovalConfig();
|
|
11
14
|
const [view, setView] = useState('main');
|
|
12
15
|
const [autoApprovalEnabled, setAutoApprovalEnabled] = useState(autoApprovalConfig.enabled);
|
|
13
16
|
const [customCommand, setCustomCommand] = useState(autoApprovalConfig.customCommand ?? '');
|
|
14
17
|
const [customCommandDraft, setCustomCommandDraft] = useState(customCommand);
|
|
15
18
|
const [timeout, setTimeout] = useState(autoApprovalConfig.timeout ?? 30);
|
|
16
19
|
const [timeoutDraft, setTimeoutDraft] = useState(timeout);
|
|
20
|
+
// Show if inheriting from global (for project scope)
|
|
21
|
+
const isInheriting = scope === 'project' && !configEditor.hasProjectOverride('autoApproval');
|
|
17
22
|
useInput((input, key) => {
|
|
18
23
|
if (shortcutManager.matchesShortcut('cancel', input, key)) {
|
|
19
24
|
if (view === 'customCommand') {
|
|
@@ -65,7 +70,7 @@ const ConfigureOther = ({ onComplete }) => {
|
|
|
65
70
|
setView('timeout');
|
|
66
71
|
break;
|
|
67
72
|
case 'save':
|
|
68
|
-
|
|
73
|
+
configEditor.setAutoApprovalConfig({
|
|
69
74
|
enabled: autoApprovalEnabled,
|
|
70
75
|
customCommand: customCommand.trim() || undefined,
|
|
71
76
|
timeout,
|
|
@@ -97,9 +102,18 @@ const ConfigureOther = ({ onComplete }) => {
|
|
|
97
102
|
setView('main');
|
|
98
103
|
} }));
|
|
99
104
|
}
|
|
105
|
+
const scopeLabel = scope === 'project' ? 'Project' : 'Global';
|
|
100
106
|
return (React.createElement(Box, { flexDirection: "column" },
|
|
101
107
|
React.createElement(Box, { marginBottom: 1 },
|
|
102
|
-
React.createElement(Text, { bold: true, color: "green" },
|
|
108
|
+
React.createElement(Text, { bold: true, color: "green" },
|
|
109
|
+
"Other & Experimental Settings (",
|
|
110
|
+
scopeLabel,
|
|
111
|
+
")")),
|
|
112
|
+
isInheriting && (React.createElement(Box, { marginBottom: 1 },
|
|
113
|
+
React.createElement(Text, { backgroundColor: "cyan", color: "black" },
|
|
114
|
+
' ',
|
|
115
|
+
"\uD83D\uDCCB Inheriting from global configuration",
|
|
116
|
+
' '))),
|
|
103
117
|
React.createElement(Box, { marginBottom: 1 },
|
|
104
118
|
React.createElement(Text, { dimColor: true }, "Toggle experimental capabilities and other miscellaneous options.")),
|
|
105
119
|
React.createElement(CustomCommandSummary, { command: customCommand }),
|
|
@@ -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 ConfigureOther from './ConfigureOther.js';
|
|
5
|
-
import {
|
|
5
|
+
import { ConfigEditorProvider } from '../contexts/ConfigEditorContext.js';
|
|
6
6
|
// Mock ink to avoid stdin issues during tests
|
|
7
7
|
vi.mock('ink', async () => {
|
|
8
8
|
const actual = await vi.importActual('ink');
|
|
@@ -19,12 +19,45 @@ vi.mock('ink-select-input', async () => {
|
|
|
19
19
|
default: ({ items }) => React.createElement(Box, { flexDirection: 'column' }, items.map((item, index) => React.createElement(Text, { key: index }, item.label))),
|
|
20
20
|
};
|
|
21
21
|
});
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
// Create mock functions that will be used by the mock class
|
|
23
|
+
const mockFns = {
|
|
24
|
+
getAutoApprovalConfig: vi.fn(),
|
|
25
|
+
setAutoApprovalConfig: vi.fn(),
|
|
26
|
+
hasProjectOverride: vi.fn().mockReturnValue(false),
|
|
27
|
+
getScope: vi.fn().mockReturnValue('global'),
|
|
28
|
+
};
|
|
29
|
+
vi.mock('../services/config/configEditor.js', () => {
|
|
30
|
+
return {
|
|
31
|
+
ConfigEditor: class {
|
|
32
|
+
constructor() {
|
|
33
|
+
Object.defineProperty(this, "getAutoApprovalConfig", {
|
|
34
|
+
enumerable: true,
|
|
35
|
+
configurable: true,
|
|
36
|
+
writable: true,
|
|
37
|
+
value: mockFns.getAutoApprovalConfig
|
|
38
|
+
});
|
|
39
|
+
Object.defineProperty(this, "setAutoApprovalConfig", {
|
|
40
|
+
enumerable: true,
|
|
41
|
+
configurable: true,
|
|
42
|
+
writable: true,
|
|
43
|
+
value: mockFns.setAutoApprovalConfig
|
|
44
|
+
});
|
|
45
|
+
Object.defineProperty(this, "hasProjectOverride", {
|
|
46
|
+
enumerable: true,
|
|
47
|
+
configurable: true,
|
|
48
|
+
writable: true,
|
|
49
|
+
value: mockFns.hasProjectOverride
|
|
50
|
+
});
|
|
51
|
+
Object.defineProperty(this, "getScope", {
|
|
52
|
+
enumerable: true,
|
|
53
|
+
configurable: true,
|
|
54
|
+
writable: true,
|
|
55
|
+
value: mockFns.getScope
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
});
|
|
28
61
|
vi.mock('../services/shortcutManager.js', () => ({
|
|
29
62
|
shortcutManager: {
|
|
30
63
|
matchesShortcut: vi.fn().mockReturnValue(false),
|
|
@@ -50,17 +83,18 @@ vi.mock('./CustomCommandSummary.js', async () => {
|
|
|
50
83
|
default: ({ command }) => React.createElement(Text, null, `Custom auto-approval command: ${command || 'Empty'}`),
|
|
51
84
|
};
|
|
52
85
|
});
|
|
53
|
-
const mockedConfigurationManager = configurationManager;
|
|
54
86
|
describe('ConfigureOther', () => {
|
|
55
87
|
beforeEach(() => {
|
|
56
88
|
vi.clearAllMocks();
|
|
57
89
|
});
|
|
58
90
|
it('renders experimental settings with auto-approval status', () => {
|
|
59
|
-
|
|
91
|
+
mockFns.getAutoApprovalConfig.mockReturnValue({
|
|
60
92
|
enabled: true,
|
|
61
93
|
customCommand: '',
|
|
94
|
+
timeout: 30,
|
|
62
95
|
});
|
|
63
|
-
const { lastFrame } = render(React.createElement(
|
|
96
|
+
const { lastFrame } = render(React.createElement(ConfigEditorProvider, { scope: "global" },
|
|
97
|
+
React.createElement(ConfigureOther, { onComplete: vi.fn() })));
|
|
64
98
|
expect(lastFrame()).toContain('Other & Experimental Settings');
|
|
65
99
|
expect(lastFrame()).toContain('Auto Approval (experimental): ✅ Enabled');
|
|
66
100
|
expect(lastFrame()).toContain('Custom auto-approval command: Empty');
|
|
@@ -68,11 +102,13 @@ describe('ConfigureOther', () => {
|
|
|
68
102
|
expect(lastFrame()).toContain('Save Changes');
|
|
69
103
|
});
|
|
70
104
|
it('shows current custom command summary', () => {
|
|
71
|
-
|
|
105
|
+
mockFns.getAutoApprovalConfig.mockReturnValue({
|
|
72
106
|
enabled: false,
|
|
73
107
|
customCommand: 'jq -n \'{"needsPermission":true}\'',
|
|
108
|
+
timeout: 30,
|
|
74
109
|
});
|
|
75
|
-
const { lastFrame } = render(React.createElement(
|
|
110
|
+
const { lastFrame } = render(React.createElement(ConfigEditorProvider, { scope: "global" },
|
|
111
|
+
React.createElement(ConfigureOther, { onComplete: vi.fn() })));
|
|
76
112
|
expect(lastFrame()).toContain('Custom auto-approval command:');
|
|
77
113
|
expect(lastFrame()).toContain('jq -n');
|
|
78
114
|
expect(lastFrame()).toContain('Edit Custom Command');
|
|
@@ -1,66 +1,18 @@
|
|
|
1
|
-
import React, { useState
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
2
|
import { Box, Text, useInput } from 'ink';
|
|
3
3
|
import SelectInput from 'ink-select-input';
|
|
4
|
-
import {
|
|
5
|
-
import { shortcutManager } from '../services/shortcutManager.js';
|
|
6
|
-
import { configurationManager } from '../services/configurationManager.js';
|
|
7
|
-
/**
|
|
8
|
-
* Format error using TaggedError discrimination
|
|
9
|
-
* Pattern matches on _tag for type-safe error display
|
|
10
|
-
*/
|
|
11
|
-
const formatError = (error) => {
|
|
12
|
-
switch (error._tag) {
|
|
13
|
-
case 'FileSystemError':
|
|
14
|
-
return `File ${error.operation} failed for ${error.path}: ${error.cause}`;
|
|
15
|
-
case 'ConfigError':
|
|
16
|
-
return `Configuration error (${error.reason}): ${error.details}`;
|
|
17
|
-
case 'ValidationError':
|
|
18
|
-
return `Validation failed for ${error.field}: ${error.constraint}`;
|
|
19
|
-
case 'GitError':
|
|
20
|
-
return `Git command failed: ${error.command} (exit ${error.exitCode})\n${error.stderr}`;
|
|
21
|
-
case 'ProcessError':
|
|
22
|
-
return `Process error: ${error.message}`;
|
|
23
|
-
}
|
|
24
|
-
};
|
|
4
|
+
import { useConfigEditor } from '../contexts/ConfigEditorContext.js';
|
|
25
5
|
const ConfigureShortcuts = ({ onComplete, }) => {
|
|
6
|
+
const configEditor = useConfigEditor();
|
|
7
|
+
const scope = configEditor.getScope();
|
|
26
8
|
const [step, setStep] = useState('menu');
|
|
27
|
-
|
|
9
|
+
// Get initial shortcuts based on scope
|
|
10
|
+
const initialShortcuts = configEditor.getShortcuts();
|
|
11
|
+
const [shortcuts, setShortcuts] = useState(initialShortcuts);
|
|
28
12
|
const [editingShortcut, setEditingShortcut] = useState(null);
|
|
29
13
|
const [error, setError] = useState(null);
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
useEffect(() => {
|
|
33
|
-
let cancelled = false;
|
|
34
|
-
const loadConfig = async () => {
|
|
35
|
-
const result = await Effect.runPromise(Effect.match(configurationManager.loadConfigEffect(), {
|
|
36
|
-
onFailure: (err) => ({
|
|
37
|
-
type: 'error',
|
|
38
|
-
error: err,
|
|
39
|
-
}),
|
|
40
|
-
onSuccess: config => ({ type: 'success', data: config }),
|
|
41
|
-
}));
|
|
42
|
-
if (!cancelled) {
|
|
43
|
-
if (result.type === 'error') {
|
|
44
|
-
// Display error using TaggedError discrimination
|
|
45
|
-
const errorMsg = formatError(result.error);
|
|
46
|
-
setError(errorMsg);
|
|
47
|
-
}
|
|
48
|
-
else if (result.data.shortcuts) {
|
|
49
|
-
setShortcuts(result.data.shortcuts);
|
|
50
|
-
}
|
|
51
|
-
setIsLoading(false);
|
|
52
|
-
}
|
|
53
|
-
};
|
|
54
|
-
loadConfig().catch(err => {
|
|
55
|
-
if (!cancelled) {
|
|
56
|
-
setError(`Unexpected error loading config: ${String(err)}`);
|
|
57
|
-
setIsLoading(false);
|
|
58
|
-
}
|
|
59
|
-
});
|
|
60
|
-
return () => {
|
|
61
|
-
cancelled = true;
|
|
62
|
-
};
|
|
63
|
-
}, []);
|
|
14
|
+
// Show if inheriting from global (for project scope)
|
|
15
|
+
const isInheriting = scope === 'project' && !configEditor.hasProjectOverride('shortcuts');
|
|
64
16
|
const getShortcutDisplayFromState = (key) => {
|
|
65
17
|
const shortcut = shortcuts[key];
|
|
66
18
|
if (!shortcut)
|
|
@@ -147,28 +99,14 @@ const ConfigureShortcuts = ({ onComplete, }) => {
|
|
|
147
99
|
return;
|
|
148
100
|
}
|
|
149
101
|
if (item.value === 'save') {
|
|
150
|
-
// Save shortcuts using
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}));
|
|
159
|
-
if (result.type === 'error') {
|
|
160
|
-
// Display error using TaggedError discrimination
|
|
161
|
-
const errorMsg = formatError(result.error);
|
|
162
|
-
setError(errorMsg);
|
|
163
|
-
}
|
|
164
|
-
else {
|
|
165
|
-
// Success - call onComplete
|
|
166
|
-
onComplete();
|
|
167
|
-
}
|
|
168
|
-
};
|
|
169
|
-
saveConfig().catch(err => {
|
|
170
|
-
setError(`Unexpected error saving shortcuts: ${String(err)}`);
|
|
171
|
-
});
|
|
102
|
+
// Save shortcuts using ConfigEditor
|
|
103
|
+
try {
|
|
104
|
+
configEditor.setShortcuts(shortcuts);
|
|
105
|
+
onComplete();
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
setError(`Error saving shortcuts: ${String(err)}`);
|
|
109
|
+
}
|
|
172
110
|
return;
|
|
173
111
|
}
|
|
174
112
|
if (item.value === 'exit') {
|
|
@@ -180,11 +118,6 @@ const ConfigureShortcuts = ({ onComplete, }) => {
|
|
|
180
118
|
setStep('capturing');
|
|
181
119
|
setError(null);
|
|
182
120
|
};
|
|
183
|
-
// Show loading indicator while loading config
|
|
184
|
-
if (isLoading) {
|
|
185
|
-
return (React.createElement(Box, { flexDirection: "column" },
|
|
186
|
-
React.createElement(Text, null, "Loading configuration...")));
|
|
187
|
-
}
|
|
188
121
|
if (step === 'capturing') {
|
|
189
122
|
return (React.createElement(Box, { flexDirection: "column" },
|
|
190
123
|
React.createElement(Text, { bold: true, color: "green" },
|
|
@@ -197,9 +130,18 @@ const ConfigureShortcuts = ({ onComplete, }) => {
|
|
|
197
130
|
React.createElement(Box, { marginTop: 1 },
|
|
198
131
|
React.createElement(Text, { dimColor: true }, "Reserved: Ctrl+C, Ctrl+D, Ctrl+[ (Esc)"))));
|
|
199
132
|
}
|
|
133
|
+
const scopeLabel = scope === 'project' ? 'Project' : 'Global';
|
|
200
134
|
return (React.createElement(Box, { flexDirection: "column" },
|
|
201
135
|
React.createElement(Box, { marginBottom: 1 },
|
|
202
|
-
React.createElement(Text, { bold: true, color: "green" },
|
|
136
|
+
React.createElement(Text, { bold: true, color: "green" },
|
|
137
|
+
"Configure Keyboard Shortcuts (",
|
|
138
|
+
scopeLabel,
|
|
139
|
+
")")),
|
|
140
|
+
isInheriting && (React.createElement(Box, { marginBottom: 1 },
|
|
141
|
+
React.createElement(Text, { backgroundColor: "cyan", color: "black" },
|
|
142
|
+
' ',
|
|
143
|
+
"\uD83D\uDCCB Inheriting from global configuration",
|
|
144
|
+
' '))),
|
|
203
145
|
error && (React.createElement(Box, { marginBottom: 1 },
|
|
204
146
|
React.createElement(Text, { color: "red" },
|
|
205
147
|
"Error: ",
|
|
@@ -2,7 +2,7 @@ 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 STATUS_LABELS = {
|
|
7
7
|
idle: 'Idle',
|
|
8
8
|
busy: 'Busy',
|
|
@@ -10,12 +10,18 @@ const STATUS_LABELS = {
|
|
|
10
10
|
pending_auto_approval: 'Pending Auto Approval',
|
|
11
11
|
};
|
|
12
12
|
const ConfigureStatusHooks = ({ onComplete, }) => {
|
|
13
|
+
const configEditor = useConfigEditor();
|
|
14
|
+
const scope = configEditor.getScope();
|
|
13
15
|
const [view, setView] = useState('menu');
|
|
14
16
|
const [selectedStatus, setSelectedStatus] = useState('idle');
|
|
15
|
-
|
|
17
|
+
// Get initial status hooks based on scope
|
|
18
|
+
const initialStatusHooks = configEditor.getStatusHooks() ?? {};
|
|
19
|
+
const [statusHooks, setStatusHooks] = useState(initialStatusHooks);
|
|
16
20
|
const [currentCommand, setCurrentCommand] = useState('');
|
|
17
21
|
const [currentEnabled, setCurrentEnabled] = useState(false);
|
|
18
22
|
const [showSaveMessage, setShowSaveMessage] = useState(false);
|
|
23
|
+
// Show if inheriting from global (for project scope)
|
|
24
|
+
const isInheriting = scope === 'project' && !configEditor.hasProjectOverride('statusHooks');
|
|
19
25
|
useInput((input, key) => {
|
|
20
26
|
if (key.escape) {
|
|
21
27
|
if (view === 'edit') {
|
|
@@ -62,7 +68,7 @@ const ConfigureStatusHooks = ({ onComplete, }) => {
|
|
|
62
68
|
};
|
|
63
69
|
const handleMenuSelect = (item) => {
|
|
64
70
|
if (item.value === 'save') {
|
|
65
|
-
|
|
71
|
+
configEditor.setStatusHooks(statusHooks);
|
|
66
72
|
setShowSaveMessage(true);
|
|
67
73
|
setTimeout(() => {
|
|
68
74
|
onComplete();
|
|
@@ -125,9 +131,18 @@ const ConfigureStatusHooks = ({ onComplete, }) => {
|
|
|
125
131
|
React.createElement(Box, { marginTop: 1 },
|
|
126
132
|
React.createElement(Text, { dimColor: true }, "Press Enter to save, Tab to toggle enabled, Esc to cancel"))));
|
|
127
133
|
}
|
|
134
|
+
const scopeLabel = scope === 'project' ? 'Project' : 'Global';
|
|
128
135
|
return (React.createElement(Box, { flexDirection: "column" },
|
|
129
136
|
React.createElement(Box, { marginBottom: 1 },
|
|
130
|
-
React.createElement(Text, { bold: true, color: "green" },
|
|
137
|
+
React.createElement(Text, { bold: true, color: "green" },
|
|
138
|
+
"Configure Status Hooks (",
|
|
139
|
+
scopeLabel,
|
|
140
|
+
")")),
|
|
141
|
+
isInheriting && (React.createElement(Box, { marginBottom: 1 },
|
|
142
|
+
React.createElement(Text, { backgroundColor: "cyan", color: "black" },
|
|
143
|
+
' ',
|
|
144
|
+
"\uD83D\uDCCB Inheriting from global configuration",
|
|
145
|
+
' '))),
|
|
131
146
|
React.createElement(Box, { marginBottom: 1 },
|
|
132
147
|
React.createElement(Text, { dimColor: true }, "Set commands to run when session status changes:")),
|
|
133
148
|
React.createElement(SelectInput, { items: getMenuItems(), onSelect: handleMenuSelect, isFocused: true, limit: 10 }),
|