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
@@ -1,4 +1,4 @@
1
- import { configurationManager } from './configurationManager.js';
1
+ import { configReader } from './config/configReader.js';
2
2
  export class ShortcutManager {
3
3
  constructor() {
4
4
  Object.defineProperty(this, "reservedKeys", {
@@ -46,19 +46,8 @@ export class ShortcutManager {
46
46
  reserved.alt === shortcut.alt &&
47
47
  reserved.shift === shortcut.shift);
48
48
  }
49
- saveShortcuts(shortcuts) {
50
- // Validate all shortcuts
51
- const currentShortcuts = configurationManager.getShortcuts();
52
- const validated = {
53
- returnToMenu: this.validateShortcut(shortcuts.returnToMenu) ||
54
- currentShortcuts.returnToMenu,
55
- cancel: this.validateShortcut(shortcuts.cancel) || currentShortcuts.cancel,
56
- };
57
- configurationManager.setShortcuts(validated);
58
- return true;
59
- }
60
49
  getShortcuts() {
61
- return configurationManager.getShortcuts();
50
+ return configReader.getShortcuts();
62
51
  }
63
52
  getRawShortcutCodes(shortcut) {
64
53
  const codes = new Set();
@@ -106,7 +95,7 @@ export class ShortcutManager {
106
95
  return Array.from(codes);
107
96
  }
108
97
  matchesShortcut(shortcutName, input, key) {
109
- const shortcuts = configurationManager.getShortcuts();
98
+ const shortcuts = configReader.getShortcuts();
110
99
  const shortcut = shortcuts[shortcutName];
111
100
  if (!shortcut)
112
101
  return false;
@@ -125,7 +114,7 @@ export class ShortcutManager {
125
114
  return input.toLowerCase() === shortcut.key.toLowerCase();
126
115
  }
127
116
  getShortcutDisplay(shortcutName) {
128
- const shortcuts = configurationManager.getShortcuts();
117
+ const shortcuts = configReader.getShortcuts();
129
118
  const shortcut = shortcuts[shortcutName];
130
119
  if (!shortcut)
131
120
  return '';
@@ -161,7 +150,7 @@ export class ShortcutManager {
161
150
  return null;
162
151
  }
163
152
  matchesRawInput(shortcutName, input) {
164
- const shortcuts = configurationManager.getShortcuts();
153
+ const shortcuts = configReader.getShortcuts();
165
154
  const shortcut = shortcuts[shortcutName];
166
155
  if (!shortcut)
167
156
  return false;
@@ -1,13 +1,13 @@
1
1
  import { describe, expect, it, beforeEach, afterEach, vi } from 'vitest';
2
2
  import { shortcutManager } from './shortcutManager.js';
3
- import { configurationManager } from './configurationManager.js';
3
+ import { configReader } from './config/configReader.js';
4
4
  describe('shortcutManager.matchesRawInput', () => {
5
5
  const shortcuts = {
6
6
  returnToMenu: { ctrl: true, key: 'e', alt: false, shift: false },
7
7
  cancel: { ctrl: true, key: 'c', alt: false, shift: false },
8
8
  };
9
9
  beforeEach(() => {
10
- vi.spyOn(configurationManager, 'getShortcuts').mockReturnValue(shortcuts);
10
+ vi.spyOn(configReader, 'getShortcuts').mockReturnValue(shortcuts);
11
11
  });
12
12
  afterEach(() => {
13
13
  vi.restoreAllMocks();
@@ -4,4 +4,5 @@ export declare abstract class BaseStateDetector implements StateDetector {
4
4
  abstract detectState(terminal: Terminal, currentState: SessionState): SessionState;
5
5
  protected getTerminalLines(terminal: Terminal, maxLines?: number): string[];
6
6
  protected getTerminalContent(terminal: Terminal, maxLines?: number): string;
7
+ abstract detectBackgroundTask(terminal: Terminal): boolean;
7
8
  }
@@ -2,4 +2,5 @@ import { SessionState, Terminal } from '../../types/index.js';
2
2
  import { BaseStateDetector } from './base.js';
3
3
  export declare class ClaudeStateDetector extends BaseStateDetector {
4
4
  detectState(terminal: Terminal, currentState: SessionState): SessionState;
5
+ detectBackgroundTask(terminal: Terminal): boolean;
5
6
  }
@@ -24,4 +24,12 @@ export class ClaudeStateDetector extends BaseStateDetector {
24
24
  // Otherwise idle
25
25
  return 'idle';
26
26
  }
27
+ detectBackgroundTask(terminal) {
28
+ const lines = this.getTerminalLines(terminal, 3);
29
+ const content = lines.join('\n').toLowerCase();
30
+ // Detect background task patterns:
31
+ // - "N background task(s)" in status bar
32
+ // - "(running)" in status bar for active background commands
33
+ return content.includes('background task') || content.includes('(running)');
34
+ }
27
35
  }
@@ -222,4 +222,106 @@ describe('ClaudeStateDetector', () => {
222
222
  expect(state).toBe('waiting_input');
223
223
  });
224
224
  });
225
+ describe('detectBackgroundTask', () => {
226
+ it('should detect background task when pattern is in last 3 lines (status bar)', () => {
227
+ // Arrange
228
+ terminal = createMockTerminal([
229
+ 'Previous conversation content',
230
+ 'More content',
231
+ '> Some command output',
232
+ '1 background task | api-call',
233
+ ]);
234
+ // Act
235
+ const hasBackgroundTask = detector.detectBackgroundTask(terminal);
236
+ // Assert
237
+ expect(hasBackgroundTask).toBe(true);
238
+ });
239
+ it('should detect background task with plural "background tasks"', () => {
240
+ // Arrange
241
+ terminal = createMockTerminal([
242
+ 'Some output',
243
+ 'More output',
244
+ '2 background tasks running',
245
+ ]);
246
+ // Act
247
+ const hasBackgroundTask = detector.detectBackgroundTask(terminal);
248
+ // Assert
249
+ expect(hasBackgroundTask).toBe(true);
250
+ });
251
+ it('should detect background task case-insensitively', () => {
252
+ // Arrange
253
+ terminal = createMockTerminal([
254
+ 'Output line 1',
255
+ 'Output line 2',
256
+ '1 BACKGROUND TASK running',
257
+ ]);
258
+ // Act
259
+ const hasBackgroundTask = detector.detectBackgroundTask(terminal);
260
+ // Assert
261
+ expect(hasBackgroundTask).toBe(true);
262
+ });
263
+ it('should return false when no background task pattern in last 3 lines', () => {
264
+ // Arrange
265
+ terminal = createMockTerminal([
266
+ 'Command completed successfully',
267
+ 'Ready for next command',
268
+ '> ',
269
+ ]);
270
+ // Act
271
+ const hasBackgroundTask = detector.detectBackgroundTask(terminal);
272
+ // Assert
273
+ expect(hasBackgroundTask).toBe(false);
274
+ });
275
+ it('should not detect background task when pattern is in conversation content (not status bar)', () => {
276
+ // Arrange - "background task" mentioned earlier in conversation, but not in last 3 lines
277
+ terminal = createMockTerminal([
278
+ 'User: Tell me about background task handling',
279
+ 'Assistant: Background task detection works by...',
280
+ 'The pattern "background task" appears in text but...',
281
+ 'This is the status bar area',
282
+ '> idle',
283
+ 'Ready',
284
+ ]);
285
+ // Act
286
+ const hasBackgroundTask = detector.detectBackgroundTask(terminal);
287
+ // Assert - should only check last 3 lines, not the conversation content
288
+ expect(hasBackgroundTask).toBe(false);
289
+ });
290
+ it('should handle empty terminal', () => {
291
+ // Arrange
292
+ terminal = createMockTerminal([]);
293
+ // Act
294
+ const hasBackgroundTask = detector.detectBackgroundTask(terminal);
295
+ // Assert
296
+ expect(hasBackgroundTask).toBe(false);
297
+ });
298
+ it('should handle terminal with fewer than 3 lines', () => {
299
+ // Arrange
300
+ terminal = createMockTerminal(['1 background task']);
301
+ // Act
302
+ const hasBackgroundTask = detector.detectBackgroundTask(terminal);
303
+ // Assert
304
+ expect(hasBackgroundTask).toBe(true);
305
+ });
306
+ it('should detect "(running)" status bar indicator', () => {
307
+ // Arrange
308
+ terminal = createMockTerminal([
309
+ 'Some conversation output',
310
+ 'More output',
311
+ 'bypass permissions on - uv run pytest tests/integration/e2e/tes... (running)',
312
+ ]);
313
+ // Act
314
+ const hasBackgroundTask = detector.detectBackgroundTask(terminal);
315
+ // Assert
316
+ expect(hasBackgroundTask).toBe(true);
317
+ });
318
+ it('should detect "(running)" case-insensitively', () => {
319
+ // Arrange
320
+ terminal = createMockTerminal(['Some output', 'command name (RUNNING)']);
321
+ // Act
322
+ const hasBackgroundTask = detector.detectBackgroundTask(terminal);
323
+ // Assert
324
+ expect(hasBackgroundTask).toBe(true);
325
+ });
326
+ });
225
327
  });
@@ -2,4 +2,5 @@ import { SessionState, Terminal } from '../../types/index.js';
2
2
  import { BaseStateDetector } from './base.js';
3
3
  export declare class ClineStateDetector extends BaseStateDetector {
4
4
  detectState(terminal: Terminal, _currentState: SessionState): SessionState;
5
+ detectBackgroundTask(_terminal: Terminal): boolean;
5
6
  }
@@ -21,4 +21,7 @@ export class ClineStateDetector extends BaseStateDetector {
21
21
  // Otherwise busy - Priority 3
22
22
  return 'busy';
23
23
  }
24
+ detectBackgroundTask(_terminal) {
25
+ return false;
26
+ }
24
27
  }
@@ -2,4 +2,5 @@ import { SessionState, Terminal } from '../../types/index.js';
2
2
  import { BaseStateDetector } from './base.js';
3
3
  export declare class CodexStateDetector extends BaseStateDetector {
4
4
  detectState(terminal: Terminal, _currentState: SessionState): SessionState;
5
+ detectBackgroundTask(_terminal: Terminal): boolean;
5
6
  }
@@ -24,4 +24,7 @@ export class CodexStateDetector extends BaseStateDetector {
24
24
  // Otherwise idle
25
25
  return 'idle';
26
26
  }
27
+ detectBackgroundTask(_terminal) {
28
+ return false;
29
+ }
27
30
  }
@@ -2,4 +2,5 @@ import { SessionState, Terminal } from '../../types/index.js';
2
2
  import { BaseStateDetector } from './base.js';
3
3
  export declare class CursorStateDetector extends BaseStateDetector {
4
4
  detectState(terminal: Terminal, _currentState: SessionState): SessionState;
5
+ detectBackgroundTask(_terminal: Terminal): boolean;
5
6
  }
@@ -16,4 +16,7 @@ export class CursorStateDetector extends BaseStateDetector {
16
16
  // Otherwise idle - Priority 3
17
17
  return 'idle';
18
18
  }
19
+ detectBackgroundTask(_terminal) {
20
+ return false;
21
+ }
19
22
  }
@@ -2,4 +2,5 @@ import { SessionState, Terminal } from '../../types/index.js';
2
2
  import { BaseStateDetector } from './base.js';
3
3
  export declare class GeminiStateDetector extends BaseStateDetector {
4
4
  detectState(terminal: Terminal, _currentState: SessionState): SessionState;
5
+ detectBackgroundTask(_terminal: Terminal): boolean;
5
6
  }
@@ -25,4 +25,7 @@ export class GeminiStateDetector extends BaseStateDetector {
25
25
  // Otherwise idle
26
26
  return 'idle';
27
27
  }
28
+ detectBackgroundTask(_terminal) {
29
+ return false;
30
+ }
28
31
  }
@@ -2,4 +2,5 @@ import { SessionState, Terminal } from '../../types/index.js';
2
2
  import { BaseStateDetector } from './base.js';
3
3
  export declare class GitHubCopilotStateDetector extends BaseStateDetector {
4
4
  detectState(terminal: Terminal, _currentState: SessionState): SessionState;
5
+ detectBackgroundTask(_terminal: Terminal): boolean;
5
6
  }
@@ -18,4 +18,7 @@ export class GitHubCopilotStateDetector extends BaseStateDetector {
18
18
  // Otherwise idle as priority 4
19
19
  return 'idle';
20
20
  }
21
+ detectBackgroundTask(_terminal) {
22
+ return false;
23
+ }
21
24
  }
@@ -2,4 +2,5 @@ import { SessionState, Terminal } from '../../types/index.js';
2
2
  import { BaseStateDetector } from './base.js';
3
3
  export declare class OpenCodeStateDetector extends BaseStateDetector {
4
4
  detectState(terminal: Terminal, _currentState: SessionState): SessionState;
5
+ detectBackgroundTask(_terminal: Terminal): boolean;
5
6
  }
@@ -14,4 +14,7 @@ export class OpenCodeStateDetector extends BaseStateDetector {
14
14
  // Otherwise idle
15
15
  return 'idle';
16
16
  }
17
+ detectBackgroundTask(_terminal) {
18
+ return false;
19
+ }
17
20
  }
