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.
Files changed (89) hide show
  1. package/README.md +11 -5
  2. package/dist/components/App.js +17 -3
  3. package/dist/components/App.test.js +5 -5
  4. package/dist/components/Configuration.d.ts +2 -0
  5. package/dist/components/Configuration.js +6 -2
  6. package/dist/components/ConfigureCommand.js +34 -11
  7. package/dist/components/ConfigureOther.js +18 -4
  8. package/dist/components/ConfigureOther.test.js +48 -12
  9. package/dist/components/ConfigureShortcuts.js +27 -85
  10. package/dist/components/ConfigureStatusHooks.js +19 -4
  11. package/dist/components/ConfigureStatusHooks.test.js +46 -12
  12. package/dist/components/ConfigureWorktree.js +18 -4
  13. package/dist/components/ConfigureWorktreeHooks.js +19 -4
  14. package/dist/components/ConfigureWorktreeHooks.test.js +49 -14
  15. package/dist/components/Menu.js +72 -14
  16. package/dist/components/NewWorktree.js +2 -2
  17. package/dist/components/NewWorktree.test.js +6 -6
  18. package/dist/components/PresetSelector.js +2 -2
  19. package/dist/constants/statusIcons.d.ts +2 -1
  20. package/dist/constants/statusIcons.js +13 -4
  21. package/dist/constants/statusIcons.test.js +41 -11
  22. package/dist/contexts/ConfigEditorContext.d.ts +21 -0
  23. package/dist/contexts/ConfigEditorContext.js +25 -0
  24. package/dist/services/autoApprovalVerifier.js +3 -3
  25. package/dist/services/autoApprovalVerifier.test.js +2 -2
  26. package/dist/services/config/configEditor.d.ts +46 -0
  27. package/dist/services/{configurationManager.effect.test.js → config/configEditor.effect.test.js} +46 -49
  28. package/dist/services/config/configEditor.js +101 -0
  29. package/dist/services/{configurationManager.selectPresetOnStart.test.js → config/configEditor.selectPresetOnStart.test.js} +27 -19
  30. package/dist/services/{configurationManager.test.js → config/configEditor.test.js} +60 -132
  31. package/dist/services/config/configReader.d.ts +28 -0
  32. package/dist/services/config/configReader.js +95 -0
  33. package/dist/services/config/configReader.multiProject.test.d.ts +1 -0
  34. package/dist/services/config/configReader.multiProject.test.js +136 -0
  35. package/dist/services/config/globalConfigManager.d.ts +30 -0
  36. package/dist/services/config/globalConfigManager.js +216 -0
  37. package/dist/services/config/index.d.ts +13 -0
  38. package/dist/services/config/index.js +13 -0
  39. package/dist/services/config/projectConfigManager.d.ts +41 -0
  40. package/dist/services/config/projectConfigManager.js +181 -0
  41. package/dist/services/config/projectConfigManager.test.d.ts +1 -0
  42. package/dist/services/config/projectConfigManager.test.js +105 -0
  43. package/dist/services/config/testUtils.d.ts +81 -0
  44. package/dist/services/config/testUtils.js +351 -0
  45. package/dist/services/sessionManager.autoApproval.test.js +5 -5
  46. package/dist/services/sessionManager.d.ts +1 -1
  47. package/dist/services/sessionManager.effect.test.js +27 -18
  48. package/dist/services/sessionManager.js +26 -44
  49. package/dist/services/sessionManager.statePersistence.test.js +5 -4
  50. package/dist/services/sessionManager.test.js +91 -50
  51. package/dist/services/shortcutManager.d.ts +0 -1
  52. package/dist/services/shortcutManager.js +5 -16
  53. package/dist/services/shortcutManager.test.js +2 -2
  54. package/dist/services/stateDetector/base.d.ts +1 -1
  55. package/dist/services/stateDetector/claude.d.ts +1 -1
  56. package/dist/services/stateDetector/claude.js +11 -4
  57. package/dist/services/stateDetector/claude.test.js +47 -24
  58. package/dist/services/stateDetector/cline.d.ts +1 -1
  59. package/dist/services/stateDetector/cline.js +1 -1
  60. package/dist/services/stateDetector/codex.d.ts +1 -1
  61. package/dist/services/stateDetector/codex.js +1 -1
  62. package/dist/services/stateDetector/cursor.d.ts +1 -1
  63. package/dist/services/stateDetector/cursor.js +1 -1
  64. package/dist/services/stateDetector/gemini.d.ts +1 -1
  65. package/dist/services/stateDetector/gemini.js +1 -1
  66. package/dist/services/stateDetector/github-copilot.d.ts +1 -1
  67. package/dist/services/stateDetector/github-copilot.js +1 -1
  68. package/dist/services/stateDetector/opencode.d.ts +1 -1
  69. package/dist/services/stateDetector/opencode.js +1 -1
  70. package/dist/services/stateDetector/types.d.ts +1 -1
  71. package/dist/services/worktreeService.d.ts +12 -0
  72. package/dist/services/worktreeService.js +24 -4
  73. package/dist/services/worktreeService.sort.test.js +105 -109
  74. package/dist/services/worktreeService.test.js +5 -5
  75. package/dist/types/index.d.ts +41 -7
  76. package/dist/utils/gitUtils.d.ts +8 -0
  77. package/dist/utils/gitUtils.js +32 -0
  78. package/dist/utils/hookExecutor.js +2 -2
  79. package/dist/utils/hookExecutor.test.js +8 -12
  80. package/dist/utils/mutex.d.ts +1 -1
  81. package/dist/utils/mutex.js +1 -1
  82. package/dist/utils/worktreeUtils.js +1 -1
  83. package/dist/utils/worktreeUtils.test.js +0 -1
  84. package/package.json +7 -7
  85. package/dist/services/configurationManager.d.ts +0 -121
  86. package/dist/services/configurationManager.js +0 -597
  87. /package/dist/services/{configurationManager.effect.test.d.ts → config/configEditor.effect.test.d.ts} +0 -0
  88. /package/dist/services/{configurationManager.selectPresetOnStart.test.d.ts → config/configEditor.selectPresetOnStart.test.d.ts} +0 -0
  89. /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 "Configuration""Configure Shortcuts" from the main menu
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
 
