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