@@ -1,4 +1,5 @@
1
1
  import { SessionState, Terminal } from '../../types/index.js';
2
2
  export interface StateDetector {
3
3
  detectState(terminal: Terminal, currentState: SessionState): SessionState;
4
+ detectBackgroundTask(terminal: Terminal): boolean;
4
5
  }
@@ -1,6 +1,18 @@
1
1
  import { Effect } from 'effect';
2
2
  import { Worktree } from '../types/index.js';
3
3
  import { GitError, FileSystemError } from '../types/errors.js';
4
+ /**
5
+ * Get all worktree last opened timestamps
6
+ */
7
+ export declare function getWorktreeLastOpened(): Record<string, number>;
8
+ /**
9
+ * Set the last opened timestamp for a worktree
10
+ */
11
+ export declare function setWorktreeLastOpened(worktreePath: string, timestamp: number): void;
12
+ /**
13
+ * Get the last opened timestamp for a specific worktree
14
+ */
15
+ export declare function getWorktreeLastOpenedTime(worktreePath: string): number | undefined;
4
16
  /**
5
17
  * WorktreeService - Git worktree management with Effect-based error handling
6
18
  *
@@ -7,8 +7,28 @@ import { GitError, FileSystemError } from '../types/errors.js';
7
7
  import { setWorktreeParentBranch } from '../utils/worktreeConfig.js';
8
8
  import { getClaudeProjectsDir, pathToClaudeProjectName, } from '../utils/claudeDir.js';
9
9
  import { executeWorktreePostCreationHook } from '../utils/hookExecutor.js';
10
- import { configurationManager } from './configurationManager.js';
10
+ import { configReader } from './config/configReader.js';
11
11
  const CLAUDE_DIR = '.claude';
12
+ // Module-level state for worktree last opened tracking (runtime state, not persisted)
13
+ const worktreeLastOpened = new Map();
14
+ /**
15
+ * Get all worktree last opened timestamps
16
+ */
17
+ export function getWorktreeLastOpened() {
18
+ return Object.fromEntries(worktreeLastOpened);
19
+ }
20
+ /**
21
+ * Set the last opened timestamp for a worktree
22
+ */
23
+ export function setWorktreeLastOpened(worktreePath, timestamp) {
24
+ worktreeLastOpened.set(worktreePath, timestamp);
25
+ }
26
+ /**
27
+ * Get the last opened timestamp for a specific worktree
28
+ */
29
+ export function getWorktreeLastOpenedTime(worktreePath) {
30
+ return worktreeLastOpened.get(worktreePath);
31
+ }
12
32
  /**
13
33
  * WorktreeService - Git worktree management with Effect-based error handling
14
34
  *
@@ -641,8 +661,8 @@ export class WorktreeService {
641
661
  if (sortByLastSession) {
642
662
  worktrees.sort((a, b) => {
643
663
  // Get last opened timestamps for both worktrees
644
- const timeA = configurationManager.getWorktreeLastOpenedTime(a.path);
645
- const timeB = configurationManager.getWorktreeLastOpenedTime(b.path);
664
+ const timeA = getWorktreeLastOpenedTime(a.path);
665
+ const timeB = getWorktreeLastOpenedTime(b.path);
646
666
  // If both timestamps are undefined, preserve original order
647
667
  if (timeA === undefined && timeB === undefined) {
648
668
  return 0;
@@ -804,7 +824,7 @@ export class WorktreeService {
804
824
  });
805
825
  }
806
826
  // Execute post-creation hook if configured
807
- const worktreeHooks = configurationManager.getWorktreeHooks();
827
+ const worktreeHooks = configReader.getWorktreeHooks();
808
828
  if (worktreeHooks.post_creation?.enabled &&
809
829
  worktreeHooks.post_creation?.command) {
810
830
  const newWorktree = {