@@ -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 { configurationManager } from '../services/configurationManager.js';
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 (configurationManager.getSelectPresetOnStart()) {
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 configurationManagerMock = {
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/configurationManager.js', () => ({
95
- configurationManager: configurationManagerMock,
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
- configurationManagerMock.getSelectPresetOnStart.mockReset();
156
- configurationManagerMock.getSelectPresetOnStart.mockReturnValue(false);
155
+ configReaderMock.getSelectPresetOnStart.mockReset();
156
+ configReaderMock.getSelectPresetOnStart.mockReturnValue(false);
157
157
  projectManagerMock.addRecentProject.mockReset();
158
158
  });
159
159
  afterEach(() => {
@@ -1,5 +1,7 @@
1
1
  import React from 'react';
2
+ import { ConfigScope } from '../types/index.js';
2
3
  interface ConfigurationProps {
4
+ scope: ConfigScope;
3
5
  onComplete: () => void;
4
6
  }
5
7
  declare const Configuration: React.FC<ConfigurationProps>;
@@ -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
- const Configuration = ({ onComplete }) => {
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" }, "Configuration")),
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 { configurationManager } from '../services/configurationManager.js';
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 presetsConfig = configurationManager.getCommandPresets();
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(configurationManager.getSelectPresetOnStart());
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
- configurationManager.setDefaultPreset(preset.id);
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
- configurationManager.addPreset(updatedPreset);
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
- configurationManager.addPreset(completePreset);
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
- configurationManager.addPreset(updatedPreset);
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
- configurationManager.deletePreset(selectedPresetId);
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
- configurationManager.setSelectPresetOnStart(newValue);
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" }, "Command Command Presets")),
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 { configurationManager } from '../services/configurationManager.js';
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 autoApprovalConfig = configurationManager.getAutoApprovalConfig();
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
- configurationManager.setAutoApprovalConfig({
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" }, "Other & Experimental Settings")),
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 { configurationManager } from '../services/configurationManager.js';
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
- vi.mock('../services/configurationManager.js', () => ({
23
- configurationManager: {
24
- getAutoApprovalConfig: vi.fn(),
25
- setAutoApprovalConfig: vi.fn(),
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
- mockedConfigurationManager.getAutoApprovalConfig.mockReturnValue({
91
+ mockFns.getAutoApprovalConfig.mockReturnValue({
60
92
  enabled: true,
61
93
  customCommand: '',
94
+ timeout: 30,
62
95
  });
63
- const { lastFrame } = render(React.createElement(ConfigureOther, { onComplete: vi.fn() }));
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
- mockedConfigurationManager.getAutoApprovalConfig.mockReturnValue({
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(ConfigureOther, { onComplete: vi.fn() }));
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, useEffect } from 'react';
1
+ import React, { useState } from 'react';
2
2
  import { Box, Text, useInput } from 'ink';
3
3
  import SelectInput from 'ink-select-input';
4
- import { Effect } from 'effect';
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
- const [shortcuts, setShortcuts] = useState(shortcutManager.getShortcuts());
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
- const [isLoading, setIsLoading] = useState(true);
31
- // Load configuration using Effect on component mount
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 Effect-based method
151
- const saveConfig = async () => {
152
- const result = await Effect.runPromise(Effect.match(configurationManager.setShortcutsEffect(shortcuts), {
153
- onFailure: (err) => ({
154
- type: 'error',
155
- error: err,
156
- }),
157
- onSuccess: () => ({ type: 'success' }),
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" }, "Configure Keyboard Shortcuts")),
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 { configurationManager } from '../services/configurationManager.js';
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
- const [statusHooks, setStatusHooks] = useState(configurationManager.getStatusHooks());
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
- configurationManager.setStatusHooks(statusHooks);
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" }, "Configure Status Hooks")),
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 }),